package bot //import "github.com/kr/pretty" import ( "fmt" "os" "os/signal" "regexp" "strings" "syscall" "time" "github.com/mattermost/mattermost-server/v5/model" "github.com/rs/zerolog/log" ) type Bot struct { APIURL string AccountEmail string AccountFirstname string AccountLastname string AccountPassword string AccountUsername string BotName string Buildarch string DebuggingChannelName string TeamName string Version string Commit string WebsocketURL string StartupUnixTime int64 client *model.Client4 webSocketClient *model.WebSocketClient botUser *model.User botTeam *model.Team debuggingChannel *model.Channel } func New(options ...func(s *Bot)) *Bot { b := new(Bot) for _, opt := range options { opt(b) } return b } func (b *Bot) identify() { log.Info(). Str("version", b.Version). Str("buildarch", b.Buildarch). Str("commit", b.Commit). Msg("starting") } func (b *Bot) Main() { println(b.BotName) b.StartupUnixTime = time.Now().Unix() b.SetupGracefulShutdown() b.setupLogging() b.client = model.NewAPIv4Client(b.APIURL) // Lets test to see if the mattermost server is up and running b.MakeSureServerIsRunning() // lets attempt to login to the Mattermost server as the bot user // This will set the token required for all future calls // You can get this token with client.AuthToken b.LoginAsTheBotUser() // If the bot user doesn't have the correct information lets update his profile b.UpdateTheBotUserIfNeeded() // Lets find our bot team b.FindBotTeam() // This is an important step. Lets make sure we use the botTeam // for all future web service requests that require a team. //client.SetTeamId(botTeam.Id) // Lets create a bot channel for logging debug messages into b.CreateBotDebuggingChannelIfNeeded() msg := fmt.Sprintf("_**%s** (version `%s`) is now starting up_", b.BotName, b.Version) b.SendMsgToDebuggingChannel(msg, "") // Lets start listening to some channels via the websocket! var err *model.AppError b.webSocketClient, err = model.NewWebSocketClient4(b.WebsocketURL, b.client.AuthToken) if err != nil { println("We failed to connect to the web socket") PrintError(err) } b.webSocketClient.Listen() go func() { for { select { case resp := <-b.webSocketClient.EventChannel: b.HandleWebSocketResponse(resp) } } }() // You can block forever with select {} } func (b *Bot) MakeSureServerIsRunning() { if props, resp := b.client.GetOldClientConfig(""); resp.Error != nil { println("There was a problem pinging the Mattermost server. Are you sure it's running?") PrintError(resp.Error) os.Exit(1) } else { println("Server detected and is running version " + props["Version"]) } } func (b *Bot) LoginAsTheBotUser() { if user, resp := b.client.Login(b.AccountEmail, b.AccountPassword); resp.Error != nil { println("There was a problem logging into the Mattermost server. Are you sure ran the setup steps from the README.md?") PrintError(resp.Error) os.Exit(1) } else { b.botUser = user } } func (b *Bot) UpdateTheBotUserIfNeeded() { if b.botUser.FirstName != b.AccountFirstname || b.botUser.LastName != b.AccountLastname || b.botUser.Username != b.AccountUsername { b.botUser.FirstName = b.AccountFirstname b.botUser.LastName = b.AccountLastname b.botUser.Username = b.AccountUsername if user, resp := b.client.UpdateUser(b.botUser); resp.Error != nil { println("We failed to update the Bot user account") PrintError(resp.Error) os.Exit(1) } else { b.botUser = user println("Looks like this might be the first run so we've updated the bots account settings") } } } func (b *Bot) FindBotTeam() { if team, resp := b.client.GetTeamByName(b.TeamName, ""); resp.Error != nil { println("We failed to get the initial load") println("or we do not appear to be a member of the team '" + b.TeamName + "'") PrintError(resp.Error) os.Exit(1) } else { b.botTeam = team } } func (b *Bot) CreateBotDebuggingChannelIfNeeded() { if rchannel, resp := b.client.GetChannelByName(b.DebuggingChannelName, b.botTeam.Id, ""); resp.Error != nil { println("We failed to get the channels") PrintError(resp.Error) } else { b.debuggingChannel = rchannel return } // Looks like we need to create the logging channel channel := &model.Channel{} channel.Name = b.DebuggingChannelName channel.DisplayName = "LSV Serious Callers Only" channel.Purpose = "Bot Channel" channel.Type = model.CHANNEL_OPEN channel.TeamId = b.botTeam.Id if rchannel, resp := b.client.CreateChannel(channel); resp.Error != nil { println("We failed to create the channel " + b.DebuggingChannelName) PrintError(resp.Error) } else { b.debuggingChannel = rchannel println("Looks like this might be the first run so we've created the channel " + b.DebuggingChannelName) } } func (b *Bot) SendMsgToChannel(msg string, replyToId string, channelId string) { post := &model.Post{} post.ChannelId = channelId post.Message = msg post.RootId = replyToId if _, resp := b.client.CreatePost(post); resp.Error != nil { println("We failed to send a message to the channel") PrintError(resp.Error) } } func (b *Bot) SendMsgToDebuggingChannel(msg string, replyToId string) { post := &model.Post{} post.ChannelId = b.debuggingChannel.Id post.Message = msg post.RootId = replyToId if _, resp := b.client.CreatePost(post); resp.Error != nil { println("We failed to send a message to the logging channel") PrintError(resp.Error) } } func (b *Bot) HandleWebSocketResponse(event *model.WebSocketEvent) { if event.Event != model.WEBSOCKET_EVENT_POSTED { return } post := model.PostFromJson(strings.NewReader(event.Data["post"].(string))) if post == nil { return } if post.UserId == b.botUser.Id { return } // FIXME check for parts and joins and whatnot and ignore those // check to see if we have been addressed if matched, _ := regexp.MatchString(`^\s*`+b.BotName+`\s*`, post.Message); matched { println("i have been addressed in channel " + post.ChannelId) //b.SendMsgToDebuggingChannel("i have been addressed in channel "+post.ChannelId, "") b.HandleMsgFromChannel(event) return } if matched, _ := regexp.MatchString(`^\s*bot([\,]?)\s*`, post.Message); matched { println("i have been addressed in channel " + post.ChannelId) //b.SendMsgToDebuggingChannel("i have been addressed in channel "+post.ChannelId, "") b.HandleMsgFromChannel(event) return } if event.Broadcast.ChannelId == b.debuggingChannel.Id { b.HandleMsgFromDebuggingChannel(event) return } } func (b *Bot) Shutdown() { syscall.Kill(syscall.Getpid(), syscall.SIGINT) } func (b *Bot) HandleMsgFromChannel(event *model.WebSocketEvent) { post := model.PostFromJson(strings.NewReader(event.Data["post"].(string))) if post == nil { return } if matched, _ := regexp.MatchString(`metar\s+[^\s]+`, post.Message); matched { b.HandleWeatherRequest(post.ChannelId, post.Id, post.Message) return } if matched, _ := regexp.MatchString(`aqi\s+[^\s]+`, post.Message); matched { b.HandleAirQualityRequest(post.ChannelId, post.Id, post.Message) return } if matched, _ := regexp.MatchString(`(?:^|\W)alive(?:$|\W)`, post.Message); matched { b.SendMsgToChannel("yes I'm running", post.Id, post.ChannelId) return } if matched, _ := regexp.MatchString(`(?:^|\W)version(?:$|\W)`, post.Message); matched { msg := fmt.Sprintf("I am running version `%s` from git: https://git.eeqj.de/sneak/sco/src/commit/%s", b.Version, b.Commit) b.SendMsgToChannel(msg, post.Id, post.ChannelId) return } if matched, _ := regexp.MatchString(`(?:^|\W)uptime(?:$|\W)`, post.Message); matched { msg := fmt.Sprintf("running: uptime %d seconds", b.Uptime()) b.SendMsgToChannel(msg, post.Id, post.ChannelId) return } b.SendMsgToChannel("I did not understand your command, sorry", post.Id, post.ChannelId) } func (b *Bot) Uptime() int64 { return (time.Now().Unix() - b.StartupUnixTime) } func (b *Bot) HandleMsgFromDebuggingChannel(event *model.WebSocketEvent) { println("responding to debugging channel msg") post := model.PostFromJson(strings.NewReader(event.Data["post"].(string))) if post == nil { return } if post.Message == "" { // null message, we can probably ignore it. return } if matched, _ := regexp.MatchString(`(?:^|\W)shutdown(?:$|\W)`, post.Message); matched { b.Shutdown() return } // if you see any word matching 'alive' then respond if matched, _ := regexp.MatchString(`(?:^|\W)alive(?:$|\W)`, post.Message); matched { b.SendMsgToDebuggingChannel("Yes I'm running", post.Id) return } // if you see any word matching 'up' then respond if matched, _ := regexp.MatchString(`(?:^|\W)up(?:$|\W)`, post.Message); matched { b.SendMsgToDebuggingChannel("Yes I'm running", post.Id) return } // if you see any word matching 'running' then respond if matched, _ := regexp.MatchString(`(?:^|\W)running(?:$|\W)`, post.Message); matched { b.SendMsgToDebuggingChannel("Yes I'm running", post.Id) return } // if you see any word matching 'hello' then respond if matched, _ := regexp.MatchString(`(?:^|\W)hello(?:$|\W)`, post.Message); matched { b.SendMsgToDebuggingChannel("Yes I'm running", post.Id) return } b.SendMsgToChannel("I did not understand your command, sorry", post.Id, post.ChannelId) } func PrintError(err *model.AppError) { println("\tError Details:") println("\t\t" + err.Message) println("\t\t" + err.Id) println("\t\t" + err.DetailedError) } func (b *Bot) SetupGracefulShutdown() { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) go func() { for _ = range c { msg := fmt.Sprintf("_**%s** (version `%s`) is now shutting down_\n\nuptime was %d secs", b.BotName, b.Version, b.Uptime()) b.SendMsgToDebuggingChannel(msg, "") if b.webSocketClient != nil { b.webSocketClient.Close() } os.Exit(0) } }() }