diff --git a/doc/EXAMPLES.md b/doc/EXAMPLES.md index cde1566..55d5c85 100644 --- a/doc/EXAMPLES.md +++ b/doc/EXAMPLES.md @@ -71,6 +71,15 @@ e.g: `curl -X POST -H "Content-Type: application/json" -d '{"message": "Hello World!", "number": "+431212131491291", "recipients": ["group.ckRzaEd4VmRzNnJaASAEsasa", "+4912812812121"]}' 'http://127.0.0.1:8080/v2/send'` + +- Send a message including a link preview + + `curl -X POST -H "Content-Type: application/json" -d '{"message": "Hey, checkout https://www.homeassistant.io", "number": "", "recipients": [], "link_preview": {"url": "https://www.homeassistant.io", "title": "Home Assistant", "base64_thumbnail": "'"$( base64 -w 0 )"'"}}' 'http://127.0.0.1:8080/v2/send'` + + e.g: + + `curl -X POST -H "Content-Type: application/json" -d '{"message": "Hey, checkout https://www.homeassistant.io", "number": "+431212131491291", "recipients": [+4354546464654], "link_preview": {"url": "https://www.homeassistant.io", "title": "Home Assistant", "base64_thumbnail": "'"$( base64 -w 0 /tmp/logo.png)"'"}}' 'http://127.0.0.1:8080/v2/send'` + - Receive messages Fetch all new messages in the inbox of the specified number. diff --git a/src/api/api.go b/src/api/api.go index af114dd..e9ef782 100644 --- a/src/api/api.go +++ b/src/api/api.go @@ -125,6 +125,7 @@ type SendMessageV2 struct { TextMode *string `json:"text_mode" enums:"normal,styled"` EditTimestamp *int64 `json:"edit_timestamp"` NotifySelf *bool `json:"notify_self"` + LinkPreview *ds.LinkPreviewType `json:"link_preview"` } type TypingIndicatorRequest struct { @@ -449,7 +450,7 @@ func (a *Api) SendV2(c *gin.Context) { data, err := a.signalClient.SendV2( req.Number, req.Message, req.Recipients, req.Base64Attachments, req.Sticker, req.Mentions, req.QuoteTimestamp, req.QuoteAuthor, req.QuoteMessage, req.QuoteMentions, - textMode, req.EditTimestamp, req.NotifySelf) + textMode, req.EditTimestamp, req.NotifySelf, req.LinkPreview) if err != nil { switch err.(type) { case *client.RateLimitErrorType: diff --git a/src/client/client.go b/src/client/client.go index 2c35e46..0171afd 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -229,10 +229,14 @@ func cleanupTmpFiles(paths []string) { } } -func cleanupAttachmentEntries(attachmentEntries []AttachmentEntry) { +func cleanupAttachmentEntries(attachmentEntries []AttachmentEntry, linkPreviewAttachmentEntry *AttachmentEntry) { for _, attachmentEntry := range attachmentEntries { attachmentEntry.cleanUp() } + + if linkPreviewAttachmentEntry != nil { + linkPreviewAttachmentEntry.cleanUp() + } } func convertInternalGroupIdToGroupId(internalId string) string { @@ -415,13 +419,41 @@ func (s *SignalClient) Init() error { return nil } +func validateLinkPreview(message string, linkPreview *ds.LinkPreviewType) error { + if linkPreview != nil { + if linkPreview.Url == "" { + return errors.New("Please provide a valid Link Preview URL") + } + + if !strings.HasPrefix(linkPreview.Url, "https") { + return errors.New("Link Preview URL must start with https://..") + } + + if linkPreview.Title == "" { + return errors.New("Please provide a valid Link Preview Title") + } + + if !strings.Contains(message, linkPreview.Url) { + return errors.New("Link Preview URL is missing in the message!") + } + } + + return nil +} + func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*SendResponse, error) { var resp SendResponse + var linkPreviewAttachmentEntry *AttachmentEntry = nil if len(signalCliSendRequest.Recipients) == 0 { return nil, errors.New("Please specify at least one recipient") } + err := validateLinkPreview(signalCliSendRequest.Message, signalCliSendRequest.LinkPreview) + if err != nil { + return nil, err + } + signalCliTextFormatStrings := []string{} if signalCliSendRequest.TextMode != nil && *signalCliSendRequest.TextMode == "styled" { textstyleParser := utils.NewTextstyleParser(signalCliSendRequest.Message) @@ -447,7 +479,7 @@ func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*Send err := attachmentEntry.storeBase64AsTemporaryFile() if err != nil { - cleanupAttachmentEntries(attachmentEntries) + cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) return nil, err } @@ -475,6 +507,9 @@ func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*Send TextStyles []string `json:"text-style,omitempty"` EditTimestamp *int64 `json:"edit-timestamp,omitempty"` NotifySelf bool `json:"notify-self,omitempty"` + PreviewUrl *string `json:"preview-url,omitempty"` + PreviewTitle *string `json:"preview-title,omitempty"` + PreviewImage *string `json:"preview-image,omitempty"` } request := Request{Message: signalCliSendRequest.Message} @@ -520,15 +555,30 @@ func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*Send request.TextStyles = signalCliTextFormatStrings } + if signalCliSendRequest.LinkPreview != nil { + request.PreviewUrl = &signalCliSendRequest.LinkPreview.Url + request.PreviewTitle = &signalCliSendRequest.LinkPreview.Title + + if signalCliSendRequest.LinkPreview.Base64Thumbnail != "" { + linkPreviewAttachmentEntry = NewAttachmentEntry(signalCliSendRequest.LinkPreview.Base64Thumbnail, s.attachmentTmpDir) + err := linkPreviewAttachmentEntry.storeBase64AsTemporaryFile() + if err != nil { + cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) + return nil, err + } + request.PreviewImage = &linkPreviewAttachmentEntry.FilePath + } + } + rawData, err := jsonRpc2Client.getRaw("send", &signalCliSendRequest.Number, request) if err != nil { - cleanupAttachmentEntries(attachmentEntries) + cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) return nil, err } err = json.Unmarshal([]byte(rawData), &resp) if err != nil { - cleanupAttachmentEntries(attachmentEntries) + cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) if strings.Contains(err.Error(), signalCliV2GroupError) { return nil, errors.New("Cannot send message to group - please first update your profile.") @@ -593,6 +643,25 @@ func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*Send cmd = append(cmd, strconv.FormatInt(*signalCliSendRequest.EditTimestamp, 10)) } + if signalCliSendRequest.LinkPreview != nil { + cmd = append(cmd, "--preview-url") + cmd = append(cmd, signalCliSendRequest.LinkPreview.Url) + + cmd = append(cmd, "--preview-title") + cmd = append(cmd, signalCliSendRequest.LinkPreview.Title) + + if signalCliSendRequest.LinkPreview.Base64Thumbnail != "" { + linkPreviewAttachmentEntry = NewAttachmentEntry(signalCliSendRequest.LinkPreview.Base64Thumbnail, s.attachmentTmpDir) + err := linkPreviewAttachmentEntry.storeBase64AsTemporaryFile() + if err != nil { + cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) + return nil, err + } + cmd = append(cmd, "--preview-image") + cmd = append(cmd, linkPreviewAttachmentEntry.FilePath) + } + } + // for backwards compatibility, if nothing is set, use the notify-self flag if signalCliSendRequest.NotifySelf == nil || *signalCliSendRequest.NotifySelf { cmd = append(cmd, "--notify-self") @@ -600,7 +669,7 @@ func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*Send rawData, err := s.cliClient.Execute(true, cmd, signalCliSendRequest.Message) if err != nil { - cleanupAttachmentEntries(attachmentEntries) + cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) if strings.Contains(err.Error(), signalCliV2GroupError) { return nil, errors.New("Cannot send message to group - please first update your profile.") } @@ -608,12 +677,12 @@ func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*Send } resp.Timestamp, err = strconv.ParseInt(strings.TrimSuffix(rawData, "\n"), 10, 64) if err != nil { - cleanupAttachmentEntries(attachmentEntries) + 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 } } - cleanupAttachmentEntries(attachmentEntries) + cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) return &resp, nil } @@ -732,7 +801,7 @@ 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} + QuoteMentions: nil, TextMode: nil, EditTimestamp: nil, LinkPreview: nil} timestamp, err := s.send(signalCliSendRequest) return timestamp, err } @@ -753,7 +822,8 @@ 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) (*[]SendResponse, error) { + quoteTimestamp *int64, quoteAuthor *string, quoteMessage *string, quoteMentions []ds.MessageMention, textMode *string, editTimestamp *int64, notifySelf *bool, + linkPreview *ds.LinkPreviewType) (*[]SendResponse, error) { if len(recps) == 0 { return nil, errors.New("Please provide at least one recipient") } @@ -804,7 +874,7 @@ func (s *SignalClient) SendV2(number string, message string, recps []string, bas 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} + TextMode: textMode, EditTimestamp: editTimestamp, NotifySelf: notifySelf, LinkPreview: linkPreview} timestamp, err := s.send(signalCliSendRequest) if err != nil { return nil, err @@ -816,7 +886,7 @@ func (s *SignalClient) SendV2(number string, message string, recps []string, bas signalCliSendRequest := ds.SignalCliSendRequest{Number: number, Message: message, Recipients: numbers, Base64Attachments: base64Attachments, RecipientType: ds.Number, Sticker: sticker, Mentions: mentions, QuoteTimestamp: quoteTimestamp, QuoteAuthor: quoteAuthor, QuoteMessage: quoteMessage, QuoteMentions: quoteMentions, - TextMode: textMode, EditTimestamp: editTimestamp, NotifySelf: notifySelf} + TextMode: textMode, EditTimestamp: editTimestamp, NotifySelf: notifySelf, LinkPreview: linkPreview} timestamp, err := s.send(signalCliSendRequest) if err != nil { return nil, err @@ -828,7 +898,7 @@ func (s *SignalClient) SendV2(number string, message string, recps []string, bas signalCliSendRequest := ds.SignalCliSendRequest{Number: number, Message: message, Recipients: usernames, Base64Attachments: base64Attachments, RecipientType: ds.Username, Sticker: sticker, Mentions: mentions, QuoteTimestamp: quoteTimestamp, QuoteAuthor: quoteAuthor, QuoteMessage: quoteMessage, QuoteMentions: quoteMentions, - TextMode: textMode, EditTimestamp: editTimestamp, NotifySelf: notifySelf} + TextMode: textMode, EditTimestamp: editTimestamp, NotifySelf: notifySelf, LinkPreview: linkPreview} timestamp, err := s.send(signalCliSendRequest) if err != nil { return nil, err diff --git a/src/datastructs/datastructs.go b/src/datastructs/datastructs.go index aef07ac..e208a8d 100644 --- a/src/datastructs/datastructs.go +++ b/src/datastructs/datastructs.go @@ -27,6 +27,13 @@ type SendMessageRecipient struct { Type string `json:"type"` } +type LinkPreviewType struct { + Url string `json:"url"` + Title string `json:"title"` + Description string `json:"description"` + Base64Thumbnail string `json:"base64_thumbnail"` +} + type SignalCliSendRequest struct { Number string Message string @@ -42,4 +49,5 @@ type SignalCliSendRequest struct { TextMode *string EditTimestamp *int64 NotifySelf *bool + LinkPreview *LinkPreviewType } diff --git a/src/docs/docs.go b/src/docs/docs.go index c0f5e60..2d9225d 100644 --- a/src/docs/docs.go +++ b/src/docs/docs.go @@ -2426,6 +2426,9 @@ const docTemplate = `{ "edit_timestamp": { "type": "integer" }, + "link_preview": { + "$ref": "#/definitions/data.LinkPreviewType" + }, "mentions": { "type": "array", "items": { @@ -2820,6 +2823,23 @@ const docTemplate = `{ } } }, + "data.LinkPreviewType": { + "type": "object", + "properties": { + "base64_thumbnail": { + "type": "string" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, "data.MessageMention": { "type": "object", "properties": { diff --git a/src/docs/swagger.json b/src/docs/swagger.json index d14c41d..52efc63 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -2423,6 +2423,9 @@ "edit_timestamp": { "type": "integer" }, + "link_preview": { + "$ref": "#/definitions/data.LinkPreviewType" + }, "mentions": { "type": "array", "items": { @@ -2817,6 +2820,23 @@ } } }, + "data.LinkPreviewType": { + "type": "object", + "properties": { + "base64_thumbnail": { + "type": "string" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, "data.MessageMention": { "type": "object", "properties": { diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index 87a6135..6037705 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -174,6 +174,8 @@ definitions: type: array edit_timestamp: type: integer + link_preview: + $ref: '#/definitions/data.LinkPreviewType' mentions: items: $ref: '#/definitions/data.MessageMention' @@ -431,6 +433,17 @@ definitions: username_link: type: string type: object + data.LinkPreviewType: + properties: + base64_thumbnail: + type: string + description: + type: string + title: + type: string + url: + type: string + type: object data.MessageMention: properties: author: