Skip to content

Repository files navigation

Signal Scheduler

Web app for scheduling Signal messages to imported group channels through AsamK/signal-cli.

The Signal number must already be registered in signal-cli. The web app uses Keycloak OpenID Connect for admin login, stores linked groups and schedules in SQLite, and sends messages from a single scheduler process.

Configuration

Create a .env file or export these variables before starting Docker Compose:

SIGNAL_SENDER_NUMBER=+49123456789
SESSION_SECRET=replace-with-a-long-random-string
OIDC_ISSUER=https://keycloak.example.com/realms/example
OIDC_CLIENT_ID=signal-scheduler
OIDC_CLIENT_SECRET=replace-with-keycloak-client-secret
APP_BASE_URL=http://localhost:8000

Optional access restrictions:

OIDC_ALLOWED_GROUP=/signal-admins
OIDC_ALLOWED_GROUPS=/signal-admins,/signal-operators
OIDC_ALLOWED_ROLE=signal-scheduler-admin
OIDC_DEBUG_CLAIMS=false
APP_TIMEZONE=Europe/Berlin
LOGOUT_REDIRECT_URL=https://keycloak.example.com/realms/example/protocol/openid-connect/logout
WEB_PORT=8000
SIGNAL_CLI_DATA_DIR=/signal-cli-config
SIGNAL_CLI_TIMEOUT_SECONDS=120
SIGNAL_RECEIVE_TIMEOUT_SECONDS=5
SIGNAL_RECEIVE_INTERVAL_SECONDS=300

If neither OIDC_ALLOWED_GROUP, OIDC_ALLOWED_GROUPS, nor OIDC_ALLOWED_ROLE is set, any authenticated Keycloak user for the configured client can access the app. Group restrictions are matched against the OpenID Connect groups claim. Set OIDC_DEBUG_CLAIMS=true temporarily to log the received claim keys, groups, and roles after a manual login without logging tokens.

Set LOGOUT_REDIRECT_URL to send users to a specific page after the app clears their local session. Leave it unset to redirect back to the app dashboard.

For local development only, you can bypass Keycloak:

AUTH_BYPASS_FOR_DEVELOPMENT=true

When this is enabled, the app uses a synthetic Development Admin user and shows an auth bypass badge in the header. Keep it unset or false outside local development.

Run

docker compose up --build

Open http://localhost:8000, sign in with Keycloak, import existing Signal groups, then create one-off, weekly, or monthly schedules.

Webhooks

Admins can create webhook endpoints under Webhooks and link each endpoint to one imported Signal group. Copy the generated URL into a Grafana webhook contact point; both POST and PUT are accepted, and Grafana's default JSON payload works without a custom template. The default title and message fields are joined into the Signal message. Generic JSON payloads can use message or text; Grafana alerts arrays and arbitrary JSON objects have readable fallbacks.

The random token in each webhook URL is the endpoint credential, so keep the URL secret. Webhook requests do not use the admin's OIDC session. A successful Signal send returns HTTP 200. A failed send is added to the existing retry queue and returns HTTP 202 so the webhook source does not create duplicate retries.

The app data is stored in ./app-data/app.db. Signal account data is stored in ./signal-cli-config and mounted directly into the web container at /signal-cli-config. Existing group ids keep the historical group. prefix in the app; direct signal-cli commands use the raw base64 group id without that prefix. The app also runs signal-cli receive every SIGNAL_RECEIVE_INTERVAL_SECONDS seconds so sessions, groups, and future bot commands stay fresh. Set the interval to 0 to disable automatic receives. Automatic receives ignore attachments because this bot only processes text commands; this prevents received media from growing ./signal-cli-config/attachments without a bound.

Container stdout/stderr uses Docker's rotating local log driver. The Compose configuration retains at most five 20 MB log files, rather than allowing the default json-file log to consume all available host storage.

Each signal-cli command gets a private temporary directory that is deleted when the process exits, preventing extracted libsignal libraries from accumulating. Compose also mounts /tmp as a 512 MB tmpfs so crash leftovers cannot grow the container's writable layer without a bound. The mount permits execution because libsignal loads its extracted native .so library directly from that directory.

The old message_sender.py script is kept as a legacy reference, but the container now runs uvicorn app.main:app.

Signal CLI

The Docker image installs the latest JVM signal-cli release on Java 25. The JVM distribution is used because upstream describes the GraalVM native image as experimental, and native-image failures terminate the process without a useful recoverable Java exception. The container has a 1 GiB memory limit, and the JVM may use up to 45 percent of that limit for its heap. The remaining memory is available to Python, thread stacks, JNI/libsignal, direct buffers, and temporary files. To run maintenance commands manually:

docker compose exec signal-scheduler-web sh
signal-cli --data-dir "$SIGNAL_CLI_DATA_DIR" --account "$SIGNAL_SENDER_NUMBER" receive --timeout 5
signal-cli --data-dir "$SIGNAL_CLI_DATA_DIR" --output json --account "$SIGNAL_SENDER_NUMBER" listGroups

By default the Docker build resolves SIGNAL_CLI_VERSION=latest to the current stable GitHub release. To force Docker to re-check the latest release, rebuild without cache:

docker compose build --no-cache signal-scheduler-web

For reproducible deployments, pin a specific release:

docker compose build --build-arg SIGNAL_CLI_VERSION=0.14.5 signal-scheduler-web

Tests

python3 -m venv /tmp/asl-signalbot-venv
/tmp/asl-signalbot-venv/bin/pip install -r requirements-dev.txt
/tmp/asl-signalbot-venv/bin/python -m pytest

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages