2026-06-25 19:57:16 +08:00

430 lines
16 KiB
Go

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
}