This commit is contained in:
parent
d86746bb0d
commit
2984a715b8
@ -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"]
|
||||
|
@ -6,3 +6,4 @@ TODO:
|
||||
- [ ] pagination for messages
|
||||
- [ ] full text search?
|
||||
- [ ] better auth lol
|
||||
- [ ] bruh
|
||||
|
29
adapters/messaging/db.go
Normal file
29
adapters/messaging/db.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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,26 +19,22 @@ 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 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)
|
||||
|
||||
func (adapter *HttpSmsMessagingAdapter) SendMessage(message string) (string, error) {
|
||||
url := fmt.Sprintf("%s/v1/messages/send", adapter.Endpoint)
|
||||
payload := strings.NewReader(adapter.encodeMessage(message))
|
||||
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", adapter.ApiToken)
|
||||
req.Header.Add("x-api-key", 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)
|
||||
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()
|
||||
@ -55,8 +44,10 @@ func (adapter *HttpSmsMessagingAdapter) SendMessage(message string) (string, err
|
||||
err = json.Unmarshal(body, &response)
|
||||
if err != nil {
|
||||
log.Printf("got error unmarshaling response: %s %s", body, err)
|
||||
return "", err
|
||||
return failure(message)
|
||||
}
|
||||
|
||||
return response.Data.RequestId, nil
|
||||
return success(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
35
adapters/messaging/ntfy.go
Normal file
35
adapters/messaging/ntfy.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
12
api/api.go
12
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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
// respond to texts with "<fren name> <content>"
|
||||
content := strings.SplitN(event.Data.Content, " ", 2)
|
||||
if len(content) < 2 {
|
||||
log.Printf("no space delimiter")
|
||||
resp.WriteHeader(http.StatusBadRequest)
|
||||
return failure(context, req, resp)
|
||||
if event.Data.Contact != allowedFrom {
|
||||
log.Printf("someone did something naughty %s", event.Data.Contact)
|
||||
return success(context, req, resp)
|
||||
}
|
||||
name := content[0]
|
||||
rawMessage := content[1]
|
||||
|
||||
fren, err := database.FindFrenByName(context.DBConn, name)
|
||||
message, err := messaging.Decode(event.Data.Content)
|
||||
if err != nil {
|
||||
log.Printf("err when getting fren %s %s", name, err)
|
||||
log.Printf("err when decoding message %s", 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,
|
||||
fren, err := database.FindFrenByName(context.DBConn, message.FrenName)
|
||||
if err != nil {
|
||||
log.Printf("err when getting fren %s %s", fren.Name, err)
|
||||
resp.WriteHeader(http.StatusBadRequest)
|
||||
return failure(context, req, resp)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
7
utils/quote_str.go
Normal file
7
utils/quote_str.go
Normal file
@ -0,0 +1,7 @@
|
||||
package utils
|
||||
|
||||
import "strings"
|
||||
|
||||
func Quote(s string) string {
|
||||
return strings.Replace(s, `"`, `\"`, -1)
|
||||
}
|
Loading…
Reference in New Issue
Block a user