diff --git a/db.py b/db.py index 8b5e3ac..0c805a0 100644 --- a/db.py +++ b/db.py @@ -1,10 +1,12 @@ from dotenv import load_dotenv -load_dotenv() import os from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.orm import declarative_base from sqlalchemy.ext.asyncio import async_sessionmaker + +load_dotenv() + DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///bot.db") if DATABASE_URL.startswith("sqlite+aiosqlite:///"): @@ -72,4 +74,10 @@ async def init_db(): tables = Base.metadata.tables.keys() print(f"Созданы таблицы: {', '.join(tables)}") else: - print("База данных уже существует и содержит таблицы, создание пропущено.") \ No newline at end of file + print("База данных уже существует и содержит таблицы, создание пропущено.") + +async def log_action(admin_id, action, details=""): + async with AsyncSessionLocal() as session: + log = ActionLog(admin_id=admin_id, action=action, details=details) + session.add(log) + await session.commit() \ No newline at end of file diff --git a/handlers/add_button.py b/handlers/add_button.py index e30319e..76136d9 100644 --- a/handlers/add_button.py +++ b/handlers/add_button.py @@ -1,3 +1,6 @@ + + + from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, CallbackQueryHandler, MessageHandler, filters from db import AsyncSessionLocal @@ -90,8 +93,27 @@ async def input_url(update: Update, context: ContextTypes.DEFAULT_TYPE): except Exception as e: if update.message: await update.message.reply_text(f'Ошибка при добавлении кнопки: {e}') + try: + type_, obj_id = target.split('_', 1) + obj_id = int(obj_id) + if type_ == 'channel': + button = Button(name=name, url=url, channel_id=obj_id) + elif type_ == 'group': + button = Button(name=name, url=url, group_id=obj_id) + else: + await update.message.reply_text('Ошибка: неверный тип объекта.') + session.close() + return ConversationHandler.END + session.add(button) + session.commit() + from db import log_action + user_id = update.effective_user.id if update.effective_user else None + log_action(user_id, "add_button", f"type={type_}, obj_id={obj_id}, name={name}, url={url}") + await update.message.reply_text('Кнопка добавлена.') + except Exception as e: + await update.message.reply_text(f'Ошибка при добавлении кнопки: {e}') finally: - await session.close() + session.close() return ConversationHandler.END add_button_conv = ConversationHandler( @@ -102,4 +124,4 @@ add_button_conv = ConversationHandler( INPUT_URL: [MessageHandler(filters.TEXT & ~filters.COMMAND, input_url)], }, fallbacks=[] - ) +) \ No newline at end of file diff --git a/handlers/add_channel.py b/handlers/add_channel.py index f39cb39..450868e 100644 --- a/handlers/add_channel.py +++ b/handlers/add_channel.py @@ -46,6 +46,9 @@ async def save_channel(update: Update, context: ContextTypes.DEFAULT_TYPE): channel = Channel(name=name, link=link) session.add(channel) await session.commit() + from db import log_action + user_id = update.effective_user.id if update.effective_user else None + await log_action(user_id, "add_channel", f"name={name}, link={link}") if update.message: await update.message.reply_text(f'Канал "{name}" добавлен.') return ConversationHandler.END diff --git a/handlers/add_group.py b/handlers/add_group.py index 4f9459f..76e0dc3 100644 --- a/handlers/add_group.py +++ b/handlers/add_group.py @@ -46,6 +46,9 @@ async def save_group(update: Update, context: ContextTypes.DEFAULT_TYPE): group = Group(name=name, link=link) session.add(group) await session.commit() + from db import log_action + user_id = update.effective_user.id if update.effective_user else None + log_action(user_id, "add_group", f"name={name}, link={link}") if update.message: await update.message.reply_text(f'Группа "{name}" добавлена.') return ConversationHandler.END diff --git a/handlers/del_button.py b/handlers/del_button.py index 0881dd3..2d62117 100644 --- a/handlers/del_button.py +++ b/handlers/del_button.py @@ -1,6 +1,6 @@ from telegram import Update from telegram.ext import CommandHandler, ContextTypes -from db import AsyncSessionLocal +from db import AsyncSessionLocal, log_action from models import Button async def del_button(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -10,9 +10,8 @@ async def del_button(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text('Используйте: /del_button <название>') return name = args[0] - session = AsyncSessionLocal() - try: - from sqlalchemy import select + from sqlalchemy import select + async with AsyncSessionLocal() as session: result = await session.execute(select(Button).where(Button.name == name)) button = result.scalar_one_or_none() if not button: @@ -21,7 +20,7 @@ async def del_button(update: Update, context: ContextTypes.DEFAULT_TYPE): return await session.delete(button) await session.commit() + user_id = update.effective_user.id if update.effective_user else None + await log_action(user_id, "del_button", f"name={name}") if update.message: - await update.message.reply_text(f'Кнопка "{name}" удалена.') - finally: - await session.close() + await update.message.reply_text(f'Кнопка \"{name}\" удалена.') diff --git a/handlers/edit_button.py b/handlers/edit_button.py index 2db3aad..880b64f 100644 --- a/handlers/edit_button.py +++ b/handlers/edit_button.py @@ -20,6 +20,9 @@ async def edit_button(update: Update, context: ContextTypes.DEFAULT_TYPE): button.name = new_name button.url = new_url await session.commit() + from db import log_action + user_id = update.effective_user.id if update.effective_user else None + log_action(user_id, "edit_button", f"old_name={name}, new_name={new_name}, new_url={new_url}") if update.message: await update.message.reply_text(f'Кнопка "{name}" изменена.') finally: diff --git a/handlers/invite_admin.py b/handlers/invite_admin.py new file mode 100644 index 0000000..278c135 --- /dev/null +++ b/handlers/invite_admin.py @@ -0,0 +1,26 @@ +from telegram import Update +from telegram.ext import CommandHandler, ContextTypes +from db import AsyncSessionLocal, log_action +from models import Admin + +async def invite_admin(update: Update, context: ContextTypes.DEFAULT_TYPE): + args = context.args + if len(args) < 3: + await update.message.reply_text("Неверная ссылка.") + return + channel_id, inviter_id, token = args + user_id = update.effective_user.id + session = AsyncSessionLocal() + admin = session.query(Admin).filter_by(invite_token=token, channel_id=channel_id).first() + if not admin: + await update.message.reply_text("Ссылка недействительна.") + session.close() + return + new_admin = Admin(tg_id=user_id, channel_id=channel_id, inviter_id=inviter_id) + session.add(new_admin) + session.commit() + session.close() + await update.message.reply_text("Вы добавлены как администратор канала!") + log_action(user_id, "invite_admin", f"channel_id={channel_id}, inviter_id={inviter_id}") + +invite_admin_handler = CommandHandler("invite_admin", invite_admin) diff --git a/handlers/new_post.py b/handlers/new_post.py index ec6fd28..a85daac 100644 --- a/handlers/new_post.py +++ b/handlers/new_post.py @@ -2,7 +2,7 @@ from telegram import Update, InputMediaPhoto, InlineKeyboardMarkup, InlineKeyboa from telegram.ext import ContextTypes, ConversationHandler, MessageHandler, CommandHandler, filters, CallbackQueryHandler, ContextTypes from db import AsyncSessionLocal -from models import Channel, Group, Button +from models import Channel, Group, Button, Admin SELECT_MEDIA, SELECT_TEXT, SELECT_TARGET = range(3) @@ -29,10 +29,20 @@ async def select_text(update: Update, context: ContextTypes.DEFAULT_TYPE): context.user_data['text'] = getattr(update.message, 'text', None) or getattr(update.message, 'caption', None) from sqlalchemy import select session = AsyncSessionLocal() + user_id = update.effective_user.id if update.effective_user else None try: - channels_result = await session.execute(select(Channel)) + # Ограничиваем каналы и группы только теми, где пользователь — админ + channels_result = await session.execute( + select(Channel).join(Button, isouter=True).join(Group, isouter=True) + .join(Admin, Channel.id == Admin.channel_id) + .where(Admin.tg_id == user_id) + ) channels = channels_result.scalars().all() - groups_result = await session.execute(select(Group)) + groups_result = await session.execute( + select(Group).join(Button, isouter=True) + .join(Admin, Group.id == Admin.channel_id) + .where(Admin.tg_id == user_id) + ) groups = groups_result.scalars().all() keyboard = [] for c in channels: @@ -41,6 +51,9 @@ async def select_text(update: Update, context: ContextTypes.DEFAULT_TYPE): keyboard.append([InlineKeyboardButton(f'Группа: {getattr(g, "name", str(g.name))}', callback_data=f'group_{getattr(g, "id", str(g.id))}')]) reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text('Выберите, куда отправить пост:', reply_markup=reply_markup) + # Сохраняем id исходного сообщения для пересылки + context.user_data['forward_message_id'] = update.message.message_id + context.user_data['forward_chat_id'] = update.message.chat_id return SELECT_TARGET finally: await session.close() @@ -80,14 +93,18 @@ async def select_target(update: Update, context: ContextTypes.DEFAULT_TYPE): await query.edit_message_text('Ошибка: ссылка должна быть username (@channel) или числовой ID (-100...)') return ConversationHandler.END try: - photo = context.user_data.get('photo') if context.user_data else None - if photo: - await context.bot.send_photo(chat_id=chat_id, photo=photo, caption=context.user_data.get('text') if context.user_data else None, reply_markup=markup) - await query.edit_message_text('Пост отправлен!') - else: - await query.edit_message_text('Ошибка: не выбрано фото для поста.') + # Пересылка исходного сообщения + await context.bot.forward_message( + chat_id=chat_id, + from_chat_id=context.user_data.get('forward_chat_id'), + message_id=context.user_data.get('forward_message_id') + ) + from db import log_action + user_id = update.effective_user.id if update.effective_user else None + log_action(user_id, "forward_post", f"chat_id={chat_id}, from_chat_id={context.user_data.get('forward_chat_id')}, message_id={context.user_data.get('forward_message_id')}") + await query.edit_message_text('Пост переслан!') except Exception as e: - await query.edit_message_text(f'Ошибка отправки поста: {e}') + await query.edit_message_text(f'Ошибка пересылки поста: {e}') finally: await session.close() return ConversationHandler.END diff --git a/handlers/share_bot.py b/handlers/share_bot.py new file mode 100644 index 0000000..10f4473 --- /dev/null +++ b/handlers/share_bot.py @@ -0,0 +1,23 @@ +import secrets +from telegram import Update +from telegram.ext import CommandHandler, ContextTypes +from db import AsyncSessionLocal, log_action +from models import Admin + +async def share_bot(update: Update, context: ContextTypes.DEFAULT_TYPE): + user_id = update.effective_user.id + channel_id = context.user_data.get("channel_id") + if not channel_id: + await update.message.reply_text("Сначала выберите канал через /channel_buttons.") + return + token = secrets.token_urlsafe(16) + session = AsyncSessionLocal() + admin = Admin(tg_id=user_id, channel_id=channel_id, inviter_id=user_id, invite_token=token) + session.add(admin) + session.commit() + session.close() + link = f"/invite_admin {channel_id} {user_id} {token}" + await update.message.reply_text(f"Инвайт-ссылка для нового администратора:\n{link}") + log_action(user_id, "share_bot", f"channel_id={channel_id}, token={token}") + +share_bot_handler = CommandHandler("share_bot", share_bot) diff --git a/main.py b/main.py index c84a329..87e5db1 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,5 @@ +from handlers.share_bot import share_bot_handler +from handlers.invite_admin import invite_admin_handler import sys import asyncio if sys.platform.startswith('win'): @@ -93,6 +95,8 @@ def main(): application.add_handler(CommandHandler('edit_button', edit_button)) application.add_handler(CommandHandler('del_button', del_button)) application.add_handler(admin_panel_conv) + application.add_handler(share_bot_handler) + application.add_handler(invite_admin_handler) import sys import asyncio if sys.platform.startswith('win'): diff --git a/models.py b/models.py index 30f8d60..efb1a89 100644 --- a/models.py +++ b/models.py @@ -1,11 +1,25 @@ +from datetime import datetime + + from sqlalchemy import Column, Integer, String, ForeignKey, Text from sqlalchemy.orm import relationship from db import Base +class ActionLog(Base): + __tablename__ = 'action_logs' + id = Column(Integer, primary_key=True) + admin_id = Column(Integer) + action = Column(String) + details = Column(String) + timestamp = Column(String, default=lambda: datetime.utcnow().isoformat()) + class Admin(Base): __tablename__ = 'admins' id = Column(Integer, primary_key=True) - tg_id = Column(Integer, unique=True, nullable=False) + tg_id = Column(Integer, nullable=False) + channel_id = Column(Integer, ForeignKey('channels.id'), nullable=True) + inviter_id = Column(Integer, nullable=True) + invite_token = Column(String, nullable=True, unique=True) class Channel(Base): __tablename__ = 'channels'