initial commit

This commit is contained in:
Udo Chudo 2025-02-23 12:05:49 +05:00
commit 68b874d791
34 changed files with 566 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

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

23
.idea/dataSources.xml generated Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="telezab" uuid="ff9e489c-52ab-4c62-b43c-d17dfe7e9364">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:D:\Projects\Python\TeleZab2.0\db\telezab.db</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
<url>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</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
</libraries>
</data-source>
</component>
</project>

9
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.11 (TeleZab2.0)" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_22" project-jdk-name="Python 3.11 (TeleZab2.0)" project-jdk-type="Python SDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/TeleZab2.0.iml" filepath="$PROJECT_DIR$/TeleZab2.0.iml" />
</modules>
</component>
</project>

34
.idea/sqlDataSources.xml generated Normal file
View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DdlMappings">
<mapping uuid="0b208055-bc8a-4f73-8d95-d9ef33a1d36c" name="telezab (DDL) Mapping">
<data-sources db="04f4618c-4b37-4c3f-af49-829b685b8abb" ddl="135f85b0-383a-4387-84ad-e842c5026d21" />
<scope>
<node negative="1">
<node kind="database" negative="1" />
<node kind="database" qname="@">
<node kind="schema" negative="1" />
</node>
<node kind="schema" qname="main" />
</node>
</scope>
</mapping>
</component>
<component name="SqlDataSourceStorage">
<option name="dataSources">
<list>
<State>
<option name="id" value="135f85b0-383a-4387-84ad-e842c5026d21" />
<option name="name" value="telezab (DDL)" />
<option name="dbmsName" value="SQLITE" />
<option name="urls">
<array>
<option value="file://$PROJECT_DIR$" />
</array>
</option>
<option name="outLayout" value="File per object by schema.groovy" />
</State>
</list>
</option>
</component>
</project>

6
.idea/sqldialects.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$" dialect="SQLite" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

9
TeleZab2.0.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.11 (TeleZab2.0)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

3
config.py Normal file
View File

@ -0,0 +1,3 @@
import os
BOT_TOKEN = os.getenv("BOT_TOKEN")
WHITELIST_CHAT_IDS = [211595028]

27
main.py Normal file
View File

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

7
main/admins.sql Normal file
View File

@ -0,0 +1,7 @@
create table admins
(
chat_id INTEGER
primary key,
username TEXT
);

10
main/events.sql Normal file
View File

@ -0,0 +1,10 @@
create table events
(
id INTEGER
primary key autoincrement,
hash TEXT
unique,
data TEXT,
delivered BOOLEAN
);

8
main/regions.sql Normal file
View File

@ -0,0 +1,8 @@
create table regions
(
region_id TEXT
primary key,
region_name TEXT,
active BOOLEAN default TRUE
);

10
main/subscriptions.sql Normal file
View File

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

10
main/user_events.sql Normal file
View File

@ -0,0 +1,10 @@
create table user_events
(
id INTEGER
primary key autoincrement,
chat_id INTEGER,
username TEXT,
action TEXT,
timestamp TEXT
);

8
main/whitelist.sql Normal file
View File

@ -0,0 +1,8 @@
create table whitelist
(
chat_id INTEGER
primary key,
username TEXT,
user_email TEXT
);

11
routers/__init__.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() # Завершаем состояние после выполнения действия

View File

@ -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("Вы не в списке разрешенных.")

27
utils/db/__init__.py Normal file
View File

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

10
utils/db/admins.py Normal file
View File

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

12
utils/db/events.py Normal file
View File

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

11
utils/db/regions.py Normal file
View File

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

22
utils/db/subscriptions.py Normal file
View File

@ -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) # Добавление этого столбца необходимо для уникального первичного ключа

13
utils/db/user_events.py Normal file
View File

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

9
utils/db/whitelist.py Normal file
View File

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

21
utils/get_regions.py Normal file
View File

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

17
utils/permissions.py Normal file
View File

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

View File

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

33
utils/subscribe.py Normal file
View File

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