diff --git a/internal/botapi/projection.go b/internal/botapi/projection.go index 57b180ae..6c4c10ca 100644 --- a/internal/botapi/projection.go +++ b/internal/botapi/projection.go @@ -356,45 +356,6 @@ func apiMediaUsesCaption(media map[string]any) bool { 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 { switch peer.Type { case domain.PeerTypeUser: diff --git a/internal/botapi/server.go b/internal/botapi/server.go index 9ef3aac9..7bfcef76 100644 --- a/internal/botapi/server.go +++ b/internal/botapi/server.go @@ -41,7 +41,6 @@ type WebAppService interface { type GatewayService interface { 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) 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) @@ -198,8 +197,6 @@ func (h *handler) handle(w http.ResponseWriter, r *http.Request) { switch strings.ToLower(method) { case "getme": h.getMe(w, r, botID) - case "getchat": - h.getChat(w, r, botID) case "setmycommands": h.setMyCommands(w, r, botID) case "deletemycommands": @@ -313,30 +310,6 @@ func (h *handler) getMe(w http.ResponseWriter, r *http.Request, botID int64) { 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) { if h.gateway == nil { writeAPIError(w, http.StatusNotImplemented, "METHOD_NOT_FOUND") @@ -1484,7 +1457,6 @@ func apiErrorDescription(err error) string { "BUTTON_URL_INVALID", "BOT_INVALID", "CHAT_ID_INVALID", - "CHAT_NOT_FOUND", "ENTITY_INVALID", "ENTITIES_TOO_LONG", "ENTITY_BOUNDS_INVALID", diff --git a/internal/botapi/server_test.go b/internal/botapi/server_test.go index 90cbf002..b4b36fdc 100644 --- a/internal/botapi/server_test.go +++ b/internal/botapi/server_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" "mime/multipart" "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) { bots := &fakeBotAPIBots{profile: domain.BotProfile{BotUserID: 1001, TokenSecret: "secret"}} h := (&handler{bots: bots}).routes() @@ -1497,10 +1442,6 @@ func (f *fakeWebAppService) SavePreparedInlineMessageFromBotAPI(_ context.Contex type fakeBotAPIGateway struct { self domain.User - chat domain.BotAPIChat - chatErr error - chatChatID int64 - updates []domain.UpdateEvent updateBotID int64 updateOffset int64 @@ -1561,11 +1502,6 @@ func (f *fakeBotAPIGateway) BotAPISelf(context.Context, int64) (domain.User, err 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) { f.updateBotID = botID f.updateOffset = offset diff --git a/internal/domain/botapi_chat.go b/internal/domain/botapi_chat.go deleted file mode 100644 index 019019ac..00000000 --- a/internal/domain/botapi_chat.go +++ /dev/null @@ -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 -} diff --git a/internal/rpc/botapi_gateway.go b/internal/rpc/botapi_gateway.go index e89c1b5a..c6a02a56 100644 --- a/internal/rpc/botapi_gateway.go +++ b/internal/rpc/botapi_gateway.go @@ -33,78 +33,6 @@ func (r *Router) BotAPISelf(ctx context.Context, botID int64) (domain.User, erro 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 // 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.