diff --git a/commands.go b/commands.go index 23b943e..35b5b01 100644 --- a/commands.go +++ b/commands.go @@ -2,70 +2,109 @@ package main import ( "fmt" - "log" "net/url" "strings" "samhofi.us/x/keybase/types/chat1" ) -func (b *bot) setupMeeting(convid chat1.ConvIDStr, sender string, args []string, membersType string) { - b.debug("command recieved in conversation %s", convid) +/* +**** this function is a special case on parameters as it must be called from 2 handlers which +**** get their information from separate types. As a result we're only passing the conversation id. +**** because of this we can't wrap handleWelcome with permissions, not that you'd want to. + */ + +// handleWelcome sends the welcome message to new conversations +func (b *bot) handleWelcome(id chat1.ConvIDStr) { + b.k.SendMessageByConvID(id, "Hello there!! I'm the Jitsi meeting bot, made by @haukened\nI can start Jitsi meetings right here in this chat!\nI can be activated in 2 ways:\n 1. `@jitsibot`\n 2.`!jitsi`\nYou can provide feedback to my humans using:\n 1. `@jitsibot feedback `\n 2. `!jitsibot feedback `\nYou can also join @jitsi_meet to talk about features, enhancements, or talk to live humans! Everyone is welcome!\nI also accept donations to offset hosting costs, just send some XLM to my wallet if you feel like it by typing `+5XLM@jitsibot`\nIf you ever need to see this message again, ask me for help or say hello to me!") +} + +/* +**** all other commands here-below should only accept a single argument of type chat1.MsgSummary +**** in order to be compliant with the permissions wrapper. Anything not should be explicitly notated. + */ + +// handlePayment controls how the bot reacts to wallet payments in chat +func (b *bot) handlePayment(m chat1.MsgSummary) { + // there can be multiple payments on each message, iterate them + for _, payment := range m.Content.Text.Payments { + if strings.Contains(payment.PaymentText, b.k.Username) { + // if the payment is successful put log the payment for wallet closure + if payment.Result.ResultTyp__ == 0 && payment.Result.Error__ == nil { + var replyInfo = botReply{convID: m.ConvID, msgID: m.Id} + b.payments[*payment.Result.Sent__] = replyInfo + b.log("payment recieved %s", payment.PaymentText) + } else { + // if the payment fails, be sad + b.k.ReactByConvID(m.ConvID, m.Id, ":cry:") + } + } + } +} + +// handleMeeting starts a new jitsi meeting +func (b *bot) handleMeeting(m chat1.MsgSummary) { + b.debug("command recieved in conversation %s", m.ConvID) + // currently we aren't sending dial-in information, so don't get it just generate the name + // use the simple method meeting, err := newJitsiMeetingSimple() if err != nil { - log.Println(err) - message := fmt.Sprintf("@%s - I'm sorry, i'm not sure what happened... I was unable to set up a new meeting.\nI've written the appropriate logs and notified my humans.", sender) - b.k.SendMessageByConvID(convid, message) + eid := b.logError(err) + message := fmt.Sprintf("@%s - I'm sorry, i'm not sure what happened... I was unable to set up a new meeting.\nI've written the appropriate logs and notified my humans. Please reference Error ID %s", m.Sender.Username, eid) + b.k.SendMessageByConvID(m.ConvID, message) return } - message := fmt.Sprintf("@%s here's your meeting: %s", sender, meeting.getURL()) - b.k.SendMessageByConvID(convid, message) + b.k.SendMessageByConvID(m.ConvID, "@%s here's your meeting: %s", m.Sender.Username, meeting.getURL()) } -func (b *bot) sendFeedback(convid chat1.ConvIDStr, mesgID chat1.MessageID, sender string, args []string) { - b.debug("feedback recieved in %s", convid) +// handleFeedback sends feedback to a keybase chat, if configured +func (b *bot) handleFeedback(m chat1.MsgSummary) { + b.log("feedback recieved in %s", m.ConvID) if b.config.FeedbackConvIDStr != "" { - feedback := strings.Join(args, " ") + args := strings.Fields(m.Content.Text.Body) + feedback := strings.Join(args[2:], " ") fcID := chat1.ConvIDStr(b.config.FeedbackConvIDStr) - if _, err := b.k.SendMessageByConvID(fcID, "Feedback from @%s:\n```%s```", sender, feedback); err != nil { - b.k.ReplyByConvID(convid, mesgID, "I'm sorry, I was unable to send your feedback because my benevolent overlords have not set a destination for feedback. :sad:") - log.Printf("Unable to send feedback: %s", err) + if _, err := b.k.SendMessageByConvID(fcID, "Feedback from @%s:\n```%s```", m.Sender.Username, feedback); err != nil { + eid := b.logError(err) + b.k.ReactByConvID(m.ConvID, m.Id, "Error ID %s", eid) } else { - b.k.ReplyByConvID(convid, mesgID, "Thanks! Your feedback has been sent to my human overlords!") + b.k.ReplyByConvID(m.ConvID, m.Id, "Thanks! Your feedback has been sent to my human overlords!") } } else { - b.debug("feedback not enabled. set --feedback-convid or BOT_FEEDBACK_CONVID") + b.k.ReplyByConvID(m.ConvID, m.Id, "I'm sorry, I was unable to send your feedback because my benevolent overlords have not set a destination for feedback. :sob:") + b.log("user tried to send feedback, but feedback is not enabled. set --feedback-convid or BOT_FEEDBACK_CONVID") } } -func (b *bot) sendWelcome(convid chat1.ConvIDStr) { - b.k.SendMessageByConvID(convid, "Hello there!! I'm the Jitsi meeting bot, made by @haukened\nI can start Jitsi meetings right here in this chat!\nI can be activated in 2 ways:\n 1. `@jitsibot`\n 2.`!jitsi`\nYou can provide feedback to my humans using:\n 1. `@jitsibot feedback `\n 2. `!jitsibot feedback `\nYou can also join @jitsi_meet to talk about features, enhancements, or talk to live humans! Everyone is welcome!\nI also accept donations to offset hosting costs, just send some XLM to my wallet if you feel like it by typing `+5XLM@jitsibot`\nIf you ever need to see this message again, ask me for help or say hello to me!") -} - -func (b *bot) setKValue(convid chat1.ConvIDStr, msgID chat1.MessageID, args []string) { - if args[0] != "set" { +// handleSetCommand processes all settings SET calls +func (b *bot) handleSetCommand(m chat1.MsgSummary) { + b.debug("%s called set command in %s", m.Sender.Username, m.ConvID) + // first normalize the text and extract the arguments + args := strings.Fields(strings.ToLower(m.Content.Text.Body)) + if args[1] != "set" { return } switch len(args) { - case 3: - if args[1] == "url" { + case 4: + if args[2] == "url" { // first validate the URL - u, err := url.ParseRequestURI(args[2]) + u, err := url.ParseRequestURI(args[3]) if err != nil { - b.k.ReplyByConvID(convid, msgID, "ERROR - `%s`", err) + eid := b.logError(err) + b.k.ReactByConvID(m.ConvID, m.Id, "Error ID %s", eid) return } // then make sure its HTTPS if u.Scheme != "https" { - b.k.ReplyByConvID(convid, msgID, "ERROR - HTTPS Required") + b.k.ReactByConvID(m.ConvID, m.Id, "ERROR: HTTPS Required") return } // then get the current options var opts ConvOptions - err = b.KVStoreGetStruct(convid, &opts) + err = b.KVStoreGetStruct(m.ConvID, &opts) if err != nil { eid := b.logError(err) - b.k.ReactByConvID(convid, msgID, "Error %s", eid) + b.k.ReactByConvID(m.ConvID, m.Id, "Error ID %s", eid) return } // then update the struct using only the scheme and hostname:port @@ -75,13 +114,13 @@ func (b *bot) setKValue(convid chat1.ConvIDStr, msgID chat1.MessageID, args []st opts.CustomURL = fmt.Sprintf("%s://%s/", u.Scheme, u.Hostname()) } // then write that back to kvstore, with revision - err = b.KVStorePutStruct(convid, opts) + err = b.KVStorePutStruct(m.ConvID, opts) if err != nil { eid := b.logError(err) - b.k.ReactByConvID(convid, msgID, "ERROR %s", eid) + b.k.ReactByConvID(m.ConvID, m.Id, "Error ID %s", eid) return } - b.k.ReactByConvID(convid, msgID, "OK!") + b.k.ReactByConvID(m.ConvID, m.Id, "OK!") return } default: @@ -89,7 +128,10 @@ func (b *bot) setKValue(convid chat1.ConvIDStr, msgID chat1.MessageID, args []st } } -func (b *bot) listKValue(convid chat1.ConvIDStr, msgID chat1.MessageID, args []string) { +// handleListCommand lists settings for the conversation +func (b *bot) handleListCommand(m chat1.MsgSummary) { + // first normalize the text and extract the arguments + args := strings.Fields(strings.ToLower(m.Content.Text.Body)) if args[0] != "list" { return } diff --git a/handlers.go b/handlers.go index 7d4405c..3a60446 100644 --- a/handlers.go +++ b/handlers.go @@ -32,61 +32,50 @@ func (b *bot) chatHandler(m chat1.MsgSummary) { } // if this chat message is a payment, add it to the bot payments if m.Content.Text.Payments != nil { - // there can be multiple payments on each message, iterate them - for _, payment := range m.Content.Text.Payments { - if strings.Contains(payment.PaymentText, b.k.Username) { - // if the payment is successful put log the payment for wallet closure - if payment.Result.ResultTyp__ == 0 && payment.Result.Error__ == nil { - var replyInfo = botReply{convID: m.ConvID, msgID: m.Id} - b.payments[*payment.Result.Sent__] = replyInfo - } else { - // if the payment fails, be sad - b.k.ReactByConvID(m.ConvID, m.Id, ":cry:") - } - } - } + b.handlePayment(m) + return } - // Determine first if this is a command + + // if its not a payment evaluate if this is a command at all if strings.HasPrefix(m.Content.Text.Body, "!") || strings.HasPrefix(m.Content.Text.Body, "@") { - // determine the root command - body := strings.ToLower(m.Content.Text.Body) - words := strings.Fields(body) - command := strings.Replace(words[0], "@", "", 1) - command = strings.Replace(command, "!", "", 1) - command = strings.ToLower(command) - // create the args - args := words[1:] - nargs := len(args) - switch command { - case b.k.Username: - if nargs > 0 { - switch args[0] { - case "set": - b.setKValue(m.ConvID, m.Id, args) - case "list": - b.listKValue(m.ConvID, m.Id, args) - } - } - case "jitsi": - if nargs == 0 { - b.setupMeeting(m.ConvID, m.Sender.Username, args, m.Channel.MembersType) - } else if nargs >= 1 { - // pop the subcommand off the front of the list - subcommand, args := args[0], args[1:] - switch subcommand { - case "meet": - b.setupMeeting(m.ConvID, m.Sender.Username, args, m.Channel.MembersType) - case "feedback": - b.sendFeedback(m.ConvID, m.Id, m.Sender.Username, args) - case "hello": - fallthrough - case "help": - b.sendWelcome(m.ConvID) - default: - return - } - } - default: + // first return if its not a command for me + if !strings.Contains(m.Content.Text.Body, b.cmd()) && !strings.Contains(m.Content.Text.Body, b.k.Username) { + return + } + // then check if this is the root command + if isRootCommand(m.Content.Text.Body, b.cmd(), b.k.Username) { + b.handleMeeting(m) + return + } + // then check sub-command variants + // meet + if hasCommandPrefix(m.Content.Text.Body, b.cmd(), b.k.Username, "meet") { + b.handleMeeting(m) + return + } + // feedback + if hasCommandPrefix(m.Content.Text.Body, b.cmd(), b.k.Username, "feedback") { + b.handleFeedback(m) + return + } + // help + if hasCommandPrefix(m.Content.Text.Body, b.cmd(), b.k.Username, "help") { + b.handleWelcome(m.ConvID) + return + } + // hello + if hasCommandPrefix(m.Content.Text.Body, b.cmd(), b.k.Username, "hello") { + b.handleWelcome(m.ConvID) + return + } + // set commands + if hasCommandPrefix(m.Content.Text.Body, b.cmd(), b.k.Username, "set") { + b.handleSetCommand(m) + return + } + // list commands + if hasCommandPrefix(m.Content.Text.Body, b.cmd(), b.k.Username, "list") { + b.handleSetCommand(m) return } } @@ -103,7 +92,7 @@ func (b *bot) convHandler(m chat1.ConvSummary) { default: b.debug("New convID found %s, sending welcome message.", m.Id) } - b.sendWelcome(m.Id) + b.handleWelcome(m.Id) } // this handles wallet events, like when someone send you money in chat diff --git a/main.go b/main.go index 7582da4..260f6f8 100644 --- a/main.go +++ b/main.go @@ -5,14 +5,12 @@ import ( "log" "os" + "github.com/teris-io/shortid" "samhofi.us/x/keybase" "samhofi.us/x/keybase/types/chat1" "samhofi.us/x/keybase/types/stellar1" ) -// this global controls debug printing -var debug bool - // Bot holds the necessary information for the bot to work. type bot struct { k *keybase.Keybase @@ -22,6 +20,11 @@ type bot struct { config botConfig } +// this allows you to change the command the bot listens to (in additon to its username) +func (b *bot) cmd() string { + return "jitsi" +} + // botConfig hold env and cli flags and options // fields must be exported for package env (reflect) to work type botConfig struct { @@ -32,34 +35,34 @@ type botConfig struct { KVStoreTeam string `env:"BOT_KVSTORE_TEAM" envDefault:""` } -// hold reply information when needed -type botReply struct { - convID chat1.ConvIDStr - msgID chat1.MessageID -} - -// Debug provides printing only when --debug flag is set or BOT_DEBUG env var is set +// debug provides printing only when --debug flag is set or BOT_DEBUG env var is set func (b *bot) debug(s string, a ...interface{}) { if b.config.Debug { - log.Printf(s, a...) - if b.config.LogConvIDStr != "" { - b.logToChat(s, a...) - } + b.log(s, a...) } } -// logToChat will send this message to the keybase chat configured in b.logConv -func (b *bot) logToChat(s string, a ...interface{}) { - // if the ConvIdStr isn't blank try to log +// logError generates an error id and returns it for error reporting, and writes the error to logging locations +func (b *bot) logError(err error) string { + // generate the error id + eid := shortid.MustGenerate() + // send the error to the log + b.log("`%s` - %s", eid, err) + // then return the error id for use + return eid +} + +// logToChat will send this message to the keybase chat configured in b.config.LogConvIDStr +func (b *bot) log(s string, a ...interface{}) { + // if the ConvIdStr isn't blank try to log to chat if b.config.LogConvIDStr != "" { // if you can't send the message, log the error to stdout if _, err := b.k.SendMessageByConvID(chat1.ConvIDStr(b.config.LogConvIDStr), s, a...); err != nil { log.Printf("Unable to log to keybase chat: %s", err) } - } else { - // otherwise (and you shouldn't be here but....) log it to stdout - log.Println("Unable to log to keybase chat, logging ConvIDStr is not set") } + // and then log it to stdout + log.Printf(s, a...) } // newBot returns a new empty bot @@ -111,22 +114,6 @@ func (b *bot) run(args []string) error { b.k.ClearCommands() b.registerCommands() - // this is just for testing, and doesn't work yet - if err := b.KVStorePutStruct("test", &ConvOptions{ConvID: "test", CustomURL: "https://te.st:888"}); err != nil { - log.Printf("KV: %+v", err) - } - var vRes1 ConvOptions - if err := b.KVStoreGetStruct("test", &vRes1); err != nil { - log.Printf("KV: %+v", err) - } else { - fmt.Printf("VR: %+v\n", vRes1) - } - var vRes2 ConvOptions - if err := b.KVStoreGetStruct("test1", &vRes2); err != nil { - log.Printf("KV: %+v", err) - } else { - fmt.Printf("VR: %+v\n", vRes2) - } log.Println("Starting...") b.k.Run(b.handlers, &b.opts) return nil diff --git a/types.go b/types.go index f14b6d4..0b10799 100644 --- a/types.go +++ b/types.go @@ -1,5 +1,13 @@ package main +import "samhofi.us/x/keybase/types/chat1" + +// hold reply information when needed +type botReply struct { + convID chat1.ConvIDStr + msgID chat1.MessageID +} + // ConvOptions stores team specific options like custom servers type ConvOptions struct { ConvID string `json:"converation_id,omitempty"` diff --git a/utils.go b/utils.go index b401642..b618b16 100644 --- a/utils.go +++ b/utils.go @@ -3,8 +3,8 @@ package main import ( "encoding/json" "fmt" + "strings" - "github.com/teris-io/shortid" "samhofi.us/x/keybase/types/chat1" ) @@ -14,6 +14,7 @@ func p(b interface{}) string { return string(s) } +// getFeedbackExtendedDescription returns the team name that feedback will be posted to, if configured func getFeedbackExtendedDescription(bc botConfig) *chat1.UserBotExtendedDescription { if bc.FeedbackTeamAdvert != "" { return &chat1.UserBotExtendedDescription{ @@ -29,11 +30,31 @@ func getFeedbackExtendedDescription(bc botConfig) *chat1.UserBotExtendedDescript } } -func (b *bot) logError(err error) string { - // generate the error id - eid := shortid.MustGenerate() - // send the error to the log - b.debug("`%s` - %s", eid, err) - // then return the error id for use - return eid +// hasCommandPrefix determines if the command matches either command or name variant +func hasCommandPrefix(s string, baseCommand string, botName string, subCommands string) bool { + // if this is actually a command + if strings.HasPrefix(s, "!") || strings.HasPrefix(s, "@") { + // generate the two possible command variants + botCommand := fmt.Sprintf("%s %s", baseCommand, subCommands) + nameCommand := fmt.Sprintf("%s %s", botName, subCommands) + // then remove the ! or @ from the string + s = strings.Replace(s, "!", "", 1) + s = strings.Replace(s, "@", "", 1) + // then check if either command variant is a match to the subCommands sent + if strings.HasPrefix(s, botCommand) || strings.HasPrefix(s, nameCommand) { + return true + } + } + return false +} + +// isRootCommand determines if the command is the root command or name with no arguments +func isRootCommand(s string, baseCommand string, botName string) bool { + // the space after is important because keybase autocompletes ! and @ with a space after + botCommand := fmt.Sprintf("!%s ", baseCommand) + nameCommand := fmt.Sprintf("@%s ", botName) + if s == botCommand || s == nameCommand { + return true + } + return false }