commit 68b874d791e3cb9713d7f317c763eddfaf082ba1 Author: UdoChudo Date: Sun Feb 23 12:05:49 2025 +0500 initial commit diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..b6ce724 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,23 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:D:\Projects\Python\TeleZab2.0\db\telezab.db + + + + $ProjectFileDir$ + + + file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar + + + file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..c7b64ee --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..68eb721 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/sqlDataSources.xml b/.idea/sqlDataSources.xml new file mode 100644 index 0000000..31a9a98 --- /dev/null +++ b/.idea/sqlDataSources.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..e3cfb27 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/TeleZab2.0.iml b/TeleZab2.0.iml new file mode 100644 index 0000000..dd06ec2 --- /dev/null +++ b/TeleZab2.0.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..4a5693c --- /dev/null +++ b/config.py @@ -0,0 +1,3 @@ +import os +BOT_TOKEN = os.getenv("BOT_TOKEN") +WHITELIST_CHAT_IDS = [211595028] diff --git a/main.py b/main.py new file mode 100644 index 0000000..ef7c5f4 --- /dev/null +++ b/main.py @@ -0,0 +1,27 @@ +import asyncio +import logging + +from aiogram import Bot, F +from aiogram import Dispatcher +from aiogram import types +from aiogram.client.default import DefaultBotProperties +from aiogram.enums import ParseMode +from routers import router as main_router +from utils.db import init_db + +import config + +async def main (): + dp = Dispatcher() + dp.include_router(main_router) + logging.basicConfig(level=logging.INFO) + bot = Bot(token=config.BOT_TOKEN, + default=DefaultBotProperties( + parse_mode=ParseMode.HTML + ) + ) + await init_db() + await dp.start_polling(bot) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/main/admins.sql b/main/admins.sql new file mode 100644 index 0000000..ee277d6 --- /dev/null +++ b/main/admins.sql @@ -0,0 +1,7 @@ +create table admins +( + chat_id INTEGER + primary key, + username TEXT +); + diff --git a/main/events.sql b/main/events.sql new file mode 100644 index 0000000..bbc031a --- /dev/null +++ b/main/events.sql @@ -0,0 +1,10 @@ +create table events +( + id INTEGER + primary key autoincrement, + hash TEXT + unique, + data TEXT, + delivered BOOLEAN +); + diff --git a/main/regions.sql b/main/regions.sql new file mode 100644 index 0000000..7548217 --- /dev/null +++ b/main/regions.sql @@ -0,0 +1,8 @@ +create table regions +( + region_id TEXT + primary key, + region_name TEXT, + active BOOLEAN default TRUE +); + diff --git a/main/subscriptions.sql b/main/subscriptions.sql new file mode 100644 index 0000000..bd26303 --- /dev/null +++ b/main/subscriptions.sql @@ -0,0 +1,10 @@ +create table subscriptions +( + chat_id INTEGER, + region_id TEXT, + username TEXT, + active BOOLEAN default TRUE, + skip BOOLEAN default FALSE, + unique (chat_id, region_id) +); + diff --git a/main/user_events.sql b/main/user_events.sql new file mode 100644 index 0000000..1245423 --- /dev/null +++ b/main/user_events.sql @@ -0,0 +1,10 @@ +create table user_events +( + id INTEGER + primary key autoincrement, + chat_id INTEGER, + username TEXT, + action TEXT, + timestamp TEXT +); + diff --git a/main/whitelist.sql b/main/whitelist.sql new file mode 100644 index 0000000..4398f95 --- /dev/null +++ b/main/whitelist.sql @@ -0,0 +1,8 @@ +create table whitelist +( + chat_id INTEGER + primary key, + username TEXT, + user_email TEXT +); + diff --git a/routers/__init__.py b/routers/__init__.py new file mode 100644 index 0000000..f93cc96 --- /dev/null +++ b/routers/__init__.py @@ -0,0 +1,11 @@ +__all__ = ("router",) + +from aiogram import Router +from .commands import router as commands_router + +router = Router(name=__name__) + +router.include_routers( + commands_router, +) + diff --git a/routers/commands/__init__.py b/routers/commands/__init__.py new file mode 100644 index 0000000..19aa54c --- /dev/null +++ b/routers/commands/__init__.py @@ -0,0 +1,18 @@ +__all__ = ("router",) + +from aiogram import Router +from .base_commands import router as base_commands_router +from .common_commands import router as common_commands_router +from .register_command import router as register_command_router +from .setting_commands import router as setting_commands +from .test_commands import router as test_commands_router + +router = Router() + +router.include_routers(base_commands_router, + register_command_router, + setting_commands, + test_commands_router) + +router.include_router(common_commands_router) + diff --git a/routers/commands/base_commands.py b/routers/commands/base_commands.py new file mode 100644 index 0000000..d604abb --- /dev/null +++ b/routers/commands/base_commands.py @@ -0,0 +1,41 @@ +from aiogram import Router, types, F +from aiogram.filters import CommandStart, Command +from aiogram.types import KeyboardButton, ReplyKeyboardMarkup +from aiogram.utils import markdown +from config import WHITELIST_CHAT_IDS + +router = Router(name=__name__) + +async def is_whitelist(chat_id: int) -> bool: + return chat_id in WHITELIST_CHAT_IDS + +@router.message(CommandStart()) +async def handle_start(message: types.Message): + + if await is_whitelist(message.chat.id): + button_settings = KeyboardButton(text="Настройки") + button_help = KeyboardButton(text="Помощь") + button_active_triggers = KeyboardButton(text="Активные тригеры") + button_row = [button_settings,button_help,button_active_triggers] + markup = ReplyKeyboardMarkup(keyboard=[button_row],resize_keyboard=True) + else: + button_register = KeyboardButton(text="Регистрация") + button_row = [button_register] + markup = ReplyKeyboardMarkup(keyboard=[button_row],resize_keyboard=True, one_time_keyboard=True) + await message.answer(text="Выберите действие:", reply_markup=markup) + + +@router.message(Command("help")) +async def handle_help(message: types.Message): + text = markdown.text("/start - Показать меню бота\n", + markdown.hbold("Настройки"),"- Перейти в режим настроек и управления подписками\n", + markdown.hbold("Активные тригеры")," - Получение активных проблем за последние 24 часа\n", + markdown.text( + markdown.hbold("Помощь - "), + markdown.hlink("Описание всех возможностей бота", + "https://confluence.is-mis.ru/pages/viewpage.action?pageId=460596141"), + sep=""), + sep="") + await message.answer(text=text) + + diff --git a/routers/commands/common_commands.py b/routers/commands/common_commands.py new file mode 100644 index 0000000..040450f --- /dev/null +++ b/routers/commands/common_commands.py @@ -0,0 +1,13 @@ + + +from aiogram import Router, types + +router = Router(name=__name__) + +@router.message() +async def unexpected_message(message: types.Message): + await message.answer(text="Неизвестная команда. попробуйте ещё раз") + +@router.message() +async def cancel_button(message: types.Message): + pass diff --git a/routers/commands/register_command.py b/routers/commands/register_command.py new file mode 100644 index 0000000..e6e5788 --- /dev/null +++ b/routers/commands/register_command.py @@ -0,0 +1,11 @@ +from aiogram import Router, types, F + +router = Router(name=__name__) +@router.message(F.text == "Регистрация") +async def handle_register(message: types.Message): + user_full_name = message.from_user.full_name + chat_id = message.chat.id + username = "@" + message.from_user.username + await message.answer(text=f"Привет, {user_full_name}!\n" + f"Твой Чат ID: {chat_id}\n" + f"Твоё имя пользователя: {username}\n") \ No newline at end of file diff --git a/routers/commands/setting_commands.py b/routers/commands/setting_commands.py new file mode 100644 index 0000000..5769796 --- /dev/null +++ b/routers/commands/setting_commands.py @@ -0,0 +1,75 @@ +from aiogram import Router, types, F +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import StatesGroup, State + +from utils.get_regions import get_sorted_regions +from utils.subscribe import handle_subscribe, handle_unsubscribe, get_user_subscriptions +from utils.show_settings_menu import show_settings_menu + +from .base_commands import is_whitelist + +router = Router(name=__name__) + +@router.message(F.text == "Настройки") +async def handle_settings_menu(message: types.Message): + chat_id = message.chat.id + whitelist = await is_whitelist(chat_id) + if not whitelist: + text = "Вы неавторизованы для использования бота." + await message.answer(text=text) + return + + # await SubscriptionState.waiting_for_action.set() # Устанавливаем состояние ожидания выбора действия + await message.answer("Выберите действие", reply_markup=show_settings_menu(chat_id)) + +@router.message(F.text == "Подписаться") +async def subscribe_command(message: types.Message,): + chat_id = message.chat.id + whitelist = await is_whitelist(chat_id) + if not whitelist: + text = "Вы неавторизованы для использования бота." + await message.answer(text=text) + # await state.clear() # Завершаем состояние, если пользователь не авторизован + return + + # await SubscriptionState.choosing_regions.set() # Переходим в состояние ожидания ввода номеров регионов + + region_list = await get_sorted_regions() + text = (f"Отправьте номер или номера регионов, на которые хотите подписаться (через запятую):\n" + f"{region_list}\n" + f"Напишите 'отмена' для отмены.") + await message.answer(text=text, reply_markup=types.ReplyKeyboardRemove()) + + +@router.message(F.text == "Отписаться") +async def unsubscribe_command(message: types.Message): + chat_id = message.chat.id + if not await is_whitelist(chat_id): + text = "Вы неавторизованы для использования бота." + await message.answer(text=text) + # await state.clear() # Завершаем состояние, если пользователь не авторизован + return + + await handle_unsubscribe(chat_id) + await message.answer("Вы успешно отписались от всех регионов.", reply_markup=show_settings_menu(chat_id)) + # await state.clear() # Завершаем состояние после выполнения действия + + +@router.message(F.text == "Мои подписки") +async def my_subscriptions_command(message: types.Message): + chat_id = message.chat.id + if not await is_whitelist(chat_id): + text = "Вы неавторизованы для использования бота." + await message.answer(text=text) + # await state.clear() # Завершаем состояние, если пользователь не авторизован + return + + # Логика для отображения подписок пользователя + subscriptions = await get_user_subscriptions(chat_id) + if subscriptions: + text = "Ваши подписки:\n" + "\n".join(f"{sub.region_id}: {sub.region_name}" for sub in subscriptions) + else: + text = "У вас нет активных подписок." + + await message.answer(text=text, reply_markup=show_settings_menu(chat_id)) + # await state.clear() # Завершаем состояние после выполнения действия diff --git a/routers/commands/test_commands.py b/routers/commands/test_commands.py new file mode 100644 index 0000000..4ade195 --- /dev/null +++ b/routers/commands/test_commands.py @@ -0,0 +1,22 @@ +# handlers.py + +from aiogram import types +from aiogram import Router, F +from sqlalchemy.future import select +from utils.db import AsyncSessionLocal +from utils.db.whitelist import Whitelist + +router = Router() + +@router.message(F.text == "Проверка функций" and F.from_user.id == 211595028) +async def check_access(message: types.Message): + chat_id = message.chat.id + async with AsyncSessionLocal() as session: + stmt = select(Whitelist).filter_by(chat_id=chat_id) + result = await session.execute(stmt) + user = result.scalars().first() + + if user: + await message.answer(f"Ваш логин: {user.username}.\nВаш E-mail: {user.user_email}\nВаш чат ID: {user.chat_id}") + else: + await message.answer("Вы не в списке разрешенных.") diff --git a/utils/db/__init__.py b/utils/db/__init__.py new file mode 100644 index 0000000..9a9c0cd --- /dev/null +++ b/utils/db/__init__.py @@ -0,0 +1,27 @@ +# utils/db/__init__.py + +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker, declarative_base + +# Создание асинхронного движка SQLAlchemy +DATABASE_URL = 'sqlite+aiosqlite:///db/telezab.db' +engine = create_async_engine(DATABASE_URL, echo=False) +AsyncSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession) + +# Базовый класс для всех моделей +Base = declarative_base() + +# Импорт моделей +from .whitelist import Whitelist +from .user_events import UserEvent +from .subscriptions import Subscription +from .regions import Region +from .events import Event +from .admins import Admin + +# Создание всех таблиц +async def init_db(): + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + print("INFO:SQLAlchemy:Database inited") + diff --git a/utils/db/admins.py b/utils/db/admins.py new file mode 100644 index 0000000..f959dce --- /dev/null +++ b/utils/db/admins.py @@ -0,0 +1,10 @@ +# utils/db/admins.py + +from sqlalchemy import Column, Integer, String +from utils.db import Base + +class Admin(Base): + __tablename__ = 'admins' + + chat_id = Column(Integer, primary_key=True, unique=True, nullable=False) + username = Column(String, nullable=False) diff --git a/utils/db/events.py b/utils/db/events.py new file mode 100644 index 0000000..08b54b6 --- /dev/null +++ b/utils/db/events.py @@ -0,0 +1,12 @@ +# utils/db/events.py + +from sqlalchemy import Column, Integer, String, Text, Boolean +from utils.db import Base + +class Event(Base): + __tablename__ = 'events' + + id = Column(Integer, primary_key=True, autoincrement=True) + hash = Column(String, unique=True, nullable=False) + data = Column(Text, nullable=False) + delivered = Column(Boolean, default=False) diff --git a/utils/db/regions.py b/utils/db/regions.py new file mode 100644 index 0000000..6bb9b1c --- /dev/null +++ b/utils/db/regions.py @@ -0,0 +1,11 @@ +# utils/db/regions.py + +from sqlalchemy import Column, String, Boolean +from utils.db import Base + +class Region(Base): + __tablename__ = 'regions' + + region_id = Column(String, primary_key=True, unique=True, nullable=False) + region_name = Column(String, nullable=False) + active = Column(Boolean, default=True) diff --git a/utils/db/subscriptions.py b/utils/db/subscriptions.py new file mode 100644 index 0000000..d6fcaa7 --- /dev/null +++ b/utils/db/subscriptions.py @@ -0,0 +1,22 @@ +# utils/db/subscriptions.py + +from sqlalchemy import Column, Integer, String, Boolean, UniqueConstraint +from utils.db import Base + +class Subscription(Base): + __tablename__ = 'subscriptions' + + chat_id = Column(Integer, nullable=False) + region_id = Column(String, nullable=False) + username = Column(String, nullable=False) + active = Column(Boolean, default=True) + skip = Column(Boolean, default=False) + + # Определение составного первичного ключа + __table_args__ = ( + UniqueConstraint('chat_id', 'region_id', name='unique_chat_region'), + {'sqlite_autoincrement': True} + ) + + # Определяем составной первичный ключ + primary_key = Column(Integer, primary_key=True, autoincrement=True) # Добавление этого столбца необходимо для уникального первичного ключа diff --git a/utils/db/user_events.py b/utils/db/user_events.py new file mode 100644 index 0000000..ad9cd5e --- /dev/null +++ b/utils/db/user_events.py @@ -0,0 +1,13 @@ +# utils/db/user_events.py + +from sqlalchemy import Column, Integer, String, Text +from utils.db import Base + +class UserEvent(Base): + __tablename__ = 'user_events' + + id = Column(Integer, primary_key=True, autoincrement=True) + chat_id = Column(Integer, nullable=False) + username = Column(String, nullable=False) + action = Column(String, nullable=False) + timestamp = Column(Text, nullable=False) diff --git a/utils/db/whitelist.py b/utils/db/whitelist.py new file mode 100644 index 0000000..de600a3 --- /dev/null +++ b/utils/db/whitelist.py @@ -0,0 +1,9 @@ +from sqlalchemy import Column, Integer, String +from utils.db import Base + +class Whitelist(Base): + __tablename__ = 'whitelist' + + chat_id = Column(Integer, primary_key=True) + username = Column(String) + user_email = Column(String) diff --git a/utils/get_regions.py b/utils/get_regions.py new file mode 100644 index 0000000..3adeceb --- /dev/null +++ b/utils/get_regions.py @@ -0,0 +1,21 @@ +from sqlalchemy import cast, Integer +from sqlalchemy.future import select +from utils.db import AsyncSessionLocal +from utils.db.regions import Region + +async def get_sorted_regions(): + """ + Получает отсортированный список активных регионов из базы данных. + + :return: Список кортежей с идентификаторами и названиями регионов + """ + async with AsyncSessionLocal() as session: + async with session.begin(): + # Формируем запрос для получения активных регионов + stmt = select(Region.region_id, Region.region_name).where(Region.active == True).order_by(cast(Region.region_id, Integer)) + + # Выполняем запрос и получаем результат + result = await session.execute(stmt) + regions = result.fetchall() # Получаем все записи как список кортежей + regions = "\n".join(f"{region_id}: {region_name}" for region_id, region_name in regions) + return regions \ No newline at end of file diff --git a/utils/permissions.py b/utils/permissions.py new file mode 100644 index 0000000..0546ee1 --- /dev/null +++ b/utils/permissions.py @@ -0,0 +1,17 @@ +import logging + + +from aiogram import types + +from utils.db import AsyncSessionLocal, Whitelist +from sqlalchemy.future import select + +async def is_whitelisted(message: types.message): + chat_id = message.chat.id + async with AsyncSessionLocal() as session: + stmt = select(Whitelist).filter_by(chat_id=chat_id) + result = await session.execute(stmt) + user = result.scalars().first() + if not user: + await message.answer("Вы не авторизованы для использования этого бота") + logging.info(f"Unauthorized access attempt by {chat_id}") \ No newline at end of file diff --git a/utils/show_settings_menu.py b/utils/show_settings_menu.py new file mode 100644 index 0000000..1bfddc3 --- /dev/null +++ b/utils/show_settings_menu.py @@ -0,0 +1,14 @@ +from aiogram.types import KeyboardButton, ReplyKeyboardMarkup + + +def show_settings_menu(chat_id): + button_subscribe = KeyboardButton(text="Подписаться") + button_unsubscribe = KeyboardButton(text="Отписаться") + button_subscription = KeyboardButton(text="Мои подписки") + button_active_regions = KeyboardButton(text="Активные регионы") + button_cancel = KeyboardButton(text="Назад") + button_row_1 = [button_subscribe,button_unsubscribe,button_subscription] + button_row_2 = [button_active_regions] + button_row_3 = [button_cancel] + markup = ReplyKeyboardMarkup(keyboard=[button_row_1,button_row_2,button_row_3], resize_keyboard=True) + return markup \ No newline at end of file diff --git a/utils/subscribe.py b/utils/subscribe.py new file mode 100644 index 0000000..1bde56c --- /dev/null +++ b/utils/subscribe.py @@ -0,0 +1,33 @@ +# utils/subscribe.py +from sqlalchemy import delete, select +from utils.db.regions import Region +from utils.db import AsyncSessionLocal, Subscription + + +async def handle_subscribe(user_id, region_ids): + async with AsyncSessionLocal() as session: + async with session.begin(): + # Пример: удаляем старые подписки и добавляем новые + await session.execute(delete(Subscription).where(Subscription.user_id == user_id)) + new_subscriptions = [Subscription(user_id=user_id, region_id=region_id) for region_id in region_ids] + session.add_all(new_subscriptions) + await session.commit() + +async def handle_unsubscribe(user_id): + async with AsyncSessionLocal() as session: + async with session.begin(): + # Удаление всех подписок пользователя + await session.execute(delete(Subscription).where(Subscription.user_id == user_id)) + await session.commit() + + +async def get_user_subscriptions(user_id): + async with AsyncSessionLocal() as session: + async with session.begin(): + result = await session.execute( + select(Subscription.region_id, Region.region_name) + .join(Region, Subscription.region_id == Region.region_id) + .where(Subscription.user_id == user_id, Subscription.active == True) + ) + subscriptions = result.fetchall() + return subscriptions