chatops-bridge 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ChatOps Bridge Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,261 @@
1
+ Metadata-Version: 2.4
2
+ Name: chatops-bridge
3
+ Version: 0.1.0
4
+ Summary: Reusable Telegram and Discord ChatOps utilities for Python projects
5
+ Author: ChatOps Bridge Contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/trunghvbk-afataw/chatops
8
+ Project-URL: Repository, https://github.com/trunghvbk-afataw/chatops
9
+ Project-URL: Issues, https://github.com/trunghvbk-afataw/chatops/issues
10
+ Keywords: telegram,discord,webhook,chatops,notifications
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Communications :: Chat
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: awesome-notify-bridge>=0.1.5
26
+ Provides-Extra: discord-bot
27
+ Requires-Dist: discord.py>=2.4.0; extra == "discord-bot"
28
+ Dynamic: license-file
29
+
30
+ # chatops-bridge
31
+
32
+ Reusable Telegram and Discord utilities for Python applications.
33
+
34
+ This package was extracted from production automation code and generalized for multi-purpose ChatOps use.
35
+ Transport-level Telegram/Discord sending is reused from `awesome-notify-bridge`.
36
+
37
+ ## Features
38
+
39
+ - Telegram send/poll helpers:
40
+ - send text and images
41
+ - split long text by Telegram limit
42
+ - poll updates and extract message text
43
+ - robust chat id matching
44
+ - Discord webhook sender:
45
+ - send message chunks safely
46
+ - upload image files
47
+ - Discord slash bot:
48
+ - register any slash commands dynamically
49
+ - optional owner and guild restriction
50
+ - Plain text formatter helpers for chat-friendly output
51
+
52
+ ## Install
53
+
54
+ Requires Python 3.9+.
55
+
56
+ ```bash
57
+ pip install chatops-bridge
58
+ ```
59
+
60
+ For slash-bot support:
61
+
62
+ ```bash
63
+ pip install "chatops-bridge[discord-bot]"
64
+ ```
65
+
66
+ Install from GitHub:
67
+
68
+ ```bash
69
+ pip install git+https://github.com/<your-org>/chatops-bridge.git
70
+ ```
71
+
72
+ ## Quick Start
73
+
74
+ ### Telegram
75
+
76
+ ```python
77
+ from chatops_bridge.telegram import send_telegram_message
78
+
79
+ send_telegram_message(
80
+ chat_id="123456789",
81
+ token="<telegram-bot-token>",
82
+ text="Hello from chatops-bridge",
83
+ )
84
+ ```
85
+
86
+ Send with images:
87
+
88
+ ```python
89
+ send_telegram_message(
90
+ chat_id="123456789",
91
+ token="<telegram-bot-token>",
92
+ text="Report attached",
93
+ image_paths=["./charts/btc.png", "./charts/eth.png"],
94
+ )
95
+ ```
96
+
97
+ ### Discord webhook
98
+
99
+ ```python
100
+ from chatops_bridge.discord import send_discord_message
101
+
102
+ send_discord_message(
103
+ webhook_url="https://discord.com/api/webhooks/...",
104
+ content="Hello from chatops-bridge",
105
+ )
106
+ ```
107
+
108
+ Return value is `True` on success and `False` on failure.
109
+
110
+ ### Discord env-based channel
111
+
112
+ ```python
113
+ from chatops_bridge.discord_channels import send_to_discord_env_channel
114
+
115
+ # export DISCORD_ALERT_WEBHOOK_URL=...
116
+ send_to_discord_env_channel("DISCORD_ALERT_WEBHOOK_URL", "Alert triggered")
117
+
118
+ # strict mode: fail fast if env var is missing
119
+ send_to_discord_env_channel("DISCORD_ALERT_WEBHOOK_URL", "Alert triggered", strict_env=True)
120
+ ```
121
+
122
+ Useful when you want channel routing from environment variables instead of hardcoding webhook URLs.
123
+
124
+ ## Telegram Update Polling
125
+
126
+ ```python
127
+ from chatops_bridge.telegram import fetch_telegram_updates, extract_update_text
128
+
129
+ updates = fetch_telegram_updates(
130
+ token="<telegram-bot-token>",
131
+ offset=None,
132
+ timeout_seconds=10,
133
+ allowed_updates=["message", "edited_message"],
134
+ )
135
+
136
+ for update in updates:
137
+ text, message = extract_update_text(update)
138
+ if text:
139
+ print("message:", text)
140
+ ```
141
+
142
+ ## Discord Slash Bot
143
+
144
+ `chatops_bridge.discord_bot.start_discord_bot(...)` starts a background thread for a Discord bot using your command list.
145
+
146
+ ```python
147
+ import discord
148
+
149
+ from chatops_bridge.discord_bot import DiscordBotConfig, SlashCommandSpec, start_discord_bot
150
+
151
+ def ping() -> str:
152
+ return "pong"
153
+
154
+ def report() -> dict:
155
+ return {"text": "Report done", "image_paths": ["./out/chart.png"]}
156
+
157
+ start_discord_bot(
158
+ token="<discord-bot-token>",
159
+ commands=[
160
+ SlashCommandSpec(name="ping", description="Health check", handler=ping),
161
+ SlashCommandSpec(name="report", description="Generate report", handler=report),
162
+ ],
163
+ config=DiscordBotConfig(
164
+ intents=discord.Intents(guilds=True),
165
+ allow_dm_commands=False,
166
+ leave_unexpected_guild=True,
167
+ response_chunk_limit=1900,
168
+ max_images_per_response=10,
169
+ defer_thinking=True,
170
+ ),
171
+ guild_id=None,
172
+ owner_user_id=None,
173
+ instance_key="primary",
174
+ )
175
+ ```
176
+
177
+ You can also run the bot with `commands=[]` (or omit `commands`) when your app only needs Discord client events/background tasks.
178
+
179
+ `instance_key` is used to manage bot thread instances by key. Calling `start_discord_bot` again with the same key returns the existing live thread.
180
+
181
+ ## Slash Bot Example
182
+
183
+ See [examples/discord_bot_example.py](examples/discord_bot_example.py).
184
+
185
+ ## Telegram Poller Bot
186
+
187
+ `chatops_bridge.telegram_poller.start_telegram_poller(...)` starts a background polling thread that automatically handles Telegram updates and dispatches them to your command handlers.
188
+
189
+ ```python
190
+ from chatops_bridge.telegram_poller import TelegramCommandSpec, TelegramPollerConfig, start_telegram_poller
191
+
192
+ def handle_status(args: list[str]) -> str:
193
+ return "Status: OK"
194
+
195
+ def handle_trade(args: list[str]) -> str:
196
+ amount = float(args[0]) if args else 100.0
197
+ return f"Trading {amount} units"
198
+
199
+ start_telegram_poller(
200
+ token="<telegram-bot-token>",
201
+ commands=[
202
+ TelegramCommandSpec(
203
+ name="status",
204
+ description="Get bot status",
205
+ handler=handle_status,
206
+ ),
207
+ TelegramCommandSpec(
208
+ name="trade",
209
+ description="Execute trade",
210
+ handler=handle_trade,
211
+ ),
212
+ ],
213
+ config=TelegramPollerConfig(
214
+ poll_timeout_seconds=30,
215
+ max_retries=3,
216
+ retry_delay_seconds=5,
217
+ allowed_updates=["message", "edited_message"],
218
+ offset_file="/tmp/telegram_offset.txt", # Persist offset on restart
219
+ ),
220
+ logger=print, # Optional logger
221
+ )
222
+ ```
223
+
224
+ The poller:
225
+ - Runs in background thread(s)
226
+ - Automatically saves offset to persist progress across restarts
227
+ - Retries on network errors with configurable backoff
228
+ - Filters update types (e.g., messages only, skip reactions)
229
+ - Parses `/command arg1 arg2` style messages
230
+ - Dispatches to appropriate handler
231
+ - Sends response back to the originating chat
232
+
233
+ Handlers receive a list of arguments and return `str` or `dict` (JSON-serialized if dict).
234
+
235
+ ## Telegram Poller Example
236
+
237
+ See [examples/telegram_poller_example.py](examples/telegram_poller_example.py).
238
+
239
+ ## Common Environment Variables
240
+
241
+ - `DISCORD_BOT_TOKEN`: bot token used by the slash-bot example.
242
+ - `DISCORD_BOT_GUILD_ID`: optional guild restriction.
243
+ - `DISCORD_BOT_OWNER_USER_ID`: optional owner-only restriction.
244
+ - `TELEGRAM_BOT_TOKEN`: polling bot token.
245
+ - `TELEGRAM_BOT_OFFSET_FILE`: optional path to persist polling offset (for restart safety).
246
+ - Custom webhook URL variables (for env-based routing), for example:
247
+ - `DISCORD_ALERT_WEBHOOK_URL`
248
+ - `DISCORD_SIGNAL_WEBHOOK_URL`
249
+
250
+ ## License
251
+
252
+ MIT
253
+
254
+ ## Contributing
255
+
256
+ We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for:
257
+ - Development setup
258
+ - Making changes and submitting PRs
259
+ - Release procedures
260
+
261
+ For questions, open an issue on [GitHub](https://github.com/trunghvbk-afataw/chatops/issues).
@@ -0,0 +1,232 @@
1
+ # chatops-bridge
2
+
3
+ Reusable Telegram and Discord utilities for Python applications.
4
+
5
+ This package was extracted from production automation code and generalized for multi-purpose ChatOps use.
6
+ Transport-level Telegram/Discord sending is reused from `awesome-notify-bridge`.
7
+
8
+ ## Features
9
+
10
+ - Telegram send/poll helpers:
11
+ - send text and images
12
+ - split long text by Telegram limit
13
+ - poll updates and extract message text
14
+ - robust chat id matching
15
+ - Discord webhook sender:
16
+ - send message chunks safely
17
+ - upload image files
18
+ - Discord slash bot:
19
+ - register any slash commands dynamically
20
+ - optional owner and guild restriction
21
+ - Plain text formatter helpers for chat-friendly output
22
+
23
+ ## Install
24
+
25
+ Requires Python 3.9+.
26
+
27
+ ```bash
28
+ pip install chatops-bridge
29
+ ```
30
+
31
+ For slash-bot support:
32
+
33
+ ```bash
34
+ pip install "chatops-bridge[discord-bot]"
35
+ ```
36
+
37
+ Install from GitHub:
38
+
39
+ ```bash
40
+ pip install git+https://github.com/<your-org>/chatops-bridge.git
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ### Telegram
46
+
47
+ ```python
48
+ from chatops_bridge.telegram import send_telegram_message
49
+
50
+ send_telegram_message(
51
+ chat_id="123456789",
52
+ token="<telegram-bot-token>",
53
+ text="Hello from chatops-bridge",
54
+ )
55
+ ```
56
+
57
+ Send with images:
58
+
59
+ ```python
60
+ send_telegram_message(
61
+ chat_id="123456789",
62
+ token="<telegram-bot-token>",
63
+ text="Report attached",
64
+ image_paths=["./charts/btc.png", "./charts/eth.png"],
65
+ )
66
+ ```
67
+
68
+ ### Discord webhook
69
+
70
+ ```python
71
+ from chatops_bridge.discord import send_discord_message
72
+
73
+ send_discord_message(
74
+ webhook_url="https://discord.com/api/webhooks/...",
75
+ content="Hello from chatops-bridge",
76
+ )
77
+ ```
78
+
79
+ Return value is `True` on success and `False` on failure.
80
+
81
+ ### Discord env-based channel
82
+
83
+ ```python
84
+ from chatops_bridge.discord_channels import send_to_discord_env_channel
85
+
86
+ # export DISCORD_ALERT_WEBHOOK_URL=...
87
+ send_to_discord_env_channel("DISCORD_ALERT_WEBHOOK_URL", "Alert triggered")
88
+
89
+ # strict mode: fail fast if env var is missing
90
+ send_to_discord_env_channel("DISCORD_ALERT_WEBHOOK_URL", "Alert triggered", strict_env=True)
91
+ ```
92
+
93
+ Useful when you want channel routing from environment variables instead of hardcoding webhook URLs.
94
+
95
+ ## Telegram Update Polling
96
+
97
+ ```python
98
+ from chatops_bridge.telegram import fetch_telegram_updates, extract_update_text
99
+
100
+ updates = fetch_telegram_updates(
101
+ token="<telegram-bot-token>",
102
+ offset=None,
103
+ timeout_seconds=10,
104
+ allowed_updates=["message", "edited_message"],
105
+ )
106
+
107
+ for update in updates:
108
+ text, message = extract_update_text(update)
109
+ if text:
110
+ print("message:", text)
111
+ ```
112
+
113
+ ## Discord Slash Bot
114
+
115
+ `chatops_bridge.discord_bot.start_discord_bot(...)` starts a background thread for a Discord bot using your command list.
116
+
117
+ ```python
118
+ import discord
119
+
120
+ from chatops_bridge.discord_bot import DiscordBotConfig, SlashCommandSpec, start_discord_bot
121
+
122
+ def ping() -> str:
123
+ return "pong"
124
+
125
+ def report() -> dict:
126
+ return {"text": "Report done", "image_paths": ["./out/chart.png"]}
127
+
128
+ start_discord_bot(
129
+ token="<discord-bot-token>",
130
+ commands=[
131
+ SlashCommandSpec(name="ping", description="Health check", handler=ping),
132
+ SlashCommandSpec(name="report", description="Generate report", handler=report),
133
+ ],
134
+ config=DiscordBotConfig(
135
+ intents=discord.Intents(guilds=True),
136
+ allow_dm_commands=False,
137
+ leave_unexpected_guild=True,
138
+ response_chunk_limit=1900,
139
+ max_images_per_response=10,
140
+ defer_thinking=True,
141
+ ),
142
+ guild_id=None,
143
+ owner_user_id=None,
144
+ instance_key="primary",
145
+ )
146
+ ```
147
+
148
+ You can also run the bot with `commands=[]` (or omit `commands`) when your app only needs Discord client events/background tasks.
149
+
150
+ `instance_key` is used to manage bot thread instances by key. Calling `start_discord_bot` again with the same key returns the existing live thread.
151
+
152
+ ## Slash Bot Example
153
+
154
+ See [examples/discord_bot_example.py](examples/discord_bot_example.py).
155
+
156
+ ## Telegram Poller Bot
157
+
158
+ `chatops_bridge.telegram_poller.start_telegram_poller(...)` starts a background polling thread that automatically handles Telegram updates and dispatches them to your command handlers.
159
+
160
+ ```python
161
+ from chatops_bridge.telegram_poller import TelegramCommandSpec, TelegramPollerConfig, start_telegram_poller
162
+
163
+ def handle_status(args: list[str]) -> str:
164
+ return "Status: OK"
165
+
166
+ def handle_trade(args: list[str]) -> str:
167
+ amount = float(args[0]) if args else 100.0
168
+ return f"Trading {amount} units"
169
+
170
+ start_telegram_poller(
171
+ token="<telegram-bot-token>",
172
+ commands=[
173
+ TelegramCommandSpec(
174
+ name="status",
175
+ description="Get bot status",
176
+ handler=handle_status,
177
+ ),
178
+ TelegramCommandSpec(
179
+ name="trade",
180
+ description="Execute trade",
181
+ handler=handle_trade,
182
+ ),
183
+ ],
184
+ config=TelegramPollerConfig(
185
+ poll_timeout_seconds=30,
186
+ max_retries=3,
187
+ retry_delay_seconds=5,
188
+ allowed_updates=["message", "edited_message"],
189
+ offset_file="/tmp/telegram_offset.txt", # Persist offset on restart
190
+ ),
191
+ logger=print, # Optional logger
192
+ )
193
+ ```
194
+
195
+ The poller:
196
+ - Runs in background thread(s)
197
+ - Automatically saves offset to persist progress across restarts
198
+ - Retries on network errors with configurable backoff
199
+ - Filters update types (e.g., messages only, skip reactions)
200
+ - Parses `/command arg1 arg2` style messages
201
+ - Dispatches to appropriate handler
202
+ - Sends response back to the originating chat
203
+
204
+ Handlers receive a list of arguments and return `str` or `dict` (JSON-serialized if dict).
205
+
206
+ ## Telegram Poller Example
207
+
208
+ See [examples/telegram_poller_example.py](examples/telegram_poller_example.py).
209
+
210
+ ## Common Environment Variables
211
+
212
+ - `DISCORD_BOT_TOKEN`: bot token used by the slash-bot example.
213
+ - `DISCORD_BOT_GUILD_ID`: optional guild restriction.
214
+ - `DISCORD_BOT_OWNER_USER_ID`: optional owner-only restriction.
215
+ - `TELEGRAM_BOT_TOKEN`: polling bot token.
216
+ - `TELEGRAM_BOT_OFFSET_FILE`: optional path to persist polling offset (for restart safety).
217
+ - Custom webhook URL variables (for env-based routing), for example:
218
+ - `DISCORD_ALERT_WEBHOOK_URL`
219
+ - `DISCORD_SIGNAL_WEBHOOK_URL`
220
+
221
+ ## License
222
+
223
+ MIT
224
+
225
+ ## Contributing
226
+
227
+ We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for:
228
+ - Development setup
229
+ - Making changes and submitting PRs
230
+ - Release procedures
231
+
232
+ For questions, open an issue on [GitHub](https://github.com/trunghvbk-afataw/chatops/issues).
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "chatops-bridge"
7
+ version = "0.1.0"
8
+ description = "Reusable Telegram and Discord ChatOps utilities for Python projects"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "ChatOps Bridge Contributors" }
14
+ ]
15
+ keywords = ["telegram", "discord", "webhook", "chatops", "notifications"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: Communications :: Chat",
27
+ "Topic :: Software Development :: Libraries"
28
+ ]
29
+ dependencies = [
30
+ "awesome-notify-bridge>=0.1.5"
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ discord-bot = [
35
+ "discord.py>=2.4.0"
36
+ ]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/trunghvbk-afataw/chatops"
40
+ Repository = "https://github.com/trunghvbk-afataw/chatops"
41
+ Issues = "https://github.com/trunghvbk-afataw/chatops/issues"
42
+
43
+ [tool.setuptools]
44
+ package-dir = {"" = "src"}
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,23 @@
1
+ """chatops_bridge public API."""
2
+
3
+ from .discord import send_discord_message
4
+ from .discord_channels import send_to_discord_env_channel
5
+ from .discord_bot import DiscordBotConfig, SlashCommandSpec
6
+ from .telegram import chat_id_matches, extract_update_text, fetch_telegram_updates, send_telegram_message
7
+ from .telegram_commands import parse_telegram_command
8
+ from .telegram_poller import TelegramCommandSpec, TelegramPollerConfig, start_telegram_poller
9
+
10
+ __all__ = [
11
+ "chat_id_matches",
12
+ "extract_update_text",
13
+ "fetch_telegram_updates",
14
+ "parse_telegram_command",
15
+ "DiscordBotConfig",
16
+ "SlashCommandSpec",
17
+ "TelegramCommandSpec",
18
+ "TelegramPollerConfig",
19
+ "send_discord_message",
20
+ "send_telegram_message",
21
+ "send_to_discord_env_channel",
22
+ "start_telegram_poller",
23
+ ]
@@ -0,0 +1,3 @@
1
+ from notify_bridge.discord import send_discord_message
2
+
3
+ __all__ = ["send_discord_message"]
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import threading
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Callable, Union
8
+
9
+ import discord
10
+ from discord import app_commands
11
+
12
+ ResponseHandler = Callable[[], Union[str, dict]]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class SlashCommandSpec:
17
+ name: str
18
+ description: str
19
+ handler: ResponseHandler
20
+
21
+ @dataclass
22
+ class DiscordBotConfig:
23
+ intents: discord.Intents | None = None
24
+ allow_dm_commands: bool = False
25
+ leave_unexpected_guild: bool = True
26
+ response_chunk_limit: int = 1900
27
+ max_images_per_response: int = 10
28
+ defer_thinking: bool = True
29
+ thread_name_prefix: str = "discord-bot"
30
+
31
+
32
+ _BOT_THREADS: dict[str, threading.Thread] = {}
33
+ _BOT_LOCK = threading.Lock()
34
+
35
+
36
+ def _split_response_text(text: str, limit: int = 1900) -> list[str]:
37
+ if len(text) <= limit:
38
+ return [text]
39
+ chunks: list[str] = []
40
+ while text:
41
+ if len(text) <= limit:
42
+ chunks.append(text)
43
+ break
44
+ cut = text.rfind("\n", 0, limit)
45
+ if cut <= 0:
46
+ cut = limit
47
+ chunks.append(text[:cut])
48
+ text = text[cut:].lstrip("\n")
49
+ return chunks
50
+
51
+
52
+ class DiscordSlashBot(discord.Client):
53
+ def __init__(
54
+ self,
55
+ *,
56
+ token: str,
57
+ commands: list[SlashCommandSpec],
58
+ config: DiscordBotConfig,
59
+ guild_id: int | None = None,
60
+ owner_user_id: int | None = None,
61
+ logger: Callable[[str], None] | None = None,
62
+ ) -> None:
63
+ intents = config.intents
64
+ if intents is None:
65
+ intents = discord.Intents.none()
66
+ intents.guilds = True
67
+ super().__init__(intents=intents)
68
+ self.tree = app_commands.CommandTree(self)
69
+ self._token = token
70
+ self._config = config
71
+ self._guild_id = guild_id
72
+ self._owner_user_id = owner_user_id
73
+ self._commands = commands
74
+ self._logger = logger or (lambda _msg: None)
75
+ self._register_commands()
76
+
77
+ def _register_commands(self) -> None:
78
+ for spec in self._commands:
79
+ self._add_command(spec)
80
+
81
+ def _add_command(self, spec: SlashCommandSpec) -> None:
82
+ @self.tree.command(name=spec.name, description=spec.description)
83
+ async def _command(interaction: discord.Interaction) -> None:
84
+ await self._run_command(interaction, spec.handler)
85
+
86
+ async def setup_hook(self) -> None:
87
+ if self._guild_id is not None:
88
+ guild = discord.Object(id=self._guild_id)
89
+ self.tree.copy_global_to(guild=guild)
90
+ synced = await self.tree.sync(guild=guild)
91
+ self._logger(f"Discord commands synced to guild {self._guild_id}: {len(synced)}")
92
+ return
93
+ synced = await self.tree.sync()
94
+ self._logger(f"Discord global commands synced: {len(synced)}")
95
+
96
+ async def on_ready(self) -> None:
97
+ self._logger(f"Discord bot connected as {self.user}")
98
+
99
+ async def on_guild_join(self, guild: discord.Guild) -> None:
100
+ if self._guild_id is not None and guild.id != self._guild_id and self._config.leave_unexpected_guild:
101
+ self._logger(f"Joined unexpected guild '{guild.name}' ({guild.id}), leaving.")
102
+ await guild.leave()
103
+
104
+ async def _run_command(self, interaction: discord.Interaction, handler: ResponseHandler) -> None:
105
+ if self._owner_user_id is not None and interaction.user.id != self._owner_user_id:
106
+ await interaction.response.send_message("Unauthorized.", ephemeral=True)
107
+ return
108
+ if interaction.guild_id is None and not self._config.allow_dm_commands:
109
+ await interaction.response.send_message("Use this command in the bot server.", ephemeral=True)
110
+ return
111
+ if self._guild_id is not None and interaction.guild_id != self._guild_id:
112
+ await interaction.response.send_message("This bot is not available in this server.", ephemeral=True)
113
+ return
114
+ await interaction.response.defer(thinking=self._config.defer_thinking)
115
+ try:
116
+ response = await asyncio.to_thread(handler)
117
+ except Exception as exc:
118
+ self._logger(f"Discord command failed: {exc}")
119
+ await interaction.followup.send(f"Command failed: {exc}")
120
+ return
121
+ await self._send_response(interaction, response)
122
+
123
+ async def _send_response(self, interaction: discord.Interaction, response: str | dict) -> None:
124
+ text = "Done"
125
+ image_paths: list[str] = []
126
+ if isinstance(response, dict):
127
+ text = str(response.get("text") or response.get("message") or "Done")
128
+ raw_images = response.get("image_paths") or []
129
+ if isinstance(raw_images, list):
130
+ image_paths = [str(path) for path in raw_images if path]
131
+ else:
132
+ text = str(response)
133
+
134
+ chunks = _split_response_text(text or "Done", limit=max(1, int(self._config.response_chunk_limit)))
135
+ for chunk in chunks:
136
+ await interaction.followup.send(chunk)
137
+
138
+ valid_files = [
139
+ discord.File(Path(p), filename=Path(p).name)
140
+ for p in image_paths[: max(0, int(self._config.max_images_per_response))]
141
+ if Path(p).exists()
142
+ ]
143
+ if valid_files:
144
+ await interaction.followup.send(files=valid_files)
145
+
146
+ def run_bot(self) -> None:
147
+ self.run(self._token, log_handler=None)
148
+
149
+
150
+ def start_discord_bot(
151
+ *,
152
+ token: str,
153
+ commands: list[SlashCommandSpec] | None = None,
154
+ config: DiscordBotConfig | None = None,
155
+ guild_id: int | None = None,
156
+ owner_user_id: int | None = None,
157
+ instance_key: str = "default",
158
+ logger: Callable[[str], None] | None = None,
159
+ ) -> threading.Thread:
160
+ resolved_commands = commands or []
161
+ resolved_config = config or DiscordBotConfig()
162
+ key = (instance_key or "default").strip() or "default"
163
+
164
+ def _run() -> None:
165
+ bot = DiscordSlashBot(
166
+ token=token,
167
+ commands=resolved_commands,
168
+ config=resolved_config,
169
+ guild_id=guild_id,
170
+ owner_user_id=owner_user_id,
171
+ logger=logger,
172
+ )
173
+ bot.run_bot()
174
+
175
+ with _BOT_LOCK:
176
+ existing = _BOT_THREADS.get(key)
177
+ if existing is not None and existing.is_alive():
178
+ return existing
179
+ thread_name = f"{resolved_config.thread_name_prefix}:{key}"
180
+ thread = threading.Thread(target=_run, name=thread_name, daemon=True)
181
+ thread.start()
182
+ _BOT_THREADS[key] = thread
183
+ return thread
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Callable
5
+
6
+ from .discord import send_discord_message
7
+
8
+ LoggerFn = Callable[[str], None]
9
+
10
+
11
+ def send_to_discord_env_channel(
12
+ channel_env_var: str,
13
+ content: str,
14
+ *,
15
+ image_paths: list[str] | None = None,
16
+ logger: LoggerFn | None = None,
17
+ strict_env: bool = False,
18
+ ) -> bool:
19
+ """Send a message to a Discord webhook URL stored in an environment variable."""
20
+ webhook_url = os.getenv(channel_env_var, "").strip()
21
+ if strict_env and not webhook_url:
22
+ raise ValueError(f"Environment variable '{channel_env_var}' is missing or empty")
23
+ return send_discord_message(webhook_url, content, image_paths=image_paths, logger=logger)
@@ -0,0 +1,8 @@
1
+ from notify_bridge.telegram import chat_id_matches, extract_update_text, fetch_telegram_updates, send_telegram_message
2
+
3
+ __all__ = [
4
+ "chat_id_matches",
5
+ "extract_update_text",
6
+ "fetch_telegram_updates",
7
+ "send_telegram_message",
8
+ ]
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def parse_telegram_command(text: str) -> tuple[str, list[str]]:
5
+ """Parse /command args from Telegram message text.
6
+
7
+ Supports bot mentions such as /status@my_bot.
8
+ """
9
+ raw = (text or "").strip()
10
+ if not raw.startswith("/"):
11
+ return "", []
12
+ parts = raw.split()
13
+ if not parts:
14
+ return "", []
15
+ cmd_token = parts[0][1:].strip().lower()
16
+ cmd = cmd_token.split("@", 1)[0]
17
+ args = parts[1:]
18
+ return cmd, args
@@ -0,0 +1,39 @@
1
+ """Helpers to format text for Telegram plain-text messages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+
8
+ def to_plain_text(text: str) -> str:
9
+ """Convert markdown-like content into plain text for Telegram readability."""
10
+ if not text:
11
+ return ""
12
+
13
+ out = text
14
+ out = out.replace("\\r\\n", "\n").replace("\\n", "\n").replace("\\t", "\t")
15
+
16
+ out = re.sub(r"```(?:\\w+)?\n", "", out)
17
+ out = out.replace("```", "")
18
+ out = re.sub(r"^#{1,6}\\s*", "", out, flags=re.MULTILINE)
19
+ out = re.sub(r"\\*\\*(.*?)\\*\\*", r"\\1", out, flags=re.DOTALL)
20
+ out = re.sub(r"__(.*?)__", r"\\1", out, flags=re.DOTALL)
21
+ out = re.sub(r"`([^`]+)`", r"\\1", out)
22
+ out = re.sub(r"\\[([^\\]]+)\\]\\(([^)]+)\\)", r"\\1 (\\2)", out)
23
+ out = re.sub(r"^>\\s?", "", out, flags=re.MULTILINE)
24
+ out = re.sub(r"^[ \\t]*[-*+]\\s+", "- ", out, flags=re.MULTILINE)
25
+ out = re.sub(r"\n{3,}", "\n\n", out)
26
+ return out.strip()
27
+
28
+
29
+ def section(title: str, body: str) -> str:
30
+ """Build a simple titled section for Telegram plain text."""
31
+ clean_body = to_plain_text(body)
32
+ if not clean_body:
33
+ return title
34
+ return f"{title}:\n{clean_body}"
35
+
36
+
37
+ def compact_lines(lines: list[str]) -> str:
38
+ cleaned = [line for line in lines if line is not None and str(line).strip() != ""]
39
+ return "\n".join(cleaned).strip()
@@ -0,0 +1,210 @@
1
+ """Telegram bot poller with background thread management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import threading
8
+ import time
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Callable, Union
12
+
13
+ from .telegram import fetch_telegram_updates, send_telegram_message, chat_id_matches
14
+ from .telegram_commands import parse_telegram_command
15
+
16
+ TelegramCommandHandler = Callable[[list[str]], Union[str, dict]]
17
+
18
+
19
+ @dataclass
20
+ class TelegramCommandSpec:
21
+ """Specification for a Telegram bot command."""
22
+
23
+ name: str
24
+ """Command name (e.g., "status", "trade")."""
25
+
26
+ description: str
27
+ """Human-readable description."""
28
+
29
+ handler: TelegramCommandHandler
30
+ """Function that handles the command. Receives list of args, returns response."""
31
+
32
+
33
+ @dataclass
34
+ class TelegramPollerConfig:
35
+ """Configuration for Telegram polling behavior."""
36
+
37
+ poll_timeout_seconds: int = 30
38
+ """Timeout for long polling (0-based)."""
39
+
40
+ max_retries: int = 3
41
+ """Max retries on network error before giving up for that cycle."""
42
+
43
+ retry_delay_seconds: int = 5
44
+ """Delay between retries."""
45
+
46
+ allowed_updates: list[str] | None = None
47
+ """Filter updates (e.g., ["message", "callback_query"]). None = all."""
48
+
49
+ offset_file: str | None = None
50
+ """Path to file for persisting offset (auto-created if missing)."""
51
+
52
+ thread_name_prefix: str = "telegram-poller"
53
+ """Prefix for thread naming."""
54
+
55
+
56
+ _TELEGRAM_POLLERS: dict[str, threading.Thread] = {}
57
+ """Map of instance_key -> polling thread."""
58
+
59
+
60
+ def _load_offset(offset_file: str | None) -> int:
61
+ """Load saved offset from file, or 0 if missing."""
62
+ if not offset_file:
63
+ return 0
64
+ path = Path(offset_file)
65
+ if path.exists():
66
+ try:
67
+ return int(path.read_text().strip())
68
+ except (ValueError, OSError):
69
+ return 0
70
+ return 0
71
+
72
+
73
+ def _save_offset(offset_file: str | None, offset: int) -> None:
74
+ """Save offset to file."""
75
+ if not offset_file:
76
+ return
77
+ path = Path(offset_file)
78
+ try:
79
+ path.parent.mkdir(parents=True, exist_ok=True)
80
+ path.write_text(str(offset))
81
+ except OSError:
82
+ pass
83
+
84
+
85
+ def _run_telegram_poller(
86
+ token: str,
87
+ commands: list[TelegramCommandSpec],
88
+ config: TelegramPollerConfig,
89
+ logger: Callable[[str], None] | None = None,
90
+ stop_event: threading.Event | None = None,
91
+ ) -> None:
92
+ """Background polling loop for Telegram updates."""
93
+
94
+ def log(msg: str) -> None:
95
+ if logger:
96
+ logger(f"[telegram-poller] {msg}")
97
+
98
+ offset = _load_offset(config.offset_file)
99
+ log(f"Starting polling with offset={offset}")
100
+
101
+ command_map: dict[str, TelegramCommandHandler] = {cmd.name: cmd.handler for cmd in commands}
102
+
103
+ while not (stop_event and stop_event.is_set()):
104
+ try:
105
+ updates = fetch_telegram_updates(
106
+ token=token,
107
+ timeout=config.poll_timeout_seconds,
108
+ allowed_updates=config.allowed_updates,
109
+ offset=offset,
110
+ retries=config.max_retries,
111
+ retry_delay=config.retry_delay_seconds,
112
+ )
113
+
114
+ for update in updates:
115
+ try:
116
+ message = update.get("message", {})
117
+ if not message:
118
+ continue
119
+
120
+ text = message.get("text", "").strip()
121
+ chat_id = message.get("chat", {}).get("id")
122
+
123
+ if not text or not chat_id:
124
+ continue
125
+
126
+ # Parse command
127
+ cmd, args = parse_telegram_command(text)
128
+ if not cmd:
129
+ continue
130
+
131
+ # Dispatch to handler
132
+ handler = command_map.get(cmd)
133
+ if not handler:
134
+ log(f"Unknown command: /{cmd} from chat {chat_id}")
135
+ continue
136
+
137
+ log(f"Handling /{cmd} with args {args} from chat {chat_id}")
138
+ response = handler(args)
139
+
140
+ # Send response
141
+ if isinstance(response, dict):
142
+ response_text = json.dumps(response)
143
+ else:
144
+ response_text = str(response)
145
+
146
+ send_telegram_message(token, chat_id, response_text)
147
+
148
+ except Exception as e:
149
+ log(f"Error handling update {update.get('update_id')}: {e}")
150
+ continue
151
+
152
+ # Update offset after successful processing
153
+ offset = update.get("update_id", offset) + 1
154
+ _save_offset(config.offset_file, offset)
155
+
156
+ except Exception as e:
157
+ log(f"Polling error: {e}")
158
+ time.sleep(config.retry_delay_seconds)
159
+
160
+
161
+ def start_telegram_poller(
162
+ *,
163
+ token: str,
164
+ commands: list[TelegramCommandSpec],
165
+ config: TelegramPollerConfig | None = None,
166
+ logger: Callable[[str], None] | None = None,
167
+ instance_key: str = "default",
168
+ ) -> threading.Thread:
169
+ """
170
+ Start a background Telegram polling thread.
171
+
172
+ Args:
173
+ token: Telegram bot token.
174
+ commands: List of TelegramCommandSpec for command handlers.
175
+ config: TelegramPollerConfig for polling behavior. Defaults to TelegramPollerConfig().
176
+ logger: Optional logger function.
177
+ instance_key: Unique key for this poller instance (allows multiple pollers per process).
178
+
179
+ Returns:
180
+ The polling thread (already started).
181
+
182
+ Raises:
183
+ ValueError: If instance_key is already in use (to prevent duplicate pollers).
184
+
185
+ Example:
186
+ >>> config = TelegramPollerConfig(poll_timeout_seconds=30, offset_file="/tmp/tg_offset")
187
+ >>> commands = [
188
+ ... TelegramCommandSpec("status", "Get status", handler=on_status),
189
+ ... TelegramCommandSpec("trade", "Execute trade", handler=on_trade),
190
+ ... ]
191
+ >>> thread = start_telegram_poller(token=token, commands=commands, config=config)
192
+ >>> # Thread is now running in background
193
+ """
194
+ if config is None:
195
+ config = TelegramPollerConfig()
196
+
197
+ if instance_key in _TELEGRAM_POLLERS:
198
+ raise ValueError(f"Telegram poller with instance_key='{instance_key}' already running")
199
+
200
+ stop_event = threading.Event()
201
+ thread = threading.Thread(
202
+ name=f"{config.thread_name_prefix}-{instance_key}",
203
+ target=_run_telegram_poller,
204
+ args=(token, commands, config, logger, stop_event),
205
+ daemon=True,
206
+ )
207
+ thread.start()
208
+ _TELEGRAM_POLLERS[instance_key] = thread
209
+
210
+ return thread
@@ -0,0 +1,261 @@
1
+ Metadata-Version: 2.4
2
+ Name: chatops-bridge
3
+ Version: 0.1.0
4
+ Summary: Reusable Telegram and Discord ChatOps utilities for Python projects
5
+ Author: ChatOps Bridge Contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/trunghvbk-afataw/chatops
8
+ Project-URL: Repository, https://github.com/trunghvbk-afataw/chatops
9
+ Project-URL: Issues, https://github.com/trunghvbk-afataw/chatops/issues
10
+ Keywords: telegram,discord,webhook,chatops,notifications
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Communications :: Chat
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: awesome-notify-bridge>=0.1.5
26
+ Provides-Extra: discord-bot
27
+ Requires-Dist: discord.py>=2.4.0; extra == "discord-bot"
28
+ Dynamic: license-file
29
+
30
+ # chatops-bridge
31
+
32
+ Reusable Telegram and Discord utilities for Python applications.
33
+
34
+ This package was extracted from production automation code and generalized for multi-purpose ChatOps use.
35
+ Transport-level Telegram/Discord sending is reused from `awesome-notify-bridge`.
36
+
37
+ ## Features
38
+
39
+ - Telegram send/poll helpers:
40
+ - send text and images
41
+ - split long text by Telegram limit
42
+ - poll updates and extract message text
43
+ - robust chat id matching
44
+ - Discord webhook sender:
45
+ - send message chunks safely
46
+ - upload image files
47
+ - Discord slash bot:
48
+ - register any slash commands dynamically
49
+ - optional owner and guild restriction
50
+ - Plain text formatter helpers for chat-friendly output
51
+
52
+ ## Install
53
+
54
+ Requires Python 3.9+.
55
+
56
+ ```bash
57
+ pip install chatops-bridge
58
+ ```
59
+
60
+ For slash-bot support:
61
+
62
+ ```bash
63
+ pip install "chatops-bridge[discord-bot]"
64
+ ```
65
+
66
+ Install from GitHub:
67
+
68
+ ```bash
69
+ pip install git+https://github.com/<your-org>/chatops-bridge.git
70
+ ```
71
+
72
+ ## Quick Start
73
+
74
+ ### Telegram
75
+
76
+ ```python
77
+ from chatops_bridge.telegram import send_telegram_message
78
+
79
+ send_telegram_message(
80
+ chat_id="123456789",
81
+ token="<telegram-bot-token>",
82
+ text="Hello from chatops-bridge",
83
+ )
84
+ ```
85
+
86
+ Send with images:
87
+
88
+ ```python
89
+ send_telegram_message(
90
+ chat_id="123456789",
91
+ token="<telegram-bot-token>",
92
+ text="Report attached",
93
+ image_paths=["./charts/btc.png", "./charts/eth.png"],
94
+ )
95
+ ```
96
+
97
+ ### Discord webhook
98
+
99
+ ```python
100
+ from chatops_bridge.discord import send_discord_message
101
+
102
+ send_discord_message(
103
+ webhook_url="https://discord.com/api/webhooks/...",
104
+ content="Hello from chatops-bridge",
105
+ )
106
+ ```
107
+
108
+ Return value is `True` on success and `False` on failure.
109
+
110
+ ### Discord env-based channel
111
+
112
+ ```python
113
+ from chatops_bridge.discord_channels import send_to_discord_env_channel
114
+
115
+ # export DISCORD_ALERT_WEBHOOK_URL=...
116
+ send_to_discord_env_channel("DISCORD_ALERT_WEBHOOK_URL", "Alert triggered")
117
+
118
+ # strict mode: fail fast if env var is missing
119
+ send_to_discord_env_channel("DISCORD_ALERT_WEBHOOK_URL", "Alert triggered", strict_env=True)
120
+ ```
121
+
122
+ Useful when you want channel routing from environment variables instead of hardcoding webhook URLs.
123
+
124
+ ## Telegram Update Polling
125
+
126
+ ```python
127
+ from chatops_bridge.telegram import fetch_telegram_updates, extract_update_text
128
+
129
+ updates = fetch_telegram_updates(
130
+ token="<telegram-bot-token>",
131
+ offset=None,
132
+ timeout_seconds=10,
133
+ allowed_updates=["message", "edited_message"],
134
+ )
135
+
136
+ for update in updates:
137
+ text, message = extract_update_text(update)
138
+ if text:
139
+ print("message:", text)
140
+ ```
141
+
142
+ ## Discord Slash Bot
143
+
144
+ `chatops_bridge.discord_bot.start_discord_bot(...)` starts a background thread for a Discord bot using your command list.
145
+
146
+ ```python
147
+ import discord
148
+
149
+ from chatops_bridge.discord_bot import DiscordBotConfig, SlashCommandSpec, start_discord_bot
150
+
151
+ def ping() -> str:
152
+ return "pong"
153
+
154
+ def report() -> dict:
155
+ return {"text": "Report done", "image_paths": ["./out/chart.png"]}
156
+
157
+ start_discord_bot(
158
+ token="<discord-bot-token>",
159
+ commands=[
160
+ SlashCommandSpec(name="ping", description="Health check", handler=ping),
161
+ SlashCommandSpec(name="report", description="Generate report", handler=report),
162
+ ],
163
+ config=DiscordBotConfig(
164
+ intents=discord.Intents(guilds=True),
165
+ allow_dm_commands=False,
166
+ leave_unexpected_guild=True,
167
+ response_chunk_limit=1900,
168
+ max_images_per_response=10,
169
+ defer_thinking=True,
170
+ ),
171
+ guild_id=None,
172
+ owner_user_id=None,
173
+ instance_key="primary",
174
+ )
175
+ ```
176
+
177
+ You can also run the bot with `commands=[]` (or omit `commands`) when your app only needs Discord client events/background tasks.
178
+
179
+ `instance_key` is used to manage bot thread instances by key. Calling `start_discord_bot` again with the same key returns the existing live thread.
180
+
181
+ ## Slash Bot Example
182
+
183
+ See [examples/discord_bot_example.py](examples/discord_bot_example.py).
184
+
185
+ ## Telegram Poller Bot
186
+
187
+ `chatops_bridge.telegram_poller.start_telegram_poller(...)` starts a background polling thread that automatically handles Telegram updates and dispatches them to your command handlers.
188
+
189
+ ```python
190
+ from chatops_bridge.telegram_poller import TelegramCommandSpec, TelegramPollerConfig, start_telegram_poller
191
+
192
+ def handle_status(args: list[str]) -> str:
193
+ return "Status: OK"
194
+
195
+ def handle_trade(args: list[str]) -> str:
196
+ amount = float(args[0]) if args else 100.0
197
+ return f"Trading {amount} units"
198
+
199
+ start_telegram_poller(
200
+ token="<telegram-bot-token>",
201
+ commands=[
202
+ TelegramCommandSpec(
203
+ name="status",
204
+ description="Get bot status",
205
+ handler=handle_status,
206
+ ),
207
+ TelegramCommandSpec(
208
+ name="trade",
209
+ description="Execute trade",
210
+ handler=handle_trade,
211
+ ),
212
+ ],
213
+ config=TelegramPollerConfig(
214
+ poll_timeout_seconds=30,
215
+ max_retries=3,
216
+ retry_delay_seconds=5,
217
+ allowed_updates=["message", "edited_message"],
218
+ offset_file="/tmp/telegram_offset.txt", # Persist offset on restart
219
+ ),
220
+ logger=print, # Optional logger
221
+ )
222
+ ```
223
+
224
+ The poller:
225
+ - Runs in background thread(s)
226
+ - Automatically saves offset to persist progress across restarts
227
+ - Retries on network errors with configurable backoff
228
+ - Filters update types (e.g., messages only, skip reactions)
229
+ - Parses `/command arg1 arg2` style messages
230
+ - Dispatches to appropriate handler
231
+ - Sends response back to the originating chat
232
+
233
+ Handlers receive a list of arguments and return `str` or `dict` (JSON-serialized if dict).
234
+
235
+ ## Telegram Poller Example
236
+
237
+ See [examples/telegram_poller_example.py](examples/telegram_poller_example.py).
238
+
239
+ ## Common Environment Variables
240
+
241
+ - `DISCORD_BOT_TOKEN`: bot token used by the slash-bot example.
242
+ - `DISCORD_BOT_GUILD_ID`: optional guild restriction.
243
+ - `DISCORD_BOT_OWNER_USER_ID`: optional owner-only restriction.
244
+ - `TELEGRAM_BOT_TOKEN`: polling bot token.
245
+ - `TELEGRAM_BOT_OFFSET_FILE`: optional path to persist polling offset (for restart safety).
246
+ - Custom webhook URL variables (for env-based routing), for example:
247
+ - `DISCORD_ALERT_WEBHOOK_URL`
248
+ - `DISCORD_SIGNAL_WEBHOOK_URL`
249
+
250
+ ## License
251
+
252
+ MIT
253
+
254
+ ## Contributing
255
+
256
+ We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for:
257
+ - Development setup
258
+ - Making changes and submitting PRs
259
+ - Release procedures
260
+
261
+ For questions, open an issue on [GitHub](https://github.com/trunghvbk-afataw/chatops/issues).
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/chatops_bridge/__init__.py
5
+ src/chatops_bridge/discord.py
6
+ src/chatops_bridge/discord_bot.py
7
+ src/chatops_bridge/discord_channels.py
8
+ src/chatops_bridge/telegram.py
9
+ src/chatops_bridge/telegram_commands.py
10
+ src/chatops_bridge/telegram_format.py
11
+ src/chatops_bridge/telegram_poller.py
12
+ src/chatops_bridge.egg-info/PKG-INFO
13
+ src/chatops_bridge.egg-info/SOURCES.txt
14
+ src/chatops_bridge.egg-info/dependency_links.txt
15
+ src/chatops_bridge.egg-info/requires.txt
16
+ src/chatops_bridge.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ awesome-notify-bridge>=0.1.5
2
+
3
+ [discord-bot]
4
+ discord.py>=2.4.0
@@ -0,0 +1 @@
1
+ chatops_bridge