Files
sm-bm-plugin/chat.go
Jakub Zych a840d0ad02 fix: enforce the chat's per-message upload total
jz.bm.chat.max_upload_bytes is now handed to golem15.chat as the total
size of one message's attachments, so the server enforces the limit the
bootstrap advertises.
2026-10-26 19:32:07 +01:00

141 lines
4.7 KiB
Go

package bm
import (
"context"
"database/sql"
"errors"
chatclasses "git.golem15.com/golem15/sm-chat-plugin/classes"
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
}
if err := p.installChatLimits(app); err != nil {
return err
}
return p.installChatAccess(app)
}
// installChatLimits makes golem15.chat enforce jz.bm.chat.max_upload_bytes
// as the total size of one message's attachments, the value the bootstrap
// advertises to the student app. golem15.chat publishes its service from
// its own database hook, which runs before this one.
func (p *Plugin) installChatLimits(app *backpack.App) error {
return lagoon.OnDatabase(app, func(_ *sql.DB, _ *gorm.DB) error {
svc, ok := app.Lookup[*chatclasses.Service]()
if !ok || svc == nil {
return errors.New("jz.bm: golem15.chat must be listed before jz.bm in summer.yaml")
}
svc.SetMaxMessageBytes(int64(p.chatMaxUploadBytes()))
return nil
})
}
// 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
}