From cf49e37751c63921e16bcb6582234e6fbfe980dd Mon Sep 17 00:00:00 2001 From: Marco Pracucci Date: Thu, 21 Apr 2022 19:04:51 +0200 Subject: [PATCH] Added Alertmanager Telegram support and blocked new OpsGenie api_key_file config option Signed-off-by: Marco Pracucci --- go.mod | 1 + go.sum | 1 + pkg/alertmanager/alertmanager.go | 4 + pkg/alertmanager/api.go | 30 +- pkg/alertmanager/api_test.go | 31 + pkg/alertmanager/multitenant_test.go | 17 + .../alertmanager/notify/telegram/telegram.go | 102 ++ vendor/gopkg.in/telebot.v3/.gitignore | 34 + vendor/gopkg.in/telebot.v3/LICENSE | 22 + vendor/gopkg.in/telebot.v3/README.md | 488 ++++++ vendor/gopkg.in/telebot.v3/admin.go | 312 ++++ vendor/gopkg.in/telebot.v3/api.go | 231 +++ vendor/gopkg.in/telebot.v3/bot.go | 1524 +++++++++++++++++ vendor/gopkg.in/telebot.v3/callbacks.go | 147 ++ vendor/gopkg.in/telebot.v3/chat.go | 268 +++ vendor/gopkg.in/telebot.v3/context.go | 471 +++++ vendor/gopkg.in/telebot.v3/editable.go | 30 + vendor/gopkg.in/telebot.v3/errors.go | 237 +++ vendor/gopkg.in/telebot.v3/file.go | 87 + vendor/gopkg.in/telebot.v3/games.go | 99 ++ vendor/gopkg.in/telebot.v3/inline.go | 139 ++ vendor/gopkg.in/telebot.v3/inline_types.go | 373 ++++ vendor/gopkg.in/telebot.v3/input_types.go | 73 + vendor/gopkg.in/telebot.v3/media.go | 342 ++++ vendor/gopkg.in/telebot.v3/message.go | 398 +++++ vendor/gopkg.in/telebot.v3/middleware.go | 22 + vendor/gopkg.in/telebot.v3/options.go | 352 ++++ vendor/gopkg.in/telebot.v3/payments.go | 132 ++ vendor/gopkg.in/telebot.v3/payments_data.go | 3 + vendor/gopkg.in/telebot.v3/poller.go | 117 ++ vendor/gopkg.in/telebot.v3/polls.go | 62 + vendor/gopkg.in/telebot.v3/sendable.go | 431 +++++ vendor/gopkg.in/telebot.v3/stickers.go | 170 ++ vendor/gopkg.in/telebot.v3/telebot.go | 242 +++ vendor/gopkg.in/telebot.v3/util.go | 293 ++++ vendor/gopkg.in/telebot.v3/voice.go | 29 + vendor/gopkg.in/telebot.v3/webhook.go | 197 +++ vendor/modules.txt | 4 + 38 files changed, 7509 insertions(+), 6 deletions(-) create mode 100644 vendor/github.com/prometheus/alertmanager/notify/telegram/telegram.go create mode 100644 vendor/gopkg.in/telebot.v3/.gitignore create mode 100644 vendor/gopkg.in/telebot.v3/LICENSE create mode 100644 vendor/gopkg.in/telebot.v3/README.md create mode 100644 vendor/gopkg.in/telebot.v3/admin.go create mode 100644 vendor/gopkg.in/telebot.v3/api.go create mode 100644 vendor/gopkg.in/telebot.v3/bot.go create mode 100644 vendor/gopkg.in/telebot.v3/callbacks.go create mode 100644 vendor/gopkg.in/telebot.v3/chat.go create mode 100644 vendor/gopkg.in/telebot.v3/context.go create mode 100644 vendor/gopkg.in/telebot.v3/editable.go create mode 100644 vendor/gopkg.in/telebot.v3/errors.go create mode 100644 vendor/gopkg.in/telebot.v3/file.go create mode 100644 vendor/gopkg.in/telebot.v3/games.go create mode 100644 vendor/gopkg.in/telebot.v3/inline.go create mode 100644 vendor/gopkg.in/telebot.v3/inline_types.go create mode 100644 vendor/gopkg.in/telebot.v3/input_types.go create mode 100644 vendor/gopkg.in/telebot.v3/media.go create mode 100644 vendor/gopkg.in/telebot.v3/message.go create mode 100644 vendor/gopkg.in/telebot.v3/middleware.go create mode 100644 vendor/gopkg.in/telebot.v3/options.go create mode 100644 vendor/gopkg.in/telebot.v3/payments.go create mode 100644 vendor/gopkg.in/telebot.v3/payments_data.go create mode 100644 vendor/gopkg.in/telebot.v3/poller.go create mode 100644 vendor/gopkg.in/telebot.v3/polls.go create mode 100644 vendor/gopkg.in/telebot.v3/sendable.go create mode 100644 vendor/gopkg.in/telebot.v3/stickers.go create mode 100644 vendor/gopkg.in/telebot.v3/telebot.go create mode 100644 vendor/gopkg.in/telebot.v3/util.go create mode 100644 vendor/gopkg.in/telebot.v3/voice.go create mode 100644 vendor/gopkg.in/telebot.v3/webhook.go diff --git a/go.mod b/go.mod index 7a2223a635..e260c30de4 100644 --- a/go.mod +++ b/go.mod @@ -217,6 +217,7 @@ require ( google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/ini.v1 v1.57.0 // indirect + gopkg.in/telebot.v3 v3.0.0 // indirect ) // Override since git.apache.org is down. The docs say to fetch from github. diff --git a/go.sum b/go.sum index fc4bb765a4..a1a619c858 100644 --- a/go.sum +++ b/go.sum @@ -2757,6 +2757,7 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/telebot.v3 v3.0.0 h1:UgHIiE/RdjoDi6nf4xACM7PU3TqiPVV9vvTydCEnrTo= gopkg.in/telebot.v3 v3.0.0/go.mod h1:7rExV8/0mDDNu9epSrDm/8j22KLaActH1Tbee6YjzWg= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/pkg/alertmanager/alertmanager.go b/pkg/alertmanager/alertmanager.go index a947118af3..a9a68a91da 100644 --- a/pkg/alertmanager/alertmanager.go +++ b/pkg/alertmanager/alertmanager.go @@ -36,6 +36,7 @@ import ( "github.com/prometheus/alertmanager/notify/pushover" "github.com/prometheus/alertmanager/notify/slack" "github.com/prometheus/alertmanager/notify/sns" + "github.com/prometheus/alertmanager/notify/telegram" "github.com/prometheus/alertmanager/notify/victorops" "github.com/prometheus/alertmanager/notify/webhook" "github.com/prometheus/alertmanager/notify/wechat" @@ -494,6 +495,9 @@ func buildReceiverIntegrations(nc *config.Receiver, tmpl *template.Template, fir for i, c := range nc.SNSConfigs { add("sns", i, c, func(l log.Logger) (notify.Notifier, error) { return sns.New(c, tmpl, l, httpOps...) }) } + for i, c := range nc.TelegramConfigs { + add("telegram", i, c, func(l log.Logger) (notify.Notifier, error) { return telegram.New(c, tmpl, l, httpOps...) }) + } // If we add support for more integrations, we need to add them to validation as well. See validation.allowedIntegrationNames field. if errs.Len() > 0 { return nil, &errs diff --git a/pkg/alertmanager/api.go b/pkg/alertmanager/api.go index 55c7d975c6..510344b30f 100644 --- a/pkg/alertmanager/api.go +++ b/pkg/alertmanager/api.go @@ -47,12 +47,13 @@ const ( ) var ( - errPasswordFileNotAllowed = errors.New("setting password_file, bearer_token_file and credentials_file is not allowed") - errOAuth2SecretFileNotAllowed = errors.New("setting OAuth2 client_secret_file is not allowed") - errProxyURLNotAllowed = errors.New("setting proxy_url is not allowed") - errTLSFileNotAllowed = errors.New("setting TLS ca_file, cert_file and key_file is not allowed") - errSlackAPIURLFileNotAllowed = errors.New("setting Slack api_url_file and global slack_api_url_file is not allowed") - errVictorOpsAPIKeyFileNotAllowed = errors.New("setting VictorOps api_key_file is not allowed") + errPasswordFileNotAllowed = errors.New("setting password_file, bearer_token_file and credentials_file is not allowed") + errOAuth2SecretFileNotAllowed = errors.New("setting OAuth2 client_secret_file is not allowed") + errProxyURLNotAllowed = errors.New("setting proxy_url is not allowed") + errTLSFileNotAllowed = errors.New("setting TLS ca_file, cert_file and key_file is not allowed") + errSlackAPIURLFileNotAllowed = errors.New("setting Slack api_url_file and global slack_api_url_file is not allowed") + errVictorOpsAPIKeyFileNotAllowed = errors.New("setting VictorOps api_key_file is not allowed") + errOpsGenieAPIKeyFileFileNotAllowed = errors.New("setting OpsGenie api_key_file and global opsgenie_api_key_file is not allowed") ) // UserConfig is used to communicate a users alertmanager configs @@ -354,6 +355,11 @@ func validateAlertmanagerConfig(cfg interface{}) error { return err } + case reflect.TypeOf(config.OpsGenieConfig{}): + if err := validateOpsGenieConfig(v.Interface().(config.OpsGenieConfig)); err != nil { + return err + } + case reflect.TypeOf(config.VictorOpsConfig{}): if err := validateVictorOpsConfig(v.Interface().(config.VictorOpsConfig)); err != nil { return err @@ -440,6 +446,9 @@ func validateGlobalConfig(cfg config.GlobalConfig) error { if cfg.SlackAPIURLFile != "" { return errSlackAPIURLFileNotAllowed } + if cfg.OpsGenieAPIKeyFile != "" { + return errOpsGenieAPIKeyFileFileNotAllowed + } return nil } @@ -460,3 +469,12 @@ func validateVictorOpsConfig(cfg config.VictorOpsConfig) error { } return nil } + +// validateOpsGenieConfig validates the OpsGenie config and returns an error if it contains +// settings now allowed by Mimir. +func validateOpsGenieConfig(cfg config.OpsGenieConfig) error { + if cfg.APIKeyFile != "" { + return errOpsGenieAPIKeyFileFileNotAllowed + } + return nil +} diff --git a/pkg/alertmanager/api_test.go b/pkg/alertmanager/api_test.go index 892dc95866..ab5e054588 100644 --- a/pkg/alertmanager/api_test.go +++ b/pkg/alertmanager/api_test.go @@ -442,6 +442,37 @@ alertmanager_config: | `, err: errors.Wrap(errSlackAPIURLFileNotAllowed, "error validating Alertmanager config"), }, + { + name: "Should return error if global opsgenie_api_key_file is set", + cfg: ` +alertmanager_config: | + global: + opsgenie_api_key_file: /secrets + + receivers: + - name: default-receiver + webhook_configs: + - url: http://localhost + + route: + receiver: 'default-receiver' +`, + err: errors.Wrap(errOpsGenieAPIKeyFileFileNotAllowed, "error validating Alertmanager config"), + }, + { + name: "Should return error if OpsGenie api_key_file is set", + cfg: ` +alertmanager_config: | + receivers: + - name: default-receiver + opsgenie_configs: + - api_key_file: /secrets + + route: + receiver: 'default-receiver' +`, + err: errors.Wrap(errOpsGenieAPIKeyFileFileNotAllowed, "error validating Alertmanager config"), + }, { name: "Should return error if VictorOps api_key_file is set", cfg: ` diff --git a/pkg/alertmanager/multitenant_test.go b/pkg/alertmanager/multitenant_test.go index 0603649fd3..4700006bba 100644 --- a/pkg/alertmanager/multitenant_test.go +++ b/pkg/alertmanager/multitenant_test.go @@ -455,6 +455,23 @@ receivers: region: us-east-1 access_key: xxx secret_key: xxx +`, backendURL) + }, + }, + "telegram": { + getAlertmanagerConfig: func(backendURL string) string { + return fmt.Sprintf(` +route: + receiver: telegram + group_wait: 0s + group_interval: 1s + +receivers: + - name: telegram + telegram_configs: + - api_url: %s + bot_token: xxx + chat_id: 111 `, backendURL) }, }, diff --git a/vendor/github.com/prometheus/alertmanager/notify/telegram/telegram.go b/vendor/github.com/prometheus/alertmanager/notify/telegram/telegram.go new file mode 100644 index 0000000000..0977cdc672 --- /dev/null +++ b/vendor/github.com/prometheus/alertmanager/notify/telegram/telegram.go @@ -0,0 +1,102 @@ +// Copyright 2022 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package telegram + +import ( + "context" + "net/http" + + "github.com/go-kit/log" + "gopkg.in/telebot.v3" + + "github.com/prometheus/alertmanager/template" + + "github.com/go-kit/log/level" + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/types" + commoncfg "github.com/prometheus/common/config" +) + +// Notifier implements a Notifier for telegram notifications. +type Notifier struct { + conf *config.TelegramConfig + tmpl *template.Template + logger log.Logger + client *telebot.Bot + retrier *notify.Retrier +} + +// New returns a new Telegram notification handler. +func New(conf *config.TelegramConfig, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) { + httpclient, err := commoncfg.NewClientFromConfig(*conf.HTTPConfig, "telegram", httpOpts...) + if err != nil { + return nil, err + } + + client, err := createTelegramClient(conf.BotToken, conf.APIUrl.String(), conf.ParseMode, httpclient) + if err != nil { + return nil, err + } + + return &Notifier{ + conf: conf, + tmpl: t, + logger: l, + client: client, + retrier: ¬ify.Retrier{}, + }, nil +} + +func (n *Notifier) Notify(ctx context.Context, alert ...*types.Alert) (bool, error) { + var ( + err error + data = notify.GetTemplateData(ctx, n.tmpl, alert, n.logger) + tmpl = notify.TmplText(n.tmpl, data, &err) + ) + + // Telegram supports 4096 chars max + messageText, truncated := notify.Truncate(tmpl(n.conf.Message), 4096) + if truncated { + level.Debug(n.logger).Log("msg", "truncated message", "truncated_message", messageText) + } + + message, err := n.client.Send(telebot.ChatID(n.conf.ChatID), messageText, &telebot.SendOptions{ + DisableNotification: n.conf.DisableNotifications, + DisableWebPagePreview: true, + }) + if err != nil { + return true, err + } + level.Debug(n.logger).Log("msg", "Telegram message successfully published", "message_id", message.ID, "chat_id", message.Chat.ID) + + return false, nil +} + +func createTelegramClient(token config.Secret, apiUrl, parseMode string, httpClient *http.Client) (*telebot.Bot, error) { + secret := string(token) + bot, err := telebot.NewBot(telebot.Settings{ + Token: secret, + URL: apiUrl, + ParseMode: parseMode, + Client: httpClient, + Offline: true, + }) + + if err != nil { + return nil, err + } + + return bot, nil +} diff --git a/vendor/gopkg.in/telebot.v3/.gitignore b/vendor/gopkg.in/telebot.v3/.gitignore new file mode 100644 index 0000000000..c81da31dbe --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/.gitignore @@ -0,0 +1,34 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.idea +.DS_Store +coverage.txt + +# Terraform artifacts +*.zip +.terraform* +terraform* +/examples/awslambdaechobot/awslambdaechobot diff --git a/vendor/gopkg.in/telebot.v3/LICENSE b/vendor/gopkg.in/telebot.v3/LICENSE new file mode 100644 index 0000000000..2965b8423b --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 llya Kowalewski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/gopkg.in/telebot.v3/README.md b/vendor/gopkg.in/telebot.v3/README.md new file mode 100644 index 0000000000..89e509d42d --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/README.md @@ -0,0 +1,488 @@ +# Telebot +>"I never knew creating Telegram bots could be so _sexy_!" + +[![GoDoc](https://godoc.org/gopkg.in/telebot.v3?status.svg)](https://godoc.org/gopkg.in/telebot.v3) +[![GitHub Actions](https://github.com/tucnak/telebot/actions/workflows/go.yml/badge.svg)](https://github.com/tucnak/telebot/actions) +[![codecov.io](https://codecov.io/gh/tucnak/telebot/coverage.svg?branch=v3)](https://codecov.io/gh/tucnak/telebot) +[![Discuss on Telegram](https://img.shields.io/badge/telegram-discuss-0088cc.svg)](https://t.me/go_telebot) + +```bash +go get -u gopkg.in/telebot.v3 +``` + +* [Overview](#overview) +* [Getting Started](#getting-started) + - [Context](#context) + - [Middleware](#middleware) + - [Poller](#poller) + - [Commands](#commands) + - [Files](#files) + - [Sendable](#sendable) + - [Editable](#editable) + - [Keyboards](#keyboards) + - [Inline mode](#inline-mode) +* [Contributing](#contributing) +* [Donate](#donate) +* [License](#license) + +# Overview +Telebot is a bot framework for [Telegram Bot API](https://core.telegram.org/bots/api). +This package provides the best of its kind API for command routing, inline query requests and keyboards, as well +as callbacks. Actually, I went a couple steps further, so instead of making a 1:1 API wrapper I chose to focus on +the beauty of API and performance. Some strong sides of Telebot are: + +* Real concise API +* Command routing +* Middleware +* Transparent File API +* Effortless bot callbacks + +All the methods of Telebot API are _extremely_ easy to memorize and get used to. Also, consider Telebot a +highload-ready solution. I'll test and benchmark the most popular actions and if necessary, optimize +against them without sacrificing API quality. + +# Getting Started +Let's take a look at the minimal Telebot setup: + +```go +package main + +import ( + "log" + "os" + "time" + + tele "gopkg.in/telebot.v3" +) + +func main() { + pref := tele.Settings{ + Token: os.Getenv("TOKEN"), + Poller: &tele.LongPoller{Timeout: 10 * time.Second}, + } + + b, err := tele.NewBot(pref) + if err != nil { + log.Fatal(err) + return + } + + b.Handle("/hello", func(c tele.Context) error { + return c.Send("Hello!") + }) + + b.Start() +} + +``` + +Simple, innit? Telebot's routing system takes care of delivering updates +to their endpoints, so in order to get to handle any meaningful event, +all you got to do is just plug your function into one of the Telebot-provided +endpoints. You can find the full list +[here](https://godoc.org/gopkg.in/tucnak/telebot.v3#pkg-constants). + +There are dozens of supported endpoints (see package consts). Let me know +if you'd like to see some endpoint or endpoint ideas implemented. This system +is completely extensible, so I can introduce them without breaking +backwards compatibility. + +## Context +Context is a special type that wraps a huge update structure and represents +the context of the current event. It provides several helpers, which allow +getting, for example, the chat that this update had been sent in, no matter +what kind of update this is. + +```go +b.Handle(tele.OnText, func(c tele.Context) error { + // All the text messages that weren't + // captured by existing handlers. + + var ( + user = c.Sender() + text = c.Text() + ) + + // Use full-fledged bot's functions + // only if you need a result: + msg, err := b.Send(user, text) + if err != nil { + return err + } + + // Instead, prefer a context short-hand: + return c.Send(text) +}) + +b.Handle(tele.OnChannelPost, func(c tele.Context) error { + // Channel posts only. + msg := c.Message() +}) + +b.Handle(tele.OnPhoto, func(c tele.Context) error { + // Photos only. + photo := c.Message().Photo +}) + +b.Handle(tele.OnQuery, func(c tele.Context) error { + // Incoming inline queries. + return c.Answer(...) +}) +``` + +## Middleware +Telebot has a simple and recognizable way to set up middleware — chained functions with access to `Context`, called before the handler execution. + +Import a `middleware` package to get some basic out-of-box middleware +implementations: +```go +import "gopkg.in/telebot.v3/middleware" +``` + +```go +// Global-scoped middleware: +b.Use(middleware.Logger()) +b.Use(middleware.AutoRespond()) + +// Group-scoped middleware: +adminOnly := b.Group(middleware.Whitelist(adminIDs...)) +adminOnly.Handle("/ban", onBan) +adminOnly.Handle("/kick", onKick) + +// Handler-scoped middleware: +b.Handle(tele.OnText, onText, middleware.IgnoreVia()) +``` + +Custom middleware example: +```go +// AutoResponder automatically responds to every callback update. +func AutoResponder(next tele.HandlerFunc) tele.HandlerFunc { + return func(c tele.Context) error { + if c.Callback() != nil { + defer c.Respond() + } + return next(c) // continue execution chain + } +} +``` + +## Poller +Telebot doesn't really care how you provide it with incoming updates, as long +as you set it up with a Poller, or call ProcessUpdate for each update: + +```go +// Poller is a provider of Updates. +// +// All pollers must implement Poll(), which accepts bot +// pointer and subscription channel and start polling +// synchronously straight away. +type Poller interface { + // Poll is supposed to take the bot object + // subscription channel and start polling + // for Updates immediately. + // + // Poller must listen for stop constantly and close + // it as soon as it's done polling. + Poll(b *Bot, updates chan Update, stop chan struct{}) +} +``` + +## Commands +When handling commands, Telebot supports both direct (`/command`) and group-like +syntax (`/command@botname`) and will never deliver messages addressed to some +other bot, even if [privacy mode](https://core.telegram.org/bots#privacy-mode) is off. + +For simplified deep-linking, Telebot also extracts payload: +```go +// Command: /start +b.Handle("/start", func(c tele.Context) error { + fmt.Println(c.Message().Payload) // +}) +``` + +For multiple arguments use: +```go +// Command: /tags <...> +b.Handle("/tags", func(c tele.Context) error { + tags := c.Args() // list of arguments splitted by a space + for _, tag := range tags { + // iterate through passed arguments + } +}) +``` + +## Files +>Telegram allows files up to 50 MB in size. + +Telebot allows to both upload (from disk or by URL) and download (from Telegram) +files in bot's scope. Also, sending any kind of media with a File created +from disk will upload the file to Telegram automatically: +```go +a := &tele.Audio{File: tele.FromDisk("file.ogg")} + +fmt.Println(a.OnDisk()) // true +fmt.Println(a.InCloud()) // false + +// Will upload the file from disk and send it to the recipient +b.Send(recipient, a) + +// Next time you'll be sending this very *Audio, Telebot won't +// re-upload the same file but rather utilize its Telegram FileID +b.Send(otherRecipient, a) + +fmt.Println(a.OnDisk()) // true +fmt.Println(a.InCloud()) // true +fmt.Println(a.FileID) // +``` + +You might want to save certain `File`s in order to avoid re-uploading. Feel free +to marshal them into whatever format, `File` only contain public fields, so no +data will ever be lost. + +## Sendable +Send is undoubtedly the most important method in Telebot. `Send()` accepts a +`Recipient` (could be user, group or a channel) and a `Sendable`. Other types other than +the Telebot-provided media types (`Photo`, `Audio`, `Video`, etc.) are `Sendable`. +If you create composite types of your own, and they satisfy the `Sendable` interface, +Telebot will be able to send them out. + +```go +// Sendable is any object that can send itself. +// +// This is pretty cool, since it lets bots implement +// custom Sendables for complex kinds of media or +// chat objects spanning across multiple messages. +type Sendable interface { + Send(*Bot, Recipient, *SendOptions) (*Message, error) +} +``` + +The only type at the time that doesn't fit `Send()` is `Album` and there is a reason +for that. Albums were added not so long ago, so they are slightly quirky for backwards +compatibilities sake. In fact, an `Album` can be sent, but never received. Instead, +Telegram returns a `[]Message`, one for each media object in the album: +```go +p := &tele.Photo{File: tele.FromDisk("chicken.jpg")} +v := &tele.Video{File: tele.FromURL("http://video.mp4")} + +msgs, err := b.SendAlbum(user, tele.Album{p, v}) +``` + +### Send options +Send options are objects and flags you can pass to `Send()`, `Edit()` and friends +as optional arguments (following the recipient and the text/media). The most +important one is called `SendOptions`, it lets you control _all_ the properties of +the message supported by Telegram. The only drawback is that it's rather +inconvenient to use at times, so `Send()` supports multiple shorthands: +```go +// regular send options +b.Send(user, "text", &tele.SendOptions{ + // ... +}) + +// ReplyMarkup is a part of SendOptions, +// but often it's the only option you need +b.Send(user, "text", &tele.ReplyMarkup{ + // ... +}) + +// flags: no notification && no web link preview +b.Send(user, "text", tele.Silent, tele.NoPreview) +``` + +Full list of supported option-flags you can find +[here](https://pkg.go.dev/gopkg.in/tucnak/telebot.v3#Option). + +## Editable +If you want to edit some existing message, you don't really need to store the +original `*Message` object. In fact, upon edit, Telegram only requires `chat_id` +and `message_id`. So you don't really need the Message as a whole. Also, you +might want to store references to certain messages in the database, so I thought +it made sense for *any* Go struct to be editable as a Telegram message, to implement +`Editable`: +```go +// Editable is an interface for all objects that +// provide "message signature", a pair of 32-bit +// message ID and 64-bit chat ID, both required +// for edit operations. +// +// Use case: DB model struct for messages to-be +// edited with, say two columns: msg_id,chat_id +// could easily implement MessageSig() making +// instances of stored messages editable. +type Editable interface { + // MessageSig is a "message signature". + // + // For inline messages, return chatID = 0. + MessageSig() (messageID int, chatID int64) +} +``` + +For example, `Message` type is Editable. Here is the implementation of `StoredMessage` +type, provided by Telebot: +```go +// StoredMessage is an example struct suitable for being +// stored in the database as-is or being embedded into +// a larger struct, which is often the case (you might +// want to store some metadata alongside, or might not.) +type StoredMessage struct { + MessageID int `sql:"message_id" json:"message_id"` + ChatID int64 `sql:"chat_id" json:"chat_id"` +} + +func (x StoredMessage) MessageSig() (int, int64) { + return x.MessageID, x.ChatID +} +``` + +Why bother at all? Well, it allows you to do things like this: +```go +// just two integer columns in the database +var msgs []tele.StoredMessage +db.Find(&msgs) // gorm syntax + +for _, msg := range msgs { + bot.Edit(&msg, "Updated text") + // or + bot.Delete(&msg) +} +``` + +I find it incredibly neat. Worth noting, at this point of time there exists +another method in the Edit family, `EditCaption()` which is of a pretty +rare use, so I didn't bother including it to `Edit()`, just like I did with +`SendAlbum()` as it would inevitably lead to unnecessary complications. +```go +var m *Message + +// change caption of a photo, audio, etc. +bot.EditCaption(m, "new caption") +``` + +## Keyboards +Telebot supports both kinds of keyboards Telegram provides: reply and inline +keyboards. Any button can also act as endpoints for `Handle()`. + +```go +var ( + // Universal markup builders. + menu = &tele.ReplyMarkup{ResizeKeyboard: true} + selector = &tele.ReplyMarkup{} + + // Reply buttons. + btnHelp = menu.Text("ℹ Help") + btnSettings = menu.Text("⚙ Settings") + + // Inline buttons. + // + // Pressing it will cause the client to + // send the bot a callback. + // + // Make sure Unique stays unique as per button kind + // since it's required for callback routing to work. + // + btnPrev = selector.Data("⬅", "prev", ...) + btnNext = selector.Data("➡", "next", ...) +) + +menu.Reply( + menu.Row(btnHelp), + menu.Row(btnSettings), +) +selector.Inline( + selector.Row(btnPrev, btnNext), +) + +b.Handle("/start", func(c tele.Context) error { + return c.Send("Hello!", menu) +}) + +// On reply button pressed (message) +b.Handle(&btnHelp, func(c tele.Context) error { + return c.Edit("Here is some help: ...") +}) + +// On inline button pressed (callback) +b.Handle(&btnPrev, func(c tele.Context) error { + return c.Respond() +}) +``` + +You can use markup constructor for every type of possible button: +```go +r := b.NewMarkup() + +// Reply buttons: +r.Text("Hello!") +r.Contact("Send phone number") +r.Location("Send location") +r.Poll(tele.PollQuiz) + +// Inline buttons: +r.Data("Show help", "help") // data is optional +r.Data("Delete item", "delete", item.ID) +r.URL("Visit", "https://google.com") +r.Query("Search", query) +r.QueryChat("Share", query) +r.Login("Login", &tele.Login{...}) +``` + +## Inline mode +So if you want to handle incoming inline queries you better plug the `tele.OnQuery` +endpoint and then use the `Answer()` method to send a list of inline queries +back. I think at the time of writing, Telebot supports all of the provided result +types (but not the cached ones). This is what it looks like: + +```go +b.Handle(tele.OnQuery, func(c tele.Context) error { + urls := []string{ + "http://photo.jpg", + "http://photo2.jpg", + } + + results := make(tele.Results, len(urls)) // []tele.Result + for i, url := range urls { + result := &tele.PhotoResult{ + URL: url, + ThumbURL: url, // required for photos + } + + results[i] = result + // needed to set a unique string ID for each result + results[i].SetResultID(strconv.Itoa(i)) + } + + return c.Answer(&tele.QueryResponse{ + Results: results, + CacheTime: 60, // a minute + }) +}) +``` + +There's not much to talk about really. It also supports some form of authentication +through deep-linking. For that, use fields `SwitchPMText` and `SwitchPMParameter` +of `QueryResponse`. + +# Contributing + +1. Fork it +2. Clone v3: `git clone -b v3 https://github.com/tucnak/telebot` +3. Create your feature branch: `git checkout -b v3-feature` +4. Make changes and add them: `git add .` +5. Commit: `git commit -m "add some feature"` +6. Push: `git push origin v3-feature` +7. Pull request + +# Donate + +I do coding for fun, but I also try to search for interesting solutions and +optimize them as much as possible. +If you feel like it's a good piece of software, I wouldn't mind a tip! + +Litecoin: `ltc1qskt5ltrtyg7esfjm0ftx6jnacwffhpzpqmerus` + +Ethereum: `0xB78A2Ac1D83a0aD0b993046F9fDEfC5e619efCAB` + +# License + +Telebot is distributed under MIT. diff --git a/vendor/gopkg.in/telebot.v3/admin.go b/vendor/gopkg.in/telebot.v3/admin.go new file mode 100644 index 0000000000..dce92bc500 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/admin.go @@ -0,0 +1,312 @@ +package telebot + +import ( + "encoding/json" + "strconv" + "time" +) + +// ChatInviteLink object represents an invite for a chat. +type ChatInviteLink struct { + // The invite link. + InviteLink string `json:"invite_link"` + + // Invite link name. + Name string `json:"name"` + + // The creator of the link. + Creator *User `json:"creator"` + + // If the link is primary. + IsPrimary bool `json:"is_primary"` + + // If the link is revoked. + IsRevoked bool `json:"is_revoked"` + + // (Optional) Point in time when the link will expire, + // use ExpireDate() to get time.Time. + ExpireUnixtime int64 `json:"expire_date,omitempty"` + + // (Optional) Maximum number of users that can be members of + // the chat simultaneously. + MemberLimit int `json:"member_limit,omitempty"` + + // (Optional) True, if users joining the chat via the link need to + // be approved by chat administrators. If True, member_limit can't be specified. + JoinRequest bool `json:"creates_join_request"` + + // (Optional) Number of pending join requests created using this link. + PendingCount int `json:"pending_join_request_count"` +} + +// ExpireDate returns the moment of the link expiration in local time. +func (c *ChatInviteLink) ExpireDate() time.Time { + return time.Unix(c.ExpireUnixtime, 0) +} + +// ChatMemberUpdate object represents changes in the status of a chat member. +type ChatMemberUpdate struct { + // Chat where the user belongs to. + Chat *Chat `json:"chat"` + + // Sender which user the action was triggered. + Sender *User `json:"from"` + + // Unixtime, use Date() to get time.Time. + Unixtime int64 `json:"date"` + + // Previous information about the chat member. + OldChatMember *ChatMember `json:"old_chat_member"` + + // New information about the chat member. + NewChatMember *ChatMember `json:"new_chat_member"` + + // (Optional) InviteLink which was used by the user to + // join the chat; for joining by invite link events only. + InviteLink *ChatInviteLink `json:"invite_link"` +} + +// Time returns the moment of the change in local time. +func (c *ChatMemberUpdate) Time() time.Time { + return time.Unix(c.Unixtime, 0) +} + +// Rights is a list of privileges available to chat members. +type Rights struct { + CanBeEdited bool `json:"can_be_edited"` + CanChangeInfo bool `json:"can_change_info"` + CanPostMessages bool `json:"can_post_messages"` + CanEditMessages bool `json:"can_edit_messages"` + CanDeleteMessages bool `json:"can_delete_messages"` + CanInviteUsers bool `json:"can_invite_users"` + CanRestrictMembers bool `json:"can_restrict_members"` + CanPinMessages bool `json:"can_pin_messages"` + CanPromoteMembers bool `json:"can_promote_members"` + CanSendMessages bool `json:"can_send_messages"` + CanSendMedia bool `json:"can_send_media_messages"` + CanSendPolls bool `json:"can_send_polls"` + CanSendOther bool `json:"can_send_other_messages"` + CanAddPreviews bool `json:"can_add_web_page_previews"` + CanManageVoiceChats bool `json:"can_manage_voice_chats"` + CanManageChat bool `json:"can_manage_chat"` +} + +// NoRights is the default Rights{}. +func NoRights() Rights { return Rights{} } + +// NoRestrictions should be used when un-restricting or +// un-promoting user. +// +// member.Rights = tele.NoRestrictions() +// b.Restrict(chat, member) +// +func NoRestrictions() Rights { + return Rights{ + CanBeEdited: true, + CanChangeInfo: false, + CanPostMessages: false, + CanEditMessages: false, + CanDeleteMessages: false, + CanInviteUsers: false, + CanRestrictMembers: false, + CanPinMessages: false, + CanPromoteMembers: false, + CanSendMessages: true, + CanSendMedia: true, + CanSendPolls: true, + CanSendOther: true, + CanAddPreviews: true, + CanManageVoiceChats: false, + CanManageChat: false, + } +} + +// AdminRights could be used to promote user to admin. +func AdminRights() Rights { + return Rights{ + CanBeEdited: true, + CanChangeInfo: true, + CanPostMessages: true, + CanEditMessages: true, + CanDeleteMessages: true, + CanInviteUsers: true, + CanRestrictMembers: true, + CanPinMessages: true, + CanPromoteMembers: true, + CanSendMessages: true, + CanSendMedia: true, + CanSendPolls: true, + CanSendOther: true, + CanAddPreviews: true, + CanManageVoiceChats: true, + CanManageChat: true, + } +} + +// Forever is a ExpireUnixtime of "forever" banning. +func Forever() int64 { + return time.Now().Add(367 * 24 * time.Hour).Unix() +} + +// Ban will ban user from chat until `member.RestrictedUntil`. +func (b *Bot) Ban(chat *Chat, member *ChatMember, revokeMessages ...bool) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": member.User.Recipient(), + "until_date": strconv.FormatInt(member.RestrictedUntil, 10), + } + if len(revokeMessages) > 0 { + params["revoke_messages"] = strconv.FormatBool(revokeMessages[0]) + } + + _, err := b.Raw("kickChatMember", params) + return err +} + +// Unban will unban user from chat, who would have thought eh? +// forBanned does nothing if the user is not banned. +func (b *Bot) Unban(chat *Chat, user *User, forBanned ...bool) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": user.Recipient(), + } + + if len(forBanned) > 0 { + params["only_if_banned"] = strconv.FormatBool(forBanned[0]) + } + + _, err := b.Raw("unbanChatMember", params) + return err +} + +// Restrict lets you restrict a subset of member's rights until +// member.RestrictedUntil, such as: +// +// * can send messages +// * can send media +// * can send other +// * can add web page previews +// +func (b *Bot) Restrict(chat *Chat, member *ChatMember) error { + prv, until := member.Rights, member.RestrictedUntil + + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "user_id": member.User.Recipient(), + "until_date": strconv.FormatInt(until, 10), + } + embedRights(params, prv) + + _, err := b.Raw("restrictChatMember", params) + return err +} + +// Promote lets you update member's admin rights, such as: +// +// * can change info +// * can post messages +// * can edit messages +// * can delete messages +// * can invite users +// * can restrict members +// * can pin messages +// * can promote members +// +func (b *Bot) Promote(chat *Chat, member *ChatMember) error { + prv := member.Rights + + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "user_id": member.User.Recipient(), + "is_anonymous": member.Anonymous, + } + embedRights(params, prv) + + _, err := b.Raw("promoteChatMember", params) + return err +} + +// AdminsOf returns a member list of chat admins. +// +// On success, returns an Array of ChatMember objects that +// contains information about all chat administrators except other bots. +// +// If the chat is a group or a supergroup and +// no administrators were appointed, only the creator will be returned. +// +func (b *Bot) AdminsOf(chat *Chat) ([]ChatMember, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + data, err := b.Raw("getChatAdministrators", params) + if err != nil { + return nil, err + } + + var resp struct { + Result []ChatMember + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} + +// Len returns the number of members in a chat. +func (b *Bot) Len(chat *Chat) (int, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + data, err := b.Raw("getChatMembersCount", params) + if err != nil { + return 0, err + } + + var resp struct { + Result int + } + if err := json.Unmarshal(data, &resp); err != nil { + return 0, wrapError(err) + } + return resp.Result, nil +} + +// SetAdminTitle sets a custom title for an administrator. +// A title should be 0-16 characters length, emoji are not allowed. +func (b *Bot) SetAdminTitle(chat *Chat, user *User, title string) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": user.Recipient(), + "custom_title": title, + } + + _, err := b.Raw("setChatAdministratorCustomTitle", params) + return err +} + +// BanSenderChat will use this method to ban a channel chat in a supergroup or a channel. +// Until the chat is unbanned, the owner of the banned chat won't be able +// to send messages on behalf of any of their channels. +func (b *Bot) BanSenderChat(chat *Chat, sender Recipient) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "sender_chat_id": sender.Recipient(), + } + + _, err := b.Raw("banChatSenderChat", params) + return err +} + +// UnbanSenderChat will use this method to unban a previously banned channel chat in a supergroup or channel. +// The bot must be an administrator for this to work and must have the appropriate administrator rights. +func (b *Bot) UnbanSenderChat(chat *Chat, sender Recipient) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "sender_chat_id": sender.Recipient(), + } + + _, err := b.Raw("unbanChatSenderChat", params) + return err +} diff --git a/vendor/gopkg.in/telebot.v3/api.go b/vendor/gopkg.in/telebot.v3/api.go new file mode 100644 index 0000000000..f283ec419e --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/api.go @@ -0,0 +1,231 @@ +package telebot + +import ( + "bytes" + "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 + } + + resp, err := b.client.Post(url, "application/json", &buf) + 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 { + 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 { + buf.Reset() + json.Indent(&buf, b, "", "\t") + return buf.String() + } + + log.Printf("[verbose] telebot: sent request\n"+ + "Method: %v\nParams: %v\nResponse: %v", + method, indent(body), indent(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 +} diff --git a/vendor/gopkg.in/telebot.v3/bot.go b/vendor/gopkg.in/telebot.v3/bot.go new file mode 100644 index 0000000000..8da821cdf5 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/bot.go @@ -0,0 +1,1524 @@ +package telebot + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "regexp" + "strconv" + "strings" +) + +// NewBot does try to build a Bot with token `token`, which +// is a secret API key assigned to particular bot. +func NewBot(pref Settings) (*Bot, error) { + if pref.Updates == 0 { + pref.Updates = 100 + } + + client := pref.Client + if client == nil { + client = http.DefaultClient + } + + if pref.URL == "" { + pref.URL = DefaultApiURL + } + if pref.Poller == nil { + pref.Poller = &LongPoller{} + } + if pref.OnError == nil { + pref.OnError = defaultOnError + } + + bot := &Bot{ + Token: pref.Token, + URL: pref.URL, + Poller: pref.Poller, + OnError: pref.OnError, + + Updates: make(chan Update, pref.Updates), + handlers: make(map[string]HandlerFunc), + stop: make(chan chan struct{}), + + synchronous: pref.Synchronous, + verbose: pref.Verbose, + parseMode: pref.ParseMode, + client: client, + } + + if pref.Offline { + bot.Me = &User{} + } else { + user, err := bot.getMe() + if err != nil { + return nil, err + } + bot.Me = user + } + + bot.group = bot.Group() + return bot, nil +} + +// Bot represents a separate Telegram bot instance. +type Bot struct { + Me *User + Token string + URL string + Updates chan Update + Poller Poller + OnError func(error, Context) + + group *Group + handlers map[string]HandlerFunc + synchronous bool + verbose bool + parseMode ParseMode + stop chan chan struct{} + client *http.Client +} + +// Settings represents a utility struct for passing certain +// properties of a bot around and is required to make bots. +type Settings struct { + URL string + Token string + + // Updates channel capacity, defaulted to 100. + Updates int + + // Poller is the provider of Updates. + Poller Poller + + // Synchronous prevents handlers from running in parallel. + // It makes ProcessUpdate return after the handler is finished. + Synchronous bool + + // Verbose forces bot to log all upcoming requests. + // Use for debugging purposes only. + Verbose bool + + // ParseMode used to set default parse mode of all sent messages. + // It attaches to every send, edit or whatever method. You also + // will be able to override the default mode by passing a new one. + ParseMode ParseMode + + // OnError is a callback function that will get called on errors + // resulted from the handler. It is used as post-middleware function. + // Notice that context can be nil. + OnError func(error, Context) + + // HTTP Client used to make requests to telegram api + Client *http.Client + + // Offline allows to create a bot without network for testing purposes. + Offline bool +} + +// Update object represents an incoming update. +type Update struct { + ID int `json:"update_id"` + + Message *Message `json:"message,omitempty"` + EditedMessage *Message `json:"edited_message,omitempty"` + ChannelPost *Message `json:"channel_post,omitempty"` + EditedChannelPost *Message `json:"edited_channel_post,omitempty"` + Callback *Callback `json:"callback_query,omitempty"` + Query *Query `json:"inline_query,omitempty"` + InlineResult *InlineResult `json:"chosen_inline_result,omitempty"` + ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"` + PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"` + Poll *Poll `json:"poll,omitempty"` + PollAnswer *PollAnswer `json:"poll_answer,omitempty"` + MyChatMember *ChatMemberUpdate `json:"my_chat_member,omitempty"` + ChatMember *ChatMemberUpdate `json:"chat_member,omitempty"` + ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"` +} + +// Command represents a bot command. +type Command struct { + // Text is a text of the command, 1-32 characters. + // Can contain only lowercase English letters, digits and underscores. + Text string `json:"command"` + + // Description of the command, 3-256 characters. + Description string `json:"description"` +} + +// Group returns a new group. +func (b *Bot) Group() *Group { + return &Group{b: b} +} + +// Use adds middleware to the global bot chain. +func (b *Bot) Use(middleware ...MiddlewareFunc) { + b.group.Use(middleware...) +} + +// Handle lets you set the handler for some command name or +// one of the supported endpoints. It also applies middleware +// if such passed to the function. +// +// Example: +// +// b.Handle("/start", func (c tele.Context) error { +// return c.Reply("Hello!") +// }) +// +// b.Handle(&inlineButton, func (c tele.Context) error { +// return c.Respond(&tele.CallbackResponse{Text: "Hello!"}) +// }) +// +// Middleware usage: +// +// b.Handle("/ban", onBan, middleware.Whitelist(ids...)) +// +func (b *Bot) Handle(endpoint interface{}, h HandlerFunc, m ...MiddlewareFunc) { + if len(b.group.middleware) > 0 { + m = append(b.group.middleware, m...) + } + + handler := func(c Context) error { + return applyMiddleware(h, m...)(c) + } + + switch end := endpoint.(type) { + case string: + b.handlers[end] = handler + case CallbackEndpoint: + b.handlers[end.CallbackUnique()] = handler + default: + panic("telebot: unsupported endpoint") + } +} + +var ( + cmdRx = regexp.MustCompile(`^(/\w+)(@(\w+))?(\s|$)(.+)?`) + cbackRx = regexp.MustCompile(`^\f([-\w]+)(\|(.+))?$`) +) + +// Start brings bot into motion by consuming incoming +// updates (see Bot.Updates channel). +func (b *Bot) Start() { + if b.Poller == nil { + panic("telebot: can't start without a poller") + } + + stop := make(chan struct{}) + stopConfirm := make(chan struct{}) + + go func() { + b.Poller.Poll(b, b.Updates, stop) + close(stopConfirm) + }() + + for { + select { + // handle incoming updates + case upd := <-b.Updates: + b.ProcessUpdate(upd) + // call to stop polling + case confirm := <-b.stop: + close(stop) + <-stopConfirm + close(confirm) + return + } + } +} + +// Stop gracefully shuts the poller down. +func (b *Bot) Stop() { + confirm := make(chan struct{}) + b.stop <- confirm + <-confirm +} + +// NewMarkup simply returns newly created markup instance. +func (b *Bot) NewMarkup() *ReplyMarkup { + return &ReplyMarkup{} +} + +// NewContext returns a new native context object, +// field by the passed update. +func (b *Bot) NewContext(u Update) Context { + return &nativeContext{ + b: b, + u: u, + } +} + +// ProcessUpdate processes a single incoming update. +// A started bot calls this function automatically. +func (b *Bot) ProcessUpdate(u Update) { + c := b.NewContext(u) + + if u.Message != nil { + m := u.Message + + if m.PinnedMessage != nil { + b.handle(OnPinned, c) + return + } + + // Commands + if m.Text != "" { + // Filtering malicious messages + if m.Text[0] == '\a' { + return + } + + match := cmdRx.FindAllStringSubmatch(m.Text, -1) + if match != nil { + // Syntax: "@ " + command, botName := match[0][1], match[0][3] + + if botName != "" && !strings.EqualFold(b.Me.Username, botName) { + return + } + + m.Payload = match[0][5] + if b.handle(command, c) { + return + } + } + + // 1:1 satisfaction + if b.handle(m.Text, c) { + return + } + + b.handle(OnText, c) + return + } + + if b.handleMedia(c) { + return + } + + if m.Contact != nil { + b.handle(OnContact, c) + return + } + if m.Location != nil { + b.handle(OnLocation, c) + return + } + if m.Venue != nil { + b.handle(OnVenue, c) + return + } + if m.Game != nil { + b.handle(OnGame, c) + return + } + if m.Dice != nil { + b.handle(OnDice, c) + return + } + if m.Invoice != nil { + b.handle(OnInvoice, c) + return + } + if m.Payment != nil { + b.handle(OnPayment, c) + return + } + + wasAdded := (m.UserJoined != nil && m.UserJoined.ID == b.Me.ID) || + (m.UsersJoined != nil && isUserInList(b.Me, m.UsersJoined)) + if m.GroupCreated || m.SuperGroupCreated || wasAdded { + b.handle(OnAddedToGroup, c) + return + } + + if m.UserJoined != nil { + b.handle(OnUserJoined, c) + return + } + + if m.UsersJoined != nil { + for _, user := range m.UsersJoined { + m.UserJoined = &user + b.handle(OnUserJoined, c) + } + return + } + + if m.UserLeft != nil { + b.handle(OnUserLeft, c) + return + } + + if m.NewGroupTitle != "" { + b.handle(OnNewGroupTitle, c) + return + } + + if m.NewGroupPhoto != nil { + b.handle(OnNewGroupPhoto, c) + return + } + + if m.GroupPhotoDeleted { + b.handle(OnGroupPhotoDeleted, c) + return + } + + if m.GroupCreated { + b.handle(OnGroupCreated, c) + return + } + + if m.SuperGroupCreated { + b.handle(OnSuperGroupCreated, c) + return + } + + if m.ChannelCreated { + b.handle(OnChannelCreated, c) + return + } + + if m.MigrateTo != 0 { + m.MigrateFrom = m.Chat.ID + b.handle(OnMigration, c) + return + } + + if m.VoiceChatStarted != nil { + b.handle(OnVoiceChatStarted, c) + return + } + + if m.VoiceChatEnded != nil { + b.handle(OnVoiceChatEnded, c) + return + } + + if m.VoiceChatParticipants != nil { + b.handle(OnVoiceChatParticipants, c) + return + } + + if m.VoiceChatScheduled != nil { + b.handle(OnVoiceChatScheduled, c) + return + } + + if m.ProximityAlert != nil { + b.handle(OnProximityAlert, c) + return + } + + if m.AutoDeleteTimer != nil { + b.handle(OnAutoDeleteTimer, c) + return + } + } + + if u.EditedMessage != nil { + b.handle(OnEdited, c) + return + } + + if u.ChannelPost != nil { + m := u.ChannelPost + + if m.PinnedMessage != nil { + b.handle(OnPinned, c) + return + } + + b.handle(OnChannelPost, c) + return + } + + if u.EditedChannelPost != nil { + b.handle(OnEditedChannelPost, c) + return + } + + if u.Callback != nil { + if data := u.Callback.Data; data != "" && data[0] == '\f' { + match := cbackRx.FindAllStringSubmatch(data, -1) + if match != nil { + unique, payload := match[0][1], match[0][3] + if handler, ok := b.handlers["\f"+unique]; ok { + u.Callback.Unique = unique + u.Callback.Data = payload + b.runHandler(handler, c) + return + } + } + } + + b.handle(OnCallback, c) + return + } + + if u.Query != nil { + b.handle(OnQuery, c) + return + } + + if u.InlineResult != nil { + b.handle(OnInlineResult, c) + return + } + + if u.ShippingQuery != nil { + b.handle(OnShipping, c) + return + } + + if u.PreCheckoutQuery != nil { + b.handle(OnCheckout, c) + return + } + + if u.Poll != nil { + b.handle(OnPoll, c) + return + } + + if u.PollAnswer != nil { + b.handle(OnPollAnswer, c) + return + } + + if u.MyChatMember != nil { + b.handle(OnMyChatMember, c) + return + } + + if u.ChatMember != nil { + b.handle(OnChatMember, c) + return + } + + if u.ChatJoinRequest != nil { + b.handle(OnChatJoinRequest, c) + return + } +} + +func (b *Bot) handle(end string, c Context) bool { + if handler, ok := b.handlers[end]; ok { + b.runHandler(handler, c) + return true + } + return false +} + +func (b *Bot) handleMedia(c Context) bool { + var ( + m = c.Message() + fired = true + ) + + switch { + case m.Photo != nil: + fired = b.handle(OnPhoto, c) + case m.Voice != nil: + fired = b.handle(OnVoice, c) + case m.Audio != nil: + fired = b.handle(OnAudio, c) + case m.Animation != nil: + fired = b.handle(OnAnimation, c) + case m.Document != nil: + fired = b.handle(OnDocument, c) + case m.Sticker != nil: + fired = b.handle(OnSticker, c) + case m.Video != nil: + fired = b.handle(OnVideo, c) + case m.VideoNote != nil: + fired = b.handle(OnVideoNote, c) + default: + return false + } + + if !fired { + return b.handle(OnMedia, c) + } + + return true +} + +// Send accepts 2+ arguments, starting with destination chat, followed by +// some Sendable (or string!) and optional send options. +// +// NOTE: +// Since most arguments are of type interface{}, but have pointer +// method receivers, make sure to pass them by-pointer, NOT by-value. +// +// What is a send option exactly? It can be one of the following types: +// +// - *SendOptions (the actual object accepted by Telegram API) +// - *ReplyMarkup (a component of SendOptions) +// - Option (a shortcut flag for popular options) +// - ParseMode (HTML, Markdown, etc) +// +func (b *Bot) Send(to Recipient, what interface{}, opts ...interface{}) (*Message, error) { + if to == nil { + return nil, ErrBadRecipient + } + + sendOpts := extractOptions(opts) + + switch object := what.(type) { + case string: + return b.sendText(to, object, sendOpts) + case Sendable: + return object.Send(b, to, sendOpts) + default: + return nil, ErrUnsupportedWhat + } +} + +// SendAlbum sends multiple instances of media as a single message. +// From all existing options, it only supports tele.Silent. +func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message, error) { + if to == nil { + return nil, ErrBadRecipient + } + + sendOpts := extractOptions(opts) + media := make([]string, len(a)) + files := make(map[string]File) + + for i, x := range a { + var ( + repr string + data []byte + file = x.MediaFile() + ) + + switch { + case file.InCloud(): + repr = file.FileID + case file.FileURL != "": + repr = file.FileURL + case file.OnDisk() || file.FileReader != nil: + repr = "attach://" + strconv.Itoa(i) + files[strconv.Itoa(i)] = *file + default: + return nil, fmt.Errorf("telebot: album entry #%d does not exist", i) + } + + im := x.InputMedia() + im.Media = repr + + if len(sendOpts.Entities) > 0 { + im.Entities = sendOpts.Entities + } else { + im.ParseMode = sendOpts.ParseMode + } + + data, _ = json.Marshal(im) + media[i] = string(data) + } + + params := map[string]string{ + "chat_id": to.Recipient(), + "media": "[" + strings.Join(media, ",") + "]", + } + b.embedSendOptions(params, sendOpts) + + data, err := b.sendFiles("sendMediaGroup", files, params) + if err != nil { + return nil, err + } + + var resp struct { + Result []Message + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + + for attachName := range files { + i, _ := strconv.Atoi(attachName) + r := resp.Result[i] + + var newID string + switch { + case r.Photo != nil: + newID = r.Photo.FileID + case r.Video != nil: + newID = r.Video.FileID + case r.Audio != nil: + newID = r.Audio.FileID + case r.Document != nil: + newID = r.Document.FileID + } + + a[i].MediaFile().FileID = newID + } + + return resp.Result, nil +} + +// Reply behaves just like Send() with an exception of "reply-to" indicator. +// This function will panic upon nil Message. +func (b *Bot) Reply(to *Message, what interface{}, opts ...interface{}) (*Message, error) { + sendOpts := extractOptions(opts) + if sendOpts == nil { + sendOpts = &SendOptions{} + } + + sendOpts.ReplyTo = to + return b.Send(to.Chat, what, sendOpts) +} + +// Forward behaves just like Send() but of all options it only supports Silent (see Bots API). +// This function will panic upon nil Editable. +func (b *Bot) Forward(to Recipient, msg Editable, opts ...interface{}) (*Message, error) { + if to == nil { + return nil, ErrBadRecipient + } + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "chat_id": to.Recipient(), + "from_chat_id": strconv.FormatInt(chatID, 10), + "message_id": msgID, + } + + sendOpts := extractOptions(opts) + b.embedSendOptions(params, sendOpts) + + data, err := b.Raw("forwardMessage", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Copy behaves just like Forward() but the copied message doesn't have a link to the original message (see Bots API). +// +// This function will panic upon nil Editable. +func (b *Bot) Copy(to Recipient, msg Editable, options ...interface{}) (*Message, error) { + if to == nil { + return nil, ErrBadRecipient + } + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "chat_id": to.Recipient(), + "from_chat_id": strconv.FormatInt(chatID, 10), + "message_id": msgID, + } + + sendOpts := extractOptions(options) + b.embedSendOptions(params, sendOpts) + + data, err := b.Raw("copyMessage", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Edit is magic, it lets you change already sent message. +// This function will panic upon nil Editable. +// +// If edited message is sent by the bot, returns it, +// otherwise returns nil and ErrTrueResult. +// +// Use cases: +// +// b.Edit(m, m.Text, newMarkup) +// b.Edit(m, "new text", tele.ModeHTML) +// b.Edit(m, &tele.ReplyMarkup{...}) +// b.Edit(m, &tele.Photo{File: ...}) +// b.Edit(m, tele.Location{42.1337, 69.4242}) +// b.Edit(c, "edit inline message from the callback") +// b.Edit(r, "edit message from chosen inline result") +// +func (b *Bot) Edit(msg Editable, what interface{}, opts ...interface{}) (*Message, error) { + var ( + method string + params = make(map[string]string) + ) + + switch v := what.(type) { + case *ReplyMarkup: + return b.EditReplyMarkup(msg, v) + case Inputtable: + return b.EditMedia(msg, v, opts...) + case string: + method = "editMessageText" + params["text"] = v + case Location: + method = "editMessageLiveLocation" + params["latitude"] = fmt.Sprintf("%f", v.Lat) + params["longitude"] = fmt.Sprintf("%f", v.Lng) + + if v.HorizontalAccuracy != nil { + params["horizontal_accuracy"] = fmt.Sprintf("%f", *v.HorizontalAccuracy) + } + if v.Heading != 0 { + params["heading"] = strconv.Itoa(v.Heading) + } + if v.AlertRadius != 0 { + params["proximity_alert_radius"] = strconv.Itoa(v.AlertRadius) + } + default: + return nil, ErrUnsupportedWhat + } + + msgID, chatID := msg.MessageSig() + + if chatID == 0 { // if inline message + params["inline_message_id"] = msgID + } else { + params["chat_id"] = strconv.FormatInt(chatID, 10) + params["message_id"] = msgID + } + + sendOpts := extractOptions(opts) + b.embedSendOptions(params, sendOpts) + + data, err := b.Raw(method, params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// EditReplyMarkup edits reply markup of already sent message. +// This function will panic upon nil Editable. +// Pass nil or empty ReplyMarkup to delete it from the message. +// +// If edited message is sent by the bot, returns it, +// otherwise returns nil and ErrTrueResult. +// +func (b *Bot) EditReplyMarkup(msg Editable, markup *ReplyMarkup) (*Message, error) { + msgID, chatID := msg.MessageSig() + params := make(map[string]string) + + if chatID == 0 { // if inline message + params["inline_message_id"] = msgID + } else { + params["chat_id"] = strconv.FormatInt(chatID, 10) + params["message_id"] = msgID + } + + if markup == nil { + // will delete reply markup + markup = &ReplyMarkup{} + } + + processButtons(markup.InlineKeyboard) + data, _ := json.Marshal(markup) + params["reply_markup"] = string(data) + + data, err := b.Raw("editMessageReplyMarkup", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// EditCaption edits already sent photo caption with known recipient and message id. +// This function will panic upon nil Editable. +// +// If edited message is sent by the bot, returns it, +// otherwise returns nil and ErrTrueResult. +// +func (b *Bot) EditCaption(msg Editable, caption string, opts ...interface{}) (*Message, error) { + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "caption": caption, + } + + if chatID == 0 { // if inline message + params["inline_message_id"] = msgID + } else { + params["chat_id"] = strconv.FormatInt(chatID, 10) + params["message_id"] = msgID + } + + sendOpts := extractOptions(opts) + b.embedSendOptions(params, sendOpts) + + data, err := b.Raw("editMessageCaption", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// EditMedia edits already sent media with known recipient and message id. +// This function will panic upon nil Editable. +// +// If edited message is sent by the bot, returns it, +// otherwise returns nil and ErrTrueResult. +// +// Use cases: +// +// b.EditMedia(m, &tele.Photo{File: tele.FromDisk("chicken.jpg")}) +// b.EditMedia(m, &tele.Video{File: tele.FromURL("http://video.mp4")}) +// +func (b *Bot) EditMedia(msg Editable, media Inputtable, opts ...interface{}) (*Message, error) { + var ( + repr string + file = media.MediaFile() + files = make(map[string]File) + + thumb *Photo + thumbName = "thumb" + ) + + switch { + case file.InCloud(): + repr = file.FileID + case file.FileURL != "": + repr = file.FileURL + case file.OnDisk() || file.FileReader != nil: + s := file.FileLocal + if file.FileReader != nil { + s = "0" + } else if s == thumbName { + thumbName = "thumb2" + } + + repr = "attach://" + s + files[s] = *file + default: + return nil, fmt.Errorf("telebot: cannot edit media, it does not exist") + } + + switch m := media.(type) { + case *Video: + thumb = m.Thumbnail + case *Audio: + thumb = m.Thumbnail + case *Document: + thumb = m.Thumbnail + case *Animation: + thumb = m.Thumbnail + } + + msgID, chatID := msg.MessageSig() + params := make(map[string]string) + + sendOpts := extractOptions(opts) + b.embedSendOptions(params, sendOpts) + + im := media.InputMedia() + im.Media = repr + + if len(sendOpts.Entities) > 0 { + im.Entities = sendOpts.Entities + } else { + im.ParseMode = sendOpts.ParseMode + } + + if thumb != nil { + im.Thumbnail = "attach://" + thumbName + files[thumbName] = *thumb.MediaFile() + } + + data, _ := json.Marshal(im) + params["media"] = string(data) + + if chatID == 0 { // if inline message + params["inline_message_id"] = msgID + } else { + params["chat_id"] = strconv.FormatInt(chatID, 10) + params["message_id"] = msgID + } + + data, err := b.sendFiles("editMessageMedia", files, params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Delete removes the message, including service messages. +// This function will panic upon nil Editable. +// +// * A message can only be deleted if it was sent less than 48 hours ago. +// * A dice message in a private chat can only be deleted if it was sent more than 24 hours ago. +// * Bots can delete outgoing messages in private chats, groups, and supergroups. +// * Bots can delete incoming messages in private chats. +// * Bots granted can_post_messages permissions can delete outgoing messages in channels. +// * If the bot is an administrator of a group, it can delete any message there. +// * If the bot has can_delete_messages permission in a supergroup or a +// channel, it can delete any message there. +// +func (b *Bot) Delete(msg Editable) error { + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "chat_id": strconv.FormatInt(chatID, 10), + "message_id": msgID, + } + + _, err := b.Raw("deleteMessage", params) + return err +} + +// Notify updates the chat action for recipient. +// +// Chat action is a status message that recipient would see where +// you typically see "Harry is typing" status message. The only +// difference is that bots' chat actions live only for 5 seconds +// and die just once the client receives a message from the bot. +// +// Currently, Telegram supports only a narrow range of possible +// actions, these are aligned as constants of this package. +// +func (b *Bot) Notify(to Recipient, action ChatAction) error { + if to == nil { + return ErrBadRecipient + } + + params := map[string]string{ + "chat_id": to.Recipient(), + "action": string(action), + } + + _, err := b.Raw("sendChatAction", params) + return err +} + +// Ship replies to the shipping query, if you sent an invoice +// requesting an address and the parameter is_flexible was specified. +// +// Example: +// +// b.Ship(query) // OK +// b.Ship(query, opts...) // OK with options +// b.Ship(query, "Oops!") // Error message +// +func (b *Bot) Ship(query *ShippingQuery, what ...interface{}) error { + params := map[string]string{ + "shipping_query_id": query.ID, + } + + if len(what) == 0 { + params["ok"] = "True" + } else if s, ok := what[0].(string); ok { + params["ok"] = "False" + params["error_message"] = s + } else { + var opts []ShippingOption + for _, v := range what { + opt, ok := v.(ShippingOption) + if !ok { + return ErrUnsupportedWhat + } + opts = append(opts, opt) + } + + params["ok"] = "True" + data, _ := json.Marshal(opts) + params["shipping_options"] = string(data) + } + + _, err := b.Raw("answerShippingQuery", params) + return err +} + +// Accept finalizes the deal. +func (b *Bot) Accept(query *PreCheckoutQuery, errorMessage ...string) error { + params := map[string]string{ + "pre_checkout_query_id": query.ID, + } + + if len(errorMessage) == 0 { + params["ok"] = "True" + } else { + params["ok"] = "False" + params["error_message"] = errorMessage[0] + } + + _, err := b.Raw("answerPreCheckoutQuery", params) + return err +} + +// Answer sends a response for a given inline query. A query can only +// be responded to once, subsequent attempts to respond to the same query +// will result in an error. +func (b *Bot) Answer(query *Query, resp *QueryResponse) error { + resp.QueryID = query.ID + + for _, result := range resp.Results { + result.Process(b) + } + + _, err := b.Raw("answerInlineQuery", resp) + return err +} + +// Respond sends a response for a given callback query. A callback can +// only be responded to once, subsequent attempts to respond to the same callback +// will result in an error. +// +// Example: +// +// b.Respond(c) +// b.Respond(c, response) +// +func (b *Bot) Respond(c *Callback, resp ...*CallbackResponse) error { + var r *CallbackResponse + if resp == nil { + r = &CallbackResponse{} + } else { + r = resp[0] + } + + r.CallbackID = c.ID + _, err := b.Raw("answerCallbackQuery", r) + return err +} + +// FileByID returns full file object including File.FilePath, allowing you to +// download the file from the server. +// +// Usually, Telegram-provided File objects miss FilePath so you might need to +// perform an additional request to fetch them. +// +func (b *Bot) FileByID(fileID string) (File, error) { + params := map[string]string{ + "file_id": fileID, + } + + data, err := b.Raw("getFile", params) + if err != nil { + return File{}, err + } + + var resp struct { + Result File + } + if err := json.Unmarshal(data, &resp); err != nil { + return File{}, wrapError(err) + } + return resp.Result, nil +} + +// Download saves the file from Telegram servers locally. +// Maximum file size to download is 20 MB. +func (b *Bot) Download(file *File, localFilename string) error { + reader, err := b.File(file) + if err != nil { + return err + } + defer reader.Close() + + out, err := os.Create(localFilename) + if err != nil { + return wrapError(err) + } + defer out.Close() + + _, err = io.Copy(out, reader) + if err != nil { + return wrapError(err) + } + + file.FileLocal = localFilename + return nil +} + +// File gets a file from Telegram servers. +func (b *Bot) File(file *File) (io.ReadCloser, error) { + f, err := b.FileByID(file.FileID) + if err != nil { + return nil, err + } + + url := b.URL + "/file/bot" + b.Token + "/" + f.FilePath + file.FilePath = f.FilePath // saving file path + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, wrapError(err) + } + + resp, err := b.client.Do(req) + if err != nil { + return nil, wrapError(err) + } + + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("telebot: expected status 200 but got %s", resp.Status) + } + + return resp.Body, nil +} + +// StopLiveLocation stops broadcasting live message location +// before Location.LivePeriod expires. +// +// It supports ReplyMarkup. +// This function will panic upon nil Editable. +// +// If the message is sent by the bot, returns it, +// otherwise returns nil and ErrTrueResult. +// +func (b *Bot) StopLiveLocation(msg Editable, opts ...interface{}) (*Message, error) { + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "chat_id": strconv.FormatInt(chatID, 10), + "message_id": msgID, + } + + sendOpts := extractOptions(opts) + b.embedSendOptions(params, sendOpts) + + data, err := b.Raw("stopMessageLiveLocation", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// StopPoll stops a poll which was sent by the bot and returns +// the stopped Poll object with the final results. +// +// It supports ReplyMarkup. +// This function will panic upon nil Editable. +// +func (b *Bot) StopPoll(msg Editable, opts ...interface{}) (*Poll, error) { + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "chat_id": strconv.FormatInt(chatID, 10), + "message_id": msgID, + } + + sendOpts := extractOptions(opts) + b.embedSendOptions(params, sendOpts) + + data, err := b.Raw("stopPoll", params) + if err != nil { + return nil, err + } + + var resp struct { + Result *Poll + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} + +// InviteLink should be used to export chat's invite link. +func (b *Bot) InviteLink(chat *Chat) (string, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + data, err := b.Raw("exportChatInviteLink", params) + if err != nil { + return "", err + } + + var resp struct { + Result string + } + if err := json.Unmarshal(data, &resp); err != nil { + return "", wrapError(err) + } + return resp.Result, nil +} + +// SetGroupTitle should be used to update group title. +func (b *Bot) SetGroupTitle(chat *Chat, title string) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "title": title, + } + + _, err := b.Raw("setChatTitle", params) + return err +} + +// SetGroupDescription should be used to update group description. +func (b *Bot) SetGroupDescription(chat *Chat, description string) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "description": description, + } + + _, err := b.Raw("setChatDescription", params) + return err +} + +// SetGroupPhoto should be used to update group photo. +func (b *Bot) SetGroupPhoto(chat *Chat, p *Photo) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + _, err := b.sendFiles("setChatPhoto", map[string]File{"photo": p.File}, params) + return err +} + +// SetGroupStickerSet should be used to update group's group sticker set. +func (b *Bot) SetGroupStickerSet(chat *Chat, setName string) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "sticker_set_name": setName, + } + + _, err := b.Raw("setChatStickerSet", params) + return err +} + +// SetGroupPermissions sets default chat permissions for all members. +func (b *Bot) SetGroupPermissions(chat *Chat, perms Rights) error { + params := map[string]interface{}{ + "chat_id": chat.Recipient(), + "permissions": perms, + } + + _, err := b.Raw("setChatPermissions", params) + return err +} + +// DeleteGroupPhoto should be used to just remove group photo. +func (b *Bot) DeleteGroupPhoto(chat *Chat) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + _, err := b.Raw("deleteChatPhoto", params) + return err +} + +// DeleteGroupStickerSet should be used to just remove group sticker set. +func (b *Bot) DeleteGroupStickerSet(chat *Chat) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + _, err := b.Raw("deleteChatStickerSet", params) + return err +} + +// Leave makes bot leave a group, supergroup or channel. +func (b *Bot) Leave(chat *Chat) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + _, err := b.Raw("leaveChat", params) + return err +} + +// Pin pins a message in a supergroup or a channel. +// +// It supports Silent option. +// This function will panic upon nil Editable. +// +func (b *Bot) Pin(msg Editable, opts ...interface{}) error { + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "chat_id": strconv.FormatInt(chatID, 10), + "message_id": msgID, + } + + sendOpts := extractOptions(opts) + b.embedSendOptions(params, sendOpts) + + _, err := b.Raw("pinChatMessage", params) + return err +} + +// Unpin unpins a message in a supergroup or a channel. +// It supports tb.Silent option. +func (b *Bot) Unpin(chat *Chat, messageID ...int) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + if len(messageID) > 0 { + params["message_id"] = strconv.Itoa(messageID[0]) + } + + _, err := b.Raw("unpinChatMessage", params) + return err +} + +// UnpinAll unpins all messages in a supergroup or a channel. +// +// It supports tb.Silent option. +func (b *Bot) UnpinAll(chat *Chat) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + _, err := b.Raw("unpinAllChatMessages", params) + return err +} + +// ChatByID fetches chat info of its ID. +// +// Including current name of the user for one-on-one conversations, +// current username of a user, group or channel, etc. +// +func (b *Bot) ChatByID(id int64) (*Chat, error) { + return b.ChatByUsername(strconv.FormatInt(id, 10)) +} + +func (b *Bot) ChatByUsername(name string) (*Chat, error) { + params := map[string]string{ + "chat_id": name, + } + + data, err := b.Raw("getChat", params) + if err != nil { + return nil, err + } + + var resp struct { + Result *Chat + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + if resp.Result.Type == ChatChannel && resp.Result.Username == "" { + resp.Result.Type = ChatChannelPrivate + } + return resp.Result, nil +} + +// ProfilePhotosOf returns list of profile pictures for a user. +func (b *Bot) ProfilePhotosOf(user *User) ([]Photo, error) { + params := map[string]string{ + "user_id": user.Recipient(), + } + + data, err := b.Raw("getUserProfilePhotos", params) + if err != nil { + return nil, err + } + + var resp struct { + Result struct { + Count int `json:"total_count"` + Photos []Photo `json:"photos"` + } + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result.Photos, nil +} + +// ChatMemberOf returns information about a member of a chat. +func (b *Bot) ChatMemberOf(chat, user Recipient) (*ChatMember, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": user.Recipient(), + } + + data, err := b.Raw("getChatMember", params) + if err != nil { + return nil, err + } + + var resp struct { + Result *ChatMember + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} + +// Commands returns the current list of the bot's commands for the given scope and user language. +func (b *Bot) Commands(opts ...interface{}) ([]Command, error) { + params := extractCommandsParams(opts...) + data, err := b.Raw("getMyCommands", params) + if err != nil { + return nil, err + } + + var resp struct { + Result []Command + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} + +// SetCommands changes the list of the bot's commands. +func (b *Bot) SetCommands(opts ...interface{}) error { + params := extractCommandsParams(opts...) + _, err := b.Raw("setMyCommands", params) + return err +} + +// DeleteCommands deletes the list of the bot's commands for the given scope and user language. +func (b *Bot) DeleteCommands(opts ...interface{}) ([]Command, error) { + params := extractCommandsParams(opts...) + data, err := b.Raw("deleteMyCommands", params) + if err != nil { + return nil, err + } + + var resp struct { + Result []Command + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return resp.Result, nil +} + +// Logout logs out from the cloud Bot API server before launching the bot locally. +func (b *Bot) Logout() (bool, error) { + data, err := b.Raw("logOut", nil) + if err != nil { + return false, err + } + + var resp struct { + Result bool `json:"result"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return false, wrapError(err) + } + + return resp.Result, nil +} + +// Close closes the bot instance before moving it from one local server to another. +func (b *Bot) Close() (bool, error) { + data, err := b.Raw("close", nil) + if err != nil { + return false, err + } + + var resp struct { + Result bool `json:"result"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return false, wrapError(err) + } + + return resp.Result, nil +} diff --git a/vendor/gopkg.in/telebot.v3/callbacks.go b/vendor/gopkg.in/telebot.v3/callbacks.go new file mode 100644 index 0000000000..23894a4f0a --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/callbacks.go @@ -0,0 +1,147 @@ +package telebot + +import "encoding/json" + +// CallbackEndpoint is an interface any element capable +// of responding to a callback `\f`. +type CallbackEndpoint interface { + CallbackUnique() string +} + +// Callback object represents a query from a callback button in an +// inline keyboard. +type Callback struct { + ID string `json:"id"` + + // For message sent to channels, Sender may be empty + Sender *User `json:"from"` + + // Message will be set if the button that originated the query + // was attached to a message sent by a bot. + Message *Message `json:"message"` + + // MessageID will be set if the button was attached to a message + // sent via the bot in inline mode. + MessageID string `json:"inline_message_id"` + + // Data associated with the callback button. Be aware that + // a bad client can send arbitrary data in this field. + Data string `json:"data"` + + // Unique displays an unique of the button from which the + // callback was fired. Sets immediately before the handling, + // while the Data field stores only with payload. + Unique string `json:"-"` +} + +// MessageSig satisfies Editable interface. +func (c *Callback) MessageSig() (string, int64) { + if c.IsInline() { + return c.MessageID, 0 + } + return c.Message.MessageSig() +} + +// IsInline says whether message is an inline message. +func (c *Callback) IsInline() bool { + return c.MessageID != "" +} + +// CallbackResponse builds a response to a Callback query. +type CallbackResponse struct { + // The ID of the callback to which this is a response. + // + // Note: Telebot sets this field automatically! + CallbackID string `json:"callback_query_id"` + + // Text of the notification. If not specified, nothing will be + // shown to the user. + Text string `json:"text,omitempty"` + + // (Optional) If true, an alert will be shown by the client instead + // of a notification at the top of the chat screen. Defaults to false. + ShowAlert bool `json:"show_alert,omitempty"` + + // (Optional) URL that will be opened by the user's client. + // If you have created a Game and accepted the conditions via + // @BotFather, specify the URL that opens your game. + // + // Note: this will only work if the query comes from a game + // callback button. Otherwise, you may use deep-linking: + // https://telegram.me/your_bot?start=XXXX + URL string `json:"url,omitempty"` +} + +// InlineButton represents a button displayed in the message. +type InlineButton struct { + // Unique slagish name for this kind of button, + // try to be as specific as possible. + // + // It will be used as a callback endpoint. + Unique string `json:"unique,omitempty"` + + Text string `json:"text"` + URL string `json:"url,omitempty"` + Data string `json:"callback_data,omitempty"` + InlineQuery string `json:"switch_inline_query,omitempty"` + InlineQueryChat string `json:"switch_inline_query_current_chat"` + Login *Login `json:"login_url,omitempty"` +} + +// With returns a copy of the button with data. +func (t *InlineButton) With(data string) *InlineButton { + return &InlineButton{ + Unique: t.Unique, + Text: t.Text, + URL: t.URL, + InlineQuery: t.InlineQuery, + InlineQueryChat: t.InlineQueryChat, + Login: t.Login, + Data: data, + } +} + +// CallbackUnique returns InlineButton.Unique. +func (t *InlineButton) CallbackUnique() string { + return "\f" + t.Unique +} + +// CallbackUnique returns KeyboardButton.Text. +func (t *ReplyButton) CallbackUnique() string { + return t.Text +} + +// CallbackUnique implements CallbackEndpoint. +func (t *Btn) CallbackUnique() string { + if t.Unique != "" { + return "\f" + t.Unique + } + return t.Text +} + +// Login represents a parameter of the inline keyboard button +// used to automatically authorize a user. Serves as a great replacement +// for the Telegram Login Widget when the user is coming from Telegram. +type Login struct { + URL string `json:"url"` + Text string `json:"forward_text,omitempty"` + Username string `json:"bot_username,omitempty"` + WriteAccess bool `json:"request_write_access,omitempty"` +} + +// MarshalJSON implements json.Marshaler interface. +// It needed to avoid InlineQueryChat and Login fields conflict. +// If you have Login field in your button, InlineQueryChat must be skipped. +func (t *InlineButton) MarshalJSON() ([]byte, error) { + type InlineButtonJSON InlineButton + + if t.Login != nil { + return json.Marshal(struct { + InlineButtonJSON + InlineQueryChat string `json:"switch_inline_query_current_chat,omitempty"` + }{ + InlineButtonJSON: InlineButtonJSON(*t), + }) + } + return json.Marshal(InlineButtonJSON(*t)) +} diff --git a/vendor/gopkg.in/telebot.v3/chat.go b/vendor/gopkg.in/telebot.v3/chat.go new file mode 100644 index 0000000000..50920f5334 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/chat.go @@ -0,0 +1,268 @@ +package telebot + +import ( + "encoding/json" + "strconv" + "time" +) + +// User object represents a Telegram user, bot. +type User struct { + ID int64 `json:"id"` + + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Username string `json:"username"` + LanguageCode string `json:"language_code"` + IsBot bool `json:"is_bot"` + + // Returns only in getMe + CanJoinGroups bool `json:"can_join_groups"` + CanReadMessages bool `json:"can_read_all_group_messages"` + SupportsInline bool `json:"supports_inline_queries"` +} + +// Recipient returns user ID (see Recipient interface). +func (u *User) Recipient() string { + return strconv.FormatInt(u.ID, 10) +} + +// Chat object represents a Telegram user, bot, group or a channel. +type Chat struct { + ID int64 `json:"id"` + + // See ChatType and consts. + Type ChatType `json:"type"` + + // Won't be there for ChatPrivate. + Title string `json:"title"` + + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Username string `json:"username"` + + // Still shows whether the user is a member + // of the chat at the moment of the request. + Still bool `json:"is_member,omitempty"` + + // Returns only in getChat + Bio string `json:"bio,omitempty"` + Photo *ChatPhoto `json:"photo,omitempty"` + Description string `json:"description,omitempty"` + InviteLink string `json:"invite_link,omitempty"` + PinnedMessage *Message `json:"pinned_message,omitempty"` + Permissions *Rights `json:"permissions,omitempty"` + SlowMode int `json:"slow_mode_delay,omitempty"` + StickerSet string `json:"sticker_set_name,omitempty"` + CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"` + LinkedChatID int64 `json:"linked_chat_id,omitempty"` + ChatLocation *ChatLocation `json:"location,omitempty"` + Private bool `json:"has_private_forwards,omitempty"` + Protected bool `json:"has_protected_content,omitempty"` +} + +type ChatLocation struct { + Location Location `json:"location,omitempty"` + Address string `json:"address,omitempty"` +} + +// ChatPhoto object represents a chat photo. +type ChatPhoto struct { + // File identifiers of small (160x160) chat photo + SmallFileID string `json:"small_file_id"` + SmallUniqueID string `json:"small_file_unique_id"` + + // File identifiers of big (640x640) chat photo + BigFileID string `json:"big_file_id"` + BigUniqueID string `json:"big_file_unique_id"` +} + +// Recipient returns chat ID (see Recipient interface). +func (c *Chat) Recipient() string { + return strconv.FormatInt(c.ID, 10) +} + +// ChatMember object represents information about a single chat member. +type ChatMember struct { + Rights + + User *User `json:"user"` + Role MemberStatus `json:"status"` + Title string `json:"custom_title"` + Anonymous bool `json:"is_anonymous"` + + // Date when restrictions will be lifted for the user, unix time. + // + // If user is restricted for more than 366 days or less than + // 30 seconds from the current time, they are considered to be + // restricted forever. + // + // Use tele.Forever(). + // + RestrictedUntil int64 `json:"until_date,omitempty"` +} + +// ChatID represents a chat or an user integer ID, which can be used +// as recipient in bot methods. It is very useful in cases where +// you have special group IDs, for example in your config, and don't +// want to wrap it into *tele.Chat every time you send messages. +// +// Example: +// +// group := tele.ChatID(-100756389456) +// b.Send(group, "Hello!") +// +// type Config struct { +// AdminGroup tele.ChatID `json:"admin_group"` +// } +// b.Send(conf.AdminGroup, "Hello!") +// +type ChatID int64 + +// Recipient returns chat ID (see Recipient interface). +func (i ChatID) Recipient() string { + return strconv.FormatInt(int64(i), 10) +} + +// ChatJoinRequest represents a join request sent to a chat. +type ChatJoinRequest struct { + // Chat to which the request was sent. + Chat *Chat `json:"chat"` + + // Sender is the user that sent the join request. + Sender *User `json:"from"` + + // Unixtime, use ChatJoinRequest.Time() to get time.Time. + Unixtime int64 `json:"date"` + + // Bio of the user, optional. + Bio string `json:"bio"` + + // InviteLink is the chat invite link that was used by + //the user to send the join request, optional. + InviteLink *ChatInviteLink `json:"invite_link"` +} + +// Time returns the moment of chat join request sending in local time. +func (r ChatJoinRequest) Time() time.Time { + return time.Unix(r.Unixtime, 0) +} + +// CreateInviteLink creates an additional invite link for a chat. +func (b *Bot) CreateInviteLink(chat Recipient, link *ChatInviteLink) (*ChatInviteLink, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + if link != nil { + params["name"] = link.Name + + if link.ExpireUnixtime != 0 { + params["expire_date"] = strconv.FormatInt(link.ExpireUnixtime, 10) + } + if link.MemberLimit > 0 { + params["member_limit"] = strconv.Itoa(link.MemberLimit) + } else if link.JoinRequest { + params["creates_join_request"] = "true" + } + } + + data, err := b.Raw("createChatInviteLink", params) + if err != nil { + return nil, err + } + + var resp struct { + Result ChatInviteLink `json:"result"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + + return &resp.Result, nil +} + +// EditInviteLink edits a non-primary invite link created by the bot. +func (b *Bot) EditInviteLink(chat Recipient, link *ChatInviteLink) (*ChatInviteLink, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + if link != nil { + params["invite_link"] = link.InviteLink + params["name"] = link.Name + + if link.ExpireUnixtime != 0 { + params["expire_date"] = strconv.FormatInt(link.ExpireUnixtime, 10) + } + if link.MemberLimit > 0 { + params["member_limit"] = strconv.Itoa(link.MemberLimit) + } else if link.JoinRequest { + params["creates_join_request"] = "true" + } + } + + data, err := b.Raw("editChatInviteLink", params) + if err != nil { + return nil, err + } + + var resp struct { + Result ChatInviteLink `json:"result"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + + return &resp.Result, nil +} + +// RevokeInviteLink revokes an invite link created by the bot. +func (b *Bot) RevokeInviteLink(chat Recipient, link string) (*ChatInviteLink, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + "invite_link": link, + } + + data, err := b.Raw("revokeChatInviteLink", params) + if err != nil { + return nil, err + } + + var resp struct { + Result ChatInviteLink `json:"result"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + + return &resp.Result, nil +} + +// ApproveChatJoinRequest approves a chat join request. +func (b *Bot) ApproveChatJoinRequest(chat Recipient, user *User) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": user.Recipient(), + } + + data, err := b.Raw("approveChatJoinRequest", params) + if err != nil { + return err + } + + return extractOk(data) +} + +// DeclineChatJoinRequest declines a chat join request. +func (b *Bot) DeclineChatJoinRequest(chat Recipient, user *User) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": user.Recipient(), + } + + data, err := b.Raw("declineChatJoinRequest", params) + if err != nil { + return err + } + + return extractOk(data) +} diff --git a/vendor/gopkg.in/telebot.v3/context.go b/vendor/gopkg.in/telebot.v3/context.go new file mode 100644 index 0000000000..6bd9ec1381 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/context.go @@ -0,0 +1,471 @@ +package telebot + +import ( + "errors" + "strings" + "sync" + "time" +) + +// HandlerFunc represents a handler function, which is +// used to handle actual endpoints. +type HandlerFunc func(Context) error + +// Context wraps an update and represents the context of current event. +type Context interface { + // Bot returns the bot instance. + Bot() *Bot + + // Update returns the original update. + Update() Update + + // Message returns stored message if such presented. + Message() *Message + + // Callback returns stored callback if such presented. + Callback() *Callback + + // Query returns stored query if such presented. + Query() *Query + + // InlineResult returns stored inline result if such presented. + InlineResult() *InlineResult + + // ShippingQuery returns stored shipping query if such presented. + ShippingQuery() *ShippingQuery + + // PreCheckoutQuery returns stored pre checkout query if such presented. + PreCheckoutQuery() *PreCheckoutQuery + + // Poll returns stored poll if such presented. + Poll() *Poll + + // PollAnswer returns stored poll answer if such presented. + PollAnswer() *PollAnswer + + // ChatMember returns chat member changes. + ChatMember() *ChatMemberUpdate + + // ChatJoinRequest returns cha + ChatJoinRequest() *ChatJoinRequest + + // Migration returns both migration from and to chat IDs. + Migration() (int64, int64) + + // Sender returns the current recipient, depending on the context type. + // Returns nil if user is not presented. + Sender() *User + + // Chat returns the current chat, depending on the context type. + // Returns nil if chat is not presented. + Chat() *Chat + + // Recipient combines both Sender and Chat functions. If there is no user + // the chat will be returned. The native context cannot be without sender, + // but it is useful in the case when the context created intentionally + // by the NewContext constructor and have only Chat field inside. + Recipient() Recipient + + // Text returns the message text, depending on the context type. + // In the case when no related data presented, returns an empty string. + Text() string + + // Data returns the current data, depending on the context type. + // If the context contains command, returns its arguments string. + // If the context contains payment, returns its payload. + // In the case when no related data presented, returns an empty string. + Data() string + + // Args returns a raw slice of command or callback arguments as strings. + // The message arguments split by space, while the callback's ones by a "|" symbol. + Args() []string + + // Send sends a message to the current recipient. + // See Send from bot.go. + Send(what interface{}, opts ...interface{}) error + + // SendAlbum sends an album to the current recipient. + // See SendAlbum from bot.go. + SendAlbum(a Album, opts ...interface{}) error + + // Reply replies to the current message. + // See Reply from bot.go. + Reply(what interface{}, opts ...interface{}) error + + // Forward forwards the given message to the current recipient. + // See Forward from bot.go. + Forward(msg Editable, opts ...interface{}) error + + // ForwardTo forwards the current message to the given recipient. + // See Forward from bot.go + ForwardTo(to Recipient, opts ...interface{}) error + + // Edit edits the current message. + // See Edit from bot.go. + Edit(what interface{}, opts ...interface{}) error + + // EditCaption edits the caption of the current message. + // See EditCaption from bot.go. + EditCaption(caption string, opts ...interface{}) error + + // EditOrSend edits the current message if the update is callback, + // otherwise the content is sent to the chat as a separate message. + EditOrSend(what interface{}, opts ...interface{}) error + + // EditOrReply edits the current message if the update is callback, + // otherwise the content is replied as a separate message. + EditOrReply(what interface{}, opts ...interface{}) error + + // Delete removes the current message. + // See Delete from bot.go. + Delete() error + + // DeleteAfter waits for the duration to elapse and then removes the + // message. It handles an error automatically using b.OnError callback. + // It returns a Timer that can be used to cancel the call using its Stop method. + DeleteAfter(d time.Duration) *time.Timer + + // Notify updates the chat action for the current recipient. + // See Notify from bot.go. + Notify(action ChatAction) error + + // Ship replies to the current shipping query. + // See Ship from bot.go. + Ship(what ...interface{}) error + + // Accept finalizes the current deal. + // See Accept from bot.go. + Accept(errorMessage ...string) error + + // Answer sends a response to the current inline query. + // See Answer from bot.go. + Answer(resp *QueryResponse) error + + // Respond sends a response for the current callback query. + // See Respond from bot.go. + Respond(resp ...*CallbackResponse) error + + // Get retrieves data from the context. + Get(key string) interface{} + + // Set saves data in the context. + Set(key string, val interface{}) +} + +// nativeContext is a native implementation of the Context interface. +// "context" is taken by context package, maybe there is a better name. +type nativeContext struct { + b *Bot + u Update + lock sync.RWMutex + store map[string]interface{} +} + +func (c *nativeContext) Bot() *Bot { + return c.b +} + +func (c *nativeContext) Update() Update { + return c.u +} + +func (c *nativeContext) Message() *Message { + switch { + case c.u.Message != nil: + return c.u.Message + case c.u.Callback != nil: + return c.u.Callback.Message + case c.u.EditedMessage != nil: + return c.u.EditedMessage + case c.u.ChannelPost != nil: + if c.u.ChannelPost.PinnedMessage != nil { + return c.u.ChannelPost.PinnedMessage + } + return c.u.ChannelPost + case c.u.EditedChannelPost != nil: + return c.u.EditedChannelPost + default: + return nil + } +} + +func (c *nativeContext) Callback() *Callback { + return c.u.Callback +} + +func (c *nativeContext) Query() *Query { + return c.u.Query +} + +func (c *nativeContext) InlineResult() *InlineResult { + return c.u.InlineResult +} + +func (c *nativeContext) ShippingQuery() *ShippingQuery { + return c.u.ShippingQuery +} + +func (c *nativeContext) PreCheckoutQuery() *PreCheckoutQuery { + return c.u.PreCheckoutQuery +} + +func (c *nativeContext) ChatMember() *ChatMemberUpdate { + switch { + case c.u.ChatMember != nil: + return c.u.ChatMember + case c.u.MyChatMember != nil: + return c.u.MyChatMember + default: + return nil + } +} + +func (c *nativeContext) ChatJoinRequest() *ChatJoinRequest { + return c.u.ChatJoinRequest +} + +func (c *nativeContext) Poll() *Poll { + return c.u.Poll +} + +func (c *nativeContext) PollAnswer() *PollAnswer { + return c.u.PollAnswer +} + +func (c *nativeContext) Migration() (int64, int64) { + return c.u.Message.MigrateFrom, c.u.Message.MigrateTo +} + +func (c *nativeContext) Sender() *User { + switch { + case c.u.Callback != nil: + return c.u.Callback.Sender + case c.Message() != nil: + return c.Message().Sender + case c.u.Query != nil: + return c.u.Query.Sender + case c.u.InlineResult != nil: + return c.u.InlineResult.Sender + case c.u.ShippingQuery != nil: + return c.u.ShippingQuery.Sender + case c.u.PreCheckoutQuery != nil: + return c.u.PreCheckoutQuery.Sender + case c.u.PollAnswer != nil: + return c.u.PollAnswer.Sender + case c.u.MyChatMember != nil: + return c.u.MyChatMember.Sender + case c.u.ChatMember != nil: + return c.u.ChatMember.Sender + case c.u.ChatJoinRequest != nil: + return c.u.ChatJoinRequest.Sender + default: + return nil + } +} + +func (c *nativeContext) Chat() *Chat { + switch { + case c.Message() != nil: + return c.Message().Chat + case c.u.MyChatMember != nil: + return c.u.MyChatMember.Chat + case c.u.ChatMember != nil: + return c.u.ChatMember.Chat + case c.u.ChatJoinRequest != nil: + return c.u.ChatJoinRequest.Chat + default: + return nil + } +} + +func (c *nativeContext) Recipient() Recipient { + chat := c.Chat() + if chat != nil { + return chat + } + return c.Sender() +} + +func (c *nativeContext) Text() string { + m := c.Message() + if m == nil { + return "" + } + if m.Caption != "" { + return m.Caption + } + return m.Text +} + +func (c *nativeContext) Data() string { + switch { + case c.u.Message != nil: + return c.u.Message.Payload + case c.u.Callback != nil: + return c.u.Callback.Data + case c.u.Query != nil: + return c.u.Query.Text + case c.u.InlineResult != nil: + return c.u.InlineResult.Query + case c.u.ShippingQuery != nil: + return c.u.ShippingQuery.Payload + case c.u.PreCheckoutQuery != nil: + return c.u.PreCheckoutQuery.Payload + default: + return "" + } +} + +func (c *nativeContext) Args() []string { + switch { + case c.u.Message != nil: + payload := strings.Trim(c.u.Message.Payload, " ") + if payload != "" { + return strings.Split(payload, " ") + } + case c.u.Callback != nil: + return strings.Split(c.u.Callback.Data, "|") + case c.u.Query != nil: + return strings.Split(c.u.Query.Text, " ") + case c.u.InlineResult != nil: + return strings.Split(c.u.InlineResult.Query, " ") + } + return nil +} + +func (c *nativeContext) Send(what interface{}, opts ...interface{}) error { + _, err := c.b.Send(c.Recipient(), what, opts...) + return err +} + +func (c *nativeContext) SendAlbum(a Album, opts ...interface{}) error { + _, err := c.b.SendAlbum(c.Recipient(), a, opts...) + return err +} + +func (c *nativeContext) Reply(what interface{}, opts ...interface{}) error { + msg := c.Message() + if msg == nil { + return ErrBadContext + } + _, err := c.b.Reply(msg, what, opts...) + return err +} + +func (c *nativeContext) Forward(msg Editable, opts ...interface{}) error { + _, err := c.b.Forward(c.Recipient(), msg, opts...) + return err +} + +func (c *nativeContext) ForwardTo(to Recipient, opts ...interface{}) error { + msg := c.Message() + if msg == nil { + return ErrBadContext + } + _, err := c.b.Forward(to, msg, opts...) + return err +} + +func (c *nativeContext) Edit(what interface{}, opts ...interface{}) error { + if c.u.InlineResult != nil { + _, err := c.b.Edit(c.u.InlineResult, what, opts...) + return err + } + if c.u.Callback != nil { + _, err := c.b.Edit(c.u.Callback, what, opts...) + return err + } + return ErrBadContext +} + +func (c *nativeContext) EditCaption(caption string, opts ...interface{}) error { + if c.u.InlineResult != nil { + _, err := c.b.EditCaption(c.u.InlineResult, caption, opts...) + return err + } + if c.u.Callback != nil { + _, err := c.b.EditCaption(c.u.Callback, caption, opts...) + return err + } + return ErrBadContext +} + +func (c *nativeContext) EditOrSend(what interface{}, opts ...interface{}) error { + err := c.Edit(what, opts...) + if err == ErrBadContext { + return c.Send(what, opts...) + } + return err +} + +func (c *nativeContext) EditOrReply(what interface{}, opts ...interface{}) error { + err := c.Edit(what, opts...) + if err == ErrBadContext { + return c.Reply(what, opts...) + } + return err +} + +func (c *nativeContext) Delete() error { + msg := c.Message() + if msg == nil { + return ErrBadContext + } + return c.b.Delete(msg) +} + +func (c *nativeContext) DeleteAfter(d time.Duration) *time.Timer { + return time.AfterFunc(d, func() { + if err := c.Delete(); err != nil { + c.b.OnError(err, c) + } + }) +} + +func (c *nativeContext) Notify(action ChatAction) error { + return c.b.Notify(c.Recipient(), action) +} + +func (c *nativeContext) Ship(what ...interface{}) error { + if c.u.ShippingQuery == nil { + return errors.New("telebot: context shipping query is nil") + } + return c.b.Ship(c.u.ShippingQuery, what...) +} + +func (c *nativeContext) Accept(errorMessage ...string) error { + if c.u.PreCheckoutQuery == nil { + return errors.New("telebot: context pre checkout query is nil") + } + return c.b.Accept(c.u.PreCheckoutQuery, errorMessage...) +} + +func (c *nativeContext) Answer(resp *QueryResponse) error { + if c.u.Query == nil { + return errors.New("telebot: context inline query is nil") + } + return c.b.Answer(c.u.Query, resp) +} + +func (c *nativeContext) Respond(resp ...*CallbackResponse) error { + if c.u.Callback == nil { + return errors.New("telebot: context callback is nil") + } + return c.b.Respond(c.u.Callback, resp...) +} + +func (c *nativeContext) Set(key string, value interface{}) { + c.lock.Lock() + defer c.lock.Unlock() + + if c.store == nil { + c.store = make(map[string]interface{}) + } + c.store[key] = value +} + +func (c *nativeContext) Get(key string) interface{} { + c.lock.RLock() + defer c.lock.RUnlock() + return c.store[key] +} diff --git a/vendor/gopkg.in/telebot.v3/editable.go b/vendor/gopkg.in/telebot.v3/editable.go new file mode 100644 index 0000000000..ec1fb5b93e --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/editable.go @@ -0,0 +1,30 @@ +package telebot + +// Editable is an interface for all objects that +// provide "message signature", a pair of 32-bit +// message ID and 64-bit chat ID, both required +// for edit operations. +// +// Use case: DB model struct for messages to-be +// edited with, say two columns: msg_id,chat_id +// could easily implement MessageSig() making +// instances of stored messages editable. +type Editable interface { + // MessageSig is a "message signature". + // + // For inline messages, return chatID = 0. + MessageSig() (messageID string, chatID int64) +} + +// StoredMessage is an example struct suitable for being +// stored in the database as-is or being embedded into +// a larger struct, which is often the case (you might +// want to store some metadata alongside, or might not.) +type StoredMessage struct { + MessageID string `sql:"message_id" json:"message_id"` + ChatID int64 `sql:"chat_id" json:"chat_id"` +} + +func (x StoredMessage) MessageSig() (string, int64) { + return x.MessageID, x.ChatID +} diff --git a/vendor/gopkg.in/telebot.v3/errors.go b/vendor/gopkg.in/telebot.v3/errors.go new file mode 100644 index 0000000000..fdb62feb9e --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/errors.go @@ -0,0 +1,237 @@ +package telebot + +import ( + "fmt" + "strings" +) + +type ( + Error struct { + Code int + Description string + Message string + } + + FloodError struct { + err *Error + RetryAfter int + } + + GroupError struct { + err *Error + MigratedTo int64 + } +) + +// ʔ returns description of error. +// A tiny shortcut to make code clearer. +func (err *Error) ʔ() string { + return err.Description +} + +// Error implements error interface. +func (err *Error) Error() string { + msg := err.Message + if msg == "" { + split := strings.Split(err.Description, ": ") + if len(split) == 2 { + msg = split[1] + } else { + msg = err.Description + } + } + return fmt.Sprintf("telegram: %s (%d)", msg, err.Code) +} + +// Error implements error interface. +func (err FloodError) Error() string { + return err.err.Error() +} + +// Error implements error interface. +func (err GroupError) Error() string { + return err.err.Error() +} + +// NewError returns new Error instance with given description. +// First element of msgs is Description. The second is optional Message. +func NewError(code int, msgs ...string) *Error { + err := &Error{Code: code} + if len(msgs) >= 1 { + err.Description = msgs[0] + } + if len(msgs) >= 2 { + err.Message = msgs[1] + } + return err +} + +// General errors +var ( + ErrTooLarge = NewError(400, "Request Entity Too Large") + ErrUnauthorized = NewError(401, "Unauthorized") + ErrNotFound = NewError(404, "Not Found") + ErrInternal = NewError(500, "Internal Server Error") +) + +// Bad request errors +var ( + ErrBadButtonData = NewError(400, "Bad Request: BUTTON_DATA_INVALID") + ErrBadPollOptions = NewError(400, "Bad Request: expected an Array of String as options") + ErrBadURLContent = NewError(400, "Bad Request: failed to get HTTP URL content") + ErrCantEditMessage = NewError(400, "Bad Request: message can't be edited") + ErrCantRemoveOwner = NewError(400, "Bad Request: can't remove chat owner") + ErrCantUploadFile = NewError(400, "Bad Request: can't upload file by URL") + ErrCantUseMediaInAlbum = NewError(400, "Bad Request: can't use the media of the specified type in the album") + ErrChatAboutNotModified = NewError(400, "Bad Request: chat description is not modified") + ErrChatNotFound = NewError(400, "Bad Request: chat not found") + ErrEmptyChatID = NewError(400, "Bad Request: chat_id is empty") + ErrEmptyMessage = NewError(400, "Bad Request: message must be non-empty") + ErrEmptyText = NewError(400, "Bad Request: text is empty") + ErrFailedImageProcess = NewError(400, "Bad Request: IMAGE_PROCESS_FAILED", "Image process failed") + ErrGroupMigrated = NewError(400, "Bad Request: group chat was upgraded to a supergroup chat") + ErrMessageNotModified = NewError(400, "Bad Request: message is not modified") + ErrNoRightsToDelete = NewError(400, "Bad Request: message can't be deleted") + ErrNoRightsToRestrict = NewError(400, "Bad Request: not enough rights to restrict/unrestrict chat member") + ErrNoRightsToSend = NewError(400, "Bad Request: have no rights to send a message") + ErrNoRightsToSendGifs = NewError(400, "Bad Request: CHAT_SEND_GIFS_FORBIDDEN", "sending GIFS is not allowed in this chat") + ErrNoRightsToSendPhoto = NewError(400, "Bad Request: not enough rights to send photos to the chat") + ErrNoRightsToSendStickers = NewError(400, "Bad Request: not enough rights to send stickers to the chat") + ErrNotFoundToDelete = NewError(400, "Bad Request: message to delete not found") + ErrNotFoundToForward = NewError(400, "Bad Request: message to forward not found") + ErrNotFoundToReply = NewError(400, "Bad Request: reply message not found") + ErrQueryTooOld = NewError(400, "Bad Request: query is too old and response timeout expired or query ID is invalid") + ErrSameMessageContent = NewError(400, "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message") + ErrStickerEmojisInvalid = NewError(400, "Bad Request: invalid sticker emojis") + ErrStickerSetInvalid = NewError(400, "Bad Request: STICKERSET_INVALID", "Stickerset is invalid") + ErrStickerSetInvalidName = NewError(400, "Bad Request: invalid sticker set name is specified") + ErrStickerSetNameOccupied = NewError(400, "Bad Request: sticker set name is already occupied") + ErrTooLongMarkup = NewError(400, "Bad Request: reply markup is too long") + ErrTooLongMessage = NewError(400, "Bad Request: message is too long") + ErrUserIsAdmin = NewError(400, "Bad Request: user is an administrator of the chat") + ErrWrongFileID = NewError(400, "Bad Request: wrong file identifier/HTTP URL specified") + ErrWrongFileIDCharacter = NewError(400, "Bad Request: wrong remote file id specified: Wrong character in the string") + ErrWrongFileIDLength = NewError(400, "Bad Request: wrong remote file id specified: Wrong string length") + ErrWrongFileIDPadding = NewError(400, "Bad Request: wrong remote file id specified: Wrong padding in the string") + ErrWrongFileIDSymbol = NewError(400, "Bad Request: wrong remote file id specified: can't unserialize it. Wrong last symbol") + ErrWrongTypeOfContent = NewError(400, "Bad Request: wrong type of the web page content") + ErrWrongURL = NewError(400, "Bad Request: wrong HTTP URL specified") + ErrForwardMessage = NewError(400, "Bad Request: administrators of the chat restricted message forwarding") +) + +// Forbidden errors +var ( + ErrBlockedByUser = NewError(403, "Forbidden: bot was blocked by the user") + ErrKickedFromGroup = NewError(403, "Forbidden: bot was kicked from the group chat") + ErrKickedFromSuperGroup = NewError(403, "Forbidden: bot was kicked from the supergroup chat") + ErrNotStartedByUser = NewError(403, "Forbidden: bot can't initiate conversation with a user") + ErrUserIsDeactivated = NewError(403, "Forbidden: user is deactivated") +) + +// Err returns Error instance by given description. +func Err(s string) error { + switch s { + case ErrTooLarge.ʔ(): + return ErrTooLarge + case ErrUnauthorized.ʔ(): + return ErrUnauthorized + case ErrNotFound.ʔ(): + return ErrNotFound + case ErrInternal.ʔ(): + return ErrInternal + case ErrBadButtonData.ʔ(): + return ErrBadButtonData + case ErrBadPollOptions.ʔ(): + return ErrBadPollOptions + case ErrBadURLContent.ʔ(): + return ErrBadURLContent + case ErrCantEditMessage.ʔ(): + return ErrCantEditMessage + case ErrCantRemoveOwner.ʔ(): + return ErrCantRemoveOwner + case ErrCantUploadFile.ʔ(): + return ErrCantUploadFile + case ErrCantUseMediaInAlbum.ʔ(): + return ErrCantUseMediaInAlbum + case ErrChatAboutNotModified.ʔ(): + return ErrChatAboutNotModified + case ErrChatNotFound.ʔ(): + return ErrChatNotFound + case ErrEmptyChatID.ʔ(): + return ErrEmptyChatID + case ErrEmptyMessage.ʔ(): + return ErrEmptyMessage + case ErrEmptyText.ʔ(): + return ErrEmptyText + case ErrFailedImageProcess.ʔ(): + return ErrFailedImageProcess + case ErrGroupMigrated.ʔ(): + return ErrGroupMigrated + case ErrMessageNotModified.ʔ(): + return ErrMessageNotModified + case ErrNoRightsToDelete.ʔ(): + return ErrNoRightsToDelete + case ErrNoRightsToRestrict.ʔ(): + return ErrNoRightsToRestrict + case ErrNoRightsToSend.ʔ(): + return ErrNoRightsToSend + case ErrNoRightsToSendGifs.ʔ(): + return ErrNoRightsToSendGifs + case ErrNoRightsToSendPhoto.ʔ(): + return ErrNoRightsToSendPhoto + case ErrNoRightsToSendStickers.ʔ(): + return ErrNoRightsToSendStickers + case ErrNotFoundToDelete.ʔ(): + return ErrNotFoundToDelete + case ErrNotFoundToForward.ʔ(): + return ErrNotFoundToForward + case ErrNotFoundToReply.ʔ(): + return ErrNotFoundToReply + case ErrQueryTooOld.ʔ(): + return ErrQueryTooOld + case ErrSameMessageContent.ʔ(): + return ErrSameMessageContent + case ErrStickerEmojisInvalid.ʔ(): + return ErrStickerEmojisInvalid + case ErrStickerSetInvalid.ʔ(): + return ErrStickerSetInvalid + case ErrStickerSetInvalidName.ʔ(): + return ErrStickerSetInvalidName + case ErrStickerSetNameOccupied.ʔ(): + return ErrStickerSetNameOccupied + case ErrTooLongMarkup.ʔ(): + return ErrTooLongMarkup + case ErrTooLongMessage.ʔ(): + return ErrTooLongMessage + case ErrUserIsAdmin.ʔ(): + return ErrUserIsAdmin + case ErrWrongFileID.ʔ(): + return ErrWrongFileID + case ErrWrongFileIDCharacter.ʔ(): + return ErrWrongFileIDCharacter + case ErrWrongFileIDLength.ʔ(): + return ErrWrongFileIDLength + case ErrWrongFileIDPadding.ʔ(): + return ErrWrongFileIDPadding + case ErrWrongFileIDSymbol.ʔ(): + return ErrWrongFileIDSymbol + case ErrWrongTypeOfContent.ʔ(): + return ErrWrongTypeOfContent + case ErrWrongURL.ʔ(): + return ErrWrongURL + case ErrBlockedByUser.ʔ(): + return ErrBlockedByUser + case ErrKickedFromGroup.ʔ(): + return ErrKickedFromGroup + case ErrKickedFromSuperGroup.ʔ(): + return ErrKickedFromSuperGroup + case ErrNotStartedByUser.ʔ(): + return ErrNotStartedByUser + case ErrUserIsDeactivated.ʔ(): + return ErrUserIsDeactivated + case ErrForwardMessage.ʔ(): + return ErrForwardMessage + default: + return nil + } +} diff --git a/vendor/gopkg.in/telebot.v3/file.go b/vendor/gopkg.in/telebot.v3/file.go new file mode 100644 index 0000000000..707c432933 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/file.go @@ -0,0 +1,87 @@ +package telebot + +import ( + "io" + "os" +) + +// File object represents any sort of file. +type File struct { + FileID string `json:"file_id"` + UniqueID string `json:"file_unique_id"` + FileSize int `json:"file_size"` + + // FilePath is used for files on Telegram server. + FilePath string `json:"file_path"` + + // FileLocal uis ed for files on local file system. + FileLocal string `json:"file_local"` + + // FileURL is used for file on the internet. + FileURL string `json:"file_url"` + + // FileReader is used for file backed with io.Reader. + FileReader io.Reader `json:"-"` + + fileName string +} + +// FromDisk constructs a new local (on-disk) file object. +// +// Note, it returns File, not *File for a very good reason: +// in telebot, File is pretty much an embeddable struct, +// so upon uploading media you'll need to set embedded File +// with something. NewFile() returning File makes it a one-liner. +// +// photo := &tele.Photo{File: tele.FromDisk("chicken.jpg")} +// +func FromDisk(filename string) File { + return File{FileLocal: filename} +} + +// FromURL constructs a new file on provided HTTP URL. +// +// Note, it returns File, not *File for a very good reason: +// in telebot, File is pretty much an embeddable struct, +// so upon uploading media you'll need to set embedded File +// with something. NewFile() returning File makes it a one-liner. +// +// photo := &tele.Photo{File: tele.FromURL("https://site.com/picture.jpg")} +// +func FromURL(url string) File { + return File{FileURL: url} +} + +// FromReader constructs a new file from io.Reader. +// +// Note, it returns File, not *File for a very good reason: +// in telebot, File is pretty much an embeddable struct, +// so upon uploading media you'll need to set embedded File +// with something. NewFile() returning File makes it a one-liner. +// +// photo := &tele.Photo{File: tele.FromReader(bytes.NewReader(...))} +// +func FromReader(reader io.Reader) File { + return File{FileReader: reader} +} + +func (f *File) stealRef(g *File) { + if g.OnDisk() { + f.FileLocal = g.FileLocal + } + + if g.FileURL != "" { + f.FileURL = g.FileURL + } +} + +// InCloud tells whether the file is present on Telegram servers. +func (f *File) InCloud() bool { + return f.FileID != "" +} + +// OnDisk will return true if file is present on disk. +func (f *File) OnDisk() bool { + _, err := os.Stat(f.FileLocal) + return err == nil +} diff --git a/vendor/gopkg.in/telebot.v3/games.go b/vendor/gopkg.in/telebot.v3/games.go new file mode 100644 index 0000000000..0a1276d555 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/games.go @@ -0,0 +1,99 @@ +package telebot + +import ( + "encoding/json" + "strconv" +) + +// Game object represents a game. +// Their short names acts as unique identifiers. +type Game struct { + Name string `json:"game_short_name"` + + Title string `json:"title"` + Description string `json:"description"` + Photo *Photo `json:"photo"` + + // (Optional) + Text string `json:"text"` + Entities []MessageEntity `json:"text_entities"` + Animation *Animation `json:"animation"` +} + +// GameHighScore object represents one row +// of the high scores table for a game. +type GameHighScore struct { + User *User `json:"user"` + Position int `json:"position"` + + Score int `json:"score"` + Force bool `json:"force"` + NoEdit bool `json:"disable_edit_message"` +} + +// GameScores returns the score of the specified user +// and several of their neighbors in a game. +// +// This function will panic upon nil Editable. +// +// Currently, it returns scores for the target user, +// plus two of their closest neighbors on each side. +// Will also return the top three users +// if the user and his neighbors are not among them. +// +func (b *Bot) GameScores(user Recipient, msg Editable) ([]GameHighScore, error) { + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "user_id": user.Recipient(), + } + + if chatID == 0 { // if inline message + params["inline_message_id"] = msgID + } else { + params["chat_id"] = strconv.FormatInt(chatID, 10) + params["message_id"] = msgID + } + + data, err := b.Raw("getGameHighScores", params) + if err != nil { + return nil, err + } + + var resp struct { + Result []GameHighScore + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + return resp.Result, nil +} + +// SetGameScore sets the score of the specified user in a game. +// +// If the message was sent by the bot, returns the edited Message, +// otherwise returns nil and ErrTrueResult. +// +func (b *Bot) SetGameScore(user Recipient, msg Editable, score GameHighScore) (*Message, error) { + msgID, chatID := msg.MessageSig() + + params := map[string]string{ + "user_id": user.Recipient(), + "score": strconv.Itoa(score.Score), + "force": strconv.FormatBool(score.Force), + "disable_edit_message": strconv.FormatBool(score.NoEdit), + } + + if chatID == 0 { // if inline message + params["inline_message_id"] = msgID + } else { + params["chat_id"] = strconv.FormatInt(chatID, 10) + params["message_id"] = msgID + } + + data, err := b.Raw("setGameScore", params) + if err != nil { + return nil, err + } + return extractMessage(data) +} diff --git a/vendor/gopkg.in/telebot.v3/inline.go b/vendor/gopkg.in/telebot.v3/inline.go new file mode 100644 index 0000000000..b3691393cb --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/inline.go @@ -0,0 +1,139 @@ +package telebot + +import ( + "encoding/json" + "fmt" +) + +// Query is an incoming inline query. When the user sends +// an empty query, your bot could return some default or +// trending results. +type Query struct { + // Unique identifier for this query. 1-64 bytes. + ID string `json:"id"` + + // Sender. + Sender *User `json:"from"` + + // Sender location, only for bots that request user location. + Location *Location `json:"location"` + + // Text of the query (up to 512 characters). + Text string `json:"query"` + + // Offset of the results to be returned, can be controlled by the bot. + Offset string `json:"offset"` + + // ChatType of the type of the chat, from which the inline query was sent. + ChatType string `json:"chat_type"` +} + +// QueryResponse builds a response to an inline Query. +type QueryResponse struct { + // The ID of the query to which this is a response. + // + // Note: Telebot sets this field automatically! + QueryID string `json:"inline_query_id"` + + // The results for the inline query. + Results Results `json:"results"` + + // (Optional) The maximum amount of time in seconds that the result + // of the inline query may be cached on the server. + CacheTime int `json:"cache_time,omitempty"` + + // (Optional) Pass True, if results may be cached on the server side + // only for the user that sent the query. By default, results may + // be returned to any user who sends the same query. + IsPersonal bool `json:"is_personal"` + + // (Optional) Pass the offset that a client should send in the next + // query with the same text to receive more results. Pass an empty + // string if there are no more results or if you don‘t support + // pagination. Offset length can’t exceed 64 bytes. + NextOffset string `json:"next_offset"` + + // (Optional) If passed, clients will display a button with specified + // text that switches the user to a private chat with the bot and sends + // the bot a start message with the parameter switch_pm_parameter. + SwitchPMText string `json:"switch_pm_text,omitempty"` + + // (Optional) Parameter for the start message sent to the bot when user + // presses the switch button. + SwitchPMParameter string `json:"switch_pm_parameter,omitempty"` +} + +// InlineResult represents a result of an inline query that was chosen +// by the user and sent to their chat partner. +type InlineResult struct { + Sender *User `json:"from"` + Location *Location `json:"location,omitempty"` + ResultID string `json:"result_id"` + Query string `json:"query"` + MessageID string `json:"inline_message_id"` // inline messages only! +} + +// MessageSig satisfies Editable interface. +func (ir *InlineResult) MessageSig() (string, int64) { + return ir.MessageID, 0 +} + +// Result represents one result of an inline query. +type Result interface { + ResultID() string + SetResultID(string) + SetParseMode(ParseMode) + SetContent(InputMessageContent) + SetReplyMarkup(*ReplyMarkup) + Process(*Bot) +} + +// Results is a slice wrapper for convenient marshalling. +type Results []Result + +// MarshalJSON makes sure IQRs have proper IDs and Type variables set. +func (results Results) MarshalJSON() ([]byte, error) { + for _, result := range results { + if result.ResultID() == "" { + result.SetResultID(fmt.Sprintf("%d", &result)) + } + if err := inferIQR(result); err != nil { + return nil, err + } + } + + return json.Marshal([]Result(results)) +} + +func inferIQR(result Result) error { + switch r := result.(type) { + case *ArticleResult: + r.Type = "article" + case *AudioResult: + r.Type = "audio" + case *ContactResult: + r.Type = "contact" + case *DocumentResult: + r.Type = "document" + case *GifResult: + r.Type = "gif" + case *LocationResult: + r.Type = "location" + case *Mpeg4GifResult: + r.Type = "mpeg4_gif" + case *PhotoResult: + r.Type = "photo" + case *VenueResult: + r.Type = "venue" + case *VideoResult: + r.Type = "video" + case *VoiceResult: + r.Type = "voice" + case *StickerResult: + r.Type = "sticker" + default: + return fmt.Errorf("telebot: result %v is not supported", result) + } + + return nil +} diff --git a/vendor/gopkg.in/telebot.v3/inline_types.go b/vendor/gopkg.in/telebot.v3/inline_types.go new file mode 100644 index 0000000000..d93cffc209 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/inline_types.go @@ -0,0 +1,373 @@ +package telebot + +// ResultBase must be embedded into all IQRs. +type ResultBase struct { + // Unique identifier for this result, 1-64 Bytes. + // If left unspecified, a 64-bit FNV-1 hash will be calculated + ID string `json:"id"` + + // Ignore. This field gets set automatically. + Type string `json:"type"` + + // Optional. Send Markdown or HTML, if you want Telegram apps to show + // bold, italic, fixed-width text or inline URLs in the media caption. + ParseMode ParseMode `json:"parse_mode,omitempty"` + + // Optional. Content of the message to be sent. + Content InputMessageContent `json:"input_message_content,omitempty"` + + // Optional. Inline keyboard attached to the message. + ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"` +} + +// ResultID returns ResultBase.ID. +func (r *ResultBase) ResultID() string { + return r.ID +} + +// SetResultID sets ResultBase.ID. +func (r *ResultBase) SetResultID(id string) { + r.ID = id +} + +// SetParseMode sets ResultBase.ParseMode. +func (r *ResultBase) SetParseMode(mode ParseMode) { + r.ParseMode = mode +} + +// SetContent sets ResultBase.Content. +func (r *ResultBase) SetContent(content InputMessageContent) { + r.Content = content +} + +// SetReplyMarkup sets ResultBase.ReplyMarkup. +func (r *ResultBase) SetReplyMarkup(markup *ReplyMarkup) { + r.ReplyMarkup = markup +} + +func (r *ResultBase) Process(b *Bot) { + if r.ParseMode == ModeDefault { + r.ParseMode = b.parseMode + } + if r.Content != nil { + c, ok := r.Content.(*InputTextMessageContent) + if ok && c.ParseMode == ModeDefault { + c.ParseMode = r.ParseMode + } + } + if r.ReplyMarkup != nil { + processButtons(r.ReplyMarkup.InlineKeyboard) + } +} + +// ArticleResult represents a link to an article or web page. +type ArticleResult struct { + ResultBase + + // Title of the result. + Title string `json:"title"` + + // Message text. Shortcut (and mutually exclusive to) specifying + // InputMessageContent. + Text string `json:"message_text,omitempty"` + + // Optional. URL of the result. + URL string `json:"url,omitempty"` + + // Optional. Pass True, if you don't want the URL to be shown in the message. + HideURL bool `json:"hide_url,omitempty"` + + // Optional. Short description of the result. + Description string `json:"description,omitempty"` + + // Optional. URL of the thumbnail for the result. + ThumbURL string `json:"thumb_url,omitempty"` + + // Optional. Width of the thumbnail for the result. + ThumbWidth int `json:"thumb_width,omitempty"` + + // Optional. Height of the thumbnail for the result. + ThumbHeight int `json:"thumb_height,omitempty"` +} + +// AudioResult represents a link to an mp3 audio file. +type AudioResult struct { + ResultBase + + // Title. + Title string `json:"title"` + + // A valid URL for the audio file. + URL string `json:"audio_url"` + + // Optional. Performer. + Performer string `json:"performer,omitempty"` + + // Optional. Audio duration in seconds. + Duration int `json:"audio_duration,omitempty"` + + // Optional. Caption, 0-1024 characters. + Caption string `json:"caption,omitempty"` + + // If Cache != "", it'll be used instead + Cache string `json:"audio_file_id,omitempty"` +} + +// ContactResult represents a contact with a phone number. +type ContactResult struct { + ResultBase + + // Contact's phone number. + PhoneNumber string `json:"phone_number"` + + // Optional. Additional data about the contact in the form of a vCard, 0-2048 bytes. + VCard string `json:"vcard,omitempty"` + + // Contact's first name. + FirstName string `json:"first_name"` + + // Optional. Contact's last name. + LastName string `json:"last_name,omitempty"` + + // Optional. URL of the thumbnail for the result. + ThumbURL string `json:"thumb_url,omitempty"` + + // Optional. Width of the thumbnail for the result. + ThumbWidth int `json:"thumb_width,omitempty"` + + // Optional. Height of the thumbnail for the result. + ThumbHeight int `json:"thumb_height,omitempty"` +} + +// DocumentResult represents a link to a file. +type DocumentResult struct { + ResultBase + + // Title for the result. + Title string `json:"title"` + + // A valid URL for the file + URL string `json:"document_url"` + + // Mime type of the content of the file, either “application/pdf” or + // “application/zip”. + MIME string `json:"mime_type"` + + // Optional. Caption of the document to be sent, 0-200 characters. + Caption string `json:"caption,omitempty"` + + // Optional. Short description of the result. + Description string `json:"description,omitempty"` + + // Optional. URL of the thumbnail (jpeg only) for the file. + ThumbURL string `json:"thumb_url,omitempty"` + + // Optional. Width of the thumbnail for the result. + ThumbWidth int `json:"thumb_width,omitempty"` + + // Optional. Height of the thumbnail for the result. + ThumbHeight int `json:"thumb_height,omitempty"` + + // If Cache != "", it'll be used instead + Cache string `json:"document_file_id,omitempty"` +} + +// GifResult represents a link to an animated GIF file. +type GifResult struct { + ResultBase + + // A valid URL for the GIF file. File size must not exceed 1MB. + URL string `json:"gif_url"` + + // Optional. Width of the GIF. + Width int `json:"gif_width,omitempty"` + + // Optional. Height of the GIF. + Height int `json:"gif_height,omitempty"` + + // Optional. Duration of the GIF. + Duration int `json:"gif_duration,omitempty"` + + // URL of the static thumbnail for the result (jpeg or gif). + ThumbURL string `json:"thumb_url"` + + // Optional. MIME type of the thumbnail, must be one of + // “image/jpeg”, “image/gif”, or “video/mp4”. + ThumbMIME string `json:"thumb_mime_type,omitempty"` + + // Optional. Title for the result. + Title string `json:"title,omitempty"` + + // Optional. Caption of the GIF file to be sent, 0-200 characters. + Caption string `json:"caption,omitempty"` + + // If Cache != "", it'll be used instead + Cache string `json:"gif_file_id,omitempty"` +} + +// LocationResult represents a location on a map. +type LocationResult struct { + ResultBase + + Location + + // Location title. + Title string `json:"title"` + + // Optional. Url of the thumbnail for the result. + ThumbURL string `json:"thumb_url,omitempty"` +} + +// Mpeg4GifResult represents a link to a video animation +// (H.264/MPEG-4 AVC video without sound). +type Mpeg4GifResult struct { + ResultBase + + // A valid URL for the MP4 file. + URL string `json:"mpeg4_url"` + + // Optional. Video width. + Width int `json:"mpeg4_width,omitempty"` + + // Optional. Video height. + Height int `json:"mpeg4_height,omitempty"` + + // Optional. Video duration. + Duration int `json:"mpeg4_duration,omitempty"` + + // URL of the static thumbnail (jpeg or gif) for the result. + ThumbURL string `json:"thumb_url,omitempty"` + + // Optional. MIME type of the thumbnail, must be one of + // “image/jpeg”, “image/gif”, or “video/mp4”. + ThumbMIME string `json:"thumb_mime_type,omitempty"` + + // Optional. Title for the result. + Title string `json:"title,omitempty"` + + // Optional. Caption of the MPEG-4 file to be sent, 0-200 characters. + Caption string `json:"caption,omitempty"` + + // If Cache != "", it'll be used instead + Cache string `json:"mpeg4_file_id,omitempty"` +} + +// PhotoResult represents a link to a photo. +type PhotoResult struct { + ResultBase + + // A valid URL of the photo. Photo must be in jpeg format. + // Photo size must not exceed 5MB. + URL string `json:"photo_url"` + + // Optional. Width of the photo. + Width int `json:"photo_width,omitempty"` + + // Optional. Height of the photo. + Height int `json:"photo_height,omitempty"` + + // Optional. Title for the result. + Title string `json:"title,omitempty"` + + // Optional. Short description of the result. + Description string `json:"description,omitempty"` + + // Optional. Caption of the photo to be sent, 0-200 characters. + Caption string `json:"caption,omitempty"` + + // URL of the thumbnail for the photo. + ThumbURL string `json:"thumb_url"` + + // If Cache != "", it'll be used instead + Cache string `json:"photo_file_id,omitempty"` +} + +// VenueResult represents a venue. +type VenueResult struct { + ResultBase + + Location + + // Title of the venue. + Title string `json:"title"` + + // Address of the venue. + Address string `json:"address"` + + // Optional. Foursquare identifier of the venue if known. + FoursquareID string `json:"foursquare_id,omitempty"` + + // Optional. URL of the thumbnail for the result. + ThumbURL string `json:"thumb_url,omitempty"` + + // Optional. Width of the thumbnail for the result. + ThumbWidth int `json:"thumb_width,omitempty"` + + // Optional. Height of the thumbnail for the result. + ThumbHeight int `json:"thumb_height,omitempty"` +} + +// VideoResult represents a link to a page containing an embedded +// video player or a video file. +type VideoResult struct { + ResultBase + + // A valid URL for the embedded video player or video file. + URL string `json:"video_url"` + + // Mime type of the content of video url, “text/html” or “video/mp4”. + MIME string `json:"mime_type"` + + // URL of the thumbnail (jpeg only) for the video. + ThumbURL string `json:"thumb_url"` + + // Title for the result. + Title string `json:"title"` + + // Optional. Caption of the video to be sent, 0-200 characters. + Caption string `json:"caption,omitempty"` + + // Optional. Video width. + Width int `json:"video_width,omitempty"` + + // Optional. Video height. + Height int `json:"video_height,omitempty"` + + // Optional. Video duration in seconds. + Duration int `json:"video_duration,omitempty"` + + // Optional. Short description of the result. + Description string `json:"description,omitempty"` + + // If Cache != "", it'll be used instead + Cache string `json:"video_file_id,omitempty"` +} + +// VoiceResult represents a link to a voice recording in an .ogg +// container encoded with OPUS. +type VoiceResult struct { + ResultBase + + // A valid URL for the voice recording. + URL string `json:"voice_url"` + + // Recording title. + Title string `json:"title"` + + // Optional. Recording duration in seconds. + Duration int `json:"voice_duration"` + + // Optional. Caption, 0-1024 characters. + Caption string `json:"caption,omitempty"` + + // If Cache != "", it'll be used instead + Cache string `json:"voice_file_id,omitempty"` +} + +// StickerResult represents an inline cached sticker response. +type StickerResult struct { + ResultBase + + // If Cache != "", it'll be used instead + Cache string `json:"sticker_file_id,omitempty"` +} diff --git a/vendor/gopkg.in/telebot.v3/input_types.go b/vendor/gopkg.in/telebot.v3/input_types.go new file mode 100644 index 0000000000..8186c0727c --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/input_types.go @@ -0,0 +1,73 @@ +package telebot + +// InputMessageContent objects represent the content of a message to be sent +// as a result of an inline query. +type InputMessageContent interface { + IsInputMessageContent() bool +} + +// InputTextMessageContent represents the content of a text message to be +// sent as the result of an inline query. +type InputTextMessageContent struct { + // Text of the message to be sent, 1-4096 characters. + Text string `json:"message_text"` + + // Optional. Send Markdown or HTML, if you want Telegram apps to show + // bold, italic, fixed-width text or inline URLs in your bot's message. + ParseMode string `json:"parse_mode,omitempty"` + + // Optional. Disables link previews for links in the sent message. + DisablePreview bool `json:"disable_web_page_preview"` +} + +func (input *InputTextMessageContent) IsInputMessageContent() bool { + return true +} + +// InputLocationMessageContent represents the content of a location message +// to be sent as the result of an inline query. +type InputLocationMessageContent struct { + Lat float32 `json:"latitude"` + Lng float32 `json:"longitude"` +} + +func (input *InputLocationMessageContent) IsInputMessageContent() bool { + return true +} + +// InputVenueMessageContent represents the content of a venue message to +// be sent as the result of an inline query. +type InputVenueMessageContent struct { + Lat float32 `json:"latitude"` + Lng float32 `json:"longitude"` + + // Name of the venue. + Title string `json:"title"` + + // Address of the venue. + Address string `json:"address"` + + // Optional. Foursquare identifier of the venue, if known. + FoursquareID string `json:"foursquare_id,omitempty"` +} + +func (input *InputVenueMessageContent) IsInputMessageContent() bool { + return true +} + +// InputContactMessageContent represents the content of a contact +// message to be sent as the result of an inline query. +type InputContactMessageContent struct { + // Contact's phone number. + PhoneNumber string `json:"phone_number"` + + // Contact's first name. + FirstName string `json:"first_name"` + + // Optional. Contact's last name. + LastName string `json:"last_name,omitempty"` +} + +func (input *InputContactMessageContent) IsInputMessageContent() bool { + return true +} diff --git a/vendor/gopkg.in/telebot.v3/media.go b/vendor/gopkg.in/telebot.v3/media.go new file mode 100644 index 0000000000..93ec1b4755 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/media.go @@ -0,0 +1,342 @@ +package telebot + +import ( + "encoding/json" +) + +// Media is a generic type for all kinds of media that includes File. +type Media interface { + // MediaType returns string-represented media type. + MediaType() string + + // MediaFile returns a pointer to the media file. + MediaFile() *File +} + +// InputMedia represents a composite InputMedia struct that is +// used by Telebot in sending and editing media methods. +type InputMedia struct { + Type string `json:"type"` + Media string `json:"media"` + Caption string `json:"caption"` + Thumbnail string `json:"thumb,omitempty"` + ParseMode string `json:"parse_mode,omitempty"` + Entities Entities `json:"caption_entities,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + Duration int `json:"duration,omitempty"` + Title string `json:"title,omitempty"` + Performer string `json:"performer,omitempty"` + Streaming bool `json:"supports_streaming,omitempty"` + DisableTypeDetection bool `json:"disable_content_type_detection,omitempty"` +} + +// Inputtable is a generic type for all kinds of media you +// can put into an album. +type Inputtable interface { + Media + + // InputMedia returns already marshalled InputMedia type + // ready to be used in sending and editing media methods. + InputMedia() InputMedia +} + +// Album lets you group multiple media into a single message. +type Album []Inputtable + +// Photo object represents a single photo file. +type Photo struct { + File + + Width int `json:"width"` + Height int `json:"height"` + Caption string `json:"caption,omitempty"` +} + +type photoSize struct { + File + + Width int `json:"width"` + Height int `json:"height"` + Caption string `json:"caption,omitempty"` +} + +func (p *Photo) MediaType() string { + return "photo" +} + +func (p *Photo) MediaFile() *File { + return &p.File +} + +func (p *Photo) InputMedia() InputMedia { + return InputMedia{ + Type: p.MediaType(), + Caption: p.Caption, + } +} + +// UnmarshalJSON is custom unmarshaller required to abstract +// away the hassle of treating different thumbnail sizes. +// Instead, Telebot chooses the hi-res one and just sticks to it. +// +// I really do find it a beautiful solution. +func (p *Photo) UnmarshalJSON(data []byte) error { + var hq photoSize + + if data[0] == '{' { + if err := json.Unmarshal(data, &hq); err != nil { + return err + } + } else { + var sizes []photoSize + if err := json.Unmarshal(data, &sizes); err != nil { + return err + } + + hq = sizes[len(sizes)-1] + } + + p.File = hq.File + p.Width = hq.Width + p.Height = hq.Height + + return nil +} + +// Audio object represents an audio file. +type Audio struct { + File + + Duration int `json:"duration,omitempty"` + + // (Optional) + Caption string `json:"caption,omitempty"` + Thumbnail *Photo `json:"thumb,omitempty"` + Title string `json:"title,omitempty"` + Performer string `json:"performer,omitempty"` + MIME string `json:"mime_type,omitempty"` + FileName string `json:"file_name,omitempty"` +} + +func (a *Audio) MediaType() string { + return "audio" +} + +func (a *Audio) MediaFile() *File { + a.fileName = a.FileName + return &a.File +} + +func (a *Audio) InputMedia() InputMedia { + return InputMedia{ + Type: a.MediaType(), + Caption: a.Caption, + Duration: a.Duration, + Title: a.Title, + Performer: a.Performer, + } +} + +// Document object represents a general file (as opposed to Photo or Audio). +// Telegram users can send files of any type of up to 1.5 GB in size. +type Document struct { + File + + // (Optional) + Thumbnail *Photo `json:"thumb,omitempty"` + Caption string `json:"caption,omitempty"` + MIME string `json:"mime_type"` + FileName string `json:"file_name,omitempty"` + DisableTypeDetection bool `json:"disable_content_type_detection,omitempty"` +} + +func (d *Document) MediaType() string { + return "document" +} + +func (d *Document) MediaFile() *File { + d.fileName = d.FileName + return &d.File +} + +func (d *Document) InputMedia() InputMedia { + return InputMedia{ + Type: d.MediaType(), + Caption: d.Caption, + DisableTypeDetection: d.DisableTypeDetection, + } +} + +// Video object represents a video file. +type Video struct { + File + + Width int `json:"width"` + Height int `json:"height"` + Duration int `json:"duration,omitempty"` + + // (Optional) + Caption string `json:"caption,omitempty"` + Thumbnail *Photo `json:"thumb,omitempty"` + Streaming bool `json:"supports_streaming,omitempty"` + MIME string `json:"mime_type,omitempty"` + FileName string `json:"file_name,omitempty"` +} + +func (v *Video) MediaType() string { + return "video" +} + +func (v *Video) MediaFile() *File { + v.fileName = v.FileName + return &v.File +} + +func (v *Video) InputMedia() InputMedia { + return InputMedia{ + Type: v.MediaType(), + Caption: v.Caption, + Width: v.Width, + Height: v.Height, + Duration: v.Duration, + Streaming: v.Streaming, + } +} + +// Animation object represents a animation file. +type Animation struct { + File + + Width int `json:"width"` + Height int `json:"height"` + Duration int `json:"duration,omitempty"` + + // (Optional) + Caption string `json:"caption,omitempty"` + Thumbnail *Photo `json:"thumb,omitempty"` + MIME string `json:"mime_type,omitempty"` + FileName string `json:"file_name,omitempty"` +} + +func (a *Animation) MediaType() string { + return "animation" +} + +func (a *Animation) MediaFile() *File { + a.fileName = a.FileName + return &a.File +} + +func (a *Animation) InputMedia() InputMedia { + return InputMedia{ + Type: a.MediaType(), + Caption: a.Caption, + Width: a.Width, + Height: a.Height, + Duration: a.Duration, + } +} + +// Voice object represents a voice note. +type Voice struct { + File + + Duration int `json:"duration"` + + // (Optional) + Caption string `json:"caption,omitempty"` + MIME string `json:"mime_type,omitempty"` +} + +func (v *Voice) MediaType() string { + return "voice" +} + +func (v *Voice) MediaFile() *File { + return &v.File +} + +// VideoNote represents a video message. +type VideoNote struct { + File + + Duration int `json:"duration"` + + // (Optional) + Thumbnail *Photo `json:"thumb,omitempty"` + Length int `json:"length,omitempty"` +} + +func (v *VideoNote) MediaType() string { + return "videoNote" +} + +func (v *VideoNote) MediaFile() *File { + return &v.File +} + +// Sticker object represents a WebP image, so-called sticker. +type Sticker struct { + File + Width int `json:"width"` + Height int `json:"height"` + Animated bool `json:"is_animated"` + Video bool `json:"is_video"` + Thumbnail *Photo `json:"thumb"` + Emoji string `json:"emoji"` + SetName string `json:"set_name"` + MaskPosition *MaskPosition `json:"mask_position"` +} + +func (s *Sticker) MediaType() string { + return "sticker" +} + +func (s *Sticker) MediaFile() *File { + return &s.File +} + +// Contact object represents a contact to Telegram user. +type Contact struct { + PhoneNumber string `json:"phone_number"` + FirstName string `json:"first_name"` + + // (Optional) + LastName string `json:"last_name"` + UserID int64 `json:"user_id,omitempty"` +} + +// Location object represents geographic position. +type Location struct { + Lat float32 `json:"latitude"` + Lng float32 `json:"longitude"` + HorizontalAccuracy *float32 `json:"horizontal_accuracy,omitempty"` + Heading int `json:"heading,omitempty"` + AlertRadius int `json:"proximity_alert_radius,omitempty"` + + // Period in seconds for which the location will be updated + // (see Live Locations, should be between 60 and 86400.) + LivePeriod int `json:"live_period,omitempty"` +} + +// Venue object represents a venue location with name, address and +// optional foursquare ID. +type Venue struct { + Location Location `json:"location"` + Title string `json:"title"` + Address string `json:"address"` + + // (Optional) + FoursquareID string `json:"foursquare_id,omitempty"` + FoursquareType string `json:"foursquare_type,omitempty"` + GooglePlaceID string `json:"google_place_id,omitempty"` + GooglePlaceType string `json:"google_place_type,omitempty"` +} + +// Dice object represents a dice with a random value +// from 1 to 6 for currently supported base emoji. +type Dice struct { + Type DiceType `json:"emoji"` + Value int `json:"value"` +} diff --git a/vendor/gopkg.in/telebot.v3/message.go b/vendor/gopkg.in/telebot.v3/message.go new file mode 100644 index 0000000000..3d4511c869 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/message.go @@ -0,0 +1,398 @@ +package telebot + +import ( + "strconv" + "time" + "unicode/utf16" +) + +// Message object represents a message. +type Message struct { + ID int `json:"message_id"` + + // For message sent to channels, Sender will be nil + Sender *User `json:"from"` + + // Unixtime, use Message.Time() to get time.Time + Unixtime int64 `json:"date"` + + // Conversation the message belongs to. + Chat *Chat `json:"chat"` + + // Sender of the message, sent on behalf of a chat. + SenderChat *Chat `json:"sender_chat"` + + // For forwarded messages, sender of the original message. + OriginalSender *User `json:"forward_from"` + + // For forwarded messages, chat of the original message when + // forwarded from a channel. + OriginalChat *Chat `json:"forward_from_chat"` + + // For forwarded messages, identifier of the original message + // when forwarded from a channel. + OriginalMessageID int `json:"forward_from_message_id"` + + // For forwarded messages, signature of the post author. + OriginalSignature string `json:"forward_signature"` + + // For forwarded messages, sender's name from users who + // disallow adding a link to their account. + OriginalSenderName string `json:"forward_sender_name"` + + // For forwarded messages, unixtime of the original message. + OriginalUnixtime int `json:"forward_date"` + + // Message is a channel post that was automatically forwarded to the connected discussion group. + AutomaticForward bool `json:"is_automatic_forward"` + + // For replies, ReplyTo represents the original message. + // + // Note that the Message object in this field will not + // contain further ReplyTo fields even if it + // itself is a reply. + ReplyTo *Message `json:"reply_to_message"` + + // Shows through which bot the message was sent. + Via *User `json:"via_bot"` + + // (Optional) Time of last edit in Unix. + LastEdit int64 `json:"edit_date"` + + // (Optional) Message can't be forwarded. + Protected bool `json:"has_protected_content,omitempty"` + + // AlbumID is the unique identifier of a media message group + // this message belongs to. + AlbumID string `json:"media_group_id"` + + // Author signature (in channels). + Signature string `json:"author_signature"` + + // For a text message, the actual UTF-8 text of the message. + Text string `json:"text"` + + // For registered commands, will contain the string payload: + // + // Ex: `/command ` or `/command@botname ` + Payload string `json:"-"` + + // For text messages, special entities like usernames, URLs, bot commands, + // etc. that appear in the text. + Entities Entities `json:"entities,omitempty"` + + // Some messages containing media, may as well have a caption. + Caption string `json:"caption,omitempty"` + + // For messages with a caption, special entities like usernames, URLs, + // bot commands, etc. that appear in the caption. + CaptionEntities Entities `json:"caption_entities,omitempty"` + + // For an audio recording, information about it. + Audio *Audio `json:"audio"` + + // For a general file, information about it. + Document *Document `json:"document"` + + // For a photo, all available sizes (thumbnails). + Photo *Photo `json:"photo"` + + // For a sticker, information about it. + Sticker *Sticker `json:"sticker"` + + // For a voice message, information about it. + Voice *Voice `json:"voice"` + + // For a video note, information about it. + VideoNote *VideoNote `json:"video_note"` + + // For a video, information about it. + Video *Video `json:"video"` + + // For a animation, information about it. + Animation *Animation `json:"animation"` + + // For a contact, contact information itself. + Contact *Contact `json:"contact"` + + // For a location, its longitude and latitude. + Location *Location `json:"location"` + + // For a venue, information about it. + Venue *Venue `json:"venue"` + + // For a poll, information the native poll. + Poll *Poll `json:"poll"` + + // For a game, information about it. + Game *Game `json:"game"` + + // For a dice, information about it. + Dice *Dice `json:"dice"` + + // For a service message, represents a user, + // that just got added to chat, this message came from. + // + // Sender leads to User, capable of invite. + // + // UserJoined might be the Bot itself. + UserJoined *User `json:"new_chat_member"` + + // For a service message, represents a user, + // that just left chat, this message came from. + // + // If user was kicked, Sender leads to a User, + // capable of this kick. + // + // UserLeft might be the Bot itself. + UserLeft *User `json:"left_chat_member"` + + // For a service message, represents a new title + // for chat this message came from. + // + // Sender would lead to a User, capable of change. + NewGroupTitle string `json:"new_chat_title"` + + // For a service message, represents all available + // thumbnails of the new chat photo. + // + // Sender would lead to a User, capable of change. + NewGroupPhoto *Photo `json:"new_chat_photo"` + + // For a service message, new members that were added to + // the group or supergroup and information about them + // (the bot itself may be one of these members). + UsersJoined []User `json:"new_chat_members"` + + // For a service message, true if chat photo just + // got removed. + // + // Sender would lead to a User, capable of change. + GroupPhotoDeleted bool `json:"delete_chat_photo"` + + // For a service message, true if group has been created. + // + // You would receive such a message if you are one of + // initial group chat members. + // + // Sender would lead to creator of the chat. + GroupCreated bool `json:"group_chat_created"` + + // For a service message, true if supergroup has been created. + // + // You would receive such a message if you are one of + // initial group chat members. + // + // Sender would lead to creator of the chat. + SuperGroupCreated bool `json:"supergroup_chat_created"` + + // For a service message, true if channel has been created. + // + // You would receive such a message if you are one of + // initial channel administrators. + // + // Sender would lead to creator of the chat. + ChannelCreated bool `json:"channel_chat_created"` + + // For a service message, the destination (supergroup) you + // migrated to. + // + // You would receive such a message when your chat has migrated + // to a supergroup. + // + // Sender would lead to creator of the migration. + MigrateTo int64 `json:"migrate_to_chat_id"` + + // For a service message, the Origin (normal group) you migrated + // from. + // + // You would receive such a message when your chat has migrated + // to a supergroup. + // + // Sender would lead to creator of the migration. + MigrateFrom int64 `json:"migrate_from_chat_id"` + + // Specified message was pinned. Note that the Message object + // in this field will not contain further ReplyTo fields even + // if it is itself a reply. + PinnedMessage *Message `json:"pinned_message"` + + // Message is an invoice for a payment. + Invoice *Invoice `json:"invoice"` + + // Message is a service message about a successful payment. + Payment *Payment `json:"successful_payment"` + + // The domain name of the website on which the user has logged in. + ConnectedWebsite string `json:"connected_website,omitempty"` + + // For a service message, a voice chat started in the chat. + VoiceChatStarted *VoiceChatStarted `json:"voice_chat_started,omitempty"` + + // For a service message, a voice chat ended in the chat. + VoiceChatEnded *VoiceChatEnded `json:"voice_chat_ended,omitempty"` + + // For a service message, some users were invited in the voice chat. + VoiceChatParticipants *VoiceChatParticipants `json:"voice_chat_participants_invited,omitempty"` + + // For a service message, a voice chat schedule in the chat. + VoiceChatScheduled *VoiceChatScheduled `json:"voice_chat_scheduled,omitempty"` + + // For a service message, represents the content of a service message, + // sent whenever a user in the chat triggers a proximity alert set by another user. + ProximityAlert *ProximityAlert `json:"proximity_alert_triggered,omitempty"` + + // For a service message, represents about a change in auto-delete timer settings. + AutoDeleteTimer *AutoDeleteTimer `json:"message_auto_delete_timer_changed,omitempty"` + + // Inline keyboard attached to the message. + ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"` +} + +// MessageEntity object represents "special" parts of text messages, +// including hashtags, usernames, URLs, etc. +type MessageEntity struct { + // Specifies entity type. + Type EntityType `json:"type"` + + // Offset in UTF-16 code units to the start of the entity. + Offset int `json:"offset"` + + // Length of the entity in UTF-16 code units. + Length int `json:"length"` + + // (Optional) For EntityTextLink entity type only. + // + // URL will be opened after user taps on the text. + URL string `json:"url,omitempty"` + + // (Optional) For EntityTMention entity type only. + User *User `json:"user,omitempty"` + + // (Optional) For EntityCodeBlock entity type only. + Language string `json:"language,omitempty"` +} + +// Entities is used to set message's text entities as a send option. +type Entities []MessageEntity + +// ProximityAlert sent whenever a user in the chat triggers +// a proximity alert set by another user. +type ProximityAlert struct { + Traveler *User `json:"traveler,omitempty"` + Watcher *User `json:"watcher,omitempty"` + Distance int `json:"distance"` +} + +// AutoDeleteTimer represents a service message about a change in auto-delete timer settings. +type AutoDeleteTimer struct { + Unixtime int `json:"message_auto_delete_time"` +} + +// MessageSig satisfies Editable interface (see Editable.) +func (m *Message) MessageSig() (string, int64) { + return strconv.Itoa(m.ID), m.Chat.ID +} + +// Time returns the moment of message creation in local time. +func (m *Message) Time() time.Time { + return time.Unix(m.Unixtime, 0) +} + +// LastEdited returns time.Time of last edit. +func (m *Message) LastEdited() time.Time { + return time.Unix(m.LastEdit, 0) +} + +// IsForwarded says whether message is forwarded copy of another +// message or not. +func (m *Message) IsForwarded() bool { + return m.OriginalSender != nil || m.OriginalChat != nil +} + +// IsReply says whether message is a reply to another message. +func (m *Message) IsReply() bool { + return m.ReplyTo != nil +} + +// Private returns true, if it's a personal message. +func (m *Message) Private() bool { + return m.Chat.Type == ChatPrivate +} + +// FromGroup returns true, if message came from a group OR a supergroup. +func (m *Message) FromGroup() bool { + return m.Chat.Type == ChatGroup || m.Chat.Type == ChatSuperGroup +} + +// FromChannel returns true, if message came from a channel. +func (m *Message) FromChannel() bool { + return m.Chat.Type == ChatChannel +} + +// IsService returns true, if message is a service message, +// returns false otherwise. +// +// Service messages are automatically sent messages, which +// typically occur on some global action. For instance, when +// anyone leaves the chat or chat title changes. +// +func (m *Message) IsService() bool { + fact := false + + fact = fact || m.UserJoined != nil + fact = fact || len(m.UsersJoined) > 0 + fact = fact || m.UserLeft != nil + fact = fact || m.NewGroupTitle != "" + fact = fact || m.NewGroupPhoto != nil + fact = fact || m.GroupPhotoDeleted + fact = fact || m.GroupCreated || m.SuperGroupCreated + fact = fact || (m.MigrateTo != m.MigrateFrom) + + return fact +} + +// EntityText returns the substring of the message identified by the +// given MessageEntity. +// +// It's safer than manually slicing Text because Telegram uses +// UTF-16 indices whereas Go string are []byte. +// +func (m *Message) EntityText(e MessageEntity) string { + text := m.Text + if text == "" { + text = m.Caption + } + + a := utf16.Encode([]rune(text)) + off, end := e.Offset, e.Offset+e.Length + + if off < 0 || end > len(a) { + return "" + } + + return string(utf16.Decode(a[off:end])) +} + +// Media returns the message's media if it contains either photo, +// voice, audio, animation, document, video or video note. +func (m *Message) Media() Media { + switch { + case m.Photo != nil: + return m.Photo + case m.Voice != nil: + return m.Voice + case m.Audio != nil: + return m.Audio + case m.Animation != nil: + return m.Animation + case m.Document != nil: + return m.Document + case m.Video != nil: + return m.Video + case m.VideoNote != nil: + return m.VideoNote + default: + return nil + } +} diff --git a/vendor/gopkg.in/telebot.v3/middleware.go b/vendor/gopkg.in/telebot.v3/middleware.go new file mode 100644 index 0000000000..fec640f35e --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/middleware.go @@ -0,0 +1,22 @@ +package telebot + +// MiddlewareFunc represents a middleware processing function, +// which get called before the endpoint group or specific handler. +type MiddlewareFunc func(HandlerFunc) HandlerFunc + +// Group is a separated group of handlers, united by the general middleware. +type Group struct { + b *Bot + middleware []MiddlewareFunc +} + +// Use adds middleware to the chain. +func (g *Group) Use(middleware ...MiddlewareFunc) { + g.middleware = append(g.middleware, middleware...) +} + +// Handle adds endpoint handler to the bot, combining group's middleware +// with the optional given middleware. +func (g *Group) Handle(endpoint interface{}, h HandlerFunc, m ...MiddlewareFunc) { + g.b.Handle(endpoint, h, append(g.middleware, m...)...) +} diff --git a/vendor/gopkg.in/telebot.v3/options.go b/vendor/gopkg.in/telebot.v3/options.go new file mode 100644 index 0000000000..1f5a0c6cd4 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/options.go @@ -0,0 +1,352 @@ +package telebot + +import ( + "encoding/json" + "fmt" + "strings" +) + +// Option is a shortcut flag type for certain message features +// (so-called options). It means that instead of passing +// fully-fledged SendOptions* to Send(), you can use these +// flags instead. +// +// Supported options are defined as iota-constants. +// +type Option int + +const ( + // NoPreview = SendOptions.DisableWebPagePreview + NoPreview Option = iota + + // Silent = SendOptions.DisableNotification + Silent + + // AllowWithoutReply = SendOptions.AllowWithoutReply + AllowWithoutReply + + // Protected = SendOptions.Protected + Protected + + // ForceReply = ReplyMarkup.ForceReply + ForceReply + + // OneTimeKeyboard = ReplyMarkup.OneTimeKeyboard + OneTimeKeyboard + + // RemoveKeyboard = ReplyMarkup.RemoveKeyboard + RemoveKeyboard +) + +// Placeholder is used to set input field placeholder as a send option. +func Placeholder(text string) *SendOptions { + return &SendOptions{ + ReplyMarkup: &ReplyMarkup{ + ForceReply: true, + Placeholder: text, + }, + } +} + +// SendOptions has most complete control over in what way the message +// must be sent, providing an API-complete set of custom properties +// and options. +// +// Despite its power, SendOptions is rather inconvenient to use all +// the way through bot logic, so you might want to consider storing +// and re-using it somewhere or be using Option flags instead. +// +type SendOptions struct { + // If the message is a reply, original message. + ReplyTo *Message + + // See ReplyMarkup struct definition. + ReplyMarkup *ReplyMarkup + + // For text messages, disables previews for links in this message. + DisableWebPagePreview bool + + // Sends the message silently. iOS users will not receive a notification, Android users will receive a notification with no sound. + DisableNotification bool + + // ParseMode controls how client apps render your message. + ParseMode ParseMode + + // Entities is a list of special entities that appear in message text, which can be specified instead of parse_mode. + Entities Entities + + // AllowWithoutReply allows sending messages not a as reply if the replied-to message has already been deleted. + AllowWithoutReply bool + + // Protected protects the contents of the sent message from forwarding and saving + Protected bool +} + +func (og *SendOptions) copy() *SendOptions { + cp := *og + if cp.ReplyMarkup != nil { + cp.ReplyMarkup = cp.ReplyMarkup.copy() + } + return &cp +} + +// ReplyMarkup controls two convenient options for bot-user communications +// such as reply keyboard and inline "keyboard" (a grid of buttons as a part +// of the message). +type ReplyMarkup struct { + // InlineKeyboard is a grid of InlineButtons displayed in the message. + // + // Note: DO NOT confuse with ReplyKeyboard and other keyboard properties! + InlineKeyboard [][]InlineButton `json:"inline_keyboard,omitempty"` + + // ReplyKeyboard is a grid, consisting of keyboard buttons. + // + // Note: you don't need to set HideCustomKeyboard field to show custom keyboard. + ReplyKeyboard [][]ReplyButton `json:"keyboard,omitempty"` + + // ForceReply forces Telegram clients to display + // a reply interface to the user (act as if the user + // has selected the bot‘s message and tapped "Reply"). + ForceReply bool `json:"force_reply,omitempty"` + + // Requests clients to resize the keyboard vertically for optimal fit + // (e.g. make the keyboard smaller if there are just two rows of buttons). + // + // Defaults to false, in which case the custom keyboard is always of the + // same height as the app's standard keyboard. + ResizeKeyboard bool `json:"resize_keyboard,omitempty"` + + // Requests clients to hide the reply keyboard as soon as it's been used. + // + // Defaults to false. + OneTimeKeyboard bool `json:"one_time_keyboard,omitempty"` + + // Requests clients to remove the reply keyboard. + // + // Defaults to false. + RemoveKeyboard bool `json:"remove_keyboard,omitempty"` + + // Use this param if you want to force reply from + // specific users only. + // + // Targets: + // 1) Users that are @mentioned in the text of the Message object; + // 2) If the bot's message is a reply (has SendOptions.ReplyTo), + // sender of the original message. + Selective bool `json:"selective,omitempty"` + + // Placeholder will be shown in the input field when the reply is active. + Placeholder string `json:"input_field_placeholder,omitempty"` +} + +func (r *ReplyMarkup) copy() *ReplyMarkup { + cp := *r + + if len(r.ReplyKeyboard) > 0 { + cp.ReplyKeyboard = make([][]ReplyButton, len(r.ReplyKeyboard)) + for i, row := range r.ReplyKeyboard { + cp.ReplyKeyboard[i] = make([]ReplyButton, len(row)) + copy(cp.ReplyKeyboard[i], row) + } + } + + if len(r.InlineKeyboard) > 0 { + cp.InlineKeyboard = make([][]InlineButton, len(r.InlineKeyboard)) + for i, row := range r.InlineKeyboard { + cp.InlineKeyboard[i] = make([]InlineButton, len(row)) + copy(cp.InlineKeyboard[i], row) + } + } + + return &cp +} + +// ReplyButton represents a button displayed in reply-keyboard. +// +// Set either Contact or Location to true in order to request +// sensitive info, such as user's phone number or current location. +// +type ReplyButton struct { + Text string `json:"text"` + + Contact bool `json:"request_contact,omitempty"` + Location bool `json:"request_location,omitempty"` + Poll PollType `json:"request_poll,omitempty"` +} + +// MarshalJSON implements json.Marshaler. It allows to pass +// PollType as keyboard's poll type instead of KeyboardButtonPollType object. +func (pt PollType) MarshalJSON() ([]byte, error) { + var aux = struct { + Type string `json:"type"` + }{ + Type: string(pt), + } + return json.Marshal(&aux) +} + +// Row represents an array of buttons, a row. +type Row []Btn + +// Row creates a row of buttons. +func (r *ReplyMarkup) Row(many ...Btn) Row { + return many +} + +// Split splits the keyboard into the rows with N maximum number of buttons. +// For example, if you pass six buttons and 3 as the max, you get two rows with +// three buttons in each. +// +// `Split(3, []Btn{six buttons...}) -> [[1, 2, 3], [4, 5, 6]]` +// `Split(2, []Btn{six buttons...}) -> [[1, 2],[3, 4],[5, 6]]` +// +func (r *ReplyMarkup) Split(max int, btns []Btn) []Row { + rows := make([]Row, (max-1+len(btns))/max) + for i, b := range btns { + i /= max + rows[i] = append(rows[i], b) + } + return rows +} + +func (r *ReplyMarkup) Inline(rows ...Row) { + inlineKeys := make([][]InlineButton, 0, len(rows)) + for i, row := range rows { + keys := make([]InlineButton, 0, len(row)) + for j, btn := range row { + btn := btn.Inline() + if btn == nil { + panic(fmt.Sprintf( + "telebot: button row %d column %d is not an inline button", + i, j)) + } + keys = append(keys, *btn) + } + inlineKeys = append(inlineKeys, keys) + } + + r.InlineKeyboard = inlineKeys +} + +func (r *ReplyMarkup) Reply(rows ...Row) { + replyKeys := make([][]ReplyButton, 0, len(rows)) + for i, row := range rows { + keys := make([]ReplyButton, 0, len(row)) + for j, btn := range row { + btn := btn.Reply() + if btn == nil { + panic(fmt.Sprintf( + "telebot: button row %d column %d is not a reply button", + i, j)) + } + keys = append(keys, *btn) + } + replyKeys = append(replyKeys, keys) + } + + r.ReplyKeyboard = replyKeys +} + +func (r *ReplyMarkup) Text(text string) Btn { + return Btn{Text: text} +} + +func (r *ReplyMarkup) Contact(text string) Btn { + return Btn{Contact: true, Text: text} +} + +func (r *ReplyMarkup) Location(text string) Btn { + return Btn{Location: true, Text: text} +} + +func (r *ReplyMarkup) Poll(text string, poll PollType) Btn { + return Btn{Poll: poll, Text: text} +} + +func (r *ReplyMarkup) Data(text, unique string, data ...string) Btn { + return Btn{ + Unique: unique, + Text: text, + Data: strings.Join(data, "|"), + } +} + +func (r *ReplyMarkup) URL(text, url string) Btn { + return Btn{Text: text, URL: url} +} + +func (r *ReplyMarkup) Query(text, query string) Btn { + return Btn{Text: text, InlineQuery: query} +} + +func (r *ReplyMarkup) QueryChat(text, query string) Btn { + return Btn{Text: text, InlineQueryChat: query} +} + +func (r *ReplyMarkup) Login(text string, login *Login) Btn { + return Btn{Login: login, Text: text} +} + +// Btn is a constructor button, which will later become either a reply, or an inline button. +type Btn struct { + Unique string `json:"unique,omitempty"` + Text string `json:"text,omitempty"` + URL string `json:"url,omitempty"` + Data string `json:"callback_data,omitempty"` + InlineQuery string `json:"switch_inline_query,omitempty"` + InlineQueryChat string `json:"switch_inline_query_current_chat,omitempty"` + Contact bool `json:"request_contact,omitempty"` + Location bool `json:"request_location,omitempty"` + Poll PollType `json:"request_poll,omitempty"` + Login *Login `json:"login_url,omitempty"` +} + +func (b Btn) Inline() *InlineButton { + return &InlineButton{ + Unique: b.Unique, + Text: b.Text, + URL: b.URL, + Data: b.Data, + InlineQuery: b.InlineQuery, + InlineQueryChat: b.InlineQueryChat, + Login: b.Login, + } +} + +func (b Btn) Reply() *ReplyButton { + if b.Unique != "" { + return nil + } + + return &ReplyButton{ + Text: b.Text, + Contact: b.Contact, + Location: b.Location, + Poll: b.Poll, + } +} + +// CommandParams controls parameters for commands-related methods (setMyCommands, deleteMyCommands and getMyCommands). +type CommandParams struct { + Commands []Command `json:"commands,omitempty"` + Scope *CommandScope `json:"scope,omitempty"` + LanguageCode string `json:"language_code,omitempty"` +} + +// CommandScope object represents a scope to which bot commands are applied. +type CommandScope struct { + Type string `json:"type"` + ChatID int64 `json:"chat_id,omitempty"` + UserID int64 `json:"user_id,omitempty"` +} + +// CommandScope types +const ( + CommandScopeDefault = "default" + CommandScopeAllPrivateChats = "all_private_chats" + CommandScopeAllGroupChats = "all_group_chats" + CommandScopeAllChatAdmin = "all_chat_administrators" + CommandScopeChat = "chat" + CommandScopeChatAdmin = "chat_administrators" + CommandScopeChatMember = "chat_member" +) diff --git a/vendor/gopkg.in/telebot.v3/payments.go b/vendor/gopkg.in/telebot.v3/payments.go new file mode 100644 index 0000000000..e67fe81120 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/payments.go @@ -0,0 +1,132 @@ +package telebot + +import ( + "encoding/json" + "math" +) + +// ShippingQuery contains information about an incoming shipping query. +type ShippingQuery struct { + Sender *User `json:"from"` + ID string `json:"id"` + Payload string `json:"invoice_payload"` + Address ShippingAddress `json:"shipping_address"` +} + +// ShippingAddress represents a shipping address. +type ShippingAddress struct { + CountryCode string `json:"country_code"` + State string `json:"state"` + City string `json:"city"` + StreetLine1 string `json:"street_line1"` + StreetLine2 string `json:"street_line2"` + PostCode string `json:"post_code"` +} + +// ShippingOption represents one shipping option. +type ShippingOption struct { + ID string `json:"id"` + Title string `json:"title"` + Prices []Price `json:"prices"` +} + +// Payment contains basic information about a successful payment. +type Payment struct { + Currency string `json:"currency"` + Total int `json:"total_amount"` + Payload string `json:"invoice_payload"` + OptionID string `json:"shipping_option_id"` + Order Order `json:"order_info"` + TelegramChargeID string `json:"telegram_payment_charge_id"` + ProviderChargeID string `json:"provider_payment_charge_id"` +} + +// PreCheckoutQuery contains information about an incoming pre-checkout query. +type PreCheckoutQuery struct { + Sender *User `json:"from"` + ID string `json:"id"` + Currency string `json:"currency"` + Payload string `json:"invoice_payload"` + Total int `json:"total_amount"` + OptionID string `json:"shipping_option_id"` + Order Order `json:"order_info"` +} + +// Order represents information about an order. +type Order struct { + Name string `json:"name"` + PhoneNumber string `json:"phone_number"` + Email string `json:"email"` + Address ShippingAddress `json:"shipping_address"` +} + +// Invoice contains basic information about an invoice. +type Invoice struct { + Title string `json:"title"` + Description string `json:"description"` + Payload string `json:"payload"` + Currency string `json:"currency"` + Prices []Price `json:"prices"` + Token string `json:"provider_token"` + Data string `json:"provider_data"` + + Photo *Photo `json:"photo"` + PhotoSize int `json:"photo_size"` + + // Unique deep-linking parameter that can be used to + // generate this invoice when used as a start parameter (0). + Start string `json:"start_parameter"` + + // Shows the total price in the smallest units of the currency. + // For example, for a price of US$ 1.45 pass amount = 145. + Total int `json:"total_amount"` + + MaxTipAmount int `json:"max_tip_amount"` + SuggestedTipAmounts []int `json:"suggested_tip_amounts"` + + NeedName bool `json:"need_name"` + NeedPhoneNumber bool `json:"need_phone_number"` + NeedEmail bool `json:"need_email"` + NeedShippingAddress bool `json:"need_shipping_address"` + SendPhoneNumber bool `json:"send_phone_number_to_provider"` + SendEmail bool `json:"send_email_to_provider"` + Flexible bool `json:"is_flexible"` +} + +// Price represents a portion of the price for goods or services. +type Price struct { + Label string `json:"label"` + Amount int `json:"amount"` +} + +// Currency contains information about supported currency for payments. +type Currency struct { + Code string `json:"code"` + Title string `json:"title"` + Symbol string `json:"symbol"` + Native string `json:"native"` + ThousandsSep string `json:"thousands_sep"` + DecimalSep string `json:"decimal_sep"` + SymbolLeft bool `json:"symbol_left"` + SpaceBetween bool `json:"space_between"` + Exp int `json:"exp"` + MinAmount interface{} `json:"min_amount"` + MaxAmount interface{} `json:"max_amount"` +} + +func (c Currency) FromTotal(total int) float64 { + return float64(total) / math.Pow(10, float64(c.Exp)) +} + +func (c Currency) ToTotal(total float64) int { + return int(total) * int(math.Pow(10, float64(c.Exp))) +} + +var SupportedCurrencies = make(map[string]Currency) + +func init() { + err := json.Unmarshal([]byte(dataCurrencies), &SupportedCurrencies) + if err != nil { + panic(err) + } +} diff --git a/vendor/gopkg.in/telebot.v3/payments_data.go b/vendor/gopkg.in/telebot.v3/payments_data.go new file mode 100644 index 0000000000..99efa76222 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/payments_data.go @@ -0,0 +1,3 @@ +package telebot + +const dataCurrencies = `{"AED":{"code":"AED","title":"United Arab Emirates Dirham","symbol":"AED","native":"\u062f.\u0625.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"367","max_amount":"3673200"},"AFN":{"code":"AFN","title":"Afghan Afghani","symbol":"AFN","native":"\u060b","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"7554","max_amount":"75540495"},"ALL":{"code":"ALL","title":"Albanian Lek","symbol":"ALL","native":"Lek","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":false,"exp":2,"min_amount":"10908","max_amount":"109085036"},"AMD":{"code":"AMD","title":"Armenian Dram","symbol":"AMD","native":"\u0564\u0580.","thousands_sep":",","decimal_sep":".","symbol_left":false,"space_between":true,"exp":2,"min_amount":"48398","max_amount":"483984962"},"ARS":{"code":"ARS","title":"Argentine Peso","symbol":"ARS","native":"$","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":true,"exp":2,"min_amount":"3720","max_amount":"37202998"},"AUD":{"code":"AUD","title":"Australian Dollar","symbol":"AU$","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"139","max_amount":"1392750"},"AZN":{"code":"AZN","title":"Azerbaijani Manat","symbol":"AZN","native":"\u043c\u0430\u043d.","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"170","max_amount":"1702500"},"BAM":{"code":"BAM","title":"Bosnia & Herzegovina Convertible Mark","symbol":"BAM","native":"KM","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"171","max_amount":"1715550"},"BDT":{"code":"BDT","title":"Bangladeshi Taka","symbol":"BDT","native":"\u09f3","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"8336","max_amount":"83367500"},"BGN":{"code":"BGN","title":"Bulgarian Lev","symbol":"BGN","native":"\u043b\u0432.","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"171","max_amount":"1716850"},"BND":{"code":"BND","title":"Brunei Dollar","symbol":"BND","native":"$","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":false,"exp":2,"min_amount":"134","max_amount":"1349850"},"BOB":{"code":"BOB","title":"Bolivian Boliviano","symbol":"BOB","native":"Bs","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":true,"exp":2,"min_amount":"687","max_amount":"6877150"},"BRL":{"code":"BRL","title":"Brazilian Real","symbol":"R$","native":"R$","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":true,"exp":2,"min_amount":"377","max_amount":"3775397"},"CAD":{"code":"CAD","title":"Canadian Dollar","symbol":"CA$","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"132","max_amount":"1321950"},"CHF":{"code":"CHF","title":"Swiss Franc","symbol":"CHF","native":"CHF","thousands_sep":"'","decimal_sep":".","symbol_left":false,"space_between":true,"exp":2,"min_amount":"99","max_amount":"993220"},"CLP":{"code":"CLP","title":"Chilean Peso","symbol":"CLP","native":"$","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":true,"exp":0,"min_amount":"666","max_amount":"6665199"},"CNY":{"code":"CNY","title":"Chinese Renminbi Yuan","symbol":"CN\u00a5","native":"CN\u00a5","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"674","max_amount":"6747298"},"COP":{"code":"COP","title":"Colombian Peso","symbol":"COP","native":"$","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":true,"exp":2,"min_amount":"315595","max_amount":"3155950000"},"CRC":{"code":"CRC","title":"Costa Rican Col\u00f3n","symbol":"CRC","native":"\u20a1","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":false,"exp":2,"min_amount":"60113","max_amount":"601130282"},"CZK":{"code":"CZK","title":"Czech Koruna","symbol":"CZK","native":"K\u010d","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"2251","max_amount":"22510978"},"DKK":{"code":"DKK","title":"Danish Krone","symbol":"DKK","native":"kr","thousands_sep":"","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"654","max_amount":"6545403"},"DOP":{"code":"DOP","title":"Dominican Peso","symbol":"DOP","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"5032","max_amount":"50329504"},"DZD":{"code":"DZD","title":"Algerian Dinar","symbol":"DZD","native":"\u062f.\u062c.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"11872","max_amount":"118729869"},"EGP":{"code":"EGP","title":"Egyptian Pound","symbol":"EGP","native":"\u062c.\u0645.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"1791","max_amount":"17912012"},"EUR":{"code":"EUR","title":"Euro","symbol":"\u20ac","native":"\u20ac","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"87","max_amount":"877155"},"GBP":{"code":"GBP","title":"British Pound","symbol":"\u00a3","native":"\u00a3","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"75","max_amount":"757605"},"GEL":{"code":"GEL","title":"Georgian Lari","symbol":"GEL","native":"GEL","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"266","max_amount":"2663750"},"GTQ":{"code":"GTQ","title":"Guatemalan Quetzal","symbol":"GTQ","native":"Q","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"768","max_amount":"7689850"},"HKD":{"code":"HKD","title":"Hong Kong Dollar","symbol":"HK$","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"784","max_amount":"7845505"},"HNL":{"code":"HNL","title":"Honduran Lempira","symbol":"HNL","native":"L","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"2427","max_amount":"24277502"},"HRK":{"code":"HRK","title":"Croatian Kuna","symbol":"HRK","native":"kn","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"650","max_amount":"6506302"},"HUF":{"code":"HUF","title":"Hungarian Forint","symbol":"HUF","native":"Ft","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"27844","max_amount":"278440341"},"IDR":{"code":"IDR","title":"Indonesian Rupiah","symbol":"IDR","native":"Rp","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":false,"exp":2,"min_amount":"1406555","max_amount":"14065550000"},"ILS":{"code":"ILS","title":"Israeli New Sheqel","symbol":"\u20aa","native":"\u20aa","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"366","max_amount":"3668230"},"INR":{"code":"INR","title":"Indian Rupee","symbol":"\u20b9","native":"\u20b9","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"7090","max_amount":"70900503"},"ISK":{"code":"ISK","title":"Icelandic Kr\u00f3na","symbol":"ISK","native":"kr","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":0,"min_amount":"119","max_amount":"1195599"},"JMD":{"code":"JMD","title":"Jamaican Dollar","symbol":"JMD","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"13153","max_amount":"131539958"},"JPY":{"code":"JPY","title":"Japanese Yen","symbol":"\u00a5","native":"\uffe5","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":0,"min_amount":"109","max_amount":"1095549"},"KES":{"code":"KES","title":"Kenyan Shilling","symbol":"KES","native":"Ksh","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"10032","max_amount":"100322011"},"KGS":{"code":"KGS","title":"Kyrgyzstani Som","symbol":"KGS","native":"KGS","thousands_sep":"\u00a0","decimal_sep":"-","symbol_left":false,"space_between":true,"exp":2,"min_amount":"6982","max_amount":"69820300"},"KRW":{"code":"KRW","title":"South Korean Won","symbol":"\u20a9","native":"\u20a9","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":0,"min_amount":"1119","max_amount":"11190001"},"KZT":{"code":"KZT","title":"Kazakhstani Tenge","symbol":"KZT","native":"\u20b8","thousands_sep":"\u00a0","decimal_sep":"-","symbol_left":true,"space_between":false,"exp":2,"min_amount":"37767","max_amount":"377674954"},"LBP":{"code":"LBP","title":"Lebanese Pound","symbol":"LBP","native":"\u0644.\u0644.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"150080","max_amount":"1500802255"},"LKR":{"code":"LKR","title":"Sri Lankan Rupee","symbol":"LKR","native":"\u0dbb\u0dd4.","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"18078","max_amount":"180789638"},"MAD":{"code":"MAD","title":"Moroccan Dirham","symbol":"MAD","native":"\u062f.\u0645.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"955","max_amount":"9554850"},"MDL":{"code":"MDL","title":"Moldovan Leu","symbol":"MDL","native":"MDL","thousands_sep":",","decimal_sep":".","symbol_left":false,"space_between":true,"exp":2,"min_amount":"1703","max_amount":"17038967"},"MNT":{"code":"MNT","title":"Mongolian T\u00f6gr\u00f6g","symbol":"MNT","native":"MNT","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":true,"space_between":false,"exp":2,"min_amount":"261750","max_amount":"2617500000"},"MUR":{"code":"MUR","title":"Mauritian Rupee","symbol":"MUR","native":"MUR","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"3438","max_amount":"34384499"},"MVR":{"code":"MVR","title":"Maldivian Rufiyaa","symbol":"MVR","native":"MVR","thousands_sep":",","decimal_sep":".","symbol_left":false,"space_between":true,"exp":2,"min_amount":"1550","max_amount":"15501063"},"MXN":{"code":"MXN","title":"Mexican Peso","symbol":"MX$","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"1898","max_amount":"18988704"},"MYR":{"code":"MYR","title":"Malaysian Ringgit","symbol":"MYR","native":"RM","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"412","max_amount":"4124501"},"MZN":{"code":"MZN","title":"Mozambican Metical","symbol":"MZN","native":"MTn","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"6188","max_amount":"61889913"},"NGN":{"code":"NGN","title":"Nigerian Naira","symbol":"NGN","native":"\u20a6","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"36174","max_amount":"361749532"},"NIO":{"code":"NIO","title":"Nicaraguan C\u00f3rdoba","symbol":"NIO","native":"C$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"3241","max_amount":"32415503"},"NOK":{"code":"NOK","title":"Norwegian Krone","symbol":"NOK","native":"kr","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":true,"space_between":true,"exp":2,"min_amount":"851","max_amount":"8510100"},"NPR":{"code":"NPR","title":"Nepalese Rupee","symbol":"NPR","native":"\u0928\u0947\u0930\u0942","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"11299","max_amount":"112995016"},"NZD":{"code":"NZD","title":"New Zealand Dollar","symbol":"NZ$","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"146","max_amount":"1461850"},"PAB":{"code":"PAB","title":"Panamanian Balboa","symbol":"PAB","native":"B\/.","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"99","max_amount":"995290"},"PEN":{"code":"PEN","title":"Peruvian Nuevo Sol","symbol":"PEN","native":"S\/.","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"333","max_amount":"3331250"},"PHP":{"code":"PHP","title":"Philippine Peso","symbol":"PHP","native":"\u20b1","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"5260","max_amount":"52602981"},"PKR":{"code":"PKR","title":"Pakistani Rupee","symbol":"PKR","native":"\u20a8","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"13921","max_amount":"139214990"},"PLN":{"code":"PLN","title":"Polish Z\u0142oty","symbol":"PLN","native":"z\u0142","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"376","max_amount":"3764026"},"PYG":{"code":"PYG","title":"Paraguayan Guaran\u00ed","symbol":"PYG","native":"\u20b2","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":true,"exp":0,"min_amount":"6013","max_amount":"60134502"},"QAR":{"code":"QAR","title":"Qatari Riyal","symbol":"QAR","native":"\u0631.\u0642.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"364","max_amount":"3641101"},"RON":{"code":"RON","title":"Romanian Leu","symbol":"RON","native":"RON","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"417","max_amount":"4172003"},"RSD":{"code":"RSD","title":"Serbian Dinar","symbol":"RSD","native":"\u0434\u0438\u043d.","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"10391","max_amount":"103910127"},"RUB":{"code":"RUB","title":"Russian Ruble","symbol":"RUB","native":"\u0440\u0443\u0431.","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"6598","max_amount":"65986027"},"SAR":{"code":"SAR","title":"Saudi Riyal","symbol":"SAR","native":"\u0631.\u0633.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"373","max_amount":"3732650"},"SEK":{"code":"SEK","title":"Swedish Krona","symbol":"SEK","native":"kr","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"904","max_amount":"9047896"},"SGD":{"code":"SGD","title":"Singapore Dollar","symbol":"SGD","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"135","max_amount":"1353897"},"THB":{"code":"THB","title":"Thai Baht","symbol":"\u0e3f","native":"\u0e3f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"3156","max_amount":"31563499"},"TJS":{"code":"TJS","title":"Tajikistani Somoni","symbol":"TJS","native":"TJS","thousands_sep":"\u00a0","decimal_sep":";","symbol_left":false,"space_between":true,"exp":2,"min_amount":"938","max_amount":"9389950"},"TRY":{"code":"TRY","title":"Turkish Lira","symbol":"TRY","native":"TL","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"526","max_amount":"5267200"},"TTD":{"code":"TTD","title":"Trinidad and Tobago Dollar","symbol":"TTD","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"675","max_amount":"6757850"},"TWD":{"code":"TWD","title":"New Taiwan Dollar","symbol":"NT$","native":"NT$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"3072","max_amount":"30722993"},"TZS":{"code":"TZS","title":"Tanzanian Shilling","symbol":"TZS","native":"TSh","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"230200","max_amount":"2302000188"},"UAH":{"code":"UAH","title":"Ukrainian Hryvnia","symbol":"UAH","native":"\u20b4","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":false,"exp":2,"min_amount":"2764","max_amount":"27648991"},"UGX":{"code":"UGX","title":"Ugandan Shilling","symbol":"UGX","native":"USh","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":0,"min_amount":"3657","max_amount":"36575502"},"USD":{"code":"USD","title":"United States Dollar","symbol":"$","native":"$","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":false,"exp":2,"min_amount":"100","max_amount":1000000},"UYU":{"code":"UYU","title":"Uruguayan Peso","symbol":"UYU","native":"$","thousands_sep":".","decimal_sep":",","symbol_left":true,"space_between":true,"exp":2,"min_amount":"3246","max_amount":"32469503"},"UZS":{"code":"UZS","title":"Uzbekistani Som","symbol":"UZS","native":"UZS","thousands_sep":"\u00a0","decimal_sep":",","symbol_left":false,"space_between":true,"exp":2,"min_amount":"832759","max_amount":"8327599915"},"VND":{"code":"VND","title":"Vietnamese \u0110\u1ed3ng","symbol":"\u20ab","native":"\u20ab","thousands_sep":".","decimal_sep":",","symbol_left":false,"space_between":true,"exp":0,"min_amount":"23084","max_amount":"230840500"},"YER":{"code":"YER","title":"Yemeni Rial","symbol":"YER","native":"\u0631.\u064a.\u200f","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"25030","max_amount":"250301249"},"ZAR":{"code":"ZAR","title":"South African Rand","symbol":"ZAR","native":"R","thousands_sep":",","decimal_sep":".","symbol_left":true,"space_between":true,"exp":2,"min_amount":"1362","max_amount":"13620106"}}` diff --git a/vendor/gopkg.in/telebot.v3/poller.go b/vendor/gopkg.in/telebot.v3/poller.go new file mode 100644 index 0000000000..ec696ba15b --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/poller.go @@ -0,0 +1,117 @@ +package telebot + +import ( + "time" +) + +// Poller is a provider of Updates. +// +// All pollers must implement Poll(), which accepts bot +// pointer and subscription channel and start polling +// synchronously straight away. +// +type Poller interface { + // Poll is supposed to take the bot object + // subscription channel and start polling + // for Updates immediately. + // + // Poller must listen for stop constantly and close + // it as soon as it's done polling. + Poll(b *Bot, updates chan Update, stop chan struct{}) +} + +// MiddlewarePoller is a special kind of poller that acts +// like a filter for updates. It could be used for spam +// handling, banning or whatever. +// +// For heavy middleware, use increased capacity. +// +type MiddlewarePoller struct { + Capacity int // Default: 1 + Poller Poller + Filter func(*Update) bool +} + +// NewMiddlewarePoller wait for it... constructs a new middleware poller. +func NewMiddlewarePoller(original Poller, filter func(*Update) bool) *MiddlewarePoller { + return &MiddlewarePoller{ + Poller: original, + Filter: filter, + } +} + +// Poll sieves updates through middleware filter. +func (p *MiddlewarePoller) Poll(b *Bot, dest chan Update, stop chan struct{}) { + if p.Capacity < 1 { + p.Capacity = 1 + } + + middle := make(chan Update, p.Capacity) + stopPoller := make(chan struct{}) + stopConfirm := make(chan struct{}) + + go func() { + p.Poller.Poll(b, middle, stopPoller) + close(stopConfirm) + }() + + for { + select { + case <-stop: + close(stopPoller) + <-stopConfirm + return + case upd := <-middle: + if p.Filter(&upd) { + dest <- upd + } + } + } +} + +// LongPoller is a classic LongPoller with timeout. +type LongPoller struct { + Limit int + Timeout time.Duration + LastUpdateID int + + // AllowedUpdates contains the update types + // you want your bot to receive. + // + // Possible values: + // message + // edited_message + // channel_post + // edited_channel_post + // inline_query + // chosen_inline_result + // callback_query + // shipping_query + // pre_checkout_query + // poll + // poll_answer + // + AllowedUpdates []string `yaml:"allowed_updates"` +} + +// Poll does long polling. +func (p *LongPoller) Poll(b *Bot, dest chan Update, stop chan struct{}) { + for { + select { + case <-stop: + return + default: + } + + updates, err := b.getUpdates(p.LastUpdateID+1, p.Limit, p.Timeout, p.AllowedUpdates) + if err != nil { + b.debug(err) + continue + } + + for _, update := range updates { + p.LastUpdateID = update.ID + dest <- update + } + } +} diff --git a/vendor/gopkg.in/telebot.v3/polls.go b/vendor/gopkg.in/telebot.v3/polls.go new file mode 100644 index 0000000000..d616f26f45 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/polls.go @@ -0,0 +1,62 @@ +package telebot + +import "time" + +// Poll contains information about a poll. +type Poll struct { + ID string `json:"id"` + Type PollType `json:"type"` + Question string `json:"question"` + Options []PollOption `json:"options"` + VoterCount int `json:"total_voter_count"` + + // (Optional) + Closed bool `json:"is_closed,omitempty"` + CorrectOption int `json:"correct_option_id,omitempty"` + MultipleAnswers bool `json:"allows_multiple_answers,omitempty"` + Explanation string `json:"explanation,omitempty"` + ParseMode ParseMode `json:"explanation_parse_mode,omitempty"` + Entities []MessageEntity `json:"explanation_entities"` + + // True by default, shouldn't be omitted. + Anonymous bool `json:"is_anonymous"` + + // (Mutually exclusive) + OpenPeriod int `json:"open_period,omitempty"` + CloseUnixdate int64 `json:"close_date,omitempty"` +} + +// PollOption contains information about one answer option in a poll. +type PollOption struct { + Text string `json:"text"` + VoterCount int `json:"voter_count"` +} + +// PollAnswer represents an answer of a user in a non-anonymous poll. +type PollAnswer struct { + PollID string `json:"poll_id"` + Sender *User `json:"user"` + Options []int `json:"option_ids"` +} + +// IsRegular says whether poll is a regular. +func (p *Poll) IsRegular() bool { + return p.Type == PollRegular +} + +// IsQuiz says whether poll is a quiz. +func (p *Poll) IsQuiz() bool { + return p.Type == PollQuiz +} + +// CloseDate returns the close date of poll in local time. +func (p *Poll) CloseDate() time.Time { + return time.Unix(p.CloseUnixdate, 0) +} + +// AddOptions adds text options to the poll. +func (p *Poll) AddOptions(opts ...string) { + for _, t := range opts { + p.Options = append(p.Options, PollOption{Text: t}) + } +} diff --git a/vendor/gopkg.in/telebot.v3/sendable.go b/vendor/gopkg.in/telebot.v3/sendable.go new file mode 100644 index 0000000000..c8ed4d7ad1 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/sendable.go @@ -0,0 +1,431 @@ +package telebot + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strconv" + "strings" +) + +// Recipient is any possible endpoint you can send +// messages to: either user, group or a channel. +type Recipient interface { + Recipient() string // must return legit Telegram chat_id or username +} + +// Sendable is any object that can send itself. +// +// This is pretty cool, since it lets bots implement +// custom Sendables for complex kind of media or +// chat objects spanning across multiple messages. +// +type Sendable interface { + Send(*Bot, Recipient, *SendOptions) (*Message, error) +} + +// Send delivers media through bot b to recipient. +func (p *Photo) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "caption": p.Caption, + } + b.embedSendOptions(params, opt) + + msg, err := b.sendMedia(p, params, nil) + if err != nil { + return nil, err + } + + msg.Photo.File.stealRef(&p.File) + *p = *msg.Photo + p.Caption = msg.Caption + + return msg, nil +} + +// Send delivers media through bot b to recipient. +func (a *Audio) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "caption": a.Caption, + "performer": a.Performer, + "title": a.Title, + "file_name": a.FileName, + } + b.embedSendOptions(params, opt) + + if a.Duration != 0 { + params["duration"] = strconv.Itoa(a.Duration) + } + + msg, err := b.sendMedia(a, params, thumbnailToFilemap(a.Thumbnail)) + if err != nil { + return nil, err + } + + if msg.Audio != nil { + msg.Audio.File.stealRef(&a.File) + *a = *msg.Audio + a.Caption = msg.Caption + } + + if msg.Document != nil { + msg.Document.File.stealRef(&a.File) + a.File = msg.Document.File + } + + return msg, nil +} + +// Send delivers media through bot b to recipient. +func (d *Document) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "caption": d.Caption, + "file_name": d.FileName, + } + b.embedSendOptions(params, opt) + + if d.FileSize != 0 { + params["file_size"] = strconv.Itoa(d.FileSize) + } + if d.DisableTypeDetection { + params["disable_content_type_detection"] = "true" + } + + msg, err := b.sendMedia(d, params, thumbnailToFilemap(d.Thumbnail)) + if err != nil { + return nil, err + } + + msg.Document.File.stealRef(&d.File) + *d = *msg.Document + d.Caption = msg.Caption + + return msg, nil +} + +// Send delivers media through bot b to recipient. +func (s *Sticker) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + } + b.embedSendOptions(params, opt) + + msg, err := b.sendMedia(s, params, nil) + if err != nil { + return nil, err + } + + msg.Sticker.File.stealRef(&s.File) + *s = *msg.Sticker + + return msg, nil +} + +// Send delivers media through bot b to recipient. +func (v *Video) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "caption": v.Caption, + "file_name": v.FileName, + } + b.embedSendOptions(params, opt) + + if v.Duration != 0 { + params["duration"] = strconv.Itoa(v.Duration) + } + if v.Width != 0 { + params["width"] = strconv.Itoa(v.Width) + } + if v.Height != 0 { + params["height"] = strconv.Itoa(v.Height) + } + if v.Streaming { + params["supports_streaming"] = "true" + } + + msg, err := b.sendMedia(v, params, thumbnailToFilemap(v.Thumbnail)) + if err != nil { + return nil, err + } + + if vid := msg.Video; vid != nil { + vid.File.stealRef(&v.File) + *v = *vid + v.Caption = msg.Caption + } else if doc := msg.Document; doc != nil { + // If video has no sound, Telegram can turn it into Document (GIF) + doc.File.stealRef(&v.File) + + v.Caption = doc.Caption + v.MIME = doc.MIME + v.Thumbnail = doc.Thumbnail + } + + return msg, nil +} + +// Send delivers animation through bot b to recipient. +func (a *Animation) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "caption": a.Caption, + "file_name": a.FileName, + } + b.embedSendOptions(params, opt) + + if a.Duration != 0 { + params["duration"] = strconv.Itoa(a.Duration) + } + if a.Width != 0 { + params["width"] = strconv.Itoa(a.Width) + } + if a.Height != 0 { + params["height"] = strconv.Itoa(a.Height) + } + + // file_name is required, without it animation sends as a document + if params["file_name"] == "" && a.File.OnDisk() { + params["file_name"] = filepath.Base(a.File.FileLocal) + } + + msg, err := b.sendMedia(a, params, thumbnailToFilemap(a.Thumbnail)) + if err != nil { + return nil, err + } + + if anim := msg.Animation; anim != nil { + anim.File.stealRef(&a.File) + *a = *msg.Animation + } else if doc := msg.Document; doc != nil { + *a = Animation{ + File: doc.File, + Thumbnail: doc.Thumbnail, + MIME: doc.MIME, + FileName: doc.FileName, + } + } + + a.Caption = msg.Caption + return msg, nil +} + +// Send delivers media through bot b to recipient. +func (v *Voice) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "caption": v.Caption, + } + b.embedSendOptions(params, opt) + + if v.Duration != 0 { + params["duration"] = strconv.Itoa(v.Duration) + } + + msg, err := b.sendMedia(v, params, nil) + if err != nil { + return nil, err + } + + msg.Voice.File.stealRef(&v.File) + *v = *msg.Voice + + return msg, nil +} + +// Send delivers media through bot b to recipient. +func (v *VideoNote) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + } + b.embedSendOptions(params, opt) + + if v.Duration != 0 { + params["duration"] = strconv.Itoa(v.Duration) + } + if v.Length != 0 { + params["length"] = strconv.Itoa(v.Length) + } + + msg, err := b.sendMedia(v, params, thumbnailToFilemap(v.Thumbnail)) + if err != nil { + return nil, err + } + + msg.VideoNote.File.stealRef(&v.File) + *v = *msg.VideoNote + + return msg, nil +} + +// Send delivers media through bot b to recipient. +func (x *Location) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "latitude": fmt.Sprintf("%f", x.Lat), + "longitude": fmt.Sprintf("%f", x.Lng), + "live_period": strconv.Itoa(x.LivePeriod), + } + if x.HorizontalAccuracy != nil { + params["horizontal_accuracy"] = fmt.Sprintf("%f", *x.HorizontalAccuracy) + } + if x.Heading != 0 { + params["heading"] = strconv.Itoa(x.Heading) + } + if x.AlertRadius != 0 { + params["proximity_alert_radius"] = strconv.Itoa(x.Heading) + } + b.embedSendOptions(params, opt) + + data, err := b.Raw("sendLocation", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Send delivers media through bot b to recipient. +func (v *Venue) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "latitude": fmt.Sprintf("%f", v.Location.Lat), + "longitude": fmt.Sprintf("%f", v.Location.Lng), + "title": v.Title, + "address": v.Address, + "foursquare_id": v.FoursquareID, + "foursquare_type": v.FoursquareType, + "google_place_id": v.GooglePlaceID, + "google_place_type": v.GooglePlaceType, + } + b.embedSendOptions(params, opt) + + data, err := b.Raw("sendVenue", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Send delivers invoice through bot b to recipient. +func (i *Invoice) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "title": i.Title, + "description": i.Description, + "start_parameter": i.Start, + "payload": i.Payload, + "provider_token": i.Token, + "currency": i.Currency, + "max_tip_amount": strconv.Itoa(i.MaxTipAmount), + "need_name": strconv.FormatBool(i.NeedName), + "need_phone_number": strconv.FormatBool(i.NeedPhoneNumber), + "need_email": strconv.FormatBool(i.NeedEmail), + "need_shipping_address": strconv.FormatBool(i.NeedShippingAddress), + "send_phone_number_to_provider": strconv.FormatBool(i.SendPhoneNumber), + "send_email_to_provider": strconv.FormatBool(i.SendEmail), + "is_flexible": strconv.FormatBool(i.Flexible), + } + if i.Photo != nil { + if i.Photo.FileURL != "" { + params["photo_url"] = i.Photo.FileURL + } + if i.PhotoSize > 0 { + params["photo_size"] = strconv.Itoa(i.PhotoSize) + } + if i.Photo.Width > 0 { + params["photo_width"] = strconv.Itoa(i.Photo.Width) + } + if i.Photo.Height > 0 { + params["photo_height"] = strconv.Itoa(i.Photo.Height) + } + } + if len(i.Prices) > 0 { + data, _ := json.Marshal(i.Prices) + params["prices"] = string(data) + } + if len(i.SuggestedTipAmounts) > 0 { + params["suggested_tip_amounts"] = "[" + strings.Join(intsToStrs(i.SuggestedTipAmounts), ",") + "]" + } + b.embedSendOptions(params, opt) + + data, err := b.Raw("sendInvoice", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Send delivers poll through bot b to recipient. +func (p *Poll) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "question": p.Question, + "type": string(p.Type), + "is_closed": strconv.FormatBool(p.Closed), + "is_anonymous": strconv.FormatBool(p.Anonymous), + "allows_multiple_answers": strconv.FormatBool(p.MultipleAnswers), + "correct_option_id": strconv.Itoa(p.CorrectOption), + } + if p.Explanation != "" { + params["explanation"] = p.Explanation + params["explanation_parse_mode"] = p.ParseMode + } + if p.OpenPeriod != 0 { + params["open_period"] = strconv.Itoa(p.OpenPeriod) + } else if p.CloseUnixdate != 0 { + params["close_date"] = strconv.FormatInt(p.CloseUnixdate, 10) + } + b.embedSendOptions(params, opt) + + var options []string + for _, o := range p.Options { + options = append(options, o.Text) + } + + opts, _ := json.Marshal(options) + params["options"] = string(opts) + + data, err := b.Raw("sendPoll", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Send delivers dice through bot b to recipient. +func (d *Dice) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "emoji": string(d.Type), + } + b.embedSendOptions(params, opt) + + data, err := b.Raw("sendDice", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} + +// Send delivers game through bot b to recipient. +func (g *Game) Send(b *Bot, to Recipient, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "game_short_name": g.Name, + } + b.embedSendOptions(params, opt) + + data, err := b.Raw("sendGame", params) + if err != nil { + return nil, err + } + + return extractMessage(data) +} diff --git a/vendor/gopkg.in/telebot.v3/stickers.go b/vendor/gopkg.in/telebot.v3/stickers.go new file mode 100644 index 0000000000..7cbd47930a --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/stickers.go @@ -0,0 +1,170 @@ +package telebot + +import ( + "encoding/json" + "strconv" +) + +// StickerSet represents a sticker set. +type StickerSet struct { + 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"` + 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"` +} + +// 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(), + "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 +} diff --git a/vendor/gopkg.in/telebot.v3/telebot.go b/vendor/gopkg.in/telebot.v3/telebot.go new file mode 100644 index 0000000000..1d828fb118 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/telebot.go @@ -0,0 +1,242 @@ +// Package telebot is a framework for Telegram bots. +// +// Example: +// +// package main +// +// import ( +// "time" +// tele "gopkg.in/tucnak/telebot.v3" +// ) +// +// func main() { +// b, err := tele.NewBot(tele.Settings{ +// Token: "...", +// Poller: &tele.LongPoller{Timeout: 10 * time.Second}, +// }) +// if err != nil { +// return +// } +// +// b.Handle(tele.OnText, func(c tele.Context) error { +// return c.Send("Hello world!") +// }) +// +// b.Start() +// } +// +package telebot + +import "errors" + +var ( + ErrBadRecipient = errors.New("telebot: recipient is nil") + ErrUnsupportedWhat = errors.New("telebot: unsupported what argument") + ErrCouldNotUpdate = errors.New("telebot: could not fetch new updates") + ErrTrueResult = errors.New("telebot: result is True") + ErrBadContext = errors.New("telebot: context does not contain message") +) + +const DefaultApiURL = "https://api.telegram.org" + +// These are one of the possible events Handle() can deal with. +// +// For convenience, all Telebot-provided endpoints start with +// an "alert" character \a. +// +const ( + // Basic message handlers. + OnText = "\atext" + OnEdited = "\aedited" + OnPhoto = "\aphoto" + OnAudio = "\aaudio" + OnAnimation = "\aanimation" + OnDocument = "\adocument" + OnSticker = "\asticker" + OnVideo = "\avideo" + OnVoice = "\avoice" + OnVideoNote = "\avideo_note" + OnContact = "\acontact" + OnLocation = "\alocation" + OnVenue = "\avenue" + OnDice = "\adice" + OnInvoice = "\ainvoice" + OnPayment = "\apayment" + OnGame = "\agame" + OnPoll = "\apoll" + OnPollAnswer = "\apoll_answer" + OnPinned = "\apinned" + + // Will fire on channel posts. + OnChannelPost = "\achannel_post" + OnEditedChannelPost = "\aedited_channel_post" + + // Will fire when bot is added to a group. + OnAddedToGroup = "\aadded_to_group" + + // Service events: + OnUserJoined = "\auser_joined" + OnUserLeft = "\auser_left" + OnNewGroupTitle = "\anew_chat_title" + OnNewGroupPhoto = "\anew_chat_photo" + OnGroupPhotoDeleted = "\achat_photo_deleted" + OnGroupCreated = "\agroup_created" + OnSuperGroupCreated = "\asupergroup_created" + OnChannelCreated = "\achannel_created" + + // Migration happens when group switches to + // a supergroup. You might want to update + // your internal references to this chat + // upon switching as its ID will change. + OnMigration = "\amigration" + + // Will fire on any unhandled media. + OnMedia = "\amedia" + + // Will fire on callback requests. + OnCallback = "\acallback" + + // Will fire on incoming inline queries. + OnQuery = "\aquery" + + // Will fire on chosen inline results. + OnInlineResult = "\ainline_result" + + // Will fire on a shipping query. + OnShipping = "\ashipping_query" + + // Will fire on pre checkout query. + OnCheckout = "\apre_checkout_query" + + // Will fire on bot's chat member changes. + OnMyChatMember = "\amy_chat_member" + + // Will fire on chat member's changes. + OnChatMember = "\achat_member" + + // Will fire on chat join request. + OnChatJoinRequest = "\achat_join_request" + + // Will fire on the start of a voice chat. + OnVoiceChatStarted = "\avoice_chat_started" + + // Will fire on the end of a voice chat. + OnVoiceChatEnded = "\avoice_chat_ended" + + // Will fire on invited participants to the voice chat. + OnVoiceChatParticipants = "\avoice_chat_participants_invited" + + // Will fire on scheduling a voice chat. + OnVoiceChatScheduled = "\avoice_chat_scheduled" + + // Will fire on a proximity alert. + OnProximityAlert = "\aproximity_alert_triggered" + + // Will fire on auto delete timer set. + OnAutoDeleteTimer = "\amessage_auto_delete_timer_changed" +) + +// ChatAction is a client-side status indicating bot activity. +type ChatAction string + +const ( + Typing ChatAction = "typing" + UploadingPhoto ChatAction = "upload_photo" + UploadingVideo ChatAction = "upload_video" + UploadingAudio ChatAction = "upload_audio" + UploadingDocument ChatAction = "upload_document" + UploadingVNote ChatAction = "upload_video_note" + RecordingVideo ChatAction = "record_video" + RecordingAudio ChatAction = "record_audio" + RecordingVNote ChatAction = "record_video_note" + FindingLocation ChatAction = "find_location" + ChoosingSticker ChatAction = "choose_sticker" +) + +// ParseMode determines the way client applications treat the text of the message +type ParseMode = string + +const ( + ModeDefault ParseMode = "" + ModeMarkdown ParseMode = "Markdown" + ModeMarkdownV2 ParseMode = "MarkdownV2" + ModeHTML ParseMode = "HTML" +) + +// EntityType is a MessageEntity type. +type EntityType string + +const ( + EntityMention EntityType = "mention" + EntityTMention EntityType = "text_mention" + EntityHashtag EntityType = "hashtag" + EntityCashtag EntityType = "cashtag" + EntityCommand EntityType = "bot_command" + EntityURL EntityType = "url" + EntityEmail EntityType = "email" + EntityPhone EntityType = "phone_number" + EntityBold EntityType = "bold" + EntityItalic EntityType = "italic" + EntityUnderline EntityType = "underline" + EntityStrikethrough EntityType = "strikethrough" + EntityCode EntityType = "code" + EntityCodeBlock EntityType = "pre" + EntityTextLink EntityType = "text_link" + EntitySpoiler EntityType = "spoiler" +) + +// ChatType represents one of the possible chat types. +type ChatType string + +const ( + ChatPrivate ChatType = "private" + ChatGroup ChatType = "group" + ChatSuperGroup ChatType = "supergroup" + ChatChannel ChatType = "channel" + ChatChannelPrivate ChatType = "privatechannel" +) + +// MemberStatus is one's chat status. +type MemberStatus string + +const ( + Creator MemberStatus = "creator" + Administrator MemberStatus = "administrator" + Member MemberStatus = "member" + Restricted MemberStatus = "restricted" + Left MemberStatus = "left" + Kicked MemberStatus = "kicked" +) + +// MaskFeature defines sticker mask position. +type MaskFeature string + +const ( + FeatureForehead MaskFeature = "forehead" + FeatureEyes MaskFeature = "eyes" + FeatureMouth MaskFeature = "mouth" + FeatureChin MaskFeature = "chin" +) + +// PollType defines poll types. +type PollType string + +const ( + // Despite "any" type isn't described in documentation, + // it needed for proper KeyboardButtonPollType marshaling. + PollAny PollType = "any" + + PollQuiz PollType = "quiz" + PollRegular PollType = "regular" +) + +type DiceType string + +var ( + Cube = &Dice{Type: "🎲"} + Dart = &Dice{Type: "🎯"} + Ball = &Dice{Type: "🏀"} + Goal = &Dice{Type: "⚽"} + Slot = &Dice{Type: "🎰"} + Bowl = &Dice{Type: "🎳"} +) diff --git a/vendor/gopkg.in/telebot.v3/util.go b/vendor/gopkg.in/telebot.v3/util.go new file mode 100644 index 0000000000..7dcb40e2f7 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/util.go @@ -0,0 +1,293 @@ +package telebot + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" +) + +var defaultOnError = func(err error, c Context) { + log.Println(c.Update().ID, err) +} + +func (b *Bot) debug(err error) { + log.Println(err) +} + +func (b *Bot) deferDebug() { + if r := recover(); r != nil { + if err, ok := r.(error); ok { + b.debug(err) + } else if str, ok := r.(string); ok { + b.debug(fmt.Errorf("%s", str)) + } + } +} + +func (b *Bot) runHandler(h HandlerFunc, c Context) { + f := func() { + defer b.deferDebug() + if err := h(c); err != nil { + b.OnError(err, c) + } + } + if b.synchronous { + f() + } else { + go f() + } +} + +func applyMiddleware(h HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc { + for i := len(middleware) - 1; i >= 0; i-- { + h = middleware[i](h) + } + return h +} + +// wrapError returns new wrapped telebot-related error. +func wrapError(err error) error { + return fmt.Errorf("telebot: %w", err) +} + +// 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 extractOptions(how []interface{}) *SendOptions { + opts := &SendOptions{} + + for _, prop := range how { + switch opt := prop.(type) { + case *SendOptions: + opts = opt.copy() + case *ReplyMarkup: + if opt != nil { + opts.ReplyMarkup = opt.copy() + } + case Option: + switch opt { + case NoPreview: + opts.DisableWebPagePreview = true + case Silent: + opts.DisableNotification = true + case AllowWithoutReply: + opts.AllowWithoutReply = true + case ForceReply: + if opts.ReplyMarkup == nil { + opts.ReplyMarkup = &ReplyMarkup{} + } + opts.ReplyMarkup.ForceReply = true + case OneTimeKeyboard: + if opts.ReplyMarkup == nil { + opts.ReplyMarkup = &ReplyMarkup{} + } + opts.ReplyMarkup.OneTimeKeyboard = true + case RemoveKeyboard: + if opts.ReplyMarkup == nil { + opts.ReplyMarkup = &ReplyMarkup{} + } + opts.ReplyMarkup.RemoveKeyboard = true + case Protected: + opts.Protected = true + default: + panic("telebot: unsupported flag-option") + } + case ParseMode: + opts.ParseMode = opt + case Entities: + opts.Entities = opt + default: + panic("telebot: unsupported send-option") + } + } + + return opts +} + +func (b *Bot) embedSendOptions(params map[string]string, opt *SendOptions) { + if b.parseMode != ModeDefault { + params["parse_mode"] = b.parseMode + } + + if opt == nil { + return + } + + if opt.ReplyTo != nil && opt.ReplyTo.ID != 0 { + params["reply_to_message_id"] = strconv.Itoa(opt.ReplyTo.ID) + } + + if opt.DisableWebPagePreview { + params["disable_web_page_preview"] = "true" + } + + if opt.DisableNotification { + params["disable_notification"] = "true" + } + + if opt.ParseMode != ModeDefault { + params["parse_mode"] = opt.ParseMode + } + + if len(opt.Entities) > 0 { + delete(params, "parse_mode") + entities, _ := json.Marshal(opt.Entities) + + if params["caption"] != "" { + params["caption_entities"] = string(entities) + } else { + params["entities"] = string(entities) + } + } + + if opt.AllowWithoutReply { + params["allow_sending_without_reply"] = "true" + } + + if opt.ReplyMarkup != nil { + processButtons(opt.ReplyMarkup.InlineKeyboard) + replyMarkup, _ := json.Marshal(opt.ReplyMarkup) + params["reply_markup"] = string(replyMarkup) + } + + if opt.Protected { + params["protect_content"] = "true" + } +} + +func processButtons(keys [][]InlineButton) { + if keys == nil || len(keys) < 1 || len(keys[0]) < 1 { + return + } + + for i := range keys { + for j := range keys[i] { + key := &keys[i][j] + if key.Unique != "" { + // Format: "\f|" + data := key.Data + if data == "" { + key.Data = "\f" + key.Unique + } else { + key.Data = "\f" + key.Unique + "|" + data + } + } + } + } +} + +func embedRights(p map[string]interface{}, rights Rights) { + data, _ := json.Marshal(rights) + _ = json.Unmarshal(data, &p) +} + +func thumbnailToFilemap(thumb *Photo) map[string]File { + if thumb != nil { + return map[string]File{"thumb": thumb.File} + } + return nil +} + +func isUserInList(user *User, list []User) bool { + for _, user2 := range list { + if user.ID == user2.ID { + return true + } + } + return false +} + +func intsToStrs(ns []int) (s []string) { + for _, n := range ns { + s = append(s, strconv.Itoa(n)) + } + return +} + +// extractCommandsParams extracts parameters for commands-related methods from the given options. +func extractCommandsParams(opts ...interface{}) (params CommandParams) { + for _, opt := range opts { + switch value := opt.(type) { + case []Command: + params.Commands = value + case string: + params.LanguageCode = value + case CommandScope: + params.Scope = &value + } + } + return +} diff --git a/vendor/gopkg.in/telebot.v3/voice.go b/vendor/gopkg.in/telebot.v3/voice.go new file mode 100644 index 0000000000..0de67f183b --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/voice.go @@ -0,0 +1,29 @@ +package telebot + +import "time" + +// VoiceChatStarted represents a service message about a voice chat +// started in the chat. +type VoiceChatStarted struct{} + +// VoiceChatEnded represents a service message about a voice chat +// ended in the chat. +type VoiceChatEnded struct { + Duration int `json:"duration"` // in seconds +} + +// VoiceChatParticipants represents a service message about new +// members invited to a voice chat +type VoiceChatParticipants struct { + Users []User `json:"users"` +} + +// VoiceChatScheduled represents a service message about a voice chat scheduled in the chat. +type VoiceChatScheduled struct { + Unixtime int64 `json:"start_date"` +} + +// StartsAt returns the point when the voice chat is supposed to be started by a chat administrator. +func (v *VoiceChatScheduled) StartsAt() time.Time { + return time.Unix(v.Unixtime, 0) +} diff --git a/vendor/gopkg.in/telebot.v3/webhook.go b/vendor/gopkg.in/telebot.v3/webhook.go new file mode 100644 index 0000000000..64e7414e25 --- /dev/null +++ b/vendor/gopkg.in/telebot.v3/webhook.go @@ -0,0 +1,197 @@ +package telebot + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" +) + +// A WebhookTLS specifies the path to a key and a cert so the poller can open +// a TLS listener. +type WebhookTLS struct { + Key string `json:"key"` + Cert string `json:"cert"` +} + +// A WebhookEndpoint describes the endpoint to which telegram will send its requests. +// This must be a public URL and can be a loadbalancer or something similar. If the +// endpoint uses TLS and the certificate is self-signed you have to add the certificate +// path of this certificate so telegram will trust it. This field can be ignored if you +// have a trusted certificate (letsencrypt, ...). +type WebhookEndpoint struct { + PublicURL string `json:"public_url"` + Cert string `json:"cert"` +} + +// A Webhook configures the poller for webhooks. It opens a port on the given +// listen address. If TLS is filled, the listener will use the key and cert to open +// a secure port. Otherwise it will use plain HTTP. +// +// If you have a loadbalancer ore other infrastructure in front of your service, you +// must fill the Endpoint structure so this poller will send this data to telegram. If +// you leave these values empty, your local address will be sent to telegram which is mostly +// not what you want (at least while developing). If you have a single instance of your +// bot you should consider to use the LongPoller instead of a WebHook. +// +// You can also leave the Listen field empty. In this case it is up to the caller to +// add the Webhook to a http-mux. +// +type Webhook struct { + Listen string `json:"url"` + MaxConnections int `json:"max_connections"` + AllowedUpdates []string `json:"allowed_updates"` + IP string `json:"ip_address"` + DropUpdates bool `json:"drop_pending_updates"` + + // (WebhookInfo) + HasCustomCert bool `json:"has_custom_certificate"` + PendingUpdates int `json:"pending_update_count"` + ErrorUnixtime int64 `json:"last_error_date"` + ErrorMessage string `json:"last_error_message"` + + TLS *WebhookTLS + Endpoint *WebhookEndpoint + + dest chan<- Update + bot *Bot +} + +func (h *Webhook) getFiles() map[string]File { + m := make(map[string]File) + + if h.TLS != nil { + m["certificate"] = FromDisk(h.TLS.Cert) + } + // check if it is overwritten by an endpoint + if h.Endpoint != nil { + if h.Endpoint.Cert == "" { + // this can be the case if there is a loadbalancer or reverseproxy in + // front with a public cert. in this case we do not need to upload it + // to telegram. we delete the certificate from the map, because someone + // can have an internal TLS listener with a private cert + delete(m, "certificate") + } else { + // someone configured a certificate + m["certificate"] = FromDisk(h.Endpoint.Cert) + } + } + return m +} + +func (h *Webhook) getParams() map[string]string { + params := make(map[string]string) + + if h.MaxConnections != 0 { + params["max_connections"] = strconv.Itoa(h.MaxConnections) + } + if len(h.AllowedUpdates) > 0 { + data, _ := json.Marshal(h.AllowedUpdates) + params["allowed_updates"] = string(data) + } + if h.IP != "" { + params["ip_address"] = h.IP + } + if h.DropUpdates { + params["drop_pending_updates"] = strconv.FormatBool(h.DropUpdates) + } + + if h.TLS != nil { + params["url"] = "https://" + h.Listen + } else { + // this will not work with telegram, they want TLS + // but i allow this because telegram will send an error + // when you register this hook. in their docs they write + // that port 80/http is allowed ... + params["url"] = "http://" + h.Listen + } + if h.Endpoint != nil { + params["url"] = h.Endpoint.PublicURL + } + return params +} + +func (h *Webhook) Poll(b *Bot, dest chan Update, stop chan struct{}) { + if err := b.SetWebhook(h); err != nil { + b.debug(err) + close(stop) + return + } + + // store the variables so the HTTP-handler can use 'em + h.dest = dest + h.bot = b + + if h.Listen == "" { + h.waitForStop(stop) + return + } + + s := &http.Server{ + Addr: h.Listen, + Handler: h, + } + + go func(stop chan struct{}) { + h.waitForStop(stop) + s.Shutdown(context.Background()) + }(stop) + + if h.TLS != nil { + s.ListenAndServeTLS(h.TLS.Cert, h.TLS.Key) + } else { + s.ListenAndServe() + } +} + +func (h *Webhook) waitForStop(stop chan struct{}) { + <-stop + close(stop) +} + +// The handler simply reads the update from the body of the requests +// and writes them to the update channel. +func (h *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var update Update + if err := json.NewDecoder(r.Body).Decode(&update); err != nil { + h.bot.debug(fmt.Errorf("cannot decode update: %v", err)) + return + } + h.dest <- update +} + +// Webhook returns the current webhook status. +func (b *Bot) Webhook() (*Webhook, error) { + data, err := b.Raw("getWebhookInfo", nil) + if err != nil { + return nil, err + } + + var resp struct { + Result Webhook + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, wrapError(err) + } + return &resp.Result, nil +} + +// SetWebhook configures a bot to receive incoming +// updates via an outgoing webhook. +func (b *Bot) SetWebhook(w *Webhook) error { + _, err := b.sendFiles("setWebhook", w.getFiles(), w.getParams()) + return err +} + +// RemoveWebhook removes webhook integration. +func (b *Bot) RemoveWebhook(dropPending ...bool) error { + drop := false + if len(dropPending) > 0 { + drop = dropPending[0] + } + _, err := b.Raw("deleteWebhook", map[string]bool{ + "drop_pending_updates": drop, + }) + return err +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 3548743cd8..69893b584b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -666,6 +666,7 @@ github.com/prometheus/alertmanager/notify/pagerduty github.com/prometheus/alertmanager/notify/pushover github.com/prometheus/alertmanager/notify/slack github.com/prometheus/alertmanager/notify/sns +github.com/prometheus/alertmanager/notify/telegram github.com/prometheus/alertmanager/notify/victorops github.com/prometheus/alertmanager/notify/webhook github.com/prometheus/alertmanager/notify/wechat @@ -1215,6 +1216,9 @@ gopkg.in/alecthomas/kingpin.v2 # gopkg.in/ini.v1 v1.57.0 ## explicit gopkg.in/ini.v1 +# gopkg.in/telebot.v3 v3.0.0 +## explicit; go 1.13 +gopkg.in/telebot.v3 # gopkg.in/yaml.v2 v2.4.0 ## explicit; go 1.15 gopkg.in/yaml.v2