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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/ab-testing/CHANGELOG.md +5 -0
- package/skills/ab-testing/SKILL.md +12 -9
- package/skills/ab-testing/evals/evals.json +18 -7
- package/skills/ab-testing/references/sample-size-guide.md +2 -2
- package/skills/ads/CHANGELOG.md +5 -0
- package/skills/ads/SKILL.md +4 -4
- package/skills/ai-seo/CHANGELOG.md +7 -0
- package/skills/ai-seo/SKILL.md +27 -20
- package/skills/analytics/CHANGELOG.md +6 -0
- package/skills/analytics/SKILL.md +4 -3
- package/skills/analytics/evals/evals.json +27 -0
- package/skills/analytics/references/ga4-implementation.md +7 -3
- package/skills/analytics/references/gtm-implementation.md +8 -2
- package/skills/aso/CHANGELOG.md +5 -0
- package/skills/aso/SKILL.md +4 -4
- package/skills/aso/evals/evals.json +15 -2
- package/skills/aso/references/benchmarks.md +4 -2
- package/skills/aso/references/google-play-specs.md +5 -1
- package/skills/brainstorm/SKILL.md +59 -16
- package/skills/brainstorm/builder-check-prompt.md +23 -0
- package/skills/churn-prevention/CHANGELOG.md +5 -0
- package/skills/churn-prevention/SKILL.md +3 -3
- package/skills/churn-prevention/evals/evals.json +14 -0
- package/skills/churn-prevention/references/dunning-playbook.md +6 -1
- package/skills/coder-brainstorming/CHANGELOG.md +10 -0
- package/skills/coder-brainstorming/SKILL.md +62 -20
- package/skills/coder-brainstorming/builder-check-prompt.md +23 -0
- package/skills/coder-brainstorming/scripts/server.cjs +1 -1
- package/skills/coder-debugging/CHANGELOG.md +4 -0
- package/skills/coder-debugging/SKILL.md +5 -5
- package/skills/coder-requesting-code-review/CHANGELOG.md +4 -0
- package/skills/coder-requesting-code-review/SKILL.md +4 -4
- package/skills/coder-subagent-driven-development/CHANGELOG.md +4 -0
- package/skills/coder-subagent-driven-development/SKILL.md +3 -3
- package/skills/coder-subagent-driven-development/scripts/sdd-workspace +3 -1
- package/skills/cold-email/CHANGELOG.md +7 -0
- package/skills/cold-email/SKILL.md +27 -7
- package/skills/cold-email/evals/evals.json +30 -0
- package/skills/cold-email/references/benchmarks.md +15 -11
- package/skills/cold-email/references/deliverability.md +110 -0
- package/skills/cold-email/references/linkedin-outreach.md +91 -0
- package/skills/cold-email/references/multichannel-cadence.md +79 -0
- package/skills/cold-email/references/outbound-plays.md +88 -0
- package/skills/cold-email/references/personalization.md +11 -0
- package/skills/cold-email/references/reply-handling.md +65 -0
- package/skills/competitor-profiling/CHANGELOG.md +5 -0
- package/skills/competitor-profiling/SKILL.md +4 -4
- package/skills/content-strategy/CHANGELOG.md +5 -0
- package/skills/content-strategy/SKILL.md +5 -3
- package/skills/copy-editing/CHANGELOG.md +5 -0
- package/skills/copy-editing/SKILL.md +5 -5
- package/skills/copy-editing/references/content-refresh.md +33 -0
- package/skills/cro/CHANGELOG.md +5 -0
- package/skills/cro/SKILL.md +14 -3
- package/skills/cro/references/form.md +1 -1
- package/skills/directory-submissions/CHANGELOG.md +5 -0
- package/skills/directory-submissions/SKILL.md +13 -4
- package/skills/directory-submissions/evals/evals.json +15 -1
- package/skills/directory-submissions/references/directory-list.md +2 -4
- package/skills/emails/CHANGELOG.md +5 -0
- package/skills/emails/SKILL.md +14 -4
- package/skills/launch/CHANGELOG.md +7 -0
- package/skills/launch/SKILL.md +8 -4
- package/skills/launch/evals/evals.json +29 -0
- package/skills/launch/references/shipped-changes.md +84 -0
- package/skills/launch/references/site-launch-qa.md +55 -0
- package/skills/lead-magnets/CHANGELOG.md +5 -0
- package/skills/lead-magnets/SKILL.md +6 -4
- package/skills/lead-magnets/evals/evals.json +18 -0
- package/skills/lead-magnets/references/content-to-skill-corpus-design.md +89 -0
- package/skills/lead-magnets/references/content-to-skill-release.md +71 -0
- package/skills/lead-magnets/references/content-to-skill.md +108 -0
- package/skills/marketing-plan/CHANGELOG.md +5 -0
- package/skills/marketing-plan/SKILL.md +3 -3
- package/skills/marketing-plan/evals/evals.json +13 -0
- package/skills/marketing-plan/references/budget-planning.md +26 -17
- package/skills/pricing/CHANGELOG.md +5 -0
- package/skills/pricing/SKILL.md +4 -4
- package/skills/pricing/evals/evals.json +12 -0
- package/skills/pricing/references/research-methods.md +8 -4
- package/skills/programmatic-seo/CHANGELOG.md +5 -0
- package/skills/programmatic-seo/SKILL.md +8 -4
- package/skills/programmatic-seo/evals/evals.json +13 -0
- package/skills/programmatic-seo/references/implementation-platforms.md +37 -0
- package/skills/prospecting/CHANGELOG.md +7 -0
- package/skills/prospecting/SKILL.md +27 -13
- package/skills/prospecting/evals/evals.json +41 -0
- package/skills/prospecting/references/account-research.md +59 -0
- package/skills/prospecting/references/b2b-prospecting.md +1 -1
- package/skills/prospecting/references/compliance.md +44 -6
- package/skills/prospecting/references/data-sources.md +13 -15
- package/skills/prospecting/references/enrichment-playbook.md +61 -0
- package/skills/prospecting/references/saas-prospecting.md +1 -1
- package/skills/prospecting/references/signal-plays.md +99 -0
- package/skills/prospecting/references/sourcing-and-enrichment.md +89 -0
- package/skills/referrals/CHANGELOG.md +5 -0
- package/skills/referrals/SKILL.md +10 -4
- package/skills/referrals/evals/evals.json +99 -0
- package/skills/referrals/references/affiliate-operations.md +64 -0
- package/skills/referrals/references/affiliate-programs.md +2 -2
- package/skills/referrals/references/program-examples.md +14 -6
- package/skills/referrals/references/review-platform-rules.md +36 -0
- package/skills/referrals/references/review-program-operations.md +62 -0
- package/skills/referrals/references/review-programs.md +96 -0
- package/skills/revops/CHANGELOG.md +5 -0
- package/skills/revops/SKILL.md +10 -6
- package/skills/revops/evals/evals.json +13 -0
- package/skills/revops/references/automation-playbooks.md +1 -1
- package/skills/revops/references/inbound-routing.md +43 -0
- package/skills/revops/references/lifecycle-definitions.md +31 -0
- package/skills/sales-enablement/CHANGELOG.md +5 -0
- package/skills/sales-enablement/SKILL.md +6 -4
- package/skills/sales-enablement/evals/evals.json +14 -0
- package/skills/sales-enablement/references/cold-call-scripts.md +68 -0
- package/skills/schema/CHANGELOG.md +5 -0
- package/skills/schema/SKILL.md +20 -8
- package/skills/schema/evals/evals.json +40 -0
- package/skills/schema/references/schema-examples.md +9 -15
- package/skills/seo-audit/CHANGELOG.md +5 -0
- package/skills/seo-audit/SKILL.md +10 -16
- package/skills/seo-audit/evals/evals.json +68 -0
- package/skills/seo-audit/references/international-seo.md +9 -11
- package/skills/seo-audit/references/local-seo-pages-and-measurement.md +39 -0
- package/skills/seo-audit/references/local-seo-profile-and-citations.md +41 -0
- package/skills/seo-audit/references/local-seo.md +86 -0
- package/skills/seo-audit/references/rankings-push.md +62 -0
- package/skills/seo-audit/references/title-tags.md +89 -0
- package/skills/signup/CHANGELOG.md +5 -0
- package/skills/signup/SKILL.md +7 -3
- package/skills/site-architecture/CHANGELOG.md +5 -0
- package/skills/site-architecture/SKILL.md +5 -4
- package/skills/site-architecture/evals/evals.json +25 -0
- package/skills/site-architecture/references/platforms-and-migration.md +84 -0
- package/skills/social/CHANGELOG.md +5 -0
- package/skills/social/SKILL.md +4 -4
- package/skills/trends/.skillignore +3 -0
- package/skills/trends/CHANGELOG.md +20 -0
- package/skills/trends/SKILL.md +12 -8
- package/skills/trends/references/agent-mode.md +36 -0
- package/skills/trends/references/category-peers.md +38 -0
- package/skills/trends/references/comparison.md +227 -0
- package/skills/trends/references/competitors.md +29 -0
- package/skills/trends/references/discovery.md +90 -0
- package/skills/trends/references/engine-contract.md +111 -1889
- package/skills/trends/references/followups.md +112 -0
- package/skills/trends/references/grok-bot-x.md +55 -0
- package/skills/trends/references/hiring-signals.md +19 -0
- package/skills/trends/references/library-queue.md +33 -0
- package/skills/trends/references/recommendations.md +86 -0
- package/skills/trends/references/research-runbook.md +781 -0
- package/skills/trends/references/runtime.md +165 -0
- package/skills/trends/references/save-html-brief.md +39 -20
- package/skills/trends/references/setup-wizard.md +253 -0
- package/skills/trends/references/synthesis.md +289 -0
- package/skills/trends/scripts/box_chrome_login.py +209 -0
- package/skills/trends/scripts/briefing.py +9 -17
- package/skills/trends/scripts/last30days.py +3247 -280
- package/skills/trends/scripts/lib/agentcookie.py +196 -0
- package/skills/trends/scripts/lib/amazon.py +877 -0
- package/skills/trends/scripts/lib/arxiv.py +42 -8
- package/skills/trends/scripts/lib/backends.py +769 -0
- package/skills/trends/scripts/lib/bird_x.py +340 -57
- package/skills/trends/scripts/lib/bluesky.py +84 -10
- package/skills/trends/scripts/lib/bounded_get.py +156 -0
- package/skills/trends/scripts/lib/brightdata.py +291 -0
- package/skills/trends/scripts/lib/categories.py +7 -1
- package/skills/trends/scripts/lib/chrome_cdp.py +455 -0
- package/skills/trends/scripts/lib/chrome_cookies.py +132 -39
- package/skills/trends/scripts/lib/cluster.py +31 -54
- package/skills/trends/scripts/lib/competitors.py +10 -1
- package/skills/trends/scripts/lib/cookie_extract.py +221 -44
- package/skills/trends/scripts/lib/cookie_paths.py +27 -0
- package/skills/trends/scripts/lib/corpus.py +439 -0
- package/skills/trends/scripts/lib/devto.py +313 -0
- package/skills/trends/scripts/lib/discovery_handoff.py +988 -0
- package/skills/trends/scripts/lib/doctor.py +2166 -0
- package/skills/trends/scripts/lib/dripstack.py +203 -0
- package/skills/trends/scripts/lib/entity_extract.py +37 -0
- package/skills/trends/scripts/lib/env.py +1092 -138
- package/skills/trends/scripts/lib/fanout.py +11 -1
- package/skills/trends/scripts/lib/feed.py +61 -0
- package/skills/trends/scripts/lib/fetch_memo.py +135 -0
- package/skills/trends/scripts/lib/freshness.py +566 -0
- package/skills/trends/scripts/lib/fusion.py +314 -28
- package/skills/trends/scripts/lib/github.py +438 -24
- package/skills/trends/scripts/lib/grok_x.py +1237 -0
- package/skills/trends/scripts/lib/grounding.py +129 -11
- package/skills/trends/scripts/lib/hackernews.py +61 -4
- package/skills/trends/scripts/lib/health.py +373 -0
- package/skills/trends/scripts/lib/hosted.py +333 -0
- package/skills/trends/scripts/lib/html_publish.py +34 -0
- package/skills/trends/scripts/lib/html_render.py +169 -3
- package/skills/trends/scripts/lib/http.py +1215 -70
- package/skills/trends/scripts/lib/instagram.py +132 -9
- package/skills/trends/scripts/lib/jobs.py +4 -2
- package/skills/trends/scripts/lib/library.py +308 -0
- package/skills/trends/scripts/lib/library_index.py +477 -0
- package/skills/trends/scripts/lib/linkedin.py +271 -24
- package/skills/trends/scripts/lib/log.py +5 -2
- package/skills/trends/scripts/lib/meta_ads.py +982 -0
- package/skills/trends/scripts/lib/normalize.py +382 -28
- package/skills/trends/scripts/lib/parallel_mcp.py +234 -0
- package/skills/trends/scripts/lib/permission_preflight.py +42 -3
- package/skills/trends/scripts/lib/perplexity.py +1080 -468
- package/skills/trends/scripts/lib/pipeline.py +5079 -211
- package/skills/trends/scripts/lib/planner.py +396 -53
- package/skills/trends/scripts/lib/polymarket.py +285 -23
- package/skills/trends/scripts/lib/prescriptions.py +432 -0
- package/skills/trends/scripts/lib/private_files.py +21 -0
- package/skills/trends/scripts/lib/providers.py +231 -10
- package/skills/trends/scripts/lib/quality_nudge.py +122 -37
- package/skills/trends/scripts/lib/reddit.py +177 -12
- package/skills/trends/scripts/lib/reddit_arctic.py +146 -5
- package/skills/trends/scripts/lib/reddit_keyless.py +121 -60
- package/skills/trends/scripts/lib/reddit_listing.py +215 -14
- package/skills/trends/scripts/lib/reddit_public.py +12 -13
- package/skills/trends/scripts/lib/reddit_search.py +304 -0
- package/skills/trends/scripts/lib/reddit_shreddit.py +51 -7
- package/skills/trends/scripts/lib/registers.py +134 -0
- package/skills/trends/scripts/lib/relevance.py +17 -0
- package/skills/trends/scripts/lib/render.py +2161 -271
- package/skills/trends/scripts/lib/rerank.py +623 -55
- package/skills/trends/scripts/lib/resolve.py +50 -1
- package/skills/trends/scripts/lib/safari_cookies.py +42 -15
- package/skills/trends/scripts/lib/schema.py +782 -1
- package/skills/trends/scripts/lib/setup_wizard.py +779 -120
- package/skills/trends/scripts/lib/signals.py +206 -7
- package/skills/trends/scripts/lib/stocktwits.py +374 -0
- package/skills/trends/scripts/lib/subproc.py +307 -53
- package/skills/trends/scripts/lib/techmeme.py +89 -64
- package/skills/trends/scripts/lib/telegram.py +317 -0
- package/skills/trends/scripts/lib/tiktok.py +13 -9
- package/skills/trends/scripts/lib/topic_shape.py +340 -0
- package/skills/trends/scripts/lib/transcribe.py +12 -3
- package/skills/trends/scripts/lib/trustpilot.py +256 -5
- package/skills/trends/scripts/lib/truthsocial.py +9 -1
- package/skills/trends/scripts/lib/ui.py +6 -0
- package/skills/trends/scripts/lib/usage.py +132 -0
- package/skills/trends/scripts/lib/web_search_keyless.py +63 -4
- package/skills/trends/scripts/lib/windows_job.py +138 -0
- package/skills/trends/scripts/lib/x_api.py +824 -0
- package/skills/trends/scripts/lib/x_envelope.py +733 -0
- package/skills/trends/scripts/lib/x_judge.py +292 -0
- package/skills/trends/scripts/lib/xai_x.py +26 -3
- package/skills/trends/scripts/lib/xquik.py +89 -15
- package/skills/trends/scripts/lib/xurl_x.py +284 -91
- package/skills/trends/scripts/lib/youtube_yt.py +570 -119
- package/skills/trends/scripts/setup-keychain.sh +12 -4
- package/skills/trends/scripts/setup-pass.sh +3 -0
- package/skills/trends/scripts/store.py +444 -50
- package/skills/trends/scripts/watchlist.py +38 -11
- package/skills/trends/scripts/x_lane_parity.py +107 -0
- package/skills/video-studio/CHANGELOG.md +8 -0
- package/skills/video-studio/SKILL.md +6 -5
- package/skills/video-studio/references/styles/product-flight.md +45 -0
- package/skills/video-studio/references/styles.md +3 -1
- package/skills/video-studio/scripts/qa.mjs +64 -2
- package/skills/video-studio/scripts/qa.test.mjs +29 -1
- package/skills/video-studio-motion/ATTRIBUTION.md +1 -1
- package/skills/video-studio-motion/CHANGELOG.md +4 -0
- package/skills/video-studio-motion/SKILL.md +8 -5
- package/skills/coder-brainstorming/spec-document-reviewer-prompt.md +0 -49
- 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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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"
|
|
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 = {
|
|
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
|