feat: complete IM P0 and P1 reliability work
This commit is contained in:
@@ -10,6 +10,8 @@ import (
|
||||
"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"
|
||||
)
|
||||
@@ -130,11 +132,16 @@ func (a *adapter) TenantGroupSettingsUpdate(id t.TenantID, topic string, patch t
|
||||
where += " AND version=?"
|
||||
args = append(args, patch.Version)
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
ctx, cancel := a.getContextForTx()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
res, err := a.db.ExecContext(ctx, "UPDATE im_group_settings SET "+strings.Join(set, ",")+" WHERE "+where, args...)
|
||||
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
|
||||
}
|
||||
@@ -144,6 +151,12 @@ func (a *adapter) TenantGroupSettingsUpdate(id t.TenantID, topic string, patch t
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -310,11 +323,16 @@ func (a *adapter) TenantGroupMemberUpdate(id t.TenantID, topic string, uid t.Uid
|
||||
where += " AND version=?"
|
||||
args = append(args, patch.Version)
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
ctx, cancel := a.getContextForTx()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
res, err := a.db.ExecContext(ctx, "UPDATE im_group_members SET "+strings.Join(set, ",")+" WHERE "+where, args...)
|
||||
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
|
||||
}
|
||||
@@ -324,7 +342,15 @@ func (a *adapter) TenantGroupMemberUpdate(id t.TenantID, topic string, uid t.Uid
|
||||
}
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
_ = a.recountGroupMembers(ctx, topic)
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -379,16 +405,29 @@ func (a *adapter) TenantGroupJoinRequestCreate(id t.TenantID, req *t.GroupJoinRe
|
||||
if req.IdempotencyKey == "" {
|
||||
req.IdempotencyKey = req.RequestID
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
ctx, cancel := a.getContextForTx()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err := a.db.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)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -439,36 +478,72 @@ func (a *adapter) TenantGroupJoinRequestList(id t.TenantID, topic, status, curso
|
||||
return items, next, rows.Err()
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupJoinRequestResolve(id t.TenantID, topic, requestID string, actor t.Uid, action, rejectReason string, version int, now time.Time) (*t.GroupJoinRequest, error) {
|
||||
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, now)
|
||||
return tenantAdp.TenantGroupJoinRequestResolve(id, topic, requestID, actor, action, rejectReason, version, memberPrivate, now)
|
||||
}
|
||||
status := t.GroupJoinRequestStatusRejected
|
||||
if action == "accept" {
|
||||
status = t.GroupJoinRequestStatusAccepted
|
||||
}
|
||||
args := []any{status, rejectReason, decodeUID(actor), now, now, topic, requestID, t.GroupJoinRequestStatusPending}
|
||||
where := "topic=? AND request_id=? AND status=?"
|
||||
if version > 0 {
|
||||
where += " AND version=?"
|
||||
args = append(args, version)
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
ctx, cancel := a.getContextForTx()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
res, err := a.db.ExecContext(ctx, "UPDATE im_group_join_requests SET status=?,reject_reason=?,handled_by=?,handled_at=?,updated_at=?,version=version+1 WHERE "+where, args...)
|
||||
tx, err := a.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if affected, _ := res.RowsAffected(); affected == 0 {
|
||||
if version > 0 {
|
||||
return nil, t.ErrInvalidResponse
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -530,6 +605,11 @@ func (a *adapter) TenantGroupAnnouncementPut(id t.TenantID, ann *t.GroupAnnounce
|
||||
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
|
||||
}
|
||||
@@ -607,17 +687,30 @@ func (a *adapter) TenantGroupMuteUpsert(id t.TenantID, mute *t.GroupMute) (*t.Gr
|
||||
if mute.Status == "" {
|
||||
mute.Status = t.GroupMuteStatusActive
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
ctx, cancel := a.getContextForTx()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err := a.db.ExecContext(ctx, `INSERT INTO im_group_mutes(topic,user_id,muted_by,muted_at,expires_at,reason,status,version)
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -627,11 +720,16 @@ func (a *adapter) TenantGroupMuteDelete(id t.TenantID, topic string, uid, actor
|
||||
} else if tenantAdp != a {
|
||||
return tenantAdp.TenantGroupMuteDelete(id, topic, uid, actor, now)
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
ctx, cancel := a.getContextForTx()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
res, err := a.db.ExecContext(ctx, "UPDATE im_group_mutes SET status=?,revoked_by=?,revoked_at=?,version=version+1,updated_at=? WHERE topic=? AND user_id=? AND status=?",
|
||||
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
|
||||
@@ -639,7 +737,397 @@ func (a *adapter) TenantGroupMuteDelete(id t.TenantID, topic string, uid, actor
|
||||
if affected, _ := res.RowsAffected(); affected == 0 {
|
||||
return t.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user