feat: host the BM chat context

When golem15.chat is active, Boot registers the context alias bm (id 1)
on its registry. Membership is the user group jz.bm.chat.group (default
studenci); CanOverseeChats is always false, so nobody reads other
people's DMs or edits their messages; the broadcast channel is
chat:context:1, never empty.

GET /_bm/api/v1/chat answers the caller's membership behind jwt.auth and
the per-user bucket jz.bm.chat (240 a minute). The chat config block
(group, moderator_group, max_upload_bytes, max_embeds) is added. jz.bm
still Requires only golem15.user; all chat wiring is gated by HasPlugin.
This commit is contained in:
2026-10-26 15:40:44 +01:00
parent 67806f49a7
commit 64601f6817
9 changed files with 482 additions and 5 deletions

91
classes/chat_context.go Normal file
View File

@@ -0,0 +1,91 @@
package classes
import (
"context"
chatcontracts "git.golem15.com/golem15/sm-chat-plugin/contracts"
userclasses "git.golem15.com/golem15/sm-user-plugin/classes"
"gorm.io/gorm"
)
var _ chatcontracts.Context = (*ChatContext)(nil)
// The BM chat context: one context of type bm with id 1, whose members are
// the members of one user group.
const (
// ChatContextType is the context alias (context_type) jz.bm registers on
// golem15.chat.
ChatContextType = "bm"
// ChatContextID is the id of the one BM chat context.
ChatContextID uint = 1
// ChatGeneralName is the name of the group conversation and its channel.
ChatGeneralName = "ogólny"
// ChatGeneralSlug is the slug of the adopted channel.
ChatGeneralSlug = "ogolny"
// ChatBroadcastChannel is the host channel golem15.chat publishes created
// events to. It must never be empty: an empty host channel silences the
// conversation channels too. No client can subscribe to it, because the
// chat authorizer only admits chat:conversation:{id}.
ChatBroadcastChannel = "chat:context:1"
)
// ChatContext is the golem15.chat context of the BM chat. Membership is the
// user group named by group; nobody oversees other people's chats, so a
// moderator reads no DM and edits no foreign message.
type ChatContext struct {
db *gorm.DB
group string
moderatorGroup string
}
// NewChatContext returns the BM chat context over db for the chat group
// code group and the moderator group code moderatorGroup.
func NewChatContext(db *gorm.DB, group, moderatorGroup string) *ChatContext {
return &ChatContext{db: db, group: group, moderatorGroup: moderatorGroup}
}
func (c *ChatContext) ID() uint { return ChatContextID }
func (c *ChatContext) Type() string { return ChatContextType }
func (c *ChatContext) ChatEnabled() bool { return true }
func (c *ChatContext) GroupName() string { return ChatGeneralName }
func (c *ChatContext) BroadcastChannel() string { return ChatBroadcastChannel }
// CanActFor is the default: a user acts only for themself.
func (c *ChatContext) CanActFor(ctx context.Context, actorID, memberID uint) bool {
return chatcontracts.DefaultCanActFor(ctx, actorID, memberID)
}
// CanOverseeChats is always false. golem15.chat grants an overseer read
// access to every conversation and edits of other people's messages, which
// no BM user gets; moderators delete through a narrower hook.
func (c *ChatContext) CanOverseeChats(context.Context, uint) (bool, error) {
return false, nil
}
// HasMember reports whether userID is in the chat group.
func (c *ChatContext) HasMember(ctx context.Context, userID uint) (bool, error) {
if userID == 0 || c.group == "" {
return false, nil
}
return userclasses.HasGroupCode(ctx, c.db, userID, c.group)
}
// Members lists the ids of the chat group's users that are not deleted.
func (c *ChatContext) Members(ctx context.Context) ([]uint, error) {
ids := []uint{}
if c.group == "" {
return ids, nil
}
err := c.db.WithContext(ctx).
Table("users_groups").
Joins("JOIN user_groups ON user_groups.id = users_groups.user_group_id").
Joins("JOIN users ON users.id = users_groups.user_id").
Where("user_groups.code = ? AND users.deleted_at IS NULL", c.group).
Distinct().
Order("users_groups.user_id").
Pluck("users_groups.user_id", &ids).Error
if err != nil {
return nil, err
}
return ids, nil
}