diff --git a/community.go b/community.go
index a8c894c..36d09b4 100644
--- a/community.go
+++ b/community.go
@@ -361,7 +361,7 @@ func MemberList(ctxt ui.AmContext) (string, any) {
ctxt.VarMap().Set("oper", "st")
ctxt.VarMap().Set("term", "")
ctxt.VarMap().Set("ofs", ofs)
- ctxt.SetFrameTitle("List Members")
+ ctxt.SetFrameTitle("Members of Community " + comm.Name)
listMax := int(ctxt.Globals().MaxCommunityMemberPage)
results, total, err := comm.ListMembers(ctxt.Ctx(), database.ListMembersFieldNone, database.ListMembersOperNone, "", ofs*listMax, listMax, showHidden)
if err != nil {
diff --git a/communityadmin.go b/communityadmin.go
index 9a7fc62..2f0fde6 100644
--- a/communityadmin.go
+++ b/communityadmin.go
@@ -14,6 +14,7 @@ import (
"context"
"errors"
"fmt"
+ "io"
"net/http"
"strconv"
"strings"
@@ -21,12 +22,48 @@ import (
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/email"
+ "git.erbosoft.com/amy/amsterdam/exports"
"git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util"
"github.com/labstack/echo/v4"
log "github.com/sirupsen/logrus"
)
+/* ExportCommunityMembers exports the members of the community as XML.
+ * Parameters:
+ * ctxt - The AmContext for the request.
+ * Returns:
+ * Command string dictating what to be rendered.
+ * Data as a parameter for the command string.
+ */
+func ExportCommunityMembers(ctxt ui.AmContext) (string, any) {
+ comm := ctxt.CurrentCommunity()
+ if !comm.TestPermission("Community.ShowAdmin", ctxt.EffectiveLevel()) {
+ return "error", ENOACCESS
+ }
+
+ // use a dedicated goroutine to generate the streamed XML and send it into one end of a pipe
+ filename := time.Now().Format("exported-users-20060102.xml")
+ r, w := io.Pipe()
+ go func() {
+ start := time.Now()
+ err := exports.VIUStreamCommunityMemberList(context.Background(), w, comm)
+ if err != nil {
+ log.Errorf("ExportCommunityMembers task failed with %v", err)
+ s := fmt.Sprintf("\r\n", err)
+ w.Write([]byte(s))
+ }
+ w.Close()
+ dur := time.Since(start)
+ log.Infof("ExportCommunityMembers task completed in %v", dur)
+ }()
+
+ // Now we connect the outlet end of the pipe to the output to the browser.
+ ctxt.SetOutputType("text/xml")
+ ctxt.SetHeader("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
+ return "stream", r
+}
+
/* CommunityAdminMenu renders the community administration menu.
* Parameters:
* ctxt - The AmContext for the request.
@@ -35,7 +72,7 @@ import (
* Data as a parameter for the command string.
*/
func CommunityAdminMenu(ctxt ui.AmContext) (string, any) {
- comm := ctxt.CurrentCommunity() // set by middleware
+ comm := ctxt.CurrentCommunity()
if !comm.TestPermission("Community.ShowAdmin", ctxt.EffectiveLevel()) {
return "error", ENOACCESS
}
diff --git a/config/config.go b/config/config.go
index b6cd906..a11e636 100644
--- a/config/config.go
+++ b/config/config.go
@@ -14,7 +14,6 @@ import (
_ "embed"
"errors"
"fmt"
- "maps"
"os"
"regexp"
"strconv"
@@ -180,7 +179,7 @@ func overlayStringArray(loaded, defaulted []string) []string {
m[s] = true
}
rc := make([]string, 0, len(m))
- for s := range maps.Keys(m) {
+ for s := range m {
rc = append(rc, s)
}
return rc
diff --git a/database/securitydefs.yaml b/database/securitydefs.yaml
index e1cb55e..4406ffb 100644
--- a/database/securitydefs.yaml
+++ b/database/securitydefs.yaml
@@ -195,6 +195,18 @@ lists:
- "Community.Member"
- "UnrestrictedUser"
- "Community.Cohost"
+ - name: "Community.AllUserlevels"
+ roles:
+ - "NotInList"
+ - "Global.Anonymous"
+ - "Global.Unverified"
+ - "Global.Normal"
+ - "Community.Member"
+ - "UnrestrictedUser"
+ - "Community.Cohost"
+ - "Community.Host"
+ - "Global.PFY"
+ - "Global.BOFH"
- name: "Conference.Read"
roles:
- "Global.Anonymous"
diff --git a/database/user.go b/database/user.go
index 731381a..e66e28c 100644
--- a/database/user.go
+++ b/database/user.go
@@ -100,7 +100,7 @@ func (p *UserPrefs) Localizer() lctime.Localizer {
func (p *UserPrefs) LanguageTag() *language.Tag {
lt, err := language.Parse(p.ReadLocale())
if err != nil {
- log.Fatalf("BOGUS LANGUAGE TAG %s in user prefs for uid %d", p.LocaleID, p.Uid)
+ log.Errorf("BOGUS LANGUAGE TAG %s in user prefs for uid %d", p.LocaleID, p.Uid)
return nil
}
return <
@@ -115,7 +115,7 @@ func (p *UserPrefs) MessagePrinter() *message.Printer {
func (p *UserPrefs) Location() *time.Location {
rc, err := time.LoadLocation(p.TimeZoneID)
if err != nil {
- log.Fatalf("BOGUS TIMEZONE TAG %s in user prefs for uid %d", p.TimeZoneID, p.Uid)
+ log.Errorf("BOGUS TIMEZONE TAG %s in user prefs for uid %d", p.TimeZoneID, p.Uid)
return time.Local
}
return rc
@@ -669,7 +669,7 @@ func crackAuthString(authString string) (int32, string, error) {
* Parameters:
* ctx - Standard Go context value.
* authString - The stored cookie authentication string.
- * remoteIP - The remote IP address wheter trhe user is logging in from.
+ * remoteIP - The remote IP address where the user is logging in from.
* Returns:
* Pointer to the authenticated User, or nil.
* Standard Go error status.
diff --git a/exports/vcard_xml.go b/exports/vcard_xml.go
index ba83917..2e2e46f 100644
--- a/exports/vcard_xml.go
+++ b/exports/vcard_xml.go
@@ -199,12 +199,12 @@ type VCKey struct {
func VCardFromContactInfo(ctx context.Context, target *VCard, ci *database.ContactInfo) error {
target.Version = "2.0"
target.FullName = ci.FullName(false)
- target.Name.Family = util.IIF(ci.FamilyName != nil, *ci.FamilyName, "")
- target.Name.Given = util.IIF(ci.GivenName != nil, *ci.GivenName, "")
- target.Name.Middle = util.IIF(ci.MiddleInit != nil, *ci.MiddleInit+".", "")
- target.Name.Prefix = util.IIF(ci.Prefix != nil, *ci.Prefix, "")
- target.Name.Suffix = util.IIF(ci.Suffix != nil, *ci.Suffix, "")
- target.URL = util.IIF(ci.URL != nil, *ci.URL, "")
+ target.Name.Family = util.SRef(ci.FamilyName)
+ target.Name.Given = util.SRef(ci.GivenName)
+ target.Name.Middle = util.SRef(ci.MiddleInit)
+ target.Name.Prefix = util.SRef(ci.Prefix)
+ target.Name.Suffix = util.SRef(ci.Suffix)
+ target.URL = util.SRef(ci.URL)
if ci.LastUpdate != nil {
target.LastUpdate = ci.LastUpdate.Format(ISO8601)
}
@@ -213,12 +213,12 @@ func VCardFromContactInfo(ctx context.Context, target *VCard, ci *database.Conta
addr[0].Home.Local = "HOME"
addr[0].Postal.Local = "POSTAL"
addr[0].Preferred.Local = "PREF"
- addr[0].Street = util.IIF(ci.Addr1 != nil, *ci.Addr1, "")
- addr[0].ExtAddr = util.IIF(ci.Addr2 != nil, *ci.Addr2, "")
- addr[0].Locality = util.IIF(ci.Locality != nil, *ci.Locality, "")
- addr[0].Region = util.IIF(ci.Region != nil, *ci.Region, "")
- addr[0].PCode = util.IIF(ci.PostalCode != nil, *ci.PostalCode, "")
- addr[0].Country = util.IIF(ci.Country != nil, *ci.Country, "")
+ addr[0].Street = util.SRef(ci.Addr1)
+ addr[0].ExtAddr = util.SRef(ci.Addr2)
+ addr[0].Locality = util.SRef(ci.Locality)
+ addr[0].Region = util.SRef(ci.Region)
+ addr[0].PCode = util.SRef(ci.PostalCode)
+ addr[0].Country = util.SRef(ci.Country)
target.Address = &addr
phcount := util.IIF(ci.Phone != nil, 1, 0) + util.IIF(ci.Fax != nil, 1, 0) + util.IIF(ci.Mobile != nil, 1, 0)
diff --git a/exports/viu_xml.go b/exports/viu_xml.go
index 9bd3049..5f1a7ab 100644
--- a/exports/viu_xml.go
+++ b/exports/viu_xml.go
@@ -12,6 +12,9 @@ package exports
import (
"context"
"encoding/xml"
+ "fmt"
+ "io"
+ "strings"
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/util"
@@ -73,6 +76,7 @@ type VIUCommunityJoin struct {
Community string `xml:",chardata"` // name of community joined
}
+// VIUUserFromUser fills in a VIUUser structure with details from the given user.
func VIUUserFromUser(ctx context.Context, target *VIUUser, u *database.User) error {
// Fill base fields first.
target.ID = int(u.Uid)
@@ -80,7 +84,7 @@ func VIUUserFromUser(ctx context.Context, target *VIUUser, u *database.User) err
target.Password.Prehashed = true
target.Password.Hash = u.Passhash
target.PasswordReminder = u.PassReminder
- target.Description = util.IIF(u.Description != nil, *u.Description, "")
+ target.Description = util.SRef(u.Description)
// Get the contact info.
ci, err := u.ContactInfo(ctx)
@@ -126,10 +130,66 @@ func VIUUserFromUser(ctx context.Context, target *VIUUser, u *database.User) err
return err
}
+ // Fill options from user flags.
target.Options.PostPictures = flags.Get(database.UserFlagPicturesInPosts)
target.Options.OptOut = flags.Get(database.UserFlagMassMailOptOut)
target.Options.NoPhoto = flags.Get(database.UserFlagDisallowSetPhoto)
- // TODO - fill in Joins
+ // Get the list of communities for the user and set up the Joins list.
+ comms, err := database.AmGetCommunitiesForUser(ctx, u.Uid)
+ if err != nil {
+ return err
+ }
+ target.Joins = make([]VIUCommunityJoin, len(comms))
+ roles := database.AmRoleList("Community.AllUserlevels")
+ for i, c := range comms {
+ _, _, level, err := c.Membership(ctx, u)
+ if err != nil {
+ return err
+ }
+ target.Joins[i].Role = roles.FindForLevel(level).ID()
+ target.Joins[i].Community = c.Alias
+ }
+
return nil
}
+
+// VIUStreamCommunityMembersList streams a list of users of the given community to the given stream in VIU format.
+func VIUStreamCommunityMemberList(ctx context.Context, w io.Writer, comm *database.Community) error {
+ // Get the list of community members.
+ total, err := comm.MemberCount(ctx, false)
+ if err != nil {
+ return err
+ }
+ users, _, err := comm.ListMembers(ctx, database.ListMembersFieldNone, database.ListMembersOperNone, "", 0, total, false)
+ if err != nil {
+ return err
+ }
+
+ // Write the header of the file.
+ var b strings.Builder
+ b.WriteString(xml.Header)
+ b.WriteString("\r\n