{"record":{"id":"732c66db2d95d626","repo":"koala73/worldmonitor","slug":"telegram-channel-must-be-linked-through-bot-pairing","errorCode":"telegram channel must be linked through bot pairing","errorMessage":"telegram channel must be linked through bot pairing","messagePattern":"telegram channel must be linked through bot pairing","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/notificationChannels.ts","lineNumber":224,"sourceCode":"    email: v.optional(v.string()),\n    webhookLabel: v.optional(v.string()),\n    scheduleWelcome: v.optional(v.boolean()),\n    // Internal-only: derived by the relay HTTP handler from Clerk, never the body.\n    verifiedAccountEmail: v.optional(v.string()),\n  },\n  handler: async (ctx, args) => {\n    const { userId, channelType, webhookEnvelope, email, webhookLabel } = args;\n    const existing = await ctx.db\n      .query(\"notificationChannels\")\n      .withIndex(\"by_user_channel\", (q) =>\n        q.eq(\"userId\", userId).eq(\"channelType\", channelType),\n      )\n      .unique();\n    const isNew = !existing;\n    let channelId = existing ? String(existing._id) : \"\";\n    const now = Date.now();\n    if (channelType === \"telegram\") {\n      throw new ConvexError(\"telegram channel must be linked through bot pairing\");\n    } else if (channelType === \"slack\") {\n      if (!webhookEnvelope) throw new ConvexError(\"webhookEnvelope required for slack channel\");\n      const doc = { userId, channelType: \"slack\" as const, webhookEnvelope, verified: true, linkedAt: now };\n      if (existing) { await ctx.db.replace(existing._id, doc); } else { channelId = String(await ctx.db.insert(\"notificationChannels\", doc)); }\n    } else if (channelType === \"email\") {\n      await assertProEntitlement(ctx, userId);\n      const recipient = requireVerifiedAccountEmail(email, args.verifiedAccountEmail);\n      const doc = { userId, channelType: \"email\" as const, email: recipient, emailOwnership: \"verified_account\" as const, verified: true, linkedAt: now };\n      if (existing) { await ctx.db.replace(existing._id, doc); } else { channelId = String(await ctx.db.insert(\"notificationChannels\", doc)); }\n    } else if (channelType === \"webhook\") {\n      if (!webhookEnvelope) throw new ConvexError(\"webhookEnvelope required for webhook channel\");\n      const doc = { userId, channelType: \"webhook\" as const, webhookEnvelope, verified: true, linkedAt: now, webhookLabel };\n      if (existing) { await ctx.db.replace(existing._id, doc); } else { channelId = String(await ctx.db.insert(\"notificationChannels\", doc)); }\n    } else {\n      throw new ConvexError(\"discord channel must be set via set-discord-oauth\");\n    }\n    if (isNew && args.scheduleWelcome === true) {\n      await ctx.scheduler.runAfter(","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/notificationChannels.ts#L206-L242","documentation":"The notification-channel linking mutation rejects channelType \"telegram\" outright: Telegram channels cannot be created by supplying a webhook/token like Slack or email. They may only be established through the bot-pairing flow (user links via the bot), so this code path treats any telegram insert/update request as unsupported.","triggerScenarios":"Calling the link-notification-channel mutation with channelType \"telegram\" and expecting it to create or update a notificationChannels document directly.","commonSituations":"Client UI or script migrated from a generic channel-linking form to Telegram; developer tries to seed a Telegram channel server-side; a stale client still offers 'Telegram' in its channel-type dropdown.","solutions":["Use the Telegram bot pairing flow (/start with the pairing code) instead of this mutation.","Remove 'telegram' from client-side channel-type options for this direct-linking path.","If a Telegram channel must be created programmatically, route through the bot-pairing internal mutation that owns that flow."],"exampleFix":"// before\nawait api.notificationChannels.link({ channelType: \"telegram\", telegramChatId: \"123\" });\n// after\n// pair via the bot, then use the pairing endpoint\nawait api.notificationChannels.completeBotPairing({ pairingCode: code });","handlingStrategy":"validation","validationCode":"if (channelType === \"telegram\") {\n  throw new Error(\"use the Telegram bot pairing flow; direct linking is not supported\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  await linkChannel({ channelType });\n} catch (e) {\n  if (String(e?.message).includes(\"bot pairing\")) {\n    startTelegramBotPairing();\n  }\n}","preventionTips":["Remove 'telegram' from direct channel-linking UI options.","Route telegram connections exclusively through the bot pairing flow.","Add a client-side enum of linkable channel types (slack, email) shared with the backend."],"tags":["telegram","notification-channels","unsupported-operation","convex"],"backgroundTag":"unsupported-operation","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}