security, audit, fom features
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-09-06 05:03:45 +09:00
parent df9d8b295d
commit 9793648ee3
11 changed files with 144 additions and 22 deletions

12
db.py
View File

@@ -1,10 +1,12 @@
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv()
import os import os
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import declarative_base from sqlalchemy.orm import declarative_base
from sqlalchemy.ext.asyncio import async_sessionmaker from sqlalchemy.ext.asyncio import async_sessionmaker
load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///bot.db") DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///bot.db")
if DATABASE_URL.startswith("sqlite+aiosqlite:///"): if DATABASE_URL.startswith("sqlite+aiosqlite:///"):
@@ -72,4 +74,10 @@ async def init_db():
tables = Base.metadata.tables.keys() tables = Base.metadata.tables.keys()
print(f"Созданы таблицы: {', '.join(tables)}") print(f"Созданы таблицы: {', '.join(tables)}")
else: else:
print("База данных уже существует и содержит таблицы, создание пропущено.") 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()

View File

@@ -1,3 +1,6 @@
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, CallbackQueryHandler, MessageHandler, filters from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, CallbackQueryHandler, MessageHandler, filters
from db import AsyncSessionLocal from db import AsyncSessionLocal
@@ -90,8 +93,27 @@ async def input_url(update: Update, context: ContextTypes.DEFAULT_TYPE):
except Exception as e: except Exception as e:
if update.message: if update.message:
await update.message.reply_text(f'Ошибка при добавлении кнопки: {e}') 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: finally:
await session.close() session.close()
return ConversationHandler.END return ConversationHandler.END
add_button_conv = ConversationHandler( add_button_conv = ConversationHandler(
@@ -102,4 +124,4 @@ add_button_conv = ConversationHandler(
INPUT_URL: [MessageHandler(filters.TEXT & ~filters.COMMAND, input_url)], INPUT_URL: [MessageHandler(filters.TEXT & ~filters.COMMAND, input_url)],
}, },
fallbacks=[] fallbacks=[]
) )

View File

@@ -46,6 +46,9 @@ async def save_channel(update: Update, context: ContextTypes.DEFAULT_TYPE):
channel = Channel(name=name, link=link) channel = Channel(name=name, link=link)
session.add(channel) session.add(channel)
await session.commit() 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: if update.message:
await update.message.reply_text(f'Канал "{name}" добавлен.') await update.message.reply_text(f'Канал "{name}" добавлен.')
return ConversationHandler.END return ConversationHandler.END

View File

@@ -46,6 +46,9 @@ async def save_group(update: Update, context: ContextTypes.DEFAULT_TYPE):
group = Group(name=name, link=link) group = Group(name=name, link=link)
session.add(group) session.add(group)
await session.commit() 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: if update.message:
await update.message.reply_text(f'Группа "{name}" добавлена.') await update.message.reply_text(f'Группа "{name}" добавлена.')
return ConversationHandler.END return ConversationHandler.END

View File

@@ -1,6 +1,6 @@
from telegram import Update from telegram import Update
from telegram.ext import CommandHandler, ContextTypes from telegram.ext import CommandHandler, ContextTypes
from db import AsyncSessionLocal from db import AsyncSessionLocal, log_action
from models import Button from models import Button
async def del_button(update: Update, context: ContextTypes.DEFAULT_TYPE): 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 <название>') await update.message.reply_text('Используйте: /del_button <название>')
return return
name = args[0] name = args[0]
session = AsyncSessionLocal() from sqlalchemy import select
try: async with AsyncSessionLocal() as session:
from sqlalchemy import select
result = await session.execute(select(Button).where(Button.name == name)) result = await session.execute(select(Button).where(Button.name == name))
button = result.scalar_one_or_none() button = result.scalar_one_or_none()
if not button: if not button:
@@ -21,7 +20,7 @@ async def del_button(update: Update, context: ContextTypes.DEFAULT_TYPE):
return return
await session.delete(button) await session.delete(button)
await session.commit() 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: if update.message:
await update.message.reply_text(f'Кнопка "{name}" удалена.') await update.message.reply_text(f'Кнопка \"{name}\" удалена.')
finally:
await session.close()

View File

@@ -20,6 +20,9 @@ async def edit_button(update: Update, context: ContextTypes.DEFAULT_TYPE):
button.name = new_name button.name = new_name
button.url = new_url button.url = new_url
await session.commit() 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: if update.message:
await update.message.reply_text(f'Кнопка "{name}" изменена.') await update.message.reply_text(f'Кнопка "{name}" изменена.')
finally: finally:

26
handlers/invite_admin.py Normal file
View File

@@ -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)

View File

@@ -2,7 +2,7 @@ from telegram import Update, InputMediaPhoto, InlineKeyboardMarkup, InlineKeyboa
from telegram.ext import ContextTypes, ConversationHandler, MessageHandler, CommandHandler, filters, CallbackQueryHandler, ContextTypes from telegram.ext import ContextTypes, ConversationHandler, MessageHandler, CommandHandler, filters, CallbackQueryHandler, ContextTypes
from db import AsyncSessionLocal 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) 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) context.user_data['text'] = getattr(update.message, 'text', None) or getattr(update.message, 'caption', None)
from sqlalchemy import select from sqlalchemy import select
session = AsyncSessionLocal() session = AsyncSessionLocal()
user_id = update.effective_user.id if update.effective_user else None
try: 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() 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() groups = groups_result.scalars().all()
keyboard = [] keyboard = []
for c in channels: 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))}')]) keyboard.append([InlineKeyboardButton(f'Группа: {getattr(g, "name", str(g.name))}', callback_data=f'group_{getattr(g, "id", str(g.id))}')])
reply_markup = InlineKeyboardMarkup(keyboard) reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text('Выберите, куда отправить пост:', reply_markup=reply_markup) 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 return SELECT_TARGET
finally: finally:
await session.close() 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...)') await query.edit_message_text('Ошибка: ссылка должна быть username (@channel) или числовой ID (-100...)')
return ConversationHandler.END return ConversationHandler.END
try: try:
photo = context.user_data.get('photo') if context.user_data else None # Пересылка исходного сообщения
if photo: await context.bot.forward_message(
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) chat_id=chat_id,
await query.edit_message_text('Пост отправлен!') from_chat_id=context.user_data.get('forward_chat_id'),
else: message_id=context.user_data.get('forward_message_id')
await query.edit_message_text('Ошибка: не выбрано фото для поста.') )
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: except Exception as e:
await query.edit_message_text(f'Ошибка отправки поста: {e}') await query.edit_message_text(f'Ошибка пересылки поста: {e}')
finally: finally:
await session.close() await session.close()
return ConversationHandler.END return ConversationHandler.END

23
handlers/share_bot.py Normal file
View File

@@ -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)

View File

@@ -1,3 +1,5 @@
from handlers.share_bot import share_bot_handler
from handlers.invite_admin import invite_admin_handler
import sys import sys
import asyncio import asyncio
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
@@ -93,6 +95,8 @@ def main():
application.add_handler(CommandHandler('edit_button', edit_button)) application.add_handler(CommandHandler('edit_button', edit_button))
application.add_handler(CommandHandler('del_button', del_button)) application.add_handler(CommandHandler('del_button', del_button))
application.add_handler(admin_panel_conv) application.add_handler(admin_panel_conv)
application.add_handler(share_bot_handler)
application.add_handler(invite_admin_handler)
import sys import sys
import asyncio import asyncio
if sys.platform.startswith('win'): if sys.platform.startswith('win'):

View File

@@ -1,11 +1,25 @@
from datetime import datetime
from sqlalchemy import Column, Integer, String, ForeignKey, Text from sqlalchemy import Column, Integer, String, ForeignKey, Text
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from db import Base 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): class Admin(Base):
__tablename__ = 'admins' __tablename__ = 'admins'
id = Column(Integer, primary_key=True) 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): class Channel(Base):
__tablename__ = 'channels' __tablename__ = 'channels'