Browse Source

Merge pull request #4 from haukened/dev

Merge Dev into Master
master
David Haukeness 5 years ago committed by GitHub
parent
commit
a061ba63a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      args.go
  2. 205
      commands.go
  3. 7
      go.mod
  4. 23
      go.sum
  5. 87
      handlers.go
  6. 12
      jitsi.go
  7. 91
      kvstore.go
  8. 44
      main.go
  9. 106
      permissions.go
  10. 22
      types.go
  11. 43
      utils.go

7
args.go

@ -20,6 +20,7 @@ func (b *bot) parseArgs(args []string) error { @@ -20,6 +20,7 @@ func (b *bot) parseArgs(args []string) error {
flags.StringVar(&cliConfig.LogConvIDStr, "log-convid", "", "sets the keybase chat1.ConvIDStr to log debugging to keybase chat.")
flags.StringVar(&cliConfig.FeedbackConvIDStr, "feedback-convid", "", "sets the keybase chat1.ConvIDStr to send feedback to.")
flags.StringVar(&cliConfig.FeedbackTeamAdvert, "feedback-team-advert", "", "sets the keybase team/channel to advertise feedback. @team#channel")
flags.StringVar(&cliConfig.KVStoreTeam, "kvstore-team", "", "sets the keybase team where kvstore values are stored")
if err := flags.Parse(args[1:]); err != nil {
return err
}
@ -38,6 +39,9 @@ func (b *bot) parseArgs(args []string) error { @@ -38,6 +39,9 @@ func (b *bot) parseArgs(args []string) error {
if cliConfig.FeedbackTeamAdvert != "" {
b.config.FeedbackTeamAdvert = cliConfig.FeedbackTeamAdvert
}
if cliConfig.KVStoreTeam != "" {
b.config.KVStoreTeam = cliConfig.KVStoreTeam
}
}
// then print the running options
@ -48,6 +52,9 @@ func (b *bot) parseArgs(args []string) error { @@ -48,6 +52,9 @@ func (b *bot) parseArgs(args []string) error {
if b.config.FeedbackConvIDStr != "" {
b.debug("Feedback enabled to %s and advertising %s", b.config.FeedbackConvIDStr, b.config.FeedbackTeamAdvert)
}
if b.config.KVStoreTeam != "" {
b.debug("keybase kvstore enabled in @%s", b.config.KVStoreTeam)
}
return nil
}

205
commands.go

@ -1,42 +1,211 @@ @@ -1,42 +1,211 @@
package main
import (
"bytes"
"fmt"
"log"
"net/url"
"strings"
"text/tabwriter"
"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)
// check and see if this conversation has a custom URL
opts := ConvOptions{}
err := b.KVStoreGetStruct(m.ConvID, &opts)
if err != nil {
b.debug("unable to get conversation options")
eid := b.logError(err)
b.k.ReactByConvID(m.ConvID, m.Id, "Error ID %s", eid)
return
}
b.debug("%+v", opts)
// 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)
// then set the Custom server URL, if it exists
if opts.ConvID == string(m.ConvID) && opts.CustomURL != "" {
meeting.CustomServer = opts.CustomURL
}
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")
}
}
// handleConfigCommand dispatches config calls
func (b *bot) handleConfigCommand(m chat1.MsgSummary) {
args := strings.Fields(strings.ToLower(m.Content.Text.Body))
if args[1] != "config" {
return
}
if len(args) >= 3 {
switch args[2] {
case "set":
b.handleConfigSet(m)
return
case "list":
b.handleConfigList(m)
return
case "help":
b.handleConfigHelp(m)
}
}
}
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!")
// handleConfigSet processes all settings SET calls
// this should be called from b.handleConfigCommand()
func (b *bot) handleConfigSet(m chat1.MsgSummary) {
// first normalize the text and extract the arguments
args := strings.Fields(strings.ToLower(m.Content.Text.Body))
if args[2] != "set" {
return
}
b.debug("config set called by @%s in %s", m.Sender.Username, m.ConvID)
switch len(args) {
case 5:
if args[3] == "url" {
// first validate the URL
u, err := url.ParseRequestURI(args[4])
if err != nil {
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.ReactByConvID(m.ConvID, m.Id, "ERROR: HTTPS Required")
return
}
// then get the current options
var opts ConvOptions
err = b.KVStoreGetStruct(m.ConvID, &opts)
if err != nil {
eid := b.logError(err)
b.k.ReactByConvID(m.ConvID, m.Id, "Error ID %s", eid)
return
}
// then update the struct using only the scheme and hostname:port
if u.Port() != "" {
opts.CustomURL = fmt.Sprintf("%s://%s:%s", u.Scheme, u.Hostname(), u.Port())
} else {
opts.CustomURL = fmt.Sprintf("%s://%s", u.Scheme, u.Hostname())
}
// ensure that the struct has convid filled out (if its new it won't)
if opts.ConvID == "" {
opts.ConvID = string(m.ConvID)
}
// then write that back to kvstore, with revision
err = b.KVStorePutStruct(m.ConvID, opts)
if err != nil {
eid := b.logError(err)
b.k.ReactByConvID(m.ConvID, m.Id, "Error ID %s", eid)
return
}
b.k.ReactByConvID(m.ConvID, m.Id, "OK!")
return
}
default:
return
}
}
// handleConfigList lists settings for the conversation
// this should be called from b.handleConfigCommand()
func (b *bot) handleConfigList(m chat1.MsgSummary) {
// first normalize the text and extract the arguments
args := strings.Fields(strings.ToLower(m.Content.Text.Body))
if args[2] != "list" {
return
}
// get the ConvOptions
var opts ConvOptions
err := b.KVStoreGetStruct(m.ConvID, &opts)
if err != nil {
eid := b.logError(err)
b.k.ReactByConvID(m.ConvID, m.Id, "Error ID %s", eid)
return
}
// then reflect the struct to a list
configOpts := structToSlice(opts)
// Then iterate those through a tabWriter
var buf bytes.Buffer
w := tabwriter.NewWriter(&buf, 0, 0, 3, ' ', 0)
fmt.Fprintln(w, "Config Options for this channel:\n```")
for _, opt := range configOpts {
fmt.Fprintf(w, "%s\t%v\t\n", opt.Name, opt.Value)
}
fmt.Fprintln(w, "```")
w.Flush()
b.k.ReplyByConvID(m.ConvID, m.Id, buf.String())
}
// handleConfigHelp shows config help
// this should be called from b.handleConfigCommand()
func (b *bot) handleConfigHelp(m chat1.MsgSummary) {
// first normalize the text and extract the arguments
args := strings.Fields(strings.ToLower(m.Content.Text.Body))
if args[2] != "help" {
return
}
b.debug("config help called by @%s in %s", m.Sender.Username, m.ConvID)
}

7
go.mod

@ -4,8 +4,13 @@ go 1.13 @@ -4,8 +4,13 @@ go 1.13
require (
github.com/caarlos0/env v3.5.0+incompatible
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/sethvargo/go-diceware v0.2.0
github.com/stretchr/testify v1.5.1 // indirect
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf
github.com/ugorji/go/codec v1.1.7
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v2 v2.2.4 // indirect
samhofi.us/x/keybase v0.0.0-20200312153536-07f5168a6a29
samhofi.us/x/keybase v0.0.0-20200315012740-74fb4a152b35
)

23
go.sum

@ -2,10 +2,13 @@ github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yi @@ -2,10 +2,13 @@ github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yi
github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y=
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/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/lithammer/shortuuid/v3 v3.0.4 h1:uj4xhotfY92Y1Oa6n6HUiFn87CdoEHYUlTy0+IgbLrs=
github.com/lithammer/shortuuid/v3 v3.0.4/go.mod h1:RviRjexKqIzx/7r1peoAITm6m7gnif/h+0zmolKJjzw=
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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sethvargo/go-diceware v0.2.0 h1:3QzXGqUe0UR9y1XYSz1dxGS+fKtXOxRqqKjy+cG1yTI=
@ -13,10 +16,18 @@ github.com/sethvargo/go-diceware v0.2.0/go.mod h1:II+37A5sTGAtg3zd/JqyVQ8qqAjSm/ @@ -13,10 +16,18 @@ github.com/sethvargo/go-diceware v0.2.0/go.mod h1:II+37A5sTGAtg3zd/JqyVQ8qqAjSm/
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA=
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
samhofi.us/x/keybase v0.0.0-20200312153536-07f5168a6a29 h1:+yj8+O6C56QM8WGZYtDwBFxkdPT4UgVU+O9W+6N85kk=
samhofi.us/x/keybase v0.0.0-20200312153536-07f5168a6a29/go.mod h1:fcva80IUFyWcHtV4bBSzgKg07K6Rvuvi3GtGCLNGkyE=
samhofi.us/x/keybase v0.0.0-20200315012740-74fb4a152b35 h1:pnpx+34wna1ML9JxHEChf3lDSBOOEh91B5IDuNFUZYk=
samhofi.us/x/keybase v0.0.0-20200315012740-74fb4a152b35/go.mod h1:fcva80IUFyWcHtV4bBSzgKg07K6Rvuvi3GtGCLNGkyE=

87
handlers.go

@ -32,53 +32,48 @@ func (b *bot) chatHandler(m chat1.MsgSummary) { @@ -32,53 +32,48 @@ 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
words := strings.Fields(m.Content.Text.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:
fallthrough
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.checkPermissionAndExecute("reader", m, b.handleMeeting)
return
}
// then check help and welcome (non-permissions)
// 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
}
// then check sub-command variants (permissions)
// meet
if hasCommandPrefix(m.Content.Text.Body, b.cmd(), b.k.Username, "meet") {
b.checkPermissionAndExecute("reader", m, b.handleMeeting)
return
}
// feedback
if hasCommandPrefix(m.Content.Text.Body, b.cmd(), b.k.Username, "feedback") {
b.checkPermissionAndExecute("reader", m, b.handleFeedback)
return
}
// config commands
if hasCommandPrefix(m.Content.Text.Body, b.cmd(), b.k.Username, "config") {
b.checkPermissionAndExecute("admin", m, b.handleConfigCommand)
return
}
}
@ -95,7 +90,7 @@ func (b *bot) convHandler(m chat1.ConvSummary) { @@ -95,7 +90,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

12
jitsi.go

@ -19,9 +19,10 @@ type phoneNumber struct { @@ -19,9 +19,10 @@ type phoneNumber struct {
}
type jitsiMeeting struct {
Name string
ID string
Phone []phoneNumber
Name string
CustomServer string
ID string
Phone []phoneNumber
}
func (j *jitsiMeeting) getJitsiName() error {
@ -34,7 +35,10 @@ func (j *jitsiMeeting) getJitsiName() error { @@ -34,7 +35,10 @@ func (j *jitsiMeeting) getJitsiName() error {
}
func (j *jitsiMeeting) getURL() string {
return fmt.Sprintf("https://meet.jit.si/%s", j.Name)
if j.CustomServer == "" {
return fmt.Sprintf("https://meet.jit.si/%s", j.Name)
}
return fmt.Sprintf("%s/%s", j.CustomServer, j.Name)
}
func (j *jitsiMeeting) getPIN() string {

91
kvstore.go

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
package main
import (
"reflect"
"github.com/ugorji/go/codec"
"samhofi.us/x/keybase/types/chat1"
)
// KvStorePutStruct marshals an interface to JSON and sends to kvstore
func (b *bot) KVStorePutStruct(convIDstr chat1.ConvIDStr, v interface{}) error {
// marshal the struct to JSON
kvstoreDataString, err := encodeStructToJSONString(v)
if err != nil {
return err
}
// put the string in kvstore
err = b.KVStorePut(string(convIDstr), getTypeName(v), kvstoreDataString)
if err != nil {
return err
}
return nil
}
// KVStoreGetStruct gets a string from kvstore and unmarshals the JSON to a struct
func (b *bot) KVStoreGetStruct(convIDstr chat1.ConvIDStr, v interface{}) error {
// get the string from kvstore
result, err := b.KVStoreGet(string(convIDstr), getTypeName(v))
if err != nil {
return err
}
// if there was no result just return and the struct is unmodified
if result == "" {
return nil
}
// unmarshal the string into JSON
err = decodeJSONStringToStruct(v, result)
if err != nil {
return err
}
return nil
}
// KVStorePut puts a string into kvstore given a key and namespace
func (b *bot) KVStorePut(namespace string, key string, value string) error {
_, err := b.k.KVPut(&b.config.KVStoreTeam, namespace, key, value)
if err != nil {
return err
}
return nil
}
// KVStoreGet gets a string from kvstore given a key and namespace
func (b *bot) KVStoreGet(namespace string, key string) (string, error) {
kvResult, err := b.k.KVGet(&b.config.KVStoreTeam, namespace, key)
if err != nil {
return "", err
}
return kvResult.EntryValue, nil
}
// getTypeName returns the name of a type, regardless of if its a pointer or not
func getTypeName(v interface{}) string {
t := reflect.TypeOf(v)
if t.Kind() == reflect.Ptr {
return t.Elem().Name()
}
return t.Name()
}
func encodeStructToJSONString(v interface{}) (string, error) {
jh := codecHandle()
var bytes []byte
err := codec.NewEncoderBytes(&bytes, jh).Encode(v)
if err != nil {
return "", err
}
result := string(bytes)
return result, nil
}
func decodeJSONStringToStruct(v interface{}, src string) error {
bytes := []byte(src)
jh := codecHandle()
return codec.NewDecoderBytes(bytes, jh).Decode(v)
}
func codecHandle() *codec.JsonHandle {
var jh codec.JsonHandle
return &jh
}

44
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 {
@ -29,36 +32,37 @@ type botConfig struct { @@ -29,36 +32,37 @@ type botConfig struct {
LogConvIDStr string `env:"BOT_LOG_CONVID" envDefault:""`
FeedbackConvIDStr string `env:"BOT_FEEDBACK_CONVID" envDefault:""`
FeedbackTeamAdvert string `env:"BOT_FEEDBACK_TEAM_ADVERT" envDefault:""`
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

106
permissions.go

@ -0,0 +1,106 @@ @@ -0,0 +1,106 @@
package main
import (
"strings"
"samhofi.us/x/keybase/types/chat1"
)
// checkPermissionAndExecute will check the minimum required role for the permission and execute the handler function if allowed
func (b *bot) checkPermissionAndExecute(requiredRole string, m chat1.MsgSummary, f func(chat1.MsgSummary)) {
// get the members of the conversation
b.debug("Executing permissions check")
// currently this doesn't work due to a keybase bug unless you're in the role of resticted bot
// the workaround is to check the general channel the old way
// so first check the new way
conversation, err := b.k.ListMembersOfConversation(m.ConvID)
if err != nil {
eid := b.logError(err)
b.k.ReactByConvID(m.ConvID, m.Id, "Error ID %s", eid)
return
}
// **** <workaround>
// check if the length of the lists are zero
// it'll look like this:
// {"owners":[],"admins":[],"writers":[],"readers":[],"bots":[],"restrictedBots":[]}
if len(conversation.Members.Owners) == 0 &&
len(conversation.Members.Admins) == 0 &&
len(conversation.Members.Writers) == 0 &&
len(conversation.Members.Readers) == 0 &&
len(conversation.Members.Bots) == 0 &&
len(conversation.Members.RestrictedBots) == 0 {
channel := chat1.ChatChannel{
Name: m.Channel.Name,
MembersType: m.Channel.MembersType,
TopicName: "general",
}
// re-map the members using the workaround, in case you're not in the restricted bot role
conversation, err = b.k.ListMembersOfChannel(channel)
if err != nil {
eid := b.logError(err)
b.k.ReactByConvID(m.ConvID, m.Id, "Error ID %s", eid)
return
}
}
/// **** </workaround>
// create a map of valid roles, according to @dxb struc
memberTypes := make(map[string]struct{})
memberTypes["owner"] = struct{}{}
memberTypes["admin"] = struct{}{}
memberTypes["writer"] = struct{}{}
memberTypes["reader"] = struct{}{}
// if the role is not in the map, its an invalid role
if _, ok := memberTypes[strings.ToLower(requiredRole)]; !ok {
// the role passed was not valid, so bail
b.log("ERROR: %s is not a valid permissions level", requiredRole)
return
}
// then descend permissions from top down
for _, member := range conversation.Members.Owners {
if strings.ToLower(member.Username) == strings.ToLower(m.Sender.Username) {
f(m)
return
}
b.debug("no")
}
// if the required role was owner, return and don't evaluate the rest
if strings.ToLower(requiredRole) == "owner" {
b.debug("user does not have required permission of: owner")
return
}
// admins
for _, member := range conversation.Members.Admins {
if strings.ToLower(member.Username) == strings.ToLower(m.Sender.Username) {
f(m)
return
}
}
if strings.ToLower(requiredRole) == "admin" {
b.debug("user does not have required permission of: admin")
return
}
// writers
for _, member := range conversation.Members.Writers {
if strings.ToLower(member.Username) == strings.ToLower(m.Sender.Username) {
f(m)
return
}
}
if strings.ToLower(requiredRole) == "writer" {
b.debug("user does not have required permission of: writer")
return
}
// readers
for _, member := range conversation.Members.Readers {
if strings.ToLower(member.Username) == strings.ToLower(m.Sender.Username) {
f(m)
return
}
}
// just return - restricted bots shouldn't be able to run commands
b.debug("user does not have required permission of: reader")
return
}

22
types.go

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
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"`
//NotificationsEnabled bool `json:"notifications_enabled,omitempty"`
CustomURL string `json:"custom_url,omitempty"`
}
// reflectStruct holds information about reflected structs!
type reflectStruct struct {
Name string
Value interface{}
}

43
utils.go

@ -3,6 +3,8 @@ package main @@ -3,6 +3,8 @@ package main
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"samhofi.us/x/keybase/types/chat1"
)
@ -13,6 +15,7 @@ func p(b interface{}) string { @@ -13,6 +15,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{
@ -27,3 +30,43 @@ func getFeedbackExtendedDescription(bc botConfig) *chat1.UserBotExtendedDescript @@ -27,3 +30,43 @@ func getFeedbackExtendedDescription(bc botConfig) *chat1.UserBotExtendedDescript
MobileBody: "Please note: Your feedback will be public!",
}
}
// 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
}
// this converts structs to slices of (Name, Value) pairs
func structToSlice(v interface{}) []reflectStruct {
x := reflect.ValueOf(v)
values := make([]reflectStruct, x.NumField())
for i := 0; i < x.NumField(); i++ {
values[i].Value = x.Field(i).Interface()
values[i].Name = x.Type().Field(i).Name
}
return values
}

Loading…
Cancel
Save