From 0625d0db351d5fa16a205a36ce371736108c758b Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Wed, 22 Oct 2025 13:38:41 -0600 Subject: [PATCH] groundwork for services by introducing service vtables, also SetMembership as groundwork for join/unjoin --- TODO.md | 1 + database/community.go | 64 ++++++++++++++++++++--- database/services.go | 118 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 172 insertions(+), 11 deletions(-) diff --git a/TODO.md b/TODO.md index ef8a364..e0a965e 100644 --- a/TODO.md +++ b/TODO.md @@ -7,6 +7,7 @@ After the point where it reaches feature parity with Venice circa 2006. * A better way to set up the database than `setup/database.sql`. Bring the table setup into the application somehow. The [migrate](https://github.com/golang-migrate/migrate) library might be of use here. * Either implement the Calendar and Chat, or take those menu entries out. + * Should those be community "services" instead? * For Chat, if it's implemented, it should use XMPP. * Implement proper help and online documentation. diff --git a/database/community.go b/database/community.go index 33aa811..bad2ef7 100644 --- a/database/community.go +++ b/database/community.go @@ -231,6 +231,58 @@ func (c *Community) MemberCount(hidden bool) (int, error) { return -1, errors.New("internal error reading member count") } +/* SetMembership sets a user's membership status within the community. + * Parameters: + * u - The user to change the membership status of. + * level - Their membership level. If this is 0, they are removed from membership. + * locked - Whether they can unjoin the community themselves. Ignored if removing them. + * Returns: + * Standard Go error status. + */ +func (c *Community) SetMembership(u *User, level uint16, locked bool) error { + if level == 0 { + _, err := amdb.Exec("DELETE FROM commmember WHERE commid = ? AND uid = ?", c.Id, u.Uid) + if err != nil { + return err + } + stuffMembership(c.Id, u.Uid, false, false, 0) + err = AmOnUserLeaveCommunityServices(c, u) + if err != nil { + return err + } + } else { + rs, err := amdb.Query("SELECT granted_lvl, locked FROM commmember WHERE commid = ? AND uid = ?", c.Id, u.Uid) + if err != nil { + return err + } + if rs.Next() { + var oldLevel uint16 + var lockStatus bool + rs.Scan(&oldLevel, &lockStatus) + if level != oldLevel || lockStatus != locked { + _, err := amdb.Exec("UPDATE commmember SET granted_lvl = ?, locked = ? WHERE commid = ? AND uid = ?", + level, locked, c.Id, u.Uid) + if err != nil { + return err + } + stuffMembership(c.Id, u.Uid, true, locked, level) + } + } else { + _, err := amdb.Exec("INSERT INTO commmember (comm_id, uid, granted_lvl, locked) VALUES (?, ?, ?, ?)", + c.Id, u.Uid, level, locked) + if err != nil { + return err + } + stuffMembership(c.Id, u.Uid, true, locked, level) + err = AmOnUserJoinCommunityServices(c, u) + if err != nil { + return nil + } + } + } + return nil +} + /* TestPermission is shorthand that tests if a user has a permission with respect to the community. * Parameters: * user - The user to be checked. @@ -670,12 +722,6 @@ func AmCreateCommunity(name string, alias string, hostUid int32, language *strin return nil, errors.New("unable to find newly-generated community") } - // Establish the community services. - err = AmEstablishCommunityServices(comm.Id) - if err != nil { - return nil, err - } - // Ensure the new host has host privileges in the community. The host's membership is "locked" so they // can't unjoin and leave the community hostless. _, err = amdb.Exec("INSERT INTO commmember (commid, uid, granted_lvl, locked) VALUES (?, ?, ?, 1)", comm.Id, hostUid, @@ -685,6 +731,12 @@ func AmCreateCommunity(name string, alias string, hostUid int32, language *strin } stuffMembership(comm.Id, hostUid, true, true, AmDefaultRole("Community.Creator").Level()) + // Establish the community services. + err = AmEstablishCommunityServices(comm) + if err != nil { + return nil, err + } + // operation was a success - add an audit record ar = AmNewAudit(AuditCommunityCreate, hostUid, remoteIP, fmt.Sprintf("id=%d", comm.Id), fmt.Sprintf("name=%s", comm.Name), fmt.Sprintf("alias=%s", comm.Alias)) diff --git a/database/services.go b/database/services.go index d4308f7..add0fd4 100644 --- a/database/services.go +++ b/database/services.go @@ -18,6 +18,33 @@ import ( "gopkg.in/yaml.v3" ) +// ServiceVTable is a serioes of functions called for services on specific events. +type ServiceVTable interface { + OnNewCommunity(*Community) error + OnDeleteCommunity(int32) error + OnUserJoinCommunity(*Community, *User) error + OnUserLeaveCommunity(*Community, *User) error +} + +// emptyServiceVTable is a default ServiceVTable that does nothing. +type emptyServiceVTable struct{} + +func (*emptyServiceVTable) OnNewCommunity(*Community) error { + return nil +} + +func (*emptyServiceVTable) OnDeleteCommunity(int32) error { + return nil +} + +func (*emptyServiceVTable) OnUserJoinCommunity(*Community, *User) error { + return nil +} + +func (*emptyServiceVTable) OnUserLeaveCommunity(*Community, *User) error { + return nil +} + // ServiceDef holds the definition for an individual service. type ServiceDef struct { Id string `yaml:"id"` @@ -29,6 +56,7 @@ type ServiceDef struct { LinkSequence int `yaml:"linkSequence"` Link string `yaml:"link"` Title string `yaml:"title"` + vtable ServiceVTable } // ServiceDomain holds each individual configured service domain. @@ -80,6 +108,13 @@ func init() { serviceRoot.Domains[i].seqOrder = sqo serviceRoot.byName[dom.DomainName] = &(serviceRoot.Domains[i]) } + dom := serviceRoot.byName["community"] + empty := emptyServiceVTable{} + dom.byId["Profile"].vtable = &empty + dom.byId["Admin"].vtable = &empty + dom.byId["SysAdmin"].vtable = &empty + dom.byId["Conference"].vtable = &empty // TODO + dom.byId["Members"].vtable = &empty servicesCache, err = lru.New2Q(50) if err != nil { panic(err) @@ -115,18 +150,19 @@ func AmGetCommunityServices(cid int32) ([]*ServiceDef, error) { return rc.([]*ServiceDef), nil } -/* AmEstablishCommunityServices extablishes the service (feature) records for a new community. +/* AmEstablishCommunityServices establishes the service (feature) records for a new community, + * and allows the services to establish themselves. * Parameters: - * cid - ID of the new community. + * c - The new community. * Returns: * Standard Go error status. */ -func AmEstablishCommunityServices(cid int32) error { +func AmEstablishCommunityServices(c *Community) error { dom := serviceRoot.byName["community"] a := make([]*ServiceDef, 0, len(dom.Services)) for i, svc := range dom.Services { if svc.Default { - _, err := amdb.Exec("INSERT INTO commftrs (commid, ftr_code) VALUES (?, ?)", cid, svc.Index) + _, err := amdb.Exec("INSERT INTO commftrs (commid, ftr_code) VALUES (?, ?)", c.Id, svc.Index) if err != nil { return err } @@ -134,7 +170,79 @@ func AmEstablishCommunityServices(cid int32) error { } } servicesCacheMutex.Lock() - servicesCache.Add(cid, a) + servicesCache.Add(c.Id, a) servicesCacheMutex.Unlock() + for _, svc := range a { + err := svc.vtable.OnNewCommunity(c) + if err != nil { + return err + } + } return nil } + +/* AmDeleteCommunityServices cleans up all services associated with a community that has gone away, + * and then cleans up the service records. + * Parameters: + * cid - The ID of the departing community. + * Returns: + * Standard Go error status. + */ +func AmDeleteCommunityServices(cid int32) error { + arr, err := AmGetCommunityServices(cid) + if err == nil { + for _, svc := range arr { + err = svc.vtable.OnDeleteCommunity(cid) + if err != nil { + break + } + } + } + if err == nil { + _, err = amdb.Exec("DELETE FROM commftrs WHERE commid = ?", cid) + servicesCacheMutex.Lock() + servicesCache.Remove(cid) + servicesCacheMutex.Unlock() + } + return err +} + +/* AmOnUserJoinCommunityServices gives services a chance to update themselves when a user joins a community. + * Parameters: + * c - The community that is being joined. + * u - The user leaving that community. + * Returns: + * Standard Go error status. + */ +func AmOnUserJoinCommunityServices(c *Community, u *User) error { + arr, err := AmGetCommunityServices(c.Id) + if err == nil { + for _, svc := range arr { + err = svc.vtable.OnUserJoinCommunity(c, u) + if err != nil { + break + } + } + } + return err +} + +/* AmOnUserLeaveCommunityServices gives services a chance to update themselves when a user leaves a community. + * Parameters: + * c - The community that is being left. + * u - The user leaving that community. + * Returns: + * Standard Go error status. + */ +func AmOnUserLeaveCommunityServices(c *Community, u *User) error { + arr, err := AmGetCommunityServices(c.Id) + if err == nil { + for _, svc := range arr { + err = svc.vtable.OnUserLeaveCommunity(c, u) + if err != nil { + break + } + } + } + return err +}