Merge pull request #29 from zhulik/master
Major overhaul to make everything easier to usebot-api-6.1
commit
b034326d85
|
@ -0,0 +1,2 @@
|
||||||
|
.idea/
|
||||||
|
coverage.out
|
|
@ -0,0 +1,5 @@
|
||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.4
|
||||||
|
- tip
|
16
README.md
16
README.md
|
@ -1,6 +1,7 @@
|
||||||
# Golang bindings for the Telegram Bot API
|
# Golang bindings for the Telegram Bot API
|
||||||
|
|
||||||
[![GoDoc](https://godoc.org/github.com/Syfaro/telegram-bot-api?status.svg)](http://godoc.org/github.com/Syfaro/telegram-bot-api)
|
[![GoDoc](https://godoc.org/github.com/Syfaro/telegram-bot-api?status.svg)](http://godoc.org/github.com/Syfaro/telegram-bot-api)
|
||||||
|
[![Travis](https://travis-ci.org/Syfaro/telegram-bot-api.svg)](https://travis-ci.org/Syfaro/telegram-bot-api)
|
||||||
|
|
||||||
All methods have been added, and all features should be available.
|
All methods have been added, and all features should be available.
|
||||||
If you want a feature that hasn't been added yet or something is broken, open an issue and I'll see what I can do.
|
If you want a feature that hasn't been added yet or something is broken, open an issue and I'll see what I can do.
|
||||||
|
@ -34,18 +35,15 @@ func main() {
|
||||||
u := tgbotapi.NewUpdate(0)
|
u := tgbotapi.NewUpdate(0)
|
||||||
u.Timeout = 60
|
u.Timeout = 60
|
||||||
|
|
||||||
err = bot.UpdatesChan(u)
|
updates, err := bot.GetUpdatesChan(u)
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for update := range bot.Updates {
|
for update := range updates {
|
||||||
log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text)
|
log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text)
|
||||||
|
|
||||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
|
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
|
||||||
msg.ReplyToMessageID = update.Message.MessageID
|
msg.ReplyToMessageID = update.Message.MessageID
|
||||||
|
|
||||||
bot.SendMessage(msg)
|
bot.Send(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -76,10 +74,10 @@ func main() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.ListenForWebhook("/"+bot.Token)
|
updates, _ := bot.ListenForWebhook("/" + bot.Token)
|
||||||
go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil)
|
go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil)
|
||||||
|
|
||||||
for update := range bot.Updates {
|
for update := range updates {
|
||||||
log.Printf("%+v\n", update)
|
log.Printf("%+v\n", update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,4 +85,4 @@ func main() {
|
||||||
|
|
||||||
If you need, you may generate a self signed certficate, as this requires HTTPS / TLS. The above example tells Telegram that this is your certificate and that it should be trusted, even though it is not properly signed.
|
If you need, you may generate a self signed certficate, as this requires HTTPS / TLS. The above example tells Telegram that this is your certificate and that it should be trusted, even though it is not properly signed.
|
||||||
|
|
||||||
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3560 -subj -nodes
|
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3560 -subj "//O=Org\CN=Test" -nodes
|
||||||
|
|
424
bot.go
424
bot.go
|
@ -2,16 +2,27 @@
|
||||||
package tgbotapi
|
package tgbotapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/technoweenie/multipartstreamer"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BotAPI has methods for interacting with all of Telegram's Bot API endpoints.
|
// BotAPI has methods for interacting with all of Telegram's Bot API endpoints.
|
||||||
type BotAPI struct {
|
type BotAPI struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Debug bool `json:"debug"`
|
Debug bool `json:"debug"`
|
||||||
Self User `json:"-"`
|
Self User `json:"-"`
|
||||||
Updates chan Update `json:"-"`
|
Client *http.Client `json:"-"`
|
||||||
Client *http.Client `json:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBotAPI creates a new BotAPI instance.
|
// NewBotAPI creates a new BotAPI instance.
|
||||||
|
@ -37,3 +48,406 @@ func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) {
|
||||||
|
|
||||||
return bot, nil
|
return bot, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MakeRequest makes a request to a specific endpoint with our token.
|
||||||
|
// All requests are POSTs because Telegram doesn't care, and it's easier.
|
||||||
|
func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse, error) {
|
||||||
|
resp, err := bot.Client.PostForm(fmt.Sprintf(APIEndpoint, bot.Token, endpoint), params)
|
||||||
|
if err != nil {
|
||||||
|
return APIResponse{}, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusForbidden {
|
||||||
|
return APIResponse{}, errors.New(APIForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return APIResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bot.Debug {
|
||||||
|
log.Println(endpoint, string(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiResp APIResponse
|
||||||
|
json.Unmarshal(bytes, &apiResp)
|
||||||
|
|
||||||
|
if !apiResp.Ok {
|
||||||
|
return APIResponse{}, errors.New(apiResp.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bot *BotAPI) makeMessageRequest(endpoint string, params url.Values) (Message, error) {
|
||||||
|
resp, err := bot.MakeRequest(endpoint, params)
|
||||||
|
if err != nil {
|
||||||
|
return Message{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var message Message
|
||||||
|
json.Unmarshal(resp.Result, &message)
|
||||||
|
|
||||||
|
bot.debugLog(endpoint, params, message)
|
||||||
|
|
||||||
|
return message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadFile makes a request to the API with a file.
|
||||||
|
//
|
||||||
|
// Requires the parameter to hold the file not be in the params.
|
||||||
|
// File should be a string to a file path, a FileBytes struct, or a FileReader struct.
|
||||||
|
func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldname string, file interface{}) (APIResponse, error) {
|
||||||
|
ms := multipartstreamer.New()
|
||||||
|
ms.WriteFields(params)
|
||||||
|
|
||||||
|
switch f := file.(type) {
|
||||||
|
case string:
|
||||||
|
fileHandle, err := os.Open(f)
|
||||||
|
if err != nil {
|
||||||
|
return APIResponse{}, err
|
||||||
|
}
|
||||||
|
defer fileHandle.Close()
|
||||||
|
|
||||||
|
fi, err := os.Stat(f)
|
||||||
|
if err != nil {
|
||||||
|
return APIResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle)
|
||||||
|
case FileBytes:
|
||||||
|
buf := bytes.NewBuffer(f.Bytes)
|
||||||
|
ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf)
|
||||||
|
case FileReader:
|
||||||
|
if f.Size == -1 {
|
||||||
|
data, err := ioutil.ReadAll(f.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return APIResponse{}, err
|
||||||
|
}
|
||||||
|
buf := bytes.NewBuffer(data)
|
||||||
|
|
||||||
|
ms.WriteReader(fieldname, f.Name, int64(len(data)), buf)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
ms.WriteReader(fieldname, f.Name, f.Size, f.Reader)
|
||||||
|
default:
|
||||||
|
return APIResponse{}, errors.New("bad file type")
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", fmt.Sprintf(APIEndpoint, bot.Token, endpoint), nil)
|
||||||
|
ms.SetupRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return APIResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := bot.Client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return APIResponse{}, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
bytes, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return APIResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bot.Debug {
|
||||||
|
log.Println(string(bytes[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiResp APIResponse
|
||||||
|
json.Unmarshal(bytes, &apiResp)
|
||||||
|
|
||||||
|
if !apiResp.Ok {
|
||||||
|
return APIResponse{}, errors.New(apiResp.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFileDirectURL returns direct URL to file
|
||||||
|
//
|
||||||
|
// Requires fileID
|
||||||
|
func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
|
||||||
|
file, err := bot.GetFile(FileConfig{fileID})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.Link(bot.Token), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMe fetches the currently authenticated bot.
|
||||||
|
//
|
||||||
|
// There are no parameters for this method.
|
||||||
|
func (bot *BotAPI) GetMe() (User, error) {
|
||||||
|
resp, err := bot.MakeRequest("getMe", nil)
|
||||||
|
if err != nil {
|
||||||
|
return User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var user User
|
||||||
|
json.Unmarshal(resp.Result, &user)
|
||||||
|
|
||||||
|
if bot.Debug {
|
||||||
|
log.Printf("getMe: %+v\n", user)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMessageToMe returns true if message directed to this bot
|
||||||
|
//
|
||||||
|
// Requires message
|
||||||
|
func (bot *BotAPI) IsMessageToMe(message Message) bool {
|
||||||
|
return strings.Contains(message.Text, "@"+bot.Self.UserName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send will send event(Message, Photo, Audio, ChatAction, anything) to Telegram
|
||||||
|
//
|
||||||
|
// Requires Chattable
|
||||||
|
func (bot *BotAPI) Send(c Chattable) (Message, error) {
|
||||||
|
switch c.(type) {
|
||||||
|
case Fileable:
|
||||||
|
return bot.sendFile(c.(Fileable))
|
||||||
|
default:
|
||||||
|
return bot.sendChattable(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bot *BotAPI) debugLog(context string, v url.Values, message interface{}) {
|
||||||
|
if bot.Debug {
|
||||||
|
log.Printf("%s req : %+v\n", context, v)
|
||||||
|
log.Printf("%s resp: %+v\n", context, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bot *BotAPI) sendExisting(method string, config Fileable) (Message, error) {
|
||||||
|
v, err := config.Values()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return Message{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
message, err := bot.makeMessageRequest(method, v)
|
||||||
|
if err != nil {
|
||||||
|
return Message{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bot *BotAPI) uploadAndSend(method string, config Fileable) (Message, error) {
|
||||||
|
params, err := config.Params()
|
||||||
|
if err != nil {
|
||||||
|
return Message{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := config.GetFile()
|
||||||
|
|
||||||
|
resp, err := bot.UploadFile(method, params, config.Name(), file)
|
||||||
|
if err != nil {
|
||||||
|
return Message{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var message Message
|
||||||
|
json.Unmarshal(resp.Result, &message)
|
||||||
|
|
||||||
|
if bot.Debug {
|
||||||
|
log.Printf("%s resp: %+v\n", method, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bot *BotAPI) sendFile(config Fileable) (Message, error) {
|
||||||
|
if config.UseExistingFile() {
|
||||||
|
return bot.sendExisting(config.Method(), config)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bot.uploadAndSend(config.Method(), config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bot *BotAPI) sendChattable(config Chattable) (Message, error) {
|
||||||
|
v, err := config.Values()
|
||||||
|
if err != nil {
|
||||||
|
return Message{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
message, err := bot.makeMessageRequest(config.Method(), v)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return Message{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserProfilePhotos gets a user's profile photos.
|
||||||
|
//
|
||||||
|
// Requires UserID.
|
||||||
|
// Offset and Limit are optional.
|
||||||
|
func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
|
||||||
|
v := url.Values{}
|
||||||
|
v.Add("user_id", strconv.Itoa(config.UserID))
|
||||||
|
if config.Offset != 0 {
|
||||||
|
v.Add("offset", strconv.Itoa(config.Offset))
|
||||||
|
}
|
||||||
|
if config.Limit != 0 {
|
||||||
|
v.Add("limit", strconv.Itoa(config.Limit))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := bot.MakeRequest("getUserProfilePhotos", v)
|
||||||
|
if err != nil {
|
||||||
|
return UserProfilePhotos{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var profilePhotos UserProfilePhotos
|
||||||
|
json.Unmarshal(resp.Result, &profilePhotos)
|
||||||
|
|
||||||
|
bot.debugLog("GetUserProfilePhoto", v, profilePhotos)
|
||||||
|
|
||||||
|
return profilePhotos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFile returns a file_id required to download a file.
|
||||||
|
//
|
||||||
|
// Requires FileID.
|
||||||
|
func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
|
||||||
|
v := url.Values{}
|
||||||
|
v.Add("file_id", config.FileID)
|
||||||
|
|
||||||
|
resp, err := bot.MakeRequest("getFile", v)
|
||||||
|
if err != nil {
|
||||||
|
return File{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var file File
|
||||||
|
json.Unmarshal(resp.Result, &file)
|
||||||
|
|
||||||
|
bot.debugLog("GetFile", v, file)
|
||||||
|
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUpdates fetches updates.
|
||||||
|
// If a WebHook is set, this will not return any data!
|
||||||
|
//
|
||||||
|
// Offset, Limit, and Timeout are optional.
|
||||||
|
// To not get old items, set Offset to one higher than the previous item.
|
||||||
|
// Set Timeout to a large number to reduce requests and get responses instantly.
|
||||||
|
func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
|
||||||
|
v := url.Values{}
|
||||||
|
if config.Offset > 0 {
|
||||||
|
v.Add("offset", strconv.Itoa(config.Offset))
|
||||||
|
}
|
||||||
|
if config.Limit > 0 {
|
||||||
|
v.Add("limit", strconv.Itoa(config.Limit))
|
||||||
|
}
|
||||||
|
if config.Timeout > 0 {
|
||||||
|
v.Add("timeout", strconv.Itoa(config.Timeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := bot.MakeRequest("getUpdates", v)
|
||||||
|
if err != nil {
|
||||||
|
return []Update{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var updates []Update
|
||||||
|
json.Unmarshal(resp.Result, &updates)
|
||||||
|
|
||||||
|
if bot.Debug {
|
||||||
|
log.Printf("getUpdates: %+v\n", updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveWebhook removes webhook
|
||||||
|
//
|
||||||
|
// There are no parameters for this method.
|
||||||
|
func (bot *BotAPI) RemoveWebhook() (APIResponse, error) {
|
||||||
|
return bot.MakeRequest("setWebhook", url.Values{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWebhook sets a webhook.
|
||||||
|
// If this is set, GetUpdates will not get any data!
|
||||||
|
//
|
||||||
|
// Requires URL OR to set Clear to true.
|
||||||
|
func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
|
||||||
|
if config.Certificate == nil {
|
||||||
|
v := url.Values{}
|
||||||
|
v.Add("url", config.URL.String())
|
||||||
|
|
||||||
|
return bot.MakeRequest("setWebhook", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := make(map[string]string)
|
||||||
|
params["url"] = config.URL.String()
|
||||||
|
|
||||||
|
resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate)
|
||||||
|
if err != nil {
|
||||||
|
return APIResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiResp APIResponse
|
||||||
|
json.Unmarshal(resp.Result, &apiResp)
|
||||||
|
|
||||||
|
if bot.Debug {
|
||||||
|
log.Printf("setWebhook resp: %+v\n", apiResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUpdatesChan starts and returns a channel for getting updates.
|
||||||
|
//
|
||||||
|
// Requires UpdateConfig
|
||||||
|
func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (<-chan Update, error) {
|
||||||
|
updatesChan := make(chan Update, 100)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
updates, err := bot.GetUpdates(config)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
log.Println("Failed to get updates, retrying in 3 seconds...")
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, update := range updates {
|
||||||
|
if update.UpdateID >= config.Offset {
|
||||||
|
config.Offset = update.UpdateID + 1
|
||||||
|
updatesChan <- update
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return updatesChan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenForWebhook registers a http handler for a webhook.
|
||||||
|
func (bot *BotAPI) ListenForWebhook(pattern string) (<-chan Update, http.Handler) {
|
||||||
|
updatesChan := make(chan Update, 100)
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
bytes, _ := ioutil.ReadAll(r.Body)
|
||||||
|
|
||||||
|
var update Update
|
||||||
|
json.Unmarshal(bytes, &update)
|
||||||
|
|
||||||
|
updatesChan <- update
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc(pattern, handler)
|
||||||
|
|
||||||
|
return updatesChan, handler
|
||||||
|
}
|
||||||
|
|
422
bot_test.go
422
bot_test.go
|
@ -1,63 +1,406 @@
|
||||||
package tgbotapi_test
|
package tgbotapi_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/syfaro/telegram-bot-api"
|
"github.com/Syfaro/telegram-bot-api"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
const (
|
||||||
botToken := os.Getenv("TELEGRAM_API_TOKEN")
|
TestToken = "153667468:AAHlSHlMqSt1f_uFmVRJbm5gntu2HI4WW8I"
|
||||||
|
ChatID = 76918703
|
||||||
|
ReplyToMessageID = 35
|
||||||
|
ExistingPhotoFileID = "AgADAgADw6cxG4zHKAkr42N7RwEN3IFShCoABHQwXEtVks4EH2wBAAEC"
|
||||||
|
ExistingDocumentFileID = "BQADAgADOQADjMcoCcioX1GrDvp3Ag"
|
||||||
|
ExistingAudioFileID = "BQADAgADRgADjMcoCdXg3lSIN49lAg"
|
||||||
|
ExistingVoiceFileID = "AwADAgADWQADjMcoCeul6r_q52IyAg"
|
||||||
|
ExistingVideoFileID = "BAADAgADZgADjMcoCav432kYe0FRAg"
|
||||||
|
ExistingStickerFileID = "BQADAgADcwADjMcoCbdl-6eB--YPAg"
|
||||||
|
)
|
||||||
|
|
||||||
if botToken == "" {
|
func getBot(t *testing.T) (*tgbotapi.BotAPI, error) {
|
||||||
log.Panic("You must provide a TELEGRAM_API_TOKEN env variable to test!")
|
bot, err := tgbotapi.NewBotAPI(TestToken)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(m.Run())
|
return bot, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewBotAPI_notoken(t *testing.T) {
|
func TestNewBotAPI_notoken(t *testing.T) {
|
||||||
_, err := tgbotapi.NewBotAPI("")
|
_, err := tgbotapi.NewBotAPI("")
|
||||||
|
|
||||||
if err.Error() != tgbotapi.APIForbidden {
|
if err == nil {
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewBotAPI_token(t *testing.T) {
|
|
||||||
_, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_API_TOKEN"))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetUpdates(t *testing.T) {
|
func TestGetUpdates(t *testing.T) {
|
||||||
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_API_TOKEN"))
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
|
|
||||||
u := tgbotapi.NewUpdate(0)
|
u := tgbotapi.NewUpdate(0)
|
||||||
|
|
||||||
_, err = bot.GetUpdates(u)
|
_, err := bot.GetUpdates(u)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSendMessage(t *testing.T) {
|
func TestSendWithMessage(t *testing.T) {
|
||||||
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_API_TOKEN"))
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewMessage(ChatID, "A test message from the test library in telegram-bot-api")
|
||||||
|
msg.ParseMode = "markdown"
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
msg := tgbotapi.NewMessage(36529758, "A test message from the test library in telegram-bot-api")
|
func TestSendWithMessageReply(t *testing.T) {
|
||||||
bot.SendMessage(msg)
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewMessage(ChatID, "A test message from the test library in telegram-bot-api")
|
||||||
|
msg.ReplyToMessageID = ReplyToMessageID
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithMessageForward(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewForward(ChatID, ChatID, ReplyToMessageID)
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithNewPhoto(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewPhotoUpload(ChatID, "tests/image.jpg")
|
||||||
|
msg.Caption = "Test"
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithNewPhotoWithFileBytes(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
data, _ := ioutil.ReadFile("tests/image.jpg")
|
||||||
|
b := tgbotapi.FileBytes{Name: "image.jpg", Bytes: data}
|
||||||
|
|
||||||
|
msg := tgbotapi.NewPhotoUpload(ChatID, b)
|
||||||
|
msg.Caption = "Test"
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithNewPhotoWithFileReader(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
f, _ := os.Open("tests/image.jpg")
|
||||||
|
reader := tgbotapi.FileReader{Name: "image.jpg", Reader: f, Size: -1}
|
||||||
|
|
||||||
|
msg := tgbotapi.NewPhotoUpload(ChatID, reader)
|
||||||
|
msg.Caption = "Test"
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithNewPhotoReply(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewPhotoUpload(ChatID, "tests/image.jpg")
|
||||||
|
msg.ReplyToMessageID = ReplyToMessageID
|
||||||
|
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithExistingPhoto(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewPhotoShare(ChatID, ExistingPhotoFileID)
|
||||||
|
msg.Caption = "Test"
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithNewDocument(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewDocumentUpload(ChatID, "tests/image.jpg")
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithExistingDocument(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewDocumentShare(ChatID, ExistingDocumentFileID)
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithNewAudio(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewAudioUpload(ChatID, "tests/audio.mp3")
|
||||||
|
msg.Title = "TEST"
|
||||||
|
msg.Duration = 10
|
||||||
|
msg.Performer = "TEST"
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithExistingAudio(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewAudioShare(ChatID, ExistingAudioFileID)
|
||||||
|
msg.Title = "TEST"
|
||||||
|
msg.Duration = 10
|
||||||
|
msg.Performer = "TEST"
|
||||||
|
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithNewVoice(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewVoiceUpload(ChatID, "tests/voice.ogg")
|
||||||
|
msg.Duration = 10
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithExistingVoice(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewVoiceShare(ChatID, ExistingVoiceFileID)
|
||||||
|
msg.Duration = 10
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithLocation(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
_, err := bot.Send(tgbotapi.NewLocation(ChatID, 40, 40))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithNewVideo(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewVideoUpload(ChatID, "tests/video.mp4")
|
||||||
|
msg.Duration = 10
|
||||||
|
msg.Caption = "TEST"
|
||||||
|
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithExistingVideo(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewVideoShare(ChatID, ExistingVideoFileID)
|
||||||
|
msg.Duration = 10
|
||||||
|
msg.Caption = "TEST"
|
||||||
|
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithNewSticker(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewStickerUpload(ChatID, "tests/image.jpg")
|
||||||
|
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithExistingSticker(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewStickerShare(ChatID, ExistingStickerFileID)
|
||||||
|
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithNewStickerAndKeyboardHide(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewStickerUpload(ChatID, "tests/image.jpg")
|
||||||
|
msg.ReplyMarkup = tgbotapi.ReplyKeyboardHide{true, false}
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendWithExistingStickerAndKeyboardHide(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
msg := tgbotapi.NewStickerShare(ChatID, ExistingStickerFileID)
|
||||||
|
msg.ReplyMarkup = tgbotapi.ReplyKeyboardHide{true, false}
|
||||||
|
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFile(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
file := tgbotapi.FileConfig{ExistingPhotoFileID}
|
||||||
|
|
||||||
|
_, err := bot.GetFile(file)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendChatConfig(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
_, err := bot.Send(tgbotapi.NewChatAction(ChatID, tgbotapi.ChatTyping))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUserProfilePhotos(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
_, err := bot.GetUserProfilePhotos(tgbotapi.NewUserProfilePhotos(ChatID))
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListenForWebhook(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
_, handler := bot.ListenForWebhook("/")
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", "", strings.NewReader("{}"))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("Home page didn't return %v", http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetWebhookWithCert(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
bot.RemoveWebhook()
|
||||||
|
|
||||||
|
wh := tgbotapi.NewWebhookWithCert("https://example.com/tgbotapi-test/"+bot.Token, "tests/cert.pem")
|
||||||
|
_, err := bot.SetWebhook(wh)
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.RemoveWebhook()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetWebhookWithoutCert(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
bot.RemoveWebhook()
|
||||||
|
|
||||||
|
wh := tgbotapi.NewWebhook("https://example.com/tgbotapi-test/" + bot.Token)
|
||||||
|
_, err := bot.SetWebhook(wh)
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.RemoveWebhook()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdatesChan(t *testing.T) {
|
||||||
|
bot, _ := getBot(t)
|
||||||
|
|
||||||
|
var ucfg tgbotapi.UpdateConfig = tgbotapi.NewUpdate(0)
|
||||||
|
ucfg.Timeout = 60
|
||||||
|
_, err := bot.GetUpdatesChan(ucfg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleNewBotAPI() {
|
func ExampleNewBotAPI() {
|
||||||
|
@ -73,14 +416,37 @@ func ExampleNewBotAPI() {
|
||||||
u := tgbotapi.NewUpdate(0)
|
u := tgbotapi.NewUpdate(0)
|
||||||
u.Timeout = 60
|
u.Timeout = 60
|
||||||
|
|
||||||
err = bot.UpdatesChan(u)
|
updates, err := bot.GetUpdatesChan(u)
|
||||||
|
|
||||||
for update := range bot.Updates {
|
for update := range updates {
|
||||||
log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text)
|
log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text)
|
||||||
|
|
||||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
|
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
|
||||||
msg.ReplyToMessageID = update.Message.MessageID
|
msg.ReplyToMessageID = update.Message.MessageID
|
||||||
|
|
||||||
bot.SendMessage(msg)
|
bot.Send(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleNewWebhook() {
|
||||||
|
bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.Debug = true
|
||||||
|
|
||||||
|
log.Printf("Authorized on account %s", bot.Self.UserName)
|
||||||
|
|
||||||
|
_, err = bot.SetWebhook(tgbotapi.NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, "cert.pem"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updates, _ := bot.ListenForWebhook("/" + bot.Token)
|
||||||
|
go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil)
|
||||||
|
|
||||||
|
for update := range updates {
|
||||||
|
log.Printf("%+v\n", update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,498 @@
|
||||||
|
package tgbotapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Telegram constants
|
||||||
|
const (
|
||||||
|
// APIEndpoint is the endpoint for all API methods, with formatting for Sprintf
|
||||||
|
APIEndpoint = "https://api.telegram.org/bot%s/%s"
|
||||||
|
// FileEndpoint is the endpoint for downloading a file from Telegram
|
||||||
|
FileEndpoint = "https://api.telegram.org/file/bot%s/%s"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constant values for ChatActions
|
||||||
|
const (
|
||||||
|
ChatTyping = "typing"
|
||||||
|
ChatUploadPhoto = "upload_photo"
|
||||||
|
ChatRecordVideo = "record_video"
|
||||||
|
ChatUploadVideo = "upload_video"
|
||||||
|
ChatRecordAudio = "record_audio"
|
||||||
|
ChatUploadAudio = "upload_audio"
|
||||||
|
ChatUploadDocument = "upload_document"
|
||||||
|
ChatFindLocation = "find_location"
|
||||||
|
)
|
||||||
|
|
||||||
|
// API errors
|
||||||
|
const (
|
||||||
|
// APIForbidden happens when a token is bad
|
||||||
|
APIForbidden = "forbidden"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constant values for ParseMode in MessageConfig
|
||||||
|
const (
|
||||||
|
ModeMarkdown = "Markdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Chattable represents any event in chat(MessageConfig, PhotoConfig, ChatActionConfig and others)
|
||||||
|
type Chattable interface {
|
||||||
|
Values() (url.Values, error)
|
||||||
|
Method() string
|
||||||
|
}
|
||||||
|
|
||||||
|
//Fileable represents any file event(PhotoConfig, DocumentConfig, AudioConfig, VoiceConfig, VideoConfig, StickerConfig)
|
||||||
|
type Fileable interface {
|
||||||
|
Chattable
|
||||||
|
Params() (map[string]string, error)
|
||||||
|
Name() string
|
||||||
|
GetFile() interface{}
|
||||||
|
UseExistingFile() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseChat is base struct for all chat event(Message, Photo and so on)
|
||||||
|
type BaseChat struct {
|
||||||
|
ChatID int
|
||||||
|
ChannelUsername string
|
||||||
|
ReplyToMessageID int
|
||||||
|
ReplyMarkup interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns url.Values representation of BaseChat
|
||||||
|
func (chat *BaseChat) Values() (url.Values, error) {
|
||||||
|
v := url.Values{}
|
||||||
|
if chat.ChannelUsername != "" {
|
||||||
|
v.Add("chat_id", chat.ChannelUsername)
|
||||||
|
} else {
|
||||||
|
v.Add("chat_id", strconv.Itoa(chat.ChatID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if chat.ReplyToMessageID != 0 {
|
||||||
|
v.Add("reply_to_message_id", strconv.Itoa(chat.ReplyToMessageID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if chat.ReplyMarkup != nil {
|
||||||
|
data, err := json.Marshal(chat.ReplyMarkup)
|
||||||
|
if err != nil {
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Add("reply_markup", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseFile is base struct for all file events(PhotoConfig, DocumentConfig, AudioConfig, VoiceConfig, VideoConfig, StickerConfig)
|
||||||
|
type BaseFile struct {
|
||||||
|
BaseChat
|
||||||
|
FilePath string
|
||||||
|
File interface{}
|
||||||
|
FileID string
|
||||||
|
UseExisting bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params returns map[string]string representation of BaseFile
|
||||||
|
func (file BaseFile) Params() (map[string]string, error) {
|
||||||
|
params := make(map[string]string)
|
||||||
|
|
||||||
|
if file.ChannelUsername != "" {
|
||||||
|
params["chat_id"] = file.ChannelUsername
|
||||||
|
} else {
|
||||||
|
params["chat_id"] = strconv.Itoa(file.ChatID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.ReplyToMessageID != 0 {
|
||||||
|
params["reply_to_message_id"] = strconv.Itoa(file.ReplyToMessageID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.ReplyMarkup != nil {
|
||||||
|
data, err := json.Marshal(file.ReplyMarkup)
|
||||||
|
if err != nil {
|
||||||
|
return params, err
|
||||||
|
}
|
||||||
|
|
||||||
|
params["reply_markup"] = string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFile returns abstract representation of File inside BaseFile
|
||||||
|
func (file BaseFile) GetFile() interface{} {
|
||||||
|
var result interface{}
|
||||||
|
if file.FilePath == "" {
|
||||||
|
result = file.File
|
||||||
|
} else {
|
||||||
|
result = file.FilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseExistingFile returns true if BaseFile contains already uploaded file by FileID
|
||||||
|
func (file BaseFile) UseExistingFile() bool {
|
||||||
|
return file.UseExisting
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageConfig contains information about a SendMessage request.
|
||||||
|
type MessageConfig struct {
|
||||||
|
BaseChat
|
||||||
|
Text string
|
||||||
|
ParseMode string
|
||||||
|
DisableWebPagePreview bool
|
||||||
|
ReplyMarkup interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns url.Values representation of MessageConfig
|
||||||
|
func (config MessageConfig) Values() (url.Values, error) {
|
||||||
|
v, _ := config.BaseChat.Values()
|
||||||
|
v.Add("text", config.Text)
|
||||||
|
v.Add("disable_web_page_preview", strconv.FormatBool(config.DisableWebPagePreview))
|
||||||
|
if config.ParseMode != "" {
|
||||||
|
v.Add("parse_mode", config.ParseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method returns Telegram API method name for sending Message
|
||||||
|
func (config MessageConfig) Method() string {
|
||||||
|
return "SendMessage"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForwardConfig contains information about a ForwardMessage request.
|
||||||
|
type ForwardConfig struct {
|
||||||
|
BaseChat
|
||||||
|
FromChatID int
|
||||||
|
FromChannelUsername string
|
||||||
|
MessageID int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns url.Values representation of ForwardConfig
|
||||||
|
func (config ForwardConfig) Values() (url.Values, error) {
|
||||||
|
v, _ := config.BaseChat.Values()
|
||||||
|
v.Add("from_chat_id", strconv.Itoa(config.FromChatID))
|
||||||
|
v.Add("message_id", strconv.Itoa(config.MessageID))
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method returns Telegram API method name for sending Forward
|
||||||
|
func (config ForwardConfig) Method() string {
|
||||||
|
return "forwardMessage"
|
||||||
|
}
|
||||||
|
|
||||||
|
// PhotoConfig contains information about a SendPhoto request.
|
||||||
|
type PhotoConfig struct {
|
||||||
|
BaseFile
|
||||||
|
Caption string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params returns map[string]string representation of PhotoConfig
|
||||||
|
func (config PhotoConfig) Params() (map[string]string, error) {
|
||||||
|
params, _ := config.BaseFile.Params()
|
||||||
|
|
||||||
|
if config.Caption != "" {
|
||||||
|
params["caption"] = config.Caption
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns url.Values representation of PhotoConfig
|
||||||
|
func (config PhotoConfig) Values() (url.Values, error) {
|
||||||
|
v, _ := config.BaseChat.Values()
|
||||||
|
|
||||||
|
v.Add(config.Name(), config.FileID)
|
||||||
|
if config.Caption != "" {
|
||||||
|
v.Add("caption", config.Caption)
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name return field name for uploading file
|
||||||
|
func (config PhotoConfig) Name() string {
|
||||||
|
return "photo"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method returns Telegram API method name for sending Photo
|
||||||
|
func (config PhotoConfig) Method() string {
|
||||||
|
return "SendPhoto"
|
||||||
|
}
|
||||||
|
|
||||||
|
// AudioConfig contains information about a SendAudio request.
|
||||||
|
type AudioConfig struct {
|
||||||
|
BaseFile
|
||||||
|
Duration int
|
||||||
|
Performer string
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns url.Values representation of AudioConfig
|
||||||
|
func (config AudioConfig) Values() (url.Values, error) {
|
||||||
|
v, _ := config.BaseChat.Values()
|
||||||
|
|
||||||
|
v.Add(config.Name(), config.FileID)
|
||||||
|
if config.Duration != 0 {
|
||||||
|
v.Add("duration", strconv.Itoa(config.Duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Performer != "" {
|
||||||
|
v.Add("performer", config.Performer)
|
||||||
|
}
|
||||||
|
if config.Title != "" {
|
||||||
|
v.Add("title", config.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params returns map[string]string representation of AudioConfig
|
||||||
|
func (config AudioConfig) Params() (map[string]string, error) {
|
||||||
|
params, _ := config.BaseFile.Params()
|
||||||
|
|
||||||
|
if config.Duration != 0 {
|
||||||
|
params["duration"] = strconv.Itoa(config.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Performer != "" {
|
||||||
|
params["performer"] = config.Performer
|
||||||
|
}
|
||||||
|
if config.Title != "" {
|
||||||
|
params["title"] = config.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name return field name for uploading file
|
||||||
|
func (config AudioConfig) Name() string {
|
||||||
|
return "audio"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method returns Telegram API method name for sending Audio
|
||||||
|
func (config AudioConfig) Method() string {
|
||||||
|
return "SendAudio"
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocumentConfig contains information about a SendDocument request.
|
||||||
|
type DocumentConfig struct {
|
||||||
|
BaseFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns url.Values representation of DocumentConfig
|
||||||
|
func (config DocumentConfig) Values() (url.Values, error) {
|
||||||
|
v, _ := config.BaseChat.Values()
|
||||||
|
|
||||||
|
v.Add(config.Name(), config.FileID)
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params returns map[string]string representation of DocumentConfig
|
||||||
|
func (config DocumentConfig) Params() (map[string]string, error) {
|
||||||
|
params, _ := config.BaseFile.Params()
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name return field name for uploading file
|
||||||
|
func (config DocumentConfig) Name() string {
|
||||||
|
return "document"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method returns Telegram API method name for sending Document
|
||||||
|
func (config DocumentConfig) Method() string {
|
||||||
|
return "sendDocument"
|
||||||
|
}
|
||||||
|
|
||||||
|
// StickerConfig contains information about a SendSticker request.
|
||||||
|
type StickerConfig struct {
|
||||||
|
BaseFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns url.Values representation of StickerConfig
|
||||||
|
func (config StickerConfig) Values() (url.Values, error) {
|
||||||
|
v, _ := config.BaseChat.Values()
|
||||||
|
|
||||||
|
v.Add(config.Name(), config.FileID)
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params returns map[string]string representation of StickerConfig
|
||||||
|
func (config StickerConfig) Params() (map[string]string, error) {
|
||||||
|
params, _ := config.BaseFile.Params()
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name return field name for uploading file
|
||||||
|
func (config StickerConfig) Name() string {
|
||||||
|
return "sticker"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method returns Telegram API method name for sending Sticker
|
||||||
|
func (config StickerConfig) Method() string {
|
||||||
|
return "sendSticker"
|
||||||
|
}
|
||||||
|
|
||||||
|
// VideoConfig contains information about a SendVideo request.
|
||||||
|
type VideoConfig struct {
|
||||||
|
BaseFile
|
||||||
|
Duration int
|
||||||
|
Caption string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns url.Values representation of VideoConfig
|
||||||
|
func (config VideoConfig) Values() (url.Values, error) {
|
||||||
|
v, _ := config.BaseChat.Values()
|
||||||
|
|
||||||
|
v.Add(config.Name(), config.FileID)
|
||||||
|
if config.Duration != 0 {
|
||||||
|
v.Add("duration", strconv.Itoa(config.Duration))
|
||||||
|
}
|
||||||
|
if config.Caption != "" {
|
||||||
|
v.Add("caption", config.Caption)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params returns map[string]string representation of VideoConfig
|
||||||
|
func (config VideoConfig) Params() (map[string]string, error) {
|
||||||
|
params, _ := config.BaseFile.Params()
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name return field name for uploading file
|
||||||
|
func (config VideoConfig) Name() string {
|
||||||
|
return "video"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method returns Telegram API method name for sending Video
|
||||||
|
func (config VideoConfig) Method() string {
|
||||||
|
return "sendVideo"
|
||||||
|
}
|
||||||
|
|
||||||
|
// VoiceConfig contains information about a SendVoice request.
|
||||||
|
type VoiceConfig struct {
|
||||||
|
BaseFile
|
||||||
|
Duration int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns url.Values representation of VoiceConfig
|
||||||
|
func (config VoiceConfig) Values() (url.Values, error) {
|
||||||
|
v, _ := config.BaseChat.Values()
|
||||||
|
|
||||||
|
v.Add(config.Name(), config.FileID)
|
||||||
|
if config.Duration != 0 {
|
||||||
|
v.Add("duration", strconv.Itoa(config.Duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params returns map[string]string representation of VoiceConfig
|
||||||
|
func (config VoiceConfig) Params() (map[string]string, error) {
|
||||||
|
params, _ := config.BaseFile.Params()
|
||||||
|
|
||||||
|
if config.Duration != 0 {
|
||||||
|
params["duration"] = strconv.Itoa(config.Duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name return field name for uploading file
|
||||||
|
func (config VoiceConfig) Name() string {
|
||||||
|
return "voice"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method returns Telegram API method name for sending Voice
|
||||||
|
func (config VoiceConfig) Method() string {
|
||||||
|
return "sendVoice"
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocationConfig contains information about a SendLocation request.
|
||||||
|
type LocationConfig struct {
|
||||||
|
BaseChat
|
||||||
|
Latitude float64
|
||||||
|
Longitude float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns url.Values representation of LocationConfig
|
||||||
|
func (config LocationConfig) Values() (url.Values, error) {
|
||||||
|
v, _ := config.BaseChat.Values()
|
||||||
|
|
||||||
|
v.Add("latitude", strconv.FormatFloat(config.Latitude, 'f', 6, 64))
|
||||||
|
v.Add("longitude", strconv.FormatFloat(config.Longitude, 'f', 6, 64))
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method returns Telegram API method name for sending Location
|
||||||
|
func (config LocationConfig) Method() string {
|
||||||
|
return "sendLocation"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatActionConfig contains information about a SendChatAction request.
|
||||||
|
type ChatActionConfig struct {
|
||||||
|
BaseChat
|
||||||
|
Action string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns url.Values representation of ChatActionConfig
|
||||||
|
func (config ChatActionConfig) Values() (url.Values, error) {
|
||||||
|
v, _ := config.BaseChat.Values()
|
||||||
|
v.Add("action", config.Action)
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method returns Telegram API method name for sending ChatAction
|
||||||
|
func (config ChatActionConfig) Method() string {
|
||||||
|
return "sendChatAction"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserProfilePhotosConfig contains information about a GetUserProfilePhotos request.
|
||||||
|
type UserProfilePhotosConfig struct {
|
||||||
|
UserID int
|
||||||
|
Offset int
|
||||||
|
Limit int
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileConfig has information about a file hosted on Telegram
|
||||||
|
type FileConfig struct {
|
||||||
|
FileID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateConfig contains information about a GetUpdates request.
|
||||||
|
type UpdateConfig struct {
|
||||||
|
Offset int
|
||||||
|
Limit int
|
||||||
|
Timeout int
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookConfig contains information about a SetWebhook request.
|
||||||
|
type WebhookConfig struct {
|
||||||
|
URL *url.URL
|
||||||
|
Certificate interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileBytes contains information about a set of bytes to upload as a File.
|
||||||
|
type FileBytes struct {
|
||||||
|
Name string
|
||||||
|
Bytes []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileReader contains information about a reader to upload as a File.
|
||||||
|
// If Size is -1, it will read the entire Reader into memory to calculate a Size.
|
||||||
|
type FileReader struct {
|
||||||
|
Name string
|
||||||
|
Reader io.Reader
|
||||||
|
Size int64
|
||||||
|
}
|
71
helpers.go
71
helpers.go
|
@ -10,10 +10,9 @@ import (
|
||||||
// chatID is where to send it, text is the message text.
|
// chatID is where to send it, text is the message text.
|
||||||
func NewMessage(chatID int, text string) MessageConfig {
|
func NewMessage(chatID int, text string) MessageConfig {
|
||||||
return MessageConfig{
|
return MessageConfig{
|
||||||
ChatID: chatID,
|
BaseChat: BaseChat{ChatID: chatID, ReplyToMessageID: 0},
|
||||||
Text: text,
|
Text: text,
|
||||||
DisableWebPagePreview: false,
|
DisableWebPagePreview: false,
|
||||||
ReplyToMessageID: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +22,7 @@ func NewMessage(chatID int, text string) MessageConfig {
|
||||||
// and messageID is the ID of the original message.
|
// and messageID is the ID of the original message.
|
||||||
func NewForward(chatID int, fromChatID int, messageID int) ForwardConfig {
|
func NewForward(chatID int, fromChatID int, messageID int) ForwardConfig {
|
||||||
return ForwardConfig{
|
return ForwardConfig{
|
||||||
ChatID: chatID,
|
BaseChat: BaseChat{ChatID: chatID},
|
||||||
FromChatID: fromChatID,
|
FromChatID: fromChatID,
|
||||||
MessageID: messageID,
|
MessageID: messageID,
|
||||||
}
|
}
|
||||||
|
@ -36,9 +35,7 @@ func NewForward(chatID int, fromChatID int, messageID int) ForwardConfig {
|
||||||
// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes.
|
// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes.
|
||||||
func NewPhotoUpload(chatID int, file interface{}) PhotoConfig {
|
func NewPhotoUpload(chatID int, file interface{}) PhotoConfig {
|
||||||
return PhotoConfig{
|
return PhotoConfig{
|
||||||
ChatID: chatID,
|
BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, File: file, UseExisting: false},
|
||||||
UseExistingPhoto: false,
|
|
||||||
File: file,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,9 +45,7 @@ func NewPhotoUpload(chatID int, file interface{}) PhotoConfig {
|
||||||
// chatID is where to send it, fileID is the ID of the file already uploaded.
|
// chatID is where to send it, fileID is the ID of the file already uploaded.
|
||||||
func NewPhotoShare(chatID int, fileID string) PhotoConfig {
|
func NewPhotoShare(chatID int, fileID string) PhotoConfig {
|
||||||
return PhotoConfig{
|
return PhotoConfig{
|
||||||
ChatID: chatID,
|
BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, FileID: fileID, UseExisting: true},
|
||||||
UseExistingPhoto: true,
|
|
||||||
FileID: fileID,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,9 +56,7 @@ func NewPhotoShare(chatID int, fileID string) PhotoConfig {
|
||||||
// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes.
|
// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes.
|
||||||
func NewAudioUpload(chatID int, file interface{}) AudioConfig {
|
func NewAudioUpload(chatID int, file interface{}) AudioConfig {
|
||||||
return AudioConfig{
|
return AudioConfig{
|
||||||
ChatID: chatID,
|
BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, File: file, UseExisting: false},
|
||||||
UseExistingAudio: false,
|
|
||||||
File: file,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,9 +66,7 @@ func NewAudioUpload(chatID int, file interface{}) AudioConfig {
|
||||||
// chatID is where to send it, fileID is the ID of the audio already uploaded.
|
// chatID is where to send it, fileID is the ID of the audio already uploaded.
|
||||||
func NewAudioShare(chatID int, fileID string) AudioConfig {
|
func NewAudioShare(chatID int, fileID string) AudioConfig {
|
||||||
return AudioConfig{
|
return AudioConfig{
|
||||||
ChatID: chatID,
|
BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, FileID: fileID, UseExisting: true},
|
||||||
UseExistingAudio: true,
|
|
||||||
FileID: fileID,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,9 +77,7 @@ func NewAudioShare(chatID int, fileID string) AudioConfig {
|
||||||
// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes.
|
// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes.
|
||||||
func NewDocumentUpload(chatID int, file interface{}) DocumentConfig {
|
func NewDocumentUpload(chatID int, file interface{}) DocumentConfig {
|
||||||
return DocumentConfig{
|
return DocumentConfig{
|
||||||
ChatID: chatID,
|
BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, File: file, UseExisting: false},
|
||||||
UseExistingDocument: false,
|
|
||||||
File: file,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,9 +87,7 @@ func NewDocumentUpload(chatID int, file interface{}) DocumentConfig {
|
||||||
// chatID is where to send it, fileID is the ID of the document already uploaded.
|
// chatID is where to send it, fileID is the ID of the document already uploaded.
|
||||||
func NewDocumentShare(chatID int, fileID string) DocumentConfig {
|
func NewDocumentShare(chatID int, fileID string) DocumentConfig {
|
||||||
return DocumentConfig{
|
return DocumentConfig{
|
||||||
ChatID: chatID,
|
BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, FileID: fileID, UseExisting: true},
|
||||||
UseExistingDocument: true,
|
|
||||||
FileID: fileID,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,9 +97,7 @@ func NewDocumentShare(chatID int, fileID string) DocumentConfig {
|
||||||
// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes.
|
// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes.
|
||||||
func NewStickerUpload(chatID int, file interface{}) StickerConfig {
|
func NewStickerUpload(chatID int, file interface{}) StickerConfig {
|
||||||
return StickerConfig{
|
return StickerConfig{
|
||||||
ChatID: chatID,
|
BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, File: file, UseExisting: false},
|
||||||
UseExistingSticker: false,
|
|
||||||
File: file,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,9 +107,7 @@ func NewStickerUpload(chatID int, file interface{}) StickerConfig {
|
||||||
// chatID is where to send it, fileID is the ID of the sticker already uploaded.
|
// chatID is where to send it, fileID is the ID of the sticker already uploaded.
|
||||||
func NewStickerShare(chatID int, fileID string) StickerConfig {
|
func NewStickerShare(chatID int, fileID string) StickerConfig {
|
||||||
return StickerConfig{
|
return StickerConfig{
|
||||||
ChatID: chatID,
|
BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, FileID: fileID, UseExisting: true},
|
||||||
UseExistingSticker: true,
|
|
||||||
FileID: fileID,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,9 +118,7 @@ func NewStickerShare(chatID int, fileID string) StickerConfig {
|
||||||
// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes.
|
// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes.
|
||||||
func NewVideoUpload(chatID int, file interface{}) VideoConfig {
|
func NewVideoUpload(chatID int, file interface{}) VideoConfig {
|
||||||
return VideoConfig{
|
return VideoConfig{
|
||||||
ChatID: chatID,
|
BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, File: file, UseExisting: false},
|
||||||
UseExistingVideo: false,
|
|
||||||
File: file,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,9 +128,7 @@ func NewVideoUpload(chatID int, file interface{}) VideoConfig {
|
||||||
// chatID is where to send it, fileID is the ID of the video already uploaded.
|
// chatID is where to send it, fileID is the ID of the video already uploaded.
|
||||||
func NewVideoShare(chatID int, fileID string) VideoConfig {
|
func NewVideoShare(chatID int, fileID string) VideoConfig {
|
||||||
return VideoConfig{
|
return VideoConfig{
|
||||||
ChatID: chatID,
|
BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, FileID: fileID, UseExisting: true},
|
||||||
UseExistingVideo: true,
|
|
||||||
FileID: fileID,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,9 +139,7 @@ func NewVideoShare(chatID int, fileID string) VideoConfig {
|
||||||
// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes.
|
// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes.
|
||||||
func NewVoiceUpload(chatID int, file interface{}) VoiceConfig {
|
func NewVoiceUpload(chatID int, file interface{}) VoiceConfig {
|
||||||
return VoiceConfig{
|
return VoiceConfig{
|
||||||
ChatID: chatID,
|
BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, File: file, UseExisting: false},
|
||||||
UseExistingVoice: false,
|
|
||||||
File: file,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,9 +149,7 @@ func NewVoiceUpload(chatID int, file interface{}) VoiceConfig {
|
||||||
// chatID is where to send it, fileID is the ID of the video already uploaded.
|
// chatID is where to send it, fileID is the ID of the video already uploaded.
|
||||||
func NewVoiceShare(chatID int, fileID string) VoiceConfig {
|
func NewVoiceShare(chatID int, fileID string) VoiceConfig {
|
||||||
return VoiceConfig{
|
return VoiceConfig{
|
||||||
ChatID: chatID,
|
BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, FileID: fileID, UseExisting: true},
|
||||||
UseExistingVoice: true,
|
|
||||||
FileID: fileID,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,11 +159,9 @@ func NewVoiceShare(chatID int, fileID string) VoiceConfig {
|
||||||
// chatID is where to send it, latitude and longitude are coordinates.
|
// chatID is where to send it, latitude and longitude are coordinates.
|
||||||
func NewLocation(chatID int, latitude float64, longitude float64) LocationConfig {
|
func NewLocation(chatID int, latitude float64, longitude float64) LocationConfig {
|
||||||
return LocationConfig{
|
return LocationConfig{
|
||||||
ChatID: chatID,
|
BaseChat: BaseChat{ChatID: chatID, ReplyToMessageID: 0, ReplyMarkup: nil},
|
||||||
Latitude: latitude,
|
Latitude: latitude,
|
||||||
Longitude: longitude,
|
Longitude: longitude,
|
||||||
ReplyToMessageID: 0,
|
|
||||||
ReplyMarkup: nil,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,8 +171,8 @@ func NewLocation(chatID int, latitude float64, longitude float64) LocationConfig
|
||||||
// chatID is where to send it, action should be set via CHAT constants.
|
// chatID is where to send it, action should be set via CHAT constants.
|
||||||
func NewChatAction(chatID int, action string) ChatActionConfig {
|
func NewChatAction(chatID int, action string) ChatActionConfig {
|
||||||
return ChatActionConfig{
|
return ChatActionConfig{
|
||||||
ChatID: chatID,
|
BaseChat: BaseChat{ChatID: chatID},
|
||||||
Action: action,
|
Action: action,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,8 +206,7 @@ func NewWebhook(link string) WebhookConfig {
|
||||||
u, _ := url.Parse(link)
|
u, _ := url.Parse(link)
|
||||||
|
|
||||||
return WebhookConfig{
|
return WebhookConfig{
|
||||||
URL: u,
|
URL: u,
|
||||||
Clear: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,7 +219,6 @@ func NewWebhookWithCert(link string, file interface{}) WebhookConfig {
|
||||||
|
|
||||||
return WebhookConfig{
|
return WebhookConfig{
|
||||||
URL: u,
|
URL: u,
|
||||||
Clear: false,
|
|
||||||
Certificate: file,
|
Certificate: file,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1125
methods.go
1125
methods.go
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -0,0 +1,18 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIC0zCCAbugAwIBAgIJAPYfllX657axMA0GCSqGSIb3DQEBCwUAMAAwHhcNMTUx
|
||||||
|
MTIxMTExMDQxWhcNMjUwODIwMTExMDQxWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||||
|
AQ8AMIIBCgKCAQEAoMMSIIgYx8pT8Kz1O8Ukd/JVyqBQYRSo0enqEzo7295VROXq
|
||||||
|
TUthbEbdi0OczUfl4IsAWppOSRrDwEguJZ0cJ/r6IxGsbrCdQr2MjgiomYtAXKKQ
|
||||||
|
GAGL5Wls+AzcRNV0OszVJzkDNFYZzgNejyitGJSNEQMyU8r2gyPyIWP9MQKQst8y
|
||||||
|
Mg91R/7l9jwf6AWwNxykZlYZurtsQ6XsBPZpF9YOFL7vZYPhKUFiNEm+74RpojC7
|
||||||
|
Gt6nztYAUI2V/F+1uoXAr8nLpbj9SD0VSwyZLRG1uIVLBzhb0lfOIzAvJ45EKki9
|
||||||
|
nejyoXfH1U5+iMzdSAdcy3MCBhpEZwJPqhDqeQIDAQABo1AwTjAdBgNVHQ4EFgQU
|
||||||
|
JE0RLM+ohLnlDz0Qk0McCxtDK2MwHwYDVR0jBBgwFoAUJE0RLM+ohLnlDz0Qk0Mc
|
||||||
|
CxtDK2MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAEmgME00JYuYZ
|
||||||
|
4wNaGrJskZ05ZnP+TXJusmBui9ToQ4UoykuyY5rsdGQ3SdzXPLdmd2nfMsw63iK2
|
||||||
|
D7rjcH/rmn6fRccZqN0o0SXd/EuHeIoeW1Xnnivbt71b6mcOAeNg1UsMYxnMAVl0
|
||||||
|
ywdkta8gURltagSfXoUbqlnSxn/zCwqaxxcQXA/CnunvRsFtQrwWjDBPg/BPULHX
|
||||||
|
DEh2AactGtnGqEZ5iap/VCOVnmL6iPdJ1x5UIF/gS6U96wL+GHfcs1jCvPg+GEwR
|
||||||
|
3inh9oTXG9L21ge4lbGiPUIMBjtVcB3bXuQbOfec9Cr3ZhcQeZj680BIRxD/pNpA
|
||||||
|
O/XeCfjfkw==
|
||||||
|
-----END CERTIFICATE-----
|
Binary file not shown.
After Width: | Height: | Size: 84 KiB |
|
@ -0,0 +1,28 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQCgwxIgiBjHylPw
|
||||||
|
rPU7xSR38lXKoFBhFKjR6eoTOjvb3lVE5epNS2FsRt2LQ5zNR+XgiwBamk5JGsPA
|
||||||
|
SC4lnRwn+vojEaxusJ1CvYyOCKiZi0BcopAYAYvlaWz4DNxE1XQ6zNUnOQM0VhnO
|
||||||
|
A16PKK0YlI0RAzJTyvaDI/IhY/0xApCy3zIyD3VH/uX2PB/oBbA3HKRmVhm6u2xD
|
||||||
|
pewE9mkX1g4Uvu9lg+EpQWI0Sb7vhGmiMLsa3qfO1gBQjZX8X7W6hcCvyculuP1I
|
||||||
|
PRVLDJktEbW4hUsHOFvSV84jMC8njkQqSL2d6PKhd8fVTn6IzN1IB1zLcwIGGkRn
|
||||||
|
Ak+qEOp5AgMBAAECggEBAJ/dPCJzlEjhL5XPONLmGXzZ1Gx5/VR86eBMv0O9jhb3
|
||||||
|
wk2QYO3aPxggZGD/rGcKz1L6hzCR77WM0wpb/N/Um1I6pxHGmnU8VjYvLh10CM0f
|
||||||
|
h7JWyfnFV+ubagxFJamhpkJuvKyTaldaI7EU8qxj47Xky18Wka53z6nbTgXcW8Sm
|
||||||
|
V4CJy9OHNgKJQnylX6zOAaxVngSGde3xLslLjsYK4w9b2+OkCSUST2XXdo+ZLXxl
|
||||||
|
cs0lEPFRM1Xh9/E6UrDrJMHHzio53L/W/+a8sIar1upgSY52pyD/tA7VSrAJ9nYC
|
||||||
|
RezOU81VTLfMO+TYmgZzSUQJYh0cR4yqJe+wgl4U550CgYEA1EcS6Z+PO5Pr3u2+
|
||||||
|
XevawSAal6y9ONkkdOoASC977W37nn0E1wlQo41dR6DESCJfiSMeN0KbmXj5Wnc/
|
||||||
|
ADu+73iGwC90G9Qs9sjD7KAFBJvuj0V8hxvpWRdIBBbf7rlOj3CV0iXRYjkJbyJa
|
||||||
|
cxuR0kiv4gTWmm5Cq+5ir8t1Oc8CgYEAwd+xOaDerNR481R+QmENFw+oR2EVMq3Q
|
||||||
|
B/vinLK0PemQWrh32iBcd+vhSilOSQtUm1nko1jLK8C4s8X2vZYua4m5tcK9VqCt
|
||||||
|
maCCq/ffxzsoW/GN8japnduz+qA+hKWJzW/aYR8tsOeqzjVqj4iIqPI4HuokrDi/
|
||||||
|
UD/QLgq5UTcCgYEAk2ZC0Kx15dXB7AtDq63xOTcUoAtXXRkSgohV58npEKXVGWkQ
|
||||||
|
Kk0SjG7Fvc35XWlY0z3qZk6/AuOIqfOxcHUMEPatAtgwlH5RNo+T1EQNF/U6wotq
|
||||||
|
e9q6vp026XgEyJwt29Y+giy2ZrDaRywgiFs1d0H3t0bKyXMUopQmPJFXdesCgYEA
|
||||||
|
psCxXcDpZjxGX/zPsGZrbOdxtRtisTlg0k0rp93pO8tV90HtDHeDMT54g2ItzJPr
|
||||||
|
TMev6XOpJNPZyf6+8GhpOuO2EQkT85u2VYoCeslz95gBabvFfIzZrUZYcnw76bm8
|
||||||
|
YjAP5DN+CEfq2PyG0Df+W1ojPSvlKSCSJQMOG1vr81cCgYEAkjPY5WR99uJxYBNI
|
||||||
|
OTFMSkETgDUbPXBu/E/h5Dtn79v8Moj9FvC7+q6sg9qXhrGhfK2xDev3/sTrbS/E
|
||||||
|
Gcf8UNIne3AXsoAS8MtkOwJXHkYaTIboIYgDX4LlDmbGQlIRaWgyh2POI6VtjLBT
|
||||||
|
ms6AdsdpIB6As9xNUBUwj/RnTZQ=
|
||||||
|
-----END PRIVATE KEY-----
|
Binary file not shown.
Binary file not shown.
|
@ -1,92 +0,0 @@
|
||||||
// Package tgutils provides extra functions to make certain tasks easier.
|
|
||||||
package tgutils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/syfaro/telegram-bot-api"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var rand uint32
|
|
||||||
var randmu sync.Mutex
|
|
||||||
|
|
||||||
func reseed() uint32 {
|
|
||||||
return uint32(time.Now().UnixNano() + int64(os.Getpid()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func nextSuffix() string {
|
|
||||||
randmu.Lock()
|
|
||||||
r := rand
|
|
||||||
if r == 0 {
|
|
||||||
r = reseed()
|
|
||||||
}
|
|
||||||
r = r*1664525 + 1013904223 // constants from Numerical Recipes
|
|
||||||
rand = r
|
|
||||||
randmu.Unlock()
|
|
||||||
return strconv.Itoa(int(1e9 + r%1e9))[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// this function ripped from ioutils.TempFile, except with a suffix, instead of prefix.
|
|
||||||
func tempFileWithSuffix(dir, suffix string) (f *os.File, err error) {
|
|
||||||
if dir == "" {
|
|
||||||
dir = os.TempDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
nconflict := 0
|
|
||||||
for i := 0; i < 10000; i++ {
|
|
||||||
name := filepath.Join(dir, nextSuffix()+suffix)
|
|
||||||
f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
|
|
||||||
if os.IsExist(err) {
|
|
||||||
if nconflict++; nconflict > 10 {
|
|
||||||
randmu.Lock()
|
|
||||||
rand = reseed()
|
|
||||||
randmu.Unlock()
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodeAudio takes a file and attempts to convert it to a .ogg for Telegram.
|
|
||||||
// It then updates the path to the audio file in the AudioConfig.
|
|
||||||
//
|
|
||||||
// This function requires ffmpeg and opusenc to be installed on the system!
|
|
||||||
func EncodeAudio(audio *tgbotapi.AudioConfig) error {
|
|
||||||
f, err := tempFileWithSuffix(os.TempDir(), "_tgutils.ogg")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
ffmpegArgs := []string{
|
|
||||||
"-i",
|
|
||||||
audio.FilePath,
|
|
||||||
"-f",
|
|
||||||
"wav",
|
|
||||||
"-",
|
|
||||||
}
|
|
||||||
|
|
||||||
opusArgs := []string{
|
|
||||||
"--bitrate",
|
|
||||||
"256",
|
|
||||||
"-",
|
|
||||||
f.Name(),
|
|
||||||
}
|
|
||||||
|
|
||||||
c1 := exec.Command("ffmpeg", ffmpegArgs...)
|
|
||||||
c2 := exec.Command("opusenc", opusArgs...)
|
|
||||||
|
|
||||||
c2.Stdin, _ = c1.StdoutPipe()
|
|
||||||
c2.Stdout = os.Stdout
|
|
||||||
c2.Start()
|
|
||||||
c1.Run()
|
|
||||||
c2.Wait()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
11
types.go
11
types.go
|
@ -3,6 +3,7 @@ package tgbotapi
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -112,6 +113,16 @@ func (m *Message) IsGroup() bool {
|
||||||
return m.From.ID != m.Chat.ID
|
return m.From.ID != m.Chat.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCommand returns true if message starts from /
|
||||||
|
func (m *Message) IsCommand() bool {
|
||||||
|
return m.Text != "" && m.Text[0] == '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command returns first word from message
|
||||||
|
func (m *Message) Command() string {
|
||||||
|
return strings.Split(m.Text, " ")[0]
|
||||||
|
}
|
||||||
|
|
||||||
// PhotoSize contains information about photos, including ID and Width and Height.
|
// PhotoSize contains information about photos, including ID and Width and Height.
|
||||||
type PhotoSize struct {
|
type PhotoSize struct {
|
||||||
FileID string `json:"file_id"`
|
FileID string `json:"file_id"`
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
package tgbotapi_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Syfaro/telegram-bot-api"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserStringWith(t *testing.T) {
|
||||||
|
user := tgbotapi.User{0, "Test", "Test", ""}
|
||||||
|
|
||||||
|
if user.String() != "Test Test" {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserStringWithUserName(t *testing.T) {
|
||||||
|
user := tgbotapi.User{0, "Test", "Test", "@test"}
|
||||||
|
|
||||||
|
if user.String() != "@test" {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessageIsGroup(t *testing.T) {
|
||||||
|
from := tgbotapi.User{ID: 0}
|
||||||
|
chat := tgbotapi.Chat{ID: 10}
|
||||||
|
message := tgbotapi.Message{From: from, Chat: chat}
|
||||||
|
|
||||||
|
if message.IsGroup() != true {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessageTime(t *testing.T) {
|
||||||
|
message := tgbotapi.Message{Date: 0}
|
||||||
|
|
||||||
|
date := time.Unix(0, 0)
|
||||||
|
if message.Time() != date {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChatIsPrivate(t *testing.T) {
|
||||||
|
chat := tgbotapi.Chat{ID: 10, Type: "private"}
|
||||||
|
|
||||||
|
if chat.IsPrivate() != true {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChatIsGroup(t *testing.T) {
|
||||||
|
chat := tgbotapi.Chat{ID: 10, Type: "group"}
|
||||||
|
|
||||||
|
if chat.IsGroup() != true {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChatIsChannel(t *testing.T) {
|
||||||
|
chat := tgbotapi.Chat{ID: 10, Type: "channel"}
|
||||||
|
|
||||||
|
if chat.IsChannel() != true {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileLink(t *testing.T) {
|
||||||
|
file := tgbotapi.File{FilePath: "test/test.txt"}
|
||||||
|
|
||||||
|
if file.Link("token") != "https://api.telegram.org/file/bottoken/test/test.txt" {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
33
updates.go
33
updates.go
|
@ -1,33 +0,0 @@
|
||||||
package tgbotapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UpdatesChan starts a channel for getting updates.
|
|
||||||
func (bot *BotAPI) UpdatesChan(config UpdateConfig) error {
|
|
||||||
bot.Updates = make(chan Update, 100)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
updates, err := bot.GetUpdates(config)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
log.Println("Failed to get updates, retrying in 3 seconds...")
|
|
||||||
time.Sleep(time.Second * 3)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, update := range updates {
|
|
||||||
if update.UpdateID >= config.Offset {
|
|
||||||
config.Offset = update.UpdateID + 1
|
|
||||||
bot.Updates <- update
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
21
webhook.go
21
webhook.go
|
@ -1,21 +0,0 @@
|
||||||
package tgbotapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ListenForWebhook registers a http handler for a webhook.
|
|
||||||
func (bot *BotAPI) ListenForWebhook(pattern string) {
|
|
||||||
bot.Updates = make(chan Update, 100)
|
|
||||||
|
|
||||||
http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
bytes, _ := ioutil.ReadAll(r.Body)
|
|
||||||
|
|
||||||
var update Update
|
|
||||||
json.Unmarshal(bytes, &update)
|
|
||||||
|
|
||||||
bot.Updates <- update
|
|
||||||
})
|
|
||||||
}
|
|
Loading…
Reference in New Issue