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 }