The participant sync and the non-member prune now remove only ogólny's rows. A user taken out of studenci, even briefly, keeps their DMs (the live membership check denies access while they are out) and gets them back when re-added, instead of losing them for good.
202 lines
7.4 KiB
Go
202 lines
7.4 KiB
Go
package classes
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
chatclasses "git.golem15.com/golem15/sm-chat-plugin/classes"
|
|
chatmodels "git.golem15.com/golem15/sm-chat-plugin/models"
|
|
chanclasses "git.golem15.com/golem15/sm-chatchannels-plugin/classes"
|
|
chanmodels "git.golem15.com/golem15/sm-chatchannels-plugin/models"
|
|
userclasses "git.golem15.com/golem15/sm-user-plugin/classes"
|
|
usermodels "git.golem15.com/golem15/sm-user-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/lagoon"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// chatGeneralLock is the transaction advisory lock that serialises finding
|
|
// or creating ogólny, so members bootstrapping at the same moment end with
|
|
// one group conversation and one channel.
|
|
const chatGeneralLock int64 = 0x6a7a626d63686174
|
|
|
|
// chatGroupNames are the display names of the default group codes; any
|
|
// other code is named after itself.
|
|
var chatGroupNames = map[string]string{
|
|
"studenci": "Studenci",
|
|
"moderatorzy": "Moderatorzy czatu",
|
|
}
|
|
|
|
// chatMembersSQL selects the ids of the users in the group with code ?
|
|
// that are not deleted.
|
|
const chatMembersSQL = `SELECT DISTINCT users_groups.user_id
|
|
FROM users_groups
|
|
JOIN user_groups ON user_groups.id = users_groups.user_group_id
|
|
JOIN users ON users.id = users_groups.user_id
|
|
WHERE user_groups.code = ? AND users.deleted_at IS NULL`
|
|
|
|
// chatGroupConversationsSQL selects the ids of the group conversations of
|
|
// context bm/1 (ogólny). DMs are never listed: their participant rows are
|
|
// kept when someone leaves the chat group, so the history comes back when
|
|
// they are re-added. Access while they are out is denied by golem15.chat's
|
|
// live membership check (ChatContext.RequireLiveMembership).
|
|
const chatGroupConversationsSQL = `SELECT id FROM golem15_chat_conversations WHERE context_type = ? AND context_id = ? AND type = ?`
|
|
|
|
// EnsureChatGroups creates a user group for each code that has none and
|
|
// returns how many it created. It never assigns a user to a group: members
|
|
// are added by hand in the backend.
|
|
func EnsureChatGroups(ctx context.Context, db *gorm.DB, codes ...string) (int64, error) {
|
|
if db == nil {
|
|
return 0, errors.New("jz.bm: chat groups need a database")
|
|
}
|
|
var created int64
|
|
seen := map[string]bool{}
|
|
for _, code := range codes {
|
|
if code == "" || seen[code] {
|
|
continue
|
|
}
|
|
seen[code] = true
|
|
name := chatGroupNames[code]
|
|
if name == "" {
|
|
name = code
|
|
}
|
|
res := db.WithContext(ctx).Exec(`INSERT INTO user_groups (name, code, description, created_at, updated_at)
|
|
SELECT ?, ?, ?, NOW(), NOW()
|
|
WHERE NOT EXISTS (SELECT 1 FROM user_groups WHERE code = ?)`, name, code, "Czat BM (jz.bm)", code)
|
|
if res.Error != nil {
|
|
return created, res.Error
|
|
}
|
|
created += res.RowsAffected
|
|
}
|
|
return created, nil
|
|
}
|
|
|
|
// EnsureGeneral finds or creates the group conversation of context bm/1
|
|
// (golem15.chat GetGroupChat, which also joins actorID) and adopts it as
|
|
// the system default channel ogólny, so mentions accrue on it. Adoption is
|
|
// idempotent. actorID must be a chat-group member.
|
|
func EnsureGeneral(ctx context.Context, db *gorm.DB, chat *chatclasses.Service, channels *chanclasses.ChannelService, actorID uint) (*chatmodels.Conversation, *chanmodels.Channel, error) {
|
|
if db == nil || chat == nil || channels == nil {
|
|
return nil, nil, errors.New("jz.bm: ogólny needs the database and the chat services")
|
|
}
|
|
var conv *chatmodels.Conversation
|
|
var ch *chanmodels.Channel
|
|
err := lagoon.Transaction(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
|
|
if err := tx.Exec("SELECT pg_advisory_xact_lock(?)", chatGeneralLock).Error; err != nil {
|
|
return err
|
|
}
|
|
c, err := chat.WithDB(tx).GetGroupChat(ctx, actorID, ChatContextType, ChatContextID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
yes := true
|
|
adopted, err := channels.WithDB(tx).AdoptConversation(ctx, ChatContextType, ChatContextID, c.ID, chanclasses.AdoptConversationInput{
|
|
Name: ChatGeneralName,
|
|
Slug: ChatGeneralSlug,
|
|
IsSystem: &yes,
|
|
IsDefault: &yes,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
conv, ch = c, adopted
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return conv, ch, nil
|
|
}
|
|
|
|
// FindGeneral returns the group conversation of context bm/1, or nil when
|
|
// nobody has opened the chat yet.
|
|
func FindGeneral(ctx context.Context, db *gorm.DB) (*chatmodels.Conversation, error) {
|
|
var conv chatmodels.Conversation
|
|
err := db.WithContext(ctx).
|
|
Where("type = ? AND context_type = ? AND context_id = ?", chatmodels.TypeGroup, ChatContextType, ChatContextID).
|
|
Order("id").Take(&conv).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &conv, nil
|
|
}
|
|
|
|
// SyncChatParticipants makes ogólny's participants follow the chat group,
|
|
// in one transaction: every member missing from ogólny (generalID) joins
|
|
// it, and users no longer in the group leave it. DM participant rows are
|
|
// kept (a re-added user gets their DMs back; until then golem15.chat's
|
|
// live membership check denies access). Nothing is pruned while no group
|
|
// with that code exists. It returns how many rows it added and removed.
|
|
func SyncChatParticipants(ctx context.Context, db *gorm.DB, generalID uint, group string) (int64, int64, error) {
|
|
if db == nil {
|
|
return 0, 0, errors.New("jz.bm: chat sync needs a database")
|
|
}
|
|
if group == "" {
|
|
return 0, 0, errors.New("jz.bm: chat sync needs a chat group code")
|
|
}
|
|
var added, removed int64
|
|
err := lagoon.Transaction(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
|
|
if generalID != 0 {
|
|
res := tx.Exec(`INSERT INTO golem15_chat_participants (conversation_id, user_id, joined_at)
|
|
SELECT ?, m.user_id, NOW() FROM (`+chatMembersSQL+`) m
|
|
ON CONFLICT (conversation_id, user_id) DO NOTHING`, generalID, group)
|
|
if res.Error != nil {
|
|
return res.Error
|
|
}
|
|
added = res.RowsAffected
|
|
}
|
|
res := tx.Exec(`DELETE FROM golem15_chat_participants
|
|
WHERE conversation_id IN (`+chatGroupConversationsSQL+`)
|
|
AND user_id NOT IN (`+chatMembersSQL+`)
|
|
AND EXISTS (SELECT 1 FROM user_groups WHERE code = ?)`, ChatContextType, ChatContextID, chatmodels.TypeGroup, group, group)
|
|
if res.Error != nil {
|
|
return res.Error
|
|
}
|
|
removed = res.RowsAffected
|
|
return nil
|
|
})
|
|
return added, removed, err
|
|
}
|
|
|
|
// PruneChatParticipant removes userID from ogólny (the group
|
|
// conversations of context bm/1); their DM rows are kept, as in
|
|
// SyncChatParticipants. It runs when someone outside the chat group asks
|
|
// for the chat.
|
|
func PruneChatParticipant(ctx context.Context, db *gorm.DB, userID uint) error {
|
|
if db == nil {
|
|
return errors.New("jz.bm: chat prune needs a database")
|
|
}
|
|
return db.WithContext(ctx).Exec(`DELETE FROM golem15_chat_participants
|
|
WHERE user_id = ? AND conversation_id IN (`+chatGroupConversationsSQL+`)`, userID, ChatContextType, ChatContextID, chatmodels.TypeGroup).Error
|
|
}
|
|
|
|
// AssignMissingHandles gives every chat-group member without a handle one
|
|
// (golem15.user AssignHandle), so they can be @mentioned, and returns how
|
|
// many it assigned.
|
|
func AssignMissingHandles(ctx context.Context, db *gorm.DB, group string) (int, error) {
|
|
if db == nil {
|
|
return 0, errors.New("jz.bm: handles need a database")
|
|
}
|
|
if group == "" {
|
|
return 0, nil
|
|
}
|
|
var users []usermodels.User
|
|
err := db.WithContext(ctx).
|
|
Where("id IN ("+chatMembersSQL+")", group).
|
|
Where("handle IS NULL OR handle = ''").
|
|
Order("id").Find(&users).Error
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
n := 0
|
|
for i := range users {
|
|
if err := userclasses.AssignHandle(ctx, db, &users[i]); err != nil {
|
|
return n, err
|
|
}
|
|
n++
|
|
}
|
|
return n, nil
|
|
}
|