Files
sm-bm-plugin/chat.go
Jakub Zych 0642fb1d08 feat: chat bootstrap, members and group sync
GET /_bm/api/v1/chat gives a chat-group member ogólny (the bm/1 group
conversation, adopted once as the system default channel under an
advisory lock), its unread and mention counts, the member's DMs newest
message first, the moderator flag and the composer limits. Every member
bootstrap syncs participants with the group: missing members join
ogólny, users no longer in the group lose their rows in every bm/1
conversation. A non-member gets only member:false and is pruned.

GET /_bm/api/v1/chat/members lists members (id, name, surname, handle,
is_bot from golem15.chatbots when active) and assigns missing handles;
non-members get 404.

The chatchannels access policy (ChatAccess) is installed from a
database hook: bm/1 for members only, nobody manages channels.
bm:chat-sync creates the groups when missing, syncs and assigns
handles. Migration 20261026130000_seed_jz_bm_chat_groups creates
studenci and moderatorzy without members; its rollback keeps them.
2026-10-26 15:46:13 +01:00

122 lines
4.0 KiB
Go

package bm
import (
"context"
"database/sql"
"errors"
chatcontracts "git.golem15.com/golem15/sm-chat-plugin/contracts"
chanclasses "git.golem15.com/golem15/sm-chatchannels-plugin/classes"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/lagoon"
"git.golem15.com/jakub/sm-bm-plugin/classes"
"gorm.io/gorm"
)
// The chat config keys and their fallbacks (config/config.yaml).
const (
configChatGroup = "jz.bm.chat.group"
configChatModeratorGroup = "jz.bm.chat.moderator_group"
configChatMaxUpload = "jz.bm.chat.max_upload_bytes"
configChatMaxEmbeds = "jz.bm.chat.max_embeds"
defaultChatGroup = "studenci"
defaultChatModeratorGroup = "moderatorzy"
defaultChatMaxUpload = 62914560
defaultChatMaxEmbeds = 3
)
// registerChat makes jz.bm the host of the BM chat: it registers the bm
// context alias on golem15.chat's registry and installs the channel access
// policy. Without golem15.chat it does nothing, so jz.bm keeps working (and
// its tests keep activating) without the chat plugins; jz.bm does not
// Require them.
func (p *Plugin) registerChat(app *backpack.App) error {
if app == nil || !app.HasPlugin("golem15.chat") {
return nil
}
reg, ok := app.Lookup[*chatcontracts.Registry]()
if !ok || reg == nil {
return errors.New("jz.bm: golem15.chat must be listed before jz.bm in summer.yaml")
}
if err := reg.Add(p.ID(), chatcontracts.Registration{Alias: classes.ChatContextType, Loader: p.loadChatContext}); err != nil {
return err
}
return p.installChatAccess(app)
}
// loadChatContext is the loader of the bm alias. The only context is id 1;
// any other id is unknown, which golem15.chat answers with its not-a-member
// error.
func (p *Plugin) loadChatContext(ctx context.Context, db *gorm.DB, id uint) (chatcontracts.Context, error) {
return classes.ChatContextLoader(p.chatGroup, p.chatModeratorGroup)(ctx, db, id)
}
// installChatAccess installs BM's access policy on golem15.chatchannels'
// channel service. The service is published from chatchannels' own
// database hook, which runs before this one because chatchannels boots
// first; under a database-less boot (tests, route:list) neither runs.
func (p *Plugin) installChatAccess(app *backpack.App) error {
if app == nil || !app.HasPlugin("golem15.chatchannels") {
return nil
}
return lagoon.OnDatabase(app, func(_ *sql.DB, _ *gorm.DB) error {
svc, ok := app.Lookup[*chanclasses.ChannelService]()
if !ok || svc == nil {
return errors.New("jz.bm: golem15.chatchannels must be listed before jz.bm in summer.yaml")
}
svc.SetAccessResolver(classes.NewChatAccess(p.database, p.chatGroup(), nil))
return nil
})
}
// database returns the published application database, or nil.
func (p *Plugin) database() *gorm.DB {
if p.app == nil {
return nil
}
db, _ := p.app.Lookup[*gorm.DB]()
return db
}
// chatGroup is the code of the user group whose members are in the chat.
func (p *Plugin) chatGroup() string {
return p.configString(configChatGroup, defaultChatGroup)
}
// chatModeratorGroup is the code of the user group whose members moderate
// the chat's channels.
func (p *Plugin) chatModeratorGroup() string {
return p.configString(configChatModeratorGroup, defaultChatModeratorGroup)
}
// chatMaxUploadBytes is the total attachment size of one message.
func (p *Plugin) chatMaxUploadBytes() int {
return p.configInt(configChatMaxUpload, defaultChatMaxUpload)
}
// chatMaxEmbeds is the number of entity embeds one message may carry.
func (p *Plugin) chatMaxEmbeds() int {
return p.configInt(configChatMaxEmbeds, defaultChatMaxEmbeds)
}
func (p *Plugin) configString(key, fallback string) string {
if p.app == nil || p.app.Config == nil {
return fallback
}
if v := p.app.Config.String(key); v != "" {
return v
}
return fallback
}
func (p *Plugin) configInt(key string, fallback int) int {
if p.app == nil || p.app.Config == nil {
return fallback
}
if v := p.app.Config.Int(key); v > 0 {
return v
}
return fallback
}