Browse Source

Merge pull request #3 from haukened/handler-v2

Handler v2
master
David Haukeness 5 years ago committed by GitHub
parent
commit
4239eb0dcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 108
      commands.go
  2. 87
      handlers.go
  3. 57
      main.go
  4. 8
      types.go
  5. 37
      utils.go

108
commands.go

@ -2,70 +2,109 @@ package main
import ( import (
"fmt" "fmt"
"log"
"net/url" "net/url"
"strings" "strings"
"samhofi.us/x/keybase/types/chat1" "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 <type anything>`\n 2. `!jitsibot feedback <type anything>`\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() meeting, err := newJitsiMeetingSimple()
if err != nil { if err != nil {
log.Println(err) 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.", sender) 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(convid, message) b.k.SendMessageByConvID(m.ConvID, message)
return return
} }
message := fmt.Sprintf("@%s here's your meeting: %s", sender, meeting.getURL()) b.k.SendMessageByConvID(m.ConvID, "@%s here's your meeting: %s", m.Sender.Username, meeting.getURL())
b.k.SendMessageByConvID(convid, message)
} }
func (b *bot) sendFeedback(convid chat1.ConvIDStr, mesgID chat1.MessageID, sender string, args []string) { // handleFeedback sends feedback to a keybase chat, if configured
b.debug("feedback recieved in %s", convid) func (b *bot) handleFeedback(m chat1.MsgSummary) {
b.log("feedback recieved in %s", m.ConvID)
if b.config.FeedbackConvIDStr != "" { 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) fcID := chat1.ConvIDStr(b.config.FeedbackConvIDStr)
if _, err := b.k.SendMessageByConvID(fcID, "Feedback from @%s:\n```%s```", sender, feedback); err != nil { if _, err := b.k.SendMessageByConvID(fcID, "Feedback from @%s:\n```%s```", m.Sender.Username, 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:") eid := b.logError(err)
log.Printf("Unable to send feedback: %s", err) b.k.ReactByConvID(m.ConvID, m.Id, "Error ID %s", eid)
} else { } 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 { } 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) { // handleSetCommand processes all settings SET calls
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 <type anything>`\n 2. `!jitsibot feedback <type anything>`\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) 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
func (b *bot) setKValue(convid chat1.ConvIDStr, msgID chat1.MessageID, args []string) { args := strings.Fields(strings.ToLower(m.Content.Text.Body))
if args[0] != "set" { if args[1] != "set" {
return return
} }
switch len(args) { switch len(args) {
case 3: case 4:
if args[1] == "url" { if args[2] == "url" {
// first validate the URL // first validate the URL
u, err := url.ParseRequestURI(args[2]) u, err := url.ParseRequestURI(args[3])
if err != nil { 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 return
} }
// then make sure its HTTPS // then make sure its HTTPS
if u.Scheme != "https" { if u.Scheme != "https" {
b.k.ReplyByConvID(convid, msgID, "ERROR - HTTPS Required") b.k.ReactByConvID(m.ConvID, m.Id, "ERROR: HTTPS Required")
return return
} }
// then get the current options // then get the current options
var opts ConvOptions var opts ConvOptions
err = b.KVStoreGetStruct(convid, &opts) err = b.KVStoreGetStruct(m.ConvID, &opts)
if err != nil { if err != nil {
eid := b.logError(err) eid := b.logError(err)
b.k.ReactByConvID(convid, msgID, "Error %s", eid) b.k.ReactByConvID(m.ConvID, m.Id, "Error ID %s", eid)
return return
} }
// then update the struct using only the scheme and hostname:port // 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()) opts.CustomURL = fmt.Sprintf("%s://%s/", u.Scheme, u.Hostname())
} }
// then write that back to kvstore, with revision // then write that back to kvstore, with revision
err = b.KVStorePutStruct(convid, opts) err = b.KVStorePutStruct(m.ConvID, opts)
if err != nil { if err != nil {
eid := b.logError(err) eid := b.logError(err)
b.k.ReactByConvID(convid, msgID, "ERROR %s", eid) b.k.ReactByConvID(m.ConvID, m.Id, "Error ID %s", eid)
return return
} }
b.k.ReactByConvID(convid, msgID, "OK!") b.k.ReactByConvID(m.ConvID, m.Id, "OK!")
return return
} }
default: 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" { if args[0] != "list" {
return return
} }

87
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 this chat message is a payment, add it to the bot payments
if m.Content.Text.Payments != nil { if m.Content.Text.Payments != nil {
// there can be multiple payments on each message, iterate them b.handlePayment(m)
for _, payment := range m.Content.Text.Payments { return
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:")
} }
// 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, "@") {
// 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
} }
// Determine first if this is a command // feedback
if strings.HasPrefix(m.Content.Text.Body, "!") || strings.HasPrefix(m.Content.Text.Body, "@") { if hasCommandPrefix(m.Content.Text.Body, b.cmd(), b.k.Username, "feedback") {
// determine the root command b.handleFeedback(m)
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 return
} }
// help
if hasCommandPrefix(m.Content.Text.Body, b.cmd(), b.k.Username, "help") {
b.handleWelcome(m.ConvID)
return
} }
default: // 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 return
} }
} }
@ -103,7 +92,7 @@ func (b *bot) convHandler(m chat1.ConvSummary) {
default: default:
b.debug("New convID found %s, sending welcome message.", m.Id) 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 // this handles wallet events, like when someone send you money in chat

57
main.go

@ -5,14 +5,12 @@ import (
"log" "log"
"os" "os"
"github.com/teris-io/shortid"
"samhofi.us/x/keybase" "samhofi.us/x/keybase"
"samhofi.us/x/keybase/types/chat1" "samhofi.us/x/keybase/types/chat1"
"samhofi.us/x/keybase/types/stellar1" "samhofi.us/x/keybase/types/stellar1"
) )
// this global controls debug printing
var debug bool
// Bot holds the necessary information for the bot to work. // Bot holds the necessary information for the bot to work.
type bot struct { type bot struct {
k *keybase.Keybase k *keybase.Keybase
@ -22,6 +20,11 @@ type bot struct {
config botConfig 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 // botConfig hold env and cli flags and options
// fields must be exported for package env (reflect) to work // fields must be exported for package env (reflect) to work
type botConfig struct { type botConfig struct {
@ -32,34 +35,34 @@ type botConfig struct {
KVStoreTeam string `env:"BOT_KVSTORE_TEAM" envDefault:""` KVStoreTeam string `env:"BOT_KVSTORE_TEAM" envDefault:""`
} }
// hold reply information when needed // debug provides printing only when --debug flag is set or BOT_DEBUG env var is set
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
func (b *bot) debug(s string, a ...interface{}) { func (b *bot) debug(s string, a ...interface{}) {
if b.config.Debug { if b.config.Debug {
log.Printf(s, a...) b.log(s, a...)
if b.config.LogConvIDStr != "" {
b.logToChat(s, a...)
} }
} }
// 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.logConv // logToChat will send this message to the keybase chat configured in b.config.LogConvIDStr
func (b *bot) logToChat(s string, a ...interface{}) { func (b *bot) log(s string, a ...interface{}) {
// if the ConvIdStr isn't blank try to log // if the ConvIdStr isn't blank try to log to chat
if b.config.LogConvIDStr != "" { if b.config.LogConvIDStr != "" {
// if you can't send the message, log the error to stdout // 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 { if _, err := b.k.SendMessageByConvID(chat1.ConvIDStr(b.config.LogConvIDStr), s, a...); err != nil {
log.Printf("Unable to log to keybase chat: %s", err) 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 // newBot returns a new empty bot
@ -111,22 +114,6 @@ func (b *bot) run(args []string) error {
b.k.ClearCommands() b.k.ClearCommands()
b.registerCommands() 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...") log.Println("Starting...")
b.k.Run(b.handlers, &b.opts) b.k.Run(b.handlers, &b.opts)
return nil return nil

8
types.go

@ -1,5 +1,13 @@
package main 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 // ConvOptions stores team specific options like custom servers
type ConvOptions struct { type ConvOptions struct {
ConvID string `json:"converation_id,omitempty"` ConvID string `json:"converation_id,omitempty"`

37
utils.go

@ -3,8 +3,8 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"github.com/teris-io/shortid"
"samhofi.us/x/keybase/types/chat1" "samhofi.us/x/keybase/types/chat1"
) )
@ -14,6 +14,7 @@ func p(b interface{}) string {
return string(s) return string(s)
} }
// getFeedbackExtendedDescription returns the team name that feedback will be posted to, if configured
func getFeedbackExtendedDescription(bc botConfig) *chat1.UserBotExtendedDescription { func getFeedbackExtendedDescription(bc botConfig) *chat1.UserBotExtendedDescription {
if bc.FeedbackTeamAdvert != "" { if bc.FeedbackTeamAdvert != "" {
return &chat1.UserBotExtendedDescription{ return &chat1.UserBotExtendedDescription{
@ -29,11 +30,31 @@ func getFeedbackExtendedDescription(bc botConfig) *chat1.UserBotExtendedDescript
} }
} }
func (b *bot) logError(err error) string { // hasCommandPrefix determines if the command matches either command or name variant
// generate the error id func hasCommandPrefix(s string, baseCommand string, botName string, subCommands string) bool {
eid := shortid.MustGenerate() // if this is actually a command
// send the error to the log if strings.HasPrefix(s, "!") || strings.HasPrefix(s, "@") {
b.debug("`%s` - %s", eid, err) // generate the two possible command variants
// then return the error id for use botCommand := fmt.Sprintf("%s %s", baseCommand, subCommands)
return eid 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
} }

Loading…
Cancel
Save