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. 97
      handlers.go
  3. 59
      main.go
  4. 8
      types.go
  5. 37
      utils.go

108
commands.go

@ -2,70 +2,109 @@ package main @@ -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 <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()
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 <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) 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 @@ -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 @@ -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
}

97
handlers.go

@ -32,61 +32,50 @@ func (b *bot) chatHandler(m chat1.MsgSummary) { @@ -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) { @@ -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

59
main.go

@ -5,14 +5,12 @@ import ( @@ -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 { @@ -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 { @@ -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 { @@ -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

8
types.go

@ -1,5 +1,13 @@ @@ -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"`

37
utils.go

@ -3,8 +3,8 @@ package main @@ -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 { @@ -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 @@ -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
}

Loading…
Cancel
Save