2026-06-11 13:33:44 +08:00

285 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package roomapi
import (
"fmt"
"html"
"net/http"
"net/url"
"strings"
"time"
"hyapp/pkg/roomid"
"hyapp/services/gateway-service/internal/auth"
"hyapp/services/gateway-service/internal/transport/http/httpkit"
roomv1 "hyapp.local/api/proto/room/v1"
userv1 "hyapp.local/api/proto/user/v1"
)
const (
roomShareLandingPathPrefix = "/share/rooms/"
roomShareAppScheme = "laluparty"
roomShareAppName = "Lalu Party"
roomShareAndroidPackage = "com.org.laluparty"
)
// getRoomShare 返回 App 侧调起系统分享面板需要的稳定资料。
// 分享本身不改变房间状态,所以这里只读 room-service 快照和 user-service 邀请码。
func (h *Handler) getRoomShare(writer http.ResponseWriter, request *http.Request) {
if h.roomQueryClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
roomID := strings.TrimSpace(request.PathValue("room_id"))
if !roomid.ValidStringID(roomID) {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
viewerUserID := auth.UserIDFromContext(request.Context())
snapshotResp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
Meta: httpkit.RoomMeta(request, roomID, ""),
RoomId: roomID,
ViewerUserId: viewerUserID,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
inviteCode, err := h.roomShareInviteCode(request, viewerUserID)
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, h.roomShareData(request, snapshotResp.GetRoom(), inviteCode))
}
// getRoomShareLanding 返回第三方平台爬取的分享落地页。
// 这个入口不要求登录,目的是让 WhatsApp/Facebook/X 先拿到 OG/Twitter meta真实进房仍由 App 登录和 JoinRoom 校验。
func (h *Handler) getRoomShareLanding(writer http.ResponseWriter, request *http.Request) {
if h.roomQueryClient == nil {
http.Error(writer, "upstream service error", http.StatusBadGateway)
return
}
roomID := strings.TrimSpace(request.PathValue("room_id"))
if !roomid.ValidStringID(roomID) {
http.Error(writer, "invalid room", http.StatusBadRequest)
return
}
snapshotResp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
Meta: httpkit.RoomMeta(request, roomID, ""),
RoomId: roomID,
})
if err != nil {
http.Error(writer, "room unavailable", http.StatusBadGateway)
return
}
data := h.roomShareData(request, snapshotResp.GetRoom(), strings.TrimSpace(request.URL.Query().Get("invite_code")))
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
writer.Header().Set("X-Content-Type-Options", "nosniff")
_, _ = writer.Write([]byte(roomShareLandingHTML(data)))
}
func (h *Handler) roomShareData(request *http.Request, snapshot *roomv1.RoomSnapshot, inviteCode string) roomShareData {
owner := h.roomShareOwner(request, snapshot)
title := roomShareTitle(snapshot)
description := roomShareDescription(snapshot, owner)
landingURL := roomShareLandingURL(request, snapshot.GetRoomId(), inviteCode)
deepLink := roomShareDeepLink(snapshot.GetRoomId(), inviteCode)
coverURL := roomShareCoverURL(snapshot, owner)
return roomShareData{
RoomID: snapshot.GetRoomId(),
RoomShortID: strings.TrimSpace(snapshot.GetRoomShortId()),
Title: title,
Description: description,
CoverURL: coverURL,
LandingURL: landingURL,
DeepLink: deepLink,
ShareText: roomShareText(title, owner),
InviteCode: strings.TrimSpace(inviteCode),
Owner: owner,
ServerTimeMS: time.Now().UTC().UnixMilli(),
Preview: roomSharePreviewData{
Type: "website",
Title: title,
Description: description,
ImageURL: coverURL,
URL: landingURL,
},
}
}
func (h *Handler) roomShareInviteCode(request *http.Request, userID int64) (string, error) {
if userID <= 0 || h.userProfileClient == nil {
return "", nil
}
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
Meta: httpkit.UserMeta(request, ""),
UserId: userID,
})
if err != nil {
return "", err
}
return strings.TrimSpace(userResp.GetUser().GetInvite().GetMyInviteCode()), nil
}
func (h *Handler) roomShareOwner(request *http.Request, snapshot *roomv1.RoomSnapshot) roomShareOwnerData {
ownerUserID := snapshot.GetOwnerUserId()
owner := roomShareOwnerData{UserID: formatOptionalUserID(ownerUserID)}
if ownerUserID <= 0 || h.userProfileClient == nil {
return owner
}
if profile, ok := h.roomDisplayProfileMap(request, []int64{ownerUserID})[ownerUserID]; ok {
owner.DisplayUserID = strings.TrimSpace(profile.DisplayUserID)
owner.Username = strings.TrimSpace(profile.Username)
owner.Avatar = strings.TrimSpace(profile.Avatar)
owner.CountryFlag = strings.TrimSpace(profile.CountryFlag)
}
return owner
}
func roomShareTitle(snapshot *roomv1.RoomSnapshot) string {
if snapshot == nil {
return roomShareAppName + " voice room"
}
if title := strings.TrimSpace(snapshot.GetRoomExt()["title"]); title != "" {
return title
}
if shortID := strings.TrimSpace(snapshot.GetRoomShortId()); shortID != "" {
return roomShareAppName + " room " + shortID
}
return roomShareAppName + " voice room"
}
func roomShareDescription(snapshot *roomv1.RoomSnapshot, owner roomShareOwnerData) string {
if snapshot != nil {
if description := strings.TrimSpace(snapshot.GetRoomExt()["description"]); description != "" {
return description
}
}
if ownerName := roomShareOwnerName(owner); ownerName != "" {
return "Join " + ownerName + " in a " + roomShareAppName + " voice room."
}
return "Join this " + roomShareAppName + " voice room."
}
func roomShareCoverURL(snapshot *roomv1.RoomSnapshot, owner roomShareOwnerData) string {
if snapshot != nil {
if coverURL := strings.TrimSpace(snapshot.GetRoomExt()["cover_url"]); coverURL != "" {
return coverURL
}
}
return strings.TrimSpace(owner.Avatar)
}
func roomShareText(title string, owner roomShareOwnerData) string {
if ownerName := roomShareOwnerName(owner); ownerName != "" {
return fmt.Sprintf("Join %s's %s room: %s", ownerName, roomShareAppName, strings.TrimSpace(title))
}
return "Join this " + roomShareAppName + " room: " + strings.TrimSpace(title)
}
func roomShareOwnerName(owner roomShareOwnerData) string {
if owner.Username != "" {
return owner.Username
}
if owner.DisplayUserID != "" {
return owner.DisplayUserID
}
return owner.UserID
}
func roomShareLandingURL(request *http.Request, roomID string, inviteCode string) string {
landing := url.URL{
Scheme: roomShareRequestScheme(request),
Host: roomShareRequestHost(request),
Path: roomShareLandingPathPrefix + url.PathEscape(strings.TrimSpace(roomID)),
}
query := landing.Query()
query.Set("source", "room_share")
if normalizedInviteCode := strings.TrimSpace(inviteCode); normalizedInviteCode != "" {
query.Set("invite_code", normalizedInviteCode)
}
landing.RawQuery = query.Encode()
return landing.String()
}
func roomShareDeepLink(roomID string, inviteCode string) string {
deepLink := url.URL{
Scheme: roomShareAppScheme,
Host: "room",
Path: "/" + strings.TrimSpace(roomID),
}
query := deepLink.Query()
query.Set("source", "room_share")
if normalizedInviteCode := strings.TrimSpace(inviteCode); normalizedInviteCode != "" {
query.Set("invite_code", normalizedInviteCode)
}
deepLink.RawQuery = query.Encode()
return deepLink.String()
}
func roomShareRequestScheme(request *http.Request) string {
if forwarded := firstHeaderValue(request.Header.Get("X-Forwarded-Proto")); forwarded == "https" || forwarded == "http" {
return forwarded
}
if request.TLS != nil {
return "https"
}
return "http"
}
func roomShareRequestHost(request *http.Request) string {
if forwarded := firstHeaderValue(request.Header.Get("X-Forwarded-Host")); forwarded != "" && !strings.ContainsAny(forwarded, "/\\\r\n") {
return forwarded
}
if host := strings.TrimSpace(request.Host); host != "" && !strings.ContainsAny(host, "/\\\r\n") {
return host
}
return "localhost"
}
func firstHeaderValue(raw string) string {
value := strings.TrimSpace(raw)
if comma := strings.Index(value, ","); comma >= 0 {
value = strings.TrimSpace(value[:comma])
}
return strings.ToLower(value)
}
func roomShareLandingHTML(data roomShareData) string {
title := html.EscapeString(data.Title)
description := html.EscapeString(data.Description)
imageURL := html.EscapeString(data.CoverURL)
landingURL := html.EscapeString(data.LandingURL)
deepLink := html.EscapeString(data.DeepLink)
appName := html.EscapeString(roomShareAppName)
return "<!doctype html><html><head>" +
`<meta charset="utf-8">` +
`<meta name="viewport" content="width=device-width,initial-scale=1">` +
"<title>" + title + "</title>" +
`<meta property="og:type" content="website">` +
`<meta property="og:site_name" content="` + appName + `">` +
`<meta property="og:title" content="` + title + `">` +
`<meta property="og:description" content="` + description + `">` +
`<meta property="og:url" content="` + landingURL + `">` +
`<meta property="og:image" content="` + imageURL + `">` +
`<meta name="twitter:card" content="summary_large_image">` +
`<meta name="twitter:title" content="` + title + `">` +
`<meta name="twitter:description" content="` + description + `">` +
`<meta name="twitter:image" content="` + imageURL + `">` +
`<meta property="al:android:package" content="` + roomShareAndroidPackage + `">` +
`<meta property="al:android:app_name" content="` + appName + `">` +
`<meta property="al:android:url" content="` + deepLink + `">` +
`<meta property="al:ios:app_name" content="` + appName + `">` +
`<meta property="al:ios:url" content="` + deepLink + `">` +
`</head><body>` +
`<main><h1>` + title + `</h1><p>` + description + `</p><p><a href="` + deepLink + `">Open room</a></p></main>` +
`</body></html>`
}