Initial import.

This commit is contained in:
K. Isom 2021-04-19 13:10:40 -07:00
commit 42aa585bd0
16 changed files with 863 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.yaml
*~
kas

11
conn/config.go Normal file
View File

@ -0,0 +1,11 @@
package conn
import (
"kas/conn/twilio"
"kas/conn/xmpp"
)
type Config struct {
XMPP *xmpp.Config `yaml:"xmpp"`
Twilio *twilio.Config `yaml:"twilio"`
}

30
conn/http/server.go Normal file
View File

@ -0,0 +1,30 @@
package http
import (
"log"
"net/http"
"sync"
)
// Main router
// Twilio handler
var server = &struct {
router *http.ServeMux
lock sync.Mutex
}{
router: http.NewServeMux(),
}
// AddRoute is used to set up routes.
//
// NB: no checking is done yet for duplicate patterns.
func AddRoute(pattern string, handler func(http.ResponseWriter, *http.Request)) {
server.lock.Lock()
defer server.lock.Unlock()
server.router.HandleFunc(pattern, handler)
}
func Start(addr string) {
go log.Print(http.ListenAndServe(addr, server.router))
}

83
conn/twilio/config.go Normal file
View File

@ -0,0 +1,83 @@
package twilio
import (
"errors"
"fmt"
"log"
"net/http"
"net/url"
)
type Config struct {
Contacts []string `yaml:"contacts"`
contacts map[string]bool
AccountSID string `yaml:"account_sid"`
AuthToken string `yaml:"auth_token"`
Number string `yaml:"telno"` // kas' telno
}
func (cfg *Config) postURL() string {
return fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json",
cfg.AccountSID)
}
func (cfg *Config) buildContacts() {
cfg.contacts = map[string]bool{}
for _, telno := range cfg.Contacts {
cfg.contacts[telno] = true
}
}
func (cfg *Config) NumberAuthorized(s string) bool {
if cfg.contacts == nil || len(cfg.contacts) != len(cfg.Contacts) {
cfg.buildContacts()
}
return cfg.contacts[s]
}
func validate(cfg *Config) error {
if len(cfg.Contacts) == 0 {
return errors.New("twilio: no authorized numbers (you won't be able to receive any messages)")
}
if cfg.AccountSID == "" {
return errors.New("twilio: missing account SID")
}
if cfg.AuthToken == "" {
return errors.New("twilio: missing auth token")
}
if cfg.Number == "" {
return errors.New("twilio: no number configured")
}
return nil
}
var config *Config
func SetConfig(cfg *Config) error {
if err := validate(cfg); err != nil {
return err
}
config = cfg
return nil
}
func Send(to string, message string) error {
form := url.Values{
"Body": {message},
"From": {config.Number},
"To": {to},
}
resp, err := http.PostForm(config.postURL(), form)
if err != nil {
log.Printf("twilio send: %s", err)
}
resp.Body.Close()
return nil
}

116
conn/twilio/message.go Normal file
View File

@ -0,0 +1,116 @@
package twilio
import (
"net/url"
"strconv"
"strings"
"time"
)
// ErrInvalidMessage is returned when a bad message is sent to the Twilio
// receive hook; it tries to clarify why the message is invalid.
type ErrInvalidMessage struct {
missing []string
cause string
}
// Error satisfies the error interface.
func (err *ErrInvalidMessage) Error() string {
errParts := []string{}
if len(err.missing) > 0 {
errParts = append(errParts, "missing="+strings.Join(err.missing, ","))
}
if err.cause != "" {
errParts = append(errParts, "cause="+err.cause)
}
return strings.Join(errParts, ", ")
}
// isInvalidMessage returns an error given a list of missing fields and an
// underlying cause, returning nil if the message is valid.
func isInvalidMessage(missing []string, cause string) error {
if len(missing) == 0 && cause == "" {
return nil
}
return &ErrInvalidMessage{
missing: missing,
cause: cause,
}
}
// Message represents an incoming Twilio message.
type Message struct {
Source string
To string
Body string
When time.Time
MediaURL string
ASID string
MSID string
Price float64
Mime string
}
// MessageFromValues returns a Message from a set of form values.
func MessageFromValues(v url.Values) (*Message, error) {
missing := []string{}
cause := ""
m := &Message{
Source: v.Get("From"),
To: v.Get("To"),
Body: v.Get("Body"),
MediaURL: v.Get("MediaUrl0"),
ASID: v.Get("AccountSid"),
MSID: v.Get("MessageSid"),
When: time.Now().UTC(),
Price: 0.0075,
}
if m.Source == "" {
missing = append(missing, "From")
}
if m.To == "" {
missing = append(missing, "To")
}
numMediaValue := v.Get("NumMedia")
if numMediaValue == "" {
numMediaValue = "0"
}
nMedia, err := strconv.Atoi(numMediaValue)
if err != nil {
return nil, err
}
if nMedia == 0 && m.Body == "" {
cause = "message has no body or media attachment"
} else if nMedia != 0 {
if m.MediaURL == "" {
missing = append(missing, "MediaUrl0")
}
m.Price = 0.01
if m.Mime = v.Get("MediaContentType0"); m.Mime == "" {
missing = append(missing, "MediaContentType0")
}
}
if sent := v.Get("DateSent"); sent != "" {
t, err := time.Parse(time.RFC1123Z, sent)
if err != nil {
return nil, err
}
m.When = t.UTC()
}
if err := isInvalidMessage(missing, cause); err != nil {
return nil, err
}
return m, nil
}

49
conn/twilio/twilio.go Normal file
View File

@ -0,0 +1,49 @@
package twilio
import (
"context"
"kas/cps"
"log"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
log.Printf("twilio receive hook: received %s request; only %s requests are supported",
r.Method, http.MethodPost)
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
err := r.ParseForm()
if err != nil {
log.Printf("twilio receive hook: couldn't parse form contents: %s", err)
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
message, err := MessageFromValues(r.Form)
if err != nil {
log.Printf("twilio receive hook: received an invalid message: %s", err)
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
if !config.NumberAuthorized(message.Source) {
log.Printf("twilio receive hook: received message from unknown sender %v", message)
http.Error(w, "not authorized", http.StatusUnauthorized)
return
}
w.Write([]byte("accepted"))
response, err := cps.Handle(context.Background(), message.Body)
if err != nil {
log.Printf("twilio receive hook: %s", err)
}
err = Send(message.Source, response.String())
if err != nil {
log.Printf("twilio receive hook: %s", err)
}
}

29
conn/xmpp/config.go Normal file
View File

@ -0,0 +1,29 @@
package xmpp
import (
"fmt"
"strings"
"gosrc.io/xmpp"
)
type Config struct {
Account string `yaml:"account"`
Password string `yaml:"password"`
}
func (c *Config) xmppConfig() (*xmpp.Config, error) {
parts := strings.SplitN(c.Account, "@", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("xmpp: invalid account '%s'", c.Account)
}
server := parts[1] + ":5222"
return &xmpp.Config{
TransportConfiguration: xmpp.TransportConfiguration{
Address: server,
},
Jid: c.Account,
Credential: xmpp.Password(c.Password),
}, nil
}

78
conn/xmpp/xmpp.go Normal file
View File

@ -0,0 +1,78 @@
package xmpp
import (
"context"
"fmt"
"kas/cps"
"log"
"gosrc.io/xmpp"
"gosrc.io/xmpp/stanza"
)
func messageHandler(s xmpp.Sender, p stanza.Packet) {
msg, ok := p.(stanza.Message)
if !ok {
log.Print("xmpp: ignore packet %T", p)
return
}
response, err := cps.Handle(context.Background(), msg.Body)
if err != nil {
err = fmt.Errorf("xmpp message handler: %w", err)
log.Print(err)
}
reply := stanza.Message{
Attrs: stanza.Attrs{
To: msg.From,
},
Body: response.String(),
}
err = s.Send(reply)
if err != nil {
log.Printf("xmpp: failed to send reply to %s: %s",
msg.From, err)
}
}
func logError(err error) {
err = fmt.Errorf("xmpp: %w", err)
log.Print(err)
}
func Start(cfg *Config) error {
log.Printf("starting XMPP server for %s", cfg.Account)
config, err := cfg.xmppConfig()
if err != nil {
return err
}
go func() {
for {
// TODO: what happens if we ask the bot to stop?
// Best answer: systemd service.
runClient(config)
}
}()
return nil
}
func runClient(cfg *xmpp.Config) {
router := xmpp.NewRouter()
router.HandleFunc("message", messageHandler)
client, err := xmpp.NewClient(cfg, router, logError)
if err != nil {
logError(err)
return
}
// If you pass the client to a connection manager, it will handle the reconnect policy
// for you automatically.
cm := xmpp.NewStreamManager(client, nil)
log.Print(cm.Run())
}

101
cps/cps.go Normal file
View File

@ -0,0 +1,101 @@
// Package cps provides the command processing system.
package cps
import (
"context"
"errors"
"fmt"
"strings"
)
type Command struct {
Context context.Context
Raw string // The original command passed to the agent.
Command string // Which command was invoked?
Arg string // Input string sans the leading command.
Args []string // Tokenised list of arguments.
Meta map[string]string // Sometimes you need additional information.
}
func (cmd *Command) Handle() (*Response, error) {
return handle(cmd)
}
// Parse takes a command string from an input string and parent context.
func Parse(ctx context.Context, line string) (*Command, error) {
line = strings.TrimSpace(line)
args := strings.Fields(line)
if len(args) == 0 {
return nil, errors.New("cps: no command line provided")
}
cmd := &Command{
Context: ctx,
Raw: line,
Command: args[0],
Args: args[1:],
Meta: map[string]string{},
}
cmd.Arg = strings.TrimSpace(strings.TrimPrefix(cmd.Raw, cmd.Command))
return cmd, nil
}
// ParseBackground calls Parse(context.Background, line).
func ParseBackground(line string) (*Command, error) {
return Parse(context.Background(), line)
}
type Response struct {
Message string
Error error
}
func (r *Response) String() string {
if r == nil {
return "carrier signal lost"
}
out := ""
if r.Message != "" {
out += r.Message
} else if r.Error != nil {
out = fmt.Sprintf("Error: %s", r.Error)
}
return out
}
var registry = map[string]func(*Command) (*Response, error){}
func Register(command string, handler func(*Command) (*Response, error)) {
if _, ok := registry[command]; ok {
panic("handler for " + command + " already registered!")
}
registry[command] = handler
}
func handle(cmd *Command) (*Response, error) {
handler, ok := registry[cmd.Command]
if !ok {
err := fmt.Errorf("cps: no handler for command '%s'", cmd.Command)
return &Response{
Message: fmt.Sprintf("%s is not a recognized command", cmd.Command),
Error: err,
}, err
}
return handler(cmd)
}
func Handle(ctx context.Context, line string) (*Response, error) {
cmd, err := Parse(ctx, line)
if err != nil {
return &Response{
Message: "Unable to process message",
Error: err,
}, err
}
return cmd.Handle()
}

103
cps/cps_test.go Normal file
View File

@ -0,0 +1,103 @@
package cps
import (
"context"
"testing"
)
type logger interface {
Log(args ...interface{})
Logf(format string, args ...interface{})
}
func (cmd *Command) cmp(o *Command, l logger) bool {
if cmd.Raw != o.Raw {
l.Logf("mismatched raw: have '%s', want '%s'", o.Raw, cmd.Raw)
return false
}
if cmd.Command != o.Command {
l.Logf("mismatched command: have '%s', want '%s'", o.Command, cmd.Command)
return false
}
if cmd.Arg != o.Arg {
l.Logf("mismatched arg: have '%s', want '%s'", o.Arg, cmd.Arg)
return false
}
if len(cmd.Args) != len(o.Args) {
l.Logf("mismatched arglen: have %d, want %d", len(o.Args), len(cmd.Args))
return false
}
for i := range cmd.Args {
if cmd.Args[i] != o.Args[i] {
l.Logf("mismatched arg %d: have '%s', want '%s'", o.Args[i], cmd.Args[i])
return false
}
}
if len(cmd.Meta) != len(o.Meta) {
l.Logf("mismatched metalen: have %d, want %d", len(o.Meta), len(cmd.Meta))
return false
}
for k := range cmd.Meta {
if cmd.Meta[k] != o.Meta[k] {
l.Logf("mismatched meta for key '%s': have '%s', want '%s'",
k, o.Meta[k], cmd.Meta[k])
return false
}
}
return true
}
func TestParseEmptyCommand(t *testing.T) {
inputLine := "hello"
expected := &Command{
Context: context.Background(),
Raw: "hello",
Command: "hello",
Arg: "",
Args: []string{},
Meta: map[string]string{},
}
cmd, err := ParseBackground(inputLine)
if err != nil {
t.Fatal(err)
}
if !expected.cmp(cmd, t) {
t.FailNow()
}
}
func TestParseNotEmptyCommand(t *testing.T) {
inputLine := "hello world this is a test"
expected := &Command{
Context: context.Background(),
Raw: "hello world this is a test",
Command: "hello",
Arg: "world this is a test",
Args: []string{
"world",
"this",
"is",
"a",
"test",
},
Meta: map[string]string{},
}
cmd, err := ParseBackground(inputLine)
if err != nil {
t.Fatal(err)
}
if !expected.cmp(cmd, t) {
t.FailNow()
}
}

22
cps/help.go Normal file
View File

@ -0,0 +1,22 @@
package cps
import (
"sort"
"strings"
)
func init() {
Register("help", helpHandler)
}
func helpHandler(cmd *Command) (*Response, error) {
knownCommands := make([]string, 0, len(registry))
for command := range registry {
knownCommands = append(knownCommands, command)
}
sort.Strings(knownCommands)
return &Response{
Message: strings.Join(knownCommands, ", "),
}, nil
}

17
cps/ping.go Normal file
View File

@ -0,0 +1,17 @@
package cps
func init() {
Register("ping", pingHandler)
}
// Ping just returns pong and is used to verify that the system is alive.
func pingHandler(cmd *Command) (*Response, error) {
message := "pong"
if len(cmd.Args) > 0 {
message += " (FYI, no additional args are used)"
}
return &Response{
Message: message,
}, nil
}

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module kas
go 1.16
require (
gopkg.in/yaml.v2 v2.4.0 // indirect
gosrc.io/xmpp v0.5.1 // indirect
)

125
go.sum Normal file
View File

@ -0,0 +1,125 @@
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
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/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
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=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gosrc.io/xmpp v0.5.1 h1:Rgrm5s2rt+npGggJH3HakQxQXR8ZZz3+QRzakRQqaq4=
gosrc.io/xmpp v0.5.1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg=
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=

64
main.go Normal file
View File

@ -0,0 +1,64 @@
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"kas/conn"
"kas/conn/http"
"kas/conn/twilio"
"kas/conn/xmpp"
"gopkg.in/yaml.v2"
)
type Config struct {
Connections conn.Config `yaml:"conns"`
HTTP string `yaml:"http"`
}
func loadConfig(path string) (*Config, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("kas: missing config file '%s'", path)
}
return nil, err
}
cfg := &Config{}
err = yaml.Unmarshal(data, cfg)
return cfg, err
}
func main() {
configPath := "kas.yaml"
flag.StringVar(&configPath, "f", configPath, "`path` to config file")
flag.Parse()
cfg, err := loadConfig(configPath)
if err != nil {
log.Fatal(err)
}
if cfg.Connections.XMPP != nil {
if err = xmpp.Start(cfg.Connections.XMPP); err != nil {
log.Fatal(err)
}
}
if cfg.Connections.Twilio != nil {
if err = twilio.SetConfig(cfg.Connections.Twilio); err != nil {
log.Fatal(err)
}
}
if cfg.HTTP != "" {
http.Start(cfg.HTTP)
}
waitForControlC()
}

24
shutdown.go Normal file
View File

@ -0,0 +1,24 @@
package main
import (
"log"
"os"
"os/signal"
"syscall"
)
func waitForControlC() {
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT)
sig := <-sigc
log.Printf("signal %v received, shutting down", sig)
go func() {
sig2 := <-sigc
log.Fatal("second kill signal %v received", sig2)
}()
os.Exit(0)
}