sgx-aa/provider.go

122 lines
3.7 KiB
Go

package main
import (
// Standard library
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
const (
// The default API endpoint used for sending messages.
upstreamEndpoint = "https://sms.aa.net.uk/sms.cgi"
// The maximum number of message parts we'll attempt to send at any one time.
defaultMessageLimit = 3
// The maximum amount of time any HTTP request might take to complete.
defaultHTTPTimeout = 15 * time.Second
)
// A Provider represents any method for sending and receiving messages from a remote source.
type Provider interface {
SendMessage(User, Message) error
}
// AndrewsArnoldProvider is a message [Provider] that implements support for the Andrews & Arnold
// SMS API.
type AndrewsArnoldProvider struct {
messageLimit int // The maximum number of message parts we'll attempt to send.
reportURI string // The URI used for requesting a delivery report.
client *http.Client // The underlying HTTP client to use in making requests to A&A.
}
// NewAndrewsArnoldProvider returns a [Provider] that can be used in sending and receiving messages
// over the Andrews & Arnold SMS API.
func NewAndrewsArnoldProvider(options ...ProviderOption) (*AndrewsArnoldProvider, error) {
var p = &AndrewsArnoldProvider{
messageLimit: defaultMessageLimit,
client: &http.Client{
Timeout: defaultHTTPTimeout,
},
}
for _, fn := range options {
if err := fn(p); err != nil {
return nil, err
}
}
return p, nil
}
type ProviderOption func(*AndrewsArnoldProvider) error
func WithReportHandler(hostname string) ProviderOption {
return func(p *AndrewsArnoldProvider) error {
if hostname == "" {
return fmt.Errorf("cannot use report handler with empty hostname")
}
p.reportURI = hostname + "/%s/sms/srr?code=%%code&da=%%da&oa=%%oa&stanzaId=%s"
return nil
}
}
// SendMessage attempts to send the given [Message], authenticating as the given source [User].
// Messages may be of a size up to the message limit defined at the [AndrewsArnoldProvider] level,
// and may fail to send otherwise. If the [Message.ID] field is set, a delivery report will be
// requested for the given message.
func (p *AndrewsArnoldProvider) SendMessage(user User, message Message) error {
if user.tel == "" || user.password == "" {
return fmt.Errorf("cannot send message for incomplete user information")
} else if message.Dest == "" {
return fmt.Errorf("cannot send message for empty destination number")
} else if message.Body == "" {
return nil
}
data := url.Values{
"username": {user.tel},
"password": {user.password},
"da": {message.Dest},
"ud": {message.Body},
"limit": {strconv.Itoa(p.messageLimit)},
"costcentre": {"sgx-aa"},
}
// Send delivery report if a (presumably unique) message ID was given.
// TODO: Maybe pull the report URI from an attached "delibery report handler".
if message.ID != "" && p.reportURI != "" {
data.Add("srr", fmt.Sprintf(p.reportURI, user.Token(), url.QueryEscape(message.ID)))
}
req, err := http.NewRequest("POST", upstreamEndpoint, strings.NewReader(data.Encode()))
if err != nil {
return fmt.Errorf("failed preparing HTTP request for message: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := p.client.Do(req)
if err != nil {
return fmt.Errorf("failed sending HTTP request for message: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 4096))
if err != nil {
return err
}
if msg, ok := strings.CutPrefix(string(body), "ERR:"); ok {
return fmt.Errorf("failed sending HTTP request for message: %s", msg)
} else if resp.StatusCode >= 400 {
return fmt.Errorf("failed sending HTTP request for message: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode))
}
return nil
}