package http import ( "hyapp/services/gateway-service/internal/transport/http/httpkit" "net/http" "strconv" "strings" "time" activityv1 "hyapp.local/api/proto/activity/v1" userv1 "hyapp.local/api/proto/user/v1" "hyapp/pkg/appcode" "hyapp/services/gateway-service/internal/auth" ) 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 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: 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: 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 !decode(writer, request, &body) { return } resp, err := h.messageClient.MarkInboxSectionRead(request.Context(), &activityv1.MarkInboxSectionReadRequest{ Meta: 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: 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: 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()}) } // 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: authRequestMeta(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 activityMeta(request *http.Request) *activityv1.RequestMeta { return &activityv1.RequestMeta{ RequestId: httpkit.RequestIDFromContext(request.Context()), Caller: "gateway-service", GatewayNodeId: "gateway-local", SentAtMs: time.Now().UnixMilli(), AppCode: appcode.FromContext(request.Context()), } } 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 }