Compare commits

..

No commits in common. "d64172562242d58a5c60497ec54a5bdd41820af6" and "78267ee0d3490794564e79c88fc5e0f3bbb1d6f5" have entirely different histories.

5 changed files with 0 additions and 221 deletions

View file

@ -356,45 +356,6 @@ func apiMediaUsesCaption(media map[string]any) bool {
return false return false
} }
// apiChatFull projects a getChat result. The bot-api chat-id encoding is applied
// here: users keep their positive id, channels/supergroups become
// -1000000000000 - channelID.
func apiChatFull(chat domain.BotAPIChat) map[string]any {
id := chat.Peer.ID
if chat.Peer.Type == domain.PeerTypeChannel {
id = -1000000000000 - chat.Peer.ID
}
out := map[string]any{"id": id, "type": chat.Type}
if chat.Title != "" {
out["title"] = chat.Title
}
if chat.Username != "" {
out["username"] = chat.Username
}
if chat.FirstName != "" {
out["first_name"] = chat.FirstName
}
if chat.LastName != "" {
out["last_name"] = chat.LastName
}
if chat.Description != "" {
out["description"] = chat.Description
}
if chat.IsForum {
out["is_forum"] = true
}
if chat.Verified {
out["is_verified"] = true
}
if chat.Scam {
out["is_scam"] = true
}
if chat.Fake {
out["is_fake"] = true
}
return out
}
func apiChat(peer domain.Peer, users map[int64]domain.User) map[string]any { func apiChat(peer domain.Peer, users map[int64]domain.User) map[string]any {
switch peer.Type { switch peer.Type {
case domain.PeerTypeUser: case domain.PeerTypeUser:

View file

@ -41,7 +41,6 @@ type WebAppService interface {
type GatewayService interface { type GatewayService interface {
BotAPISelf(ctx context.Context, botID int64) (domain.User, error) BotAPISelf(ctx context.Context, botID int64) (domain.User, error)
BotAPIChat(ctx context.Context, botID, chatID int64) (domain.BotAPIChat, error)
BotAPIUpdates(ctx context.Context, botID int64, offset int64) ([]domain.UpdateEvent, error) BotAPIUpdates(ctx context.Context, botID int64, offset int64) ([]domain.UpdateEvent, error)
BotAPISendMessage(ctx context.Context, botID, chatID int64, text string, entities []domain.MessageEntity, replyMarkup *domain.MessageReplyMarkup, disableWebPagePreview, silent bool, replyToMessageID int) (domain.Message, error) BotAPISendMessage(ctx context.Context, botID, chatID int64, text string, entities []domain.MessageEntity, replyMarkup *domain.MessageReplyMarkup, disableWebPagePreview, silent bool, replyToMessageID int) (domain.Message, error)
BotAPISendRichMessage(ctx context.Context, botID, chatID int64, rich domain.BotAPIRichMessageInput, replyMarkup *domain.MessageReplyMarkup, silent, noForwards bool, replyToMessageID int, effectID int64) (domain.Message, error) BotAPISendRichMessage(ctx context.Context, botID, chatID int64, rich domain.BotAPIRichMessageInput, replyMarkup *domain.MessageReplyMarkup, silent, noForwards bool, replyToMessageID int, effectID int64) (domain.Message, error)
@ -198,8 +197,6 @@ func (h *handler) handle(w http.ResponseWriter, r *http.Request) {
switch strings.ToLower(method) { switch strings.ToLower(method) {
case "getme": case "getme":
h.getMe(w, r, botID) h.getMe(w, r, botID)
case "getchat":
h.getChat(w, r, botID)
case "setmycommands": case "setmycommands":
h.setMyCommands(w, r, botID) h.setMyCommands(w, r, botID)
case "deletemycommands": case "deletemycommands":
@ -313,30 +310,6 @@ func (h *handler) getMe(w http.ResponseWriter, r *http.Request, botID int64) {
writeAPIOK(w, apiUser(u)) writeAPIOK(w, apiUser(u))
} }
func (h *handler) getChat(w http.ResponseWriter, r *http.Request, botID int64) {
if h.gateway == nil {
writeAPIError(w, http.StatusNotImplemented, "METHOD_NOT_FOUND")
return
}
values, err := requestValues(r)
if err != nil {
writeAPIError(w, http.StatusBadRequest, "BAD_REQUEST")
return
}
// Numeric chat_id only - no @username resolution.
chatID, err := strconv.ParseInt(strings.TrimSpace(values["chat_id"]), 10, 64)
if err != nil || chatID == 0 {
writeAPIError(w, http.StatusBadRequest, "CHAT_ID_INVALID")
return
}
chat, err := h.gateway.BotAPIChat(r.Context(), botID, chatID)
if err != nil {
writeAPIError(w, http.StatusBadRequest, apiErrorDescription(err))
return
}
writeAPIOK(w, apiChatFull(chat))
}
func (h *handler) getUpdates(w http.ResponseWriter, r *http.Request, botID int64) { func (h *handler) getUpdates(w http.ResponseWriter, r *http.Request, botID int64) {
if h.gateway == nil { if h.gateway == nil {
writeAPIError(w, http.StatusNotImplemented, "METHOD_NOT_FOUND") writeAPIError(w, http.StatusNotImplemented, "METHOD_NOT_FOUND")
@ -1484,7 +1457,6 @@ func apiErrorDescription(err error) string {
"BUTTON_URL_INVALID", "BUTTON_URL_INVALID",
"BOT_INVALID", "BOT_INVALID",
"CHAT_ID_INVALID", "CHAT_ID_INVALID",
"CHAT_NOT_FOUND",
"ENTITY_INVALID", "ENTITY_INVALID",
"ENTITIES_TOO_LONG", "ENTITIES_TOO_LONG",
"ENTITY_BOUNDS_INVALID", "ENTITY_BOUNDS_INVALID",

View file

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
@ -162,60 +161,6 @@ func TestGetMeUsesGateway(t *testing.T) {
} }
} }
func TestGetChatUsesGateway(t *testing.T) {
bots := &fakeBotAPIBots{profile: domain.BotProfile{BotUserID: 1001, TokenSecret: "secret"}}
gateway := &fakeBotAPIGateway{
chat: domain.BotAPIChat{
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 42},
Type: "supergroup",
Title: "Test",
Username: "test1",
Description: "a test group",
IsForum: true,
},
}
h := (&handler{bots: bots, gateway: gateway}).routes()
rec := performBotAPIRequest(t, h, bots.profile, "getChat", `{"chat_id":-1000000000042}`)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body = %s", rec.Code, rec.Body.String())
}
if gateway.chatChatID != -1000000000042 {
t.Fatalf("gateway chat_id = %d, want -1000000000042", gateway.chatChatID)
}
var resp struct {
OK bool `json:"ok"`
Result struct {
ID int64 `json:"id"`
Type string `json:"type"`
Title string `json:"title"`
Username string `json:"username"`
Description string `json:"description"`
IsForum bool `json:"is_forum"`
} `json:"result"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
if !resp.OK || resp.Result.ID != -1000000000042 || resp.Result.Type != "supergroup" ||
resp.Result.Username != "test1" || resp.Result.Description != "a test group" || !resp.Result.IsForum {
t.Fatalf("response = %s", rec.Body.String())
}
// A private chat the bot cannot see comes back as chat not found.
gateway.chatErr = errors.New("CHAT_NOT_FOUND")
rec = performBotAPIRequest(t, h, bots.profile, "getChat", `{"chat_id":-1000000000099}`)
if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "CHAT_NOT_FOUND") {
t.Fatalf("not-found response status=%d body=%s", rec.Code, rec.Body.String())
}
// @username is rejected before it reaches the gateway.
rec = performBotAPIRequest(t, h, bots.profile, "getChat", `{"chat_id":"@test1"}`)
if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "CHAT_ID_INVALID") {
t.Fatalf("username response status=%d body=%s", rec.Code, rec.Body.String())
}
}
func TestBotCommandsPreserveEphemeralFlag(t *testing.T) { func TestBotCommandsPreserveEphemeralFlag(t *testing.T) {
bots := &fakeBotAPIBots{profile: domain.BotProfile{BotUserID: 1001, TokenSecret: "secret"}} bots := &fakeBotAPIBots{profile: domain.BotProfile{BotUserID: 1001, TokenSecret: "secret"}}
h := (&handler{bots: bots}).routes() h := (&handler{bots: bots}).routes()
@ -1497,10 +1442,6 @@ func (f *fakeWebAppService) SavePreparedInlineMessageFromBotAPI(_ context.Contex
type fakeBotAPIGateway struct { type fakeBotAPIGateway struct {
self domain.User self domain.User
chat domain.BotAPIChat
chatErr error
chatChatID int64
updates []domain.UpdateEvent updates []domain.UpdateEvent
updateBotID int64 updateBotID int64
updateOffset int64 updateOffset int64
@ -1561,11 +1502,6 @@ func (f *fakeBotAPIGateway) BotAPISelf(context.Context, int64) (domain.User, err
return f.self, nil return f.self, nil
} }
func (f *fakeBotAPIGateway) BotAPIChat(_ context.Context, _ int64, chatID int64) (domain.BotAPIChat, error) {
f.chatChatID = chatID
return f.chat, f.chatErr
}
func (f *fakeBotAPIGateway) BotAPIUpdates(_ context.Context, botID int64, offset int64) ([]domain.UpdateEvent, error) { func (f *fakeBotAPIGateway) BotAPIUpdates(_ context.Context, botID int64, offset int64) ([]domain.UpdateEvent, error) {
f.updateBotID = botID f.updateBotID = botID
f.updateOffset = offset f.updateOffset = offset

View file

@ -1,18 +0,0 @@
package domain
// BotAPIChat is a peer resolved for the Bot API getChat method. The bot need
// not be a member: a public channel or supergroup resolves (projected as a
// preview), while a private chat the bot has no access to resolves to an error.
type BotAPIChat struct {
Peer Peer // domain peer; the Bot API chat-id encoding is applied by the projection
Type string // "private" | "group" | "supergroup" | "channel"
Title string
Username string
FirstName string
LastName string
Description string // channel/supergroup "about"
IsForum bool
Verified bool
Scam bool
Fake bool
}

View file

@ -33,78 +33,6 @@ func (r *Router) BotAPISelf(ctx context.Context, botID int64) (domain.User, erro
return u, nil return u, nil
} }
// BotAPIChat resolves a chat for the Bot API getChat method. chat_id is numeric
// only (no @username). Public channels/supergroups resolve even when the bot is
// not a member; a private chat the bot cannot access is CHAT_NOT_FOUND.
func (r *Router) BotAPIChat(ctx context.Context, botID, chatID int64) (domain.BotAPIChat, error) {
if r == nil || botID == 0 {
return domain.BotAPIChat{}, errors.New("BOT_INVALID")
}
peer, ok := botAPIPeerFromChatID(chatID)
if !ok {
return domain.BotAPIChat{}, errors.New("CHAT_ID_INVALID")
}
switch peer.Type {
case domain.PeerTypeUser:
if r.deps.Users == nil {
return domain.BotAPIChat{}, errors.New("CHAT_NOT_FOUND")
}
u, found, err := r.deps.Users.ByID(ctx, botID, peer.ID)
if err != nil {
return domain.BotAPIChat{}, err
}
if !found {
return domain.BotAPIChat{}, errors.New("CHAT_NOT_FOUND")
}
return domain.BotAPIChat{
Peer: peer,
Type: "private",
FirstName: u.FirstName,
LastName: u.LastName,
Username: u.Username,
Verified: u.Verified,
Scam: u.Scam,
Fake: u.Fake,
}, nil
case domain.PeerTypeChannel:
if r.deps.Channels == nil {
return domain.BotAPIChat{}, errors.New("CHAT_NOT_FOUND")
}
view, err := r.deps.Channels.ResolveChannel(ctx, botID, peer.ID)
if err != nil {
return domain.BotAPIChat{}, botAPIChatErr(err)
}
ch := view.Channel
typ := "supergroup"
if ch.Broadcast && !ch.Megagroup {
typ = "channel"
}
return domain.BotAPIChat{
Peer: peer,
Type: typ,
Title: ch.Title,
Username: ch.Username,
Description: ch.About,
IsForum: ch.Forum,
Verified: ch.Verified,
Scam: ch.Scam,
Fake: ch.Fake,
}, nil
}
return domain.BotAPIChat{}, errors.New("CHAT_ID_INVALID")
}
func botAPIChatErr(err error) error {
switch {
case errors.Is(err, domain.ErrChannelInvalid),
errors.Is(err, domain.ErrChannelPrivate),
errors.Is(err, domain.ErrChannelUserBanned):
return errors.New("CHAT_NOT_FOUND")
default:
return channelInvalidErr(err)
}
}
// BotAPIUpdates returns durable update_id based events projected for the HTTP // BotAPIUpdates returns durable update_id based events projected for the HTTP
// Bot API. New deployments use the dedicated Bot API queue; the legacy // Bot API. New deployments use the dedicated Bot API queue; the legacy
// user_update_events fallback is kept for tests that have not wired the queue. // user_update_events fallback is kept for tests that have not wired the queue.