Compare commits
No commits in common. "d2ffaa92bfdcaf3615fcf57bb547637accf1ea40" and "d64172562242d58a5c60497ec54a5bdd41820af6" have entirely different histories.
d2ffaa92bf
...
d641725622
5 changed files with 15 additions and 336 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -1551,9 +1500,6 @@ type fakeBotAPIGateway struct {
|
|||
chat domain.BotAPIChat
|
||||
chatErr error
|
||||
chatChatID int64
|
||||
memberCount int
|
||||
member domain.BotAPIChatMember
|
||||
memberErr error
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,7 +79,7 @@ func (r *Router) BotAPIChat(ctx context.Context, botID, chatID int64) (domain.Bo
|
|||
if ch.Broadcast && !ch.Megagroup {
|
||||
typ = "channel"
|
||||
}
|
||||
out := domain.BotAPIChat{
|
||||
return domain.BotAPIChat{
|
||||
Peer: peer,
|
||||
Type: typ,
|
||||
Title: ch.Title,
|
||||
|
|
@ -89,81 +89,11 @@ func (r *Router) BotAPIChat(ctx context.Context, botID, chatID int64) (domain.Bo
|
|||
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
|
||||
}, 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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue