285 lines
9.9 KiB
Go
285 lines
9.9 KiB
Go
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>`
|
||
}
|