Repository files navigation

BridgeMost 👻

CIPyPI

Multi-Platform ↔ Mattermost Transparent Bridge

BridgeMost makes your messages from Telegram, Google Chat, or any supported platform appear natively in Mattermost — as your real user, with your avatar and name. Bot responses relay back instantly via WebSocket.

Unlike Matterbridge or webhooks that post with [User] prefixes, BridgeMost posts as your actual Mattermost account using Personal Access Tokens. Nobody in Mattermost can tell you're writing from another platform.

🔌 Supported Platforms (Adapters)

PlatformStatusDescription
Telegram✅ ProductionFull support — text, media, voice, reactions, edits, deletes
Google Chat✅ v2.1.0Workspace ghost mode via Service Account + domain-wide delegation
Slack🔜 PlannedUser token impersonation
Matrix🔜 PlannedApplication Service ghost mode

Plugin architecture (v2.0+): Each platform is an independent adapter module. Adding a new platform = one Python file implementing BaseAdapter. Zero changes to the core engine.

✨ Features

FeatureDescription
🪪 Transparent identityPosts as your real MM user (avatar, name, everything)
📁 Full mediaPhotos, documents, audio, video, voice — bidirectional
🎤 Voice-to-textVoice messages auto-transcribed via Whisper API
🤖 Multi-bot routingTalk to multiple MM bots; switch with /bridge bot <name>
📲 DM Bridge modeGive each MM bot its own dedicated TG bot — DM it directly (v2.2.0)
/️⃣ Hermes slash passthrough/new, /model, /help, etc. cross Telegram → Mattermost unchanged (v2.2.4)
🧠 Telegram clean modeTool chatter stays in MM; Telegram sees a neural-link placeholder + clean final response (v2.2.5)
↪️ Reply/thread syncTelegram replies map to Mattermost threads and threaded MM replies come back as native Telegram replies (v2.2.6)
⚡ Real-time WebSocketResponses arrive instantly (no polling)
✏️ Edit & delete syncEdits and deletes stay in sync both ways
😀 ReactionsEmoji reactions synced bidirectionally
⌨️ Typing indicatorSynthetic "Bot is typing..." on the chat side
📝 MarkdownMM markdown auto-converted to platform format
🔒 Startup checksValidates tokens + discovers channels before starting
💾 Persistent mappingSQLite store for message IDs (survives restarts)
🩺 Health endpointHTTP /health on configurable port
👥 Multi-userMultiple users, each with their own identity and bot routing
🐳 DockerMulti-stage image, ~55 MB

~55 MB RAM · ~250 ms latency · asyncio-based · Python 3.11+

Hermes slash commands over Telegram

BridgeMost now preserves generic slash commands when the upstream Mattermost bot is Hermes. That means commands like:

  • /new
  • /model
  • /help
  • /commands
  • /reasoning

arrive in Mattermost exactly as typed, instead of being swallowed by Telegram-side command handlers.

BridgeMost local command namespace

To avoid collisions with Hermes, BridgeMost keeps its own local controls under /bridge:

  • /bridge bot — list bots or switch the active relay target
  • /bridge bots — inspect available bot routes
  • /bridge status — inspect bridge-local status
  • /bridge help — show the local command help

Legacy /bot and /bots aliases still work in Telegram for compatibility, but /status is now reserved for Hermes passthrough.

Telegram clean mode (v2.2.5)

When the upstream Mattermost bot is Hermes, BridgeMost can now keep Telegram clean:

  • internal tool-progress posts (terminal:, execute_code:, skill_view:, etc.) stay in Mattermost
  • Telegram gets a placeholder such as 🧠⚡ Conectando a la red neuronal...
  • the placeholder is then edited in place into the real final answer
  • the final answer can be revealed progressively for a streaming-like UX

This behavior is configurable through telegram_presentation: in config.yaml.

Reply/thread sync (v2.2.6)

  • Replying to a Telegram message now posts into the corresponding Mattermost thread root
  • Replies emitted by Mattermost bots with root_id come back to Telegram as native replies when the root message is known
  • Clean mode preserves the reply target, so the placeholder and final edited answer stay visually attached to the original Telegram message

Multi-user ready: Multiple people can use the same BridgeMost instance — each with their own chat account, Mattermost identity, and bot routing. Add users to config.yaml and they appear as themselves in Mattermost. No shared accounts, no impersonation.


🏗️ Architecture (v2.0+)

┌──────────────┐
│ Telegram │─┐
├──────────────┤ │ ┌──────────────┐ ┌──────────────┐
│ Google Chat │─┼────────►│ BridgeMost │◄───────►│ Mattermost │
├──────────────┤ │ │ Core Engine │ WS+API │ (Bots) │
│ Slack │─┤ └──────────────┘ └──────────────┘
├──────────────┤ │ Adapters │ Core │ MM
│ Matrix │─┘
└──────────────┘

Three layers:

  1. Adapters — Platform-specific plugins (telegram.py, googlechat.py, etc.)
  2. Core Engine — Routing, mapping, sync, retry, health — platform-agnostic
  3. Mattermost Connector — WebSocket, REST API, file upload

Each adapter implements BaseAdapter (8 methods: start, stop, send_message, edit, delete, react, typing, clear_reactions).


🚀 Installation — Step by Step

What you need

#ItemWhere to get it
1Mattermost server (self-hosted)You must be admin or have an admin enable PAT support
2Chat platform bot tokenTelegram: @BotFather/newbot
3Your platform user IDTelegram: message @userinfobot
4Python 3.11+python3 --version to check
5Gitgit --version to check

Step 1 — Enable Personal Access Tokens on Mattermost

⚠️This step is REQUIRED. Without it, BridgeMost cannot post as your user.

Option A — Via Mattermost UI (admin):

  1. Go to System Console → Authentication → Token Access
  2. Set Enable Personal Access Tokens to true
  3. Save

Option B — Via command line (requires access to the server):

# If mmctl is available:
mmctl --local config set ServiceSettings.EnableUserAccessTokens true# Or via REST API with admin token:
curl -X PUT http://localhost:8065/api/v4/config/patch \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ServiceSettings": {"EnableUserAccessTokens": true}}'

Step 2 — Clone and install

git clone https://github.com/JuanjoPM-Developer/BridgeMost.git
cd BridgeMost
python3 -m venv .venv
source .venv/bin/activate
pip install -e .

Or via PyPI:

pip install bridgemost

Or via Docker:

docker compose up -d

Step 3 — Configure

Option A — Interactive wizard (recommended for Telegram):

python3 -m bridgemost setup

The wizard will:

  1. Connect to your Mattermost server
  2. Log you in (password is NOT stored)
  3. Auto-create a Personal Access Token for BridgeMost
  4. List all bots on the server — you pick which ones to bridge
  5. Ask for your platform bot token and user ID
  6. Generate config.yaml automatically

Option B — Manual configuration:

cp config.example.yaml config.yaml

Then edit config.yaml — see the Configuration Reference below.

Step 4 — Run

# Foreground (for testing):
python3 -m bridgemost
# Or as a systemd service (recommended for production):
sudo cp bridgemost.service.example /etc/systemd/system/bridgemost.service
# Edit the service file — update paths to match your installation
sudo systemctl daemon-reload
sudo systemctl enable --now bridgemost

Step 5 — Test

  1. Send a message from your chat platform to the BridgeMost bot
  2. The message should appear in Mattermost as your real user
  3. When the MM bot responds, the response should appear in your chat

⚙️ Configuration Reference

Minimal config.yaml (Telegram adapter)

telegram:
bot_token: "123456:ABC-DEF..."# From @BotFathermattermost:
url: "http://localhost:8065"# Your MM server URL (http or https)bot_token: "abc123..."# Any bot's access token (for WebSocket)bot_user_id: "a1b2c3d4..."# User ID of that botusers:
- telegram_id: 123456789# Your numeric platform user IDtelegram_name: "Your Name"# Display name (for logs only)mm_user_id: "x1y2z3..."# Your Mattermost user IDmm_token: "your-pat-here"# Your Personal Access Tokenbots:
- name: "mybot"# Friendly name (used with /bot command)mm_bot_id: "bot-user-id-here"# The bot's Mattermost user IDmm_dm_channel: ""# Leave empty — auto-discovered at startupdefault: true # First bot to talk to when bridge starts

How to find each value

FieldHow to get it
telegram.bot_token@BotFather/newbot → copy the token
telegram_idSend any message to @userinfobot
mattermost.urlThe URL you use to open Mattermost in your browser
mattermost.bot_tokenMM → Integrations → Bot Accounts → pick any bot → copy token. Or ask your admin.
mattermost.bot_user_idmmctl user search <botname> → copy id. Or: curl http://YOUR_MM/api/v4/users/username/<botname> -H "Authorization: Bearer TOKEN""id"
mm_user_idSame as above with your own username
mm_token (PAT)MM → Profile → Security → Personal Access Tokens → Create. Or wizard creates it.
mm_bot_idThe Mattermost user ID of each bot you want to talk to
mm_dm_channelLeave empty — auto-discovered at startup.

Optional sections

# Voice-to-text transcription (requires a Whisper-compatible API)voice_to_text:
url: "http://localhost:9000"# Whisper endpointapi_key: ""# For OpenAI/Groq; empty for local Whispermodel: "large-v3"# large-v3, whisper-1, whisper-large-v3-turbolanguage: ""# "es", "en", or "" for auto-detectkeep_audio: true # Also attach audio file alongside transcript# Health monitoring endpointhealth:
port: 9191# HTTP health check on this port# Message persistencestorage:
data_dir: ""# SQLite DB location; empty = working directory# Logginglogging:
level: "INFO"# DEBUG, INFO, WARNING, ERRORfile: ""# Log file path, or "" for stdout only

🤖 Chat Commands (Telegram adapter)

CommandDescription
/botList all available bots and show which one is active
/bot nameSwitch to a different bot
/botsShow all bots with live 🟢/⚫ online status
/statusDetailed info about the active bot

🎤 Voice-to-Text

When voice_to_text is configured, voice messages are transcribed before posting:

🎤 Hello, this is what I said in the voice message

If keep_audio: true, the original audio file is also attached.

Compatible APIs:


📊 Health Endpoint

curl http://localhost:9191/health
{
"status": "ok",
"version": "2.0.1",
"transport": "websocket",
"uptime": "2h15m30s",
"messages": { "tg_to_mm": 42, "mm_to_tg": 38, "errors": 0 },
"store": { "persistent_mappings": 156 }
}

🔧 Troubleshooting

ProblemSolution
FATAL: Token validation FAILEDPAT is invalid/expired. Create a new one in MM → Profile → Security → PAT. Also verify EnableUserAccessTokens is true.
⚠️ Token expirado alertSame — renew PAT, update mm_token in config.yaml, restart.
Zero DM channels discoveredMake sure you've DM'd each bot in MM at least once. Verify mm_bot_id values are correct (26 alphanumeric chars).
WS auth rejected (CLOSE on connect)The mattermost.bot_token is invalid. Get a valid one from Integrations → Bot Accounts.
OSError: [Errno 98] address already in useAnother process on health port. Change health.port in config.
[BotName] prefix on messagesNormal in multi-bot mode to identify which bot responded. Single bot = no prefix.
Voice not transcribedCheck voice_to_text.url is reachable. For OpenAI/Groq, verify api_key.
EnableUserAccessTokens keeps resettingSomething is toggling it. Lock the setting and audit admin access.

🛡️ Security

  • config.yaml contains secrets — it's in .gitignore, never commit it
  • PATs have your full user permissions — use a dedicated account if concerned
  • Health endpoint binds to 127.0.0.1 (not exposed externally)
  • Only users whose ID is in config can use the bridge
  • Message mappings stored in local SQLite (30-day auto-prune)

🔌 Writing a Custom Adapter

Create a new file in src/bridgemost/adapters/ that implements BaseAdapter:

frombridgemost.adapters.baseimportBaseAdapter, InboundMessage, OutboundMessageclassMyPlatformAdapter(BaseAdapter):
asyncdefstart(self): ...
asyncdefstop(self): ...
asyncdefsend_message(self, chat_id, msg: OutboundMessage) ->int|None: ...
asyncdefedit_message(self, chat_id, msg_id, text): ...
asyncdefdelete_message(self, chat_id, msg_id): ...
asyncdefset_reaction(self, chat_id, msg_id, emoji): ...
asyncdefclear_reactions(self, chat_id, msg_id): ...
defstart_typing_loop(self, chat_id): ...
defstop_typing_loop(self, chat_id): ...

The core engine handles all Mattermost interaction, message tracking, retry, and health monitoring.


📋 Changelog

VersionDateHighlight
v2.1.02026-03-25Google Chat adapter — Service Account ghost mode, polling, edit/delete/reactions
v2.0.22026-03-25README rewritten for multi-platform architecture
v2.0.12026-03-25Audit cleanup: platform-agnostic emoji names, encapsulation fix
v2.0.02026-03-25Plugin adapter architecture — Telegram extracted as adapter, core engine separated
v1.0.02026-03-25Stable release — PyPI, CI/CD, full test suite
v0.9.x2026-03-24/25Stickers, locations, polls, file relay, Docker, 71 tests
v0.8.x2026-03-24SQLite store, WS jitter, rate limiter, bot commands
v0.7.02026-03-247-bug audit, PAT health check, error alerts
v0.6.02026-03-24Interactive setup wizard
v0.5.02026-03-24Startup resilience, token validation
v0.4.02026-03-24Voice-to-text via Whisper
v0.3.x2026-03-24Multi-bot routing, synthetic typing
v0.2.02026-03-24Emoji/reaction relay
v0.1.x2026-03-24WebSocket transport, edit/delete sync
v0.0.52026-03-24First public release

See CHANGELOG.md for full details.


📄 License

MIT — see LICENSE

🙏 Built with

About

Multi-platform ↔ Mattermost transparent bridge. Messages appear as the real user. Plugin adapter architecture — Telegram ready, Google Chat/Slack/Matrix planned. Ghost mode.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

BridgeMost 👻

CIPyPI

Multi-Platform ↔ Mattermost Transparent Bridge

BridgeMost makes your messages from Telegram, Google Chat, or any supported platform appear natively in Mattermost — as your real user, with your avatar and name. Bot responses relay back instantly via WebSocket.

Unlike Matterbridge or webhooks that post with [User] prefixes, BridgeMost posts as your actual Mattermost account using Personal Access Tokens. Nobody in Mattermost can tell you're writing from another platform.

🔌 Supported Platforms (Adapters)

PlatformStatusDescription
Telegram✅ ProductionFull support — text, media, voice, reactions, edits, deletes
Google Chat✅ v2.1.0Workspace ghost mode via Service Account + domain-wide delegation
Slack🔜 PlannedUser token impersonation
Matrix🔜 PlannedApplication Service ghost mode

Plugin architecture (v2.0+): Each platform is an independent adapter module. Adding a new platform = one Python file implementing BaseAdapter. Zero changes to the core engine.

✨ Features

FeatureDescription
🪪 Transparent identityPosts as your real MM user (avatar, name, everything)
📁 Full mediaPhotos, documents, audio, video, voice — bidirectional
🎤 Voice-to-textVoice messages auto-transcribed via Whisper API
🤖 Multi-bot routingTalk to multiple MM bots; switch with /bridge bot <name>
📲 DM Bridge modeGive each MM bot its own dedicated TG bot — DM it directly (v2.2.0)
/️⃣ Hermes slash passthrough/new, /model, /help, etc. cross Telegram → Mattermost unchanged (v2.2.4)
🧠 Telegram clean modeTool chatter stays in MM; Telegram sees a neural-link placeholder + clean final response (v2.2.5)
↪️ Reply/thread syncTelegram replies map to Mattermost threads and threaded MM replies come back as native Telegram replies (v2.2.6)
⚡ Real-time WebSocketResponses arrive instantly (no polling)
✏️ Edit & delete syncEdits and deletes stay in sync both ways
😀 ReactionsEmoji reactions synced bidirectionally
⌨️ Typing indicatorSynthetic "Bot is typing..." on the chat side
📝 MarkdownMM markdown auto-converted to platform format
🔒 Startup checksValidates tokens + discovers channels before starting
💾 Persistent mappingSQLite store for message IDs (survives restarts)
🩺 Health endpointHTTP /health on configurable port
👥 Multi-userMultiple users, each with their own identity and bot routing
🐳 DockerMulti-stage image, ~55 MB

~55 MB RAM · ~250 ms latency · asyncio-based · Python 3.11+

Hermes slash commands over Telegram

BridgeMost now preserves generic slash commands when the upstream Mattermost bot is Hermes. That means commands like:

  • /new
  • /model
  • /help
  • /commands
  • /reasoning

arrive in Mattermost exactly as typed, instead of being swallowed by Telegram-side command handlers.

BridgeMost local command namespace

To avoid collisions with Hermes, BridgeMost keeps its own local controls under /bridge:

  • /bridge bot — list bots or switch the active relay target
  • /bridge bots — inspect available bot routes
  • /bridge status — inspect bridge-local status
  • /bridge help — show the local command help

Legacy /bot and /bots aliases still work in Telegram for compatibility, but /status is now reserved for Hermes passthrough.

Telegram clean mode (v2.2.5)

When the upstream Mattermost bot is Hermes, BridgeMost can now keep Telegram clean:

  • internal tool-progress posts (terminal:, execute_code:, skill_view:, etc.) stay in Mattermost
  • Telegram gets a placeholder such as 🧠⚡ Conectando a la red neuronal...
  • the placeholder is then edited in place into the real final answer
  • the final answer can be revealed progressively for a streaming-like UX

This behavior is configurable through telegram_presentation: in config.yaml.

Reply/thread sync (v2.2.6)

  • Replying to a Telegram message now posts into the corresponding Mattermost thread root
  • Replies emitted by Mattermost bots with root_id come back to Telegram as native replies when the root message is known
  • Clean mode preserves the reply target, so the placeholder and final edited answer stay visually attached to the original Telegram message

Multi-user ready: Multiple people can use the same BridgeMost instance — each with their own chat account, Mattermost identity, and bot routing. Add users to config.yaml and they appear as themselves in Mattermost. No shared accounts, no impersonation.


🏗️ Architecture (v2.0+)

┌──────────────┐
│ Telegram │─┐
├──────────────┤ │ ┌──────────────┐ ┌──────────────┐
│ Google Chat │─┼────────►│ BridgeMost │◄───────►│ Mattermost │
├──────────────┤ │ │ Core Engine │ WS+API │ (Bots) │
│ Slack │─┤ └──────────────┘ └──────────────┘
├──────────────┤ │ Adapters │ Core │ MM
│ Matrix │─┘
└──────────────┘

Three layers:

  1. Adapters — Platform-specific plugins (telegram.py, googlechat.py, etc.)
  2. Core Engine — Routing, mapping, sync, retry, health — platform-agnostic
  3. Mattermost Connector — WebSocket, REST API, file upload

Each adapter implements BaseAdapter (8 methods: start, stop, send_message, edit, delete, react, typing, clear_reactions).


🚀 Installation — Step by Step

What you need

#ItemWhere to get it
1Mattermost server (self-hosted)You must be admin or have an admin enable PAT support
2Chat platform bot tokenTelegram: @BotFather/newbot
3Your platform user IDTelegram: message @userinfobot
4Python 3.11+python3 --version to check
5Gitgit --version to check

Step 1 — Enable Personal Access Tokens on Mattermost

⚠️This step is REQUIRED. Without it, BridgeMost cannot post as your user.

Option A — Via Mattermost UI (admin):

  1. Go to System Console → Authentication → Token Access
  2. Set Enable Personal Access Tokens to true
  3. Save

Option B — Via command line (requires access to the server):

# If mmctl is available:
mmctl --local config set ServiceSettings.EnableUserAccessTokens true# Or via REST API with admin token:
curl -X PUT http://localhost:8065/api/v4/config/patch \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ServiceSettings": {"EnableUserAccessTokens": true}}'

Step 2 — Clone and install

git clone https://github.com/JuanjoPM-Developer/BridgeMost.git
cd BridgeMost
python3 -m venv .venv
source .venv/bin/activate
pip install -e .

Or via PyPI:

pip install bridgemost

Or via Docker:

docker compose up -d

Step 3 — Configure

Option A — Interactive wizard (recommended for Telegram):

python3 -m bridgemost setup

The wizard will:

  1. Connect to your Mattermost server
  2. Log you in (password is NOT stored)
  3. Auto-create a Personal Access Token for BridgeMost
  4. List all bots on the server — you pick which ones to bridge
  5. Ask for your platform bot token and user ID
  6. Generate config.yaml automatically

Option B — Manual configuration:

cp config.example.yaml config.yaml

Then edit config.yaml — see the Configuration Reference below.

Step 4 — Run

# Foreground (for testing):
python3 -m bridgemost
# Or as a systemd service (recommended for production):
sudo cp bridgemost.service.example /etc/systemd/system/bridgemost.service
# Edit the service file — update paths to match your installation
sudo systemctl daemon-reload
sudo systemctl enable --now bridgemost

Step 5 — Test

  1. Send a message from your chat platform to the BridgeMost bot
  2. The message should appear in Mattermost as your real user
  3. When the MM bot responds, the response should appear in your chat

⚙️ Configuration Reference

Minimal config.yaml (Telegram adapter)

telegram:
bot_token: "123456:ABC-DEF..."# From @BotFathermattermost:
url: "http://localhost:8065"# Your MM server URL (http or https)bot_token: "abc123..."# Any bot's access token (for WebSocket)bot_user_id: "a1b2c3d4..."# User ID of that botusers:
- telegram_id: 123456789# Your numeric platform user IDtelegram_name: "Your Name"# Display name (for logs only)mm_user_id: "x1y2z3..."# Your Mattermost user IDmm_token: "your-pat-here"# Your Personal Access Tokenbots:
- name: "mybot"# Friendly name (used with /bot command)mm_bot_id: "bot-user-id-here"# The bot's Mattermost user IDmm_dm_channel: ""# Leave empty — auto-discovered at startupdefault: true # First bot to talk to when bridge starts

How to find each value

FieldHow to get it
telegram.bot_token@BotFather/newbot → copy the token
telegram_idSend any message to @userinfobot
mattermost.urlThe URL you use to open Mattermost in your browser
mattermost.bot_tokenMM → Integrations → Bot Accounts → pick any bot → copy token. Or ask your admin.
mattermost.bot_user_idmmctl user search <botname> → copy id. Or: curl http://YOUR_MM/api/v4/users/username/<botname> -H "Authorization: Bearer TOKEN""id"
mm_user_idSame as above with your own username
mm_token (PAT)MM → Profile → Security → Personal Access Tokens → Create. Or wizard creates it.
mm_bot_idThe Mattermost user ID of each bot you want to talk to
mm_dm_channelLeave empty — auto-discovered at startup.

Optional sections

# Voice-to-text transcription (requires a Whisper-compatible API)voice_to_text:
url: "http://localhost:9000"# Whisper endpointapi_key: ""# For OpenAI/Groq; empty for local Whispermodel: "large-v3"# large-v3, whisper-1, whisper-large-v3-turbolanguage: ""# "es", "en", or "" for auto-detectkeep_audio: true # Also attach audio file alongside transcript# Health monitoring endpointhealth:
port: 9191# HTTP health check on this port# Message persistencestorage:
data_dir: ""# SQLite DB location; empty = working directory# Logginglogging:
level: "INFO"# DEBUG, INFO, WARNING, ERRORfile: ""# Log file path, or "" for stdout only

🤖 Chat Commands (Telegram adapter)

CommandDescription
/botList all available bots and show which one is active
/bot nameSwitch to a different bot
/botsShow all bots with live 🟢/⚫ online status
/statusDetailed info about the active bot

🎤 Voice-to-Text

When voice_to_text is configured, voice messages are transcribed before posting:

🎤 Hello, this is what I said in the voice message

If keep_audio: true, the original audio file is also attached.

Compatible APIs:


📊 Health Endpoint

curl http://localhost:9191/health
{
"status": "ok",
"version": "2.0.1",
"transport": "websocket",
"uptime": "2h15m30s",
"messages": { "tg_to_mm": 42, "mm_to_tg": 38, "errors": 0 },
"store": { "persistent_mappings": 156 }
}

🔧 Troubleshooting

ProblemSolution
FATAL: Token validation FAILEDPAT is invalid/expired. Create a new one in MM → Profile → Security → PAT. Also verify EnableUserAccessTokens is true.
⚠️ Token expirado alertSame — renew PAT, update mm_token in config.yaml, restart.
Zero DM channels discoveredMake sure you've DM'd each bot in MM at least once. Verify mm_bot_id values are correct (26 alphanumeric chars).
WS auth rejected (CLOSE on connect)The mattermost.bot_token is invalid. Get a valid one from Integrations → Bot Accounts.
OSError: [Errno 98] address already in useAnother process on health port. Change health.port in config.
[BotName] prefix on messagesNormal in multi-bot mode to identify which bot responded. Single bot = no prefix.
Voice not transcribedCheck voice_to_text.url is reachable. For OpenAI/Groq, verify api_key.
EnableUserAccessTokens keeps resettingSomething is toggling it. Lock the setting and audit admin access.

🛡️ Security

  • config.yaml contains secrets — it's in .gitignore, never commit it
  • PATs have your full user permissions — use a dedicated account if concerned
  • Health endpoint binds to 127.0.0.1 (not exposed externally)
  • Only users whose ID is in config can use the bridge
  • Message mappings stored in local SQLite (30-day auto-prune)

🔌 Writing a Custom Adapter

Create a new file in src/bridgemost/adapters/ that implements BaseAdapter:

frombridgemost.adapters.baseimportBaseAdapter, InboundMessage, OutboundMessageclassMyPlatformAdapter(BaseAdapter):
asyncdefstart(self): ...
asyncdefstop(self): ...
asyncdefsend_message(self, chat_id, msg: OutboundMessage) ->int|None: ...
asyncdefedit_message(self, chat_id, msg_id, text): ...
asyncdefdelete_message(self, chat_id, msg_id): ...
asyncdefset_reaction(self, chat_id, msg_id, emoji): ...
asyncdefclear_reactions(self, chat_id, msg_id): ...
defstart_typing_loop(self, chat_id): ...
defstop_typing_loop(self, chat_id): ...

The core engine handles all Mattermost interaction, message tracking, retry, and health monitoring.


📋 Changelog

VersionDateHighlight
v2.1.02026-03-25Google Chat adapter — Service Account ghost mode, polling, edit/delete/reactions
v2.0.22026-03-25README rewritten for multi-platform architecture
v2.0.12026-03-25Audit cleanup: platform-agnostic emoji names, encapsulation fix
v2.0.02026-03-25Plugin adapter architecture — Telegram extracted as adapter, core engine separated
v1.0.02026-03-25Stable release — PyPI, CI/CD, full test suite
v0.9.x2026-03-24/25Stickers, locations, polls, file relay, Docker, 71 tests
v0.8.x2026-03-24SQLite store, WS jitter, rate limiter, bot commands
v0.7.02026-03-247-bug audit, PAT health check, error alerts
v0.6.02026-03-24Interactive setup wizard
v0.5.02026-03-24Startup resilience, token validation
v0.4.02026-03-24Voice-to-text via Whisper
v0.3.x2026-03-24Multi-bot routing, synthetic typing
v0.2.02026-03-24Emoji/reaction relay
v0.1.x2026-03-24WebSocket transport, edit/delete sync
v0.0.52026-03-24First public release

See CHANGELOG.md for full details.


📄 License

MIT — see LICENSE

🙏 Built with

About

Multi-platform ↔ Mattermost transparent bridge. Messages appear as the real user. Plugin adapter architecture — Telegram ready, Google Chat/Slack/Matrix planned. Ghost mode.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

BridgeMost 👻

CIPyPI

Multi-Platform ↔ Mattermost Transparent Bridge

BridgeMost makes your messages from Telegram, Google Chat, or any supported platform appear natively in Mattermost — as your real user, with your avatar and name. Bot responses relay back instantly via WebSocket.

Unlike Matterbridge or webhooks that post with [User] prefixes, BridgeMost posts as your actual Mattermost account using Personal Access Tokens. Nobody in Mattermost can tell you're writing from another platform.

🔌 Supported Platforms (Adapters)

PlatformStatusDescription
Telegram✅ ProductionFull support — text, media, voice, reactions, edits, deletes
Google Chat✅ v2.1.0Workspace ghost mode via Service Account + domain-wide delegation
Slack🔜 PlannedUser token impersonation
Matrix🔜 PlannedApplication Service ghost mode

Plugin architecture (v2.0+): Each platform is an independent adapter module. Adding a new platform = one Python file implementing BaseAdapter. Zero changes to the core engine.

✨ Features

FeatureDescription
🪪 Transparent identityPosts as your real MM user (avatar, name, everything)
📁 Full mediaPhotos, documents, audio, video, voice — bidirectional
🎤 Voice-to-textVoice messages auto-transcribed via Whisper API
🤖 Multi-bot routingTalk to multiple MM bots; switch with /bridge bot <name>
📲 DM Bridge modeGive each MM bot its own dedicated TG bot — DM it directly (v2.2.0)
/️⃣ Hermes slash passthrough/new, /model, /help, etc. cross Telegram → Mattermost unchanged (v2.2.4)
🧠 Telegram clean modeTool chatter stays in MM; Telegram sees a neural-link placeholder + clean final response (v2.2.5)
↪️ Reply/thread syncTelegram replies map to Mattermost threads and threaded MM replies come back as native Telegram replies (v2.2.6)
⚡ Real-time WebSocketResponses arrive instantly (no polling)
✏️ Edit & delete syncEdits and deletes stay in sync both ways
😀 ReactionsEmoji reactions synced bidirectionally
⌨️ Typing indicatorSynthetic "Bot is typing..." on the chat side
📝 MarkdownMM markdown auto-converted to platform format
🔒 Startup checksValidates tokens + discovers channels before starting
💾 Persistent mappingSQLite store for message IDs (survives restarts)
🩺 Health endpointHTTP /health on configurable port
👥 Multi-userMultiple users, each with their own identity and bot routing
🐳 DockerMulti-stage image, ~55 MB

~55 MB RAM · ~250 ms latency · asyncio-based · Python 3.11+

Hermes slash commands over Telegram

BridgeMost now preserves generic slash commands when the upstream Mattermost bot is Hermes. That means commands like:

  • /new
  • /model
  • /help
  • /commands
  • /reasoning

arrive in Mattermost exactly as typed, instead of being swallowed by Telegram-side command handlers.

BridgeMost local command namespace

To avoid collisions with Hermes, BridgeMost keeps its own local controls under /bridge:

  • /bridge bot — list bots or switch the active relay target
  • /bridge bots — inspect available bot routes
  • /bridge status — inspect bridge-local status
  • /bridge help — show the local command help

Legacy /bot and /bots aliases still work in Telegram for compatibility, but /status is now reserved for Hermes passthrough.

Telegram clean mode (v2.2.5)

When the upstream Mattermost bot is Hermes, BridgeMost can now keep Telegram clean:

  • internal tool-progress posts (terminal:, execute_code:, skill_view:, etc.) stay in Mattermost
  • Telegram gets a placeholder such as 🧠⚡ Conectando a la red neuronal...
  • the placeholder is then edited in place into the real final answer
  • the final answer can be revealed progressively for a streaming-like UX

This behavior is configurable through telegram_presentation: in config.yaml.

Reply/thread sync (v2.2.6)

  • Replying to a Telegram message now posts into the corresponding Mattermost thread root
  • Replies emitted by Mattermost bots with root_id come back to Telegram as native replies when the root message is known
  • Clean mode preserves the reply target, so the placeholder and final edited answer stay visually attached to the original Telegram message

Multi-user ready: Multiple people can use the same BridgeMost instance — each with their own chat account, Mattermost identity, and bot routing. Add users to config.yaml and they appear as themselves in Mattermost. No shared accounts, no impersonation.


🏗️ Architecture (v2.0+)

┌──────────────┐
│ Telegram │─┐
├──────────────┤ │ ┌──────────────┐ ┌──────────────┐
│ Google Chat │─┼────────►│ BridgeMost │◄───────►│ Mattermost │
├──────────────┤ │ │ Core Engine │ WS+API │ (Bots) │
│ Slack │─┤ └──────────────┘ └──────────────┘
├──────────────┤ │ Adapters │ Core │ MM
│ Matrix │─┘
└──────────────┘

Three layers:

  1. Adapters — Platform-specific plugins (telegram.py, googlechat.py, etc.)
  2. Core Engine — Routing, mapping, sync, retry, health — platform-agnostic
  3. Mattermost Connector — WebSocket, REST API, file upload

Each adapter implements BaseAdapter (8 methods: start, stop, send_message, edit, delete, react, typing, clear_reactions).


🚀 Installation — Step by Step

What you need

#ItemWhere to get it
1Mattermost server (self-hosted)You must be admin or have an admin enable PAT support
2Chat platform bot tokenTelegram: @BotFather/newbot
3Your platform user IDTelegram: message @userinfobot
4Python 3.11+python3 --version to check
5Gitgit --version to check

Step 1 — Enable Personal Access Tokens on Mattermost

⚠️This step is REQUIRED. Without it, BridgeMost cannot post as your user.

Option A — Via Mattermost UI (admin):

  1. Go to System Console → Authentication → Token Access
  2. Set Enable Personal Access Tokens to true
  3. Save

Option B — Via command line (requires access to the server):

# If mmctl is available:
mmctl --local config set ServiceSettings.EnableUserAccessTokens true# Or via REST API with admin token:
curl -X PUT http://localhost:8065/api/v4/config/patch \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ServiceSettings": {"EnableUserAccessTokens": true}}'

Step 2 — Clone and install

git clone https://github.com/JuanjoPM-Developer/BridgeMost.git
cd BridgeMost
python3 -m venv .venv
source .venv/bin/activate
pip install -e .

Or via PyPI:

pip install bridgemost

Or via Docker:

docker compose up -d

Step 3 — Configure

Option A — Interactive wizard (recommended for Telegram):

python3 -m bridgemost setup

The wizard will:

  1. Connect to your Mattermost server
  2. Log you in (password is NOT stored)
  3. Auto-create a Personal Access Token for BridgeMost
  4. List all bots on the server — you pick which ones to bridge
  5. Ask for your platform bot token and user ID
  6. Generate config.yaml automatically

Option B — Manual configuration:

cp config.example.yaml config.yaml

Then edit config.yaml — see the Configuration Reference below.

Step 4 — Run

# Foreground (for testing):
python3 -m bridgemost
# Or as a systemd service (recommended for production):
sudo cp bridgemost.service.example /etc/systemd/system/bridgemost.service
# Edit the service file — update paths to match your installation
sudo systemctl daemon-reload
sudo systemctl enable --now bridgemost

Step 5 — Test

  1. Send a message from your chat platform to the BridgeMost bot
  2. The message should appear in Mattermost as your real user
  3. When the MM bot responds, the response should appear in your chat

⚙️ Configuration Reference

Minimal config.yaml (Telegram adapter)

telegram:
bot_token: "123456:ABC-DEF..."# From @BotFathermattermost:
url: "http://localhost:8065"# Your MM server URL (http or https)bot_token: "abc123..."# Any bot's access token (for WebSocket)bot_user_id: "a1b2c3d4..."# User ID of that botusers:
- telegram_id: 123456789# Your numeric platform user IDtelegram_name: "Your Name"# Display name (for logs only)mm_user_id: "x1y2z3..."# Your Mattermost user IDmm_token: "your-pat-here"# Your Personal Access Tokenbots:
- name: "mybot"# Friendly name (used with /bot command)mm_bot_id: "bot-user-id-here"# The bot's Mattermost user IDmm_dm_channel: ""# Leave empty — auto-discovered at startupdefault: true # First bot to talk to when bridge starts

How to find each value

FieldHow to get it
telegram.bot_token@BotFather/newbot → copy the token
telegram_idSend any message to @userinfobot
mattermost.urlThe URL you use to open Mattermost in your browser
mattermost.bot_tokenMM → Integrations → Bot Accounts → pick any bot → copy token. Or ask your admin.
mattermost.bot_user_idmmctl user search <botname> → copy id. Or: curl http://YOUR_MM/api/v4/users/username/<botname> -H "Authorization: Bearer TOKEN""id"
mm_user_idSame as above with your own username
mm_token (PAT)MM → Profile → Security → Personal Access Tokens → Create. Or wizard creates it.
mm_bot_idThe Mattermost user ID of each bot you want to talk to
mm_dm_channelLeave empty — auto-discovered at startup.

Optional sections

# Voice-to-text transcription (requires a Whisper-compatible API)voice_to_text:
url: "http://localhost:9000"# Whisper endpointapi_key: ""# For OpenAI/Groq; empty for local Whispermodel: "large-v3"# large-v3, whisper-1, whisper-large-v3-turbolanguage: ""# "es", "en", or "" for auto-detectkeep_audio: true # Also attach audio file alongside transcript# Health monitoring endpointhealth:
port: 9191# HTTP health check on this port# Message persistencestorage:
data_dir: ""# SQLite DB location; empty = working directory# Logginglogging:
level: "INFO"# DEBUG, INFO, WARNING, ERRORfile: ""# Log file path, or "" for stdout only

🤖 Chat Commands (Telegram adapter)

CommandDescription
/botList all available bots and show which one is active
/bot nameSwitch to a different bot
/botsShow all bots with live 🟢/⚫ online status
/statusDetailed info about the active bot

🎤 Voice-to-Text

When voice_to_text is configured, voice messages are transcribed before posting:

🎤 Hello, this is what I said in the voice message

If keep_audio: true, the original audio file is also attached.

Compatible APIs:


📊 Health Endpoint

curl http://localhost:9191/health
{
"status": "ok",
"version": "2.0.1",
"transport": "websocket",
"uptime": "2h15m30s",
"messages": { "tg_to_mm": 42, "mm_to_tg": 38, "errors": 0 },
"store": { "persistent_mappings": 156 }
}

🔧 Troubleshooting

ProblemSolution
FATAL: Token validation FAILEDPAT is invalid/expired. Create a new one in MM → Profile → Security → PAT. Also verify EnableUserAccessTokens is true.
⚠️ Token expirado alertSame — renew PAT, update mm_token in config.yaml, restart.
Zero DM channels discoveredMake sure you've DM'd each bot in MM at least once. Verify mm_bot_id values are correct (26 alphanumeric chars).
WS auth rejected (CLOSE on connect)The mattermost.bot_token is invalid. Get a valid one from Integrations → Bot Accounts.
OSError: [Errno 98] address already in useAnother process on health port. Change health.port in config.
[BotName] prefix on messagesNormal in multi-bot mode to identify which bot responded. Single bot = no prefix.
Voice not transcribedCheck voice_to_text.url is reachable. For OpenAI/Groq, verify api_key.
EnableUserAccessTokens keeps resettingSomething is toggling it. Lock the setting and audit admin access.

🛡️ Security

  • config.yaml contains secrets — it's in .gitignore, never commit it
  • PATs have your full user permissions — use a dedicated account if concerned
  • Health endpoint binds to 127.0.0.1 (not exposed externally)
  • Only users whose ID is in config can use the bridge
  • Message mappings stored in local SQLite (30-day auto-prune)

🔌 Writing a Custom Adapter

Create a new file in src/bridgemost/adapters/ that implements BaseAdapter:

frombridgemost.adapters.baseimportBaseAdapter, InboundMessage, OutboundMessageclassMyPlatformAdapter(BaseAdapter):
asyncdefstart(self): ...
asyncdefstop(self): ...
asyncdefsend_message(self, chat_id, msg: OutboundMessage) ->int|None: ...
asyncdefedit_message(self, chat_id, msg_id, text): ...
asyncdefdelete_message(self, chat_id, msg_id): ...
asyncdefset_reaction(self, chat_id, msg_id, emoji): ...
asyncdefclear_reactions(self, chat_id, msg_id): ...
defstart_typing_loop(self, chat_id): ...
defstop_typing_loop(self, chat_id): ...

The core engine handles all Mattermost interaction, message tracking, retry, and health monitoring.


📋 Changelog

VersionDateHighlight
v2.1.02026-03-25Google Chat adapter — Service Account ghost mode, polling, edit/delete/reactions
v2.0.22026-03-25README rewritten for multi-platform architecture
v2.0.12026-03-25Audit cleanup: platform-agnostic emoji names, encapsulation fix
v2.0.02026-03-25Plugin adapter architecture — Telegram extracted as adapter, core engine separated
v1.0.02026-03-25Stable release — PyPI, CI/CD, full test suite
v0.9.x2026-03-24/25Stickers, locations, polls, file relay, Docker, 71 tests
v0.8.x2026-03-24SQLite store, WS jitter, rate limiter, bot commands
v0.7.02026-03-247-bug audit, PAT health check, error alerts
v0.6.02026-03-24Interactive setup wizard
v0.5.02026-03-24Startup resilience, token validation
v0.4.02026-03-24Voice-to-text via Whisper
v0.3.x2026-03-24Multi-bot routing, synthetic typing
v0.2.02026-03-24Emoji/reaction relay
v0.1.x2026-03-24WebSocket transport, edit/delete sync
v0.0.52026-03-24First public release

See CHANGELOG.md for full details.


📄 License

MIT — see LICENSE

🙏 Built with

About

Multi-platform ↔ Mattermost transparent bridge. Messages appear as the real user. Plugin adapter architecture — Telegram ready, Google Chat/Slack/Matrix planned. Ghost mode.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

BridgeMost 👻

CIPyPI

Multi-Platform ↔ Mattermost Transparent Bridge

BridgeMost makes your messages from Telegram, Google Chat, or any supported platform appear natively in Mattermost — as your real user, with your avatar and name. Bot responses relay back instantly via WebSocket.

Unlike Matterbridge or webhooks that post with [User] prefixes, BridgeMost posts as your actual Mattermost account using Personal Access Tokens. Nobody in Mattermost can tell you're writing from another platform.

🔌 Supported Platforms (Adapters)

PlatformStatusDescription
Telegram✅ ProductionFull support — text, media, voice, reactions, edits, deletes
Google Chat✅ v2.1.0Workspace ghost mode via Service Account + domain-wide delegation
Slack🔜 PlannedUser token impersonation
Matrix🔜 PlannedApplication Service ghost mode

Plugin architecture (v2.0+): Each platform is an independent adapter module. Adding a new platform = one Python file implementing BaseAdapter. Zero changes to the core engine.

✨ Features

FeatureDescription
🪪 Transparent identityPosts as your real MM user (avatar, name, everything)
📁 Full mediaPhotos, documents, audio, video, voice — bidirectional
🎤 Voice-to-textVoice messages auto-transcribed via Whisper API
🤖 Multi-bot routingTalk to multiple MM bots; switch with /bridge bot <name>
📲 DM Bridge modeGive each MM bot its own dedicated TG bot — DM it directly (v2.2.0)
/️⃣ Hermes slash passthrough/new, /model, /help, etc. cross Telegram → Mattermost unchanged (v2.2.4)
🧠 Telegram clean modeTool chatter stays in MM; Telegram sees a neural-link placeholder + clean final response (v2.2.5)
↪️ Reply/thread syncTelegram replies map to Mattermost threads and threaded MM replies come back as native Telegram replies (v2.2.6)
⚡ Real-time WebSocketResponses arrive instantly (no polling)
✏️ Edit & delete syncEdits and deletes stay in sync both ways
😀 ReactionsEmoji reactions synced bidirectionally
⌨️ Typing indicatorSynthetic "Bot is typing..." on the chat side
📝 MarkdownMM markdown auto-converted to platform format
🔒 Startup checksValidates tokens + discovers channels before starting
💾 Persistent mappingSQLite store for message IDs (survives restarts)
🩺 Health endpointHTTP /health on configurable port
👥 Multi-userMultiple users, each with their own identity and bot routing
🐳 DockerMulti-stage image, ~55 MB

~55 MB RAM · ~250 ms latency · asyncio-based · Python 3.11+

Hermes slash commands over Telegram

BridgeMost now preserves generic slash commands when the upstream Mattermost bot is Hermes. That means commands like:

  • /new
  • /model
  • /help
  • /commands
  • /reasoning

arrive in Mattermost exactly as typed, instead of being swallowed by Telegram-side command handlers.

BridgeMost local command namespace

To avoid collisions with Hermes, BridgeMost keeps its own local controls under /bridge:

  • /bridge bot — list bots or switch the active relay target
  • /bridge bots — inspect available bot routes
  • /bridge status — inspect bridge-local status
  • /bridge help — show the local command help

Legacy /bot and /bots aliases still work in Telegram for compatibility, but /status is now reserved for Hermes passthrough.

Telegram clean mode (v2.2.5)

When the upstream Mattermost bot is Hermes, BridgeMost can now keep Telegram clean:

  • internal tool-progress posts (terminal:, execute_code:, skill_view:, etc.) stay in Mattermost
  • Telegram gets a placeholder such as 🧠⚡ Conectando a la red neuronal...
  • the placeholder is then edited in place into the real final answer
  • the final answer can be revealed progressively for a streaming-like UX

This behavior is configurable through telegram_presentation: in config.yaml.

Reply/thread sync (v2.2.6)

  • Replying to a Telegram message now posts into the corresponding Mattermost thread root
  • Replies emitted by Mattermost bots with root_id come back to Telegram as native replies when the root message is known
  • Clean mode preserves the reply target, so the placeholder and final edited answer stay visually attached to the original Telegram message

Multi-user ready: Multiple people can use the same BridgeMost instance — each with their own chat account, Mattermost identity, and bot routing. Add users to config.yaml and they appear as themselves in Mattermost. No shared accounts, no impersonation.


🏗️ Architecture (v2.0+)

┌──────────────┐
│ Telegram │─┐
├──────────────┤ │ ┌──────────────┐ ┌──────────────┐
│ Google Chat │─┼────────►│ BridgeMost │◄───────►│ Mattermost │
├──────────────┤ │ │ Core Engine │ WS+API │ (Bots) │
│ Slack │─┤ └──────────────┘ └──────────────┘
├──────────────┤ │ Adapters │ Core │ MM
│ Matrix │─┘
└──────────────┘

Three layers:

  1. Adapters — Platform-specific plugins (telegram.py, googlechat.py, etc.)
  2. Core Engine — Routing, mapping, sync, retry, health — platform-agnostic
  3. Mattermost Connector — WebSocket, REST API, file upload

Each adapter implements BaseAdapter (8 methods: start, stop, send_message, edit, delete, react, typing, clear_reactions).


🚀 Installation — Step by Step

What you need

#ItemWhere to get it
1Mattermost server (self-hosted)You must be admin or have an admin enable PAT support
2Chat platform bot tokenTelegram: @BotFather/newbot
3Your platform user IDTelegram: message @userinfobot
4Python 3.11+python3 --version to check
5Gitgit --version to check

Step 1 — Enable Personal Access Tokens on Mattermost

⚠️This step is REQUIRED. Without it, BridgeMost cannot post as your user.

Option A — Via Mattermost UI (admin):

  1. Go to System Console → Authentication → Token Access
  2. Set Enable Personal Access Tokens to true
  3. Save

Option B — Via command line (requires access to the server):

# If mmctl is available:
mmctl --local config set ServiceSettings.EnableUserAccessTokens true# Or via REST API with admin token:
curl -X PUT http://localhost:8065/api/v4/config/patch \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ServiceSettings": {"EnableUserAccessTokens": true}}'

Step 2 — Clone and install

git clone https://github.com/JuanjoPM-Developer/BridgeMost.git
cd BridgeMost
python3 -m venv .venv
source .venv/bin/activate
pip install -e .

Or via PyPI:

pip install bridgemost

Or via Docker:

docker compose up -d

Step 3 — Configure

Option A — Interactive wizard (recommended for Telegram):

python3 -m bridgemost setup

The wizard will:

  1. Connect to your Mattermost server
  2. Log you in (password is NOT stored)
  3. Auto-create a Personal Access Token for BridgeMost
  4. List all bots on the server — you pick which ones to bridge
  5. Ask for your platform bot token and user ID
  6. Generate config.yaml automatically

Option B — Manual configuration:

cp config.example.yaml config.yaml

Then edit config.yaml — see the Configuration Reference below.

Step 4 — Run

# Foreground (for testing):
python3 -m bridgemost
# Or as a systemd service (recommended for production):
sudo cp bridgemost.service.example /etc/systemd/system/bridgemost.service
# Edit the service file — update paths to match your installation
sudo systemctl daemon-reload
sudo systemctl enable --now bridgemost

Step 5 — Test

  1. Send a message from your chat platform to the BridgeMost bot
  2. The message should appear in Mattermost as your real user
  3. When the MM bot responds, the response should appear in your chat

⚙️ Configuration Reference

Minimal config.yaml (Telegram adapter)

telegram:
bot_token: "123456:ABC-DEF..."# From @BotFathermattermost:
url: "http://localhost:8065"# Your MM server URL (http or https)bot_token: "abc123..."# Any bot's access token (for WebSocket)bot_user_id: "a1b2c3d4..."# User ID of that botusers:
- telegram_id: 123456789# Your numeric platform user IDtelegram_name: "Your Name"# Display name (for logs only)mm_user_id: "x1y2z3..."# Your Mattermost user IDmm_token: "your-pat-here"# Your Personal Access Tokenbots:
- name: "mybot"# Friendly name (used with /bot command)mm_bot_id: "bot-user-id-here"# The bot's Mattermost user IDmm_dm_channel: ""# Leave empty — auto-discovered at startupdefault: true # First bot to talk to when bridge starts

How to find each value

FieldHow to get it
telegram.bot_token@BotFather/newbot → copy the token
telegram_idSend any message to @userinfobot
mattermost.urlThe URL you use to open Mattermost in your browser
mattermost.bot_tokenMM → Integrations → Bot Accounts → pick any bot → copy token. Or ask your admin.
mattermost.bot_user_idmmctl user search <botname> → copy id. Or: curl http://YOUR_MM/api/v4/users/username/<botname> -H "Authorization: Bearer TOKEN""id"
mm_user_idSame as above with your own username
mm_token (PAT)MM → Profile → Security → Personal Access Tokens → Create. Or wizard creates it.
mm_bot_idThe Mattermost user ID of each bot you want to talk to
mm_dm_channelLeave empty — auto-discovered at startup.

Optional sections

# Voice-to-text transcription (requires a Whisper-compatible API)voice_to_text:
url: "http://localhost:9000"# Whisper endpointapi_key: ""# For OpenAI/Groq; empty for local Whispermodel: "large-v3"# large-v3, whisper-1, whisper-large-v3-turbolanguage: ""# "es", "en", or "" for auto-detectkeep_audio: true # Also attach audio file alongside transcript# Health monitoring endpointhealth:
port: 9191# HTTP health check on this port# Message persistencestorage:
data_dir: ""# SQLite DB location; empty = working directory# Logginglogging:
level: "INFO"# DEBUG, INFO, WARNING, ERRORfile: ""# Log file path, or "" for stdout only

🤖 Chat Commands (Telegram adapter)

CommandDescription
/botList all available bots and show which one is active
/bot nameSwitch to a different bot
/botsShow all bots with live 🟢/⚫ online status
/statusDetailed info about the active bot

🎤 Voice-to-Text

When voice_to_text is configured, voice messages are transcribed before posting:

🎤 Hello, this is what I said in the voice message

If keep_audio: true, the original audio file is also attached.

Compatible APIs:


📊 Health Endpoint

curl http://localhost:9191/health
{
"status": "ok",
"version": "2.0.1",
"transport": "websocket",
"uptime": "2h15m30s",
"messages": { "tg_to_mm": 42, "mm_to_tg": 38, "errors": 0 },
"store": { "persistent_mappings": 156 }
}

🔧 Troubleshooting

ProblemSolution
FATAL: Token validation FAILEDPAT is invalid/expired. Create a new one in MM → Profile → Security → PAT. Also verify EnableUserAccessTokens is true.
⚠️ Token expirado alertSame — renew PAT, update mm_token in config.yaml, restart.
Zero DM channels discoveredMake sure you've DM'd each bot in MM at least once. Verify mm_bot_id values are correct (26 alphanumeric chars).
WS auth rejected (CLOSE on connect)The mattermost.bot_token is invalid. Get a valid one from Integrations → Bot Accounts.
OSError: [Errno 98] address already in useAnother process on health port. Change health.port in config.
[BotName] prefix on messagesNormal in multi-bot mode to identify which bot responded. Single bot = no prefix.
Voice not transcribedCheck voice_to_text.url is reachable. For OpenAI/Groq, verify api_key.
EnableUserAccessTokens keeps resettingSomething is toggling it. Lock the setting and audit admin access.

🛡️ Security

  • config.yaml contains secrets — it's in .gitignore, never commit it
  • PATs have your full user permissions — use a dedicated account if concerned
  • Health endpoint binds to 127.0.0.1 (not exposed externally)
  • Only users whose ID is in config can use the bridge
  • Message mappings stored in local SQLite (30-day auto-prune)

🔌 Writing a Custom Adapter

Create a new file in src/bridgemost/adapters/ that implements BaseAdapter:

frombridgemost.adapters.baseimportBaseAdapter, InboundMessage, OutboundMessageclassMyPlatformAdapter(BaseAdapter):
asyncdefstart(self): ...
asyncdefstop(self): ...
asyncdefsend_message(self, chat_id, msg: OutboundMessage) ->int|None: ...
asyncdefedit_message(self, chat_id, msg_id, text): ...
asyncdefdelete_message(self, chat_id, msg_id): ...
asyncdefset_reaction(self, chat_id, msg_id, emoji): ...
asyncdefclear_reactions(self, chat_id, msg_id): ...
defstart_typing_loop(self, chat_id): ...
defstop_typing_loop(self, chat_id): ...

The core engine handles all Mattermost interaction, message tracking, retry, and health monitoring.


📋 Changelog

VersionDateHighlight
v2.1.02026-03-25Google Chat adapter — Service Account ghost mode, polling, edit/delete/reactions
v2.0.22026-03-25README rewritten for multi-platform architecture
v2.0.12026-03-25Audit cleanup: platform-agnostic emoji names, encapsulation fix
v2.0.02026-03-25Plugin adapter architecture — Telegram extracted as adapter, core engine separated
v1.0.02026-03-25Stable release — PyPI, CI/CD, full test suite
v0.9.x2026-03-24/25Stickers, locations, polls, file relay, Docker, 71 tests
v0.8.x2026-03-24SQLite store, WS jitter, rate limiter, bot commands
v0.7.02026-03-247-bug audit, PAT health check, error alerts
v0.6.02026-03-24Interactive setup wizard
v0.5.02026-03-24Startup resilience, token validation
v0.4.02026-03-24Voice-to-text via Whisper
v0.3.x2026-03-24Multi-bot routing, synthetic typing
v0.2.02026-03-24Emoji/reaction relay
v0.1.x2026-03-24WebSocket transport, edit/delete sync
v0.0.52026-03-24First public release

See CHANGELOG.md for full details.


📄 License

MIT — see LICENSE

🙏 Built with

About

Multi-platform ↔ Mattermost transparent bridge. Messages appear as the real user. Plugin adapter architecture — Telegram ready, Google Chat/Slack/Matrix planned. Ghost mode.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

BridgeMost 👻

CIPyPI

Multi-Platform ↔ Mattermost Transparent Bridge

BridgeMost makes your messages from Telegram, Google Chat, or any supported platform appear natively in Mattermost — as your real user, with your avatar and name. Bot responses relay back instantly via WebSocket.

Unlike Matterbridge or webhooks that post with [User] prefixes, BridgeMost posts as your actual Mattermost account using Personal Access Tokens. Nobody in Mattermost can tell you're writing from another platform.

🔌 Supported Platforms (Adapters)

PlatformStatusDescription
Telegram✅ ProductionFull support — text, media, voice, reactions, edits, deletes
Google Chat✅ v2.1.0Workspace ghost mode via Service Account + domain-wide delegation
Slack🔜 PlannedUser token impersonation
Matrix🔜 PlannedApplication Service ghost mode

Plugin architecture (v2.0+): Each platform is an independent adapter module. Adding a new platform = one Python file implementing BaseAdapter. Zero changes to the core engine.

✨ Features

FeatureDescription
🪪 Transparent identityPosts as your real MM user (avatar, name, everything)
📁 Full mediaPhotos, documents, audio, video, voice — bidirectional
🎤 Voice-to-textVoice messages auto-transcribed via Whisper API
🤖 Multi-bot routingTalk to multiple MM bots; switch with /bridge bot <name>
📲 DM Bridge modeGive each MM bot its own dedicated TG bot — DM it directly (v2.2.0)
/️⃣ Hermes slash passthrough/new, /model, /help, etc. cross Telegram → Mattermost unchanged (v2.2.4)
🧠 Telegram clean modeTool chatter stays in MM; Telegram sees a neural-link placeholder + clean final response (v2.2.5)
↪️ Reply/thread syncTelegram replies map to Mattermost threads and threaded MM replies come back as native Telegram replies (v2.2.6)
⚡ Real-time WebSocketResponses arrive instantly (no polling)
✏️ Edit & delete syncEdits and deletes stay in sync both ways
😀 ReactionsEmoji reactions synced bidirectionally
⌨️ Typing indicatorSynthetic "Bot is typing..." on the chat side
📝 MarkdownMM markdown auto-converted to platform format
🔒 Startup checksValidates tokens + discovers channels before starting
💾 Persistent mappingSQLite store for message IDs (survives restarts)
🩺 Health endpointHTTP /health on configurable port
👥 Multi-userMultiple users, each with their own identity and bot routing
🐳 DockerMulti-stage image, ~55 MB

~55 MB RAM · ~250 ms latency · asyncio-based · Python 3.11+

Hermes slash commands over Telegram

BridgeMost now preserves generic slash commands when the upstream Mattermost bot is Hermes. That means commands like:

  • /new
  • /model
  • /help
  • /commands
  • /reasoning

arrive in Mattermost exactly as typed, instead of being swallowed by Telegram-side command handlers.

BridgeMost local command namespace

To avoid collisions with Hermes, BridgeMost keeps its own local controls under /bridge:

  • /bridge bot — list bots or switch the active relay target
  • /bridge bots — inspect available bot routes
  • /bridge status — inspect bridge-local status
  • /bridge help — show the local command help

Legacy /bot and /bots aliases still work in Telegram for compatibility, but /status is now reserved for Hermes passthrough.

Telegram clean mode (v2.2.5)

When the upstream Mattermost bot is Hermes, BridgeMost can now keep Telegram clean:

  • internal tool-progress posts (terminal:, execute_code:, skill_view:, etc.) stay in Mattermost
  • Telegram gets a placeholder such as 🧠⚡ Conectando a la red neuronal...
  • the placeholder is then edited in place into the real final answer
  • the final answer can be revealed progressively for a streaming-like UX

This behavior is configurable through telegram_presentation: in config.yaml.

Reply/thread sync (v2.2.6)

  • Replying to a Telegram message now posts into the corresponding Mattermost thread root
  • Replies emitted by Mattermost bots with root_id come back to Telegram as native replies when the root message is known
  • Clean mode preserves the reply target, so the placeholder and final edited answer stay visually attached to the original Telegram message

Multi-user ready: Multiple people can use the same BridgeMost instance — each with their own chat account, Mattermost identity, and bot routing. Add users to config.yaml and they appear as themselves in Mattermost. No shared accounts, no impersonation.


🏗️ Architecture (v2.0+)

┌──────────────┐
│ Telegram │─┐
├──────────────┤ │ ┌──────────────┐ ┌──────────────┐
│ Google Chat │─┼────────►│ BridgeMost │◄───────►│ Mattermost │
├──────────────┤ │ │ Core Engine │ WS+API │ (Bots) │
│ Slack │─┤ └──────────────┘ └──────────────┘
├──────────────┤ │ Adapters │ Core │ MM
│ Matrix │─┘
└──────────────┘

Three layers:

  1. Adapters — Platform-specific plugins (telegram.py, googlechat.py, etc.)
  2. Core Engine — Routing, mapping, sync, retry, health — platform-agnostic
  3. Mattermost Connector — WebSocket, REST API, file upload

Each adapter implements BaseAdapter (8 methods: start, stop, send_message, edit, delete, react, typing, clear_reactions).


🚀 Installation — Step by Step

What you need

#ItemWhere to get it
1Mattermost server (self-hosted)You must be admin or have an admin enable PAT support
2Chat platform bot tokenTelegram: @BotFather/newbot
3Your platform user IDTelegram: message @userinfobot
4Python 3.11+python3 --version to check
5Gitgit --version to check

Step 1 — Enable Personal Access Tokens on Mattermost

⚠️This step is REQUIRED. Without it, BridgeMost cannot post as your user.

Option A — Via Mattermost UI (admin):

  1. Go to System Console → Authentication → Token Access
  2. Set Enable Personal Access Tokens to true
  3. Save

Option B — Via command line (requires access to the server):

# If mmctl is available:
mmctl --local config set ServiceSettings.EnableUserAccessTokens true# Or via REST API with admin token:
curl -X PUT http://localhost:8065/api/v4/config/patch \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ServiceSettings": {"EnableUserAccessTokens": true}}'

Step 2 — Clone and install

git clone https://github.com/JuanjoPM-Developer/BridgeMost.git
cd BridgeMost
python3 -m venv .venv
source .venv/bin/activate
pip install -e .

Or via PyPI:

pip install bridgemost

Or via Docker:

docker compose up -d

Step 3 — Configure

Option A — Interactive wizard (recommended for Telegram):

python3 -m bridgemost setup

The wizard will:

  1. Connect to your Mattermost server
  2. Log you in (password is NOT stored)
  3. Auto-create a Personal Access Token for BridgeMost
  4. List all bots on the server — you pick which ones to bridge
  5. Ask for your platform bot token and user ID
  6. Generate config.yaml automatically

Option B — Manual configuration:

cp config.example.yaml config.yaml

Then edit config.yaml — see the Configuration Reference below.

Step 4 — Run

# Foreground (for testing):
python3 -m bridgemost
# Or as a systemd service (recommended for production):
sudo cp bridgemost.service.example /etc/systemd/system/bridgemost.service
# Edit the service file — update paths to match your installation
sudo systemctl daemon-reload
sudo systemctl enable --now bridgemost

Step 5 — Test

  1. Send a message from your chat platform to the BridgeMost bot
  2. The message should appear in Mattermost as your real user
  3. When the MM bot responds, the response should appear in your chat

⚙️ Configuration Reference

Minimal config.yaml (Telegram adapter)

telegram:
bot_token: "123456:ABC-DEF..."# From @BotFathermattermost:
url: "http://localhost:8065"# Your MM server URL (http or https)bot_token: "abc123..."# Any bot's access token (for WebSocket)bot_user_id: "a1b2c3d4..."# User ID of that botusers:
- telegram_id: 123456789# Your numeric platform user IDtelegram_name: "Your Name"# Display name (for logs only)mm_user_id: "x1y2z3..."# Your Mattermost user IDmm_token: "your-pat-here"# Your Personal Access Tokenbots:
- name: "mybot"# Friendly name (used with /bot command)mm_bot_id: "bot-user-id-here"# The bot's Mattermost user IDmm_dm_channel: ""# Leave empty — auto-discovered at startupdefault: true # First bot to talk to when bridge starts

How to find each value

FieldHow to get it
telegram.bot_token@BotFather/newbot → copy the token
telegram_idSend any message to @userinfobot
mattermost.urlThe URL you use to open Mattermost in your browser
mattermost.bot_tokenMM → Integrations → Bot Accounts → pick any bot → copy token. Or ask your admin.
mattermost.bot_user_idmmctl user search <botname> → copy id. Or: curl http://YOUR_MM/api/v4/users/username/<botname> -H "Authorization: Bearer TOKEN""id"
mm_user_idSame as above with your own username
mm_token (PAT)MM → Profile → Security → Personal Access Tokens → Create. Or wizard creates it.
mm_bot_idThe Mattermost user ID of each bot you want to talk to
mm_dm_channelLeave empty — auto-discovered at startup.

Optional sections

# Voice-to-text transcription (requires a Whisper-compatible API)voice_to_text:
url: "http://localhost:9000"# Whisper endpointapi_key: ""# For OpenAI/Groq; empty for local Whispermodel: "large-v3"# large-v3, whisper-1, whisper-large-v3-turbolanguage: ""# "es", "en", or "" for auto-detectkeep_audio: true # Also attach audio file alongside transcript# Health monitoring endpointhealth:
port: 9191# HTTP health check on this port# Message persistencestorage:
data_dir: ""# SQLite DB location; empty = working directory# Logginglogging:
level: "INFO"# DEBUG, INFO, WARNING, ERRORfile: ""# Log file path, or "" for stdout only

🤖 Chat Commands (Telegram adapter)

CommandDescription
/botList all available bots and show which one is active
/bot nameSwitch to a different bot
/botsShow all bots with live 🟢/⚫ online status
/statusDetailed info about the active bot

🎤 Voice-to-Text

When voice_to_text is configured, voice messages are transcribed before posting:

🎤 Hello, this is what I said in the voice message

If keep_audio: true, the original audio file is also attached.

Compatible APIs:


📊 Health Endpoint

curl http://localhost:9191/health
{
"status": "ok",
"version": "2.0.1",
"transport": "websocket",
"uptime": "2h15m30s",
"messages": { "tg_to_mm": 42, "mm_to_tg": 38, "errors": 0 },
"store": { "persistent_mappings": 156 }
}

🔧 Troubleshooting

ProblemSolution
FATAL: Token validation FAILEDPAT is invalid/expired. Create a new one in MM → Profile → Security → PAT. Also verify EnableUserAccessTokens is true.
⚠️ Token expirado alertSame — renew PAT, update mm_token in config.yaml, restart.
Zero DM channels discoveredMake sure you've DM'd each bot in MM at least once. Verify mm_bot_id values are correct (26 alphanumeric chars).
WS auth rejected (CLOSE on connect)The mattermost.bot_token is invalid. Get a valid one from Integrations → Bot Accounts.
OSError: [Errno 98] address already in useAnother process on health port. Change health.port in config.
[BotName] prefix on messagesNormal in multi-bot mode to identify which bot responded. Single bot = no prefix.
Voice not transcribedCheck voice_to_text.url is reachable. For OpenAI/Groq, verify api_key.
EnableUserAccessTokens keeps resettingSomething is toggling it. Lock the setting and audit admin access.

🛡️ Security

  • config.yaml contains secrets — it's in .gitignore, never commit it
  • PATs have your full user permissions — use a dedicated account if concerned
  • Health endpoint binds to 127.0.0.1 (not exposed externally)
  • Only users whose ID is in config can use the bridge
  • Message mappings stored in local SQLite (30-day auto-prune)

🔌 Writing a Custom Adapter

Create a new file in src/bridgemost/adapters/ that implements BaseAdapter:

frombridgemost.adapters.baseimportBaseAdapter, InboundMessage, OutboundMessageclassMyPlatformAdapter(BaseAdapter):
asyncdefstart(self): ...
asyncdefstop(self): ...
asyncdefsend_message(self, chat_id, msg: OutboundMessage) ->int|None: ...
asyncdefedit_message(self, chat_id, msg_id, text): ...
asyncdefdelete_message(self, chat_id, msg_id): ...
asyncdefset_reaction(self, chat_id, msg_id, emoji): ...
asyncdefclear_reactions(self, chat_id, msg_id): ...
defstart_typing_loop(self, chat_id): ...
defstop_typing_loop(self, chat_id): ...

The core engine handles all Mattermost interaction, message tracking, retry, and health monitoring.


📋 Changelog

VersionDateHighlight
v2.1.02026-03-25Google Chat adapter — Service Account ghost mode, polling, edit/delete/reactions
v2.0.22026-03-25README rewritten for multi-platform architecture
v2.0.12026-03-25Audit cleanup: platform-agnostic emoji names, encapsulation fix
v2.0.02026-03-25Plugin adapter architecture — Telegram extracted as adapter, core engine separated
v1.0.02026-03-25Stable release — PyPI, CI/CD, full test suite
v0.9.x2026-03-24/25Stickers, locations, polls, file relay, Docker, 71 tests
v0.8.x2026-03-24SQLite store, WS jitter, rate limiter, bot commands
v0.7.02026-03-247-bug audit, PAT health check, error alerts
v0.6.02026-03-24Interactive setup wizard
v0.5.02026-03-24Startup resilience, token validation
v0.4.02026-03-24Voice-to-text via Whisper
v0.3.x2026-03-24Multi-bot routing, synthetic typing
v0.2.02026-03-24Emoji/reaction relay
v0.1.x2026-03-24WebSocket transport, edit/delete sync
v0.0.52026-03-24First public release

See CHANGELOG.md for full details.


📄 License

MIT — see LICENSE

🙏 Built with

About

Multi-platform ↔ Mattermost transparent bridge. Messages appear as the real user. Plugin adapter architecture — Telegram ready, Google Chat/Slack/Matrix planned. Ghost mode.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

BridgeMost 👻

CIPyPI

Multi-Platform ↔ Mattermost Transparent Bridge

BridgeMost makes your messages from Telegram, Google Chat, or any supported platform appear natively in Mattermost — as your real user, with your avatar and name. Bot responses relay back instantly via WebSocket.

Unlike Matterbridge or webhooks that post with [User] prefixes, BridgeMost posts as your actual Mattermost account using Personal Access Tokens. Nobody in Mattermost can tell you're writing from another platform.

🔌 Supported Platforms (Adapters)

PlatformStatusDescription
Telegram✅ ProductionFull support — text, media, voice, reactions, edits, deletes
Google Chat✅ v2.1.0Workspace ghost mode via Service Account + domain-wide delegation
Slack🔜 PlannedUser token impersonation
Matrix🔜 PlannedApplication Service ghost mode

Plugin architecture (v2.0+): Each platform is an independent adapter module. Adding a new platform = one Python file implementing BaseAdapter. Zero changes to the core engine.

✨ Features

FeatureDescription
🪪 Transparent identityPosts as your real MM user (avatar, name, everything)
📁 Full mediaPhotos, documents, audio, video, voice — bidirectional
🎤 Voice-to-textVoice messages auto-transcribed via Whisper API
🤖 Multi-bot routingTalk to multiple MM bots; switch with /bridge bot <name>
📲 DM Bridge modeGive each MM bot its own dedicated TG bot — DM it directly (v2.2.0)
/️⃣ Hermes slash passthrough/new, /model, /help, etc. cross Telegram → Mattermost unchanged (v2.2.4)
🧠 Telegram clean modeTool chatter stays in MM; Telegram sees a neural-link placeholder + clean final response (v2.2.5)
↪️ Reply/thread syncTelegram replies map to Mattermost threads and threaded MM replies come back as native Telegram replies (v2.2.6)
⚡ Real-time WebSocketResponses arrive instantly (no polling)
✏️ Edit & delete syncEdits and deletes stay in sync both ways
😀 ReactionsEmoji reactions synced bidirectionally
⌨️ Typing indicatorSynthetic "Bot is typing..." on the chat side
📝 MarkdownMM markdown auto-converted to platform format
🔒 Startup checksValidates tokens + discovers channels before starting
💾 Persistent mappingSQLite store for message IDs (survives restarts)
🩺 Health endpointHTTP /health on configurable port
👥 Multi-userMultiple users, each with their own identity and bot routing
🐳 DockerMulti-stage image, ~55 MB

~55 MB RAM · ~250 ms latency · asyncio-based · Python 3.11+

Hermes slash commands over Telegram

BridgeMost now preserves generic slash commands when the upstream Mattermost bot is Hermes. That means commands like:

  • /new
  • /model
  • /help
  • /commands
  • /reasoning

arrive in Mattermost exactly as typed, instead of being swallowed by Telegram-side command handlers.

BridgeMost local command namespace

To avoid collisions with Hermes, BridgeMost keeps its own local controls under /bridge:

  • /bridge bot — list bots or switch the active relay target
  • /bridge bots — inspect available bot routes
  • /bridge status — inspect bridge-local status
  • /bridge help — show the local command help

Legacy /bot and /bots aliases still work in Telegram for compatibility, but /status is now reserved for Hermes passthrough.

Telegram clean mode (v2.2.5)

When the upstream Mattermost bot is Hermes, BridgeMost can now keep Telegram clean:

  • internal tool-progress posts (terminal:, execute_code:, skill_view:, etc.) stay in Mattermost
  • Telegram gets a placeholder such as 🧠⚡ Conectando a la red neuronal...
  • the placeholder is then edited in place into the real final answer
  • the final answer can be revealed progressively for a streaming-like UX

This behavior is configurable through telegram_presentation: in config.yaml.

Reply/thread sync (v2.2.6)

  • Replying to a Telegram message now posts into the corresponding Mattermost thread root
  • Replies emitted by Mattermost bots with root_id come back to Telegram as native replies when the root message is known
  • Clean mode preserves the reply target, so the placeholder and final edited answer stay visually attached to the original Telegram message

Multi-user ready: Multiple people can use the same BridgeMost instance — each with their own chat account, Mattermost identity, and bot routing. Add users to config.yaml and they appear as themselves in Mattermost. No shared accounts, no impersonation.


🏗️ Architecture (v2.0+)

┌──────────────┐
│ Telegram │─┐
├──────────────┤ │ ┌──────────────┐ ┌──────────────┐
│ Google Chat │─┼────────►│ BridgeMost │◄───────►│ Mattermost │
├──────────────┤ │ │ Core Engine │ WS+API │ (Bots) │
│ Slack │─┤ └──────────────┘ └──────────────┘
├──────────────┤ │ Adapters │ Core │ MM
│ Matrix │─┘
└──────────────┘

Three layers:

  1. Adapters — Platform-specific plugins (telegram.py, googlechat.py, etc.)
  2. Core Engine — Routing, mapping, sync, retry, health — platform-agnostic
  3. Mattermost Connector — WebSocket, REST API, file upload

Each adapter implements BaseAdapter (8 methods: start, stop, send_message, edit, delete, react, typing, clear_reactions).


🚀 Installation — Step by Step

What you need

#ItemWhere to get it
1Mattermost server (self-hosted)You must be admin or have an admin enable PAT support
2Chat platform bot tokenTelegram: @BotFather/newbot
3Your platform user IDTelegram: message @userinfobot
4Python 3.11+python3 --version to check
5Gitgit --version to check

Step 1 — Enable Personal Access Tokens on Mattermost

⚠️This step is REQUIRED. Without it, BridgeMost cannot post as your user.

Option A — Via Mattermost UI (admin):

  1. Go to System Console → Authentication → Token Access
  2. Set Enable Personal Access Tokens to true
  3. Save

Option B — Via command line (requires access to the server):

# If mmctl is available:
mmctl --local config set ServiceSettings.EnableUserAccessTokens true# Or via REST API with admin token:
curl -X PUT http://localhost:8065/api/v4/config/patch \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ServiceSettings": {"EnableUserAccessTokens": true}}'

Step 2 — Clone and install

git clone https://github.com/JuanjoPM-Developer/BridgeMost.git
cd BridgeMost
python3 -m venv .venv
source .venv/bin/activate
pip install -e .

Or via PyPI:

pip install bridgemost

Or via Docker:

docker compose up -d

Step 3 — Configure

Option A — Interactive wizard (recommended for Telegram):

python3 -m bridgemost setup

The wizard will:

  1. Connect to your Mattermost server
  2. Log you in (password is NOT stored)
  3. Auto-create a Personal Access Token for BridgeMost
  4. List all bots on the server — you pick which ones to bridge
  5. Ask for your platform bot token and user ID
  6. Generate config.yaml automatically

Option B — Manual configuration:

cp config.example.yaml config.yaml

Then edit config.yaml — see the Configuration Reference below.

Step 4 — Run

# Foreground (for testing):
python3 -m bridgemost
# Or as a systemd service (recommended for production):
sudo cp bridgemost.service.example /etc/systemd/system/bridgemost.service
# Edit the service file — update paths to match your installation
sudo systemctl daemon-reload
sudo systemctl enable --now bridgemost

Step 5 — Test

  1. Send a message from your chat platform to the BridgeMost bot
  2. The message should appear in Mattermost as your real user
  3. When the MM bot responds, the response should appear in your chat

⚙️ Configuration Reference

Minimal config.yaml (Telegram adapter)

telegram:
bot_token: "123456:ABC-DEF..."# From @BotFathermattermost:
url: "http://localhost:8065"# Your MM server URL (http or https)bot_token: "abc123..."# Any bot's access token (for WebSocket)bot_user_id: "a1b2c3d4..."# User ID of that botusers:
- telegram_id: 123456789# Your numeric platform user IDtelegram_name: "Your Name"# Display name (for logs only)mm_user_id: "x1y2z3..."# Your Mattermost user IDmm_token: "your-pat-here"# Your Personal Access Tokenbots:
- name: "mybot"# Friendly name (used with /bot command)mm_bot_id: "bot-user-id-here"# The bot's Mattermost user IDmm_dm_channel: ""# Leave empty — auto-discovered at startupdefault: true # First bot to talk to when bridge starts

How to find each value

FieldHow to get it
telegram.bot_token@BotFather/newbot → copy the token
telegram_idSend any message to @userinfobot
mattermost.urlThe URL you use to open Mattermost in your browser
mattermost.bot_tokenMM → Integrations → Bot Accounts → pick any bot → copy token. Or ask your admin.
mattermost.bot_user_idmmctl user search <botname> → copy id. Or: curl http://YOUR_MM/api/v4/users/username/<botname> -H "Authorization: Bearer TOKEN""id"
mm_user_idSame as above with your own username
mm_token (PAT)MM → Profile → Security → Personal Access Tokens → Create. Or wizard creates it.
mm_bot_idThe Mattermost user ID of each bot you want to talk to
mm_dm_channelLeave empty — auto-discovered at startup.

Optional sections

# Voice-to-text transcription (requires a Whisper-compatible API)voice_to_text:
url: "http://localhost:9000"# Whisper endpointapi_key: ""# For OpenAI/Groq; empty for local Whispermodel: "large-v3"# large-v3, whisper-1, whisper-large-v3-turbolanguage: ""# "es", "en", or "" for auto-detectkeep_audio: true # Also attach audio file alongside transcript# Health monitoring endpointhealth:
port: 9191# HTTP health check on this port# Message persistencestorage:
data_dir: ""# SQLite DB location; empty = working directory# Logginglogging:
level: "INFO"# DEBUG, INFO, WARNING, ERRORfile: ""# Log file path, or "" for stdout only

🤖 Chat Commands (Telegram adapter)

CommandDescription
/botList all available bots and show which one is active
/bot nameSwitch to a different bot
/botsShow all bots with live 🟢/⚫ online status
/statusDetailed info about the active bot

🎤 Voice-to-Text

When voice_to_text is configured, voice messages are transcribed before posting:

🎤 Hello, this is what I said in the voice message

If keep_audio: true, the original audio file is also attached.

Compatible APIs:


📊 Health Endpoint

curl http://localhost:9191/health
{
"status": "ok",
"version": "2.0.1",
"transport": "websocket",
"uptime": "2h15m30s",
"messages": { "tg_to_mm": 42, "mm_to_tg": 38, "errors": 0 },
"store": { "persistent_mappings": 156 }
}

🔧 Troubleshooting

ProblemSolution
FATAL: Token validation FAILEDPAT is invalid/expired. Create a new one in MM → Profile → Security → PAT. Also verify EnableUserAccessTokens is true.
⚠️ Token expirado alertSame — renew PAT, update mm_token in config.yaml, restart.
Zero DM channels discoveredMake sure you've DM'd each bot in MM at least once. Verify mm_bot_id values are correct (26 alphanumeric chars).
WS auth rejected (CLOSE on connect)The mattermost.bot_token is invalid. Get a valid one from Integrations → Bot Accounts.
OSError: [Errno 98] address already in useAnother process on health port. Change health.port in config.
[BotName] prefix on messagesNormal in multi-bot mode to identify which bot responded. Single bot = no prefix.
Voice not transcribedCheck voice_to_text.url is reachable. For OpenAI/Groq, verify api_key.
EnableUserAccessTokens keeps resettingSomething is toggling it. Lock the setting and audit admin access.

🛡️ Security

  • config.yaml contains secrets — it's in .gitignore, never commit it
  • PATs have your full user permissions — use a dedicated account if concerned
  • Health endpoint binds to 127.0.0.1 (not exposed externally)
  • Only users whose ID is in config can use the bridge
  • Message mappings stored in local SQLite (30-day auto-prune)

🔌 Writing a Custom Adapter

Create a new file in src/bridgemost/adapters/ that implements BaseAdapter:

frombridgemost.adapters.baseimportBaseAdapter, InboundMessage, OutboundMessageclassMyPlatformAdapter(BaseAdapter):
asyncdefstart(self): ...
asyncdefstop(self): ...
asyncdefsend_message(self, chat_id, msg: OutboundMessage) ->int|None: ...
asyncdefedit_message(self, chat_id, msg_id, text): ...
asyncdefdelete_message(self, chat_id, msg_id): ...
asyncdefset_reaction(self, chat_id, msg_id, emoji): ...
asyncdefclear_reactions(self, chat_id, msg_id): ...
defstart_typing_loop(self, chat_id): ...
defstop_typing_loop(self, chat_id): ...

The core engine handles all Mattermost interaction, message tracking, retry, and health monitoring.


📋 Changelog

VersionDateHighlight
v2.1.02026-03-25Google Chat adapter — Service Account ghost mode, polling, edit/delete/reactions
v2.0.22026-03-25README rewritten for multi-platform architecture
v2.0.12026-03-25Audit cleanup: platform-agnostic emoji names, encapsulation fix
v2.0.02026-03-25Plugin adapter architecture — Telegram extracted as adapter, core engine separated
v1.0.02026-03-25Stable release — PyPI, CI/CD, full test suite
v0.9.x2026-03-24/25Stickers, locations, polls, file relay, Docker, 71 tests
v0.8.x2026-03-24SQLite store, WS jitter, rate limiter, bot commands
v0.7.02026-03-247-bug audit, PAT health check, error alerts
v0.6.02026-03-24Interactive setup wizard
v0.5.02026-03-24Startup resilience, token validation
v0.4.02026-03-24Voice-to-text via Whisper
v0.3.x2026-03-24Multi-bot routing, synthetic typing
v0.2.02026-03-24Emoji/reaction relay
v0.1.x2026-03-24WebSocket transport, edit/delete sync
v0.0.52026-03-24First public release

See CHANGELOG.md for full details.


📄 License

MIT — see LICENSE

🙏 Built with

About

Multi-platform ↔ Mattermost transparent bridge. Messages appear as the real user. Plugin adapter architecture — Telegram ready, Google Chat/Slack/Matrix planned. Ghost mode.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

BridgeMost 👻

CIPyPI

Multi-Platform ↔ Mattermost Transparent Bridge

BridgeMost makes your messages from Telegram, Google Chat, or any supported platform appear natively in Mattermost — as your real user, with your avatar and name. Bot responses relay back instantly via WebSocket.

Unlike Matterbridge or webhooks that post with [User] prefixes, BridgeMost posts as your actual Mattermost account using Personal Access Tokens. Nobody in Mattermost can tell you're writing from another platform.

🔌 Supported Platforms (Adapters)

PlatformStatusDescription
Telegram✅ ProductionFull support — text, media, voice, reactions, edits, deletes
Google Chat✅ v2.1.0Workspace ghost mode via Service Account + domain-wide delegation
Slack🔜 PlannedUser token impersonation
Matrix🔜 PlannedApplication Service ghost mode

Plugin architecture (v2.0+): Each platform is an independent adapter module. Adding a new platform = one Python file implementing BaseAdapter. Zero changes to the core engine.

✨ Features

FeatureDescription
🪪 Transparent identityPosts as your real MM user (avatar, name, everything)
📁 Full mediaPhotos, documents, audio, video, voice — bidirectional
🎤 Voice-to-textVoice messages auto-transcribed via Whisper API
🤖 Multi-bot routingTalk to multiple MM bots; switch with /bridge bot <name>
📲 DM Bridge modeGive each MM bot its own dedicated TG bot — DM it directly (v2.2.0)
/️⃣ Hermes slash passthrough/new, /model, /help, etc. cross Telegram → Mattermost unchanged (v2.2.4)
🧠 Telegram clean modeTool chatter stays in MM; Telegram sees a neural-link placeholder + clean final response (v2.2.5)
↪️ Reply/thread syncTelegram replies map to Mattermost threads and threaded MM replies come back as native Telegram replies (v2.2.6)
⚡ Real-time WebSocketResponses arrive instantly (no polling)
✏️ Edit & delete syncEdits and deletes stay in sync both ways
😀 ReactionsEmoji reactions synced bidirectionally
⌨️ Typing indicatorSynthetic "Bot is typing..." on the chat side
📝 MarkdownMM markdown auto-converted to platform format
🔒 Startup checksValidates tokens + discovers channels before starting
💾 Persistent mappingSQLite store for message IDs (survives restarts)
🩺 Health endpointHTTP /health on configurable port
👥 Multi-userMultiple users, each with their own identity and bot routing
🐳 DockerMulti-stage image, ~55 MB

~55 MB RAM · ~250 ms latency · asyncio-based · Python 3.11+

Hermes slash commands over Telegram

BridgeMost now preserves generic slash commands when the upstream Mattermost bot is Hermes. That means commands like:

  • /new
  • /model
  • /help
  • /commands
  • /reasoning

arrive in Mattermost exactly as typed, instead of being swallowed by Telegram-side command handlers.

BridgeMost local command namespace

To avoid collisions with Hermes, BridgeMost keeps its own local controls under /bridge:

  • /bridge bot — list bots or switch the active relay target
  • /bridge bots — inspect available bot routes
  • /bridge status — inspect bridge-local status
  • /bridge help — show the local command help

Legacy /bot and /bots aliases still work in Telegram for compatibility, but /status is now reserved for Hermes passthrough.

Telegram clean mode (v2.2.5)

When the upstream Mattermost bot is Hermes, BridgeMost can now keep Telegram clean:

  • internal tool-progress posts (terminal:, execute_code:, skill_view:, etc.) stay in Mattermost
  • Telegram gets a placeholder such as 🧠⚡ Conectando a la red neuronal...
  • the placeholder is then edited in place into the real final answer
  • the final answer can be revealed progressively for a streaming-like UX

This behavior is configurable through telegram_presentation: in config.yaml.

Reply/thread sync (v2.2.6)

  • Replying to a Telegram message now posts into the corresponding Mattermost thread root
  • Replies emitted by Mattermost bots with root_id come back to Telegram as native replies when the root message is known
  • Clean mode preserves the reply target, so the placeholder and final edited answer stay visually attached to the original Telegram message

Multi-user ready: Multiple people can use the same BridgeMost instance — each with their own chat account, Mattermost identity, and bot routing. Add users to config.yaml and they appear as themselves in Mattermost. No shared accounts, no impersonation.


🏗️ Architecture (v2.0+)

┌──────────────┐
│ Telegram │─┐
├──────────────┤ │ ┌──────────────┐ ┌──────────────┐
│ Google Chat │─┼────────►│ BridgeMost │◄───────►│ Mattermost │
├──────────────┤ │ │ Core Engine │ WS+API │ (Bots) │
│ Slack │─┤ └──────────────┘ └──────────────┘
├──────────────┤ │ Adapters │ Core │ MM
│ Matrix │─┘
└──────────────┘

Three layers:

  1. Adapters — Platform-specific plugins (telegram.py, googlechat.py, etc.)
  2. Core Engine — Routing, mapping, sync, retry, health — platform-agnostic
  3. Mattermost Connector — WebSocket, REST API, file upload

Each adapter implements BaseAdapter (8 methods: start, stop, send_message, edit, delete, react, typing, clear_reactions).


🚀 Installation — Step by Step

What you need

#ItemWhere to get it
1Mattermost server (self-hosted)You must be admin or have an admin enable PAT support
2Chat platform bot tokenTelegram: @BotFather/newbot
3Your platform user IDTelegram: message @userinfobot
4Python 3.11+python3 --version to check
5Gitgit --version to check

Step 1 — Enable Personal Access Tokens on Mattermost

⚠️This step is REQUIRED. Without it, BridgeMost cannot post as your user.

Option A — Via Mattermost UI (admin):

  1. Go to System Console → Authentication → Token Access
  2. Set Enable Personal Access Tokens to true
  3. Save

Option B — Via command line (requires access to the server):

# If mmctl is available:
mmctl --local config set ServiceSettings.EnableUserAccessTokens true# Or via REST API with admin token:
curl -X PUT http://localhost:8065/api/v4/config/patch \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ServiceSettings": {"EnableUserAccessTokens": true}}'

Step 2 — Clone and install

git clone https://github.com/JuanjoPM-Developer/BridgeMost.git
cd BridgeMost
python3 -m venv .venv
source .venv/bin/activate
pip install -e .

Or via PyPI:

pip install bridgemost

Or via Docker:

docker compose up -d

Step 3 — Configure

Option A — Interactive wizard (recommended for Telegram):

python3 -m bridgemost setup

The wizard will:

  1. Connect to your Mattermost server
  2. Log you in (password is NOT stored)
  3. Auto-create a Personal Access Token for BridgeMost
  4. List all bots on the server — you pick which ones to bridge
  5. Ask for your platform bot token and user ID
  6. Generate config.yaml automatically

Option B — Manual configuration:

cp config.example.yaml config.yaml

Then edit config.yaml — see the Configuration Reference below.

Step 4 — Run

# Foreground (for testing):
python3 -m bridgemost
# Or as a systemd service (recommended for production):
sudo cp bridgemost.service.example /etc/systemd/system/bridgemost.service
# Edit the service file — update paths to match your installation
sudo systemctl daemon-reload
sudo systemctl enable --now bridgemost

Step 5 — Test

  1. Send a message from your chat platform to the BridgeMost bot
  2. The message should appear in Mattermost as your real user
  3. When the MM bot responds, the response should appear in your chat

⚙️ Configuration Reference

Minimal config.yaml (Telegram adapter)

telegram:
bot_token: "123456:ABC-DEF..."# From @BotFathermattermost:
url: "http://localhost:8065"# Your MM server URL (http or https)bot_token: "abc123..."# Any bot's access token (for WebSocket)bot_user_id: "a1b2c3d4..."# User ID of that botusers:
- telegram_id: 123456789# Your numeric platform user IDtelegram_name: "Your Name"# Display name (for logs only)mm_user_id: "x1y2z3..."# Your Mattermost user IDmm_token: "your-pat-here"# Your Personal Access Tokenbots:
- name: "mybot"# Friendly name (used with /bot command)mm_bot_id: "bot-user-id-here"# The bot's Mattermost user IDmm_dm_channel: ""# Leave empty — auto-discovered at startupdefault: true # First bot to talk to when bridge starts

How to find each value

FieldHow to get it
telegram.bot_token@BotFather/newbot → copy the token
telegram_idSend any message to @userinfobot
mattermost.urlThe URL you use to open Mattermost in your browser
mattermost.bot_tokenMM → Integrations → Bot Accounts → pick any bot → copy token. Or ask your admin.
mattermost.bot_user_idmmctl user search <botname> → copy id. Or: curl http://YOUR_MM/api/v4/users/username/<botname> -H "Authorization: Bearer TOKEN""id"
mm_user_idSame as above with your own username
mm_token (PAT)MM → Profile → Security → Personal Access Tokens → Create. Or wizard creates it.
mm_bot_idThe Mattermost user ID of each bot you want to talk to
mm_dm_channelLeave empty — auto-discovered at startup.

Optional sections

# Voice-to-text transcription (requires a Whisper-compatible API)voice_to_text:
url: "http://localhost:9000"# Whisper endpointapi_key: ""# For OpenAI/Groq; empty for local Whispermodel: "large-v3"# large-v3, whisper-1, whisper-large-v3-turbolanguage: ""# "es", "en", or "" for auto-detectkeep_audio: true # Also attach audio file alongside transcript# Health monitoring endpointhealth:
port: 9191# HTTP health check on this port# Message persistencestorage:
data_dir: ""# SQLite DB location; empty = working directory# Logginglogging:
level: "INFO"# DEBUG, INFO, WARNING, ERRORfile: ""# Log file path, or "" for stdout only

🤖 Chat Commands (Telegram adapter)

CommandDescription
/botList all available bots and show which one is active
/bot nameSwitch to a different bot
/botsShow all bots with live 🟢/⚫ online status
/statusDetailed info about the active bot

🎤 Voice-to-Text

When voice_to_text is configured, voice messages are transcribed before posting:

🎤 Hello, this is what I said in the voice message

If keep_audio: true, the original audio file is also attached.

Compatible APIs:


📊 Health Endpoint

curl http://localhost:9191/health
{
"status": "ok",
"version": "2.0.1",
"transport": "websocket",
"uptime": "2h15m30s",
"messages": { "tg_to_mm": 42, "mm_to_tg": 38, "errors": 0 },
"store": { "persistent_mappings": 156 }
}

🔧 Troubleshooting

ProblemSolution
FATAL: Token validation FAILEDPAT is invalid/expired. Create a new one in MM → Profile → Security → PAT. Also verify EnableUserAccessTokens is true.
⚠️ Token expirado alertSame — renew PAT, update mm_token in config.yaml, restart.
Zero DM channels discoveredMake sure you've DM'd each bot in MM at least once. Verify mm_bot_id values are correct (26 alphanumeric chars).
WS auth rejected (CLOSE on connect)The mattermost.bot_token is invalid. Get a valid one from Integrations → Bot Accounts.
OSError: [Errno 98] address already in useAnother process on health port. Change health.port in config.
[BotName] prefix on messagesNormal in multi-bot mode to identify which bot responded. Single bot = no prefix.
Voice not transcribedCheck voice_to_text.url is reachable. For OpenAI/Groq, verify api_key.
EnableUserAccessTokens keeps resettingSomething is toggling it. Lock the setting and audit admin access.

🛡️ Security

  • config.yaml contains secrets — it's in .gitignore, never commit it
  • PATs have your full user permissions — use a dedicated account if concerned
  • Health endpoint binds to 127.0.0.1 (not exposed externally)
  • Only users whose ID is in config can use the bridge
  • Message mappings stored in local SQLite (30-day auto-prune)

🔌 Writing a Custom Adapter

Create a new file in src/bridgemost/adapters/ that implements BaseAdapter:

frombridgemost.adapters.baseimportBaseAdapter, InboundMessage, OutboundMessageclassMyPlatformAdapter(BaseAdapter):
asyncdefstart(self): ...
asyncdefstop(self): ...
asyncdefsend_message(self, chat_id, msg: OutboundMessage) ->int|None: ...
asyncdefedit_message(self, chat_id, msg_id, text): ...
asyncdefdelete_message(self, chat_id, msg_id): ...
asyncdefset_reaction(self, chat_id, msg_id, emoji): ...
asyncdefclear_reactions(self, chat_id, msg_id): ...
defstart_typing_loop(self, chat_id): ...
defstop_typing_loop(self, chat_id): ...

The core engine handles all Mattermost interaction, message tracking, retry, and health monitoring.


📋 Changelog

VersionDateHighlight
v2.1.02026-03-25Google Chat adapter — Service Account ghost mode, polling, edit/delete/reactions
v2.0.22026-03-25README rewritten for multi-platform architecture
v2.0.12026-03-25Audit cleanup: platform-agnostic emoji names, encapsulation fix
v2.0.02026-03-25Plugin adapter architecture — Telegram extracted as adapter, core engine separated
v1.0.02026-03-25Stable release — PyPI, CI/CD, full test suite
v0.9.x2026-03-24/25Stickers, locations, polls, file relay, Docker, 71 tests
v0.8.x2026-03-24SQLite store, WS jitter, rate limiter, bot commands
v0.7.02026-03-247-bug audit, PAT health check, error alerts
v0.6.02026-03-24Interactive setup wizard
v0.5.02026-03-24Startup resilience, token validation
v0.4.02026-03-24Voice-to-text via Whisper
v0.3.x2026-03-24Multi-bot routing, synthetic typing
v0.2.02026-03-24Emoji/reaction relay
v0.1.x2026-03-24WebSocket transport, edit/delete sync
v0.0.52026-03-24First public release

See CHANGELOG.md for full details.


📄 License

MIT — see LICENSE

🙏 Built with

About

Multi-platform ↔ Mattermost transparent bridge. Messages appear as the real user. Plugin adapter architecture — Telegram ready, Google Chat/Slack/Matrix planned. Ghost mode.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

BridgeMost 👻

CIPyPI

Multi-Platform ↔ Mattermost Transparent Bridge

BridgeMost makes your messages from Telegram, Google Chat, or any supported platform appear natively in Mattermost — as your real user, with your avatar and name. Bot responses relay back instantly via WebSocket.

Unlike Matterbridge or webhooks that post with [User] prefixes, BridgeMost posts as your actual Mattermost account using Personal Access Tokens. Nobody in Mattermost can tell you're writing from another platform.

🔌 Supported Platforms (Adapters)

PlatformStatusDescription
Telegram✅ ProductionFull support — text, media, voice, reactions, edits, deletes
Google Chat✅ v2.1.0Workspace ghost mode via Service Account + domain-wide delegation
Slack🔜 PlannedUser token impersonation
Matrix🔜 PlannedApplication Service ghost mode

Plugin architecture (v2.0+): Each platform is an independent adapter module. Adding a new platform = one Python file implementing BaseAdapter. Zero changes to the core engine.

✨ Features

FeatureDescription
🪪 Transparent identityPosts as your real MM user (avatar, name, everything)
📁 Full mediaPhotos, documents, audio, video, voice — bidirectional
🎤 Voice-to-textVoice messages auto-transcribed via Whisper API
🤖 Multi-bot routingTalk to multiple MM bots; switch with /bridge bot <name>
📲 DM Bridge modeGive each MM bot its own dedicated TG bot — DM it directly (v2.2.0)
/️⃣ Hermes slash passthrough/new, /model, /help, etc. cross Telegram → Mattermost unchanged (v2.2.4)
🧠 Telegram clean modeTool chatter stays in MM; Telegram sees a neural-link placeholder + clean final response (v2.2.5)
↪️ Reply/thread syncTelegram replies map to Mattermost threads and threaded MM replies come back as native Telegram replies (v2.2.6)
⚡ Real-time WebSocketResponses arrive instantly (no polling)
✏️ Edit & delete syncEdits and deletes stay in sync both ways
😀 ReactionsEmoji reactions synced bidirectionally
⌨️ Typing indicatorSynthetic "Bot is typing..." on the chat side
📝 MarkdownMM markdown auto-converted to platform format
🔒 Startup checksValidates tokens + discovers channels before starting
💾 Persistent mappingSQLite store for message IDs (survives restarts)
🩺 Health endpointHTTP /health on configurable port
👥 Multi-userMultiple users, each with their own identity and bot routing
🐳 DockerMulti-stage image, ~55 MB

~55 MB RAM · ~250 ms latency · asyncio-based · Python 3.11+

Hermes slash commands over Telegram

BridgeMost now preserves generic slash commands when the upstream Mattermost bot is Hermes. That means commands like:

  • /new
  • /model
  • /help
  • /commands
  • /reasoning

arrive in Mattermost exactly as typed, instead of being swallowed by Telegram-side command handlers.

BridgeMost local command namespace

To avoid collisions with Hermes, BridgeMost keeps its own local controls under /bridge:

  • /bridge bot — list bots or switch the active relay target
  • /bridge bots — inspect available bot routes
  • /bridge status — inspect bridge-local status
  • /bridge help — show the local command help

Legacy /bot and /bots aliases still work in Telegram for compatibility, but /status is now reserved for Hermes passthrough.

Telegram clean mode (v2.2.5)

When the upstream Mattermost bot is Hermes, BridgeMost can now keep Telegram clean:

  • internal tool-progress posts (terminal:, execute_code:, skill_view:, etc.) stay in Mattermost
  • Telegram gets a placeholder such as 🧠⚡ Conectando a la red neuronal...
  • the placeholder is then edited in place into the real final answer
  • the final answer can be revealed progressively for a streaming-like UX

This behavior is configurable through telegram_presentation: in config.yaml.

Reply/thread sync (v2.2.6)

  • Replying to a Telegram message now posts into the corresponding Mattermost thread root
  • Replies emitted by Mattermost bots with root_id come back to Telegram as native replies when the root message is known
  • Clean mode preserves the reply target, so the placeholder and final edited answer stay visually attached to the original Telegram message

Multi-user ready: Multiple people can use the same BridgeMost instance — each with their own chat account, Mattermost identity, and bot routing. Add users to config.yaml and they appear as themselves in Mattermost. No shared accounts, no impersonation.


🏗️ Architecture (v2.0+)

┌──────────────┐
│ Telegram │─┐
├──────────────┤ │ ┌──────────────┐ ┌──────────────┐
│ Google Chat │─┼────────►│ BridgeMost │◄───────►│ Mattermost │
├──────────────┤ │ │ Core Engine │ WS+API │ (Bots) │
│ Slack │─┤ └──────────────┘ └──────────────┘
├──────────────┤ │ Adapters │ Core │ MM
│ Matrix │─┘
└──────────────┘

Three layers:

  1. Adapters — Platform-specific plugins (telegram.py, googlechat.py, etc.)
  2. Core Engine — Routing, mapping, sync, retry, health — platform-agnostic
  3. Mattermost Connector — WebSocket, REST API, file upload

Each adapter implements BaseAdapter (8 methods: start, stop, send_message, edit, delete, react, typing, clear_reactions).


🚀 Installation — Step by Step

What you need

#ItemWhere to get it
1Mattermost server (self-hosted)You must be admin or have an admin enable PAT support
2Chat platform bot tokenTelegram: @BotFather/newbot
3Your platform user IDTelegram: message @userinfobot
4Python 3.11+python3 --version to check
5Gitgit --version to check

Step 1 — Enable Personal Access Tokens on Mattermost

⚠️This step is REQUIRED. Without it, BridgeMost cannot post as your user.

Option A — Via Mattermost UI (admin):

  1. Go to System Console → Authentication → Token Access
  2. Set Enable Personal Access Tokens to true
  3. Save

Option B — Via command line (requires access to the server):

# If mmctl is available:
mmctl --local config set ServiceSettings.EnableUserAccessTokens true# Or via REST API with admin token:
curl -X PUT http://localhost:8065/api/v4/config/patch \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ServiceSettings": {"EnableUserAccessTokens": true}}'

Step 2 — Clone and install

git clone https://github.com/JuanjoPM-Developer/BridgeMost.git
cd BridgeMost
python3 -m venv .venv
source .venv/bin/activate
pip install -e .

Or via PyPI:

pip install bridgemost

Or via Docker:

docker compose up -d

Step 3 — Configure

Option A — Interactive wizard (recommended for Telegram):

python3 -m bridgemost setup

The wizard will:

  1. Connect to your Mattermost server
  2. Log you in (password is NOT stored)
  3. Auto-create a Personal Access Token for BridgeMost
  4. List all bots on the server — you pick which ones to bridge
  5. Ask for your platform bot token and user ID
  6. Generate config.yaml automatically

Option B — Manual configuration:

cp config.example.yaml config.yaml

Then edit config.yaml — see the Configuration Reference below.

Step 4 — Run

# Foreground (for testing):
python3 -m bridgemost
# Or as a systemd service (recommended for production):
sudo cp bridgemost.service.example /etc/systemd/system/bridgemost.service
# Edit the service file — update paths to match your installation
sudo systemctl daemon-reload
sudo systemctl enable --now bridgemost

Step 5 — Test

  1. Send a message from your chat platform to the BridgeMost bot
  2. The message should appear in Mattermost as your real user
  3. When the MM bot responds, the response should appear in your chat

⚙️ Configuration Reference

Minimal config.yaml (Telegram adapter)

telegram:
bot_token: "123456:ABC-DEF..."# From @BotFathermattermost:
url: "http://localhost:8065"# Your MM server URL (http or https)bot_token: "abc123..."# Any bot's access token (for WebSocket)bot_user_id: "a1b2c3d4..."# User ID of that botusers:
- telegram_id: 123456789# Your numeric platform user IDtelegram_name: "Your Name"# Display name (for logs only)mm_user_id: "x1y2z3..."# Your Mattermost user IDmm_token: "your-pat-here"# Your Personal Access Tokenbots:
- name: "mybot"# Friendly name (used with /bot command)mm_bot_id: "bot-user-id-here"# The bot's Mattermost user IDmm_dm_channel: ""# Leave empty — auto-discovered at startupdefault: true # First bot to talk to when bridge starts

How to find each value

FieldHow to get it
telegram.bot_token@BotFather/newbot → copy the token
telegram_idSend any message to @userinfobot
mattermost.urlThe URL you use to open Mattermost in your browser
mattermost.bot_tokenMM → Integrations → Bot Accounts → pick any bot → copy token. Or ask your admin.
mattermost.bot_user_idmmctl user search <botname> → copy id. Or: curl http://YOUR_MM/api/v4/users/username/<botname> -H "Authorization: Bearer TOKEN""id"
mm_user_idSame as above with your own username
mm_token (PAT)MM → Profile → Security → Personal Access Tokens → Create. Or wizard creates it.
mm_bot_idThe Mattermost user ID of each bot you want to talk to
mm_dm_channelLeave empty — auto-discovered at startup.

Optional sections

# Voice-to-text transcription (requires a Whisper-compatible API)voice_to_text:
url: "http://localhost:9000"# Whisper endpointapi_key: ""# For OpenAI/Groq; empty for local Whispermodel: "large-v3"# large-v3, whisper-1, whisper-large-v3-turbolanguage: ""# "es", "en", or "" for auto-detectkeep_audio: true # Also attach audio file alongside transcript# Health monitoring endpointhealth:
port: 9191# HTTP health check on this port# Message persistencestorage:
data_dir: ""# SQLite DB location; empty = working directory# Logginglogging:
level: "INFO"# DEBUG, INFO, WARNING, ERRORfile: ""# Log file path, or "" for stdout only

🤖 Chat Commands (Telegram adapter)

CommandDescription
/botList all available bots and show which one is active
/bot nameSwitch to a different bot
/botsShow all bots with live 🟢/⚫ online status
/statusDetailed info about the active bot

🎤 Voice-to-Text

When voice_to_text is configured, voice messages are transcribed before posting:

🎤 Hello, this is what I said in the voice message

If keep_audio: true, the original audio file is also attached.

Compatible APIs:


📊 Health Endpoint

curl http://localhost:9191/health
{
"status": "ok",
"version": "2.0.1",
"transport": "websocket",
"uptime": "2h15m30s",
"messages": { "tg_to_mm": 42, "mm_to_tg": 38, "errors": 0 },
"store": { "persistent_mappings": 156 }
}

🔧 Troubleshooting

ProblemSolution
FATAL: Token validation FAILEDPAT is invalid/expired. Create a new one in MM → Profile → Security → PAT. Also verify EnableUserAccessTokens is true.
⚠️ Token expirado alertSame — renew PAT, update mm_token in config.yaml, restart.
Zero DM channels discoveredMake sure you've DM'd each bot in MM at least once. Verify mm_bot_id values are correct (26 alphanumeric chars).
WS auth rejected (CLOSE on connect)The mattermost.bot_token is invalid. Get a valid one from Integrations → Bot Accounts.
OSError: [Errno 98] address already in useAnother process on health port. Change health.port in config.
[BotName] prefix on messagesNormal in multi-bot mode to identify which bot responded. Single bot = no prefix.
Voice not transcribedCheck voice_to_text.url is reachable. For OpenAI/Groq, verify api_key.
EnableUserAccessTokens keeps resettingSomething is toggling it. Lock the setting and audit admin access.

🛡️ Security

  • config.yaml contains secrets — it's in .gitignore, never commit it
  • PATs have your full user permissions — use a dedicated account if concerned
  • Health endpoint binds to 127.0.0.1 (not exposed externally)
  • Only users whose ID is in config can use the bridge
  • Message mappings stored in local SQLite (30-day auto-prune)

🔌 Writing a Custom Adapter

Create a new file in src/bridgemost/adapters/ that implements BaseAdapter:

frombridgemost.adapters.baseimportBaseAdapter, InboundMessage, OutboundMessageclassMyPlatformAdapter(BaseAdapter):
asyncdefstart(self): ...
asyncdefstop(self): ...
asyncdefsend_message(self, chat_id, msg: OutboundMessage) ->int|None: ...
asyncdefedit_message(self, chat_id, msg_id, text): ...
asyncdefdelete_message(self, chat_id, msg_id): ...
asyncdefset_reaction(self, chat_id, msg_id, emoji): ...
asyncdefclear_reactions(self, chat_id, msg_id): ...
defstart_typing_loop(self, chat_id): ...
defstop_typing_loop(self, chat_id): ...

The core engine handles all Mattermost interaction, message tracking, retry, and health monitoring.


📋 Changelog

VersionDateHighlight
v2.1.02026-03-25Google Chat adapter — Service Account ghost mode, polling, edit/delete/reactions
v2.0.22026-03-25README rewritten for multi-platform architecture
v2.0.12026-03-25Audit cleanup: platform-agnostic emoji names, encapsulation fix
v2.0.02026-03-25Plugin adapter architecture — Telegram extracted as adapter, core engine separated
v1.0.02026-03-25Stable release — PyPI, CI/CD, full test suite
v0.9.x2026-03-24/25Stickers, locations, polls, file relay, Docker, 71 tests
v0.8.x2026-03-24SQLite store, WS jitter, rate limiter, bot commands
v0.7.02026-03-247-bug audit, PAT health check, error alerts
v0.6.02026-03-24Interactive setup wizard
v0.5.02026-03-24Startup resilience, token validation
v0.4.02026-03-24Voice-to-text via Whisper
v0.3.x2026-03-24Multi-bot routing, synthetic typing
v0.2.02026-03-24Emoji/reaction relay
v0.1.x2026-03-24WebSocket transport, edit/delete sync
v0.0.52026-03-24First public release

See CHANGELOG.md for full details.


📄 License

MIT — see LICENSE

🙏 Built with

About

Multi-platform ↔ Mattermost transparent bridge. Messages appear as the real user. Plugin adapter architecture — Telegram ready, Google Chat/Slack/Matrix planned. Ghost mode.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages