fix: repair group management edge cases
This commit is contained in:
+147
-38
@@ -122,11 +122,6 @@ func withIMHTTPProtocol(prefix string, next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
req = req.WithContext(context.WithValue(req.Context(), imHTTPProtocolValidated, true))
|
||||
if req.Method == http.MethodGet || req.Method == http.MethodHead {
|
||||
if principal, err := resolveIMHTTPBearer(req); err == nil {
|
||||
dispatchIMBusinessEvents(principal.TenantID)
|
||||
}
|
||||
}
|
||||
serveIMHTTPIdempotent(wrt, req, next)
|
||||
})
|
||||
}
|
||||
@@ -187,7 +182,6 @@ func serveIMHTTPIdempotent(wrt http.ResponseWriter, req *http.Request, next http
|
||||
next.ServeHTTP(wrt, req)
|
||||
return
|
||||
}
|
||||
dispatchIMBusinessEvents(principal.TenantID)
|
||||
key := strings.TrimSpace(req.Header.Get("Idempotency-Key"))
|
||||
if key == "" || len(key) > 128 {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "Idempotency-Key is required", map[string]any{"field": "Idempotency-Key"})
|
||||
@@ -514,6 +508,7 @@ type imGroupPatchRequest struct {
|
||||
type imGroupMemberSettingsRequest struct {
|
||||
Pinned *bool `json:"pinned,omitempty"`
|
||||
MuteNotifications *bool `json:"mute_notifications,omitempty"`
|
||||
Version int `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type imGroupMemberProfileUpdateRequest struct {
|
||||
@@ -570,12 +565,18 @@ type imGroupOwnerTransferRequest struct {
|
||||
Version int `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type imGroupDissolveRequest struct {
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type imGroupMuteBatchRequest struct {
|
||||
Members []struct {
|
||||
UserID string `json:"user_id"`
|
||||
ExpiresAt string `json:"expires_at,omitempty"`
|
||||
} `json:"members"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
UserIDs []string `json:"user_ids,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Version int `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type imSystemMessageActor struct {
|
||||
@@ -1248,6 +1249,7 @@ func serveIMPasswordResetConfirm(wrt http.ResponseWriter, req *http.Request) {
|
||||
if globals.sessionStore != nil {
|
||||
globals.sessionStore.EvictUser(vc.TargetUser, "")
|
||||
}
|
||||
publishRealtimeEvictUser(tenant.ID, vc.TargetUser, "password_reset", "")
|
||||
writeIMSuccess(wrt, req, map[string]any{"status": "ok"})
|
||||
}
|
||||
|
||||
@@ -4320,10 +4322,7 @@ func serveIMMessageRecall(wrt http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
topic = normalizeConversationSettingsTopic(topic, principal.Uid)
|
||||
var body imMessageRecallRequest
|
||||
if !decodeIMJSONBody(wrt, req, &body) || body.Version <= 0 {
|
||||
if body.Version <= 0 {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "version"})
|
||||
}
|
||||
if !decodeIMJSONBody(wrt, req, &body) {
|
||||
return
|
||||
}
|
||||
msg, authorized, err := authorizeIMMessageMutation(principal, topic, seq, true)
|
||||
@@ -4339,8 +4338,12 @@ func serveIMMessageRecall(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "message recall window expired", nil)
|
||||
return
|
||||
}
|
||||
version := body.Version
|
||||
if version <= 0 {
|
||||
version = msg.Version
|
||||
}
|
||||
recalledAt := time.Now().UTC().Round(time.Millisecond)
|
||||
updated, err := store.Messages.Recall(principal.TenantID, topic, seq, principal.Uid, body.Version, recalledAt)
|
||||
updated, err := store.Messages.Recall(principal.TenantID, topic, seq, principal.Uid, version, recalledAt)
|
||||
if err != nil {
|
||||
writeIMMessageMutationError(wrt, req, err)
|
||||
return
|
||||
@@ -4683,7 +4686,6 @@ func serveIMFriendRequestCreate(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
dispatchIMBusinessEvents(principal.TenantID)
|
||||
if imReliableHTTPEventsEnabled(req) {
|
||||
if err = publishIMRelationEvent(principal.TenantID, principal.Uid, target, created.RequestID,
|
||||
"friend.request.created", "新的好友申请", message); err != nil {
|
||||
@@ -4864,7 +4866,8 @@ func serveIMGroupCreate(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topicName, principal.Uid, "group.created",
|
||||
map[string]any{"topic": topicName, "member_user_ids": userIDs(memberIDs)})
|
||||
created, err := store.Topics.Get(principal.TenantID, topicName)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
@@ -4888,7 +4891,7 @@ func serveIMGroupResource(wrt http.ResponseWriter, req *http.Request) {
|
||||
serveIMGroupSettings(wrt, req, topic)
|
||||
return
|
||||
}
|
||||
if tail == "member-settings" {
|
||||
if tail == "member-settings" || tail == "members/me/settings" {
|
||||
serveIMGroupMemberSettings(wrt, req, topic)
|
||||
return
|
||||
}
|
||||
@@ -5098,6 +5101,9 @@ func serveIMGroupDetail(wrt http.ResponseWriter, req *http.Request, topic string
|
||||
}
|
||||
public := groupPublicMap(group.Public)
|
||||
currentVersion := intFromAny(public["version"])
|
||||
if settings, err := store.GroupBusiness.GetSettings(principal.TenantID, topic); err == nil && settings != nil && settings.Version > 0 {
|
||||
currentVersion = settings.Version
|
||||
}
|
||||
if currentVersion <= 0 {
|
||||
currentVersion = 1
|
||||
}
|
||||
@@ -5151,7 +5157,8 @@ func serveIMGroupDetail(wrt http.ResponseWriter, req *http.Request, topic string
|
||||
return
|
||||
}
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.settings.updated",
|
||||
map[string]any{"topic": topic})
|
||||
updated, err := store.Topics.Get(principal.TenantID, topic)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
@@ -5164,6 +5171,33 @@ func serveIMGroupDetail(wrt http.ResponseWriter, req *http.Request, topic string
|
||||
}
|
||||
}
|
||||
writeIMSuccess(wrt, req, payload)
|
||||
case http.MethodDelete:
|
||||
if !(selfSub.ModeGiven & selfSub.ModeWant).IsOwner() {
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
|
||||
return
|
||||
}
|
||||
if !consumeIMActionTokenHeader(wrt, req, principal, "group_dissolve") {
|
||||
return
|
||||
}
|
||||
var body imGroupDissolveRequest
|
||||
if req.Body != nil && req.Body != http.NoBody {
|
||||
if !decodeIMJSONBody(wrt, req, &body) {
|
||||
return
|
||||
}
|
||||
}
|
||||
settings, err := store.GroupBusiness.Dissolve(principal.TenantID, topic, principal.Uid, strings.TrimSpace(body.Reason), time.Now().UTC().Round(time.Millisecond))
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.dissolved",
|
||||
map[string]any{"topic": topic})
|
||||
payload := map[string]any{"topic": topic, "deleted": true}
|
||||
if settings != nil {
|
||||
payload["status"] = settings.GroupStatus
|
||||
payload["version"] = settings.Version
|
||||
}
|
||||
writeIMSuccess(wrt, req, payload)
|
||||
default:
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
}
|
||||
@@ -5205,13 +5239,21 @@ func serveIMGroupMemberSettings(wrt http.ResponseWriter, req *http.Request, topi
|
||||
return
|
||||
}
|
||||
private := groupPrivateMap(selfSub.Private)
|
||||
currentVersion := intFromAny(private["version"])
|
||||
if currentVersion <= 0 {
|
||||
currentVersion = 1
|
||||
}
|
||||
if body.Version > 0 && body.Version != currentVersion {
|
||||
writeIMError(wrt, req, http.StatusConflict, 30006, "VERSION_CONFLICT", "version conflict", map[string]any{"current_version": currentVersion})
|
||||
return
|
||||
}
|
||||
if body.Pinned != nil {
|
||||
private["pinned"] = *body.Pinned
|
||||
}
|
||||
if body.MuteNotifications != nil {
|
||||
private["mute_notifications"] = *body.MuteNotifications
|
||||
}
|
||||
private["version"] = intFromAny(private["version"]) + 1
|
||||
private["version"] = currentVersion + 1
|
||||
private["updated_at"] = time.Now().UTC().Round(time.Millisecond).Format(time.RFC3339Nano)
|
||||
if err := store.Subs.Update(principal.TenantID, topic, principal.Uid, map[string]any{"Private": private}); err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
@@ -5224,7 +5266,8 @@ func serveIMGroupMemberSettings(wrt http.ResponseWriter, req *http.Request, topi
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.member.updated",
|
||||
map[string]any{"user_id": principal.Uid.UserId()})
|
||||
selfSub.Private = private
|
||||
writeIMSuccess(wrt, req, groupMemberSettingsPayload(selfSub))
|
||||
}
|
||||
@@ -5275,7 +5318,8 @@ func serveIMGroupMyProfile(wrt http.ResponseWriter, req *http.Request, topic str
|
||||
if _, err := store.GroupBusiness.UpdateMember(principal.TenantID, topic, principal.Uid, types.GroupMemberPatch{Nickname: &nickname}); err != nil {
|
||||
if errors.Is(err, types.ErrNotFound) {
|
||||
logs.Warn.Printf("group[%s]: member profile saved to subscription only; business member row missing for user %s", topic, principal.Uid.UserId())
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.member.updated",
|
||||
map[string]any{"user_id": principal.Uid.UserId()})
|
||||
writeIMSuccess(wrt, req, map[string]any{"nickname": nickname, "version": currentVersion + 1, "updated_at": updatedAt.Format(time.RFC3339Nano)})
|
||||
return
|
||||
}
|
||||
@@ -5285,7 +5329,8 @@ func serveIMGroupMyProfile(wrt http.ResponseWriter, req *http.Request, topic str
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.member.updated",
|
||||
map[string]any{"user_id": principal.Uid.UserId()})
|
||||
writeIMSuccess(wrt, req, map[string]any{"nickname": nickname, "version": currentVersion + 1, "updated_at": updatedAt.Format(time.RFC3339Nano)})
|
||||
}
|
||||
|
||||
@@ -5306,7 +5351,8 @@ func serveIMGroupLeave(wrt http.ResponseWriter, req *http.Request, topic string)
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.member.left",
|
||||
map[string]any{"user_id": principal.Uid.UserId()})
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "status": "left"})
|
||||
}
|
||||
|
||||
@@ -5466,7 +5512,8 @@ func serveIMGroupInvitations(wrt http.ResponseWriter, req *http.Request, topic s
|
||||
publishIMGroupInvitationNotification(principal.TenantID, inv)
|
||||
results = append(results, item)
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.invitations.updated",
|
||||
map[string]any{"topic": topic, "items": results})
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": results})
|
||||
}
|
||||
|
||||
@@ -5630,7 +5677,8 @@ func serveIMGroupInvitationResource(wrt http.ResponseWriter, req *http.Request)
|
||||
[]imSystemMessageAction{{Type: "group_profile", Label: "查看群聊", TargetID: inv.Topic}},
|
||||
map[string]any{"invitation_id": inv.InvitationID, "source": "invitation"})
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, inv.Topic, principal.Uid, "group.member.joined",
|
||||
map[string]any{"user_id": principal.Uid.UserId(), "invitation_id": inv.InvitationID})
|
||||
writeIMSuccess(wrt, req, groupInvitationPayload(inv))
|
||||
}
|
||||
|
||||
@@ -5665,7 +5713,8 @@ func serveIMGroupJoinRequests(wrt http.ResponseWriter, req *http.Request, topic
|
||||
writeIMRelationError(wrt, req, joinErr)
|
||||
return
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.member.joined",
|
||||
map[string]any{"user_id": principal.Uid.UserId(), "joined": joined, "source": "open_group"})
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "status": "joined", "joined": joined, "source": "open_group"})
|
||||
return
|
||||
}
|
||||
@@ -5694,7 +5743,8 @@ func serveIMGroupJoinRequests(wrt http.ResponseWriter, req *http.Request, topic
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.join_request.created",
|
||||
map[string]any{"request_id": item.RequestID, "user_id": principal.Uid.UserId()})
|
||||
writeIMSuccess(wrt, req, groupJoinRequestPayload(item))
|
||||
return
|
||||
}
|
||||
@@ -5723,6 +5773,7 @@ func serveIMGroupJoinRequests(wrt http.ResponseWriter, req *http.Request, topic
|
||||
}
|
||||
items = nil
|
||||
}
|
||||
items = dedupePendingGroupJoinRequests(items, status)
|
||||
payloads, err := groupJoinRequestPayloadsWithUsers(principal.TenantID, items)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
@@ -5771,7 +5822,8 @@ func serveIMGroupJoinRequestAction(wrt http.ResponseWriter, req *http.Request, t
|
||||
[]imSystemMessageAction{{Type: "group_profile", Label: "查看群聊", TargetID: topic}},
|
||||
map[string]any{"request_id": updated.RequestID, "source": "approval"})
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.join_request."+updated.Status,
|
||||
map[string]any{"request_id": updated.RequestID, "user_id": updated.ApplicantUserID.UserId(), "status": updated.Status})
|
||||
writeIMSuccess(wrt, req, groupJoinRequestPayload(updated))
|
||||
}
|
||||
|
||||
@@ -5843,7 +5895,8 @@ func serveIMGroupAnnouncements(wrt http.ResponseWriter, req *http.Request, topic
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.announcement.updated",
|
||||
map[string]any{"announcement_id": announcement.AnnouncementID, "topic": topic})
|
||||
writeIMSuccess(wrt, req, groupAnnouncementPayloadWithPermissions(topic, announcement, selfSub))
|
||||
case strings.HasPrefix(tail, "announcements/") && strings.HasSuffix(tail, "/read") && req.Method == http.MethodPost:
|
||||
announcementID := strings.TrimSuffix(strings.TrimPrefix(tail, "announcements/"), "/read")
|
||||
@@ -6001,7 +6054,8 @@ func serveIMGroupGovernance(wrt http.ResponseWriter, req *http.Request, topic st
|
||||
nil,
|
||||
map[string]any{"mute_all": *body.MuteAll, "version": settings.Version})
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.settings.updated",
|
||||
map[string]any{"topic": topic, "version": settings.Version})
|
||||
writeIMSuccess(wrt, req, groupGovernancePayloadFromSettings(settings, selfSub))
|
||||
default:
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
@@ -6082,7 +6136,8 @@ func serveIMGroupAdmins(wrt http.ResponseWriter, req *http.Request, topic, tail
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.role.updated",
|
||||
map[string]any{"topic": topic, "results": results})
|
||||
writeIMSuccess(wrt, req, map[string]any{"succeeded": results, "admins": groupAdminItems(members), "version": body.Version + 1, "updated_at": time.Now().UTC().Format(time.RFC3339Nano)})
|
||||
case strings.HasPrefix(tail, "admins/") && req.Method == http.MethodDelete:
|
||||
userID := strings.TrimPrefix(tail, "admins/")
|
||||
@@ -6096,7 +6151,8 @@ func serveIMGroupAdmins(wrt http.ResponseWriter, req *http.Request, topic, tail
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.role.updated",
|
||||
map[string]any{"user_id": uid.UserId(), "role": "member"})
|
||||
writeIMSuccess(wrt, req, map[string]any{"user_id": uid.UserId(), "role": "member", "updated_at": time.Now().UTC().Format(time.RFC3339Nano)})
|
||||
default:
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
@@ -6120,6 +6176,9 @@ func serveIMGroupOwnerTransfer(wrt http.ResponseWriter, req *http.Request, topic
|
||||
if !decodeIMJSONBody(wrt, req, &body) {
|
||||
return
|
||||
}
|
||||
if !consumeIMActionTokenHeader(wrt, req, principal, "group_owner_transfer") {
|
||||
return
|
||||
}
|
||||
target := types.ParseUserId(strings.TrimSpace(body.TargetUserID))
|
||||
if target.IsZero() || target == principal.Uid {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "target_user_id"})
|
||||
@@ -6135,7 +6194,8 @@ func serveIMGroupOwnerTransfer(wrt http.ResponseWriter, req *http.Request, topic
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.owner.transferred",
|
||||
map[string]any{"owner": target.UserId(), "previous_owner": principal.Uid.UserId()})
|
||||
writeIMSuccess(wrt, req, map[string]any{"owner": target.UserId(), "previous_owner": principal.Uid.UserId(), "version": body.Version + 1})
|
||||
}
|
||||
|
||||
@@ -6162,8 +6222,16 @@ func serveIMGroupMutes(wrt http.ResponseWriter, req *http.Request, topic, tail s
|
||||
return
|
||||
}
|
||||
if len(body.Members) == 0 || len(body.Members) > 100 {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "members"})
|
||||
return
|
||||
for _, userID := range cleanStringList(body.UserIDs, 100) {
|
||||
body.Members = append(body.Members, struct {
|
||||
UserID string `json:"user_id"`
|
||||
ExpiresAt string `json:"expires_at,omitempty"`
|
||||
}{UserID: userID})
|
||||
}
|
||||
if len(body.Members) == 0 || len(body.Members) > 100 {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "members"})
|
||||
return
|
||||
}
|
||||
}
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
mutes := make([]types.GroupMute, 0, len(body.Members))
|
||||
@@ -6185,7 +6253,8 @@ func serveIMGroupMutes(wrt http.ResponseWriter, req *http.Request, topic, tail s
|
||||
}
|
||||
mutes = append(mutes, *mute)
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.mute.updated",
|
||||
map[string]any{"topic": topic, "items": groupMutePayloads(mutes)})
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": groupMutePayloads(mutes)})
|
||||
case strings.HasPrefix(tail, "mutes/") && req.Method == http.MethodDelete:
|
||||
uid := types.ParseUserId(strings.TrimSpace(strings.TrimPrefix(tail, "mutes/")))
|
||||
@@ -6197,7 +6266,8 @@ func serveIMGroupMutes(wrt http.ResponseWriter, req *http.Request, topic, tail s
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.mute.updated",
|
||||
map[string]any{"user_id": uid.UserId(), "muted": false})
|
||||
writeIMSuccess(wrt, req, map[string]any{"user_id": uid.UserId(), "muted": false})
|
||||
default:
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
@@ -6639,7 +6709,6 @@ func serveIMFriendRequestAction(wrt http.ResponseWriter, req *http.Request) {
|
||||
{UserID: updated.TargetID.UserId(), Click: map[string]any{"type": "user_profile", "target_id": updated.TargetID.UserId()}},
|
||||
}, nil, map[string]any{"request_id": updated.RequestID})
|
||||
}
|
||||
dispatchIMBusinessEvents(principal.TenantID)
|
||||
if imReliableHTTPEventsEnabled(req) {
|
||||
eventType := "friend.request." + status
|
||||
if err = publishIMRelationEvent(principal.TenantID, principal.Uid, updated.ApplicantID,
|
||||
@@ -7163,11 +7232,30 @@ func defaultIMActionTarget(action, fallback string) string {
|
||||
switch action {
|
||||
case "set_independent_password", "enable_youth_mode", "close_youth_mode", "reset_independent_password":
|
||||
return "youth_mode"
|
||||
case "group_owner_transfer", "group_dissolve":
|
||||
return action
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
func consumeIMActionTokenHeader(wrt http.ResponseWriter, req *http.Request, principal *imPrincipal, action string) bool {
|
||||
actionToken := strings.TrimSpace(req.Header.Get("X-Action-Token"))
|
||||
if actionToken == "" {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "X-Action-Token"})
|
||||
return false
|
||||
}
|
||||
if store.Security == nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return false
|
||||
}
|
||||
if _, err := store.Security.ConsumeActionToken(principal.TenantID, principal.Uid, actionToken, action, defaultIMActionTarget(action, action), time.Now().UTC().Round(time.Millisecond)); err != nil {
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30042, imReasonActionToken, "action token invalid", nil)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func hashIMActionToken(tenantID types.TenantID, uid types.Uid, action, target, tokenID, secret string) string {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(fmt.Sprintf("%d", tenantID)))
|
||||
@@ -7930,6 +8018,26 @@ func groupJoinRequestPayloads(items []types.GroupJoinRequest) []map[string]any {
|
||||
return out
|
||||
}
|
||||
|
||||
func dedupePendingGroupJoinRequests(items []types.GroupJoinRequest, status string) []types.GroupJoinRequest {
|
||||
if len(items) < 2 || status != types.GroupJoinRequestStatusPending {
|
||||
return items
|
||||
}
|
||||
seen := make(map[string]struct{}, len(items))
|
||||
deduped := make([]types.GroupJoinRequest, 0, len(items))
|
||||
for i := len(items) - 1; i >= 0; i-- {
|
||||
key := items[i].Topic + ":" + items[i].ApplicantUserID.UserId()
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
deduped = append(deduped, items[i])
|
||||
}
|
||||
for left, right := 0, len(deduped)-1; left < right; left, right = left+1, right-1 {
|
||||
deduped[left], deduped[right] = deduped[right], deduped[left]
|
||||
}
|
||||
return deduped
|
||||
}
|
||||
|
||||
func groupJoinRequestPayloadsWithUsers(tenantID types.TenantID, items []types.GroupJoinRequest) ([]map[string]any, error) {
|
||||
payloads := groupJoinRequestPayloads(items)
|
||||
if len(items) == 0 || store.Users == nil {
|
||||
@@ -9327,7 +9435,8 @@ func userProfilePayload10(profile *types.UserProfile) map[string]any {
|
||||
|
||||
func validIMSecurityChallengeScene(scene string) bool {
|
||||
switch strings.TrimSpace(scene) {
|
||||
case "set_independent_password", "enable_youth_mode", "close_youth_mode", "reset_independent_password":
|
||||
case "set_independent_password", "enable_youth_mode", "close_youth_mode", "reset_independent_password",
|
||||
"group_owner_transfer", "group_dissolve":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -9930,7 +10039,6 @@ func requireIMGroupMember(wrt http.ResponseWriter, req *http.Request, topic stri
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
|
||||
return nil, nil, false
|
||||
}
|
||||
dispatchIMGroupEvents(principal.TenantID)
|
||||
sub, err := store.Subs.Get(principal.TenantID, topic, principal.Uid, false)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
@@ -11297,6 +11405,7 @@ func broadcastIMConversationSystemMessage(tenantID types.TenantID, msg *types.Me
|
||||
logs.Warn.Printf("im system message: hub route queue full topic=%s seq=%d", msg.Topic, msg.SeqId)
|
||||
}
|
||||
deliverIMConversationSystemMessageToP2PSessions(tenantID, msg, out)
|
||||
publishRealtimeTopicMessage(msg.Topic, out)
|
||||
broadcastIMConversationSystemPresence(tenantID, msg, timestamp)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user