diff --git a/admin.go b/admin.go index c4c828d..d58a34d 100644 --- a/admin.go +++ b/admin.go @@ -27,6 +27,7 @@ type Rights struct { CanAddPreviews bool `json:"can_add_web_page_previews"` CanManageVideoChats bool `json:"can_manage_video_chats"` CanManageChat bool `json:"can_manage_chat"` + CanManageTopics bool `json:"can_manage_topics"` } // NoRights is the default Rights{}. @@ -35,9 +36,8 @@ func NoRights() Rights { return Rights{} } // NoRestrictions should be used when un-restricting or // un-promoting user. // -// member.Rights = tele.NoRestrictions() -// b.Restrict(chat, member) -// +// member.Rights = tele.NoRestrictions() +// b.Restrict(chat, member) func NoRestrictions() Rights { return Rights{ CanBeEdited: true, @@ -56,6 +56,7 @@ func NoRestrictions() Rights { CanAddPreviews: true, CanManageVideoChats: false, CanManageChat: false, + CanManageTopics: false, } } @@ -78,6 +79,7 @@ func AdminRights() Rights { CanAddPreviews: true, CanManageVideoChats: true, CanManageChat: true, + CanManageTopics: true, } } @@ -120,11 +122,10 @@ func (b *Bot) Unban(chat *Chat, user *User, forBanned ...bool) error { // Restrict lets you restrict a subset of member's rights until // member.RestrictedUntil, such as: // -// * can send messages -// * can send media -// * can send other -// * can add web page previews -// +// - can send messages +// - can send media +// - can send other +// - can add web page previews func (b *Bot) Restrict(chat *Chat, member *ChatMember) error { prv, until := member.Rights, member.RestrictedUntil @@ -141,15 +142,14 @@ func (b *Bot) Restrict(chat *Chat, member *ChatMember) error { // Promote lets you update member's admin rights, such as: // -// * can change info -// * can post messages -// * can edit messages -// * can delete messages -// * can invite users -// * can restrict members -// * can pin messages -// * can promote members -// +// - can change info +// - can post messages +// - can edit messages +// - can delete messages +// - can invite users +// - can restrict members +// - can pin messages +// - can promote members func (b *Bot) Promote(chat *Chat, member *ChatMember) error { prv := member.Rights @@ -171,7 +171,6 @@ func (b *Bot) Promote(chat *Chat, member *ChatMember) error { // // If the chat is a group or a supergroup and // no administrators were appointed, only the creator will be returned. -// func (b *Bot) AdminsOf(chat *Chat) ([]ChatMember, error) { params := map[string]string{ "chat_id": chat.Recipient(), diff --git a/chat.go b/chat.go index ab76746..c297b1a 100644 --- a/chat.go +++ b/chat.go @@ -10,13 +10,16 @@ import ( type User struct { ID int64 `json:"id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Username string `json:"username"` - LanguageCode string `json:"language_code"` - IsBot bool `json:"is_bot"` - IsPremium bool `json:"is_premium"` - AddedToMenu bool `json:"added_to_attachment_menu"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + IsForum bool `json:"is_forum"` + Username string `json:"username"` + LanguageCode string `json:"language_code"` + IsBot bool `json:"is_bot"` + IsPremium bool `json:"is_premium"` + AddedToMenu bool `json:"added_to_attachment_menu"` + Usernames []string `json:"active_usernames"` + EmojiStatusCustomEmojiID string `json:"emoji_status_custom_emoji_id"` // Returns only in getMe CanJoinGroups bool `json:"can_join_groups"` @@ -162,14 +165,13 @@ func (c *ChatMemberUpdate) Time() time.Time { // // Example: // -// group := tele.ChatID(-100756389456) -// b.Send(group, "Hello!") -// -// type Config struct { -// AdminGroup tele.ChatID `json:"admin_group"` -// } -// b.Send(conf.AdminGroup, "Hello!") +// group := tele.ChatID(-100756389456) +// b.Send(group, "Hello!") // +// type Config struct { +// AdminGroup tele.ChatID `json:"admin_group"` +// } +// b.Send(conf.AdminGroup, "Hello!") type ChatID int64 // Recipient returns chat ID (see Recipient interface). diff --git a/message.go b/message.go index 47c0721..50bc48e 100644 --- a/message.go +++ b/message.go @@ -10,6 +10,9 @@ import ( type Message struct { ID int `json:"message_id"` + // (Optional) Unique identifier of a message thread to which the message belongs; for supergroups only + ThreadID int `json:"message_thread_id"` + // For message sent to channels, Sender will be nil Sender *User `json:"from"` @@ -59,6 +62,9 @@ type Message struct { // (Optional) Time of last edit in Unix. LastEdit int64 `json:"edit_date"` + // (Optional True, if the message is sent to a forum topic + TopicMessage bool `json:"is_topic_message"` + // (Optional) Message can't be forwarded. Protected bool `json:"has_protected_content,omitempty"` @@ -250,6 +256,15 @@ type Message struct { // Inline keyboard attached to the message. ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"` + + // Service message: forum topic created + TopicCreated *TopicCreated `json:"forum_topic_created,omitempty"` + + // Service message: forum topic closed + TopicClosed *TopicClosed `json:"forum_topic_closed,omitempty"` + + // Service message: forum topic reopened + TopicReopened *TopicReopened `json:"forum_topic_reopened,omitempty"` } // MessageEntity object represents "special" parts of text messages, @@ -365,7 +380,6 @@ func (m *Message) FromChannel() bool { // Service messages are automatically sent messages, which // typically occur on some global action. For instance, when // anyone leaves the chat or chat title changes. -// func (m *Message) IsService() bool { fact := false @@ -386,7 +400,6 @@ func (m *Message) IsService() bool { // // It's safer than manually slicing Text because Telegram uses // UTF-16 indices whereas Go string are []byte. -// func (m *Message) EntityText(e MessageEntity) string { text := m.Text if text == "" { diff --git a/options.go b/options.go index 2aa2805..d235239 100644 --- a/options.go +++ b/options.go @@ -11,7 +11,6 @@ import ( // flags instead. // // Supported options are defined as iota-constants. -// type Option int const ( @@ -54,7 +53,6 @@ func Placeholder(text string) *SendOptions { // Despite its power, SendOptions is rather inconvenient to use all // the way through bot logic, so you might want to consider storing // and re-using it somewhere or be using Option flags instead. -// type SendOptions struct { // If the message is a reply, original message. ReplyTo *Message @@ -79,6 +77,9 @@ type SendOptions struct { // Protected protects the contents of the sent message from forwarding and saving Protected bool + + // MessageThreadID supports sending messages to a thread. + MessageThreadID int } func (og *SendOptions) copy() *SendOptions { @@ -189,6 +190,10 @@ func (b *Bot) embedSendOptions(params map[string]string, opt *SendOptions) { if opt.Protected { params["protect_content"] = "true" } + + if opt.MessageThreadID != 0 { + params["message_thread_id"] = strconv.Itoa(opt.MessageThreadID) + } } func processButtons(keys [][]InlineButton) { diff --git a/sendable.go b/sendable.go index 2e782e3..ecaae7f 100644 --- a/sendable.go +++ b/sendable.go @@ -18,7 +18,6 @@ type Recipient interface { // This is pretty cool, since it lets bots implement // custom Sendables for complex kind of media or // chat objects spanning across multiple messages. -// type Sendable interface { Send(*Bot, Recipient, *SendOptions) (*Message, error) } diff --git a/telebot.go b/telebot.go index 2aa1cd9..37277be 100644 --- a/telebot.go +++ b/telebot.go @@ -2,29 +2,28 @@ // // Example: // -// package main +// package main // -// import ( -// "time" -// tele "gopkg.in/telebot.v3" -// ) +// import ( +// "time" +// tele "gopkg.in/telebot.v3" +// ) // -// func main() { -// b, err := tele.NewBot(tele.Settings{ -// Token: "...", -// Poller: &tele.LongPoller{Timeout: 10 * time.Second}, -// }) -// if err != nil { -// return -// } -// -// b.Handle("/start", func(c tele.Context) error { -// return c.Send("Hello world!") -// }) -// -// b.Start() +// func main() { +// b, err := tele.NewBot(tele.Settings{ +// Token: "...", +// Poller: &tele.LongPoller{Timeout: 10 * time.Second}, +// }) +// if err != nil { +// return // } // +// b.Handle("/start", func(c tele.Context) error { +// return c.Send("Hello world!") +// }) +// +// b.Start() +// } package telebot import "errors" @@ -43,7 +42,6 @@ const DefaultApiURL = "https://api.telegram.org" // // For convenience, all Telebot-provided endpoints start with // an "alert" character \a. -// const ( // Basic message handlers. OnText = "\atext" @@ -68,6 +66,9 @@ const ( OnPinned = "\apinned" OnChannelPost = "\achannel_post" OnEditedChannelPost = "\aedited_channel_post" + OnTopicCreated = "\atopic_created" + OnTopicReopened = "\atopic_reopened" + OnTopicClosed = "\atopic_closed" OnAddedToGroup = "\aadded_to_group" OnUserJoined = "\auser_joined" diff --git a/topic.go b/topic.go new file mode 100644 index 0000000..e72ea87 --- /dev/null +++ b/topic.go @@ -0,0 +1,126 @@ +package telebot + +import ( + "encoding/json" + "strconv" +) + +type Topic struct { + Name string `json:"name"` + IconColor int `json:"icon_color"` + IconCustomEmojiID string `json:"icon_custom_emoji_id"` + MessageThreadID int `json:"message_thread_id"` +} + +type TopicCreated struct { + Topic +} + +type TopicClosed struct{} + +type TopicDeleted struct { + Name string `json:"name"` + IconCustomEmojiID string `json:"icon_custom_emoji_id"` +} + +type TopicReopened struct { + Topic +} + +// CreateTopic creates a topic in a forum supergroup chat. +func (b *Bot) CreateTopic(chat *Chat, forum *Topic) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "name": forum.Name, + } + + if forum.IconColor != 0 { + params["icon_color"] = strconv.Itoa(forum.IconColor) + } + if forum.IconCustomEmojiID != "" { + params["icon_custom_emoji_id"] = forum.IconCustomEmojiID + } + + _, err := b.Raw("createForumTopic", params) + return err +} + +// EditTopic edits name and icon of a topic in a forum supergroup chat. +func (b *Bot) EditTopic(chat *Chat, forum *Topic) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "message_thread_id": forum.MessageThreadID, + } + + if forum.Name != "" { + params["name"] = forum.Name + } + if forum.IconCustomEmojiID != "" { + params["icon_custom_emoji_id"] = forum.IconCustomEmojiID + } + + _, err := b.Raw("editForumTopic", params) + return err +} + +// CloseTopic closes an open topic in a forum supergroup chat. +func (b *Bot) CloseTopic(chat *Chat, forum *Topic) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "message_thread_id": forum.MessageThreadID, + } + + _, err := b.Raw("closeForumTopic", params) + return err +} + +// ReopenTopic reopens a closed topic in a forum supergroup chat. +func (b *Bot) ReopenTopic(chat *Chat, forum *Topic) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "message_thread_id": forum.MessageThreadID, + } + + _, err := b.Raw("reopenForumTopic", params) + return err +} + +// DeleteTopic deletes a forum topic along with all its messages in a forum supergroup chat. +func (b *Bot) DeleteTopic(chat *Chat, forum *Topic) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "message_thread_id": forum.MessageThreadID, + } + + _, err := b.Raw("deleteForumTopic", params) + return err +} + +// UnpinAllTopicMessages clears the list of pinned messages in a forum topic. The bot must be an administrator in the chat for this to work and must have the can_pin_messages administrator right in the supergroup. +func (b *Bot) UnpinAllTopicMessages(chat *Chat, forum *Topic) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "message_thread_id": forum.MessageThreadID, + } + + _, err := b.Raw("unpinAllForumTopicMessages", params) + return err +} + +// TopicIconStickers gets custom emoji stickers, which can be used as a forum topic icon by any user +func (b *Bot) TopicIconStickers() ([]Sticker, error) { + params := map[string]string{} + + data, err := b.Raw("getForumTopicIconStickers", params) + if err != nil { + return nil, err + } + + var resp struct { + Result []Sticker + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} diff --git a/update.go b/update.go index 76da7cd..70e0da9 100644 --- a/update.go +++ b/update.go @@ -98,6 +98,18 @@ func (b *Bot) ProcessUpdate(u Update) { b.handle(OnPayment, c) return } + if m.TopicClosed != nil { + b.handle(OnTopicCreated, c) + return + } + if m.TopicReopened != nil { + b.handle(OnTopicReopened, c) + return + } + if m.TopicClosed != nil { + b.handle(OnTopicClosed, c) + return + } wasAdded := (m.UserJoined != nil && m.UserJoined.ID == b.Me.ID) || (m.UsersJoined != nil && isUserInList(b.Me, m.UsersJoined))