{"record":{"id":"f74eb7533516676d","repo":"santifer/career-ops","slug":"telegram-channel-entry-name-needs-a-channel-handle-5-32","errorCode":null,"errorMessage":"telegram-channel: \"${entry.name || '?'}\" needs a channel handle (5-32 letters, digits or underscores, no @), got ${JSON.stringify(entry.channel)}","messagePattern":"telegram-channel: \"(.+?)\" needs a channel handle \\(5-32 letters, digits or underscores, no @\\), got (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/telegram-channel.mjs","lineNumber":363,"sourceCode":"  const n = Number(raw);\n  if (!Number.isInteger(n) || n < 1) return fallback;\n  return Math.min(n, cap);\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'telegram-channel',\n\n  /**\n   * @param {{ name?: string, channel?: string, max_pages?: number|string, since_days?: number|string }} entry\n   * @param {{ fetchText: Function, sleep?: Function, maxPages?: number, sinceMs?: number }} ctx\n   */\n  async fetch(entry, ctx) {\n    const channel = String(entry.channel || '').replace(/^@/, '').trim();\n    // The handle is the only value that reaches the URL, and the pattern pins\n    // it to t.me/s/<handle>: no path separators, no query, no other host.\n    if (!CHANNEL_RE.test(channel)) {\n      throw new Error(`telegram-channel: \"${entry.name || '?'}\" needs a channel handle (5-32 letters, digits or underscores, no @), got ${JSON.stringify(entry.channel)}`);\n    }\n    // Pages are walked until the window is reached, so the inventory inside\n    // `since_days` is complete (Source Indexing Policy, rule 3), up to a cap\n    // that keeps one channel under ~5 s. ctx.maxPages (verify-portals) wins.\n    const entryPages = pageCount(entry.max_pages, MAX_PAGES_CAP, MAX_PAGES_CAP);\n    const maxPages = Math.min(entryPages, ctx.maxPages ?? Infinity);\n    const sinceDays = Number(entry.since_days);\n    const windowDays = Number.isFinite(sinceDays) && sinceDays > 0 ? sinceDays : DEFAULT_SINCE_DAYS;\n    // The scanner's own early-stop window (max_posting_age_days) can only be\n    // narrower than the entry's; stop at whichever comes first.\n    const cutoff = Math.max(Date.now() - windowDays * DAY_MS, Number.isFinite(ctx.sinceMs) ? Number(ctx.sinceMs) : 0);\n    const label = entry.name || `@${channel}`;\n\n    const jobs = [];\n    let before = null;\n    let read = 0;\n    let reachedCutoff = false;\n    let page = 0;","sourceCodeStart":345,"sourceCodeEnd":381,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/telegram-channel.mjs#L345-L381","documentation":"The telegram-channel provider fetches t.me/s/<handle> public previews, so the channel handle is the only interpolated value and is pinned by CHANNEL_RE to 5-32 letters/digits/underscores. An entry whose channel field is missing, empty, or malformed (e.g. contains '@', spaces, dashes, or a full URL) fails this validation with the raw JSON of what was provided.","triggerScenarios":"portals.yml entry omits channel; channel: '@mychannel' (the @ is stripped before the check, so a bare @ is fine, but e.g. 'channel: https://t.me/mychannel' or 'my channel' fails); channel contains a hyphen or is shorter than 5 chars.","commonSituations":"Pasting the full t.me URL instead of the handle; usernames with dashes (Telegram allows them for some bots but not per this strict pattern); typo leaving the value empty due to YAML quoting issues.","solutions":["Set channel to just the handle: channel: mychannel (5-32 [A-Za-z0-9_], no @, no URL)","If the channel id contains characters outside the pattern, confirm it is a valid public channel handle","Remove @-prefix confusion — it is stripped automatically, the raw handle is expected"],"exampleFix":"// before\nchannel: 'https://t.me/my_channel'\n// after\nchannel: 'my_channel'","handlingStrategy":"validation","validationCode":"const CHANNEL_RE = /^[A-Za-z0-9_]{5,32}$/;\nconst ch = String(entry.channel ?? '').replace(/^@/, '').trim();\nif (!CHANNEL_RE.test(ch)) {\n  throw new Error(`${entry.name}: channel must be a bare handle (5-32 letters/digits/underscores), got ${JSON.stringify(entry.channel)}`);\n}","typeGuard":"function isValidTelegramChannel(entry) {\n  const ch = String(entry?.channel ?? '').replace(/^@/, '').trim();\n  return /^[A-Za-z0-9_]{5,32}$/.test(ch);\n}","tryCatchPattern":"try {\n  offers = await provider.fetch(entry, ctx);\n} catch (e) {\n  if (e.message.startsWith('telegram-channel:') && e.message.includes('needs a channel handle')) {\n    console.warn(`${entry.name}: fix the channel field — bare handle only, no URL`);\n    return [];\n  }\n  throw e;\n}","preventionTips":["Store the bare handle, never the full t.me URL","Test new entries by opening https://t.me/s/<handle> in a browser","Quote YAML values to avoid parsing surprises","Check for Telegram handles with dashes — they don't match the strict pattern"],"tags":["config","validation","telegram"],"backgroundTag":"invalid-argument-format","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}