1249 lines
43 KiB
Go
1249 lines
43 KiB
Go
//go:build mysql
|
|
// +build mysql
|
|
|
|
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/tinode/chat/server/db/common"
|
|
"github.com/tinode/chat/server/store"
|
|
t "github.com/tinode/chat/server/store/types"
|
|
)
|
|
|
|
func (a *adapter) TenantGroupSettingsCreate(id t.TenantID, settings *t.GroupSettings) (*t.GroupSettings, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupSettingsCreate(id, settings)
|
|
}
|
|
if settings == nil || settings.Topic == "" || settings.OwnerUserID.IsZero() {
|
|
return nil, t.ErrMalformed
|
|
}
|
|
now := time.Now().UTC().Round(time.Millisecond)
|
|
if settings.CreatedAt.IsZero() {
|
|
settings.CreatedAt = now
|
|
}
|
|
if settings.UpdatedAt.IsZero() {
|
|
settings.UpdatedAt = settings.CreatedAt
|
|
}
|
|
if settings.GroupStatus == "" {
|
|
settings.GroupStatus = t.GroupStatusActive
|
|
}
|
|
if settings.MaxAdminCount <= 0 {
|
|
settings.MaxAdminCount = 10
|
|
}
|
|
if settings.Version <= 0 {
|
|
settings.Version = 1
|
|
}
|
|
ctx, cancel := a.getContext()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
_, err := a.db.ExecContext(ctx, `INSERT INTO im_group_settings
|
|
(topic,owner_user_id,name,avatar_file_id,description,group_status,join_approval,mute_all,member_upload_files,member_private_chat,member_invite,max_member_count,max_admin_count,member_count,version,created_by,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
ON DUPLICATE KEY UPDATE name=VALUES(name),avatar_file_id=VALUES(avatar_file_id),description=VALUES(description),updated_at=VALUES(updated_at)`,
|
|
settings.Topic, decodeUID(settings.OwnerUserID), settings.Name, settings.AvatarFileID, settings.Description, settings.GroupStatus,
|
|
settings.JoinApproval, settings.MuteAll, settings.MemberUploadFiles, settings.MemberPrivateChat, settings.MemberInvite,
|
|
settings.MaxMemberCount, settings.MaxAdminCount, settings.MemberCount, settings.Version, decodeUID(settings.CreatedBy), settings.CreatedAt, settings.UpdatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return a.TenantGroupSettingsGet(id, settings.Topic)
|
|
}
|
|
|
|
func (a *adapter) TenantGroupSettingsGet(id t.TenantID, topic string) (*t.GroupSettings, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupSettingsGet(id, topic)
|
|
}
|
|
if topic == "" {
|
|
return nil, t.ErrMalformed
|
|
}
|
|
ctx, cancel := a.getContext()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
return a.groupSettingsByTopic(ctx, topic)
|
|
}
|
|
|
|
func (a *adapter) TenantGroupSettingsUpdate(id t.TenantID, topic string, patch t.GroupSettingsPatch) (*t.GroupSettings, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupSettingsUpdate(id, topic, patch)
|
|
}
|
|
if topic == "" {
|
|
return nil, t.ErrMalformed
|
|
}
|
|
set := []string{"updated_at=?"}
|
|
args := []any{time.Now().UTC().Round(time.Millisecond)}
|
|
if patch.Name != nil {
|
|
set = append(set, "name=?")
|
|
args = append(args, *patch.Name)
|
|
}
|
|
if patch.AvatarFileID != nil {
|
|
set = append(set, "avatar_file_id=?")
|
|
args = append(args, *patch.AvatarFileID)
|
|
}
|
|
if patch.Description != nil {
|
|
set = append(set, "description=?")
|
|
args = append(args, *patch.Description)
|
|
}
|
|
if patch.OwnerUserID != nil {
|
|
set = append(set, "owner_user_id=?")
|
|
args = append(args, decodeUID(*patch.OwnerUserID))
|
|
}
|
|
if patch.JoinApproval != nil {
|
|
set = append(set, "join_approval=?")
|
|
args = append(args, *patch.JoinApproval)
|
|
}
|
|
if patch.MuteAll != nil {
|
|
set = append(set, "mute_all=?")
|
|
args = append(args, *patch.MuteAll)
|
|
}
|
|
if patch.MemberUploadFiles != nil {
|
|
set = append(set, "member_upload_files=?")
|
|
args = append(args, *patch.MemberUploadFiles)
|
|
}
|
|
if patch.MemberPrivateChat != nil {
|
|
set = append(set, "member_private_chat=?")
|
|
args = append(args, *patch.MemberPrivateChat)
|
|
}
|
|
if patch.MemberInvite != nil {
|
|
set = append(set, "member_invite=?")
|
|
args = append(args, *patch.MemberInvite)
|
|
}
|
|
if patch.MemberCountDelta != 0 {
|
|
set = append(set, "member_count=GREATEST(member_count+?,0)")
|
|
args = append(args, patch.MemberCountDelta)
|
|
}
|
|
set = append(set, "version=version+1")
|
|
args = append(args, topic)
|
|
where := "topic=?"
|
|
if patch.Version > 0 {
|
|
where += " AND version=?"
|
|
args = append(args, patch.Version)
|
|
}
|
|
ctx, cancel := a.getContextForTx()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
tx, err := a.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback()
|
|
res, err := tx.ExecContext(ctx, "UPDATE im_group_settings SET "+strings.Join(set, ",")+" WHERE "+where, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if affected, _ := res.RowsAffected(); affected == 0 {
|
|
if patch.Version > 0 {
|
|
return nil, t.ErrInvalidResponse
|
|
}
|
|
return nil, t.ErrNotFound
|
|
}
|
|
if err = insertGroupEventTx(tx, topic, t.ZeroUid, "group.settings.updated", map[string]any{"topic": topic}); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
return a.groupSettingsByTopic(ctx, topic)
|
|
}
|
|
|
|
func (a *adapter) TenantGroupMemberUpsert(id t.TenantID, member *t.GroupMember) (*t.GroupMember, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupMemberUpsert(id, member)
|
|
}
|
|
if member == nil || member.Topic == "" || member.UserID.IsZero() {
|
|
return nil, t.ErrMalformed
|
|
}
|
|
now := time.Now().UTC().Round(time.Millisecond)
|
|
if member.JoinedAt.IsZero() {
|
|
member.JoinedAt = now
|
|
}
|
|
if member.CreatedAt.IsZero() {
|
|
member.CreatedAt = now
|
|
}
|
|
if member.UpdatedAt.IsZero() {
|
|
member.UpdatedAt = now
|
|
}
|
|
if member.Role == "" {
|
|
member.Role = t.GroupMemberRoleMember
|
|
}
|
|
if member.MemberStatus == "" {
|
|
member.MemberStatus = t.GroupMemberStatusActive
|
|
}
|
|
if member.JoinSource == "" {
|
|
member.JoinSource = "manual"
|
|
}
|
|
if member.Version <= 0 {
|
|
member.Version = 1
|
|
}
|
|
ctx, cancel := a.getContext()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
_, err := a.db.ExecContext(ctx, `INSERT INTO im_group_members
|
|
(topic,user_id,role,member_status,nickname,pinned,mute_notifications,join_source,joined_by,joined_at,version,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
ON DUPLICATE KEY UPDATE role=VALUES(role),member_status=VALUES(member_status),nickname=VALUES(nickname),pinned=VALUES(pinned),mute_notifications=VALUES(mute_notifications),join_source=VALUES(join_source),joined_by=VALUES(joined_by),joined_at=IF(member_status='active',joined_at,VALUES(joined_at)),left_at=NULL,removed_by=NULL,version=version+1,updated_at=VALUES(updated_at)`,
|
|
member.Topic, decodeUID(member.UserID), member.Role, member.MemberStatus, member.Nickname, member.Pinned, member.MuteNotifications,
|
|
member.JoinSource, nullableUID(member.JoinedBy), member.JoinedAt, member.Version, member.CreatedAt, member.UpdatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_ = a.recountGroupMembers(ctx, member.Topic)
|
|
return a.TenantGroupMemberGet(id, member.Topic, member.UserID)
|
|
}
|
|
|
|
func (a *adapter) TenantGroupMemberGet(id t.TenantID, topic string, uid t.Uid) (*t.GroupMember, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupMemberGet(id, topic, uid)
|
|
}
|
|
if topic == "" || uid.IsZero() {
|
|
return nil, t.ErrMalformed
|
|
}
|
|
ctx, cancel := a.getContext()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
return a.groupMemberByQuery(ctx, "SELECT * FROM im_group_members WHERE topic=? AND user_id=?", topic, decodeUID(uid))
|
|
}
|
|
|
|
func (a *adapter) TenantGroupMemberList(id t.TenantID, topic string, opts t.GroupMemberListOptions) ([]t.GroupMember, string, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, "", err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupMemberList(id, topic, opts)
|
|
}
|
|
if topic == "" {
|
|
return nil, "", t.ErrMalformed
|
|
}
|
|
if opts.Limit <= 0 || opts.Limit > 100 {
|
|
opts.Limit = 30
|
|
}
|
|
where := []string{"topic=?", "member_status=?"}
|
|
args := []any{topic, t.GroupMemberStatusActive}
|
|
if opts.Role != "" {
|
|
where = append(where, "role=?")
|
|
args = append(args, opts.Role)
|
|
}
|
|
if opts.Query != "" {
|
|
where = append(where, "nickname LIKE ?")
|
|
args = append(args, "%"+opts.Query+"%")
|
|
}
|
|
if opts.Cursor != "" {
|
|
parsed, err := strconv.ParseInt(opts.Cursor, 10, 64)
|
|
if err != nil || parsed <= 0 {
|
|
return nil, "", t.ErrMalformed
|
|
}
|
|
where = append(where, "id>?")
|
|
args = append(args, parsed)
|
|
}
|
|
args = append(args, opts.Limit+1)
|
|
ctx, cancel := a.getContext()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
rows, err := a.db.QueryxContext(ctx, "SELECT * FROM im_group_members WHERE "+strings.Join(where, " AND ")+" ORDER BY id LIMIT ?", args...)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer rows.Close()
|
|
items := make([]t.GroupMember, 0, opts.Limit)
|
|
next := ""
|
|
for rows.Next() {
|
|
var item t.GroupMember
|
|
if err = rows.StructScan(&item); err != nil {
|
|
return nil, "", err
|
|
}
|
|
scanGroupMember(&item)
|
|
if len(items) == opts.Limit {
|
|
next = strconv.FormatInt(item.ID, 10)
|
|
continue
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, next, rows.Err()
|
|
}
|
|
|
|
func (a *adapter) TenantGroupMemberUpdate(id t.TenantID, topic string, uid t.Uid, patch t.GroupMemberPatch) (*t.GroupMember, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupMemberUpdate(id, topic, uid, patch)
|
|
}
|
|
set := []string{"updated_at=?", "version=version+1"}
|
|
args := []any{time.Now().UTC().Round(time.Millisecond)}
|
|
if patch.Role != nil {
|
|
set = append(set, "role=?")
|
|
args = append(args, *patch.Role)
|
|
}
|
|
if patch.MemberStatus != nil {
|
|
set = append(set, "member_status=?")
|
|
args = append(args, *patch.MemberStatus)
|
|
}
|
|
if patch.Nickname != nil {
|
|
set = append(set, "nickname=?")
|
|
args = append(args, *patch.Nickname)
|
|
}
|
|
if patch.Pinned != nil {
|
|
set = append(set, "pinned=?")
|
|
args = append(args, *patch.Pinned)
|
|
}
|
|
if patch.MuteNotifications != nil {
|
|
set = append(set, "mute_notifications=?")
|
|
args = append(args, *patch.MuteNotifications)
|
|
}
|
|
if patch.LeftAt != nil {
|
|
set = append(set, "left_at=?")
|
|
args = append(args, *patch.LeftAt)
|
|
}
|
|
if patch.RemovedBy != nil {
|
|
set = append(set, "removed_by=?")
|
|
args = append(args, nullableUID(*patch.RemovedBy))
|
|
}
|
|
args = append(args, topic, decodeUID(uid))
|
|
where := "topic=? AND user_id=?"
|
|
if patch.Version > 0 {
|
|
where += " AND version=?"
|
|
args = append(args, patch.Version)
|
|
}
|
|
ctx, cancel := a.getContextForTx()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
tx, err := a.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback()
|
|
res, err := tx.ExecContext(ctx, "UPDATE im_group_members SET "+strings.Join(set, ",")+" WHERE "+where, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if affected, _ := res.RowsAffected(); affected == 0 {
|
|
if patch.Version > 0 {
|
|
return nil, t.ErrInvalidResponse
|
|
}
|
|
return nil, t.ErrNotFound
|
|
}
|
|
if err = recountGroupMembersTx(tx, topic); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = insertGroupEventTx(tx, topic, uid, "group.member.updated", map[string]any{"user_id": uid.UserId()}); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
return a.groupMemberByQuery(ctx, "SELECT * FROM im_group_members WHERE topic=? AND user_id=?", topic, decodeUID(uid))
|
|
}
|
|
|
|
func (a *adapter) TenantGroupInvitationCreate(id t.TenantID, inv *t.GroupInvitation) (*t.GroupInvitation, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupInvitationCreate(id, inv)
|
|
}
|
|
now := time.Now().UTC().Round(time.Millisecond)
|
|
if inv.InvitationID == "" {
|
|
inv.InvitationID = "ginv_" + store.Store.GetUidString()
|
|
}
|
|
if inv.IdempotencyKey == "" {
|
|
inv.IdempotencyKey = inv.InvitationID
|
|
}
|
|
if inv.Status == "" {
|
|
inv.Status = t.GroupInvitationStatusJoined
|
|
}
|
|
if inv.ExpiresAt.IsZero() {
|
|
inv.ExpiresAt = now.Add(7 * 24 * time.Hour)
|
|
}
|
|
ctx, cancel := a.getContext()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
_, err := a.db.ExecContext(ctx, `INSERT INTO im_group_invitations(invitation_id,topic,inviter_user_id,target_user_id,message,status,result,join_request_id,idempotency_key,expires_at,handled_at,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
inv.InvitationID, inv.Topic, decodeUID(inv.InviterUserID), decodeUID(inv.TargetUserID), inv.Message, inv.Status, inv.Result, inv.JoinRequestID, inv.IdempotencyKey, inv.ExpiresAt, inv.HandledAt, now, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return inv, nil
|
|
}
|
|
|
|
func (a *adapter) TenantGroupJoinRequestCreate(id t.TenantID, req *t.GroupJoinRequest) (*t.GroupJoinRequest, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupJoinRequestCreate(id, req)
|
|
}
|
|
now := time.Now().UTC().Round(time.Millisecond)
|
|
if req.Status == "" {
|
|
req.Status = t.GroupJoinRequestStatusPending
|
|
}
|
|
if req.Source == "" {
|
|
req.Source = "manual"
|
|
}
|
|
if req.Version <= 0 {
|
|
req.Version = 1
|
|
}
|
|
if req.IdempotencyKey == "" {
|
|
req.IdempotencyKey = req.RequestID
|
|
}
|
|
ctx, cancel := a.getContextForTx()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
tx, err := a.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback()
|
|
_, err = tx.ExecContext(ctx, `INSERT INTO im_group_join_requests(request_id,topic,applicant_user_id,inviter_user_id,source,qr_token_hash,message,status,reject_reason,expires_at,idempotency_key,version,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
req.RequestID, req.Topic, decodeUID(req.ApplicantUserID), nullableUID(req.InviterUserID), req.Source, req.QRTokenHash, req.Message, req.Status, req.RejectReason, req.ExpiresAt, req.IdempotencyKey, req.Version, now, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = insertGroupEventTx(tx, req.Topic, req.ApplicantUserID, "group.join_request.created", map[string]any{
|
|
"request_id": req.RequestID, "applicant_user_id": req.ApplicantUserID.UserId(),
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
return a.groupJoinRequestByQuery(ctx, "SELECT * FROM im_group_join_requests WHERE request_id=?", req.RequestID)
|
|
}
|
|
|
|
func (a *adapter) TenantGroupJoinRequestList(id t.TenantID, topic, status, cursor string, limit int) ([]t.GroupJoinRequest, string, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, "", err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupJoinRequestList(id, topic, status, cursor, limit)
|
|
}
|
|
where := []string{"topic=?"}
|
|
args := []any{topic}
|
|
if status != "" && status != "all" {
|
|
where = append(where, "status=?")
|
|
args = append(args, status)
|
|
}
|
|
if cursor != "" {
|
|
parsed, err := strconv.ParseInt(cursor, 10, 64)
|
|
if err != nil || parsed <= 0 {
|
|
return nil, "", t.ErrMalformed
|
|
}
|
|
where = append(where, "id>?")
|
|
args = append(args, parsed)
|
|
}
|
|
args = append(args, limit+1)
|
|
ctx, cancel := a.getContext()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
rows, err := a.db.QueryxContext(ctx, "SELECT * FROM im_group_join_requests WHERE "+strings.Join(where, " AND ")+" ORDER BY id LIMIT ?", args...)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer rows.Close()
|
|
items := make([]t.GroupJoinRequest, 0, limit)
|
|
next := ""
|
|
for rows.Next() {
|
|
var item t.GroupJoinRequest
|
|
if err = rows.StructScan(&item); err != nil {
|
|
return nil, "", err
|
|
}
|
|
scanGroupJoinRequest(&item)
|
|
if len(items) == limit {
|
|
next = strconv.FormatInt(item.ID, 10)
|
|
continue
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, next, rows.Err()
|
|
}
|
|
|
|
func (a *adapter) TenantGroupJoinRequestResolve(id t.TenantID, topic, requestID string, actor t.Uid, action, rejectReason string, version int, memberPrivate any, now time.Time) (*t.GroupJoinRequest, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupJoinRequestResolve(id, topic, requestID, actor, action, rejectReason, version, memberPrivate, now)
|
|
}
|
|
status := t.GroupJoinRequestStatusRejected
|
|
if action == "accept" {
|
|
status = t.GroupJoinRequestStatusAccepted
|
|
}
|
|
ctx, cancel := a.getContextForTx()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
tx, err := a.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback()
|
|
var current t.GroupJoinRequest
|
|
err = tx.QueryRowxContext(ctx, "SELECT * FROM im_group_join_requests WHERE topic=? AND request_id=? FOR UPDATE", topic, requestID).StructScan(¤t)
|
|
if err == sql.ErrNoRows {
|
|
return nil, t.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scanGroupJoinRequest(¤t)
|
|
if current.Status != t.GroupJoinRequestStatusPending {
|
|
return nil, t.ErrInvalidResponse
|
|
}
|
|
if version > 0 && current.Version != version {
|
|
return nil, t.ErrInvalidResponse
|
|
}
|
|
if action == "accept" {
|
|
sub := &t.Subscription{TenantID: id, User: current.ApplicantUserID.String(), Topic: topic,
|
|
ModeWant: t.ModeCPublic, ModeGiven: t.ModeCPublic, Private: memberPrivate}
|
|
sub.InitTimes()
|
|
if err = createSubscription(tx, sub, false); err != nil {
|
|
return nil, err
|
|
}
|
|
member := &t.GroupMember{Topic: topic, UserID: current.ApplicantUserID, Role: t.GroupMemberRoleMember,
|
|
MemberStatus: t.GroupMemberStatusActive, JoinSource: "approval", JoinedBy: actor, JoinedAt: now,
|
|
Version: 1, CreatedAt: now, UpdatedAt: now}
|
|
if err = upsertGroupMemberTx(tx, member); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if _, err = tx.ExecContext(ctx, `UPDATE im_group_join_requests SET status=?,reject_reason=?,handled_by=?,handled_at=?,updated_at=?,version=version+1
|
|
WHERE topic=? AND request_id=? AND status=?`, status, rejectReason, decodeUID(actor), now, now, topic, requestID, t.GroupJoinRequestStatusPending); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = recountGroupMembersTx(tx, topic); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = updateGroupTopicSubCountTx(tx, topic); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = insertGroupEventTx(tx, topic, actor, "group.join_request."+status, map[string]any{
|
|
"request_id": requestID, "applicant_user_id": current.ApplicantUserID.UserId(), "status": status,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
return a.groupJoinRequestByQuery(ctx, "SELECT * FROM im_group_join_requests WHERE topic=? AND request_id=?", topic, requestID)
|
|
}
|
|
|
|
func (a *adapter) TenantGroupAnnouncementLatest(id t.TenantID, topic string) (*t.GroupAnnouncement, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupAnnouncementLatest(id, topic)
|
|
}
|
|
ctx, cancel := a.getContext()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
return a.groupAnnouncementByQuery(ctx, "SELECT * FROM im_group_announcements WHERE topic=? AND status=? ORDER BY version DESC,id DESC LIMIT 1", topic, t.GroupAnnouncementStatusCurrent)
|
|
}
|
|
|
|
func (a *adapter) TenantGroupAnnouncementPut(id t.TenantID, ann *t.GroupAnnouncement, expectedVersion int) (*t.GroupAnnouncement, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupAnnouncementPut(id, ann, expectedVersion)
|
|
}
|
|
ctx, cancel := a.getContextForTx()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
tx, err := a.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
tx.Rollback()
|
|
}
|
|
}()
|
|
var currentVersion int
|
|
err = tx.QueryRowContext(ctx, "SELECT COALESCE(MAX(version),0) FROM im_group_announcements WHERE topic=?", ann.Topic).Scan(¤tVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if expectedVersion > 0 && currentVersion > 0 && expectedVersion != currentVersion {
|
|
return nil, t.ErrInvalidResponse
|
|
}
|
|
version := currentVersion + 1
|
|
now := ann.PublishedAt
|
|
if now.IsZero() {
|
|
now = time.Now().UTC().Round(time.Millisecond)
|
|
}
|
|
if _, err = tx.ExecContext(ctx, "UPDATE im_group_announcements SET status=?,updated_at=? WHERE topic=? AND status=?", t.GroupAnnouncementStatusSuperseded, now, ann.Topic, t.GroupAnnouncementStatusCurrent); err != nil {
|
|
return nil, err
|
|
}
|
|
if ann.AnnouncementID == "" {
|
|
ann.AnnouncementID = "ann_" + store.Store.GetUidString()
|
|
}
|
|
if ann.IdempotencyKey == "" {
|
|
ann.IdempotencyKey = ann.AnnouncementID
|
|
}
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO im_group_announcements(announcement_id,topic,author_user_id,content,status,version,published_at,idempotency_key,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?)`, ann.AnnouncementID, ann.Topic, decodeUID(ann.AuthorUserID), ann.Content, t.GroupAnnouncementStatusCurrent, version, now, ann.IdempotencyKey, now, now); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = insertGroupEventTx(tx, ann.Topic, ann.AuthorUserID, "group.announcement.updated", map[string]any{
|
|
"announcement_id": ann.AnnouncementID, "version": version,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
return a.TenantGroupAnnouncementLatest(id, ann.Topic)
|
|
}
|
|
|
|
func (a *adapter) TenantGroupAnnouncementRead(id t.TenantID, topic, announcementID string, uid t.Uid, readAt time.Time) error {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupAnnouncementRead(id, topic, announcementID, uid, readAt)
|
|
}
|
|
ctx, cancel := a.getContext()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
_, err := a.db.ExecContext(ctx, `INSERT INTO im_group_announcement_reads(announcement_id,topic,user_id,read_at)
|
|
VALUES(?,?,?,?) ON DUPLICATE KEY UPDATE read_at=VALUES(read_at),updated_at=CURRENT_TIMESTAMP(3)`, announcementID, topic, decodeUID(uid), readAt)
|
|
return err
|
|
}
|
|
|
|
func (a *adapter) TenantGroupMuteList(id t.TenantID, topic, cursor string, limit int) ([]t.GroupMute, string, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, "", err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupMuteList(id, topic, cursor, limit)
|
|
}
|
|
where := []string{"topic=?", "status=?"}
|
|
args := []any{topic, t.GroupMuteStatusActive}
|
|
if cursor != "" {
|
|
parsed, err := strconv.ParseInt(cursor, 10, 64)
|
|
if err != nil || parsed <= 0 {
|
|
return nil, "", t.ErrMalformed
|
|
}
|
|
where = append(where, "user_id>?")
|
|
args = append(args, parsed)
|
|
}
|
|
args = append(args, limit+1)
|
|
ctx, cancel := a.getContext()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
rows, err := a.db.QueryxContext(ctx, "SELECT * FROM im_group_mutes WHERE "+strings.Join(where, " AND ")+" ORDER BY user_id LIMIT ?", args...)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
defer rows.Close()
|
|
items := make([]t.GroupMute, 0, limit)
|
|
next := ""
|
|
for rows.Next() {
|
|
var item t.GroupMute
|
|
if err = rows.StructScan(&item); err != nil {
|
|
return nil, "", err
|
|
}
|
|
scanGroupMute(&item)
|
|
if len(items) == limit {
|
|
next = strconv.FormatInt(item.UserIDRaw, 10)
|
|
continue
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, next, rows.Err()
|
|
}
|
|
|
|
func (a *adapter) TenantGroupMuteUpsert(id t.TenantID, mute *t.GroupMute) (*t.GroupMute, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupMuteUpsert(id, mute)
|
|
}
|
|
now := mute.MutedAt
|
|
if now.IsZero() {
|
|
now = time.Now().UTC().Round(time.Millisecond)
|
|
}
|
|
if mute.Status == "" {
|
|
mute.Status = t.GroupMuteStatusActive
|
|
}
|
|
ctx, cancel := a.getContextForTx()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
tx, err := a.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tx.Rollback()
|
|
_, err = tx.ExecContext(ctx, `INSERT INTO im_group_mutes(topic,user_id,muted_by,muted_at,expires_at,reason,status,version)
|
|
VALUES(?,?,?,?,?,?,?,1)
|
|
ON DUPLICATE KEY UPDATE muted_by=VALUES(muted_by),muted_at=VALUES(muted_at),expires_at=VALUES(expires_at),reason=VALUES(reason),status=VALUES(status),revoked_by=NULL,revoked_at=NULL,version=version+1,updated_at=CURRENT_TIMESTAMP(3)`,
|
|
mute.Topic, decodeUID(mute.UserID), decodeUID(mute.MutedBy), now, mute.ExpiresAt, mute.Reason, mute.Status)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = insertGroupEventTx(tx, mute.Topic, mute.MutedBy, "group.mute.updated", map[string]any{
|
|
"user_id": mute.UserID.UserId(), "muted": true, "expires_at": mute.ExpiresAt,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
return a.groupMuteByQuery(ctx, "SELECT * FROM im_group_mutes WHERE topic=? AND user_id=?", mute.Topic, decodeUID(mute.UserID))
|
|
}
|
|
|
|
func (a *adapter) TenantGroupMuteDelete(id t.TenantID, topic string, uid, actor t.Uid, now time.Time) error {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupMuteDelete(id, topic, uid, actor, now)
|
|
}
|
|
ctx, cancel := a.getContextForTx()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
tx, err := a.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
res, err := tx.ExecContext(ctx, "UPDATE im_group_mutes SET status=?,revoked_by=?,revoked_at=?,version=version+1,updated_at=? WHERE topic=? AND user_id=? AND status=?",
|
|
t.GroupMuteStatusRevoked, decodeUID(actor), now, now, topic, decodeUID(uid), t.GroupMuteStatusActive)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if affected, _ := res.RowsAffected(); affected == 0 {
|
|
return t.ErrNotFound
|
|
}
|
|
if err = insertGroupEventTx(tx, topic, actor, "group.mute.updated", map[string]any{
|
|
"user_id": uid.UserId(), "muted": false,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (a *adapter) TenantGroupCreate(id t.TenantID, topic *t.Topic, owner t.Uid, ownerPrivate any,
|
|
settings *t.GroupSettings, members []t.Uid, memberPrivate any, now time.Time) error {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupCreate(id, topic, owner, ownerPrivate, settings, members, memberPrivate, now)
|
|
}
|
|
if topic == nil || settings == nil || topic.Id == "" || settings.Topic != topic.Id || owner.IsZero() {
|
|
return t.ErrMalformed
|
|
}
|
|
ctx, cancel := a.getContextForTx()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
tx, err := a.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
if err = a.topicCreate(tx, topic); err != nil {
|
|
return err
|
|
}
|
|
ownerSub := &t.Subscription{TenantID: id, ObjHeader: t.ObjHeader{CreatedAt: topic.CreatedAt}, User: owner.String(),
|
|
Topic: topic.Id, ModeGiven: t.ModeCFull, ModeWant: topic.GetAccess(owner), Private: ownerPrivate}
|
|
ownerSub.InitTimes()
|
|
if err = createSubscription(tx, ownerSub, false); err != nil {
|
|
return err
|
|
}
|
|
settings.MemberCount = 1
|
|
if err = insertGroupSettingsTx(tx, settings); err != nil {
|
|
return err
|
|
}
|
|
ownerMember := &t.GroupMember{Topic: topic.Id, UserID: owner, Role: t.GroupMemberRoleOwner,
|
|
MemberStatus: t.GroupMemberStatusActive, JoinSource: "create", JoinedBy: owner, JoinedAt: now,
|
|
Version: 1, CreatedAt: now, UpdatedAt: now}
|
|
if err = upsertGroupMemberTx(tx, ownerMember); err != nil {
|
|
return err
|
|
}
|
|
seen := map[t.Uid]bool{owner: true}
|
|
for _, uid := range members {
|
|
if uid.IsZero() || seen[uid] {
|
|
continue
|
|
}
|
|
seen[uid] = true
|
|
sub := &t.Subscription{TenantID: id, User: uid.String(), Topic: topic.Id,
|
|
ModeWant: t.ModeCPublic, ModeGiven: t.ModeCPublic, Private: memberPrivate}
|
|
sub.InitTimes()
|
|
if err = createSubscription(tx, sub, false); err != nil {
|
|
return err
|
|
}
|
|
member := &t.GroupMember{Topic: topic.Id, UserID: uid, Role: t.GroupMemberRoleMember,
|
|
MemberStatus: t.GroupMemberStatusActive, JoinSource: "create", JoinedBy: owner, JoinedAt: now,
|
|
Version: 1, CreatedAt: now, UpdatedAt: now}
|
|
if err = upsertGroupMemberTx(tx, member); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err = recountGroupMembersTx(tx, topic.Id); err != nil {
|
|
return err
|
|
}
|
|
if err = updateGroupTopicSubCountTx(tx, topic.Id); err != nil {
|
|
return err
|
|
}
|
|
if err = insertGroupEventTx(tx, topic.Id, owner, "group.created", map[string]any{
|
|
"owner_user_id": owner.UserId(), "member_count": len(seen),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (a *adapter) TenantGroupInviteMember(id t.TenantID, topic string, inviter, target t.Uid,
|
|
message, idempotencyKey string, private any, now time.Time) (bool, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return false, err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupInviteMember(id, topic, inviter, target, message, idempotencyKey, private, now)
|
|
}
|
|
ctx, cancel := a.getContextForTx()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
tx, err := a.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer tx.Rollback()
|
|
var deletedAt sql.NullTime
|
|
err = tx.QueryRowContext(ctx, "SELECT deletedat FROM im_subscriptions WHERE topic=? AND userid=? FOR UPDATE", topic, decodeUID(target)).Scan(&deletedAt)
|
|
if err != nil && err != sql.ErrNoRows {
|
|
return false, err
|
|
}
|
|
if err == nil && !deletedAt.Valid {
|
|
return false, nil
|
|
}
|
|
sub := &t.Subscription{TenantID: id, User: target.String(), Topic: topic,
|
|
ModeWant: t.ModeCPublic, ModeGiven: t.ModeCPublic, Private: private}
|
|
sub.InitTimes()
|
|
if err = createSubscription(tx, sub, false); err != nil {
|
|
return false, err
|
|
}
|
|
invitationID := "ginv_" + store.Store.GetUidString()
|
|
if idempotencyKey == "" {
|
|
idempotencyKey = invitationID
|
|
} else {
|
|
idempotencyKey += ":" + target.UserId()
|
|
}
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO im_group_invitations
|
|
(invitation_id,topic,inviter_user_id,target_user_id,message,status,result,idempotency_key,expires_at,handled_at,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`, invitationID, topic, decodeUID(inviter), decodeUID(target), message,
|
|
t.GroupInvitationStatusJoined, "joined", idempotencyKey, now.Add(7*24*time.Hour), now, now, now); err != nil {
|
|
return false, err
|
|
}
|
|
member := &t.GroupMember{Topic: topic, UserID: target, Role: t.GroupMemberRoleMember,
|
|
MemberStatus: t.GroupMemberStatusActive, JoinSource: "invitation", JoinedBy: inviter, JoinedAt: now,
|
|
Version: 1, CreatedAt: now, UpdatedAt: now}
|
|
if err = upsertGroupMemberTx(tx, member); err != nil {
|
|
return false, err
|
|
}
|
|
if err = recountGroupMembersTx(tx, topic); err != nil {
|
|
return false, err
|
|
}
|
|
if err = updateGroupTopicSubCountTx(tx, topic); err != nil {
|
|
return false, err
|
|
}
|
|
if err = insertGroupEventTx(tx, topic, inviter, "group.member.joined", map[string]any{
|
|
"user_id": target.UserId(), "source": "invitation",
|
|
}); err != nil {
|
|
return false, err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (a *adapter) TenantGroupLeave(id t.TenantID, topic string, uid t.Uid, now time.Time) error {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupLeave(id, topic, uid, now)
|
|
}
|
|
ctx, cancel := a.getContextForTx()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
tx, err := a.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
res, err := tx.ExecContext(ctx, "UPDATE im_subscriptions SET updatedat=?,deletedat=? WHERE topic=? AND userid=? AND deletedat IS NULL", now, now, topic, decodeUID(uid))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if affected, _ := res.RowsAffected(); affected == 0 {
|
|
return t.ErrNotFound
|
|
}
|
|
if _, err = tx.ExecContext(ctx, `UPDATE im_group_members SET member_status=?,left_at=?,version=version+1,updated_at=?
|
|
WHERE topic=? AND user_id=? AND member_status=?`, t.GroupMemberStatusLeft, now, now, topic, decodeUID(uid), t.GroupMemberStatusActive); err != nil {
|
|
return err
|
|
}
|
|
if err = recountGroupMembersTx(tx, topic); err != nil {
|
|
return err
|
|
}
|
|
if err = updateGroupTopicSubCountTx(tx, topic); err != nil {
|
|
return err
|
|
}
|
|
if err = insertGroupEventTx(tx, topic, uid, "group.member.left", map[string]any{"user_id": uid.UserId()}); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (a *adapter) TenantGroupRoleUpdate(id t.TenantID, topic string, actor, target t.Uid,
|
|
role string, modeGiven t.AccessMode, now time.Time) error {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupRoleUpdate(id, topic, actor, target, role, modeGiven, now)
|
|
}
|
|
ctx, cancel := a.getContextForTx()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
tx, err := a.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
res, err := tx.ExecContext(ctx, "UPDATE im_subscriptions SET modegiven=?,updatedat=? WHERE topic=? AND userid=? AND deletedat IS NULL",
|
|
modeGiven.String(), now, topic, decodeUID(target))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if affected, _ := res.RowsAffected(); affected == 0 {
|
|
return t.ErrNotFound
|
|
}
|
|
res, err = tx.ExecContext(ctx, "UPDATE im_group_members SET role=?,version=version+1,updated_at=? WHERE topic=? AND user_id=? AND member_status=?",
|
|
role, now, topic, decodeUID(target), t.GroupMemberStatusActive)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if affected, _ := res.RowsAffected(); affected == 0 {
|
|
return t.ErrNotFound
|
|
}
|
|
if err = insertGroupEventTx(tx, topic, actor, "group.role.updated", map[string]any{
|
|
"user_id": target.UserId(), "role": role,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (a *adapter) TenantGroupOwnerTransfer(id t.TenantID, topic string, previousOwner, newOwner t.Uid,
|
|
version int, now time.Time) error {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupOwnerTransfer(id, topic, previousOwner, newOwner, version, now)
|
|
}
|
|
ctx, cancel := a.getContextForTx()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
tx, err := a.db.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
updates := []struct {
|
|
uid t.Uid
|
|
mode t.AccessMode
|
|
role string
|
|
}{{previousOwner, t.ModeCAdmin | t.ModeCPublic, t.GroupMemberRoleAdmin}, {newOwner, t.ModeCFull, t.GroupMemberRoleOwner}}
|
|
for _, update := range updates {
|
|
res, execErr := tx.ExecContext(ctx, "UPDATE im_subscriptions SET modegiven=?,updatedat=? WHERE topic=? AND userid=? AND deletedat IS NULL",
|
|
update.mode.String(), now, topic, decodeUID(update.uid))
|
|
if execErr != nil {
|
|
return execErr
|
|
}
|
|
if affected, _ := res.RowsAffected(); affected == 0 {
|
|
return t.ErrNotFound
|
|
}
|
|
res, execErr = tx.ExecContext(ctx, "UPDATE im_group_members SET role=?,version=version+1,updated_at=? WHERE topic=? AND user_id=? AND member_status=?",
|
|
update.role, now, topic, decodeUID(update.uid), t.GroupMemberStatusActive)
|
|
if execErr != nil {
|
|
return execErr
|
|
}
|
|
if affected, _ := res.RowsAffected(); affected == 0 {
|
|
return t.ErrNotFound
|
|
}
|
|
}
|
|
if _, err = tx.ExecContext(ctx, "UPDATE im_topics SET owner=?,updatedat=? WHERE name=?", decodeUID(newOwner), now, topic); err != nil {
|
|
return err
|
|
}
|
|
args := []any{decodeUID(newOwner), now, topic}
|
|
query := "UPDATE im_group_settings SET owner_user_id=?,version=version+1,updated_at=? WHERE topic=?"
|
|
if version > 0 {
|
|
query += " AND version=?"
|
|
args = append(args, version)
|
|
}
|
|
res, err := tx.ExecContext(ctx, query, args...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if affected, _ := res.RowsAffected(); affected == 0 {
|
|
return t.ErrInvalidResponse
|
|
}
|
|
if err = insertGroupEventTx(tx, topic, previousOwner, "group.owner.transferred", map[string]any{
|
|
"previous_owner": previousOwner.UserId(), "owner": newOwner.UserId(),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (a *adapter) TenantGroupCanPost(id t.TenantID, topic string, uid t.Uid, admin bool, now time.Time) (bool, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return false, err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupCanPost(id, topic, uid, admin, now)
|
|
}
|
|
if admin {
|
|
return true, nil
|
|
}
|
|
ctx, cancel := a.getContext()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
var muteAll, memberMuted bool
|
|
err := a.db.QueryRowContext(ctx, `SELECT
|
|
COALESCE((SELECT mute_all FROM im_group_settings WHERE topic=? AND group_status=? LIMIT 1),0),
|
|
EXISTS(SELECT 1 FROM im_group_mutes WHERE topic=? AND user_id=? AND status=? AND (expires_at IS NULL OR expires_at>?))`,
|
|
topic, t.GroupStatusActive, topic, decodeUID(uid), t.GroupMuteStatusActive, now).Scan(&muteAll, &memberMuted)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return !muteAll && !memberMuted, nil
|
|
}
|
|
|
|
func (a *adapter) TenantGroupEventListPending(id t.TenantID, limit int, now time.Time) ([]t.GroupEvent, error) {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return nil, err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupEventListPending(id, limit, now)
|
|
}
|
|
ctx, cancel := a.getContext()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
rows, err := a.db.QueryxContext(ctx, `SELECT * FROM im_group_events_outbox
|
|
WHERE status='pending' AND (next_retry_at IS NULL OR next_retry_at<=?) ORDER BY id LIMIT ?`, now, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
events := make([]t.GroupEvent, 0, limit)
|
|
for rows.Next() {
|
|
var event t.GroupEvent
|
|
if err = rows.StructScan(&event); err != nil {
|
|
return nil, err
|
|
}
|
|
event.ActorUserID = store.EncodeUid(event.ActorRaw)
|
|
events = append(events, event)
|
|
}
|
|
return events, rows.Err()
|
|
}
|
|
|
|
func (a *adapter) TenantGroupEventMarkDelivered(id t.TenantID, eventID string, now time.Time) error {
|
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
|
return err
|
|
} else if tenantAdp != a {
|
|
return tenantAdp.TenantGroupEventMarkDelivered(id, eventID, now)
|
|
}
|
|
ctx, cancel := a.getContext()
|
|
if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
_, err := a.db.ExecContext(ctx, `UPDATE im_group_events_outbox SET status='delivered',attempts=attempts+1,
|
|
delivered_at=?,updated_at=? WHERE event_id=? AND status='pending'`, now, now, eventID)
|
|
return err
|
|
}
|
|
|
|
func insertGroupSettingsTx(tx *sqlx.Tx, settings *t.GroupSettings) error {
|
|
_, err := tx.Exec(`INSERT INTO im_group_settings
|
|
(topic,owner_user_id,name,avatar_file_id,description,group_status,join_approval,mute_all,member_upload_files,member_private_chat,member_invite,max_member_count,max_admin_count,member_count,version,created_by,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, settings.Topic, decodeUID(settings.OwnerUserID), settings.Name,
|
|
settings.AvatarFileID, settings.Description, settings.GroupStatus, settings.JoinApproval, settings.MuteAll,
|
|
settings.MemberUploadFiles, settings.MemberPrivateChat, settings.MemberInvite, settings.MaxMemberCount,
|
|
settings.MaxAdminCount, settings.MemberCount, settings.Version, decodeUID(settings.CreatedBy), settings.CreatedAt, settings.UpdatedAt)
|
|
return err
|
|
}
|
|
|
|
func upsertGroupMemberTx(tx *sqlx.Tx, member *t.GroupMember) error {
|
|
_, err := tx.Exec(`INSERT INTO im_group_members
|
|
(topic,user_id,role,member_status,nickname,pinned,mute_notifications,join_source,joined_by,joined_at,version,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
ON DUPLICATE KEY UPDATE role=VALUES(role),member_status=VALUES(member_status),nickname=VALUES(nickname),
|
|
pinned=VALUES(pinned),mute_notifications=VALUES(mute_notifications),join_source=VALUES(join_source),
|
|
joined_by=VALUES(joined_by),joined_at=VALUES(joined_at),left_at=NULL,removed_by=NULL,version=version+1,updated_at=VALUES(updated_at)`,
|
|
member.Topic, decodeUID(member.UserID), member.Role, member.MemberStatus, member.Nickname, member.Pinned,
|
|
member.MuteNotifications, member.JoinSource, nullableUID(member.JoinedBy), member.JoinedAt, member.Version,
|
|
member.CreatedAt, member.UpdatedAt)
|
|
return err
|
|
}
|
|
|
|
func recountGroupMembersTx(tx *sqlx.Tx, topic string) error {
|
|
_, err := tx.Exec(`UPDATE im_group_settings SET member_count=(SELECT COUNT(*) FROM im_group_members
|
|
WHERE topic=? AND member_status=?),updated_at=CURRENT_TIMESTAMP(3) WHERE topic=?`, topic, t.GroupMemberStatusActive, topic)
|
|
return err
|
|
}
|
|
|
|
func updateGroupTopicSubCountTx(tx *sqlx.Tx, topic string) error {
|
|
_, err := tx.Exec(`UPDATE im_topics SET subcnt=(SELECT COUNT(*) FROM im_subscriptions
|
|
WHERE topic IN (?,?) AND deletedat IS NULL) WHERE name=?`, topic, t.GrpToChn(topic), topic)
|
|
return err
|
|
}
|
|
|
|
func insertGroupEventTx(tx *sqlx.Tx, topic string, actor t.Uid, eventType string, payload any) error {
|
|
_, err := tx.Exec(`INSERT INTO im_group_events_outbox(event_id,topic,actor_user_id,event_type,payload,status)
|
|
VALUES(?,?,?,?,?,'pending')`, "gev_"+store.Store.GetUidString(), topic, decodeUID(actor), eventType, common.ToJSON(payload))
|
|
return err
|
|
}
|
|
|
|
func (a *adapter) routeGroupTenant(id t.TenantID) (*adapter, error) {
|
|
if !a.tenantScopedDB {
|
|
tenantAdp, err := a.routedTenantAdapter(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return tenantAdp, nil
|
|
}
|
|
return a, validTenant(id)
|
|
}
|
|
|
|
func (a *adapter) groupSettingsByTopic(ctx context.Context, topic string) (*t.GroupSettings, error) {
|
|
var item t.GroupSettings
|
|
err := a.db.QueryRowxContext(ctx, "SELECT * FROM im_group_settings WHERE topic=?", topic).StructScan(&item)
|
|
if err == sql.ErrNoRows {
|
|
return nil, t.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scanGroupSettings(&item)
|
|
return &item, nil
|
|
}
|
|
|
|
func (a *adapter) groupMemberByQuery(ctx context.Context, query string, args ...any) (*t.GroupMember, error) {
|
|
var item t.GroupMember
|
|
err := a.db.QueryRowxContext(ctx, query, args...).StructScan(&item)
|
|
if err == sql.ErrNoRows {
|
|
return nil, t.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scanGroupMember(&item)
|
|
return &item, nil
|
|
}
|
|
|
|
func (a *adapter) groupJoinRequestByQuery(ctx context.Context, query string, args ...any) (*t.GroupJoinRequest, error) {
|
|
var item t.GroupJoinRequest
|
|
err := a.db.QueryRowxContext(ctx, query, args...).StructScan(&item)
|
|
if err == sql.ErrNoRows {
|
|
return nil, t.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scanGroupJoinRequest(&item)
|
|
return &item, nil
|
|
}
|
|
|
|
func (a *adapter) groupAnnouncementByQuery(ctx context.Context, query string, args ...any) (*t.GroupAnnouncement, error) {
|
|
var item t.GroupAnnouncement
|
|
err := a.db.QueryRowxContext(ctx, query, args...).StructScan(&item)
|
|
if err == sql.ErrNoRows {
|
|
return nil, t.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
item.AuthorUserID = store.EncodeUid(item.AuthorUserIDRaw)
|
|
return &item, nil
|
|
}
|
|
|
|
func (a *adapter) groupMuteByQuery(ctx context.Context, query string, args ...any) (*t.GroupMute, error) {
|
|
var item t.GroupMute
|
|
err := a.db.QueryRowxContext(ctx, query, args...).StructScan(&item)
|
|
if err == sql.ErrNoRows {
|
|
return nil, t.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scanGroupMute(&item)
|
|
return &item, nil
|
|
}
|
|
|
|
func (a *adapter) recountGroupMembers(ctx context.Context, topic string) error {
|
|
_, err := a.db.ExecContext(ctx, "UPDATE im_group_settings SET member_count=(SELECT COUNT(*) FROM im_group_members WHERE topic=? AND member_status=?),updated_at=CURRENT_TIMESTAMP(3) WHERE topic=?", topic, t.GroupMemberStatusActive, topic)
|
|
return err
|
|
}
|
|
|
|
func scanGroupSettings(item *t.GroupSettings) {
|
|
item.OwnerUserID = store.EncodeUid(item.OwnerUserIDRaw)
|
|
item.CreatedBy = store.EncodeUid(item.CreatedByRaw)
|
|
if item.DissolvedByRaw != nil {
|
|
item.DissolvedBy = store.EncodeUid(*item.DissolvedByRaw)
|
|
}
|
|
}
|
|
|
|
func scanGroupMember(item *t.GroupMember) {
|
|
item.UserID = store.EncodeUid(item.UserIDRaw)
|
|
if item.JoinedByRaw != nil {
|
|
item.JoinedBy = store.EncodeUid(*item.JoinedByRaw)
|
|
}
|
|
if item.RemovedByRaw != nil {
|
|
item.RemovedBy = store.EncodeUid(*item.RemovedByRaw)
|
|
}
|
|
}
|
|
|
|
func scanGroupJoinRequest(item *t.GroupJoinRequest) {
|
|
item.ApplicantUserID = store.EncodeUid(item.ApplicantUserRaw)
|
|
if item.InviterUserRaw != nil {
|
|
item.InviterUserID = store.EncodeUid(*item.InviterUserRaw)
|
|
}
|
|
if item.HandledByRaw != nil {
|
|
item.HandledBy = store.EncodeUid(*item.HandledByRaw)
|
|
}
|
|
}
|
|
|
|
func scanGroupMute(item *t.GroupMute) {
|
|
item.UserID = store.EncodeUid(item.UserIDRaw)
|
|
item.MutedBy = store.EncodeUid(item.MutedByRaw)
|
|
if item.RevokedRaw != nil {
|
|
item.RevokedBy = store.EncodeUid(*item.RevokedRaw)
|
|
}
|
|
}
|