init commit
This commit is contained in:
1
app/bot/__init__.py
Normal file
1
app/bot/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
app/bot/handlers/__init__.py
Normal file
1
app/bot/handlers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
123
app/bot/handlers/add_group.py
Normal file
123
app/bot/handlers/add_group.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from telegram import Update
|
||||
from telegram.constants import ParseMode, ChatType
|
||||
from telegram.ext import ContextTypes
|
||||
from telegram.error import BadRequest
|
||||
|
||||
from app.bot.messages import (
|
||||
ASK_ADD_GROUP,
|
||||
GROUP_BOUND,
|
||||
NEED_ADD_FIRST,
|
||||
NO_RIGHTS_CHANNEL,
|
||||
NO_RIGHTS_GROUP,
|
||||
)
|
||||
from app.db.session import get_session
|
||||
from app.db.models import User, Chat
|
||||
from .utils import parse_chat_id, verify_and_fetch_chat
|
||||
|
||||
STATE_KEY = "await_chat_id"
|
||||
|
||||
|
||||
def _get_forwarded_chat_id(message) -> int | None:
|
||||
"""
|
||||
Универсально достаём chat_id источника пересылки.
|
||||
Поддерживает старые поля (forward_from_chat) и новые (forward_origin.chat).
|
||||
"""
|
||||
if not message:
|
||||
return None
|
||||
|
||||
# Старое поле (иногда ещё присутствует)
|
||||
fwd_chat = getattr(message, "forward_from_chat", None)
|
||||
if fwd_chat:
|
||||
return fwd_chat.id
|
||||
|
||||
# Новая схема Bot API: forward_origin с типами MessageOrigin*
|
||||
origin = getattr(message, "forward_origin", None)
|
||||
if origin:
|
||||
chat = getattr(origin, "chat", None) # у MessageOriginChat/Channel есть .chat
|
||||
if chat:
|
||||
return chat.id
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def add_group_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
# ensure user exists
|
||||
with get_session() as s:
|
||||
u = s.query(User).filter_by(tg_id=update.effective_user.id).first()
|
||||
if not u:
|
||||
u = User(tg_id=update.effective_user.id, name=update.effective_user.full_name)
|
||||
s.add(u)
|
||||
s.commit()
|
||||
ctx.user_data[STATE_KEY] = True
|
||||
await update.effective_message.reply_text(ASK_ADD_GROUP, parse_mode=ParseMode.MARKDOWN)
|
||||
|
||||
|
||||
async def add_group_capture(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
if update.effective_chat.type != ChatType.PRIVATE:
|
||||
return
|
||||
if not ctx.user_data.get(STATE_KEY):
|
||||
return
|
||||
|
||||
msg = update.effective_message
|
||||
raw = None
|
||||
|
||||
# 1) Пересланное сообщение из чата/канала
|
||||
fwd_cid = _get_forwarded_chat_id(msg)
|
||||
if fwd_cid:
|
||||
raw = fwd_cid
|
||||
else:
|
||||
# 2) Ввод в виде текста или подписи (caption)
|
||||
txt = (getattr(msg, "text", None) or getattr(msg, "caption", None) or "").strip()
|
||||
if not txt:
|
||||
await msg.reply_text("Вставьте chat_id или перешлите сообщение из группы/канала.")
|
||||
return
|
||||
|
||||
if txt.startswith("@") or "t.me/" in txt:
|
||||
raw = txt
|
||||
else:
|
||||
cid = parse_chat_id(txt)
|
||||
if cid is None:
|
||||
await msg.reply_text("Не удалось распознать chat_id. Пример: -1001234567890 или @username.")
|
||||
return
|
||||
raw = cid
|
||||
|
||||
# Проверяем, что бот в чате и что у него есть право постить (если нужно)
|
||||
try:
|
||||
chat, member, can_post = await verify_and_fetch_chat(ctx, raw)
|
||||
except BadRequest:
|
||||
await msg.reply_text("Такого чата не существует или у меня нет доступа. Проверьте chat_id/username.")
|
||||
return
|
||||
|
||||
if member is None:
|
||||
await msg.reply_text(NEED_ADD_FIRST.format(title_or_id=(chat.title or chat.id)))
|
||||
return
|
||||
|
||||
# Сохраняем привязку
|
||||
with get_session() as s:
|
||||
me = s.query(User).filter_by(tg_id=update.effective_user.id).first()
|
||||
row = s.query(Chat).filter_by(chat_id=chat.id).first()
|
||||
if not row:
|
||||
row = Chat(
|
||||
chat_id=chat.id,
|
||||
type=chat.type,
|
||||
title=chat.title,
|
||||
owner_user_id=me.id,
|
||||
can_post=can_post,
|
||||
)
|
||||
s.add(row)
|
||||
else:
|
||||
row.title = chat.title
|
||||
row.type = chat.type
|
||||
if row.owner_user_id is None:
|
||||
row.owner_user_id = me.id
|
||||
row.can_post = can_post
|
||||
s.commit()
|
||||
|
||||
ctx.user_data.pop(STATE_KEY, None)
|
||||
|
||||
rights = (
|
||||
"✅ У меня есть право публиковать."
|
||||
if can_post
|
||||
else (NO_RIGHTS_CHANNEL if chat.type == ChatType.CHANNEL else NO_RIGHTS_GROUP)
|
||||
)
|
||||
await msg.reply_text(GROUP_BOUND.format(title_or_id=(chat.title or chat.id), rights=rights))
|
||||
118
app/bot/handlers/callbacks.py
Normal file
118
app/bot/handlers/callbacks.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from telegram import Update, InputMediaPhoto, InputMediaVideo, InputMediaAnimation
|
||||
from telegram.constants import ParseMode, ChatType
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
from app.db.session import get_session
|
||||
from app.db.models import Draft, Chat, Delivery
|
||||
from app.bot.keyboards.common import kb_choose_chat
|
||||
from app.bot.messages import READY_SELECT_CHAT, SENT_OK, SEND_ERR, NEED_MEDIA_BEFORE_NEXT, CANCELLED
|
||||
from .drafts import STATE_DRAFT, KEY_DRAFT_ID, STATE_AWAIT_TEXT, STATE_CONFIRM
|
||||
from .add_group import STATE_KEY # чтобы не конфликтовать по user_data ключам
|
||||
|
||||
|
||||
async def on_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
q = update.callback_query
|
||||
await q.answer()
|
||||
data = q.data
|
||||
|
||||
# --- Переход с медиа на текст ---
|
||||
if data.startswith("draft_next_text:"):
|
||||
draft_id = int(data.split(":")[1])
|
||||
with get_session() as s:
|
||||
d = s.get(Draft, draft_id)
|
||||
if not d:
|
||||
await q.edit_message_text("Черновик не найден.")
|
||||
return
|
||||
if len(d.media) == 0:
|
||||
await q.edit_message_text(NEED_MEDIA_BEFORE_NEXT)
|
||||
return
|
||||
ctx.user_data[KEY_DRAFT_ID] = draft_id
|
||||
ctx.user_data[STATE_DRAFT] = STATE_AWAIT_TEXT
|
||||
await q.edit_message_text("Шаг 2/3 — текст.\nОтправьте текст поста.")
|
||||
|
||||
# --- Подтверждение (Отправить) -> выбор чата ---
|
||||
elif data.startswith("draft_confirm_send:"):
|
||||
draft_id = int(data.split(":")[1])
|
||||
with get_session() as s:
|
||||
d = s.get(Draft, draft_id)
|
||||
if not d:
|
||||
await q.edit_message_text("Черновик не найден.")
|
||||
return
|
||||
d.status = "ready"
|
||||
s.commit()
|
||||
# Разрешённые чаты владельца черновика
|
||||
chats = s.query(Chat).filter_by(owner_user_id=d.user_id, can_post=True).all()
|
||||
buttons = [(c.title or str(c.chat_id), c.chat_id) for c in chats]
|
||||
kb = kb_choose_chat(draft_id, buttons)
|
||||
if not kb:
|
||||
await q.edit_message_text("Нет чатов с правом публикации. Добавьте/обновите через /add_group.")
|
||||
return
|
||||
ctx.user_data[STATE_DRAFT] = STATE_CONFIRM
|
||||
await q.edit_message_text(READY_SELECT_CHAT, reply_markup=kb)
|
||||
|
||||
# --- Отмена черновика ---
|
||||
elif data.startswith("draft_cancel:"):
|
||||
draft_id = int(data.split(":")[1])
|
||||
with get_session() as s:
|
||||
d = s.get(Draft, draft_id)
|
||||
if d:
|
||||
d.status = "cancelled"
|
||||
s.commit()
|
||||
ctx.user_data.pop(KEY_DRAFT_ID, None)
|
||||
ctx.user_data.pop(STATE_DRAFT, None)
|
||||
await q.edit_message_text(CANCELLED)
|
||||
|
||||
# --- Отправка в выбранный чат ---
|
||||
elif data.startswith("send:"):
|
||||
_, draft_id, chat_id = data.split(":")
|
||||
draft_id = int(draft_id); chat_id = int(chat_id)
|
||||
|
||||
with get_session() as s:
|
||||
d = s.get(Draft, draft_id)
|
||||
if not d:
|
||||
await q.edit_message_text("Черновик не найден.")
|
||||
return
|
||||
|
||||
media = list(sorted(d.media, key=lambda m: m.order))
|
||||
sent_ids = []
|
||||
try:
|
||||
if media:
|
||||
if len(media) > 1:
|
||||
im = []
|
||||
for i, m in enumerate(media):
|
||||
cap = d.text if i == 0 else None
|
||||
if m.kind == "photo":
|
||||
im.append(InputMediaPhoto(media=m.file_id, caption=cap, parse_mode=ParseMode.HTML))
|
||||
elif m.kind == "video":
|
||||
im.append(InputMediaVideo(media=m.file_id, caption=cap, parse_mode=ParseMode.HTML))
|
||||
elif m.kind == "animation":
|
||||
im.append(InputMediaAnimation(media=m.file_id, caption=cap, parse_mode=ParseMode.HTML))
|
||||
msgs = await ctx.bot.send_media_group(chat_id=chat_id, media=im)
|
||||
sent_ids = [str(m.message_id) for m in msgs]
|
||||
else:
|
||||
m = media[0]
|
||||
if m.kind == "photo":
|
||||
msg = await ctx.bot.send_photo(chat_id=chat_id, photo=m.file_id, caption=d.text, parse_mode=ParseMode.HTML)
|
||||
elif m.kind == "video":
|
||||
msg = await ctx.bot.send_video(chat_id=chat_id, video=m.file_id, caption=d.text, parse_mode=ParseMode.HTML)
|
||||
else:
|
||||
msg = await ctx.bot.send_animation(chat_id=chat_id, animation=m.file_id, caption=d.text, parse_mode=ParseMode.HTML)
|
||||
sent_ids = [str(msg.message_id)]
|
||||
else:
|
||||
msg = await ctx.bot.send_message(chat_id=chat_id, text=d.text or "(пусто)", parse_mode=ParseMode.HTML)
|
||||
sent_ids = [str(msg.message_id)]
|
||||
|
||||
deliv = Delivery(draft_id=d.id, chat_id=chat_id, status="sent", message_ids=",".join(sent_ids))
|
||||
s.add(deliv)
|
||||
d.status = "sent"
|
||||
s.commit()
|
||||
|
||||
# Сбрасываем пользовательское состояние редактора
|
||||
ctx.user_data.pop(KEY_DRAFT_ID, None)
|
||||
ctx.user_data.pop(STATE_DRAFT, None)
|
||||
|
||||
await q.edit_message_text(SENT_OK)
|
||||
except Exception as e:
|
||||
deliv = Delivery(draft_id=d.id, chat_id=chat_id, status="failed", error=str(e))
|
||||
s.add(deliv); s.commit()
|
||||
await q.edit_message_text(SEND_ERR.format(e=e))
|
||||
79
app/bot/handlers/drafts.py
Normal file
79
app/bot/handlers/drafts.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from datetime import datetime
|
||||
from telegram import Update
|
||||
from telegram.constants import ChatType, ParseMode
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
from app.db.session import get_session
|
||||
from app.db.models import User, Draft
|
||||
from app.bot.messages import (
|
||||
ASK_MEDIA, ASK_TEXT, TEXT_ADDED, CONFIRM, ALREADY_AT_TEXT, ALREADY_READY, NEED_START_NEW
|
||||
)
|
||||
from app.bot.keyboards.common import kb_next_text, kb_confirm
|
||||
from .add_group import add_group_capture, STATE_KEY # /add_group ожидание
|
||||
|
||||
# Состояния пошагового редактора
|
||||
STATE_DRAFT = "draft_state"
|
||||
KEY_DRAFT_ID = "draft_id"
|
||||
STATE_AWAIT_MEDIA = "await_media"
|
||||
STATE_AWAIT_TEXT = "await_text"
|
||||
STATE_CONFIRM = "confirm"
|
||||
|
||||
|
||||
def _start_new_draft(tg_id: int) -> Draft:
|
||||
with get_session() as s:
|
||||
u = s.query(User).filter_by(tg_id=tg_id).first()
|
||||
if not u:
|
||||
u = User(tg_id=tg_id, name="")
|
||||
s.add(u); s.commit(); s.refresh(u)
|
||||
# Закрываем предыдущие "editing" как cancelled (по желанию)
|
||||
s.query(Draft).filter(Draft.user_id == u.id, Draft.status == "editing").update({"status": "cancelled"})
|
||||
d = Draft(user_id=u.id, status="editing")
|
||||
s.add(d); s.commit(); s.refresh(d)
|
||||
return d
|
||||
|
||||
|
||||
def _get_draft(draft_id: int) -> Draft | None:
|
||||
with get_session() as s:
|
||||
return s.get(Draft, draft_id)
|
||||
|
||||
|
||||
async def new_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
d = _start_new_draft(update.effective_user.id)
|
||||
ctx.user_data[KEY_DRAFT_ID] = d.id
|
||||
ctx.user_data[STATE_DRAFT] = STATE_AWAIT_MEDIA
|
||||
await update.effective_message.reply_text(ASK_MEDIA, reply_markup=kb_next_text(d.id))
|
||||
|
||||
|
||||
async def on_text(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
# Если ждём chat_id для /add_group, отдаём управление туда
|
||||
if ctx.user_data.get(STATE_KEY):
|
||||
return await add_group_capture(update, ctx)
|
||||
|
||||
if update.effective_chat.type != ChatType.PRIVATE:
|
||||
return
|
||||
|
||||
draft_id = ctx.user_data.get(KEY_DRAFT_ID)
|
||||
state = ctx.user_data.get(STATE_DRAFT)
|
||||
|
||||
if not draft_id or not state:
|
||||
await update.effective_message.reply_text(NEED_START_NEW)
|
||||
return
|
||||
|
||||
if state == STATE_AWAIT_MEDIA:
|
||||
# Сначала медиа
|
||||
await update.effective_message.reply_text("Сначала пришлите медиа и нажмите «Дальше — текст».")
|
||||
return
|
||||
|
||||
if state == STATE_CONFIRM:
|
||||
await update.effective_message.reply_text(ALREADY_READY)
|
||||
return
|
||||
|
||||
if state == STATE_AWAIT_TEXT:
|
||||
with get_session() as s:
|
||||
d = s.get(Draft, draft_id)
|
||||
d.text = update.effective_message.text_html_urled
|
||||
d.updated_at = datetime.utcnow()
|
||||
s.commit()
|
||||
ctx.user_data[STATE_DRAFT] = STATE_CONFIRM
|
||||
await update.effective_message.reply_text(TEXT_ADDED, parse_mode=ParseMode.HTML)
|
||||
await update.effective_message.reply_text(CONFIRM, reply_markup=kb_confirm(draft_id))
|
||||
51
app/bot/handlers/join_info.py
Normal file
51
app/bot/handlers/join_info.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from telegram import Update
|
||||
from telegram.constants import ChatMemberStatus, ChatType, ParseMode
|
||||
from telegram.ext import ContextTypes
|
||||
from app.bot.messages import JOIN_INFO_GROUP, JOIN_INFO_CHANNEL
|
||||
|
||||
async def on_my_chat_member(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
"""
|
||||
Сообщаем инструкцию и chat_id, когда бота добавили в группу/канал
|
||||
или повысили до администратора.
|
||||
"""
|
||||
mcm = update.my_chat_member
|
||||
if not mcm:
|
||||
return
|
||||
|
||||
chat = mcm.chat
|
||||
new_status = mcm.new_chat_member.status
|
||||
if new_status not in (ChatMemberStatus.MEMBER, ChatMemberStatus.ADMINISTRATOR):
|
||||
return
|
||||
|
||||
title = chat.title or str(chat.id)
|
||||
chat_id = chat.id
|
||||
|
||||
# Текст подсказки
|
||||
if chat.type in (ChatType.GROUP, ChatType.SUPERGROUP):
|
||||
text = JOIN_INFO_GROUP.format(title=title, chat_id=chat_id)
|
||||
# Пытаемся написать прямо в группу
|
||||
try:
|
||||
await ctx.bot.send_message(chat_id=chat_id, text=text, parse_mode=ParseMode.MARKDOWN)
|
||||
except Exception:
|
||||
# как запасной вариант — в ЛС пользователю, который добавил
|
||||
if update.effective_user:
|
||||
try:
|
||||
await ctx.bot.send_message(update.effective_user.id, text=text, parse_mode=ParseMode.MARKDOWN)
|
||||
except Exception:
|
||||
pass
|
||||
elif chat.type == ChatType.CHANNEL:
|
||||
text = JOIN_INFO_CHANNEL.format(title=title, chat_id=chat_id)
|
||||
# В канале можем не иметь права постинга — пробуем ЛС добавившему
|
||||
sent = False
|
||||
try:
|
||||
# если права даны, сообщим прямо в канал
|
||||
await ctx.bot.send_message(chat_id=chat_id, text=text, parse_mode=ParseMode.MARKDOWN)
|
||||
sent = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not sent and update.effective_user:
|
||||
try:
|
||||
await ctx.bot.send_message(update.effective_user.id, text=text, parse_mode=ParseMode.MARKDOWN)
|
||||
except Exception:
|
||||
pass
|
||||
51
app/bot/handlers/media.py
Normal file
51
app/bot/handlers/media.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from datetime import datetime
|
||||
from telegram import Update
|
||||
from telegram.constants import ChatType
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
from app.db.session import get_session
|
||||
from app.db.models import Draft, DraftMedia
|
||||
from app.bot.messages import MEDIA_ADDED, ALREADY_AT_TEXT, NEED_START_NEW
|
||||
from .drafts import KEY_DRAFT_ID, STATE_DRAFT, STATE_AWAIT_MEDIA, STATE_AWAIT_TEXT, STATE_CONFIRM
|
||||
from .add_group import add_group_capture, STATE_KEY # перехват для /add_group
|
||||
|
||||
|
||||
async def on_media(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
if update.effective_chat.type != ChatType.PRIVATE:
|
||||
return
|
||||
|
||||
# Если пользователь сейчас привязывает чат — используем пересланное медиа для извлечения chat_id
|
||||
if ctx.user_data.get(STATE_KEY):
|
||||
return await add_group_capture(update, ctx)
|
||||
|
||||
draft_id = ctx.user_data.get(KEY_DRAFT_ID)
|
||||
state = ctx.user_data.get(STATE_DRAFT)
|
||||
|
||||
if not draft_id or not state:
|
||||
await update.effective_message.reply_text(NEED_START_NEW)
|
||||
return
|
||||
|
||||
if state != STATE_AWAIT_MEDIA:
|
||||
# Уже перешли к тексту/подтверждению — блокируем добавление медиа
|
||||
await update.effective_message.reply_text(ALREADY_AT_TEXT if state == STATE_AWAIT_TEXT else "Редактор на шаге подтверждения.")
|
||||
return
|
||||
|
||||
kind = None
|
||||
file_id = None
|
||||
if update.message.photo:
|
||||
kind = "photo"; file_id = update.message.photo[-1].file_id
|
||||
elif update.message.video:
|
||||
kind = "video"; file_id = update.message.video.file_id
|
||||
elif update.message.animation:
|
||||
kind = "animation"; file_id = update.message.animation.file_id
|
||||
else:
|
||||
return
|
||||
|
||||
with get_session() as s:
|
||||
d = s.get(Draft, draft_id)
|
||||
order = len(d.media)
|
||||
m = DraftMedia(draft_id=d.id, kind=kind, file_id=file_id, order=order)
|
||||
s.add(m); d.updated_at = datetime.utcnow()
|
||||
s.commit()
|
||||
|
||||
await update.effective_message.reply_text(MEDIA_ADDED.format(kind=kind))
|
||||
36
app/bot/handlers/start.py
Normal file
36
app/bot/handlers/start.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from telegram import Update
|
||||
from telegram.constants import ChatType, ParseMode
|
||||
from telegram.ext import ContextTypes
|
||||
from app.bot.messages import START, HELP, NO_CHATS
|
||||
from app.db.session import get_session
|
||||
from app.db.models import User, Chat
|
||||
|
||||
async def start(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
# register user
|
||||
with get_session() as s:
|
||||
u = s.query(User).filter_by(tg_id=update.effective_user.id).first()
|
||||
if not u:
|
||||
u = User(tg_id=update.effective_user.id, name=update.effective_user.full_name)
|
||||
s.add(u); s.commit()
|
||||
await update.effective_message.reply_text(START)
|
||||
|
||||
async def help_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
await update.effective_message.reply_text(HELP)
|
||||
|
||||
async def groups_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||
with get_session() as s:
|
||||
u = s.query(User).filter_by(tg_id=update.effective_user.id).first()
|
||||
if not u:
|
||||
await update.effective_message.reply_text(NO_CHATS)
|
||||
return
|
||||
chats = s.query(Chat).filter_by(owner_user_id=u.id).order_by(Chat.created_at.desc()).all()
|
||||
|
||||
if not chats:
|
||||
await update.effective_message.reply_text(NO_CHATS)
|
||||
return
|
||||
lines = []
|
||||
for c in chats:
|
||||
icon = "📣" if c.type == ChatType.CHANNEL else "👥"
|
||||
post = "✅ могу публиковать" if c.can_post else "⚠️ нет прав публикации"
|
||||
lines.append(f"{icon} {c.title or c.chat_id} — {post}")
|
||||
await update.effective_message.reply_text("\n".join(lines), parse_mode=ParseMode.HTML)
|
||||
43
app/bot/handlers/utils.py
Normal file
43
app/bot/handlers/utils.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import re
|
||||
from typing import Tuple, Optional, Any
|
||||
from telegram.constants import ChatType
|
||||
from telegram.error import Forbidden, BadRequest
|
||||
|
||||
async def verify_and_fetch_chat(ctx, raw: str | int):
|
||||
"""Return (chat, member, can_post) for the bot in that chat."""
|
||||
bot = ctx.bot
|
||||
|
||||
# Normalize t.me links to @username
|
||||
if isinstance(raw, str) and "t.me/" in raw:
|
||||
raw = raw.strip().split("t.me/")[-1]
|
||||
raw = raw.strip().lstrip("/")
|
||||
if raw and not raw.startswith("@"):
|
||||
raw = "@" + raw
|
||||
|
||||
chat = await bot.get_chat(raw)
|
||||
try:
|
||||
member = await bot.get_chat_member(chat.id, bot.id)
|
||||
except Forbidden:
|
||||
return chat, None, False
|
||||
except BadRequest as e:
|
||||
raise e
|
||||
|
||||
can_post = False
|
||||
if chat.type == ChatType.CHANNEL:
|
||||
status = getattr(member, "status", "")
|
||||
if status in ("administrator", "creator"):
|
||||
flag = getattr(member, "can_post_messages", None)
|
||||
can_post = True if (flag is None or flag is True) else False
|
||||
elif chat.type in (ChatType.SUPERGROUP, ChatType.GROUP):
|
||||
status = getattr(member, "status", "")
|
||||
can_post = status in ("member", "administrator", "creator")
|
||||
return chat, member, can_post
|
||||
|
||||
def parse_chat_id(text: str) -> int | None:
|
||||
m = re.search(r'(-?\d{5,})', text or "")
|
||||
if m:
|
||||
try:
|
||||
return int(m.group(1))
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
1
app/bot/keyboards/__init__.py
Normal file
1
app/bot/keyboards/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
21
app/bot/keyboards/common.py
Normal file
21
app/bot/keyboards/common.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
|
||||
def kb_next_text(draft_id: int):
|
||||
return InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton("Дальше — текст", callback_data=f"draft_next_text:{draft_id}")]]
|
||||
)
|
||||
|
||||
def kb_confirm(draft_id: int):
|
||||
return InlineKeyboardMarkup(
|
||||
[
|
||||
[
|
||||
InlineKeyboardButton("Отправить", callback_data=f"draft_confirm_send:{draft_id}"),
|
||||
InlineKeyboardButton("Отменить", callback_data=f"draft_cancel:{draft_id}"),
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
def kb_choose_chat(draft_id: int, chats: list[tuple[str, int]]):
|
||||
# chats: list of (title, chat_id)
|
||||
rows = [[InlineKeyboardButton(title, callback_data=f"send:{draft_id}:{chat_id}")] for title, chat_id in chats]
|
||||
return InlineKeyboardMarkup(rows) if rows else None
|
||||
72
app/bot/messages.py
Normal file
72
app/bot/messages.py
Normal file
@@ -0,0 +1,72 @@
|
||||
START = (
|
||||
"Привет! Я помогу отправлять сообщения в ваши группы и каналы.\n\n"
|
||||
"1) Добавьте меня в группу/канал (в канале — дайте право публиковать).\n"
|
||||
"2) В ЛС боту нажмите /add_group и вставьте chat_id или перешлите сюда сообщение из этого чата.\n"
|
||||
"3) Создайте черновик /new и отправьте в выбранный чат.\n\n"
|
||||
"Команды:\n"
|
||||
"/add_group — привязать группу/канал вручную\n"
|
||||
"/groups — список моих чатов\n"
|
||||
"/new — создать черновик поста\n"
|
||||
"/help — справка"
|
||||
)
|
||||
|
||||
HELP = (
|
||||
"1) В Telegram добавьте бота в группу/канал (для каналов — админ с правом «Публиковать сообщения»).\n"
|
||||
"2) В ЛС — /add_group: вставьте chat_id (например, -100123...) или перешлите сюда любое сообщение из чата.\n"
|
||||
"3) Создайте черновик /new и отправьте его в выбранный чат."
|
||||
)
|
||||
|
||||
ASK_ADD_GROUP = (
|
||||
"Отправьте *chat_id* группы/канала (например, `-1001234567890`) "
|
||||
"или *перешлите сюда* любое сообщение из нужной группы/канала."
|
||||
)
|
||||
|
||||
NO_CHATS = "Пока ни одного чата не привязано. Нажмите /add_group для привязки."
|
||||
|
||||
# Пошаговый редактор
|
||||
ASK_MEDIA = (
|
||||
"Шаг 1/3 — медиа.\nПришлите фото/видео/гиф. Можно несколько (альбом).\n"
|
||||
"Когда закончите — нажмите «Дальше — текст»."
|
||||
)
|
||||
ASK_TEXT = (
|
||||
"Шаг 2/3 — текст.\nОтправьте текст поста. Он станет подписью к медиа (или отдельным сообщением, если медиа нет)."
|
||||
)
|
||||
CONFIRM = (
|
||||
"Шаг 3/3 — подтверждение.\nПроверьте пост и нажмите «Отправить» или «Отменить»."
|
||||
)
|
||||
|
||||
TEXT_ADDED = "Текст добавлен в черновик."
|
||||
MEDIA_ADDED = "Медиа добавлено ({kind})."
|
||||
NEED_MEDIA_BEFORE_NEXT = "Нужно добавить минимум одно медиа перед переходом к тексту."
|
||||
ALREADY_AT_TEXT = "Вы уже перешли к вводу текста. Пришлите текст или нажмите «Отменить»."
|
||||
ALREADY_READY = "Пост готов к отправке — нажмите «Отправить» или «Отменить»."
|
||||
NEED_START_NEW = "Сначала начните новый пост: /new"
|
||||
CANCELLED = "Черновик отменён."
|
||||
|
||||
READY_SELECT_CHAT = "Куда отправить?"
|
||||
SENT_OK = "✅ Отправлено."
|
||||
SEND_ERR = "❌ Ошибка отправки: {e}"
|
||||
|
||||
GROUP_BOUND = "Чат «{title_or_id}» привязан.\n{rights}"
|
||||
NEED_ADD_FIRST = "Я не добавлен в «{title_or_id}». Сначала добавьте бота в этот чат и повторите /add_group."
|
||||
NO_RIGHTS_CHANNEL = "⚠️ Я в канале не администратор. Дайте боту право «Публиковать сообщения» и повторите /add_group."
|
||||
NO_RIGHTS_GROUP = "⚠️ Похоже, я не могу публиковать. Проверьте права чата."
|
||||
|
||||
# --- Новое: инструкции при добавлении бота в чат/канал ---
|
||||
JOIN_INFO_GROUP = (
|
||||
"Спасибо, что добавили меня в группу «{title}»!\n"
|
||||
"ID группы: `{chat_id}`\n\n"
|
||||
"Чтобы привязать эту группу к своему аккаунту:\n"
|
||||
"1) Откройте ЛС со мной\n"
|
||||
"2) Выполните команду /add_group\n"
|
||||
"3) Вставьте ID выше *или* просто перешлите сюда любое сообщение из этой группы."
|
||||
)
|
||||
|
||||
JOIN_INFO_CHANNEL = (
|
||||
"Спасибо, что добавили меня в канал «{title}»!\n"
|
||||
"ID канала: `{chat_id}`\n\n"
|
||||
"Чтобы привязать канал к своему аккаунту:\n"
|
||||
"1) Откройте ЛС со мной и выполните /add_group\n"
|
||||
"2) Вставьте ID выше *или* перешлите сюда сообщение из канала\n\n"
|
||||
"⚠️ Для публикации сообщений мне нужно право «Публиковать сообщения» (сделайте бота администратором с этим правом)."
|
||||
)
|
||||
Reference in New Issue
Block a user