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
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:///"):
@@ -73,3 +75,9 @@ async def init_db():
print(f"Созданы таблицы: {', '.join(tables)}")
else:
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.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=[]
)
)

View File

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

View File

@@ -133,6 +133,16 @@ async def input_group_link(update: Update, context: ContextTypes.DEFAULT_TYPE):
return ConversationHandler.END
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)
@@ -153,6 +163,7 @@ async def input_group_link(update: Update, context: ContextTypes.DEFAULT_TYPE):
await session.commit()
await update.message.reply_text(f'Группа "{name}" добавлена и привязана к вашему админ-аккаунту.')
>>>>>>> main
return ConversationHandler.END

View File

@@ -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}\" удалена.')

View File

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

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 db import AsyncSessionLocal
<<<<<<< HEAD
from models import Channel, Group, Button, Admin
=======
from models import Channel, Group
from .permissions import get_or_create_admin, list_channels_for_admin, has_scope_on_channel, SCOPE_POST
from models import Channel, Group, Button
>>>>>>> main
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 asyncio
if sys.platform.startswith('win'):
@@ -129,7 +131,12 @@ def main():
application.add_handler(CommandHandler('edit_button', edit_button))
application.add_handler(CommandHandler('del_button', del_button))
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)
>>>>>>> main
import sys
import asyncio
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
>>>>>>> main
from sqlalchemy.orm import relationship
from db import Base
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_MANAGE_BTNS = 2 # право управлять кнопками (опционально)
@@ -31,10 +48,14 @@ class ChannelAccess(Base):
expires_at = Column(DateTime, nullable=True)
channel = relationship("Channel", foreign_keys=[channel_id])
>>>>>>> main
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'