mirror of https://github.com/tucnak/telebot
Compare commits
92 Commits
Author | SHA1 | Date |
---|---|---|
Demian | 4d9811176e | 1 week ago |
Demian | cc61a965b4 | 2 months ago |
Demian | 7ee7bd3a21 | 2 months ago |
demget | 898c3e18f9 | 2 months ago |
Nikolay Pavlovich | f1007b7c08 | 2 months ago |
Demian | 4e8d423603 | 2 months ago |
Олегсей | 0442831ecc | 2 months ago |
Demian | 7655e7a9ca | 2 months ago |
Demian | 9c7a18be08 | 2 months ago |
Олегсей | 39d57c70bd | 2 months ago |
Demian | a02de5490f | 2 months ago |
Megum1n | 08f2e85c39 | 2 months ago |
zhongqi | b19cf78b1f | 2 months ago |
Олегсей | e8d6ed91d9 | 2 months ago |
David Shekunts | 14ff5ae5d9 | 2 months ago |
JunJi | e90e62d100 | 2 months ago |
mhrlife | 226075b9d0 | 2 months ago |
Asadbek | 8dd54ae1f6 | 2 months ago |
Taras | f1b0fab4df | 2 months ago |
Maxim Nurmukhametov | af64372b8e | 2 months ago |
Demian | 8b69e47820 | 2 months ago |
Demian | 6713166829 | 2 months ago |
Олегсей | 1c5b5de314 | 2 months ago |
Олегсей | d67653533a | 2 months ago |
Demian | c8882690a2 | 2 months ago |
Олегсей | c37b2b97b3 | 2 months ago |
Demian | ca0a8527e0 | 2 months ago |
Олегсей | ffbf081045 | 2 months ago |
Demian | 864bef4e4d | 2 months ago |
Demian | 2c98c59344 | 2 months ago |
Demian | a8acf84301 | 4 months ago |
Demian | 10fbde9b66 | 4 months ago |
Demian | 1acc928a92 | 4 months ago |
Demian | c9cec214a9 | 4 months ago |
Demian | c4fce10013 | 4 months ago |
Demian | 47227931ec | 4 months ago |
Nash-Well | e0c24df69d | 4 months ago |
Nash-Well | 77f77a6840 | 4 months ago |
Demian | a26ba9f0bd | 6 months ago |
Demian | af50a953b5 | 6 months ago |
nash | e16cc083f7 | 7 months ago |
Demian | 35d5dd372a | 7 months ago |
Demian | 9eb53434a0 | 7 months ago |
Nash-Well | 6fdf666a11 | 7 months ago |
Nikita | b67df6444e | 7 months ago |
Nikita | 260a67b811 | 7 months ago |
Nikita | cb88a11861 | 7 months ago |
Demian | 0d0d5e7473 | 7 months ago |
Patrick | e4f891ba6c | 7 months ago |
Nash-Well | af31945794 | 7 months ago |
Nash-Well | 5ff8b89b2b | 7 months ago |
Nikita | df250e64a2 | 7 months ago |
Nash-Well | 4670ccf5a3 | 7 months ago |
Demian | 435ad606fa | 7 months ago |
Nash-Well | 32b47a49ef | 7 months ago |
Reza Alipour | 9c4cc7713e | 7 months ago |
Reza Alipour | 49db4f5fac | 7 months ago |
70sh1 | 06bef0a71e | 7 months ago |
落心 | 59775c24a0 | 7 months ago |
david | 4cce303fbc | 7 months ago |
Demian | 71ac2995cc | 10 months ago |
Demian | b1cf07b13d | 11 months ago |
Demian | 10d3480782 | 11 months ago |
Demian | 06bbf9e69f | 11 months ago |
Demian | 7b08a8f939 | 11 months ago |
Mikhail | e6590d806a | 11 months ago |
Demian | 78610849e1 | 11 months ago |
Demian | cd009a68b3 | 11 months ago |
Vlad Lukyanov | 270e53bb90 | 11 months ago |
Demian | 76f1c0eef8 | 11 months ago |
awohsen | 4e31cc6eac | 11 months ago |
George | 3c4d8425f4 | 11 months ago |
Demian | c581e8389b | 11 months ago |
Nikita | fb8ce2a4b1 | 11 months ago |
Demian | 03dcac73d8 | 11 months ago |
Demian | d883371ded | 11 months ago |
Nikita | 5d4079a489 | 11 months ago |
demget | 2f38e4952f | 11 months ago |
Demian | 5f0d89877d | 11 months ago |
Demian | 48c941a8d9 | 11 months ago |
demget | bb76a9a642 | 11 months ago |
demget | 699e5dbbdd | 1 year ago |
demget | de01d4553e | 1 year ago |
Akionka | eb290fd947 | 1 year ago |
Max Kuznetsov | bae47b52d4 | 2 years ago |
demget | fbd35f2103 | 2 years ago |
Demian | 92739f1414 | 2 years ago |
demget | 8a14d8a96b | 2 years ago |
Ruslan | 0b9f842a27 | 2 years ago |
Demian | ad84cbde5a | 2 years ago |
Demian | 4c17ca7dc7 | 2 years ago |
Alex Osinniy | c2b50e878f | 2 years ago |
@ -1,331 +1,118 @@
|
||||
package telebot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Raw lets you call any method of Bot API manually.
|
||||
// It also handles API errors, so you only need to unwrap
|
||||
// result field from json data.
|
||||
func (b *Bot) Raw(method string, payload interface{}) ([]byte, error) {
|
||||
url := b.URL + "/bot" + b.Token + "/" + method
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cancel the request immediately without waiting for the timeout when bot is about to stop.
|
||||
// This may become important if doing long polling with long timeout.
|
||||
exit := make(chan struct{})
|
||||
defer close(exit)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-b.stopClient:
|
||||
cancel()
|
||||
case <-exit:
|
||||
}
|
||||
}()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf)
|
||||
if err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := b.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
resp.Close = true
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
|
||||
if b.verbose {
|
||||
verbose(method, payload, data)
|
||||
}
|
||||
|
||||
// returning data as well
|
||||
return data, extractOk(data)
|
||||
}
|
||||
|
||||
func (b *Bot) sendFiles(method string, files map[string]File, params map[string]string) ([]byte, error) {
|
||||
rawFiles := make(map[string]interface{})
|
||||
for name, f := range files {
|
||||
switch {
|
||||
case f.InCloud():
|
||||
params[name] = f.FileID
|
||||
case f.FileURL != "":
|
||||
params[name] = f.FileURL
|
||||
case f.OnDisk():
|
||||
rawFiles[name] = f.FileLocal
|
||||
case f.FileReader != nil:
|
||||
rawFiles[name] = f.FileReader
|
||||
default:
|
||||
return nil, fmt.Errorf("telebot: file for field %s doesn't exist", name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(rawFiles) == 0 {
|
||||
return b.Raw(method, params)
|
||||
}
|
||||
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
writer := multipart.NewWriter(pipeWriter)
|
||||
|
||||
go func() {
|
||||
defer pipeWriter.Close()
|
||||
|
||||
for field, file := range rawFiles {
|
||||
if err := addFileToWriter(writer, files[field].fileName, field, file); err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
for field, value := range params {
|
||||
if err := writer.WriteField(field, value); err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
url := b.URL + "/bot" + b.Token + "/" + method
|
||||
|
||||
resp, err := b.client.Post(url, writer.FormDataContentType(), pipeReader)
|
||||
if err != nil {
|
||||
err = wrapError(err)
|
||||
pipeReader.CloseWithError(err)
|
||||
return nil, err
|
||||
}
|
||||
resp.Close = true
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusInternalServerError {
|
||||
return nil, ErrInternal
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
|
||||
return data, extractOk(data)
|
||||
}
|
||||
|
||||
func addFileToWriter(writer *multipart.Writer, filename, field string, file interface{}) error {
|
||||
var reader io.Reader
|
||||
if r, ok := file.(io.Reader); ok {
|
||||
reader = r
|
||||
} else if path, ok := file.(string); ok {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
reader = f
|
||||
} else {
|
||||
return fmt.Errorf("telebot: file for field %v should be io.ReadCloser or string", field)
|
||||
}
|
||||
|
||||
part, err := writer.CreateFormFile(field, filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(part, reader)
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *Bot) sendText(to Recipient, text string, opt *SendOptions) (*Message, error) {
|
||||
params := map[string]string{
|
||||
"chat_id": to.Recipient(),
|
||||
"text": text,
|
||||
}
|
||||
b.embedSendOptions(params, opt)
|
||||
|
||||
data, err := b.Raw("sendMessage", params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return extractMessage(data)
|
||||
}
|
||||
|
||||
func (b *Bot) sendMedia(media Media, params map[string]string, files map[string]File) (*Message, error) {
|
||||
kind := media.MediaType()
|
||||
what := "send" + strings.Title(kind)
|
||||
|
||||
if kind == "videoNote" {
|
||||
kind = "video_note"
|
||||
}
|
||||
|
||||
sendFiles := map[string]File{kind: *media.MediaFile()}
|
||||
for k, v := range files {
|
||||
sendFiles[k] = v
|
||||
}
|
||||
|
||||
data, err := b.sendFiles(what, sendFiles, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return extractMessage(data)
|
||||
}
|
||||
|
||||
func (b *Bot) getMe() (*User, error) {
|
||||
data, err := b.Raw("getMe", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Result *User
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
func (b *Bot) getUpdates(offset, limit int, timeout time.Duration, allowed []string) ([]Update, error) {
|
||||
params := map[string]string{
|
||||
"offset": strconv.Itoa(offset),
|
||||
"timeout": strconv.Itoa(int(timeout / time.Second)),
|
||||
}
|
||||
|
||||
if limit != 0 {
|
||||
params["limit"] = strconv.Itoa(limit)
|
||||
}
|
||||
if len(allowed) > 0 {
|
||||
data, _ := json.Marshal(allowed)
|
||||
params["allowed_updates"] = string(data)
|
||||
}
|
||||
|
||||
data, err := b.Raw("getUpdates", params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Result []Update
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
// extractOk checks given result for error. If result is ok returns nil.
|
||||
// In other cases it extracts API error. If error is not presented
|
||||
// in errors.go, it will be prefixed with `unknown` keyword.
|
||||
func extractOk(data []byte) error {
|
||||
var e struct {
|
||||
Ok bool `json:"ok"`
|
||||
Code int `json:"error_code"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
}
|
||||
if json.NewDecoder(bytes.NewReader(data)).Decode(&e) != nil {
|
||||
return nil // FIXME
|
||||
}
|
||||
if e.Ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := Err(e.Description)
|
||||
switch err {
|
||||
case nil:
|
||||
case ErrGroupMigrated:
|
||||
migratedTo, ok := e.Parameters["migrate_to_chat_id"]
|
||||
if !ok {
|
||||
return NewError(e.Code, e.Description)
|
||||
}
|
||||
|
||||
return GroupError{
|
||||
err: err.(*Error),
|
||||
MigratedTo: int64(migratedTo.(float64)),
|
||||
}
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
switch e.Code {
|
||||
case http.StatusTooManyRequests:
|
||||
retryAfter, ok := e.Parameters["retry_after"]
|
||||
if !ok {
|
||||
return NewError(e.Code, e.Description)
|
||||
}
|
||||
|
||||
err = FloodError{
|
||||
err: NewError(e.Code, e.Description),
|
||||
RetryAfter: int(retryAfter.(float64)),
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("telegram: %s (%d)", e.Description, e.Code)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// extractMessage extracts common Message result from given data.
|
||||
// Should be called after extractOk or b.Raw() to handle possible errors.
|
||||
func extractMessage(data []byte) (*Message, error) {
|
||||
var resp struct {
|
||||
Result *Message
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
var resp struct {
|
||||
Result bool
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
if resp.Result {
|
||||
return nil, ErrTrueResult
|
||||
}
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
func verbose(method string, payload interface{}, data []byte) {
|
||||
body, _ := json.Marshal(payload)
|
||||
body = bytes.ReplaceAll(body, []byte(`\"`), []byte(`"`))
|
||||
body = bytes.ReplaceAll(body, []byte(`"{`), []byte(`{`))
|
||||
body = bytes.ReplaceAll(body, []byte(`}"`), []byte(`}`))
|
||||
|
||||
indent := func(b []byte) string {
|
||||
var buf bytes.Buffer
|
||||
json.Indent(&buf, b, "", " ")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"[verbose] telebot: sent request\nMethod: %v\nParams: %v\nResponse: %v",
|
||||
method, indent(body), indent(data),
|
||||
)
|
||||
import "io"
|
||||
|
||||
// API is the interface that wraps all basic methods for interacting
|
||||
// with Telegram Bot API.
|
||||
type API interface {
|
||||
Raw(method string, payload interface{}) ([]byte, error)
|
||||
|
||||
Accept(query *PreCheckoutQuery, errorMessage ...string) error
|
||||
AddStickerToSet(of Recipient, name string, sticker InputSticker) error
|
||||
AdminsOf(chat *Chat) ([]ChatMember, error)
|
||||
Answer(query *Query, resp *QueryResponse) error
|
||||
AnswerWebApp(query *Query, r Result) (*WebAppMessage, error)
|
||||
ApproveJoinRequest(chat Recipient, user *User) error
|
||||
Ban(chat *Chat, member *ChatMember, revokeMessages ...bool) error
|
||||
BanSenderChat(chat *Chat, sender Recipient) error
|
||||
BusinessConnection(id string) (*BusinessConnection, error)
|
||||
ChatByID(id int64) (*Chat, error)
|
||||
ChatByUsername(name string) (*Chat, error)
|
||||
ChatMemberOf(chat, user Recipient) (*ChatMember, error)
|
||||
Close() (bool, error)
|
||||
CloseGeneralTopic(chat *Chat) error
|
||||
CloseTopic(chat *Chat, topic *Topic) error
|
||||
Commands(opts ...interface{}) ([]Command, error)
|
||||
Copy(to Recipient, msg Editable, opts ...interface{}) (*Message, error)
|
||||
CopyMany(to Recipient, msgs []Editable, opts ...*SendOptions) ([]Message, error)
|
||||
CreateInviteLink(chat Recipient, link *ChatInviteLink) (*ChatInviteLink, error)
|
||||
CreateInvoiceLink(i Invoice) (string, error)
|
||||
CreateStickerSet(of Recipient, set *StickerSet) error
|
||||
CreateTopic(chat *Chat, topic *Topic) (*Topic, error)
|
||||
CustomEmojiStickers(ids []string) ([]Sticker, error)
|
||||
DeclineJoinRequest(chat Recipient, user *User) error
|
||||
DefaultRights(forChannels bool) (*Rights, error)
|
||||
Delete(msg Editable) error
|
||||
DeleteCommands(opts ...interface{}) error
|
||||
DeleteGroupPhoto(chat *Chat) error
|
||||
DeleteGroupStickerSet(chat *Chat) error
|
||||
DeleteMany(msgs []Editable) error
|
||||
DeleteSticker(sticker string) error
|
||||
DeleteStickerSet(name string) error
|
||||
DeleteTopic(chat *Chat, topic *Topic) error
|
||||
Download(file *File, localFilename string) error
|
||||
Edit(msg Editable, what interface{}, opts ...interface{}) (*Message, error)
|
||||
EditCaption(msg Editable, caption string, opts ...interface{}) (*Message, error)
|
||||
EditGeneralTopic(chat *Chat, topic *Topic) error
|
||||
EditInviteLink(chat Recipient, link *ChatInviteLink) (*ChatInviteLink, error)
|
||||
EditMedia(msg Editable, media Inputtable, opts ...interface{}) (*Message, error)
|
||||
EditReplyMarkup(msg Editable, markup *ReplyMarkup) (*Message, error)
|
||||
EditTopic(chat *Chat, topic *Topic) error
|
||||
File(file *File) (io.ReadCloser, error)
|
||||
FileByID(fileID string) (File, error)
|
||||
Forward(to Recipient, msg Editable, opts ...interface{}) (*Message, error)
|
||||
ForwardMany(to Recipient, msgs []Editable, opts ...*SendOptions) ([]Message, error)
|
||||
GameScores(user Recipient, msg Editable) ([]GameHighScore, error)
|
||||
HideGeneralTopic(chat *Chat) error
|
||||
InviteLink(chat *Chat) (string, error)
|
||||
Leave(chat Recipient) error
|
||||
Len(chat *Chat) (int, error)
|
||||
Logout() (bool, error)
|
||||
MenuButton(chat *User) (*MenuButton, error)
|
||||
MyDescription(language string) (*BotInfo, error)
|
||||
MyName(language string) (*BotInfo, error)
|
||||
MyShortDescription(language string) (*BotInfo, error)
|
||||
Notify(to Recipient, action ChatAction, threadID ...int) error
|
||||
Pin(msg Editable, opts ...interface{}) error
|
||||
ProfilePhotosOf(user *User) ([]Photo, error)
|
||||
Promote(chat *Chat, member *ChatMember) error
|
||||
React(to Recipient, msg Editable, r Reactions) error
|
||||
RefundStars(to Recipient, chargeID string) error
|
||||
RemoveWebhook(dropPending ...bool) error
|
||||
ReopenGeneralTopic(chat *Chat) error
|
||||
ReopenTopic(chat *Chat, topic *Topic) error
|
||||
ReplaceStickerInSet(of Recipient, stickerSet, oldSticker string, sticker InputSticker) (bool, error)
|
||||
Reply(to *Message, what interface{}, opts ...interface{}) (*Message, error)
|
||||
Respond(c *Callback, resp ...*CallbackResponse) error
|
||||
Restrict(chat *Chat, member *ChatMember) error
|
||||
RevokeInviteLink(chat Recipient, link string) (*ChatInviteLink, error)
|
||||
Send(to Recipient, what interface{}, opts ...interface{}) (*Message, error)
|
||||
SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message, error)
|
||||
SendPaid(to Recipient, stars int, a PaidAlbum, opts ...interface{}) (*Message, error)
|
||||
SetAdminTitle(chat *Chat, user *User, title string) error
|
||||
SetCommands(opts ...interface{}) error
|
||||
SetCustomEmojiStickerSetThumb(name, id string) error
|
||||
SetDefaultRights(rights Rights, forChannels bool) error
|
||||
SetGameScore(user Recipient, msg Editable, score GameHighScore) (*Message, error)
|
||||
SetGroupDescription(chat *Chat, description string) error
|
||||
SetGroupPermissions(chat *Chat, perms Rights) error
|
||||
SetGroupStickerSet(chat *Chat, setName string) error
|
||||
SetGroupTitle(chat *Chat, title string) error
|
||||
SetMenuButton(chat *User, mb interface{}) error
|
||||
SetMyDescription(desc, language string) error
|
||||
SetMyName(name, language string) error
|
||||
SetMyShortDescription(desc, language string) error
|
||||
SetStickerEmojis(sticker string, emojis []string) error
|
||||
SetStickerKeywords(sticker string, keywords []string) error
|
||||
SetStickerMaskPosition(sticker string, mask MaskPosition) error
|
||||
SetStickerPosition(sticker string, position int) error
|
||||
SetStickerSetThumb(of Recipient, set *StickerSet) error
|
||||
SetStickerSetTitle(s StickerSet) error
|
||||
SetWebhook(w *Webhook) error
|
||||
Ship(query *ShippingQuery, what ...interface{}) error
|
||||
StarTransactions(offset, limit int) ([]StarTransaction, error)
|
||||
StickerSet(name string) (*StickerSet, error)
|
||||
StopLiveLocation(msg Editable, opts ...interface{}) (*Message, error)
|
||||
StopPoll(msg Editable, opts ...interface{}) (*Poll, error)
|
||||
TopicIconStickers() ([]Sticker, error)
|
||||
Unban(chat *Chat, user *User, forBanned ...bool) error
|
||||
UnbanSenderChat(chat *Chat, sender Recipient) error
|
||||
UnhideGeneralTopic(chat *Chat) error
|
||||
Unpin(chat Recipient, messageID ...int) error
|
||||
UnpinAll(chat Recipient) error
|
||||
UnpinAllGeneralTopicMessages(chat *Chat) error
|
||||
UnpinAllTopicMessages(chat *Chat, topic *Topic) error
|
||||
UploadSticker(to Recipient, format StickerSetFormat, f File) (*File, error)
|
||||
UserBoosts(chat, user Recipient) ([]Boost, error)
|
||||
Webhook() (*Webhook, error)
|
||||
}
|
||||
|
@ -0,0 +1,113 @@
|
||||
package telebot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Boost contains information about a chat boost.
|
||||
type Boost struct {
|
||||
// Unique identifier of the boost.
|
||||
ID string `json:"boost_id"`
|
||||
|
||||
// Point in time (Unix timestamp) when the chat was boosted.
|
||||
AddUnixtime int64 `json:"add_date"`
|
||||
|
||||
// Point in time (Unix timestamp) when the boost will automatically expire,
|
||||
// unless the booster's Telegram Premium subscription is prolonged.
|
||||
ExpirationUnixtime int64 `json:"expiration_date"`
|
||||
|
||||
// Source of the added boost.
|
||||
Source *BoostSource `json:"source"`
|
||||
}
|
||||
|
||||
// AddDate returns the moment of time when the chat has been boosted in local time.
|
||||
func (c *Boost) AddDate() time.Time {
|
||||
return time.Unix(c.AddUnixtime, 0)
|
||||
}
|
||||
|
||||
// ExpirationDate returns the moment of time when the boost of the channel
|
||||
// will expire in local time.
|
||||
func (c *Boost) ExpirationDate() time.Time {
|
||||
return time.Unix(c.ExpirationUnixtime, 0)
|
||||
}
|
||||
|
||||
// BoostSourceType describes a type of boost.
|
||||
type BoostSourceType = string
|
||||
|
||||
const (
|
||||
BoostPremium BoostSourceType = "premium"
|
||||
BoostGiftCode BoostSourceType = "gift_code"
|
||||
BoostGiveaway BoostSourceType = "giveaway"
|
||||
)
|
||||
|
||||
// BoostSource describes the source of a chat boost.
|
||||
type BoostSource struct {
|
||||
// Source of the boost, always (“premium”, “gift_code”, “giveaway”).
|
||||
Source BoostSourceType `json:"source"`
|
||||
|
||||
// User that boosted the chat.
|
||||
Booster *User `json:"user"`
|
||||
|
||||
// Identifier of a message in the chat with the giveaway; the message
|
||||
// could have been deleted already. May be 0 if the message isn't sent yet.
|
||||
GiveawayMessageID int `json:"giveaway_message_id,omitempty"`
|
||||
|
||||
// (Optional) True, if the giveaway was completed, but there was
|
||||
// no user to win the prize.
|
||||
Unclaimed bool `json:"is_unclaimed,omitempty"`
|
||||
}
|
||||
|
||||
// BoostAdded represents a service message about a user boosting a chat.
|
||||
type BoostAdded struct {
|
||||
// Number of boosts added by the user.
|
||||
Count int `json:"boost_count"`
|
||||
}
|
||||
|
||||
// BoostUpdated represents a boost added to a chat or changed.
|
||||
type BoostUpdated struct {
|
||||
// Chat which was boosted.
|
||||
Chat *Chat `json:"chat"`
|
||||
|
||||
// Information about the chat boost.
|
||||
Boost *Boost `json:"boost"`
|
||||
}
|
||||
|
||||
// BoostRemoved represents a boost removed from a chat.
|
||||
type BoostRemoved struct {
|
||||
// Chat which was boosted.
|
||||
Chat *Chat `json:"chat"`
|
||||
|
||||
// Unique identifier of the boost.
|
||||
BoostID string `json:"boost_id"`
|
||||
|
||||
// Point in time (Unix timestamp) when the boost was removed.
|
||||
RemoveUnixtime int64 `json:"remove_date"`
|
||||
|
||||
// Source of the removed boost.
|
||||
Source *BoostSource `json:"source"`
|
||||
}
|
||||
|
||||
// UserBoosts gets the list of boosts added to a chat by a user.
|
||||
// Requires administrator rights in the chat.
|
||||
func (b *Bot) UserBoosts(chat, user Recipient) ([]Boost, error) {
|
||||
params := map[string]string{
|
||||
"chat_id": chat.Recipient(),
|
||||
"user_id": user.Recipient(),
|
||||
}
|
||||
|
||||
data, err := b.Raw("getUserChatBoosts", params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Result struct {
|
||||
Boosts []Boost `json:"boosts"`
|
||||
}
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
return resp.Result.Boosts, nil
|
||||
}
|
@ -0,0 +1,377 @@
|
||||
package telebot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Raw lets you call any method of Bot API manually.
|
||||
// It also handles API errors, so you only need to unwrap
|
||||
// result field from json data.
|
||||
func (b *Bot) Raw(method string, payload interface{}) ([]byte, error) {
|
||||
url := b.URL + "/bot" + b.Token + "/" + method
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cancel the request immediately without waiting for the timeout
|
||||
// when bot is about to stop.
|
||||
// This may become important if doing long polling with long timeout.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
b.stopMu.RLock()
|
||||
stopCh := b.stopClient
|
||||
b.stopMu.RUnlock()
|
||||
|
||||
select {
|
||||
case <-stopCh:
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, &buf)
|
||||
if err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := b.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
resp.Close = true
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
|
||||
if b.verbose {
|
||||
verbose(method, payload, data)
|
||||
}
|
||||
|
||||
// returning data as well
|
||||
return data, extractOk(data)
|
||||
}
|
||||
|
||||
func (b *Bot) sendFiles(method string, files map[string]File, params map[string]string) ([]byte, error) {
|
||||
rawFiles := make(map[string]interface{})
|
||||
for name, f := range files {
|
||||
switch {
|
||||
case f.InCloud():
|
||||
params[name] = f.FileID
|
||||
case f.FileURL != "":
|
||||
params[name] = f.FileURL
|
||||
case f.OnDisk():
|
||||
rawFiles[name] = f.FileLocal
|
||||
case f.FileReader != nil:
|
||||
rawFiles[name] = f.FileReader
|
||||
default:
|
||||
return nil, fmt.Errorf("telebot: file for field %s doesn't exist", name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(rawFiles) == 0 {
|
||||
return b.Raw(method, params)
|
||||
}
|
||||
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
writer := multipart.NewWriter(pipeWriter)
|
||||
|
||||
go func() {
|
||||
defer pipeWriter.Close()
|
||||
|
||||
for field, file := range rawFiles {
|
||||
if err := addFileToWriter(writer, files[field].fileName, field, file); err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
for field, value := range params {
|
||||
if err := writer.WriteField(field, value); err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
url := b.URL + "/bot" + b.Token + "/" + method
|
||||
|
||||
resp, err := b.client.Post(url, writer.FormDataContentType(), pipeReader)
|
||||
if err != nil {
|
||||
err = wrapError(err)
|
||||
pipeReader.CloseWithError(err)
|
||||
return nil, err
|
||||
}
|
||||
resp.Close = true
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusInternalServerError {
|
||||
return nil, ErrInternal
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
|
||||
return data, extractOk(data)
|
||||
}
|
||||
|
||||
func addFileToWriter(writer *multipart.Writer, filename, field string, file interface{}) error {
|
||||
var reader io.Reader
|
||||
if r, ok := file.(io.Reader); ok {
|
||||
reader = r
|
||||
} else if path, ok := file.(string); ok {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
reader = f
|
||||
} else {
|
||||
return fmt.Errorf("telebot: file for field %v should be io.ReadCloser or string", field)
|
||||
}
|
||||
|
||||
part, err := writer.CreateFormFile(field, filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(part, reader)
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *File) process(name string, files map[string]File) string {
|
||||
switch {
|
||||
case f.InCloud():
|
||||
return f.FileID
|
||||
case f.FileURL != "":
|
||||
return f.FileURL
|
||||
case f.OnDisk() || f.FileReader != nil:
|
||||
files[name] = *f
|
||||
return "attach://" + name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *Bot) sendText(to Recipient, text string, opt *SendOptions) (*Message, error) {
|
||||
params := map[string]string{
|
||||
"chat_id": to.Recipient(),
|
||||
"text": text,
|
||||
}
|
||||
b.embedSendOptions(params, opt)
|
||||
|
||||
data, err := b.Raw("sendMessage", params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return extractMessage(data)
|
||||
}
|
||||
|
||||
func (b *Bot) sendMedia(media Media, params map[string]string, files map[string]File) (*Message, error) {
|
||||
kind := media.MediaType()
|
||||
what := "send" + strings.Title(kind)
|
||||
|
||||
if kind == "videoNote" {
|
||||
kind = "video_note"
|
||||
}
|
||||
|
||||
sendFiles := map[string]File{kind: *media.MediaFile()}
|
||||
for k, v := range files {
|
||||
sendFiles[k] = v
|
||||
}
|
||||
|
||||
data, err := b.sendFiles(what, sendFiles, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return extractMessage(data)
|
||||
}
|
||||
|
||||
func (b *Bot) getMe() (*User, error) {
|
||||
data, err := b.Raw("getMe", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Result *User
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
func (b *Bot) getUpdates(offset, limit int, timeout time.Duration, allowed []string) ([]Update, error) {
|
||||
params := map[string]string{
|
||||
"offset": strconv.Itoa(offset),
|
||||
"timeout": strconv.Itoa(int(timeout / time.Second)),
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(allowed)
|
||||
params["allowed_updates"] = string(data)
|
||||
|
||||
if limit != 0 {
|
||||
params["limit"] = strconv.Itoa(limit)
|
||||
}
|
||||
|
||||
data, err := b.Raw("getUpdates", params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Result []Update
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
func (b *Bot) forwardCopyMany(to Recipient, msgs []Editable, key string, opts ...*SendOptions) ([]Message, error) {
|
||||
params := map[string]string{
|
||||
"chat_id": to.Recipient(),
|
||||
}
|
||||
|
||||
embedMessages(params, msgs)
|
||||
|
||||
if len(opts) > 0 {
|
||||
b.embedSendOptions(params, opts[0])
|
||||
}
|
||||
|
||||
data, err := b.Raw(key, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Result []Message
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
var resp struct {
|
||||
Result bool
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
// extractOk checks given result for error. If result is ok returns nil.
|
||||
// In other cases it extracts API error. If error is not presented
|
||||
// in errors.go, it will be prefixed with `unknown` keyword.
|
||||
func extractOk(data []byte) error {
|
||||
var e struct {
|
||||
Ok bool `json:"ok"`
|
||||
Code int `json:"error_code"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
}
|
||||
if json.NewDecoder(bytes.NewReader(data)).Decode(&e) != nil {
|
||||
return nil // FIXME
|
||||
}
|
||||
if e.Ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := Err(e.Description)
|
||||
switch err {
|
||||
case nil:
|
||||
case ErrGroupMigrated:
|
||||
migratedTo, ok := e.Parameters["migrate_to_chat_id"]
|
||||
if !ok {
|
||||
return NewError(e.Code, e.Description)
|
||||
}
|
||||
|
||||
return GroupError{
|
||||
err: err.(*Error),
|
||||
MigratedTo: int64(migratedTo.(float64)),
|
||||
}
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
switch e.Code {
|
||||
case http.StatusTooManyRequests:
|
||||
retryAfter, ok := e.Parameters["retry_after"]
|
||||
if !ok {
|
||||
return NewError(e.Code, e.Description)
|
||||
}
|
||||
|
||||
err = FloodError{
|
||||
err: NewError(e.Code, e.Description),
|
||||
RetryAfter: int(retryAfter.(float64)),
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("telegram: %s (%d)", e.Description, e.Code)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// extractMessage extracts common Message result from given data.
|
||||
// Should be called after extractOk or b.Raw() to handle possible errors.
|
||||
func extractMessage(data []byte) (*Message, error) {
|
||||
var resp struct {
|
||||
Result *Message
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
var resp struct {
|
||||
Result bool
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
if resp.Result {
|
||||
return nil, ErrTrueResult
|
||||
}
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
func verbose(method string, payload interface{}, data []byte) {
|
||||
body, _ := json.Marshal(payload)
|
||||
body = bytes.ReplaceAll(body, []byte(`\"`), []byte(`"`))
|
||||
body = bytes.ReplaceAll(body, []byte(`"{`), []byte(`{`))
|
||||
body = bytes.ReplaceAll(body, []byte(`}"`), []byte(`}`))
|
||||
|
||||
indent := func(b []byte) string {
|
||||
var buf bytes.Buffer
|
||||
json.Indent(&buf, b, "", " ")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"[verbose] telebot: sent request\nMethod: %v\nParams: %v\nResponse: %v",
|
||||
method, indent(body), indent(data),
|
||||
)
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
package telebot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BusinessConnection struct {
|
||||
// Unique identifier of the business connection
|
||||
ID string `json:"id"`
|
||||
|
||||
// Business account user that created the business connection
|
||||
Sender *User `json:"user"`
|
||||
|
||||
// Identifier of a private chat with the user who created the business connection. This
|
||||
// number may have more than 32 significant bits and some programming languages may
|
||||
// have difficulty/silent defects in interpreting it. But it has at most 52 significant bits,
|
||||
// so a 64-bit integer or double-precision float type are safe for storing this identifier.
|
||||
UserChatID int64 `json:"user_chat_id"`
|
||||
|
||||
// Unixtime, use BusinessConnection.Time() to get time.Time.
|
||||
Unixtime int64 `json:"date"`
|
||||
|
||||
// True, if the bot can act on behalf of the business account in chats that were active in the last 24 hours
|
||||
CanReply bool `json:"can_reply"`
|
||||
|
||||
// True, if the connection is active
|
||||
Enabled bool `json:"is_enabled"`
|
||||
}
|
||||
|
||||
// Time returns the moment of business connection creation in local time.
|
||||
func (b *BusinessConnection) Time() time.Time {
|
||||
return time.Unix(b.Unixtime, 0)
|
||||
}
|
||||
|
||||
type BusinessMessagesDeleted struct {
|
||||
// Unique identifier of the business connection
|
||||
BusinessConnectionID string `json:"business_connection_id"`
|
||||
|
||||
// Information about a chat in the business account. The bot
|
||||
// may not have access to the chat or the corresponding user.
|
||||
Chat *Chat `json:"chat"`
|
||||
|
||||
// The list of identifiers of deleted messages in the chat of the business account
|
||||
MessageIDs []int `json:"message_ids"`
|
||||
}
|
||||
|
||||
type BusinessIntro struct {
|
||||
// (Optional)
|
||||
// Title text of the business intro
|
||||
Title string `json:"title"`
|
||||
|
||||
// Message text of the business intro
|
||||
Message string `json:"message"`
|
||||
|
||||
// Sticker of the business intro
|
||||
Sticker *Sticker `json:"sticker"`
|
||||
}
|
||||
|
||||
type BusinessLocation struct {
|
||||
// Address of the business
|
||||
Address string `json:"address"`
|
||||
|
||||
// (Optional) Location of the business
|
||||
Location *Location `json:"location"`
|
||||
}
|
||||
|
||||
type BusinessOpeningHoursInterval struct {
|
||||
// The minute's sequence number in a week, starting on Monday,
|
||||
// marking the start of the time interval during which the business
|
||||
// is open; 0 - 7 * 24 * 60
|
||||
OpeningMinute int `json:"opening_minute"`
|
||||
|
||||
// The minute's sequence number in a week, starting on Monday,
|
||||
// marking the start of the time interval during which the business
|
||||
// is open; 0 - 7 * 24 * 60
|
||||
ClosingMinute int `json:"closing_minute"`
|
||||
}
|
||||
|
||||
type BusinessOpeningHours struct {
|
||||
// Unique name of the time zone for which the opening hours are defined
|
||||
Timezone string `json:"time_zone_name"`
|
||||
|
||||
// List of time intervals describing business opening hours
|
||||
OpeningHours []BusinessOpeningHoursInterval `json:"opening_hours"`
|
||||
}
|
||||
|
||||
// BusinessConnection returns the information about the connection of the bot with a business account.
|
||||
func (b *Bot) BusinessConnection(id string) (*BusinessConnection, error) {
|
||||
params := map[string]string{
|
||||
"business_connection_id": id,
|
||||
}
|
||||
|
||||
data, err := b.Raw("getBusinessConnection", params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Result *BusinessConnection
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package telebot
|
||||
|
||||
import "time"
|
||||
|
||||
// Giveaway represents a message about a scheduled giveaway.
|
||||
type Giveaway struct {
|
||||
// The list of chats which the user must join to participate in the giveaway.
|
||||
Chats []Chat `json:"chats"`
|
||||
|
||||
// Point in time (Unix timestamp) when winners of the giveaway will be selected.
|
||||
SelectionUnixtime int64 `json:"winners_selection_date"`
|
||||
|
||||
// The number of users which are supposed to be selected as winners of the giveaway.
|
||||
WinnerCount int `json:"winner_count"`
|
||||
|
||||
// (Optional) True, if only users who join the chats after the giveaway
|
||||
// started should be eligible to win.
|
||||
OnlyNewMembers bool `json:"only_new_members"`
|
||||
|
||||
// (Optional) True, if the list of giveaway winners will be visible to everyone.
|
||||
HasPublicWinners bool `json:"has_public_winners"`
|
||||
|
||||
// (Optional) Description of additional giveaway prize.
|
||||
PrizeDescription string `json:"prize_description"`
|
||||
|
||||
// (Optional) A list of two-letter ISO 3166-1 alpha-2 country codes indicating
|
||||
// the countries from which eligible users for the giveaway must come.
|
||||
// If empty, then all users can participate in the giveaway. Users with a phone number
|
||||
// that was bought on Fragment can always participate in giveaways.
|
||||
CountryCodes []string `json:"country_codes"`
|
||||
|
||||
// (Optional) The number of months the Telegram Premium subscription won from
|
||||
// the giveaway will be active for.
|
||||
PremiumMonthCount int `json:"premium_subscription_month_count"`
|
||||
}
|
||||
|
||||
// SelectionDate returns the moment of when winners of the giveaway were selected in local time.
|
||||
func (g *Giveaway) SelectionDate() time.Time {
|
||||
return time.Unix(g.SelectionUnixtime, 0)
|
||||
}
|
||||
|
||||
// GiveawayWinners object represents a message about the completion of a
|
||||
// giveaway with public winners.
|
||||
type GiveawayWinners struct {
|
||||
// The chat that created the giveaway.
|
||||
Chat *Chat `json:"chat"`
|
||||
|
||||
// Identifier of the message with the giveaway in the chat.
|
||||
MessageID int `json:"message_id"`
|
||||
|
||||
// Point in time (Unix timestamp) when winners of the giveaway were selected.
|
||||
SelectionUnixtime int64 `json:"winners_selection_date"`
|
||||
|
||||
// The number of users which are supposed to be selected as winners of the giveaway.
|
||||
WinnerCount int `json:"winner_count"`
|
||||
|
||||
// List of up to 100 winners of the giveaway.
|
||||
Winners []User `json:"winners"`
|
||||
|
||||
// (Optional) The number of other chats the user had to join in order
|
||||
// to be eligible for the giveaway.
|
||||
AdditionalChats int `json:"additional_chat_count"`
|
||||
|
||||
// (Optional) The number of months the Telegram Premium subscription won from
|
||||
// the giveaway will be active for.
|
||||
PremiumMonthCount int `json:"premium_subscription_month_count"`
|
||||
|
||||
// (Optional) Number of undistributed prizes.
|
||||
UnclaimedPrizes int `json:"unclaimed_prize_count"`
|
||||
|
||||
// (Optional) True, if only users who had joined the chats after the giveaway started
|
||||
// were eligible to win.
|
||||
OnlyNewMembers bool `json:"only_new_members"`
|
||||
|
||||
// (Optional) True, if the giveaway was canceled because the payment for it was refunded.
|
||||
Refunded bool `json:"was_refunded"`
|
||||
|
||||
// (Optional) Description of additional giveaway prize.
|
||||
PrizeDescription string `json:"prize_description"`
|
||||
}
|
||||
|
||||
// SelectionDate returns the moment of when winners of the giveaway
|
||||
// were selected in local time.
|
||||
func (g *GiveawayWinners) SelectionDate() time.Time {
|
||||
return time.Unix(g.SelectionUnixtime, 0)
|
||||
}
|
||||
|
||||
// GiveawayCreated represents a service message about the creation of a scheduled giveaway.
|
||||
// Currently holds no information.
|
||||
type GiveawayCreated struct{}
|
||||
|
||||
// GiveawayCompleted represents a service message about the completion of a
|
||||
// giveaway without public winners.
|
||||
type GiveawayCompleted struct {
|
||||
// Number of winners in the giveaway.
|
||||
WinnerCount int `json:"winner_count"`
|
||||
|
||||
// (Optional) Number of undistributed prizes.
|
||||
UnclaimedPrizes int `json:"unclaimed_prize_count"`
|
||||
|
||||
// (Optional) Message with the giveaway that was completed, if it wasn't deleted.
|
||||
Message *Message `json:"giveaway_message"`
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
article_message: This is an article.
|
||||
|
||||
nested:
|
||||
example: |-
|
||||
This is {{ . }}.
|
||||
another:
|
||||
example: |-
|
||||
This is {{ . }}.
|
@ -0,0 +1,44 @@
|
||||
package telebot
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAlbumSetCaption(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
media Inputtable
|
||||
}{
|
||||
{
|
||||
name: "photo",
|
||||
media: &Photo{Caption: "wrong_caption"},
|
||||
},
|
||||
{
|
||||
name: "animation",
|
||||
media: &Animation{Caption: "wrong_caption"},
|
||||
},
|
||||
{
|
||||
name: "video",
|
||||
media: &Video{Caption: "wrong_caption"},
|
||||
},
|
||||
{
|
||||
name: "audio",
|
||||
media: &Audio{Caption: "wrong_caption"},
|
||||
},
|
||||
{
|
||||
name: "document",
|
||||
media: &Document{Caption: "wrong_caption"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var a Album
|
||||
a = append(a, tt.media)
|
||||
a = append(a, &Photo{Caption: "random_caption"})
|
||||
a.SetCaption("correct_caption")
|
||||
assert.Equal(t, "correct_caption", a[0].InputMedia().Caption)
|
||||
assert.Equal(t, "random_caption", a[1].InputMedia().Caption)
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package telebot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
const (
|
||||
ReactionTypeEmoji = "emoji"
|
||||
ReactionTypeCustomEmoji = "custom_emoji"
|
||||
)
|
||||
|
||||
// Reaction describes the type of reaction.
|
||||
// Describes an instance of ReactionTypeCustomEmoji and ReactionTypeEmoji.
|
||||
type Reaction struct {
|
||||
// Type of the reaction, always “emoji”
|
||||
Type string `json:"type"`
|
||||
|
||||
// Reaction emoji.
|
||||
Emoji string `json:"emoji,omitempty"`
|
||||
|
||||
// Custom emoji identifier.
|
||||
CustomEmojiID string `json:"custom_emoji_id,omitempty"`
|
||||
}
|
||||
|
||||
// ReactionCount represents a reaction added to a message along
|
||||
// with the number of times it was added.
|
||||
type ReactionCount struct {
|
||||
// Type of the reaction.
|
||||
Type Reaction `json:"type"`
|
||||
|
||||
// Number of times the reaction was added.
|
||||
Count int `json:"total_count"`
|
||||
}
|
||||
|
||||
// Reactions represents an object of reaction options.
|
||||
type Reactions struct {
|
||||
// List of reaction types to set on the message.
|
||||
Reactions []Reaction `json:"reaction"`
|
||||
|
||||
// Pass True to set the reaction with a big animation.
|
||||
Big bool `json:"is_big"`
|
||||
}
|
||||
|
||||
// React changes the chosen reactions on a message. Service messages can't be
|
||||
// reacted to. Automatically forwarded messages from a channel to its discussion group have
|
||||
// the same available reactions as messages in the channel.
|
||||
func (b *Bot) React(to Recipient, msg Editable, r Reactions) error {
|
||||
if to == nil {
|
||||
return ErrBadRecipient
|
||||
}
|
||||
|
||||
msgID, _ := msg.MessageSig()
|
||||
params := map[string]string{
|
||||
"chat_id": to.Recipient(),
|
||||
"message_id": msgID,
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(r.Reactions)
|
||||
params["reaction"] = string(data)
|
||||
|
||||
if r.Big {
|
||||
params["is_big"] = "true"
|
||||
}
|
||||
|
||||
_, err := b.Raw("setMessageReaction", params)
|
||||
return err
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package react
|
||||
|
||||
import (
|
||||
tele "gopkg.in/telebot.v3"
|
||||
)
|
||||
|
||||
type Reaction = tele.Reaction
|
||||
|
||||
func React(r ...Reaction) tele.Reactions {
|
||||
return tele.Reactions{Reactions: r}
|
||||
}
|
||||
|
||||
// Currently available emojis.
|
||||
var (
|
||||
ThumbUp = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👍"}
|
||||
ThumbDown = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👎"}
|
||||
Heart = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "❤"}
|
||||
Fire = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🔥"}
|
||||
HeartEyes = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😍"}
|
||||
ClappingHands = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👏"}
|
||||
GrinningFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😁"}
|
||||
ThinkingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤔"}
|
||||
ExplodingHead = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤯"}
|
||||
ScreamingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😱"}
|
||||
SwearingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤬"}
|
||||
CryingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😢"}
|
||||
PartyPopper = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🎉"}
|
||||
StarStruck = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤩"}
|
||||
VomitingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤮"}
|
||||
PileOfPoo = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "💩"}
|
||||
PrayingHands = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🙏"}
|
||||
OkHand = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👌"}
|
||||
DoveOfPeace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🕊"}
|
||||
ClownFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤡"}
|
||||
YawningFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🥱"}
|
||||
WoozyFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🥴"}
|
||||
Whale = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🐳"}
|
||||
HeartOnFire = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "❤🔥"}
|
||||
MoonFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🌚"}
|
||||
HotDog = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🌭"}
|
||||
HundredPoints = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "💯"}
|
||||
RollingOnTheFloorLaughing = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤣"}
|
||||
Lightning = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "⚡"}
|
||||
Banana = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🍌"}
|
||||
Trophy = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🏆"}
|
||||
BrokenHeart = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "💔"}
|
||||
FaceWithRaisedEyebrow = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤨"}
|
||||
NeutralFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😐"}
|
||||
Strawberry = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🍓"}
|
||||
Champagne = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🍾"}
|
||||
KissMark = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "💋"}
|
||||
MiddleFinger = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🖕"}
|
||||
EvilFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😈"}
|
||||
SleepingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😴"}
|
||||
LoudlyCryingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😭"}
|
||||
NerdFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤓"}
|
||||
Ghost = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👻"}
|
||||
Engineer = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👨💻"}
|
||||
Eyes = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👀"}
|
||||
JackOLantern = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🎃"}
|
||||
NoMonkey = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🙈"}
|
||||
SmilingFaceWithHalo = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😇"}
|
||||
FearfulFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😨"}
|
||||
Handshake = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤝"}
|
||||
WritingHand = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "✍"}
|
||||
HuggingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤗"}
|
||||
Brain = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🫡"}
|
||||
SantaClaus = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🎅"}
|
||||
ChristmasTree = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🎄"}
|
||||
Snowman = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "☃"}
|
||||
NailPolish = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "💅"}
|
||||
ZanyFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤪"}
|
||||
Moai = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🗿"}
|
||||
Cool = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🆒"}
|
||||
HeartWithArrow = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "💘"}
|
||||
HearMonkey = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🙉"}
|
||||
Unicorn = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🦄"}
|
||||
FaceBlowingKiss = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😘"}
|
||||
Pill = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "💊"}
|
||||
SpeaklessMonkey = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🙊"}
|
||||
Sunglasses = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😎"}
|
||||
AlienMonster = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "👾"}
|
||||
ManShrugging = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤷♂️"}
|
||||
PersonShrugging = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤷"}
|
||||
WomanShrugging = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "🤷♀️"}
|
||||
PoutingFace = Reaction{Type: tele.ReactionTypeEmoji, Emoji: "😡"}
|
||||
)
|
@ -0,0 +1,73 @@
|
||||
package telebot
|
||||
|
||||
import "time"
|
||||
|
||||
type TransactionType = string
|
||||
|
||||
const (
|
||||
TransactionTypeUser TransactionType = "user"
|
||||
TransactionTypeFragment TransactionType = "fragment"
|
||||
TransactionPartnerTelegramAds TransactionType = "telegram_ads"
|
||||
TransactionTypeOther TransactionType = "other"
|
||||
)
|
||||
|
||||
type RevenueState = string
|
||||
|
||||
const (
|
||||
RevenueStatePending RevenueState = "pending"
|
||||
RevenueStateSucceeded RevenueState = "succeeded"
|
||||
RevenueStateFailed RevenueState = "failed"
|
||||
)
|
||||
|
||||
type StarTransaction struct {
|
||||
// Unique identifier of the transaction. Coincides with the identifier of the
|
||||
// original transaction for refund transactions. Coincides with
|
||||
// SuccessfulPayment.telegram_payment_charge_id for successful incoming
|
||||
// payments from users.
|
||||
ID string `json:"id"`
|
||||
|
||||
// Number of Telegram Stars transferred by the transaction
|
||||
Amount int `json:"amount"`
|
||||
|
||||
// Date the transaction was created in Unix time
|
||||
Unixtime int64 `json:"date"`
|
||||
|
||||
// (Optional) Source of an incoming transaction (e.g., a user purchasing goods
|
||||
// or services, Fragment refunding a failed withdrawal). Only for incoming transactions
|
||||
Source TransactionPartner `json:"source"`
|
||||
|
||||
// (Optional) Receiver of an outgoing transaction (e.g., a user for a purchase
|
||||
// refund, Fragment for a withdrawal). Only for outgoing transactions
|
||||
Receiver TransactionPartner `json:"receiver"`
|
||||
}
|
||||
|
||||
type TransactionPartner struct {
|
||||
// Type of the state
|
||||
Type TransactionType `json:"type"`
|
||||
User *User `json:"user,omitempty"`
|
||||
Payload string `json:"invoice_payload"`
|
||||
|
||||
// (Optional) State of the transaction if the transaction is outgoing$$
|
||||
Withdrawal RevenueWithdrawal `json:"withdrawal_state,omitempty"`
|
||||
}
|
||||
|
||||
type RevenueWithdrawal struct {
|
||||
// Type of the state
|
||||
Type RevenueState `json:"type"`
|
||||
|
||||
// Date the withdrawal was completed in Unix time
|
||||
Unixtime int `json:"date,omitempty"`
|
||||
|
||||
// An HTTPS URL that can be used to see transaction details
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// Time returns the date of the transaction.
|
||||
func (c *StarTransaction) Time() time.Time {
|
||||
return time.Unix(c.Unixtime, 0)
|
||||
}
|
||||
|
||||
// Time returns the date of the withdrawal.
|
||||
func (s *RevenueWithdrawal) Time() time.Time {
|
||||
return time.Unix(int64(s.Unixtime), 0)
|
||||
}
|
@ -0,0 +1,331 @@
|
||||
package telebot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type (
|
||||
StickerSetType = string
|
||||
StickerSetFormat = string
|
||||
MaskFeature = string
|
||||
)
|
||||
|
||||
const (
|
||||
StickerRegular StickerSetType = "regular"
|
||||
StickerMask StickerSetType = "mask"
|
||||
StickerCustomEmoji StickerSetType = "custom_emoji"
|
||||
)
|
||||
|
||||
const (
|
||||
StickerStatic StickerSetFormat = "static"
|
||||
StickerAnimated StickerSetFormat = "animated"
|
||||
StickerVideo StickerSetFormat = "video"
|
||||
)
|
||||
|
||||
const (
|
||||
MaskForehead MaskFeature = "forehead"
|
||||
MaskEyes MaskFeature = "eyes"
|
||||
MaskMouth MaskFeature = "mouth"
|
||||
MaskChin MaskFeature = "chin"
|
||||
)
|
||||
|
||||
// StickerSet represents a sticker set.
|
||||
type StickerSet struct {
|
||||
Type StickerSetType `json:"sticker_type"`
|
||||
Format StickerSetFormat `json:"sticker_format"`
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Stickers []Sticker `json:"stickers"`
|
||||
Thumbnail *Photo `json:"thumbnail"`
|
||||
Emojis string `json:"emojis"`
|
||||
ContainsMasks bool `json:"contains_masks"` // FIXME: can be removed
|
||||
MaskPosition *MaskPosition `json:"mask_position"`
|
||||
Repaint bool `json:"needs_repainting"`
|
||||
|
||||
// Input is a field used in createNewStickerSet method to specify a list
|
||||
// of pre-defined stickers of type InputSticker to add to the set.
|
||||
Input []InputSticker
|
||||
}
|
||||
|
||||
type InputSticker struct {
|
||||
File
|
||||
Sticker string `json:"sticker"`
|
||||
Format string `json:"format"`
|
||||
MaskPosition *MaskPosition `json:"mask_position"`
|
||||
Emojis []string `json:"emoji_list"`
|
||||
Keywords []string `json:"keywords"`
|
||||
}
|
||||
|
||||
// MaskPosition describes the position on faces where
|
||||
// a mask should be placed by default.
|
||||
type MaskPosition struct {
|
||||
Feature MaskFeature `json:"point"`
|
||||
XShift float32 `json:"x_shift"`
|
||||
YShift float32 `json:"y_shift"`
|
||||
Scale float32 `json:"scale"`
|
||||
}
|
||||
|
||||
// UploadSticker uploads a sticker file for later use.
|
||||
func (b *Bot) UploadSticker(to Recipient, format StickerSetFormat, f File) (*File, error) {
|
||||
params := map[string]string{
|
||||
"user_id": to.Recipient(),
|
||||
"sticker_format": format,
|
||||
}
|
||||
|
||||
data, err := b.sendFiles("uploadStickerFile", map[string]File{"0": f}, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Result File
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
return &resp.Result, nil
|
||||
}
|
||||
|
||||
// StickerSet returns a sticker set on success.
|
||||
func (b *Bot) StickerSet(name string) (*StickerSet, error) {
|
||||
data, err := b.Raw("getStickerSet", map[string]string{"name": name})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Result *StickerSet
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
// CreateStickerSet creates a new sticker set.
|
||||
func (b *Bot) CreateStickerSet(of Recipient, set *StickerSet) error {
|
||||
files := make(map[string]File)
|
||||
for i, s := range set.Input {
|
||||
repr := s.File.process(strconv.Itoa(i), files)
|
||||
if repr == "" {
|
||||
return fmt.Errorf("telebot: sticker #%d does not exist", i+1)
|
||||
}
|
||||
set.Input[i].Sticker = repr
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(set.Input)
|
||||
|
||||
params := map[string]string{
|
||||
"user_id": of.Recipient(),
|
||||
"name": set.Name,
|
||||
"title": set.Title,
|
||||
"stickers": string(data),
|
||||
}
|
||||
if set.Type != "" {
|
||||
params["sticker_type"] = set.Type
|
||||
}
|
||||
if set.Repaint {
|
||||
params["needs_repainting"] = "true"
|
||||
}
|
||||
|
||||
_, err := b.sendFiles("createNewStickerSet", files, params)
|
||||
return err
|
||||
}
|
||||
|
||||
// AddStickerToSet adds a new sticker to the existing sticker set.
|
||||
func (b *Bot) AddStickerToSet(of Recipient, name string, sticker InputSticker) error {
|
||||
files := make(map[string]File)
|
||||
repr := sticker.File.process("0", files)
|
||||
if repr == "" {
|
||||
return errors.New("telebot: sticker does not exist")
|
||||
}
|
||||
|
||||
sticker.Sticker = repr
|
||||
data, _ := json.Marshal(sticker)
|
||||
|
||||
params := map[string]string{
|
||||
"user_id": of.Recipient(),
|
||||
"name": name,
|
||||
"sticker": string(data),
|
||||
}
|
||||
|
||||
_, err := b.sendFiles("addStickerToSet", files, params)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetStickerPosition moves a sticker in set to a specific position.
|
||||
func (b *Bot) SetStickerPosition(sticker string, position int) error {
|
||||
params := map[string]string{
|
||||
"sticker": sticker,
|
||||
"position": strconv.Itoa(position),
|
||||
}
|
||||
|
||||
_, err := b.Raw("setStickerPositionInSet", params)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteSticker deletes a sticker from a set created by the bot.
|
||||
func (b *Bot) DeleteSticker(sticker string) error {
|
||||
_, err := b.Raw("deleteStickerFromSet", map[string]string{"sticker": sticker})
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// SetStickerSetThumb sets a thumbnail of the sticker set.
|
||||
// Animated thumbnails can be set for animated sticker sets only.
|
||||
//
|
||||
// Thumbnail must be a PNG image, up to 128 kilobytes in size
|
||||
// and have width and height exactly 100px, or a TGS animation
|
||||
// up to 32 kilobytes in size.
|
||||
//
|
||||
// Animated sticker set thumbnail can't be uploaded via HTTP URL.
|
||||
func (b *Bot) SetStickerSetThumb(of Recipient, set *StickerSet) error {
|
||||
if set.Thumbnail == nil {
|
||||
return errors.New("telebot: thumbnail is required")
|
||||
}
|
||||
|
||||
files := make(map[string]File)
|
||||
repr := set.Thumbnail.File.process("thumb", files)
|
||||
if repr == "" {
|
||||
return errors.New("telebot: thumbnail does not exist")
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"user_id": of.Recipient(),
|
||||
"name": set.Name,
|
||||
"format": set.Format,
|
||||
"thumbnail": repr,
|
||||
}
|
||||
|
||||
_, err := b.sendFiles("setStickerSetThumbnail", files, params)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetStickerSetTitle sets the title of a created sticker set.
|
||||
func (b *Bot) SetStickerSetTitle(s StickerSet) error {
|
||||
params := map[string]string{
|
||||
"name": s.Name,
|
||||
"title": s.Title,
|
||||
}
|
||||
|
||||
_, err := b.Raw("setStickerSetTitle", params)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteStickerSet deletes a sticker set that was created by the bot.
|
||||
func (b *Bot) DeleteStickerSet(name string) error {
|
||||
params := map[string]string{"name": name}
|
||||
|
||||
_, err := b.Raw("deleteStickerSet", params)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetStickerEmojis changes the list of emoji assigned to a regular or custom emoji sticker.
|
||||
func (b *Bot) SetStickerEmojis(sticker string, emojis []string) error {
|
||||
data, err := json.Marshal(emojis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"sticker": sticker,
|
||||
"emoji_list": string(data),
|
||||
}
|
||||
|
||||
_, err = b.Raw("setStickerEmojiList", params)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetStickerKeywords changes search keywords assigned to a regular or custom emoji sticker.
|
||||
func (b *Bot) SetStickerKeywords(sticker string, keywords []string) error {
|
||||
mk, err := json.Marshal(keywords)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"sticker": sticker,
|
||||
"keywords": string(mk),
|
||||
}
|
||||
|
||||
_, err = b.Raw("setStickerKeywords", params)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetStickerMaskPosition changes the mask position of a mask sticker.
|
||||
func (b *Bot) SetStickerMaskPosition(sticker string, mask MaskPosition) error {
|
||||
data, err := json.Marshal(mask)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"sticker": sticker,
|
||||
"mask_position": string(data),
|
||||
}
|
||||
|
||||
_, err = b.Raw("setStickerMaskPosition", params)
|
||||
return err
|
||||
}
|
||||
|
||||
// CustomEmojiStickers returns the information about custom emoji stickers by their ids.
|
||||
func (b *Bot) CustomEmojiStickers(ids []string) ([]Sticker, error) {
|
||||
data, _ := json.Marshal(ids)
|
||||
|
||||
params := map[string]string{
|
||||
"custom_emoji_ids": string(data),
|
||||
}
|
||||
|
||||
data, err := b.Raw("getCustomEmojiStickers", 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
|
||||
}
|
||||
|
||||
// SetCustomEmojiStickerSetThumb sets the thumbnail of a custom emoji sticker set.
|
||||
func (b *Bot) SetCustomEmojiStickerSetThumb(name, id string) error {
|
||||
params := map[string]string{
|
||||
"name": name,
|
||||
"custom_emoji_id": id,
|
||||
}
|
||||
|
||||
_, err := b.Raw("setCustomEmojiStickerSetThumbnail", params)
|
||||
return err
|
||||
}
|
||||
|
||||
// ReplaceStickerInSet returns True on success, if existing sticker was replaced with a new one.
|
||||
func (b *Bot) ReplaceStickerInSet(of Recipient, stickerSet, oldSticker string, sticker InputSticker) (bool, error) {
|
||||
files := make(map[string]File)
|
||||
|
||||
repr := sticker.File.process("0", files)
|
||||
if repr == "" {
|
||||
return false, errors.New("telebot: sticker does not exist")
|
||||
}
|
||||
sticker.Sticker = repr
|
||||
|
||||
data, err := json.Marshal(sticker)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"user_id": of.Recipient(),
|
||||
"name": stickerSet,
|
||||
"old_sticker": oldSticker,
|
||||
"sticker": string(data),
|
||||
}
|
||||
|
||||
_, err = b.sendFiles("replaceStickerInSet", files, params)
|
||||
return true, err
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package telebot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStickerSet(t *testing.T) {
|
||||
if b == nil {
|
||||
t.Skip("Cached bot instance is bad (probably wrong or empty TELEBOT_SECRET)")
|
||||
}
|
||||
if userID == 0 {
|
||||
t.Skip("USER_ID is required for StickerSet methods test")
|
||||
}
|
||||
|
||||
input := []InputSticker{
|
||||
{
|
||||
File: FromURL("https://placehold.co/512/000000/FFFFFF/png"),
|
||||
Emojis: []string{"🤖"},
|
||||
Keywords: []string{"telebot", "robot", "bot"},
|
||||
},
|
||||
{
|
||||
File: FromURL("https://placehold.co/512/000000/999999/png"),
|
||||
Emojis: []string{"🤖"},
|
||||
Keywords: []string{"telebot", "robot", "bot"},
|
||||
},
|
||||
}
|
||||
|
||||
original := &StickerSet{
|
||||
Name: fmt.Sprintf("telebot_%d_by_%s", time.Now().Unix(), b.Me.Username),
|
||||
Type: StickerRegular,
|
||||
Format: StickerStatic,
|
||||
Title: "Telebot Stickers",
|
||||
Input: input[:1],
|
||||
}
|
||||
|
||||
// 1
|
||||
err := b.CreateStickerSet(user, original)
|
||||
require.NoError(t, err)
|
||||
// 2
|
||||
err = b.AddStickerToSet(user, original.Name, input[1])
|
||||
require.NoError(t, err)
|
||||
|
||||
original.Thumbnail = &Photo{File: thumb}
|
||||
err = b.SetStickerSetThumb(user, original)
|
||||
require.NoError(t, err)
|
||||
|
||||
set, err := b.StickerSet(original.Name)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, original.Name, set.Name)
|
||||
require.Equal(t, len(input), len(set.Stickers))
|
||||
|
||||
_, err = b.Send(user, &set.Stickers[0])
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = b.Send(user, &set.Stickers[1])
|
||||
require.NoError(t, err)
|
||||
}
|
@ -1,212 +0,0 @@
|
||||
package telebot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type StickerSetType = string
|
||||
|
||||
const (
|
||||
StickerRegular = "regular"
|
||||
StickerMask = "mask"
|
||||
StickerCustomEmoji = "custom_emoji"
|
||||
)
|
||||
|
||||
// StickerSet represents a sticker set.
|
||||
type StickerSet struct {
|
||||
Type StickerSetType `json:"sticker_type"`
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Animated bool `json:"is_animated"`
|
||||
Video bool `json:"is_video"`
|
||||
Stickers []Sticker `json:"stickers"`
|
||||
Thumbnail *Photo `json:"thumb"`
|
||||
PNG *File `json:"png_sticker"`
|
||||
TGS *File `json:"tgs_sticker"`
|
||||
WebM *File `json:"webm_sticker"`
|
||||
Emojis string `json:"emojis"`
|
||||
ContainsMasks bool `json:"contains_masks"` // FIXME: can be removed
|
||||
MaskPosition *MaskPosition `json:"mask_position"`
|
||||
}
|
||||
|
||||
// MaskPosition describes the position on faces where
|
||||
// a mask should be placed by default.
|
||||
type MaskPosition struct {
|
||||
Feature MaskFeature `json:"point"`
|
||||
XShift float32 `json:"x_shift"`
|
||||
YShift float32 `json:"y_shift"`
|
||||
Scale float32 `json:"scale"`
|
||||
}
|
||||
|
||||
// MaskFeature defines sticker mask position.
|
||||
type MaskFeature string
|
||||
|
||||
const (
|
||||
FeatureForehead MaskFeature = "forehead"
|
||||
FeatureEyes MaskFeature = "eyes"
|
||||
FeatureMouth MaskFeature = "mouth"
|
||||
FeatureChin MaskFeature = "chin"
|
||||
)
|
||||
|
||||
// UploadSticker uploads a PNG file with a sticker for later use.
|
||||
func (b *Bot) UploadSticker(to Recipient, png *File) (*File, error) {
|
||||
files := map[string]File{
|
||||
"png_sticker": *png,
|
||||
}
|
||||
params := map[string]string{
|
||||
"user_id": to.Recipient(),
|
||||
}
|
||||
|
||||
data, err := b.sendFiles("uploadStickerFile", files, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Result File
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
return &resp.Result, nil
|
||||
}
|
||||
|
||||
// StickerSet returns a sticker set on success.
|
||||
func (b *Bot) StickerSet(name string) (*StickerSet, error) {
|
||||
data, err := b.Raw("getStickerSet", map[string]string{"name": name})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Result *StickerSet
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
// CreateStickerSet creates a new sticker set.
|
||||
func (b *Bot) CreateStickerSet(to Recipient, s StickerSet) error {
|
||||
files := make(map[string]File)
|
||||
if s.PNG != nil {
|
||||
files["png_sticker"] = *s.PNG
|
||||
}
|
||||
if s.TGS != nil {
|
||||
files["tgs_sticker"] = *s.TGS
|
||||
}
|
||||
if s.WebM != nil {
|
||||
files["webm_sticker"] = *s.WebM
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"user_id": to.Recipient(),
|
||||
"sticker_type": s.Type,
|
||||
"name": s.Name,
|
||||
"title": s.Title,
|
||||
"emojis": s.Emojis,
|
||||
"contains_masks": strconv.FormatBool(s.ContainsMasks),
|
||||
}
|
||||
|
||||
if s.MaskPosition != nil {
|
||||
data, _ := json.Marshal(&s.MaskPosition)
|
||||
params["mask_position"] = string(data)
|
||||
}
|
||||
|
||||
_, err := b.sendFiles("createNewStickerSet", files, params)
|
||||
return err
|
||||
}
|
||||
|
||||
// AddSticker adds a new sticker to the existing sticker set.
|
||||
func (b *Bot) AddSticker(to Recipient, s StickerSet) error {
|
||||
files := make(map[string]File)
|
||||
if s.PNG != nil {
|
||||
files["png_sticker"] = *s.PNG
|
||||
} else if s.TGS != nil {
|
||||
files["tgs_sticker"] = *s.TGS
|
||||
} else if s.WebM != nil {
|
||||
files["webm_sticker"] = *s.WebM
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"user_id": to.Recipient(),
|
||||
"name": s.Name,
|
||||
"emojis": s.Emojis,
|
||||
}
|
||||
|
||||
if s.MaskPosition != nil {
|
||||
data, _ := json.Marshal(&s.MaskPosition)
|
||||
params["mask_position"] = string(data)
|
||||
}
|
||||
|
||||
_, err := b.sendFiles("addStickerToSet", files, params)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetStickerPosition moves a sticker in set to a specific position.
|
||||
func (b *Bot) SetStickerPosition(sticker string, position int) error {
|
||||
params := map[string]string{
|
||||
"sticker": sticker,
|
||||
"position": strconv.Itoa(position),
|
||||
}
|
||||
|
||||
_, err := b.Raw("setStickerPositionInSet", params)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteSticker deletes a sticker from a set created by the bot.
|
||||
func (b *Bot) DeleteSticker(sticker string) error {
|
||||
_, err := b.Raw("deleteStickerFromSet", map[string]string{"sticker": sticker})
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// SetStickerSetThumb sets a thumbnail of the sticker set.
|
||||
// Animated thumbnails can be set for animated sticker sets only.
|
||||
//
|
||||
// Thumbnail must be a PNG image, up to 128 kilobytes in size
|
||||
// and have width and height exactly 100px, or a TGS animation
|
||||
// up to 32 kilobytes in size.
|
||||
//
|
||||
// Animated sticker set thumbnail can't be uploaded via HTTP URL.
|
||||
//
|
||||
func (b *Bot) SetStickerSetThumb(to Recipient, s StickerSet) error {
|
||||
files := make(map[string]File)
|
||||
if s.PNG != nil {
|
||||
files["thumb"] = *s.PNG
|
||||
} else if s.TGS != nil {
|
||||
files["thumb"] = *s.TGS
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"name": s.Name,
|
||||
"user_id": to.Recipient(),
|
||||
}
|
||||
|
||||
_, err := b.sendFiles("setStickerSetThumb", files, params)
|
||||
return err
|
||||
}
|
||||
|
||||
// CustomEmojiStickers returns the information about custom emoji stickers by their ids.
|
||||
func (b *Bot) CustomEmojiStickers(ids []string) ([]Sticker, error) {
|
||||
data, _ := json.Marshal(ids)
|
||||
|
||||
params := map[string]string{
|
||||
"custom_emoji_ids": string(data),
|
||||
}
|
||||
|
||||
data, err := b.Raw("getCustomEmojiStickers", 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
|
||||
}
|
@ -0,0 +1,184 @@
|
||||
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"`
|
||||
ThreadID int `json:"message_thread_id"`
|
||||
}
|
||||
|
||||
// CreateTopic creates a topic in a forum supergroup chat.
|
||||
func (b *Bot) CreateTopic(chat *Chat, topic *Topic) (*Topic, error) {
|
||||
params := map[string]string{
|
||||
"chat_id": chat.Recipient(),
|
||||
"name": topic.Name,
|
||||
}
|
||||
|
||||
if topic.IconColor != 0 {
|
||||
params["icon_color"] = strconv.Itoa(topic.IconColor)
|
||||
}
|
||||
if topic.IconCustomEmojiID != "" {
|
||||
params["icon_custom_emoji_id"] = topic.IconCustomEmojiID
|
||||
}
|
||||
|
||||
data, err := b.Raw("createForumTopic", params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Result *Topic
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, wrapError(err)
|
||||
}
|
||||
return resp.Result, err
|
||||
}
|
||||
|
||||
// EditTopic edits name and icon of a topic in a forum supergroup chat.
|
||||
func (b *Bot) EditTopic(chat *Chat, topic *Topic) error {
|
||||
params := map[string]interface{}{
|
||||
"chat_id": chat.Recipient(),
|
||||
"message_thread_id": topic.ThreadID,
|
||||
}
|
||||
|
||||
if topic.Name != "" {
|
||||
params["name"] = topic.Name
|
||||
}
|
||||
if topic.IconCustomEmojiID != "" {
|
||||
params["icon_custom_emoji_id"] = topic.IconCustomEmojiID
|
||||
}
|
||||
|
||||
_, err := b.Raw("editForumTopic", params)
|
||||
return err
|
||||
}
|
||||
|
||||
// CloseTopic closes an open topic in a forum supergroup chat.
|
||||
func (b *Bot) CloseTopic(chat *Chat, topic *Topic) error {
|
||||
params := map[string]interface{}{
|
||||
"chat_id": chat.Recipient(),
|
||||
"message_thread_id": topic.ThreadID,
|
||||
}
|
||||
|
||||
_, err := b.Raw("closeForumTopic", params)
|
||||
return err
|
||||
}
|
||||
|
||||
// ReopenTopic reopens a closed topic in a forum supergroup chat.
|
||||
func (b *Bot) ReopenTopic(chat *Chat, topic *Topic) error {
|
||||
params := map[string]interface{}{
|
||||
"chat_id": chat.Recipient(),
|
||||
"message_thread_id": topic.ThreadID,
|
||||
}
|
||||
|
||||
_, 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, topic *Topic) error {
|
||||
params := map[string]interface{}{
|
||||
"chat_id": chat.Recipient(),
|
||||
"message_thread_id": topic.ThreadID,
|
||||
}
|
||||
|
||||
_, 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, topic *Topic) error {
|
||||
params := map[string]interface{}{
|
||||
"chat_id": chat.Recipient(),
|
||||
"message_thread_id": topic.ThreadID,
|
||||
}
|
||||
|
||||
_, 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
|
||||
}
|
||||
|
||||
// EditGeneralTopic edits name of the 'General' topic in a forum supergroup chat.
|
||||
func (b *Bot) EditGeneralTopic(chat *Chat, topic *Topic) error {
|
||||
params := map[string]interface{}{
|
||||
"chat_id": chat.Recipient(),
|
||||
"name": topic.Name,
|
||||
}
|
||||
|
||||
_, err := b.Raw("editGeneralForumTopic", params)
|
||||
return err
|
||||
}
|
||||
|
||||
// CloseGeneralTopic closes an open 'General' topic in a forum supergroup chat.
|
||||
func (b *Bot) CloseGeneralTopic(chat *Chat) error {
|
||||
params := map[string]interface{}{
|
||||
"chat_id": chat.Recipient(),
|
||||
}
|
||||
|
||||
_, err := b.Raw("closeGeneralForumTopic", params)
|
||||
return err
|
||||
}
|
||||
|
||||
// ReopenGeneralTopic reopens a closed 'General' topic in a forum supergroup chat.
|
||||
func (b *Bot) ReopenGeneralTopic(chat *Chat) error {
|
||||
params := map[string]interface{}{
|
||||
"chat_id": chat.Recipient(),
|
||||
}
|
||||
|
||||
_, err := b.Raw("reopenGeneralForumTopic", params)
|
||||
return err
|
||||
}
|
||||
|
||||
// HideGeneralTopic hides the 'General' topic in a forum supergroup chat.
|
||||
func (b *Bot) HideGeneralTopic(chat *Chat) error {
|
||||
params := map[string]interface{}{
|
||||
"chat_id": chat.Recipient(),
|
||||
}
|
||||
|
||||
_, err := b.Raw("hideGeneralForumTopic", params)
|
||||
return err
|
||||
}
|
||||
|
||||
// UnhideGeneralTopic unhides the 'General' topic in a forum supergroup chat.
|
||||
func (b *Bot) UnhideGeneralTopic(chat *Chat) error {
|
||||
params := map[string]interface{}{
|
||||
"chat_id": chat.Recipient(),
|
||||
}
|
||||
|
||||
_, err := b.Raw("unhideGeneralForumTopic", params)
|
||||
return err
|
||||
}
|
||||
|
||||
// UnpinAllGeneralTopicMessages clears the list of pinned messages in a General 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) UnpinAllGeneralTopicMessages(chat *Chat) error {
|
||||
params := map[string]interface{}{
|
||||
"chat_id": chat.Recipient(),
|
||||
}
|
||||
|
||||
_, err := b.Raw("unpinAllGeneralForumTopicMessages", params)
|
||||
return err
|
||||
}
|
Loading…
Reference in New Issue