bot: inline keyboard, channel posting, format switch, abuse subscribe; sources: own TG channels; hide keys tag
This commit is contained in:
40
README.md
40
README.md
@@ -1,3 +1,21 @@
|
|||||||
|
### Telegram-бот: кнопки, канал, формат
|
||||||
|
|
||||||
|
```bash
|
||||||
|
set OMNI_BOT_TOKEN=<токен BotFather> # .bot_token, в git не попадает
|
||||||
|
set OMNI_BOT_OWNERS=<твой numeric id> # кому отвечает, можно несколько через запятую
|
||||||
|
python -m omni.bot
|
||||||
|
```
|
||||||
|
|
||||||
|
Экран бота — инлайн-кнопки: `Лента`, `Абузы`, `Источники`, `Цифры`, теги `Промо / Триалы / Сливы`, `Подписка: вкл/выкл`, `Формат: кратко/полно`, `Пост в канал`.
|
||||||
|
|
||||||
|
Команды: `/new [N]`, `/abuse [тег]`, `/find <слово>`, `/src <слово>`, `/run <категория>`, `/add <url>`, `/auto [N]`, `/stats`, `/sub [@канал]`, `/unsub [@канал]`, `/subs`, `/post [@канал]`.
|
||||||
|
|
||||||
|
**Постинг в канал.** Добавь бота админом канала с правом публикации и отправь `/sub @твойканал` — бот каждые 5 минут проверяет новые абузы и публикует только те, что появились после подписки. `/post @твойканал` — опубликовать подборку сразу, `/unsub @твойканал` — остановить. Дальше можно подключить свой бот: код один, токен свой, база общая.
|
||||||
|
|
||||||
|
**Формат текста.** `кратко` — заголовок, источник, тег. `полно` — плюс ссылка на запись. Переключается кнопкой и хранится на чат/канал: канал можно держать в одном формате, личку — в другом.
|
||||||
|
|
||||||
|
**Про ключи не публикуем.** Тег `keys` отфильтрован на уровне бота (`HIDE_TAGS` в `omni/bot.py`): записи, у которых после фильтра не осталось тегов, не попадают ни в `/abuse`, ни в автопостинг.
|
||||||
|
|
||||||
# omni-parse
|
# omni-parse
|
||||||
|
|
||||||
Один парсер под любую тему: Reddit, форумы, поисковики, X/Twitter, карточки товаров и платные search-API — в одну SQLite-базу с FTS5, дедупом и рейтингом.
|
Один парсер под любую тему: Reddit, форумы, поисковики, X/Twitter, карточки товаров и платные search-API — в одну SQLite-базу с FTS5, дедупом и рейтингом.
|
||||||
@@ -176,7 +194,7 @@ MIT.
|
|||||||
|
|
||||||
## Абузы, авто-добавление и Telegram-бот
|
## Абузы, авто-добавление и Telegram-бот
|
||||||
|
|
||||||
- **абузы**: категория `abuse` — 31 живых площадок (XenForo/Discourse/Telegram/RSS), классификатор `omni/abuse.py` раскладывает записи по тегам `ключи / промо / триалы / сливы / софт / продажа / новости`. В панели — отдельная вкладка (`?view=abuse`), в боте — `/abuse [тег]`.
|
- **абузы**: категория `abuse` — 31 живая площадка (XenForo/Discourse/Telegram/RSS), включая свои каналы, классификатор `omni/abuse.py` раскладывает записи по тегам `ключи / промо / триалы / сливы / софт / продажа / новости`. В панели — отдельная вкладка (`?view=abuse`), в боте — `/abuse [тег]`.
|
||||||
- **авто-добавление**: `omni/discover.py` ищет фиды на уже подключённых хостах и каталогах, проверяет их теми же адаптерами и сохраняет только те, что реально отдали записи. В панели — кнопка «авто-добавление», в API — `POST /api/discover?hosts=40&add=25`.
|
- **авто-добавление**: `omni/discover.py` ищет фиды на уже подключённых хостах и каталогах, проверяет их теми же адаптерами и сохраняет только те, что реально отдали записи. В панели — кнопка «авто-добавление», в API — `POST /api/discover?hosts=40&add=25`.
|
||||||
- **новости**: любые RSS/Atom/JSON-ленты (редакции, релизы, сообщества) подключаются как обычный источник или через `/add <url>` в боте — отдельной схемы не требуется.
|
- **новости**: любые RSS/Atom/JSON-ленты (редакции, релизы, сообщества) подключаются как обычный источник или через `/add <url>` в боте — отдельной схемы не требуется.
|
||||||
|
|
||||||
@@ -190,3 +208,23 @@ python -m omni.bot
|
|||||||
|
|
||||||
Команды: `/new [N]`, `/abuse [тег]`, `/find <слово>`, `/src <слово>`, `/run <категория>`, `/add <url>`, `/stats`. Бот работает на той же базе и том же реестре, что панель; отвечает только id из `OMNI_BOT_ALLOW`.
|
Команды: `/new [N]`, `/abuse [тег]`, `/find <слово>`, `/src <слово>`, `/run <категория>`, `/add <url>`, `/stats`. Бот работает на той же базе и том же реестре, что панель; отвечает только id из `OMNI_BOT_ALLOW`.
|
||||||
|
|
||||||
|
|
||||||
|
## Что нужно подключить
|
||||||
|
|
||||||
|
1. **Python 3.11+** и зависимости: `pip install -r requirements.txt` (httpx, curl_cffi, bs4, lxml). Больше ничего не обязательно — база создаётся сама в `data/omni.sqlite3`.
|
||||||
|
2. **Ни одного ключа не нужно.** Reddit (архив), форумы (Discourse/XenForo/веб-Telegram), HN, arXiv, GitHub, HF, поисковики работают без регистрации. Платные search-API (`TAVILY_API_KEY`, `EXA_API_KEY`, `YOU_API_KEY`, `FIRECRAWL_API_KEY`) подхватываются из env, если они есть: с ними больше покрытие, без них проект полностью рабочий.
|
||||||
|
3. **Свои источники**: кнопка «авто-добавление» в панели, `POST /api/discover`, `/auto` в боте или `python -m omni add <url>` — адрес проверяется реальным запросом и сохраняется только если отдал записи.
|
||||||
|
4. **Свой бот**: токен BotFather в `OMNI_BOT_TOKEN`, свой numeric id в `OMNI_BOT_OWNERS`, бота админом канала и `/sub @канал`.
|
||||||
|
|
||||||
|
## Проект обновляется
|
||||||
|
|
||||||
|
Проект живой: реестр источников, адаптеры и классификатор абузов правятся по мере того, как площадки ломают разметку. Обновление:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python -m omni serve --port 8077 # панель
|
||||||
|
python -m omni.bot # бот, если нужен
|
||||||
|
```
|
||||||
|
|
||||||
|
База в `data/` не перезаписывается — источники, здоровье и подписки остаются на месте. Ближайшие шаги: отдельная новостная лента с группировкой по сюжетам, скоринг абузов по свежести и частоте площадки, экспорт подборки в канал по расписанию.
|
||||||
|
|||||||
264
omni/bot.py
264
omni/bot.py
@@ -12,6 +12,9 @@
|
|||||||
/add <url> добавить источник (сначала реальная проверка фида)
|
/add <url> добавить источник (сначала реальная проверка фида)
|
||||||
/run <категория> прогнать категорию
|
/run <категория> прогнать категорию
|
||||||
/auto [n] автодобавление новых фидов
|
/auto [n] автодобавление новых фидов
|
||||||
|
/sub [@канал] подписать чат/канал на новые абузы
|
||||||
|
/unsub [@канал] отписаться
|
||||||
|
/post [@канал] опубликовать абузы сейчас
|
||||||
/stats цифры
|
/stats цифры
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -44,13 +47,16 @@ def _api(method, **params):
|
|||||||
return json.loads(resp.read().decode("utf-8"))
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def _send(chat_id, text, preview=True):
|
def _send(chat_id, text, preview=True, reply_markup=None):
|
||||||
text = text[:MAX_LEN]
|
text = text[:MAX_LEN]
|
||||||
if os.environ.get("OMNI_BOT_DRY"):
|
if os.environ.get("OMNI_BOT_DRY"):
|
||||||
print("[DRY] " + text.replace("</b>", "").replace("<b>", "")[:1200])
|
print("[DRY] " + text.replace("</b>", "").replace("<b>", "")[:1200])
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
kw = {}
|
||||||
|
if reply_markup:
|
||||||
|
kw["reply_markup"] = json.dumps(reply_markup, ensure_ascii=False)
|
||||||
return _api("sendMessage", chat_id=chat_id, text=text, parse_mode="HTML",
|
return _api("sendMessage", chat_id=chat_id, text=text, parse_mode="HTML",
|
||||||
disable_web_page_preview="false" if preview else "true")
|
disable_web_page_preview="false" if preview else "true", **kw)
|
||||||
|
|
||||||
|
|
||||||
def _esc(s):
|
def _esc(s):
|
||||||
@@ -76,7 +82,8 @@ def cmd_start(chat_id, arg):
|
|||||||
"/add <url> — подключить фид\n"
|
"/add <url> — подключить фид\n"
|
||||||
"/run abuse — прогнать категорию\n"
|
"/run abuse — прогнать категорию\n"
|
||||||
"/auto 10 — автодобавление\n"
|
"/auto 10 — автодобавление\n"
|
||||||
"/stats — цифры")
|
"/stats — цифры\n/sub @канал — постить новые абузы в канал\n/post @канал — опубликовать сейчас",
|
||||||
|
reply_markup=KB_MAIN)
|
||||||
|
|
||||||
|
|
||||||
def cmd_new(chat_id, arg):
|
def cmd_new(chat_id, arg):
|
||||||
@@ -169,9 +176,213 @@ def cmd_stats(chat_id, arg):
|
|||||||
s.get("items"), len(a), alive, len(sources.CATEGORIES), s.get("runs"), s.get("priced")))
|
s.get("items"), len(a), alive, len(sources.CATEGORIES), s.get("runs"), s.get("priced")))
|
||||||
|
|
||||||
|
|
||||||
|
# --- подписка, формат, канал -------------------------------------------------
|
||||||
|
HIDE_TAGS = {"keys"} # про ключи не постим и не показываем
|
||||||
|
SUBS_SQL = ("CREATE TABLE IF NOT EXISTS abuse_subs (chat_id INTEGER PRIMARY KEY, "
|
||||||
|
"ts INTEGER DEFAULT 0, last_id INTEGER DEFAULT 0)")
|
||||||
|
|
||||||
|
KB_MAIN = {"inline_keyboard": [
|
||||||
|
[{"text": "Лента", "callback_data": "m:new"}, {"text": "Абузы", "callback_data": "m:abuse"}],
|
||||||
|
[{"text": "Источники", "callback_data": "m:src"}, {"text": "Цифры", "callback_data": "m:stats"}],
|
||||||
|
[{"text": "Промо", "callback_data": "t:promo"}, {"text": "Триалы", "callback_data": "t:trial"},
|
||||||
|
{"text": "Сливы", "callback_data": "t:leak"}],
|
||||||
|
[{"text": "Подписка: вкл", "callback_data": "sub:on"}, {"text": "выкл", "callback_data": "sub:off"}],
|
||||||
|
[{"text": "Формат: кратко", "callback_data": "fmt:short"},
|
||||||
|
{"text": "полно", "callback_data": "fmt:full"}],
|
||||||
|
[{"text": "Пост в канал", "callback_data": "post:now"}],
|
||||||
|
]}
|
||||||
|
|
||||||
|
PREFS_SQL = "CREATE TABLE IF NOT EXISTS bot_prefs (chat_id INTEGER PRIMARY KEY, fmt TEXT DEFAULT 'short')"
|
||||||
|
|
||||||
|
|
||||||
|
def _conn():
|
||||||
|
import sqlite3
|
||||||
|
from .config import DB
|
||||||
|
c = sqlite3.connect(str(DB), timeout=30)
|
||||||
|
c.execute(SUBS_SQL)
|
||||||
|
c.execute(PREFS_SQL)
|
||||||
|
c.commit()
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def prefs_get(chat_id):
|
||||||
|
c = _conn()
|
||||||
|
r = c.execute("SELECT fmt FROM bot_prefs WHERE chat_id=?", (chat_id,)).fetchone()
|
||||||
|
c.close()
|
||||||
|
return (r[0] if r else "short")
|
||||||
|
|
||||||
|
|
||||||
|
def prefs_set(chat_id, fmt):
|
||||||
|
c = _conn()
|
||||||
|
c.execute("INSERT INTO bot_prefs(chat_id, fmt) VALUES(?,?) "
|
||||||
|
"ON CONFLICT(chat_id) DO UPDATE SET fmt=excluded.fmt", (chat_id, fmt))
|
||||||
|
c.commit()
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
|
||||||
|
def subs_add(target, last_id=0):
|
||||||
|
c = _conn()
|
||||||
|
if not last_id:
|
||||||
|
last_id = c.execute("SELECT COALESCE(MAX(id),0) FROM items").fetchone()[0]
|
||||||
|
c.execute("INSERT INTO abuse_subs(chat_id, ts, last_id) VALUES(?,?,?) "
|
||||||
|
"ON CONFLICT(chat_id) DO UPDATE SET last_id=excluded.last_id",
|
||||||
|
(target, int(time.time()), last_id))
|
||||||
|
c.commit()
|
||||||
|
c.close()
|
||||||
|
return last_id
|
||||||
|
|
||||||
|
|
||||||
|
def subs_del(target):
|
||||||
|
c = _conn()
|
||||||
|
n = c.execute("DELETE FROM abuse_subs WHERE chat_id=?", (target,)).rowcount
|
||||||
|
c.commit()
|
||||||
|
c.close()
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def subs_all():
|
||||||
|
c = _conn()
|
||||||
|
rows = c.execute("SELECT chat_id, last_id FROM abuse_subs ORDER BY ts DESC").fetchall()
|
||||||
|
c.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_chat(arg, chat_id):
|
||||||
|
a = (arg or "").strip()
|
||||||
|
if not a:
|
||||||
|
return chat_id
|
||||||
|
if a.lstrip("-").isdigit():
|
||||||
|
return int(a)
|
||||||
|
try:
|
||||||
|
r = _api("getChat", chat_id=a)
|
||||||
|
return (r.get("result") or {}).get("id")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _scan_safe(limit=10, tag=None, window=6000):
|
||||||
|
d = abuse_mod.scan(limit=limit * 3, tag=tag, window=window)
|
||||||
|
out = []
|
||||||
|
for it in d.get("items", []):
|
||||||
|
tags = [t for t in (it.get("tags") or []) if t not in HIDE_TAGS]
|
||||||
|
if not tags:
|
||||||
|
continue
|
||||||
|
it = dict(it)
|
||||||
|
it["tags"] = tags
|
||||||
|
out.append(it)
|
||||||
|
d["items"] = out[:limit]
|
||||||
|
d["hidden_keys"] = len(d.get("items", [])) - len(out)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def _line(it, fmt="short"):
|
||||||
|
title = _esc((it.get("title") or "")[:150])
|
||||||
|
src = _esc(it.get("source") or "")
|
||||||
|
tags = _esc(",".join(it.get("tags") or []))
|
||||||
|
head = "<b>%s</b>\n<code>%s</code> · %s" % (title, src, tags)
|
||||||
|
if fmt == "full":
|
||||||
|
url = it.get("url") or ""
|
||||||
|
if url:
|
||||||
|
head += ' · <a href="%s">открыть</a>' % _esc(url)
|
||||||
|
return head
|
||||||
|
|
||||||
|
|
||||||
|
def abuse_text(tag=None, limit=6, fmt="short"):
|
||||||
|
d = _scan_safe(limit=limit, tag=tag)
|
||||||
|
if not d["items"]:
|
||||||
|
return None
|
||||||
|
head = "абузы · %s · %d" % (tag or "все", d["total"])
|
||||||
|
return head + "\n\n" + "\n\n".join(_line(i, fmt) for i in d["items"])
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_abuse(chat_id, arg):
|
||||||
|
tag = arg.strip().split()[0] if arg.strip() else None
|
||||||
|
txt = abuse_text(tag, fmt=prefs_get(chat_id))
|
||||||
|
_send(chat_id, txt or "абузов не видно", reply_markup=KB_MAIN)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_sub(chat_id, arg):
|
||||||
|
target = _resolve_chat(arg, chat_id)
|
||||||
|
if not target:
|
||||||
|
return _send(chat_id, "не вижу такой чат: добавь бота админом и повтори /sub @канал", reply_markup=KB_MAIN)
|
||||||
|
last = subs_add(target)
|
||||||
|
where = "сюда" if target == chat_id else "<code>%s</code>" % target
|
||||||
|
_send(chat_id, "подписан: %s, шлю только новые абузы (после id %d). /unsub — отписаться" % (where, last),
|
||||||
|
reply_markup=KB_MAIN)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_unsub(chat_id, arg):
|
||||||
|
target = _resolve_chat(arg, chat_id) or chat_id
|
||||||
|
n = subs_del(target)
|
||||||
|
_send(chat_id, "отписал" if n else "подписки не было", reply_markup=KB_MAIN)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_subs(chat_id, arg):
|
||||||
|
rows = subs_all()
|
||||||
|
if not rows:
|
||||||
|
return _send(chat_id, "подписок нет", reply_markup=KB_MAIN)
|
||||||
|
body = "\n".join("<code>%s</code> · id %s" % (c, i) for c, i in rows[:20])
|
||||||
|
_send(chat_id, "<b>подписки (%d)</b>\n%s" % (len(rows), body), reply_markup=KB_MAIN)
|
||||||
|
|
||||||
|
|
||||||
|
def publish(target, tag=None, limit=5):
|
||||||
|
"""Один пост с абузами в указанный чат/канал."""
|
||||||
|
fmt = prefs_get(target) if isinstance(target, int) else "short"
|
||||||
|
txt = abuse_text(tag, limit=limit, fmt=fmt)
|
||||||
|
if not txt:
|
||||||
|
return False
|
||||||
|
_send(target, txt)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_test(chat_id, arg):
|
||||||
|
target = _resolve_chat(arg, chat_id)
|
||||||
|
if not target:
|
||||||
|
return _send(chat_id, "куда постить? /post @канал", reply_markup=KB_MAIN)
|
||||||
|
ok = publish(target)
|
||||||
|
_send(chat_id, "опубликовал" if ok else "нечего публиковать", reply_markup=KB_MAIN)
|
||||||
|
|
||||||
|
|
||||||
|
def push_new(per_chat=5, window=2000):
|
||||||
|
subs = subs_all()
|
||||||
|
if not subs:
|
||||||
|
return 0
|
||||||
|
d = _scan_safe(limit=400, window=window)
|
||||||
|
items = sorted(d["items"], key=lambda x: x.get("id") or 0)
|
||||||
|
sent = 0
|
||||||
|
for target, last in subs:
|
||||||
|
fresh = [i for i in items if (i.get("id") or 0) > (last or 0)]
|
||||||
|
if not fresh:
|
||||||
|
continue
|
||||||
|
chunk = fresh[-per_chat:]
|
||||||
|
fmt = prefs_get(target) if isinstance(target, int) else "short"
|
||||||
|
text = ("<b>новые абузы · %d</b>\n\n" % len(fresh)) + "\n\n".join(_line(i, fmt) for i in chunk)
|
||||||
|
try:
|
||||||
|
_send(target, text[:MAX_LEN])
|
||||||
|
sent += 1
|
||||||
|
except Exception as e:
|
||||||
|
print("push fail", target, e, flush=True)
|
||||||
|
continue
|
||||||
|
subs_add(target, last_id=chunk[-1].get("id") or last)
|
||||||
|
return sent
|
||||||
|
|
||||||
|
|
||||||
|
def pusher(every=300):
|
||||||
|
while True:
|
||||||
|
time.sleep(every)
|
||||||
|
try:
|
||||||
|
n = push_new()
|
||||||
|
if n:
|
||||||
|
print("pushed", n, flush=True)
|
||||||
|
except Exception as e:
|
||||||
|
print("pusher fail", e, flush=True)
|
||||||
|
|
||||||
|
|
||||||
CMDS = {"/start": cmd_start, "/help": cmd_start, "/new": cmd_new, "/abuse": cmd_abuse,
|
CMDS = {"/start": cmd_start, "/help": cmd_start, "/new": cmd_new, "/abuse": cmd_abuse,
|
||||||
"/find": cmd_find, "/src": cmd_src, "/add": cmd_add, "/run": cmd_run,
|
"/find": cmd_find, "/src": cmd_src, "/add": cmd_add, "/run": cmd_run,
|
||||||
"/auto": cmd_auto, "/stats": cmd_stats}
|
"/auto": cmd_auto, "/stats": cmd_stats,
|
||||||
|
"/sub": cmd_sub, "/unsub": cmd_unsub, "/subs": cmd_subs,
|
||||||
|
"/post": cmd_test}
|
||||||
|
|
||||||
|
|
||||||
def handle(update):
|
def handle(update):
|
||||||
@@ -198,21 +409,62 @@ def handle(update):
|
|||||||
_send(chat_id, "ошибка: %s" % _esc(type(e).__name__ + ": " + str(e)[:160]))
|
_send(chat_id, "ошибка: %s" % _esc(type(e).__name__ + ": " + str(e)[:160]))
|
||||||
|
|
||||||
|
|
||||||
|
def handle_cb(cb):
|
||||||
|
cb_id = cb.get("id")
|
||||||
|
data = cb.get("data") or ""
|
||||||
|
msg = cb.get("message") or {}
|
||||||
|
chat_id = (msg.get("chat") or {}).get("id")
|
||||||
|
try:
|
||||||
|
_api("answerCallbackQuery", callback_query_id=cb_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if not chat_id or (OWNERS and (cb.get("from") or {}).get("id") not in OWNERS):
|
||||||
|
return
|
||||||
|
fmt = prefs_get(chat_id)
|
||||||
|
if data == "m:new":
|
||||||
|
return cmd_new(chat_id, "5")
|
||||||
|
if data == "m:abuse":
|
||||||
|
return cmd_abuse(chat_id, "")
|
||||||
|
if data == "m:src":
|
||||||
|
return cmd_src(chat_id, "")
|
||||||
|
if data == "m:stats":
|
||||||
|
return cmd_stats(chat_id, "")
|
||||||
|
if data.startswith("t:"):
|
||||||
|
return cmd_abuse(chat_id, data[2:])
|
||||||
|
if data == "sub:on":
|
||||||
|
return cmd_sub(chat_id, "")
|
||||||
|
if data == "sub:off":
|
||||||
|
return cmd_unsub(chat_id, "")
|
||||||
|
if data.startswith("fmt:"):
|
||||||
|
prefs_set(chat_id, data[4:])
|
||||||
|
return _send(chat_id, "формат: %s" % ("кратко" if data[4:] == "short" else "полно"), reply_markup=KB_MAIN)
|
||||||
|
if data == "post:now":
|
||||||
|
return cmd_test(chat_id, "")
|
||||||
|
|
||||||
|
|
||||||
def run_forever():
|
def run_forever():
|
||||||
if not TOKEN:
|
if not TOKEN:
|
||||||
raise SystemExit("нужен OMNI_BOT_TOKEN")
|
raise SystemExit("нужен OMNI_BOT_TOKEN")
|
||||||
offset = 0
|
offset = 0
|
||||||
|
import threading
|
||||||
|
threading.Thread(target=pusher, daemon=True).start()
|
||||||
print("bot up", flush=True)
|
print("bot up", flush=True)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
r = _api("getUpdates", offset=offset, timeout=30)
|
r = _api("getUpdates", offset=offset, timeout=25, allowed_updates='["message","callback_query"]')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("poll fail", e, flush=True)
|
print("poll fail", e, flush=True)
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
continue
|
continue
|
||||||
for up in r.get("result", []):
|
for up in r.get("result", []):
|
||||||
offset = up["update_id"] + 1
|
offset = up["update_id"] + 1
|
||||||
handle(up)
|
if up.get("callback_query"):
|
||||||
|
try:
|
||||||
|
handle_cb(up["callback_query"])
|
||||||
|
except Exception as e:
|
||||||
|
print("cb fail", e, flush=True)
|
||||||
|
else:
|
||||||
|
handle(up)
|
||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -179,6 +179,13 @@ SOURCES_BASE = [
|
|||||||
S("tg_openai", "forum", "html", "https://t.me/s/openai", "tg_web", ("telegram",)),
|
S("tg_openai", "forum", "html", "https://t.me/s/openai", "tg_web", ("telegram",)),
|
||||||
S("tg_huggingface", "forum", "html", "https://t.me/s/huggingface", "tg_web", ("telegram",)),
|
S("tg_huggingface", "forum", "html", "https://t.me/s/huggingface", "tg_web", ("telegram",)),
|
||||||
]
|
]
|
||||||
|
# ---------- свои каналы (проверены 26.10.26) ----------
|
||||||
|
EXTRA4 = [
|
||||||
|
{"name": "tg_alstack", "category": "community", "kind": "html",
|
||||||
|
"url": "https://t.me/s/AlStack", "adapter": "tg_web", "tags": ["telegram", "own"], "limit": 20},
|
||||||
|
{"name": "tg_abuz_ai_soft_posts", "category": "abuse", "kind": "html",
|
||||||
|
"url": "https://t.me/s/Abuz_ai_soft", "adapter": "tg_web", "tags": ["telegram", "abuse", "own"], "limit": 20},
|
||||||
|
]
|
||||||
|
|
||||||
def _dedup(rows):
|
def _dedup(rows):
|
||||||
seen, out = set(), []
|
seen, out = set(), []
|
||||||
@@ -190,7 +197,7 @@ def _dedup(rows):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
SOURCES = _dedup(SOURCES_BASE + [Source(**d) for d in EXTRA] + [Source(**d) for d in EXTRA2] + [Source(**d) for d in EXTRA3] + [Source(**d) for d in ABUSE_LIVE])
|
SOURCES = _dedup(SOURCES_BASE + [Source(**d) for d in EXTRA] + [Source(**d) for d in EXTRA2] + [Source(**d) for d in EXTRA3] + [Source(**d) for d in ABUSE_LIVE] + [Source(**d) for d in EXTRA4])
|
||||||
|
|
||||||
BY_NAME = {s.name: s for s in SOURCES}
|
BY_NAME = {s.name: s for s in SOURCES}
|
||||||
CATEGORIES = sorted({s.category for s in SOURCES})
|
CATEGORIES = sorted({s.category for s in SOURCES})
|
||||||
|
|||||||
Reference in New Issue
Block a user