diff --git a/README.md b/README.md index 8bb3256..48b7570 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,18 @@ curl \ HTML-Content might not be fully rendered in your Matrix-Client - see the corresponding [Matrix specs](https://spec.matrix.org/unstable/client-server-api/#mroommessage-msgtypes). This also holds for Markdown, as it is transfered to the corresponding HTML-syntax. +### Deleting a Message + +You can delete a message, this will send a notification in response to the original message informing you that the message is "deleted". + +You need the message ID for deleting a message. As it might contain characters not valid in uris we provide an additional `id_url_encoded` field for messages, use that value for deleting a message. + +```bash +curl \ + --request DELETE \ + "https://pushbits.example.com/message/${MESSAGE_ID}?token=$PB_TOKEN" +``` + ## 👮 Acknowledgments The idea for this software and most parts of the initial source are heavily inspired by [Gotify](https://gotify.net/). diff --git a/internal/api/errors.go b/internal/api/errors.go new file mode 100644 index 0000000..db296e3 --- /dev/null +++ b/internal/api/errors.go @@ -0,0 +1,5 @@ +package api + +import "errors" + +var ErrorMessageNotFound = errors.New("Message not found") diff --git a/internal/api/notification.go b/internal/api/notification.go index 9cd51d0..064c716 100644 --- a/internal/api/notification.go +++ b/internal/api/notification.go @@ -3,6 +3,7 @@ package api import ( "log" "net/http" + "net/url" "strings" "time" @@ -52,6 +53,7 @@ func (h *NotificationHandler) CreateNotification(ctx *gin.Context) { } notification.ID = messageID + notification.UrlEncodedID = url.QueryEscape(messageID) ctx.JSON(http.StatusOK, ¬ification) } @@ -70,5 +72,9 @@ func (h *NotificationHandler) DeleteNotification(ctx *gin.Context) { Date: time.Now(), } - h.DP.DeleteNotification(application, &n) + if success := successOrAbort(ctx, http.StatusInternalServerError, h.DP.DeleteNotification(application, &n)); !success { + return + } + + ctx.Status(http.StatusOK) } diff --git a/internal/api/util.go b/internal/api/util.go index 177f537..d045139 100644 --- a/internal/api/util.go +++ b/internal/api/util.go @@ -11,7 +11,13 @@ import ( func successOrAbort(ctx *gin.Context, code int, err error) bool { if err != nil { - ctx.AbortWithError(code, err) + // If we know the error force error code + switch err { + case ErrorMessageNotFound: + ctx.AbortWithError(http.StatusNotFound, err) + default: + ctx.AbortWithError(code, err) + } } return err == nil diff --git a/internal/dispatcher/notification.go b/internal/dispatcher/notification.go index edc9c97..c17c634 100644 --- a/internal/dispatcher/notification.go +++ b/internal/dispatcher/notification.go @@ -1,19 +1,30 @@ package dispatcher import ( + "errors" "fmt" "html" "log" "strings" "github.com/gomarkdown/markdown" + "github.com/matrix-org/gomatrix" + "github.com/pushbits/server/internal/api" "github.com/pushbits/server/internal/model" ) +type MessageFormat string + +const ( + FormatHTML = MessageFormat("org.matrix.custom.html") +) + type ReplyEvent struct { - Body string `json:"body"` - Msgtype string `json:"msgtype"` - RelatesTo RelatesTo `json:"m.relates_to,omitempty"` + Body string `json:"body"` + FormattedBody string `json:"formatted_body"` + Msgtype string `json:"msgtype"` + RelatesTo RelatesTo `json:"m.relates_to,omitempty"` + Format MessageFormat `json:"format"` } type RelatesTo struct { @@ -39,30 +50,56 @@ func (d *Dispatcher) SendNotification(a *model.Application, n *model.Notificatio // DeleteNotification sends a notification to the specified user that another notificaion is deleted func (d *Dispatcher) DeleteNotification(a *model.Application, n *model.DeleteNotification) error { + var deleteMessageFormattedBody string + var deleteMessageBody string log.Printf("Sending delete notification to room %s", a.MatrixID) + + deleteMessage, err := d.getMessage(a, n.ID) + + if err != nil { + log.Println(err) + return api.ErrorMessageNotFound + } + + if val, ok := deleteMessage.Content["body"]; ok { + body, ok := val.(string) + if ok { + deleteMessageBody = body + deleteMessageFormattedBody = body + } + } else { + log.Println("Message to delete has wrong format") + return api.ErrorMessageNotFound + } + + if val, ok := deleteMessage.Content["formatted_body"]; ok { + body, ok := val.(string) + if ok { + deleteMessageFormattedBody = body + } + } + + // formatting according to https://matrix.org/docs/spec/client_server/latest#fallbacks-and-event-representation + formattedBody := fmt.Sprintf("
In reply to %s
%s
\n
This message got deleted.", deleteMessage.RoomID, deleteMessage.ID, deleteMessage.Sender, deleteMessage.Sender, deleteMessageFormattedBody) + body := fmt.Sprintf("> <%s>%s\n\nThis message got deleted", deleteMessage.Sender, deleteMessageBody) + event := ReplyEvent{ - Body: "This message got deleted.", - Msgtype: "m.text", + FormattedBody: formattedBody, + Body: body, + Msgtype: "m.text", + Format: FormatHTML, } irt := make(map[string]string) - irt["event_id"] = n.ID rt := RelatesTo{ InReplyTo: irt, } event.RelatesTo = rt + irt["event_id"] = deleteMessage.ID - _, err := d.client.SendMessageEvent(a.MatrixID, "m.room.message", event) + _, err = d.client.SendMessageEvent(a.MatrixID, "m.room.message", event) return err - - /* - messages, _ := d.client.Messages(a.MatrixID, "", "", 'b', 10) - - js, _ := json.Marshal(messages) - - log.Println(string(js)) - */ } // HTML-formats the title @@ -130,3 +167,21 @@ func (d *Dispatcher) coloredText(color string, text string) string { return "" + text + "" } + +// TODO cubicroot: find a way to only request on specific event +func (d *Dispatcher) getMessage(a *model.Application, id string) (gomatrix.Event, error) { + start := "" + end := "" + maxPages := 10 // maximum pages to request (10 messages per page) + + for i := 0; i < maxPages; i++ { + messages, _ := d.client.Messages(a.MatrixID, start, end, 'b', 10) + for _, event := range messages.Chunk { + if event.ID == id { + return event, nil + } + } + start = messages.End + } + return gomatrix.Event{}, errors.New("Message not found") +} diff --git a/internal/model/notification.go b/internal/model/notification.go index 7bb64b7..569e8ee 100644 --- a/internal/model/notification.go +++ b/internal/model/notification.go @@ -7,6 +7,7 @@ import ( // Notification holds information like the message, the title, and the priority of a notification. type Notification struct { ID string `json:"id"` + UrlEncodedID string `json:"id_url_encoded"` ApplicationID uint `json:"appid"` Message string `json:"message" form:"message" query:"message" binding:"required"` Title string `json:"title" form:"title" query:"title"` diff --git a/internal/router/router.go b/internal/router/router.go index 1debc4a..6392973 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -46,7 +46,7 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp r.GET("/health", healthHandler.Health) r.POST("/message", auth.RequireApplicationToken(), notificationHandler.CreateNotification) - r.DELETE("/message/:messageid", api.RequireIDInURI(), auth.RequireApplicationToken(), notificationHandler.DeleteNotification) + r.DELETE("/message/:messageid", api.RequireMessageIDInURI(), auth.RequireApplicationToken(), notificationHandler.DeleteNotification) r.GET("/test", auth.RequireApplicationToken(), notificationHandler.DeleteNotification)