From 5be204dc19ae7ea2db74eeb18ec5194e4dadf834 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Wed, 2 Jun 2021 17:46:04 +0200 Subject: [PATCH 1/8] proof of concept --- internal/api/notification.go | 9 ++++++ internal/dispatcher/notification.go | 43 +++++++++++++++++++++++++++++ internal/model/notification.go | 11 ++++++++ internal/router/router.go | 2 ++ 4 files changed, 65 insertions(+) diff --git a/internal/api/notification.go b/internal/api/notification.go index bfb8af0..b4a2100 100644 --- a/internal/api/notification.go +++ b/internal/api/notification.go @@ -19,6 +19,7 @@ type NotificationDatabase interface { // The NotificationDispatcher interface for relaying notifications. type NotificationDispatcher interface { SendNotification(a *model.Application, n *model.Notification) error + SendDeleteNotification(a *model.Application, n *model.DeleteNotification) error } // NotificationHandler holds information for processing requests about notifications. @@ -51,3 +52,11 @@ func (h *NotificationHandler) CreateNotification(ctx *gin.Context) { ctx.JSON(http.StatusOK, ¬ification) } + +func (h *NotificationHandler) DeleteNotification(ctx *gin.Context) { + application := authentication.GetApplication(ctx) + + n := model.DeleteNotification{} + + h.DP.SendDeleteNotification(application, &n) +} diff --git a/internal/dispatcher/notification.go b/internal/dispatcher/notification.go index 2a59a6a..0eb15c3 100644 --- a/internal/dispatcher/notification.go +++ b/internal/dispatcher/notification.go @@ -1,6 +1,7 @@ package dispatcher import ( + "encoding/json" "fmt" "html" "log" @@ -10,6 +11,16 @@ import ( "github.com/pushbits/server/internal/model" ) +type ExampleEvent struct { + Body string `json:"body"` + Msgtype string `json:"msgtype"` + RelatesTo RelatesTo `json:"m.relates_to,omitempty"` +} + +type RelatesTo struct { + InReplyTo map[string]string `json:"m.in_reply_to"` +} + // SendNotification sends a notification to the specified user. func (d *Dispatcher) SendNotification(a *model.Application, n *model.Notification) error { log.Printf("Sending notification to room %s.", a.MatrixID) @@ -27,6 +38,38 @@ func (d *Dispatcher) SendNotification(a *model.Application, n *model.Notificatio return err } +func (d *Dispatcher) SendDeleteNotification(a *model.Application, n *model.DeleteNotification) error { + log.Printf("Sending delete notification to room %s", a.MatrixID) + event := ExampleEvent{ + Body: "Testmessage", + Msgtype: "m.text", + } + + irt := make(map[string]string) + + irt["event_id"] = "$uf5OLKPaefHTZhc2lxSIY7If7pLFcNHcMZLbMfS-7qw" + + rt := RelatesTo{ + InReplyTo: irt, + } + + event.RelatesTo = rt + + _, err := d.client.SendMessageEvent(a.MatrixID, "m.room.message", event) + + if err != nil { + log.Println(err) + } + + messages, _ := d.client.Messages(a.MatrixID, "", "", 'b', 10) + + js, _ := json.Marshal(messages) + + log.Println(string(js)) + + return nil +} + // HTML-formats the title func (d *Dispatcher) getFormattedTitle(n *model.Notification) string { trimmedTitle := strings.TrimSpace(n.Title) diff --git a/internal/model/notification.go b/internal/model/notification.go index ef8d6ed..0b229cf 100644 --- a/internal/model/notification.go +++ b/internal/model/notification.go @@ -14,3 +14,14 @@ type Notification struct { Extras map[string]interface{} `json:"extras,omitempty" form:"-" query:"-"` Date time.Time `json:"date"` } + +// DeleteNotification holds information like the message, the reply to message id and the priority of a deletion notification. +type DeleteNotification struct { + ID uint `json:"id"` + DeleteID uint `json:"deleteid"` + ApplicationID uint `json:"appid"` + Message string `json:"message" form:"message" query:"message" binding:"required"` + Priority int `json:"priority" form:"priority" query:"priority"` + Extras map[string]interface{} `json:"extras,omitempty" form:"-" query:"-"` + Date time.Time `json:"date"` +} diff --git a/internal/router/router.go b/internal/router/router.go index 587c256..9dd5e87 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -47,6 +47,8 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp r.POST("/message", auth.RequireApplicationToken(), notificationHandler.CreateNotification) + r.GET("/test", auth.RequireApplicationToken(), notificationHandler.DeleteNotification) + userGroup := r.Group("/user") userGroup.Use(auth.RequireAdmin()) { From b392ea1b44fa25f86e6070aeb028866f28cee57d Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sun, 6 Jun 2021 19:30:25 +0200 Subject: [PATCH 2/8] add endpoint for delete --- internal/api/context.go | 11 +++++++++ internal/api/middleware.go | 17 ++++++++++++++ internal/api/notification.go | 24 +++++++++++++++----- internal/dispatcher/notification.go | 35 +++++++++++++---------------- internal/model/notification.go | 11 +++------ internal/router/router.go | 1 + 6 files changed, 65 insertions(+), 34 deletions(-) diff --git a/internal/api/context.go b/internal/api/context.go index c408515..90a5ec8 100644 --- a/internal/api/context.go +++ b/internal/api/context.go @@ -20,6 +20,17 @@ func getID(ctx *gin.Context) (uint, error) { return id, nil } +func getMessageID(ctx *gin.Context) (string, error) { + id, ok := ctx.MustGet("messageid").(string) + if !ok { + err := errors.New("an error occured while retrieving messageID from context") + ctx.AbortWithError(http.StatusInternalServerError, err) + return "", err + } + + return id, nil +} + func getApplication(ctx *gin.Context, db Database) (*model.Application, error) { id, err := getID(ctx) if err != nil { diff --git a/internal/api/middleware.go b/internal/api/middleware.go index a6507e1..3d84840 100644 --- a/internal/api/middleware.go +++ b/internal/api/middleware.go @@ -8,6 +8,10 @@ type idInURI struct { ID uint `uri:"id" binding:"required"` } +type messageIdInURI struct { + MessageID string `uri:"messageid" binding:"required"` +} + // RequireIDInURI returns a Gin middleware which requires an ID to be supplied in the URI of the request. func RequireIDInURI() gin.HandlerFunc { return func(ctx *gin.Context) { @@ -20,3 +24,16 @@ func RequireIDInURI() gin.HandlerFunc { ctx.Set("id", requestModel.ID) } } + +// RequireMessageIDInURI returns a Gin middleware which requires an messageID to be supplied in the URI of the request. +func RequireMessageIDInURI() gin.HandlerFunc { + return func(ctx *gin.Context) { + var requestModel messageIdInURI + + if err := ctx.BindUri(&requestModel); err != nil { + return + } + + ctx.Set("messageid", requestModel.MessageID) + } +} diff --git a/internal/api/notification.go b/internal/api/notification.go index b4a2100..9cd51d0 100644 --- a/internal/api/notification.go +++ b/internal/api/notification.go @@ -18,8 +18,8 @@ type NotificationDatabase interface { // The NotificationDispatcher interface for relaying notifications. type NotificationDispatcher interface { - SendNotification(a *model.Application, n *model.Notification) error - SendDeleteNotification(a *model.Application, n *model.DeleteNotification) error + SendNotification(a *model.Application, n *model.Notification) (id string, err error) + DeleteNotification(a *model.Application, n *model.DeleteNotification) error } // NotificationHandler holds information for processing requests about notifications. @@ -39,24 +39,36 @@ func (h *NotificationHandler) CreateNotification(ctx *gin.Context) { application := authentication.GetApplication(ctx) log.Printf("Sending notification for application %s.", application.Name) - notification.ID = 0 notification.ApplicationID = application.ID if strings.TrimSpace(notification.Title) == "" { notification.Title = application.Name } notification.Date = time.Now() - if success := successOrAbort(ctx, http.StatusInternalServerError, h.DP.SendNotification(application, ¬ification)); !success { + messageID, err := h.DP.SendNotification(application, ¬ification) + + if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success { return } + notification.ID = messageID + ctx.JSON(http.StatusOK, ¬ification) } +// DeleteNotification is used to delete (or mark as deleted) a notification for a user func (h *NotificationHandler) DeleteNotification(ctx *gin.Context) { application := authentication.GetApplication(ctx) + id, err := getMessageID(ctx) - n := model.DeleteNotification{} + if success := successOrAbort(ctx, http.StatusUnprocessableEntity, err); !success { + return + } - h.DP.SendDeleteNotification(application, &n) + n := model.DeleteNotification{ + ID: id, + Date: time.Now(), + } + + h.DP.DeleteNotification(application, &n) } diff --git a/internal/dispatcher/notification.go b/internal/dispatcher/notification.go index 0eb15c3..edc9c97 100644 --- a/internal/dispatcher/notification.go +++ b/internal/dispatcher/notification.go @@ -1,7 +1,6 @@ package dispatcher import ( - "encoding/json" "fmt" "html" "log" @@ -11,7 +10,7 @@ import ( "github.com/pushbits/server/internal/model" ) -type ExampleEvent struct { +type ReplyEvent struct { Body string `json:"body"` Msgtype string `json:"msgtype"` RelatesTo RelatesTo `json:"m.relates_to,omitempty"` @@ -22,7 +21,7 @@ type RelatesTo struct { } // SendNotification sends a notification to the specified user. -func (d *Dispatcher) SendNotification(a *model.Application, n *model.Notification) error { +func (d *Dispatcher) SendNotification(a *model.Application, n *model.Notification) (id string, err error) { log.Printf("Sending notification to room %s.", a.MatrixID) plainMessage := strings.TrimSpace(n.Message) @@ -33,41 +32,37 @@ func (d *Dispatcher) SendNotification(a *model.Application, n *model.Notificatio text := fmt.Sprintf("%s\n\n%s", plainTitle, plainMessage) formattedText := fmt.Sprintf("%s %s", title, message) - _, err := d.client.SendFormattedText(a.MatrixID, text, formattedText) + respSendEvent, err := d.client.SendFormattedText(a.MatrixID, text, formattedText) - return err + return respSendEvent.EventID, err } -func (d *Dispatcher) SendDeleteNotification(a *model.Application, n *model.DeleteNotification) error { +// DeleteNotification sends a notification to the specified user that another notificaion is deleted +func (d *Dispatcher) DeleteNotification(a *model.Application, n *model.DeleteNotification) error { log.Printf("Sending delete notification to room %s", a.MatrixID) - event := ExampleEvent{ - Body: "Testmessage", + event := ReplyEvent{ + Body: "This message got deleted.", Msgtype: "m.text", } irt := make(map[string]string) - - irt["event_id"] = "$uf5OLKPaefHTZhc2lxSIY7If7pLFcNHcMZLbMfS-7qw" - + irt["event_id"] = n.ID rt := RelatesTo{ InReplyTo: irt, } - event.RelatesTo = rt _, err := d.client.SendMessageEvent(a.MatrixID, "m.room.message", event) - if err != nil { - log.Println(err) - } + return err - messages, _ := d.client.Messages(a.MatrixID, "", "", 'b', 10) + /* + messages, _ := d.client.Messages(a.MatrixID, "", "", 'b', 10) - js, _ := json.Marshal(messages) + js, _ := json.Marshal(messages) - log.Println(string(js)) - - return nil + log.Println(string(js)) + */ } // HTML-formats the title diff --git a/internal/model/notification.go b/internal/model/notification.go index 0b229cf..7bb64b7 100644 --- a/internal/model/notification.go +++ b/internal/model/notification.go @@ -6,7 +6,7 @@ import ( // Notification holds information like the message, the title, and the priority of a notification. type Notification struct { - ID uint `json:"id"` + ID string `json:"id"` ApplicationID uint `json:"appid"` Message string `json:"message" form:"message" query:"message" binding:"required"` Title string `json:"title" form:"title" query:"title"` @@ -17,11 +17,6 @@ type Notification struct { // DeleteNotification holds information like the message, the reply to message id and the priority of a deletion notification. type DeleteNotification struct { - ID uint `json:"id"` - DeleteID uint `json:"deleteid"` - ApplicationID uint `json:"appid"` - Message string `json:"message" form:"message" query:"message" binding:"required"` - Priority int `json:"priority" form:"priority" query:"priority"` - Extras map[string]interface{} `json:"extras,omitempty" form:"-" query:"-"` - Date time.Time `json:"date"` + ID string `json:"id" form:"id"` + Date time.Time `json:"date"` } diff --git a/internal/router/router.go b/internal/router/router.go index 9dd5e87..1debc4a 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -46,6 +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.GET("/test", auth.RequireApplicationToken(), notificationHandler.DeleteNotification) From eebc7f7e316d3fd8a2bd696879f3d116039e2526 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Sun, 6 Jun 2021 21:13:07 +0200 Subject: [PATCH 3/8] get delete messages to work --- README.md | 12 ++++ internal/api/errors.go | 5 ++ internal/api/notification.go | 8 ++- internal/api/util.go | 8 ++- internal/dispatcher/notification.go | 85 ++++++++++++++++++++++++----- internal/model/notification.go | 1 + internal/router/router.go | 2 +- 7 files changed, 103 insertions(+), 18 deletions(-) create mode 100644 internal/api/errors.go 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) From 60277386d942e684cf3157b4444cef3204fd6d41 Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Tue, 8 Jun 2021 13:00:31 +0200 Subject: [PATCH 4/8] polish implementation --- internal/dispatcher/notification.go | 105 +++++++++++++++++++++------- 1 file changed, 79 insertions(+), 26 deletions(-) diff --git a/internal/dispatcher/notification.go b/internal/dispatcher/notification.go index c17c634..9b974d0 100644 --- a/internal/dispatcher/notification.go +++ b/internal/dispatcher/notification.go @@ -1,7 +1,6 @@ package dispatcher import ( - "errors" "fmt" "html" "log" @@ -13,22 +12,41 @@ import ( "github.com/pushbits/server/internal/model" ) +// MessageFormat is a matrix message format type MessageFormat string +// MsgType is a matrix msgtype +type MsgType string + +// Define matrix constants const ( - FormatHTML = MessageFormat("org.matrix.custom.html") + MessageFormatHTML = MessageFormat("org.matrix.custom.html") + MsgTypeText = MsgType("m.text") ) -type ReplyEvent struct { +// MessageEvent is the content of a matrix message event +type MessageEvent struct { Body string `json:"body"` FormattedBody string `json:"formatted_body"` - Msgtype string `json:"msgtype"` + MsgType MsgType `json:"msgtype"` RelatesTo RelatesTo `json:"m.relates_to,omitempty"` Format MessageFormat `json:"format"` + NewContent NewContent `json:"m.new_content,omitempty"` } +// RelatesTo holds information about relations to other message events type RelatesTo struct { - InReplyTo map[string]string `json:"m.in_reply_to"` + InReplyTo map[string]string `json:"m.in_reply_to,omitempty"` + RelType string `json:"rel_type,omitempty"` + EventID string `json:"event_id,omitempty"` +} + +// NewContent holds information about an updated message event +type NewContent struct { + Body string `json:"body"` + FormattedBody string `json:"formatted_body"` + MsgType MsgType `json:"msgtype"` + Format MessageFormat `json:"format"` } // SendNotification sends a notification to the specified user. @@ -50,10 +68,11 @@ 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) + var oldFormattedBody string + var oldBody string + // get the message we want to delete deleteMessage, err := d.getMessage(a, n.ID) if err != nil { @@ -64,8 +83,8 @@ func (d *Dispatcher) DeleteNotification(a *model.Application, n *model.DeleteNot if val, ok := deleteMessage.Content["body"]; ok { body, ok := val.(string) if ok { - deleteMessageBody = body - deleteMessageFormattedBody = body + oldBody = body + oldFormattedBody = body } } else { log.Println("Message to delete has wrong format") @@ -75,29 +94,63 @@ func (d *Dispatcher) DeleteNotification(a *model.Application, n *model.DeleteNot if val, ok := deleteMessage.Content["formatted_body"]; ok { body, ok := val.(string) if ok { - deleteMessageFormattedBody = body + oldFormattedBody = body } } + // update the message with strikethrough + newBody := fmt.Sprintf("%s\n- deleted", oldBody) + newFormattedBody := fmt.Sprintf("%s
- deleted", oldFormattedBody) + + newMessage := NewContent{ + Body: newBody, + FormattedBody: newFormattedBody, + MsgType: MsgTypeText, + Format: MessageFormatHTML, + } + + replaceRelation := RelatesTo{ + RelType: "m.replace", + EventID: deleteMessage.ID, + } + + replaceEvent := MessageEvent{ + Body: oldBody, + FormattedBody: oldFormattedBody, + MsgType: MsgTypeText, + NewContent: newMessage, + RelatesTo: replaceRelation, + Format: MessageFormatHTML, + } + + _, err = d.client.SendMessageEvent(a.MatrixID, "m.room.message", replaceEvent) + + if err != nil { + log.Println(err) + return err + } + + // send a notification about the deletion // 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) + notificationFormattedBody := fmt.Sprintf("
In reply to %s
%s
\n
This message got deleted.", deleteMessage.RoomID, deleteMessage.ID, deleteMessage.Sender, deleteMessage.Sender, oldFormattedBody) + notificationBody := fmt.Sprintf("> <%s>%s\n\nThis message got deleted", deleteMessage.Sender, oldBody) - event := ReplyEvent{ - FormattedBody: formattedBody, - Body: body, - Msgtype: "m.text", - Format: FormatHTML, + notificationEvent := MessageEvent{ + FormattedBody: notificationFormattedBody, + Body: notificationBody, + MsgType: MsgTypeText, + Format: MessageFormatHTML, } - irt := make(map[string]string) - rt := RelatesTo{ - InReplyTo: irt, - } - event.RelatesTo = rt - irt["event_id"] = deleteMessage.ID + notificationReply := make(map[string]string) + notificationReply["event_id"] = deleteMessage.ID - _, err = d.client.SendMessageEvent(a.MatrixID, "m.room.message", event) + notificationRelation := RelatesTo{ + InReplyTo: notificationReply, + } + notificationEvent.RelatesTo = notificationRelation + + _, err = d.client.SendMessageEvent(a.MatrixID, "m.room.message", notificationEvent) return err } @@ -168,7 +221,7 @@ func (d *Dispatcher) coloredText(color string, text string) string { return "" + text + "" } -// TODO cubicroot: find a way to only request on specific event +// Searches in the messages list for the given id func (d *Dispatcher) getMessage(a *model.Application, id string) (gomatrix.Event, error) { start := "" end := "" @@ -183,5 +236,5 @@ func (d *Dispatcher) getMessage(a *model.Application, id string) (gomatrix.Event } start = messages.End } - return gomatrix.Event{}, errors.New("Message not found") + return gomatrix.Event{}, api.ErrorMessageNotFound } From 975c0a246b9c808df7a45422b64b27940783830f Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Tue, 8 Jun 2021 13:10:59 +0200 Subject: [PATCH 5/8] remove debug --- internal/router/router.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/router/router.go b/internal/router/router.go index 6392973..68f61ca 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -48,8 +48,6 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp r.POST("/message", auth.RequireApplicationToken(), notificationHandler.CreateNotification) r.DELETE("/message/:messageid", api.RequireMessageIDInURI(), auth.RequireApplicationToken(), notificationHandler.DeleteNotification) - r.GET("/test", auth.RequireApplicationToken(), notificationHandler.DeleteNotification) - userGroup := r.Group("/user") userGroup.Use(auth.RequireAdmin()) { From c5e4669fdab01f12abd6b220d07acfb347900c8b Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Fri, 11 Jun 2021 10:21:48 +0200 Subject: [PATCH 6/8] clean up --- internal/dispatcher/notification.go | 11 ++++++++--- internal/model/notification.go | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/dispatcher/notification.go b/internal/dispatcher/notification.go index 9b974d0..6571d8d 100644 --- a/internal/dispatcher/notification.go +++ b/internal/dispatcher/notification.go @@ -82,10 +82,15 @@ func (d *Dispatcher) DeleteNotification(a *model.Application, n *model.DeleteNot if val, ok := deleteMessage.Content["body"]; ok { body, ok := val.(string) - if ok { - oldBody = body - oldFormattedBody = body + + if !ok { + log.Println("Event does not have a body") + return api.ErrorMessageNotFound } + + oldBody = body + oldFormattedBody = body + } else { log.Println("Message to delete has wrong format") return api.ErrorMessageNotFound diff --git a/internal/model/notification.go b/internal/model/notification.go index 569e8ee..fc46221 100644 --- a/internal/model/notification.go +++ b/internal/model/notification.go @@ -16,7 +16,7 @@ type Notification struct { Date time.Time `json:"date"` } -// DeleteNotification holds information like the message, the reply to message id and the priority of a deletion notification. +// DeleteNotification holds information like the message ID of a deletion notification. type DeleteNotification struct { ID string `json:"id" form:"id"` Date time.Time `json:"date"` From ac5819dfc93a228d57f1d03b95267076ab30f47e Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Fri, 11 Jun 2021 10:47:11 +0200 Subject: [PATCH 7/8] move respond/replace logic to separate functions --- internal/api/errors.go | 2 +- internal/dispatcher/notification.go | 157 ++++++++++++++++------------ 2 files changed, 94 insertions(+), 65 deletions(-) diff --git a/internal/api/errors.go b/internal/api/errors.go index db296e3..64c744a 100644 --- a/internal/api/errors.go +++ b/internal/api/errors.go @@ -2,4 +2,4 @@ package api import "errors" -var ErrorMessageNotFound = errors.New("Message not found") +var ErrorMessageNotFound = errors.New("message not found") diff --git a/internal/dispatcher/notification.go b/internal/dispatcher/notification.go index 6571d8d..16248de 100644 --- a/internal/dispatcher/notification.go +++ b/internal/dispatcher/notification.go @@ -80,82 +80,23 @@ func (d *Dispatcher) DeleteNotification(a *model.Application, n *model.DeleteNot return api.ErrorMessageNotFound } - if val, ok := deleteMessage.Content["body"]; ok { - body, ok := val.(string) + oldBody, oldFormattedBody, err = bodiesFromMessage(deleteMessage) - if !ok { - log.Println("Event does not have a body") - return api.ErrorMessageNotFound - } - - oldBody = body - oldFormattedBody = 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 { - oldFormattedBody = body - } + if err != nil { + return err } // update the message with strikethrough newBody := fmt.Sprintf("%s\n- deleted", oldBody) newFormattedBody := fmt.Sprintf("%s
- deleted", oldFormattedBody) - newMessage := NewContent{ - Body: newBody, - FormattedBody: newFormattedBody, - MsgType: MsgTypeText, - Format: MessageFormatHTML, - } - - replaceRelation := RelatesTo{ - RelType: "m.replace", - EventID: deleteMessage.ID, - } - - replaceEvent := MessageEvent{ - Body: oldBody, - FormattedBody: oldFormattedBody, - MsgType: MsgTypeText, - NewContent: newMessage, - RelatesTo: replaceRelation, - Format: MessageFormatHTML, - } - - _, err = d.client.SendMessageEvent(a.MatrixID, "m.room.message", replaceEvent) + _, err = d.replaceMessage(a, newBody, newFormattedBody, deleteMessage.ID, oldBody, oldFormattedBody) if err != nil { - log.Println(err) return err } - // send a notification about the deletion - // formatting according to https://matrix.org/docs/spec/client_server/latest#fallbacks-and-event-representation - notificationFormattedBody := fmt.Sprintf("
In reply to %s
%s
\n
This message got deleted.", deleteMessage.RoomID, deleteMessage.ID, deleteMessage.Sender, deleteMessage.Sender, oldFormattedBody) - notificationBody := fmt.Sprintf("> <%s>%s\n\nThis message got deleted", deleteMessage.Sender, oldBody) - - notificationEvent := MessageEvent{ - FormattedBody: notificationFormattedBody, - Body: notificationBody, - MsgType: MsgTypeText, - Format: MessageFormatHTML, - } - - notificationReply := make(map[string]string) - notificationReply["event_id"] = deleteMessage.ID - - notificationRelation := RelatesTo{ - InReplyTo: notificationReply, - } - notificationEvent.RelatesTo = notificationRelation - - _, err = d.client.SendMessageEvent(a.MatrixID, "m.room.message", notificationEvent) + _, err = d.respondToMessage(a, "This message got deleted", "This message got deleted.", deleteMessage) return err } @@ -243,3 +184,91 @@ func (d *Dispatcher) getMessage(a *model.Application, id string) (gomatrix.Event } return gomatrix.Event{}, api.ErrorMessageNotFound } + +// Replaces the content of a matrix message +func (d *Dispatcher) replaceMessage(a *model.Application, newBody, newFormattedBody string, messageID string, oldBody, oldFormattedBody string) (*gomatrix.RespSendEvent, error) { + newMessage := NewContent{ + Body: newBody, + FormattedBody: newFormattedBody, + MsgType: MsgTypeText, + Format: MessageFormatHTML, + } + + replaceRelation := RelatesTo{ + RelType: "m.replace", + EventID: messageID, + } + + replaceEvent := MessageEvent{ + Body: oldBody, + FormattedBody: oldFormattedBody, + MsgType: MsgTypeText, + NewContent: newMessage, + RelatesTo: replaceRelation, + Format: MessageFormatHTML, + } + + sendEvent, err := d.client.SendMessageEvent(a.MatrixID, "m.room.message", replaceEvent) + + if err != nil { + log.Println(err) + return nil, err + } + + return sendEvent, nil +} + +// Sends a notification in response to another matrix message event +func (d *Dispatcher) respondToMessage(a *model.Application, body, formattedBody string, respondMessage gomatrix.Event) (*gomatrix.RespSendEvent, error) { + oldBody, oldFormattedBody, err := bodiesFromMessage(respondMessage) + + if err != nil { + return nil, err + } + + // formatting according to https://matrix.org/docs/spec/client_server/latest#fallbacks-and-event-representation + newFormattedBody := fmt.Sprintf("
In reply to %s
%s
\n
%s", respondMessage.RoomID, respondMessage.ID, respondMessage.Sender, respondMessage.Sender, oldFormattedBody, formattedBody) + newBody := fmt.Sprintf("> <%s>%s\n\n%s", respondMessage.Sender, oldBody, body) + + notificationEvent := MessageEvent{ + FormattedBody: newFormattedBody, + Body: newBody, + MsgType: MsgTypeText, + Format: MessageFormatHTML, + } + + notificationReply := make(map[string]string) + notificationReply["event_id"] = respondMessage.ID + + notificationRelation := RelatesTo{ + InReplyTo: notificationReply, + } + notificationEvent.RelatesTo = notificationRelation + + return d.client.SendMessageEvent(a.MatrixID, "m.room.message", notificationEvent) +} + +// Extracts body and formatted body from a matrix message event +func bodiesFromMessage(message gomatrix.Event) (body, formattedBody string, err error) { + if val, ok := message.Content["body"]; ok { + body, ok := val.(string) + + if !ok { + return "", "", api.ErrorMessageNotFound + } + + formattedBody = body + + } else { + return "", "", api.ErrorMessageNotFound + } + + if val, ok := message.Content["formatted_body"]; ok { + body, ok := val.(string) + if ok { + formattedBody = body + } + } + + return body, formattedBody, nil +} From c1cc369d1e741195320137253c190d60dad1343e Mon Sep 17 00:00:00 2001 From: Cubicroot Date: Thu, 17 Jun 2021 17:37:24 +0200 Subject: [PATCH 8/8] more strict type handling --- internal/dispatcher/notification.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/dispatcher/notification.go b/internal/dispatcher/notification.go index 16248de..9fce7a0 100644 --- a/internal/dispatcher/notification.go +++ b/internal/dispatcher/notification.go @@ -265,9 +265,11 @@ func bodiesFromMessage(message gomatrix.Event) (body, formattedBody string, err if val, ok := message.Content["formatted_body"]; ok { body, ok := val.(string) - if ok { - formattedBody = body + if !ok { + return "", "", api.ErrorMessageNotFound } + + formattedBody = body } return body, formattedBody, nil