Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
BUILD_TARGET=native # all, jre, native
RUN_MODE=json-rpc-native # normal, json-rpc, native, json-rpc-native
21 changes: 15 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.ref }}
- name: Login to Docker Hub
- name: Login to container registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
registry: ${{ vars.IMAGE_REGISTRY || 'docker.io' }}
username: ${{ secrets.REGISTRY_USERNAME || secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD || secrets.DOCKERHUB_TOKEN }}
- name: Free up disk space
run: |
# Remove Java (JDKs)
Expand Down Expand Up @@ -76,11 +77,19 @@ jobs:
- name: Build
env:
VERSION: ${{ github.run_number }}
IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'docker.io' }}
IMAGE_REPO: ${{ vars.IMAGE_REPO || 'bbernhard/signal-cli-rest-api' }}
run: |
df -h
echo "Start CI build"
docker run --privileged --rm tonistiigi/binfmt --install all
podman manifest create build
podman build --format docker --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build .
podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:${EPOCHSECONDS}-ci
podman manifest create build-all
podman build --format docker --layers --target all --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-all .
podman manifest push localhost/build-all docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${EPOCHSECONDS}-ci
podman manifest create build-jre
podman build --format docker --layers --target jre --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre .
podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${EPOCHSECONDS}-ci-jre
podman manifest create build-native
podman build --format docker --layers --target native --platform linux/amd64,linux/arm64 --manifest localhost/build-native .
podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${EPOCHSECONDS}-ci-native

25 changes: 18 additions & 7 deletions .github/workflows/release-dev-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.ref }}
- name: Login to Docker Hub
- name: Login to container registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
registry: ${{ vars.IMAGE_REGISTRY || 'docker.io' }}
username: ${{ secrets.REGISTRY_USERNAME || secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD || secrets.DOCKERHUB_TOKEN }}
- name: Free up disk space
run: |
# Remove Java (JDKs)
Expand Down Expand Up @@ -80,10 +81,20 @@ jobs:
- name: Release
env:
VERSION: ${{ github.event.inputs.version }}
IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'docker.io' }}
IMAGE_REPO: ${{ vars.IMAGE_REPO || 'bbernhard/signal-cli-rest-api' }}
run: |
echo "Start dev build"
docker run --privileged --rm tonistiigi/binfmt --install all
podman manifest create build
podman build --format docker --build-arg BUILD_VERSION_ARG=${VERSION} --manifest localhost/build --platform linux/amd64,linux/arm64,linux/arm/v7 .
podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-dev
podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:latest-dev
podman manifest create build-all
podman build --format docker --layers --target all --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-all .
podman manifest push localhost/build-all docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-dev
podman manifest push localhost/build-all docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-dev
podman manifest create build-jre
podman build --format docker --layers --target jre --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre .
podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-dev-jre
podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-dev-jre
podman manifest create build-native
podman build --format docker --layers --target native --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64 --manifest localhost/build-native .
podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-dev-native
podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-dev-native
25 changes: 18 additions & 7 deletions .github/workflows/release-productive-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.ref }}
- name: Login to Docker Hub
- name: Login to container registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
registry: ${{ vars.IMAGE_REGISTRY || 'docker.io' }}
username: ${{ secrets.REGISTRY_USERNAME || secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD || secrets.DOCKERHUB_TOKEN }}
- name: Free up disk space
run: |
# Remove Java (JDKs)
Expand Down Expand Up @@ -80,10 +81,20 @@ jobs:
- name: Release
env:
VERSION: ${{ github.event.inputs.version }}
IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'docker.io' }}
IMAGE_REPO: ${{ vars.IMAGE_REPO || 'bbernhard/signal-cli-rest-api' }}
run: |
echo "Start productive build"
docker run --privileged --rm tonistiigi/binfmt --install all
podman manifest create build
podman build --format docker --build-arg BUILD_VERSION_ARG=${VERSION} --manifest localhost/build --platform linux/amd64,linux/arm64,linux/arm/v7 .
podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}
podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:latest
podman manifest create build-all
podman build --format docker --layers --target all --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-all .
podman manifest push localhost/build-all docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}
podman manifest push localhost/build-all docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest
podman manifest create build-jre
podman build --format docker --layers --target jre --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build-jre .
podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-jre
podman manifest push localhost/build-jre docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-jre
podman manifest create build-native
podman build --format docker --layers --target native --build-arg BUILD_VERSION_ARG=${VERSION} --platform linux/amd64,linux/arm64 --manifest localhost/build-native .
podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:${VERSION}-native
podman manifest push localhost/build-native docker://${IMAGE_REGISTRY}/${IMAGE_REPO}:latest-native
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ signal-cli-config
src/main
src/signal-cli-rest-api
.idea/
./persistence
.persistence
100 changes: 100 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# AGENTS.md

## Project Overview

Go-based REST API wrapping [signal-cli](https://github.com/AsamK/signal-cli) for Signal Messenger. Runs exclusively inside Docker; there is no standalone `go run` dev server for the full API. The Go app orchestrates `signal-cli` (a Java binary) or `signal-cli-native` (GraalVM precompiled), communicating via CLI args in normal/native mode or JSON-RPC in daemon mode.

## Architecture

- `src/` — Go source (the REST API server)
- `main.go` — entrypoint; sets up Gin router, routes, cron, plugin loader
- `api/api.go` — all HTTP handlers
- `client/` — signal-cli interaction layer; three modes: `Normal`, `Native`, `JsonRpc` (see `client.go` constants)
- `datastructs/datastructs.go` — shared structs
- `utils/` — helpers, plugin config, text-style parser
- `docs/` — Swagger docs (auto-generated by `swag init`)
- `scripts/jsonrpc2-helper.go` — daemon manager for json-rpc mode
- `plugin_loader.go` — Go plugin system using Lua scripts (v1/v2 plugin API)
- `plugins/` — example Lua plugins and persistence plugin
- `Dockerfile` — multi-stage build with three release targets: `all`, `jre`, and `native`
- `entrypoint.sh` — container entrypoint; validates MODE against available binaries, handles UID/GID, chown, supervisor for json-rpc mode

## Build & Run

- **Build & test (local):** All commands run from `src/`
```bash
cd src
go test ./client -v
go test ./utils -v
go build -o signal-cli-rest-api main.go
```
- **Build & run (Docker, the primary workflow):**
```bash
# Edit docker-compose.yml: change image to build: "."
docker compose build
docker compose up
```
- **Build specific variant (Docker):**
```bash
docker build --target all . # all-in-one (all four modes)
docker build --target jre . # JRE variant (normal + json-rpc)
docker build --target native . # native variant (native + json-rpc-native)
```
- **No linter or typecheck config exists** — standard Go tooling only (e.g., `go vet`).

## Swagger / API Docs

Docs are auto-generated from swag annotations in `main.go` and `api/api.go`.

```bash
cd src
go run github.com/swaggo/swag/cmd/swag@v1.16.6 init --requiredByDefault --outputTypes "go,json"
```

The receive V1 schemas require an extra step (`add_v1_receive_schemas.go`); see `src/docs/README.md`. CI checks that generated docs are up-to-date (workflow: `check-docs.yml`).

## Image Variants

The Dockerfile produces three release targets from a shared `base` stage:

| Target | Tag suffix | Contains | Modes | Platforms |
|---|---|---|---|---|
| `all` | `latest`, `X.Y.Z` | headless JRE + signal-cli Java dist + signal-cli-native | `normal`, `json-rpc`, `native`, `json-rpc-native` | amd64, arm64, arm/v7 |
| `jre` | `latest-jre`, `X.Y.Z-jre` | headless JRE + signal-cli Java dist | `normal`, `json-rpc` | amd64, arm64, arm/v7 |
| `native` | `latest-native`, `X.Y.Z-native` | signal-cli-native only (no JRE) | `native`, `json-rpc-native` | amd64, arm64 |

The `all` target is the backwards-compatible default — it contains both runtimes so all four modes work, matching the original monolithic image.

The `entrypoint.sh` validates that the requested `MODE` matches binaries available in the running image variant and exits with a clear error if mismatched.

The JRE variant uses `openjdk-25-jre-headless` (not the full `openjdk-25-jre`) to avoid ~280 MB of GUI libraries that a CLI tool never uses.

## Key Environment Variables

| Variable | Default | Purpose |
|---|---|---|
| `MODE` | `normal` | `normal`, `native`, `json-rpc`, `json-rpc-native` |
| `PORT` | `8080` | Listen port |
| `ENABLE_PLUGINS` | `false` | Enable Lua plugin system |
| `AUTO_RECEIVE_SCHEDULE` | — | Cron expression; **incompatible with json-rpc mode** |
| `SIGNAL_CLI_CMD_TIMEOUT` | — | CLI timeout; **incompatible with json-rpc mode** |
| `RECEIVE_WEBHOOK_URL` | — | Webhook on receive; **json-rpc mode only** |
| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` |
| `DEFAULT_SIGNAL_TEXT_MODE` | `normal` | `normal` or `styled` |

## CI Build Optimization

All three targets (`all`, `jre`, `native`) share the `buildcontainer` and `base` stages in the Dockerfile. Without caching, Podman rebuilds these from scratch for each `--target` invocation — tripling the build time.

The `--layers` flag on every `podman build` call tells Podman to cache intermediate layers in the local store. Since `all` is built first, its `base` and `buildcontainer` layers are already cached when `jre` and `native` are built next, so only the target-specific layers are rebuilt.

Build order matters: `all` → `jre` → `native`. All three share `base` + `buildcontainer`; `jre` additionally shares its layers with `all` (since `all` extends `jre`).

## Gotchas

- The Dockerfile pins `SIGNAL_CLI_VERSION` and `LIBSIGNAL_CLIENT_VERSION` as build args. After bumping these, verify the `libsignal-client-*.jar` filename still matches.
- Multi-arch builds: the `jre` target covers `linux/amd64`, `linux/arm64`, `linux/arm/v7`; the `native` target covers `linux/amd64`, `linux/arm64` only (no armv7 native binary).
- `AUTO_RECEIVE_SCHEDULE` and `SIGNAL_CLI_CMD_TIMEOUT` cause a fatal error in json-rpc mode.
- `RECEIVE_WEBHOOK_URL` is only valid in json-rpc mode; fatal in other modes.
- CI uses Podman for multi-arch builds, not Docker Buildx. Each CI/release workflow builds all three targets (`all`, `jre`, `native`) with `--layers` for cache reuse between targets.
- Release versions are published via `publish.sh` triggering GitHub Actions (dev vs stable tags, with `-jre` and `-native` suffixes for those variants; `all` variant gets the plain version/latest tags).
73 changes: 54 additions & 19 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,9 @@ RUN cd /tmp/signal-cli-rest-api-src/scripts && go build -o jsonrpc2-helper
# build plugin_loader
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
# ---- Shared base for both image variants ----

# 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
FROM ubuntu:noble AS base

ENV GIN_MODE=release

Expand All @@ -150,31 +146,20 @@ 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 openjdk-25-jre curl locales \
&& rm -rf /var/lib/apt/lists/*
&& apt-get install -y --no-install-recommends util-linux curl locales \
&& rm -rf /var/lib/apt/lists/*

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 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 /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)"; \
case "$arch" in \
armv7l) echo "GRAALVM doesn't support 32bit" && rm /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native /usr/bin/signal-cli-native ;; \
esac;

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
Expand All @@ -192,3 +177,53 @@ ENTRYPOINT ["/entrypoint.sh"]

HEALTHCHECK --interval=20s --timeout=10s --retries=3 \
CMD curl -f http://localhost:${PORT}/v1/health || exit 1

# ---- JRE variant: MODE=normal, json-rpc ----
# Includes headless JRE + signal-cli Java dist. No native binary.
# Supports: linux/amd64, linux/arm64, linux/arm/v7

FROM base AS jre

ARG SIGNAL_CLI_VERSION

RUN dpkg-reconfigure debconf --frontend=noninteractive \
&& apt-get update \
&& apt-get install -y --no-install-recommends openjdk-25-jre-headless supervisor \
&& rm -rf /var/lib/apt/lists/*

COPY --from=buildcontainer /opt/signal-cli-${SIGNAL_CLI_VERSION} /opt/signal-cli-${SIGNAL_CLI_VERSION}

RUN ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli /usr/bin/signal-cli \
&& mkdir -p /home/.local/share/signal-cli

# ---- All-in-one variant: MODE=normal, json-rpc, native, json-rpc-native ----
# Includes headless JRE + signal-cli Java dist + signal-cli-native.
# Supports: linux/amd64, linux/arm64, linux/arm/v7
# (arm/v7 gets JRE modes only — signal-cli-native is a dummy on arm/v7)

FROM jre AS all

ARG SIGNAL_CLI_VERSION

COPY --from=buildcontainer /tmp/signal-cli-native /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native

RUN ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native /usr/bin/signal-cli-native

# ---- Native variant: MODE=native, json-rpc-native ----
# Includes signal-cli-native only. No JRE, no Java dist.
# Supports: linux/amd64, linux/arm64 (no arm/v7 — GraalVM doesn't produce 32-bit binaries)

FROM base AS native

ARG SIGNAL_CLI_VERSION

RUN dpkg-reconfigure debconf --frontend=noninteractive \
&& apt-get update \
&& apt-get install -y --no-install-recommends supervisor \
&& rm -rf /var/lib/apt/lists/*

COPY --from=buildcontainer /tmp/signal-cli-native /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native

RUN mkdir -p /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin \
&& ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native /usr/bin/signal-cli-native \
&& mkdir -p /home/.local/share/signal-cli
Loading