Compare commits

...

22 Commits

Author SHA1 Message Date
Bernhard B.
2fe9eb5bf3
Merge pull request #880 from bbernhard/dependabot/go_modules/src/github.com/gorilla/websocket-1.5.3
Bump github.com/gorilla/websocket from 1.5.0 to 1.5.3 in /src
2026-08-26 22:24:54 +02:00
dependabot[bot]
1491a52285
Bump github.com/gorilla/websocket from 1.5.0 to 1.5.3 in /src
Bumps [github.com/gorilla/websocket](https://github.com/gorilla/websocket) from 1.5.0 to 1.5.3.
- [Release notes](https://github.com/gorilla/websocket/releases)
- [Commits](https://github.com/gorilla/websocket/compare/v1.5.0...v1.5.3)

---
updated-dependencies:
- dependency-name: github.com/gorilla/websocket
  dependency-version: 1.5.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-24 21:37:32 +00:00
Bernhard B
a08db23a31 fixed bug in textstyleparser
* the textstyleparser now properly handles `***` and marks the
  text as bold and italic.

see #879
2026-08-16 22:51:31 +02:00
Bernhard B
5cc379fde0 only (re-)build github pages documentation when a new release is published
see #872
2026-08-09 21:39:35 +02:00
Bernhard B
1a2da6c79b updated signal-cli to v0.14.7 2026-08-01 15:06:43 +02:00
Bernhard B
54507eba20 handle status code -4 in the getRaw method of the JsonRpc2Client 2026-07-23 22:51:35 +02:00
Bernhard B
6a225e62c1 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.
2026-07-23 22:47:13 +02:00
Bernhard B
0e36e15f60 remove debug logging 2026-07-18 22:21:40 +02:00
Bernhard B
7138ffde7c fixed version in deploy-pages.yml 2026-07-18 13:09:22 +02:00
Bernhard B
7c12204889 execute deploy-pages workflow for every commit 2026-07-18 13:05:41 +02:00
Bernhard B
bef11316e3 dummy change to test github workflow 2026-07-18 13:03:20 +02:00
Bernhard B
2d03a6c0ce added debug output to deploy-pages.yml 2026-07-18 12:53:53 +02:00
Bernhard B.
d258f01d59
Merge pull request #869 from Gara-Dorta/json-schema-dowload
feat: download the json schemas from the signal-cli releases
2026-07-14 21:50:32 +02:00
Gara Dorta
b13a74f43e feat: download the json schemas from the signal-cli releases 2026-07-14 20:08:28 +02:00
Bernhard B.
be0842fdb2
Merge pull request #844 from Gara-Dorta/deploy-pages
feat: deploy pages with CI
2026-07-13 22:29:46 +02:00
Bernhard B.
17b86412f3
Merge pull request #868 from bbernhard/dependabot/go_modules/src/golang.org/x/crypto-0.52.0
Bump golang.org/x/crypto from 0.51.0 to 0.52.0 in /src
2026-07-11 22:23:26 +02:00
dependabot[bot]
2ee3907d9e
Bump golang.org/x/crypto from 0.51.0 to 0.52.0 in /src
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.51.0 to 0.52.0.
- [Commits](https://github.com/golang/crypto/compare/v0.51.0...v0.52.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.52.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 21:35:10 +00:00
Bernhard B.
af64b772cd
Merge pull request #865 from bbernhard/dependabot/go_modules/src/golang.org/x/net-0.55.0
Bump golang.org/x/net from 0.47.0 to 0.55.0 in /src
2026-07-02 21:57:51 +02:00
dependabot[bot]
e34cd5af87
Bump golang.org/x/net from 0.47.0 to 0.55.0 in /src
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.47.0 to 0.55.0.
- [Commits](https://github.com/golang/net/compare/v0.47.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 06:13:13 +00:00
Bernhard B.
fe9df012f1
Merge pull request #859 from pinkustar/expose-group-membership
Expose group membership (isMember) on the /v1/groups endpoints
2026-06-25 22:14:25 +02:00
PINKU
ede08b64d8 expose group membership isMember via the REST API 2026-06-05 10:10:04 +02:00
Gara Dorta
6e1698e820 feat: deploy pages with CI
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 12:01:26 +01:00
16 changed files with 327 additions and 114 deletions

38
.github/workflows/deploy-pages.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: Deploy API Docs to GitHub Pages
on:
release:
types:
- published
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build-and-deploy:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Pages
uses: actions/configure-pages@v6
- name: Build static documentation bundle
run: |
src/docs/build-static-bundle.sh site "${{ github.event.release.tag_name }}"
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v5
with:
path: site
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5

View File

@ -1,5 +1,5 @@
ARG SIGNAL_CLI_VERSION=0.14.5 ARG SIGNAL_CLI_VERSION=0.14.7
ARG LIBSIGNAL_CLIENT_VERSION=0.94.4 ARG LIBSIGNAL_CLIENT_VERSION=0.99.1
ARG SWAG_VERSION=1.16.4 ARG SWAG_VERSION=1.16.4
@ -16,7 +16,7 @@ RUN dpkg-reconfigure debconf --frontend=noninteractive \
&& apt-get update \ && apt-get update \
&& apt-get -y install --no-install-recommends \ && apt-get -y install --no-install-recommends \
wget git locales zip unzip \ wget git locales zip unzip \
file build-essential libz-dev zlib1g-dev binutils openjdk-25-jdk \ file build-essential libz-dev zlib1g-dev binutils \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
#COPY ext/libraries/libsignal-client/v${LIBSIGNAL_CLIENT_VERSION} /tmp/libsignal-client-libraries #COPY ext/libraries/libsignal-client/v${LIBSIGNAL_CLIENT_VERSION} /tmp/libsignal-client-libraries
@ -40,15 +40,6 @@ ENV JAVA_OPTS="-Djdk.lang.Process.launchMechanism=vfork"
ENV LANG en_US.UTF-8 ENV LANG en_US.UTF-8
# building the jsonSchemas for armv7l unfortunately doesn't work, so as a temporary fix, do not build the json schema
# for armv7. should be removed as soon as https://github.com/AsamK/signal-cli/pull/2040 is merged
RUN if [ "$(uname -m)" != "armv7l" ]; then \
cd /tmp \
&& git clone https://github.com/AsamK/signal-cli.git --branch v${SIGNAL_CLI_VERSION} --single-branch signal-cli-source \
&& cd signal-cli-source \
&& ./gradlew jsonSchemas; \
fi;
RUN go install github.com/swaggo/swag/cmd/swag@v${SWAG_VERSION} RUN go install github.com/swaggo/swag/cmd/swag@v${SWAG_VERSION}
RUN cd /tmp/ \ RUN cd /tmp/ \
@ -112,13 +103,11 @@ RUN cd /tmp/signal-cli-rest-api-src && ${GOPATH}/bin/swag init --requiredByDefau
# manually add the json schemas for the receive V1 endpoint to the docs # manually add the json schemas for the receive V1 endpoint to the docs
# (building the jsonSchemas for armv7l unfortunately doesn't work, so as a temporary fix, do not build the json schema RUN cd /tmp/signal-cli-rest-api-src/docs \
# for armv7. should be removed as soon as https://github.com/AsamK/signal-cli/pull/2040 is merged) && wget https://github.com/AsamK/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}-json-schemas.tar.gz \
RUN if [ "$(uname -m)" != "armv7l" ]; then \ && mkdir signal-cli-schemas \
cd /tmp/signal-cli-rest-api-src/docs \ && tar xf signal-cli-${SIGNAL_CLI_VERSION}-json-schemas.tar.gz -C signal-cli-schemas \
&& cp -r /tmp/signal-cli-source/build/generated/META-INF/schemas signal-cli-schemas \ && go run add_v1_receive_schemas.go signal-cli-schemas
&& go run add_v1_receive_schemas.go signal-cli-schemas; \
fi;
# build signal-cli-rest-api # build signal-cli-rest-api
RUN cd /tmp/signal-cli-rest-api-src && go build -o signal-cli-rest-api main.go RUN cd /tmp/signal-cli-rest-api-src && go build -o signal-cli-rest-api main.go

View File

@ -173,10 +173,6 @@ type TrustIdentityRequest struct {
TrustAllKnownKeys *bool `json:"trust_all_known_keys,omitempty" example:"false"` TrustAllKnownKeys *bool `json:"trust_all_known_keys,omitempty" example:"false"`
} }
type SendMessageResponse struct {
Timestamp string `json:"timestamp"`
}
type RemoteDeleteResponse struct { type RemoteDeleteResponse struct {
Timestamp string `json:"timestamp"` Timestamp string `json:"timestamp"`
} }
@ -476,7 +472,6 @@ func (a *Api) VerifyRegisteredNumber(c *gin.Context) {
// @Router /v1/send [post] // @Router /v1/send [post]
// @Deprecated // @Deprecated
func (a *Api) Send(c *gin.Context) { func (a *Api) Send(c *gin.Context) {
var req SendMessageV1 var req SendMessageV1
err := c.BindJSON(&req) err := c.BindJSON(&req)
if err != nil { if err != nil {
@ -489,12 +484,12 @@ func (a *Api) Send(c *gin.Context) {
base64Attachments = append(base64Attachments, req.Base64Attachment) 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 { if err != nil {
c.JSON(400, Error{Msg: err.Error()}) c.JSON(400, Error{Msg: err.Error()})
return return
} }
c.JSON(201, SendMessageResponse{Timestamp: strconv.FormatInt(timestamp.Timestamp, 10)}) c.JSON(201, resp)
} }
// @Summary Send a signal message. // @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. // @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 // @Accept json
// @Produce json // @Produce json
// @Success 201 {object} SendMessageResponse // @Success 201 {object} ds.SendMessageResponse
// @Failure 400 {object} SendMessageError // @Failure 400 {object} SendMessageError
// @Param data body SendMessageV2 true "Input Data" // @Param data body SendMessageV2 true "Input Data"
// @Router /v2/send [post] // @Router /v2/send [post]
@ -574,7 +569,7 @@ func (a *Api) SendV2(c *gin.Context) {
return 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{}) { func (a *Api) handleSignalReceive(ws *websocket.Conn, number string, stop chan struct{}) {

View File

@ -119,6 +119,7 @@ type GroupEntry struct {
InternalId string `json:"internal_id"` InternalId string `json:"internal_id"`
Members []string `json:"members"` Members []string `json:"members"`
Blocked bool `json:"blocked"` Blocked bool `json:"blocked"`
Member bool `json:"member"`
PendingInvites []string `json:"pending_invites"` PendingInvites []string `json:"pending_invites"`
PendingRequests []string `json:"pending_requests"` PendingRequests []string `json:"pending_requests"`
InviteLink string `json:"invite_link"` InviteLink string `json:"invite_link"`
@ -143,6 +144,7 @@ type ExpandedGroupEntry struct {
InternalId string `json:"internal_id"` InternalId string `json:"internal_id"`
Members []GroupMember `json:"members"` Members []GroupMember `json:"members"`
Blocked bool `json:"blocked"` Blocked bool `json:"blocked"`
Member bool `json:"member"`
PendingInvites []GroupMember `json:"pending_invites"` PendingInvites []GroupMember `json:"pending_invites"`
PendingRequests []GroupMember `json:"pending_requests"` PendingRequests []GroupMember `json:"pending_requests"`
InviteLink string `json:"invite_link"` InviteLink string `json:"invite_link"`
@ -477,8 +479,8 @@ func validateLinkPreview(message string, linkPreview *ds.LinkPreviewType) error
return nil return nil
} }
func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*SendResponse, error) { func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*ds.SendMessageResponse, error) {
var resp SendResponse var rawData string
var linkPreviewAttachmentEntry *AttachmentEntry = nil var linkPreviewAttachmentEntry *AttachmentEntry = nil
if len(signalCliSendRequest.Recipients) == 0 { if len(signalCliSendRequest.Recipients) == 0 {
@ -613,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 { if err != nil {
cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry)
return nil, err 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 { } 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 { if signalCliSendRequest.RecipientType == ds.Number {
cmd = append(cmd, signalCliSendRequest.Recipients...) cmd = append(cmd, signalCliSendRequest.Recipients...)
} else if signalCliSendRequest.RecipientType == ds.Group { } else if signalCliSendRequest.RecipientType == ds.Group {
@ -717,23 +709,48 @@ func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*Send
cmd = append(cmd, "--view-once") 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 { if err != nil {
cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry)
return nil, err
}
}
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) { if strings.Contains(err.Error(), signalCliV2GroupError) {
return nil, errors.New("Cannot send message to group - please first update your profile.") return nil, errors.New("Cannot send message to group - please first update your profile.")
} }
return nil, err 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
}
}
cleanupAttachmentEntries(attachmentEntries, linkPreviewAttachmentEntry) 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 return &resp, nil
} }
@ -899,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 recipientType := ds.Number
if isGroup { if isGroup {
recipientType = ds.Group recipientType = ds.Group
@ -908,8 +925,8 @@ func (s *SignalClient) SendV1(number string, message string, recipients []string
signalCliSendRequest := ds.SignalCliSendRequest{Number: number, Message: message, Recipients: recipients, Base64Attachments: base64Attachments, signalCliSendRequest := ds.SignalCliSendRequest{Number: number, Message: message, Recipients: recipients, Base64Attachments: base64Attachments,
RecipientType: recipientType, Sticker: "", Mentions: nil, QuoteTimestamp: nil, QuoteAuthor: nil, QuoteMessage: nil, RecipientType: recipientType, Sticker: "", Mentions: nil, QuoteTimestamp: nil, QuoteAuthor: nil, QuoteMessage: nil,
QuoteMentions: nil, TextMode: nil, EditTimestamp: nil, LinkPreview: nil} QuoteMentions: nil, TextMode: nil, EditTimestamp: nil, LinkPreview: nil}
timestamp, err := s.send(signalCliSendRequest) resp, err := s.send(signalCliSendRequest)
return timestamp, err return resp, err
} }
func (s *SignalClient) getJsonRpc2Client() (*JsonRpc2Client, error) { func (s *SignalClient) getJsonRpc2Client() (*JsonRpc2Client, error) {
@ -929,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, 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, editTimestamp *int64, notifySelf *bool,
linkPreview *ds.LinkPreviewType, viewOnce *bool) (*[]SendResponse, error) { linkPreview *ds.LinkPreviewType, viewOnce *bool) (*[]ds.SendMessageResponse, error) {
if len(recps) == 0 { if len(recps) == 0 {
return nil, errors.New("Please provide at least one recipient") return nil, errors.New("Please provide at least one recipient")
} }
@ -975,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.") 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 { for _, group := range groups {
signalCliSendRequest := ds.SignalCliSendRequest{Number: number, Message: message, Recipients: []string{group}, Base64Attachments: base64Attachments, signalCliSendRequest := ds.SignalCliSendRequest{Number: number, Message: message, Recipients: []string{group}, Base64Attachments: base64Attachments,
RecipientType: ds.Group, Sticker: sticker, Mentions: mentions, QuoteTimestamp: quoteTimestamp, RecipientType: ds.Group, Sticker: sticker, Mentions: mentions, QuoteTimestamp: quoteTimestamp,
QuoteAuthor: quoteAuthor, QuoteMessage: quoteMessage, QuoteMentions: quoteMentions, QuoteAuthor: quoteAuthor, QuoteMessage: quoteMessage, QuoteMentions: quoteMentions,
TextMode: textMode, EditTimestamp: editTimestamp, NotifySelf: notifySelf, LinkPreview: linkPreview, ViewOnce: viewOnce} TextMode: textMode, EditTimestamp: editTimestamp, NotifySelf: notifySelf, LinkPreview: linkPreview, ViewOnce: viewOnce}
timestamp, err := s.send(signalCliSendRequest) resp, err := s.send(signalCliSendRequest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
timestamps = append(timestamps, *timestamp) responses = append(responses, *resp)
} }
if len(numbers) > 0 { if len(numbers) > 0 {
@ -993,11 +1010,11 @@ func (s *SignalClient) SendV2(number string, message string, recps []string, bas
RecipientType: ds.Number, Sticker: sticker, Mentions: mentions, QuoteTimestamp: quoteTimestamp, RecipientType: ds.Number, Sticker: sticker, Mentions: mentions, QuoteTimestamp: quoteTimestamp,
QuoteAuthor: quoteAuthor, QuoteMessage: quoteMessage, QuoteMentions: quoteMentions, QuoteAuthor: quoteAuthor, QuoteMessage: quoteMessage, QuoteMentions: quoteMentions,
TextMode: textMode, EditTimestamp: editTimestamp, NotifySelf: notifySelf, LinkPreview: linkPreview, ViewOnce: viewOnce} TextMode: textMode, EditTimestamp: editTimestamp, NotifySelf: notifySelf, LinkPreview: linkPreview, ViewOnce: viewOnce}
timestamp, err := s.send(signalCliSendRequest) resp, err := s.send(signalCliSendRequest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
timestamps = append(timestamps, *timestamp) responses = append(responses, *resp)
} }
if len(usernames) > 0 { if len(usernames) > 0 {
@ -1005,14 +1022,14 @@ func (s *SignalClient) SendV2(number string, message string, recps []string, bas
RecipientType: ds.Username, Sticker: sticker, Mentions: mentions, QuoteTimestamp: quoteTimestamp, RecipientType: ds.Username, Sticker: sticker, Mentions: mentions, QuoteTimestamp: quoteTimestamp,
QuoteAuthor: quoteAuthor, QuoteMessage: quoteMessage, QuoteMentions: quoteMentions, QuoteAuthor: quoteAuthor, QuoteMessage: quoteMessage, QuoteMentions: quoteMentions,
TextMode: textMode, EditTimestamp: editTimestamp, NotifySelf: notifySelf, LinkPreview: linkPreview, ViewOnce: viewOnce} TextMode: textMode, EditTimestamp: editTimestamp, NotifySelf: notifySelf, LinkPreview: linkPreview, ViewOnce: viewOnce}
timestamp, err := s.send(signalCliSendRequest) resp, err := s.send(signalCliSendRequest)
if err != nil { if err != nil {
return nil, err 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) { func (s *SignalClient) Receive(number string, timeout int64, ignoreAttachments bool, ignoreStories bool, ignoreAvatars bool, ignoreStickers bool, maxMessages int64, sendReadReceipts bool) (string, error) {
@ -1330,6 +1347,25 @@ func (s *SignalClient) RemoveAdminsFromGroup(number string, groupId string, admi
return s.updateGroupAdmins(number, groupId, admins, false) return s.updateGroupAdmins(number, groupId, admins, false)
} }
func signalCliGroupEntryToExpandedGroupEntry(signalCliGroupEntry SignalCliGroupEntry) ExpandedGroupEntry {
var groupEntry ExpandedGroupEntry
groupEntry.InternalId = signalCliGroupEntry.Id
groupEntry.Name = signalCliGroupEntry.Name
groupEntry.Id = convertInternalGroupIdToGroupId(signalCliGroupEntry.Id)
groupEntry.Blocked = signalCliGroupEntry.IsBlocked
groupEntry.Member = signalCliGroupEntry.IsMember
groupEntry.Description = signalCliGroupEntry.Description
groupEntry.Permissions.SendMessages = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionSendMessage)
groupEntry.Permissions.EditGroup = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionSendMessage)
groupEntry.Permissions.AddMembers = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionAddMember)
groupEntry.Members = signalCliGroupEntry.Members
groupEntry.PendingInvites = signalCliGroupEntry.PendingMembers
groupEntry.PendingRequests = signalCliGroupEntry.RequestingMembers
groupEntry.Admins = signalCliGroupEntry.Admins
groupEntry.InviteLink = signalCliGroupEntry.GroupInviteLink
return groupEntry
}
func (s *SignalClient) GetGroupsExpanded(number string) ([]ExpandedGroupEntry, error) { func (s *SignalClient) GetGroupsExpanded(number string) ([]ExpandedGroupEntry, error) {
groupEntries := []ExpandedGroupEntry{} groupEntries := []ExpandedGroupEntry{}
@ -1360,22 +1396,7 @@ func (s *SignalClient) GetGroupsExpanded(number string) ([]ExpandedGroupEntry, e
} }
for _, signalCliGroupEntry := range signalCliGroupEntries { for _, signalCliGroupEntry := range signalCliGroupEntries {
var groupEntry ExpandedGroupEntry groupEntries = append(groupEntries, signalCliGroupEntryToExpandedGroupEntry(signalCliGroupEntry))
groupEntry.InternalId = signalCliGroupEntry.Id
groupEntry.Name = signalCliGroupEntry.Name
groupEntry.Id = convertInternalGroupIdToGroupId(signalCliGroupEntry.Id)
groupEntry.Blocked = signalCliGroupEntry.IsBlocked
groupEntry.Description = signalCliGroupEntry.Description
groupEntry.Permissions.SendMessages = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionSendMessage)
groupEntry.Permissions.EditGroup = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionSendMessage)
groupEntry.Permissions.AddMembers = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionAddMember)
groupEntry.Members = signalCliGroupEntry.Members
groupEntry.PendingInvites = signalCliGroupEntry.PendingMembers
groupEntry.PendingRequests = signalCliGroupEntry.RequestingMembers
groupEntry.Admins = signalCliGroupEntry.Admins
groupEntry.InviteLink = signalCliGroupEntry.GroupInviteLink
groupEntries = append(groupEntries, groupEntry)
} }
return groupEntries, nil return groupEntries, nil
@ -1390,7 +1411,7 @@ func (s *SignalClient) GetGroups(number string) ([]GroupEntry, error) {
groupEntries := []GroupEntry{} groupEntries := []GroupEntry{}
for _, expandedGroupEntry := range expandedGroupEntries { for _, expandedGroupEntry := range expandedGroupEntries {
groupEntry := GroupEntry{InternalId: expandedGroupEntry.InternalId, Name: expandedGroupEntry.Name, groupEntry := GroupEntry{InternalId: expandedGroupEntry.InternalId, Name: expandedGroupEntry.Name,
Id: expandedGroupEntry.Id, Blocked: expandedGroupEntry.Blocked, Description: expandedGroupEntry.Description, Id: expandedGroupEntry.Id, Blocked: expandedGroupEntry.Blocked, Member: expandedGroupEntry.Member, Description: expandedGroupEntry.Description,
Permissions: expandedGroupEntry.Permissions, InviteLink: expandedGroupEntry.InviteLink} Permissions: expandedGroupEntry.Permissions, InviteLink: expandedGroupEntry.InviteLink}
members := []string{} members := []string{}

93
src/client/groups_test.go Normal file
View File

@ -0,0 +1,93 @@
package client
import (
"encoding/json"
"testing"
)
// sampleListGroupsJSON mirrors the shape of signal-cli's `listGroups` JSON output
// (see signal-cli's ListGroupsCommand / the jsonrpc man page). It contains a group
// the account is still in (isMember=true), a group it has left or been removed from
// (isMember=false) — the "ghost" group that the REST API previously could not
// distinguish — and a blocked-but-still-member group to verify the two flags are
// mapped independently.
const sampleListGroupsJSON = `[
{
"id": "Pmpi+EfPWmsxiomLe9Nx2XF9HOE483p6iKiFj65iMwI=",
"name": "Current Group",
"description": "still a member",
"isMember": true,
"isBlocked": false,
"members": [{"number": "+15551230001", "uuid": "11111111-1111-1111-1111-111111111111"}],
"pendingMembers": [],
"requestingMembers": [],
"admins": [{"number": "+15551230001", "uuid": "11111111-1111-1111-1111-111111111111"}],
"groupInviteLink": "",
"permissionAddMember": "EVERY_MEMBER",
"permissionSendMessage": "EVERY_MEMBER"
},
{
"id": "Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MGFiY2RlZmdoaWo=",
"name": "Left Group",
"description": "removed or left",
"isMember": false,
"isBlocked": false,
"members": [],
"pendingMembers": [],
"requestingMembers": [],
"admins": [],
"groupInviteLink": ""
},
{
"id": "YmxvY2tlZGdyb3VwaWQwMDAwMDAwMDAwMDAwMDAwMDA=",
"name": "Blocked But Member",
"description": "blocked yet still a member",
"isMember": true,
"isBlocked": true,
"members": [],
"pendingMembers": [],
"requestingMembers": [],
"admins": [],
"groupInviteLink": ""
}
]`
// TestSignalCliGroupEntryToExpandedGroupEntry verifies that the signal-cli isMember
// flag is carried through to the REST ExpandedGroupEntry.Member field, independently
// of the isBlocked -> Blocked mapping.
func TestSignalCliGroupEntryToExpandedGroupEntry(t *testing.T) {
var entries []SignalCliGroupEntry
if err := json.Unmarshal([]byte(sampleListGroupsJSON), &entries); err != nil {
t.Fatalf("failed to unmarshal sample listGroups JSON: %v", err)
}
if len(entries) != 3 {
t.Fatalf("expected 3 group entries, got %d", len(entries))
}
cases := []struct {
name string
wantMember bool
wantBlocked bool
}{
{"Current Group", true, false},
{"Left Group", false, false},
{"Blocked But Member", true, true},
}
for i, c := range cases {
got := signalCliGroupEntryToExpandedGroupEntry(entries[i])
if got.Name != c.name {
t.Errorf("entry %d: Name = %q, want %q", i, got.Name, c.name)
}
if got.Member != c.wantMember {
t.Errorf("%s: Member = %v, want %v (must reflect signal-cli isMember)", c.name, got.Member, c.wantMember)
}
if got.Blocked != c.wantBlocked {
t.Errorf("%s: Blocked = %v, want %v", c.name, got.Blocked, c.wantBlocked)
}
if got.InternalId != entries[i].Id {
t.Errorf("%s: InternalId = %q, want %q", c.name, got.InternalId, entries[i].Id)
}
}
}

View File

@ -186,6 +186,8 @@ func (r *JsonRpc2Client) getRaw(command string, account *string, args interface{
ChallengeTokens: challengeTokens, ChallengeTokens: challengeTokens,
Err: errors.New(resp.Err.Message), Err: errors.New(resp.Err.Message),
} }
} else if resp.Err.Code == -4 {
return "", errors.New("Unable to complete request due to untrusted recipient keys")
} }
return "", errors.New(resp.Err.Message) return "", errors.New(resp.Err.Message)
} }

View File

@ -58,3 +58,19 @@ type GroupPermissions struct {
EditGroup string `json:"edit_group" enums:"only-admins,every-member"` EditGroup string `json:"edit_group" enums:"only-admins,every-member"`
SendMessages string `json:"send_messages" 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

@ -45,7 +45,7 @@ Install [go](https://go.dev/).
cd docs cd docs
``` ```
1. Add the signal-cli receive V1 schemas 1. Add the signal-cli receive V1 schemas
* Download the `signal-cli-x.y.z-json-schemas.tar.gz` schema files from https://github.com/Gara-Dorta/signal-cli/releases * Download the `signal-cli-x.y.z-json-schemas.tar.gz` schema files from https://github.com/AsamK/signal-cli/releases
* Extract the files * Extract the files
* Run the script to add the schemas * Run the script to add the schemas
```bash ```bash

24
src/docs/build-static-bundle.sh Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
DOCS_DIR="$ROOT_DIR/src/docs"
INDEX_FILE="$DOCS_DIR/index.html"
OUT_DIR="${1:?output directory argument is required}"
DOCS_VERSION="${2:?docs version argument is required}"
NORMALIZED_DOCS_VERSION="${DOCS_VERSION#v}"
echo "Normalized docs version: $NORMALIZED_DOCS_VERSION"
rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR"
cp "$DOCS_DIR/swagger.json" "$OUT_DIR/swagger.json"
cp "$INDEX_FILE" "$OUT_DIR/index.html"
touch "$OUT_DIR/.nojekyll"
jq --arg version "$NORMALIZED_DOCS_VERSION" '.info.version = $version' "$OUT_DIR/swagger.json" > "$OUT_DIR/swagger.json.tmp"
mv "$OUT_DIR/swagger.json.tmp" "$OUT_DIR/swagger.json"
echo "Static bundle created at: $OUT_DIR"

View File

@ -806,6 +806,9 @@ const docTemplate = `{
"invite_link": { "invite_link": {
"type": "string" "type": "string"
}, },
"member": {
"type": "boolean"
},
"members": { "members": {
"items": { "items": {
"type": "string" "type": "string"
@ -838,6 +841,7 @@ const docTemplate = `{
"id", "id",
"internal_id", "internal_id",
"invite_link", "invite_link",
"member",
"members", "members",
"name", "name",
"pending_invites", "pending_invites",

View File

@ -11,7 +11,7 @@
window.onload = function () { window.onload = function () {
// Begin Swagger UI call region // Begin Swagger UI call region
const ui = SwaggerUIBundle({ const ui = SwaggerUIBundle({
url: "src/docs/swagger.json", //Location of Open API spec in the repo url: "swagger.json",
dom_id: '#swagger-ui', dom_id: '#swagger-ui',
deepLinking: true, deepLinking: true,
presets: [ presets: [

View File

@ -801,6 +801,9 @@
"invite_link": { "invite_link": {
"type": "string" "type": "string"
}, },
"member": {
"type": "boolean"
},
"members": { "members": {
"items": { "items": {
"type": "string" "type": "string"
@ -833,6 +836,7 @@
"id", "id",
"internal_id", "internal_id",
"invite_link", "invite_link",
"member",
"members", "members",
"name", "name",
"pending_invites", "pending_invites",

View File

@ -1,6 +1,6 @@
module github.com/bbernhard/signal-cli-rest-api module github.com/bbernhard/signal-cli-rest-api
go 1.24.0 go 1.25.0
require ( require (
github.com/bbernhard/gluasql v0.2.0 github.com/bbernhard/gluasql v0.2.0
@ -9,7 +9,7 @@ require (
github.com/gabriel-vasile/mimetype v1.4.8 github.com/gabriel-vasile/mimetype v1.4.8
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/gofrs/uuid v4.4.0+incompatible github.com/gofrs/uuid v4.4.0+incompatible
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.3
github.com/h2non/filetype v1.1.3 github.com/h2non/filetype v1.1.3
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.1 github.com/sirupsen/logrus v1.9.1
@ -57,11 +57,11 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.14.0 // indirect golang.org/x/arch v0.14.0 // indirect
golang.org/x/crypto v0.45.0 // indirect golang.org/x/crypto v0.52.0 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.38.0 // indirect golang.org/x/tools v0.44.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@ -71,8 +71,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
@ -170,21 +170,21 @@ golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4=
golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -194,10 +194,10 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -205,13 +205,13 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 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-20190717185122-a985d3407aa7/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 h1:pl8qT5D+48655f14yDURpIZwSPvMWuuekfAP+gxtjvk=
google.golang.org/genproto v0.0.0-20210506142907-4a47615972c2/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210506142907-4a47615972c2/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=

View File

@ -23,6 +23,7 @@ const (
MonoSpaceBegin = 6 MonoSpaceBegin = 6
StrikethroughBegin = 8 StrikethroughBegin = 8
SpoilerBegin = 9 SpoilerBegin = 9
BoldItalicBegin = 10
) )
const EscapeCharacter rune = '\\' const EscapeCharacter rune = '\\'
@ -119,7 +120,14 @@ func (l *TextstyleParser) handleToken(tokenType int, signalCliStylingType string
} else { } else {
if l.tokens.Peek().Token == tokenType { if l.tokens.Peek().Token == tokenType {
tokenBeginState := l.tokens.Pop() tokenBeginState := l.tokens.Pop()
l.signalCliFormatStrings = append(l.signalCliFormatStrings, strconv.Itoa(tokenBeginState.BeginPos)+":"+strconv.Itoa(getUtf16StringLength(l.fullString)-tokenBeginState.BeginPos)+":"+signalCliStylingType) length := getUtf16StringLength(l.fullString) - tokenBeginState.BeginPos
if tokenType == BoldItalicBegin {
l.signalCliFormatStrings = append(l.signalCliFormatStrings, strconv.Itoa(tokenBeginState.BeginPos)+":"+strconv.Itoa(length)+":"+Bold)
l.signalCliFormatStrings = append(l.signalCliFormatStrings, strconv.Itoa(tokenBeginState.BeginPos)+":"+strconv.Itoa(length)+":"+Italic)
} else {
l.signalCliFormatStrings = append(l.signalCliFormatStrings, strconv.Itoa(tokenBeginState.BeginPos)+":"+strconv.Itoa(length)+":"+signalCliStylingType)
}
} else { } else {
l.tokens.Push(TokenState{BeginPos: getUtf16StringLength(l.fullString), Token: tokenType}) l.tokens.Push(TokenState{BeginPos: getUtf16StringLength(l.fullString), Token: tokenType})
} }
@ -137,14 +145,27 @@ func (l *TextstyleParser) Parse() (string, []string) {
nextRune := l.peek() nextRune := l.peek()
if c == '*' { if c == '*' {
if nextRune == '*' { //Bold if nextRune == '*' {
l.next() l.next()
if l.peek() == '*' { // Check for *** before treating it as **
l.next()
if prevChar == EscapeCharacter {
prevChar = c
l.fullString += "***"
continue
}
l.handleToken(BoldItalicBegin, "BOLD_ITALIC")
} else {
if prevChar == EscapeCharacter { if prevChar == EscapeCharacter {
prevChar = c prevChar = c
l.fullString += "**" l.fullString += "**"
continue continue
} }
l.handleToken(BoldBegin, Bold) l.handleToken(BoldBegin, Bold)
}
} else { //Italic } else { //Italic
if prevChar == EscapeCharacter { if prevChar == EscapeCharacter {
prevChar = c prevChar = c

View File

@ -155,5 +155,11 @@ func TestEscapeNew(t *testing.T) {
message, signalCliFormatStrings := textstyleParser.Parse() message, signalCliFormatStrings := textstyleParser.Parse()
expectMessageEqual(t, message, "Test ** * ~ Escape") expectMessageEqual(t, message, "Test ** * ~ Escape")
expectFormatStringsEqual(t, signalCliFormatStrings, []string{}) expectFormatStringsEqual(t, signalCliFormatStrings, []string{})
}
func TestBoldItalic(t *testing.T) {
textstyleParser := NewTextstyleParser("***Bold Italic Text***")
message, signalCliFormatStrings := textstyleParser.Parse()
expectMessageEqual(t, message, "Bold Italic Text")
expectFormatStringsEqual(t, signalCliFormatStrings, []string{"0:16:BOLD", "0:16:ITALIC"})
} }