122 lines
3.7 KiB
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
|
|
}
|