Compare commits

..

33 Commits

Author SHA1 Message Date
Bernhard B
cbefcb2ecd Merge branch 'master' into rootless_s6 2026-03-07 23:22:39 +01:00
Bernhard B.
76511ae14f
Merge pull request #809 from bexelbie/feature/ignore-avatars-stickers
Add --ignore-avatars and --ignore-stickers support
2026-03-07 23:17:23 +01:00
Bernhard B.
fcc62a7fe7
Merge branch 'master' into feature/ignore-avatars-stickers 2026-03-07 23:17:01 +01:00
Bernhard B
f1c7525292 Merge branch 'master' into rootless_s6 2026-03-07 23:07:23 +01:00
Bernhard B
05235bd7ae fixed docker-compose.yml
* removed accidental debug commit
2026-03-07 23:01:40 +01:00
Bernhard B
d7ffe54883 removed debug logging 2026-03-07 22:58:57 +01:00
Brian (bex) Exelbierd
2760fe0b70 add --ignore-avatars and --ignore-stickers support
Expose the new signal-cli v0.14.0 --ignore-avatars and --ignore-stickers
flags across all modes:

* JSON-RPC mode: JSON_RPC_IGNORE_AVATARS and JSON_RPC_IGNORE_STICKERS
  environment variables
* Normal/Native mode: ignore_avatars and ignore_stickers query parameters
  on the /v1/receive endpoint
* Auto-receive scheduler: AUTO_RECEIVE_SCHEDULE_IGNORE_AVATARS and
  AUTO_RECEIVE_SCHEDULE_IGNORE_STICKERS environment variables

Follows the existing pattern of --ignore-attachments and --ignore-stories.

Refs #776, #723

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-07 22:52:26 +01:00
Bernhard B
4fb1be6657 removed fifo pathname from json-rpc config
* not needed anymore
2026-03-07 21:17:45 +01:00
Bernhard B
bb8c0efcac changes signal-json-rpc run script
* switched to signal-cli daemon mode - this way we don't need to
  pipe signal-cli's output to netcat.

* removed fifo pathname from the json-rpc config (not needed anymore)
2026-03-07 21:08:18 +01:00
Bernhard B
1543997e02 set S6_OVERLAY_VERSION in target container 2026-03-07 17:02:54 +01:00
Bernhard B
b527cba584 Merge branch 'master' into rootless_s6 2026-03-07 16:55:15 +01:00
Bernhard B
877bc9e845 remove accidental commit 2026-03-07 16:53:27 +01:00
Bernhard B
808bad21ab Merge branch 'master' into rootless_s6 2026-03-07 16:52:50 +01:00
Bernhard B.
53cd2dc014
Merge pull request #798 from poggenpower/non-root
non-root container with s6-overlay
2026-03-07 16:16:21 +01:00
Bernhard B
ddc5aa55df remove netcat-openbsd
* not needed anymore
2026-03-07 14:08:09 +01:00
Bernhard B
00bbfca878 updated signal-cli-native to 0.14.0+5 2026-03-07 14:08:09 +01:00
Bernhard B.
884582a71e
Merge pull request #807 from EvanHahn/debug-log-level-env-variable-docs
Mention `LOG_LEVEL` environment variable in debug docs
2026-03-06 17:38:45 +01:00
Evan Hahn
e3d503c746 Mention LOG_LEVEL environment variable in debug docs
This updates the debug docs to mention the `LOG_LEVEL` environment
variable.
2026-03-06 08:13:13 -06:00
Bernhard B
af34a0881c fixed small bug in json-rpc reconnect mechanism
* after we successfully reconnected, wait for new data
2026-03-05 20:50:42 +01:00
Bernhard B
c7cb9ab13e improved json-rpc reconnection logic
* when the connection the signal-cli daemon is lost in json-rpc mode,
  the open connection will be closed and a new connection attempt will
  be made. If after 15 connection attempts we are unable to connect to
  the signal-cli daemon, we give up and abort.
2026-03-05 20:43:22 +01:00
Bernhard B
af18c7aea8 switched to signal-cli daemon mode
* this gets rid off the ugly netcat workaround and should improve
  the general stability of the connection to the signal-cli binary
  in json-rpc mode.
2026-03-05 20:41:17 +01:00
Thomas Laubrock
ab30ba5bba add GIN_MODE, moved S6_OVERLAY_VERSION to the top 2026-03-04 21:18:31 +00:00
Bernhard B
ed3626fa77 removed "apt-key add"
* "apt-key add" was removed from Debian, since it was insecure.
2026-03-04 21:36:34 +01:00
Bernhard B
6257fa754d updated buildcontainer to golang:1.26-trixie 2026-03-04 20:15:18 +01:00
Bernhard B
dd6d763618 switch back to old buildcontainer image 2026-03-04 17:55:31 +01:00
Bernhard B
1ea89705d5 updated signal-cli-native to v0.14.0 2026-03-04 16:33:45 +01:00
Bernhard B
9e5d73b5c0 temporarily switch back to signal-cli-native v0.13.24 2026-03-03 22:08:00 +01:00
Bernhard B.
d080e8d478
Merge pull request #793 from revilo951/master
Swap PendingRequests and PendingInvites assignments
2026-03-03 21:25:24 +01:00
Bernhard B
a9c367a5b1 updated golang buildcontainer 2026-03-03 21:16:20 +01:00
Bernhard B
8d13f5f383 updated signal-cli-native version 2026-03-02 17:38:47 +01:00
Bernhard B
bd9e648739 updated signal-cli to v0.14.0 2026-03-01 22:37:52 +01:00
Thomas Laubrock
24513e9cf3 non-root container with s6-overlay
This using s6-overlay to manage processes need to run in the container.
jsonrpc2-helper is migrated into the startscript.
2026-02-27 13:40:58 +00:00
revilo951
3a36a04b09
Swap PendingRequests and PendingInvites assignments
Fix for https://github.com/bbernhard/signal-cli-rest-api/issues/792
2026-02-23 11:00:15 +11:00
25 changed files with 303 additions and 176 deletions

View File

@ -1,14 +1,15 @@
ARG SIGNAL_CLI_VERSION=0.13.24
ARG LIBSIGNAL_CLIENT_VERSION=0.87.0
ARG SIGNAL_CLI_NATIVE_PACKAGE_VERSION=0.13.24+morph027+2
ARG SIGNAL_CLI_VERSION=0.14.0
ARG LIBSIGNAL_CLIENT_VERSION=0.87.4
ARG SIGNAL_CLI_NATIVE_PACKAGE_VERSION=0.14.0+morph027+5
ARG SWAG_VERSION=1.16.4
ARG GRAALVM_VERSION=21.0.0
#ARG GRAALVM_VERSION=25.0.2
ARG GRAALVM_VERSION=25.0.2
ARG S6_OVERLAY_VERSION=v3.2.2.0
ARG BUILD_VERSION_ARG=unset
FROM golang:1.24-bookworm AS buildcontainer
FROM golang:1.26-trixie AS buildcontainer
ARG SIGNAL_CLI_VERSION
ARG LIBSIGNAL_CLIENT_VERSION
@ -32,8 +33,8 @@ RUN arch="$(uname -m)"; \
RUN dpkg-reconfigure debconf --frontend=noninteractive \
&& apt-get update \
&& apt-get -y install --no-install-recommends \
wget software-properties-common git locales zip unzip \
file build-essential libz-dev zlib1g-dev \
wget git locales zip unzip \
file build-essential libz-dev zlib1g-dev binutils \
&& rm -rf /var/lib/apt/lists/*
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
@ -42,7 +43,7 @@ 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
#RUN cd /tmp/ \
# && git clone https://github.com/swaggo/swag.git swag-${SWAG_VERSION} \
@ -91,8 +92,8 @@ RUN if [ "$(uname -m)" = "x86_64" ]; then \
&& ./gradlew -q nativeCompile; \
elif [ "$(uname -m)" = "aarch64" ] ; then \
echo "Use native image from @morph027 (https://packaging.gitlab.io/signal-cli/) for arm64 - many thanks to @morph027" \
&& curl -fsSL https://packaging.gitlab.io/signal-cli/gpg.key | apt-key add - \
&& echo "deb https://packaging.gitlab.io/signal-cli focal main" > /etc/apt/sources.list.d/morph027-signal-cli.list \
&& curl -fsSL https://packaging.gitlab.io/signal-cli/gpg.key | gpg -o /usr/share/keyrings/signal-cli-native.pgp --dearmor \
&& echo "deb [signed-by=/usr/share/keyrings/signal-cli-native.pgp] https://packaging.gitlab.io/signal-cli signalcli main" > /etc/apt/sources.list.d/morph027-signal-cli.list \
&& mkdir -p /tmp/signal-cli-native \
&& cd /tmp/signal-cli-native \
#&& wget https://gitlab.com/packaging/signal-cli/-/jobs/3716873649/artifacts/download?file_type=archive -O /tmp/signal-cli-native/archive.zip \
@ -154,42 +155,63 @@ 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
# 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:jammy
ENV GIN_MODE=release
ENV PORT=8080
ARG TARGETARCH # set by buildx
ARG SIGNAL_CLI_VERSION
ARG BUILD_VERSION_ARG
ARG S6_OVERLAY_VERSION
ENV GIN_MODE=release
# Set environment variables to keep the image clean
ENV DEBIAN_FRONTEND=noninteractive
ENV PORT=8080
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 supervisor netcat openjdk-21-jre curl locales \
&& apt-get install -y --no-install-recommends util-linux openjdk-25-jre curl locales xz-utils \
&& apt-get clean \
&& 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-${SIGNAL_CLI_VERSION}-source/build/native/nativeCompile/signal-cli /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 \
&& 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 /signal-cli-config/ \
&& mkdir -p /home/.local/share/signal-cli
&& 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/
# remove the temporary created signal-cli-native on armv7, as GRAALVM doesn't support 32bit
RUN arch="$(uname -m)"; \
@ -201,16 +223,24 @@ 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
ENTRYPOINT ["/entrypoint.sh"]
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"]
HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
CMD curl -f http://localhost:${PORT}/v1/health || exit 1

View File

@ -156,4 +156,6 @@ There are a bunch of environmental variables that can be set inside the docker c
* `JSON_RPC_IGNORE_ATTACHMENTS`: When set to `true`, attachments are not automatically downloaded in json-rpc mode (default: `false`)
* `JSON_RPC_IGNORE_STORIES`: When set to `true`, stories are not automatically downloaded in json-rpc mode (default: `false`)
* `JSON_RPC_IGNORE_AVATARS`: When set to `true`, avatars are not automatically downloaded in json-rpc mode (default: `false`)
* `JSON_RPC_IGNORE_STICKERS`: When set to `true`, sticker packs are not automatically downloaded in json-rpc mode (default: `false`)
* `JSON_RPC_TRUST_NEW_IDENTITIES`: Choose how to trust new identities in json-rpc mode. Supported values: `on-first-use`, `always`, `never`. (default: `on-first-use`)

View File

@ -4,6 +4,8 @@ This can be done by putting the docker container into debug mode with the follow
```curl -X POST -H "Content-Type: application/json" -d '{"logging": {"level": "debug"}}' 'http://127.0.0.1:8080/v1/configuration'```
Alternatively, you can set the `LOG_LEVEL` environment variable.
Once the docker container is in debug mode, execute the REST API command you want to debug.
e.g Let's assume we are experiencing some problems with sending messages. So, let's send a Signal message with

View File

@ -1,4 +1,3 @@
version: "3"
services:
signal-cli-rest-api:
image: bbernhard/signal-cli-rest-api:latest
@ -9,3 +8,4 @@ services:
- "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

43
entrypoint.old.sh Executable file
View File

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

@ -1,23 +1,22 @@
diff --git a/build.gradle.kts b/build.gradle.kts
index f51d9f1c..6357f590 100644
index d8e36ea4..dba4dae3 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -55,6 +55,7 @@ dependencies {
implementation(libs.slf4j.jul)
@@ -91,6 +91,7 @@ dependencies {
implementation(libs.logback)
implementation(libs.zxing)
implementation(project(":libsignal-cli"))
+ implementation(files("/tmp/libsignal-client.jar"))
+ implementation(files("/tmp/libsignal-client.jar"))
}
configurations {
@@ -63,6 +64,10 @@ configurations {
@@ -99,6 +100,9 @@ configurations {
}
}
+configurations.all {
+ exclude(group = "org.signal", module = "libsignal-client")
+ exclude(group = "org.signal", module = "libsignal-client")
+}
+
tasks.withType<AbstractArchiveTask>().configureEach {
isPreserveFileTimestamps = false

View File

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

@ -0,0 +1 @@
longrun

View File

@ -0,0 +1,10 @@
#!/command/with-contenv sh
# File: /etc/s6-overlay/s6-rc.d/signal-json-rpc/run
if [ "$MODE" != "json-rpc" ]; 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

@ -0,0 +1 @@
longrun

View File

1
s6-services/user/type Normal file
View File

@ -0,0 +1 @@
bundle

View File

@ -215,7 +215,7 @@ type RemoteDeleteRequest struct {
}
type DeleteLocalAccountDataRequest struct {
IgnoreRegistered bool `json:"ignore_registered" example:"false"`
IgnoreRegistered bool `json:"ignore_registered" example:"false"`
}
type DeviceLinkUriResponse struct {
@ -364,30 +364,30 @@ func (a *Api) UnregisterNumber(c *gin.Context) {
// @Failure 400 {object} Error
// @Router /v1/devices/{number}/local-data [delete]
func (a *Api) DeleteLocalAccountData(c *gin.Context) {
number, err := url.PathUnescape(c.Param("number"))
if err != nil {
c.JSON(400, Error{Msg: "Couldn't process request - malformed number"})
return
}
if number == "" {
c.JSON(400, Error{Msg: "Couldn't process request - number missing"})
return
}
number, err := url.PathUnescape(c.Param("number"))
if err != nil {
c.JSON(400, Error{Msg: "Couldn't process request - malformed number"})
return
}
if number == "" {
c.JSON(400, Error{Msg: "Couldn't process request - number missing"})
return
}
req := DeleteLocalAccountDataRequest{}
if c.Request.Body != nil && c.Request.ContentLength != 0 {
if err := c.BindJSON(&req); err != nil {
c.JSON(400, Error{Msg: "Couldn't process request - invalid request"})
return
}
}
req := DeleteLocalAccountDataRequest{}
if c.Request.Body != nil && c.Request.ContentLength != 0 {
if err := c.BindJSON(&req); err != nil {
c.JSON(400, Error{Msg: "Couldn't process request - invalid request"})
return
}
}
if err := a.signalClient.DeleteLocalAccountData(number, req.IgnoreRegistered); err != nil {
c.JSON(400, Error{Msg: err.Error()})
return
}
if err := a.signalClient.DeleteLocalAccountData(number, req.IgnoreRegistered); err != nil {
c.JSON(400, Error{Msg: err.Error()})
return
}
c.Status(http.StatusNoContent)
c.Status(http.StatusNoContent)
}
// @Summary Verify a registered phone number.
@ -670,6 +670,8 @@ func StringToBool(input string) bool {
// @Param timeout query string false "Receive timeout in seconds (default: 1)"
// @Param ignore_attachments query string false "Specify whether the attachments of the received message should be ignored" (default: false)"
// @Param ignore_stories query string false "Specify whether stories should be ignored when receiving messages" (default: false)"
// @Param ignore_avatars query string false "Specify whether avatar downloads should be ignored when receiving messages" (default: false)"
// @Param ignore_stickers query string false "Specify whether sticker pack downloads should be ignored when receiving messages" (default: false)"
// @Param max_messages query string false "Specify the maximum number of messages to receive (default: unlimited)". Not available in json-rpc mode.
// @Param send_read_receipts query string false "Specify whether read receipts should be sent when receiving messages" (default: false)"
// @Router /v1/receive/{number} [get]
@ -718,13 +720,25 @@ func (a *Api) Receive(c *gin.Context) {
return
}
ignoreAvatars := c.DefaultQuery("ignore_avatars", "false")
if ignoreAvatars != "true" && ignoreAvatars != "false" {
c.JSON(400, Error{Msg: "Couldn't process request - ignore_avatars parameter needs to be either 'true' or 'false'"})
return
}
ignoreStickers := c.DefaultQuery("ignore_stickers", "false")
if ignoreStickers != "true" && ignoreStickers != "false" {
c.JSON(400, Error{Msg: "Couldn't process request - ignore_stickers parameter needs to be either 'true' or 'false'"})
return
}
sendReadReceipts := c.DefaultQuery("send_read_receipts", "false")
if sendReadReceipts != "true" && sendReadReceipts != "false" {
c.JSON(400, Error{Msg: "Couldn't process request - send_read_receipts parameter needs to be either 'true' or 'false'"})
return
}
jsonStr, err := a.signalClient.Receive(number, timeoutInt, StringToBool(ignoreAttachments), StringToBool(ignoreStories), maxMessagesInt, StringToBool(sendReadReceipts))
jsonStr, err := a.signalClient.Receive(number, timeoutInt, StringToBool(ignoreAttachments), StringToBool(ignoreStories), StringToBool(ignoreAvatars), StringToBool(ignoreStickers), maxMessagesInt, StringToBool(sendReadReceipts))
if err != nil {
c.JSON(400, Error{Msg: err.Error()})
return
@ -1174,19 +1188,19 @@ func (a *Api) GetQrCodeLink(c *gin.Context) {
// @Failure 400 {object} Error
// @Router /v1/qrcodelink/raw [get]
func (a *Api) GetQrCodeLinkUri(c *gin.Context) {
deviceName := c.Query("device_name")
if deviceName == "" {
c.JSON(400, Error{Msg: "Please provide a name for the device"})
return
}
deviceName := c.Query("device_name")
if deviceName == "" {
c.JSON(400, Error{Msg: "Please provide a name for the device"})
return
}
deviceLinkUri, err := a.signalClient.GetDeviceLinkUri(deviceName)
if err != nil {
c.JSON(400, Error{Msg: err.Error()})
return
}
deviceLinkUri, err := a.signalClient.GetDeviceLinkUri(deviceName)
if err != nil {
c.JSON(400, Error{Msg: err.Error()})
return
}
c.JSON(200, DeviceLinkUriResponse{DeviceLinkUri: deviceLinkUri})
c.JSON(200, DeviceLinkUriResponse{DeviceLinkUri: deviceLinkUri})
}
// @Summary List all accounts
@ -2080,7 +2094,7 @@ func (a *Api) RemoveDevice(c *gin.Context) {
}
deviceIdStr := c.Param("deviceId")
deviceId, err := strconv.ParseInt(deviceIdStr, 10, 64)
deviceId, err := strconv.ParseInt(deviceIdStr, 10, 64)
if err != nil {
c.JSON(400, Error{Msg: "deviceId must be numeric"})
return

View File

@ -410,7 +410,7 @@ func (s *SignalClient) GetSignalCliMode() SignalCliMode {
return s.signalCliMode
}
func (s *SignalClient) Init() error {
func (s *SignalClient) Init(maxRetries int) error {
s.signalCliApiConfig = utils.NewSignalCliApiConfig()
err := s.signalCliApiConfig.Load(s.signalCliApiConfigPath)
if err != nil {
@ -427,7 +427,7 @@ func (s *SignalClient) Init() error {
tcpPortsNumberMapping := s.jsonRpc2ClientConfig.GetTcpPortsForNumbers()
for number, tcpPort := range tcpPortsNumberMapping {
s.jsonRpc2Clients[number] = NewJsonRpc2Client(s.signalCliApiConfig, number)
err := s.jsonRpc2Clients[number].Dial("127.0.0.1:" + strconv.FormatInt(tcpPort, 10))
err := s.jsonRpc2Clients[number].Dial("127.0.0.1:"+strconv.FormatInt(tcpPort, 10), maxRetries)
if err != nil {
return err
}
@ -993,7 +993,7 @@ func (s *SignalClient) SendV2(number string, message string, recps []string, bas
return &timestamps, nil
}
func (s *SignalClient) Receive(number string, timeout int64, ignoreAttachments bool, ignoreStories 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) {
if s.signalCliMode == JsonRpc {
return "", errors.New("Not implemented")
} else {
@ -1007,6 +1007,14 @@ func (s *SignalClient) Receive(number string, timeout int64, ignoreAttachments b
command = append(command, "--ignore-stories")
}
if ignoreAvatars {
command = append(command, "--ignore-avatars")
}
if ignoreStickers {
command = append(command, "--ignore-stickers")
}
if maxMessages > 0 {
command = append(command, "--max-messages")
command = append(command, strconv.FormatInt(maxMessages, 10))
@ -1349,7 +1357,7 @@ func (s *SignalClient) GetGroups(number string) ([]GroupEntry, error) {
}
pendingMembers = append(pendingMembers, identifier)
}
groupEntry.PendingRequests = pendingMembers
groupEntry.PendingInvites = pendingMembers
requestingMembers := []string{}
for _, val := range signalCliGroupEntry.RequestingMembers {
@ -1359,7 +1367,7 @@ func (s *SignalClient) GetGroups(number string) ([]GroupEntry, error) {
}
requestingMembers = append(requestingMembers, identifier)
}
groupEntry.PendingInvites = requestingMembers
groupEntry.PendingRequests = requestingMembers
admins := []string{}
for _, val := range signalCliGroupEntry.Admins {
@ -2634,8 +2642,8 @@ func (s *SignalClient) ListContacts(number string, allRecipients bool, recipient
if s.signalCliMode == JsonRpc {
type Request struct {
AllRecipients bool `json:"allRecipients,omitempty"`
Recipient string `json:"recipient,omitempty"`
AllRecipients bool `json:"allRecipients,omitempty"`
Recipient string `json:"recipient,omitempty"`
}
req := Request{}
if allRecipients {
@ -2705,7 +2713,6 @@ func (s *SignalClient) ListContacts(number string, allRecipients bool, recipient
return resp, nil
}
func (s *SignalClient) SetPin(number string, registrationLockPin string) error {
if s.signalCliMode == JsonRpc {
type Request struct {

View File

@ -2,14 +2,14 @@ package client
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"net"
"net/http"
"strconv"
"sync"
"time"
"net/http"
"bytes"
"strconv"
"github.com/bbernhard/signal-cli-rest-api/utils"
uuid "github.com/gofrs/uuid"
@ -60,11 +60,11 @@ type JsonRpc2Client struct {
conn net.Conn
receivedResponsesById map[string]chan JsonRpc2MessageResponse
receivedMessagesChannels map[string]chan JsonRpc2ReceivedMessage
lastTimeErrorMessageSent time.Time
signalCliApiConfig *utils.SignalCliApiConfig
number string
receivedMessagesMutex sync.Mutex
receivedResponsesMutex sync.Mutex
address string
}
func NewJsonRpc2Client(signalCliApiConfig *utils.SignalCliApiConfig, number string) *JsonRpc2Client {
@ -76,10 +76,24 @@ func NewJsonRpc2Client(signalCliApiConfig *utils.SignalCliApiConfig, number stri
}
}
func (r *JsonRpc2Client) Dial(address string) error {
func (r *JsonRpc2Client) Dial(address string, maxRetries int) error {
var err error
r.conn, err = net.Dial("tcp", address)
if err != nil {
r.address = address
connected := false
for i := 0; i < maxRetries; i++ {
r.conn, err = net.Dial("tcp", address)
if err != nil {
log.Info("Waiting for signal-cli to start up in daemon mode...")
time.Sleep(2 * time.Second)
continue
}
connected = true
log.Info("Successfully connected to signal-cli in daemon mode")
break
}
if !connected {
return err
}
@ -207,11 +221,14 @@ func (r *JsonRpc2Client) ReceiveData(number string, receiveWebhookUrl string) {
for {
str, err := connbuf.ReadString('\n')
if err != nil {
elapsed := time.Since(r.lastTimeErrorMessageSent)
if (elapsed) > time.Duration(5*time.Minute) { //avoid spamming the log file and only log the message at max every 5 minutes
log.Error("Couldn't read data for number ", number, ": ", err.Error(), ". Is the number properly registered?")
r.lastTimeErrorMessageSent = time.Now()
log.Error("Lost connection to signal-cli...attempting to reconnect (", err.Error(), ")")
r.conn.Close()
err = r.Dial(r.address, 15)
if err != nil {
log.Fatal("Unable to reconnect to signal-cli: ", err.Error(), "...aborting")
}
connbuf = bufio.NewReader(r.conn)
log.Info("Successfully reconnected to signal-cli")
continue
}
log.Debug("json-rpc received data: ", str)
@ -248,7 +265,7 @@ func (r *JsonRpc2Client) ReceiveData(number string, receiveWebhookUrl string) {
}
}
} else {
log.Error("Received unparsable message: ", str)
log.Warn("Received unparsable message: ", str)
}
}
}

View File

@ -2043,6 +2043,18 @@ const docTemplate = `{
"name": "ignore_stories",
"in": "query"
},
{
"type": "string",
"description": "Specify whether avatar downloads should be ignored when receiving messages",
"name": "ignore_avatars",
"in": "query"
},
{
"type": "string",
"description": "Specify whether sticker pack downloads should be ignored when receiving messages",
"name": "ignore_stickers",
"in": "query"
},
{
"type": "string",
"description": "Specify the maximum number of messages to receive (default: unlimited)",

View File

@ -2040,6 +2040,18 @@
"name": "ignore_stories",
"in": "query"
},
{
"type": "string",
"description": "Specify whether avatar downloads should be ignored when receiving messages",
"name": "ignore_avatars",
"in": "query"
},
{
"type": "string",
"description": "Specify whether sticker pack downloads should be ignored when receiving messages",
"name": "ignore_stickers",
"in": "query"
},
{
"type": "string",
"description": "Specify the maximum number of messages to receive (default: unlimited)",

View File

@ -1916,6 +1916,16 @@ paths:
in: query
name: ignore_stories
type: string
- description: Specify whether avatar downloads should be ignored when receiving
messages
in: query
name: ignore_avatars
type: string
- description: Specify whether sticker pack downloads should be ignored when
receiving messages
in: query
name: ignore_stickers
type: string
- description: 'Specify the maximum number of messages to receive (default:
unlimited)'
in: query

View File

@ -163,7 +163,7 @@ func main() {
jsonRpc2ClientConfigPathPath := *signalCliConfig + "/jsonrpc2.yml"
signalCliApiConfigPath := *signalCliConfig + "/api-config.yml"
signalClient := client.NewSignalClient(*signalCliConfig, *attachmentTmpDir, *avatarTmpDir, signalCliMode, jsonRpc2ClientConfigPathPath, signalCliApiConfigPath, webhookUrl)
err = signalClient.Init()
err = signalClient.Init(15)
if err != nil {
log.Fatal("Couldn't init Signal Client: ", err.Error())
}
@ -395,6 +395,8 @@ func main() {
autoReceiveScheduleReceiveTimeout := utils.GetEnv("AUTO_RECEIVE_SCHEDULE_RECEIVE_TIMEOUT", "10")
autoReceiveScheduleIgnoreAttachments := utils.GetEnv("AUTO_RECEIVE_SCHEDULE_IGNORE_ATTACHMENTS", "false")
autoReceiveScheduleIgnoreStories := utils.GetEnv("AUTO_RECEIVE_SCHEDULE_IGNORE_STORIES", "false")
autoReceiveScheduleIgnoreAvatars := utils.GetEnv("AUTO_RECEIVE_SCHEDULE_IGNORE_AVATARS", "false")
autoReceiveScheduleIgnoreStickers := utils.GetEnv("AUTO_RECEIVE_SCHEDULE_IGNORE_STICKERS", "false")
autoReceiveScheduleSendReadReceipts := utils.GetEnv("AUTO_RECEIVE_SCHEDULE_SEND_READ_RECEIPTS", "false")
c := cron.New()
@ -424,6 +426,8 @@ func main() {
q.Add("timeout", autoReceiveScheduleReceiveTimeout)
q.Add("ignore_attachments", autoReceiveScheduleIgnoreAttachments)
q.Add("ignore_stories", autoReceiveScheduleIgnoreStories)
q.Add("ignore_avatars", autoReceiveScheduleIgnoreAvatars)
q.Add("ignore_stickers", autoReceiveScheduleIgnoreStickers)
q.Add("send_read_receipts", autoReceiveScheduleSendReadReceipts)
req.URL.RawQuery = q.Encode()

View File

@ -1,33 +1,15 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"github.com/bbernhard/signal-cli-rest-api/utils"
log "github.com/sirupsen/logrus"
)
const supervisorctlConfigTemplate = `
[program:%s]
process_name=%s
command=bash -c "nc -l -p %d <%s | signal-cli --output=json --config %s%s jsonRpc%s%s >%s"
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", "")
@ -41,72 +23,57 @@ func main() {
jsonRpc2ClientConfig := utils.NewJsonRpc2ClientConfig()
var tcpPort int64 = 6001
fifoPathname := "/tmp/sigsocket1"
jsonRpc2ClientConfig.AddEntry(utils.MULTI_ACCOUNT_NUMBER, utils.JsonRpc2ClientConfigEntry{TcpPort: tcpPort})
jsonRpc2ClientConfig.AddEntry(utils.MULTI_ACCOUNT_NUMBER, utils.JsonRpc2ClientConfigEntry{TcpPort: tcpPort, FifoPathname: fifoPathname})
args := []string{"--output=json", "--config", signalCliConfigDir}
os.Remove(fifoPathname) //remove any existing named pipe
_, err := exec.Command("mkfifo", fifoPathname).Output()
if err != nil {
log.Fatal("Couldn't create fifo with name ", fifoPathname, ": ", err.Error())
}
uid := utils.GetEnv("SIGNAL_CLI_UID", "1000")
gid := utils.GetEnv("SIGNAL_CLI_GID", "1000")
_, err = exec.Command("chown", uid+":"+gid, fifoPathname).Output()
if err != nil {
log.Fatal("Couldn't change permissions of fifo with name ", fifoPathname, ": ", err.Error())
}
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"
}
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" {
trustNewIdentities = " --trust-new-identities on-first-use"
args = append(args, []string{"--trust-new-identities", "on-first-use"}...)
} else if trustNewIdentitiesEnv == "always" {
trustNewIdentities = " --trust-new-identities always"
args = append(args, []string{"--trust-new-identities", "always"}...)
} else if trustNewIdentitiesEnv == "never" {
trustNewIdentities = " --trust-new-identities never"
args = append(args, []string{"--trust-new-identities", "never"}...)
} else if trustNewIdentitiesEnv != "" {
log.Fatal("Invalid JSON_RPC_TRUST_NEW_IDENTITIES environment variable set!")
}
log.Info("Updated jsonrpc2.yml")
args = append(args, "daemon")
//write supervisorctl config
supervisorctlConfigFilename := "/etc/supervisor/conf.d/" + "signal-cli-json-rpc-1.conf"
supervisorctlConfig := fmt.Sprintf(supervisorctlConfigTemplate, supervisorctlProgramName, supervisorctlProgramName,
tcpPort, fifoPathname, signalCliConfigDir, trustNewIdentities, signalCliIgnoreAttachments, signalCliIgnoreStories, fifoPathname,
supervisorctlProgramName, supervisorctlProgramName)
err = ioutil.WriteFile(supervisorctlConfigFilename, []byte(supervisorctlConfig), 0644)
if err != nil {
log.Fatal("Couldn't write ", supervisorctlConfigFilename, ": ", err.Error())
ignoreAttachments := utils.GetEnv("JSON_RPC_IGNORE_ATTACHMENTS", "")
if ignoreAttachments == "true" {
args = append(args, "--ignore-attachments")
}
ignoreStories := utils.GetEnv("JSON_RPC_IGNORE_STORIES", "")
if ignoreStories == "true" {
args = append(args, "--ignore-stories")
}
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")
}
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/signal-cli", args, env)
if err != nil {
log.Fatal("Couldn't start signal-cli in json-rpc mode: ", err.Error())
}
}

View File

@ -2,15 +2,15 @@ package utils
import (
"errors"
"gopkg.in/yaml.v2"
"io/ioutil"
"gopkg.in/yaml.v2"
)
const MULTI_ACCOUNT_NUMBER string = "<multi-account>"
type JsonRpc2ClientConfigEntry struct {
TcpPort int64 `yaml:"tcp_port"`
FifoPathname string `yaml:"fifo_pathname"`
TcpPort int64 `yaml:"tcp_port"`
}
type JsonRpc2ClientConfigEntries struct {
@ -47,14 +47,6 @@ func (c *JsonRpc2ClientConfig) GetTcpPortForNumber(number string) (int64, error)
return 0, errors.New("Number " + number + " not found in local map")
}
func (c *JsonRpc2ClientConfig) GetFifoPathnameForNumber(number string) (string, error) {
if val, ok := c.config.Entries[number]; ok {
return val.FifoPathname, nil
}
return "", errors.New("Number " + number + " not found in local map")
}
func (c *JsonRpc2ClientConfig) GetTcpPortsForNumbers() map[string]int64 {
mapping := make(map[string]int64)
for number, val := range c.config.Entries {