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

1""" 

2Channel Metadata Catalog — Static registry of all 31 supported channels. 

3 

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. 

6 

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""" 

19 

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 }, 

244 

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 }, 

365 

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 }, 

587 

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 }, 

649 

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 }, 

730 

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} 

801 

802 

803def get_channel_metadata(channel_type: str): 

804 """Get metadata for a single channel, or None.""" 

805 return CHANNEL_CATALOG.get(channel_type) 

806 

807 

808def list_all_channels(): 

809 """Return the full catalog dict.""" 

810 return CHANNEL_CATALOG 

811 

812 

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} 

816 

817 

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} 

821 

822 

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

827 

828 

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. 

832 

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 )