From 9325022e460163bcbf928d461ae912e11941a213 Mon Sep 17 00:00:00 2001 From: Era Dorta Date: Thu, 28 Aug 2025 00:27:09 +0200 Subject: [PATCH 1/8] Add message deletion endpoint --- src/api/api.go | 47 +++++++++++++++++++++++++++++ src/client/client.go | 71 ++++++++++++++++++++++++++++++++++++++++++++ src/main.go | 5 ++++ 3 files changed, 123 insertions(+) diff --git a/src/api/api.go b/src/api/api.go index d0a476a..dbb8cf7 100644 --- a/src/api/api.go +++ b/src/api/api.go @@ -163,6 +163,10 @@ type SendMessageResponse struct { Timestamp string `json:"timestamp"` } +type RemoteDeleteResponse struct { + Timestamp string `json:"timestamp"` +} + type TrustModeRequest struct { TrustMode string `json:"trust_mode"` } @@ -209,6 +213,11 @@ type SetPinRequest struct { Pin string `json:"pin"` } +type RemoteDeleteRequest struct { + Recipient string `json:"recipient"` + Timestamp int64 `json:"timestamp"` +} + type Api struct { signalClient *client.SignalClient wsMutex sync.Mutex @@ -2322,3 +2331,41 @@ func (a *Api) RemovePin(c *gin.Context) { c.Status(204) } + +// @Summary Send a signal message. +// @Tags Messages +// @Description Send a signal message +// @Accept json +// @Produce json +// @Success 201 {string} string "OK" +// @Failure 400 {object} Error +// @Param number path string true "Registered Phone Number" +// @Param data body TypingIndicatorRequest true "Type" +// @Router /v1/remote-delete/{number} [post] +func (a *Api) RemoteDelete(c *gin.Context) { + + var req RemoteDeleteRequest + err := c.BindJSON(&req) + if err != nil { + c.JSON(400, Error{Msg: "Couldn't process request - invalid request"}) + return + } + + number, err := url.PathUnescape(c.Param("number")) + if err != nil { + c.JSON(400, Error{Msg: "Couldn't process request - malformed number"}) + return + } + if number == "" { + c.JSON(400, Error{Msg: "Couldn't process request - number missing"}) + return + } + + timestamp, err := a.signalClient.RemoteDelete(number, req.Recipient, req.Timestamp) + + if err != nil { + c.JSON(400, Error{Msg: err.Error()}) + return + } + c.JSON(201, RemoteDeleteResponse{Timestamp: strconv.FormatInt(timestamp.Timestamp, 10)}) +} diff --git a/src/client/client.go b/src/client/client.go index d38fd47..b6d6de8 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -163,6 +163,10 @@ type SendResponse struct { Timestamp int64 `json:"timestamp"` } +type RemoteDeleteResponse struct { + Timestamp int64 `json:"timestamp"` +} + type About struct { SupportedApiVersions []string `json:"versions"` BuildNr int `json:"build"` @@ -2530,3 +2534,70 @@ func (s *SignalClient) RemovePin(number string) error { } return nil } + +func (s *SignalClient) RemoteDelete(number string, recipient string, timestamp int64) (RemoteDeleteResponse, error) { + // see https://github.com/AsamK/signal-cli/blob/master/man/signal-cli.1.adoc#remotedelete + var err error + var resp RemoteDeleteResponse + var rawData string + + recp := recipient + isGroup := false + if strings.HasPrefix(recipient, groupPrefix) { + isGroup = true + recp, err = ConvertGroupIdToInternalGroupId(recipient) + if err != nil { + return resp, errors.New("Invalid group id") + } + } + + if s.signalCliMode == JsonRpc { + type Request struct { + Recipient string `json:"recipient,omitempty"` + GroupId string `json:"group-id,omitempty"` + Timestamp int64 `json:"target-timestamp"` + } + request := Request{} + if !isGroup { + request.Recipient = recp + } else { + request.GroupId = recp + } + request.Timestamp = timestamp + + jsonRpc2Client, err := s.getJsonRpc2Client() + if err != nil { + return resp, err + } + rawData, err = jsonRpc2Client.getRaw("remoteDelete", &number, request) + if err != nil { + return resp, err + } + + err = json.Unmarshal([]byte(rawData), &resp) + + return resp, err + } else { + cmd := []string{ + "--config", s.signalCliConfig, + "-a", number, + "remoteDelete", + } + if !isGroup { + cmd = append(cmd, recp) + } else { + cmd = append(cmd, []string{"-g", recp}...) + } + cmd = append(cmd, []string{"-t", strconv.FormatInt(timestamp, 10)}...) + rawData, err = s.cliClient.Execute(true, cmd, "") + if err != nil { + return resp, err + } + + resp.Timestamp, err = strconv.ParseInt(strings.TrimSuffix(rawData, "\n"), 10, 64) + if err != nil { + return resp, err + } + return resp, nil + } +} diff --git a/src/main.go b/src/main.go index e9d0eb5..1c1d0c0 100644 --- a/src/main.go +++ b/src/main.go @@ -279,6 +279,11 @@ func main() { typingIndicator.DELETE(":number", api.SendStopTyping) } + remoteDelete := v1.Group("remote-delete") + { + remoteDelete.POST(":number", api.RemoteDelete) + } + reactions := v1.Group("/reactions") { reactions.POST(":number", api.SendReaction) From bcb2100169356c1400d8489829fdd96ec35f39ab Mon Sep 17 00:00:00 2001 From: Era Dorta Date: Thu, 28 Aug 2025 00:32:10 +0200 Subject: [PATCH 2/8] Fix comments for the RemoteDelete function --- src/api/api.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/api/api.go b/src/api/api.go index dbb8cf7..74ffbdc 100644 --- a/src/api/api.go +++ b/src/api/api.go @@ -2332,18 +2332,17 @@ func (a *Api) RemovePin(c *gin.Context) { c.Status(204) } -// @Summary Send a signal message. +// @Summary Delete a signal message. // @Tags Messages -// @Description Send a signal message +// @Description Delete a signal message // @Accept json // @Produce json -// @Success 201 {string} string "OK" +// @Success 201 {object} RemoteDeleteResponse // @Failure 400 {object} Error // @Param number path string true "Registered Phone Number" -// @Param data body TypingIndicatorRequest true "Type" +// @Param data body RemoteDeleteRequest true "Type" // @Router /v1/remote-delete/{number} [post] func (a *Api) RemoteDelete(c *gin.Context) { - var req RemoteDeleteRequest err := c.BindJSON(&req) if err != nil { From a93eed6e560de30539100e91002dfa3a4f0824b6 Mon Sep 17 00:00:00 2001 From: Era Dorta Date: Thu, 28 Aug 2025 00:34:41 +0200 Subject: [PATCH 3/8] Add extra error check after unmarshal --- src/client/client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client/client.go b/src/client/client.go index b6d6de8..b14faad 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -2575,6 +2575,9 @@ func (s *SignalClient) RemoteDelete(number string, recipient string, timestamp i } err = json.Unmarshal([]byte(rawData), &resp) + if err != nil { + return resp, errors.New("Couldn't process request - invalid signal-cli response") + } return resp, err } else { From 6b3916e5970a53135a8941d0ad3130f03f5bb8b1 Mon Sep 17 00:00:00 2001 From: Era Dorta Date: Thu, 28 Aug 2025 00:37:56 +0200 Subject: [PATCH 4/8] Remove if statement --- src/client/client.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/client/client.go b/src/client/client.go index b14faad..6ea8a0c 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -2598,9 +2598,6 @@ func (s *SignalClient) RemoteDelete(number string, recipient string, timestamp i } resp.Timestamp, err = strconv.ParseInt(strings.TrimSuffix(rawData, "\n"), 10, 64) - if err != nil { return resp, err - } - return resp, nil } } From d25a454f39e8705e0eb666f57e6a3df19ca119f8 Mon Sep 17 00:00:00 2001 From: Era Dorta Date: Thu, 28 Aug 2025 00:40:22 +0200 Subject: [PATCH 5/8] Fix tab error --- src/client/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/client.go b/src/client/client.go index 6ea8a0c..a3b3a13 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -2598,6 +2598,6 @@ func (s *SignalClient) RemoteDelete(number string, recipient string, timestamp i } resp.Timestamp, err = strconv.ParseInt(strings.TrimSuffix(rawData, "\n"), 10, 64) - return resp, err + return resp, err } } From f078e1a2aecb546aceb25091e417bb7d53f8c5e0 Mon Sep 17 00:00:00 2001 From: Era Dorta Date: Fri, 29 Aug 2025 19:55:45 +0200 Subject: [PATCH 6/8] Add documentation --- src/docs/docs.go | 66 +++++++++++++++++++++++++++++++++++++++++++ src/docs/swagger.json | 66 +++++++++++++++++++++++++++++++++++++++++++ src/docs/swagger.yaml | 43 ++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+) diff --git a/src/docs/docs.go b/src/docs/docs.go index e81bcbb..7ac72f3 100644 --- a/src/docs/docs.go +++ b/src/docs/docs.go @@ -1854,6 +1854,53 @@ const docTemplate = `{ } } }, + "/v1/remote-delete/{number}": { + "post": { + "description": "Delete a signal message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Delete a signal message.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "description": "Type", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.RemoteDeleteRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/api.RemoteDeleteResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, "/v1/search/{number}": { "get": { "description": "Check if one or more phone numbers are registered with the Signal Service.", @@ -2394,6 +2441,25 @@ const docTemplate = `{ } } }, + "api.RemoteDeleteRequest": { + "type": "object", + "properties": { + "recipient": { + "type": "string" + }, + "timestamp": { + "type": "integer" + } + } + }, + "api.RemoteDeleteResponse": { + "type": "object", + "properties": { + "timestamp": { + "type": "string" + } + } + }, "api.SearchResponse": { "type": "object", "properties": { diff --git a/src/docs/swagger.json b/src/docs/swagger.json index e8b1a45..89873b9 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -1851,6 +1851,53 @@ } } }, + "/v1/remote-delete/{number}": { + "post": { + "description": "Delete a signal message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Delete a signal message.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "description": "Type", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.RemoteDeleteRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/api.RemoteDeleteResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, "/v1/search/{number}": { "get": { "description": "Check if one or more phone numbers are registered with the Signal Service.", @@ -2391,6 +2438,25 @@ } } }, + "api.RemoteDeleteRequest": { + "type": "object", + "properties": { + "recipient": { + "type": "string" + }, + "timestamp": { + "type": "integer" + } + } + }, + "api.RemoteDeleteResponse": { + "type": "object", + "properties": { + "timestamp": { + "type": "string" + } + } + }, "api.SearchResponse": { "type": "object", "properties": { diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index 5638db2..9f15502 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -121,6 +121,18 @@ definitions: use_voice: type: boolean type: object + api.RemoteDeleteRequest: + properties: + recipient: + type: string + timestamp: + type: integer + type: object + api.RemoteDeleteResponse: + properties: + timestamp: + type: string + type: object api.SearchResponse: properties: number: @@ -1706,6 +1718,37 @@ paths: summary: Verify a registered phone number. tags: - Devices + /v1/remote-delete/{number}: + post: + consumes: + - application/json + description: Delete a signal message + parameters: + - description: Registered Phone Number + in: path + name: number + required: true + type: string + - description: Type + in: body + name: data + required: true + schema: + $ref: '#/definitions/api.RemoteDeleteRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/api.RemoteDeleteResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + summary: Delete a signal message. + tags: + - Messages /v1/search/{number}: get: consumes: From af3eadbbc0cc988d223926fc4f1a2e4c94355425 Mon Sep 17 00:00:00 2001 From: Era Dorta Date: Fri, 29 Aug 2025 19:59:56 +0200 Subject: [PATCH 7/8] Replace the handler from post to delete --- src/api/api.go | 2 +- src/docs/docs.go | 2 +- src/docs/swagger.json | 2 +- src/docs/swagger.yaml | 2 +- src/main.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/api/api.go b/src/api/api.go index 74ffbdc..2462129 100644 --- a/src/api/api.go +++ b/src/api/api.go @@ -2341,7 +2341,7 @@ func (a *Api) RemovePin(c *gin.Context) { // @Failure 400 {object} Error // @Param number path string true "Registered Phone Number" // @Param data body RemoteDeleteRequest true "Type" -// @Router /v1/remote-delete/{number} [post] +// @Router /v1/remote-delete/{number} [delete] func (a *Api) RemoteDelete(c *gin.Context) { var req RemoteDeleteRequest err := c.BindJSON(&req) diff --git a/src/docs/docs.go b/src/docs/docs.go index 7ac72f3..c8ca455 100644 --- a/src/docs/docs.go +++ b/src/docs/docs.go @@ -1855,7 +1855,7 @@ const docTemplate = `{ } }, "/v1/remote-delete/{number}": { - "post": { + "delete": { "description": "Delete a signal message", "consumes": [ "application/json" diff --git a/src/docs/swagger.json b/src/docs/swagger.json index 89873b9..ba1d52f 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -1852,7 +1852,7 @@ } }, "/v1/remote-delete/{number}": { - "post": { + "delete": { "description": "Delete a signal message", "consumes": [ "application/json" diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index 9f15502..9263b8c 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -1719,7 +1719,7 @@ paths: tags: - Devices /v1/remote-delete/{number}: - post: + delete: consumes: - application/json description: Delete a signal message diff --git a/src/main.go b/src/main.go index 1c1d0c0..499b605 100644 --- a/src/main.go +++ b/src/main.go @@ -281,7 +281,7 @@ func main() { remoteDelete := v1.Group("remote-delete") { - remoteDelete.POST(":number", api.RemoteDelete) + remoteDelete.DELETE(":number", api.RemoteDelete) } reactions := v1.Group("/reactions") From de2cb043fc83e07229518b51d9cafd3bd2815a2d Mon Sep 17 00:00:00 2001 From: Era Dorta Date: Tue, 9 Sep 2025 13:29:17 +0100 Subject: [PATCH 8/8] fix: use getRecipientType instead of HasPrefix in RemoteDelete --- src/client/client.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/client/client.go b/src/client/client.go index a3b3a13..cf55d14 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -2543,12 +2543,20 @@ func (s *SignalClient) RemoteDelete(number string, recipient string, timestamp i recp := recipient isGroup := false - if strings.HasPrefix(recipient, groupPrefix) { + + recipientType, err := getRecipientType(recipient) + if err != nil { + return resp, err + } + + if recipientType == ds.Group { isGroup = true recp, err = ConvertGroupIdToInternalGroupId(recipient) if err != nil { return resp, errors.New("Invalid group id") } + } else if recipientType != ds.Number && recipientType != ds.Username { + return resp, errors.New("Invalid recipient type") } if s.signalCliMode == JsonRpc {