luckiest-co 1.0.46 → 1.0.48

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.
Files changed (266) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/package.json +1 -1
  4. package/skills/ab-testing/CHANGELOG.md +5 -0
  5. package/skills/ab-testing/SKILL.md +12 -9
  6. package/skills/ab-testing/evals/evals.json +18 -7
  7. package/skills/ab-testing/references/sample-size-guide.md +2 -2
  8. package/skills/ads/CHANGELOG.md +5 -0
  9. package/skills/ads/SKILL.md +4 -4
  10. package/skills/ai-seo/CHANGELOG.md +7 -0
  11. package/skills/ai-seo/SKILL.md +27 -20
  12. package/skills/analytics/CHANGELOG.md +6 -0
  13. package/skills/analytics/SKILL.md +4 -3
  14. package/skills/analytics/evals/evals.json +27 -0
  15. package/skills/analytics/references/ga4-implementation.md +7 -3
  16. package/skills/analytics/references/gtm-implementation.md +8 -2
  17. package/skills/aso/CHANGELOG.md +5 -0
  18. package/skills/aso/SKILL.md +4 -4
  19. package/skills/aso/evals/evals.json +15 -2
  20. package/skills/aso/references/benchmarks.md +4 -2
  21. package/skills/aso/references/google-play-specs.md +5 -1
  22. package/skills/brainstorm/SKILL.md +59 -16
  23. package/skills/brainstorm/builder-check-prompt.md +23 -0
  24. package/skills/churn-prevention/CHANGELOG.md +5 -0
  25. package/skills/churn-prevention/SKILL.md +3 -3
  26. package/skills/churn-prevention/evals/evals.json +14 -0
  27. package/skills/churn-prevention/references/dunning-playbook.md +6 -1
  28. package/skills/coder-brainstorming/CHANGELOG.md +10 -0
  29. package/skills/coder-brainstorming/SKILL.md +62 -20
  30. package/skills/coder-brainstorming/builder-check-prompt.md +23 -0
  31. package/skills/coder-brainstorming/scripts/server.cjs +1 -1
  32. package/skills/coder-debugging/CHANGELOG.md +4 -0
  33. package/skills/coder-debugging/SKILL.md +5 -5
  34. package/skills/coder-requesting-code-review/CHANGELOG.md +4 -0
  35. package/skills/coder-requesting-code-review/SKILL.md +4 -4
  36. package/skills/coder-subagent-driven-development/CHANGELOG.md +4 -0
  37. package/skills/coder-subagent-driven-development/SKILL.md +3 -3
  38. package/skills/coder-subagent-driven-development/scripts/sdd-workspace +3 -1
  39. package/skills/cold-email/CHANGELOG.md +7 -0
  40. package/skills/cold-email/SKILL.md +27 -7
  41. package/skills/cold-email/evals/evals.json +30 -0
  42. package/skills/cold-email/references/benchmarks.md +15 -11
  43. package/skills/cold-email/references/deliverability.md +110 -0
  44. package/skills/cold-email/references/linkedin-outreach.md +91 -0
  45. package/skills/cold-email/references/multichannel-cadence.md +79 -0
  46. package/skills/cold-email/references/outbound-plays.md +88 -0
  47. package/skills/cold-email/references/personalization.md +11 -0
  48. package/skills/cold-email/references/reply-handling.md +65 -0
  49. package/skills/competitor-profiling/CHANGELOG.md +5 -0
  50. package/skills/competitor-profiling/SKILL.md +4 -4
  51. package/skills/content-strategy/CHANGELOG.md +5 -0
  52. package/skills/content-strategy/SKILL.md +5 -3
  53. package/skills/copy-editing/CHANGELOG.md +5 -0
  54. package/skills/copy-editing/SKILL.md +5 -5
  55. package/skills/copy-editing/references/content-refresh.md +33 -0
  56. package/skills/cro/CHANGELOG.md +5 -0
  57. package/skills/cro/SKILL.md +14 -3
  58. package/skills/cro/references/form.md +1 -1
  59. package/skills/directory-submissions/CHANGELOG.md +5 -0
  60. package/skills/directory-submissions/SKILL.md +13 -4
  61. package/skills/directory-submissions/evals/evals.json +15 -1
  62. package/skills/directory-submissions/references/directory-list.md +2 -4
  63. package/skills/emails/CHANGELOG.md +5 -0
  64. package/skills/emails/SKILL.md +14 -4
  65. package/skills/launch/CHANGELOG.md +7 -0
  66. package/skills/launch/SKILL.md +8 -4
  67. package/skills/launch/evals/evals.json +29 -0
  68. package/skills/launch/references/shipped-changes.md +84 -0
  69. package/skills/launch/references/site-launch-qa.md +55 -0
  70. package/skills/lead-magnets/CHANGELOG.md +5 -0
  71. package/skills/lead-magnets/SKILL.md +6 -4
  72. package/skills/lead-magnets/evals/evals.json +18 -0
  73. package/skills/lead-magnets/references/content-to-skill-corpus-design.md +89 -0
  74. package/skills/lead-magnets/references/content-to-skill-release.md +71 -0
  75. package/skills/lead-magnets/references/content-to-skill.md +108 -0
  76. package/skills/marketing-plan/CHANGELOG.md +5 -0
  77. package/skills/marketing-plan/SKILL.md +3 -3
  78. package/skills/marketing-plan/evals/evals.json +13 -0
  79. package/skills/marketing-plan/references/budget-planning.md +26 -17
  80. package/skills/pricing/CHANGELOG.md +5 -0
  81. package/skills/pricing/SKILL.md +4 -4
  82. package/skills/pricing/evals/evals.json +12 -0
  83. package/skills/pricing/references/research-methods.md +8 -4
  84. package/skills/programmatic-seo/CHANGELOG.md +5 -0
  85. package/skills/programmatic-seo/SKILL.md +8 -4
  86. package/skills/programmatic-seo/evals/evals.json +13 -0
  87. package/skills/programmatic-seo/references/implementation-platforms.md +37 -0
  88. package/skills/prospecting/CHANGELOG.md +7 -0
  89. package/skills/prospecting/SKILL.md +27 -13
  90. package/skills/prospecting/evals/evals.json +41 -0
  91. package/skills/prospecting/references/account-research.md +59 -0
  92. package/skills/prospecting/references/b2b-prospecting.md +1 -1
  93. package/skills/prospecting/references/compliance.md +44 -6
  94. package/skills/prospecting/references/data-sources.md +13 -15
  95. package/skills/prospecting/references/enrichment-playbook.md +61 -0
  96. package/skills/prospecting/references/saas-prospecting.md +1 -1
  97. package/skills/prospecting/references/signal-plays.md +99 -0
  98. package/skills/prospecting/references/sourcing-and-enrichment.md +89 -0
  99. package/skills/referrals/CHANGELOG.md +5 -0
  100. package/skills/referrals/SKILL.md +10 -4
  101. package/skills/referrals/evals/evals.json +99 -0
  102. package/skills/referrals/references/affiliate-operations.md +64 -0
  103. package/skills/referrals/references/affiliate-programs.md +2 -2
  104. package/skills/referrals/references/program-examples.md +14 -6
  105. package/skills/referrals/references/review-platform-rules.md +36 -0
  106. package/skills/referrals/references/review-program-operations.md +62 -0
  107. package/skills/referrals/references/review-programs.md +96 -0
  108. package/skills/revops/CHANGELOG.md +5 -0
  109. package/skills/revops/SKILL.md +10 -6
  110. package/skills/revops/evals/evals.json +13 -0
  111. package/skills/revops/references/automation-playbooks.md +1 -1
  112. package/skills/revops/references/inbound-routing.md +43 -0
  113. package/skills/revops/references/lifecycle-definitions.md +31 -0
  114. package/skills/sales-enablement/CHANGELOG.md +5 -0
  115. package/skills/sales-enablement/SKILL.md +6 -4
  116. package/skills/sales-enablement/evals/evals.json +14 -0
  117. package/skills/sales-enablement/references/cold-call-scripts.md +68 -0
  118. package/skills/schema/CHANGELOG.md +5 -0
  119. package/skills/schema/SKILL.md +20 -8
  120. package/skills/schema/evals/evals.json +40 -0
  121. package/skills/schema/references/schema-examples.md +9 -15
  122. package/skills/seo-audit/CHANGELOG.md +5 -0
  123. package/skills/seo-audit/SKILL.md +10 -16
  124. package/skills/seo-audit/evals/evals.json +68 -0
  125. package/skills/seo-audit/references/international-seo.md +9 -11
  126. package/skills/seo-audit/references/local-seo-pages-and-measurement.md +39 -0
  127. package/skills/seo-audit/references/local-seo-profile-and-citations.md +41 -0
  128. package/skills/seo-audit/references/local-seo.md +86 -0
  129. package/skills/seo-audit/references/rankings-push.md +62 -0
  130. package/skills/seo-audit/references/title-tags.md +89 -0
  131. package/skills/signup/CHANGELOG.md +5 -0
  132. package/skills/signup/SKILL.md +7 -3
  133. package/skills/site-architecture/CHANGELOG.md +5 -0
  134. package/skills/site-architecture/SKILL.md +5 -4
  135. package/skills/site-architecture/evals/evals.json +25 -0
  136. package/skills/site-architecture/references/platforms-and-migration.md +84 -0
  137. package/skills/social/CHANGELOG.md +5 -0
  138. package/skills/social/SKILL.md +4 -4
  139. package/skills/trends/.skillignore +3 -0
  140. package/skills/trends/CHANGELOG.md +20 -0
  141. package/skills/trends/SKILL.md +12 -8
  142. package/skills/trends/references/agent-mode.md +36 -0
  143. package/skills/trends/references/category-peers.md +38 -0
  144. package/skills/trends/references/comparison.md +227 -0
  145. package/skills/trends/references/competitors.md +29 -0
  146. package/skills/trends/references/discovery.md +90 -0
  147. package/skills/trends/references/engine-contract.md +111 -1889
  148. package/skills/trends/references/followups.md +112 -0
  149. package/skills/trends/references/grok-bot-x.md +55 -0
  150. package/skills/trends/references/hiring-signals.md +19 -0
  151. package/skills/trends/references/library-queue.md +33 -0
  152. package/skills/trends/references/recommendations.md +86 -0
  153. package/skills/trends/references/research-runbook.md +781 -0
  154. package/skills/trends/references/runtime.md +165 -0
  155. package/skills/trends/references/save-html-brief.md +39 -20
  156. package/skills/trends/references/setup-wizard.md +253 -0
  157. package/skills/trends/references/synthesis.md +289 -0
  158. package/skills/trends/scripts/box_chrome_login.py +209 -0
  159. package/skills/trends/scripts/briefing.py +9 -17
  160. package/skills/trends/scripts/last30days.py +3247 -280
  161. package/skills/trends/scripts/lib/agentcookie.py +196 -0
  162. package/skills/trends/scripts/lib/amazon.py +877 -0
  163. package/skills/trends/scripts/lib/arxiv.py +42 -8
  164. package/skills/trends/scripts/lib/backends.py +769 -0
  165. package/skills/trends/scripts/lib/bird_x.py +340 -57
  166. package/skills/trends/scripts/lib/bluesky.py +84 -10
  167. package/skills/trends/scripts/lib/bounded_get.py +156 -0
  168. package/skills/trends/scripts/lib/brightdata.py +291 -0
  169. package/skills/trends/scripts/lib/categories.py +7 -1
  170. package/skills/trends/scripts/lib/chrome_cdp.py +455 -0
  171. package/skills/trends/scripts/lib/chrome_cookies.py +132 -39
  172. package/skills/trends/scripts/lib/cluster.py +31 -54
  173. package/skills/trends/scripts/lib/competitors.py +10 -1
  174. package/skills/trends/scripts/lib/cookie_extract.py +221 -44
  175. package/skills/trends/scripts/lib/cookie_paths.py +27 -0
  176. package/skills/trends/scripts/lib/corpus.py +439 -0
  177. package/skills/trends/scripts/lib/devto.py +313 -0
  178. package/skills/trends/scripts/lib/discovery_handoff.py +988 -0
  179. package/skills/trends/scripts/lib/doctor.py +2166 -0
  180. package/skills/trends/scripts/lib/dripstack.py +203 -0
  181. package/skills/trends/scripts/lib/entity_extract.py +37 -0
  182. package/skills/trends/scripts/lib/env.py +1092 -138
  183. package/skills/trends/scripts/lib/fanout.py +11 -1
  184. package/skills/trends/scripts/lib/feed.py +61 -0
  185. package/skills/trends/scripts/lib/fetch_memo.py +135 -0
  186. package/skills/trends/scripts/lib/freshness.py +566 -0
  187. package/skills/trends/scripts/lib/fusion.py +314 -28
  188. package/skills/trends/scripts/lib/github.py +438 -24
  189. package/skills/trends/scripts/lib/grok_x.py +1237 -0
  190. package/skills/trends/scripts/lib/grounding.py +129 -11
  191. package/skills/trends/scripts/lib/hackernews.py +61 -4
  192. package/skills/trends/scripts/lib/health.py +373 -0
  193. package/skills/trends/scripts/lib/hosted.py +333 -0
  194. package/skills/trends/scripts/lib/html_publish.py +34 -0
  195. package/skills/trends/scripts/lib/html_render.py +169 -3
  196. package/skills/trends/scripts/lib/http.py +1215 -70
  197. package/skills/trends/scripts/lib/instagram.py +132 -9
  198. package/skills/trends/scripts/lib/jobs.py +4 -2
  199. package/skills/trends/scripts/lib/library.py +308 -0
  200. package/skills/trends/scripts/lib/library_index.py +477 -0
  201. package/skills/trends/scripts/lib/linkedin.py +271 -24
  202. package/skills/trends/scripts/lib/log.py +5 -2
  203. package/skills/trends/scripts/lib/meta_ads.py +982 -0
  204. package/skills/trends/scripts/lib/normalize.py +382 -28
  205. package/skills/trends/scripts/lib/parallel_mcp.py +234 -0
  206. package/skills/trends/scripts/lib/permission_preflight.py +42 -3
  207. package/skills/trends/scripts/lib/perplexity.py +1080 -468
  208. package/skills/trends/scripts/lib/pipeline.py +5079 -211
  209. package/skills/trends/scripts/lib/planner.py +396 -53
  210. package/skills/trends/scripts/lib/polymarket.py +285 -23
  211. package/skills/trends/scripts/lib/prescriptions.py +432 -0
  212. package/skills/trends/scripts/lib/private_files.py +21 -0
  213. package/skills/trends/scripts/lib/providers.py +231 -10
  214. package/skills/trends/scripts/lib/quality_nudge.py +122 -37
  215. package/skills/trends/scripts/lib/reddit.py +177 -12
  216. package/skills/trends/scripts/lib/reddit_arctic.py +146 -5
  217. package/skills/trends/scripts/lib/reddit_keyless.py +121 -60
  218. package/skills/trends/scripts/lib/reddit_listing.py +215 -14
  219. package/skills/trends/scripts/lib/reddit_public.py +12 -13
  220. package/skills/trends/scripts/lib/reddit_search.py +304 -0
  221. package/skills/trends/scripts/lib/reddit_shreddit.py +51 -7
  222. package/skills/trends/scripts/lib/registers.py +134 -0
  223. package/skills/trends/scripts/lib/relevance.py +17 -0
  224. package/skills/trends/scripts/lib/render.py +2161 -271
  225. package/skills/trends/scripts/lib/rerank.py +623 -55
  226. package/skills/trends/scripts/lib/resolve.py +50 -1
  227. package/skills/trends/scripts/lib/safari_cookies.py +42 -15
  228. package/skills/trends/scripts/lib/schema.py +782 -1
  229. package/skills/trends/scripts/lib/setup_wizard.py +779 -120
  230. package/skills/trends/scripts/lib/signals.py +206 -7
  231. package/skills/trends/scripts/lib/stocktwits.py +374 -0
  232. package/skills/trends/scripts/lib/subproc.py +307 -53
  233. package/skills/trends/scripts/lib/techmeme.py +89 -64
  234. package/skills/trends/scripts/lib/telegram.py +317 -0
  235. package/skills/trends/scripts/lib/tiktok.py +13 -9
  236. package/skills/trends/scripts/lib/topic_shape.py +340 -0
  237. package/skills/trends/scripts/lib/transcribe.py +12 -3
  238. package/skills/trends/scripts/lib/trustpilot.py +256 -5
  239. package/skills/trends/scripts/lib/truthsocial.py +9 -1
  240. package/skills/trends/scripts/lib/ui.py +6 -0
  241. package/skills/trends/scripts/lib/usage.py +132 -0
  242. package/skills/trends/scripts/lib/web_search_keyless.py +63 -4
  243. package/skills/trends/scripts/lib/windows_job.py +138 -0
  244. package/skills/trends/scripts/lib/x_api.py +824 -0
  245. package/skills/trends/scripts/lib/x_envelope.py +733 -0
  246. package/skills/trends/scripts/lib/x_judge.py +292 -0
  247. package/skills/trends/scripts/lib/xai_x.py +26 -3
  248. package/skills/trends/scripts/lib/xquik.py +89 -15
  249. package/skills/trends/scripts/lib/xurl_x.py +284 -91
  250. package/skills/trends/scripts/lib/youtube_yt.py +570 -119
  251. package/skills/trends/scripts/setup-keychain.sh +12 -4
  252. package/skills/trends/scripts/setup-pass.sh +3 -0
  253. package/skills/trends/scripts/store.py +444 -50
  254. package/skills/trends/scripts/watchlist.py +38 -11
  255. package/skills/trends/scripts/x_lane_parity.py +107 -0
  256. package/skills/video-studio/CHANGELOG.md +8 -0
  257. package/skills/video-studio/SKILL.md +6 -5
  258. package/skills/video-studio/references/styles/product-flight.md +45 -0
  259. package/skills/video-studio/references/styles.md +3 -1
  260. package/skills/video-studio/scripts/qa.mjs +64 -2
  261. package/skills/video-studio/scripts/qa.test.mjs +29 -1
  262. package/skills/video-studio-motion/ATTRIBUTION.md +1 -1
  263. package/skills/video-studio-motion/CHANGELOG.md +4 -0
  264. package/skills/video-studio-motion/SKILL.md +8 -5
  265. package/skills/coder-brainstorming/spec-document-reviewer-prompt.md +0 -49
  266. package/skills/trends/scripts/lib/reddit_rss.py +0 -224
@@ -0,0 +1,317 @@
1
+ """Telegram public channel posts via ScrapeCreators API for /last30days.
2
+
3
+ Uses ScrapeCreators REST API to fetch recent posts from named public Telegram
4
+ channels. No keyword search - channel handles only.
5
+
6
+ Requires SCRAPECREATORS_API_KEY in config plus a channel list (TELEGRAM_SOURCES
7
+ env var or --telegram-sources CLI flag).
8
+
9
+ API docs: https://docs.scrapecreators.com/v1/telegram/channel/posts
10
+ """
11
+
12
+ import math
13
+ import os
14
+ import re
15
+ from typing import Any
16
+
17
+ from . import dates, http, log
18
+ from .relevance import token_overlap_relevance as _compute_relevance
19
+
20
+ SCRAPECREATORS_BASE = "https://api.scrapecreators.com/v1/telegram"
21
+
22
+ DEPTH_PAGE_CAPS = {
23
+ "quick": 1,
24
+ "default": 3,
25
+ "deep": 6,
26
+ }
27
+
28
+
29
+ def _log(msg: str):
30
+ log.source_log("Telegram", msg, tty_only=False)
31
+
32
+
33
+ class InvalidChannelHandle(ValueError):
34
+ """Raised when a channel handle is rejected (joinchat, numeric -100 ID)."""
35
+
36
+
37
+ def parse_channel_handle(raw: str) -> str:
38
+ """Normalize a Telegram channel identifier to a bare handle.
39
+
40
+ Accepts:
41
+ - bare username: aipost
42
+ - @handle: @aipost
43
+ - t.me URL: https://t.me/aipost
44
+ - t.me/s preview URL: https://t.me/s/aipost
45
+
46
+ Rejects (raises InvalidChannelHandle):
47
+ - joinchat links: https://t.me/joinchat/xxxxx
48
+ - numeric -100 supergroup IDs: -1001234567890
49
+
50
+ Returns:
51
+ Bare handle string (no @ prefix).
52
+ """
53
+ handle = raw.strip()
54
+ if not handle:
55
+ raise InvalidChannelHandle("Empty channel handle")
56
+
57
+ if handle.lstrip("-").isdigit() and handle.startswith("-100"):
58
+ raise InvalidChannelHandle(
59
+ f"Numeric supergroup IDs are not supported: {handle}"
60
+ )
61
+
62
+ if handle.startswith("@"):
63
+ handle = handle[1:]
64
+ if not handle:
65
+ raise InvalidChannelHandle("Empty handle after @ prefix")
66
+ return handle
67
+
68
+ url_match = re.match(
69
+ r"(?:https?://)?(?:www\.)?t\.me/(?:s/)?([^/?#]+)",
70
+ handle,
71
+ re.IGNORECASE,
72
+ )
73
+ if url_match:
74
+ extracted = url_match.group(1)
75
+ if extracted.lower() == "joinchat":
76
+ raise InvalidChannelHandle(
77
+ f"Private joinchat links are not supported: {raw}"
78
+ )
79
+ return extracted
80
+
81
+ if "joinchat" in handle.lower():
82
+ raise InvalidChannelHandle(
83
+ f"Private joinchat links are not supported: {raw}"
84
+ )
85
+
86
+ return handle
87
+
88
+
89
+ def parse_channel_sources(raw: str) -> list[str]:
90
+ """Parse a comma-separated list of channel handles.
91
+
92
+ Filters out invalid handles (logs a warning) and returns valid ones.
93
+ """
94
+ handles: list[str] = []
95
+ for part in raw.split(","):
96
+ part = part.strip()
97
+ if not part:
98
+ continue
99
+ try:
100
+ handle = parse_channel_handle(part)
101
+ if handle.lower() not in {h.lower() for h in handles}:
102
+ handles.append(handle)
103
+ except InvalidChannelHandle as exc:
104
+ _log(f"Skipping invalid channel: {exc}")
105
+ return handles
106
+
107
+
108
+ def _get_channel_sources(config: dict[str, Any]) -> list[str]:
109
+ """Get configured Telegram channel sources from config or env."""
110
+ raw = config.get("TELEGRAM_SOURCES") or os.environ.get("TELEGRAM_SOURCES") or ""
111
+ return parse_channel_sources(raw)
112
+
113
+
114
+ def is_telegram_configured(config: dict[str, Any]) -> bool:
115
+ """True when Telegram has both API key and at least one channel."""
116
+ return bool(
117
+ config.get("SCRAPECREATORS_API_KEY")
118
+ and _get_channel_sources(config)
119
+ )
120
+
121
+
122
+ def _parse_date(item: dict[str, Any]) -> str | None:
123
+ """Parse date from Telegram post to YYYY-MM-DD."""
124
+ for key in ("published_at", "date", "created_at"):
125
+ val = item.get(key)
126
+ if val is None:
127
+ continue
128
+ dt = dates.parse_date(str(val))
129
+ if dt:
130
+ return dt.strftime("%Y-%m-%d")
131
+ return None
132
+
133
+
134
+ def _parse_post(
135
+ raw: dict[str, Any],
136
+ channel: dict[str, Any],
137
+ topic: str,
138
+ index: int,
139
+ ) -> dict[str, Any]:
140
+ """Parse a single Telegram post into normalized dict."""
141
+ post_id = str(raw.get("id") or f"TG{index + 1}")
142
+ text = str(raw.get("text") or "").strip()
143
+ url = str(raw.get("url") or "")
144
+ date_str = _parse_date(raw)
145
+
146
+ handle = str(raw.get("channel_handle") or channel.get("handle") or "")
147
+ author_name = str(raw.get("author_name") or channel.get("name") or handle)
148
+
149
+ view_count = raw.get("view_count") or 0
150
+ reaction_count = raw.get("reaction_count") or 0
151
+ subscriber_count = channel.get("subscriber_count") or 0
152
+
153
+ text_relevance = _compute_relevance(topic, text)
154
+ rank_score = max(0.3, 1.0 - (index * 0.02))
155
+ engagement_boost = min(0.2, math.log1p(view_count + reaction_count * 10) / 50)
156
+ relevance = min(1.0, text_relevance * 0.5 + rank_score * 0.3 + engagement_boost + 0.1)
157
+
158
+ return {
159
+ "id": post_id,
160
+ "handle": handle,
161
+ "display_name": author_name,
162
+ "text": text,
163
+ "url": url,
164
+ "date": date_str,
165
+ "engagement": {
166
+ "views": view_count,
167
+ "reactions": reaction_count,
168
+ "subscribers": subscriber_count,
169
+ },
170
+ "relevance": round(relevance, 2),
171
+ "why_relevant": f"Telegram @{handle}: {text[:60]}" if text else f"Telegram: @{handle}",
172
+ }
173
+
174
+
175
+ def _fetch_channel_posts(
176
+ handle: str,
177
+ token: str,
178
+ *,
179
+ from_date: str,
180
+ topic: str,
181
+ max_pages: int,
182
+ ) -> list[dict[str, Any]]:
183
+ """Fetch posts from a single channel, paginating until date cutoff."""
184
+ items: list[dict[str, Any]] = []
185
+ cursor: str | None = None
186
+ pages_fetched = 0
187
+
188
+ while pages_fetched < max_pages:
189
+ _log(f"Fetching @{handle} (page {pages_fetched + 1}/{max_pages})")
190
+
191
+ params: dict[str, Any] = {"handle": handle}
192
+ if cursor:
193
+ params["cursor"] = cursor
194
+
195
+ try:
196
+ data = http.get(
197
+ f"{SCRAPECREATORS_BASE}/channel/posts",
198
+ params=params,
199
+ headers=http.scrapecreators_headers(token),
200
+ timeout=30,
201
+ retries=2,
202
+ )
203
+ except http.HTTPError as exc:
204
+ _log(f"HTTP error fetching @{handle}: {exc}")
205
+ break
206
+
207
+ if not data.get("success"):
208
+ error_msg = data.get("error") or data.get("message") or "Unknown error"
209
+ _log(f"API error for @{handle}: {error_msg}")
210
+ break
211
+
212
+ channel = data.get("channel") or {}
213
+ posts = data.get("posts") or []
214
+
215
+ if not posts:
216
+ _log(f"No posts returned for @{handle}")
217
+ break
218
+
219
+ page_all_old = True
220
+ for idx, raw_post in enumerate(posts):
221
+ parsed = _parse_post(raw_post, channel, topic, len(items) + idx)
222
+ items.append(parsed)
223
+ if parsed["date"] and parsed["date"] >= from_date:
224
+ page_all_old = False
225
+
226
+ pages_fetched += 1
227
+
228
+ if page_all_old:
229
+ _log(f"All posts on page older than {from_date}, stopping pagination")
230
+ break
231
+
232
+ cursor = data.get("cursor")
233
+ if not data.get("has_more") or not cursor:
234
+ break
235
+
236
+ return items
237
+
238
+
239
+ def search_telegram(
240
+ topic: str,
241
+ from_date: str,
242
+ to_date: str,
243
+ depth: str = "default",
244
+ token: str | None = None,
245
+ config: dict[str, Any] | None = None,
246
+ ) -> dict[str, Any]:
247
+ """Fetch recent posts from configured Telegram channels.
248
+
249
+ Args:
250
+ topic: Search topic (for relevance scoring)
251
+ from_date: Start date (YYYY-MM-DD)
252
+ to_date: End date (YYYY-MM-DD)
253
+ depth: 'quick', 'default', or 'deep'
254
+ token: ScrapeCreators API key
255
+ config: Config dict (for TELEGRAM_SOURCES)
256
+
257
+ Returns:
258
+ Dict with 'items' list and optional 'error'.
259
+ """
260
+ config = config or {}
261
+
262
+ if not token:
263
+ return {"items": [], "error": "No SCRAPECREATORS_API_KEY configured"}
264
+
265
+ channels = _get_channel_sources(config)
266
+ if not channels:
267
+ return {"items": [], "error": "No TELEGRAM_SOURCES configured (channel list required)"}
268
+
269
+ base_cap = DEPTH_PAGE_CAPS.get(depth, DEPTH_PAGE_CAPS["default"])
270
+ override = config.get("TELEGRAM_MAX_PAGES")
271
+ if override:
272
+ try:
273
+ max_pages = max(base_cap, int(override))
274
+ except (ValueError, TypeError):
275
+ max_pages = base_cap
276
+ else:
277
+ max_pages = base_cap
278
+
279
+ _log(f"Searching {len(channels)} channel(s) for '{topic}' (depth={depth}, max_pages={max_pages})")
280
+
281
+ all_items: list[dict[str, Any]] = []
282
+ for handle in channels:
283
+ channel_items = _fetch_channel_posts(
284
+ handle,
285
+ token,
286
+ from_date=from_date,
287
+ topic=topic,
288
+ max_pages=max_pages,
289
+ )
290
+ all_items.extend(channel_items)
291
+
292
+ in_range = [
293
+ item for item in all_items
294
+ if item["date"] and from_date <= item["date"] <= to_date
295
+ ]
296
+ out_of_range = len(all_items) - len(in_range)
297
+ if in_range:
298
+ items = in_range
299
+ if out_of_range:
300
+ _log(f"Filtered {out_of_range} posts outside date range")
301
+ else:
302
+ items = all_items
303
+ _log(f"No posts within date range, keeping all {len(items)}")
304
+
305
+ items.sort(key=lambda x: x.get("relevance", 0), reverse=True)
306
+
307
+ _log(f"Found {len(items)} Telegram posts")
308
+ return {"items": items}
309
+
310
+
311
+ def parse_telegram_response(response: dict[str, Any]) -> list[dict[str, Any]]:
312
+ """Parse Telegram search response to normalized format.
313
+
314
+ Returns:
315
+ List of item dicts ready for normalization.
316
+ """
317
+ return response.get("items", [])
@@ -355,13 +355,17 @@ def fetch_captions(
355
355
  if not url:
356
356
  continue
357
357
  try:
358
- data = http.get(
359
- f"{SCRAPECREATORS_BASE}/video/transcript",
360
- params={"url": url},
361
- headers=http.scrapecreators_headers(token),
362
- timeout=15,
363
- retries=1,
364
- )
358
+ # Isolate transcript fetch errors from the pipeline-level
359
+ # capture_failures() context so an individual video's 400
360
+ # doesn't poison the entire source outcome.
361
+ with http.capture_failures() as _tf:
362
+ data = http.get(
363
+ f"{SCRAPECREATORS_BASE}/video/transcript",
364
+ params={"url": url},
365
+ headers=http.scrapecreators_headers(token),
366
+ timeout=15,
367
+ retries=1,
368
+ )
365
369
  transcript = data.get("transcript")
366
370
  if transcript:
367
371
  if isinstance(transcript, list):
@@ -446,7 +450,7 @@ def search_and_enrich(
446
450
  items.append(item)
447
451
 
448
452
  # Sort merged results by views descending
449
- items.sort(key=lambda x: x.get("engagement", {}).get("views", 0), reverse=True)
453
+ items.sort(key=lambda x: x.get("engagement", {}).get("views") or 0, reverse=True)
450
454
 
451
455
  if not items:
452
456
  return {"items": [], "error": last_error}
@@ -524,7 +528,7 @@ def enrich_with_comments(
524
528
 
525
529
  enriched_count = 0
526
530
  with ThreadPoolExecutor(max_workers=min(4, len(top_items))) as executor:
527
- futures = {executor.submit(_enrich_one, item): item for item in top_items}
531
+ futures = {http.submit_with_context(executor, _enrich_one, item): item for item in top_items}
528
532
  for future in as_completed(futures):
529
533
  if future.result():
530
534
  enriched_count += 1