botapi: implement getChat

Adds the getChat method to the HTTP Bot API gateway. Numeric chat_id only (no
@username). Resolution goes through the shared peer resolvers:

- user: ByID; unknown -> CHAT_NOT_FOUND
- channel/supergroup: ResolveChannel, so a public one resolves even when the bot
  is not a member (projected as a preview); a private one the bot cannot access
  -> CHAT_NOT_FOUND, a banned bot likewise

The Chat projection returns id (bot-api encoded), type ("channel" for a
broadcast, "supergroup" for a megagroup, "private" for a user), title, username,
first/last name, description, is_forum, and the scam/fake/verified flags.
This commit is contained in:
Astra 2026-09-09 16:14:49 +01:00
parent 78267ee0d3
commit d022521d67
5 changed files with 221 additions and 0 deletions

View file

@ -33,6 +33,78 @@ 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.