Coverage for integrations / channels / metadata.py: 100.0%
19 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""
2Channel Metadata Catalog — Static registry of all 31 supported channels.
4Provides per-channel: display name, auth method, setup fields, capabilities, icon, color.
5Used by the setup wizard UI and the /api/social/channels/catalog endpoint.
7OAuth click-through (PR O):
8- Channels that publish an OAuth 2.0 authorize endpoint additionally declare
9 ``oauth_authorize_url`` / ``oauth_token_url`` / ``oauth_scopes`` /
10 ``oauth_extra_params`` / ``oauth_token_response_map`` / ``oauth_uses_pkce``.
11- The Connect_Channel agent tool consults these fields *only when* the
12 operator has set ``HARTOS_OAUTH_CLIENT_<TYPE>`` env (client id + secret).
13 When env is unset the channel falls back to the existing setup_fields
14 paste-token flow — zero regression for legacy operators.
15- ``external_url`` points at the provider's app-management portal
16 (BotFather, Discord dev portal, etc.) and is also used by the paste
17 fallback for a "Open <provider>" button.
18"""
20CHANNEL_CATALOG = {
21 # ── Core Adapters ──────────────────────────────────────────
22 'telegram': {
23 'display_name': 'Telegram',
24 'icon': 'telegram',
25 'color': '#0088cc',
26 'category': 'core',
27 'auth_method': 'api_key',
28 'setup_fields': [
29 {'key': 'bot_token', 'label': 'Bot Token', 'type': 'password',
30 'help': 'Create a bot via @BotFather and paste the token here.'},
31 ],
32 'capabilities': {
33 'text': True, 'image': True, 'video': True, 'audio': True,
34 'document': True, 'sticker': True, 'location': True, 'voice': True,
35 'reactions': True, 'message_edit': True, 'message_delete': True,
36 'streaming': True, 'groups': True, 'threads': False,
37 'buttons': True, 'typing': True,
38 'max_message_length': 4096,
39 },
40 },
41 'discord': {
42 'display_name': 'Discord',
43 'icon': 'discord',
44 'color': '#5865f2',
45 'category': 'core',
46 'auth_method': 'api_key',
47 # OAuth bot-install URL (PR O). 274877990912 = Send Messages |
48 # Read Message History | View Channels — minimum bot perms.
49 'oauth_authorize_url': 'https://discord.com/api/oauth2/authorize',
50 'oauth_token_url': 'https://discord.com/api/oauth2/token',
51 'oauth_scopes': 'bot applications.commands',
52 'oauth_extra_params': {'permissions': '274877990912'},
53 'oauth_token_response_map': {'access_token': 'bot_token'},
54 'external_url': 'https://discord.com/developers/applications',
55 'setup_fields': [
56 {'key': 'bot_token', 'label': 'Bot Token', 'type': 'password',
57 'help': 'Create an application at discord.com/developers, add a Bot, and copy the token.'},
58 ],
59 'capabilities': {
60 'text': True, 'image': True, 'video': True, 'audio': True,
61 'document': True, 'sticker': False, 'location': False, 'voice': False,
62 'reactions': True, 'message_edit': True, 'message_delete': True,
63 'streaming': True, 'groups': True, 'threads': True,
64 'buttons': True, 'typing': True,
65 'max_message_length': 2000,
66 },
67 },
68 'slack': {
69 'display_name': 'Slack',
70 'icon': 'slack',
71 'color': '#4a154b',
72 'category': 'core',
73 'auth_method': 'api_key',
74 # Slack v2 OAuth — bot scopes only (no user scopes by default).
75 # Returns access_token=xoxb-... in the bot section of v2 response;
76 # token-exchange endpoint maps the bot.access_token into bot_token.
77 'oauth_authorize_url': 'https://slack.com/oauth/v2/authorize',
78 'oauth_token_url': 'https://slack.com/api/oauth.v2.access',
79 'oauth_scopes': 'chat:write,channels:history,channels:read,im:history,im:read,im:write,users:read',
80 'oauth_token_response_map': {
81 # Slack's bot_user_id comes from auth.test at adapter
82 # connect time (slack_adapter.py:101), not from this map —
83 # that's why we only persist bot.access_token here.
84 # signing_secret is operator-paste-only (not in OAuth resp).
85 'bot.access_token': 'bot_token',
86 },
87 'external_url': 'https://api.slack.com/apps',
88 'setup_fields': [
89 {'key': 'bot_token', 'label': 'Bot Token (xoxb-...)', 'type': 'password',
90 'help': 'Create a Slack App, install it to your workspace, and copy the Bot User OAuth Token.'},
91 {'key': 'signing_secret', 'label': 'Signing Secret', 'type': 'password',
92 'help': 'Found in your Slack App settings under Basic Information.'},
93 ],
94 'capabilities': {
95 'text': True, 'image': True, 'video': False, 'audio': False,
96 'document': True, 'sticker': False, 'location': False, 'voice': False,
97 'reactions': True, 'message_edit': True, 'message_delete': True,
98 'streaming': True, 'groups': True, 'threads': True,
99 'buttons': True, 'typing': True,
100 'max_message_length': 40000,
101 },
102 },
103 'whatsapp': {
104 'display_name': 'WhatsApp',
105 'icon': 'whatsapp',
106 'color': '#25d366',
107 'category': 'core',
108 # auth_method=qr_session — same vocabulary already used by
109 # telegram_user / discord_user. Pairs the user's existing
110 # WhatsApp account on their phone with a Nunba-side WAHA
111 # gateway by displaying a QR they scan from WhatsApp's own
112 # "Linked devices" UI. No API keys, no developer portal.
113 'auth_method': 'qr_session',
114 'setup_fields': [
115 # phone_number is the only user-visible field on the
116 # default path — everything else is provisioned by the
117 # WAHA gateway running on this Nunba install.
118 {'key': 'phone_number', 'label': 'Your WhatsApp Number', 'type': 'tel',
119 'help': 'Your own E.164 number (e.g. +<country><number>). '
120 'Each Nunba install binds its owner.'},
121 # auto: True — register_channel pre-fills these from env
122 # defaults (`WHATSAPP_API_URL`, `WHATSAPP_API_KEY`) so the
123 # user never has to know about WAHA's existence on the
124 # happy path. Connect_Channel's form-builder skips fields
125 # with auto:True; admin Channels page still shows them so
126 # operators can override for non-localhost WAHA.
127 {'key': 'api_url', 'label': 'WAHA Base URL', 'type': 'text',
128 'auto': True, 'default': 'http://localhost:3000',
129 'help': 'WAHA HTTP API endpoint. Defaults to localhost:3000; '
130 'override via WHATSAPP_API_URL env or admin page.'},
131 {'key': 'access_token', 'label': 'API Key', 'type': 'password',
132 'auto': True, 'default': '',
133 'help': 'WAHA API key. Empty for localhost WAHA in no-auth '
134 'mode (default); set via WHATSAPP_API_KEY env when '
135 'fronting WAHA with auth.'},
136 {'key': 'enable_self_chat_agent', 'label': 'Self-chat → Nunba',
137 'type': 'toggle', 'default': True,
138 'help': 'When you tap your own contact in WhatsApp ("Message '
139 'Yourself"), Nunba saves the note to memory and replies '
140 'in the same thread. Private — never fanned out.'},
141 ],
142 'capabilities': {
143 'text': True, 'image': True, 'video': True, 'audio': True,
144 'document': True, 'sticker': True, 'location': True, 'voice': True,
145 'reactions': False, 'message_edit': False, 'message_delete': False,
146 'streaming': False, 'groups': True, 'threads': False,
147 'buttons': False, 'typing': True,
148 'max_message_length': 4096,
149 },
150 },
151 'signal': {
152 'display_name': 'Signal',
153 'icon': 'signal',
154 'color': '#3a76f0',
155 'category': 'core',
156 'auth_method': 'credentials',
157 'setup_fields': [
158 {'key': 'signal_cli_path', 'label': 'signal-cli Path', 'type': 'text',
159 'help': 'Path to signal-cli binary on the server.'},
160 {'key': 'phone_number', 'label': 'Registered Phone Number', 'type': 'text',
161 'help': 'Phone number registered with Signal (e.g. +1234567890).'},
162 ],
163 'capabilities': {
164 'text': True, 'image': True, 'video': True, 'audio': True,
165 'document': True, 'sticker': True, 'location': False, 'voice': True,
166 'reactions': True, 'message_edit': False, 'message_delete': True,
167 'streaming': False, 'groups': True, 'threads': False,
168 'buttons': False, 'typing': True,
169 'max_message_length': 6000,
170 },
171 },
172 'imessage': {
173 'display_name': 'iMessage',
174 'icon': 'imessage',
175 'color': '#34c759',
176 'category': 'core',
177 'auth_method': 'credentials',
178 'setup_fields': [
179 {'key': 'bridge_url', 'label': 'iMessage Bridge URL', 'type': 'text',
180 'help': 'Requires a macOS machine running the iMessage bridge.'},
181 ],
182 'capabilities': {
183 'text': True, 'image': True, 'video': True, 'audio': True,
184 'document': True, 'sticker': False, 'location': False, 'voice': False,
185 'reactions': True, 'message_edit': False, 'message_delete': False,
186 'streaming': False, 'groups': True, 'threads': False,
187 'buttons': False, 'typing': True,
188 'max_message_length': 20000,
189 },
190 },
191 'google_chat': {
192 'display_name': 'Google Chat',
193 'icon': 'google_chat',
194 'color': '#00ac47',
195 'category': 'core',
196 'auth_method': 'oauth2',
197 # Google requires PKCE for installed-app flows + access_type=offline
198 # for refresh tokens. prompt=consent forces the consent screen
199 # so the user always sees what scopes are being granted.
200 'oauth_authorize_url': 'https://accounts.google.com/o/oauth2/v2/auth',
201 'oauth_token_url': 'https://oauth2.googleapis.com/token',
202 'oauth_scopes': 'https://www.googleapis.com/auth/chat.messages '
203 'https://www.googleapis.com/auth/chat.spaces',
204 'oauth_extra_params': {'access_type': 'offline', 'prompt': 'consent'},
205 'oauth_token_response_map': {
206 'access_token': 'access_token',
207 'refresh_token': 'refresh_token',
208 },
209 'oauth_uses_pkce': True,
210 'external_url': 'https://console.cloud.google.com/apis/credentials',
211 'setup_fields': [
212 {'key': 'client_id', 'label': 'OAuth Client ID', 'type': 'text',
213 'help': 'From Google Cloud Console → APIs & Services → Credentials.'},
214 {'key': 'client_secret', 'label': 'OAuth Client Secret', 'type': 'password'},
215 ],
216 'capabilities': {
217 'text': True, 'image': True, 'video': False, 'audio': False,
218 'document': True, 'sticker': False, 'location': False, 'voice': False,
219 'reactions': False, 'message_edit': True, 'message_delete': True,
220 'streaming': False, 'groups': True, 'threads': True,
221 'buttons': True, 'typing': False,
222 'max_message_length': 4096,
223 },
224 },
225 'web': {
226 'display_name': 'Web Chat',
227 'icon': 'web',
228 'color': '#6c63ff',
229 'category': 'core',
230 'auth_method': 'api_key',
231 'setup_fields': [
232 {'key': 'widget_key', 'label': 'Widget API Key', 'type': 'password',
233 'help': 'Auto-generated. Embed the widget JS on your site.'},
234 ],
235 'capabilities': {
236 'text': True, 'image': True, 'video': True, 'audio': True,
237 'document': True, 'sticker': False, 'location': False, 'voice': False,
238 'reactions': False, 'message_edit': True, 'message_delete': True,
239 'streaming': True, 'groups': False, 'threads': False,
240 'buttons': True, 'typing': True,
241 'max_message_length': 10000,
242 },
243 },
245 # ── Enterprise Adapters ────────────────────────────────────
246 'teams': {
247 'display_name': 'Microsoft Teams',
248 'icon': 'teams',
249 'color': '#6264a7',
250 'category': 'enterprise',
251 'auth_method': 'oauth2',
252 # Microsoft Identity v2.0 endpoint. Tenant 'common' supports
253 # multi-tenant org sign-in; operator can override per-tenant via
254 # HARTOS_OAUTH_TENANT_TEAMS env if they registered a single-tenant
255 # app. PKCE recommended for public clients; we always send it.
256 'oauth_authorize_url': 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
257 'oauth_token_url': 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
258 'oauth_scopes': 'https://graph.microsoft.com/Chat.ReadWrite '
259 'https://graph.microsoft.com/ChannelMessage.Send '
260 'offline_access',
261 'oauth_token_response_map': {
262 'access_token': 'access_token',
263 'refresh_token': 'refresh_token',
264 },
265 'oauth_uses_pkce': True,
266 'external_url': 'https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade',
267 'setup_fields': [
268 {'key': 'app_id', 'label': 'Microsoft App ID', 'type': 'text',
269 'help': 'From Azure Bot registration.'},
270 {'key': 'app_password', 'label': 'App Password', 'type': 'password'},
271 ],
272 'capabilities': {
273 'text': True, 'image': True, 'video': True, 'audio': True,
274 'document': True, 'sticker': False, 'location': False, 'voice': False,
275 'reactions': True, 'message_edit': True, 'message_delete': True,
276 'streaming': True, 'groups': True, 'threads': True,
277 'buttons': True, 'typing': True,
278 'max_message_length': 28000,
279 },
280 },
281 'mattermost': {
282 'display_name': 'Mattermost',
283 'icon': 'mattermost',
284 'color': '#0058cc',
285 'category': 'enterprise',
286 'auth_method': 'websocket_token',
287 'setup_fields': [
288 {'key': 'server_url', 'label': 'Mattermost Server URL', 'type': 'text',
289 'help': 'e.g. https://your-mattermost.example.com'},
290 {'key': 'access_token', 'label': 'Personal Access Token', 'type': 'password',
291 'help': 'Generate in Mattermost → Account Settings → Security.'},
292 ],
293 'capabilities': {
294 'text': True, 'image': True, 'video': True, 'audio': True,
295 'document': True, 'sticker': False, 'location': False, 'voice': False,
296 'reactions': True, 'message_edit': True, 'message_delete': True,
297 'streaming': True, 'groups': True, 'threads': True,
298 'buttons': True, 'typing': True,
299 'max_message_length': 16383,
300 },
301 },
302 'matrix': {
303 'display_name': 'Matrix',
304 'icon': 'matrix',
305 'color': '#0dbd8b',
306 'category': 'enterprise',
307 'auth_method': 'websocket_token',
308 'setup_fields': [
309 {'key': 'homeserver_url', 'label': 'Homeserver URL', 'type': 'text',
310 'help': 'e.g. https://matrix.org'},
311 {'key': 'access_token', 'label': 'Access Token', 'type': 'password',
312 'help': 'From Element → Settings → Help & About → Access Token.'},
313 ],
314 'capabilities': {
315 'text': True, 'image': True, 'video': True, 'audio': True,
316 'document': True, 'sticker': True, 'location': True, 'voice': False,
317 'reactions': True, 'message_edit': True, 'message_delete': True,
318 'streaming': True, 'groups': True, 'threads': True,
319 'buttons': False, 'typing': True,
320 'max_message_length': 65536,
321 },
322 },
323 'nextcloud': {
324 'display_name': 'Nextcloud Talk',
325 'icon': 'nextcloud',
326 'color': '#0082c9',
327 'category': 'enterprise',
328 'auth_method': 'websocket_token',
329 'setup_fields': [
330 {'key': 'server_url', 'label': 'Nextcloud URL', 'type': 'text'},
331 {'key': 'username', 'label': 'Username', 'type': 'text'},
332 {'key': 'app_password', 'label': 'App Password', 'type': 'password',
333 'help': 'Generate in Nextcloud → Settings → Security → App Passwords.'},
334 ],
335 'capabilities': {
336 'text': True, 'image': True, 'video': True, 'audio': True,
337 'document': True, 'sticker': False, 'location': False, 'voice': False,
338 'reactions': True, 'message_edit': True, 'message_delete': True,
339 'streaming': True, 'groups': True, 'threads': False,
340 'buttons': False, 'typing': True,
341 'max_message_length': 32000,
342 },
343 },
344 'rocketchat': {
345 'display_name': 'Rocket.Chat',
346 'icon': 'rocketchat',
347 'color': '#f5455c',
348 'category': 'enterprise',
349 'auth_method': 'websocket_token',
350 'setup_fields': [
351 {'key': 'server_url', 'label': 'Rocket.Chat URL', 'type': 'text'},
352 {'key': 'user_id', 'label': 'User ID', 'type': 'text'},
353 {'key': 'auth_token', 'label': 'Auth Token', 'type': 'password',
354 'help': 'Generate in Administration → Integrations or your profile.'},
355 ],
356 'capabilities': {
357 'text': True, 'image': True, 'video': True, 'audio': True,
358 'document': True, 'sticker': False, 'location': False, 'voice': False,
359 'reactions': True, 'message_edit': True, 'message_delete': True,
360 'streaming': True, 'groups': True, 'threads': True,
361 'buttons': True, 'typing': True,
362 'max_message_length': 65536,
363 },
364 },
366 # ── Social Adapters ────────────────────────────────────────
367 'messenger': {
368 'display_name': 'Facebook Messenger',
369 'icon': 'messenger',
370 'color': '#0084ff',
371 'category': 'social',
372 'auth_method': 'api_key',
373 # Meta's Login Dialog → page access token. pages_messaging is
374 # required to send DMs; pages_show_list lets the user pick which
375 # Page to bind. After /oauth/callback, the access_token is a
376 # short-lived user token; we exchange it for a long-lived page
377 # token in api_channels._exchange_oauth_code (Meta-specific
378 # post-step) so what gets stored is page_access_token.
379 'oauth_authorize_url': 'https://www.facebook.com/v18.0/dialog/oauth',
380 'oauth_token_url': 'https://graph.facebook.com/v18.0/oauth/access_token',
381 'oauth_scopes': 'pages_messaging,pages_show_list,pages_manage_metadata',
382 'oauth_token_response_map': {'access_token': 'page_access_token'},
383 'external_url': 'https://developers.facebook.com/apps/',
384 'setup_fields': [
385 {'key': 'page_access_token', 'label': 'Page Access Token', 'type': 'password',
386 'help': 'From Meta Developer Portal → Your App → Messenger → Settings.'},
387 {'key': 'app_secret', 'label': 'App Secret', 'type': 'password'},
388 {'key': 'verify_token', 'label': 'Webhook Verify Token', 'type': 'text'},
389 ],
390 'capabilities': {
391 'text': True, 'image': True, 'video': True, 'audio': True,
392 'document': True, 'sticker': False, 'location': True, 'voice': False,
393 'reactions': False, 'message_edit': False, 'message_delete': False,
394 'streaming': False, 'groups': False, 'threads': False,
395 'buttons': True, 'typing': True,
396 'max_message_length': 2000,
397 },
398 },
399 'instagram': {
400 'display_name': 'Instagram',
401 'icon': 'instagram',
402 'color': '#e4405f',
403 'category': 'social',
404 'auth_method': 'api_key',
405 # Instagram Graph API uses Meta's same OAuth machinery; instagram_basic
406 # + instagram_manage_messages = DM access on a Professional account.
407 'oauth_authorize_url': 'https://www.facebook.com/v18.0/dialog/oauth',
408 'oauth_token_url': 'https://graph.facebook.com/v18.0/oauth/access_token',
409 'oauth_scopes': 'instagram_basic,instagram_manage_messages,pages_show_list',
410 'oauth_token_response_map': {'access_token': 'page_access_token'},
411 'external_url': 'https://developers.facebook.com/apps/',
412 'setup_fields': [
413 {'key': 'page_access_token', 'label': 'Instagram Access Token', 'type': 'password',
414 'help': 'Requires a connected Facebook Page with Instagram Professional account.'},
415 {'key': 'app_secret', 'label': 'App Secret', 'type': 'password'},
416 ],
417 'capabilities': {
418 'text': True, 'image': True, 'video': False, 'audio': False,
419 'document': False, 'sticker': False, 'location': False, 'voice': False,
420 'reactions': False, 'message_edit': False, 'message_delete': False,
421 'streaming': False, 'groups': False, 'threads': False,
422 'buttons': False, 'typing': True,
423 'max_message_length': 1000,
424 },
425 },
426 'twitter': {
427 'display_name': 'Twitter / X',
428 'icon': 'twitter',
429 'color': '#1da1f2',
430 'category': 'social',
431 'auth_method': 'api_key',
432 # Twitter / X v2 OAuth 2.0 with PKCE (mandatory for OAuth 2.0).
433 # The legacy 5-token v1.1 form stays as the paste fallback for
434 # operators using the older API; OAuth 2.0 only returns
435 # access_token + refresh_token (no api_key/secret).
436 'oauth_authorize_url': 'https://twitter.com/i/oauth2/authorize',
437 'oauth_token_url': 'https://api.twitter.com/2/oauth2/token',
438 'oauth_scopes': 'tweet.read tweet.write users.read dm.read dm.write offline.access',
439 'oauth_token_response_map': {
440 'access_token': 'bearer_token',
441 'refresh_token': 'refresh_token',
442 },
443 'oauth_uses_pkce': True,
444 'external_url': 'https://developer.twitter.com/en/portal/dashboard',
445 'setup_fields': [
446 {'key': 'bearer_token', 'label': 'API v2 Bearer Token', 'type': 'password',
447 'help': 'From developer.twitter.com → Projects → Keys & Tokens.'},
448 {'key': 'api_key', 'label': 'API Key', 'type': 'password'},
449 {'key': 'api_secret', 'label': 'API Secret', 'type': 'password'},
450 {'key': 'access_token', 'label': 'Access Token', 'type': 'password'},
451 {'key': 'access_secret', 'label': 'Access Token Secret', 'type': 'password'},
452 ],
453 'capabilities': {
454 'text': True, 'image': True, 'video': True, 'audio': False,
455 'document': False, 'sticker': False, 'location': False, 'voice': False,
456 'reactions': False, 'message_edit': False, 'message_delete': True,
457 'streaming': False, 'groups': False, 'threads': False,
458 'buttons': False, 'typing': False,
459 'max_message_length': 10000,
460 },
461 },
462 'line': {
463 'display_name': 'LINE',
464 'icon': 'line',
465 'color': '#00b900',
466 'category': 'social',
467 'auth_method': 'api_key',
468 # LINE Login OAuth 2.1 — issues a user access_token; for Messaging
469 # API channel binding the operator still needs a Channel Access
470 # Token (long-lived) which OAuth doesn't issue. We therefore
471 # treat this as half-OAuth: the click-through performs identity
472 # verification + paste-token follow-up for the Channel Access
473 # Token. oauth_token_response_map is intentionally empty so
474 # /oauth/callback does NOT auto-write a binding; instead it
475 # bounces back to the paste form pre-filled with the verified
476 # channel_id (api_channels handles this LINE-specific path).
477 'oauth_authorize_url': 'https://access.line.me/oauth2/v2.1/authorize',
478 'oauth_token_url': 'https://api.line.me/oauth2/v2.1/token',
479 'oauth_scopes': 'profile openid',
480 'oauth_token_response_map': {},
481 'external_url': 'https://developers.line.biz/console/',
482 'setup_fields': [
483 {'key': 'channel_access_token', 'label': 'Channel Access Token', 'type': 'password',
484 'help': 'From LINE Developers Console → Messaging API → Channel access token.'},
485 {'key': 'channel_secret', 'label': 'Channel Secret', 'type': 'password'},
486 ],
487 'capabilities': {
488 'text': True, 'image': True, 'video': True, 'audio': True,
489 'document': False, 'sticker': True, 'location': True, 'voice': False,
490 'reactions': False, 'message_edit': False, 'message_delete': False,
491 'streaming': False, 'groups': True, 'threads': False,
492 'buttons': True, 'typing': True,
493 'max_message_length': 5000,
494 },
495 },
496 'viber': {
497 'display_name': 'Viber',
498 'icon': 'viber',
499 'color': '#665cac',
500 'category': 'social',
501 'auth_method': 'api_key',
502 'setup_fields': [
503 {'key': 'auth_token', 'label': 'Bot Auth Token', 'type': 'password',
504 'help': 'From partners.viber.com → Create Bot Account.'},
505 ],
506 'capabilities': {
507 'text': True, 'image': True, 'video': True, 'audio': False,
508 'document': True, 'sticker': True, 'location': True, 'voice': False,
509 'reactions': False, 'message_edit': False, 'message_delete': False,
510 'streaming': False, 'groups': True, 'threads': False,
511 'buttons': True, 'typing': True,
512 'max_message_length': 7000,
513 },
514 },
515 'wechat': {
516 'display_name': 'WeChat',
517 'icon': 'wechat',
518 'color': '#07c160',
519 'category': 'social',
520 'auth_method': 'api_key',
521 'setup_fields': [
522 {'key': 'app_id', 'label': 'Official Account App ID', 'type': 'text'},
523 {'key': 'app_secret', 'label': 'App Secret', 'type': 'password'},
524 {'key': 'token', 'label': 'Server Token', 'type': 'password',
525 'help': 'The token you set in WeChat Official Account settings.'},
526 ],
527 'capabilities': {
528 'text': True, 'image': True, 'video': True, 'audio': True,
529 'document': False, 'sticker': False, 'location': True, 'voice': True,
530 'reactions': False, 'message_edit': False, 'message_delete': False,
531 'streaming': False, 'groups': False, 'threads': False,
532 'buttons': False, 'typing': False,
533 'max_message_length': 2048,
534 },
535 },
536 'zalo': {
537 'display_name': 'Zalo',
538 'icon': 'zalo',
539 'color': '#0068ff',
540 'category': 'social',
541 'auth_method': 'api_key',
542 'setup_fields': [
543 {'key': 'oa_access_token', 'label': 'Official Account Access Token', 'type': 'password',
544 'help': 'From Zalo Developers → Your OA → Settings.'},
545 ],
546 'capabilities': {
547 'text': True, 'image': True, 'video': False, 'audio': False,
548 'document': True, 'sticker': True, 'location': False, 'voice': False,
549 'reactions': False, 'message_edit': False, 'message_delete': False,
550 'streaming': False, 'groups': False, 'threads': False,
551 'buttons': True, 'typing': True,
552 'max_message_length': 2000,
553 },
554 },
555 'twitch': {
556 'display_name': 'Twitch',
557 'icon': 'twitch',
558 'color': '#9146ff',
559 'category': 'social',
560 'auth_method': 'api_key',
561 # Twitch authorization code flow. chat:read + chat:edit are the
562 # IRC scopes needed for the legacy chat bot; channel:bot is the
563 # newer EventSub scope for the Helix-based bot path. We request
564 # both so the adapter can pick at runtime.
565 'oauth_authorize_url': 'https://id.twitch.tv/oauth2/authorize',
566 'oauth_token_url': 'https://id.twitch.tv/oauth2/token',
567 'oauth_scopes': 'chat:read chat:edit channel:bot user:bot',
568 'oauth_token_response_map': {
569 'access_token': 'oauth_token',
570 'refresh_token': 'refresh_token',
571 },
572 'external_url': 'https://dev.twitch.tv/console/apps',
573 'setup_fields': [
574 {'key': 'oauth_token', 'label': 'OAuth Token', 'type': 'password',
575 'help': 'Generate at twitchapps.com/tmi or via Twitch Developer portal.'},
576 {'key': 'channel_name', 'label': 'Channel Name', 'type': 'text'},
577 ],
578 'capabilities': {
579 'text': True, 'image': False, 'video': False, 'audio': False,
580 'document': False, 'sticker': False, 'location': False, 'voice': False,
581 'reactions': False, 'message_edit': False, 'message_delete': True,
582 'streaming': False, 'groups': True, 'threads': False,
583 'buttons': False, 'typing': False,
584 'max_message_length': 500,
585 },
586 },
588 # ── Decentralized Adapters ─────────────────────────────────
589 'nostr': {
590 'display_name': 'Nostr',
591 'icon': 'nostr',
592 'color': '#8e30eb',
593 'category': 'decentralized',
594 'auth_method': 'api_key',
595 'setup_fields': [
596 {'key': 'private_key', 'label': 'Private Key (nsec)', 'type': 'password',
597 'help': 'Your Nostr private key. Keep this safe!'},
598 {'key': 'relays', 'label': 'Relay URLs (comma-separated)', 'type': 'text',
599 'help': 'e.g. wss://relay.damus.io,wss://nos.lol'},
600 ],
601 'capabilities': {
602 'text': True, 'image': True, 'video': False, 'audio': False,
603 'document': False, 'sticker': False, 'location': False, 'voice': False,
604 'reactions': True, 'message_edit': False, 'message_delete': False,
605 'streaming': False, 'groups': True, 'threads': True,
606 'buttons': False, 'typing': False,
607 'max_message_length': 65535,
608 },
609 },
610 'tlon': {
611 'display_name': 'Tlon (Urbit)',
612 'icon': 'tlon',
613 'color': '#1a1a2e',
614 'category': 'decentralized',
615 'auth_method': 'credentials',
616 'setup_fields': [
617 {'key': 'ship_url', 'label': 'Ship URL', 'type': 'text',
618 'help': 'Your Urbit ship HTTP URL (e.g. http://localhost:8080).'},
619 {'key': 'access_code', 'label': 'Access Code (+code)', 'type': 'password'},
620 ],
621 'capabilities': {
622 'text': True, 'image': True, 'video': False, 'audio': False,
623 'document': False, 'sticker': False, 'location': False, 'voice': False,
624 'reactions': False, 'message_edit': True, 'message_delete': True,
625 'streaming': False, 'groups': True, 'threads': True,
626 'buttons': False, 'typing': False,
627 'max_message_length': 65535,
628 },
629 },
630 'openprose': {
631 'display_name': 'OpenProse',
632 'icon': 'openprose',
633 'color': '#ff6b35',
634 'category': 'decentralized',
635 'auth_method': 'api_key',
636 'setup_fields': [
637 {'key': 'node_url', 'label': 'Node URL', 'type': 'text'},
638 {'key': 'api_key', 'label': 'API Key', 'type': 'password'},
639 ],
640 'capabilities': {
641 'text': True, 'image': True, 'video': False, 'audio': False,
642 'document': True, 'sticker': False, 'location': False, 'voice': False,
643 'reactions': True, 'message_edit': True, 'message_delete': True,
644 'streaming': False, 'groups': True, 'threads': True,
645 'buttons': False, 'typing': False,
646 'max_message_length': 65535,
647 },
648 },
650 # ── Bridge / User-Account Adapters ─────────────────────────
651 'telegram_user': {
652 'display_name': 'Telegram (User Account)',
653 'icon': 'telegram',
654 'color': '#0088cc',
655 'category': 'bridge',
656 'auth_method': 'qr_session',
657 'setup_fields': [
658 {'key': 'api_id', 'label': 'API ID', 'type': 'text',
659 'help': 'From my.telegram.org → API Development Tools.'},
660 {'key': 'api_hash', 'label': 'API Hash', 'type': 'password'},
661 {'key': 'phone', 'label': 'Phone Number', 'type': 'text',
662 'help': 'Your Telegram phone number for 2FA verification.'},
663 ],
664 'capabilities': {
665 'text': True, 'image': True, 'video': True, 'audio': True,
666 'document': True, 'sticker': True, 'location': True, 'voice': True,
667 'reactions': True, 'message_edit': True, 'message_delete': True,
668 'streaming': True, 'groups': True, 'threads': False,
669 'buttons': False, 'typing': True,
670 'max_message_length': 4096,
671 },
672 },
673 'discord_user': {
674 'display_name': 'Discord (User Account)',
675 'icon': 'discord',
676 'color': '#5865f2',
677 'category': 'bridge',
678 'auth_method': 'qr_session',
679 'setup_fields': [
680 {'key': 'user_token', 'label': 'User Token', 'type': 'password',
681 'help': 'WARNING: Self-bots violate Discord ToS. Use at your own risk.'},
682 ],
683 'capabilities': {
684 'text': True, 'image': True, 'video': True, 'audio': True,
685 'document': True, 'sticker': True, 'location': False, 'voice': False,
686 'reactions': True, 'message_edit': True, 'message_delete': True,
687 'streaming': True, 'groups': True, 'threads': True,
688 'buttons': False, 'typing': True,
689 'max_message_length': 2000,
690 },
691 },
692 'zalo_user': {
693 'display_name': 'Zalo (User Account)',
694 'icon': 'zalo',
695 'color': '#0068ff',
696 'category': 'bridge',
697 'auth_method': 'phone_2fa',
698 'setup_fields': [
699 {'key': 'phone', 'label': 'Phone Number', 'type': 'text'},
700 ],
701 'capabilities': {
702 'text': True, 'image': True, 'video': False, 'audio': False,
703 'document': True, 'sticker': True, 'location': False, 'voice': False,
704 'reactions': False, 'message_edit': False, 'message_delete': False,
705 'streaming': False, 'groups': True, 'threads': False,
706 'buttons': False, 'typing': True,
707 'max_message_length': 2000,
708 },
709 },
710 'bluebubbles': {
711 'display_name': 'BlueBubbles (iMessage)',
712 'icon': 'imessage',
713 'color': '#34c759',
714 'category': 'bridge',
715 'auth_method': 'websocket_token',
716 'setup_fields': [
717 {'key': 'server_url', 'label': 'BlueBubbles Server URL', 'type': 'text',
718 'help': 'Your BlueBubbles server address (requires macOS host).'},
719 {'key': 'password', 'label': 'Server Password', 'type': 'password'},
720 ],
721 'capabilities': {
722 'text': True, 'image': True, 'video': True, 'audio': True,
723 'document': True, 'sticker': False, 'location': False, 'voice': False,
724 'reactions': True, 'message_edit': False, 'message_delete': False,
725 'streaming': False, 'groups': True, 'threads': False,
726 'buttons': False, 'typing': True,
727 'max_message_length': 20000,
728 },
729 },
731 # ── Utility Adapters ───────────────────────────────────────
732 'email': {
733 'display_name': 'Email',
734 'icon': 'email',
735 'color': '#ea4335',
736 'category': 'utility',
737 'auth_method': 'credentials',
738 'setup_fields': [
739 {'key': 'imap_host', 'label': 'IMAP Server', 'type': 'text',
740 'help': 'e.g. imap.gmail.com'},
741 {'key': 'smtp_host', 'label': 'SMTP Server', 'type': 'text',
742 'help': 'e.g. smtp.gmail.com'},
743 {'key': 'email', 'label': 'Email Address', 'type': 'text'},
744 {'key': 'password', 'label': 'Password / App Password', 'type': 'password',
745 'help': 'Use an App Password for Gmail (2FA required).'},
746 ],
747 'capabilities': {
748 'text': True, 'image': True, 'video': False, 'audio': False,
749 'document': True, 'sticker': False, 'location': False, 'voice': False,
750 'reactions': False, 'message_edit': False, 'message_delete': False,
751 'streaming': False, 'groups': False, 'threads': True,
752 'buttons': False, 'typing': False,
753 'max_message_length': 100000,
754 },
755 },
756 'voice': {
757 'display_name': 'Voice (Twilio/Vonage)',
758 'icon': 'voice',
759 'color': '#f22f46',
760 'category': 'utility',
761 'auth_method': 'api_key',
762 'setup_fields': [
763 {'key': 'provider', 'label': 'Provider', 'type': 'select',
764 'options': ['twilio', 'vonage'],
765 'help': 'Choose your voice provider.'},
766 {'key': 'account_sid', 'label': 'Account SID', 'type': 'text'},
767 {'key': 'auth_token', 'label': 'Auth Token', 'type': 'password'},
768 {'key': 'phone_number', 'label': 'Phone Number', 'type': 'text'},
769 ],
770 'capabilities': {
771 'text': False, 'image': False, 'video': False, 'audio': True,
772 'document': False, 'sticker': False, 'location': False, 'voice': True,
773 'reactions': False, 'message_edit': False, 'message_delete': False,
774 'streaming': False, 'groups': False, 'threads': False,
775 'buttons': False, 'typing': False,
776 'max_message_length': 0,
777 },
778 },
779 'hardware': {
780 'display_name': 'Hardware (GPIO/ROS)',
781 'icon': 'hardware',
782 'color': '#607d8b',
783 'category': 'utility',
784 'auth_method': 'credentials',
785 'setup_fields': [
786 {'key': 'interface', 'label': 'Interface Type', 'type': 'select',
787 'options': ['gpio', 'ros', 'serial'],
788 'help': 'Choose the hardware interface.'},
789 {'key': 'port', 'label': 'Port / Topic', 'type': 'text'},
790 ],
791 'capabilities': {
792 'text': True, 'image': False, 'video': False, 'audio': False,
793 'document': False, 'sticker': False, 'location': False, 'voice': False,
794 'reactions': False, 'message_edit': False, 'message_delete': False,
795 'streaming': False, 'groups': False, 'threads': False,
796 'buttons': False, 'typing': False,
797 'max_message_length': 1024,
798 },
799 },
800}
803def get_channel_metadata(channel_type: str):
804 """Get metadata for a single channel, or None."""
805 return CHANNEL_CATALOG.get(channel_type)
808def list_all_channels():
809 """Return the full catalog dict."""
810 return CHANNEL_CATALOG
813def get_channels_by_category(category: str):
814 """Filter channels by category (core, enterprise, social, decentralized, bridge, utility)."""
815 return {k: v for k, v in CHANNEL_CATALOG.items() if v.get('category') == category}
818def get_channels_by_auth_method(method: str):
819 """Filter channels by auth method."""
820 return {k: v for k, v in CHANNEL_CATALOG.items() if v.get('auth_method') == method}
823def is_oauth_capable(channel_type: str) -> bool:
824 """True iff the channel's metadata declares an OAuth authorize URL."""
825 meta = CHANNEL_CATALOG.get(channel_type)
826 return bool(meta and meta.get('oauth_authorize_url'))
829def is_oauth_configured(channel_type: str, env_lookup=None) -> bool:
830 """True iff the channel is OAuth-capable AND the operator has set
831 HARTOS_OAUTH_CLIENT_<TYPE> + HARTOS_OAUTH_SECRET_<TYPE> env vars.
833 When False, Connect_Channel falls back to the paste-token form
834 so legacy operators with pre-registered tokens stay unaffected.
835 """
836 if not is_oauth_capable(channel_type):
837 return False
838 if env_lookup is None:
839 import os
840 env_lookup = os.environ.get
841 upper = channel_type.upper()
842 return bool(
843 env_lookup(f'HARTOS_OAUTH_CLIENT_{upper}')
844 and env_lookup(f'HARTOS_OAUTH_SECRET_{upper}')
845 )