337 lines
10 KiB
Go
337 lines
10 KiB
Go
/*
|
|
* 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/.
|
|
*
|
|
* SPDX-License-Identifier: MPL-2.0
|
|
*/
|
|
// The database package contains database management and storage logic.
|
|
package database
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"errors"
|
|
"slices"
|
|
"sync"
|
|
|
|
"git.erbosoft.com/amy/amsterdam/config"
|
|
"git.erbosoft.com/amy/amsterdam/util"
|
|
lru "github.com/hashicorp/golang-lru"
|
|
"github.com/jmoiron/sqlx"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// ServiceVTable is a series of functions called for services on specific events.
|
|
type ServiceVTable interface {
|
|
OnNewCommunity(context.Context, *sqlx.Tx, *Community) error
|
|
OnDeleteCommunity(context.Context, *sqlx.Tx, int32, *util.WorkerPool) error
|
|
OnUserJoinCommunity(context.Context, *sqlx.Tx, *Community, *User) error
|
|
OnUserLeaveCommunity(context.Context, *sqlx.Tx, *Community, *User) error
|
|
}
|
|
|
|
// emptyServiceVTable is a default ServiceVTable that does nothing.
|
|
type emptyServiceVTable struct{}
|
|
|
|
func (*emptyServiceVTable) OnNewCommunity(context.Context, *sqlx.Tx, *Community) error {
|
|
return nil
|
|
}
|
|
|
|
func (*emptyServiceVTable) OnDeleteCommunity(context.Context, *sqlx.Tx, int32, *util.WorkerPool) error {
|
|
return nil
|
|
}
|
|
|
|
func (*emptyServiceVTable) OnUserJoinCommunity(context.Context, *sqlx.Tx, *Community, *User) error {
|
|
return nil
|
|
}
|
|
|
|
func (*emptyServiceVTable) OnUserLeaveCommunity(context.Context, *sqlx.Tx, *Community, *User) error {
|
|
return nil
|
|
}
|
|
|
|
// ServiceDef holds the definition for an individual service.
|
|
type ServiceDef struct {
|
|
Id string `yaml:"id"`
|
|
Index int16 `yaml:"index"`
|
|
Default bool `yaml:"default"`
|
|
Locked bool `yaml:"locked"`
|
|
RequirePermission string `yaml:"requirePermission"`
|
|
RequireRole string `yaml:"requireRole"`
|
|
LinkSequence int `yaml:"linkSequence"`
|
|
Link string `yaml:"link"`
|
|
Title string `yaml:"title"`
|
|
vtable ServiceVTable
|
|
}
|
|
|
|
// ServiceDomain holds each individual configured service domain.
|
|
type ServiceDomain struct {
|
|
DomainName string `yaml:"domain"`
|
|
Services []ServiceDef `yaml:"services"`
|
|
byId map[string]*ServiceDef
|
|
byIndex map[int16]*ServiceDef
|
|
seqOrder []*ServiceDef
|
|
}
|
|
|
|
// ServiceConfiguration holds the service configuration.
|
|
type ServiceConfiguration struct {
|
|
Domains []ServiceDomain `yaml:"domains"`
|
|
byName map[string]*ServiceDomain
|
|
}
|
|
|
|
//go:embed servicedefs.yaml
|
|
var initServiceData []byte
|
|
|
|
// The service configuration loaded from YAML.
|
|
var serviceRoot ServiceConfiguration
|
|
|
|
// The services cache for communities.
|
|
var servicesCache *lru.TwoQueueCache
|
|
|
|
// Mutex on the services cache.
|
|
var servicesCacheMutex sync.Mutex
|
|
|
|
// init loads the service configuration and builds all the internal indexes.
|
|
func init() {
|
|
if err := yaml.Unmarshal(initServiceData, &serviceRoot); err != nil {
|
|
panic(err) // can't happen
|
|
}
|
|
serviceRoot.byName = make(map[string]*ServiceDomain)
|
|
for i, dom := range serviceRoot.Domains {
|
|
serviceRoot.Domains[i].byId = make(map[string]*ServiceDef)
|
|
serviceRoot.Domains[i].byIndex = make(map[int16]*ServiceDef)
|
|
sqo := make([]*ServiceDef, 0, len(serviceRoot.Domains[i].Services))
|
|
for j, svc := range serviceRoot.Domains[i].Services {
|
|
serviceRoot.Domains[i].byId[svc.Id] = &(serviceRoot.Domains[i].Services[j])
|
|
serviceRoot.Domains[i].byIndex[svc.Index] = &(serviceRoot.Domains[i].Services[j])
|
|
sqo = append(sqo, &(serviceRoot.Domains[i].Services[j]))
|
|
}
|
|
slices.SortFunc(sqo, func(a, b *ServiceDef) int {
|
|
return a.LinkSequence - b.LinkSequence
|
|
})
|
|
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 = &(conferenceServiceVTable{})
|
|
dom.byId["Members"].vtable = &empty
|
|
}
|
|
|
|
// setupServicesCache sets up the services cache.
|
|
func setupServicesCache() {
|
|
var err error
|
|
servicesCache, err = lru.New2Q(config.GlobalConfig.Tuning.Caches.Services)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
/* AmGetServiceIndex returns the service index for the given service by domain and identifier.
|
|
* Parameters:
|
|
* domain - The domain of the service to look for.
|
|
* id - The identifier of the service.
|
|
* Returns:
|
|
* The service index, if the service is found.
|
|
* Standard Go error status.
|
|
*/
|
|
func AmGetServiceIndex(domain, id string) (int16, error) {
|
|
if d, ok := serviceRoot.byName[domain]; ok {
|
|
if svc, ok2 := d.byId[id]; ok2 {
|
|
return svc.Index, nil
|
|
}
|
|
}
|
|
return -1, errors.New("service not found")
|
|
}
|
|
|
|
/* AmGetCommunityServices returns all the community service definitions for a community.
|
|
* Parameters:
|
|
* ctx - Standard Go context value.
|
|
* cid - Community ID to get services for.
|
|
* Returns:
|
|
* Array of ServiceDef pointers for the community's services.
|
|
* Standard Go error status.
|
|
*/
|
|
func AmGetCommunityServices(ctx context.Context, cid int32) ([]*ServiceDef, error) {
|
|
servicesCacheMutex.Lock()
|
|
defer servicesCacheMutex.Unlock()
|
|
rc, ok := servicesCache.Get(cid)
|
|
if !ok {
|
|
rs, err := amdb.QueryContext(ctx, "SELECT ftr_code FROM commftrs WHERE commid = ?", cid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dom := serviceRoot.byName["community"]
|
|
a := make([]*ServiceDef, 0, len(dom.Services))
|
|
for rs.Next() {
|
|
var ndx int16
|
|
if err = rs.Scan(&ndx); err == nil {
|
|
a = append(a, dom.byIndex[ndx])
|
|
}
|
|
}
|
|
servicesCache.Add(cid, a)
|
|
rc = a
|
|
}
|
|
return rc.([]*ServiceDef), nil
|
|
}
|
|
|
|
/* AmGetCommunityServices returns all the community service definitions for a community, using a transaction.
|
|
* Parameters:
|
|
* ctx - Standard Go context value.
|
|
* tx - Transaction to be used.
|
|
* cid - Community ID to get services for.
|
|
* Returns:
|
|
* Array of ServiceDef pointers for the community's services.
|
|
* Standard Go error status.
|
|
*/
|
|
func AmGetCommunityServicesTx(ctx context.Context, tx *sqlx.Tx, cid int32) ([]*ServiceDef, error) {
|
|
servicesCacheMutex.Lock()
|
|
defer servicesCacheMutex.Unlock()
|
|
rc, ok := servicesCache.Get(cid)
|
|
if !ok {
|
|
rs, err := tx.QueryContext(ctx, "SELECT ftr_code FROM commftrs WHERE commid = ?", cid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dom := serviceRoot.byName["community"]
|
|
a := make([]*ServiceDef, 0, len(dom.Services))
|
|
for rs.Next() {
|
|
var ndx int16
|
|
if err = rs.Scan(&ndx); err == nil {
|
|
a = append(a, dom.byIndex[ndx])
|
|
}
|
|
}
|
|
servicesCache.Add(cid, a)
|
|
rc = a
|
|
}
|
|
return rc.([]*ServiceDef), nil
|
|
}
|
|
|
|
/* AmEstablishCommunityServices establishes the service (feature) records for a new community,
|
|
* and allows the services to establish themselves.
|
|
* Parameters:
|
|
* ctx - Standard Go context value.
|
|
* tx - The transaction to use.
|
|
* c - The new community.
|
|
* Returns:
|
|
* Standard Go error status.
|
|
*/
|
|
func AmEstablishCommunityServices(ctx context.Context, tx *sqlx.Tx, c *Community) error {
|
|
dom := serviceRoot.byName["community"]
|
|
a := make([]*ServiceDef, 0, len(dom.Services))
|
|
for i, svc := range dom.Services {
|
|
if svc.Default {
|
|
_, err := tx.ExecContext(ctx, "INSERT INTO commftrs (commid, ftr_code) VALUES (?, ?)", c.Id, svc.Index)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a = append(a, &(dom.Services[i]))
|
|
}
|
|
}
|
|
servicesCacheMutex.Lock()
|
|
servicesCache.Add(c.Id, a)
|
|
servicesCacheMutex.Unlock()
|
|
for _, svc := range a {
|
|
if err := svc.vtable.OnNewCommunity(ctx, tx, c); 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:
|
|
* ctx - Standard Go context value.
|
|
* tx - The transaction to use.
|
|
* cid - The ID of the departing community.
|
|
* background - Pool in which background jobs can be submitted to clean up the database.
|
|
* Returns:
|
|
* Standard Go error status.
|
|
*/
|
|
func AmDeleteCommunityServices(ctx context.Context, tx *sqlx.Tx, cid int32, background *util.WorkerPool) error {
|
|
arr, err := AmGetCommunityServices(ctx, cid)
|
|
if err == nil {
|
|
for _, svc := range arr {
|
|
if err = svc.vtable.OnDeleteCommunity(ctx, tx, cid, background); err != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if err == nil {
|
|
_, err = tx.ExecContext(ctx, "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:
|
|
* ctx - Standard Go context value.
|
|
* tx - The current database transaction.
|
|
* c - The community that is being joined.
|
|
* u - The user leaving that community.
|
|
* Returns:
|
|
* Standard Go error status.
|
|
*/
|
|
func AmOnUserJoinCommunityServices(ctx context.Context, tx *sqlx.Tx, c *Community, u *User) error {
|
|
arr, err := AmGetCommunityServicesTx(ctx, tx, c.Id)
|
|
if err == nil {
|
|
for _, svc := range arr {
|
|
if err = svc.vtable.OnUserJoinCommunity(ctx, tx, c, u); err != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
/* AmOnUserLeaveCommunityServices gives services a chance to update themselves when a user leaves a community.
|
|
* Parameters:
|
|
* ctx - Standard Go context value.
|
|
* tx - The current database transaction.
|
|
* c - The community that is being left.
|
|
* u - The user leaving that community.
|
|
* Returns:
|
|
* Standard Go error status.
|
|
*/
|
|
func AmOnUserLeaveCommunityServices(ctx context.Context, tx *sqlx.Tx, c *Community, u *User) error {
|
|
arr, err := AmGetCommunityServicesTx(ctx, tx, c.Id)
|
|
if err == nil {
|
|
for _, svc := range arr {
|
|
if err = svc.vtable.OnUserLeaveCommunity(ctx, tx, c, u); err != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
/* AmTestService checks whether the community supports a service.
|
|
* Parameters:
|
|
* ctx - Standard Go context value.
|
|
* c - The community that is being tested.
|
|
* serviceId - The service ID to test.
|
|
* Returns:
|
|
* true if the community supports that service, false if not.
|
|
* Standard Go error status.
|
|
*/
|
|
func AmTestService(ctx context.Context, c *Community, serviceId string) (bool, error) {
|
|
arr, err := AmGetCommunityServices(ctx, c.Id)
|
|
if err == nil {
|
|
for _, svc := range arr {
|
|
if svc.Id == serviceId {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
return false, err
|
|
}
|