Merge branch 'dev' of https://git.118im.com/im_team/im_server into dev
This commit is contained in:
+171
-12
@@ -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)
|
||||
@@ -5583,6 +5619,11 @@ func serveIMGroupInvitations(wrt http.ResponseWriter, req *http.Request, topic s
|
||||
}
|
||||
if joined {
|
||||
item["status"] = types.GroupInvitationStatusJoined
|
||||
if imReliableHTTPEventsEnabled(req) {
|
||||
publishIMGroupMemberJoinedSystemNotice(principal.TenantID, topic, principal.Uid, uid,
|
||||
"有成员通过邀请加入了群聊。",
|
||||
map[string]any{"source": "direct_invitation"})
|
||||
}
|
||||
} else {
|
||||
item["status"] = "already_member"
|
||||
}
|
||||
@@ -5827,6 +5868,11 @@ func serveIMGroupJoinRequests(wrt http.ResponseWriter, req *http.Request, topic
|
||||
}
|
||||
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.member.joined",
|
||||
map[string]any{"user_id": principal.Uid.UserId(), "joined": joined, "source": "open_group"})
|
||||
if joined && imReliableHTTPEventsEnabled(req) {
|
||||
publishIMGroupMemberJoinedSystemNotice(principal.TenantID, topic, principal.Uid, principal.Uid,
|
||||
"有成员加入了群聊。",
|
||||
map[string]any{"source": "open_group"})
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "status": "joined", "joined": joined, "source": "open_group"})
|
||||
return
|
||||
}
|
||||
@@ -6347,19 +6393,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
|
||||
@@ -6400,6 +6461,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"})
|
||||
@@ -8712,6 +8777,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,
|
||||
@@ -11583,7 +11659,9 @@ 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)
|
||||
if !publishRealtimeP2PConversationSystemMessage(tenantID, msg.Topic, out) {
|
||||
publishRealtimeTopicMessage(msg.Topic, out)
|
||||
}
|
||||
broadcastIMConversationSystemPresence(tenantID, msg, timestamp)
|
||||
}
|
||||
|
||||
@@ -11607,6 +11685,28 @@ func publishIMGroupInvitationNotification(tenantID types.TenantID, inv *types.Gr
|
||||
}
|
||||
}
|
||||
|
||||
func publishIMGroupMemberJoinedSystemNotice(tenantID types.TenantID, topic string, inviter, target types.Uid, text string, extra map[string]any) {
|
||||
if target.IsZero() {
|
||||
return
|
||||
}
|
||||
actors := make([]imSystemMessageActor, 0, 2)
|
||||
if !inviter.IsZero() && inviter != target {
|
||||
actors = append(actors, imSystemMessageActor{
|
||||
UserID: inviter.UserId(),
|
||||
Click: map[string]any{"type": "user_profile", "target_id": inviter.UserId()},
|
||||
})
|
||||
}
|
||||
actors = append(actors, imSystemMessageActor{
|
||||
UserID: target.UserId(),
|
||||
Click: map[string]any{"type": "user_profile", "target_id": target.UserId()},
|
||||
})
|
||||
publishIMConversationSystemNotice(tenantID, topic,
|
||||
"system_action", "group_member_joined", text,
|
||||
actors,
|
||||
[]imSystemMessageAction{{Type: "group_profile", Label: "查看群聊", TargetID: topic}},
|
||||
extra)
|
||||
}
|
||||
|
||||
func deliverIMConversationSystemMessageToP2PSessions(tenantID types.TenantID, msg *types.Message, out *ServerComMessage) {
|
||||
if globals.sessionStore == nil || msg == nil || out == nil || out.Data == nil {
|
||||
return
|
||||
@@ -11629,9 +11729,10 @@ func deliverIMConversationSystemMessageToP2PSessions(tenantID types.TenantID, ms
|
||||
if session.getSub(msg.Topic) != nil {
|
||||
return true
|
||||
}
|
||||
msgCopy := out.copy()
|
||||
msgCopy.RcptTo = session.uid.UserId()
|
||||
msgCopy.Data.Topic = msg.Topic
|
||||
msgCopy, ok := p2pConversationSystemMessageForUser(out, msg.Topic, session.uid)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if !session.queueOut(msgCopy) {
|
||||
logs.Warn.Printf("im system message: session queue full topic=%s seq=%d user=%s", msg.Topic, msg.SeqId, session.uid.UserId())
|
||||
}
|
||||
@@ -11639,6 +11740,43 @@ func deliverIMConversationSystemMessageToP2PSessions(tenantID types.TenantID, ms
|
||||
})
|
||||
}
|
||||
|
||||
func publishRealtimeP2PConversationSystemMessage(tenantID types.TenantID, topic string, out *ServerComMessage) bool {
|
||||
uid1, uid2, err := types.ParseP2P(topic)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, uid := range []types.Uid{uid1, uid2} {
|
||||
msgCopy, ok := p2pConversationSystemMessageForUser(out, topic, uid)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
publishRealtimeUserMessage(uid, msgCopy)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func p2pConversationSystemMessageForUser(out *ServerComMessage, topic string, uid types.Uid) (*ServerComMessage, bool) {
|
||||
if out == nil || uid.IsZero() {
|
||||
return nil, false
|
||||
}
|
||||
viewTopic, err := types.P2PNameForUser(uid, topic)
|
||||
if err != nil || viewTopic == "" {
|
||||
return nil, false
|
||||
}
|
||||
msgCopy := out.copy()
|
||||
msgCopy.RcptTo = uid.UserId()
|
||||
if msgCopy.Data != nil {
|
||||
msgCopy.Data.Topic = viewTopic
|
||||
}
|
||||
if msgCopy.Info != nil {
|
||||
msgCopy.Info.Topic = viewTopic
|
||||
}
|
||||
if msgCopy.Pres != nil {
|
||||
msgCopy.Pres.Topic = viewTopic
|
||||
}
|
||||
return msgCopy, true
|
||||
}
|
||||
|
||||
func broadcastIMConversationSystemPresence(tenantID types.TenantID, msg *types.Message, timestamp time.Time) {
|
||||
if globals.hub == nil || msg == nil || msg.SeqId <= 0 || msg.Topic == "" {
|
||||
return
|
||||
@@ -11724,6 +11862,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"
|
||||
@@ -11751,6 +11895,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)
|
||||
|
||||
Reference in New Issue
Block a user