diff --git a/src/api/api.go b/src/api/api.go index 465fa03..e553803 100644 --- a/src/api/api.go +++ b/src/api/api.go @@ -138,6 +138,7 @@ type SendMessageV2 struct { QuoteMessage *string `json:"quote_message,omitempty"` QuoteMentions []ds.MessageMention `json:"quote_mentions,omitempty"` TextMode *string `json:"text_mode,omitempty" enums:"normal,styled"` + TextStyles []string `json:"text_styles,omitempty" example:"0:5:BOLD,10:3:ITALIC"` EditTimestamp *int64 `json:"edit_timestamp,omitempty"` NotifySelf *bool `json:"notify_self,omitempty"` LinkPreview *ds.LinkPreviewType `json:"link_preview,omitempty"` @@ -508,7 +509,7 @@ func (a *Api) Send(c *gin.Context) { // @Summary Send a signal message. // @Tags Messages -// @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. +// @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. Alternatively, pass the styles directly via text_styles in signal-cli's 'start:length:STYLE' format (STYLE is one of BOLD, ITALIC, SPOILER, STRIKETHROUGH, MONOSPACE); text_styles can't be combined with text_mode 'styled'. // @Accept json // @Produce json // @Success 201 {array} ds.SendMessageResponse @@ -547,8 +548,21 @@ func (a *Api) SendV2(c *gin.Context) { return } + if len(req.TextStyles) > 0 && req.TextMode != nil && *req.TextMode == "styled" { + c.JSON(400, Error{Msg: "'text_styles' can't be combined with text_mode 'styled'!"}) + return + } + + for _, textStyle := range req.TextStyles { + if !utils.IsValidTextStyle(textStyle) { + c.JSON(400, Error{Msg: "Invalid text style '" + textStyle + "' - expected format is 'start:length:STYLE' (e.g. '0:5:BOLD')"}) + return + } + } + + // the default text mode doesn't apply when the text styles are passed directly textMode := req.TextMode - if textMode == nil { + if textMode == nil && len(req.TextStyles) == 0 { defaultSignalTextMode := utils.GetEnv("DEFAULT_SIGNAL_TEXT_MODE", "normal") if defaultSignalTextMode == "styled" { styledStr := "styled" @@ -564,7 +578,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, req.LinkPreview, req.ViewOnce) + textMode, req.TextStyles, req.EditTimestamp, req.NotifySelf, req.LinkPreview, req.ViewOnce) if err != nil { switch err.(type) { case *client.RateLimitErrorType: diff --git a/src/client/client.go b/src/client/client.go index 6dc0d1f..2e21e9c 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -501,8 +501,8 @@ func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*ds.S return nil, err } - signalCliTextFormatStrings := []string{} - if signalCliSendRequest.TextMode != nil && *signalCliSendRequest.TextMode == "styled" { + signalCliTextFormatStrings := signalCliSendRequest.TextStyles + if len(signalCliTextFormatStrings) == 0 && signalCliSendRequest.TextMode != nil && *signalCliSendRequest.TextMode == "styled" { textstyleParser := utils.NewTextstyleParser(signalCliSendRequest.Message) signalCliSendRequest.Message, signalCliTextFormatStrings = textstyleParser.Parse() } @@ -954,7 +954,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, + quoteTimestamp *int64, quoteAuthor *string, quoteMessage *string, quoteMentions []ds.MessageMention, textMode *string, textStyles []string, editTimestamp *int64, notifySelf *bool, linkPreview *ds.LinkPreviewType, viewOnce *bool) (*[]ds.SendMessageResponse, error) { if len(recps) == 0 { return nil, errors.New("Please provide at least one recipient") @@ -1006,7 +1006,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, LinkPreview: linkPreview, ViewOnce: viewOnce} + TextMode: textMode, TextStyles: textStyles, EditTimestamp: editTimestamp, NotifySelf: notifySelf, LinkPreview: linkPreview, ViewOnce: viewOnce} resp, err := s.send(signalCliSendRequest) if err != nil { return nil, err @@ -1018,7 +1018,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, LinkPreview: linkPreview, ViewOnce: viewOnce} + TextMode: textMode, TextStyles: textStyles, EditTimestamp: editTimestamp, NotifySelf: notifySelf, LinkPreview: linkPreview, ViewOnce: viewOnce} resp, err := s.send(signalCliSendRequest) if err != nil { return nil, err @@ -1030,7 +1030,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, LinkPreview: linkPreview, ViewOnce: viewOnce} + TextMode: textMode, TextStyles: textStyles, EditTimestamp: editTimestamp, NotifySelf: notifySelf, LinkPreview: linkPreview, ViewOnce: viewOnce} resp, err := s.send(signalCliSendRequest) if err != nil { return nil, err diff --git a/src/datastructs/datastructs.go b/src/datastructs/datastructs.go index 9be8f47..38606f2 100644 --- a/src/datastructs/datastructs.go +++ b/src/datastructs/datastructs.go @@ -47,6 +47,7 @@ type SignalCliSendRequest struct { QuoteMessage *string QuoteMentions []MessageMention TextMode *string + TextStyles []string EditTimestamp *int64 NotifySelf *bool LinkPreview *LinkPreviewType diff --git a/src/docs/docs.go b/src/docs/docs.go index db26180..9e70fd6 100644 --- a/src/docs/docs.go +++ b/src/docs/docs.go @@ -468,6 +468,16 @@ const docTemplate = `{ ], "type": "string" }, + "text_styles": { + "example": [ + "0:5:BOLD", + "10:3:ITALIC" + ], + "items": { + "type": "string" + }, + "type": "array" + }, "view_once": { "type": "boolean" } @@ -5113,7 +5123,7 @@ const docTemplate = `{ "consumes": [ "application/json" ], - "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.", + "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. Alternatively, pass the styles directly via text_styles in signal-cli's 'start:length:STYLE' format (STYLE is one of BOLD, ITALIC, SPOILER, STRIKETHROUGH, MONOSPACE); text_styles can't be combined with text_mode 'styled'.", "parameters": [ { "description": "Input Data", diff --git a/src/docs/swagger.json b/src/docs/swagger.json index c8e910b..1c96f30 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -463,6 +463,16 @@ ], "type": "string" }, + "text_styles": { + "example": [ + "0:5:BOLD", + "10:3:ITALIC" + ], + "items": { + "type": "string" + }, + "type": "array" + }, "view_once": { "type": "boolean" } @@ -5108,7 +5118,7 @@ "consumes": [ "application/json" ], - "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.", + "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. Alternatively, pass the styles directly via text_styles in signal-cli's 'start:length:STYLE' format (STYLE is one of BOLD, ITALIC, SPOILER, STRIKETHROUGH, MONOSPACE); text_styles can't be combined with text_mode 'styled'.", "parameters": [ { "description": "Input Data", diff --git a/src/utils/textstyleparser.go b/src/utils/textstyleparser.go index f8b4beb..216e7bf 100644 --- a/src/utils/textstyleparser.go +++ b/src/utils/textstyleparser.go @@ -2,6 +2,7 @@ package utils import ( "strconv" + "strings" "unicode/utf16" "unicode/utf8" ) @@ -28,6 +29,20 @@ const ( const EscapeCharacter rune = '\\' +// IsValidTextStyle checks a signal-cli text style in the format start:length:STYLE +func IsValidTextStyle(textStyle string) bool { + parts := strings.SplitN(textStyle, ":", 3) + if len(parts) != 3 { + return false + } + for _, num := range parts[:2] { + if _, err := strconv.ParseUint(num, 10, 32); err != nil { + return false + } + } + return StringInSlice(parts[2], []string{Bold, Italic, Monospace, Strikethrough, Spoiler}) +} + func getUtf16StringLength(s string) int { runes := []rune(s) //turn string to slice diff --git a/src/utils/textstyleparser_test.go b/src/utils/textstyleparser_test.go index 0ad3237..672f90f 100644 --- a/src/utils/textstyleparser_test.go +++ b/src/utils/textstyleparser_test.go @@ -163,3 +163,16 @@ func TestBoldItalic(t *testing.T) { expectMessageEqual(t, message, "Bold Italic Text") expectFormatStringsEqual(t, signalCliFormatStrings, []string{"0:16:BOLD", "0:16:ITALIC"}) } + +func TestIsValidTextStyle(t *testing.T) { + for _, textStyle := range []string{"0:5:BOLD", "10:3:ITALIC", "2:1:SPOILER", "0:4:STRIKETHROUGH", "7:2:MONOSPACE"} { + if !IsValidTextStyle(textStyle) { + t.Errorf("expected %q to be valid", textStyle) + } + } + for _, textStyle := range []string{"", "foo", "0:5", "0:5:bold", "0:5:NORMAL", "-1:5:BOLD", "+1:5:BOLD", "a:5:BOLD", "0:5:BOLD:X"} { + if IsValidTextStyle(textStyle) { + t.Errorf("expected %q to be invalid", textStyle) + } + } +}