Compare commits

3 Commits

Author SHA1 Message Date
f18cd78ad5 merge branch 'main' into security
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-07 14:22:43 +09:00
86f7c65697 Merge branch 'main' into security
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-07 14:22:04 +09:00
9793648ee3 security, audit, fom features
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-06 05:03:45 +09:00
11 changed files with 147 additions and 12 deletions

10
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:///"):
@@ -73,3 +75,9 @@ async def init_db():
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

@@ -57,6 +57,16 @@ async def input_channel_link(update: Update, context: ContextTypes.DEFAULT_TYPE)
return ConversationHandler.END return ConversationHandler.END
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
<<<<<<< HEAD
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}" добавлен.')
=======
admin = await _get_or_create_admin(session, user.id) admin = await _get_or_create_admin(session, user.id)
# если канал уже есть — обновим имя и владельца # если канал уже есть — обновим имя и владельца
@@ -72,6 +82,7 @@ async def input_channel_link(update: Update, context: ContextTypes.DEFAULT_TYPE)
session.add(channel) session.add(channel)
await session.commit() await session.commit()
await update.message.reply_text(f'Канал "{name}" добавлен и привязан к вашему админ-аккаунту.') await update.message.reply_text(f'Канал "{name}" добавлен и привязан к вашему админ-аккаунту.')
>>>>>>> main
return ConversationHandler.END return ConversationHandler.END
add_channel_conv = ConversationHandler( add_channel_conv = ConversationHandler(

View File

@@ -133,6 +133,16 @@ async def input_group_link(update: Update, context: ContextTypes.DEFAULT_TYPE):
return ConversationHandler.END return ConversationHandler.END
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
<<<<<<< HEAD
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}" добавлена.')
=======
# гарантируем наличие админа # гарантируем наличие админа
admin = await _get_or_create_admin(session, user.id) admin = await _get_or_create_admin(session, user.id)
@@ -153,6 +163,7 @@ async def input_group_link(update: Update, context: ContextTypes.DEFAULT_TYPE):
await session.commit() await session.commit()
await update.message.reply_text(f'Группа "{name}" добавлена и привязана к вашему админ-аккаунту.') await update.message.reply_text(f'Группа "{name}" добавлена и привязана к вашему админ-аккаунту.')
>>>>>>> main
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()
try:
from sqlalchemy import select from sqlalchemy import select
async with AsyncSessionLocal() as session:
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

@@ -13,9 +13,13 @@ from telegram.error import BadRequest
from sqlalchemy import select as sa_select from sqlalchemy import select as sa_select
from db import AsyncSessionLocal from db import AsyncSessionLocal
<<<<<<< HEAD
from models import Channel, Group, Button, Admin
=======
from models import Channel, Group from models import Channel, Group
from .permissions import get_or_create_admin, list_channels_for_admin, has_scope_on_channel, SCOPE_POST from .permissions import get_or_create_admin, list_channels_for_admin, has_scope_on_channel, SCOPE_POST
from models import Channel, Group, Button from models import Channel, Group, Button
>>>>>>> main
SELECT_MEDIA, SELECT_TEXT, SELECT_TARGET = range(3) SELECT_MEDIA, SELECT_TEXT, SELECT_TARGET = range(3)

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'):
@@ -129,7 +131,12 @@ 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)
<<<<<<< HEAD
application.add_handler(share_bot_handler)
application.add_handler(invite_admin_handler)
=======
application.add_handler(share_channel_conv) application.add_handler(share_channel_conv)
>>>>>>> main
import sys import sys
import asyncio import asyncio
if sys.platform.startswith('win'): if sys.platform.startswith('win'):

View File

@@ -1,8 +1,25 @@
<<<<<<< HEAD
from datetime import datetime
from sqlalchemy import Column, Integer, String, ForeignKey, Text
=======
from sqlalchemy import Column, Integer, String, ForeignKey, Text, DateTime, Boolean from sqlalchemy import Column, Integer, String, ForeignKey, Text, DateTime, Boolean
>>>>>>> main
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from db import Base from db import Base
from datetime import datetime from datetime import datetime
<<<<<<< HEAD
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())
=======
# Битовые флаги прав # Битовые флаги прав
SCOPE_POST = 1 # право постить SCOPE_POST = 1 # право постить
SCOPE_MANAGE_BTNS = 2 # право управлять кнопками (опционально) SCOPE_MANAGE_BTNS = 2 # право управлять кнопками (опционально)
@@ -31,10 +48,14 @@ class ChannelAccess(Base):
expires_at = Column(DateTime, nullable=True) expires_at = Column(DateTime, nullable=True)
channel = relationship("Channel", foreign_keys=[channel_id]) channel = relationship("Channel", foreign_keys=[channel_id])
>>>>>>> main
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'