A freely extensible universal message gateway. More at github.com/codename-test/EverywhereYouGo
3.3K
中文 | English
Universal Message Forwarding Platform — Data → Parse → Route → Push
Receives any HTTP request, extracts structured fields through parsers, routes by conditions to multiple push channels.
One-click Deployment (Recommended):
curl -O https://raw.githubusercontent.com/codename-test/EverywhereYouGo/main/deploy/init.sh
chmod +x init.sh
./init.sh
# Follow prompts to select deployment mode
Supports 5 deployment modes: default (quick start), t1-host (host network), t2-bridge (bridge network), t3-nginx (Nginx + manual certificate), t4-acme (Nginx + Let's Encrypt auto certificate).
More deployment options in deploy/README.en.md.
After startup: Admin UI at https://<Host IP>:5001 (self-signed certificate, browser needs to allow); Webhook receiver and health check on http://<Host IP>:5000.
HTTP POST → Data Source → Parser → Route Match → Template Render → Push Channel
| Component | Description |
|---|---|
| Data Source | Listens on port to receive HTTP POST |
| Parser | Python script, extracts fields and defines variable names |
| Route | Condition expression matches channel-template pairs |
| Template | Simple / Jinja2 renders title and content |
| Channel | WeChat Work, DingTalk, Feishu, Telegram, Bark, Email (SMTP) |
Set EGO_AUTH_TOKEN environment variable to enable access control:
EGO_AUTH_TOKEN=your-secret-token python3 main.py
Authorization: Bearer your-secret-token header/api/health does not require authenticationOptionally set EGO_SECRET_KEY to customize Flask session key.
Configuration lives in two places, with distinct roles:
| Storage | Role |
|---|---|
SQLite (ego.db) | Runtime source of truth — all reads/writes go through it |
config/*.json | Export / backup medium — for backup, versioning and migration |
| File | Content |
|---|---|
config/parsers.json | Parser metadata |
config/sources.json | Data source definitions |
config/channels.json | Push channel configurations |
config/templates.json | Push templates |
config/bindings.json | Channel bindings (with condition expressions) |
Load rules at startup:
config/*.json as a snapshotSo use the WebUI for day-to-day config changes (they take effect immediately).
Hand-editing config/*.json is only read on first import when the database is empty —
it is not the normal path for applying changes.
System settings (DND, log level, etc.), the message log and the queue are also stored in SQLite.
For backup/restore use Settings → Backup, which packages config/*.json plus your uploaded
parsers/*.py and channels/*.py.
Restore is not the same path as startup loading: on restore, config from the backup is
written to the database (config/*.json → SQLite), then plugins are reloaded and every
data-source listener is restarted.
Restore is a partial restore: only config files present in the backup are written to the
database; anything not included is left unchanged (not wiped), and a notice listing the missing
files is returned. A hand-made or truncated ZIP therefore cannot destroy data it never contained.
To restore a table to empty, ship a file containing [] rather than omitting it
(file present and [] → table cleared; file absent → table untouched).
⚠️ Back up before upgrading: export a ZIP via Settings → Backup before any version/image upgrade. Backup files contain full push credentials (SMTP password / auth code / tokens) — keep them safe.
Built-in plugins and user-uploaded plugins live in separate directories:
| Directory | Content | Docker |
|---|---|---|
parsers_builtin/ | Built-in parsers | Shipped in the image, no volume |
parsers/ | Your uploaded parsers | ego_parsers volume — survives container recreation |
channels_builtin/ | Built-in channel plugins | Shipped in the image, no volume |
channels/ | Your uploaded channel plugins | ego_channels volume — survives container recreation |
Rules:
Why the split is required: mounting a volume over the directory that holds the built-ins makes Docker copy the image's contents into the volume on first creation, and the volume becomes authoritative from then on — image upgrades would never reach the built-ins. With one shared directory there is no way out: you either lose user files, or the built-ins can never be upgraded.
Upgrading from v1.3.0 to v1.3.1+ Older versions wrote user-uploaded plugins directly into the built-in directory, which is not persisted — recreating the container would lose them. Before upgrading to v1.3.1+, back up your user plugins first:
docker cp the plugin directory from the old container (in the old version,
user plugins share one unvolume-mounted directory with the built-ins — the exact
path depends on your old deployment):
docker cp <ego-container>:/app/parsers_builtin /tmp/old-parsers
docker cp <ego-container>:/app/channels_builtin /tmp/old-channels
ego_parsers/ego_channels). A regular image
upgrade (pull the new image, recreate the container) does not require re-uploading
plugins — they survive via the volumes.Volume operation semantics (
ego_parsers/ego_channels):
docker compose up(or recreating the container) → volume is kept, plugins survivedocker compose down→ named volumes are kept by default, plugins survivedocker compose down -v→ volumes are deleted, all user plugins are lost — confirm first
Place .py files in parsers/ directory, define a parse() function:
def parse(raw_body: bytes, headers: dict, query_params: dict) -> dict:
data = json.loads(raw_body)
event = data.get("Event", "")
name = data.get("Item", {}).get("Name", "")
return {
"title": name,
"event": event,
"name": name,
}
Fields in returned dict except title are also used for:
event == 'library.new' and media_type == 'Movie'{name} / {{ msg.name }}Supports and, or, parentheses grouping:
| Example | Description |
|---|---|
event == 'library.new' | New items only |
event == 'library.new' and media_type == 'Movie' | New movies only |
event == 'library.new' or event == 'test' | New items or test messages |
Set DND time period, messages enter queue and wait, automatically flush when period ends. Urgent routes are not affected by DND.
Deduplication granularity is message × channel: each channel binding has its own
dedup_key_expr and dedup_window (default 3600 seconds), independent of the others.
A channel that hits is skipped while the rest still send; the whole message is marked
DISCARDED only when every channel hits.
When multiple channels match, thread pool sends in parallel, total latency depends on the slowest single channel.
Each data source automatically saves the last 20 request samples, can select samples in WebUI for test parsing and pushing.
Failed messages support original resend (using the parsed msg_json) or re-parse and resend.
By default only the channels that failed are retried — already-succeeded channels are
not pushed a second time. Use scope=all to force a full re-push.
config/*.json + parsers/*.py + channels/*.py)Security note
- The backup ZIP contains full push credentials (SMTP password / auth code / token, etc.) — keep it secure and never share it.
- JSON export masks sensitive fields (
password/token/secret/webhook/device_key, etc.) as***for display and archiving only. Full credentials are preserved only in the backup ZIP and restored from it.
Automatically isolates a channel that keeps failing, so one broken third party cannot drag down the whole send path:
Per-channel rate limit (messages per minute) to avoid getting blocked by the remote side. Retries cannot fix a 429 — limiting has to happen before sending. A message that cannot get a token waits in the queue instead of being dropped.
Token bucket: each channel has an independent token bucket whose capacity (burst)
equals the configured per-minute quota; it refills at quota ÷ 60 tokens per second.
Sending one message takes 1 token; when the bucket is empty the message is queued —
not dropped — until a token is available (up to EGO_RATE_MAX_WAIT seconds, after which
it is deferred instead).
| Where | What you can do |
|---|---|
| Channel list → Resilience column | See the rate-limit badge and breaker countdown; reset a tripped channel with one click |
| Channel edit dialog | Set this channel's outbound rate limit (empty/0 = unlimited) |
| Settings → Resilience (channel circuit breaker) | Hand-tune sliding window, consecutive-failure threshold, cooldown base/cap, probe count, etc. |
Parameter precedence: system_config (settings page / direct DB edit) > environment
variable > built-in default. Changes take effect immediately, no restart needed.
| Endpoint | Description |
|---|---|
GET /api/metrics | Queue depth, DLQ count, per-channel success rate, end-to-end latency, breaker & rate-limit state; ?hours=N sets the stats window (default 24h) |
GET /api/resilience | Channels currently tripped / rate-limited |
GET /api/queue/stats | Queue and dead-letter counts |
GET /api/health | Health check (SQLite / disk / config / queue) |
On SIGTERM EGo stops accepting new messages first, then waits for in-flight tasks
(up to 30 seconds); anything unfinished is moved to the dead-letter queue — a container
restart does not lose messages.
Built-in Chinese and English bilingual support, switch languages anytime via language switch button in top-right corner of navigation bar.
| Channel | Method | Type Identifier |
|---|---|---|
| WeChat Work Bot | Webhook | wechat_work_bot |
| WeChat Work API | App Message | wechat_work_api |
| DingTalk | Webhook | dingtalk |
| Feishu | Webhook | feishu |
| Telegram | Bot API | telegram_bot |
| Bark | API | bark |
| Email (SMTP) | SMTP | smtp_email |
| Variable | Default | Description |
|---|---|---|
WEB_PORT | 5000 | HTTP port (Webhook receiver / health check) |
WEB_SSL_PORT | 5001 | HTTPS port (Admin page, not enabled when certificate is missing) |
EGO_SSL_ENABLED | 1 | Set to 0 to completely disable built-in HTTPS (HTTP only, no redirect, no certificate generation) |
EGO_SSL_DIR | ./certs | SSL certificate directory, where ego.crt and ego.key are stored |
EGO_SSL_CERT | ./certs/ego.crt | Certificate file path (overrides EGO_SSL_DIR) |
EGO_SSL_KEY | ./certs/ego.key | Private key file path (overrides EGO_SSL_DIR) |
DB_PATH | ego.db | Database path |
LOG_LEVEL | INFO | Log level |
EGO_AUTH_TOKEN | (empty) | Access control Token |
EGO_SECRET_KEY | (auto) | Flask session key |
EGO_INGRESS_WORKERS | 8 | Ingress worker threads per port source |
EGO_INGRESS_MAX_QUEUE | 200 | Ingress queue cap; beyond it returns 503 (backpressure) |
EGO_CLEANUP_INTERVAL | 600 | Interval for purging old messages / dedup keys (s) |
EGO_BREAKER_WINDOW | 60 | Circuit breaker sliding window (s) |
EGO_BREAKER_MIN_SAMPLES | 5 | Min samples before the failure-ratio rule applies |
EGO_BREAKER_FAILURE_RATIO | 0.5 | Failure ratio that trips the breaker |
EGO_BREAKER_CONSECUTIVE | 5 | Consecutive-failure threshold (low-traffic channels) |
EGO_BREAKER_OPEN_BASE | 30 | Base cooldown (s), doubles on each open |
EGO_BREAKER_OPEN_MAX | 600 | Cooldown cap (s) |
EGO_BREAKER_HALF_OPEN_OK | 3 | Consecutive probe successes needed to recover |
EGO_RATE_MAX_WAIT | 1.0 | Max wait for a rate-limit token (s), then defer |
EGO_RATE_MISS_TTL | 30 | Re-check interval for channels without a rate limit (s) |
MIT
Content type
Image
Digest
sha256:ef70cb03f…
Size
28.2 MB
Last updated
2 days ago
docker pull codenametest/everywhereyougo