Compare commits

..

No commits in common. "master" and "0.100-pre" have entirely different histories.

26 changed files with 229 additions and 581 deletions

View File

@ -1,38 +0,0 @@
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,10 +1,8 @@
ARG SIGNAL_CLI_VERSION=0.14.8
ARG LIBSIGNAL_CLIENT_VERSION=0.102.1
ARG SIGNAL_CLI_VERSION=0.14.4.1
ARG LIBSIGNAL_CLIENT_VERSION=0.94.1
ARG SWAG_VERSION=1.16.4
ARG S6_OVERLAY_VERSION=v3.2.2.0
ARG BUILD_VERSION_ARG=unset
FROM golang:1.26-trixie AS buildcontainer
@ -18,7 +16,7 @@ RUN dpkg-reconfigure debconf --frontend=noninteractive \
&& apt-get update \
&& apt-get -y install --no-install-recommends \
wget git locales zip unzip \
file build-essential libz-dev zlib1g-dev binutils \
file build-essential libz-dev zlib1g-dev binutils openjdk-25-jdk \
&& rm -rf /var/lib/apt/lists/*
#COPY ext/libraries/libsignal-client/v${LIBSIGNAL_CLIENT_VERSION} /tmp/libsignal-client-libraries
@ -40,7 +38,16 @@ RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
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}
@ -105,11 +112,13 @@ 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
RUN cd /tmp/signal-cli-rest-api-src/docs \
&& wget https://github.com/AsamK/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}-json-schemas.tar.gz \
&& mkdir signal-cli-schemas \
&& tar xf signal-cli-${SIGNAL_CLI_VERSION}-json-schemas.tar.gz -C signal-cli-schemas \
&& go run add_v1_receive_schemas.go signal-cli-schemas
# (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/signal-cli-rest-api-src/docs \
&& cp -r /tmp/signal-cli-source/build/generated/META-INF/schemas signal-cli-schemas \
&& go run add_v1_receive_schemas.go signal-cli-schemas; \
fi;
# build signal-cli-rest-api
RUN cd /tmp/signal-cli-rest-api-src && go build -o signal-cli-rest-api main.go
@ -122,63 +131,43 @@ RUN cd /tmp/signal-cli-rest-api-src/scripts && go build -o jsonrpc2-helper
RUN cd /tmp/signal-cli-rest-api-src && go build -buildmode=plugin -o signal-cli-rest-api_plugin_loader.so plugin_loader.go
# Start a fresh container for release container
FROM debian:trixie-slim
ARG TARGETARCH # set by buildx
ARG SIGNAL_CLI_VERSION
ARG BUILD_VERSION_ARG
ARG S6_OVERLAY_VERSION
# eclipse-temurin doesn't provide a OpenJDK 21 image for armv7 (see https://github.com/adoptium/containers/issues/502). Until this
# is fixed we use the standard ubuntu image
#FROM eclipse-temurin:21-jre-jammy
FROM ubuntu:noble
ENV GIN_MODE=release
# Set environment variables to keep the image clean
ENV DEBIAN_FRONTEND=noninteractive
ENV PORT=8080
ARG SIGNAL_CLI_VERSION
ARG BUILD_VERSION_ARG
ENV BUILD_VERSION=$BUILD_VERSION_ARG
ENV SIGNAL_CLI_REST_API_PLUGIN_SHARED_OBJ_DIR=/usr/bin/
RUN dpkg-reconfigure debconf --frontend=noninteractive \
&& apt-get update \
&& apt-get install -y --no-install-recommends util-linux openjdk-25-jre curl locales xz-utils \
&& apt-get clean \
&& apt-get install -y --no-install-recommends util-linux supervisor openjdk-25-jre curl locales \
&& rm -rf /var/lib/apt/lists/*
RUN if [ -z "$TARGETARCH" ]; then \
# Fallback for older Docker versions not using BuildKit
TARGETARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/'); \
else \
echo "Building for architecture: $TARGETARCH"; \
fi;
# install s6-overlay as service control system
RUN curl -fL -o /tmp/s6-overlay-noarch.tar.xz \
"https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" && \
tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz && \
if [ "$TARGETARCH" = "amd64" ]; then S6_ARCH="x86_64"; \
elif [ "$TARGETARCH" = "arm64" ]; then S6_ARCH="aarch64"; \
elif [ "$TARGETARCH" = "arm" ]; then S6_ARCH="arm"; \
else S6_ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/'); fi;\
curl -fL -o /tmp/s6-overlay-bin.tar.xz \
"https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" && \
tar -C / -Jxpf /tmp/s6-overlay-bin.tar.xz && \
rm /tmp/s6-overlay-*.tar.xz
COPY --from=buildcontainer /tmp/signal-cli-rest-api-src/signal-cli-rest-api /usr/bin/signal-cli-rest-api
COPY --from=buildcontainer /opt/signal-cli-${SIGNAL_CLI_VERSION} /opt/signal-cli-${SIGNAL_CLI_VERSION}
COPY --from=buildcontainer /tmp/signal-cli-native /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native
COPY --from=buildcontainer /tmp/signal-cli-rest-api-src/scripts/jsonrpc2-helper /usr/bin/jsonrpc2-helper
COPY --from=buildcontainer /tmp/signal-cli-rest-api-src/signal-cli-rest-api_plugin_loader.so /usr/bin/signal-cli-rest-api_plugin_loader.so
COPY entrypoint.sh /entrypoint.sh
RUN groupadd -g 1000 signal-api \
RUN userdel ubuntu -r \
&& groupadd -g 1000 signal-api \
&& useradd --no-log-init -M -d /home -s /bin/bash -u 1000 -g 1000 signal-api \
&& ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli /usr/bin/signal-cli \
&& ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native /usr/bin/signal-cli-native \
&& mkdir -p /home/.local/share/signal-cli \
&& chown -R signal-api:signal-api /home
COPY --chmod=755 ./s6-services/ /etc/s6-overlay/s6-rc.d/
&& mkdir -p /signal-cli-config/ \
&& mkdir -p /home/.local/share/signal-cli
# remove the temporary created signal-cli-native on armv7, as GRAALVM doesn't support 32bit
RUN arch="$(uname -m)"; \
@ -190,24 +179,16 @@ RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales && \
update-locale LANG=en_US.UTF-8
ENV LANG=en_US.UTF-8
ENV LANG en_US.UTF-8
EXPOSE ${PORT}
ENV SIGNAL_CLI_CONFIG_DIR=/home/.local/share/signal-cli
ENV SIGNAL_CLI_UID=1000
ENV SIGNAL_CLI_GID=1000
ENV SIGNAL_CLI_CHOWN_ON_STARTUP=true
RUN mkdir -p /tmp/s6-runtime && chown -R signal-api:signal-api /tmp/s6-runtime /etc/s6-overlay
USER signal-api
# Mandatory ENV for non-root s6
ENV S6_RUNTIME_PATH=/tmp/s6-runtime
ENV S6_READ_ONLY_ROOT=1
ENV S6_VERBOSITY=2
WORKDIR /home
ENTRYPOINT ["/init"]
ENTRYPOINT ["/entrypoint.sh"]
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
CMD curl -f http://localhost:${PORT}/v1/health || exit 1

View File

@ -1,16 +1,11 @@
services:
signal-cli-rest-api:
user: "1000:1000" # change to UID:GID you preferre
image: bbernhard/signal-cli-rest-api:rootless-latest
image: bbernhard/signal-cli-rest-api:latest
environment:
- MODE=normal #supported modes: json-rpc, native, normal
#- AUTO_RECEIVE_SCHEDULE=0 22 * * * #enable this parameter on demand (see description below)
ports:
- "8080:8080" #map docker port 8080 to host port 8080.
volumes:
- "./signal-cli-config:/home/.local/share/signal-cli" #map "signal-cli-config" folder on host system into docker container. the folder contains the password and cryptographic keys when a new number is registered. Make sure that the user has permissions to read and write here.
security_opt:
- no-new-privileges:true # additonal security control
tmpfs:
- /run:exec,size=64m,uid=1000,gid=1000,mode=0755 # Make sure this UID:GID fits those from above
- "./signal-cli-config:/home/.local/share/signal-cli" #map "signal-cli-config" folder on host system into docker container. the folder contains the password and cryptographic keys when a new number is registered

View File

@ -1,43 +0,0 @@
#!/bin/sh
set -x
set -e
[ -z "${SIGNAL_CLI_CONFIG_DIR}" ] && echo "SIGNAL_CLI_CONFIG_DIR environmental variable needs to be set! Aborting!" && exit 1;
usermod -u ${SIGNAL_CLI_UID} signal-api
groupmod -o -g ${SIGNAL_CLI_GID} signal-api
# Fix permissions to ensure backward compatibility if SIGNAL_CLI_CHOWN_ON_STARTUP is not set to "false"
if [ "$SIGNAL_CLI_CHOWN_ON_STARTUP" != "false" ]; then
echo "Changing ownership of ${SIGNAL_CLI_CONFIG_DIR} to ${SIGNAL_CLI_UID}:${SIGNAL_CLI_GID}"
chown ${SIGNAL_CLI_UID}:${SIGNAL_CLI_GID} -R ${SIGNAL_CLI_CONFIG_DIR}
else
echo "Skipping chown on startup since SIGNAL_CLI_CHOWN_ON_STARTUP is set to 'false'"
fi
# Show warning on docker exec
cat <<EOF >> /root/.bashrc
echo "WARNING: signal-cli-rest-api runs as signal-api (not as root!)"
echo "Run 'su signal-api' before using signal-cli!"
echo "If you want to use signal-cli directly, don't forget to specify the config directory. e.g: \"signal-cli --config ${SIGNAL_CLI_CONFIG_DIR}\""
EOF
cap_prefix="-cap_"
caps="$cap_prefix$(seq -s ",$cap_prefix" 0 $(cat /proc/sys/kernel/cap_last_cap))"
# TODO: check mode
if [ "$MODE" = "json-rpc" ]
then
/usr/bin/jsonrpc2-helper
if [ -n "$JAVA_OPTS" ] ; then
echo "export JAVA_OPTS='$JAVA_OPTS'" >> /etc/default/supervisor
fi
service supervisor start
supervisorctl start all
fi
export HOST_IP=$(hostname -I | awk '{print $1}')
# Start API as signal-api user
exec setpriv --reuid=${SIGNAL_CLI_UID} --regid=${SIGNAL_CLI_GID} --init-groups --inh-caps=$caps signal-cli-rest-api -signal-cli-config=${SIGNAL_CLI_CONFIG_DIR}

View File

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

View File

@ -1,3 +0,0 @@
#!/command/with-contenv sh
# Use with-contenv to import environment variables like SIGNAL_CLI_CONFIG_DIR
exec signal-cli-rest-api -signal-cli-config="${SIGNAL_CLI_CONFIG_DIR}"

View File

@ -1 +0,0 @@
longrun

View File

@ -1,10 +0,0 @@
#!/command/with-contenv sh
# File: /etc/s6-overlay/s6-rc.d/signal-json-rpc/run
if [ "$MODE" != "json-rpc" ] && [ "$MODE" != "json-rpc-native" ]; then
echo "Running as mode: $MODE - skipping json-rpc setup"
sleep infinity # do nothing, but keep service running
exit 0
fi
exec jsonrpc2-helper

View File

@ -1 +0,0 @@
longrun

View File

@ -1 +0,0 @@
bundle

View File

@ -173,6 +173,10 @@ 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"`
}
@ -472,6 +476,7 @@ 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 {
@ -484,12 +489,12 @@ func (a *Api) Send(c *gin.Context) {
base64Attachments = append(base64Attachments, req.Base64Attachment)
}
resp, err := a.signalClient.SendV1(req.Number, req.Message, req.Recipients, base64Attachments, req.IsGroup)
timestamp, 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, resp)
c.JSON(201, SendMessageResponse{Timestamp: strconv.FormatInt(timestamp.Timestamp, 10)})
}
// @Summary Send a signal message.
@ -497,7 +502,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} ds.SendMessageResponse
// @Success 201 {object} SendMessageResponse
// @Failure 400 {object} SendMessageError
// @Param data body SendMessageV2 true "Input Data"
// @Router /v2/send [post]
@ -569,7 +574,7 @@ func (a *Api) SendV2(c *gin.Context) {
return
}
c.JSON(201, data)
c.JSON(201, SendMessageResponse{Timestamp: strconv.FormatInt((*data)[0].Timestamp, 10)})
}
func (a *Api) handleSignalReceive(ws *websocket.Conn, number string, stop chan struct{}) {

View File

@ -119,7 +119,6 @@ type GroupEntry struct {
InternalId string `json:"internal_id"`
Members []string `json:"members"`
Blocked bool `json:"blocked"`
Member bool `json:"member"`
PendingInvites []string `json:"pending_invites"`
PendingRequests []string `json:"pending_requests"`
InviteLink string `json:"invite_link"`
@ -144,7 +143,6 @@ type ExpandedGroupEntry struct {
InternalId string `json:"internal_id"`
Members []GroupMember `json:"members"`
Blocked bool `json:"blocked"`
Member bool `json:"member"`
PendingInvites []GroupMember `json:"pending_invites"`
PendingRequests []GroupMember `json:"pending_requests"`
InviteLink string `json:"invite_link"`
@ -479,8 +477,8 @@ func validateLinkPreview(message string, linkPreview *ds.LinkPreviewType) error
return nil
}
func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*ds.SendMessageResponse, error) {
var rawData string
func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*SendResponse, error) {
var resp SendResponse
var linkPreviewAttachmentEntry *AttachmentEntry = nil
if len(signalCliSendRequest.Recipients) == 0 {
@ -615,13 +613,23 @@ func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*ds.S
}
}
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{"--output", "json", "--config", s.signalCliConfig, "-a", signalCliSendRequest.Number, "send", "--message-from-stdin"}
cmd := []string{"--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 {
@ -709,48 +717,23 @@ func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*ds.S
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)
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) {
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
}
}
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
}
@ -916,7 +899,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) (*ds.SendMessageResponse, error) {
func (s *SignalClient) SendV1(number string, message string, recipients []string, base64Attachments []string, isGroup bool) (*SendResponse, error) {
recipientType := ds.Number
if isGroup {
recipientType = ds.Group
@ -925,8 +908,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}
resp, err := s.send(signalCliSendRequest)
return resp, err
timestamp, err := s.send(signalCliSendRequest)
return timestamp, err
}
func (s *SignalClient) getJsonRpc2Client() (*JsonRpc2Client, error) {
@ -946,7 +929,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) (*[]ds.SendMessageResponse, error) {
linkPreview *ds.LinkPreviewType, viewOnce *bool) (*[]SendResponse, error) {
if len(recps) == 0 {
return nil, errors.New("Please provide at least one recipient")
}
@ -992,17 +975,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.")
}
responses := []ds.SendMessageResponse{}
timestamps := []SendResponse{}
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}
resp, err := s.send(signalCliSendRequest)
timestamp, err := s.send(signalCliSendRequest)
if err != nil {
return nil, err
}
responses = append(responses, *resp)
timestamps = append(timestamps, *timestamp)
}
if len(numbers) > 0 {
@ -1010,11 +993,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}
resp, err := s.send(signalCliSendRequest)
timestamp, err := s.send(signalCliSendRequest)
if err != nil {
return nil, err
}
responses = append(responses, *resp)
timestamps = append(timestamps, *timestamp)
}
if len(usernames) > 0 {
@ -1022,14 +1005,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}
resp, err := s.send(signalCliSendRequest)
timestamp, err := s.send(signalCliSendRequest)
if err != nil {
return nil, err
}
responses = append(responses, *resp)
timestamps = append(timestamps, *timestamp)
}
return &responses, nil
return &timestamps, nil
}
func (s *SignalClient) Receive(number string, timeout int64, ignoreAttachments bool, ignoreStories bool, ignoreAvatars bool, ignoreStickers bool, maxMessages int64, sendReadReceipts bool) (string, error) {
@ -1347,25 +1330,6 @@ func (s *SignalClient) RemoveAdminsFromGroup(number string, groupId string, admi
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) {
groupEntries := []ExpandedGroupEntry{}
@ -1396,7 +1360,22 @@ func (s *SignalClient) GetGroupsExpanded(number string) ([]ExpandedGroupEntry, e
}
for _, signalCliGroupEntry := range signalCliGroupEntries {
groupEntries = append(groupEntries, signalCliGroupEntryToExpandedGroupEntry(signalCliGroupEntry))
var groupEntry ExpandedGroupEntry
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
@ -1411,7 +1390,7 @@ func (s *SignalClient) GetGroups(number string) ([]GroupEntry, error) {
groupEntries := []GroupEntry{}
for _, expandedGroupEntry := range expandedGroupEntries {
groupEntry := GroupEntry{InternalId: expandedGroupEntry.InternalId, Name: expandedGroupEntry.Name,
Id: expandedGroupEntry.Id, Blocked: expandedGroupEntry.Blocked, Member: expandedGroupEntry.Member, Description: expandedGroupEntry.Description,
Id: expandedGroupEntry.Id, Blocked: expandedGroupEntry.Blocked, Description: expandedGroupEntry.Description,
Permissions: expandedGroupEntry.Permissions, InviteLink: expandedGroupEntry.InviteLink}
members := []string{}

View File

@ -1,93 +0,0 @@
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,8 +186,6 @@ func (r *JsonRpc2Client) getRaw(command string, account *string, args interface{
ChallengeTokens: challengeTokens,
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)
}

View File

@ -58,19 +58,3 @@ 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

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

View File

@ -1,24 +0,0 @@
#!/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

@ -372,6 +372,17 @@ const docTemplate = `{
],
"type": "object"
},
"api.SendMessageResponse": {
"properties": {
"timestamp": {
"type": "string"
}
},
"required": [
"timestamp"
],
"type": "object"
},
"api.SendMessageV1": {
"properties": {
"base64_attachment": {
@ -795,9 +806,6 @@ const docTemplate = `{
"invite_link": {
"type": "string"
},
"member": {
"type": "boolean"
},
"members": {
"items": {
"type": "string"
@ -830,7 +838,6 @@ const docTemplate = `{
"id",
"internal_id",
"invite_link",
"member",
"members",
"name",
"pending_invites",
@ -1097,48 +1104,6 @@ const docTemplate = `{
],
"type": "object"
},
"data.SendMessageError": {
"properties": {
"number": {
"type": "string"
},
"reason": {
"type": "string"
},
"username": {
"type": "string"
},
"uuid": {
"type": "string"
}
},
"type": "object"
},
"data.SendMessageErrors": {
"properties": {
"recipients": {
"items": {
"$ref": "#/definitions/data.SendMessageError"
},
"type": "array"
}
},
"type": "object"
},
"data.SendMessageResponse": {
"properties": {
"errors": {
"$ref": "#/definitions/data.SendMessageErrors"
},
"timestamp": {
"type": "string"
}
},
"required": [
"timestamp"
],
"type": "object"
},
"receive.AdminDelete": {
"properties": {
"targetAuthor": {
@ -5066,7 +5031,7 @@ const docTemplate = `{
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/data.SendMessageResponse"
"$ref": "#/definitions/api.SendMessageResponse"
}
},
"400": {

View File

@ -367,6 +367,17 @@
],
"type": "object"
},
"api.SendMessageResponse": {
"properties": {
"timestamp": {
"type": "string"
}
},
"required": [
"timestamp"
],
"type": "object"
},
"api.SendMessageV1": {
"properties": {
"base64_attachment": {
@ -790,9 +801,6 @@
"invite_link": {
"type": "string"
},
"member": {
"type": "boolean"
},
"members": {
"items": {
"type": "string"
@ -825,7 +833,6 @@
"id",
"internal_id",
"invite_link",
"member",
"members",
"name",
"pending_invites",
@ -1092,48 +1099,6 @@
],
"type": "object"
},
"data.SendMessageError": {
"properties": {
"number": {
"type": "string"
},
"reason": {
"type": "string"
},
"username": {
"type": "string"
},
"uuid": {
"type": "string"
}
},
"type": "object"
},
"data.SendMessageErrors": {
"properties": {
"recipients": {
"items": {
"$ref": "#/definitions/data.SendMessageError"
},
"type": "array"
}
},
"type": "object"
},
"data.SendMessageResponse": {
"properties": {
"errors": {
"$ref": "#/definitions/data.SendMessageErrors"
},
"timestamp": {
"type": "string"
}
},
"required": [
"timestamp"
],
"type": "object"
},
"receive.AdminDelete": {
"properties": {
"targetAuthor": {
@ -5061,7 +5026,7 @@
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/data.SendMessageResponse"
"$ref": "#/definitions/api.SendMessageResponse"
}
},
"400": {

View File

@ -1,6 +1,6 @@
module github.com/bbernhard/signal-cli-rest-api
go 1.25.0
go 1.24.0
require (
github.com/bbernhard/gluasql v0.2.0
@ -9,7 +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/gorilla/websocket v1.5.3
github.com/gorilla/websocket v1.5.0
github.com/h2non/filetype v1.1.3
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.1
@ -57,11 +57,11 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.14.0 // indirect
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/tools v0.48.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/protobuf v1.36.4 // 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/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
@ -170,27 +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/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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
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-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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -200,13 +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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4=
golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE=
golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 h1:RJhm5l6Fo4rmEIcndxDllNhhf/fAx8qIm4t6A7vpm2A=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -214,17 +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.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.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
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.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/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
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/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=

View File

@ -1,15 +1,32 @@
package main
import (
"os"
"strconv"
"fmt"
"io/ioutil"
"os/exec"
"strings"
"syscall"
"github.com/bbernhard/signal-cli-rest-api/utils"
log "github.com/sirupsen/logrus"
)
const supervisorctlConfigTemplate = `
[program:%s]
process_name=%s
command=%s --output=json --config %s%s daemon %s%s%s%s --tcp 127.0.0.1:%d
autostart=true
autorestart=true
startretries=10
user=signal-api
directory=/usr/bin/
redirect_stderr=true
stdout_logfile=/var/log/%s/out.log
stderr_logfile=/var/log/%s/err.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
numprocs=1
`
func main() {
signalCliConfigDir := "/home/.local/share/signal-cli/"
signalCliConfigDirEnv := utils.GetEnv("SIGNAL_CLI_CONFIG_DIR", "")
@ -23,9 +40,8 @@ func main() {
jsonRpc2ClientConfig := utils.NewJsonRpc2ClientConfig()
var tcpPort int64 = 6001
jsonRpc2ClientConfig.AddEntry(utils.MULTI_ACCOUNT_NUMBER, utils.JsonRpc2ClientConfigEntry{TcpPort: tcpPort})
args := []string{"--output=json", "--config", signalCliConfigDir}
jsonRpc2ClientConfig.AddEntry(utils.MULTI_ACCOUNT_NUMBER, utils.JsonRpc2ClientConfigEntry{TcpPort: tcpPort})
signalCliBinary := "signal-cli"
signalMode := utils.GetEnv("MODE", "json-rpc")
@ -35,53 +51,67 @@ func main() {
log.Fatal("The mode needs to be either 'json-rpc' or 'json-rpc-native'")
}
signalCliIgnoreAttachments := ""
ignoreAttachments := utils.GetEnv("JSON_RPC_IGNORE_ATTACHMENTS", "")
if ignoreAttachments == "true" {
signalCliIgnoreAttachments = " --ignore-attachments"
}
signalCliIgnoreStories := ""
ignoreStories := utils.GetEnv("JSON_RPC_IGNORE_STORIES", "")
if ignoreStories == "true" {
signalCliIgnoreStories = " --ignore-stories"
}
signalCliIgnoreAvatars := ""
ignoreAvatars := utils.GetEnv("JSON_RPC_IGNORE_AVATARS", "")
if ignoreAvatars == "true" {
signalCliIgnoreAvatars = " --ignore-avatars"
}
signalCliIgnoreStickers := ""
ignoreStickers := utils.GetEnv("JSON_RPC_IGNORE_STICKERS", "")
if ignoreStickers == "true" {
signalCliIgnoreStickers = " --ignore-stickers"
}
supervisorctlProgramName := "signal-cli-json-rpc-1"
supervisorctlLogFolder := "/var/log/" + supervisorctlProgramName
_, err := exec.Command("mkdir", "-p", supervisorctlLogFolder).Output()
if err != nil {
log.Fatal("Couldn't create log folder ", supervisorctlLogFolder, ": ", err.Error())
}
trustNewIdentities := ""
trustNewIdentitiesEnv := utils.GetEnv("JSON_RPC_TRUST_NEW_IDENTITIES", "")
if trustNewIdentitiesEnv == "on-first-use" {
args = append(args, []string{"--trust-new-identities", "on-first-use"}...)
trustNewIdentities = " --trust-new-identities on-first-use"
} else if trustNewIdentitiesEnv == "always" {
args = append(args, []string{"--trust-new-identities", "always"}...)
trustNewIdentities = " --trust-new-identities always"
} else if trustNewIdentitiesEnv == "never" {
args = append(args, []string{"--trust-new-identities", "never"}...)
trustNewIdentities = " --trust-new-identities never"
} else if trustNewIdentitiesEnv != "" {
log.Fatal("Invalid JSON_RPC_TRUST_NEW_IDENTITIES environment variable set!")
}
args = append(args, "daemon")
log.Info("Updated jsonrpc2.yml")
ignoreStories := utils.GetEnv("JSON_RPC_IGNORE_STORIES", "")
if ignoreStories == "true" {
args = append(args, "--ignore-stories")
//write supervisorctl config
supervisorctlConfigFilename := "/etc/supervisor/conf.d/" + "signal-cli-json-rpc-1.conf"
supervisorctlConfig := fmt.Sprintf(supervisorctlConfigTemplate, supervisorctlProgramName, supervisorctlProgramName, signalCliBinary,
signalCliConfigDir, trustNewIdentities, signalCliIgnoreAttachments, signalCliIgnoreStories,
signalCliIgnoreAvatars, signalCliIgnoreStickers, tcpPort,
supervisorctlProgramName, supervisorctlProgramName)
err = ioutil.WriteFile(supervisorctlConfigFilename, []byte(supervisorctlConfig), 0644)
if err != nil {
log.Fatal("Couldn't write ", supervisorctlConfigFilename, ": ", err.Error())
}
ignoreAvatars := utils.GetEnv("JSON_RPC_IGNORE_AVATARS", "")
if ignoreAvatars == "true" {
args = append(args, "--ignore-avatars")
}
ignoreStickers := utils.GetEnv("JSON_RPC_IGNORE_STICKERS", "")
if ignoreStickers == "true" {
args = append(args, "--ignore-stickers")
}
ignoreAttachments := utils.GetEnv("JSON_RPC_IGNORE_ATTACHMENTS", "")
if ignoreAttachments == "true" {
args = append(args, "--ignore-attachments")
}
args = append(args, []string{"--tcp", "127.0.0.1:" + strconv.FormatInt(tcpPort, 10)}...)
// write jsonrpc.yml config file
err := jsonRpc2ClientConfig.Persist(signalCliConfigDir + "jsonrpc2.yml")
err = jsonRpc2ClientConfig.Persist(signalCliConfigDir + "jsonrpc2.yml")
if err != nil {
log.Fatal("Couldn't persist jsonrpc2.yaml: ", err.Error())
}
log.Info("Updated jsonrpc2.yml")
env := os.Environ()
err = syscall.Exec("/usr/bin/"+signalCliBinary, args, env)
if err != nil {
log.Fatal("Couldn't start signal-cli in json-rpc mode: ", err.Error())
}
}

View File

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

View File

@ -155,11 +155,5 @@ func TestEscapeNew(t *testing.T) {
message, signalCliFormatStrings := textstyleParser.Parse()
expectMessageEqual(t, message, "Test ** * ~ Escape")
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"})
}