diff --git a/database/security.go b/database/security.go index 2856675..4193621 100644 --- a/database/security.go +++ b/database/security.go @@ -91,16 +91,17 @@ type CfgPermission struct { // CfgSecurityDefs is the master structure for security definitions. type CfgSecurityDefs struct { - Scopes []CfgScope `yaml:"scopes"` - Roles []CfgRole `yaml:"roles"` - Defaults []CfgDefault `yaml:"defaults"` - Lists []CfgRoleList `yaml:"lists"` - Permissions []CfgPermission `yaml:"permissions"` - scopeMap map[string]*CfgScope - roleMap map[string]*CfgRole - defaultsMap map[string]*CfgDefault - listsMap map[string]*CfgRoleList - permsMap map[string]*CfgPermission + Scopes []CfgScope `yaml:"scopes"` + Roles []CfgRole `yaml:"roles"` + Defaults []CfgDefault `yaml:"defaults"` + Lists []CfgRoleList `yaml:"lists"` + Permissions []CfgPermission `yaml:"permissions"` + scopeMap map[string]*CfgScope + roleMap map[string]*CfgRole + roleMapReverse map[uint16]*CfgRole + defaultsMap map[string]*CfgDefault + listsMap map[string]*CfgRoleList + permsMap map[string]*CfgPermission } //go:embed securitydefs.yaml @@ -153,10 +154,12 @@ func init() { securityRoot.scopeMap[sc.Name] = &(securityRoot.Scopes[i]) } securityRoot.roleMap = make(map[string]*CfgRole) + securityRoot.roleMapReverse = make(map[uint16]*CfgRole) for i, ro := range securityRoot.Roles { scope := securityRoot.scopeMap[ro.Scope] securityRoot.Roles[i].level = parseLevelValue(scope.bounds, ro.Value) securityRoot.roleMap[ro.Internal] = &(securityRoot.Roles[i]) + securityRoot.roleMapReverse[securityRoot.Roles[i].level] = &(securityRoot.Roles[i]) } securityRoot.defaultsMap = make(map[string]*CfgDefault) for i, def := range securityRoot.Defaults { @@ -247,6 +250,20 @@ func AmRole(id string) Role { return rc } +/* AmRoleForLevel returns a Role given an integer level. + * Parameters: + * level - Level of the Role to look up. + * Returns: + * The specified role. + */ +func AmRoleForLevel(level uint16) Role { + rc, ok := securityRoot.roleMapReverse[level] + if !ok { + log.Errorf("AmRoleForLevel('%d') - role not found!", level) + } + return rc +} + /* AmDefaultRole returns a Role given a default ID. * Parameters: * id - ID of the default to look up. diff --git a/database/securitydefs.yaml b/database/securitydefs.yaml index 320a6eb..e1cb55e 100644 --- a/database/securitydefs.yaml +++ b/database/securitydefs.yaml @@ -123,6 +123,14 @@ lists: - "Global.Normal" - "UnrestrictedUser" - "Global.PFY" + - name: "Global.AllUserLevels" + roles: + - "Global.Anonymous" + - "Global.Unverified" + - "Global.Normal" + - "UnrestrictedUser" + - "Global.PFY" + - "Global.BOFH" - name: "Global.CreateCommunity" default: "Global.Normal" roles: diff --git a/database/user.go b/database/user.go index 4e0d0d7..731381a 100644 --- a/database/user.go +++ b/database/user.go @@ -121,6 +121,22 @@ func (p *UserPrefs) Location() *time.Location { return rc } +// LocationISO8601Offset returns an offset value for the user's time location. +func (p *UserPrefs) LocationISO8601Offset() string { + loc := p.Location() + _, secondsOut := time.Now().In(loc).Zone() + if secondsOut == 0 { + return "Z" + } + minutesOut := secondsOut / 60 + if minutesOut < 0 { + minutesOut = -minutesOut + return fmt.Sprintf("-%02d:%02d", minutesOut/60, minutesOut%60) + } else { + return fmt.Sprintf("+%02d:%02d", minutesOut/60, minutesOut%60) + } +} + // User represents a user in the Amsterdam database. type User struct { Mutex sync.RWMutex diff --git a/exports/vcard_xml.go b/exports/vcard_xml.go new file mode 100644 index 0000000..ba83917 --- /dev/null +++ b/exports/vcard_xml.go @@ -0,0 +1,272 @@ +/* + * Amsterdam Web Communities System + * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +// Package exports contains interfacing code for external data formats. +package exports + +import ( + "context" + "encoding/xml" + "errors" + + "git.erbosoft.com/amy/amsterdam/database" + "git.erbosoft.com/amy/amsterdam/util" +) + +/* + * This file defines data structures for XML vCards as defined by the XMPP Foundation specification XEP-0054, "vcard-temp", + * https://xmpp.org/extensions/xep-0054.html. + */ + +// VCard is the top level vCard structure. +type VCard struct { + XMLName xml.Name `xml:"vcard-temp vCard"` // name must be "vCard" in the "vcard-temp" namespace + Version string `xml:"VERSION"` // vCard version number + FullName string `xml:"FN"` // full name + Name VCName `xml:"N"` // broken-up name components + Nickname string `xml:"NICKNAME"` // nickname (not used in Amsterdam) + Photo *VCPhoto `xml:"PHOTO"` // user photo (not used in Amsterdam) + BDay string `xml:"BDAY"` // birthday, ISO 8601 format + Address *[]VCAddress `xml:"ADR"` // address + AddressLabel *[]VCAddressLabel `xml:"LABEL"` // address label + Email *[]VCEmail `xml:"EMAIL"` // E-mail address + Tel *[]VCTelephone `xml:"TEL"` // telephone number + JabberID string `xml:"JABBERID"` // Jabber/XMPP address (user@host) (XMPP extension) (not used in Amsterdam) + Mailer string `xml:"MAILER"` // mailer user agent (not used in Amsterdam) + TZ string `xml:"TZ"` // time zone indicator, ISO 8601 formatted UTC offset + Geolocation *VCGeolocation `xml:"GEO"` // geolocation (not used in Amsterdam) + Title string `xml:"TITLE"` // job title (not used in Amsterdam) + Role string `xml:"ROLE"` // job role (not used in Amsterdam) + Logo *VCLogo `xml:"LOGO"` // organization logo (not used in Amsterdam) + Agent *VCAgent `xml:"AGENT"` // agent for the organization (not used in Amsterdam) + Org *VCOrganization `xml:"ORG"` // organization + Categories *VCCategory `xml:"CATEGORIES"` // categories + Note string `xml:"NOTE"` // text note (not used by Amsterdam) + ProductID string `xml:"PRODID"` // product ID that generated this vCard (not used by Amsterdam) + LastUpdate string `xml:"REV"` // last update to this information, ISO 8601 format + SortString string `xml:"SORT-STRING"` // sort string + Sound *VCSound `xml:"SOUND"` // pronunciation property (not used in Amsterdam) + UID string `xml:"UID"` // unique identifier (not necessarily an Amsterdam UID!) (not used in Amsterdam) + URL string `xml:"URL"` // URL + Class *VCClass `xml:"CLASS"` // privacy classification (not used in Amsterdam) + Key *VCKey `xml:"KEY"` // authentication credential or encryption key (not used in Amsterdam) + Description string `xml:"DESC"` // description string value (XMPP extension) +} + +// VCPhoto is the "photo" attachment to the VCard. +type VCPhoto struct { + XMLName xml.Name `xml:"PHOTO"` // must be a "PHOTO" tag + Type string `xml:"TYPE"` // data type for BINVAL + BinaryValue string `xml:"BINVAL"` // binary photo value (Base64 encoded) + ExternalValue string `xml:"EXTVAL"` // external value of photo (URL) +} + +// Name is the "structured name" property of the vCard. +type VCName struct { + XMLName xml.Name `xml:"N"` // must be an "N" tag + Family string `xml:"FAMILY"` // family name + Given string `xml:"GIVEN"` // given name + Middle string `xml:"MIDDLE"` // middle name/initial + Prefix string `xml:"PREFIX"` // prefix + Suffix string `xml:"SUFFIX"` // suffix +} + +// VCAddress is the "address" property of the vCard. +type VCAddress struct { + XMLName xml.Name `xml:"ADR"` // must be a "ADR" tag + Work xml.Name `xml:"WORK"` // Presence indicates work address + Home xml.Name `xml:"HOME"` // Presence indicates home address + Postal xml.Name `xml:"POSTAL"` // presence indicates postal address + Parcel xml.Name `xml:"PARCEL"` // presence indicates parcel address + Domestic xml.Name `xml:"DOM"` // presence indicates domestic address + International xml.Name `xml:"INTL"` // presence indicates international address + Preferred xml.Name `xml:"PREF"` // Presence indicates preferred address + POBox string `xml:"POBOX"` // post office box + Locality string `xml:"LOCALITY"` // locality (city) + Region string `xml:"REGION"` // region (state/province) + PCode string `xml:"PCODE"` // postal code + Country string `xml:"CTRY"` // country + Street string `xml:"STREET"` // street address (addr line 1) + ExtAddr string `xml:"EXTADR"` // extended address (addr line 2) +} + +// VCAddressLabel is the "address label" property of the vCard. +type VCAddressLabel struct { + XMLName xml.Name `xml:"LABEL"` // must be a "LABEL" tag + Work xml.Name `xml:"WORK"` // Presence indicates work address + Home xml.Name `xml:"HOME"` // Presence indicates home address + Postal xml.Name `xml:"POSTAL"` // presence indicates postal address + Parcel xml.Name `xml:"PARCEL"` // presence indicates parcel address + Domestic xml.Name `xml:"DOM"` // presence indicates domestic address + International xml.Name `xml:"INTL"` // presence indicates international address + Preferred xml.Name `xml:"PREF"` // Presence indicates preferred address + Lines []string `xml:"LINE"` // lines of text on the address label +} + +// VCEmail is the "E-mail address" property of the vCard. +type VCEmail struct { + XMLName xml.Name `xml:"EMAIL"` // must be an "EMAIL" tag + Work xml.Name `xml:"WORK"` // presence indicates work address + Home xml.Name `xml:"HOME"` // presence indicates home address + Internet xml.Name `xml:"INTERNET"` // presence indicates Internet address + Preferred xml.Name `xml:"PREF"` // Presence indicates preferred address + X400 xml.Name `xml:"X400"` // Presence indicates X.400 address + UserID string `xml:"USERID"` // user ID (address) +} + +// VCTelephone is the "telephone number" property of the vCard. +type VCTelephone struct { + XMLName xml.Name `xml:"TEL"` // must be a "TEL" tag + Work xml.Name `xml:"WORK"` // presence indicates work number + Home xml.Name `xml:"HOME"` // presence indicates home number + Voice xml.Name `xml:"VOICE"` // presence indicates voice number + Fax xml.Name `xml:"FAX"` // presence indicates fax number + Pager xml.Name `xml:"PAGER"` // presence indicates pager number + Message xml.Name `xml:"MSG"` // presence indicates message number + Cell xml.Name `xml:"CELL"` // presence indicates cellphone number + Video xml.Name `xml:"VIDEO"` // presence indicates videophone number + BBS xml.Name `xml:"BBS"` // presence indicates BBS number + Modem xml.Name `xml:"MODEM"` // presence indicates modem number + ISDN xml.Name `xml:"ISDN"` // presence indicates ISDN number + PCS xml.Name `xml:"PCS"` // presence indicates PCS number + Preferred xml.Name `xml:"PREF"` // presence indicates preferred number + Number string `xml:"NUMBER"` // the number +} + +// VCGeolocation is the "geolocation" property of the vCard. +type VCGeolocation struct { + XMLName xml.Name `xml:"GEO"` // must be a "GEO" tag + Latitude string `xml:"LAT"` // latitude to six decimal places (North is positive) + Longitude string `xml:"LONG"` // longitude to six decimal places (East is positive) +} + +// VCLogo is the "logo" property of the vCard. +type VCLogo struct { + XMLName xml.Name `xml:"LOGO"` // must be a "LOGO" tag + Type string `xml:"TYPE"` // data type for BINVAL + BinaryValue string `xml:"BINVAL"` // binary photo value (Base64 encoded) + ExternalValue string `xml:"EXTVAL"` // external value of photo (URL) +} + +// VCAgent is the "agent" property of the vCard. +type VCAgent struct { + XMLName xml.Name `xml:"AGENT"` // must be an "AGENT" tag + VCard *VCard `xml:"vcard-temp vCard"` // vCard with agent contact info + ExternalValue string `xml:"EXTVAL"` // external value, such as URL to contact info +} + +// VCOrganization is the "organization" property of the vCard. +type VCOrganization struct { + XMLName xml.Name `xml:"ORG"` // must be an "ORG" tag + OrgName string `xml:"ORGNAME"` // organization name + OrgUnit *[]string `xml:"ORGUNIT"` // organization unit(s) +} + +// VCCategory is the "category" property of the vCard. +type VCCategory struct { + XMLName xml.Name `xml:"CATEGORIES"` // must be a "CATEGORIES" tag + Keywords []string `xml:"KEYWORD"` // keywords +} + +// VCSound is the "pronunciation guide" property of the vCard. +type VCSound struct { + XMLName xml.Name `xml:"SOUND"` // must be a "SOUND" tag + Phonetic string `xml:"PHONETIC"` // phonetic pronunciation + BinaryValue string `xml:"BINVAL"` // binary audio value (Base64 encoded) + ExternalValue string `xml:"EXTVAL"` // external value of audio (URL) +} + +// VCClass is the "privacy classification" property of the vCard. +type VCClass struct { + XMLName xml.Name `xml:"CLASS"` // must be a "CLASS" tag + Public xml.Name `xml:"PUBLIC"` // presence indicates public information + Private xml.Name `xml:"PRIVATE"` // presence indicates private information + Confidential xml.Name `xml:"CONFIDENTIAL"` // presence indicates confidential information +} + +// VCKey is the "authentication or encryption key" property of the vCard. +type VCKey struct { + XMLName xml.Name `xml:"KEY"` // must be a "KEY" tag + Type string `xml:"TYPE"` // type indicator + Credential string `xml:"CRED"` // credential value +} + +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, "") + if ci.LastUpdate != nil { + target.LastUpdate = ci.LastUpdate.Format(ISO8601) + } + + addr := make([]VCAddress, 1) + 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, "") + 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) + if phcount > 0 { + phone := make([]VCTelephone, phcount) + i := 0 + if ci.Phone != nil { + phone[i].Home.Local = "HOME" + phone[i].Voice.Local = "VOICE" + phone[i].Preferred.Local = "PREF" + phone[i].Number = *ci.Phone + i++ + } + if ci.Fax != nil { + phone[i].Home.Local = "HOME" + phone[i].Fax.Local = "FAX" + phone[i].Preferred.Local = "PREF" + phone[i].Number = *ci.Fax + i++ + } + if ci.Mobile != nil { + phone[i].Home.Local = "HOME" + phone[i].Cell.Local = "CELL" + phone[i].Voice.Local = "VOICE" + phone[i].Preferred.Local = "PREF" + phone[i].Number = *ci.Mobile + i++ + } + if i == phcount { + target.Tel = &phone + } else { + return errors.New("internal error in phone array") + } + } + + if ci.Email != nil { + email := make([]VCEmail, 1) + email[0].Home.Local = "HOME" + email[0].Internet.Local = "INTERNET" + email[0].Preferred.Local = "PREF" + email[0].UserID = *ci.Email + target.Email = &email + } + + if ci.Company != nil { + var org VCOrganization + org.OrgName = *ci.Company + target.Org = &org + } + return nil +} diff --git a/exports/vcif_xml.go b/exports/vcif_xml.go index ba7e4d9..9545a2b 100644 --- a/exports/vcif_xml.go +++ b/exports/vcif_xml.go @@ -25,8 +25,12 @@ import ( * Amsterdam uses this name for the format for backwards compatibility. */ +// ISO8601 is the full ISO 8601 formatting string. const ISO8601 = "20060102T150405" +// ISO8601_DATE is the ISO 8601 date-only formatting string. +const ISO8601_DATE = "20060102" + // VCIFBase is the top-level element for a VCIF file. type VCIFBase struct { XMLName xml.Name `xml:"vcif"` // I am the element diff --git a/exports/viu_xml.go b/exports/viu_xml.go new file mode 100644 index 0000000..df4f5c7 --- /dev/null +++ b/exports/viu_xml.go @@ -0,0 +1,137 @@ +/* + * Amsterdam Web Communities System + * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +// Package exports contains interfacing code for external data formats. +package exports + +import ( + "context" + "encoding/xml" + + "git.erbosoft.com/amy/amsterdam/database" + "git.erbosoft.com/amy/amsterdam/util" +) + +/* + * This file contains structures for working with Venice-Import-Users (VIU) XML files. + * Amsterdam uses this name for them for backward compatibility. + */ + +// VIUBase is the top level structure of the Venice-Import-Users format. +type VIUBase struct { + XMLName xml.Name `xml:"venice-import-users"` // must be a tag + Users []VIUUser `xml:"venice-user"` // the list of users +} + +// VIUUser is the structure representing a single user. +type VIUUser struct { + XMLName xml.Name `xml:"venice-user"` // must be a tag + ID int `xml:"id,attr"` // the UID for the user + Username string `xml:"username"` // user name + Password VIUPassword `xml:"password"` // password information + PasswordReminder string `xml:"password-reminder"` // password reminder string + Description string `xml:"description"` // description string + Options VIUOptions `xml:"options"` // user options + VCard VCard `xml:"vcard-temp vCard"` // user contact info in vCard XML format + Joins []VIUCommunityJoin `xml:"join"` // joined communities +} + +// VIUPassword represents the user password information. +type VIUPassword struct { + XMLName xml.Name `xml:"password"` // must be a tag + Prehashed bool `xml:"prehashed,attr"` // has this password been prehashed? + Hash string `xml:",chardata"` // password information +} + +// VIUOptions represents the user options. +type VIUOptions struct { + XMLName xml.Name `xml:"options"` // must be an tag + Confirmed bool `xml:"confirmed,attr"` // E-mail address confirmed? + Locked bool `xml:"locked,attr"` // user account locked? + Role string `xml:"role,attr"` // user's base role + HideAddr bool `xml:"hideaddr,attr"` // hide address? + HidePhone bool `xml:"hidephone,attr"` // hide phone number? + HideFax bool `xml:"hidefax,attr"` // hide fax number? + HideEmail bool `xml:"hideemail,attr"` // hide E-mail address? + AutoJoin bool `xml:"autojoin,attr"` // auto-join communities? + PostPictures bool `xml:"postpictures,attr"` // show pictures in posts? + OptOut bool `xml:"optout,attr"` // opt out of mass E-mail? + NoPhoto bool `xml:"nophoto,attr"` // disallow photo uploads? + Locale string `xml:"locale,attr"` // user locale + ZoneHint string `xml:"zonehint,attr"` // user timezone hint +} + +// VIUCommunityJoin represnts all the communities the user has joined. +type VIUCommunityJoin struct { + XMLName xml.Name `xml:"join"` // must be a tag + Role string `xml:"role,attr"` // role we have in the community + Community string `xml:",chardata"` // name of community joined +} + +func VIUUserFromUser(ctx context.Context, target *VIUUser, u *database.User) error { + // Fill base fields first. + target.ID = int(u.Uid) + target.Username = u.Username + target.Password.Prehashed = true + target.Password.Hash = u.Passhash + target.PasswordReminder = u.PassReminder + target.Description = util.IIF(u.Description != nil, *u.Description, "") + + // Get the contact info. + ci, err := u.ContactInfo(ctx) + if err != nil { + return err + } + + // Fill the contact info into the VCard. + err = VCardFromContactInfo(ctx, &(target.VCard), ci) + if err != nil { + return err + } + + // Fill extra fields into the VCard. + if u.DOB != nil { + target.VCard.BDay = u.DOB.Format(ISO8601_DATE) + } + + // Fill in the Options structure from what we have. + target.Options.Confirmed = u.VerifyEMail + target.Options.Locked = u.Lockout + target.Options.Role = database.AmRoleList("Global.AllUserLevels").FindForLevel(u.BaseLevel).ID() + target.Options.HideAddr = ci.PrivateAddr + target.Options.HidePhone = ci.PrivatePhone + target.Options.HideFax = ci.PrivateFax + target.Options.HideEmail = ci.PrivateEmail + + // Load user preferences. + prefs, err := u.Prefs(ctx) + if err != nil { + return err + } + + // Fill in from user preferences. + target.Options.Locale = prefs.LocaleID + target.Options.ZoneHint = prefs.TimeZoneID + target.VCard.TZ = prefs.LocationISO8601Offset() + + // Load user flags. + flags, err := u.Flags(ctx) + if err != nil { + return err + } + + target.Options.PostPictures = flags.Get(database.UserFlagPicturesInPosts) + target.Options.OptOut = flags.Get(database.UserFlagMassMailOptOut) + target.Options.NoPhoto = flags.Get(database.UserFlagDisallowSetPhoto) + + // TODO - fill in Autojoin option and Joins + /* + AutoJoin bool `xml:"autojoin,attr"` // auto-join communities? + */ + return nil +} diff --git a/util/util.go b/util/util.go index 097bd02..258d7ad 100644 --- a/util/util.go +++ b/util/util.go @@ -180,3 +180,12 @@ func MyIPAddress() (net.IP, error) { localAddr := conn.LocalAddr().(*net.UDPAddr) return localAddr.IP, nil } + +// IIF is an "immediate-if" function returning its second argument if the first one is true, the third one if not. +func IIF[A any](expr bool, v1, v2 A) A { + if expr { + return v1 + } else { + return v2 + } +}