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.
This commit is contained in:
Bernhard B 2026-07-23 22:47:13 +02:00
parent 0e36e15f60
commit 6a225e62c1
5 changed files with 75 additions and 43 deletions

View File

@ -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{}) {

View File

@ -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 &timestamps, 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) {

View File

@ -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"`
}

View File

@ -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

View File

@ -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=