fix for preview
This commit is contained in:
parent
935876570b
commit
d70148aca3
5 changed files with 240 additions and 14 deletions
|
|
@ -42,6 +42,11 @@ type ChannelStore interface {
|
|||
// viewerUserID may be zero for anonymous public-link projection; this lookup
|
||||
// never returns viewer-specific membership or dialog state.
|
||||
ResolvePublicChannelUsername(ctx context.Context, viewerUserID int64, username string) (domain.Channel, bool, error)
|
||||
// ResolvePublicChannelInvite resolves an active (not revoked, not deleted,
|
||||
// not expired) invite hash to its target channel/supergroup for the
|
||||
// anonymous public invite-link preview page. It never returns viewer-specific
|
||||
// membership state and does not require a userID.
|
||||
ResolvePublicChannelInvite(ctx context.Context, hash string) (domain.Channel, domain.ChannelInvite, bool, error)
|
||||
SearchPublicChannels(ctx context.Context, viewerUserID int64, query string, limit int) (domain.PublicChannelSearchResult, error)
|
||||
SetSignatures(ctx context.Context, userID, channelID int64, enabled bool) (domain.Channel, error)
|
||||
SetChannelPhoto(ctx context.Context, userID, channelID int64, photo *domain.Photo, date int) (domain.SetChannelPhotoResult, error)
|
||||
|
|
|
|||
|
|
@ -273,6 +273,30 @@ func (s *ChannelStore) CheckInvite(_ context.Context, userID int64, hash string,
|
|||
}, nil
|
||||
}
|
||||
|
||||
// ResolvePublicChannelInvite is the anonymous, unauthenticated counterpart of
|
||||
// CheckInvite: no userID/membership lookup, just "is this hash currently
|
||||
// joinable" plus the public channel projection for the invite preview page.
|
||||
func (s *ChannelStore) ResolvePublicChannelInvite(_ context.Context, hash string) (domain.Channel, domain.ChannelInvite, bool, error) {
|
||||
hash = strings.TrimSpace(hash)
|
||||
if hash == "" {
|
||||
return domain.Channel{}, domain.ChannelInvite{}, false, nil
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
invite, ok := s.invites[hash]
|
||||
if !ok || invite.Revoked {
|
||||
return domain.Channel{}, domain.ChannelInvite{}, false, nil
|
||||
}
|
||||
if invite.ExpireDate > 0 && invite.ExpireDate < int(time.Now().Unix()) {
|
||||
return domain.Channel{}, domain.ChannelInvite{}, false, nil
|
||||
}
|
||||
channel, ok := s.channels[invite.ChannelID]
|
||||
if !ok || channel.Deleted {
|
||||
return domain.Channel{}, domain.ChannelInvite{}, false, nil
|
||||
}
|
||||
return channel, invite, true, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) ImportInvite(_ context.Context, req domain.ImportChannelInviteRequest) (domain.CreateChannelResult, error) {
|
||||
if req.UserID == 0 || strings.TrimSpace(req.Hash) == "" {
|
||||
return domain.CreateChannelResult{}, domain.ErrInviteHashEmpty
|
||||
|
|
|
|||
|
|
@ -36,6 +36,27 @@ func (s *ChannelStore) CheckInvite(ctx context.Context, userID int64, hash strin
|
|||
return domain.CheckChannelInviteResult{Channel: channel, Invite: invite, Already: already, Self: member}, nil
|
||||
}
|
||||
|
||||
// ResolvePublicChannelInvite is the anonymous, unauthenticated counterpart of
|
||||
// CheckInvite: no userID/membership lookup, just "is this hash currently
|
||||
// joinable" plus the public channel projection for the invite preview page.
|
||||
func (s *ChannelStore) ResolvePublicChannelInvite(ctx context.Context, hash string) (domain.Channel, domain.ChannelInvite, bool, error) {
|
||||
hash = strings.TrimSpace(hash)
|
||||
if hash == "" {
|
||||
return domain.Channel{}, domain.ChannelInvite{}, false, nil
|
||||
}
|
||||
channel, invite, err := s.getInviteByHash(ctx, s.db, hash)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrInviteHashInvalid) {
|
||||
return domain.Channel{}, domain.ChannelInvite{}, false, nil
|
||||
}
|
||||
return domain.Channel{}, domain.ChannelInvite{}, false, err
|
||||
}
|
||||
if invite.ExpireDate > 0 && invite.ExpireDate < nowUnix() {
|
||||
return domain.Channel{}, domain.ChannelInvite{}, false, nil
|
||||
}
|
||||
return channel, invite, true, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) ImportInvite(ctx context.Context, req domain.ImportChannelInviteRequest) (domain.CreateChannelResult, error) {
|
||||
if req.UserID == 0 || strings.TrimSpace(req.Hash) == "" {
|
||||
return domain.CreateChannelResult{}, domain.ErrInviteHashEmpty
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ type UsernameResolver interface {
|
|||
// 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
|
||||
// "/+<hash>" landing page) to its target channel/supergroup.
|
||||
ResolvePublicChannelInvite(ctx context.Context, hash string) (domain.Channel, domain.ChannelInvite, bool, error)
|
||||
}
|
||||
|
||||
type AnonymousPrivacyResolver interface {
|
||||
|
|
@ -161,6 +164,7 @@ func newHandler(cfg Config, logger *zap.Logger) (http.Handler, error) {
|
|||
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)
|
||||
|
|
@ -221,7 +225,12 @@ func (h *handler) addList(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func (h *handler) usernameLink(w http.ResponseWriter, r *http.Request) {
|
||||
username := strings.TrimSpace(r.PathValue("username"))
|
||||
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
|
||||
|
|
@ -280,6 +289,70 @@ func (h *handler) usernameLink(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// inviteLink serves the "/+<hash>" 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.appURL("join", "invite", 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) {
|
||||
|
|
@ -299,6 +372,39 @@ func (h *handler) publicAvatar(w http.ResponseWriter, r *http.Request) {
|
|||
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)
|
||||
|
|
@ -319,12 +425,12 @@ func (h *handler) publicAvatar(w http.ResponseWriter, r *http.Request) {
|
|||
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))
|
||||
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", zap.String("username", username), zap.Int64("photo_id", photoID), zap.Int64("total", chunk.Total), zap.Int("bytes", len(chunk.Bytes)))
|
||||
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
|
||||
}
|
||||
|
|
@ -337,7 +443,7 @@ func (h *handler) publicAvatar(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
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))
|
||||
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
|
||||
}
|
||||
|
|
@ -407,6 +513,14 @@ 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 (
|
||||
|
|
@ -515,9 +629,23 @@ func (h *handler) publicUserPeer(ctx context.Context, requested string, u domain
|
|||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
|
@ -575,6 +703,15 @@ func (p publicPeer) buttonLabel() string {
|
|||
}
|
||||
}
|
||||
|
||||
// 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:
|
||||
|
|
@ -821,6 +958,23 @@ func validUsernamePath(username string) bool {
|
|||
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"
|
||||
|
|
@ -900,16 +1054,28 @@ type usernamePageData struct {
|
|||
}
|
||||
|
||||
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 {
|
||||
Username string
|
||||
Message string
|
||||
HomeURL string
|
||||
AppName string
|
||||
DownloadURL string
|
||||
}{Username: username, HomeURL: h.publicBaseURL + "/", AppName: h.appName, DownloadURL: h.downloadURL}); err != nil {
|
||||
h.logger.Error("Render public username not-found page failed", zap.String("username", username), zap.Error(err))
|
||||
}{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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1137,7 +1303,7 @@ var usernameLandingTemplate = template.Must(template.New("username-landing").Par
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#2563eb">
|
||||
<title>{{.Title}} (@{{.Username}}) - {{.AppName}}</title>
|
||||
<title>{{.Title}}{{if .Username}} (@{{.Username}}){{end}} - {{.AppName}}</title>
|
||||
<meta name="description" content="{{.Description}}">
|
||||
<meta name="robots" content="index,follow,max-image-preview:large">
|
||||
<link rel="canonical" href="{{.CanonicalURL}}">
|
||||
|
|
@ -1183,7 +1349,7 @@ var usernameLandingTemplate = template.Must(template.New("username-landing").Par
|
|||
<span>{{.Title}}</span>
|
||||
{{if .Verified}}<svg class="badge" viewBox="0 0 24 24" fill="none" aria-label="Verified"><circle cx="12" cy="12" r="11" fill="#2563eb"/><path d="M7 12.5l3.2 3.2L17 9" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>{{end}}
|
||||
</div>
|
||||
<div class="sub">@{{.Username}}</div>
|
||||
{{if .Username}}<div class="sub">@{{.Username}}</div>{{end}}
|
||||
{{if .Extra}}<div class="extra">{{.Extra}}</div>{{end}}
|
||||
{{if .AboutText}}<div class="about">{{.AboutText}}</div>{{end}}
|
||||
<div class="actions">
|
||||
|
|
@ -1203,7 +1369,7 @@ var usernameNotFoundTemplate = template.Must(template.New("username-not-found").
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#2563eb">
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<title>Username not found - {{.AppName}}</title>
|
||||
<title>Nothing found - {{.AppName}}</title>
|
||||
<link rel="icon" type="image/png" href="/_public/assets/logo.png">
|
||||
<style>` + publicPageStyleSheet + `</style>
|
||||
</head>
|
||||
|
|
@ -1219,8 +1385,7 @@ var usernameNotFoundTemplate = template.Must(template.New("username-not-found").
|
|||
<div class="card">
|
||||
<div class="nf-emoji">🤔</div>
|
||||
<h2 style="font-size:22px">Nothing found</h2>
|
||||
<div class="about">{{if .Username}}@{{.Username}} is not an active public {{.AppName}} username.{{else}}This is not a valid public {{.AppName}} username.{{end}}</div>
|
||||
<div class="actions"><a class="btn btn-primary" href="{{.HomeURL}}">Back to {{.AppName}}</a></div>
|
||||
<div class="about">{{.Message}}</div>
|
||||
</div>
|
||||
</main>` + publicBgIconsScript + `
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -458,7 +458,7 @@ func TestHandlerReturnsTrustedUsernameNotFoundPage(t *testing.T) {
|
|||
for _, path := range []string{"/MissingName", "/bad-name", "/Nope"} {
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
if rr.Code != http.StatusNotFound || !strings.Contains(rr.Body.String(), "Username not found") || !strings.Contains(rr.Body.String(), "noindex,nofollow") {
|
||||
if rr.Code != http.StatusNotFound || !strings.Contains(rr.Body.String(), "Nothing found") || !strings.Contains(rr.Body.String(), "noindex,nofollow") {
|
||||
t.Fatalf("%s status=%d body=%s", path, rr.Code, rr.Body.String())
|
||||
}
|
||||
if strings.Contains(rr.Body.String(), "telesrv://resolve") {
|
||||
|
|
@ -594,6 +594,17 @@ func (f fakeChannels) ResolvePublicChannelUsername(_ context.Context, _ int64, u
|
|||
return ch, ok, nil
|
||||
}
|
||||
|
||||
// ResolvePublicChannelInvite reuses the same map, keyed by whatever string
|
||||
// the test registers as the invite "hash" (map keys are otherwise usernames,
|
||||
// but nothing stops a test from also registering a hash-shaped key).
|
||||
func (f fakeChannels) ResolvePublicChannelInvite(_ context.Context, hash string) (domain.Channel, domain.ChannelInvite, bool, error) {
|
||||
ch, ok := f[hash]
|
||||
if !ok {
|
||||
return domain.Channel{}, domain.ChannelInvite{}, false, nil
|
||||
}
|
||||
return ch, domain.ChannelInvite{ChannelID: ch.ID, Hash: hash}, true, nil
|
||||
}
|
||||
|
||||
type fakeAnonymousPrivacy map[domain.PrivacyKey]bool
|
||||
|
||||
func (f fakeAnonymousPrivacy) CanSeeAnonymous(_ context.Context, _ int64, key domain.PrivacyKey) (bool, error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue