// Package web serves telesrv's read-only public link landing pages. package web import ( "context" "errors" "fmt" "html/template" "net" "net/http" "net/url" "strconv" "strings" "time" "unicode/utf8" "go.uber.org/zap" "telesrv/internal/domain" "telesrv/internal/links" ) type Config struct { Addr string PublicBaseURL string AppScheme string AppLinkBase string WebBaseURL string AppName string DownloadURL string StickerSets StickerSetResolver Users UsernameResolver Channels PublicChannelResolver Privacy AnonymousPrivacyResolver Photos ProfilePhotoResolver UniqueGifts UniqueStarGiftResolver GiftWithdrawals StarGiftWithdrawalResolver // TelegramLogin is the optional OIDC/Login HTTP adapter. Public Web owns // the listener so discovery/auth/token and public links share the exact // externally registered origin behind one reverse proxy. TelegramLogin http.Handler } type StickerSetResolver interface { ResolveStickerSet(ctx context.Context, ref domain.StickerSetRef) (domain.StickerSet, []domain.Document, bool, error) } type UsernameResolver interface { ByUsername(ctx context.Context, username string) (domain.User, bool, error) } // PublicChannelResolver exposes only the viewer-independent public username // projection. viewerUserID is always zero for this anonymous Web endpoint. type PublicChannelResolver interface { ResolvePublicChannelUsername(ctx context.Context, viewerUserID int64, username string) (domain.Channel, bool, error) // ResolvePublicChannelInvite resolves a private invite-link hash (the // "/+" landing page) to its target channel/supergroup. ResolvePublicChannelInvite(ctx context.Context, hash string) (domain.Channel, domain.ChannelInvite, bool, error) } type AnonymousPrivacyResolver interface { CanSeeAnonymous(ctx context.Context, ownerUserID int64, key domain.PrivacyKey) (bool, error) } type ProfilePhotoResolver interface { CurrentProfilePhotoKind(ctx context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind) (domain.Photo, bool, error) GetPhoto(ctx context.Context, id int64) (domain.Photo, bool, error) GetFile(ctx context.Context, req domain.FileDownloadRequest) (domain.FileChunk, bool, error) } type UniqueStarGiftResolver interface { UniqueBySlug(ctx context.Context, slug string) (domain.UniqueStarGift, bool, error) } type StarGiftWithdrawalResolver interface { ResolveWithdrawal(ctx context.Context, providerRequestID string) (domain.StarGiftWithdrawal, bool, error) CompleteWithdrawal(ctx context.Context, providerRequestID string, date int) (domain.StarGiftWithdrawal, error) } func Start(ctx context.Context, cfg Config, logger *zap.Logger) (*http.Server, error) { addr := strings.TrimSpace(cfg.Addr) if addr == "" { return nil, nil } if logger == nil { logger = zap.NewNop() } handler, err := newHandler(cfg, logger) if err != nil { return nil, err } srv := &http.Server{ Addr: addr, Handler: handler, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 16 << 10, } ln, err := net.Listen("tcp", addr) if err != nil { return nil, err } go func() { logger.Info("Public link Web endpoint enabled", zap.String("addr", addr), zap.String("public_base_url", cfg.PublicBaseURL), zap.String("app_scheme", cfg.AppScheme), zap.String("app_link_base", cfg.AppLinkBase), zap.String("web_base_url", cfg.WebBaseURL), zap.String("download_url", cfg.DownloadURL)) if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Warn("Public link Web endpoint exited", zap.Error(err)) } }() go func() { <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() _ = srv.Shutdown(shutdownCtx) }() return srv, nil } func NewHandler(cfg Config) (http.Handler, error) { return newHandler(cfg, zap.NewNop()) } func newHandler(cfg Config, logger *zap.Logger) (http.Handler, error) { var err error if cfg.StickerSets == nil { return nil, fmt.Errorf("public Web sticker set resolver is nil") } if strings.TrimSpace(cfg.AppName) == "" { cfg.AppName = links.DefaultAppName } if cfg.PublicBaseURL, err = links.ValidateBaseURL(cfg.PublicBaseURL); err != nil { return nil, fmt.Errorf("public base URL: %w", err) } appLinks, err := links.NewAppLinkBuilder(cfg.AppScheme, cfg.AppLinkBase) if err != nil { return nil, fmt.Errorf("app links: %w", err) } // appLinks.legacyScheme is validated/defaulted internally but private; our // own path-based appURL/appInviteURL/appUsernameURL use the raw field // below directly, so it needs the same validation applied here too. if cfg.AppScheme, err = links.ValidateAppScheme(cfg.AppScheme); err != nil { return nil, fmt.Errorf("app scheme: %w", err) } // WebBaseURL is optional: leaving it unset disables the "Open in Web" // button on the username landing page instead of falling back to a // default URL that would point at someone else's Web client. if strings.TrimSpace(cfg.WebBaseURL) != "" { if cfg.WebBaseURL, err = links.ValidateBaseURL(cfg.WebBaseURL); err != nil { return nil, fmt.Errorf("Web base URL: %w", err) } } else { cfg.WebBaseURL = "" } if cfg.AppName, err = links.ValidateAppName(cfg.AppName); err != nil { return nil, fmt.Errorf("app name: %w", err) } if strings.TrimSpace(cfg.DownloadURL) == "" { cfg.DownloadURL = links.DefaultDownloadURL } if cfg.DownloadURL, err = links.ValidateBaseURL(cfg.DownloadURL); err != nil { return nil, fmt.Errorf("download URL: %w", err) } if logger == nil { logger = zap.NewNop() } // links.ValidateBaseURL above guarantees cfg.PublicBaseURL parses with a // non-empty host; publicHost is the bare "host[:port]" used to build // owpg:/// deep links (the client reinterprets those as // https:///, see LaunchActivity's "owpg" scheme case). publicHost := cfg.PublicBaseURL if u, err := url.Parse(cfg.PublicBaseURL); err == nil { publicHost = u.Host } h := &handler{ stickerSets: cfg.StickerSets, users: cfg.Users, channels: cfg.Channels, privacy: cfg.Privacy, photos: cfg.Photos, uniqueGifts: cfg.UniqueGifts, giftWithdrawals: cfg.GiftWithdrawals, publicBaseURL: cfg.PublicBaseURL, publicHost: publicHost, appScheme: cfg.AppScheme, appLinks: appLinks, webBaseURL: cfg.WebBaseURL, appName: cfg.AppName, downloadURL: cfg.DownloadURL, logger: logger, } mux := http.NewServeMux() mux.HandleFunc("GET /healthz", h.healthz) mux.HandleFunc("GET /_public/assets/logo.png", h.brandLogo) mux.HandleFunc("GET /_public/assets/fonts/{file}", h.brandFont) mux.HandleFunc("GET /_public/avatar/{username}/{photoID}", h.publicAvatar) mux.HandleFunc("GET /_public/invite-avatar/{hash}/{photoID}", h.publicInviteAvatar) mux.HandleFunc("GET /addstickers/{shortName}", h.addStickers) mux.HandleFunc("GET /addemoji/{shortName}", h.addEmoji) mux.HandleFunc("GET /addlist/{slug}", h.addList) mux.HandleFunc("GET /nft/{slug}", h.uniqueGift) mux.HandleFunc("GET /nft/{slug}/{$}", h.uniqueGift) mux.HandleFunc("GET /gift-withdrawal/{requestID}", h.starGiftWithdrawal) mux.HandleFunc("POST /gift-withdrawal/{requestID}", h.completeStarGiftWithdrawal) if cfg.TelegramLogin != nil { mux.Handle("GET /.well-known/openid-configuration", cfg.TelegramLogin) mux.Handle("GET /.well-known/jwks.json", cfg.TelegramLogin) mux.Handle("GET /auth", cfg.TelegramLogin) mux.Handle("GET /crossapp", cfg.TelegramLogin) mux.Handle("GET /inapp", cfg.TelegramLogin) mux.Handle("POST /auth/status", cfg.TelegramLogin) mux.Handle("POST /token", cfg.TelegramLogin) mux.Handle("GET /telegram-login.js", cfg.TelegramLogin) mux.Handle("GET /js/telegram-login.js", cfg.TelegramLogin) } mux.HandleFunc("GET /{username}", h.usernameLink) mux.HandleFunc("GET /{username}/{$}", h.usernameLink) return publicSecurityHeaders(mux), nil } type handler struct { stickerSets StickerSetResolver users UsernameResolver channels PublicChannelResolver privacy AnonymousPrivacyResolver photos ProfilePhotoResolver uniqueGifts UniqueStarGiftResolver giftWithdrawals StarGiftWithdrawalResolver publicBaseURL string publicHost string appScheme string appLinks links.AppLinkBuilder webBaseURL string appName string downloadURL string logger *zap.Logger } type starGiftWithdrawalPage struct { AppName string Title string Slug string Status string OwnerAddress string GiftAddress string ExpiresAt string CanComplete bool } var starGiftWithdrawalTemplate = template.Must(template.New("star-gift-withdrawal").Parse(` {{.Title}} · {{.AppName}}

{{.Title}}

Collectible: {{.Slug}}

{{if .CanComplete}}

This export is handled only by {{.AppName}}'s internal ledger. No external blockchain or wallet is contacted.

Expires: {{.ExpiresAt}}

{{else}}

Status: {{.Status}}

{{if .OwnerAddress}}

Owner address: {{.OwnerAddress}}

Gift address: {{.GiftAddress}}

{{end}}{{end}}
`)) func (h *handler) starGiftWithdrawal(w http.ResponseWriter, r *http.Request) { h.renderStarGiftWithdrawal(w, r, false) } func (h *handler) completeStarGiftWithdrawal(w http.ResponseWriter, r *http.Request) { h.renderStarGiftWithdrawal(w, r, true) } func (h *handler) renderStarGiftWithdrawal(w http.ResponseWriter, r *http.Request, complete bool) { requestID := strings.TrimSpace(r.PathValue("requestID")) if h.giftWithdrawals == nil || requestID == "" || len(requestID) > 256 { http.NotFound(w, r) return } var withdrawal domain.StarGiftWithdrawal var found bool var err error if complete { withdrawal, err = h.giftWithdrawals.CompleteWithdrawal(r.Context(), requestID, int(time.Now().Unix())) found = err == nil } else { withdrawal, found, err = h.giftWithdrawals.ResolveWithdrawal(r.Context(), requestID) } if err != nil || !found { http.NotFound(w, r) return } page := starGiftWithdrawalPage{AppName: h.appName, Title: withdrawal.Gift.Title, Slug: withdrawal.Gift.Slug, Status: withdrawal.Status, OwnerAddress: withdrawal.Gift.OwnerAddress, GiftAddress: withdrawal.Gift.GiftAddress, ExpiresAt: time.Unix(int64(withdrawal.ExpiresAt), 0).UTC().Format(time.RFC3339), CanComplete: withdrawal.Status == "pending" && withdrawal.ExpiresAt > int(time.Now().Unix())} if page.Title == "" { page.Title = "Collectible gift export" } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := starGiftWithdrawalTemplate.Execute(w, page); err != nil { h.logger.Warn("render star gift withdrawal", zap.Error(err)) } } func (h *handler) healthz(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte("ok\n")) } func (h *handler) addStickers(w http.ResponseWriter, r *http.Request) { h.serveSet(w, r, "addstickers") } func (h *handler) addEmoji(w http.ResponseWriter, r *http.Request) { h.serveSet(w, r, "addemoji") } func (h *handler) addList(w http.ResponseWriter, r *http.Request) { slug := strings.TrimSpace(r.PathValue("slug")) if !validSlugPath(slug) { http.NotFound(w, r) return } app := h.appURL("addlist", slug) data := pageData{ AppName: h.appName, Title: "Shared Folder", KindLabel: "shared folder", Subtitle: slug, Description: "This page opens the app so you can preview and add this shared folder.", CanonicalURL: h.publicURL("addlist", slug), AppURL: template.URL(app), DownloadURL: h.downloadURL, } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=60") if err := landingTemplate.Execute(w, data); err != nil { http.Error(w, "render shared folder page failed", http.StatusInternalServerError) } } func (h *handler) uniqueGift(w http.ResponseWriter, r *http.Request) { slug := r.PathValue("slug") if h.uniqueGifts == nil || !validStarGiftSlugPath(slug) { http.NotFound(w, r) return } unique, found, err := h.uniqueGifts.UniqueBySlug(r.Context(), slug) if err != nil { h.logger.Error("Public unique star gift lookup failed", zap.String("slug", slug), zap.Error(err)) http.Error(w, "collectible gift lookup failed", http.StatusInternalServerError) return } if !found { http.NotFound(w, r) return } canonicalSlug := unique.Slug if unique.ID <= 0 || unique.GiftID <= 0 || unique.Num <= 0 || !validStarGiftSlugPath(canonicalSlug) || !strings.EqualFold(slug, canonicalSlug) || !utf8.ValidString(unique.Title) || utf8.RuneCountInString(unique.Title) > domain.MaxStarGiftTitleRunes { h.logger.Error("Public unique star gift resolver returned invalid aggregate", zap.String("requested_slug", slug), zap.String("resolved_slug", canonicalSlug), zap.Int64("unique_id", unique.ID), zap.Int64("gift_id", unique.GiftID), zap.Int("num", unique.Num)) http.Error(w, "collectible gift lookup failed", http.StatusInternalServerError) return } if slug != canonicalSlug || strings.HasSuffix(r.URL.Path, "/") { http.Redirect(w, r, h.publicURL("nft", canonicalSlug), http.StatusPermanentRedirect) return } title := strings.TrimSpace(unique.Title) if title == "" { title = "Collectible gift" } subtitle := fmt.Sprintf("Collectible #%d", unique.Num) if unique.AvailabilityIssued > 0 && unique.AvailabilityTotal >= unique.AvailabilityIssued { subtitle += fmt.Sprintf(" · %s/%s issued", groupedDecimal(unique.AvailabilityIssued), groupedDecimal(unique.AvailabilityTotal)) } app := h.appURL("nft", canonicalSlug) data := pageData{ AppName: h.appName, Title: title, KindLabel: "collectible gift", Subtitle: subtitle, Description: "This collectible was created from a gift on " + h.appName + ". Open it in the app to view its current details.", CanonicalURL: h.publicURL("nft", canonicalSlug), AppURL: template.URL(app), LegacyTgURL: template.URL(legacyTgURL("nft", "slug", canonicalSlug)), } data.AppURLJS = template.JS(strconv.Quote(app)) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=60, must-revalidate") if err := landingTemplate.Execute(w, data); err != nil { h.logger.Error("Render public unique star gift page failed", zap.String("slug", canonicalSlug), zap.Error(err)) } } func (h *handler) usernameLink(w http.ResponseWriter, r *http.Request) { raw := strings.TrimSpace(r.PathValue("username")) if strings.HasPrefix(raw, "+") { h.inviteLink(w, r, strings.TrimPrefix(raw, "+")) return } username := raw if !validUsernamePath(username) { h.serveUsernameNotFound(w, username) return } params, ok := publicResolveQuery(r.URL.RawQuery) if !ok { http.Error(w, "public link query is too large or invalid", http.StatusRequestURITooLong) return } peer, found, err := h.resolvePublicPeer(r.Context(), username) if err != nil { h.logger.Error("Public username lookup failed", zap.String("username", username), zap.Error(err)) http.Error(w, "username lookup failed", http.StatusInternalServerError) return } if !found { h.serveUsernameNotFound(w, username) return } // owpg:/// — built from params BEFORE "domain" is added // below, since owpg encodes the username in the path, not as a query key. app := h.appUsernameURL(peer.username, params) params.Set("domain", peer.username) description := peer.about if description == "" { description = peer.fallbackDescription(h.appName) } grad := avatarGradient(peer.id) data := usernamePageData{ AppName: h.appName, AppInitial: appInitial(h.appName), Title: peer.title, Username: peer.username, Verified: peer.verified, Extra: peer.extra(), Description: description, AboutText: peer.about, CanonicalURL: h.publicUsernameURL(peer.username), HomeURL: h.publicBaseURL + "/", DownloadURL: h.downloadURL, AppURL: template.URL(app), ButtonLabel: peer.buttonLabel(), Initials: peer.initials(), GradFrom: grad[0], GradTo: grad[1], } if h.webBaseURL != "" { legacy := schemeURLValues("tg", "resolve", params) data.WebURL = template.URL(publicWebAppURL(h.webBaseURL, legacy)) } if peer.hasPhoto { data.PhotoURL = h.publicAvatarURL(peer.username, peer.photo.ID) } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=60, must-revalidate") if err := usernameLandingTemplate.Execute(w, data); err != nil { h.logger.Error("Render public username page failed", zap.String("username", peer.username), zap.Error(err)) } } // inviteLink serves the "/+" private invite-link preview page: the // channel/supergroup's photo, title, and member count, with a Join button // that deep-links into the app. It never requires or reveals viewer-specific // membership state. func (h *handler) inviteLink(w http.ResponseWriter, r *http.Request, hash string) { if !validInviteHashPath(hash) || h.channels == nil { h.serveInviteNotFound(w) return } ch, invite, found, err := h.channels.ResolvePublicChannelInvite(r.Context(), hash) if err != nil { h.logger.Error("Public invite lookup failed", zap.String("invite_hash", hash), zap.Error(err)) http.Error(w, "invite lookup failed", http.StatusInternalServerError) return } if !found { h.serveInviteNotFound(w) return } peer, ok, err := h.publicInviteChannelPeer(r.Context(), ch) if err != nil || !ok { if err != nil { h.logger.Error("Public invite channel projection failed", zap.String("invite_hash", hash), zap.Error(err)) } h.serveInviteNotFound(w) return } title := peer.title if title == "" { title = strings.TrimSpace(invite.Title) } app := h.appInviteURL(hash) description := peer.about if description == "" { description = peer.fallbackDescription(h.appName) } grad := avatarGradient(peer.id) data := usernamePageData{ AppName: h.appName, AppInitial: appInitial(h.appName), Title: title, Verified: peer.verified, Extra: peer.extra(), Description: description, AboutText: peer.about, CanonicalURL: h.publicInviteURL(hash), HomeURL: h.publicBaseURL + "/", DownloadURL: h.downloadURL, AppURL: template.URL(app), ButtonLabel: peer.inviteButtonLabel(), Initials: peer.initials(), GradFrom: grad[0], GradTo: grad[1], } if peer.hasPhoto { data.PhotoURL = h.publicInviteAvatarURL(hash, peer.photo.ID) } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=60, must-revalidate") if err := usernameLandingTemplate.Execute(w, data); err != nil { h.logger.Error("Render public invite page failed", zap.String("invite_hash", hash), zap.Error(err)) } } const maxPublicAvatarBytes = 4 << 20 func (h *handler) publicAvatar(w http.ResponseWriter, r *http.Request) { username := strings.TrimSpace(r.PathValue("username")) photoID, err := strconv.ParseInt(r.PathValue("photoID"), 10, 64) if err != nil || photoID <= 0 || !validUsernamePath(username) || h.photos == nil { http.NotFound(w, r) return } peer, found, err := h.resolvePublicPeer(r.Context(), username) if err != nil { h.logger.Error("Public avatar peer lookup failed", zap.String("username", username), zap.Error(err)) http.Error(w, "avatar lookup failed", http.StatusInternalServerError) return } if !found || !peer.hasPhoto || peer.photo.ID != photoID { http.NotFound(w, r) return } h.servePeerAvatarPhoto(w, r, peer, photoID, zap.String("username", username)) } func (h *handler) publicInviteAvatar(w http.ResponseWriter, r *http.Request) { hash := strings.TrimSpace(r.PathValue("hash")) photoID, err := strconv.ParseInt(r.PathValue("photoID"), 10, 64) if err != nil || photoID <= 0 || !validInviteHashPath(hash) || h.photos == nil || h.channels == nil { http.NotFound(w, r) return } ch, _, found, err := h.channels.ResolvePublicChannelInvite(r.Context(), hash) if err != nil { h.logger.Error("Public invite avatar lookup failed", zap.String("invite_hash", hash), zap.Error(err)) http.Error(w, "avatar lookup failed", http.StatusInternalServerError) return } if !found { http.NotFound(w, r) return } peer, ok, err := h.publicInviteChannelPeer(r.Context(), ch) if err != nil || !ok || !peer.hasPhoto || peer.photo.ID != photoID { http.NotFound(w, r) return } h.servePeerAvatarPhoto(w, r, peer, photoID, zap.String("invite_hash", hash)) } // servePeerAvatarPhoto streams an already-authorized public peer's profile // photo. logField identifies the lookup key (username or invite hash) for // diagnostics only; the caller has already verified peer.hasPhoto and that // peer.photo.ID matches the requested photoID. func (h *handler) servePeerAvatarPhoto(w http.ResponseWriter, r *http.Request, peer publicPeer, photoID int64, logField zap.Field) { size, inline, ok := bestPublicPhotoSize(peer.photo.Sizes) if !ok { http.NotFound(w, r) return } etag := fmt.Sprintf("\"public-avatar-%d-%s-%d\"", peer.photo.ID, size.Type, size.Size) w.Header().Set("ETag", etag) w.Header().Set("Cache-Control", "public, max-age=300, must-revalidate") if r.Header.Get("If-None-Match") == etag { w.WriteHeader(http.StatusNotModified) return } data := inline mimeType := "" if len(data) == 0 { chunk, found, err := h.photos.GetFile(r.Context(), domain.FileDownloadRequest{ LocationKey: fmt.Sprintf("photo:%d:%s", peer.photo.ID, size.Type), Limit: maxPublicAvatarBytes + 1, }) if err != nil { h.logger.Error("Read public avatar blob failed", logField, zap.Int64("photo_id", photoID), zap.Error(err)) http.Error(w, "avatar read failed", http.StatusInternalServerError) return } if !found || chunk.Total <= 0 || chunk.Total > maxPublicAvatarBytes || int64(len(chunk.Bytes)) != chunk.Total { h.logger.Warn("Public avatar blob is missing or outside bounds", logField, zap.Int64("photo_id", photoID), zap.Int64("total", chunk.Total), zap.Int("bytes", len(chunk.Bytes))) http.NotFound(w, r) return } data = chunk.Bytes mimeType = chunk.MimeType } if len(data) == 0 || len(data) > maxPublicAvatarBytes { http.NotFound(w, r) return } detected := http.DetectContentType(data) if !safePublicImageType(detected) { h.logger.Warn("Public avatar blob is not a safe raster image", logField, zap.Int64("photo_id", photoID), zap.String("detected_type", detected), zap.String("stored_type", mimeType)) http.NotFound(w, r) return } w.Header().Set("Content-Type", detected) w.Header().Set("Content-Length", strconv.Itoa(len(data))) if peer.photo.Date > 0 { w.Header().Set("Last-Modified", time.Unix(int64(peer.photo.Date), 0).UTC().Format(http.TimeFormat)) } w.WriteHeader(http.StatusOK) _, _ = w.Write(data) } func (h *handler) serveSet(w http.ResponseWriter, r *http.Request, pathKind string) { shortName := strings.TrimSpace(r.PathValue("shortName")) if !validShortNamePath(shortName) { http.NotFound(w, r) return } set, docs, found, err := h.stickerSets.ResolveStickerSet(r.Context(), domain.StickerSetRef{ Kind: domain.StickerSetRefByShortName, ShortName: shortName, }) if err != nil { http.Error(w, "sticker set lookup failed", http.StatusInternalServerError) return } if !found || set.Deleted { http.NotFound(w, r) return } canonicalKind := linkKind(set) if canonicalKind != pathKind { http.Redirect(w, r, h.publicURL(canonicalKind, set.ShortName), http.StatusPermanentRedirect) return } count := set.Count if count == 0 { count = len(docs) } app := h.appURL(canonicalKind, set.ShortName) data := pageData{ AppName: h.appName, Title: fallbackTitle(set), KindLabel: kindLabel(set), Subtitle: fmt.Sprintf("@%s · %d %s", set.ShortName, count, itemNoun(set, count)), Description: "This page opens the app so you can preview and install the set. Files are still fetched by the app through MTProto.", CanonicalURL: h.publicURL(canonicalKind, set.ShortName), AppURL: template.URL(app), DownloadURL: h.downloadURL, } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=60") if err := landingTemplate.Execute(w, data); err != nil { http.Error(w, "render sticker set page failed", http.StatusInternalServerError) } } func (h *handler) publicURL(kind, value string) string { return h.publicBaseURL + "/" + kind + "/" + url.PathEscape(value) } func (h *handler) publicUsernameURL(username string) string { return h.publicBaseURL + "/" + url.PathEscape(username) } func (h *handler) publicAvatarURL(username string, photoID int64) string { return h.publicBaseURL + "/_public/avatar/" + url.PathEscape(username) + "/" + strconv.FormatInt(photoID, 10) } func (h *handler) publicInviteURL(hash string) string { return h.publicBaseURL + "/+" + url.PathEscape(hash) } func (h *handler) publicInviteAvatarURL(hash string, photoID int64) string { return h.publicBaseURL + "/_public/invite-avatar/" + url.PathEscape(hash) + "/" + strconv.FormatInt(photoID, 10) } type publicPeerKind string const ( publicPeerUser publicPeerKind = "user" publicPeerBot publicPeerKind = "bot" publicPeerChannel publicPeerKind = "channel" publicPeerSupergroup publicPeerKind = "supergroup" ) type publicPeer struct { id int64 kind publicPeerKind username string title string about string verified bool memberCount int photo domain.Photo hasPhoto bool } func (h *handler) resolvePublicPeer(ctx context.Context, username string) (publicPeer, bool, error) { var ( u domain.User userOK bool ch domain.Channel chOK bool err error ) if h.users != nil { u, userOK, err = h.users.ByUsername(ctx, username) if err != nil { return publicPeer{}, false, err } } if h.channels != nil { ch, chOK, err = h.channels.ResolvePublicChannelUsername(ctx, 0, username) if err != nil { return publicPeer{}, false, err } } if userOK && chOK { return publicPeer{}, false, fmt.Errorf("public username %q has multiple owners", username) } if userOK { return h.publicUserPeer(ctx, username, u) } if chOK { return h.publicChannelPeer(ctx, username, ch) } return publicPeer{}, false, nil } func (h *handler) publicUserPeer(ctx context.Context, requested string, u domain.User) (publicPeer, bool, error) { if u.ID == 0 || !strings.EqualFold(strings.TrimSpace(u.Username), requested) || !validUsernamePath(u.Username) { return publicPeer{}, false, fmt.Errorf("user username lookup returned invalid owner for %q", requested) } title := strings.TrimSpace(u.FirstName + " " + u.LastName) if title == "" { title = u.Username } if err := validatePublicPeerText(title, u.About); err != nil { return publicPeer{}, false, fmt.Errorf("invalid public user %q: %w", u.Username, err) } about := strings.TrimSpace(u.About) photoKind := domain.ProfilePhotoKindProfile if !u.Bot && h.privacy != nil { visible, err := h.privacy.CanSeeAnonymous(ctx, u.ID, domain.PrivacyKeyAbout) if err != nil { return publicPeer{}, false, fmt.Errorf("evaluate public about privacy: %w", err) } if !visible { about = "" } visible, err = h.privacy.CanSeeAnonymous(ctx, u.ID, domain.PrivacyKeyProfilePhoto) if err != nil { return publicPeer{}, false, fmt.Errorf("evaluate public profile photo privacy: %w", err) } if !visible { photoKind = domain.ProfilePhotoKindFallback } } peer := publicPeer{ id: u.ID, kind: publicPeerUser, username: u.Username, title: title, about: about, verified: u.Verified, } if u.Bot { peer.kind = publicPeerBot } if h.photos != nil { photo, found, err := h.photos.CurrentProfilePhotoKind(ctx, domain.PeerTypeUser, u.ID, photoKind) if err != nil { return publicPeer{}, false, fmt.Errorf("load public user photo: %w", err) } if found && photo.ID != 0 { if _, _, renderable := bestPublicPhotoSize(photo.Sizes); renderable { peer.photo, peer.hasPhoto = photo, true } } } return peer, true, nil } func (h *handler) publicChannelPeer(ctx context.Context, requested string, ch domain.Channel) (publicPeer, bool, error) { if !strings.EqualFold(strings.TrimSpace(ch.Username), requested) || !validUsernamePath(ch.Username) { return publicPeer{}, false, fmt.Errorf("channel username lookup returned invalid owner for %q", requested) } return h.channelToPublicPeer(ctx, ch) } // publicInviteChannelPeer projects a channel resolved via an invite hash. // Unlike publicChannelPeer it does not require (or use) a public username — // invite links exist precisely for channels/groups that may not have one. func (h *handler) publicInviteChannelPeer(ctx context.Context, ch domain.Channel) (publicPeer, bool, error) { return h.channelToPublicPeer(ctx, ch) } func (h *handler) channelToPublicPeer(ctx context.Context, ch domain.Channel) (publicPeer, bool, error) { if ch.ID == 0 || ch.Deleted || ch.ParticipantsCount < 0 || (!ch.Broadcast && !ch.Megagroup) { return publicPeer{}, false, fmt.Errorf("channel %d is not eligible for a public projection", ch.ID) } if err := validatePublicPeerText(ch.Title, ch.About); err != nil { return publicPeer{}, false, fmt.Errorf("invalid public channel %q: %w", ch.Username, err) } peer := publicPeer{ id: ch.ID, kind: publicPeerChannel, username: ch.Username, title: strings.TrimSpace(ch.Title), about: strings.TrimSpace(ch.About), verified: ch.Verified, memberCount: ch.ParticipantsCount, } if ch.Megagroup { peer.kind = publicPeerSupergroup } if h.photos != nil && ch.PhotoID != 0 { photo, found, err := h.photos.GetPhoto(ctx, ch.PhotoID) if err != nil { return publicPeer{}, false, fmt.Errorf("load public channel photo: %w", err) } if !found { return publicPeer{}, false, fmt.Errorf("channel %q current photo %d is missing", ch.Username, ch.PhotoID) } if photo.ID == ch.PhotoID { if _, _, renderable := bestPublicPhotoSize(photo.Sizes); renderable { peer.photo, peer.hasPhoto = photo, true } } else { return publicPeer{}, false, fmt.Errorf("channel %q photo lookup returned id %d, want %d", ch.Username, photo.ID, ch.PhotoID) } } return peer, true, nil } func validatePublicPeerText(title, about string) error { if strings.TrimSpace(title) == "" || utf8.RuneCountInString(title) > 256 { return fmt.Errorf("title is empty or too long") } if utf8.RuneCountInString(about) > 4096 { return fmt.Errorf("about is too long") } return nil } func (p publicPeer) buttonLabel() string { switch p.kind { case publicPeerBot: return "Start Bot" case publicPeerChannel: return "View Channel" case publicPeerSupergroup: return "View Group" default: return "Send Message" } } // inviteButtonLabel is used on invite-link preview pages, where the viewer is // not yet a member — "Join", not "View" like the public-username page. func (p publicPeer) inviteButtonLabel() string { if p.kind == publicPeerSupergroup { return "Join Group" } return "Join Channel" } func (p publicPeer) extra() string { switch p.kind { case publicPeerBot: return "bot" case publicPeerChannel: return groupedDecimal(p.memberCount) + " " + plural(p.memberCount, "subscriber", "subscribers") case publicPeerSupergroup: return groupedDecimal(p.memberCount) + " " + plural(p.memberCount, "member", "members") default: return "" } } func (p publicPeer) fallbackDescription(appName string) string { switch p.kind { case publicPeerBot: return "Open " + appName + " to start a chat with this bot." case publicPeerChannel: return "Open " + appName + " to view and join this channel." case publicPeerSupergroup: return "Open " + appName + " to view and join this group." default: return "Open " + appName + " to send a message to @" + p.username + "." } } func (p publicPeer) initials() string { words := strings.Fields(p.title) if len(words) == 0 { words = []string{p.username} } first := []rune(words[0]) if len(first) == 0 { return "T" } out := []rune{first[0]} if len(words) > 1 { last := []rune(words[len(words)-1]) if len(last) > 0 { out = append(out, last[0]) } } return strings.ToUpper(string(out)) } var publicAvatarGradients = [...][2]string{ {"#FF885E", "#FF516A"}, {"#FFCD6A", "#FFA85C"}, {"#82B1FF", "#665FFF"}, {"#A0DE7E", "#54CB68"}, {"#53EDD6", "#28C9B7"}, {"#72D5FD", "#2A9EF1"}, {"#E0A2F3", "#D669ED"}, } func avatarGradient(id int64) [2]string { if id < 0 { id = -id } return publicAvatarGradients[id%int64(len(publicAvatarGradients))] } func groupedDecimal(n int) string { if n < 0 { n = 0 } s := strconv.Itoa(n) for i := len(s) - 3; i > 0; i -= 3 { s = s[:i] + " " + s[i:] } return s } func plural(n int, one, many string) string { if n == 1 { return one } return many } func appInitial(name string) string { for _, r := range name { return strings.ToUpper(string(r)) } return "T" } const ( maxPublicLinkRawQuery = 2048 maxPublicLinkParams = 16 maxPublicLinkValues = 2 maxPublicLinkValueLen = 512 ) func publicResolveQuery(raw string) (url.Values, bool) { if len(raw) > maxPublicLinkRawQuery { return nil, false } values, err := url.ParseQuery(raw) if err != nil || len(values) > maxPublicLinkParams { return nil, false } out := make(url.Values, len(values)+1) for key, items := range values { if strings.EqualFold(key, "domain") { continue } if !validPublicQueryKey(key) || len(items) > maxPublicLinkValues { return nil, false } for _, value := range items { if len(value) > maxPublicLinkValueLen || !utf8.ValidString(value) { return nil, false } out.Add(key, value) } } return out, true } func validPublicQueryKey(key string) bool { if key == "" || len(key) > 32 { return false } for _, r := range key { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' { continue } return false } return true } func bestPublicPhotoSize(sizes []domain.PhotoSize) (domain.PhotoSize, []byte, bool) { var ( best domain.PhotoSize bestBytes []byte bestScore int64 = -1 ) for _, size := range sizes { if !validPhotoSizeType(size.Type) { continue } var inline []byte switch size.Kind { case domain.PhotoSizeKindCached: if len(size.Bytes) == 0 || len(size.Bytes) > maxPublicAvatarBytes { continue } inline = size.Bytes case domain.PhotoSizeKindDefault, domain.PhotoSizeKindProgressive: // Downloadable static raster size. default: continue } score := int64(size.W) * int64(size.H) if score <= 0 { score = int64(size.Size) } if score > bestScore { best, bestBytes, bestScore = size, inline, score } } return best, bestBytes, bestScore >= 0 } func validPhotoSizeType(value string) bool { if value == "" || len(value) > 8 { return false } for _, r := range value { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' { continue } return false } return true } func safePublicImageType(value string) bool { switch value { case "image/jpeg", "image/png", "image/gif", "image/webp": return true default: return false } } func legacyTgURL(kind, key, value string) string { return schemeURL("tg", kind, key, value) } func schemeURLValues(scheme, kind string, values url.Values) string { return (&url.URL{Scheme: scheme, Host: kind, RawQuery: values.Encode()}).String() } func publicWebAppURL(webBaseURL, legacyURL string) string { return strings.TrimRight(webBaseURL, "/") + "/#?tgaddr=" + url.QueryEscape(legacyURL) } func publicSecurityHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Security-Policy", "default-src 'none'; img-src 'self' data:; font-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'") w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(), usb=()") w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") next.ServeHTTP(w, r) }) } func validShortNamePath(shortName string) bool { if shortName == "" || len(shortName) > 64 { return false } for _, r := range shortName { switch { case r >= 'a' && r <= 'z': case r >= 'A' && r <= 'Z': case r >= '0' && r <= '9': case r == '_' || r == '.': default: return false } } return true } func validSlugPath(slug string) bool { return links.ValidChatlistSlug(slug) } func validStarGiftSlugPath(slug string) bool { if slug == "" || len(slug) > domain.MaxStarGiftSlugBytes { return false } for _, r := range slug { switch { case r >= 'a' && r <= 'z': case r >= 'A' && r <= 'Z': case r >= '0' && r <= '9': case r == '.' || r == '_' || r == '-': default: return false } } return true } func validUsernamePath(username string) bool { if username == "" || len(username) < 5 || len(username) > 32 { return false } for i, r := range username { switch { case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z': case r >= '0' && r <= '9', r == '_': if i == 0 { return false } default: return false } } return true } func validInviteHashPath(hash string) bool { if hash == "" || len(hash) > 64 { return false } for _, r := range hash { switch { case r >= 'a' && r <= 'z': case r >= 'A' && r <= 'Z': case r >= '0' && r <= '9': case r == '_' || r == '-': default: return false } } return true } func linkKind(set domain.StickerSet) string { if set.Kind == domain.StickerSetKindEmoji || set.Emojis { return "addemoji" } return "addstickers" } func fallbackTitle(set domain.StickerSet) string { if title := strings.TrimSpace(set.Title); title != "" { return title } return set.ShortName } func kindLabel(set domain.StickerSet) string { switch { case set.Kind == domain.StickerSetKindEmoji || set.Emojis: return "custom emoji set" case set.Kind == domain.StickerSetKindMasks || set.Masks: return "mask set" default: return "sticker set" } } func itemNoun(set domain.StickerSet, count int) string { if set.Kind == domain.StickerSetKindEmoji || set.Emojis { if count == 1 { return "custom emoji" } return "custom emoji" } if count == 1 { return "sticker" } return "stickers" } // appURL/appInviteURL/appUsernameURL build owpg:/// deep links — // mirrors publicURL/publicInviteURL/publicUsernameURL (https:///) // but with the app scheme instead. The client's "owpg" URI handler reinterprets // owpg:///? as https:///? and falls // through to the normal t.me-style path router (LaunchActivity.java), which // expects a path, not a tg://-style "resolve?domain=" query. Do NOT use // schemeURL/appLinks.Build here — those formats (route-as-host with a query // key, or upstream's own AppLinkBuilder) are query-param based and only match // the real tg:// convention; our patched clients parse a path segment. func (h *handler) appURL(kind, value string) string { return h.appScheme + "://" + h.publicHost + "/" + kind + "/" + url.PathEscape(value) } func (h *handler) appInviteURL(hash string) string { return h.appScheme + "://" + h.publicHost + "/+" + url.PathEscape(hash) } func (h *handler) appUsernameURL(username string, extraQuery url.Values) string { u := h.appScheme + "://" + h.publicHost + "/" + url.PathEscape(username) if len(extraQuery) > 0 { u += "?" + extraQuery.Encode() } return u } // schemeURL/schemeURLValues build the real tg:// deep-link format // (tg://resolve?domain=X) — used only for the legacy-client fallback link // (LegacyTgURL), never for the app's own owpg:// scheme. func schemeURL(scheme, kind, key, value string) string { return scheme + "://" + kind + "?" + key + "=" + url.QueryEscape(value) } type pageData struct { AppName string Title string KindLabel string Subtitle string Description string CanonicalURL string DownloadURL string AppURL template.URL LegacyTgURL template.URL AppURLJS template.JS } type usernamePageData struct { AppName string AppInitial string Title string Username string Verified bool Extra string Description string AboutText string CanonicalURL string HomeURL string DownloadURL string PhotoURL string Initials string GradFrom string GradTo string ButtonLabel string AppURL template.URL WebURL template.URL } func (h *handler) serveUsernameNotFound(w http.ResponseWriter, username string) { message := "This is not a valid public " + h.appName + " username." if username != "" { message = "@" + username + " is not an active public " + h.appName + " username." } h.serveNotFound(w, message) } func (h *handler) serveInviteNotFound(w http.ResponseWriter) { h.serveNotFound(w, "This invite link is not active anymore.") } func (h *handler) serveNotFound(w http.ResponseWriter, message string) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=30, must-revalidate") w.WriteHeader(http.StatusNotFound) if err := usernameNotFoundTemplate.Execute(w, struct { Message string HomeURL string AppName string DownloadURL string }{Message: message, HomeURL: h.publicBaseURL + "/", AppName: h.appName, DownloadURL: h.downloadURL}); err != nil { h.logger.Error("Render public not-found page failed", zap.Error(err)) } } // publicPageStyleSheet is the shared visual system for every public landing // page (username profile, not-found, sticker/addlist): CSS variables (light // theme + dark-scheme override), the card/avatar/button component classes, // and the floating background orbs. Fonts are self-hosted (no Google Fonts // CDN call from visitors' browsers) via the /_public/assets/fonts/ route. const publicPageStyleSheet = ` @font-face{font-family:'Plus Jakarta Sans';font-style:normal;font-weight:400;font-display:swap;src:url('/_public/assets/fonts/plus-jakarta-sans-400.woff2') format('woff2')} @font-face{font-family:'Plus Jakarta Sans';font-style:normal;font-weight:500;font-display:swap;src:url('/_public/assets/fonts/plus-jakarta-sans-500.woff2') format('woff2')} @font-face{font-family:'Plus Jakarta Sans';font-style:normal;font-weight:600;font-display:swap;src:url('/_public/assets/fonts/plus-jakarta-sans-600.woff2') format('woff2')} @font-face{font-family:'Plus Jakarta Sans';font-style:normal;font-weight:700;font-display:swap;src:url('/_public/assets/fonts/plus-jakarta-sans-700.woff2') format('woff2')} @font-face{font-family:'Plus Jakarta Sans';font-style:normal;font-weight:800;font-display:swap;src:url('/_public/assets/fonts/plus-jakarta-sans-800.woff2') format('woff2')} :root{ --accent:#2563eb; --accent-bright:#38bdf8; --grad:linear-gradient(135deg,#38bdf8 0%,#2563eb 55%,#1e40af 100%); --bg:#f7f9fc; --surface:#ffffff; --surface-2:#f1f5f9; --line:#e2e8f0; --line-strong:#cbd5e1; --ink:#050508; --body:#3f4b5c; --muted:#64748b; --hero-glow:rgba(37,99,235,0.14); --hero-grid:rgba(5,5,8,0.04); --radius:18px; --radius-lg:24px; --shadow:0 28px 70px -36px rgba(5,5,8,0.35); } @media (prefers-color-scheme: dark){ :root{ --accent:#60a5fa; --accent-bright:#7dd3fc; --grad:linear-gradient(135deg,#7dd3fc 0%,#3b82f6 50%,#2563eb 100%); --bg:#08080e; --surface:#12121a; --surface-2:#181822; --line:#222228; --line-strong:#2e2e36; --ink:#f8fafc; --body:#a8b4c4; --muted:#7b8798; --hero-glow:rgba(59,130,246,0.25); --hero-grid:rgba(255,255,255,0.04); --shadow:0 32px 80px -40px rgba(0,0,0,0.75); } } *{box-sizing:border-box} html,body{height:100%} body{ margin:0; color:var(--body); background:var(--bg); font-family:'Plus Jakarta Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif; -webkit-font-smoothing:antialiased; line-height:1.65; min-height:100vh; display:flex; flex-direction:column; background-image: radial-gradient(900px 480px at 50% -8%, var(--hero-glow) 0%, rgba(0,0,0,0) 70%), linear-gradient(var(--hero-grid) 1px, transparent 1px), linear-gradient(90deg, var(--hero-grid) 1px, transparent 1px); background-size:auto, 44px 44px, 44px 44px; background-attachment:fixed; } h1,h2,h3{color:var(--ink); letter-spacing:-0.03em; font-weight:800; margin:0} a{color:var(--accent); text-decoration:none} header{ display:flex; align-items:center; justify-content:space-between; padding:18px 26px; max-width:1100px; width:100%; margin:0 auto; } .brand{display:flex; align-items:center; gap:11px; font-weight:800; font-size:19px; color:var(--ink); letter-spacing:-0.02em} .brand img{width:34px; height:34px; border-radius:9px; display:block} .btn{ display:inline-flex; align-items:center; justify-content:center; gap:8px; font-weight:700; font-size:0.92rem; padding:12px 20px; border-radius:11px; border:none; cursor:pointer; text-decoration:none; line-height:1; transition:transform .16s ease, box-shadow .16s ease, background .16s ease, border-color .16s ease, color .16s ease; font-family:inherit; } .btn:hover{transform:translateY(-1px)} .btn-primary{background:var(--grad); color:#fff; box-shadow:0 12px 28px -14px rgba(37,99,235,0.65); width:100%} .btn-primary:hover{box-shadow:0 16px 36px -14px rgba(37,99,235,0.75)} .btn-ghost{background:var(--surface); color:var(--ink); border:1px solid var(--line)} .btn-ghost:hover{border-color:var(--accent); color:var(--accent)} main{flex:1; display:flex; align-items:center; justify-content:center; padding:24px} .card{ width:100%; max-width:430px; background:var(--surface); border:1px solid var(--line); border-radius:var(--radius-lg); padding:38px 30px 30px; text-align:center; box-shadow:var(--shadow); } .avatar-wrap{position:relative; width:124px; height:124px; margin:0 auto 20px} .avatar{position:absolute; inset:0; width:124px; height:124px; border-radius:50%; display:block} .avatar-photo{object-fit:cover; background:var(--surface-2)} .name{font-size:25px; font-weight:800; line-height:1.2; color:var(--ink); word-break:break-word; display:inline-flex; align-items:center; gap:8px; justify-content:center} .badge{width:20px; height:20px; flex:0 0 auto} .sub{color:var(--muted); font-size:15px; margin-top:6px; font-weight:500} .about{color:var(--body); font-size:15px; line-height:1.6; margin-top:18px; white-space:pre-wrap; word-break:break-word; text-align:center} .actions{margin-top:26px; display:grid; gap:10px} .scam{display:inline-block; margin-top:12px; color:#e5484d; border:1px solid rgba(229,72,77,0.25); background:rgba(229,72,77,0.08); font-size:12px; font-weight:700; letter-spacing:.4px; padding:4px 10px; border-radius:8px} .nf-emoji{font-size:60px; line-height:1; margin-bottom:14px} @media (max-width:480px){ .card{padding:30px 22px 24px} header{padding:16px 18px} } /* Floating background orbs. */ .bg-orbs{position:fixed; inset:-60px; z-index:-1; pointer-events:none; transition:transform .3s ease-out; will-change:transform} .bg-orb{position:absolute; border-radius:50%; filter:blur(100px); pointer-events:none} .bg-orb--1{width:700px; height:700px; top:-15%; left:-10%; background:color-mix(in srgb,var(--accent-bright) 40%,transparent); animation:orbFloat1 20s ease-in-out infinite} .bg-orb--2{width:600px; height:600px; top:25%; right:-15%; background:color-mix(in srgb,var(--accent) 38%,transparent); animation:orbFloat2 24s ease-in-out infinite} .bg-orb--3{width:500px; height:500px; bottom:-15%; left:30%; background:color-mix(in srgb,var(--accent-bright) 30%,transparent); animation:orbFloat3 28s ease-in-out infinite} @keyframes orbFloat1{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(60px,-40px) scale(1.08)}66%{transform:translate(-30px,30px) scale(.92)}} @keyframes orbFloat2{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(-50px,-35px) scale(.93)}66%{transform:translate(45px,25px) scale(1.07)}} @keyframes orbFloat3{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(40px,45px) scale(1.06)}66%{transform:translate(-55px,-25px) scale(.94)}} @media (max-width:720px){ .bg-orb{filter:blur(60px)} .bg-orb--1{width:350px;height:350px} .bg-orb--2{width:300px;height:300px} .bg-orb--3{width:250px;height:250px} } @media (prefers-reduced-motion: reduce){ .bg-orb{animation:none} } /* Drifting line-icon particles. */ .bg-icons{position:fixed; inset:0; z-index:-1; overflow:hidden; pointer-events:none; color:var(--accent)} .bg-icon{position:absolute; left:0; top:0; will-change:transform} ` // publicBgOrbsMarkup is the floating-orb + particle container markup, placed // right after on every public landing page. const publicBgOrbsMarkup = ` ` // publicBgIconsScript drives the orb parallax and drifting line-icon // particles. Self-contained vanilla JS, no external requests. const publicBgIconsScript = ` ` var usernameLandingTemplate = template.Must(template.New("username-landing").Parse(` {{.Title}}{{if .Username}} (@{{.Username}}){{end}} - {{.AppName}} {{if .PhotoURL}}{{end}} {{if .PhotoURL}}{{end}} ` + publicBgOrbsMarkup + `
{{.AppName}} {{.AppName}} Download
{{if .PhotoURL}}{{end}}
{{.Title}} {{if .Verified}}{{end}}
{{if .Username}}
@{{.Username}}
{{end}} {{if .Extra}}
{{.Extra}}
{{end}} {{if .AboutText}}
{{.AboutText}}
{{end}}
{{.ButtonLabel}} {{if .WebURL}}Open in Web{{end}}
` + publicBgIconsScript + ` `)) var usernameNotFoundTemplate = template.Must(template.New("username-not-found").Parse(` Nothing found - {{.AppName}} ` + publicBgOrbsMarkup + `
{{.AppName}} {{.AppName}} Download
🤔

Nothing found

{{.Message}}
` + publicBgIconsScript + ` `)) var landingTemplate = template.Must(template.New("landing").Parse(` {{.Title}} - {{.AppName}} ` + publicBgOrbsMarkup + `
{{.AppName}} {{.AppName}} Download
{{.KindLabel}}

{{.Title}}

{{if .Subtitle}}
{{.Subtitle}}
{{end}} {{if .Description}}
{{.Description}}
{{end}} {{if .LegacyTgURL}}
Old test clients only: open with tg://
{{end}}
` + publicBgIconsScript + ` {{if .AppURLJS}}{{end}} `))