{{.Title}}{{if .Verified}}✓{{end}}
@{{.Username}}
{{if .Extra}}{{.Extra}}
{{end}}{{.Description}}
Old test clients only: open with tg://
package stickerlinks 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 Users UsernameResolver Channels PublicChannelResolver Privacy AnonymousPrivacyResolver Photos ProfilePhotoResolver } type Resolver 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) } 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) } func Start(ctx context.Context, cfg Config, resolver Resolver, logger *zap.Logger) (*http.Server, error) { addr := strings.TrimSpace(cfg.Addr) if addr == "" { return nil, nil } if resolver == nil { return nil, fmt.Errorf("sticker links resolver is nil") } if logger == nil { logger = zap.NewNop() } handler := newHandler(resolver, cfg.Users, cfg.Channels, cfg.Privacy, cfg.Photos, cfg.PublicBaseURL, logger) 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", normalizePublicBaseURL(cfg.PublicBaseURL))) 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(resolver Resolver, publicBaseURL string) http.Handler { return newHandler(resolver, nil, nil, nil, nil, publicBaseURL, zap.NewNop()) } func NewHandlerWithUsers(resolver Resolver, users UsernameResolver, publicBaseURL string) http.Handler { return newHandler(resolver, users, nil, nil, nil, publicBaseURL, zap.NewNop()) } func NewHandlerWithPublicPeers( resolver Resolver, users UsernameResolver, channels PublicChannelResolver, privacy AnonymousPrivacyResolver, photos ProfilePhotoResolver, publicBaseURL string, ) http.Handler { return newHandler(resolver, users, channels, privacy, photos, publicBaseURL, zap.NewNop()) } func newHandler( resolver Resolver, users UsernameResolver, channels PublicChannelResolver, privacy AnonymousPrivacyResolver, photos ProfilePhotoResolver, publicBaseURL string, logger *zap.Logger, ) http.Handler { if logger == nil { logger = zap.NewNop() } h := &handler{ resolver: resolver, users: users, channels: channels, privacy: privacy, photos: photos, publicBaseURL: normalizePublicBaseURL(publicBaseURL), logger: logger, } mux := http.NewServeMux() mux.HandleFunc("GET /healthz", h.healthz) mux.HandleFunc("GET /_public/avatar/{username}/{photoID}", h.publicAvatar) mux.HandleFunc("GET /addstickers/{shortName}", h.addStickers) mux.HandleFunc("GET /addemoji/{shortName}", h.addEmoji) mux.HandleFunc("GET /addlist/{slug}", h.addList) mux.HandleFunc("GET /{username}", h.usernameLink) mux.HandleFunc("GET /{username}/{$}", h.usernameLink) return publicSecurityHeaders(mux) } type handler struct { resolver Resolver users UsernameResolver channels PublicChannelResolver privacy AnonymousPrivacyResolver photos ProfilePhotoResolver publicBaseURL string logger *zap.Logger } 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 := appURL("addlist", "slug", slug) data := pageData{ 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), LegacyTgURL: template.URL(legacyTgURL("addlist", "slug", slug)), } 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") if err := landingTemplate.Execute(w, data); err != nil { http.Error(w, "render shared folder page failed", http.StatusInternalServerError) } } func (h *handler) usernameLink(w http.ResponseWriter, r *http.Request) { username := strings.TrimSpace(r.PathValue("username")) 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 } params.Set("domain", peer.username) app := schemeURLValues("telesrv", "resolve", params) legacy := schemeURLValues("tg", "resolve", params) description := peer.about if description == "" { description = peer.fallbackDescription() } data := usernamePageData{ Title: peer.title, Username: peer.username, Verified: peer.verified, Extra: peer.extra(), Description: description, CanonicalURL: h.publicUsernameURL(peer.username), HomeURL: h.publicBaseURL + "/", AppURL: template.URL(app), LegacyTgURL: template.URL(legacy), WebURL: template.URL(publicWebAppURL(legacy)), ButtonLabel: peer.buttonLabel(), Initials: peer.initials(), } if peer.hasPhoto { data.PhotoURL = h.publicAvatarURL(peer.username, peer.photo.ID) } 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 := usernameLandingTemplate.Execute(w, data); err != nil { h.logger.Error("Render public username page failed", zap.String("username", peer.username), 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 } 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", zap.String("username", username), 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", zap.String("username", username), 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", zap.String("username", username), 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.resolver.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 := appURL(canonicalKind, "set", set.ShortName) data := pageData{ 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), LegacyTgURL: template.URL(legacyTgURL(canonicalKind, "set", set.ShortName)), } 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") 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) } type publicPeerKind string const ( publicPeerUser publicPeerKind = "user" publicPeerBot publicPeerKind = "bot" publicPeerChannel publicPeerKind = "channel" publicPeerSupergroup publicPeerKind = "supergroup" ) type publicPeer struct { 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{ 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 ch.ID == 0 || ch.Deleted || ch.ParticipantsCount < 0 || (!ch.Broadcast && !ch.Megagroup) || !strings.EqualFold(strings.TrimSpace(ch.Username), requested) || !validUsernamePath(ch.Username) { return publicPeer{}, false, fmt.Errorf("channel username lookup returned invalid owner for %q", requested) } if err := validatePublicPeerText(ch.Title, ch.About); err != nil { return publicPeer{}, false, fmt.Errorf("invalid public channel %q: %w", ch.Username, err) } peer := publicPeer{ 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" } } 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() string { switch p.kind { case publicPeerBot: return "Open telesrv to start a chat with this bot." case publicPeerChannel: return "Open telesrv to view and join this channel." case publicPeerSupergroup: return "Open telesrv to view and join this group." default: return "Open telesrv 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)) } 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 } 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 schemeURLValues(scheme, kind string, values url.Values) string { return (&url.URL{Scheme: scheme, Host: kind, RawQuery: values.Encode()}).String() } func publicWebAppURL(legacyURL string) string { return "https://web.telesrv.net/#?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:; style-src 'unsafe-inline'; script-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; 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 normalizePublicBaseURL(raw string) string { normalized, err := links.ValidateBaseURL(raw) if err != nil { return links.DefaultPublicBaseURL } return normalized } 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 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 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" } func appURL(kind, key, value string) string { return schemeURL("telesrv", kind, key, value) } func legacyTgURL(kind, key, value string) string { return schemeURL("tg", kind, key, value) } func schemeURL(scheme, kind, key, value string) string { return scheme + "://" + kind + "?" + key + "=" + url.QueryEscape(value) } type pageData struct { Title string KindLabel string Subtitle string Description string CanonicalURL string AppURL template.URL LegacyTgURL template.URL AppURLJS template.JS } type usernamePageData struct { Title string Username string Verified bool Extra string Description string CanonicalURL string HomeURL string PhotoURL string Initials string ButtonLabel string AppURL template.URL LegacyTgURL template.URL WebURL template.URL AppURLJS template.JS } func (h *handler) serveUsernameNotFound(w http.ResponseWriter, username 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 { Username string HomeURL string }{Username: username, HomeURL: h.publicBaseURL + "/"}); err != nil { h.logger.Error("Render public username not-found page failed", zap.String("username", username), zap.Error(err)) } } var usernameLandingTemplate = template.Must(template.New("username-landing").Parse(`
@{{.Username}}
{{if .Extra}}{{.Extra}}
{{end}}{{.Description}}
Old test clients only: open with tg://
{{if .Username}}@{{.Username}} is not an active public telesrv username.{{else}}This is not a valid public telesrv username.{{end}}
Back to telesrv{{.Description}}