11 changed files with 552 additions and 95 deletions
@ -1,42 +1,211 @@ |
|||||||
package main |
package main |
||||||
|
|
||||||
import ( |
import ( |
||||||
|
"bytes" |
||||||
"fmt" |
"fmt" |
||||||
"log" |
"net/url" |
||||||
"strings" |
"strings" |
||||||
|
"text/tabwriter" |
||||||
|
|
||||||
"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) |
||||||
|
// 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() |
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()) |
// then set the Custom server URL, if it exists
|
||||||
b.k.SendMessageByConvID(convid, message) |
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) { |
// 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") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 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) { |
// handleConfigSet 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!") |
// 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) |
||||||
} |
} |
||||||
|
@ -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 |
||||||
|
} |
@ -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 |
||||||
|
} |
@ -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{} |
||||||
|
} |
Loading…
Reference in new issue