Initial import.
This commit is contained in:
83
conn/twilio/config.go
Normal file
83
conn/twilio/config.go
Normal 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
116
conn/twilio/message.go
Normal 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
49
conn/twilio/twilio.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user