From 6a225e62c15f5274e24158b779769b8470374cd7 Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Thu, 23 Jul 2026 22:47:13 +0200 Subject: [PATCH] fixed bug in send endpoint * in case a message is sent to either multiple recipients or a group, it is always possible that the message couldn't be sent to some recipients (e.g because of an untrusted identity). This is now reflected in the API by returning an additional `errors` object in that case. --- src/api/api.go | 13 ++---- src/client/client.go | 83 ++++++++++++++++++++-------------- src/datastructs/datastructs.go | 16 +++++++ src/go.mod | 1 + src/go.sum | 5 ++ 5 files changed, 75 insertions(+), 43 deletions(-) diff --git a/src/api/api.go b/src/api/api.go index 3ec81a2..210660b 100644 --- a/src/api/api.go +++ b/src/api/api.go @@ -173,10 +173,6 @@ type TrustIdentityRequest struct { TrustAllKnownKeys *bool `json:"trust_all_known_keys,omitempty" example:"false"` } -type SendMessageResponse struct { - Timestamp string `json:"timestamp"` -} - type RemoteDeleteResponse struct { Timestamp string `json:"timestamp"` } @@ -476,7 +472,6 @@ func (a *Api) VerifyRegisteredNumber(c *gin.Context) { // @Router /v1/send [post] // @Deprecated func (a *Api) Send(c *gin.Context) { - var req SendMessageV1 err := c.BindJSON(&req) if err != nil { @@ -489,12 +484,12 @@ func (a *Api) Send(c *gin.Context) { base64Attachments = append(base64Attachments, req.Base64Attachment) } - timestamp, err := a.signalClient.SendV1(req.Number, req.Message, req.Recipients, base64Attachments, req.IsGroup) + resp, err := a.signalClient.SendV1(req.Number, req.Message, req.Recipients, base64Attachments, req.IsGroup) if err != nil { c.JSON(400, Error{Msg: err.Error()}) return } - c.JSON(201, SendMessageResponse{Timestamp: strconv.FormatInt(timestamp.Timestamp, 10)}) + c.JSON(201, resp) } // @Summary Send a signal message. @@ -502,7 +497,7 @@ func (a *Api) Send(c *gin.Context) { // @Description Send a signal message. Set the text_mode to 'styled' in case you want to add formatting to your text message. Styling Options: \*italic text\*, \*\*bold text\*\*, ~strikethrough text~, ||spoiler||, \`monospace\`. If you want to escape a formatting character, prefix it with two backslashes. // @Accept json // @Produce json -// @Success 201 {object} SendMessageResponse +// @Success 201 {object} ds.SendMessageResponse // @Failure 400 {object} SendMessageError // @Param data body SendMessageV2 true "Input Data" // @Router /v2/send [post] @@ -574,7 +569,7 @@ func (a *Api) SendV2(c *gin.Context) { return } - c.JSON(201, SendMessageResponse{Timestamp: strconv.FormatInt((*data)[0].Timestamp, 10)}) + c.JSON(201, data) } func (a *Api) handleSignalReceive(ws *websocket.Conn, number string, stop chan struct{}) { diff --git a/src/client/client.go b/src/client/client.go index 92008db..18d9049 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -479,8 +479,8 @@ func validateLinkPreview(message string, linkPreview *ds.LinkPreviewType) error return nil } -func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*SendResponse, error) { - var resp SendResponse +func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*ds.SendMessageResponse, error) { + var rawData string var linkPreviewAttachmentEntry *AttachmentEntry = nil if len(signalCliSendRequest.Recipients) == 0 { @@ -615,23 +615,13 @@ func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*Send } } - rawData, err := jsonRpc2Client.getRaw("send", &signalCliSendRequest.Number, request) + rawData, err = jsonRpc2Client.getRaw("send", &signalCliSendRequest.Number, request) if err != nil { cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) return nil, err } - - err = json.Unmarshal([]byte(rawData), &resp) - if err != nil { - cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) - - if strings.Contains(err.Error(), signalCliV2GroupError) { - return nil, errors.New("Cannot send message to group - please first update your profile.") - } - return nil, err - } } else { - cmd := []string{"--config", s.signalCliConfig, "-a", signalCliSendRequest.Number, "send", "--message-from-stdin"} + cmd := []string{"--output", "json", "--config", s.signalCliConfig, "-a", signalCliSendRequest.Number, "send", "--message-from-stdin"} if signalCliSendRequest.RecipientType == ds.Number { cmd = append(cmd, signalCliSendRequest.Recipients...) } else if signalCliSendRequest.RecipientType == ds.Group { @@ -719,23 +709,48 @@ func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*Send cmd = append(cmd, "--view-once") } - rawData, err := s.cliClient.Execute(true, cmd, signalCliSendRequest.Message) + rawData, err = s.cliClient.Execute(true, cmd, signalCliSendRequest.Message) if err != nil { cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) - if strings.Contains(err.Error(), signalCliV2GroupError) { - return nil, errors.New("Cannot send message to group - please first update your profile.") - } return nil, err } - resp.Timestamp, err = strconv.ParseInt(strings.TrimSuffix(rawData, "\n"), 10, 64) - if err != nil { - cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) - return nil, errors.New(strings.Replace(rawData, "\n", "", -1)) //in case we can't parse the timestamp, it means signal-cli threw an error. So instead of returning the parsing error, return the actual error from signal-cli + } + + type SignalCliSendResponse struct { + Timestamp int64 `json:"timestamp"` + Results []struct { + RecipientAddress struct { + Uuid string `json:"uuid"` + Number string `json:"number"` + Username string `json:"username"` + } `json:"recipientAddress"` + Type string `json:"type"` + } `json:"results"` + } + + var signalCliSendResponse SignalCliSendResponse + err = json.Unmarshal([]byte(rawData), &signalCliSendResponse) + if err != nil { + cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) + + if strings.Contains(err.Error(), signalCliV2GroupError) { + return nil, errors.New("Cannot send message to group - please first update your profile.") } + return nil, err } cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) + resp := ds.SendMessageResponse{Timestamp: strconv.FormatInt(signalCliSendResponse.Timestamp, 10)} + for _, entry := range signalCliSendResponse.Results { + if entry.Type != "SUCCESS" { + if resp.Errors == nil { + resp.Errors = &ds.SendMessageErrors{} + } + sendMessageError := ds.SendMessageError{Uuid: entry.RecipientAddress.Uuid, Number: entry.RecipientAddress.Number, Username: entry.RecipientAddress.Username, Reason: entry.Type} + resp.Errors.Recipients = append(resp.Errors.Recipients, sendMessageError) + } + } return &resp, nil } @@ -901,7 +916,7 @@ func (s *SignalClient) VerifyRegisteredNumber(number string, token string, pin s } } -func (s *SignalClient) SendV1(number string, message string, recipients []string, base64Attachments []string, isGroup bool) (*SendResponse, error) { +func (s *SignalClient) SendV1(number string, message string, recipients []string, base64Attachments []string, isGroup bool) (*ds.SendMessageResponse, error) { recipientType := ds.Number if isGroup { recipientType = ds.Group @@ -910,8 +925,8 @@ func (s *SignalClient) SendV1(number string, message string, recipients []string signalCliSendRequest := ds.SignalCliSendRequest{Number: number, Message: message, Recipients: recipients, Base64Attachments: base64Attachments, RecipientType: recipientType, Sticker: "", Mentions: nil, QuoteTimestamp: nil, QuoteAuthor: nil, QuoteMessage: nil, QuoteMentions: nil, TextMode: nil, EditTimestamp: nil, LinkPreview: nil} - timestamp, err := s.send(signalCliSendRequest) - return timestamp, err + resp, err := s.send(signalCliSendRequest) + return resp, err } func (s *SignalClient) getJsonRpc2Client() (*JsonRpc2Client, error) { @@ -931,7 +946,7 @@ func (s *SignalClient) getJsonRpc2Clients() []*JsonRpc2Client { func (s *SignalClient) SendV2(number string, message string, recps []string, base64Attachments []string, sticker string, mentions []ds.MessageMention, quoteTimestamp *int64, quoteAuthor *string, quoteMessage *string, quoteMentions []ds.MessageMention, textMode *string, editTimestamp *int64, notifySelf *bool, - linkPreview *ds.LinkPreviewType, viewOnce *bool) (*[]SendResponse, error) { + linkPreview *ds.LinkPreviewType, viewOnce *bool) (*[]ds.SendMessageResponse, error) { if len(recps) == 0 { return nil, errors.New("Please provide at least one recipient") } @@ -977,17 +992,17 @@ func (s *SignalClient) SendV2(number string, message string, recps []string, bas return nil, errors.New("A signal message cannot be sent to more than one group at once! Please use multiple REST API calls for that.") } - timestamps := []SendResponse{} + responses := []ds.SendMessageResponse{} for _, group := range groups { signalCliSendRequest := ds.SignalCliSendRequest{Number: number, Message: message, Recipients: []string{group}, Base64Attachments: base64Attachments, RecipientType: ds.Group, Sticker: sticker, Mentions: mentions, QuoteTimestamp: quoteTimestamp, QuoteAuthor: quoteAuthor, QuoteMessage: quoteMessage, QuoteMentions: quoteMentions, TextMode: textMode, EditTimestamp: editTimestamp, NotifySelf: notifySelf, LinkPreview: linkPreview, ViewOnce: viewOnce} - timestamp, err := s.send(signalCliSendRequest) + resp, err := s.send(signalCliSendRequest) if err != nil { return nil, err } - timestamps = append(timestamps, *timestamp) + responses = append(responses, *resp) } if len(numbers) > 0 { @@ -995,11 +1010,11 @@ func (s *SignalClient) SendV2(number string, message string, recps []string, bas RecipientType: ds.Number, Sticker: sticker, Mentions: mentions, QuoteTimestamp: quoteTimestamp, QuoteAuthor: quoteAuthor, QuoteMessage: quoteMessage, QuoteMentions: quoteMentions, TextMode: textMode, EditTimestamp: editTimestamp, NotifySelf: notifySelf, LinkPreview: linkPreview, ViewOnce: viewOnce} - timestamp, err := s.send(signalCliSendRequest) + resp, err := s.send(signalCliSendRequest) if err != nil { return nil, err } - timestamps = append(timestamps, *timestamp) + responses = append(responses, *resp) } if len(usernames) > 0 { @@ -1007,14 +1022,14 @@ func (s *SignalClient) SendV2(number string, message string, recps []string, bas RecipientType: ds.Username, Sticker: sticker, Mentions: mentions, QuoteTimestamp: quoteTimestamp, QuoteAuthor: quoteAuthor, QuoteMessage: quoteMessage, QuoteMentions: quoteMentions, TextMode: textMode, EditTimestamp: editTimestamp, NotifySelf: notifySelf, LinkPreview: linkPreview, ViewOnce: viewOnce} - timestamp, err := s.send(signalCliSendRequest) + resp, err := s.send(signalCliSendRequest) if err != nil { return nil, err } - timestamps = append(timestamps, *timestamp) + responses = append(responses, *resp) } - return ×tamps, nil + return &responses, nil } func (s *SignalClient) Receive(number string, timeout int64, ignoreAttachments bool, ignoreStories bool, ignoreAvatars bool, ignoreStickers bool, maxMessages int64, sendReadReceipts bool) (string, error) { diff --git a/src/datastructs/datastructs.go b/src/datastructs/datastructs.go index 4046111..9be8f47 100644 --- a/src/datastructs/datastructs.go +++ b/src/datastructs/datastructs.go @@ -58,3 +58,19 @@ type GroupPermissions struct { EditGroup string `json:"edit_group" enums:"only-admins,every-member"` SendMessages string `json:"send_messages" enums:"only-admins,every-member"` } + +type SendMessageError struct { + Username string `json:"username,omitempty"` + Number string `json:"number,omitempty"` + Uuid string `json:"uuid,omitempty"` + Reason string `json:"reason,omitempty"` +} + +type SendMessageErrors struct { + Recipients []SendMessageError `json:"recipients,omitempty"` +} + +type SendMessageResponse struct { + Timestamp string `json:"timestamp"` + Errors *SendMessageErrors `json:"errors,omitempty"` +} diff --git a/src/go.mod b/src/go.mod index 2573564..ac90042 100644 --- a/src/go.mod +++ b/src/go.mod @@ -9,6 +9,7 @@ require ( github.com/gabriel-vasile/mimetype v1.4.8 github.com/gin-gonic/gin v1.10.0 github.com/gofrs/uuid v4.4.0+incompatible + github.com/golang/protobuf v1.5.2 github.com/gorilla/websocket v1.5.0 github.com/h2non/filetype v1.1.3 github.com/robfig/cron/v3 v3.0.1 diff --git a/src/go.sum b/src/go.sum index 55bd023..3dcd083 100644 --- a/src/go.sum +++ b/src/go.sum @@ -62,10 +62,12 @@ github.com/gocraft/dbr/v2 v2.7.2 h1:ccUxMuz6RdZvD7VPhMRRMSS/ECF3gytPhPtcavjktHk= github.com/gocraft/dbr/v2 v2.7.2/go.mod h1:5bCqyIXO5fYn3jEp/L06QF4K1siFdhxChMjdNu6YJrg= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/flatbuffers v2.0.6+incompatible h1:XHFReMv7nFFusa+CEokzWbzaYocKXI6C7hdU5Kgh9Lw= github.com/google/flatbuffers v2.0.6+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -213,10 +215,13 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20210506142907-4a47615972c2 h1:pl8qT5D+48655f14yDURpIZwSPvMWuuekfAP+gxtjvk= google.golang.org/genproto v0.0.0-20210506142907-4a47615972c2/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/grpc v1.37.0 h1:uSZWeQJX5j11bIQ4AJoj+McDBo29cY1MCoC1wO3ts+c= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=