Files
press/ai_core/telegram_publisher.py
T
apuc c6048aeea0 feat: AI Press Secretary - content generation and publishing platform
Features:
- AI-powered post generation from brand profiles
- Image generation with templates
- Content planning
- Telegram publishing
- Social profile analyzer
- Community organization support
- Markdown to HTML conversion for Telegram
- Inline AI chat for post editing

Deploy:
- Add deployment scripts for Linux servers
- Add PowerShell deploy script for Windows
- Add Nginx and SSL setup scripts
- Add backup and restore scripts
2026-08-27 15:24:15 +03:00

187 lines
6.8 KiB
Python

"""Публикация постов в Telegram-каналы и чаты через Bot API.
Использует TOKEN из config — тот же токен, что и у бота.
Бот должен быть админом в канале/чате, куда публикуем."""
import re
import requests
from config import TOKEN, get_proxies
TELEGRAM_API = f"https://api.telegram.org/bot{TOKEN}"
def _md_to_html(text: str) -> str:
"""Конвертирует простой Markdown в HTML для Telegram.
Поддерживает: **жирный**, *курсив*, __подчёркнутый~~зачёркнутый~~,
`моноширинный`, ```блок кода```, заголовки ### в <b>,
нумерованные/маркированные списки.
Telegram HTML: <b>, <i>, <u>, <s>, <code>, <pre>, <a>.
"""
# Сначала экранируем HTML-символы в тексте (но не наши теги)
text = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
# Блоки кода ```...```
text = re.sub(r'```(\w*)\n(.*?)```', r'<pre>\2</pre>', text, flags=re.DOTALL)
text = re.sub(r'```(.*?)```', r'<pre>\1</pre>', text, flags=re.DOTALL)
# Inline code `...`
text = re.sub(r'`([^`]+)`', r'<code>\1</code>', text)
# Заголовки ### **Текст** → <b>Текст</b>
text = re.sub(r'^#{1,6}\s+\*\*(.+?)\*\*\s*$', r'<b>\1</b>', text, flags=re.MULTILINE)
text = re.sub(r'^#{1,6}\s+(.+?)\s*$', r'<b>\1</b>', text, flags=re.MULTILINE)
# Жирный **...**
text = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', text)
# Курсив *...* (но не внутри <b> тегов)
text = re.sub(r'(?<!\*)\*([^*\n]+?)\*(?!\*)', r'<i>\1</i>', text)
# Подчёркнутый __...__
text = re.sub(r'__(.+?)__', r'<u>\1</u>', text)
# Зачёркнутый ~~...~~
text = re.sub(r'~~(.+?)~~', r'<s>\1</s>', text)
return text
def _api_call(method, params):
"""Вызывает Telegram Bot API метод. Возвращает (ok, result_or_error)."""
try:
resp = requests.post(
f"{TELEGRAM_API}/{method}",
data=params,
proxies=get_proxies(),
timeout=30,
)
data = resp.json()
if data.get("ok"):
return True, data.get("result", {})
return False, data.get("description", "Неизвестная ошибка")
except Exception as e:
return False, str(e)
def resolve_chat(username_or_id):
"""Разрешает @username или числовой ID в информацию о чате.
Возвращает dict: {chat_id, title, username, type} или {error}."""
# Нормализуем
identifier = username_or_id.strip()
if identifier.startswith("@"):
identifier = identifier # оставляем как есть для API
elif identifier.lstrip("-").isdigit():
identifier = int(identifier)
ok, result = _api_call("getChat", {"chat_id": identifier})
if not ok:
return {"error": result}
chat_id = result.get("id")
chat_type = result.get("type", "unknown")
title = result.get("title") or result.get("first_name", "") or result.get("username", "")
username = result.get("username", "")
return {
"chat_id": chat_id,
"title": title,
"username": username,
"type": chat_type,
}
def check_bot_is_admin(chat_id):
"""Проверяет, является ли бот админом в чате/канале.
Возвращает (is_admin, error_message)."""
ok, result = _api_call("getChatMember", {
"chat_id": chat_id,
"user_id": _get_bot_id(),
})
if not ok:
return False, f"Не удалось проверить статус: {result}"
status = result.get("status", "")
# administrator — для чатов, owner — для каналов
if status in ("administrator", "creator", "owner"):
return True, None
return False, f"Бот не админ в этом чате (статус: {status}). Добавьте бота в админы."
def _get_bot_id():
"""Возвращает ID бота (кешируется)."""
global _cached_bot_id
if _cached_bot_id is not None:
return _cached_bot_id
ok, result = _api_call("getMe", {})
if ok:
_cached_bot_id = result.get("id")
return _cached_bot_id
return None
_cached_bot_id = None
def publish_post(chat_id, text, image_bytes=None):
"""Публикует пост в канал/чат.
- text: текст поста (Markdown конвертируется в HTML)
- image_bytes: опционально, байты картинки
Возвращает (ok, message_or_message_id)."""
text = _md_to_html(text)
if image_bytes:
ok, result = _api_call_send_photo(chat_id, image_bytes, text)
else:
ok, result = _api_call("sendMessage", {
"chat_id": chat_id,
"text": text,
"parse_mode": "HTML",
})
if ok:
msg_id = result.get("message_id")
return True, f"Опубликовано (message_id={msg_id})"
return False, result
def _api_call_send_photo(chat_id, image_bytes, caption):
"""Отправляет фото с подписью.
Если текст длиннее 1024 символов — фото без подписи + текст отдельным сообщением."""
long_text = len(caption) > 1024
try:
files = {"photo": ("image.png", image_bytes, "image/png")}
data = {
"chat_id": chat_id,
"caption": "" if long_text else caption,
"parse_mode": "HTML" if not long_text else None,
}
# Убираем None из data
data = {k: v for k, v in data.items() if v is not None}
resp = requests.post(
f"{TELEGRAM_API}/sendPhoto",
data=data,
files=files,
proxies=get_proxies(),
timeout=60,
)
data = resp.json()
if not data.get("ok"):
return False, data.get("description", "Ошибка отправки фото")
# Если текст длинный — отправляем его отдельным сообщением
if long_text:
ok2, result2 = _api_call("sendMessage", {
"chat_id": chat_id,
"text": caption,
"parse_mode": "HTML",
})
if not ok2:
# Фото ушло, но текст не отправился — всё равно считаем успехом,
# но возвращаем предупреждение
return True, data.get("result", {})
return True, data.get("result", {})
except Exception as e:
return False, str(e)