Alter 3 files

Add `.dockerignore`
Add `Dockerfile`
Update `main.go`
This commit is contained in:
akp 2024-11-01 19:53:36 +00:00
parent 29d7f5c9ed
commit d501754b0b
No known key found for this signature in database
GPG key ID: CF8D58F3DEB20755
3 changed files with 133 additions and 7 deletions

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
stupid-option-2/
track-generator/

12
Dockerfile Normal file
View file

@ -0,0 +1,12 @@
FROM golang:1.23 as builder
RUN mkdir /build
ADD . /build/
WORKDIR /build
RUN CGO_ENABLED=0 GOOS=linux go build -a -buildvcs=false -installsuffix cgo -ldflags '-extldflags "-static" -s -w' -o main git.tdpain.net/codemicro/pi-phone
FROM alpine
COPY --from=builder /build/main /
WORKDIR /run
CMD ["../main"]

View file

@ -2,12 +2,17 @@ package main
import (
"embed"
"encoding/json"
"fmt"
"github.com/twilio/twilio-go/twiml"
"go.akpain.net/cfger"
"io"
"log/slog"
"net/http"
"net/url"
"os"
"strings"
"time"
)
func main() {
@ -18,21 +23,75 @@ func main() {
}
var (
httpAddr = cfger.GetEnv("PIPHONE_HTTP_ADDRESS").WithDefault("127.0.0.1:8080").AsString()
externalAddr = strings.TrimSuffix(cfger.GetEnv("PIPHONE_EXTERNAL_ADDRESS").Required().AsString(), "/")
httpAddr = cfger.GetEnv("PIPHONE_HTTP_ADDRESS").WithDefault("127.0.0.1:8080").AsString()
externalAddr = strings.TrimSuffix(cfger.GetEnv("PIPHONE_EXTERNAL_ADDRESS").Required().AsString(), "/")
twilioAccountSID = cfger.GetEnv("PIPHONE_TWILIO_ACCOUNT_SID").Required().AsString()
twilioAccountAuthToken = cfger.GetEnv("PIPHONE_TWILIO_ACCOUNT_AUTH_TOKEN").Required().AsString()
twilioFromNumber = cfger.GetEnv("PIPHONE_TWILIO_FROM_NUMBER").Required().AsString()
scheduledCalls = cfger.GetEnv("PIPHONE_SCHEDULED_CALLS").AsString()
)
func run() error {
if err := scheduleCalls(); err != nil {
return err
}
return runServer()
}
func scheduleCalls() error {
if scheduledCalls == "" {
return nil
}
var schedule []struct {
Time int64 `json:"t"`
Number string `json:"n"`
}
if err := json.Unmarshal([]byte(scheduledCalls), &schedule); err != nil {
return fmt.Errorf("unable to unmarshal scheduled calls: %w", err)
}
n := len(schedule)
for _, call := range schedule {
delta := call.Time - time.Now().Unix()
if delta < 0 {
slog.Warn("scheduled call time is in the past, ignoring", "time", call.Time)
n -= 1
continue
}
go func() {
time.Sleep(time.Second * time.Duration(delta))
logger := slog.Default().With("time", call.Time, "toNumber", call.Number)
if resp, err := initiateOutboundCall(call.Number); err != nil {
logger.Error("erorred in scheduled call", "error", err)
} else if resp.StatusCode/100 != 2 {
logger.Error("unexpected status code from call initiation", "statusCode", resp.StatusCode)
}
}()
}
slog.Info("scheduled calls", "n", n)
return nil
}
//go:embed audio
var audioAssets embed.FS
func run() error {
func runServer() error {
mux := http.NewServeMux()
s := server{}
mux.HandleFunc("GET /calls/new", s.handleNewCall)
mux.HandleFunc("GET /calls/menu", s.handleCallMenu)
mux.Handle("/", http.FileServer(http.FS(audioAssets)))
mux.HandleFunc("POST /startCall", s.logCaller(s.handleStartCall))
mux.HandleFunc("GET /api/calls/new", s.logCaller(s.handleNewCall))
mux.HandleFunc("GET /api/calls/menu", s.logCaller(s.handleCallMenu))
mux.Handle("GET /audio/", http.FileServer(http.FS(audioAssets)))
mux.HandleFunc("GET /", s.handleIndex)
slog.Info("server alive", "address", httpAddr)
return http.ListenAndServe(httpAddr, mux)
@ -42,8 +101,33 @@ func getExternalUrlTo(path string) string {
return externalAddr + "/" + path
}
func initiateOutboundCall(phoneTo string) (*http.Response, error) {
slog.Info("initiating call", "to", phoneTo)
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Calls.json", twilioAccountSID), strings.NewReader(url.Values{
"From": []string{twilioFromNumber},
"To": []string{phoneTo},
"Url": []string{getExternalUrlTo("api/calls/new")},
"Method": []string{"GET"},
}.Encode()))
if err != nil {
return nil, fmt.Errorf("create http request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth(twilioAccountSID, twilioAccountAuthToken)
return http.DefaultClient.Do(req)
}
type server struct{}
func (server) logCaller(handler http.HandlerFunc) http.HandlerFunc {
return func(rw http.ResponseWriter, rq *http.Request) {
slog.Info("call request", "route", rq.URL.Path, "from", rq.URL.Query().Get("From"), "to", rq.URL.Query().Get("To"))
handler(rw, rq)
}
}
func (server) die(rw http.ResponseWriter, rq *http.Request, err error) {
slog.Error("unhandled error in HTTP handler", "url", rq.URL, "error", err)
rw.WriteHeader(http.StatusInternalServerError)
@ -64,7 +148,7 @@ func (s server) handleNewCall(rw http.ResponseWriter, rq *http.Request) {
elems := []twiml.Element{
&twiml.VoiceGather{
Method: "GET",
Action: getExternalUrlTo("calls/menu"),
Action: getExternalUrlTo("api/calls/menu"),
Timeout: "10",
NumDigits: "1",
ActionOnEmptyResult: "true",
@ -115,3 +199,31 @@ func (s server) handleCallMenu(rw http.ResponseWriter, rq *http.Request) {
s.writeTwimlOrDie(rw, rq, elems)
return
}
func (s server) handleIndex(rw http.ResponseWriter, _ *http.Request) {
rw.Header().Set("Content-Type", "text/html")
rw.Write([]byte(`<!DOCTYPE html><html><body>
<h1>Pi hotline control panel</h1>
<form action="startCall" method="POST">
<label for="number">Start a call</label><input type="text" id="number" name="number" placeholder="Phone number"><input type="submit">
</form>
<p>Nuffield G17 is: +441214158512</p>
</body></html>`))
}
func (s server) handleStartCall(rw http.ResponseWriter, rq *http.Request) {
resp, err := initiateOutboundCall(rq.FormValue("number"))
if err != nil {
s.die(rw, rq, err)
return
}
defer resp.Body.Close()
rw.Header().Set("Content-Type", "text/plain")
body, err := io.ReadAll(resp.Body)
if err != nil {
s.die(rw, rq, err)
}
_, _ = rw.Write(append([]byte(resp.Status+"\n\n"), body...))
}