2026-05-12 09:53:20 +08:00

285 lines
9.8 KiB
Go

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
}