diff --git a/Dockerfile b/Dockerfile index 68276d5..8353067 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +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 @@ -34,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 && \ @@ -93,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 \ @@ -171,8 +170,9 @@ ENV PORT=8080 ENV BUILD_VERSION=$BUILD_VERSION_ARG ENV SIGNAL_CLI_REST_API_PLUGIN_SHARED_OBJ_DIR=/usr/bin/ -RUN apt-get update \ - && apt-get install -y --no-install-recommends netcat-traditional openjdk-21-jre curl locales xz-utils\ +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 \ && rm -rf /var/lib/apt/lists/* diff --git a/doc/DEBUG.md b/doc/DEBUG.md index 1aeaf2a..befd49d 100644 --- a/doc/DEBUG.md +++ b/doc/DEBUG.md @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 194f112..30c3f7c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,22 @@ -version: "3" services: signal-cli-rest-api: - image: bbernhard/signal-cli-rest-api:latest + #image: bbernhard/signal-cli-rest-api:latest-dev + build: "." environment: - - MODE=normal #supported modes: json-rpc, native, normal + - MODE=normal #supported modes: json-rpc, json-rpc-native, native, normal + - ENABLE_PLUGINS=true + - DEFAULT_SIGNAL_TEXT_MODE=styled + - SWAGGER_IP=127.0.0.1 + - PODMAN_USERNS=keep-id + #- JSON_RPC_IGNORE_ATTACHMENTS=true + #- JSON_RPC_IGNORE_STORIES=true + #- RECEIVE_WEBHOOK_URL=http://127.0.0.1:8089/webhook + #- JSON_RPC_TRUST_NEW_IDENTITIES=always + #- RECEIVE_WEBHOOK_URL=http://127.0.0.1:8080/v1/plugins/abc #- AUTO_RECEIVE_SCHEDULE=0 22 * * * #enable this parameter on demand (see description below) + #network_mode: host 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 + - "./plugins:/plugins" diff --git a/ext/libraries/libsignal-client/signal-cli-native.patch b/ext/libraries/libsignal-client/signal-cli-native.patch index cf506f0..31d3326 100644 --- a/ext/libraries/libsignal-client/signal-cli-native.patch +++ b/ext/libraries/libsignal-client/signal-cli-native.patch @@ -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().configureEach { isPreserveFileTimestamps = false diff --git a/ext/libraries/libsignal-client/v0.87.4/arm64/libsignal_jni.so b/ext/libraries/libsignal-client/v0.87.4/arm64/libsignal_jni.so new file mode 100644 index 0000000..2139d11 Binary files /dev/null and b/ext/libraries/libsignal-client/v0.87.4/arm64/libsignal_jni.so differ diff --git a/ext/libraries/libsignal-client/v0.87.4/armv7/libsignal_jni.so b/ext/libraries/libsignal-client/v0.87.4/armv7/libsignal_jni.so new file mode 100644 index 0000000..41e847f Binary files /dev/null and b/ext/libraries/libsignal-client/v0.87.4/armv7/libsignal_jni.so differ diff --git a/ext/libraries/libsignal-client/v0.87.4/x86-64/libsignal_jni.so b/ext/libraries/libsignal-client/v0.87.4/x86-64/libsignal_jni.so new file mode 100644 index 0000000..fdcd355 Binary files /dev/null and b/ext/libraries/libsignal-client/v0.87.4/x86-64/libsignal_jni.so differ diff --git a/src/api/api.go b/src/api/api.go index 4b3bdbb..c524fa1 100644 --- a/src/api/api.go +++ b/src/api/api.go @@ -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. @@ -843,6 +843,7 @@ func (a *Api) AddMembersToGroup(c *gin.Context) { err = a.signalClient.AddMembersToGroup(number, groupId, req.Members) if err != nil { + log.Info("ERR NOT NULL") switch err.(type) { case *client.NotFoundError: c.JSON(404, Error{Msg: err.Error()}) @@ -1174,19 +1175,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 +2081,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 diff --git a/src/client/client.go b/src/client/client.go index 4de0812..dcd6b48 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -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 } @@ -1349,7 +1349,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 +1359,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 +2634,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 +2705,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 { diff --git a/src/client/jsonrpc2.go b/src/client/jsonrpc2.go index b2fae23..c9f3007 100644 --- a/src/client/jsonrpc2.go +++ b/src/client/jsonrpc2.go @@ -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) } } } diff --git a/src/main.go b/src/main.go index 68ac19c..67f6bbe 100644 --- a/src/main.go +++ b/src/main.go @@ -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()) } diff --git a/src/scripts/jsonrpc2-helper.go b/src/scripts/jsonrpc2-helper.go index a7bbc5d..829f089 100644 --- a/src/scripts/jsonrpc2-helper.go +++ b/src/scripts/jsonrpc2-helper.go @@ -14,7 +14,7 @@ import ( 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" +command=signal-cli --output=json --config %s%s daemon %s%s --tcp 127.0.0.1:%d autostart=true autorestart=true startretries=10 @@ -96,7 +96,7 @@ func main() { supervisorctlConfigFilename := "/etc/supervisor/conf.d/" + "signal-cli-json-rpc-1.conf" supervisorctlConfig := fmt.Sprintf(supervisorctlConfigTemplate, supervisorctlProgramName, supervisorctlProgramName, - tcpPort, fifoPathname, signalCliConfigDir, trustNewIdentities, signalCliIgnoreAttachments, signalCliIgnoreStories, fifoPathname, + signalCliConfigDir, trustNewIdentities, signalCliIgnoreAttachments, signalCliIgnoreStories, tcpPort, supervisorctlProgramName, supervisorctlProgramName) err = ioutil.WriteFile(supervisorctlConfigFilename, []byte(supervisorctlConfig), 0644)