Sign inSign up

codenametest/everywhereyougo

By codenametest

•Updated 2 days ago

A freely extensible universal message gateway. More at github.com/codename-test/EverywhereYouGo

Image
Networking
1

3.3K

codenametest/everywhereyougo repository overview

⁠EverywhereYouGo (EGo) v1.3.2

中文⁠ | 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.

⁠Docker Deployment

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.

⁠Architecture

HTTP POST → Data Source → Parser → Route Match → Template Render → Push Channel
ComponentDescription
Data SourceListens on port to receive HTTP POST
ParserPython script, extracts fields and defines variable names
RouteCondition expression matches channel-template pairs
TemplateSimple / Jinja2 renders title and content
ChannelWeChat Work, DingTalk, Feishu, Telegram, Bark, Email (SMTP)

⁠Authentication

Set EGO_AUTH_TOKEN environment variable to enable access control:

EGO_AUTH_TOKEN=your-secret-token python3 main.py
  • Web pages require login via token input page
  • API calls require Authorization: Bearer your-secret-token header
  • Health check /api/health does not require authentication

Optionally set EGO_SECRET_KEY to customize Flask session key.

⁠Configuration Files

Configuration lives in two places, with distinct roles:

StorageRole
SQLite (ego.db)Runtime source of truth — all reads/writes go through it
config/*.jsonExport / backup medium — for backup, versioning and migration
FileContent
config/parsers.jsonParser metadata
config/sources.jsonData source definitions
config/channels.jsonPush channel configurations
config/templates.jsonPush templates
config/bindings.jsonChannel bindings (with condition expressions)

Load rules at startup:

  1. Database not empty → the database wins; JSON is not read, and the current config is written back to config/*.json as a snapshot
  2. Database empty and JSON present → import from JSON (first run / migration / restore)
  3. Database empty and no JSON → export the initial config to JSON

So 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.

⁠Plugin Directories

Built-in plugins and user-uploaded plugins live in separate directories:

DirectoryContentDocker
parsers_builtin/Built-in parsersShipped in the image, no volume
parsers/Your uploaded parsersego_parsers volume — survives container recreation
channels_builtin/Built-in channel pluginsShipped in the image, no volume
channels/Your uploaded channel pluginsego_channels volume — survives container recreation

Rules:

  • Resolution order: user directory first, then built-in
  • Uploading a name that collides with a built-in is rejected (with a clear error) — built-ins are updated with the image, so a same-named file would never take effect
  • Built-in plugins are read-only: viewable in the WebUI, but not editable or deletable
  • To actually change a built-in's behaviour, put your version under a different filename in the user directory, or edit the source and rebuild the image

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:

  1. Export (pick one):
    • Export a ZIP via the old container's Settings → Backup (if the old version supports it);
    • Or 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
      
  2. Upgrade the image: from v1.3.1+ onward, user plugins live in the user directory backed by named volumes (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.
  3. Restore (only when migrating from the old version): restore writes into the new user directory and automatically skips entries that collide with built-ins (to prevent stale copies from shadowing the new built-in plugins).

Volume operation semantics (ego_parsers / ego_channels):

  • docker compose up (or recreating the container) → volume is kept, plugins survive
  • docker compose down → named volumes are kept by default, plugins survive
  • docker compose down -v → volumes are deleted, all user plugins are lost — confirm first

⁠Parsers

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:

  • Route condition matching: event == 'library.new' and media_type == 'Movie'
  • Template variable reference: {name} / {{ msg.name }}

⁠Route Conditions

Supports and, or, parentheses grouping:

ExampleDescription
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

⁠Features

⁠Do Not Disturb (DND)

Set DND time period, messages enter queue and wait, automatically flush when period ends. Urgent routes are not affected by DND.

⁠Message Deduplication

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.

⁠Parallel Push

When multiple channels match, thread pool sends in parallel, total latency depends on the slowest single channel.

⁠Sample Data & Online Debugging

Each data source automatically saves the last 20 request samples, can select samples in WebUI for test parsing and pushing.

⁠Message Resend

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.

⁠Import & Export
  • Backup: Download ZIP package (config/*.json + parsers/*.py + channels/*.py)
  • Restore: Upload ZIP package; config from the backup is written to the database (JSON → DB), plugin files are restored and hot-reloaded. Config not included in the backup is left unchanged, with a notice listing the missing files
  • Preview: runs the same validation as a real restore (size / completeness / JSON shape / plugin loadability); the "confirm restore" button only appears when it passes
  • JSON Import: Supports dry_run preview, insert/overwrite two modes, dependency check

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.
⁠Channel Circuit Breaker

Automatically isolates a channel that keeps failing, so one broken third party cannot drag down the whole send path:

  • Failure ratio > 50% within a sliding window (default 60s), or 5 consecutive failures → trip
  • Cooldown backs off exponentially: 30 → 60 → 120 → … → 600 s (capped at 10 min)
  • After cooldown it enters half-open probing; 3 consecutive successes restore it
  • 4xx does not count as failure (a business rejection is not a service outage) — only 5xx / timeouts / connection errors are counted
  • While tripped, messages stay queued: no retry budget is consumed and nothing is dropped. State is persisted and survives restarts.
⁠Outbound Rate Limiting

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).

⁠Resilience UI
WhereWhat you can do
Channel list → Resilience columnSee the rate-limit badge and breaker countdown; reset a tripped channel with one click
Channel edit dialogSet 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.

⁠Observability
EndpointDescription
GET /api/metricsQueue 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/resilienceChannels currently tripped / rate-limited
GET /api/queue/statsQueue and dead-letter counts
GET /api/healthHealth check (SQLite / disk / config / queue)
⁠Graceful Stop

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.

⁠Internationalization

Built-in Chinese and English bilingual support, switch languages anytime via language switch button in top-right corner of navigation bar.

⁠Channel Types

ChannelMethodType Identifier
WeChat Work BotWebhookwechat_work_bot
WeChat Work APIApp Messagewechat_work_api
DingTalkWebhookdingtalk
FeishuWebhookfeishu
TelegramBot APItelegram_bot
BarkAPIbark
Email (SMTP)SMTPsmtp_email

⁠Environment Variables

VariableDefaultDescription
WEB_PORT5000HTTP port (Webhook receiver / health check)
WEB_SSL_PORT5001HTTPS port (Admin page, not enabled when certificate is missing)
EGO_SSL_ENABLED1Set to 0 to completely disable built-in HTTPS (HTTP only, no redirect, no certificate generation)
EGO_SSL_DIR./certsSSL certificate directory, where ego.crt and ego.key are stored
EGO_SSL_CERT./certs/ego.crtCertificate file path (overrides EGO_SSL_DIR)
EGO_SSL_KEY./certs/ego.keyPrivate key file path (overrides EGO_SSL_DIR)
DB_PATHego.dbDatabase path
LOG_LEVELINFOLog level
EGO_AUTH_TOKEN(empty)Access control Token
EGO_SECRET_KEY(auto)Flask session key
EGO_INGRESS_WORKERS8Ingress worker threads per port source
EGO_INGRESS_MAX_QUEUE200Ingress queue cap; beyond it returns 503 (backpressure)
EGO_CLEANUP_INTERVAL600Interval for purging old messages / dedup keys (s)
EGO_BREAKER_WINDOW60Circuit breaker sliding window (s)
EGO_BREAKER_MIN_SAMPLES5Min samples before the failure-ratio rule applies
EGO_BREAKER_FAILURE_RATIO0.5Failure ratio that trips the breaker
EGO_BREAKER_CONSECUTIVE5Consecutive-failure threshold (low-traffic channels)
EGO_BREAKER_OPEN_BASE30Base cooldown (s), doubles on each open
EGO_BREAKER_OPEN_MAX600Cooldown cap (s)
EGO_BREAKER_HALF_OPEN_OK3Consecutive probe successes needed to recover
EGO_RATE_MAX_WAIT1.0Max wait for a rate-limit token (s), then defer
EGO_RATE_MISS_TTL30Re-check interval for channels without a rate limit (s)

⁠License

MIT

Tag summary

Content type

Image

Digest

sha256:ef70cb03f…

Size

28.2 MB

Last updated

2 days ago

docker pull codenametest/everywhereyougo