diff --git a/internal/botapi/projection.go b/internal/botapi/projection.go index ddfc39f6..57b180ae 100644 --- a/internal/botapi/projection.go +++ b/internal/botapi/projection.go @@ -392,127 +392,9 @@ func apiChatFull(chat domain.BotAPIChat) map[string]any { if chat.Fake { out["is_fake"] = true } - if chat.SlowModeDelay > 0 { - out["slow_mode_delay"] = chat.SlowModeDelay - } - if chat.LinkedChatID != 0 { - out["linked_chat_id"] = -1000000000000 - chat.LinkedChatID - } - if chat.Permissions != nil { - out["permissions"] = apiChatPermissions(*chat.Permissions) - } - if chat.PinnedMessage != nil { - out["pinned_message"] = apiMessage(*chat.PinnedMessage, chat.PinnedMessageUsers) - } return out } -// apiChatPermissions projects a channel's default restrictions as a Bot API -// ChatPermissions object (a right is granted when the matching restriction is -// off). -func apiChatPermissions(b domain.ChannelBannedRights) map[string]any { - text := !b.SendMessages && !b.SendPlain - return map[string]any{ - "can_send_messages": text, - "can_send_audios": !b.SendMedia && !b.SendAudios, - "can_send_documents": !b.SendMedia && !b.SendDocs, - "can_send_photos": !b.SendMedia && !b.SendPhotos, - "can_send_videos": !b.SendMedia && !b.SendVideos, - "can_send_video_notes": !b.SendMedia && !b.SendRoundvideos, - "can_send_voice_notes": !b.SendMedia && !b.SendVoices, - "can_send_polls": !b.SendPolls, - "can_send_other_messages": !b.SendStickers && !b.SendGifs && !b.SendGames && !b.SendInline, - "can_add_web_page_previews": !b.EmbedLinks, - "can_change_info": !b.ChangeInfo, - "can_invite_users": !b.InviteUsers, - "can_pin_messages": !b.PinMessages, - "can_manage_topics": !b.ManageTopics, - } -} - -// apiChatMember projects a resolved member as a Bot API ChatMember object. -func apiChatMember(m domain.BotAPIChatMember) map[string]any { - out := map[string]any{ - "status": botAPIMemberStatus(m.Member), - "user": apiUser(userOrPlaceholder(m.User, m.Member.UserID)), - } - switch out["status"] { - case "creator": - if m.Member.AdminRights.Anonymous { - out["is_anonymous"] = true - } - if m.Member.Rank != "" { - out["custom_title"] = m.Member.Rank - } - case "administrator": - a := m.Member.AdminRights - out["can_be_edited"] = false - out["is_anonymous"] = a.Anonymous - out["can_manage_chat"] = a.ManageChat - out["can_delete_messages"] = a.DeleteMessages - out["can_manage_video_chats"] = a.ManageCall - out["can_restrict_members"] = a.BanUsers - out["can_promote_members"] = a.AddAdmins - out["can_change_info"] = a.ChangeInfo - out["can_invite_users"] = a.InviteUsers - out["can_post_messages"] = a.PostMessages - out["can_edit_messages"] = a.EditMessages - out["can_pin_messages"] = a.PinMessages - out["can_manage_topics"] = a.ManageTopics - out["can_post_stories"] = a.PostStories - out["can_edit_stories"] = a.EditStories - out["can_delete_stories"] = a.DeleteStories - if m.Member.Rank != "" { - out["custom_title"] = m.Member.Rank - } - case "restricted": - b := m.Member.BannedRights - out["is_member"] = m.Member.Status == domain.ChannelMemberActive - for k, v := range apiChatPermissions(b) { - out[k] = v - } - if b.UntilDate > 0 { - out["until_date"] = b.UntilDate - } - case "kicked": - if m.Member.BannedRights.UntilDate > 0 { - out["until_date"] = m.Member.BannedRights.UntilDate - } - } - return out -} - -func userOrPlaceholder(u domain.User, id int64) domain.User { - if u.ID != 0 { - return u - } - return domain.User{ID: id} -} - -func botAPIMemberStatus(m domain.ChannelMember) string { - switch { - case m.Role == domain.ChannelRoleCreator: - return "creator" - case m.Status == domain.ChannelMemberKicked, m.Status == domain.ChannelMemberBanned, m.BannedRights.ViewMessages: - return "kicked" - case m.Role == domain.ChannelRoleAdmin: - return "administrator" - case m.Status == domain.ChannelMemberLeft: - return "left" - case botAPIMemberRestricted(m.BannedRights): - return "restricted" - default: - return "member" - } -} - -func botAPIMemberRestricted(b domain.ChannelBannedRights) bool { - return b.SendMessages || b.SendMedia || b.SendStickers || b.SendGifs || b.SendGames || - b.SendInline || b.EmbedLinks || b.SendPolls || b.ChangeInfo || b.InviteUsers || - b.PinMessages || b.ManageTopics || b.SendPhotos || b.SendVideos || b.SendRoundvideos || - b.SendAudios || b.SendVoices || b.SendDocs || b.SendPlain || b.SendReactions -} - 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 19a40719..9ef3aac9 100644 --- a/internal/botapi/server.go +++ b/internal/botapi/server.go @@ -42,8 +42,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) - BotAPIChatMemberCount(ctx context.Context, botID, chatID int64) (int, error) - BotAPIChatMember(ctx context.Context, botID, chatID, userID int64) (domain.BotAPIChatMember, 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) @@ -202,10 +200,6 @@ func (h *handler) handle(w http.ResponseWriter, r *http.Request) { h.getMe(w, r, botID) case "getchat": h.getChat(w, r, botID) - case "getchatmembercount", "getchatmemberscount": - h.getChatMemberCount(w, r, botID) - case "getchatmember": - h.getChatMember(w, r, botID) case "setmycommands": h.setMyCommands(w, r, botID) case "deletemycommands": @@ -343,57 +337,6 @@ func (h *handler) getChat(w http.ResponseWriter, r *http.Request, botID int64) { writeAPIOK(w, apiChatFull(chat)) } -func (h *handler) getChatMemberCount(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 - } - chatID, err := strconv.ParseInt(strings.TrimSpace(values["chat_id"]), 10, 64) - if err != nil || chatID == 0 { - writeAPIError(w, http.StatusBadRequest, "CHAT_ID_INVALID") - return - } - count, err := h.gateway.BotAPIChatMemberCount(r.Context(), botID, chatID) - if err != nil { - writeAPIError(w, http.StatusBadRequest, apiErrorDescription(err)) - return - } - writeAPIOK(w, count) -} - -func (h *handler) getChatMember(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 - } - chatID, err := strconv.ParseInt(strings.TrimSpace(values["chat_id"]), 10, 64) - if err != nil || chatID == 0 { - writeAPIError(w, http.StatusBadRequest, "CHAT_ID_INVALID") - return - } - userID, err := strconv.ParseInt(strings.TrimSpace(values["user_id"]), 10, 64) - if err != nil || userID <= 0 { - writeAPIError(w, http.StatusBadRequest, "USER_ID_INVALID") - return - } - member, err := h.gateway.BotAPIChatMember(r.Context(), botID, chatID, userID) - if err != nil { - writeAPIError(w, http.StatusBadRequest, apiErrorDescription(err)) - return - } - writeAPIOK(w, apiChatMember(member)) -} - func (h *handler) getUpdates(w http.ResponseWriter, r *http.Request, botID int64) { if h.gateway == nil { writeAPIError(w, http.StatusNotImplemented, "METHOD_NOT_FOUND") diff --git a/internal/botapi/server_test.go b/internal/botapi/server_test.go index 60600c0a..90cbf002 100644 --- a/internal/botapi/server_test.go +++ b/internal/botapi/server_test.go @@ -216,57 +216,6 @@ func TestGetChatUsesGateway(t *testing.T) { } } -func TestGetChatMemberCountAndMember(t *testing.T) { - bots := &fakeBotAPIBots{profile: domain.BotProfile{BotUserID: 1001, TokenSecret: "secret"}} - gateway := &fakeBotAPIGateway{ - memberCount: 7, - member: domain.BotAPIChatMember{ - User: domain.User{ID: 500, FirstName: "Ann"}, - Member: domain.ChannelMember{ - UserID: 500, Role: domain.ChannelRoleAdmin, Status: domain.ChannelMemberActive, - AdminRights: domain.ChannelAdminRights{BanUsers: true, PinMessages: true}, - Rank: "mod", - }, - }, - } - h := (&handler{bots: bots, gateway: gateway}).routes() - - rec := performBotAPIRequest(t, h, bots.profile, "getChatMemberCount", `{"chat_id":-1000000000042}`) - if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"result":7`) { - t.Fatalf("getChatMemberCount status=%d body=%s", rec.Code, rec.Body.String()) - } - - rec = performBotAPIRequest(t, h, bots.profile, "getChatMember", `{"chat_id":-1000000000042,"user_id":500}`) - if rec.Code != http.StatusOK { - t.Fatalf("getChatMember status=%d body=%s", rec.Code, rec.Body.String()) - } - var resp struct { - OK bool `json:"ok"` - Result struct { - Status string `json:"status"` - CustomTitle string `json:"custom_title"` - CanRestrict bool `json:"can_restrict_members"` - CanPromote bool `json:"can_promote_members"` - User struct { - ID int64 `json:"id"` - } `json:"user"` - } `json:"result"` - } - if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { - t.Fatalf("decode: %v", err) - } - if !resp.OK || resp.Result.Status != "administrator" || resp.Result.User.ID != 500 || - resp.Result.CustomTitle != "mod" || !resp.Result.CanRestrict || resp.Result.CanPromote { - t.Fatalf("getChatMember result = %s", rec.Body.String()) - } - - // user_id is required. - rec = performBotAPIRequest(t, h, bots.profile, "getChatMember", `{"chat_id":-1000000000042}`) - if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "USER_ID_INVALID") { - t.Fatalf("missing user_id 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() @@ -1548,12 +1497,9 @@ func (f *fakeWebAppService) SavePreparedInlineMessageFromBotAPI(_ context.Contex type fakeBotAPIGateway struct { self domain.User - chat domain.BotAPIChat - chatErr error - chatChatID int64 - memberCount int - member domain.BotAPIChatMember - memberErr error + chat domain.BotAPIChat + chatErr error + chatChatID int64 updates []domain.UpdateEvent updateBotID int64 @@ -1620,14 +1566,6 @@ func (f *fakeBotAPIGateway) BotAPIChat(_ context.Context, _ int64, chatID int64) return f.chat, f.chatErr } -func (f *fakeBotAPIGateway) BotAPIChatMemberCount(context.Context, int64, int64) (int, error) { - return f.memberCount, f.memberErr -} - -func (f *fakeBotAPIGateway) BotAPIChatMember(context.Context, int64, int64, int64) (domain.BotAPIChatMember, error) { - return f.member, f.memberErr -} - 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 index 5b2172d9..019019ac 100644 --- a/internal/domain/botapi_chat.go +++ b/internal/domain/botapi_chat.go @@ -15,18 +15,4 @@ type BotAPIChat struct { Verified bool Scam bool Fake bool - - // Channel/supergroup only, from the full view. - SlowModeDelay int - LinkedChatID int64 // domain channel id; the projection applies the Bot API encoding - Permissions *ChannelBannedRights // default restrictions; nil for a user chat - PinnedMessage *Message - PinnedMessageUsers []User -} - -// BotAPIChatMember is a resolved chat member for the Bot API getChatMember -// method. -type BotAPIChatMember struct { - User User - Member ChannelMember } diff --git a/internal/rpc/botapi_gateway.go b/internal/rpc/botapi_gateway.go index 8bca5446..e89c1b5a 100644 --- a/internal/rpc/botapi_gateway.go +++ b/internal/rpc/botapi_gateway.go @@ -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.GetChannel(ctx, botID, peer.ID) + view, err := r.deps.Channels.ResolveChannel(ctx, botID, peer.ID) if err != nil { return domain.BotAPIChat{}, botAPIChatErr(err) } @@ -79,91 +79,21 @@ func (r *Router) BotAPIChat(ctx context.Context, botID, chatID int64) (domain.Bo if ch.Broadcast && !ch.Megagroup { typ = "channel" } - 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{ + 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") } -// 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),