Skip to content

Commit 5d44e48

Browse files
Remove Telegram broadcast channels as a course source
The t.me/s/<channel> preview indexes time-bound announcements (e.g. "...Live Q&A in 2 hours") that later surface as stale answers. Drop the loader, the [sources.telegram] config, and the per-course telegram_channel handles, and regenerate generated_config.py. The loader, check script, and config snippet are archived under .tmp/ (gitignored) so the source can be re-enabled once dated posts can be filtered or expired.
1 parent ab13042 commit 5d44e48

5 files changed

Lines changed: 8 additions & 222 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ __pycache__/
88
.wrangler/
99
dist/
1010
tmp/
11+
.tmp/
1112
*.ndjson
1213
artifacts/
1314
.aws-sam/

config.toml

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ course = "stock-markets-analytics-zoomcamp"
8282
[courses."data-engineering-zoomcamp"]
8383
name = "Data Engineering Zoomcamp"
8484
slack_channels = ["C01FABYF2RG"]
85-
telegram_channel = "dezoomcamp"
8685
faq_enabled = true
8786
docs_prefix = "courses/data-engineering-zoomcamp/"
8887

@@ -93,8 +92,6 @@ ref = "main"
9392
[courses."mlops-zoomcamp"]
9493
name = "MLOps Zoomcamp"
9594
slack_channels = ["C02R98X7DS9"]
96-
# Public preview titled "MLOps Zoomcamp announcements".
97-
telegram_channel = "dtc_courses"
9895
faq_enabled = true
9996
docs_prefix = "courses/mlops-zoomcamp/"
10097

@@ -105,7 +102,6 @@ ref = "main"
105102
[courses."llm-zoomcamp"]
106103
name = "LLM Zoomcamp"
107104
slack_channels = ["C06TEGTGM3J"]
108-
telegram_channel = "llm_zoomcamp"
109105
faq_enabled = true
110106
docs_prefix = "courses/llm-zoomcamp/"
111107

@@ -116,7 +112,6 @@ ref = "main"
116112
[courses."machine-learning-zoomcamp"]
117113
name = "Machine Learning Zoomcamp"
118114
slack_channels = ["C0288NJ5XSA"]
119-
telegram_channel = "mlzoomcamp"
120115
faq_enabled = true
121116
docs_prefix = "courses/ml-zoomcamp/"
122117

@@ -127,7 +122,6 @@ ref = "main"
127122
[courses."ai-dev-tools-zoomcamp"]
128123
name = "AI Dev Tools Zoomcamp"
129124
slack_channels = ["C09HWT76L95"]
130-
telegram_channel = "aidevtoolszoomcamp"
131125
faq_enabled = true
132126
docs_prefix = "courses/ai-dev-tools-zoomcamp/"
133127

@@ -138,7 +132,6 @@ ref = "main"
138132
[courses."stock-markets-analytics-zoomcamp"]
139133
name = "Stock Markets Analytics Zoomcamp"
140134
slack_channels = ["C06L1RTF10F"]
141-
telegram_channel = "stockanalyticszoomcamp"
142135
faq_enabled = true
143136
docs_prefix = "courses/stock-markets-analytics-zoomcamp/"
144137

@@ -151,15 +144,6 @@ enabled = true
151144
courses_url = "https://datatalks.club/faq/json/courses.json"
152145
item_id_prefix = "faq"
153146

154-
[sources.telegram]
155-
# Keyless ingestion of each course's public Telegram broadcast channel via the
156-
# t.me/s/<channel> web preview. The channel handle is set per course as
157-
# `telegram_channel`. Only public channels expose this preview.
158-
enabled = true
159-
item_id_prefix = "telegram"
160-
scope = "course"
161-
lookback_months = 12
162-
163147
[sources.docs]
164148
enabled = true
165149
item_id_prefix = "docs"

scripts/check_telegram.py

Lines changed: 0 additions & 51 deletions
This file was deleted.

src/faq_assistant/generated_config.py

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -66,43 +66,37 @@
6666
'github_repositories': [{'ref': 'main',
6767
'repo': 'DataTalksClub/ai-dev-tools-zoomcamp'}],
6868
'name': 'AI Dev Tools Zoomcamp',
69-
'slack_channels': ['C09HWT76L95'],
70-
'telegram_channel': 'aidevtoolszoomcamp'},
69+
'slack_channels': ['C09HWT76L95']},
7170
'data-engineering-zoomcamp': {'docs_prefix': 'courses/data-engineering-zoomcamp/',
7271
'faq_enabled': True,
7372
'github_repositories': [{'ref': 'main',
7473
'repo': 'DataTalksClub/data-engineering-zoomcamp'}],
7574
'name': 'Data Engineering Zoomcamp',
76-
'slack_channels': ['C01FABYF2RG'],
77-
'telegram_channel': 'dezoomcamp'},
75+
'slack_channels': ['C01FABYF2RG']},
7876
'llm-zoomcamp': {'docs_prefix': 'courses/llm-zoomcamp/',
7977
'faq_enabled': True,
8078
'github_repositories': [{'ref': 'main',
8179
'repo': 'DataTalksClub/llm-zoomcamp'}],
8280
'name': 'LLM Zoomcamp',
83-
'slack_channels': ['C06TEGTGM3J'],
84-
'telegram_channel': 'llm_zoomcamp'},
81+
'slack_channels': ['C06TEGTGM3J']},
8582
'machine-learning-zoomcamp': {'docs_prefix': 'courses/ml-zoomcamp/',
8683
'faq_enabled': True,
8784
'github_repositories': [{'ref': 'main',
8885
'repo': 'DataTalksClub/machine-learning-zoomcamp'}],
8986
'name': 'Machine Learning Zoomcamp',
90-
'slack_channels': ['C0288NJ5XSA'],
91-
'telegram_channel': 'mlzoomcamp'},
87+
'slack_channels': ['C0288NJ5XSA']},
9288
'mlops-zoomcamp': {'docs_prefix': 'courses/mlops-zoomcamp/',
9389
'faq_enabled': True,
9490
'github_repositories': [{'ref': 'main',
9591
'repo': 'DataTalksClub/mlops-zoomcamp'}],
9692
'name': 'MLOps Zoomcamp',
97-
'slack_channels': ['C02R98X7DS9'],
98-
'telegram_channel': 'dtc_courses'},
93+
'slack_channels': ['C02R98X7DS9']},
9994
'stock-markets-analytics-zoomcamp': {'docs_prefix': 'courses/stock-markets-analytics-zoomcamp/',
10095
'faq_enabled': True,
10196
'github_repositories': [{'ref': 'main',
10297
'repo': 'DataTalksClub/stock-markets-analytics-zoomcamp'}],
10398
'name': 'Stock Markets Analytics Zoomcamp',
104-
'slack_channels': ['C06L1RTF10F'],
105-
'telegram_channel': 'stockanalyticszoomcamp'}},
99+
'slack_channels': ['C06L1RTF10F']}},
106100
'ingestion': {'batch_size': 100,
107101
'chunk': {'max_chars': 1800, 'overlap_chars': 150},
108102
'github': {'fetch_library': 'gitsource', 'token_env': 'GITHUB_TOKEN'},
@@ -187,8 +181,4 @@
187181
'scope': 'docs'},
188182
'faq': {'courses_url': 'https://datatalks.club/faq/json/courses.json',
189183
'enabled': True,
190-
'item_id_prefix': 'faq'},
191-
'telegram': {'enabled': True,
192-
'item_id_prefix': 'telegram',
193-
'lookback_months': 12,
194-
'scope': 'course'}}}
184+
'item_id_prefix': 'faq'}}}

src/faq_assistant/sources.py

Lines changed: 0 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
from __future__ import annotations
22

33
import hashlib
4-
import html as html_lib
54
import re
6-
from datetime import datetime, timedelta, timezone
75
from typing import Any
86
from urllib.parse import urljoin
97

@@ -32,9 +30,6 @@ def load_source_documents(config: dict[str, Any]) -> list[SourceDocument]:
3230
if config["sources"]["course_repositories"]["enabled"]:
3331
documents.extend(load_course_repository_documents(config))
3432

35-
if config["sources"].get("telegram", {}).get("enabled"):
36-
documents.extend(load_telegram_documents(config))
37-
3833
return documents
3934

4035

@@ -184,139 +179,6 @@ def load_course_repository_documents(config: dict[str, Any]) -> list[SourceDocum
184179
return documents
185180

186181

187-
TELEGRAM_PREVIEW_BASE = "https://t.me/s/"
188-
TELEGRAM_LINK_BASE = "https://t.me/"
189-
# Telegram serves an empty preview to some default user agents; use a browser-like one.
190-
TELEGRAM_USER_AGENT = "Mozilla/5.0 (compatible; faq-assistant-ingest/1.0)"
191-
192-
193-
def load_telegram_documents(config: dict[str, Any]) -> list[SourceDocument]:
194-
"""Index recent posts from each course's public Telegram broadcast channel.
195-
196-
Uses the keyless ``t.me/s/<channel>`` web preview (no bot token / API key),
197-
walking backwards with ``?before=<id>`` until posts predate the lookback
198-
window. Only public channels expose this preview.
199-
"""
200-
source_config = config["sources"]["telegram"]
201-
lookback_months = int(source_config.get("lookback_months", 12))
202-
cutoff = datetime.now(timezone.utc) - timedelta(days=round(lookback_months * 30.44))
203-
204-
documents: list[SourceDocument] = []
205-
for course, course_config in config["courses"].items():
206-
channel = str(course_config.get("telegram_channel") or "").strip().lstrip("@")
207-
if not channel:
208-
continue
209-
try:
210-
posts = fetch_telegram_posts(channel, cutoff)
211-
except Exception as e:
212-
print(f"warning: failed to fetch telegram channel {channel}: {e}")
213-
continue
214-
215-
for post in posts:
216-
documents.append(
217-
SourceDocument(
218-
source_type="telegram",
219-
scope="course",
220-
course=course,
221-
course_name=course_config["name"],
222-
section="Telegram announcements",
223-
title=telegram_title(post["text"]),
224-
text=post["text"],
225-
url=f"{TELEGRAM_LINK_BASE}{post['id']}",
226-
repo=None,
227-
path=None,
228-
source_id=f"telegram:{post['id']}",
229-
)
230-
)
231-
232-
return documents
233-
234-
235-
def fetch_telegram_posts(
236-
channel: str, cutoff: datetime, max_pages: int = 200
237-
) -> list[dict[str, Any]]:
238-
collected: dict[str, dict[str, Any]] = {}
239-
before: int | None = None
240-
241-
for _ in range(max_pages):
242-
params = {"before": before} if before else {}
243-
response = requests.get(
244-
f"{TELEGRAM_PREVIEW_BASE}{channel}",
245-
params=params,
246-
headers={"User-Agent": TELEGRAM_USER_AGENT},
247-
timeout=60,
248-
)
249-
response.raise_for_status()
250-
251-
posts = parse_telegram_page(response.text, channel)
252-
if not posts:
253-
break
254-
255-
reached_cutoff = False
256-
for post in posts:
257-
if post["datetime"] < cutoff:
258-
reached_cutoff = True
259-
continue
260-
collected[post["id"]] = post
261-
262-
if reached_cutoff:
263-
break
264-
before = min(post["seq"] for post in posts)
265-
266-
return sorted(collected.values(), key=lambda post: post["seq"], reverse=True)
267-
268-
269-
def parse_telegram_page(html_text: str, channel: str) -> list[dict[str, Any]]:
270-
anchors = list(re.finditer(rf'data-post="{re.escape(channel)}/(\d+)"', html_text))
271-
posts: list[dict[str, Any]] = []
272-
273-
for index, anchor in enumerate(anchors):
274-
seq = int(anchor.group(1))
275-
end = anchors[index + 1].start() if index + 1 < len(anchors) else len(html_text)
276-
segment = html_text[anchor.end() : end]
277-
278-
time_match = re.search(r'<time datetime="([^"]+)"', segment)
279-
if not time_match:
280-
continue
281-
posted_at = datetime.fromisoformat(time_match.group(1))
282-
283-
text_match = re.search(
284-
r'tgme_widget_message_text[^>]*>(.*?)</div>', segment, re.DOTALL
285-
)
286-
text = telegram_html_to_text(text_match.group(1)) if text_match else ""
287-
if not text:
288-
continue # media-only / empty post
289-
290-
posts.append(
291-
{"id": f"{channel}/{seq}", "seq": seq, "datetime": posted_at, "text": text}
292-
)
293-
294-
return posts
295-
296-
297-
def telegram_html_to_text(fragment: str) -> str:
298-
fragment = re.sub(r"<br\s*/?>", "\n", fragment, flags=re.IGNORECASE)
299-
# Preserve links as Markdown so the answer can cite the real URL.
300-
fragment = re.sub(
301-
r'<a\b[^>]*\bhref="([^"]+)"[^>]*>(.*?)</a>',
302-
lambda m: f"[{strip_tags(m.group(2))}]({m.group(1)})",
303-
fragment,
304-
flags=re.DOTALL | re.IGNORECASE,
305-
)
306-
return clean_text(html_lib.unescape(strip_tags(fragment)))
307-
308-
309-
def telegram_title(text: str, limit: int = 80) -> str:
310-
first_line = next((line.strip() for line in text.splitlines() if line.strip()), "")
311-
if len(first_line) > limit:
312-
first_line = first_line[: limit - 1].rstrip() + "…"
313-
return first_line or "Telegram post"
314-
315-
316-
def strip_tags(value: str) -> str:
317-
return re.sub(r"<[^>]+>", "", value)
318-
319-
320182
def read_github_files(github_config: dict[str, Any], required_prefix: str | None = None):
321183
owner, repo_name = github_config["repo"].split("/", 1)
322184
include = list(github_config.get("include", []))

0 commit comments

Comments
 (0)