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.
This commit is contained in:
2026-10-26 15:46:13 +01:00
parent 07d67be1e7
commit 0642fb1d08
9 changed files with 734 additions and 40 deletions

59
console/chat_sync.go Normal file
View File

@@ -0,0 +1,59 @@
package console
import (
"context"
"fmt"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/jakub/sm-bm-plugin/classes"
"gorm.io/gorm"
)
// chatSyncName is the name of the chat sync command.
const chatSyncName = "bm:chat-sync"
// ChatSyncCommand returns the chat sync console command. withDB runs the
// command's work with the application's database; groups returns the chat
// group code and the moderator group code. It never adds anyone to a group:
// members are assigned by hand in the backend.
func ChatSyncCommand(withDB func(ctx context.Context, fn func(*gorm.DB) error) error, groups func() (string, string)) bonfire.Command {
return bonfire.Command{
Name: chatSyncName,
Description: "Create the chat groups if missing, sync ogólny participants with the chat group and give members a handle",
Run: func(ctx context.Context, _ bonfire.Input, out bonfire.Output) error {
group, moderatorGroup := groups()
return withDB(ctx, func(db *gorm.DB) error {
created, err := classes.EnsureChatGroups(ctx, db, group, moderatorGroup)
if err != nil {
return fmt.Errorf("%s: groups: %w", chatSyncName, err)
}
general, err := classes.FindGeneral(ctx, db)
if err != nil {
return fmt.Errorf("%s: ogólny: %w", chatSyncName, err)
}
var generalID uint
if general != nil {
generalID = general.ID
}
added, removed, err := classes.SyncChatParticipants(ctx, db, generalID, group)
if err != nil {
return fmt.Errorf("%s: participants: %w", chatSyncName, err)
}
handles, err := classes.AssignMissingHandles(ctx, db, group)
if err != nil {
return fmt.Errorf("%s: handles: %w", chatSyncName, err)
}
out.Printf("groups created: %d\n", created)
if general == nil {
out.Printf("ogólny does not exist yet; it is created when a member first opens the chat\n")
} else {
out.Printf("ogólny participants added: %d\n", added)
}
out.Printf("participant rows removed: %d\n", removed)
out.Printf("handles assigned: %d\n", handles)
out.Success(chatSyncName + ": chat group " + group + " synced")
return nil
})
},
}
}