package messageapi import ( "net/http" "strconv" "strings" activityv1 "hyapp.local/api/proto/activity/v1" userv1 "hyapp.local/api/proto/user/v1" "hyapp/services/gateway-service/internal/auth" "hyapp/services/gateway-service/internal/transport/http/httpkit" ) type messageTabSectionData struct { Section string `json:"section"` Title string `json:"title"` UnreadCount int64 `json:"unread_count"` Source string `json:"source"` } type inboxMessageData struct { MessageID string `json:"message_id"` Title string `json:"title"` Summary string `json:"summary"` Body string `json:"body"` IconURL string `json:"icon_url"` ImageURL string `json:"image_url"` ActionType string `json:"action_type"` ActionParam string `json:"action_param"` Read bool `json:"read"` SentAtMS int64 `json:"sent_at_ms"` } type actionConfirmData struct { ConfirmID string `json:"confirm_id"` Status string `json:"status"` Action string `json:"action,omitempty"` BusinessType string `json:"business_type,omitempty"` BusinessSubtype string `json:"business_subtype,omitempty"` BusinessID string `json:"business_id,omitempty"` SenderUserID string `json:"sender_user_id,omitempty"` ReceiverUserID string `json:"receiver_user_id,omitempty"` ProcessedAtMS int64 `json:"processed_at_ms,omitempty"` CreatedAtMS int64 `json:"created_at_ms,omitempty"` UpdatedAtMS int64 `json:"updated_at_ms,omitempty"` } type userProfileBatchData struct { UserID string `json:"user_id"` DisplayUserID string `json:"display_user_id"` Username string `json:"username"` Avatar string `json:"avatar,omitempty"` Gender string `json:"gender,omitempty"` Country string `json:"country,omitempty"` RegionID int64 `json:"region_id,omitempty"` AppCode string `json:"app_code"` } // listMessageTabs returns backend unread summary and declares user conversations as Tencent IM SDK owned. func (h *Handler) listMessageTabs(writer http.ResponseWriter, request *http.Request) { if h.messageClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.messageClient.ListMessageTabs(request.Context(), &activityv1.ListMessageTabsRequest{ Meta: httpkit.ActivityMeta(request), UserId: auth.UserIDFromContext(request.Context()), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } sections := make([]messageTabSectionData, 0, len(resp.GetSections())) for _, section := range resp.GetSections() { sections = append(sections, messageTabSectionData{ Section: section.GetSection(), Title: section.GetTitle(), UnreadCount: section.GetUnreadCount(), Source: section.GetSource(), }) } httpkit.WriteOK(writer, request, map[string]any{"sections": sections}) } // listInboxMessages reads system/activity inbox list; private user conversations stay in Tencent IM SDK. func (h *Handler) listInboxMessages(writer http.ResponseWriter, request *http.Request) { if h.messageClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } pageSize, ok := parseMessagePageSize(request.URL.Query().Get("page_size")) if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } section := strings.TrimSpace(request.URL.Query().Get("section")) resp, err := h.messageClient.ListInboxMessages(request.Context(), &activityv1.ListInboxMessagesRequest{ Meta: httpkit.ActivityMeta(request), UserId: auth.UserIDFromContext(request.Context()), Section: section, PageSize: pageSize, PageToken: strings.TrimSpace(request.URL.Query().Get("page_token")), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } items := make([]inboxMessageData, 0, len(resp.GetItems())) for _, item := range resp.GetItems() { items = append(items, inboxMessageData{ MessageID: item.GetMessageId(), Title: item.GetTitle(), Summary: item.GetSummary(), Body: item.GetBody(), IconURL: item.GetIconUrl(), ImageURL: item.GetImageUrl(), ActionType: item.GetActionType(), ActionParam: item.GetActionParam(), Read: item.GetRead(), SentAtMS: item.GetSentAtMs(), }) } httpkit.WriteOK(writer, request, map[string]any{"section": resp.GetSection(), "items": items, "next_page_token": resp.GetNextPageToken()}) } // markInboxSectionRead marks all current visible unread messages in one backend-owned section. func (h *Handler) markInboxSectionRead(writer http.ResponseWriter, request *http.Request) { if h.messageClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } var body struct { Section string `json:"section"` } if !httpkit.Decode(writer, request, &body) { return } resp, err := h.messageClient.MarkInboxSectionRead(request.Context(), &activityv1.MarkInboxSectionReadRequest{ Meta: httpkit.ActivityMeta(request), UserId: auth.UserIDFromContext(request.Context()), Section: body.Section, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, map[string]any{"section": resp.GetSection(), "read_count": resp.GetReadCount()}) } // markInboxMessageReadByPath handles /api/v1/messages/{message_id}/read. func (h *Handler) markInboxMessageReadByPath(writer http.ResponseWriter, request *http.Request) { if h.messageClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } messageID := strings.TrimSpace(request.PathValue("message_id")) if messageID == "" { httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") return } h.markInboxMessageRead(writer, request, messageID) } // deleteInboxMessageByPath handles /api/v1/messages/{message_id}. func (h *Handler) deleteInboxMessageByPath(writer http.ResponseWriter, request *http.Request) { if h.messageClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } messageID := strings.TrimSpace(request.PathValue("message_id")) if messageID == "" { httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") return } if request.Method == http.MethodDelete { h.deleteInboxMessage(writer, request, messageID) return } httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") } func (h *Handler) markInboxMessageRead(writer http.ResponseWriter, request *http.Request, messageID string) { if request.Method != http.MethodPost { httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") return } resp, err := h.messageClient.MarkInboxMessageRead(request.Context(), &activityv1.MarkInboxMessageReadRequest{ Meta: httpkit.ActivityMeta(request), UserId: auth.UserIDFromContext(request.Context()), MessageId: messageID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, map[string]any{"message_id": resp.GetMessageId(), "read": resp.GetRead(), "read_at_ms": resp.GetReadAtMs()}) } func (h *Handler) deleteInboxMessage(writer http.ResponseWriter, request *http.Request, messageID string) { resp, err := h.messageClient.DeleteInboxMessage(request.Context(), &activityv1.DeleteInboxMessageRequest{ Meta: httpkit.ActivityMeta(request), UserId: auth.UserIDFromContext(request.Context()), MessageId: messageID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, map[string]any{"message_id": resp.GetMessageId(), "deleted": resp.GetDeleted()}) } // acceptActionConfirm handles an IM confirm accept click; only confirm_id and command_id come from App. func (h *Handler) acceptActionConfirm(writer http.ResponseWriter, request *http.Request) { if h.actionClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } confirmID := strings.TrimSpace(request.PathValue("confirm_id")) var body struct { CommandID string `json:"command_id"` } if confirmID == "" { httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") return } if !httpkit.Decode(writer, request, &body) { return } resp, err := h.actionClient.AcceptActionConfirm(request.Context(), &activityv1.AcceptActionConfirmRequest{ Meta: httpkit.ActivityMeta(request), ConfirmId: confirmID, ActorUserId: auth.UserIDFromContext(request.Context()), CommandId: body.CommandID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, actionConfirmResponseData(resp.GetConfirm())) } // rejectActionConfirm handles an IM confirm reject click with an optional reason. func (h *Handler) rejectActionConfirm(writer http.ResponseWriter, request *http.Request) { if h.actionClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } confirmID := strings.TrimSpace(request.PathValue("confirm_id")) var body struct { CommandID string `json:"command_id"` Reason string `json:"reason"` } if confirmID == "" { httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") return } if !httpkit.Decode(writer, request, &body) { return } resp, err := h.actionClient.RejectActionConfirm(request.Context(), &activityv1.RejectActionConfirmRequest{ Meta: httpkit.ActivityMeta(request), ConfirmId: confirmID, ActorUserId: auth.UserIDFromContext(request.Context()), CommandId: body.CommandID, Reason: body.Reason, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, actionConfirmResponseData(resp.GetConfirm())) } // batchActionConfirmStatus returns confirm status for current user's visible confirm IDs. func (h *Handler) batchActionConfirmStatus(writer http.ResponseWriter, request *http.Request) { if h.actionClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } var body struct { ConfirmIDs []string `json:"confirm_ids"` } if !httpkit.Decode(writer, request, &body) { return } resp, err := h.actionClient.BatchGetActionConfirmStatus(request.Context(), &activityv1.BatchGetActionConfirmStatusRequest{ Meta: httpkit.ActivityMeta(request), UserId: auth.UserIDFromContext(request.Context()), ConfirmIds: body.ConfirmIDs, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } items := make([]actionConfirmData, 0, len(resp.GetItems())) for _, item := range resp.GetItems() { items = append(items, actionConfirmResponseData(item)) } httpkit.WriteOK(writer, request, map[string]any{"items": items}) } // listActionConfirms returns confirm states for a C2C peer conversation. func (h *Handler) listActionConfirms(writer http.ResponseWriter, request *http.Request) { if h.actionClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } peerUserID, err := strconv.ParseInt(strings.TrimSpace(request.URL.Query().Get("peer_user_id")), 10, 64) if err != nil || peerUserID <= 0 { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } pageSize, ok := parseMessagePageSize(request.URL.Query().Get("page_size")) if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.actionClient.ListConversationActionConfirms(request.Context(), &activityv1.ListConversationActionConfirmsRequest{ Meta: httpkit.ActivityMeta(request), UserId: auth.UserIDFromContext(request.Context()), PeerUserId: peerUserID, PageSize: pageSize, PageToken: strings.TrimSpace(request.URL.Query().Get("page_token")), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } items := make([]actionConfirmData, 0, len(resp.GetItems())) for _, item := range resp.GetItems() { items = append(items, actionConfirmResponseData(item)) } httpkit.WriteOK(writer, request, map[string]any{"items": items, "next_page_token": resp.GetNextPageToken()}) } func actionConfirmResponseData(confirm *activityv1.ActionConfirm) actionConfirmData { if confirm == nil { return actionConfirmData{} } return actionConfirmData{ ConfirmID: confirm.GetConfirmId(), Status: confirm.GetStatus(), Action: confirm.GetAction(), BusinessType: confirm.GetBusinessType(), BusinessSubtype: confirm.GetBusinessSubtype(), BusinessID: confirm.GetBusinessId(), SenderUserID: strconv.FormatInt(confirm.GetSenderUserId(), 10), ReceiverUserID: strconv.FormatInt(confirm.GetReceiverUserId(), 10), ProcessedAtMS: confirm.GetProcessedAtMs(), CreatedAtMS: confirm.GetCreatedAtMs(), UpdatedAtMS: confirm.GetUpdatedAtMs(), } } // batchUserProfiles returns only safe profile fields needed to render Tencent IM conversation rows. func (h *Handler) batchUserProfiles(writer http.ResponseWriter, request *http.Request) { if h.userProfileClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } userIDs, ok := parseBatchUserIDs(request) if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{ Meta: httpkit.UserMeta(request, ""), UserIds: userIDs, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } items := make([]userProfileBatchData, 0, len(resp.GetUsers())) for _, userID := range userIDs { if user := resp.GetUsers()[userID]; user != nil { items = append(items, userProfileBatchData{ UserID: strconv.FormatInt(user.GetUserId(), 10), DisplayUserID: user.GetDisplayUserId(), Username: user.GetUsername(), Avatar: user.GetAvatar(), Gender: user.GetGender(), Country: user.GetCountry(), RegionID: user.GetRegionId(), AppCode: user.GetAppCode(), }) } } httpkit.WriteOK(writer, request, map[string]any{"profiles": items}) } func parseMessagePageSize(raw string) (int32, bool) { raw = strings.TrimSpace(raw) if raw == "" { return 0, true } value, err := strconv.Atoi(raw) if err != nil || value < 0 || value > 1000 { return 0, false } return int32(value), true } func parseBatchUserIDs(request *http.Request) ([]int64, bool) { rawValues := append([]string(nil), request.URL.Query()["user_ids"]...) rawValues = append(rawValues, request.URL.Query()["user_id"]...) seen := make(map[int64]bool) result := make([]int64, 0, len(rawValues)) for _, raw := range rawValues { for piece := range strings.SplitSeq(raw, ",") { piece = strings.TrimSpace(piece) if piece == "" { continue } userID, err := strconv.ParseInt(piece, 10, 64) if err != nil || userID <= 0 { return nil, false } if !seen[userID] { seen[userID] = true result = append(result, userID) } } } if len(result) == 0 || len(result) > 100 { return nil, false } return result, true }