feat: sync Bot API gateway support
This commit is contained in:
parent
9a501f900a
commit
4c0cc2b7a7
44 changed files with 4609 additions and 49 deletions
|
|
@ -21,12 +21,17 @@ import (
|
|||
type Config struct {
|
||||
Addr string
|
||||
PublicBaseURL string
|
||||
Users UsernameResolver
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, cfg Config, resolver Resolver, logger *zap.Logger) (*http.Server, error) {
|
||||
addr := strings.TrimSpace(cfg.Addr)
|
||||
if addr == "" {
|
||||
|
|
@ -38,7 +43,7 @@ func Start(ctx context.Context, cfg Config, resolver Resolver, logger *zap.Logge
|
|||
if logger == nil {
|
||||
logger = zap.NewNop()
|
||||
}
|
||||
handler := NewHandler(resolver, cfg.PublicBaseURL)
|
||||
handler := NewHandlerWithUsers(resolver, cfg.Users, cfg.PublicBaseURL)
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: handler,
|
||||
|
|
@ -64,8 +69,13 @@ func Start(ctx context.Context, cfg Config, resolver Resolver, logger *zap.Logge
|
|||
}
|
||||
|
||||
func NewHandler(resolver Resolver, publicBaseURL string) http.Handler {
|
||||
return NewHandlerWithUsers(resolver, nil, publicBaseURL)
|
||||
}
|
||||
|
||||
func NewHandlerWithUsers(resolver Resolver, users UsernameResolver, publicBaseURL string) http.Handler {
|
||||
h := &handler{
|
||||
resolver: resolver,
|
||||
users: users,
|
||||
publicBaseURL: normalizePublicBaseURL(publicBaseURL),
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
|
|
@ -73,11 +83,13 @@ func NewHandler(resolver Resolver, publicBaseURL string) http.Handler {
|
|||
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)
|
||||
return mux
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
resolver Resolver
|
||||
users UsernameResolver
|
||||
publicBaseURL string
|
||||
}
|
||||
|
||||
|
|
@ -118,6 +130,43 @@ 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"))
|
||||
if h.users == nil || !validUsernamePath(username) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
u, found, err := h.users.ByUsername(r.Context(), username)
|
||||
if err != nil {
|
||||
http.Error(w, "username lookup failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !found || !u.Bot || strings.TrimSpace(u.Username) == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
title := strings.TrimSpace(u.FirstName)
|
||||
if title == "" {
|
||||
title = u.Username
|
||||
}
|
||||
app := schemeURL("telesrv", "resolve", "domain", u.Username)
|
||||
data := pageData{
|
||||
Title: title,
|
||||
KindLabel: "bot",
|
||||
Subtitle: "@" + u.Username,
|
||||
Description: "This page opens the app so you can start a chat with this bot.",
|
||||
CanonicalURL: h.publicUsernameURL(u.Username),
|
||||
AppURL: template.URL(app),
|
||||
LegacyTgURL: template.URL(schemeURL("tg", "resolve", "domain", u.Username)),
|
||||
}
|
||||
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 bot page failed", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) serveSet(w http.ResponseWriter, r *http.Request, pathKind string) {
|
||||
shortName := strings.TrimSpace(r.PathValue("shortName"))
|
||||
if !validShortNamePath(shortName) {
|
||||
|
|
@ -167,6 +216,10 @@ 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 normalizePublicBaseURL(raw string) string {
|
||||
u, err := url.Parse(links.NormalizeBaseURL(raw))
|
||||
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||
|
|
@ -199,6 +252,24 @@ 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"
|
||||
|
|
|
|||
|
|
@ -107,6 +107,42 @@ func TestHandlerServesChatlistLandingPage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandlerServesBotUsernameLandingPage(t *testing.T) {
|
||||
users := fakeUsers{
|
||||
"tetrisbot": {
|
||||
ID: 1001,
|
||||
Username: "TetrisBot",
|
||||
FirstName: "Tetris Bot",
|
||||
Bot: true,
|
||||
},
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/TetrisBot", nil)
|
||||
|
||||
NewHandlerWithUsers(fakeResolver{}, users, "http://127.0.0.1:2401").ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
||||
}
|
||||
body := rr.Body.String()
|
||||
for _, want := range []string{
|
||||
"Tetris Bot",
|
||||
"bot",
|
||||
"@TetrisBot",
|
||||
"http://127.0.0.1:2401/TetrisBot",
|
||||
"telesrv://resolve?domain=TetrisBot",
|
||||
"tg://resolve?domain=TetrisBot",
|
||||
"start a chat with this bot",
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Fatalf("body missing %q:\n%s", want, body)
|
||||
}
|
||||
}
|
||||
if strings.Contains(body, `window.location.href = "tg://`) {
|
||||
t.Fatalf("landing page must not auto-open tg:// and steal official Telegram:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerRedirectsMismatchedKindToCanonicalURL(t *testing.T) {
|
||||
resolver := fakeResolver{
|
||||
"emoji_pack": {
|
||||
|
|
@ -131,13 +167,23 @@ func TestHandlerRedirectsMismatchedKindToCanonicalURL(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHandlerNotFoundForMissingOrInvalidShortName(t *testing.T) {
|
||||
handler := NewHandler(fakeResolver{}, "https://telesrv.net")
|
||||
handler := NewHandlerWithUsers(fakeResolver{}, fakeUsers{
|
||||
"alice": {
|
||||
ID: 2001,
|
||||
Username: "Alice",
|
||||
FirstName: "Alice",
|
||||
},
|
||||
}, "https://telesrv.net")
|
||||
for _, path := range []string{
|
||||
"/addstickers/missing_pack",
|
||||
"/addstickers/bad-name",
|
||||
"/addemoji/%E4%B8%AD%E6%96%87",
|
||||
"/addlist/bad!slug",
|
||||
"/addlist/%E4%B8%AD%E6%96%87",
|
||||
"/MissingBot",
|
||||
"/Alice",
|
||||
"/bad-name-bot",
|
||||
"/1stBot",
|
||||
} {
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
|
|
@ -175,3 +221,10 @@ type errorResolver struct{}
|
|||
func (errorResolver) ResolveStickerSet(context.Context, domain.StickerSetRef) (domain.StickerSet, []domain.Document, bool, error) {
|
||||
return domain.StickerSet{}, nil, false, errors.New("boom")
|
||||
}
|
||||
|
||||
type fakeUsers map[string]domain.User
|
||||
|
||||
func (f fakeUsers) ByUsername(_ context.Context, username string) (domain.User, bool, error) {
|
||||
u, ok := f[strings.ToLower(strings.TrimPrefix(username, "@"))]
|
||||
return u, ok, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue