Browse Source

initial commit

master
David Haukeness 5 years ago
commit
8879a32137
No known key found for this signature in database
GPG Key ID: 54F2372DDB7F9462
  1. 3
      .gitignore
  2. 7
      LICENSE
  3. 35
      args.go
  4. 33
      commands.go
  5. 12
      go.mod
  6. 38
      go.sum
  7. 103
      handlers.go
  8. 107
      jitsi.go
  9. BIN
      jitsy-bot
  10. 100
      main.go
  11. 11
      utils.go

3
.gitignore vendored

@ -0,0 +1,3 @@
jitsi-bot
.vscode/launch.json
.github

7
LICENSE

@ -0,0 +1,7 @@
Copyright 2020 David Haukeness
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

35
args.go

@ -0,0 +1,35 @@
package main
import (
"flag"
"github.com/caarlos0/env"
)
// parseArgs parses command line and environment args and sets globals
func (b *bot) parseArgs(args []string) error {
// parse the env variables into the bot config
if err := env.Parse(&b.config); err != nil {
return err
}
// then parse CLI args as overrides
flags := flag.NewFlagSet(args[0], flag.ExitOnError)
cliConfig := botConfig{}
flags.BoolVar(&cliConfig.Debug, "debug", false, "enables command debugging")
if err := flags.Parse(args[1:]); err != nil {
return err
}
// then override the environment vars if there were cli args
if flags.NFlag() > 0 {
if cliConfig.Debug == true {
b.config.Debug = true
}
}
// then print the running options
b.debug("Debug Enabled")
return nil
}

33
commands.go

@ -0,0 +1,33 @@
package main
import (
"bytes"
"fmt"
"log"
"text/tabwriter"
"samhofi.us/x/keybase/types/chat1"
)
func (b *bot) setupMeeting(convid chat1.ConvIDStr, msgid chat1.MessageID, words []string, membersType string) {
b.debug("command recieved in conversation %s", convid)
meeting, err := newJitsiMeeting()
if err != nil {
log.Println(err)
b.k.SendMessageByConvID(convid, "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.")
return
}
var buf bytes.Buffer
w := tabwriter.NewWriter(&buf, 0, 4, 3, ' ', 0)
fmt.Fprintln(w, "Here's your meeting:")
fmt.Fprintf(w, "URL:\t%s\n", meeting.getURL())
fmt.Fprintf(w, "PIN:\t%s\n", meeting.getPIN())
fmt.Fprintln(w, "Dial In:\t")
fmt.Fprintln(w, "```")
for _, phone := range meeting.Phone {
fmt.Fprintf(w, " %s\t%s\t\n", phone.Country, phone.Number)
}
fmt.Fprintln(w, "```")
w.Flush()
b.k.SendMessageByConvID(convid, buf.String())
}

12
go.mod

@ -0,0 +1,12 @@
module jitsy-bot
go 1.13
require (
github.com/caarlos0/env v3.5.0+incompatible
github.com/lithammer/shortuuid/v3 v3.0.4
github.com/onsi/ginkgo v1.12.0 // indirect
github.com/onsi/gomega v1.9.0 // indirect
github.com/tjarratt/babble v0.0.0-20191209142150-eecdf8c2339d
samhofi.us/x/keybase v0.0.0-20200312153536-07f5168a6a29
)

38
go.sum

@ -0,0 +1,38 @@
github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs=
github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/lithammer/shortuuid v3.0.0+incompatible h1:NcD0xWW/MZYXEHa6ITy6kaXN5nwm/V115vj2YXfhS0w=
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/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/tjarratt/babble v0.0.0-20191209142150-eecdf8c2339d h1:b7oHBI6TgTdCDuqTijsVldzlh+6cfQpdYLz1EKtCAoY=
github.com/tjarratt/babble v0.0.0-20191209142150-eecdf8c2339d/go.mod h1:O5hBrCGqzfb+8WyY8ico2AyQau7XQwAfEQeEQ5/5V9E=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
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=

103
handlers.go

@ -0,0 +1,103 @@
package main
import (
"fmt"
"log"
"strings"
"samhofi.us/x/keybase"
"samhofi.us/x/keybase/types/chat1"
"samhofi.us/x/keybase/types/stellar1"
)
// RegisterHandlers is called by main to map these handler funcs to events
func (b *bot) registerHandlers() {
chat := b.chatHandler
conv := b.convHandler
wallet := b.walletHandler
err := b.errHandler
b.handlers = keybase.Handlers{
ChatHandler: &chat,
ConversationHandler: &conv,
WalletHandler: &wallet,
ErrorHandler: &err,
}
}
// chatHandler should handle all messages coming from the chat
func (b *bot) chatHandler(m chat1.MsgSummary) {
// only handle text, we don't really care about attachments
if m.Content.TypeName != "text" {
return
}
// 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:")
}
}
}
}
// if the message is @myusername just perform the default function
if strings.HasPrefix(m.Content.Text.Body, fmt.Sprintf("@%s", b.k.Username)) {
words := strings.Fields(m.Content.Text.Body)
b.setupMeeting(m.ConvID, m.Id, words, m.Channel.MembersType)
}
// its a command for me, iterate through extended commands
if strings.HasPrefix(m.Content.Text.Body, "!") {
// break up the message into words
words := strings.Fields(m.Content.Text.Body)
// strip the ! from the first word, and lowercase to derive the command
thisCommand := strings.ToLower(strings.Replace(words[0], "!", "", 1))
// decide if this is askind for extended commands
switch thisCommand {
case "meeting":
fallthrough
case "jitsi":
b.setupMeeting(m.ConvID, m.Id, words, m.Channel.MembersType)
default:
return
}
}
}
// handle conversations (this fires when a new conversation is initiated)
// i.e. when someone opens a conversation to you but hasn't sent a message yet
func (b *bot) convHandler(m chat1.ConvSummary) {
switch m.Channel.MembersType {
case "team":
b.debug("Added to new team: @%s (%s) Sending welcome message", m.Channel.Name, m.Id)
case "impteamnative":
b.debug("New conversation found %s (%s) Sending welcome message", m.Channel.Name, m.Id)
default:
b.debug("New convID found %s, sending welcome message.", m.Id)
}
b.k.SendMessageByConvID(m.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`\nI also accept donations to offset hosting costs,\njust send some XLM to my wallet if you feel like it by typing `+5XLM@urbandictionary`")
}
// this handles wallet events, like when someone send you money in chat
func (b *bot) walletHandler(m stellar1.PaymentDetailsLocal) {
// if the payment is successful
if m.Summary.StatusSimplified == 3 {
// get the reply info and see if it exists
replyInfo := b.payments[m.Summary.Id]
if replyInfo.convID != "" {
b.k.ReplyByConvID(replyInfo.convID, replyInfo.msgID, "Thank you so much! I'll use this to offset my hosting costs!")
}
}
}
// this handles all errors returned from the keybase binary
func (b *bot) errHandler(m error) {
log.Println("---[ error ]---")
log.Println(p(m))
}

107
jitsi.go

@ -0,0 +1,107 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"sort"
"strconv"
"github.com/lithammer/shortuuid/v3"
)
type phoneNumber struct {
Country string
Number string
}
type jitsiMeeting struct {
Name string
ID string
Phone []phoneNumber
}
func (j *jitsiMeeting) getJitsiName() {
j.Name = shortuuid.New()
}
func (j *jitsiMeeting) getURL() string {
return fmt.Sprintf("https://meet.jit.si/%s", j.Name)
}
func (j *jitsiMeeting) getPIN() string {
if len(j.ID) == 10 {
return fmt.Sprintf("%s %s %s#", j.ID[0:4], j.ID[4:8], j.ID[8:10])
}
return fmt.Sprintf("%s#", j.ID)
}
func (j *jitsiMeeting) getJitsiID() error {
type jitsiMeetingHTTPResponse struct {
Message string `json:"message,omitempty"`
ID int64 `json:"id,omitempty"`
Conference string `json:"conference,omitempty"`
}
queryURL := fmt.Sprintf("https://api.jitsi.net/conferenceMapper?conference=%s@conference.meet.jit.si", url.QueryEscape(j.Name))
resp, err := http.Get(queryURL)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
var jR jitsiMeetingHTTPResponse
if err := json.Unmarshal(respBody, &jR); err != nil {
return err
}
j.ID = strconv.FormatInt(jR.ID, 10)
return nil
}
func (j *jitsiMeeting) getJitsiNumbers() error {
type jitsiPhoneHTTPResponse struct {
Message string `json:"message,omitempty"`
Numbers map[string][]string `json:"numbers,omitempty"`
Enabled bool `json:"numbersEnabled,omitempty"`
}
queryURL := fmt.Sprintf("https://api.jitsi.net/phoneNumberList?conference=%s@conference.meet.jit.si", url.QueryEscape(j.Name))
resp, err := http.Get(queryURL)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
var jR jitsiPhoneHTTPResponse
if err := json.Unmarshal(respBody, &jR); err != nil {
return err
}
for key, value := range jR.Numbers {
j.Phone = append(j.Phone, phoneNumber{
Country: key,
Number: value[0],
})
}
// then sort them alphabetically
sort.Slice(j.Phone, func(a, b int) bool { return j.Phone[a].Country < j.Phone[b].Country })
return nil
}
func newJitsiMeeting() (jitsiMeeting, error) {
result := jitsiMeeting{}
result.getJitsiName()
if err := result.getJitsiID(); err != nil {
return result, err
}
if err := result.getJitsiNumbers(); err != nil {
return result, err
}
return result, nil
}

BIN
jitsy-bot

Binary file not shown.

100
main.go

@ -0,0 +1,100 @@
package main
import (
"log"
"os"
"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
handlers keybase.Handlers
opts keybase.RunOptions
payments map[stellar1.PaymentID]botReply
config botConfig
}
// botConfig hold env and cli flags and options
// fields must be exported for package env (reflect) to work
type botConfig struct {
Debug bool `env:"BOT_DEBUG" envDefault:"false"`
}
// 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
func (b *bot) debug(s string, a ...interface{}) {
if b.config.Debug {
log.Printf(s, a...)
}
}
// newBot returns a new empty bot
func newBot() *bot {
var b bot
b.k = keybase.NewKeybase()
b.handlers = keybase.Handlers{}
b.opts = keybase.RunOptions{}
b.payments = make(map[stellar1.PaymentID]botReply)
return &b
}
// this handles setting up command advertisements and aliases
func (b *bot) registerCommands() {
opts := keybase.AdvertiseCommandsOptions{
Advertisements: []chat1.AdvertiseCommandAPIParam{
{
Typ: "public",
Commands: []chat1.UserBotCommandInput{
{
Name: "jitsi",
Description: "Starts a meet.jit.si meeting",
Usage: "",
},
{
Name: "meeting",
Description: "Starts a meet.jit.si meeting",
Usage: "",
},
},
},
},
}
b.k.AdvertiseCommands(opts)
}
// run performs a proxy main function
func (b *bot) run(args []string) error {
// parse the arguments
err := b.parseArgs(args)
if err != nil {
return err
}
b.registerHandlers()
b.registerCommands()
log.Println("Starting...")
b.k.Run(b.handlers, &b.opts)
return nil
}
// main is a thin skeleton, proxied to Bot.Run()
func main() {
b := newBot()
if err := b.run(os.Args); err != nil {
log.Printf("%s", err)
os.Exit(1)
}
}

11
utils.go

@ -0,0 +1,11 @@
package main
import (
"encoding/json"
)
// this JSON pretty prints errors and debug
func p(b interface{}) string {
s, _ := json.MarshalIndent(b, "", " ")
return string(s)
}
Loading…
Cancel
Save