Initial import.
This commit is contained in:
commit
42aa585bd0
|
@ -0,0 +1,3 @@
|
|||
*.yaml
|
||||
*~
|
||||
kas
|
|
@ -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"`
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue