feat: add message mutation sync

This commit is contained in:
richjohnrambo
2026-09-15 15:54:52 +08:00
parent 55afd976c9
commit bc27a5c574
12 changed files with 949 additions and 13 deletions
+95 -8
View File
@@ -4074,6 +4074,8 @@ func serveIMTopicMessageAction(wrt http.ResponseWriter, req *http.Request) {
serveIMTopicDraft(wrt, req)
case strings.HasSuffix(req.URL.Path, "/calls"):
serveIMTopicCalls(wrt, req)
case strings.HasSuffix(req.URL.Path, "/message-mutations"):
serveIMMessageMutations(wrt, req)
case strings.HasSuffix(req.URL.Path, "/messages/search"):
serveIMMessageSearch(wrt, req)
case strings.Contains(req.URL.Path, "/messages/") && strings.HasSuffix(req.URL.Path, "/context"):
@@ -4198,6 +4200,40 @@ func serveIMMessageSearch(wrt http.ResponseWriter, req *http.Request) {
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": items, "next_cursor": nextCursor})
}
func serveIMMessageMutations(wrt http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
return
}
principal, topic, ok := authenticateTopicReader(wrt, req, topicFromMessageMutationsPath(req.URL.Path))
if !ok {
return
}
sinceVersion, err := parseIMMutationSince(req.URL.Query().Get("since_version"), req.URL.Query().Get("since"))
if err != nil {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "since_version"})
return
}
limit := parseIMLimit(req.URL.Query().Get("limit"), 50, 100)
mutations, nextCursor, hasMore, mutationVersion, err := store.Messages.Mutations(principal.TenantID, topic, principal.Uid, sinceVersion, limit)
if err != nil {
writeIMRelationError(wrt, req, err)
return
}
items := make([]map[string]any, 0, len(mutations))
for _, mutation := range mutations {
items = append(items, messageMutationEventPayload(mutation))
}
writeIMSuccess(wrt, req, map[string]any{
"topic": topic,
"items": items,
"next_cursor": nextCursor,
"has_more": hasMore,
"mutation_version": mutationVersion,
"expired": false,
})
}
func serveIMMessageContext(wrt http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
@@ -6324,19 +6360,34 @@ func serveIMGroupMutes(wrt http.ResponseWriter, req *http.Request, topic, tail s
if !ok {
return
}
if !(selfSub.ModeGiven & selfSub.ModeWant).IsAdmin() {
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
return
}
isAdmin := (selfSub.ModeGiven & selfSub.ModeWant).IsAdmin()
switch {
case tail == "mutes" && req.Method == http.MethodGet:
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
var items []types.GroupMute
nextCursor := ""
if isAdmin {
var err error
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
}
} else {
item, err := store.GroupBusiness.GetMute(principal.TenantID, topic, principal.Uid)
if err != nil && !errors.Is(err, types.ErrNotFound) {
writeIMRelationError(wrt, req, err)
return
}
if item != nil {
items = append(items, *item)
}
}
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": groupMutePayloads(items), "next_cursor": nextCursor})
case (tail == "mutes" || tail == "mutes/batch") && req.Method == http.MethodPost:
if !isAdmin {
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
return
}
var body imGroupMuteBatchRequest
if !decodeIMJSONBody(wrt, req, &body) {
return
@@ -6377,6 +6428,10 @@ func serveIMGroupMutes(wrt http.ResponseWriter, req *http.Request, topic, tail s
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:
if !isAdmin {
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
return
}
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"})
@@ -8689,6 +8744,17 @@ func messageSearchPayload(msg types.Message, query string) map[string]any {
}
}
func messageMutationEventPayload(mutation types.MessageMutation) map[string]any {
return map[string]any{
"seq": mutation.SeqId,
"version": mutation.Version,
"event": mutation.Event,
"operator": mutation.Operator,
"mutated_at": mutation.MutatedAt.UTC().Format(time.RFC3339Nano),
"mutation_version": mutation.MutationVersion,
}
}
func messageContextPayload(msg types.Message) map[string]any {
return map[string]any{
"seq": msg.SeqId,
@@ -11701,6 +11767,12 @@ func topicFromMessagesSearchPath(path string) string {
return topicFromPathWithSuffix(path, prefix, suffix)
}
func topicFromMessageMutationsPath(path string) string {
const prefix = "/v1/im/topics/"
const suffix = "/message-mutations"
return topicFromPathWithSuffix(path, prefix, suffix)
}
func topicFromMediaPath(path string) string {
const prefix = "/v1/im/topics/"
const suffix = "/media"
@@ -11728,6 +11800,21 @@ func topicFromPathWithSuffix(path, prefix, suffix string) string {
return strings.Trim(rest, "/")
}
func parseIMMutationSince(primary, fallback string) (int64, error) {
value := strings.TrimSpace(primary)
if value == "" {
value = strings.TrimSpace(fallback)
}
if value == "" {
return 0, nil
}
parsed, err := strconv.ParseInt(value, 10, 64)
if err != nil || parsed < 0 {
return 0, strconv.ErrSyntax
}
return parsed, nil
}
func messageContextPath(path string) (string, int64, bool) {
const prefix = "/v1/im/topics/"
idx := strings.Index(path, prefix)