botapi: getChatMemberCount, getChatMember, and a fuller getChat

- getChatMemberCount: channel/supergroup participant count (numeric chat_id).
- getChatMember: resolves a member via GetParticipant, projected to a Bot API
  ChatMember (creator/administrator/restricted/member/left/kicked with the
  matching rights); a user simply not in an accessible chat returns "left".
- getChat now uses the full channel view and adds permissions (from the default
  restrictions), slow_mode_delay, linked_chat_id and pinned_message.

Channel-only methods reject user chat_ids; private chats the bot cannot access
return CHAT_NOT_FOUND.
This commit is contained in:
Astra 2026-09-09 16:26:33 +01:00
parent 71da34607e
commit d9ee8cbee0
5 changed files with 336 additions and 15 deletions

View file

@ -70,7 +70,7 @@ func (r *Router) BotAPIChat(ctx context.Context, botID, chatID int64) (domain.Bo
if r.deps.Channels == nil {
return domain.BotAPIChat{}, errors.New("CHAT_NOT_FOUND")
}
view, err := r.deps.Channels.ResolveChannel(ctx, botID, peer.ID)
view, err := r.deps.Channels.GetChannel(ctx, botID, peer.ID)
if err != nil {
return domain.BotAPIChat{}, botAPIChatErr(err)
}
@ -79,21 +79,91 @@ func (r *Router) BotAPIChat(ctx context.Context, botID, chatID int64) (domain.Bo
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
out := 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,
SlowModeDelay: ch.SlowmodeSeconds,
LinkedChatID: ch.LinkedChatID,
}
if typ == "supergroup" {
perms := ch.DefaultBannedRights
out.Permissions = &perms
}
if ch.PinnedMessageID > 0 {
if hist, msgErr := r.deps.Channels.GetMessages(ctx, botID, peer.ID, []int{ch.PinnedMessageID}); msgErr == nil && len(hist.Messages) > 0 {
pinned := botAPIMessageFromChannel(botID, hist.Messages[0])
out.PinnedMessage = &pinned
out.PinnedMessageUsers = hist.Users
}
}
return out, nil
}
return domain.BotAPIChat{}, errors.New("CHAT_ID_INVALID")
}
// BotAPIChatMemberCount resolves getChatMemberCount. Channels/supergroups only.
func (r *Router) BotAPIChatMemberCount(ctx context.Context, botID, chatID int64) (int, error) {
if r == nil || botID == 0 {
return 0, errors.New("BOT_INVALID")
}
peer, ok := botAPIPeerFromChatID(chatID)
if !ok || peer.Type != domain.PeerTypeChannel {
return 0, errors.New("CHAT_ID_INVALID")
}
if r.deps.Channels == nil {
return 0, errors.New("CHAT_NOT_FOUND")
}
view, err := r.deps.Channels.ResolveChannel(ctx, botID, peer.ID)
if err != nil {
return 0, botAPIChatErr(err)
}
return view.Channel.ParticipantsCount, nil
}
// BotAPIChatMember resolves getChatMember. Channels/supergroups only.
func (r *Router) BotAPIChatMember(ctx context.Context, botID, chatID, userID int64) (domain.BotAPIChatMember, error) {
if r == nil || botID == 0 {
return domain.BotAPIChatMember{}, errors.New("BOT_INVALID")
}
if userID <= 0 {
return domain.BotAPIChatMember{}, errors.New("USER_ID_INVALID")
}
peer, ok := botAPIPeerFromChatID(chatID)
if !ok || peer.Type != domain.PeerTypeChannel {
return domain.BotAPIChatMember{}, errors.New("CHAT_ID_INVALID")
}
if r.deps.Channels == nil {
return domain.BotAPIChatMember{}, errors.New("CHAT_NOT_FOUND")
}
member, err := r.deps.Channels.GetParticipant(ctx, botID, peer.ID, userID)
switch {
case err == nil:
case errors.Is(err, domain.ErrUserNotParticipant):
// Bot API returns a "left" member for a user who is simply not in the
// chat, as long as the chat itself is accessible.
member = domain.ChannelMember{ChannelID: peer.ID, UserID: userID, Role: domain.ChannelRoleMember, Status: domain.ChannelMemberLeft}
default:
return domain.BotAPIChatMember{}, botAPIChatErr(err)
}
out := domain.BotAPIChatMember{Member: member}
if r.deps.Users != nil {
if u, found, uErr := r.deps.Users.ByID(ctx, botID, userID); uErr == nil && found {
out.User = u
}
}
if out.User.ID == 0 {
out.User = domain.User{ID: userID}
}
return out, nil
}
func botAPIChatErr(err error) error {
switch {
case errors.Is(err, domain.ErrChannelInvalid),