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.
181 lines
6.6 KiB
Go
181 lines
6.6 KiB
Go
package classes
|
|
|
|
import (
|
|
"context"
|
|
|
|
chatcontracts "git.golem15.com/golem15/sm-chat-plugin/contracts"
|
|
chancontracts "git.golem15.com/golem15/sm-chatchannels-plugin/contracts"
|
|
userclasses "git.golem15.com/golem15/sm-user-plugin/classes"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var (
|
|
_ chatcontracts.Context = (*ChatContext)(nil)
|
|
_ chancontracts.AccessResolver = (*ChatAccess)(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"
|
|
)
|
|
|
|
// MemberFunc reports whether userID belongs to the user group with code
|
|
// group. IsChatMember is the production check; tests inject their own.
|
|
type MemberFunc func(ctx context.Context, db *gorm.DB, userID uint, group string) (bool, error)
|
|
|
|
// IsChatMember reports whether userID is in the chat group group. No user,
|
|
// no group code or no database is never a member.
|
|
func IsChatMember(ctx context.Context, db *gorm.DB, userID uint, group string) (bool, error) {
|
|
if userID == 0 || group == "" || db == nil {
|
|
return false, nil
|
|
}
|
|
return userclasses.HasGroupCode(ctx, db, userID, group)
|
|
}
|
|
|
|
// IsChatModerator reports whether userID is in the moderator group
|
|
// moderatorGroup.
|
|
func IsChatModerator(ctx context.Context, db *gorm.DB, userID uint, moderatorGroup string) (bool, error) {
|
|
return IsChatMember(ctx, db, userID, moderatorGroup)
|
|
}
|
|
|
|
// ChatMemberIDs lists the ids of the users in group that are not deleted,
|
|
// ordered by id. A group code with no group row has no member.
|
|
func ChatMemberIDs(ctx context.Context, db *gorm.DB, group string) ([]uint, error) {
|
|
ids := []uint{}
|
|
if group == "" || db == nil {
|
|
return ids, nil
|
|
}
|
|
err := 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", group).
|
|
Distinct().
|
|
Order("users_groups.user_id").
|
|
Pluck("users_groups.user_id", &ids).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
// 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
|
|
isMember MemberFunc
|
|
}
|
|
|
|
// 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}
|
|
}
|
|
|
|
// WithMembership replaces the group check (IsChatMember) and returns c.
|
|
func (c *ChatContext) WithMembership(fn MemberFunc) *ChatContext {
|
|
c.isMember = fn
|
|
return c
|
|
}
|
|
|
|
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
|
|
}
|
|
if c.isMember != nil {
|
|
return c.isMember(ctx, c.db, userID, c.group)
|
|
}
|
|
return IsChatMember(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) {
|
|
return ChatMemberIDs(ctx, c.db, c.group)
|
|
}
|
|
|
|
// ChatContextLoader is the golem15.chat loader of the bm alias. The only
|
|
// context is id 1; any other id is unknown (nil, nil), which golem15.chat
|
|
// answers with its not-a-member error. The group codes are read per load,
|
|
// so a config change needs no re-registration.
|
|
func ChatContextLoader(group, moderatorGroup func() string) chatcontracts.Loader {
|
|
return func(_ context.Context, db *gorm.DB, id uint) (chatcontracts.Context, error) {
|
|
if id != ChatContextID {
|
|
return nil, nil
|
|
}
|
|
return NewChatContext(db, group(), moderatorGroup()), nil
|
|
}
|
|
}
|
|
|
|
// ChatAccess is golem15.chatchannels' access policy for BM: chat-group
|
|
// members may access context bm/1 and nothing else, and nobody manages it,
|
|
// so no channel is ever created through the channel service.
|
|
type ChatAccess struct {
|
|
db func() *gorm.DB
|
|
group string
|
|
isMember MemberFunc
|
|
}
|
|
|
|
// NewChatAccess returns the access policy for the chat group group. db is
|
|
// resolved per check; isMember nil means IsChatMember.
|
|
func NewChatAccess(db func() *gorm.DB, group string, isMember MemberFunc) *ChatAccess {
|
|
if isMember == nil {
|
|
isMember = IsChatMember
|
|
}
|
|
return &ChatAccess{db: db, group: group, isMember: isMember}
|
|
}
|
|
|
|
// CanAccessContext is true only for a chat-group member and context bm/1.
|
|
// Any error, or no database, denies.
|
|
func (a *ChatAccess) CanAccessContext(ctx context.Context, userID uint, contextType string, contextID uint) bool {
|
|
if contextType != ChatContextType || contextID != ChatContextID || userID == 0 || a.group == "" || a.db == nil {
|
|
return false
|
|
}
|
|
db := a.db()
|
|
if db == nil {
|
|
return false
|
|
}
|
|
ok, err := a.isMember(ctx, db, userID, a.group)
|
|
return err == nil && ok
|
|
}
|
|
|
|
// CanManageContext is always false: nobody creates or edits channels.
|
|
func (a *ChatAccess) CanManageContext(context.Context, uint, string, uint) bool {
|
|
return false
|
|
}
|