summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth Hunt <elizabeth.hunt@simponic.xyz>2025-01-05 15:16:26 -0800
committerElizabeth Hunt <elizabeth.hunt@simponic.xyz>2025-01-05 15:29:23 -0800
commit2984a715b830410b6d6ce2a8aaa1fc8a2388ee99 (patch)
tree09dc00606931885e8b345791cd1a301335dd494c
parentd86746bb0ddcb7dcfc6225f9fe37f6034c958913 (diff)
downloadphoneof-2984a715b830410b6d6ce2a8aaa1fc8a2388ee99.tar.gz
phoneof-2984a715b830410b6d6ce2a8aaa1fc8a2388ee99.zip
add ntfy integration
-rw-r--r--Dockerfile2
-rw-r--r--README.md1
-rw-r--r--adapters/messaging/db.go29
-rw-r--r--adapters/messaging/http_sms.go69
-rw-r--r--adapters/messaging/messaging.go60
-rw-r--r--adapters/messaging/ntfy.go35
-rw-r--r--api/api.go12
-rw-r--r--api/chat/chat.go61
-rw-r--r--args/args.go8
-rw-r--r--static/js/components/chat.js6
-rw-r--r--utils/quote_str.go7
11 files changed, 205 insertions, 85 deletions
diff --git a/Dockerfile b/Dockerfile
index e84c50b..68c157a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,4 +10,4 @@ RUN go build -o /app/phoneof
EXPOSE 8080
-CMD ["/app/phoneof", "--server", "--migrate", "--port", "8080", "--template-path", "/app/templates", "--database-path", "/app/db/phoneof.db", "--static-path", "/app/static", "--scheduler", "--httpsms-endpoint", "https://httpsms.internal.simponic.xyz"]
+CMD ["/app/phoneof", "--server", "--migrate", "--port", "8080", "--template-path", "/app/templates", "--database-path", "/app/db/phoneof.db", "--static-path", "/app/static", "--scheduler", "--httpsms-endpoint", "https://httpsms.internal.simponic.xyz", "--ntfy-endpoint", "https://ntfy.simponic.hatecomputers.club", "--ntfy-topic", "sms"]
diff --git a/README.md b/README.md
index 8802af4..4c794cd 100644
--- a/README.md
+++ b/README.md
@@ -6,3 +6,4 @@ TODO:
- [ ] pagination for messages
- [ ] full text search?
- [ ] better auth lol
+- [ ] bruh
diff --git a/adapters/messaging/db.go b/adapters/messaging/db.go
new file mode 100644
index 0000000..4cad3e2
--- /dev/null
+++ b/adapters/messaging/db.go
@@ -0,0 +1,29 @@
+package messaging
+
+import (
+ "database/sql"
+ "log"
+ "time"
+
+ "git.simponic.xyz/simponic/phoneof/database"
+)
+
+func PersistMessageContinuation(dbConn *sql.DB, frenId string, messageId string, sentAt time.Time, frenSent bool) Continuation {
+ return func(message Message) ContinuationChain {
+ log.Printf("persisting message %v %s %s %s %v", message, frenId, messageId, sentAt, frenSent)
+ return func(success Continuation, failure Continuation) ContinuationChain {
+ _, err := database.SaveMessage(dbConn, &database.Message{
+ Id: messageId,
+ FrenId: frenId,
+ Message: message.Message,
+ Time: sentAt,
+ FrenSent: frenSent,
+ })
+ if err != nil {
+ log.Printf("err when saving message %s", err)
+ return failure(message)
+ }
+ return success(message)
+ }
+ }
+}
diff --git a/adapters/messaging/http_sms.go b/adapters/messaging/http_sms.go
index c722f21..8d1c99f 100644
--- a/adapters/messaging/http_sms.go
+++ b/adapters/messaging/http_sms.go
@@ -11,13 +11,6 @@ import (
"git.simponic.xyz/simponic/phoneof/utils"
)
-type HttpSmsMessagingAdapter struct {
- ApiToken string
- FromPhoneNumber string
- ToPhoneNumber string
- Endpoint string
-}
-
type HttpSmsMessageData struct {
RequestId string `json:"request_id"`
}
@@ -26,37 +19,35 @@ type HttpSmsMessageSendResponse struct {
Data HttpSmsMessageData `json:"data"`
}
-func (adapter *HttpSmsMessagingAdapter) encodeMessage(message string) string {
- requestId := utils.RandomId()
- return fmt.Sprintf(`{"from":"%s","to":"%s","content":"%s","request_id":"%s"}`, adapter.FromPhoneNumber, adapter.ToPhoneNumber, message, requestId)
-}
-
-func (adapter *HttpSmsMessagingAdapter) SendMessage(message string) (string, error) {
- url := fmt.Sprintf("%s/v1/messages/send", adapter.Endpoint)
- payload := strings.NewReader(adapter.encodeMessage(message))
-
- req, _ := http.NewRequest("POST", url, payload)
- req.Header.Add("x-api-key", adapter.ApiToken)
- req.Header.Add("Content-Type", "application/json")
- res, err := http.DefaultClient.Do(req)
- if err != nil {
- log.Printf("got err sending message send req %s", err)
- return "", err
- }
-
- if res.StatusCode/100 != 2 {
- return "", fmt.Errorf("error sending message: %s %d", message, res.StatusCode)
+func HttpSmsContinuation(apiToken string, fromPhoneNumber string, toPhoneNumber string, httpSmsEndpoint string) Continuation {
+ return func(message Message) ContinuationChain {
+ encodedMsg := fmt.Sprintf(`{"from":"%s","to":"%s","content":"%s"}`, fromPhoneNumber, toPhoneNumber, utils.Quote(message.Encode()))
+ log.Println(encodedMsg)
+
+ return func(success Continuation, failure Continuation) ContinuationChain {
+ url := fmt.Sprintf("%s/v1/messages/send", httpSmsEndpoint)
+ payload := strings.NewReader(encodedMsg)
+
+ req, _ := http.NewRequest("POST", url, payload)
+ req.Header.Add("x-api-key", apiToken)
+ req.Header.Add("Content-Type", "application/json")
+ res, err := http.DefaultClient.Do(req)
+ if err != nil || res.StatusCode/100 != 2 {
+ log.Printf("got err sending message send req %s %v %s", message, res, err)
+ return failure(message)
+ }
+
+ defer res.Body.Close()
+ body, _ := io.ReadAll(res.Body)
+
+ var response HttpSmsMessageSendResponse
+ err = json.Unmarshal(body, &response)
+ if err != nil {
+ log.Printf("got error unmarshaling response: %s %s", body, err)
+ return failure(message)
+ }
+
+ return success(message)
+ }
}
-
- defer res.Body.Close()
- body, _ := io.ReadAll(res.Body)
-
- var response HttpSmsMessageSendResponse
- err = json.Unmarshal(body, &response)
- if err != nil {
- log.Printf("got error unmarshaling response: %s %s", body, err)
- return "", err
- }
-
- return response.Data.RequestId, nil
}
diff --git a/adapters/messaging/messaging.go b/adapters/messaging/messaging.go
index 607258a..f802503 100644
--- a/adapters/messaging/messaging.go
+++ b/adapters/messaging/messaging.go
@@ -1,5 +1,61 @@
package messaging
-type MessagingAdapter interface {
- SendMessage(message string) (string, error)
+import (
+ "fmt"
+ "log"
+ "strings"
+ "time"
+)
+
+type Message struct {
+ FrenName string
+ Message string
+}
+
+func (m *Message) Encode() string {
+ return m.FrenName + " " + m.Message
+}
+
+func Decode(message string) (*Message, error) {
+ content := strings.SplitN(message, " ", 2)
+ if len(content) < 2 {
+ return nil, fmt.Errorf("no space delimiter")
+ }
+ return &Message{
+ FrenName: content[0],
+ Message: content[1],
+ }, nil
+}
+
+func IdContinuation(message Message) ContinuationChain {
+ return func(success Continuation, _failure Continuation) ContinuationChain {
+ return success(message)
+ }
}
+
+func FailurePassingContinuation(message Message) ContinuationChain {
+ return func(_success Continuation, failure Continuation) ContinuationChain {
+ return failure(message)
+ }
+}
+
+func LogContinuation(message Message) ContinuationChain {
+ return func(success Continuation, _failure Continuation) ContinuationChain {
+ now := time.Now().UTC()
+
+ log.Println(now, message)
+ return success(message)
+ }
+}
+
+// basically b(a(message)) if and only if b is successful
+func Compose(a Continuation, b Continuation) Continuation {
+ return func(message Message) ContinuationChain {
+ return func(success Continuation, failure Continuation) ContinuationChain {
+ return b(message)(a, FailurePassingContinuation)(success, failure)
+ }
+ }
+}
+
+type Continuation func(Message) ContinuationChain
+type ContinuationChain func(Continuation, Continuation) ContinuationChain
diff --git a/adapters/messaging/ntfy.go b/adapters/messaging/ntfy.go
new file mode 100644
index 0000000..837c01b
--- /dev/null
+++ b/adapters/messaging/ntfy.go
@@ -0,0 +1,35 @@
+package messaging
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "strings"
+
+ "git.simponic.xyz/simponic/phoneof/utils"
+)
+
+func SendNtfy(topic string, ntfyEndpoint string) Continuation {
+ return func(message Message) ContinuationChain {
+ return func(success Continuation, failure Continuation) ContinuationChain {
+ log.Println(message)
+ if message.FrenName != "ntfy" {
+ log.Printf("fren name for message %v is not ntfy so we wont send it there", message)
+ return success(message)
+ }
+ encodedMsg := fmt.Sprintf(`{"message": "%s", "topic": "%s"}`, utils.Quote(message.Message), utils.Quote(topic))
+
+ url := ntfyEndpoint
+ payload := strings.NewReader(encodedMsg)
+
+ req, _ := http.NewRequest("PUT", url, payload)
+ req.Header.Add("Content-Type", "application/json")
+ res, err := http.DefaultClient.Do(req)
+ if err != nil || res.StatusCode/100 != 2 {
+ log.Printf("got err sending message send req %s %v %s", encodedMsg, res, err)
+ return failure(message)
+ }
+ return success(message)
+ }
+ }
+}
diff --git a/api/api.go b/api/api.go
index 07db731..cb5101b 100644
--- a/api/api.go
+++ b/api/api.go
@@ -99,19 +99,15 @@ func MakeMux(argv *args.Arguments, dbConn *sql.DB) *http.ServeMux {
LogRequestContinuation(requestContext, r, w)(chat.ValidateFren, FailurePassingContinuation)(chat.FetchMessagesContinuation, FailurePassingContinuation)(template.TemplateContinuation("messages.html", false), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
})
- messageHandler := messaging.HttpSmsMessagingAdapter{
- ApiToken: os.Getenv("HTTPSMS_API_TOKEN"),
- FromPhoneNumber: os.Getenv("FROM_PHONE_NUMBER"),
- ToPhoneNumber: os.Getenv("TO_PHONE_NUMBER"),
- Endpoint: argv.HttpSmsEndpoint,
- }
- sendMessageContinuation := chat.SendMessageContinuation(&messageHandler)
+ httpsms := messaging.HttpSmsContinuation(os.Getenv("HTTPSMS_API_TOKEN"), os.Getenv("FROM_PHONE_NUMBER"), os.Getenv("TO_PHONE_NUMBER"), argv.HttpSmsEndpoint)
+ ntfy := messaging.SendNtfy(argv.NtfyTopic, argv.NtfyEndpoint)
+ sendMessageContinuation := chat.SendMessageContinuation(messaging.Compose(ntfy, httpsms))
mux.HandleFunc("POST /chat", func(w http.ResponseWriter, r *http.Request) {
requestContext := makeRequestContext()
LogRequestContinuation(requestContext, r, w)(chat.ValidateFren, FailurePassingContinuation)(sendMessageContinuation, FailurePassingContinuation)(template.TemplateContinuation("chat.html", true), FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
})
- smsEventProcessor := chat.ChatEventProcessorContinuation(os.Getenv("HTTPSMS_SIGNING_KEY"))
+ smsEventProcessor := chat.ChatEventProcessorContinuation(os.Getenv("TO_PHONE_NUMBER"), os.Getenv("HTTPSMS_SIGNING_KEY"), ntfy)
mux.HandleFunc("POST /chat/event", func(w http.ResponseWriter, r *http.Request) {
requestContext := makeRequestContext()
LogRequestContinuation(requestContext, r, w)(smsEventProcessor, FailurePassingContinuation)(LogExecutionTimeContinuation, LogExecutionTimeContinuation)(IdContinuation, IdContinuation)
diff --git a/api/chat/chat.go b/api/chat/chat.go
index 51ee47d..fcff9f0 100644
--- a/api/chat/chat.go
+++ b/api/chat/chat.go
@@ -2,13 +2,15 @@ package chat
import (
"encoding/json"
- "github.com/golang-jwt/jwt/v5"
+ "fmt"
"io"
"log"
"net/http"
"strings"
"time"
+ "github.com/golang-jwt/jwt/v5"
+
"git.simponic.xyz/simponic/phoneof/adapters/messaging"
"git.simponic.xyz/simponic/phoneof/api/types"
"git.simponic.xyz/simponic/phoneof/database"
@@ -59,28 +61,27 @@ func FetchMessagesContinuation(context *types.RequestContext, req *http.Request,
}
}
-func SendMessageContinuation(messagingAdapter messaging.MessagingAdapter) func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+func SendMessageContinuation(messagingPipeline messaging.Continuation) func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
rawMessage := req.FormValue("message")
now := time.Now().UTC()
- messageRequestId, err := messagingAdapter.SendMessage(context.User.Name + " " + rawMessage)
+
+ persist := messaging.PersistMessageContinuation(context.DBConn, context.User.Id, context.Id, now, true)
+ var err error
+ messaging.LogContinuation(messaging.Message{
+ FrenName: context.User.Name,
+ Message: rawMessage,
+ })(messagingPipeline, messaging.FailurePassingContinuation)(persist, messaging.FailurePassingContinuation)(messaging.IdContinuation, func(message messaging.Message) messaging.ContinuationChain {
+ err = fmt.Errorf("err sending message from: %s %s", context.User, rawMessage)
+ return messaging.FailurePassingContinuation(message)
+ })
+
if err != nil {
- log.Printf("err sending message %s %s %s", context.User, rawMessage, err)
// yeah this might be a 400 or whatever, i’ll fix it later
resp.WriteHeader(http.StatusInternalServerError)
return failure(context, req, resp)
}
-
- message, err := database.SaveMessage(context.DBConn, &database.Message{
- Id: messageRequestId,
- FrenId: context.User.Id,
- Message: rawMessage,
- Time: now,
- FrenSent: true,
- })
-
- log.Printf("Saved message %v", message)
return success(context, req, resp)
}
}
@@ -120,7 +121,7 @@ type HttpSmsEvent struct {
Id string `json:"id"`
}
-func ChatEventProcessorContinuation(signingKey string) func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
+func ChatEventProcessorContinuation(allowedFrom string, signingKey string, messagingPipeline messaging.Continuation) func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain {
return func(success types.Continuation, failure types.Continuation) types.ContinuationChain {
// check signing
@@ -156,38 +157,34 @@ func ChatEventProcessorContinuation(signingKey string) func(context *types.Reque
log.Printf("got non-receive event %s", event.Type)
return success(context, req, resp)
}
+ if event.Data.Contact != allowedFrom {
+ log.Printf("someone did something naughty %s", event.Data.Contact)
+ return success(context, req, resp)
+ }
- // respond to texts with "<fren name> <content>"
- content := strings.SplitN(event.Data.Content, " ", 2)
- if len(content) < 2 {
- log.Printf("no space delimiter")
+ message, err := messaging.Decode(event.Data.Content)
+ if err != nil {
+ log.Printf("err when decoding message %s", err)
resp.WriteHeader(http.StatusBadRequest)
return failure(context, req, resp)
}
- name := content[0]
- rawMessage := content[1]
- fren, err := database.FindFrenByName(context.DBConn, name)
+ fren, err := database.FindFrenByName(context.DBConn, message.FrenName)
if err != nil {
- log.Printf("err when getting fren %s %s", name, err)
+ log.Printf("err when getting fren %s %s", fren.Name, err)
resp.WriteHeader(http.StatusBadRequest)
return failure(context, req, resp)
}
- // save the message!
- _, err = database.SaveMessage(context.DBConn, &database.Message{
- Id: event.Id,
- FrenId: fren.Id,
- Message: rawMessage,
- Time: event.Data.Timestamp,
- FrenSent: false,
+ persist := messaging.PersistMessageContinuation(context.DBConn, fren.Id, context.Id, event.Data.Timestamp, false)
+ messaging.LogContinuation(*message)(messagingPipeline, messaging.FailurePassingContinuation)(persist, messaging.FailurePassingContinuation)(messaging.IdContinuation, func(message messaging.Message) messaging.ContinuationChain {
+ err = fmt.Errorf("err propagating stuff for message %s", message)
+ return messaging.FailurePassingContinuation(message)
})
if err != nil {
- log.Printf("err when saving message %s %s", name, err)
resp.WriteHeader(http.StatusInternalServerError)
return failure(context, req, resp)
}
-
return success(context, req, resp)
}
}
diff --git a/args/args.go b/args/args.go
index 458d36e..6c112c6 100644
--- a/args/args.go
+++ b/args/args.go
@@ -17,6 +17,9 @@ type Arguments struct {
HttpSmsEndpoint string
+ NtfyEndpoint string
+ NtfyTopic string
+
Port int
Server bool
}
@@ -60,6 +63,9 @@ func GetArgs() (*Arguments, error) {
httpSmsEndpoint := flag.String("httpsms-endpoint", "https://httpsms.com", "HTTPSMS endpoint")
+ ntfyTopic := flag.String("ntfy-topic", "sms", "NTFY endpoint")
+ ntfyEndpoint := flag.String("ntfy-endpoint", "https://ntfy.simponic.hatecomputers.club", "HTTPSMS endpoint")
+
scheduler := flag.Bool("scheduler", false, "Run scheduled jobs via cron")
migrate := flag.Bool("migrate", false, "Run the migrations")
@@ -77,6 +83,8 @@ func GetArgs() (*Arguments, error) {
Migrate: *migrate,
Scheduler: *scheduler,
HttpSmsEndpoint: *httpSmsEndpoint,
+ NtfyTopic: *ntfyTopic,
+ NtfyEndpoint: *ntfyEndpoint,
}
err := validateArgs(args)
if err != nil {
diff --git a/static/js/components/chat.js b/static/js/components/chat.js
index bdc8ad1..e5d1185 100644
--- a/static/js/components/chat.js
+++ b/static/js/components/chat.js
@@ -6,14 +6,14 @@ const runChat = async () => {
r.text(),
);
- const { scrollTop, scrollHeight } = document.getElementById(
+ const { scrollTop, scrollTopMax } = document.getElementById(
"chat-container",
) ?? { scrollTop: 0 };
- const isAtEdge = scrollTop === scrollHeight || scrollTop === 0;
+ const isAtEdge = scrollTop > (0.92 * scrollTopMax) || scrollTop === 0;
document.getElementById("messages").innerHTML = html;
if (isAtEdge) {
document.getElementById("chat-container").scrollTop =
- document.getElementById("chat-container").scrollHeight;
+ document.getElementById("chat-container").scrollTopMax;
} else {
// save the position.
document.getElementById("chat-container").scrollTop = scrollTop;
diff --git a/utils/quote_str.go b/utils/quote_str.go
new file mode 100644
index 0000000..acff5a2
--- /dev/null
+++ b/utils/quote_str.go
@@ -0,0 +1,7 @@
+package utils
+
+import "strings"
+
+func Quote(s string) string {
+ return strings.Replace(s, `"`, `\"`, -1)
+}