diff --git a/Gopkg.lock b/Gopkg.lock index 1ee5dd92b..d4df9df9c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -199,6 +199,12 @@ revision = "5f041e8faa004a95c88a202771f4cc3e991971e6" version = "v2.0.1" +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + [[projects]] name = "github.com/spf13/afero" packages = [".","mem"] @@ -271,6 +277,12 @@ revision = "3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4" version = "v0.9.0" +[[projects]] + branch = "v2" + name = "gopkg.in/tucnak/telebot.v2" + packages = ["."] + revision = "b59ea4aec33eba0a5d2eeda2638efe000fa93f22" + [[projects]] branch = "v2" name = "gopkg.in/yaml.v2" @@ -301,6 +313,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "6a262eb088f5665cba517b1096fd973db6bb53be9ca5bbeb2633deeb002cf680" + inputs-digest = "dd20fd0a6e568589dc93d298956d12e8b0f3904e7311c072972b9d5f1f419ef1" solver-name = "gps-cdcl" solver-version = 1 diff --git a/cmd/config.go b/cmd/config.go index e08b86bc8..d6cacb12b 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -24,7 +24,7 @@ import ( var configCmd = &cobra.Command{ Use: "config SUBCOMMAND", Short: "config modifies kubewatch configuration", - Long: `config command allows admin setup his own configuration for running kubewatch`, + Long: `config command allows admin setup his own configuration for running kubewatch`, Run: func(cmd *cobra.Command, args []string) { cmd.Help() }, @@ -38,4 +38,5 @@ func init() { configCmd.AddCommand(resourceConfigCmd) configCmd.AddCommand(flockConfigCmd) configCmd.AddCommand(webhookConfigCmd) + configCmd.AddCommand(telegramCmd) } diff --git a/cmd/telegram.go b/cmd/telegram.go new file mode 100644 index 000000000..df377ff16 --- /dev/null +++ b/cmd/telegram.go @@ -0,0 +1,60 @@ +// Copyright © 2018 NAME HERE +// +// 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 cmd + +import ( + "github.com/Sirupsen/logrus" + "github.com/bitnami-labs/kubewatch/config" + "github.com/spf13/cobra" +) + +// telegramCmd represents the telegram command +var telegramCmd = &cobra.Command{ + Use: "telegram", + Short: "specific telegram configuration", + Long: `specific telegram configuration`, + Run: func(cmd *cobra.Command, args []string) { + conf, err := config.New() + if err != nil { + logrus.Fatal(err) + } + + token, err := cmd.Flags().GetString("token") + if err == nil { + if len(token) > 0 { + conf.Handler.Telegram.Token = token + } + } else { + logrus.Fatal(err) + } + channel, err := cmd.Flags().GetString("channel") + if err == nil { + if len(channel) > 0 { + conf.Handler.Telegram.Channel = channel + } + } else { + logrus.Fatal(err) + } + + if err = conf.Write(); err != nil { + logrus.Fatal(err) + } + }, +} + +func init() { + telegramCmd.Flags().StringP("channel", "c", "", "Specify telegram channel") + telegramCmd.Flags().StringP("token", "t", "", "Specify telegram token") +} diff --git a/config/config.go b/config/config.go index 7b7cf6ca9..90fcd6405 100644 --- a/config/config.go +++ b/config/config.go @@ -33,6 +33,7 @@ type Handler struct { Mattermost Mattermost `json:"mattermost"` Flock Flock `json:"flock"` Webhook Webhook `json:"webhook"` + Telegram Telegram `json:"telegram"` } // Resource contains resource configuration @@ -91,6 +92,12 @@ type Webhook struct { Url string `json:"url"` } +// Telegram contains slack configuration +type Telegram struct { + Token string `json:"token"` + Channel string `json:"channel"` +} + // New creates new config object func New() (*Config, error) { c := &Config{} @@ -186,6 +193,13 @@ func (c *Config) CheckMissingResourceEnvvars() { if (c.Handler.Slack.Token == "") && (os.Getenv("SLACK_TOKEN") != "") { c.Handler.Slack.Token = os.Getenv("SLACK_TOKEN") } + + if (c.Handler.Telegram.Channel == "") && (os.Getenv("TELEGRAM_CHANNEL") != "") { + c.Handler.Telegram.Channel = os.Getenv("TELEGRAM_CHANNEL") + } + if (c.Handler.Telegram.Token == "") && (os.Getenv("TELEGRAM_TOKEN") != "") { + c.Handler.Telegram.Token = os.Getenv("TELEGRAM_TOKEN") + } } func (c *Config) Write() error { diff --git a/main.go b/main.go index 887abb7ef..39f1ca18f 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,9 @@ limitations under the License. package main -import "github.com/bitnami-labs/kubewatch/cmd" +import ( + "github.com/bitnami-labs/kubewatch/cmd" +) func main() { cmd.Execute() diff --git a/pkg/client/run.go b/pkg/client/run.go index 93931034b..e5e04316e 100644 --- a/pkg/client/run.go +++ b/pkg/client/run.go @@ -20,12 +20,13 @@ import ( "log" "github.com/bitnami-labs/kubewatch/config" - "github.com/bitnami-labs/kubewatch/pkg/handlers" - "github.com/bitnami-labs/kubewatch/pkg/handlers/slack" "github.com/bitnami-labs/kubewatch/pkg/controller" + "github.com/bitnami-labs/kubewatch/pkg/handlers" + "github.com/bitnami-labs/kubewatch/pkg/handlers/flock" "github.com/bitnami-labs/kubewatch/pkg/handlers/hipchat" "github.com/bitnami-labs/kubewatch/pkg/handlers/mattermost" - "github.com/bitnami-labs/kubewatch/pkg/handlers/flock" + "github.com/bitnami-labs/kubewatch/pkg/handlers/slack" + "github.com/bitnami-labs/kubewatch/pkg/handlers/telegram" "github.com/bitnami-labs/kubewatch/pkg/handlers/webhook" ) @@ -35,6 +36,8 @@ func Run(conf *config.Config) { switch { case len(conf.Handler.Slack.Channel) > 0 || len(conf.Handler.Slack.Token) > 0: eventHandler = new(slack.Slack) + case len(conf.Handler.Telegram.Channel) > 0 || len(conf.Handler.Telegram.Token) > 0: + eventHandler = new(telegram.Telegram) case len(conf.Handler.Hipchat.Room) > 0 || len(conf.Handler.Hipchat.Token) > 0: eventHandler = new(hipchat.Hipchat) case len(conf.Handler.Mattermost.Channel) > 0 || len(conf.Handler.Mattermost.Url) > 0: diff --git a/pkg/handlers/handler.go b/pkg/handlers/handler.go index aab5fd4e4..a744d97a6 100644 --- a/pkg/handlers/handler.go +++ b/pkg/handlers/handler.go @@ -18,10 +18,11 @@ package handlers import ( "github.com/bitnami-labs/kubewatch/config" - "github.com/bitnami-labs/kubewatch/pkg/handlers/slack" + "github.com/bitnami-labs/kubewatch/pkg/handlers/flock" "github.com/bitnami-labs/kubewatch/pkg/handlers/hipchat" "github.com/bitnami-labs/kubewatch/pkg/handlers/mattermost" - "github.com/bitnami-labs/kubewatch/pkg/handlers/flock" + "github.com/bitnami-labs/kubewatch/pkg/handlers/slack" + "github.com/bitnami-labs/kubewatch/pkg/handlers/telegram" "github.com/bitnami-labs/kubewatch/pkg/handlers/webhook" ) @@ -36,12 +37,13 @@ type Handler interface { // Map maps each event handler function to a name for easily lookup var Map = map[string]interface{}{ - "default": &Default{}, - "slack": &slack.Slack{}, - "hipchat": &hipchat.Hipchat{}, + "default": &Default{}, + "slack": &slack.Slack{}, + "hipchat": &hipchat.Hipchat{}, "mattermost": &mattermost.Mattermost{}, - "flock": &flock.Flock{}, - "webhook": &webhook.Webhook{}, + "flock": &flock.Flock{}, + "webhook": &webhook.Webhook{}, + "telegram": &telegram.Telegram{}, } // Default handler implements Handler interface, diff --git a/pkg/handlers/telegram/telegram.go b/pkg/handlers/telegram/telegram.go new file mode 100644 index 000000000..a645352cd --- /dev/null +++ b/pkg/handlers/telegram/telegram.go @@ -0,0 +1,116 @@ +/* +Copyright 2016 Skippbox, Ltd. + +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 ( + "fmt" + "log" + "os" + "time" + + // "gopkg.in/telegram-bot-api.v4" + tb "gopkg.in/tucnak/telebot.v2" + + "github.com/bitnami-labs/kubewatch/config" + kbEvent "github.com/bitnami-labs/kubewatch/pkg/event" +) + +var telegramErrMsg = ` +%s + +You need to set both token and channel for slack notify, +using "--token/-t" and "--channel/-c", or using environment variables: + +export KW_TELEGRAM_TOKEN=telegram_token +export KW_TELEGRAM_CHANNEL=telegram_channel + +Command line flags will override environment variables + +` + +// Telegram handler implements handler.Handler interface, +// Notify event to telegram channel +type Telegram struct { + Token string + Channel string +} + +type TelegramMessage struct { + Text string `json:"text"` +} + +// Init prepares slack configuration +func (s *Telegram) Init(c *config.Config) error { + token := c.Handler.Telegram.Token + channel := c.Handler.Telegram.Channel + + if token == "" { + token = os.Getenv("KW_TELEGRAM_TOKEN") + } + + if channel == "" { + channel = os.Getenv("KW_TELEGRAM_CHANNEL") + } + + s.Token = token + s.Channel = channel + + return checkMissingTelegramVars(s) +} + +func (s *Telegram) ObjectCreated(obj interface{}) { + notifyTelegram(s, obj, "created") +} + +func (s *Telegram) ObjectDeleted(obj interface{}) { + notifyTelegram(s, obj, "deleted") +} + +func (s *Telegram) ObjectUpdated(oldObj, newObj interface{}) { + notifyTelegram(s, newObj, "updated") +} + +func notifyTelegram(t *Telegram, obj interface{}, action string) { + e := kbEvent.New(obj, action) + b, err := tb.NewBot(tb.Settings{ + Token: t.Token, + Poller: &tb.LongPoller{Timeout: 10 * time.Second}, + }) + + if err != nil { + log.Fatal(err) + return + } + + stChannel := &tb.Chat{ + Type: tb.ChatChannel, + Username: t.Channel, + } + var opts tb.SendOptions + opts.ParseMode = tb.ModeMarkdown + + b.Send(stChannel, e.Message(), &opts) + log.Printf("Message successfully sent to channel %s", t.Channel) +} + +func checkMissingTelegramVars(t *Telegram) error { + if t.Token == "" || t.Channel == "" { + return fmt.Errorf(telegramErrMsg, "Missing telegram token or channel") + } + + return nil +} diff --git a/pkg/handlers/telegram/telegram_test.go b/pkg/handlers/telegram/telegram_test.go new file mode 100644 index 000000000..097ba3919 --- /dev/null +++ b/pkg/handlers/telegram/telegram_test.go @@ -0,0 +1,48 @@ +/* +Copyright 2016 Skippbox, Ltd. + +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 ( + "fmt" + "reflect" + "testing" + + "github.com/bitnami-labs/kubewatch/config" +) + +func TestTelegramInit(t *testing.T) { + s := &Telegram{} + expectedError := fmt.Errorf(telegramErrMsg, "Missing telegram token or channel") + + var Tests = []struct { + telegram config.Telegram + err error + }{ + {config.Telegram{Token: "foo", Channel: "bar"}, nil}, + {config.Telegram{Token: "foo"}, expectedError}, + {config.Telegram{Channel: "bar"}, expectedError}, + {config.Telegram{}, expectedError}, + } + + for _, tt := range Tests { + c := &config.Config{} + c.Handler.Telegram = tt.telegram + if err := s.Init(c); !reflect.DeepEqual(err, tt.err) { + t.Fatalf("Init(): %v, tt.err: %v", err, tt.err) + } + } +} diff --git a/vendor/github.com/pkg/errors/.gitignore b/vendor/github.com/pkg/errors/.gitignore new file mode 100644 index 000000000..daf913b1b --- /dev/null +++ b/vendor/github.com/pkg/errors/.gitignore @@ -0,0 +1,24 @@ +# 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 diff --git a/vendor/github.com/pkg/errors/.travis.yml b/vendor/github.com/pkg/errors/.travis.yml new file mode 100644 index 000000000..588ceca18 --- /dev/null +++ b/vendor/github.com/pkg/errors/.travis.yml @@ -0,0 +1,11 @@ +language: go +go_import_path: github.com/pkg/errors +go: + - 1.4.3 + - 1.5.4 + - 1.6.2 + - 1.7.1 + - tip + +script: + - go test -v ./... diff --git a/vendor/github.com/pkg/errors/LICENSE b/vendor/github.com/pkg/errors/LICENSE new file mode 100644 index 000000000..835ba3e75 --- /dev/null +++ b/vendor/github.com/pkg/errors/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2015, Dave Cheney +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/pkg/errors/README.md b/vendor/github.com/pkg/errors/README.md new file mode 100644 index 000000000..273db3c98 --- /dev/null +++ b/vendor/github.com/pkg/errors/README.md @@ -0,0 +1,52 @@ +# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) + +Package errors provides simple error handling primitives. + +`go get github.com/pkg/errors` + +The traditional error handling idiom in Go is roughly akin to +```go +if err != nil { + return err +} +``` +which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error. + +## Adding context to an error + +The errors.Wrap function returns a new error that adds context to the original error. For example +```go +_, err := ioutil.ReadAll(r) +if err != nil { + return errors.Wrap(err, "read failed") +} +``` +## Retrieving the cause of an error + +Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`. +```go +type causer interface { + Cause() error +} +``` +`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example: +```go +switch err := errors.Cause(err).(type) { +case *MyError: + // handle specifically +default: + // unknown error +} +``` + +[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors). + +## Contributing + +We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high. + +Before proposing a change, please discuss your change by raising an issue. + +## Licence + +BSD-2-Clause diff --git a/vendor/github.com/pkg/errors/appveyor.yml b/vendor/github.com/pkg/errors/appveyor.yml new file mode 100644 index 000000000..a932eade0 --- /dev/null +++ b/vendor/github.com/pkg/errors/appveyor.yml @@ -0,0 +1,32 @@ +version: build-{build}.{branch} + +clone_folder: C:\gopath\src\github.com\pkg\errors +shallow_clone: true # for startup speed + +environment: + GOPATH: C:\gopath + +platform: + - x64 + +# http://www.appveyor.com/docs/installed-software +install: + # some helpful output for debugging builds + - go version + - go env + # pre-installed MinGW at C:\MinGW is 32bit only + # but MSYS2 at C:\msys64 has mingw64 + - set PATH=C:\msys64\mingw64\bin;%PATH% + - gcc --version + - g++ --version + +build_script: + - go install -v ./... + +test_script: + - set PATH=C:\gopath\bin;%PATH% + - go test -v ./... + +#artifacts: +# - path: '%GOPATH%\bin\*.exe' +deploy: off diff --git a/vendor/github.com/pkg/errors/bench_test.go b/vendor/github.com/pkg/errors/bench_test.go new file mode 100644 index 000000000..0416a3cbb --- /dev/null +++ b/vendor/github.com/pkg/errors/bench_test.go @@ -0,0 +1,59 @@ +// +build go1.7 + +package errors + +import ( + "fmt" + "testing" + + stderrors "errors" +) + +func noErrors(at, depth int) error { + if at >= depth { + return stderrors.New("no error") + } + return noErrors(at+1, depth) +} +func yesErrors(at, depth int) error { + if at >= depth { + return New("ye error") + } + return yesErrors(at+1, depth) +} + +func BenchmarkErrors(b *testing.B) { + var toperr error + type run struct { + stack int + std bool + } + runs := []run{ + {10, false}, + {10, true}, + {100, false}, + {100, true}, + {1000, false}, + {1000, true}, + } + for _, r := range runs { + part := "pkg/errors" + if r.std { + part = "errors" + } + name := fmt.Sprintf("%s-stack-%d", part, r.stack) + b.Run(name, func(b *testing.B) { + var err error + f := yesErrors + if r.std { + f = noErrors + } + b.ReportAllocs() + for i := 0; i < b.N; i++ { + err = f(0, r.stack) + } + b.StopTimer() + toperr = err + }) + } +} diff --git a/vendor/github.com/pkg/errors/errors.go b/vendor/github.com/pkg/errors/errors.go new file mode 100644 index 000000000..842ee8045 --- /dev/null +++ b/vendor/github.com/pkg/errors/errors.go @@ -0,0 +1,269 @@ +// Package errors provides simple error handling primitives. +// +// The traditional error handling idiom in Go is roughly akin to +// +// if err != nil { +// return err +// } +// +// which applied recursively up the call stack results in error reports +// without context or debugging information. The errors package allows +// programmers to add context to the failure path in their code in a way +// that does not destroy the original value of the error. +// +// Adding context to an error +// +// The errors.Wrap function returns a new error that adds context to the +// original error by recording a stack trace at the point Wrap is called, +// and the supplied message. For example +// +// _, err := ioutil.ReadAll(r) +// if err != nil { +// return errors.Wrap(err, "read failed") +// } +// +// If additional control is required the errors.WithStack and errors.WithMessage +// functions destructure errors.Wrap into its component operations of annotating +// an error with a stack trace and an a message, respectively. +// +// Retrieving the cause of an error +// +// Using errors.Wrap constructs a stack of errors, adding context to the +// preceding error. Depending on the nature of the error it may be necessary +// to reverse the operation of errors.Wrap to retrieve the original error +// for inspection. Any error value which implements this interface +// +// type causer interface { +// Cause() error +// } +// +// can be inspected by errors.Cause. errors.Cause will recursively retrieve +// the topmost error which does not implement causer, which is assumed to be +// the original cause. For example: +// +// switch err := errors.Cause(err).(type) { +// case *MyError: +// // handle specifically +// default: +// // unknown error +// } +// +// causer interface is not exported by this package, but is considered a part +// of stable public API. +// +// Formatted printing of errors +// +// All error values returned from this package implement fmt.Formatter and can +// be formatted by the fmt package. The following verbs are supported +// +// %s print the error. If the error has a Cause it will be +// printed recursively +// %v see %s +// %+v extended format. Each Frame of the error's StackTrace will +// be printed in detail. +// +// Retrieving the stack trace of an error or wrapper +// +// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are +// invoked. This information can be retrieved with the following interface. +// +// type stackTracer interface { +// StackTrace() errors.StackTrace +// } +// +// Where errors.StackTrace is defined as +// +// type StackTrace []Frame +// +// The Frame type represents a call site in the stack trace. Frame supports +// the fmt.Formatter interface that can be used for printing information about +// the stack trace of this error. For example: +// +// if err, ok := err.(stackTracer); ok { +// for _, f := range err.StackTrace() { +// fmt.Printf("%+s:%d", f) +// } +// } +// +// stackTracer interface is not exported by this package, but is considered a part +// of stable public API. +// +// See the documentation for Frame.Format for more details. +package errors + +import ( + "fmt" + "io" +) + +// New returns an error with the supplied message. +// New also records the stack trace at the point it was called. +func New(message string) error { + return &fundamental{ + msg: message, + stack: callers(), + } +} + +// Errorf formats according to a format specifier and returns the string +// as a value that satisfies error. +// Errorf also records the stack trace at the point it was called. +func Errorf(format string, args ...interface{}) error { + return &fundamental{ + msg: fmt.Sprintf(format, args...), + stack: callers(), + } +} + +// fundamental is an error that has a message and a stack, but no caller. +type fundamental struct { + msg string + *stack +} + +func (f *fundamental) Error() string { return f.msg } + +func (f *fundamental) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + io.WriteString(s, f.msg) + f.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, f.msg) + case 'q': + fmt.Fprintf(s, "%q", f.msg) + } +} + +// WithStack annotates err with a stack trace at the point WithStack was called. +// If err is nil, WithStack returns nil. +func WithStack(err error) error { + if err == nil { + return nil + } + return &withStack{ + err, + callers(), + } +} + +type withStack struct { + error + *stack +} + +func (w *withStack) Cause() error { return w.error } + +func (w *withStack) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v", w.Cause()) + w.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, w.Error()) + case 'q': + fmt.Fprintf(s, "%q", w.Error()) + } +} + +// Wrap returns an error annotating err with a stack trace +// at the point Wrap is called, and the supplied message. +// If err is nil, Wrap returns nil. +func Wrap(err error, message string) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: message, + } + return &withStack{ + err, + callers(), + } +} + +// Wrapf returns an error annotating err with a stack trace +// at the point Wrapf is call, and the format specifier. +// If err is nil, Wrapf returns nil. +func Wrapf(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } + return &withStack{ + err, + callers(), + } +} + +// WithMessage annotates err with a new message. +// If err is nil, WithMessage returns nil. +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: message, + } +} + +type withMessage struct { + cause error + msg string +} + +func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } +func (w *withMessage) Cause() error { return w.cause } + +func (w *withMessage) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v\n", w.Cause()) + io.WriteString(s, w.msg) + return + } + fallthrough + case 's', 'q': + io.WriteString(s, w.Error()) + } +} + +// Cause returns the underlying cause of the error, if possible. +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement Cause, the original error will +// be returned. If the error is nil, nil will be returned without further +// investigation. +func Cause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + cause, ok := err.(causer) + if !ok { + break + } + err = cause.Cause() + } + return err +} diff --git a/vendor/github.com/pkg/errors/errors_test.go b/vendor/github.com/pkg/errors/errors_test.go new file mode 100644 index 000000000..1d8c63558 --- /dev/null +++ b/vendor/github.com/pkg/errors/errors_test.go @@ -0,0 +1,226 @@ +package errors + +import ( + "errors" + "fmt" + "io" + "reflect" + "testing" +) + +func TestNew(t *testing.T) { + tests := []struct { + err string + want error + }{ + {"", fmt.Errorf("")}, + {"foo", fmt.Errorf("foo")}, + {"foo", New("foo")}, + {"string with format specifiers: %v", errors.New("string with format specifiers: %v")}, + } + + for _, tt := range tests { + got := New(tt.err) + if got.Error() != tt.want.Error() { + t.Errorf("New.Error(): got: %q, want %q", got, tt.want) + } + } +} + +func TestWrapNil(t *testing.T) { + got := Wrap(nil, "no error") + if got != nil { + t.Errorf("Wrap(nil, \"no error\"): got %#v, expected nil", got) + } +} + +func TestWrap(t *testing.T) { + tests := []struct { + err error + message string + want string + }{ + {io.EOF, "read error", "read error: EOF"}, + {Wrap(io.EOF, "read error"), "client error", "client error: read error: EOF"}, + } + + for _, tt := range tests { + got := Wrap(tt.err, tt.message).Error() + if got != tt.want { + t.Errorf("Wrap(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want) + } + } +} + +type nilError struct{} + +func (nilError) Error() string { return "nil error" } + +func TestCause(t *testing.T) { + x := New("error") + tests := []struct { + err error + want error + }{{ + // nil error is nil + err: nil, + want: nil, + }, { + // explicit nil error is nil + err: (error)(nil), + want: nil, + }, { + // typed nil is nil + err: (*nilError)(nil), + want: (*nilError)(nil), + }, { + // uncaused error is unaffected + err: io.EOF, + want: io.EOF, + }, { + // caused error returns cause + err: Wrap(io.EOF, "ignored"), + want: io.EOF, + }, { + err: x, // return from errors.New + want: x, + }, { + WithMessage(nil, "whoops"), + nil, + }, { + WithMessage(io.EOF, "whoops"), + io.EOF, + }, { + WithStack(nil), + nil, + }, { + WithStack(io.EOF), + io.EOF, + }} + + for i, tt := range tests { + got := Cause(tt.err) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("test %d: got %#v, want %#v", i+1, got, tt.want) + } + } +} + +func TestWrapfNil(t *testing.T) { + got := Wrapf(nil, "no error") + if got != nil { + t.Errorf("Wrapf(nil, \"no error\"): got %#v, expected nil", got) + } +} + +func TestWrapf(t *testing.T) { + tests := []struct { + err error + message string + want string + }{ + {io.EOF, "read error", "read error: EOF"}, + {Wrapf(io.EOF, "read error without format specifiers"), "client error", "client error: read error without format specifiers: EOF"}, + {Wrapf(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"}, + } + + for _, tt := range tests { + got := Wrapf(tt.err, tt.message).Error() + if got != tt.want { + t.Errorf("Wrapf(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want) + } + } +} + +func TestErrorf(t *testing.T) { + tests := []struct { + err error + want string + }{ + {Errorf("read error without format specifiers"), "read error without format specifiers"}, + {Errorf("read error with %d format specifier", 1), "read error with 1 format specifier"}, + } + + for _, tt := range tests { + got := tt.err.Error() + if got != tt.want { + t.Errorf("Errorf(%v): got: %q, want %q", tt.err, got, tt.want) + } + } +} + +func TestWithStackNil(t *testing.T) { + got := WithStack(nil) + if got != nil { + t.Errorf("WithStack(nil): got %#v, expected nil", got) + } +} + +func TestWithStack(t *testing.T) { + tests := []struct { + err error + want string + }{ + {io.EOF, "EOF"}, + {WithStack(io.EOF), "EOF"}, + } + + for _, tt := range tests { + got := WithStack(tt.err).Error() + if got != tt.want { + t.Errorf("WithStack(%v): got: %v, want %v", tt.err, got, tt.want) + } + } +} + +func TestWithMessageNil(t *testing.T) { + got := WithMessage(nil, "no error") + if got != nil { + t.Errorf("WithMessage(nil, \"no error\"): got %#v, expected nil", got) + } +} + +func TestWithMessage(t *testing.T) { + tests := []struct { + err error + message string + want string + }{ + {io.EOF, "read error", "read error: EOF"}, + {WithMessage(io.EOF, "read error"), "client error", "client error: read error: EOF"}, + } + + for _, tt := range tests { + got := WithMessage(tt.err, tt.message).Error() + if got != tt.want { + t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want) + } + } + +} + +// errors.New, etc values are not expected to be compared by value +// but the change in errors#27 made them incomparable. Assert that +// various kinds of errors have a functional equality operator, even +// if the result of that equality is always false. +func TestErrorEquality(t *testing.T) { + vals := []error{ + nil, + io.EOF, + errors.New("EOF"), + New("EOF"), + Errorf("EOF"), + Wrap(io.EOF, "EOF"), + Wrapf(io.EOF, "EOF%d", 2), + WithMessage(nil, "whoops"), + WithMessage(io.EOF, "whoops"), + WithStack(io.EOF), + WithStack(nil), + } + + for i := range vals { + for j := range vals { + _ = vals[i] == vals[j] // mustn't panic + } + } +} diff --git a/vendor/github.com/pkg/errors/example_test.go b/vendor/github.com/pkg/errors/example_test.go new file mode 100644 index 000000000..c1fc13e38 --- /dev/null +++ b/vendor/github.com/pkg/errors/example_test.go @@ -0,0 +1,205 @@ +package errors_test + +import ( + "fmt" + + "github.com/pkg/errors" +) + +func ExampleNew() { + err := errors.New("whoops") + fmt.Println(err) + + // Output: whoops +} + +func ExampleNew_printf() { + err := errors.New("whoops") + fmt.Printf("%+v", err) + + // Example output: + // whoops + // github.com/pkg/errors_test.ExampleNew_printf + // /home/dfc/src/github.com/pkg/errors/example_test.go:17 + // testing.runExample + // /home/dfc/go/src/testing/example.go:114 + // testing.RunExamples + // /home/dfc/go/src/testing/example.go:38 + // testing.(*M).Run + // /home/dfc/go/src/testing/testing.go:744 + // main.main + // /github.com/pkg/errors/_test/_testmain.go:106 + // runtime.main + // /home/dfc/go/src/runtime/proc.go:183 + // runtime.goexit + // /home/dfc/go/src/runtime/asm_amd64.s:2059 +} + +func ExampleWithMessage() { + cause := errors.New("whoops") + err := errors.WithMessage(cause, "oh noes") + fmt.Println(err) + + // Output: oh noes: whoops +} + +func ExampleWithStack() { + cause := errors.New("whoops") + err := errors.WithStack(cause) + fmt.Println(err) + + // Output: whoops +} + +func ExampleWithStack_printf() { + cause := errors.New("whoops") + err := errors.WithStack(cause) + fmt.Printf("%+v", err) + + // Example Output: + // whoops + // github.com/pkg/errors_test.ExampleWithStack_printf + // /home/fabstu/go/src/github.com/pkg/errors/example_test.go:55 + // testing.runExample + // /usr/lib/go/src/testing/example.go:114 + // testing.RunExamples + // /usr/lib/go/src/testing/example.go:38 + // testing.(*M).Run + // /usr/lib/go/src/testing/testing.go:744 + // main.main + // github.com/pkg/errors/_test/_testmain.go:106 + // runtime.main + // /usr/lib/go/src/runtime/proc.go:183 + // runtime.goexit + // /usr/lib/go/src/runtime/asm_amd64.s:2086 + // github.com/pkg/errors_test.ExampleWithStack_printf + // /home/fabstu/go/src/github.com/pkg/errors/example_test.go:56 + // testing.runExample + // /usr/lib/go/src/testing/example.go:114 + // testing.RunExamples + // /usr/lib/go/src/testing/example.go:38 + // testing.(*M).Run + // /usr/lib/go/src/testing/testing.go:744 + // main.main + // github.com/pkg/errors/_test/_testmain.go:106 + // runtime.main + // /usr/lib/go/src/runtime/proc.go:183 + // runtime.goexit + // /usr/lib/go/src/runtime/asm_amd64.s:2086 +} + +func ExampleWrap() { + cause := errors.New("whoops") + err := errors.Wrap(cause, "oh noes") + fmt.Println(err) + + // Output: oh noes: whoops +} + +func fn() error { + e1 := errors.New("error") + e2 := errors.Wrap(e1, "inner") + e3 := errors.Wrap(e2, "middle") + return errors.Wrap(e3, "outer") +} + +func ExampleCause() { + err := fn() + fmt.Println(err) + fmt.Println(errors.Cause(err)) + + // Output: outer: middle: inner: error + // error +} + +func ExampleWrap_extended() { + err := fn() + fmt.Printf("%+v\n", err) + + // Example output: + // error + // github.com/pkg/errors_test.fn + // /home/dfc/src/github.com/pkg/errors/example_test.go:47 + // github.com/pkg/errors_test.ExampleCause_printf + // /home/dfc/src/github.com/pkg/errors/example_test.go:63 + // testing.runExample + // /home/dfc/go/src/testing/example.go:114 + // testing.RunExamples + // /home/dfc/go/src/testing/example.go:38 + // testing.(*M).Run + // /home/dfc/go/src/testing/testing.go:744 + // main.main + // /github.com/pkg/errors/_test/_testmain.go:104 + // runtime.main + // /home/dfc/go/src/runtime/proc.go:183 + // runtime.goexit + // /home/dfc/go/src/runtime/asm_amd64.s:2059 + // github.com/pkg/errors_test.fn + // /home/dfc/src/github.com/pkg/errors/example_test.go:48: inner + // github.com/pkg/errors_test.fn + // /home/dfc/src/github.com/pkg/errors/example_test.go:49: middle + // github.com/pkg/errors_test.fn + // /home/dfc/src/github.com/pkg/errors/example_test.go:50: outer +} + +func ExampleWrapf() { + cause := errors.New("whoops") + err := errors.Wrapf(cause, "oh noes #%d", 2) + fmt.Println(err) + + // Output: oh noes #2: whoops +} + +func ExampleErrorf_extended() { + err := errors.Errorf("whoops: %s", "foo") + fmt.Printf("%+v", err) + + // Example output: + // whoops: foo + // github.com/pkg/errors_test.ExampleErrorf + // /home/dfc/src/github.com/pkg/errors/example_test.go:101 + // testing.runExample + // /home/dfc/go/src/testing/example.go:114 + // testing.RunExamples + // /home/dfc/go/src/testing/example.go:38 + // testing.(*M).Run + // /home/dfc/go/src/testing/testing.go:744 + // main.main + // /github.com/pkg/errors/_test/_testmain.go:102 + // runtime.main + // /home/dfc/go/src/runtime/proc.go:183 + // runtime.goexit + // /home/dfc/go/src/runtime/asm_amd64.s:2059 +} + +func Example_stackTrace() { + type stackTracer interface { + StackTrace() errors.StackTrace + } + + err, ok := errors.Cause(fn()).(stackTracer) + if !ok { + panic("oops, err does not implement stackTracer") + } + + st := err.StackTrace() + fmt.Printf("%+v", st[0:2]) // top two frames + + // Example output: + // github.com/pkg/errors_test.fn + // /home/dfc/src/github.com/pkg/errors/example_test.go:47 + // github.com/pkg/errors_test.Example_stackTrace + // /home/dfc/src/github.com/pkg/errors/example_test.go:127 +} + +func ExampleCause_printf() { + err := errors.Wrap(func() error { + return func() error { + return errors.Errorf("hello %s", fmt.Sprintf("world")) + }() + }(), "failed") + + fmt.Printf("%v", err) + + // Output: failed: hello world +} diff --git a/vendor/github.com/pkg/errors/format_test.go b/vendor/github.com/pkg/errors/format_test.go new file mode 100644 index 000000000..15fd7d89d --- /dev/null +++ b/vendor/github.com/pkg/errors/format_test.go @@ -0,0 +1,535 @@ +package errors + +import ( + "errors" + "fmt" + "io" + "regexp" + "strings" + "testing" +) + +func TestFormatNew(t *testing.T) { + tests := []struct { + error + format string + want string + }{{ + New("error"), + "%s", + "error", + }, { + New("error"), + "%v", + "error", + }, { + New("error"), + "%+v", + "error\n" + + "github.com/pkg/errors.TestFormatNew\n" + + "\t.+/github.com/pkg/errors/format_test.go:26", + }, { + New("error"), + "%q", + `"error"`, + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.error, tt.format, tt.want) + } +} + +func TestFormatErrorf(t *testing.T) { + tests := []struct { + error + format string + want string + }{{ + Errorf("%s", "error"), + "%s", + "error", + }, { + Errorf("%s", "error"), + "%v", + "error", + }, { + Errorf("%s", "error"), + "%+v", + "error\n" + + "github.com/pkg/errors.TestFormatErrorf\n" + + "\t.+/github.com/pkg/errors/format_test.go:56", + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.error, tt.format, tt.want) + } +} + +func TestFormatWrap(t *testing.T) { + tests := []struct { + error + format string + want string + }{{ + Wrap(New("error"), "error2"), + "%s", + "error2: error", + }, { + Wrap(New("error"), "error2"), + "%v", + "error2: error", + }, { + Wrap(New("error"), "error2"), + "%+v", + "error\n" + + "github.com/pkg/errors.TestFormatWrap\n" + + "\t.+/github.com/pkg/errors/format_test.go:82", + }, { + Wrap(io.EOF, "error"), + "%s", + "error: EOF", + }, { + Wrap(io.EOF, "error"), + "%v", + "error: EOF", + }, { + Wrap(io.EOF, "error"), + "%+v", + "EOF\n" + + "error\n" + + "github.com/pkg/errors.TestFormatWrap\n" + + "\t.+/github.com/pkg/errors/format_test.go:96", + }, { + Wrap(Wrap(io.EOF, "error1"), "error2"), + "%+v", + "EOF\n" + + "error1\n" + + "github.com/pkg/errors.TestFormatWrap\n" + + "\t.+/github.com/pkg/errors/format_test.go:103\n", + }, { + Wrap(New("error with space"), "context"), + "%q", + `"context: error with space"`, + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.error, tt.format, tt.want) + } +} + +func TestFormatWrapf(t *testing.T) { + tests := []struct { + error + format string + want string + }{{ + Wrapf(io.EOF, "error%d", 2), + "%s", + "error2: EOF", + }, { + Wrapf(io.EOF, "error%d", 2), + "%v", + "error2: EOF", + }, { + Wrapf(io.EOF, "error%d", 2), + "%+v", + "EOF\n" + + "error2\n" + + "github.com/pkg/errors.TestFormatWrapf\n" + + "\t.+/github.com/pkg/errors/format_test.go:134", + }, { + Wrapf(New("error"), "error%d", 2), + "%s", + "error2: error", + }, { + Wrapf(New("error"), "error%d", 2), + "%v", + "error2: error", + }, { + Wrapf(New("error"), "error%d", 2), + "%+v", + "error\n" + + "github.com/pkg/errors.TestFormatWrapf\n" + + "\t.+/github.com/pkg/errors/format_test.go:149", + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.error, tt.format, tt.want) + } +} + +func TestFormatWithStack(t *testing.T) { + tests := []struct { + error + format string + want []string + }{{ + WithStack(io.EOF), + "%s", + []string{"EOF"}, + }, { + WithStack(io.EOF), + "%v", + []string{"EOF"}, + }, { + WithStack(io.EOF), + "%+v", + []string{"EOF", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:175"}, + }, { + WithStack(New("error")), + "%s", + []string{"error"}, + }, { + WithStack(New("error")), + "%v", + []string{"error"}, + }, { + WithStack(New("error")), + "%+v", + []string{"error", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:189", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:189"}, + }, { + WithStack(WithStack(io.EOF)), + "%+v", + []string{"EOF", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:197", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:197"}, + }, { + WithStack(WithStack(Wrapf(io.EOF, "message"))), + "%+v", + []string{"EOF", + "message", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:205", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:205", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:205"}, + }, { + WithStack(Errorf("error%d", 1)), + "%+v", + []string{"error1", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:216", + "github.com/pkg/errors.TestFormatWithStack\n" + + "\t.+/github.com/pkg/errors/format_test.go:216"}, + }} + + for i, tt := range tests { + testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true) + } +} + +func TestFormatWithMessage(t *testing.T) { + tests := []struct { + error + format string + want []string + }{{ + WithMessage(New("error"), "error2"), + "%s", + []string{"error2: error"}, + }, { + WithMessage(New("error"), "error2"), + "%v", + []string{"error2: error"}, + }, { + WithMessage(New("error"), "error2"), + "%+v", + []string{ + "error", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:244", + "error2"}, + }, { + WithMessage(io.EOF, "addition1"), + "%s", + []string{"addition1: EOF"}, + }, { + WithMessage(io.EOF, "addition1"), + "%v", + []string{"addition1: EOF"}, + }, { + WithMessage(io.EOF, "addition1"), + "%+v", + []string{"EOF", "addition1"}, + }, { + WithMessage(WithMessage(io.EOF, "addition1"), "addition2"), + "%v", + []string{"addition2: addition1: EOF"}, + }, { + WithMessage(WithMessage(io.EOF, "addition1"), "addition2"), + "%+v", + []string{"EOF", "addition1", "addition2"}, + }, { + Wrap(WithMessage(io.EOF, "error1"), "error2"), + "%+v", + []string{"EOF", "error1", "error2", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:272"}, + }, { + WithMessage(Errorf("error%d", 1), "error2"), + "%+v", + []string{"error1", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:278", + "error2"}, + }, { + WithMessage(WithStack(io.EOF), "error"), + "%+v", + []string{ + "EOF", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:285", + "error"}, + }, { + WithMessage(Wrap(WithStack(io.EOF), "inside-error"), "outside-error"), + "%+v", + []string{ + "EOF", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:293", + "inside-error", + "github.com/pkg/errors.TestFormatWithMessage\n" + + "\t.+/github.com/pkg/errors/format_test.go:293", + "outside-error"}, + }} + + for i, tt := range tests { + testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true) + } +} + +func TestFormatGeneric(t *testing.T) { + starts := []struct { + err error + want []string + }{ + {New("new-error"), []string{ + "new-error", + "github.com/pkg/errors.TestFormatGeneric\n" + + "\t.+/github.com/pkg/errors/format_test.go:315"}, + }, {Errorf("errorf-error"), []string{ + "errorf-error", + "github.com/pkg/errors.TestFormatGeneric\n" + + "\t.+/github.com/pkg/errors/format_test.go:319"}, + }, {errors.New("errors-new-error"), []string{ + "errors-new-error"}, + }, + } + + wrappers := []wrapper{ + { + func(err error) error { return WithMessage(err, "with-message") }, + []string{"with-message"}, + }, { + func(err error) error { return WithStack(err) }, + []string{ + "github.com/pkg/errors.(func·002|TestFormatGeneric.func2)\n\t" + + ".+/github.com/pkg/errors/format_test.go:333", + }, + }, { + func(err error) error { return Wrap(err, "wrap-error") }, + []string{ + "wrap-error", + "github.com/pkg/errors.(func·003|TestFormatGeneric.func3)\n\t" + + ".+/github.com/pkg/errors/format_test.go:339", + }, + }, { + func(err error) error { return Wrapf(err, "wrapf-error%d", 1) }, + []string{ + "wrapf-error1", + "github.com/pkg/errors.(func·004|TestFormatGeneric.func4)\n\t" + + ".+/github.com/pkg/errors/format_test.go:346", + }, + }, + } + + for s := range starts { + err := starts[s].err + want := starts[s].want + testFormatCompleteCompare(t, s, err, "%+v", want, false) + testGenericRecursive(t, err, want, wrappers, 3) + } +} + +func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) { + got := fmt.Sprintf(format, arg) + gotLines := strings.SplitN(got, "\n", -1) + wantLines := strings.SplitN(want, "\n", -1) + + if len(wantLines) > len(gotLines) { + t.Errorf("test %d: wantLines(%d) > gotLines(%d):\n got: %q\nwant: %q", n+1, len(wantLines), len(gotLines), got, want) + return + } + + for i, w := range wantLines { + match, err := regexp.MatchString(w, gotLines[i]) + if err != nil { + t.Fatal(err) + } + if !match { + t.Errorf("test %d: line %d: fmt.Sprintf(%q, err):\n got: %q\nwant: %q", n+1, i+1, format, got, want) + } + } +} + +var stackLineR = regexp.MustCompile(`\.`) + +// parseBlocks parses input into a slice, where: +// - incase entry contains a newline, its a stacktrace +// - incase entry contains no newline, its a solo line. +// +// Detecting stack boundaries only works incase the WithStack-calls are +// to be found on the same line, thats why it is optionally here. +// +// Example use: +// +// for _, e := range blocks { +// if strings.ContainsAny(e, "\n") { +// // Match as stack +// } else { +// // Match as line +// } +// } +// +func parseBlocks(input string, detectStackboundaries bool) ([]string, error) { + var blocks []string + + stack := "" + wasStack := false + lines := map[string]bool{} // already found lines + + for _, l := range strings.Split(input, "\n") { + isStackLine := stackLineR.MatchString(l) + + switch { + case !isStackLine && wasStack: + blocks = append(blocks, stack, l) + stack = "" + lines = map[string]bool{} + case isStackLine: + if wasStack { + // Detecting two stacks after another, possible cause lines match in + // our tests due to WithStack(WithStack(io.EOF)) on same line. + if detectStackboundaries { + if lines[l] { + if len(stack) == 0 { + return nil, errors.New("len of block must not be zero here") + } + + blocks = append(blocks, stack) + stack = l + lines = map[string]bool{l: true} + continue + } + } + + stack = stack + "\n" + l + } else { + stack = l + } + lines[l] = true + case !isStackLine && !wasStack: + blocks = append(blocks, l) + default: + return nil, errors.New("must not happen") + } + + wasStack = isStackLine + } + + // Use up stack + if stack != "" { + blocks = append(blocks, stack) + } + return blocks, nil +} + +func testFormatCompleteCompare(t *testing.T, n int, arg interface{}, format string, want []string, detectStackBoundaries bool) { + gotStr := fmt.Sprintf(format, arg) + + got, err := parseBlocks(gotStr, detectStackBoundaries) + if err != nil { + t.Fatal(err) + } + + if len(got) != len(want) { + t.Fatalf("test %d: fmt.Sprintf(%s, err) -> wrong number of blocks: got(%d) want(%d)\n got: %s\nwant: %s\ngotStr: %q", + n+1, format, len(got), len(want), prettyBlocks(got), prettyBlocks(want), gotStr) + } + + for i := range got { + if strings.ContainsAny(want[i], "\n") { + // Match as stack + match, err := regexp.MatchString(want[i], got[i]) + if err != nil { + t.Fatal(err) + } + if !match { + t.Fatalf("test %d: block %d: fmt.Sprintf(%q, err):\ngot:\n%q\nwant:\n%q\nall-got:\n%s\nall-want:\n%s\n", + n+1, i+1, format, got[i], want[i], prettyBlocks(got), prettyBlocks(want)) + } + } else { + // Match as message + if got[i] != want[i] { + t.Fatalf("test %d: fmt.Sprintf(%s, err) at block %d got != want:\n got: %q\nwant: %q", n+1, format, i+1, got[i], want[i]) + } + } + } +} + +type wrapper struct { + wrap func(err error) error + want []string +} + +func prettyBlocks(blocks []string, prefix ...string) string { + var out []string + + for _, b := range blocks { + out = append(out, fmt.Sprintf("%v", b)) + } + + return " " + strings.Join(out, "\n ") +} + +func testGenericRecursive(t *testing.T, beforeErr error, beforeWant []string, list []wrapper, maxDepth int) { + if len(beforeWant) == 0 { + panic("beforeWant must not be empty") + } + for _, w := range list { + if len(w.want) == 0 { + panic("want must not be empty") + } + + err := w.wrap(beforeErr) + + // Copy required cause append(beforeWant, ..) modified beforeWant subtly. + beforeCopy := make([]string, len(beforeWant)) + copy(beforeCopy, beforeWant) + + beforeWant := beforeCopy + last := len(beforeWant) - 1 + var want []string + + // Merge two stacks behind each other. + if strings.ContainsAny(beforeWant[last], "\n") && strings.ContainsAny(w.want[0], "\n") { + want = append(beforeWant[:last], append([]string{beforeWant[last] + "((?s).*)" + w.want[0]}, w.want[1:]...)...) + } else { + want = append(beforeWant, w.want...) + } + + testFormatCompleteCompare(t, maxDepth, err, "%+v", want, false) + if maxDepth > 0 { + testGenericRecursive(t, err, want, list, maxDepth-1) + } + } +} diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go new file mode 100644 index 000000000..6b1f2891a --- /dev/null +++ b/vendor/github.com/pkg/errors/stack.go @@ -0,0 +1,178 @@ +package errors + +import ( + "fmt" + "io" + "path" + "runtime" + "strings" +) + +// Frame represents a program counter inside a stack frame. +type Frame uintptr + +// pc returns the program counter for this frame; +// multiple frames may have the same PC value. +func (f Frame) pc() uintptr { return uintptr(f) - 1 } + +// file returns the full path to the file that contains the +// function for this Frame's pc. +func (f Frame) file() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + file, _ := fn.FileLine(f.pc()) + return file +} + +// line returns the line number of source code of the +// function for this Frame's pc. +func (f Frame) line() int { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return 0 + } + _, line := fn.FileLine(f.pc()) + return line +} + +// Format formats the frame according to the fmt.Formatter interface. +// +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+s path of source file relative to the compile time GOPATH +// %+v equivalent to %+s:%d +func (f Frame) Format(s fmt.State, verb rune) { + switch verb { + case 's': + switch { + case s.Flag('+'): + pc := f.pc() + fn := runtime.FuncForPC(pc) + if fn == nil { + io.WriteString(s, "unknown") + } else { + file, _ := fn.FileLine(pc) + fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file) + } + default: + io.WriteString(s, path.Base(f.file())) + } + case 'd': + fmt.Fprintf(s, "%d", f.line()) + case 'n': + name := runtime.FuncForPC(f.pc()).Name() + io.WriteString(s, funcname(name)) + case 'v': + f.Format(s, 's') + io.WriteString(s, ":") + f.Format(s, 'd') + } +} + +// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). +type StackTrace []Frame + +func (st StackTrace) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case s.Flag('+'): + for _, f := range st { + fmt.Fprintf(s, "\n%+v", f) + } + case s.Flag('#'): + fmt.Fprintf(s, "%#v", []Frame(st)) + default: + fmt.Fprintf(s, "%v", []Frame(st)) + } + case 's': + fmt.Fprintf(s, "%s", []Frame(st)) + } +} + +// stack represents a stack of program counters. +type stack []uintptr + +func (s *stack) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case st.Flag('+'): + for _, pc := range *s { + f := Frame(pc) + fmt.Fprintf(st, "\n%+v", f) + } + } + } +} + +func (s *stack) StackTrace() StackTrace { + f := make([]Frame, len(*s)) + for i := 0; i < len(f); i++ { + f[i] = Frame((*s)[i]) + } + return f +} + +func callers() *stack { + const depth = 32 + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + var st stack = pcs[0:n] + return &st +} + +// funcname removes the path prefix component of a function's name reported by func.Name(). +func funcname(name string) string { + i := strings.LastIndex(name, "/") + name = name[i+1:] + i = strings.Index(name, ".") + return name[i+1:] +} + +func trimGOPATH(name, file string) string { + // Here we want to get the source file path relative to the compile time + // GOPATH. As of Go 1.6.x there is no direct way to know the compiled + // GOPATH at runtime, but we can infer the number of path segments in the + // GOPATH. We note that fn.Name() returns the function name qualified by + // the import path, which does not include the GOPATH. Thus we can trim + // segments from the beginning of the file path until the number of path + // separators remaining is one more than the number of path separators in + // the function name. For example, given: + // + // GOPATH /home/user + // file /home/user/src/pkg/sub/file.go + // fn.Name() pkg/sub.Type.Method + // + // We want to produce: + // + // pkg/sub/file.go + // + // From this we can easily see that fn.Name() has one less path separator + // than our desired output. We count separators from the end of the file + // path until it finds two more than in the function name and then move + // one character forward to preserve the initial path segment without a + // leading separator. + const sep = "/" + goal := strings.Count(name, sep) + 2 + i := len(file) + for n := 0; n < goal; n++ { + i = strings.LastIndex(file[:i], sep) + if i == -1 { + // not enough separators found, set i so that the slice expression + // below leaves file unmodified + i = -len(sep) + break + } + } + // get back to 0 or trim the leading separator + file = file[i+len(sep):] + return file +} diff --git a/vendor/github.com/pkg/errors/stack_test.go b/vendor/github.com/pkg/errors/stack_test.go new file mode 100644 index 000000000..510c27a9f --- /dev/null +++ b/vendor/github.com/pkg/errors/stack_test.go @@ -0,0 +1,292 @@ +package errors + +import ( + "fmt" + "runtime" + "testing" +) + +var initpc, _, _, _ = runtime.Caller(0) + +func TestFrameLine(t *testing.T) { + var tests = []struct { + Frame + want int + }{{ + Frame(initpc), + 9, + }, { + func() Frame { + var pc, _, _, _ = runtime.Caller(0) + return Frame(pc) + }(), + 20, + }, { + func() Frame { + var pc, _, _, _ = runtime.Caller(1) + return Frame(pc) + }(), + 28, + }, { + Frame(0), // invalid PC + 0, + }} + + for _, tt := range tests { + got := tt.Frame.line() + want := tt.want + if want != got { + t.Errorf("Frame(%v): want: %v, got: %v", uintptr(tt.Frame), want, got) + } + } +} + +type X struct{} + +func (x X) val() Frame { + var pc, _, _, _ = runtime.Caller(0) + return Frame(pc) +} + +func (x *X) ptr() Frame { + var pc, _, _, _ = runtime.Caller(0) + return Frame(pc) +} + +func TestFrameFormat(t *testing.T) { + var tests = []struct { + Frame + format string + want string + }{{ + Frame(initpc), + "%s", + "stack_test.go", + }, { + Frame(initpc), + "%+s", + "github.com/pkg/errors.init\n" + + "\t.+/github.com/pkg/errors/stack_test.go", + }, { + Frame(0), + "%s", + "unknown", + }, { + Frame(0), + "%+s", + "unknown", + }, { + Frame(initpc), + "%d", + "9", + }, { + Frame(0), + "%d", + "0", + }, { + Frame(initpc), + "%n", + "init", + }, { + func() Frame { + var x X + return x.ptr() + }(), + "%n", + `\(\*X\).ptr`, + }, { + func() Frame { + var x X + return x.val() + }(), + "%n", + "X.val", + }, { + Frame(0), + "%n", + "", + }, { + Frame(initpc), + "%v", + "stack_test.go:9", + }, { + Frame(initpc), + "%+v", + "github.com/pkg/errors.init\n" + + "\t.+/github.com/pkg/errors/stack_test.go:9", + }, { + Frame(0), + "%v", + "unknown:0", + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.Frame, tt.format, tt.want) + } +} + +func TestFuncname(t *testing.T) { + tests := []struct { + name, want string + }{ + {"", ""}, + {"runtime.main", "main"}, + {"github.com/pkg/errors.funcname", "funcname"}, + {"funcname", "funcname"}, + {"io.copyBuffer", "copyBuffer"}, + {"main.(*R).Write", "(*R).Write"}, + } + + for _, tt := range tests { + got := funcname(tt.name) + want := tt.want + if got != want { + t.Errorf("funcname(%q): want: %q, got %q", tt.name, want, got) + } + } +} + +func TestTrimGOPATH(t *testing.T) { + var tests = []struct { + Frame + want string + }{{ + Frame(initpc), + "github.com/pkg/errors/stack_test.go", + }} + + for i, tt := range tests { + pc := tt.Frame.pc() + fn := runtime.FuncForPC(pc) + file, _ := fn.FileLine(pc) + got := trimGOPATH(fn.Name(), file) + testFormatRegexp(t, i, got, "%s", tt.want) + } +} + +func TestStackTrace(t *testing.T) { + tests := []struct { + err error + want []string + }{{ + New("ooh"), []string{ + "github.com/pkg/errors.TestStackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:172", + }, + }, { + Wrap(New("ooh"), "ahh"), []string{ + "github.com/pkg/errors.TestStackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:177", // this is the stack of Wrap, not New + }, + }, { + Cause(Wrap(New("ooh"), "ahh")), []string{ + "github.com/pkg/errors.TestStackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:182", // this is the stack of New + }, + }, { + func() error { return New("ooh") }(), []string{ + `github.com/pkg/errors.(func·009|TestStackTrace.func1)` + + "\n\t.+/github.com/pkg/errors/stack_test.go:187", // this is the stack of New + "github.com/pkg/errors.TestStackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:187", // this is the stack of New's caller + }, + }, { + Cause(func() error { + return func() error { + return Errorf("hello %s", fmt.Sprintf("world")) + }() + }()), []string{ + `github.com/pkg/errors.(func·010|TestStackTrace.func2.1)` + + "\n\t.+/github.com/pkg/errors/stack_test.go:196", // this is the stack of Errorf + `github.com/pkg/errors.(func·011|TestStackTrace.func2)` + + "\n\t.+/github.com/pkg/errors/stack_test.go:197", // this is the stack of Errorf's caller + "github.com/pkg/errors.TestStackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:198", // this is the stack of Errorf's caller's caller + }, + }} + for i, tt := range tests { + x, ok := tt.err.(interface { + StackTrace() StackTrace + }) + if !ok { + t.Errorf("expected %#v to implement StackTrace() StackTrace", tt.err) + continue + } + st := x.StackTrace() + for j, want := range tt.want { + testFormatRegexp(t, i, st[j], "%+v", want) + } + } +} + +func stackTrace() StackTrace { + const depth = 8 + var pcs [depth]uintptr + n := runtime.Callers(1, pcs[:]) + var st stack = pcs[0:n] + return st.StackTrace() +} + +func TestStackTraceFormat(t *testing.T) { + tests := []struct { + StackTrace + format string + want string + }{{ + nil, + "%s", + `\[\]`, + }, { + nil, + "%v", + `\[\]`, + }, { + nil, + "%+v", + "", + }, { + nil, + "%#v", + `\[\]errors.Frame\(nil\)`, + }, { + make(StackTrace, 0), + "%s", + `\[\]`, + }, { + make(StackTrace, 0), + "%v", + `\[\]`, + }, { + make(StackTrace, 0), + "%+v", + "", + }, { + make(StackTrace, 0), + "%#v", + `\[\]errors.Frame{}`, + }, { + stackTrace()[:2], + "%s", + `\[stack_test.go stack_test.go\]`, + }, { + stackTrace()[:2], + "%v", + `\[stack_test.go:225 stack_test.go:272\]`, + }, { + stackTrace()[:2], + "%+v", + "\n" + + "github.com/pkg/errors.stackTrace\n" + + "\t.+/github.com/pkg/errors/stack_test.go:225\n" + + "github.com/pkg/errors.TestStackTraceFormat\n" + + "\t.+/github.com/pkg/errors/stack_test.go:276", + }, { + stackTrace()[:2], + "%#v", + `\[\]errors.Frame{stack_test.go:225, stack_test.go:284}`, + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.StackTrace, tt.format, tt.want) + } +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/.gitignore b/vendor/gopkg.in/tucnak/telebot.v2/.gitignore new file mode 100644 index 000000000..daf913b1b --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/.gitignore @@ -0,0 +1,24 @@ +# 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 diff --git a/vendor/gopkg.in/tucnak/telebot.v2/.travis.yml b/vendor/gopkg.in/tucnak/telebot.v2/.travis.yml new file mode 100644 index 000000000..3f745a3ac --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/.travis.yml @@ -0,0 +1,4 @@ +language: go + +env: + - TELEBOT_SECRET=107177593:AAHBJfF3nv3pZXVjXpoowVhv_KSGw56s8zo diff --git a/vendor/gopkg.in/tucnak/telebot.v2/LICENSE b/vendor/gopkg.in/tucnak/telebot.v2/LICENSE new file mode 100644 index 000000000..2965b8423 --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/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/tucnak/telebot.v2/README.md b/vendor/gopkg.in/tucnak/telebot.v2/README.md new file mode 100644 index 000000000..e0801d39a --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/README.md @@ -0,0 +1,442 @@ +# Telebot +>"I never knew creating Telegram bots could be so _sexy_!" + +[![GoDoc](https://godoc.org/gopkg.in/tucnak/telebot.v2?status.svg)](https://godoc.org/gopkg.in/tucnak/telebot.v2) +[![Travis](https://travis-ci.org/tucnak/telebot.svg?branch=v2)](https://travis-ci.org/tucnak/telebot) + +```bash +go get -u gopkg.in/tucnak/telebot.v2 +``` + +* [Overview](#overview) +* [Getting Started](#getting-started) + - [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 of the 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 ( + "time" + "log" + + tb "gopkg.in/tucnak/telebot.v2" +) + +func main() { + b, err := tb.NewBot(tb.Settings{ + Token: "TOKEN_HERE", + Poller: &tb.LongPoller{Timeout: 10 * time.Second}, + }) + + if err != nil { + log.Fatal(err) + return + } + + b.Handle("/hello", func(m *tb.Message) { + b.Send(m.Sender, "hello world") + }) + + b.Start() +} + +``` + +Simple, innit? Telebot's routing system takes care of deliviering updates +to their endpoints, so in order to get to handle any meaningful event, +all you got to do is just plug your function to one of the Telebot-provided +endpoints. You can find the full list +[here](https://godoc.org/gopkg.in/tucnak/telebot.v2#pkg-constants). + +```go +b, _ := tb.NewBot(settings) + +b.Handle(tb.OnText, func(m *tb.Message) { + // all the text messages that weren't + // captured by existing handlers +}) + +b.Handle(tb.OnPhoto, func(m *tb.Message) { + // photos only +}) + +b.Handle(tb.OnChannelPost, func (m *tb.Message) { + // channel posts only +}) + +b.Handle(tb.Query, func (q *tb.Query) { + // incoming inline queries +}) +``` + +Now there's a dozen of supported endpoints (see package consts). Let me know +if you'd like to see some endpoint or endpoint idea implemented. This system +is completely extensible, so I can introduce them without breaking +backwards-compatibity. + +## Poller +Telebot doesn't really care how you provide it with incoming updates, as long +as you set it up with a Poller: +```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{}) +} +``` + +Telegram Bot API supports long polling and webhook integration. I don't really +care about webhooks, so the only concrete Poller you'll find in the library +is the `LongPoller`. Poller means you can plug telebot into whatever existing +bot infrastructure (load balancers?) you need, if you need to. Another great thing +about pollers is that you can chain them, making some sort of middleware: +```go +poller := &tb.LongPoller{Timeout: 15 * time.Second} +spamProtected := tb.NewMiddlewarePoller(poller, func(upd *tb.Update) bool { + if upd.Message == nil { + return true + } + + if strings.Contains(upd.Message.Text, "spam") { + return false + } + + return true +}) + +bot, _ := tb.NewBot(tb.Settings{ + // ... + Poller: spamProtected, +}) + +// graceful shutdown +go func() { + <-time.After(N * time.Second) + bot.Stop() +})() + +bot.Start() // blocks until shutdown + +fmt.Println(poller.LastUpdateID) // 134237 +``` + +## 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(m *tb.Message) { + if !m.Private() { + return + } + + fmt.Println(m.Payload) // +}) +``` + +## Files +>Telegram allows files up to 20 MB in size. + +Telebot allows to both upload (from disk / by URL) and download (from Telegram) +and 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 := &tb.Audio{File: tb.FromDisk("file.ogg")} + +fmt.Println(a.OnDisk()) // true +fmt.Println(a.InCloud()) // false + +// Will upload the file from disk and send it to recipient +bot.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 +bot.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 undoubteldy the most important method in Telebot. `Send()` accepts a +`Recipient` (could be user, group or a channel) and a `Sendable`. FYI, not only +all telebot-provided media types (`Photo`, `Audio`, `Video`, etc.) are `Sendable`, +but you can create composite types of your own. As long as they satisfy `Sendable`, +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 kind 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 := &tb.Photo{File: tb.FromDisk("chicken.jpg")} +v := &tb.Video{File: tb.FromURL("http://video.mp4")} + +msgs, err := b.SendAlbum(user, tb.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", &tb.SendOptions{ + // ... +}) + +// ReplyMarkup is a part of SendOptions, +// but often it's the only option you need +b.Send(user, "text", &tb.ReplyMarkup{ + // ... +}) + +// flags: no notification && no web link preview +b.Send(user, "text", tb.Silent, tb.NoPreview) +``` + +Full list of supported option-flags you can find +[here](https://github.com/tucnak/telebot/blob/v2/options.go#L9). + +## 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 the 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 collums: 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 []tb.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 an endpoints for `Handle()`: + +```go +func main() { + b, _ := tb.NewBot(tb.Settings{...}) + + // This button will be displayed in user's + // reply keyboard. + replyBtn := tb.ReplyButton{Text: "🌕 Button #1"} + replyKeys := [][]tb.ReplyButton{ + []tb.ReplyButton{replyBtn}, + // ... + } + + // And this one — just under the message itself. + // Pressing it will cause the client to send + // the bot a callback. + // + // Make sure Unique stays unique as it has to be + // for callback routing to work. + inlineBtn := tb.InlineButton{ + Unique: "sad_moon", + Text: "🌚 Button #2", + } + inlineKeys := [][]tb.InlineButton{ + []tb.InlineButton{inlineBtn}, + // ... + } + + b.Handle(&replyBtn, func(m *tb.Message) { + // on reply button pressed + }) + + b.Handle(&inlineBtn, func(c *tb.Callback) { + // on inline button pressed (callback!) + + // always respond! + b.Respond(c, &tb.CallbackResponse{...}) + }) + + // Command: /start + b.Handle("/start", func(m *tb.Message) { + if !m.Private() { + return + } + + b.Send(m.Sender, "Hello!", &tb.ReplyMarkup{ + ReplyKeyboard: replyKeys, + InlineKeyboard: inlineKeys, + }) + }) + + b.Start() +} +``` + +## Inline mode +So if you want to handle incoming inline queries you better plug the `tb.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 how it looks like: + +```go +b.Handle(tb.OnQuery, func(q *tb.Query) { + urls := []string{ + "http://photo.jpg", + "http://photo2.jpg", + } + + results := make(tb.Results, len(urls)) // []tb.Result + for i, url := range urls { + result := &tb.PhotoResult{ + URL: url, + + // required for photos + ThumbURL: url, + } + + results[i] = result + results[i].SetResultID(strconv.Itoa(i)) // It's needed to set a unique string ID for each result + } + + err := b.Answer(q, &tb.QueryResponse{ + Results: results, + CacheTime: 60, // a minute + }) + + if err != nil { + fmt.Println(err) + } +}) +``` + +There's not much to talk about really. It also support some form of authentication +through deep-linking. For that, use fields `SwitchPMText` and `SwitchPMParameter` +of `QueryResponse`. + +# Contributing + +1. Fork it +2. Clone it: `git clone https://github.com/tucnak/telebot` +3. Create your feature branch: `git checkout -b my-new-feature` +4. Make changes and add them: `git add .` +5. Commit: `git commit -m 'Add some feature'` +6. Push: `git push origin my-new-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! + +Bitcoin: `1DkfrFvSRqgBnBuxv9BzAz83dqur5zrdTH` + +# License + +Telebot is distributed under MIT. diff --git a/vendor/gopkg.in/tucnak/telebot.v2/admin.go b/vendor/gopkg.in/tucnak/telebot.v2/admin.go new file mode 100644 index 000000000..d590d3204 --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/admin.go @@ -0,0 +1,205 @@ +package telebot + +import ( + "encoding/json" + "strconv" + "time" + + "github.com/pkg/errors" +) + +// Rights is a list of privileges available to chat members. +type Rights struct { + CanBeEdited bool `json:"can_be_edited,omitempty"` // 1 + CanChangeInfo bool `json:"can_change_info,omitempty"` // 2 + CanPostMessages bool `json:"can_post_messages,omitempty"` // 3 + CanEditMessages bool `json:"can_edit_messages,omitempty"` // 4 + CanDeleteMessages bool `json:"can_delete_messages,omitempty"` // 5 + CanInviteUsers bool `json:"can_invite_users,omitempty"` // 6 + CanRestrictMembers bool `json:"can_restrict_members,omitempty"` // 7 + CanPinMessages bool `json:"can_pin_messages,omitempty"` // 8 + CanPromoteMembers bool `json:"can_promote_members,omitempty"` // 9 + CanSendMessages bool `json:"can_send_messages,omitempty"` // 10 + CanSendMedia bool `json:"can_send_media_messages,omitempty"` // 11 + CanSendOther bool `json:"can_send_other_messages,omitempty"` // 12 + CanAddPreviews bool `json:"can_add_web_page_previews,omitempty"` // 13 +} + +// NoRights is the default Rights{} +func NoRights() Rights { return Rights{} } + +// NoRestrictions should be used when un-restricting or +// un-promoting user. +// +// member.Rights = NoRestrictions() +// bot.Restrict(chat, member) +// +func NoRestrictions() Rights { + return Rights{ + true, false, false, false, false, // 1-5 + false, false, false, false, true, // 6-10 + true, true, true} +} + +// AdminRights could be used to promote user to admin. +func AdminRights() Rights { + return Rights{ + true, true, true, true, true, // 1-5 + true, true, true, true, true, // 6-10 + true, true, true} // 11-13 +} + +// Forever is a Unixtime 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) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": member.User.Recipient(), + "until_date": strconv.FormatInt(member.RestrictedUntil, 10), + } + + respJSON, err := b.Raw("kickChatMember", params) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// Unban will unban user from chat, who would have thought eh? +func (b *Bot) Unban(chat *Chat, user *User) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": user.Recipient(), + } + + respJSON, err := b.Raw("unbanChatMember", params) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// Restrict let's 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]string{ + "chat_id": chat.Recipient(), + "user_id": member.User.Recipient(), + "until_date": strconv.FormatInt(until, 10), + } + + embedRights(params, prv) + + respJSON, err := b.Raw("restrictChatMember", params) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// 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]string{ + "chat_id": chat.Recipient(), + "user_id": member.User.Recipient(), + } + + embedRights(params, prv) + + respJSON, err := b.Raw("promoteChatMember", params) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// AdminsOf return 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(), + } + + respJSON, err := b.Raw("getChatAdministrators", params) + if err != nil { + return nil, err + } + + var resp struct { + Ok bool + Result []ChatMember + Description string `json:"description"` + } + + err = json.Unmarshal(respJSON, &resp) + if err != nil { + return nil, errors.Wrap(err, "bad response json") + } + + if !resp.Ok { + return nil, errors.Errorf("api error: %s", resp.Description) + } + + return resp.Result, nil +} + +// Len return the number of members in a chat. +func (b *Bot) Len(chat *Chat) (int, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + respJSON, err := b.Raw("getChatMembersCount", params) + if err != nil { + return 0, err + } + + var resp struct { + Ok bool + Result int + Description string `json:"description"` + } + + err = json.Unmarshal(respJSON, &resp) + if err != nil { + return 0, errors.Wrap(err, "bad response json") + } + + if !resp.Ok { + return 0, errors.Errorf("api error: %s", resp.Description) + } + + return resp.Result, nil +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/api.go b/vendor/gopkg.in/tucnak/telebot.v2/api.go new file mode 100644 index 000000000..a7303e7bf --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/api.go @@ -0,0 +1,187 @@ +package telebot + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" +) + +// Raw lets you call any method of Bot API manually. +func (b *Bot) Raw(method string, payload interface{}) ([]byte, error) { + url := fmt.Sprintf("https://api.telegram.org/bot%s/%s", b.Token, method) + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(payload); err != nil { + return []byte{}, wrapSystem(err) + } + + resp, err := b.client.Post(url, "application/json", &buf) + if err != nil { + return []byte{}, errors.Wrap(err, "http.Post failed") + } + resp.Close = true + defer resp.Body.Close() + json, err := ioutil.ReadAll(resp.Body) + if err != nil { + return []byte{}, wrapSystem(err) + } + + return json, nil +} + +func (b *Bot) sendFiles( + method string, + files map[string]string, + params map[string]string) ([]byte, error) { + // --- + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + for name, path := range files { + if err := func() error { + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + part, err := writer.CreateFormFile(name, filepath.Base(path)) + if err != nil { + return err + } + + _, err = io.Copy(part, file) + return err + } (); err != nil { + return nil, wrapSystem(err) + } + + } + + for field, value := range params { + writer.WriteField(field, value) + } + + if err := writer.Close(); err != nil { + return nil, wrapSystem(err) + } + + url := fmt.Sprintf("https://api.telegram.org/bot%s/%s", b.Token, method) + req, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, wrapSystem(err) + } + + req.Header.Add("Content-Type", writer.FormDataContentType()) + + resp, err := b.client.Do(req) + if err != nil { + return nil, errors.Wrap(err, "http.Post failed") + } + + if resp.StatusCode == http.StatusInternalServerError { + return nil, errors.New("api error: internal server error") + } + + json, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, wrapSystem(err) + } + + return json, nil +} + +func (b *Bot) sendObject(f *File, what string, params map[string]string) (*Message, error) { + sendWhat := "send" + strings.Title(what) + + if what == "videoNote" { + what = "video_note" + } + + var respJSON []byte + var err error + + if f.InCloud() { + params[what] = f.FileID + respJSON, err = b.Raw(sendWhat, params) + } else if f.FileURL != "" { + params[what] = f.FileURL + respJSON, err = b.Raw(sendWhat, params) + } else { + respJSON, err = b.sendFiles(sendWhat, + map[string]string{what: f.FileLocal}, params) + } + + if err != nil { + return nil, err + } + + return extractMsgResponse(respJSON) +} + +func (b *Bot) getMe() (*User, error) { + meJSON, err := b.Raw("getMe", nil) + if err != nil { + return nil, err + } + + var botInfo struct { + Ok bool + Result *User + Description string + } + + err = json.Unmarshal(meJSON, &botInfo) + if err != nil { + return nil, errors.Wrap(err, "bad response json") + } + + if !botInfo.Ok { + return nil, errors.Errorf("api error: %s", botInfo.Description) + } + + return botInfo.Result, nil + +} + +func (b *Bot) getUpdates(offset int, timeout time.Duration) (upd []Update, err error) { + params := map[string]string{ + "offset": strconv.Itoa(offset), + "timeout": strconv.Itoa(int(timeout / time.Second)), + } + updatesJSON, errCommand := b.Raw("getUpdates", params) + if errCommand != nil { + err = errCommand + return + + } + var updatesReceived struct { + Ok bool + Result []Update + Description string + } + + err = json.Unmarshal(updatesJSON, &updatesReceived) + if err != nil { + err = errors.Wrap(err, "bad response json") + return + } + + if !updatesReceived.Ok { + err = errors.Errorf("api error: %s", updatesReceived.Description) + return + } + + return updatesReceived.Result, nil +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/bot.go b/vendor/gopkg.in/tucnak/telebot.v2/bot.go new file mode 100644 index 000000000..49f26d676 --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/bot.go @@ -0,0 +1,1144 @@ +package telebot + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "regexp" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +// 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 + } + + bot := &Bot{ + Token: pref.Token, + Updates: make(chan Update, pref.Updates), + Poller: pref.Poller, + + handlers: make(map[string]interface{}), + stop: make(chan struct{}), + reporter: pref.Reporter, + client: client, + } + + user, err := bot.getMe() + if err != nil { + return nil, err + } + + bot.Me = user + return bot, nil +} + +// Bot represents a separate Telegram bot instance. +type Bot struct { + Me *User + Token string + Updates chan Update + Poller Poller + + handlers map[string]interface{} + reporter func(error) + stop 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 { + // Telegram token + Token string + + // Updates channel capacity + Updates int // Default: 100 + + // Poller is the provider of Updates. + Poller Poller + + // Reporter is a callback function that will get called + // on any panics recovered from endpoint handlers. + Reporter func(error) + + // HTTP Client used to make requests to telegram api + Client *http.Client +} + +// 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"` + + ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result,omitempty"` +} + +// ChosenInlineResult represents a result of an inline query that was chosen +// by the user and sent to their chat partner. +type ChosenInlineResult struct { + ResultID string `json:"result_id"` + Query string `json:"query"` + // Inline messages only! + MessageID string `json:"inline_message_id"` + + From User `json:"from"` + Location *Location `json:"location,omitempty"` +} + +// Handle lets you set the handler for some command name or +// one of the supported endpoints. +// +// Example: +// +// b.handle("/help", func (m *tb.Message) {}) +// b.handle(tb.OnEdited, func (m *tb.Message) {}) +// b.handle(tb.OnQuery, func (q *tb.Query) {}) +// +// // make a hook for one of your preserved (by-pointer) +// // inline buttons. +// b.handle(&inlineButton, func (c *tb.Callback) {}) +// +func (b *Bot) Handle(endpoint interface{}, handler interface{}) { + 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+)(\|(.+))?$`) +) + +func (b *Bot) handleCommand(m *Message, cmdName, cmdBot string) bool { + + return false +} + +// 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") + } + + stopPoller := make(chan struct{}) + + go b.Poller.Poll(b, b.Updates, stopPoller) + + for { + select { + // handle incoming updates + case upd := <-b.Updates: + b.incomingUpdate(&upd) + + // call to stop polling + case <-b.stop: + stopPoller <- struct{}{} + + // polling has stopped + case <-stopPoller: + return + } + } +} + +func (b *Bot) incomingUpdate(upd *Update) { + if upd.Message != nil { + m := upd.Message + + if m.PinnedMessage != nil { + b.handle(OnPinned, m) + return + } + + // Commands + if m.Text != "" { + // Filtering malicious messsages + if m.Text[0] == '\a' { + return + } + + match := cmdRx.FindAllStringSubmatch(m.Text, -1) + + // Command found - handle and return + if match != nil { + // Syntax: "@ " + command, botName := match[0][1], match[0][3] + m.Payload = match[0][5] + + if botName != "" && !strings.EqualFold(b.Me.Username, botName) { + return + } + + if b.handle(command, m) { + return + } + } + + // 1:1 satisfaction + if b.handle(m.Text, m) { + return + } + + // OnText + b.handle(OnText, m) + return + } + + // on media + if b.handleMedia(m) { + return + } + + // OnAddedToGroup + 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, m) + return + } + + if m.UserJoined != nil { + b.handle(OnUserJoined, m) + return + } + + if m.UsersJoined != nil { + for _, user := range m.UsersJoined { + m.UserJoined = &user + b.handle(OnUserJoined, m) + } + + return + } + + if m.UserLeft != nil { + b.handle(OnUserLeft, m) + return + } + + if m.NewGroupTitle != "" { + b.handle(OnNewGroupTitle, m) + return + } + + if m.NewGroupPhoto != nil { + b.handle(OnNewGroupPhoto, m) + return + } + + if m.GroupPhotoDeleted { + b.handle(OnGroupPhotoDeleted, m) + return + } + + if m.MigrateTo != 0 { + if handler, ok := b.handlers[OnMigration]; ok { + if handler, ok := handler.(func(int64, int64)); ok { + // i'm not 100% sure that any of the values + // won't be cached, so I pass them all in: + go func(b *Bot, handler func(int64, int64), from, to int64) { + defer b.deferDebug() + handler(from, to) + }(b, handler, m.MigrateFrom, m.MigrateTo) + + } else { + panic("telebot: migration handler is bad") + } + } + + return + } + + return + } + + if upd.EditedMessage != nil { + b.handle(OnEdited, upd.EditedMessage) + return + } + + if upd.ChannelPost != nil { + b.handle(OnChannelPost, upd.ChannelPost) + return + } + + if upd.EditedChannelPost != nil { + b.handle(OnEditedChannelPost, upd.EditedChannelPost) + return + } + + if upd.Callback != nil { + if upd.Callback.Data != "" { + data := upd.Callback.Data + + if 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 { + if handler, ok := handler.(func(*Callback)); ok { + upd.Callback.Data = payload + // i'm not 100% sure that any of the values + // won't be cached, so I pass them all in: + go func(b *Bot, handler func(*Callback), c *Callback) { + defer b.deferDebug() + handler(c) + }(b, handler, upd.Callback) + + return + } + } + + } + } + } + + if handler, ok := b.handlers[OnCallback]; ok { + if handler, ok := handler.(func(*Callback)); ok { + // i'm not 100% sure that any of the values + // won't be cached, so I pass them all in: + go func(b *Bot, handler func(*Callback), c *Callback) { + defer b.deferDebug() + handler(c) + }(b, handler, upd.Callback) + + } else { + panic("telebot: callback handler is bad") + } + } + return + } + + if upd.Query != nil { + if handler, ok := b.handlers[OnQuery]; ok { + if handler, ok := handler.(func(*Query)); ok { + // i'm not 100% sure that any of the values + // won't be cached, so I pass them all in: + go func(b *Bot, handler func(*Query), q *Query) { + defer b.deferDebug() + handler(q) + }(b, handler, upd.Query) + + } else { + panic("telebot: query handler is bad") + } + } + return + } + + if upd.ChosenInlineResult != nil { + if handler, ok := b.handlers[OnChosenInlineResult]; ok { + if handler, ok := handler.(func(*ChosenInlineResult)); ok { + // i'm not 100% sure that any of the values + // won't be cached, so I pass them all in: + go func(b *Bot, handler func(*ChosenInlineResult), + r *ChosenInlineResult) { + defer b.deferDebug() + handler(r) + }(b, handler, upd.ChosenInlineResult) + + } else { + panic("telebot: chosen inline result handler is bad") + } + } + return + } +} + +func (b *Bot) handle(end string, m *Message) bool { + handler, ok := b.handlers[end] + if !ok { + return false + } + + if handler, ok := handler.(func(*Message)); ok { + // i'm not 100% sure that any of the values + // won't be cached, so I pass them all in: + go func(b *Bot, handler func(*Message), m *Message) { + defer b.deferDebug() + handler(m) + }(b, handler, m) + + return true + } + + return false +} + +func (b *Bot) handleMedia(m *Message) bool { + if m.Photo != nil { + b.handle(OnPhoto, m) + return true + } + + if m.Voice != nil { + b.handle(OnVoice, m) + return true + } + + if m.Audio != nil { + b.handle(OnAudio, m) + return true + } + + if m.Document != nil { + b.handle(OnDocument, m) + return true + } + + if m.Sticker != nil { + b.handle(OnSticker, m) + return true + } + + if m.Video != nil { + b.handle(OnVideo, m) + return true + } + + if m.VideoNote != nil { + b.handle(OnVideoNote, m) + return true + } + + if m.Contact != nil { + b.handle(OnContact, m) + return true + } + + if m.Location != nil { + b.handle(OnLocation, m) + return true + } + + if m.Venue != nil { + b.handle(OnVenue, m) + return true + } + + return false +} + +// Stop gracefully shuts the poller down. +func (b *Bot) Stop() { + b.stop <- struct{}{} +} + +// 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{}, make sure to pass +// them by-pointer, NOT by-value, which will result in a panic. +// +// 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 shorcut flag for popular options) +// - ParseMode (HTML, Markdown, etc) +// +// This function will panic upon unsupported payloads and options! +func (b *Bot) Send(to Recipient, what interface{}, options ...interface{}) (*Message, error) { + sendOpts := extractOptions(options) + + switch object := what.(type) { + case string: + return b.sendText(to, object, sendOpts) + case Sendable: + return object.Send(b, to, sendOpts) + default: + panic("telebot: unsupported sendable") + } +} + +// SendAlbum is used when sending multiple instances of media as a single +// message (so-called album). +// +// From all existing options, it only supports telebot.Silent. +func (b *Bot) SendAlbum(to Recipient, a Album, options ...interface{}) ([]Message, error) { + media := make([]string, len(a)) + files := make(map[string]string) + + for i, x := range a { + var ( + f *File + caption string + mediaRepr, mediaType string + ) + + switch y := x.(type) { + case *Photo: + f = &y.File + mediaType = "photo" + caption = y.Caption + case *Video: + f = &y.File + mediaType = "video" + caption = y.Caption + default: + return nil, errors.Errorf("telebot: album entry #%d is not valid", i) + } + + if f.InCloud() { + mediaRepr = f.FileID + } else if f.FileURL != "" { + mediaRepr = f.FileURL + } else if f.OnDisk() { + mediaRepr = fmt.Sprintf("attach://%d", i) + files[strconv.Itoa(i)] = f.FileLocal + } else { + return nil, errors.Errorf( + "telebot: album entry #%d doesn't exist anywhere", i) + } + + jsonRepr, _ := json.Marshal(map[string]string{ + "type": mediaType, + "media": mediaRepr, + "caption": caption, + }) + + media[i] = string(jsonRepr) + } + + params := map[string]string{ + "chat_id": to.Recipient(), + "media": "[" + strings.Join(media, ",") + "]", + } + + sendOpts := extractOptions(options) + embedSendOptions(params, sendOpts) + + respJSON, err := b.sendFiles("sendMediaGroup", files, params) + if err != nil { + return nil, err + } + + var resp struct { + Ok bool + Result []Message + Description string + } + + err = json.Unmarshal(respJSON, &resp) + if err != nil { + return nil, errors.Wrap(err, "bad response json") + } + + if !resp.Ok { + return nil, errors.Errorf("api error: %s", resp.Description) + } + + for attachName, _ := range files { + i, _ := strconv.Atoi(attachName) + + var newID string + if resp.Result[i].Photo != nil { + newID = resp.Result[i].Photo.FileID + } else { + newID = resp.Result[i].Video.FileID + } + + a[i].MediaFile().FileID = newID + } + + return resp.Result, nil +} + +// Reply behaves just like Send() with an exception of "reply-to" indicator. +func (b *Bot) Reply(to *Message, what interface{}, options ...interface{}) (*Message, error) { + // This function will panic upon unsupported payloads and options! + sendOpts := extractOptions(options) + 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 unsupported payloads and options! +func (b *Bot) Forward(to Recipient, what *Message, options ...interface{}) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "from_chat_id": what.Chat.Recipient(), + "message_id": strconv.Itoa(what.ID), + } + + sendOpts := extractOptions(options) + embedSendOptions(params, sendOpts) + + respJSON, err := b.Raw("forwardMessage", params) + if err != nil { + return nil, err + } + + return extractMsgResponse(respJSON) +} + +// Edit is magic, it lets you change already sent message. +// +// Use cases: +// +// b.Edit(msg, msg.Text, newMarkup) +// b.Edit(msg, "new text", tb.ModeHTML) +// +// // Edit live location: +// b.Edit(liveMsg, tb.Location{42.1337, 69.4242}) +// +func (b *Bot) Edit(message Editable, what interface{}, options ...interface{}) (*Message, error) { + messageID, chatID := message.MessageSig() + // TODO: add support for inline messages (chatID = 0) + + params := map[string]string{} + + switch v := what.(type) { + case string: + params["text"] = v + case Location: + params["latitude"] = fmt.Sprintf("%f", v.Lat) + params["longitude"] = fmt.Sprintf("%f", v.Lng) + default: + panic("telebot: unsupported what argument") + } + + // if inline message + if chatID == 0 { + params["inline_message_id"] = messageID + } else { + params["chat_id"] = strconv.FormatInt(chatID, 10) + params["message_id"] = messageID + } + + sendOpts := extractOptions(options) + embedSendOptions(params, sendOpts) + + respJSON, err := b.Raw("editMessageText", params) + if err != nil { + return nil, err + } + + return extractMsgResponse(respJSON) +} + +// EditCaption used to edit already sent photo caption with known recepient and message id. +// +// On success, returns edited message object +func (b *Bot) EditCaption(originalMsg Editable, caption string) (*Message, error) { + messageID, chatID := originalMsg.MessageSig() + + params := map[string]string{"caption": caption} + + // if inline message + if chatID == 0 { + params["inline_message_id"] = messageID + } else { + params["chat_id"] = strconv.FormatInt(chatID, 10) + params["message_id"] = messageID + } + + respJSON, err := b.Raw("editMessageCaption", params) + if err != nil { + return nil, err + } + + return extractMsgResponse(respJSON) +} + +// Delete removes the message, including service messages, +// with the following limitations: +// +// * A message can only be deleted if it was sent less than 48 hours ago. +// * Bots can delete outgoing messages in groups and supergroups. +// * 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(message Editable) error { + messageID, chatID := message.MessageSig() + + params := map[string]string{ + "chat_id": strconv.FormatInt(chatID, 10), + "message_id": messageID, + } + + respJSON, err := b.Raw("deleteMessage", params) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// 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 recieves 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(recipient Recipient, action ChatAction) error { + params := map[string]string{ + "chat_id": recipient.Recipient(), + "action": string(action), + } + + respJSON, err := b.Raw("sendChatAction", params) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// 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, response *QueryResponse) error { + response.QueryID = query.ID + + for _, result := range response.Results { + result.Process() + } + + respJSON, err := b.Raw("answerInlineQuery", response) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// 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: +// +// bot.Respond(c) +// bot.Respond(c, response) +// +func (b *Bot) Respond(callback *Callback, responseOptional ...*CallbackResponse) error { + var response *CallbackResponse + if responseOptional == nil { + response = &CallbackResponse{} + } else { + response = responseOptional[0] + } + + response.CallbackID = callback.ID + respJSON, err := b.Raw("answerCallbackQuery", response) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// 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, + } + + respJSON, err := b.Raw("getFile", params) + if err != nil { + return File{}, err + } + + var resp struct { + Ok bool + Description string + Result File + } + + err = json.Unmarshal(respJSON, &resp) + if err != nil { + return File{}, errors.Wrap(err, "bad response json") + } + + if !resp.Ok { + return File{}, errors.Errorf("api error: %s", resp.Description) + + } + + return resp.Result, nil +} + +// Download saves the file from Telegram servers locally. +// +// Maximum file size to download is 20 MB. +func (b *Bot) Download(f *File, localFilename string) error { + g, err := b.FileByID(f.FileID) + if err != nil { + return err + } + + url := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", + b.Token, g.FilePath) + + out, err := os.Create(localFilename) + if err != nil { + return wrapSystem(err) + } + defer out.Close() + + resp, err := http.Get(url) + if err != nil { + return wrapSystem(err) + } + defer resp.Body.Close() + + _, err = io.Copy(out, resp.Body) + if err != nil { + return wrapSystem(err) + } + + g.FileLocal = localFilename + *f = g + + return nil +} + +// StopLiveLocation should be called to stop broadcasting live message location +// before Location.LivePeriod expires. +// +// It supports telebot.ReplyMarkup. +func (b *Bot) StopLiveLocation(message Editable, options ...interface{}) (*Message, error) { + messageID, chatID := message.MessageSig() + + params := map[string]string{ + "chat_id": fmt.Sprintf("%d", chatID), + "message_id": messageID, + } + + sendOpts := extractOptions(options) + embedSendOptions(params, sendOpts) + + respJSON, err := b.Raw("stopMessageLiveLocation", params) + if err != nil { + return nil, err + } + + return extractMsgResponse(respJSON) +} + +// GetInviteLink should be used to export chat's invite link. +func (b *Bot) GetInviteLink(chat *Chat) (string, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + respJSON, err := b.Raw("exportChatInviteLink", params) + if err != nil { + return "", err + } + + var resp struct { + Ok bool + Description string + Result string + } + + err = json.Unmarshal(respJSON, &resp) + if err != nil { + return "", errors.Wrap(err, "bad response json") + } + + if !resp.Ok { + return "", errors.Errorf("api error: %s", resp.Description) + } + + return resp.Result, nil +} + +// SetChatTitle should be used to update group title. +func (b *Bot) SetGroupTitle(chat *Chat, newTitle string) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "title": newTitle, + } + + respJSON, err := b.Raw("setChatTitle", params) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// SetGroupDescription should be used to update group title. +func (b *Bot) SetGroupDescription(chat *Chat, description string) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + "description": description, + } + + respJSON, err := b.Raw("setChatDescription", params) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// 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(), + } + + respJSON, err := b.sendFiles("setChatPhoto", + map[string]string{"photo": p.FileLocal}, params) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// 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, + } + + respJSON, err := b.Raw("setChatStickerSet", params) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// DeleteGroupPhoto should be used to just remove group photo. +func (b *Bot) DeleteGroupPhoto(chat *Chat) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + respJSON, err := b.Raw("deleteGroupPhoto", params) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// 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(), + } + + respJSON, err := b.Raw("deleteChatStickerSet", params) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// Leave makes bot leave a group, supergroup or channel. +func (b *Bot) Leave(chat *Chat) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + respJSON, err := b.Raw("leaveChat", params) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// Use this method to pin a message in a supergroup or a channel. +// +// It supports telebot.Silent option. +func (b *Bot) Pin(message Editable, options ...interface{}) error { + messageID, chatID := message.MessageSig() + + params := map[string]string{ + "chat_id": strconv.FormatInt(chatID, 10), + "message_id": messageID, + } + + sendOpts := extractOptions(options) + embedSendOptions(params, sendOpts) + + respJSON, err := b.Raw("pinChatMessage", params) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// Use this method to unpin a message in a supergroup or a channel. +// +// It supports telebot.Silent option. +func (b *Bot) Unpin(chat *Chat) error { + params := map[string]string{ + "chat_id": chat.Recipient(), + } + + respJSON, err := b.Raw("unpinChatMessage", params) + if err != nil { + return err + } + + return extractOkResponse(respJSON) +} + +// 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. +// +// Returns a Chat object on success. +func (b *Bot) ChatByID(id string) (*Chat, error) { + params := map[string]string{ + "chat_id": id, + } + + respJSON, err := b.Raw("getChat", params) + if err != nil { + return nil, err + } + + var resp struct { + Ok bool + Description string + Result *Chat + } + + err = json.Unmarshal(respJSON, &resp) + if err != nil { + return nil, errors.Wrap(err, "bad response json") + } + + if !resp.Ok { + return nil, errors.Errorf("api error: %s", resp.Description) + } + + if resp.Result.Type == ChatChannel && resp.Result.Username == "" { + //Channel is Private + resp.Result.Type = ChatChannelPrivate + } + + return resp.Result, nil +} + +// ProfilePhotosOf return list of profile pictures for a user. +func (b *Bot) ProfilePhotosOf(user *User) ([]Photo, error) { + params := map[string]string{ + "user_id": user.Recipient(), + } + + respJSON, err := b.Raw("getUserProfilePhotos", params) + if err != nil { + return nil, err + } + + var resp struct { + Ok bool + Result struct { + Count int `json:"total_count"` + Photos []Photo `json:"photos"` + } + + Description string `json:"description"` + } + + err = json.Unmarshal(respJSON, &resp) + if err != nil { + return nil, errors.Wrap(err, "bad response json") + } + + if !resp.Ok { + return nil, errors.Errorf("api error: %s", resp.Description) + } + + return resp.Result.Photos, nil +} + +// ChatMemberOf return information about a member of a chat. +// +// Returns a ChatMember object on success. +func (b *Bot) ChatMemberOf(chat *Chat, user *User) (*ChatMember, error) { + params := map[string]string{ + "chat_id": chat.Recipient(), + "user_id": user.Recipient(), + } + + respJSON, err := b.Raw("getChatMember", params) + if err != nil { + return nil, err + } + + var resp struct { + Ok bool + Result *ChatMember + Description string `json:"description"` + } + + err = json.Unmarshal(respJSON, &resp) + if err != nil { + return nil, errors.Wrap(err, "bad response json") + } + + if !resp.Ok { + return nil, errors.Errorf("api error: %s", resp.Description) + } + + return resp.Result, nil +} + +// FileURLByID returns direct url for files using FileId which you can get from File object +func (b *Bot) FileURLByID(fileID string) (string, error) { + f, err := b.FileByID(fileID) + if err != nil { + return "", err + } + return "https://api.telegram.org/file/bot" + b.Token + "/" + f.FilePath, nil +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/callbacks.go b/vendor/gopkg.in/tucnak/telebot.v2/callbacks.go new file mode 100644 index 000000000..86567ade3 --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/callbacks.go @@ -0,0 +1,81 @@ +package telebot + +// 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"` +} + +// CallbackResponse builds a response to a Callback query. +// +// See also: https://core.telegram.org/bots/api#answerCallbackQuery +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"` + + Action func(*Callback) `json:"-"` +} + +// CallbackUnique returns InlineButto.Unique. +func (t *InlineButton) CallbackUnique() string { + return "\f" + t.Unique +} + +// CallbackUnique returns KeyboardButton.Text. +func (t *ReplyButton) CallbackUnique() string { + return t.Text +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/chat.go b/vendor/gopkg.in/tucnak/telebot.v2/chat.go new file mode 100644 index 000000000..f8ef81c39 --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/chat.go @@ -0,0 +1,58 @@ +package telebot + +import "strconv" + +// User object represents a Telegram user, bot +type User struct { + ID int `json:"id"` + + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Username string `json:"username"` +} + +// Recipient returns user ID (see Recipient interface). +func (u *User) Recipient() string { + return strconv.Itoa(u.ID) +} + +// Chat object represents a Telegram user, bot, group or a channel. +type Chat struct { + ID int64 `json:"id"` + + // See telebot.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"` +} + +// Recipient returns chat ID (see Recipient interface). +func (c *Chat) Recipient() string { + if c.Type == ChatChannel { + return "@" + c.Username + } + 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"` + + // 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 tb.Forever(). + // + RestrictedUntil int64 `json:"until_date,omitempty"` +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/editable.go b/vendor/gopkg.in/tucnak/telebot.v2/editable.go new file mode 100644 index 000000000..f00a95f77 --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/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 collums: 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/tucnak/telebot.v2/file.go b/vendor/gopkg.in/tucnak/telebot.v2/file.go new file mode 100644 index 000000000..eaef457fc --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/file.go @@ -0,0 +1,70 @@ +package telebot + +import ( + "os" +) + +// File object represents any sort of file. +type File struct { + FileID string `json:"file_id"` + FileSize int `json:"file_size"` + + // file on telegram server https://core.telegram.org/bots/api#file + FilePath string `json:"file_path"` + + // file on local file system. + FileLocal string `json:"file_local"` + + // file on the internet + FileURL string `json:"file_url"` +} + +// 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 := &tb.Photo{File: tb.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 := &tb.Photo{File: tb.FromURL("https://site.com/picture.jpg")} +// +func FromURL(url string) File { + return File{FileURL: url} +} + +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 { + if _, err := os.Stat(f.FileLocal); err != nil { + return false + } + + return true +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/filters.go b/vendor/gopkg.in/tucnak/telebot.v2/filters.go new file mode 100644 index 000000000..7384fffab --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/filters.go @@ -0,0 +1,66 @@ +package telebot + +// Filter is some thing that does filtering for +// incoming updates. +// +// Return false if you wish to sieve the update out. +type Filter interface { + Filter(*Update) bool +} + +// FilterFunc is basically a lightweight version of Filter. +type FilterFunc func(*Update) bool + +func NewChain(parent Poller) *Chain { + c := &Chain{} + c.Poller = parent + c.Filter = func(upd *Update) bool { + for _, filter := range c.Filters { + switch f := filter.(type) { + case Filter: + if !f.Filter(upd) { + return false + } + + case FilterFunc: + if !f(upd) { + return false + } + + case func(*Update) bool: + if !f(upd) { + return false + } + } + + } + + return true + } + + return c +} + +// Chain is a chain of middle +type Chain struct { + MiddlewarePoller + + // (Filter | FilterFunc | func(*Update) bool) + Filters []interface{} +} + +// Add accepts either Filter interface or FilterFunc +func (c *Chain) Add(filter interface{}) { + switch filter.(type) { + case Filter: + break + case FilterFunc: + break + case func(*Update) bool: + break + default: + panic("telebot: unsupported filter type") + } + + c.Filters = append(c.Filters, filter) +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/inline.go b/vendor/gopkg.in/tucnak/telebot.v2/inline.go new file mode 100644 index 000000000..e7630a4bf --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/inline.go @@ -0,0 +1,120 @@ +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. + From 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"` +} + +// QueryResponse builds a response to an inline Query. +// See also: https://core.telegram.org/bots/api#answerinlinequery +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"` +} + +// Result represents one result of an inline query. +type Result interface { + ResultID() string + SetResultID(string) + Process() +} + +// 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("result %v is not supported", result) + } + + return nil +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/inline_types.go b/vendor/gopkg.in/tucnak/telebot.v2/inline_types.go new file mode 100644 index 000000000..0582b51c6 --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/inline_types.go @@ -0,0 +1,309 @@ +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. Content of the message to be sent. + Content *InputMessageContent `json:"input_message_content,omitempty"` + + // Optional. Inline keyboard attached to the message. + ReplyMarkup *InlineKeyboardMarkup `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 +} + +func (r *ResultBase) Process() { + if r.ReplyMarkup != nil { + processButtons(r.ReplyMarkup.InlineKeyboard) + } +} + +// ArticleResult represents a link to an article or web page. +// See also: https://core.telegram.org/bots/api#inlinequeryresultarticle +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"` +} + +// 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"` + + // If Cache != "", it'll be used instead + Cache string `json:"audio_file_id,omitempty"` +} + +// ContentResult represents a contact with a phone number. +// See also: https://core.telegram.org/bots/api#inlinequeryresultcontact +type ContactResult struct { + ResultBase + + // 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"` + + // Optional. URL of the thumbnail for the result. + ThumbURL string `json:"thumb_url,omitempty"` +} + +// DocumentResult represents a link to a file. +// See also: https://core.telegram.org/bots/api#inlinequeryresultdocument +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"` + + // If Cache != "", it'll be used instead + Cache string `json:"document_file_id,omitempty"` +} + +// GifResult represents a link to an animated GIF file. +// See also: https://core.telegram.org/bots/api#inlinequeryresultgif +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. 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"` + + // URL of the static thumbnail for the result (jpeg or gif). + ThumbURL string `json:"thumb_url"` + + // If Cache != "", it'll be used instead + Cache string `json:"gif_file_id,omitempty"` +} + +// LocationResult represents a location on a map. +// See also: https://core.telegram.org/bots/api#inlinequeryresultlocation +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"` +} + +// ResultMpeg4Gif represents a link to a video animation +// (H.264/MPEG-4 AVC video without sound). +// See also: https://core.telegram.org/bots/api#inlinequeryresultmpeg4gif +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"` + + // URL of the static thumbnail (jpeg or gif) for the result. + ThumbURL string `json:"thumb_url,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"` +} + +// ResultResult represents a link to a photo. +// See also: https://core.telegram.org/bots/api#inlinequeryresultphoto +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. +// See also: https://core.telegram.org/bots/api#inlinequeryresultvenue +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"` +} + +// VideoResult represents a link to a page containing an embedded +// video player or a video file. +// See also: https://core.telegram.org/bots/api#inlinequeryresultvideo +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. +// +// See also: https://core.telegram.org/bots/api#inlinequeryresultvoice +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"` + + // 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/tucnak/telebot.v2/input_types.go b/vendor/gopkg.in/tucnak/telebot.v2/input_types.go new file mode 100644 index 000000000..3bf85bff9 --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/input_types.go @@ -0,0 +1,78 @@ +package telebot + +// InputMessageContent objects represent the content of a message to be sent +// as a result of an inline query. +// See also: https://core.telegram.org/bots/api#inputmessagecontent +type InputMessageContent interface { + IsInputMessageContent() bool +} + +// InputTextMessageContent represents the content of a text message to be +// sent as the result of an inline query. +// See also: https://core.telegram.org/bots/api#inputtextmessagecontent +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. +// See also: https://core.telegram.org/bots/api#inputlocationmessagecontent +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. +// See also: https://core.telegram.org/bots/api#inputvenuemessagecontent +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. +// See also: https://core.telegram.org/bots/api#inputcontactmessagecontent +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/tucnak/telebot.v2/media.go b/vendor/gopkg.in/tucnak/telebot.v2/media.go new file mode 100644 index 000000000..4d4ec93c4 --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/media.go @@ -0,0 +1,176 @@ +package telebot + +import ( + "encoding/json" +) + +// Album lets you group multiple media (so-called InputMedia) +// into a single messsage. +// +// On older clients albums look like N regular messages. +type Album []InputMedia + +// InputMedia is a generic type for all kinds of media you +// can put into an album. +type InputMedia interface { + // As some files must be uploaded (instead of referencing) + // outer layers of Telebot require it. + MediaFile() *File +} + +// Photo object represents a single photo file. +type Photo struct { + File + + Width int `json:"width"` + Height int `json:"height"` + + // (Optional) + Caption string `json:"caption,omitempty"` +} + +type photoSize struct { + File + Width int `json:"width"` + Height int `json:"height"` + Caption string `json:"caption,omitempty"` +} + +// MediaFile returns &Photo.File +func (p *Photo) MediaFile() *File { + return &p.File +} + +// 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(jsonStr []byte) error { + var hq photoSize + + if jsonStr[0] == '{' { + if err := json.Unmarshal(jsonStr, &hq); err != nil { + return err + } + } else { + var sizes []photoSize + + if err := json.Unmarshal(jsonStr, &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 of the recording in seconds as defined by sender. + Duration int `json:"duration,omitempty"` + + // (Optional) + Caption string `json:"caption,omitempty"` + Title string `json:"title,omitempty"` + Performer string `json:"performer,omitempty"` + MIME string `json:"mime_type,omitempty"` +} + +// 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 + + // Original filename as defined by sender. + FileName string `json:"file_name"` + + // (Optional) + Thumbnail *Photo `json:"thumb,omitempty"` + Caption string `json:"caption,omitempty"` + MIME string `json:"mime_type"` +} + +// 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"` + MIME string `json:"mime_type,omitempty"` +} + +// MediaFile returns &Video.File +func (v *Video) MediaFile() *File { + return &v.File +} + +// Voice object represents a voice note. +type Voice struct { + File + + // Duration of the recording in seconds as defined by sender. + Duration int `json:"duration"` + + // (Optional) + MIME string `json:"mime_type,omitempty"` +} + +// VideoNote represents a video message (available in Telegram apps +// as of v.4.0). +type VideoNote struct { + File + + // Duration of the recording in seconds as defined by sender. + Duration int `json:"duration"` + + // (Optional) + Thumbnail *Photo `json:"thumb,omitempty"` +} + +// 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 int `json:"user_id,omitempty"` +} + +// Location object represents geographic position. +type Location struct { + // Latitude + Lat float32 `json:"latitude"` + // Longitude + Lng float32 `json:"longitude"` + + // 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"` +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/message.go b/vendor/gopkg.in/tucnak/telebot.v2/message.go new file mode 100644 index 000000000..4b87d50a2 --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/message.go @@ -0,0 +1,267 @@ +package telebot + +import ( + "strconv" + "time" +) + +// 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"` + + // 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, unixtime of the original message. + OriginalUnixtime int `json:"forward_date"` + + // 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"` + + // (Optional) Time of last edit in Unix + LastEdit int64 `json:"edit_date"` + + // 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 []MessageEntity `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 []MessageEntity `json:"caption_entities,omitempty"` + + // For an audio recording, information about it. + Audio *Audio `json:"audio"` + + // For a gneral 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 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 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 recieve 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 super group has been created. + // + // You would recieve 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 recieve 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 (super group) you + // migrated to. + // + // You would recieve such a message when your chat has migrated + // to a super group. + // + // 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 recieve such a message when your chat has migrated + // to a super group. + // + // 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"` +} + +// 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"` +} + +// 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 super group. +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 +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/options.go b/vendor/gopkg.in/tucnak/telebot.v2/options.go new file mode 100644 index 000000000..bacd66fe9 --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/options.go @@ -0,0 +1,146 @@ +package telebot + +// Option is a shorcut 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 + + // ForceReply = ReplyMarkup.ForceReply + ForceReply + + // OneTimeKeyboard = ReplyMarkup.OneTimeKeyboard + OneTimeKeyboard +) + +// 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 +} + +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. + ResizeReplyKeyboard 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. + // + // Dafaults to false. + ReplyKeyboardRemove 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"` +} + +func (og *ReplyMarkup) copy() *ReplyMarkup { + cp := *og + + cp.ReplyKeyboard = make([][]ReplyButton, len(og.ReplyKeyboard)) + for i, row := range og.ReplyKeyboard { + cp.ReplyKeyboard[i] = make([]ReplyButton, len(row)) + for j, btn := range row { + cp.ReplyKeyboard[i][j] = btn + } + } + + cp.InlineKeyboard = make([][]InlineButton, len(og.InlineKeyboard)) + for i, row := range og.InlineKeyboard { + cp.InlineKeyboard[i] = make([]InlineButton, len(row)) + for j, btn := range row { + cp.InlineKeyboard[i][j] = btn + } + } + + 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. +// (Available in private chats only.) +type ReplyButton struct { + Text string `json:"text"` + + Contact bool `json:"request_contact,omitempty"` + Location bool `json:"request_location,omitempty"` + + Action func(*Callback) `json:"-"` +} + +// InlineKeyboardMarkup represents an inline keyboard that appears +// right next to the message it belongs to. +type InlineKeyboardMarkup struct { + // Array of button rows, each represented by + // an Array of KeyboardButton objects. + InlineKeyboard [][]InlineButton `json:"inline_keyboard,omitempty"` +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/poller.go b/vendor/gopkg.in/tucnak/telebot.v2/poller.go new file mode 100644 index 000000000..72a371f6a --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/poller.go @@ -0,0 +1,106 @@ +package telebot + +import ( + "time" + + "github.com/pkg/errors" +) + +var ( + ErrCouldNotUpdate = errors.New("getUpdates() failed") +) + +// 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{}) { + cap := 1 + if p.Capacity > 1 { + cap = p.Capacity + } + + middle := make(chan Update, cap) + stopPoller := make(chan struct{}) + + go p.Poller.Poll(b, middle, stopPoller) + + for { + select { + // call to stop + case <-stop: + stopPoller <- struct{}{} + + // poller is done + case <-stopPoller: + close(stop) + return + + case upd := <-middle: + if p.Filter(&upd) { + dest <- upd + } + } + } +} + +// LongPoller is a classic LongPoller with timeout. +type LongPoller struct { + Timeout time.Duration + + LastUpdateID int +} + +// Poll does long polling. +func (p *LongPoller) Poll(b *Bot, dest chan Update, stop chan struct{}) { + go func(stop chan struct{}) { + <-stop + close(stop) + }(stop) + + for { + updates, err := b.getUpdates(p.LastUpdateID+1, p.Timeout) + + if err != nil { + b.debug(ErrCouldNotUpdate) + continue + } + + for _, update := range updates { + p.LastUpdateID = update.ID + dest <- update + } + } +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/sendable.go b/vendor/gopkg.in/tucnak/telebot.v2/sendable.go new file mode 100644 index 000000000..056ffd5ce --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/sendable.go @@ -0,0 +1,197 @@ +package telebot + +import "fmt" + +// Recipient is any possible endpoint you can send +// messages to: either user, group or a channel. +type Recipient interface { + // Must return legit Telegram chat_id or username + Recipient() string +} + +// 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, + } + + embedSendOptions(params, opt) + + msg, err := b.sendObject(&p.File, "photo", params) + if err != nil { + return nil, err + } + + msg.Photo.File.stealRef(&p.File) + *p = *msg.Photo + + 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, + } + embedSendOptions(params, opt) + + msg, err := b.sendObject(&a.File, "audio", params) + if err != nil { + return nil, err + } + + msg.Audio.File.stealRef(&a.File) + *a = *msg.Audio + + 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, + } + embedSendOptions(params, opt) + + msg, err := b.sendObject(&d.File, "document", params) + if err != nil { + return nil, err + } + + msg.Document.File.stealRef(&d.File) + *d = *msg.Document + + 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(), + } + embedSendOptions(params, opt) + + msg, err := b.sendObject(&s.File, "sticker", params) + 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, + } + embedSendOptions(params, opt) + + msg, err := b.sendObject(&v.File, "video", params) + if err != nil { + return nil, err + } + + if vid := msg.Video; vid != nil { + vid.File.stealRef(&v.File) + *v = *vid + } 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 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(), + } + embedSendOptions(params, opt) + + msg, err := b.sendObject(&v.File, "voice", params) + 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(), + } + embedSendOptions(params, opt) + + msg, err := b.sendObject(&v.File, "videoNote", params) + 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": fmt.Sprintf("%d", x.LivePeriod), + } + embedSendOptions(params, opt) + + respJSON, err := b.Raw("sendLocation", params) + if err != nil { + return nil, err + } + + return extractMsgResponse(respJSON) +} + +// 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, + } + embedSendOptions(params, opt) + + respJSON, err := b.Raw("sendVenue", params) + if err != nil { + return nil, err + } + + return extractMsgResponse(respJSON) +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/stickers.go b/vendor/gopkg.in/tucnak/telebot.v2/stickers.go new file mode 100644 index 000000000..579532779 --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/stickers.go @@ -0,0 +1,23 @@ +package telebot + +// Sticker object represents a WebP image, so-called sticker. +type Sticker struct { + File + + Width int `json:"width"` + Height int `json:"height"` + + Thumbnail *Photo `json:"thumb,omitempty"` + Emoji string `json:"emoji,omitempty"` + SetName string `json:"set_name,omitempty"` + MaskPosition *MaskPosition `json:"mask_position,omitempty"` +} + +// 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"` +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/telebot.go b/vendor/gopkg.in/tucnak/telebot.v2/telebot.go new file mode 100644 index 000000000..028329f1f --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/telebot.go @@ -0,0 +1,158 @@ +// Package telebot is a framework for Telegram bots. +// +// Example: +// +// import ( +// "time" +// tb "gopkg.in/tucnak/telebot.v2" +// ) +// +// func main() { +// b, err := tb.NewBot(tb.Settings{ +// Token: "TOKEN_HERE", +// Poller: &tb.LongPoller{10 * time.Second}, +// }) +// +// if err != nil { +// return +// } +// +// b.Handle(tb.OnMessage, func(m *tb.Message) { +// b.Send(m.Sender, "hello world") +// } +// +// b.Start() +// } +// +package telebot + +// 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. + // + // Handler: func(*Message) + OnText = "\atext" + OnPhoto = "\aphoto" + OnAudio = "\aaudio" + OnDocument = "\adocument" + OnSticker = "\asticker" + OnVideo = "\avideo" + OnVoice = "\avoice" + OnVideoNote = "\avideo_note" + OnContact = "\acontact" + OnLocation = "\alocation" + OnVenue = "\avenue" + OnEdited = "\aedited" + OnPinned = "\apinned" + OnChannelPost = "\achan_post" + OnEditedChannelPost = "\achan_edited_post" + + // Will fire when bot is added to a group. + OnAddedToGroup = "\aadded_to_group" + // Group events: + OnUserJoined = "\auser_joined" + OnUserLeft = "\auser_left" + OnNewGroupTitle = "\anew_chat_title" + OnNewGroupPhoto = "\anew_chat_photo" + OnGroupPhotoDeleted = "\achat_photo_del" + + // Migration happens when group switches to + // a super group. You might want to update + // your internal references to this chat + // upon switching as its ID will change. + // + // Handler: func(from, to int64) + OnMigration = "\amigration" + + // Will fire on callback requests. + // + // Handler: func(*Callback) + OnCallback = "\acallback" + + // Will fire on incoming inline queries. + // + // Handler: func(*Query) + OnQuery = "\aquery" + + // Will fire on chosen inline results. + // + // Handler: func(*ChosenInlineResult) + OnChosenInlineResult = "\achosen_inline_result" +) + +// 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" + FindingLocation ChatAction = "find_location" +) + +// ParseMode determines the way client applications treat the text of the message +type ParseMode string + +const ( + ModeDefault ParseMode = "" + ModeMarkdown ParseMode = "Markdown" + ModeHTML ParseMode = "HTML" +) + +// EntityType is a MessageEntity type. +type EntityType string + +const ( + EntityMention EntityType = "mention" + EntityTMention EntityType = "text_mention" + EntityHashtag EntityType = "hashtag" + EntityCommand EntityType = "bot_command" + EntityURL EntityType = "url" + EntityEmail EntityType = "email" + EntityBold EntityType = "bold" + EntityItalic EntityType = "italic" + EntityCode EntityType = "code" + EntityCodeBlock EntityType = "pre" + EntityTextLink EntityType = "text_link" +) + +// 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" +) diff --git a/vendor/gopkg.in/tucnak/telebot.v2/telebot_test.go b/vendor/gopkg.in/tucnak/telebot.v2/telebot_test.go new file mode 100644 index 000000000..3bcd5c459 --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/telebot_test.go @@ -0,0 +1,60 @@ +package telebot + +import ( + "fmt" + "os" + "testing" +) + +func TestBot(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + token := os.Getenv("TELEBOT_SECRET") + if token == "" { + fmt.Println("ERROR: " + + "In order to test telebot functionality, you need to set up " + + "TELEBOT_SECRET environmental variable, which represents an API " + + "key to a Telegram bot.\n") + t.Fatal("Could't find TELEBOT_SECRET, aborting.") + } + + _, err := NewBot(Settings{Token: token}) + if err != nil { + t.Fatal("couldn't create bot:", err) + } +} + +func TestRecipient(t *testing.T) { + token := os.Getenv("TELEBOT_SECRET") + if token == "" { + fmt.Println("ERROR: " + + "In order to test telebot functionality, you need to set up " + + "TELEBOT_SECRET environmental variable, which represents an API " + + "key to a Telegram bot.\n") + t.Fatal("Could't find TELEBOT_SECRET, aborting.") + } + + bot, err := NewBot(Settings{Token: token}) + if err != nil { + t.Fatal("couldn't create bot:", err) + } + + bot.Send(&User{}, "") + bot.Send(&Chat{}, "") +} + +func TestFile(t *testing.T) { + file := FromDisk("telebot.go") + + if file.InCloud() { + t.Fatal("Newly created file can't exist on Telegram servers!") + } + + file.FileID = "magic" + + if file.FileLocal != "telebot.go" { + t.Fatal("File doesn't preserve its original filename.") + } +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/util.go b/vendor/gopkg.in/tucnak/telebot.v2/util.go new file mode 100644 index 000000000..fc615b421 --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/util.go @@ -0,0 +1,209 @@ +package telebot + +import ( + "encoding/json" + "strconv" + + "github.com/pkg/errors" +) + +func (b *Bot) debug(err error) { + if b.reporter != nil { + b.reporter(errors.WithStack(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(errors.Errorf("%s", str)) + } + } +} + +func (b *Bot) sendText(to Recipient, text string, opt *SendOptions) (*Message, error) { + params := map[string]string{ + "chat_id": to.Recipient(), + "text": text, + } + embedSendOptions(params, opt) + + respJSON, err := b.Raw("sendMessage", params) + if err != nil { + return nil, err + } + + return extractMsgResponse(respJSON) +} + +func wrapSystem(err error) error { + return errors.Wrap(err, "system error") +} + +func isUserInList(user *User, list []User) bool { + for _, user2 := range list { + if user.ID == user2.ID { + return true + } + } + + return false +} + +func extractMsgResponse(respJSON []byte) (*Message, error) { + var resp struct { + Ok bool + Result *Message + Description string + } + + err := json.Unmarshal(respJSON, &resp) + if err != nil { + var resp struct { + Ok bool + Result bool + Description string + } + + err := json.Unmarshal(respJSON, &resp) + if err != nil { + return nil, errors.Wrap(err, "bad response json") + } + + if !resp.Ok { + return nil, errors.Errorf("api error: %s", resp.Description) + } + } + + if !resp.Ok { + return nil, errors.Errorf("api error: %s", resp.Description) + } + + return resp.Result, nil +} + +func extractOkResponse(respJSON []byte) error { + var resp struct { + Ok bool + Description string + } + + err := json.Unmarshal(respJSON, &resp) + if err != nil { + return errors.Wrap(err, "bad response json") + } + + if !resp.Ok { + return errors.Errorf("api error: %s", resp.Description) + } + + return nil +} + +func extractOptions(how []interface{}) *SendOptions { + var opts *SendOptions + + for _, prop := range how { + switch opt := prop.(type) { + case *SendOptions: + opts = opt.copy() + + case *ReplyMarkup: + if opts == nil { + opts = &SendOptions{} + } + opts.ReplyMarkup = opt.copy() + + case Option: + if opts == nil { + opts = &SendOptions{} + } + + switch opt { + case NoPreview: + opts.DisableWebPagePreview = true + case Silent: + opts.DisableNotification = 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 + default: + panic("telebot: unsupported flag-option") + } + + case ParseMode: + if opts == nil { + opts = &SendOptions{} + } + opts.ParseMode = opt + + default: + panic("telebot: unsupported send-option") + } + } + + return opts +} + +func embedSendOptions(params map[string]string, opt *SendOptions) { + 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"] = string(opt.ParseMode) + } + + if opt.ReplyMarkup != nil { + processButtons(opt.ReplyMarkup.InlineKeyboard) + replyMarkup, _ := json.Marshal(opt.ReplyMarkup) + params["reply_markup"] = string(replyMarkup) + } +} + +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]string, prv Rights) { + jsonRepr, _ := json.Marshal(prv) + json.Unmarshal(jsonRepr, &p) +} diff --git a/vendor/gopkg.in/tucnak/telebot.v2/webhook.go b/vendor/gopkg.in/tucnak/telebot.v2/webhook.go new file mode 100644 index 000000000..da757dc3d --- /dev/null +++ b/vendor/gopkg.in/tucnak/telebot.v2/webhook.go @@ -0,0 +1,149 @@ +package telebot + +import ( + "context" + "encoding/json" + "fmt" + "net/http" +) + +// A WebhookTLS specifies the path to a key and a cert so the poller can open +// a TLS listener +type WebhookTLS struct { + Key string + Cert string +} + +// 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 selfsigned 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 certifcate (letsencrypt, ...). +type WebhookEndpoint struct { + PublicURL string + Cert string +} + +// A Webhook configures the poller for webhooks. It opens a port on the given +// listen adress. 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 adress 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 + TLS *WebhookTLS + Endpoint *WebhookEndpoint + dest chan<- Update + bot *Bot +} + +type registerResult struct { + Ok bool `json:"ok"` + ErrorCode int `json:"error_code"` + Description string `json:"description"` +} + +func (h *Webhook) getFiles() map[string]string { + m := make(map[string]string) + if h.TLS != nil { + m["certificate"] = 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"] = h.Endpoint.Cert + } + } + return m +} + +func (h *Webhook) getParams() map[string]string { + param := make(map[string]string) + if h.TLS != nil { + param["url"] = fmt.Sprintf("https://%s", 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 ... + param["url"] = fmt.Sprintf("http://%s", h.Listen) + } + if h.Endpoint != nil { + param["url"] = h.Endpoint.PublicURL + } + return param +} + +func (h *Webhook) Poll(b *Bot, dest chan Update, stop chan struct{}) { + res, err := b.sendFiles("setWebhook", h.getFiles(), h.getParams()) + if err != nil { + b.debug(fmt.Errorf("setWebhook failed %q: %v", string(res), err)) + close(stop) + return + } + var result registerResult + err = json.Unmarshal(res, &result) + if err != nil { + b.debug(fmt.Errorf("bad json data %q: %v", string(res), err)) + close(stop) + return + } + if !result.Ok { + b.debug(fmt.Errorf("cannot register webhook: %s", result.Description)) + 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 + err := json.NewDecoder(r.Body).Decode(&update) + if err != nil { + h.bot.debug(fmt.Errorf("cannot decode update: %v", err)) + return + } + h.dest <- update +}