- ChatContext implements golem15.chat's MessageModerator: moderator
group members who are chat members delete in group conversations
only, never in DMs
- GET /chat/embeds?type=deadline|class|reading&q= (members only,
at most 20 rows, escaped ILIKE, q at most 100 characters)
- POST /chat/embeds/resolve {refs} (members only, at most 50 distinct
refs, 8 KiB body): current data or null per ref
202 lines
7.5 KiB
Go
202 lines
7.5 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)
|
|
_ chatcontracts.MessageModerator = (*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. Moderators delete
|
|
// through CanModerateMessage, in group conversations only.
|
|
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
|
|
}
|
|
|
|
// CanModerateMessage is golem15.chat's delete-only moderation hook (D-10):
|
|
// a member of the moderator group who is also a chat-group member may
|
|
// delete other people's messages in group conversations (# ogólny). In a
|
|
// DM the answer is always false, so moderators never act on private
|
|
// conversations (D-24). It grants no viewing or editing.
|
|
func (c *ChatContext) CanModerateMessage(ctx context.Context, userID uint, conversationType string) (bool, error) {
|
|
if conversationType != "group" || userID == 0 || c.moderatorGroup == "" {
|
|
return false, nil
|
|
}
|
|
member, err := c.HasMember(ctx, userID)
|
|
if err != nil || !member {
|
|
return false, err
|
|
}
|
|
if c.isMember != nil {
|
|
return c.isMember(ctx, c.db, userID, c.moderatorGroup)
|
|
}
|
|
return IsChatModerator(ctx, c.db, userID, c.moderatorGroup)
|
|
}
|
|
|
|
// 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
|
|
}
|