diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..42688e9 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,11 @@ +version: 2 +jobs: + build: + docker: + - image: circleci/golang:1.13 + working_directory: /go/src/github.com/go-chat-bot/bot + steps: + - checkout + - run: go test -v -cover -race -coverprofile=coverage.out + - run: go get github.com/mattn/goveralls + - run: goveralls -coverprofile=coverage.out -service=circle-ci -repotoken=$COVERALLS_TOKEN \ No newline at end of file diff --git a/.gitignore b/.gitignore index f980622..7e8b8d7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ go-bot *.o *.a *.so +pkg/ # Folders _obj @@ -33,3 +34,8 @@ _testmain.go # test coverage report files *.out *.html + +# go deps storage +src/github.com/* +src/gopkg.in/* + diff --git a/README.md b/README.md index db257a8..16e3557 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,38 @@ # go-bot -[![Circle CI](https://circleci.com/gh/go-chat-bot/bot/tree/master.svg?style=svg)](https://circleci.com/gh/go-chat-bot/bot/tree/master) [![GoDoc](https://godoc.org/github.com/go-chat-bot/bot?status.png)](https://godoc.org/github.com/go-chat-bot/bot) [![Coverage Status](https://coveralls.io/repos/github/go-chat-bot/bot/badge.svg?branch=master)](https://coveralls.io/github/go-chat-bot/bot?branch=master) +[![Circle CI](https://circleci.com/gh/go-chat-bot/bot/tree/master.svg?style=svg)](https://circleci.com/gh/go-chat-bot/bot/tree/master) [![GoDoc](https://godoc.org/github.com/go-chat-bot/bot?status.png)](https://godoc.org/github.com/go-chat-bot/bot) [![Coverage Status](https://coveralls.io/repos/github/go-chat-bot/bot/badge.svg?branch=master)](https://coveralls.io/github/go-chat-bot/bot?branch=master) ![Go report](https://goreportcard.com/badge/github.com/go-chat-bot/bot) [![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com) IRC, Slack & Telegram bot written in [Go][go] using [go-ircevent][go-ircevent] for IRC connectivity, [nlopes/slack](https://github.com/nlopes/slack) for Slack and [Syfaro/telegram-bot-api](https://github.com/Syfaro/telegram-bot-api) for Telegram. ![2016-01-17 11 21 38 036](https://cloud.githubusercontent.com/assets/1084729/12377689/5bf7d5f2-bd0d-11e5-87d9-525481f01c3a.gif) -## See the bot in action on IRC - -To see the bot in action, send a private message to **go-bot** on Freenode or join the channel **#go-bot @ irc.freenode.org**. - -Type `!help` in the channel or send `!help` in private message. - ## Plugins Please see the [plugins repository](https://github.com/go-chat-bot/plugins) for a complete list of plugins. You can also write your own, it's really simple. +## Compiling and testing the bot and plugins (Debug) + +This project uses the new [Go 1.11 modules](https://github.com/golang/go/wiki/Modules) if you have Go 1.11 installed, just clone the project and follow the instructions bellow, when you build Go will automatically download all dependencies. + +To test the bot, use the [debug](https://github.com/go-chat-bot/bot/tree/master/debug) console app. + +- Clone this repository or use `go get github.com/go-chat-bot/bot` +- Build everything: `go build ./...` +- Build and execute the debug app: + - `cd debug` + - `go build` + - `./debug` +- This will open a console where you can type commands +- Type `!help` to see the list of available commands + +### Testing your plugin + +- Add your plugin to `debug/main.go` import list +- Build the debug app +- Execute it and test with the interactive console + ## Protocols ### Slack @@ -61,26 +76,27 @@ To deploy your own go-bot to IRC, you need to: Here is a full example: ```Go package main - import ( - "github.com/go-chat-bot/bot/irc" - _ "github.com/go-chat-bot/plugins/catfacts" - _ "github.com/go-chat-bot/plugins/catgif" - _ "github.com/go-chat-bot/plugins/chucknorris" - // Import all the commands you wish to use - "os" - "strings" - ) - - func main() { - irc.Run(&irc.Config{ - Server: os.Getenv("IRC_SERVER"), - Channels: strings.Split(os.Getenv("IRC_CHANNELS"), ","), - User: os.Getenv("IRC_USER"), - Nick: os.Getenv("IRC_NICK"), - Password: os.Getenv("IRC_PASSWORD"), - UseTLS: true, - Debug: os.Getenv("DEBUG") != "",}) - } + +import ( + "github.com/go-chat-bot/bot/irc" + _ "github.com/go-chat-bot/plugins/catfacts" + _ "github.com/go-chat-bot/plugins/catgif" + _ "github.com/go-chat-bot/plugins/chucknorris" + // Import all the commands you wish to use + "os" + "strings" +) + +func main() { + irc.Run(&irc.Config{ + Server: os.Getenv("IRC_SERVER"), + Channels: strings.Split(os.Getenv("IRC_CHANNELS"), ","), + User: os.Getenv("IRC_USER"), + Nick: os.Getenv("IRC_NICK"), + Password: os.Getenv("IRC_PASSWORD"), + UseTLS: true, + Debug: os.Getenv("DEBUG") != "",}) +} ``` To join channels with passwords just put the password after the channel name separated by a space: @@ -153,6 +169,60 @@ func main() { } ``` +### Google Chat + +To deploy your go-bot to Google Chat (also known as Hangouts Chat, not plain +Hangouts) you will first need to follow documentation to [setup pub/sub +project](https://developers.google.com/hangouts/chat/how-tos/pub-sub) in Google +Cloud. This will enable your bot to receive messages even when it is behind a +firewall. + +Condensed, the steps you will need to take are as follows: +* Create new project in google cloud console + * ID of the project will be used in Config.PubSubProject +* Create service credentials for this project + * Path to downloaded credentials file should be in env variable GOOGLE_APPLICATION_CREDENTIALS + * Choose "Pub/Sub Editor" role for the credential +* Enable Pub/Sub API in cloud console +* Create new topic in the Pub/Sub (say "google-chat") + * Use the value of "Topic Name" (**not** "Topic ID") for Config.TopicName (e.g. /project//topics/) +* Modify permissions on created topic so that + "chat-api-push@system.gserviceaccount.com" has Pub/Sub Publisher permissions +* Enable hangouts chat api in Cloud Console +* Go to hangouts chat API config in the Cloud Console and fill in info + * Connection settings - use Pub/Sub and fill in topic string you created + above + +Config.SubscriptionName should be unique for each environment or you'll not +process messages correctly. If you encounter issues make sure your credentials +are correct and permissions for topics/queues are set up correctly. + +Config.WelcomeMessage is sent each time the bot joins a new room or private chat. + +Full example is here: +```Go +package main + +import ( + "os" + + "github.com/go-chat-bot/bot/google-chat" + _ "github.com/go-chat-bot/plugins/godoc" + _ "github.com/go-chat-bot/plugins/catfacts" + _ "github.com/go-chat-bot/plugins/catgif" + _ "github.com/go-chat-bot/plugins/chucknorris" +) + +func main() { + googlechat.Run(&googlechat.Config{ + PubSubProject: os.Getenv("HANGOUTS_PROJECT"), + TopicName: os.Getenv("HANGOUTS_TOPIC"), + SubscriptionName: os.Getenv("HANGOUTS_SUB"), + WelcomeMessage: os.Getenv("HANGOUTS_WELCOME"), +} + +``` + ## Deploying your own bot To see an example project on how to deploy your bot, please see my own configuration: diff --git a/bot.go b/bot.go index c33cd72..4266096 100644 --- a/bot.go +++ b/bot.go @@ -2,17 +2,22 @@ package bot import ( + "errors" "log" "math/rand" "time" - "github.com/robfig/cron" + "github.com/robfig/cron/v3" ) const ( // CmdPrefix is the prefix used to identify a command. // !hello would be identified as a command CmdPrefix = "!" + + // MsgBuffer is the max number of messages which can be buffered + // while waiting to flush them to the chat service. + MsgBuffer = 100 ) // Bot handles the bot instance @@ -20,36 +25,147 @@ type Bot struct { handlers *Handlers cron *cron.Cron disabledCmds []string + msgsToSend chan responseMessage + done chan struct{} + + // Protocol and Server are used by MesssageStreams to + // determine if this is the correct bot to send a message on + // see: + // https://github.com/go-chat-bot/bot/issues/37#issuecomment-277661159 + // https://github.com/go-chat-bot/bot/issues/97#issuecomment-442827599 + Protocol string + // Server and Protocol are used by MesssageStreams to + // determine if this is the correct bot to send a message on + // see: + // https://github.com/go-chat-bot/bot/issues/37#issuecomment-277661159 + // https://github.com/go-chat-bot/bot/issues/97#issuecomment-442827599 + Server string +} + +// Config configuration for this Bot instance +type Config struct { + // Protocol and Server are used by MesssageStreams to + /// determine if this is the correct bot to send a message on + Protocol string + // Server and Protocol are used by MesssageStreams to + // determine if this is the correct bot to send a message on + Server string +} + +type responseMessage struct { + target, message string + sender *User + protoParams interface{} +} + +// OutgoingMessage collects all the information for a message to go out. +type OutgoingMessage struct { + Target string + Message string + Sender *User + ProtoParams interface{} } // ResponseHandler must be implemented by the protocol to handle the bot responses type ResponseHandler func(target, message string, sender *User) +// ResponseHandlerV2 may be implemented by the protocol to handle the bot responses +type ResponseHandlerV2 func(OutgoingMessage) + +// ErrorHandler will be called when an error happens +type ErrorHandler func(msg string, err error) + // Handlers that must be registered to receive callbacks from the bot type Handlers struct { - Response ResponseHandler + Response ResponseHandler + ResponseV2 ResponseHandlerV2 + Errored ErrorHandler +} + +func logErrorHandler(msg string, err error) { + log.Printf("%s: %s", msg, err.Error()) } // New configures a new bot instance -func New(h *Handlers) *Bot { +func New(h *Handlers, bc *Config) *Bot { + if h.Errored == nil { + h.Errored = logErrorHandler + } + b := &Bot{ - handlers: h, - cron: cron.New(), + handlers: h, + cron: cron.New(), + msgsToSend: make(chan responseMessage, MsgBuffer), + done: make(chan struct{}), + Protocol: bc.Protocol, + Server: bc.Server, } + + // Launch the background goroutine that isolates the possibly non-threadsafe + // message sending logic of the underlying transport layer. + go b.processMessages() + + b.startMessageStreams() + b.startPeriodicCommands() return b } +func (b *Bot) startMessageStreams() { + for _, v := range messageStreamConfigs { + + go func(b *Bot, config *messageStreamConfig) { + msMap.Lock() + ms := &MessageStream{ + Data: make(chan MessageStreamMessage), + Done: make(chan bool), + } + var err = config.msgFunc(ms) + if err != nil { + b.errored("MessageStream "+config.streamName+" failed ", err) + } + msKey := messageStreamKey{ + Protocol: b.Protocol, + Server: b.Server, + StreamName: config.streamName, + } + // thread safe write + msMap.messageStreams[msKey] = ms + msMap.Unlock() + b.handleMessageStream(config.streamName, ms) + }(b, v) + } +} + func (b *Bot) startPeriodicCommands() { for _, config := range periodicCommands { func(b *Bot, config PeriodicConfig) { b.cron.AddFunc(config.CronSpec, func() { - for _, channel := range config.Channels { - message, err := config.CmdFunc(channel) + switch config.Version { + case v1: + for _, channel := range config.Channels { + message, err := config.CmdFunc(channel) + if err != nil { + b.errored("Periodic command failed ", err) + } else if message != "" { + b.SendMessage(OutgoingMessage{ + Target: channel, + Message: message, + }) + } + } + case v2: + results, err := config.CmdFuncV2() if err != nil { - log.Print("Periodic command failed ", err) - } else if message != "" { - b.handlers.Response(channel, message, nil) + b.errored("Periodic command failed ", err) + return + } + for _, result := range results { + b.SendMessage(OutgoingMessage{ + Target: result.Channel, + Message: result.Message, + ProtoParams: result.ProtoParams, + }) } } }) @@ -62,9 +178,13 @@ func (b *Bot) startPeriodicCommands() { // MessageReceived must be called by the protocol upon receiving a message func (b *Bot) MessageReceived(channel *ChannelData, message *Message, sender *User) { - command, err := parse(message.Text, channel, sender) + command, err := parse(message, channel, sender) if err != nil { - b.handlers.Response(channel.Channel, err.Error(), sender) + b.SendMessage(OutgoingMessage{ + Target: channel.Channel, + Message: err.Error(), + Sender: sender, + }) return } @@ -91,6 +211,65 @@ func (b *Bot) MessageReceived(channel *ChannelData, message *Message, sender *Us } } +// SendMessage queues a message. +func (b *Bot) SendMessage(om OutgoingMessage) { + message := b.executeFilterCommands(&FilterCmd{ + Target: om.Target, + Message: om.Message, + User: om.Sender, + }) + if message == "" { + return + } + + select { + case b.msgsToSend <- responseMessage{ + target: om.Target, + message: message, + sender: om.Sender, + protoParams: om.ProtoParams, + }: + default: + b.errored("Failed to queue message to send.", errors.New("Too busy")) + } +} + +func (b *Bot) sendResponse(resp responseMessage) { + if b.handlers.ResponseV2 != nil { + b.handlers.ResponseV2(OutgoingMessage{ + Message: resp.message, + ProtoParams: resp.protoParams, + Sender: resp.sender, + Target: resp.target, + }) + return + } + b.handlers.Response(resp.target, resp.message, resp.sender) +} + +func (b *Bot) errored(msg string, err error) { + if b.handlers.Errored != nil { + b.handlers.Errored(msg, err) + } +} + +func (b *Bot) processMessages() { + for { + select { + case msg := <-b.msgsToSend: + b.sendResponse(msg) + case <-b.done: + return + } + } +} + +// Close will shut down the message sending capabilities of this bot. Call +// this when you are done using the bot. +func (b *Bot) Close() { + close(b.done) +} + func init() { rand.Seed(time.Now().UnixNano()) } diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 5fe34fb..0000000 --- a/circle.yml +++ /dev/null @@ -1,7 +0,0 @@ -test: - pre: - - go get github.com/mattn/goveralls - override: - - go test -v -cover -race -coverprofile=/home/ubuntu/coverage.out - post: - - goveralls -coverprofile=/home/ubuntu/coverage.out -service=circle-ci diff --git a/cmd.go b/cmd.go index 6f4f1ec..d8467e8 100644 --- a/cmd.go +++ b/cmd.go @@ -1,11 +1,17 @@ package bot import ( + "errors" "fmt" - "log" "sync" ) +var ( + // ErrProtocolServerMismatch server && proto must match + ErrProtocolServerMismatch = errors.New("the specified protocol and server do not correspond to this bot instance") + errNoChannelSpecified = errors.New("no channel was specified for this message") +) + // Cmd holds the parsed user's input for easier handling of commands type Cmd struct { Raw string // Raw is full string passed to the command @@ -24,6 +30,7 @@ type ChannelData struct { Protocol string // What protocol the message was sent on (irc, slack, telegram) Server string // The server hostname the message was sent on Channel string // The channel name the message appeared in + HumanName string // The human readable name of the channel. IsPrivate bool // Whether the channel is a group or private chat } @@ -34,8 +41,17 @@ func (c *ChannelData) URI() string { // Message holds the message info - for IRC and Slack networks, this can include whether the message was an action. type Message struct { - Text string // The actual content of this Message - IsAction bool // True if this was a '/me does something' message + Text string // The actual content of this Message + IsAction bool // True if this was a '/me does something' message + ProtoMsg interface{} // The underlying object that we got from the protocol pkg +} + +// FilterCmd holds information about what is output being filtered - message and +// channel where it is being sent +type FilterCmd struct { + Target string // Channel or user the message is being sent to + Message string // Message text being sent + User *User // User who triggered original message } // PassiveCmd holds the information which will be passed to passive commands when receiving a message @@ -49,9 +65,11 @@ type PassiveCmd struct { // PeriodicConfig holds a cron specification for periodically notifying the configured channels type PeriodicConfig struct { - CronSpec string // CronSpec that schedules some function - Channels []string // A list of channels to notify - CmdFunc func(channel string) (string, error) // func to be executed at the period specified on CronSpec + Version int + CronSpec string // CronSpec that schedules some function + Channels []string // A list of channels to notify, ignored for V2 + CmdFunc func(channel string) (string, error) // func to be executed at the period specified on CronSpec + CmdFuncV2 func() ([]CmdResult, error) // func v2 to be executed at the period specified on CronSpec } // User holds user id, nick and real name @@ -62,33 +80,55 @@ type User struct { IsBot bool } +// MessageStream allows event information to be transmitted to an arbitrary channel +// https://github.com/go-chat-bot/bot/issues/97 +type MessageStream struct { + Data chan MessageStreamMessage + // Done is almost never called, usually the bot should just leave the chan open + Done chan bool +} + +// MessageStreamMessage the actual Message passed back to MessageStream in a chan +type MessageStreamMessage struct { + Message string + ChannelData *ChannelData +} + type customCommand struct { - Version int - Cmd string - CmdFuncV1 activeCmdFuncV1 - CmdFuncV2 activeCmdFuncV2 - CmdFuncV3 activeCmdFuncV3 - Description string - ExampleArgs string + Version int + Cmd string + CmdFuncV1 activeCmdFuncV1 + CmdFuncV2 activeCmdFuncV2 + CmdFuncV3 activeCmdFuncV3 + PassiveFuncV1 passiveCmdFuncV1 + PassiveFuncV2 passiveCmdFuncV2 + FilterFuncV1 filterCmdFuncV1 + Description string + ExampleArgs string } // CmdResult is the result message of V2 commands type CmdResult struct { - Channel string // The channel where the bot should send the message - Message string // The message to be sent + Channel string // The channel where the bot should send the message + Message string // The message to be sent + ProtoParams interface{} } // CmdResultV3 is the result message of V3 commands type CmdResultV3 struct { - Channel string - Message chan string - Done chan bool + Channel string + Message chan string + Done chan bool + ProtoParams interface{} } const ( v1 = iota v2 v3 + pv1 + pv2 + fv1 ) const ( @@ -97,15 +137,45 @@ const ( errorExecutingCommand = "Error executing %s: %s" ) -type passiveCmdFunc func(cmd *PassiveCmd) (string, error) +type passiveCmdFuncV1 func(cmd *PassiveCmd) (string, error) +type passiveCmdFuncV2 func(cmd *PassiveCmd) (CmdResultV3, error) + type activeCmdFuncV1 func(cmd *Cmd) (string, error) type activeCmdFuncV2 func(cmd *Cmd) (CmdResult, error) type activeCmdFuncV3 func(cmd *Cmd) (CmdResultV3, error) +type filterCmdFuncV1 func(cmd *FilterCmd) (string, error) + +type messageStreamFunc func(ms *MessageStream) error + +type messageStreamSyncMap struct { + sync.RWMutex + messageStreams map[messageStreamKey]*MessageStream +} +type messageStreamKey struct { + StreamName string + Server string + Protocol string +} + +// messageStreamConfig holds the registered function for the streamname +type messageStreamConfig struct { + version int + streamName string + msgFunc messageStreamFunc +} + var ( commands = make(map[string]*customCommand) - passiveCommands = make(map[string]passiveCmdFunc) + passiveCommands = make(map[string]*customCommand) + filterCommands = make(map[string]*customCommand) periodicCommands = make(map[string]PeriodicConfig) + + messageStreamConfigs []*messageStreamConfig + + msMap = &messageStreamSyncMap{ + messageStreams: make(map[messageStreamKey]*MessageStream), + } ) // RegisterCommand adds a new command to the bot. @@ -148,25 +218,83 @@ func RegisterCommandV3(command, description, exampleArgs string, cmdFunc activeC } } +// RegisterMessageStream adds a new message stream to the bot. +// The command should be registered in the Init() func of your package +// MessageStreams send messages to a channel +// streamName: String used to identify the command, for internal use only (ex: webhook) +// messageStreamFunc: Function which will be executed. It will received a MessageStream with a chan to push +func RegisterMessageStream(streamName string, msgFunc messageStreamFunc) { + messageStreamConfigs = append(messageStreamConfigs, &messageStreamConfig{ + version: v1, + streamName: streamName, + msgFunc: msgFunc, + }) +} + // RegisterPassiveCommand adds a new passive command to the bot. // The command should be registered in the Init() func of your package // Passive commands receives all the text posted to a channel without any parsing // command: String used to identify the command, for internal use only (ex: logs) // cmdFunc: Function which will be executed. It will received the raw message, channel and nick -func RegisterPassiveCommand(command string, cmdFunc func(cmd *PassiveCmd) (string, error)) { - passiveCommands[command] = cmdFunc +func RegisterPassiveCommand(command string, cmdFunc passiveCmdFuncV1) { + passiveCommands[command] = &customCommand{ + Version: pv1, + Cmd: command, + PassiveFuncV1: cmdFunc, + } +} + +// RegisterPassiveCommandV2 adds a new passive command to the bot. +// The command should be registered in the Init() func of your package +// Passive commands receives all the text posted to a channel without any parsing +// command: String used to identify the command, for internal use only (ex: logs) +// cmdFunc: Function which will be executed. It will received the raw message, channel and nick +func RegisterPassiveCommandV2(command string, cmdFunc passiveCmdFuncV2) { + passiveCommands[command] = &customCommand{ + Version: pv2, + Cmd: command, + PassiveFuncV2: cmdFunc, + } +} + +// RegisterFilterCommand adds a command that is run every time bot is about to +// send a message. The comand should be registered in the Init() func of your +// package. +// Filter commands receive message and its destination and should return +// modified version. Returning empty string prevents message being sent +// completely +// command: String used to identify the command, for internal use only (ex: silence) +// cmdFunc: Function which will be executed. It will receive the message, target +// channel and nick who triggered original message +func RegisterFilterCommand(command string, cmdFunc filterCmdFuncV1) { + filterCommands[command] = &customCommand{ + Version: fv1, + Cmd: command, + FilterFuncV1: cmdFunc, + } } // RegisterPeriodicCommand adds a command that is run periodically. // The command should be registered in the Init() func of your package // config: PeriodicConfig which specify CronSpec and a channel list -// cmdFunc: A no-arg function which gets triggered periodically +// cmdFunc: A function with single string argument (channel) which gets triggered periodically func RegisterPeriodicCommand(command string, config PeriodicConfig) { + config.Version = v1 + periodicCommands[command] = config +} + +// RegisterPeriodicCommandV2 adds a command that is run periodically. +// The command should be registered in the Init() func of your package +// config: PeriodicConfig which specifies CronSpec +// cmdFuncV2: A no-arg function which gets triggered periodically +// It should return slice of CmdResults (channel and message to send to it) +func RegisterPeriodicCommandV2(command string, config PeriodicConfig) { + config.Version = v2 periodicCommands[command] = config } // Disable allows disabling commands that were registered. -// It is usefull when running multiple bot instances to disabled some plugins like url which +// It is useful when running multiple bot instances to disabled some plugins like url which // is already present on some protocols. func (b *Bot) Disable(cmds []string) { b.disabledCmds = append(b.disabledCmds, cmds...) @@ -174,32 +302,72 @@ func (b *Bot) Disable(cmds []string) { func (b *Bot) executePassiveCommands(cmd *PassiveCmd) { var wg sync.WaitGroup - mutex := &sync.Mutex{} for k, v := range passiveCommands { if b.isDisabled(k) { continue } - cmdFunc := v wg.Add(1) - go func() { + go func(cmdFunc *customCommand) { defer wg.Done() - result, err := cmdFunc(cmd) - if err != nil { - log.Println(err) - } else { - mutex.Lock() - b.handlers.Response(cmd.Channel, result, cmd.User) - mutex.Unlock() + switch cmdFunc.Version { + case pv1: + result, err := cmdFunc.PassiveFuncV1(cmd) + if err != nil { + b.errored(fmt.Sprintf("Error executing %s", cmdFunc.Cmd), err) + } else { + b.SendMessage(OutgoingMessage{ + Target: cmd.Channel, + Message: result, + Sender: cmd.User, + }) + } + case pv2: + result, err := cmdFunc.PassiveFuncV2(cmd) + if err != nil { + b.errored(fmt.Sprintf("Error executing %s", cmdFunc.Cmd), err) + return + } + for { + select { + case message := <-result.Message: + if message != "" { + b.SendMessage(OutgoingMessage{ + Target: result.Channel, + Message: message, + Sender: cmd.User, + ProtoParams: result.ProtoParams, + }) + } + case <-result.Done: + return + } + } + default: } - }() + }(v) } wg.Wait() } +func (b *Bot) executeFilterCommands(cmd *FilterCmd) string { + for k, filter := range filterCommands { + switch filter.Version { + case fv1: + filtered, err := filter.FilterFuncV1(cmd) + if err != nil { + b.errored(fmt.Sprintf("Error executing filter %s", k), err) + continue + } + cmd.Message = filtered + } + } + return cmd.Message +} + func (b *Bot) isDisabled(cmd string) bool { for _, c := range b.disabledCmds { if c == cmd { @@ -213,7 +381,7 @@ func (b *Bot) handleCmd(c *Cmd) { cmd := commands[c.Command] if cmd == nil { - log.Printf("Command not found %v", c.Command) + b.errored(fmt.Sprintf("Command not found %v", c.Command), errors.New("Command not found")) return } @@ -222,7 +390,11 @@ func (b *Bot) handleCmd(c *Cmd) { message, err := cmd.CmdFuncV1(c) b.checkCmdError(err, c) if message != "" { - b.handlers.Response(c.Channel, message, c.User) + b.SendMessage(OutgoingMessage{ + Target: c.Channel, + Message: message, + Sender: c.User, + }) } case v2: result, err := cmd.CmdFuncV2(c) @@ -232,7 +404,12 @@ func (b *Bot) handleCmd(c *Cmd) { } if result.Message != "" { - b.handlers.Response(result.Channel, result.Message, c.User) + b.SendMessage(OutgoingMessage{ + Target: result.Channel, + Message: result.Message, + Sender: c.User, + ProtoParams: result.ProtoParams, + }) } case v3: result, err := cmd.CmdFuncV3(c) @@ -244,7 +421,12 @@ func (b *Bot) handleCmd(c *Cmd) { select { case message := <-result.Message: if message != "" { - b.handlers.Response(result.Channel, message, c.User) + b.SendMessage(OutgoingMessage{ + Target: result.Channel, + Message: message, + Sender: c.User, + ProtoParams: result.ProtoParams, + }) } case <-result.Done: return @@ -256,7 +438,48 @@ func (b *Bot) handleCmd(c *Cmd) { func (b *Bot) checkCmdError(err error, c *Cmd) { if err != nil { errorMsg := fmt.Sprintf(errorExecutingCommand, c.Command, err.Error()) - log.Printf(errorMsg) - b.handlers.Response(c.Channel, errorMsg, c.User) + b.errored(errorMsg, err) + b.SendMessage(OutgoingMessage{ + Target: c.Channel, + Message: errorMsg, + Sender: c.User, + }) + } +} + +// handleMessageStream +// if there are two bots (telegram, irc) and three messsages(a, b, c) then there will be six entries in messageStreams[key] +// when a message is sent into a chan it has a good chance of arriving at the wrong bot instance +// for every message we check to see if it matched this b.Protocol and b.Server +// if it doesn't we lookup the entry in messageStreams[key] send it to *that* Data chan +func (b *Bot) handleMessageStream(streamName string, ms *MessageStream) { + for { + select { + case d := <-ms.Data: + + if d.ChannelData.Protocol != b.Protocol || d.ChannelData.Server != b.Server { + // then lookup who it *should* be sent to and send it back into *that* chan + key := messageStreamKey{Protocol: d.ChannelData.Protocol, Server: d.ChannelData.Server, StreamName: streamName} + msMap.RLock() + msMap.messageStreams[key].Data <- d + msMap.RUnlock() + continue + } + + // this message is meant for us! + + if d.ChannelData.Channel == "" { + b.errored("handleMessageStream: "+d.Message, errNoChannelSpecified) + continue + } + if d.Message != "" { + b.SendMessage(OutgoingMessage{ + Target: d.ChannelData.Channel, + Message: d.Message, + }) + } + case <-ms.Done: + return + } } } diff --git a/cmd_test.go b/cmd_test.go index f348e2a..99ae550 100644 --- a/cmd_test.go +++ b/cmd_test.go @@ -6,13 +6,19 @@ import ( "reflect" "sort" "strings" + "sync" "testing" + "time" ) var ( - channel string - replies []string - user *User + channel string + replies chan string + cmdError chan string + user *User + msgs []string + errs []string + protoParams interface{} ) const ( @@ -22,27 +28,79 @@ const ( cmdExampleArgs = "arg1 arg2" ) +func waitMessages(t *testing.T, count int, errorCount int) { + for { + select { + case reply := <-replies: + msgs = append(msgs, reply) + case err := <-cmdError: + errs = append(errs, err) + case <-time.After(1 * time.Second): + t.Error("Timeout waiting for messages") + t.Errorf("msgs received: %v", msgs) + t.Errorf("errs received: %v", errs) + t.Fatal() + } + if len(msgs) == count && len(errs) == errorCount { + return + } + } +} + func responseHandler(target string, message string, sender *User) { channel = target user = sender - replies = append(replies, message) + replies <- message +} + +func responseHandlerV2(om OutgoingMessage) { + channel = om.Target + user = om.Sender + protoParams = om.ProtoParams + replies <- om.Message +} + +func errorHandler(msg string, err error) { + cmdError <- fmt.Sprintf("%s: %s", msg, err) } -func resetResponses() { +func reset() { channel = "" user = &User{Nick: ""} - replies = []string{} + replies = make(chan string, 10) + cmdError = make(chan string, 10) + msgs = []string{} + errs = []string{} + protoParams = nil commands = make(map[string]*customCommand) + periodicCommands = make(map[string]PeriodicConfig) + passiveCommands = make(map[string]*customCommand) + filterCommands = make(map[string]*customCommand) } func newBot() *Bot { return New(&Handlers{ Response: responseHandler, - }) + Errored: errorHandler, + }, + &Config{ + Protocol: "test", + Server: "test", + }, + ) } -func resetRegisteredPeriodicCommands() { - periodicCommands = make(map[string]PeriodicConfig) +func newBotV2() *Bot { + return New(&Handlers{ + Response: responseHandler, + ResponseV2: responseHandlerV2, + Errored: errorHandler, + }, + &Config{ + Protocol: "test", + Server: "test", + }, + ) } func registerValidCommand() { @@ -53,15 +111,18 @@ func registerValidCommand() { } func TestPeriodicCommands(t *testing.T) { - resetResponses() - resetRegisteredPeriodicCommands() + reset() RegisterPeriodicCommand("morning", PeriodicConfig{ - CronSpec: "0 0 08 * * mon-fri", + CronSpec: "0 08 * * mon-fri", Channels: []string{"#channel"}, CmdFunc: func(channel string) (string, error) { return "ok " + channel, nil }, }) - b := New(&Handlers{Response: responseHandler}) + b := New( + &Handlers{Response: responseHandler}, + &Config{Protocol: "test", Server: "test"}, + ) + defer b.Close() entries := b.cron.Entries() if len(entries) != 1 { @@ -73,30 +134,31 @@ func TestPeriodicCommands(t *testing.T) { entries[0].Job.Run() - if len(replies) != 1 { - t.Fatal("Should have one reply in the channel") - } - if replies[0] != "ok #channel" { + waitMessages(t, 1, 0) + + if msgs[0] != "ok #channel" { t.Fatal("Invalid reply") } } - func TestMultiplePeriodicCommands(t *testing.T) { - resetResponses() - resetRegisteredPeriodicCommands() + reset() RegisterPeriodicCommand("morning", PeriodicConfig{ - CronSpec: "0 0 08 * * mon-fri", + CronSpec: "0 08 * * mon-fri", Channels: []string{"#channel"}, CmdFunc: func(channel string) (string, error) { return "ok_morning " + channel, nil }, }) RegisterPeriodicCommand("afternoon", PeriodicConfig{ - CronSpec: "0 0 12 * * mon-fri", + CronSpec: "0 12 * * mon-fri", Channels: []string{"#channel"}, CmdFunc: func(channel string) (string, error) { return "ok_afternoon " + channel, nil }, }) - b := New(&Handlers{Response: responseHandler}) + b := New( + &Handlers{Response: responseHandler}, + &Config{Protocol: "test", Server: "test"}, + ) + defer b.Close() entries := b.cron.Entries() if len(entries) != 2 { @@ -112,27 +174,30 @@ func TestMultiplePeriodicCommands(t *testing.T) { entries[0].Job.Run() entries[1].Job.Run() - if len(replies) != 2 { + waitMessages(t, 2, 0) + + if len(msgs) != 2 { t.Fatal("Should have two replies in the channel") } - if replies[0] != "ok_morning #channel" { - t.Fatal("Invalid reply in first cron job") + sort.Strings(msgs) + if msgs[0] != "ok_afternoon #channel" { + t.Fatal("Invalid reply in afternoon cron job") } - if replies[1] != "ok_afternoon #channel" { - t.Fatal("Invalid reply in second cron job") + if msgs[1] != "ok_morning #channel" { + t.Fatalf("Invalid reply in morning cron job.") } } func TestErroredPeriodicCommand(t *testing.T) { - resetResponses() - resetRegisteredPeriodicCommands() + reset() RegisterPeriodicCommand("bugged", PeriodicConfig{ - CronSpec: "0 0 08 * * mon-fri", + CronSpec: "0 08 * * mon-fri", Channels: []string{"#channel"}, CmdFunc: func(channel string) (string, error) { return "bug", errors.New("error") }, }) - b := New(&Handlers{Response: responseHandler}) + b := newBot() + defer b.Close() entries := b.cron.Entries() @@ -141,18 +206,106 @@ func TestErroredPeriodicCommand(t *testing.T) { } entries[0].Job.Run() + waitMessages(t, 0, 1) - if len(replies) != 0 { - t.Fatal("Should not have a reply in the channel") + if len(msgs) != 0 { + t.Error("Should not have a reply in the channel") + } + if len(errs) != 1 { + t.Error("Expected 1 error") + } +} + +func TestPeriodicCommandsV2(t *testing.T) { + reset() + RegisterPeriodicCommandV2("morning", + PeriodicConfig{ + CronSpec: "0 08 * * mon-fri", + CmdFuncV2: func() ([]CmdResult, error) { + ret := []CmdResult{ + {Message: "message 1", Channel: "#channel1"}, + {Message: "message 2", Channel: "#channel2"}} + return ret, nil + }, + }) + channels := make([]string, 0, 2) + b := New(&Handlers{Response: func(target string, message string, sender *User) { + channels = append(channels, target) + channel = target + user = sender + replies <- message + }}, + &Config{Protocol: "test", Server: "test"}) + + defer b.Close() + + entries := b.cron.Entries() + if len(entries) != 1 { + t.Fatal("Should have one cron job entry") + } + if entries[0].Next.Hour() != 8 { + t.Fatal("Cron job should be scheduled to 8am") + } + + entries[0].Job.Run() + + waitMessages(t, 2, 0) + if len(channels) != 2 { + t.Fatal("Should have 2 destinations channels", len(channels)) + } + + if msgs[0] != "message 1" { + t.Fatal("Invalid first reply") + } + + if channels[0] != "#channel1" { + t.Fatal("Invalid channel for first message", channels[0]) + } + + if msgs[1] != "message 2" { + t.Fatal("Invalid second reply") + } + + if channels[1] != "#channel2" { + t.Fatal("Invalid channel for second message", channels[1]) + } +} + +func TestErroredPeriodicCommandsV2(t *testing.T) { + reset() + RegisterPeriodicCommandV2("morning", + PeriodicConfig{ + CronSpec: "0 08 * * mon-fri", + CmdFuncV2: func() ([]CmdResult, error) { + return nil, errors.New("error") + }, + }) + b := newBot() + defer b.Close() + + entries := b.cron.Entries() + if len(entries) != 1 { + t.Fatal("Should have one cron job entry") + } + if entries[0].Next.Hour() != 8 { + t.Fatal("Cron job should be scheduled to 8am") + } + + entries[0].Job.Run() + + waitMessages(t, 0, 1) + if len(msgs) != 0 { + t.Error("Should not have a reply in the channel") + } + if len(errs) != 1 { + t.Error("Expected 1 error") } } func TestDisabledCommands(t *testing.T) { - resetResponses() + reset() commands = make(map[string]*customCommand) - b := New(&Handlers{ - Response: responseHandler, - }) + b := newBot() RegisterCommand("cmd", "", "", func(c *Cmd) (string, error) { @@ -166,79 +319,87 @@ func TestDisabledCommands(t *testing.T) { b.Disable([]string{"cmd"}) b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd"}, &User{Nick: "user"}) - if len(replies) != 0 { + + time.Sleep(100) + if len(msgs) != 0 { t.Fatal("Should not execute disabled active commands") } b.Disable([]string{"passive"}) b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "regular message"}, &User{Nick: "user"}) - if len(replies) != 0 { + time.Sleep(100) + if len(msgs) != 0 { t.Fatal("Should not execute disabled passive commands") } } func TestCommandNotRegistered(t *testing.T) { - resetResponses() + reset() + b := newBot() + b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!not_a_cmd"}, &User{}) - newBot().MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!not_a_cmd"}, &User{}) + time.Sleep(100) - if len(replies) != 0 { + if len(msgs) != 0 { t.Fatal("Should not reply if a command is not found") } } func TestInvalidCmdArgs(t *testing.T) { - resetResponses() + reset() registerValidCommand() - newBot().MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd \"invalid arg"}, &User{Nick: "user"}) + b := newBot() + b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd \"invalid arg"}, &User{Nick: "user"}) + + waitMessages(t, 1, 0) if channel != "#go-bot" { t.Error("Should reply to #go-bot channel") } - if len(replies) != 1 { - t.Fatal("Invalid reply") - } - if !strings.HasPrefix(replies[0], "Error parsing") { + if !strings.HasPrefix(msgs[0], "Error parsing") { t.Fatal("Should reply with an error message") } } func TestErroredCmd(t *testing.T) { - resetResponses() + reset() cmdError := errors.New("error") RegisterCommand("cmd", "", "", func(c *Cmd) (string, error) { return "", cmdError }) - newBot().MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd"}, &User{Nick: "user"}) + b := newBot() + b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd"}, &User{Nick: "user"}) + + waitMessages(t, 1, 1) if channel != "#go-bot" { t.Fatal("Invalid channel") } - if len(replies) != 1 { - t.Fatal("Invalid reply") - } - if replies[0] != fmt.Sprintf(errorExecutingCommand, "cmd", cmdError.Error()) { + if msgs[0] != fmt.Sprintf(errorExecutingCommand, "cmd", cmdError.Error()) { t.Fatal("Reply should contain the error message") } + if len(errs) != 1 { + t.Error("Expected the command to return an error") + } } func TestValidCmdOnChannel(t *testing.T) { - resetResponses() + reset() registerValidCommand() - newBot().MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd"}, &User{Nick: "user"}) + b := newBot() + b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd"}, &User{Nick: "user"}) + + waitMessages(t, 1, 0) if channel != "#go-bot" { t.Fatal("Command called on channel should reply to channel") } - if len(replies) != 1 { - t.Fatal("Should have one reply on channel") - } - if replies[0] != expectedMsg { + if msgs[0] != expectedMsg { t.Fatal("Invalid command reply") } } @@ -255,77 +416,88 @@ func TestChannelData(t *testing.T) { } func TestHelpWithNoArgs(t *testing.T) { - resetResponses() + reset() registerValidCommand() - newBot().MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!help"}, &User{Nick: "user"}) + b := newBot() + b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!help"}, &User{Nick: "user"}) + + waitMessages(t, 2, 0) expectedReply := []string{ fmt.Sprintf(helpAboutCommand, CmdPrefix), fmt.Sprintf(availableCommands, "cmd"), } - if !reflect.DeepEqual(replies, expectedReply) { - t.Fatalf("Invalid reply. Expected %v got %v", expectedReply, replies) + if !reflect.DeepEqual(msgs, expectedReply) { + t.Fatalf("Invalid reply. Expected %v got %v", expectedReply, msgs) } } func TestDisableHelp(t *testing.T) { - resetResponses() + reset() registerValidCommand() b := newBot() b.Disable([]string{"help"}) b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!help"}, &User{Nick: "user"}) + time.Sleep(100) + if len(replies) > 0 { t.Fatalf("Should not execute help after disabling it") } } func TestHelpForACommand(t *testing.T) { - resetResponses() + reset() registerValidCommand() - newBot().MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!help cmd"}, &User{Nick: "user"}) + b := newBot() + b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!help cmd"}, &User{Nick: "user"}) + + waitMessages(t, 2, 0) expectedReply := []string{ fmt.Sprintf(helpDescripton, cmdDescription), fmt.Sprintf(helpUsage, CmdPrefix, cmd, cmdExampleArgs), } - if !reflect.DeepEqual(replies, expectedReply) { - t.Fatalf("Invalid reply. Expected %v got %v", expectedReply, replies) + if !reflect.DeepEqual(msgs, expectedReply) { + t.Fatalf("Invalid reply. Expected %v got %v", expectedReply, msgs) } } func TestHelpWithNonExistingCommand(t *testing.T) { - resetResponses() + reset() registerValidCommand() - newBot().MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!help not_a_cmd"}, &User{Nick: "user"}) + b := newBot() + b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!help not_a_cmd"}, &User{Nick: "user"}) expectedReply := []string{ fmt.Sprintf(helpAboutCommand, CmdPrefix), fmt.Sprintf(availableCommands, "cmd"), } - if !reflect.DeepEqual(replies, expectedReply) { - t.Fatalf("Invalid reply. Expected %v got %v", expectedReply, replies) + waitMessages(t, 2, 0) + + if !reflect.DeepEqual(msgs, expectedReply) { + t.Fatalf("Invalid reply. Expected %v got %v", expectedReply, msgs) } } func TestHelpWithInvalidArgs(t *testing.T) { - resetResponses() + reset() registerValidCommand() - newBot().MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!help cmd \"invalid arg"}, &User{Nick: "user"}) + b := newBot() + b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!help cmd \"invalid arg"}, &User{Nick: "user"}) - if len(replies) != 1 { - t.Fatal("Invalid reply") - } - if !strings.HasPrefix(replies[0], "Error parsing") { + waitMessages(t, 1, 0) + + if !strings.HasPrefix(msgs[0], "Error parsing") { t.Fatal("Should reply with an error message") } } func TestCmdV2(t *testing.T) { - resetResponses() + reset() RegisterCommandV2("cmd", "", "", func(c *Cmd) (CmdResult, error) { return CmdResult{ @@ -333,24 +505,61 @@ func TestCmdV2(t *testing.T) { Message: "message"}, nil }) - newBot().MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd"}, &User{Nick: "user"}) + b := newBot() + b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd"}, &User{Nick: "user"}) + + waitMessages(t, 1, 0) + + if channel != "#channel" { + t.Error("Wrong channel") + } + if !reflect.DeepEqual([]string{"message"}, msgs) { + t.Error("Invalid reply") + } +} + +func TestCmdV2WithProtoParams(t *testing.T) { + reset() + RegisterCommandV2("cmd", "", "", + func(c *Cmd) (CmdResult, error) { + return CmdResult{ + Channel: "#channel", + Message: "message", + ProtoParams: &CmdResult{Message: "Nested!"}, + }, nil + }) + + b := newBotV2() + b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd"}, &User{Nick: "user"}) + + waitMessages(t, 1, 0) if channel != "#channel" { t.Error("Wrong channel") } - if !reflect.DeepEqual([]string{"message"}, replies) { + if !reflect.DeepEqual([]string{"message"}, msgs) { t.Error("Invalid reply") } + if pa, ok := protoParams.(*CmdResult); ok { + if pa.Message != "Nested!" { + t.Error("Information lost in copying.") + } + } else { + t.Error("Failed to pass proto args through.") + } } func TestCmdV2WithoutSpecifyingChannel(t *testing.T) { - resetResponses() + reset() RegisterCommandV2("cmd", "", "", func(c *Cmd) (CmdResult, error) { return CmdResult{Message: "message"}, nil }) - newBot().MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd"}, &User{Nick: "user"}) + b := newBot() + b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd"}, &User{Nick: "user"}) + + waitMessages(t, 1, 0) if channel != "#go-bot" { t.Error("Should reply to original channel if no channel is returned") @@ -358,44 +567,74 @@ func TestCmdV2WithoutSpecifyingChannel(t *testing.T) { } func TestPassiveCommand(t *testing.T) { - resetResponses() - - passiveCommands = make(map[string]passiveCmdFunc) - - echo := func(cmd *PassiveCmd) (string, error) { - return cmd.Raw, nil - } - ping := func(cmd *PassiveCmd) (string, error) { - return "pong", nil - } - errored := func(cmd *PassiveCmd) (string, error) { - return "", errors.New("error") - } + reset() + passiveCommands = make(map[string]*customCommand) + echo := func(cmd *PassiveCmd) (string, error) { return cmd.Raw, nil } + ping := func(cmd *PassiveCmd) (string, error) { return "pong", nil } + errored := func(cmd *PassiveCmd) (string, error) { return "", errors.New("error") } RegisterPassiveCommand("echo", echo) RegisterPassiveCommand("ping", ping) RegisterPassiveCommand("errored", errored) - newBot().MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "test"}, &User{Nick: "user"}) + b := newBot() + b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "test"}, &User{Nick: "user"}) + + waitMessages(t, 2, 1) if channel != "#go-bot" { t.Error("Invalid channel") } - if len(replies) != 2 { + if len(msgs) != 2 { t.Fatal("Invalid reply") } + if len(errs) != 1 { + t.Error("Expected 1 error") + } - sort.Strings(replies) - if replies[0] != "pong" { + sort.Strings(msgs) + if msgs[0] != "pong" { t.Error("ping command not executed") } - if replies[1] != "test" { + if msgs[1] != "test" { t.Error("echo command not executed") } } +func TestPassiveCommandV2(t *testing.T) { + reset() + result := CmdResultV3{ + Channel: "#channel", + Message: make(chan string), + Done: make(chan bool)} + + ping := func(cmd *PassiveCmd) (CmdResultV3, error) { return result, nil } + errored := func(cmd *PassiveCmd) (CmdResultV3, error) { return CmdResultV3{}, errors.New("error") } + + RegisterPassiveCommandV2("ping", ping) + RegisterPassiveCommandV2("errored", errored) + + b := newBot() + go b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "test"}, &User{Nick: "user"}) + result.Message <- "pong" + result.Done <- true + + waitMessages(t, 1, 1) + + if channel != "#channel" { + t.Error("Invalid channel") + } + if len(msgs) != 1 { + t.Fatal("Invalid reply") + } + + if msgs[0] != "pong" { + t.Error("ping command not executed") + } +} + func TestCmdV3(t *testing.T) { - resetResponses() + reset() result := CmdResultV3{ Channel: "#channel", Message: make(chan string), @@ -405,20 +644,23 @@ func TestCmdV3(t *testing.T) { return result, nil }) - go newBot().MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd"}, &User{Nick: "user"}) + b := newBot() + go b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd"}, &User{Nick: "user"}) result.Message <- "message" result.Done <- true + waitMessages(t, 1, 0) + if channel != "#channel" { t.Error("Wrong channel") } - if !reflect.DeepEqual([]string{"message"}, replies) { + if !reflect.DeepEqual([]string{"message"}, msgs) { t.Error("Invalid reply") } } func TestCmdV3WithoutSpecifyingChannel(t *testing.T) { - resetResponses() + reset() result := CmdResultV3{ Message: make(chan string), Done: make(chan bool)} @@ -427,14 +669,162 @@ func TestCmdV3WithoutSpecifyingChannel(t *testing.T) { return result, nil }) - go newBot().MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd"}, &User{Nick: "user"}) + b := newBot() + go b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "!cmd"}, &User{Nick: "user"}) result.Message <- "message" result.Done <- true + waitMessages(t, 1, 0) + if channel != "#go-bot" { t.Error("Should reply to original channel if no channel is returned") } - if !reflect.DeepEqual([]string{"message"}, replies) { + if !reflect.DeepEqual([]string{"message"}, msgs) { t.Error("Invalid reply") } } + +func TestFilterCommand(t *testing.T) { + reset() + passiveCommands = make(map[string]*customCommand) + ping := func(cmd *PassiveCmd) (string, error) { return "pong", nil } + modified := func(cmd *FilterCmd) (string, error) { return "PONG!", nil } + errored := func(cmd *FilterCmd) (string, error) { return "", errors.New("error") } + + RegisterPassiveCommand("ping", ping) + RegisterFilterCommand("modified", modified) + RegisterFilterCommand("errored", errored) + + b := newBot() + b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "test"}, &User{Nick: "user"}) + + waitMessages(t, 1, 1) + + if channel != "#go-bot" { + t.Error("Invalid channel") + } + if len(msgs) != 1 { + t.Fatal("Invalid reply") + } + if len(errs) != 1 { + t.Error("Expected 1 error") + } + + sort.Strings(msgs) + if msgs[0] != "PONG!" { + t.Error("filter command not working") + } +} + +func TestFilterCommandSilence(t *testing.T) { + reset() + passiveCommands = make(map[string]*customCommand) + ping := func(cmd *PassiveCmd) (string, error) { return "pong", nil } + silenced := func(cmd *FilterCmd) (string, error) { return "", nil } + errored := func(cmd *FilterCmd) (string, error) { return "Ignored", errors.New("error") } + + RegisterPassiveCommand("ping", ping) + RegisterFilterCommand("silenced", silenced) + RegisterFilterCommand("errored", errored) + + b := newBot() + b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "test"}, &User{Nick: "user"}) + + waitMessages(t, 0, 1) + + if len(msgs) != 0 { + t.Fatal("Expected no messages!") + } + if len(errs) != 1 { + t.Error("Expected 1 error") + } +} + +func TestFilterCommandSilenceSendV2(t *testing.T) { + reset() + passiveCommands = make(map[string]*customCommand) + ping := func(cmd *PassiveCmd) (string, error) { return "pong", nil } + silenced := func(cmd *FilterCmd) (string, error) { return "", nil } + errored := func(cmd *FilterCmd) (string, error) { return "Ignored", errors.New("error") } + + RegisterPassiveCommand("ping", ping) + RegisterFilterCommand("silenced", silenced) + RegisterFilterCommand("errored", errored) + + b := newBotV2() + b.MessageReceived(&ChannelData{Channel: "#go-bot"}, &Message{Text: "test"}, &User{Nick: "user"}) + + waitMessages(t, 0, 1) + + if len(msgs) != 0 { + t.Fatal("Expected no messages!") + } + if len(errs) != 1 { + t.Error("Expected 1 error") + } +} + +// how to test channels.. +// https://www.hugopicado.com/2016/10/01/testing-over-golang-channels.html + +func TestMessageStreams(t *testing.T) { + var mutex = &sync.Mutex{} + reset() + + var msSender1 *MessageStream + var msSender2 *MessageStream + + RegisterMessageStream("streamOne", func(ms1 *MessageStream) error { + mutex.Lock() + msSender1 = ms1 + mutex.Unlock() + return nil + }) + RegisterMessageStream("streamTwo", func(ms2 *MessageStream) error { + mutex.Lock() + msSender2 = ms2 + mutex.Unlock() + return nil + }) + + b1 := New(&Handlers{Response: responseHandler, Errored: errorHandler}, &Config{Protocol: "protoA", Server: "test"}) + b2 := New(&Handlers{Response: responseHandler, Errored: errorHandler}, &Config{Protocol: "protoB", Server: "test"}) + + msmB1 := MessageStreamMessage{ + Message: "hello botOne", + ChannelData: &ChannelData{Server: b1.Server, Protocol: b1.Protocol, Channel: "#go-bot"}, + } + msmB2 := MessageStreamMessage{ + Message: "hello botTwo", + ChannelData: &ChannelData{Server: b2.Server, Protocol: b2.Protocol, Channel: "#go-bot"}, + } + + // give New() a second to make() the chans and setup the objects + time.Sleep(2 * time.Second) + + // when you send a message destined for b1 #go-bot, even if you send it to b2, it should arrive at b1 + mutex.Lock() + msSender1.Data <- msmB1 + if "hello botOne" != <-replies { + t.Fatal("message not Recieved at Channel") + } + + msSender2.Data <- msmB1 + if "hello botOne" != <-replies { + t.Fatal("message not Recieved at Channel") + } + + // and vice-versa + // when you send a message destined for b2 #go-bots, even if you send it to b1, it should arrive at b2 + msSender1.Data <- msmB2 + if "hello botTwo" != <-replies { + t.Fatal("message not Recieved at Channel") + } + + msSender2.Data <- msmB2 + if "hello botTwo" != <-replies { + t.Fatal("message not Recieved at Channel") + } + mutex.Unlock() + +} diff --git a/debug/main.go b/debug/main.go new file mode 100644 index 0000000..5dcee54 --- /dev/null +++ b/debug/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "bufio" + "fmt" + "os" + + "github.com/go-chat-bot/bot" + _ "github.com/go-chat-bot/plugins-br/cnpj" + _ "github.com/go-chat-bot/plugins-br/cotacao" + _ "github.com/go-chat-bot/plugins-br/cpf" + _ "github.com/go-chat-bot/plugins-br/dilma" + _ "github.com/go-chat-bot/plugins-br/lula" + _ "github.com/go-chat-bot/plugins-br/megasena" + _ "github.com/go-chat-bot/plugins/9gag" + _ "github.com/go-chat-bot/plugins/catfacts" + _ "github.com/go-chat-bot/plugins/catgif" + _ "github.com/go-chat-bot/plugins/chucknorris" + _ "github.com/go-chat-bot/plugins-br/gloria_a_deus" + _ "github.com/go-chat-bot/plugins/cmd" + _ "github.com/go-chat-bot/plugins/crypto" + _ "github.com/go-chat-bot/plugins/encoding" + _ "github.com/go-chat-bot/plugins/example" + _ "github.com/go-chat-bot/plugins/gif" + _ "github.com/go-chat-bot/plugins/godoc" + _ "github.com/go-chat-bot/plugins/guid" + _ "github.com/go-chat-bot/plugins/jira" + _ "github.com/go-chat-bot/plugins/puppet" + _ "github.com/go-chat-bot/plugins/treta" + _ "github.com/go-chat-bot/plugins/uptime" + _ "github.com/go-chat-bot/plugins/url" + _ "github.com/go-chat-bot/plugins/web" +) + +func responseHandler(target string, message string, sender *bot.User) { + if message == "" { + return + } + fmt.Println(fmt.Sprintf("%s: %s", sender.Nick, message)) +} + +func main() { + b := bot.New(&bot.Handlers{ + Response: responseHandler, + }, + &bot.Config{ + Protocol: "debug", + Server: "debug", + }, + ) + + fmt.Println("Type a command or !help for available commands...") + + for { + r := bufio.NewReader(os.Stdin) + + input, err := r.ReadString('\n') + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + b.MessageReceived( + &bot.ChannelData{ + Protocol: "debug", + Server: "", + Channel: "console", + IsPrivate: true, + }, + &bot.Message{Text: input}, + &bot.User{ID: "id", RealName: "Debug Console", Nick: "bot", IsBot: false}) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..da83f57 --- /dev/null +++ b/go.mod @@ -0,0 +1,42 @@ +module github.com/go-chat-bot/bot + +require ( + cloud.google.com/go v0.100.2 + cloud.google.com/go/compute v1.1.0 // indirect + cloud.google.com/go/iam v0.1.1 // indirect + cloud.google.com/go/pubsub v1.17.1 + github.com/Jeffail/gabs v1.4.0 // indirect + github.com/andygrunwald/go-jira v1.14.1-0.20220125145100-3555edb9bbda // indirect + github.com/beevik/guid v0.0.0-20170504223318-d0ea8faecee0 // indirect + github.com/cloudfoundry/gosigar v1.3.3 // indirect + github.com/detached/gorocket v0.0.0-20170629192631-d44bbd3f26d2 // indirect + github.com/go-chat-bot/plugins v0.0.0-20210423135617-90af38949890 + github.com/go-chat-bot/plugins-br v0.0.0-20200917130500-b69d8e0f9584 + github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/go-cmp v0.5.7 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect + github.com/martinusso/go-docs v1.0.0 // indirect + github.com/mattn/go-shellwords v1.0.12 + github.com/mozillazg/go-unidecode v0.1.1 + github.com/onsi/ginkgo v1.10.1 // indirect + github.com/onsi/gomega v1.7.0 // indirect + github.com/pyinx/gorocket v0.0.0-20170810024322-78ae1353729f + github.com/robfig/cron/v3 v3.0.1 + github.com/slack-go/slack v0.10.1 + github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect + github.com/stretchr/testify v1.7.0 // indirect + github.com/technoweenie/multipartstreamer v1.0.1 // indirect + github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 + golang.org/x/net v0.0.0-20220127074510-2fabfed7e28f // indirect + golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 + golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect + google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350 // indirect + google.golang.org/grpc v1.44.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/telegram-bot-api.v3 v3.0.0 +) + +go 1.13 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f9eda67 --- /dev/null +++ b/go.sum @@ -0,0 +1,751 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= +cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.1.0 h1:pyPhehLfZ6pVzRgJmXGYvCY4K7WSWRhVw0AwhgVvS84= +cloud.google.com/go/compute v1.1.0/go.mod h1:2NIffxgWfORSI7EOYMFatGTfjMLnqrOKBEyYb6NoRgA= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v0.1.1 h1:4CapQyNFjiksks1/x7jsvsygFPhihslYk5GptIrlX68= +cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= +cloud.google.com/go/kms v1.0.0 h1:YkIeqPXqTAlwXk3Z2/WG0d6h1tqJQjU354WftjEoP9E= +cloud.google.com/go/kms v1.0.0/go.mod h1:nhUehi+w7zht2XrUfvTRNpxrfayBHqP4lu2NSywui/0= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/pubsub v1.17.1 h1:s2UGTTphpnUQ0Wppkp2OprR4pS3nlBpPvyL2GV9cqdc= +cloud.google.com/go/pubsub v1.17.1/go.mod h1:4qDxMr1WsM9+aQAz36ltDwCIM+R0QdlseyFjBuNvnss= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo= +github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/andygrunwald/go-jira v1.5.0 h1:/1CyYLNdwus7TvB/DHyD3udb52K12aYL9m7WaGAO9m4= +github.com/andygrunwald/go-jira v1.5.0/go.mod h1:yNYQrX3nGSrVdcVsM2mWz2pm7tTeDtYfRyVEkc3VUiY= +github.com/andygrunwald/go-jira v1.14.0 h1:7GT/3qhar2dGJ0kq8w0d63liNyHOnxZsUZ9Pe4+AKBI= +github.com/andygrunwald/go-jira v1.14.0/go.mod h1:KMo2f4DgMZA1C9FdImuLc04x4WQhn5derQpnsuBFgqE= +github.com/andygrunwald/go-jira v1.14.1-0.20220125145100-3555edb9bbda h1:Ia6b4e/0qTV1VcWCUECOFjdPTyRAaLhKf3q+ZVQKZoI= +github.com/andygrunwald/go-jira v1.14.1-0.20220125145100-3555edb9bbda/go.mod h1:m62VSchfJSdD0PNkndBdbclfKRPGbKYkPhDN3Spm90I= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/beevik/guid v0.0.0-20170504223318-d0ea8faecee0 h1:oLd/YLOTOgA4D4aAUhIE8vhl/LAP1ZJrj0mDQpl7GB8= +github.com/beevik/guid v0.0.0-20170504223318-d0ea8faecee0/go.mod h1:XzXWuOd1wJ63MtICHh5+PnvCuxsB/d58T8TswEhI/9I= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudfoundry/gosigar v1.1.0 h1:V/dVCzhKOdIU3WRB5inQU20s4yIgL9Dxx/Mhi0SF8eM= +github.com/cloudfoundry/gosigar v1.1.0/go.mod h1:3qLfc2GlfmwOx2+ZDaRGH3Y9fwQ0sQeaAleo2GV5pH0= +github.com/cloudfoundry/gosigar v1.3.3 h1:aR9qIZ/Njb4GYPUGnoJMfXdhz+AOy80r4r3LUQC5A0g= +github.com/cloudfoundry/gosigar v1.3.3/go.mod h1:4TGthjsfIxe6Svlzn48EUw9xTTjT/2NnWyeRrferFP0= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/detached/gorocket v0.0.0-20170629192631-d44bbd3f26d2 h1:zwp9mAr+YvsgLCFIVJ3/m61Z+NRX35jbD0HBa62ryHY= +github.com/detached/gorocket v0.0.0-20170629192631-d44bbd3f26d2/go.mod h1:w5eKhlAkZwY6VBm2Sa1Evdte2+Fqhc+dnSk7/KTN5FM= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/structs v1.0.0 h1:BrX964Rv5uQ3wwS+KRUAJCBBw5PQmgJfJ6v4yly5QwU= +github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chat-bot/plugins v0.0.0-20181006134258-491b3f9878d6 h1:qNYjVQnDwznjLk+OnNdczA5SXwEa/RwjPTZSQCKofF4= +github.com/go-chat-bot/plugins v0.0.0-20181006134258-491b3f9878d6/go.mod h1:Ga63x4EC4NFYr/KGzhn8D8fLj89sfJA/dpBsuowiHOQ= +github.com/go-chat-bot/plugins v0.0.0-20210423135617-90af38949890 h1:HOa74NO1AF9Gn+K0rQInzzol0ja9NCTkX0xlfmG8PkU= +github.com/go-chat-bot/plugins v0.0.0-20210423135617-90af38949890/go.mod h1:Ga63x4EC4NFYr/KGzhn8D8fLj89sfJA/dpBsuowiHOQ= +github.com/go-chat-bot/plugins-br v0.0.0-20200917130500-b69d8e0f9584 h1:6i1FuHDlNZ6OtsEeE6GKa7e078pbXafYbnvo1PG3WHw= +github.com/go-chat-bot/plugins-br v0.0.0-20200917130500-b69d8e0f9584/go.mod h1:KU0Ieo/D/HBwPY6n3tLWanM5GemW6iWXdbgm96qRW2Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU= +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= +github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 h1:4EZlYQIiyecYJlUbVkFXCXHz1QPhVXcHnQKAzBTPfQo= +github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4/go.mod h1:lEO7XoHJ/xNRBCxrn4h/CEB67h0kW1B0t4ooP2yrjUA= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/martinusso/go-docs v0.0.0-20161215163720-81905d575a58 h1:VmcrkkMjTdCGOsuuMnn7P2X9dGh3meUNASx6kHIpe7A= +github.com/martinusso/go-docs v0.0.0-20161215163720-81905d575a58/go.mod h1:QymHbiLXXhrSGV5xTWYfEBt9mau3hHwVOT9Y7tpolJU= +github.com/martinusso/go-docs v1.0.0 h1:4NTC5WegPzZAl51EM8V8Y78HjC3adRu3qfJIW9Q2idQ= +github.com/martinusso/go-docs v1.0.0/go.mod h1:QymHbiLXXhrSGV5xTWYfEBt9mau3hHwVOT9Y7tpolJU= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mozillazg/go-unidecode v0.1.1 h1:uiRy1s4TUqLbcROUrnCN/V85Jlli2AmDF6EeAXOeMHE= +github.com/mozillazg/go-unidecode v0.1.1/go.mod h1:fYMdhyjni9ZeEmS6OE/GJHDLsF8TQvIVDwYR/drR26Q= +github.com/onsi/ginkgo v1.2.1-0.20160409220416-2c2e9bb47b4e/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.2.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/pyinx/gorocket v0.0.0-20170810024322-78ae1353729f h1:N1r6pSlez3lLsqaNHbtrHW9ZuzrilETIabr9jPNj3Zs= +github.com/pyinx/gorocket v0.0.0-20170810024322-78ae1353729f/go.mod h1:nh/AiOs8vRCaqnSOHVzyta23ZLm5ck/st4brrxtQJEo= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/slack-go/slack v0.6.4 h1:cxOqFgM5RW6mdEyDqAJutFk3qiORK9oHRKi5bPqkY9o= +github.com/slack-go/slack v0.6.4/go.mod h1:sGRjv3w+ERAUMMMbldHObQPBcNSyVB7KLKYfnwUFBfw= +github.com/slack-go/slack v0.10.1 h1:BGbxa0kMsGEvLOEoZmYs8T1wWfoZXwmQFBb6FgYCXUA= +github.com/slack-go/slack v0.10.1/go.mod h1:wWL//kk0ho+FcQXcBTmEafUI5dz4qz5f4mMk8oIkioQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= +github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= +github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= +github.com/thoj/go-ircevent v0.0.0-20180816043103-14f3614f28c3 h1:389FrrKIAlxqQMTscCQ7VH3JAVuxb/pe53v2LBiA7z8= +github.com/thoj/go-ircevent v0.0.0-20180816043103-14f3614f28c3/go.mod h1:QYOctLs5qEsaIrA/PKEc4YqAv2SozbxNEX0vMPs84p4= +github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 h1:l/T7dYuJEQZOwVOpjIXr1180aM9PZL/d1MnMVIxefX4= +github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64/go.mod h1:Q1NAJOuRdQCqN/VIWdnaaEhV8LpeO2rtlBP7/iDJNII= +github.com/trivago/tgo v1.0.5 h1:ihzy8zFF/LPsd8oxsjYOE8CmyOTNViyFCy0EaFreUIk= +github.com/trivago/tgo v1.0.5/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc= +github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM= +github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.1 h1:8dP3SGL7MPB94crU3bEPplMPe83FI4EouesJUeFHv50= +go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20171107184841-a337091b0525/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127074510-2fabfed7e28f h1:o66Bv9+w/vuk7Krcig9jZqD01FP7BL8OliFqqw0xzPI= +golang.org/x/net v0.0.0-20220127074510-2fabfed7e28f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190912141932-bc967efca4b8 h1:41hwlulw1prEMBxLQSlMSux1zxJf07B3WPsdjJlKZxE= +golang.org/x/sys v0.0.0-20190912141932-bc967efca4b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.1.1-0.20171102192421-88f656faf3f3/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.10.0 h1:7tmAxx3oKE98VMZ+SBZzvYYWRQ9HODBxmC8mXUsraSQ= +google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.64.0/go.mod h1:931CdxA8Rm4t6zqTFGSsgwbAEZ2+GMYurbndwSimebM= +google.golang.org/api v0.65.0 h1:MTW9c+LIBAbwoS1Gb+YV7NjFBt2f7GtAS5hIzh2NjgQ= +google.golang.org/api v0.65.0/go.mod h1:ArYhxgGadlWmqO1IqVujw6Cs8IdD33bTmzKo2Sh+cbg= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51 h1:Ex1mq5jaJof+kRnYi3SlYJ8KKa9Ao3NHyIT5XJ1gF6U= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211019152133-63b7e35f4404/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350 h1:YxHp5zqIcAShDEvRr5/0rVESVS+njYF68PSdazrNLJo= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1 h1:q4XQuHFC6I28BKZpo6IYyb3mNO+l7lSOxRuYTCiDfXk= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/telegram-bot-api.v3 v3.0.0 h1:Y6QmqOMwRKv5NUdlvzEBtEZChjsrqdTS6O858cvuCww= +gopkg.in/telegram-bot-api.v3 v3.0.0/go.mod h1:WxP4rAHcQNrXhQLGIK9aVLkpygV4Qq8YS3yjjJ/0VLA= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/google-chat/chat_msg.go b/google-chat/chat_msg.go new file mode 100644 index 0000000..13a5f7c --- /dev/null +++ b/google-chat/chat_msg.go @@ -0,0 +1,59 @@ +package googlechat + +import "time" + +// ChatMessage is message type from Pub/Sub events +type ChatMessage struct { + Type string `json:"type"` + EventTime time.Time `json:"eventTime"` + Token string `json:"token"` + Message struct { + Name string `json:"name"` + Sender struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + AvatarURL string `json:"avatarUrl"` + Email string `json:"email"` + Type string `json:"type"` + } `json:"sender"` + CreateTime time.Time `json:"createTime"` + Text string `json:"text"` + Thread struct { + Name string `json:"name"` + RetentionSettings struct { + State string `json:"state"` + } `json:"retentionSettings"` + } `json:"thread"` + Space struct { + Name string `json:"name"` + Type string `json:"type"` + } `json:"space"` + ArgumentText string `json:"argumentText"` + } `json:"message"` + User struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + AvatarURL string `json:"avatarUrl"` + Email string `json:"email"` + Type string `json:"type"` + } `json:"user"` + Space struct { + Name string `json:"name"` + Type string `json:"type"` + DisplayName string `json:"displayName"` + } `json:"space"` + ConfigCompleteRedirectURL string `json:"configCompleteRedirectUrl"` +} + +// ReplyThread is a part of reply messages +type ReplyThread struct { + Name string `json:"name,omitempty"` +} + +// ReplyMessage is partial hangouts format of messages used +// For details see +// https://developers.google.com/hangouts/chat/reference/rest/v1/spaces.messages#Message +type ReplyMessage struct { + Text string `json:"text"` + Thread *ReplyThread `json:"thread,omitempty"` +} diff --git a/google-chat/google-chat.go b/google-chat/google-chat.go new file mode 100644 index 0000000..7b2a09c --- /dev/null +++ b/google-chat/google-chat.go @@ -0,0 +1,168 @@ +package googlechat + +import ( + "bytes" + "context" + "encoding/json" + "log" + "net/http" + "strings" + "time" + + "cloud.google.com/go/pubsub" + "github.com/go-chat-bot/bot" + "golang.org/x/oauth2/google" +) + +const ( + chatAuthScope = "https://www.googleapis.com/auth/chat.bot" + apiEndpoint = "https://chat.googleapis.com/v1/" + protocol = "googlechat" + server = "chat.google.com" +) + +var ( + httpChatClient *http.Client + b *bot.Bot +) + +// Config must contain basic configuration for the bot to be able to work +type Config struct { + PubSubProject string + TopicName string + SubscriptionName string + WelcomeMessage string +} + +func responseHandler(target string, message string, sender *bot.User) { + var space, thread string + + // this define thread in the reply if we can so we don't alwayus start new + targets := strings.Split(target, ":") + if len(targets) < 2 { + space = target + } else { + space = targets[0] + thread = targets[1] + } + + reply, err := json.Marshal(&ReplyMessage{ + Text: message, + Thread: &ReplyThread{ + Name: thread}}) + + log.Printf("Replying: space: %s thread: %s message: %s\n", + space, thread, message) + resp, err := httpChatClient.Post(apiEndpoint+space+"/messages", + "application/json", + bytes.NewReader(reply)) + if err != nil { + log.Printf("Error posting reply: %v", err) + } + + log.Printf("Response: %s\n", resp.Status) +} + +// Run reads the config, establishes OAuth connection & Pub/Sub subscription to +// the message queue +func Run(config *Config) { + var err error + ctx := context.Background() + httpChatClient, err = google.DefaultClient(ctx, chatAuthScope) + if err != nil { + log.Printf("Error setting http client: %v\n", err) + return + } + + client, err := pubsub.NewClient(ctx, config.PubSubProject) + if err != nil { + log.Printf("Error creating client: %v\n", err) + return + } + + topic := client.Topic(config.TopicName) + + // Create a new subscription to the previously created topic + // with the given name. + sub := client.Subscription(config.SubscriptionName) + ok, err := sub.Exists(ctx) + if err != nil { + log.Printf("Error getting subscription: %v\n", err) + return + } + if !ok { + // Subscription doesn't exist. + sub, err = client.CreateSubscription(ctx, config.SubscriptionName, + pubsub.SubscriptionConfig{ + Topic: topic, + AckDeadline: 10 * time.Second, + }) + if err != nil { + log.Printf("Error subscribing: %v\n", err) + return + } + } + + b = bot.New(&bot.Handlers{ + Response: responseHandler, + }, + &bot.Config{ + Protocol: protocol, + Server: server, + }, + ) + + err = sub.Receive(context.Background(), + func(ctx context.Context, m *pubsub.Message) { + var msg ChatMessage + err = json.Unmarshal(m.Data, &msg) + if err != nil { + log.Printf("Failed message unmarshal(%v): %s\n", err, m.Data) + m.Ack() + return + } + + log.Printf("Space: %s (%s)\n", msg.Space.Name, msg.Space.DisplayName) + log.Printf("Message type: %s\n", msg.Type) + log.Printf("From: %s (%s)\n", msg.User.Name, msg.User.DisplayName) + switch msg.Type { + case "ADDED_TO_SPACE": + if config.WelcomeMessage != "" { + log.Printf("Sending welcome message to %s\n", msg.Space.Name) + b.SendMessage(bot.OutgoingMessage{ + Target: msg.Space.Name, + Message: config.WelcomeMessage, + }) + } + case "REMOVED_FROM_SPACE": + break + case "MESSAGE": + log.Printf("Message: %s\n", msg.Message.ArgumentText) + b.MessageReceived( + &bot.ChannelData{ + Protocol: protocol, + Server: server, + HumanName: msg.Space.DisplayName, + Channel: msg.Space.Name + ":" + msg.Message.Thread.Name, + IsPrivate: msg.Space.Type == "DM", + }, + &bot.Message{ + Text: msg.Message.ArgumentText, + IsAction: false, + }, + &bot.User{ + ID: msg.User.Name, + Nick: msg.User.DisplayName, + RealName: msg.User.DisplayName, + }) + } + + m.Ack() + }) + if err != nil { + log.Printf("Error setting up receiving: %v\n", err) + return + } + // Wait indefinetely + select {} +} diff --git a/help.go b/help.go index a02c2f6..4bf2f10 100644 --- a/help.go +++ b/help.go @@ -14,7 +14,10 @@ const ( ) func (b *Bot) help(c *Cmd) { - cmd, _ := parse(CmdPrefix+c.RawArgs, c.ChannelData, c.User) + msg := &Message{ + Text: CmdPrefix + c.RawArgs, + } + cmd, _ := parse(msg, c.ChannelData, c.User) if cmd == nil { b.showAvailabeCommands(c.Channel, c.User) return @@ -31,9 +34,17 @@ func (b *Bot) help(c *Cmd) { func (b *Bot) showHelp(c *Cmd, help *customCommand) { if help.Description != "" { - b.handlers.Response(c.Channel, fmt.Sprintf(helpDescripton, help.Description), c.User) + b.SendMessage(OutgoingMessage{ + Target: c.Channel, + Message: fmt.Sprintf(helpDescripton, help.Description), + Sender: c.User, + }) } - b.handlers.Response(c.Channel, fmt.Sprintf(helpUsage, CmdPrefix, c.Command, help.ExampleArgs), c.User) + b.SendMessage(OutgoingMessage{ + Target: c.Channel, + Message: fmt.Sprintf(helpUsage, CmdPrefix, c.Command, help.ExampleArgs), + Sender: c.User, + }) } func (b *Bot) showAvailabeCommands(channel string, sender *User) { @@ -41,6 +52,14 @@ func (b *Bot) showAvailabeCommands(channel string, sender *User) { for k := range commands { cmds = append(cmds, k) } - b.handlers.Response(channel, fmt.Sprintf(helpAboutCommand, CmdPrefix), sender) - b.handlers.Response(channel, fmt.Sprintf(availableCommands, strings.Join(cmds, ", ")), sender) + b.SendMessage(OutgoingMessage{ + Target: channel, + Message: fmt.Sprintf(helpAboutCommand, CmdPrefix), + Sender: sender, + }) + b.SendMessage(OutgoingMessage{ + Target: channel, + Message: fmt.Sprintf(availableCommands, strings.Join(cmds, ", ")), + Sender: sender, + }) } diff --git a/irc/irc.go b/irc/irc.go index 0fb0075..b5b62f4 100644 --- a/irc/irc.go +++ b/irc/irc.go @@ -3,7 +3,9 @@ package irc import ( "crypto/tls" + "fmt" "log" + "regexp" "strings" "github.com/go-chat-bot/bot" @@ -16,6 +18,7 @@ type Config struct { Channels []string // Channels to connect. Ex: []string{"#go-bot", "#channel mypassword"} User string // The IRC username the bot will use Nick string // The nick the bot will use + RealName string // The real name (longer string) the bot will use Password string // Server password UseTLS bool // Should connect using TLS? TLSServerName string // Must supply if UseTLS is true @@ -23,11 +26,14 @@ type Config struct { } var ( - ircConn *ircevent.Connection - config *Config - b *bot.Bot + ircConn *ircevent.Connection + config *Config + b *bot.Bot + nickStartRE *regexp.Regexp ) +const protocol = "irc" + func responseHandler(target string, message string, sender *bot.User) { channel := target if ircConn.GetNick() == target { @@ -44,12 +50,12 @@ func responseHandler(target string, message string, sender *bot.User) { func onPRIVMSG(e *ircevent.Event) { b.MessageReceived( &bot.ChannelData{ - Protocol: "irc", + Protocol: protocol, Server: ircConn.Server, Channel: e.Arguments[0], IsPrivate: e.Arguments[0] == ircConn.GetNick()}, &bot.Message{ - Text: e.Message(), + Text: nickStartRE.ReplaceAllString(e.Message(), ""), }, &bot.User{ ID: e.Host, @@ -60,7 +66,7 @@ func onPRIVMSG(e *ircevent.Event) { func onCTCPACTION(e *ircevent.Event) { b.MessageReceived( &bot.ChannelData{ - Protocol: "irc", + Protocol: protocol, Server: ircConn.Server, Channel: e.Arguments[0], IsPrivate: false, @@ -90,12 +96,14 @@ func onWelcome(e *ircevent.Event) { } } -// Run reads the Config, connect to the specified IRC server and starts the bot. -// The bot will automatically join all the channels specified in the configuration -func Run(c *Config) { +// SetUp returns a bot for irc according to the Config, but does not run it. +// When you are ready to run the bot, call Run(nil). +// This is useful if you need a pointer to the bot, otherwise you can simply call Run(). +func SetUp(c *Config) *bot.Bot { config = c ircConn = ircevent.IRC(c.User, c.Nick) + ircConn.RealName = c.RealName ircConn.Password = c.Password ircConn.UseTLS = c.UseTLS ircConn.TLSConfig = &tls.Config{ @@ -105,13 +113,35 @@ func Run(c *Config) { b = bot.New(&bot.Handlers{ Response: responseHandler, - }) + }, + &bot.Config{ + Protocol: protocol, + Server: c.Server, + }, + ) + // prepare regex to strip from messages - nick followed by colon/comma and spaces + nickStartRE = regexp.MustCompile(fmt.Sprintf("%s[,:] *", c.Nick)) ircConn.AddCallback("001", onWelcome) ircConn.AddCallback("PRIVMSG", onPRIVMSG) ircConn.AddCallback("CTCP_ACTION", onCTCPACTION) - err := ircConn.Connect(c.Server) + return b +} + +// SetUpConn wraps SetUp and returns ircConn in addition to bot +func SetUpConn(c *Config) (*bot.Bot, *ircevent.Connection) { + return SetUp(c), ircConn +} + +// Run reads the Config, connect to the specified IRC server and starts the bot. +// The bot will automatically join all the channels specified in the configuration +func Run(c *Config) { + if c != nil { + SetUp(c) + } + + err := ircConn.Connect(config.Server) if err != nil { log.Fatal(err) } diff --git a/parser.go b/parser.go index b3debbd..6873fd5 100644 --- a/parser.go +++ b/parser.go @@ -4,38 +4,48 @@ import ( "errors" "regexp" "strings" + "unicode" "github.com/mattn/go-shellwords" + unidecode "github.com/mozillazg/go-unidecode" ) var ( re = regexp.MustCompile("\\s+") // Matches one or more spaces ) -func parse(s string, channel *ChannelData, user *User) (*Cmd, error) { - c := &Cmd{Raw: s} - s = strings.TrimSpace(s) +func parse(m *Message, channel *ChannelData, user *User) (*Cmd, error) { + s := strings.TrimSpace(m.Text) if !strings.HasPrefix(s, CmdPrefix) { return nil, nil } - c.Channel = strings.TrimSpace(channel.Channel) - c.ChannelData = channel - c.User = user - - // Trim the prefix and extra spaces - c.Message = strings.TrimPrefix(s, CmdPrefix) - c.Message = strings.TrimSpace(c.Message) + c := &Cmd{ + Channel: strings.TrimSpace(channel.Channel), + ChannelData: channel, + Message: strings.TrimSpace(strings.TrimPrefix(s, CmdPrefix)), + Raw: m.Text, + User: user, + } // check if we have the command and not only the prefix if c.Message == "" { return nil, nil } + firstOccurrence := true + firstUnicodeSpace := func(c rune) bool { + isFirstSpace := unicode.IsSpace(c) && firstOccurrence + if isFirstSpace { + firstOccurrence = false + } + return isFirstSpace + } + // get the command - pieces := strings.SplitN(c.Message, " ", 2) - c.Command = pieces[0] + pieces := strings.FieldsFunc(c.Message, firstUnicodeSpace) + c.Command = strings.ToLower(unidecode.Unidecode(pieces[0])) if len(pieces) > 1 { // get the arguments and remove extra spaces @@ -47,9 +57,8 @@ func parse(s string, channel *ChannelData, user *User) (*Cmd, error) { c.Args = parsedArgs } - c.MessageData = &Message{ - Text: c.Message, - } + m.Text = c.Message + c.MessageData = m return c, nil } diff --git a/parser_test.go b/parser_test.go index 435d127..8d81d7d 100644 --- a/parser_test.go +++ b/parser_test.go @@ -11,6 +11,10 @@ func TestParser(t *testing.T) { cmdWithoutArgs := CmdPrefix + "cmd" cmdWithArgs := CmdPrefix + "cmd arg1 arg2 " cmdWithQuotes := CmdPrefix + "cmd \"arg1 arg2\"" + cmdMixedCaseWithoutArgs := CmdPrefix + "Cmd" + cmdMixedCaseWithArgs := CmdPrefix + "Cmd arg1 arg2 " + cmdMixedCaseWithQuotes := CmdPrefix + "Cmd \"arg1 arg2\"" + cmdUnicode := CmdPrefix + "Çmd" tests := []struct { msg string @@ -50,67 +54,113 @@ func TestParser(t *testing.T) { Args: []string{"arg1 arg2"}, MessageData: &Message{Text: strings.TrimLeft("cmd \"arg1 arg2\"", CmdPrefix)}, }}, + {cmdMixedCaseWithoutArgs, &Cmd{ + Raw: cmdMixedCaseWithoutArgs, + Command: "cmd", + Channel: channel.Channel, + ChannelData: channel, + User: user, + Message: "Cmd", + MessageData: &Message{Text: strings.TrimLeft("Cmd", CmdPrefix)}, + }}, + {cmdMixedCaseWithArgs, &Cmd{ + Raw: cmdMixedCaseWithArgs, + Command: "cmd", + Channel: channel.Channel, + ChannelData: channel, + User: user, + Message: "Cmd arg1 arg2", + RawArgs: "arg1 arg2", + Args: []string{"arg1", "arg2"}, + MessageData: &Message{Text: strings.TrimLeft("Cmd arg1 arg2", CmdPrefix)}, + }}, + {cmdMixedCaseWithQuotes, &Cmd{ + Raw: cmdMixedCaseWithQuotes, + Command: "cmd", + Channel: channel.Channel, + ChannelData: channel, + User: user, + Message: "Cmd \"arg1 arg2\"", + RawArgs: "\"arg1 arg2\"", + Args: []string{"arg1 arg2"}, + MessageData: &Message{Text: strings.TrimLeft("Cmd \"arg1 arg2\"", CmdPrefix)}, + }}, + {cmdUnicode, &Cmd{ + Raw: cmdUnicode, + Command: "cmd", + Channel: channel.Channel, + ChannelData: channel, + User: user, + Message: "Çmd", + MessageData: &Message{Text: strings.TrimLeft("Çmd", CmdPrefix)}, + }}, } for _, test := range tests { - cmd, _ := parse(test.msg, channel, user) - if test.expected != nil && cmd != nil { - if test.expected.Raw != cmd.Raw { - t.Errorf("Expected Raw:\n%#v\ngot:\n%#v", test.expected.Raw, cmd.Raw) - } - if test.expected.Channel != cmd.Channel { - t.Errorf("Expected Channel:\n%#v\ngot:\n%#v", test.expected.Channel, cmd.Channel) - } - if test.expected.Message != cmd.Message { - t.Errorf("Expected Message:\n%#v\ngot:\n%#v", test.expected.Message, cmd.Message) - } - if test.expected.Command != cmd.Command { - t.Errorf("Expected Command:\n%#v\ngot:\n%#v", test.expected.Command, cmd.Command) - } - if test.expected.RawArgs != cmd.RawArgs { - t.Errorf("Expected RawArgs:\n%#v\ngot:\n%#v", test.expected.RawArgs, cmd.RawArgs) - } - if test.expected.ChannelData.Protocol != cmd.ChannelData.Protocol { - t.Errorf("Expected ChannelData.Protocol:\n%#v\ngot:\n%#v", test.expected.ChannelData.Protocol, cmd.ChannelData.Protocol) - } - if test.expected.ChannelData.Server != cmd.ChannelData.Server { - t.Errorf("Expected ChannelData.Server:\n%#v\ngot:\n%#v", test.expected.ChannelData.Server, cmd.ChannelData.Server) - } - if test.expected.ChannelData.Channel != cmd.ChannelData.Channel { - t.Errorf("Expected ChannelData.Channel:\n%#v\ngot:\n%#v", test.expected.ChannelData.Channel, cmd.ChannelData.Channel) - } - if test.expected.ChannelData.IsPrivate != cmd.ChannelData.IsPrivate { - t.Errorf("Expected ChannelData.IsPrivate:\n%#v\ngot:\n%#v", test.expected.ChannelData.IsPrivate, cmd.ChannelData.IsPrivate) - } - if test.expected.User.ID != cmd.User.ID { - t.Errorf("Expected User.ID:\n%#v\ngot:\n%#v", test.expected.User.ID, cmd.User.ID) - } - if test.expected.User.Nick != cmd.User.Nick { - t.Errorf("Expected User.Nick:\n%#v\ngot:\n%#v", test.expected.User.Nick, cmd.User.Nick) - } - if test.expected.User.RealName != cmd.User.RealName { - t.Errorf("Expected User.RealName:\n%#v\ngot:\n%#v", test.expected.User.RealName, cmd.User.RealName) - } - if test.expected.User.IsBot != cmd.User.IsBot { - t.Errorf("Expected User.IsBot:\n%#v\ngot:\n%#v", test.expected.User.IsBot, cmd.User.IsBot) - } - if test.expected.MessageData.Text != cmd.MessageData.Text { - t.Errorf("Expected MessageData.Text:\n%#v\ngot:\n%#v", test.expected.MessageData.Text, cmd.MessageData.Text) - } - if test.expected.MessageData.IsAction != cmd.MessageData.IsAction { - t.Errorf("Expected MessageData.IsAction:\n%#v\ngot:\n%#v", test.expected.MessageData.IsAction, cmd.MessageData.IsAction) - } - for i, arg := range test.expected.Args { - if arg != cmd.Args[i] { - t.Errorf("Expected cmd.Args[]:\n%#v\ngot:\n%#v", arg, cmd.Args[i]) + t.Run(test.msg, func(t *testing.T) { + cmd, _ := parse(&Message{Text: test.msg}, channel, user) + if test.expected != nil && cmd != nil { + if test.expected.Raw != cmd.Raw { + t.Errorf("Expected Raw:\n%#v\ngot:\n%#v", test.expected.Raw, cmd.Raw) + } + if test.expected.Channel != cmd.Channel { + t.Errorf("Expected Channel:\n%#v\ngot:\n%#v", test.expected.Channel, cmd.Channel) + } + if test.expected.Message != cmd.Message { + t.Errorf("Expected Message:\n%#v\ngot:\n%#v", test.expected.Message, cmd.Message) + } + if test.expected.Command != cmd.Command { + t.Errorf("Expected Command:\n%#v\ngot:\n%#v", test.expected.Command, cmd.Command) + } + if test.expected.RawArgs != cmd.RawArgs { + t.Errorf("Expected RawArgs:\n%#v\ngot:\n%#v", test.expected.RawArgs, cmd.RawArgs) + } + if test.expected.ChannelData.Protocol != cmd.ChannelData.Protocol { + t.Errorf("Expected ChannelData.Protocol:\n%#v\ngot:\n%#v", test.expected.ChannelData.Protocol, cmd.ChannelData.Protocol) + } + if test.expected.ChannelData.Server != cmd.ChannelData.Server { + t.Errorf("Expected ChannelData.Server:\n%#v\ngot:\n%#v", test.expected.ChannelData.Server, cmd.ChannelData.Server) + } + if test.expected.ChannelData.Channel != cmd.ChannelData.Channel { + t.Errorf("Expected ChannelData.Channel:\n%#v\ngot:\n%#v", test.expected.ChannelData.Channel, cmd.ChannelData.Channel) + } + if test.expected.ChannelData.IsPrivate != cmd.ChannelData.IsPrivate { + t.Errorf("Expected ChannelData.IsPrivate:\n%#v\ngot:\n%#v", test.expected.ChannelData.IsPrivate, cmd.ChannelData.IsPrivate) + } + if test.expected.User.ID != cmd.User.ID { + t.Errorf("Expected User.ID:\n%#v\ngot:\n%#v", test.expected.User.ID, cmd.User.ID) + } + if test.expected.User.Nick != cmd.User.Nick { + t.Errorf("Expected User.Nick:\n%#v\ngot:\n%#v", test.expected.User.Nick, cmd.User.Nick) + } + if test.expected.User.RealName != cmd.User.RealName { + t.Errorf("Expected User.RealName:\n%#v\ngot:\n%#v", test.expected.User.RealName, cmd.User.RealName) + } + if test.expected.User.IsBot != cmd.User.IsBot { + t.Errorf("Expected User.IsBot:\n%#v\ngot:\n%#v", test.expected.User.IsBot, cmd.User.IsBot) + } + if test.expected.MessageData.Text != cmd.MessageData.Text { + t.Errorf("Expected MessageData.Text:\n%#v\ngot:\n%#v", test.expected.MessageData.Text, cmd.MessageData.Text) + } + if test.expected.MessageData.IsAction != cmd.MessageData.IsAction { + t.Errorf("Expected MessageData.IsAction:\n%#v\ngot:\n%#v", test.expected.MessageData.IsAction, cmd.MessageData.IsAction) + } + for i, arg := range test.expected.Args { + if arg != cmd.Args[i] { + t.Errorf("Expected cmd.Args[]:\n%#v\ngot:\n%#v", arg, cmd.Args[i]) + } } } - } + }) } } func TestInvalidArguments(t *testing.T) { - cmd, err := parse("!cmd Invalid \"arg", &ChannelData{Channel: "#go-bot"}, &User{Nick: "user123"}) + cmd, err := parse( + &Message{Text: "!cmd Invalid \"arg"}, + &ChannelData{Channel: "#go-bot"}, + &User{Nick: "user123"}, + ) if err == nil { t.Error("Expected error, got nil") } diff --git a/rocket/rocket.go b/rocket/rocket.go index 33edad6..6d37f39 100644 --- a/rocket/rocket.go +++ b/rocket/rocket.go @@ -2,10 +2,11 @@ package rocket import ( "fmt" + "log" + "github.com/go-chat-bot/bot" "github.com/pyinx/gorocket/api" "github.com/pyinx/gorocket/rest" - "log" ) var ( @@ -13,6 +14,10 @@ var ( config *Config ) +const ( + protocol = "rocket" +) + // Config must contain the necessary data to connect to an rocket.chat server type Config struct { Server string @@ -49,12 +54,18 @@ func Run(c *Config) { client = rest.NewClient(config.Server, config.Port, config.UseTLS, config.Debug) err := client.Login(api.UserCredentials{Email: config.Email, Name: config.User, Password: config.Password}) if err != nil { - log.Fatal("login err: %s\n", err) + log.Fatalf("login err: %s\n", err) } b := bot.New(&bot.Handlers{ Response: responseHandler, - }) + }, + &bot.Config{ + Protocol: protocol, + Server: config.Server, + }, + ) + b.Disable([]string{"url"}) msgChan := client.GetAllMessages() @@ -65,12 +76,12 @@ func Run(c *Config) { if !ownMessage(c, msg) { b.MessageReceived( &bot.ChannelData{ - Protocol: "rocket", + Protocol: protocol, Server: "", Channel: msg.ChannelId, IsPrivate: false, }, - &bot.Message{Text: msg.Text, IsAction: true}, + &bot.Message{Text: msg.Text}, &bot.User{ID: msg.User.Id, RealName: msg.User.UserName, Nick: msg.User.UserName, IsBot: false}) } diff --git a/slack/slack.go b/slack/slack.go index 9368791..6a5c2e4 100644 --- a/slack/slack.go +++ b/slack/slack.go @@ -5,21 +5,91 @@ import ( "fmt" "github.com/go-chat-bot/bot" - "github.com/nlopes/slack" + "github.com/slack-go/slack" ) +// MessageFilter allows implementing a filter function to transform the messages +// before sending to the channel, it is run before the bot sends the message to slack +type MessageFilter func(string, *bot.User) (string, slack.PostMessageParameters) + var ( rtm *slack.RTM api *slack.Client teaminfo *slack.TeamInfo - channelList = map[string]slack.Channel{} - params = slack.PostMessageParameters{AsUser: true} - botUserID = "" + channelList = map[string]slack.Channel{} + params = slack.PostMessageParameters{AsUser: true} + messageFilter MessageFilter = defaultMessageFilter + botUserID = "" ) +const protocol = "slack" + +func defaultMessageFilter(message string, _ *bot.User) (string, slack.PostMessageParameters) { + return message, params +} + func responseHandler(target string, message string, sender *bot.User) { - api.PostMessage(target, message, params) + message, params := messageFilter(message, sender) + _, _, err := api.PostMessage( + target, + slack.MsgOptionPostMessageParameters(params), + slack.MsgOptionText(message, false), + ) + if err != nil { + fmt.Printf("Error sending a slack message: %s\n", err.Error()) + } +} + +func responseHandlerV2(om bot.OutgoingMessage) { + message, params := messageFilter(om.Message, om.Sender) + if pmp, ok := om.ProtoParams.(*slack.PostMessageParameters); ok { + params = *pmp + } + _, _, err := api.PostMessage( + om.Target, + slack.MsgOptionPostMessageParameters(params), + slack.MsgOptionText(message, false), + ) + if err != nil { + fmt.Printf("Error sending a slack message: %s\n", err.Error()) + } +} + +// AddReactionToMessage allows you to add a reaction, to a message. +func AddReactionToMessage(msgid, channel string, reaction string) error { + toReact := slack.ItemRef{ + Timestamp: msgid, + Channel: channel, + } + + return api.AddReaction(reaction, toReact) +} + +// RemoveReactionFromMessage allows you to remove a reaction, from a message. +func RemoveReactionFromMessage(msgid, channel string, reaction string) error { + reactionRef := slack.ItemRef{ + Timestamp: msgid, + Channel: channel, + } + + return api.RemoveReaction(reaction, reactionRef) +} + +// FindUserBySlackID converts a slack.User into a bot.User struct +func FindUserBySlackID(userID string) *bot.User { + slackUser, err := api.GetUserInfo(userID) + if err != nil { + fmt.Printf("Error retrieving slack user: %s\n", err) + return &bot.User{ + ID: userID, + IsBot: false} + } + return &bot.User{ + ID: userID, + Nick: slackUser.Name, + RealName: slackUser.Profile.RealName, + IsBot: slackUser.IsBot} } // Extracts user information from slack API @@ -33,22 +103,18 @@ func extractUser(event *slack.MessageEvent) *bot.User { userID = event.User isBot = false } - slackUser, err := api.GetUserInfo(userID) - if err != nil { - fmt.Printf("Error retrieving slack user: %s\n", err) - return &bot.User{ - ID: userID, - IsBot: isBot} + user := FindUserBySlackID(userID) + if len(user.Nick) == 0 { + user.IsBot = isBot } - return &bot.User{ - ID: userID, - Nick: slackUser.Name, - RealName: slackUser.Profile.RealName, - IsBot: isBot} + + return user } func extractText(event *slack.MessageEvent) *bot.Message { - msg := &bot.Message{} + msg := &bot.Message{ + ProtoMsg: event, + } if len(event.Text) != 0 { msg.Text = event.Text if event.SubType == "me_message" { @@ -73,7 +139,8 @@ func readBotInfo(api *slack.Client) { } func readChannelData(api *slack.Client) { - channels, err := api.GetChannels(true) + params := slack.GetConversationsParameters{} + channels, _, err := api.GetConversations(¶ms) if err != nil { fmt.Printf("Error getting Channels: %s\n", err) return @@ -87,6 +154,16 @@ func ownMessage(UserID string) bool { return botUserID == UserID } +// RunWithFilter executes the bot and sets up a message filter which will +// receive all the messages before they are sent to slack +func RunWithFilter(token string, customMessageFilter MessageFilter) { + if customMessageFilter == nil { + panic("A valid message filter must be provided.") + } + messageFilter = customMessageFilter + Run(token) +} + // Run connects to slack RTM API using the provided token func Run(token string) { api = slack.New(token) @@ -94,8 +171,15 @@ func Run(token string) { teaminfo, _ = api.GetTeamInfo() b := bot.New(&bot.Handlers{ - Response: responseHandler, - }) + Response: responseHandler, + ResponseV2: responseHandlerV2, + }, + &bot.Config{ + Protocol: protocol, + Server: teaminfo.Domain, + }, + ) + b.Disable([]string{"url"}) go rtm.ManageConnection() @@ -114,22 +198,26 @@ Loop: readChannelData(api) case *slack.MessageEvent: - if !ev.Hidden && !ownMessage(ev.User) { - C := channelList[ev.Channel] - var channel = ev.Channel - if C.IsChannel { - channel = fmt.Sprintf("#%s", C.Name) - } - b.MessageReceived( - &bot.ChannelData{ - Protocol: "slack", - Server: teaminfo.Domain, - Channel: channel, - IsPrivate: !C.IsChannel, - }, - extractText(ev), - extractUser(ev)) + if ev.Hidden || ownMessage(ev.User) { + continue + } + + C := channelList[ev.Channel] + var channel = ev.Channel + if C.IsChannel { + channel = fmt.Sprintf("#%s", C.Name) } + go b.MessageReceived( + &bot.ChannelData{ + Protocol: protocol, + Server: teaminfo.Domain, + Channel: channel, + HumanName: C.Name, + IsPrivate: !C.IsChannel, + }, + extractText(ev), + extractUser(ev), + ) case *slack.RTMError: fmt.Printf("Error: %s\n", ev.Error()) diff --git a/telegram/telegram.go b/telegram/telegram.go index c9f520b..9688d19 100644 --- a/telegram/telegram.go +++ b/telegram/telegram.go @@ -7,13 +7,18 @@ import ( "strings" "github.com/go-chat-bot/bot" - "gopkg.in/telegram-bot-api.v3" + tgbotapi "gopkg.in/telegram-bot-api.v3" ) var ( tg *tgbotapi.BotAPI ) +const ( + protocol = "telegram" + server = "telegram" +) + func responseHandler(target string, message string, sender *bot.User) { id, err := strconv.ParseInt(target, 10, 64) if err != nil { @@ -21,12 +26,11 @@ func responseHandler(target string, message string, sender *bot.User) { return } msg := tgbotapi.NewMessage(id, message) - msg.ReplyToMessageID = msg.ReplyToMessageID tg.Send(msg) } -// Run executes the bot and connects to Telegram using the provided token. Use the debug flag if you wish to see all trafic logged +// Run executes the bot and connects to Telegram using the provided token. Use the debug flag if you wish to see all traffic logged func Run(token string, debug bool) { var err error tg, err = tgbotapi.NewBotAPI(token) @@ -48,13 +52,18 @@ func Run(token string, debug bool) { b := bot.New(&bot.Handlers{ Response: responseHandler, - }) + }, &bot.Config{ + Protocol: protocol, + Server: server, + }, + ) + b.Disable([]string{"url"}) for update := range updates { target := &bot.ChannelData{ - Protocol: "telegram", - Server: "telegram", + Protocol: protocol, + Server: server, Channel: strconv.FormatInt(update.Message.Chat.ID, 10), IsPrivate: update.Message.Chat.IsPrivate()} name := []string{update.Message.From.FirstName, update.Message.From.LastName}