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 "" + `` + `` + "" + title + "" + `` + `` + `` + `` + `` + `` + `` + `` + `` + `` + `` + `` + `` + `` + `` + `` + `

` + title + `

` + description + `

Open room

` + `` }