Alter 3 files
Add `.dockerignore` Add `Dockerfile` Update `main.go`
This commit is contained in:
parent
29d7f5c9ed
commit
d501754b0b
3 changed files with 133 additions and 7 deletions
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
stupid-option-2/
|
||||
track-generator/
|
12
Dockerfile
Normal file
12
Dockerfile
Normal 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"]
|
126
pi-phone/main.go
126
pi-phone/main.go
|
@ -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...))
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue