feat: persist group business state in tables
This commit is contained in:
+334
-124
@@ -2956,6 +2956,40 @@ func serveIMGroupCreate(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
if _, err := store.GroupBusiness.CreateSettings(principal.TenantID, &types.GroupSettings{
|
||||
Topic: topicName,
|
||||
OwnerUserID: principal.Uid,
|
||||
Name: name,
|
||||
AvatarFileID: strings.TrimSpace(body.AvatarFileID),
|
||||
GroupStatus: types.GroupStatusActive,
|
||||
MemberUploadFiles: true,
|
||||
MemberPrivateChat: true,
|
||||
MemberInvite: true,
|
||||
MaxAdminCount: groupMaxAdminCount,
|
||||
MemberCount: 1,
|
||||
Version: 1,
|
||||
CreatedBy: principal.Uid,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}); err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
if _, err := store.GroupBusiness.UpsertMember(principal.TenantID, &types.GroupMember{
|
||||
Topic: topicName,
|
||||
UserID: principal.Uid,
|
||||
Role: types.GroupMemberRoleOwner,
|
||||
MemberStatus: types.GroupMemberStatusActive,
|
||||
JoinSource: "create",
|
||||
JoinedBy: principal.Uid,
|
||||
JoinedAt: now,
|
||||
Version: 1,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}); err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
var members []*types.Subscription
|
||||
seen := map[types.Uid]bool{principal.Uid: true}
|
||||
for _, rawID := range body.MemberUserIDs {
|
||||
@@ -2978,6 +3012,30 @@ func serveIMGroupCreate(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
for _, sub := range members {
|
||||
uid := types.ParseUid(sub.User)
|
||||
if uid.IsZero() {
|
||||
uid = types.ParseUserId(sub.User)
|
||||
}
|
||||
if uid.IsZero() {
|
||||
continue
|
||||
}
|
||||
if _, err := store.GroupBusiness.UpsertMember(principal.TenantID, &types.GroupMember{
|
||||
Topic: topicName,
|
||||
UserID: uid,
|
||||
Role: types.GroupMemberRoleMember,
|
||||
MemberStatus: types.GroupMemberStatusActive,
|
||||
JoinSource: "create",
|
||||
JoinedBy: principal.Uid,
|
||||
JoinedAt: now,
|
||||
Version: 1,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}); err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
_ = store.Topics.UpdateSubCnt(principal.TenantID, topicName)
|
||||
}
|
||||
created, err := store.Topics.Get(principal.TenantID, topicName)
|
||||
@@ -3186,7 +3244,11 @@ func serveIMGroupDetail(wrt http.ResponseWriter, req *http.Request, topic string
|
||||
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "group not found", nil)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, groupDetailPayload(group, selfSub))
|
||||
payload := groupDetailPayload(group, selfSub)
|
||||
if settings, err := store.GroupBusiness.GetSettings(principal.TenantID, topic); err == nil {
|
||||
payload = mergeGroupSettingsPayload(payload, settings)
|
||||
}
|
||||
writeIMSuccess(wrt, req, payload)
|
||||
case http.MethodPatch:
|
||||
access := selfSub.ModeGiven & selfSub.ModeWant
|
||||
if !access.IsAdmin() {
|
||||
@@ -3239,12 +3301,18 @@ func serveIMGroupDetail(wrt http.ResponseWriter, req *http.Request, topic string
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
patch := types.GroupSettingsPatch{Name: body.Name, AvatarFileID: body.AvatarFileID, Description: body.Description, Version: body.Version}
|
||||
_, _ = store.GroupBusiness.UpdateSettings(principal.TenantID, topic, patch)
|
||||
updated, err := store.Topics.Get(principal.TenantID, topic)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, groupDetailPayload(updated, selfSub))
|
||||
payload := groupDetailPayload(updated, selfSub)
|
||||
if settings, err := store.GroupBusiness.GetSettings(principal.TenantID, topic); err == nil {
|
||||
payload = mergeGroupSettingsPayload(payload, settings)
|
||||
}
|
||||
writeIMSuccess(wrt, req, payload)
|
||||
default:
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
}
|
||||
@@ -3264,7 +3332,12 @@ func serveIMGroupSettings(wrt http.ResponseWriter, req *http.Request, topic stri
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, groupSettingsPayload(group, selfSub))
|
||||
payload := groupSettingsPayload(group, selfSub)
|
||||
if settings, err := store.GroupBusiness.GetSettings(principal.TenantID, topic); err == nil {
|
||||
payload = mergeGroupSettingsPayload(payload, settings)
|
||||
payload["governance"] = groupGovernancePayloadFromSettings(settings, selfSub)
|
||||
}
|
||||
writeIMSuccess(wrt, req, payload)
|
||||
}
|
||||
|
||||
func serveIMGroupMemberSettings(wrt http.ResponseWriter, req *http.Request, topic string) {
|
||||
@@ -3293,6 +3366,7 @@ func serveIMGroupMemberSettings(wrt http.ResponseWriter, req *http.Request, topi
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
_, _ = store.GroupBusiness.UpdateMember(principal.TenantID, topic, principal.Uid, types.GroupMemberPatch{Pinned: body.Pinned, MuteNotifications: body.MuteNotifications})
|
||||
selfSub.Private = private
|
||||
writeIMSuccess(wrt, req, groupMemberSettingsPayload(selfSub))
|
||||
}
|
||||
@@ -3332,6 +3406,7 @@ func serveIMGroupMyProfile(wrt http.ResponseWriter, req *http.Request, topic str
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
_, _ = store.GroupBusiness.UpdateMember(principal.TenantID, topic, principal.Uid, types.GroupMemberPatch{Nickname: &nickname, Version: body.Version})
|
||||
writeIMSuccess(wrt, req, map[string]any{"nickname": nickname, "version": currentVersion + 1, "updated_at": updatedAt.Format(time.RFC3339Nano)})
|
||||
}
|
||||
|
||||
@@ -3352,6 +3427,10 @@ func serveIMGroupLeave(wrt http.ResponseWriter, req *http.Request, topic string)
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
leftAt := time.Now().UTC().Round(time.Millisecond)
|
||||
leftStatus := types.GroupMemberStatusLeft
|
||||
_, _ = store.GroupBusiness.UpdateMember(principal.TenantID, topic, principal.Uid, types.GroupMemberPatch{MemberStatus: &leftStatus, LeftAt: &leftAt})
|
||||
_, _ = store.GroupBusiness.UpdateSettings(principal.TenantID, topic, types.GroupSettingsPatch{MemberCountDelta: -1})
|
||||
_ = store.Topics.UpdateSubCnt(principal.TenantID, topic)
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "status": "left"})
|
||||
}
|
||||
@@ -3448,6 +3527,43 @@ func serveIMGroupInvitations(wrt http.ResponseWriter, req *http.Request, topic s
|
||||
item["status"] = "failed"
|
||||
item["reason"] = relationErrorReason(err)
|
||||
} else {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
if _, err := store.GroupBusiness.CreateInvitation(principal.TenantID, &types.GroupInvitation{
|
||||
InvitationID: "ginv_" + store.Store.GetUidString(),
|
||||
Topic: topic,
|
||||
InviterUserID: principal.Uid,
|
||||
TargetUserID: uid,
|
||||
Message: strings.TrimSpace(body.Message),
|
||||
Status: types.GroupInvitationStatusJoined,
|
||||
Result: "joined",
|
||||
ExpiresAt: now.Add(7 * 24 * time.Hour),
|
||||
HandledAt: &now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
IdempotencyKey: strings.TrimSpace(req.Header.Get("Idempotency-Key")),
|
||||
}); err != nil {
|
||||
item["status"] = "failed"
|
||||
item["reason"] = relationErrorReason(err)
|
||||
results = append(results, item)
|
||||
continue
|
||||
}
|
||||
if _, err := store.GroupBusiness.UpsertMember(principal.TenantID, &types.GroupMember{
|
||||
Topic: topic,
|
||||
UserID: uid,
|
||||
Role: types.GroupMemberRoleMember,
|
||||
MemberStatus: types.GroupMemberStatusActive,
|
||||
JoinSource: "invitation",
|
||||
JoinedBy: principal.Uid,
|
||||
JoinedAt: now,
|
||||
Version: 1,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}); err != nil {
|
||||
item["status"] = "failed"
|
||||
item["reason"] = relationErrorReason(err)
|
||||
results = append(results, item)
|
||||
continue
|
||||
}
|
||||
item["status"] = "joined"
|
||||
}
|
||||
results = append(results, item)
|
||||
@@ -3467,50 +3583,42 @@ func serveIMGroupJoinRequests(wrt http.ResponseWriter, req *http.Request, topic
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
|
||||
return
|
||||
}
|
||||
group, err := store.Topics.Get(principal.TenantID, topic)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
status := strings.TrimSpace(req.URL.Query().Get("status"))
|
||||
if status == "" {
|
||||
status = "pending"
|
||||
}
|
||||
items := filterGroupJoinRequests(groupJoinRequests(group), status)
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": items, "next_cursor": ""})
|
||||
items, nextCursor, err := store.GroupBusiness.ListJoinRequests(principal.TenantID, topic, status, strings.TrimSpace(req.URL.Query().Get("cursor")), parseIMLimit(req.URL.Query().Get("limit"), 50, 100))
|
||||
if err != nil {
|
||||
if !errors.Is(err, types.ErrNotFound) {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
items = nil
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": groupJoinRequestPayloads(items), "next_cursor": nextCursor})
|
||||
case http.MethodPost:
|
||||
var body imGroupJoinRequestCreateRequest
|
||||
if !decodeIMJSONBody(wrt, req, &body) {
|
||||
return
|
||||
}
|
||||
group, err := store.Topics.Get(principal.TenantID, topic)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
aux := groupAuxMap(group.Aux)
|
||||
requests := groupJoinRequests(group)
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
requestID := "gjr_" + store.Store.GetUidString()
|
||||
item := map[string]any{
|
||||
"request_id": requestID,
|
||||
"user_id": principal.Uid.UserId(),
|
||||
"inviter_id": "",
|
||||
"source": defaultString(strings.TrimSpace(body.Source), "manual"),
|
||||
"message": strings.TrimSpace(body.Message),
|
||||
"status": "pending",
|
||||
"version": 1,
|
||||
"created_at": now.Format(time.RFC3339Nano),
|
||||
"updated_at": now.Format(time.RFC3339Nano),
|
||||
"qr_token": strings.TrimSpace(body.QRToken),
|
||||
}
|
||||
requests = append(requests, item)
|
||||
aux["join_requests"] = requests
|
||||
if err = store.Topics.Update(principal.TenantID, topic, map[string]any{"Aux": aux}); err != nil {
|
||||
item, err := store.GroupBusiness.CreateJoinRequest(principal.TenantID, &types.GroupJoinRequest{
|
||||
RequestID: requestID,
|
||||
Topic: topic,
|
||||
ApplicantUserID: principal.Uid,
|
||||
Source: defaultString(strings.TrimSpace(body.Source), "manual"),
|
||||
Message: strings.TrimSpace(body.Message),
|
||||
Status: types.GroupJoinRequestStatusPending,
|
||||
Version: 1,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, item)
|
||||
writeIMSuccess(wrt, req, groupJoinRequestPayload(item))
|
||||
default:
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
}
|
||||
@@ -3538,25 +3646,13 @@ func serveIMGroupJoinRequestAction(wrt http.ResponseWriter, req *http.Request, t
|
||||
if !decodeIMJSONBody(wrt, req, &body) {
|
||||
return
|
||||
}
|
||||
group, err := store.Topics.Get(principal.TenantID, topic)
|
||||
updated, err := store.GroupBusiness.ResolveJoinRequest(principal.TenantID, topic, requestID, principal.Uid, action, "", body.Version, time.Now().UTC().Round(time.Millisecond))
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
aux := groupAuxMap(group.Aux)
|
||||
requests := groupJoinRequests(group)
|
||||
updated, found := resolveGroupJoinRequest(requests, requestID, action, principal.Uid, body.Version)
|
||||
if !found {
|
||||
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "join request not found", nil)
|
||||
return
|
||||
}
|
||||
aux["join_requests"] = requests
|
||||
if err = store.Topics.Update(principal.TenantID, topic, map[string]any{"Aux": aux}); err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
if action == "accept" {
|
||||
uid := types.ParseUserId(stringFromAny(updated["user_id"]))
|
||||
uid := updated.ApplicantUserID
|
||||
if !uid.IsZero() {
|
||||
_ = store.Subs.Create(principal.TenantID, &types.Subscription{
|
||||
TenantID: principal.TenantID,
|
||||
@@ -3566,10 +3662,12 @@ func serveIMGroupJoinRequestAction(wrt http.ResponseWriter, req *http.Request, t
|
||||
ModeGiven: types.ModeCPublic,
|
||||
Private: groupMemberPrivate("", false, false, 1, time.Now().UTC().Round(time.Millisecond)),
|
||||
})
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
_, _ = store.GroupBusiness.UpsertMember(principal.TenantID, &types.GroupMember{Topic: topic, UserID: uid, Role: types.GroupMemberRoleMember, MemberStatus: types.GroupMemberStatusActive, JoinSource: "approval", JoinedBy: principal.Uid, JoinedAt: now, Version: 1, CreatedAt: now, UpdatedAt: now})
|
||||
_ = store.Topics.UpdateSubCnt(principal.TenantID, topic)
|
||||
}
|
||||
}
|
||||
writeIMSuccess(wrt, req, updated)
|
||||
writeIMSuccess(wrt, req, groupJoinRequestPayload(updated))
|
||||
}
|
||||
|
||||
func serveIMGroupSummary(wrt http.ResponseWriter, req *http.Request, topic string) {
|
||||
@@ -3581,16 +3679,12 @@ func serveIMGroupSummary(wrt http.ResponseWriter, req *http.Request, topic strin
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
group, err := store.Topics.Get(principal.TenantID, topic)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
announcement := latestGroupAnnouncement(group)
|
||||
requests, _, _ := store.GroupBusiness.ListJoinRequests(principal.TenantID, topic, types.GroupJoinRequestStatusPending, "", 100)
|
||||
announcement, _ := store.GroupBusiness.LatestAnnouncement(principal.TenantID, topic)
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"topic": topic,
|
||||
"pending_join_request_count": len(filterGroupJoinRequests(groupJoinRequests(group), "pending")),
|
||||
"announcement": announcement,
|
||||
"pending_join_request_count": len(requests),
|
||||
"announcement": groupAnnouncementPayload(announcement),
|
||||
"permissions": groupPermissions(selfSub.ModeGiven & selfSub.ModeWant),
|
||||
})
|
||||
}
|
||||
@@ -3600,14 +3694,14 @@ func serveIMGroupAnnouncements(wrt http.ResponseWriter, req *http.Request, topic
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
group, err := store.Topics.Get(principal.TenantID, topic)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case tail == "announcements/latest" && req.Method == http.MethodGet:
|
||||
writeIMSuccess(wrt, req, latestGroupAnnouncement(group))
|
||||
ann, err := store.GroupBusiness.LatestAnnouncement(principal.TenantID, topic)
|
||||
if err != nil && !errors.Is(err, types.ErrNotFound) {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, groupAnnouncementPayload(ann))
|
||||
case tail == "announcements/current" && req.Method == http.MethodPut:
|
||||
if !(selfSub.ModeGiven & selfSub.ModeWant).IsAdmin() {
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
|
||||
@@ -3622,36 +3716,35 @@ func serveIMGroupAnnouncements(wrt http.ResponseWriter, req *http.Request, topic
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "content"})
|
||||
return
|
||||
}
|
||||
aux := groupAuxMap(group.Aux)
|
||||
current := latestGroupAnnouncement(group)
|
||||
version := intFromAny(current["version"])
|
||||
if version <= 0 {
|
||||
version = 1
|
||||
}
|
||||
if body.Version > 0 && body.Version != version {
|
||||
writeIMError(wrt, req, http.StatusConflict, 30006, "VERSION_CONFLICT", "version conflict", map[string]any{"current_version": version})
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
announcement := map[string]any{
|
||||
"announcement_id": defaultString(stringFromAny(current["announcement_id"]), "ann_"+store.Store.GetUidString()),
|
||||
"content": content,
|
||||
"author": principal.Uid.UserId(),
|
||||
"version": version + 1,
|
||||
"published_at": now.Format(time.RFC3339Nano),
|
||||
}
|
||||
aux["announcement"] = announcement
|
||||
if err = store.Topics.Update(principal.TenantID, topic, map[string]any{"Aux": aux}); err != nil {
|
||||
announcement, err := store.GroupBusiness.PutAnnouncement(principal.TenantID, &types.GroupAnnouncement{
|
||||
AnnouncementID: "ann_" + store.Store.GetUidString(),
|
||||
Topic: topic,
|
||||
AuthorUserID: principal.Uid,
|
||||
Content: content,
|
||||
Status: types.GroupAnnouncementStatusCurrent,
|
||||
PublishedAt: now,
|
||||
}, body.Version)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, announcement)
|
||||
writeIMSuccess(wrt, req, groupAnnouncementPayload(announcement))
|
||||
case strings.HasPrefix(tail, "announcements/") && strings.HasSuffix(tail, "/read") && req.Method == http.MethodPost:
|
||||
announcementID := strings.TrimSuffix(strings.TrimPrefix(tail, "announcements/"), "/read")
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
if err := store.GroupBusiness.ReadAnnouncement(principal.TenantID, topic, announcementID, principal.Uid, now); err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{"announcement_id": announcementID, "read_at": now.Format(time.RFC3339Nano)})
|
||||
case strings.HasPrefix(tail, "announcements/") && req.Method == http.MethodGet:
|
||||
writeIMSuccess(wrt, req, latestGroupAnnouncement(group))
|
||||
ann, err := store.GroupBusiness.LatestAnnouncement(principal.TenantID, topic)
|
||||
if err != nil && !errors.Is(err, types.ErrNotFound) {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, groupAnnouncementPayload(ann))
|
||||
default:
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
}
|
||||
@@ -3662,13 +3755,18 @@ func serveIMGroupGovernance(wrt http.ResponseWriter, req *http.Request, topic st
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
group, err := store.Topics.Get(principal.TenantID, topic)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
settings, err := store.GroupBusiness.GetSettings(principal.TenantID, topic)
|
||||
if err != nil && !errors.Is(err, types.ErrNotFound) {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
if settings != nil {
|
||||
writeIMSuccess(wrt, req, groupGovernancePayloadFromSettings(settings, selfSub))
|
||||
return
|
||||
}
|
||||
group, _ := store.Topics.Get(principal.TenantID, topic)
|
||||
writeIMSuccess(wrt, req, groupGovernancePayload(group, selfSub))
|
||||
case http.MethodPatch:
|
||||
if !(selfSub.ModeGiven & selfSub.ModeWant).IsAdmin() {
|
||||
@@ -3679,28 +3777,19 @@ func serveIMGroupGovernance(wrt http.ResponseWriter, req *http.Request, topic st
|
||||
if !decodeIMJSONBody(wrt, req, &body) {
|
||||
return
|
||||
}
|
||||
aux := groupAuxMap(group.Aux)
|
||||
governance := groupGovernanceMap(aux["governance"])
|
||||
currentVersion := intFromAny(governance["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
|
||||
}
|
||||
applyBoolPatch(governance, "join_approval", body.JoinApproval)
|
||||
applyBoolPatch(governance, "mute_all", body.MuteAll)
|
||||
applyBoolPatch(governance, "member_upload_files", body.MemberUploadFiles)
|
||||
applyBoolPatch(governance, "member_private_chat", body.MemberPrivateChat)
|
||||
applyBoolPatch(governance, "member_invite", body.MemberInvite)
|
||||
governance["version"] = currentVersion + 1
|
||||
aux["governance"] = governance
|
||||
if err = store.Topics.Update(principal.TenantID, topic, map[string]any{"Aux": aux}); err != nil {
|
||||
settings, err := store.GroupBusiness.UpdateSettings(principal.TenantID, topic, types.GroupSettingsPatch{
|
||||
JoinApproval: body.JoinApproval,
|
||||
MuteAll: body.MuteAll,
|
||||
MemberUploadFiles: body.MemberUploadFiles,
|
||||
MemberPrivateChat: body.MemberPrivateChat,
|
||||
MemberInvite: body.MemberInvite,
|
||||
Version: body.Version,
|
||||
})
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, governance)
|
||||
writeIMSuccess(wrt, req, groupGovernancePayloadFromSettings(settings, selfSub))
|
||||
default:
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
}
|
||||
@@ -3770,6 +3859,8 @@ func serveIMGroupAdmins(wrt http.ResponseWriter, req *http.Request, topic, tail
|
||||
item["ok"] = false
|
||||
item["reason"] = relationErrorReason(err)
|
||||
} else {
|
||||
role := types.GroupMemberRoleAdmin
|
||||
_, _ = store.GroupBusiness.UpdateMember(principal.TenantID, topic, uid, types.GroupMemberPatch{Role: &role})
|
||||
item["ok"] = true
|
||||
}
|
||||
results = append(results, item)
|
||||
@@ -3787,6 +3878,8 @@ func serveIMGroupAdmins(wrt http.ResponseWriter, req *http.Request, topic, tail
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
role := types.GroupMemberRoleMember
|
||||
_, _ = store.GroupBusiness.UpdateMember(principal.TenantID, topic, uid, types.GroupMemberPatch{Role: &role})
|
||||
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)
|
||||
@@ -3829,6 +3922,11 @@ func serveIMGroupOwnerTransfer(wrt http.ResponseWriter, req *http.Request, topic
|
||||
return
|
||||
}
|
||||
_ = store.Topics.OwnerChange(principal.TenantID, topic, target)
|
||||
adminRole := types.GroupMemberRoleAdmin
|
||||
ownerRole := types.GroupMemberRoleOwner
|
||||
_, _ = store.GroupBusiness.UpdateMember(principal.TenantID, topic, principal.Uid, types.GroupMemberPatch{Role: &adminRole})
|
||||
_, _ = store.GroupBusiness.UpdateMember(principal.TenantID, topic, target, types.GroupMemberPatch{Role: &ownerRole})
|
||||
_, _ = store.GroupBusiness.UpdateSettings(principal.TenantID, topic, types.GroupSettingsPatch{OwnerUserID: &target, Version: body.Version})
|
||||
writeIMSuccess(wrt, req, map[string]any{"owner": target.UserId(), "previous_owner": principal.Uid.UserId(), "version": body.Version + 1})
|
||||
}
|
||||
|
||||
@@ -3841,15 +3939,14 @@ func serveIMGroupMutes(wrt http.ResponseWriter, req *http.Request, topic, tail s
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
|
||||
return
|
||||
}
|
||||
group, err := store.Topics.Get(principal.TenantID, topic)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
aux := groupAuxMap(group.Aux)
|
||||
switch {
|
||||
case tail == "mutes" && req.Method == http.MethodGet:
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": groupMuteItems(aux), "next_cursor": ""})
|
||||
items, nextCursor, err := store.GroupBusiness.ListMutes(principal.TenantID, topic, strings.TrimSpace(req.URL.Query().Get("cursor")), parseIMLimit(req.URL.Query().Get("limit"), 50, 100))
|
||||
if err != nil && !errors.Is(err, types.ErrNotFound) {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": groupMutePayloads(items), "next_cursor": nextCursor})
|
||||
case tail == "mutes" && req.Method == http.MethodPost:
|
||||
var body imGroupMuteBatchRequest
|
||||
if !decodeIMJSONBody(wrt, req, &body) {
|
||||
@@ -3859,29 +3956,34 @@ func serveIMGroupMutes(wrt http.ResponseWriter, req *http.Request, topic, tail s
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "members"})
|
||||
return
|
||||
}
|
||||
mutes := groupMuteItems(aux)
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
mutes := make([]types.GroupMute, 0, len(body.Members))
|
||||
for _, member := range body.Members {
|
||||
uid := types.ParseUserId(strings.TrimSpace(member.UserID))
|
||||
if uid.IsZero() {
|
||||
continue
|
||||
}
|
||||
mutes = upsertGroupMute(mutes, map[string]any{"user_id": uid.UserId(), "muted_by": principal.Uid.UserId(), "muted_at": now.Format(time.RFC3339Nano), "expires_at": strings.TrimSpace(member.ExpiresAt), "reason": strings.TrimSpace(body.Reason)})
|
||||
var expiresAt *time.Time
|
||||
if raw := strings.TrimSpace(member.ExpiresAt); raw != "" {
|
||||
if parsed, err := time.Parse(time.RFC3339Nano, raw); err == nil {
|
||||
expiresAt = &parsed
|
||||
}
|
||||
}
|
||||
mute, err := store.GroupBusiness.UpsertMute(principal.TenantID, &types.GroupMute{Topic: topic, UserID: uid, MutedBy: principal.Uid, MutedAt: now, ExpiresAt: expiresAt, Reason: strings.TrimSpace(body.Reason), Status: types.GroupMuteStatusActive})
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
mutes = append(mutes, *mute)
|
||||
}
|
||||
aux["mutes"] = mutes
|
||||
if err = store.Topics.Update(principal.TenantID, topic, map[string]any{"Aux": aux}); err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": 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/")))
|
||||
if uid.IsZero() {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "user_id"})
|
||||
return
|
||||
}
|
||||
aux["mutes"] = removeGroupMute(groupMuteItems(aux), uid.UserId())
|
||||
if err = store.Topics.Update(principal.TenantID, topic, map[string]any{"Aux": aux}); err != nil {
|
||||
if err := store.GroupBusiness.DeleteMute(principal.TenantID, topic, uid, principal.Uid, time.Now().UTC().Round(time.Millisecond)); err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
@@ -4919,6 +5021,114 @@ func groupGovernancePayload(group *types.Topic, selfSub *types.Subscription) map
|
||||
return governance
|
||||
}
|
||||
|
||||
func groupGovernancePayloadFromSettings(settings *types.GroupSettings, selfSub *types.Subscription) map[string]any {
|
||||
governance := defaultGroupGovernance()
|
||||
if settings != nil {
|
||||
governance["join_approval"] = settings.JoinApproval
|
||||
governance["mute_all"] = settings.MuteAll
|
||||
governance["member_upload_files"] = settings.MemberUploadFiles
|
||||
governance["member_private_chat"] = settings.MemberPrivateChat
|
||||
governance["member_invite"] = settings.MemberInvite
|
||||
governance["version"] = settings.Version
|
||||
}
|
||||
editable := false
|
||||
if selfSub != nil {
|
||||
editable = (selfSub.ModeGiven & selfSub.ModeWant).IsAdmin()
|
||||
}
|
||||
for _, key := range []string{"join_approval", "mute_all", "member_upload_files", "member_private_chat", "member_invite"} {
|
||||
governance[key+"_editable"] = editable
|
||||
}
|
||||
return governance
|
||||
}
|
||||
|
||||
func mergeGroupSettingsPayload(payload map[string]any, settings *types.GroupSettings) map[string]any {
|
||||
if payload == nil {
|
||||
payload = map[string]any{}
|
||||
}
|
||||
if settings == nil {
|
||||
return payload
|
||||
}
|
||||
payload["topic"] = settings.Topic
|
||||
payload["name"] = settings.Name
|
||||
payload["avatar_file_id"] = settings.AvatarFileID
|
||||
payload["description"] = settings.Description
|
||||
payload["owner"] = settings.OwnerUserID.UserId()
|
||||
payload["member_count"] = settings.MemberCount
|
||||
payload["version"] = settings.Version
|
||||
return payload
|
||||
}
|
||||
|
||||
func groupJoinRequestPayload(req *types.GroupJoinRequest) map[string]any {
|
||||
if req == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
data := map[string]any{
|
||||
"request_id": req.RequestID,
|
||||
"user_id": req.ApplicantUserID.UserId(),
|
||||
"inviter_id": req.InviterUserID.UserId(),
|
||||
"source": req.Source,
|
||||
"message": req.Message,
|
||||
"status": req.Status,
|
||||
"version": req.Version,
|
||||
"created_at": req.CreatedAt.UTC().Format(time.RFC3339Nano),
|
||||
"updated_at": req.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
||||
}
|
||||
if req.HandledBy != types.ZeroUid {
|
||||
data["handled_by"] = req.HandledBy.UserId()
|
||||
}
|
||||
if req.HandledAt != nil {
|
||||
data["handled_at"] = req.HandledAt.UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
if req.RejectReason != "" {
|
||||
data["reject_reason"] = req.RejectReason
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func groupJoinRequestPayloads(items []types.GroupJoinRequest) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(items))
|
||||
for i := range items {
|
||||
out = append(out, groupJoinRequestPayload(&items[i]))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func groupAnnouncementPayload(ann *types.GroupAnnouncement) map[string]any {
|
||||
if ann == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
return map[string]any{
|
||||
"announcement_id": ann.AnnouncementID,
|
||||
"content": ann.Content,
|
||||
"author": ann.AuthorUserID.UserId(),
|
||||
"version": ann.Version,
|
||||
"published_at": ann.PublishedAt.UTC().Format(time.RFC3339Nano),
|
||||
}
|
||||
}
|
||||
|
||||
func groupMutePayload(mute types.GroupMute) map[string]any {
|
||||
data := map[string]any{
|
||||
"user_id": mute.UserID.UserId(),
|
||||
"muted_by": mute.MutedBy.UserId(),
|
||||
"muted_at": mute.MutedAt.UTC().Format(time.RFC3339Nano),
|
||||
"reason": mute.Reason,
|
||||
"status": mute.Status,
|
||||
"version": mute.Version,
|
||||
}
|
||||
if mute.ExpiresAt != nil {
|
||||
data["expires_at"] = mute.ExpiresAt.UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func groupMutePayloads(items []types.GroupMute) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, groupMutePayload(item))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func groupAuxMap(value any) map[string]any {
|
||||
switch aux := value.(type) {
|
||||
case map[string]any:
|
||||
|
||||
Reference in New Issue
Block a user