├── frontend_service ├── templates │ ├── error.html │ ├── index.html │ └── anal.html ├── tailwind.css ├── tailwind.config.js ├── justfile ├── Cargo.toml ├── .sqlx │ ├── query-2b2d662b9231e94808963925c2a833bd4cce8dc022530fd431101d3c46eb3495.json │ ├── query-77e0affdcee96a93c7d8a8f27066718ad7b15e4b1fd83aeba7e8111f686d0892.json │ ├── query-72d9ad6396d3e5018b486adc6d72f55a51d5c6b8ae4a75ad4cd63a9f117911d5.json │ ├── query-cfd1b3376cf38d6c95de5b322b06e4ea46761616d08359fd7a0338c6772338d0.json │ ├── query-d58bea9c2e057f5bb802d8fa42f376d74788326776a860cfaf67a9f41c236f66.json │ └── query-b71fb7c351e79409048d0f56acddce1d5b525af4920017f49582050154dcc904.json ├── Dockerfile └── src │ └── main.rs ├── ura_button ├── README.md └── esp32_device │ ├── src │ ├── WifiController.h │ ├── IterablePreferences.h │ ├── main.cpp │ └── WifiController.cpp │ └── platformio.ini ├── bot_service ├── filters │ ├── __init__.py │ ├── user.py │ └── command_mention.py ├── handlers │ ├── groups │ │ ├── __init__.py │ │ ├── join.py │ │ └── control.py │ ├── channels │ │ ├── __init__.py │ │ ├── join.py │ │ └── control.py │ ├── friends │ │ ├── __init__.py │ │ ├── request.py │ │ └── control.py │ ├── user_properties │ │ ├── __init__.py │ │ ├── setnickname.py │ │ ├── export.py │ │ ├── autoend.py │ │ └── analytics.py │ ├── admin │ │ ├── __init__.py │ │ ├── send.py │ │ ├── degrade.py │ │ ├── ban.py │ │ ├── whois.py │ │ └── notify.py │ ├── start.py │ ├── cancel.py │ ├── __init__.py │ ├── report.py │ ├── srat.py │ ├── api.py │ └── info.py ├── Dockerfile ├── middlewares │ ├── __init__.py │ ├── db.py │ ├── util.py │ ├── throttling.py │ ├── degrade.py │ ├── auth.py │ ├── group.py │ └── channel.py └── main.py ├── README.md ├── brocker ├── __init__.py ├── export_info.py ├── base.py └── message_sender.py ├── utils ├── verify_name.py ├── generate_random_secret.py ├── find_button_by_callback.py ├── paged_keyboard.py └── send_srat_notification.py ├── api_services ├── api_middlewares │ ├── __init__.py │ └── auth.py └── srat_service │ ├── Dockerfile │ └── main.py ├── autoend_service ├── Dockerfile └── main.py ├── requirements.txt ├── export_info_service ├── Dockerfile └── main.py ├── send_message_service ├── Dockerfile └── main.py ├── db ├── __init__.py ├── fields.py ├── Notify.py ├── ToiletSessions.py ├── User.py ├── ApiAuth.py └── UserUnion.py ├── integrated.env ├── keyboards ├── whois_keyboard.py ├── sret_keyboard.py ├── srat_var_keyboard.py ├── group │ ├── join_group_keyboard.py │ └── groups_keyboard.py ├── friend │ ├── request_friend_keyboard.py │ └── friends_keyboard.py ├── notify_keyboard.py ├── api_keyboard.py ├── guide_keyboard.py └── channels_keyboard.py ├── nginx.conf ├── .github └── workflows │ └── deploy.yml ├── LICENSE ├── config.py ├── setup_logger.py ├── docker-compose.yml └── .gitignore /frontend_service/templates/error.html: -------------------------------------------------------------------------------- 1 | ЧЕТА НЕ ТАК ПОШЛО ИДИ НАХУЙ <3 2 | -------------------------------------------------------------------------------- /ura_button/README.md: -------------------------------------------------------------------------------- 1 | # YRA BUTTON 2 | Носимое устройство для отправки уведомлений в YRA. -------------------------------------------------------------------------------- /frontend_service/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /bot_service/filters/__init__.py: -------------------------------------------------------------------------------- 1 | from .command_mention import CommandMention 2 | from .user import UserAuthFilter -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **ARCHIVE** 2 | 3 | # [YPA Project](https://t.me/uragv_bot) 4 | Бот для того чтобы держать вкурсе, когда ты идешь ... 5 | 6 | -------------------------------------------------------------------------------- /brocker/__init__.py: -------------------------------------------------------------------------------- 1 | from . import base 2 | 3 | 4 | async def init(): 5 | await (await base.storer.get_connection()).channel() 6 | -------------------------------------------------------------------------------- /utils/verify_name.py: -------------------------------------------------------------------------------- 1 | def verify_name(name: str): 2 | for el in name: 3 | if not el.isnumeric() and not el.isalpha(): 4 | return False 5 | 6 | return True 7 | -------------------------------------------------------------------------------- /frontend_service/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./templates/**/*.html"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /bot_service/handlers/groups/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | 3 | from . import control 4 | from . import join 5 | 6 | router = Router() 7 | 8 | router.include_router(control.router) 9 | router.include_router(join.router) 10 | -------------------------------------------------------------------------------- /bot_service/handlers/channels/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | 3 | from . import control 4 | from . import join 5 | 6 | router = Router() 7 | 8 | router.include_router(control.router) 9 | router.include_router(join.router) 10 | -------------------------------------------------------------------------------- /bot_service/handlers/friends/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | 3 | from . import control 4 | from . import request 5 | 6 | router = Router() 7 | 8 | router.include_router(control.router) 9 | router.include_router(request.router) 10 | -------------------------------------------------------------------------------- /utils/generate_random_secret.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | import string 3 | 4 | letters = string.ascii_letters + string.digits 5 | 6 | 7 | def generate_random_secret(length: int) -> str: 8 | return ''.join(secrets.choice(letters) for _ in range(length)) 9 | -------------------------------------------------------------------------------- /api_services/api_middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from .auth import AuthMiddleware 4 | 5 | 6 | def setup(app: FastAPI, raise_unauthorized: bool = False): 7 | app.add_middleware(AuthMiddleware, raise_unauthorized=raise_unauthorized) 8 | -------------------------------------------------------------------------------- /bot_service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim-buster 2 | 3 | WORKDIR /app 4 | 5 | COPY ../requirements.txt requirements.txt 6 | RUN pip3 install -r requirements.txt 7 | 8 | 9 | COPY .. . 10 | 11 | WORKDIR /app/bot_service 12 | CMD ["python3.10", "-m", "main"] 13 | -------------------------------------------------------------------------------- /autoend_service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim-buster 2 | 3 | WORKDIR /app 4 | 5 | COPY ../requirements.txt requirements.txt 6 | RUN pip3 install -r requirements.txt 7 | 8 | 9 | COPY .. . 10 | 11 | WORKDIR /app/autoend_service 12 | CMD ["python3.10", "-m", "main"] 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram==3.3.0 2 | python-dotenv==1.0.1 3 | asyncpg==0.29.0 4 | redis==5.0.2 5 | tortoise-orm==0.20.0 6 | loguru==0.7.2 7 | aiormq==6.8.0 8 | sentry-sdk==2.10.0 9 | pytz==2024.1 10 | aiohttp==3.9.5 11 | pydantic==2.5.3 12 | fastapi==0.111.1 13 | starlette==0.37.2 -------------------------------------------------------------------------------- /export_info_service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim-buster 2 | 3 | WORKDIR /app 4 | 5 | COPY ../requirements.txt requirements.txt 6 | RUN pip3 install -r requirements.txt 7 | 8 | 9 | COPY .. . 10 | 11 | WORKDIR /app/export_info_service 12 | CMD ["python3.10", "-m", "main"] 13 | -------------------------------------------------------------------------------- /send_message_service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim-buster 2 | 3 | WORKDIR /app 4 | 5 | COPY ../requirements.txt requirements.txt 6 | RUN pip3 install -r requirements.txt 7 | 8 | 9 | COPY .. . 10 | 11 | WORKDIR /app/send_message_service 12 | CMD ["python3.10", "-m", "main"] 13 | -------------------------------------------------------------------------------- /api_services/srat_service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim-buster 2 | 3 | WORKDIR /app 4 | 5 | COPY ../requirements.txt requirements.txt 6 | RUN pip3 install -r requirements.txt 7 | 8 | 9 | COPY .. . 10 | 11 | WORKDIR /app/api_services/srat_service 12 | CMD ["fastapi", "run", "main.py", "--port", "80"] 13 | -------------------------------------------------------------------------------- /frontend_service/justfile: -------------------------------------------------------------------------------- 1 | set dotenv-load 2 | 3 | dev: 4 | watchexec -e rs,html -r just dev-no-watch 5 | 6 | dev-no-watch: 7 | tailwindcss -c tailwind.config.js -i ./tailwind.css -o ./public/styles.css 8 | cargo run 9 | 10 | build: 11 | tailwindcss -c tailwind.config.js -i ./tailwind.css -o ./public/styles.css --minify 12 | cargo build --release 13 | -------------------------------------------------------------------------------- /utils/find_button_by_callback.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 2 | 3 | 4 | def find_button_by_callback_data(reply_markup: InlineKeyboardMarkup, callback_data: str) -> InlineKeyboardButton: 5 | for line in reply_markup.inline_keyboard: 6 | for button in line: 7 | if button.callback_data == callback_data: 8 | return button 9 | -------------------------------------------------------------------------------- /bot_service/handlers/user_properties/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | 3 | from . import setnickname 4 | from . import autoend 5 | from . import analytics 6 | from . import export 7 | 8 | router = Router() 9 | 10 | router.include_router(setnickname.router) 11 | router.include_router(autoend.router) 12 | router.include_router(analytics.router) 13 | router.include_router(export.router) 14 | -------------------------------------------------------------------------------- /db/__init__.py: -------------------------------------------------------------------------------- 1 | import urllib.parse 2 | 3 | from tortoise import Tortoise 4 | 5 | from config import DB as cDB 6 | 7 | 8 | async def init(): 9 | await Tortoise.init( 10 | db_url=f"postgres://{cDB.user}:{urllib.parse.quote_plus(cDB.password)}@{cDB.host}:{cDB.port}/{cDB.db_name}", 11 | modules={"models": ['db.User', 'db.ToiletSessions', 'db.UserUnion', 'db.Notify', 'db.ApiAuth']} 12 | ) 13 | -------------------------------------------------------------------------------- /integrated.env: -------------------------------------------------------------------------------- 1 | DEBUG=FALSE 2 | 3 | DB_HOST=postgres 4 | DB_PORT=5432 5 | DB_NAME=urabot 6 | DB_USER=urabot 7 | DB_PASSWORD=urabot 8 | DATABASE_URL=postgresql://urabot:urabot@postgres/urabot 9 | 10 | REDIS_HOST=redis 11 | REDIS_PORT=6379 12 | REDIS_NAME=0 13 | REDIS_USER=urabot 14 | REDIS_PASSWORD=urabot 15 | 16 | AMQP_HOST=rabbitmq 17 | AMQP_PORT=5672 18 | AMQP_VHOST= 19 | AMQP_USER=urabot 20 | AMQP_PASSWORD=urabot -------------------------------------------------------------------------------- /keyboards/whois_keyboard.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 2 | from aiogram.utils.keyboard import InlineKeyboardBuilder 3 | 4 | 5 | def get(user_id: int) -> InlineKeyboardMarkup: 6 | kb = InlineKeyboardBuilder() 7 | 8 | kb.row(InlineKeyboardButton( 9 | text='Написать', 10 | url=f'tg://user?id={user_id}' 11 | )) 12 | 13 | return kb.as_markup() 14 | -------------------------------------------------------------------------------- /bot_service/handlers/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | 3 | from . import ban 4 | from . import notify 5 | from . import whois 6 | from . import send 7 | from . import degrade 8 | 9 | router = Router() 10 | 11 | router.include_router(ban.router) 12 | router.include_router(notify.router) 13 | router.include_router(whois.router) 14 | router.include_router(send.router) 15 | router.include_router(degrade.router) 16 | -------------------------------------------------------------------------------- /bot_service/handlers/start.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, F 2 | from aiogram import types 3 | from aiogram.filters import Command, MagicData 4 | 5 | from keyboards import srat_var_keyboard 6 | 7 | router = Router() 8 | 9 | 10 | @router.message(Command("start"), MagicData(~F.command.args)) 11 | async def start(message: types.Message): 12 | await message.reply('Выберете действие.', reply_markup=srat_var_keyboard.get()) 13 | -------------------------------------------------------------------------------- /ura_button/esp32_device/src/WifiController.h: -------------------------------------------------------------------------------- 1 | #ifndef WIFICONTROLLER_H 2 | #define WIFICONTROLLER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace WiFiController { 10 | extern std::map saved; 11 | 12 | void setup(AsyncWebServer *server); 13 | void connect(); 14 | void handle(); 15 | } 16 | 17 | #endif WIFICONTROLLER_H 18 | -------------------------------------------------------------------------------- /db/fields.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from tortoise import fields 4 | 5 | 6 | class PermissionField(fields.BooleanField): 7 | PERMISSION_FIELD = True 8 | 9 | def __init__(self): 10 | super().__init__(default=False) 11 | 12 | 13 | class AutoNowDatetimeField(fields.DatetimeField): 14 | def __init__(self, *args, **kwargs): 15 | kwargs.update(default=datetime.now) 16 | super().__init__(*args, **kwargs) 17 | -------------------------------------------------------------------------------- /keyboards/sret_keyboard.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 2 | from aiogram.utils.keyboard import InlineKeyboardBuilder 3 | 4 | 5 | def get(autoend: bool) -> InlineKeyboardMarkup: 6 | kb = InlineKeyboardBuilder() 7 | 8 | kb.add(InlineKeyboardButton( 9 | text=f'{["Включить", "Выключить"][autoend]} автозавершение сранья', 10 | callback_data='chg_aend_srat' 11 | )) 12 | 13 | return kb.as_markup() 14 | -------------------------------------------------------------------------------- /frontend_service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ura-web" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | askama = { version = "0.12.1", features = ["with-axum"] } 8 | askama_axum = "0.4.0" 9 | axum = "0.7.5" 10 | serde = { version = "1.0.204", features = ["derive"] } 11 | sqlx = { version = "0.7.4", features = ["postgres", "runtime-tokio"] } 12 | tokio = { version = "1.38.1", features = ["full"] } 13 | tower-http = { version = "0.5.2", features = ["fs"] } 14 | -------------------------------------------------------------------------------- /db/Notify.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields 2 | from tortoise.models import Model 3 | 4 | from db.fields import AutoNowDatetimeField 5 | 6 | 7 | class Notify(Model): 8 | message_id = fields.BigIntField(pk=True, unique=True) 9 | 10 | initiated_by = fields.ForeignKeyField('models.User', related_name='notifys_inited') 11 | created_at = AutoNowDatetimeField() 12 | 13 | scheduled_users_count = fields.IntField() 14 | executed_users_count = fields.IntField(default=0) 15 | -------------------------------------------------------------------------------- /bot_service/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Dispatcher 2 | 3 | from .throttling import ThrottlingMiddleware 4 | from .auth import AuthMiddleware 5 | from .db import DatabaseMiddleware 6 | from .degrade import DegradationMiddleware 7 | 8 | 9 | def setup(dp: Dispatcher): 10 | dp.update.middleware.register(ThrottlingMiddleware()) 11 | dp.update.middleware.register(DatabaseMiddleware()) 12 | dp.update.middleware.register(AuthMiddleware()) 13 | dp.update.middleware.register(DegradationMiddleware()) 14 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | 3 | } 4 | 5 | http { 6 | error_log /var/log/nginx/error.log info; 7 | access_log /var/log/nginx/access.log; 8 | 9 | server { 10 | listen 80; 11 | 12 | location / { 13 | proxy_pass "http://frontend-service:3000"; 14 | } 15 | 16 | location /webhook/ { 17 | proxy_pass "http://bot-service:8375"; 18 | } 19 | 20 | location /api/srat/ { 21 | proxy_pass "http://api-srat-service:80"; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /bot_service/filters/user.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters import BaseFilter 2 | from aiogram.types import Message 3 | 4 | from db.User import User 5 | 6 | 7 | class UserAuthFilter(BaseFilter): 8 | def __init__(self, 9 | admin: bool = None, 10 | ): 11 | self.admin = admin 12 | 13 | async def __call__(self, message: Message, user: User) -> bool: 14 | if user is None: 15 | return False 16 | 17 | if self.admin: 18 | return user.admin 19 | 20 | return True 21 | -------------------------------------------------------------------------------- /bot_service/middlewares/db.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import Any, Dict, Callable 3 | 4 | import tortoise.transactions 5 | from aiogram.types import Update 6 | 7 | from bot_service.middlewares.util import UtilMiddleware 8 | 9 | 10 | class DatabaseMiddleware(UtilMiddleware, ABC): 11 | async def __call__( 12 | self, 13 | handler: Callable, 14 | event: Update, 15 | data: Dict[str, Any] 16 | ) -> Any: 17 | async with tortoise.transactions.in_transaction(): 18 | return await handler(event, data) 19 | -------------------------------------------------------------------------------- /brocker/export_info.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from loguru import logger 4 | 5 | from . import base 6 | 7 | 8 | async def export_info(send_to: int, user_id: int): 9 | channel = await base.storer.get_channel() 10 | 11 | body = json.dumps({ 12 | "send_to_user_id": send_to, 13 | "user_id": user_id, 14 | 15 | }, separators=(',', ':')).encode() 16 | 17 | await channel.basic_publish( 18 | body, 19 | routing_key='export_info', 20 | ) 21 | 22 | logger.debug(f'Booked export info to {send_to} user_id {user_id}.') 23 | 24 | -------------------------------------------------------------------------------- /keyboards/srat_var_keyboard.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from aiogram.types import ReplyKeyboardMarkup 4 | from aiogram.utils.keyboard import ReplyKeyboardBuilder 5 | 6 | 7 | class SretActions(Enum): 8 | SRET = (1, 'Я иду срать') 9 | DRISHET = (2, 'Я иду ЛЮТЕЙШЕ ДРИСТАТЬ') 10 | PERNUL = (3, 'Я просто пернул') 11 | END = (0, 'Я закончил срать') 12 | 13 | 14 | def get() -> ReplyKeyboardMarkup: 15 | kb = ReplyKeyboardBuilder() 16 | 17 | for el in SretActions: 18 | kb.button(text=el.value[1]) 19 | 20 | kb.adjust(2) 21 | 22 | return kb.as_markup() 23 | -------------------------------------------------------------------------------- /db/ToiletSessions.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | from tortoise import fields 4 | from tortoise.models import Model 5 | 6 | from db.fields import AutoNowDatetimeField 7 | 8 | 9 | class SretType(IntEnum): 10 | SRET = 1 11 | DRISHET = 2 12 | PERNUL = 3 13 | 14 | 15 | class SretSession(Model): 16 | message_id = fields.BigIntField() 17 | user = fields.ForeignKeyField('models.User', index=True) 18 | 19 | start = AutoNowDatetimeField(index=True) 20 | end = fields.DatetimeField(null=True, default=None) 21 | 22 | autoend = fields.BooleanField() 23 | sret_type = fields.IntEnumField(SretType) 24 | -------------------------------------------------------------------------------- /ura_button/esp32_device/platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:nodemcu-32s] 12 | platform = espressif32 13 | board = nodemcu-32s 14 | framework = arduino 15 | 16 | monitor_speed = 115200 17 | 18 | lib_deps = 19 | Bounce2 20 | ArduinoJson 21 | ESP Async WebServer 22 | -------------------------------------------------------------------------------- /bot_service/middlewares/util.py: -------------------------------------------------------------------------------- 1 | import aiogram 2 | from aiogram import BaseMiddleware 3 | from aiogram.types import Update 4 | 5 | 6 | class UtilMiddleware(BaseMiddleware): 7 | def get_user(self, event: Update): 8 | if event.message: 9 | return event.message.from_user 10 | 11 | elif event.callback_query: 12 | return event.callback_query.from_user 13 | 14 | elif event.inline_query: 15 | return event.inline_query.from_user 16 | 17 | elif event.chosen_inline_result: 18 | return event.chosen_inline_result.from_user 19 | 20 | elif event.chat_member: 21 | return event.chat_member.from_user 22 | -------------------------------------------------------------------------------- /bot_service/handlers/cancel.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from aiogram import Router 4 | from aiogram import types 5 | from aiogram.filters import Command 6 | from aiogram.fsm.context import FSMContext 7 | 8 | import config 9 | 10 | router = Router() 11 | 12 | 13 | @router.message(Command("cancel")) 14 | async def cancel(message: types.Message, state: FSMContext): 15 | last_msg = (await state.get_data()).get('last_msg') 16 | await state.clear() 17 | 18 | info_message = await message.reply('Отменили.') 19 | if last_msg is not None: 20 | await config.bot.delete_message(message.chat.id, last_msg) 21 | 22 | await asyncio.sleep(3) 23 | await message.delete() 24 | await info_message.delete() 25 | -------------------------------------------------------------------------------- /frontend_service/.sqlx/query-2b2d662b9231e94808963925c2a833bd4cce8dc022530fd431101d3c46eb3495.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "SELECT sret_type, COUNT(*) count FROM sretsession GROUP BY sret_type", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "sret_type", 9 | "type_info": "Int2" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "count", 14 | "type_info": "Int8" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [] 19 | }, 20 | "nullable": [ 21 | false, 22 | null 23 | ] 24 | }, 25 | "hash": "2b2d662b9231e94808963925c2a833bd4cce8dc022530fd431101d3c46eb3495" 26 | } 27 | -------------------------------------------------------------------------------- /frontend_service/.sqlx/query-77e0affdcee96a93c7d8a8f27066718ad7b15e4b1fd83aeba7e8111f686d0892.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "SELECT u.name, COUNT(*) count FROM sretsession as s JOIN \"user\" u ON u.uid = s.user_id GROUP BY u.name ORDER BY COUNT(*) DESC LIMIT 10", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "name", 9 | "type_info": "Varchar" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "count", 14 | "type_info": "Int8" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [] 19 | }, 20 | "nullable": [ 21 | false, 22 | null 23 | ] 24 | }, 25 | "hash": "77e0affdcee96a93c7d8a8f27066718ad7b15e4b1fd83aeba7e8111f686d0892" 26 | } 27 | -------------------------------------------------------------------------------- /frontend_service/.sqlx/query-72d9ad6396d3e5018b486adc6d72f55a51d5c6b8ae4a75ad4cd63a9f117911d5.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "SELECT u.name, COUNT(*) count FROM sretsession as s JOIN \"user\" u on u.uid = s.user_id WHERE s.sret_type = 3 GROUP BY u.name ORDER BY COUNT(*) DESC", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "name", 9 | "type_info": "Varchar" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "count", 14 | "type_info": "Int8" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [] 19 | }, 20 | "nullable": [ 21 | false, 22 | null 23 | ] 24 | }, 25 | "hash": "72d9ad6396d3e5018b486adc6d72f55a51d5c6b8ae4a75ad4cd63a9f117911d5" 26 | } 27 | -------------------------------------------------------------------------------- /frontend_service/.sqlx/query-cfd1b3376cf38d6c95de5b322b06e4ea46761616d08359fd7a0338c6772338d0.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "SELECT u.name, COUNT(*) count FROM sretsession as s JOIN \"user\" u on u.uid = s.user_id WHERE s.sret_type IN (1, 2) GROUP BY u.name ORDER BY COUNT(*) DESC", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "name", 9 | "type_info": "Varchar" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "count", 14 | "type_info": "Int8" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [] 19 | }, 20 | "nullable": [ 21 | false, 22 | null 23 | ] 24 | }, 25 | "hash": "cfd1b3376cf38d6c95de5b322b06e4ea46761616d08359fd7a0338c6772338d0" 26 | } 27 | -------------------------------------------------------------------------------- /frontend_service/.sqlx/query-d58bea9c2e057f5bb802d8fa42f376d74788326776a860cfaf67a9f41c236f66.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "SELECT u.name, COUNT(*) count FROM sretsession as s JOIN \"user\" u on u.uid = s.user_id WHERE s.sret_type = 3 GROUP BY u.name ORDER BY COUNT(*) DESC LIMIT 10", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "name", 9 | "type_info": "Varchar" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "count", 14 | "type_info": "Int8" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [] 19 | }, 20 | "nullable": [ 21 | false, 22 | null 23 | ] 24 | }, 25 | "hash": "d58bea9c2e057f5bb802d8fa42f376d74788326776a860cfaf67a9f41c236f66" 26 | } 27 | -------------------------------------------------------------------------------- /frontend_service/.sqlx/query-b71fb7c351e79409048d0f56acddce1d5b525af4920017f49582050154dcc904.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "SELECT u.name, COUNT(*) count FROM sretsession as s JOIN \"user\" u on u.uid = s.user_id WHERE s.sret_type IN (1, 2) GROUP BY u.name ORDER BY COUNT(*) DESC LIMIT 10", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "name", 9 | "type_info": "Varchar" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "count", 14 | "type_info": "Int8" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [] 19 | }, 20 | "nullable": [ 21 | false, 22 | null 23 | ] 24 | }, 25 | "hash": "b71fb7c351e79409048d0f56acddce1d5b525af4920017f49582050154dcc904" 26 | } 27 | -------------------------------------------------------------------------------- /bot_service/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | 3 | from . import cancel 4 | from . import admin 5 | from . import info 6 | from . import groups 7 | from . import friends 8 | from . import srat 9 | from . import start 10 | from . import user_properties 11 | from . import report 12 | from . import api 13 | from . import channels 14 | 15 | router = Router() 16 | 17 | router.include_router(cancel.router) 18 | router.include_router(start.router) 19 | router.include_router(srat.router) 20 | router.include_router(info.router) 21 | router.include_router(admin.router) 22 | router.include_router(groups.router) 23 | router.include_router(channels.router) 24 | router.include_router(friends.router) 25 | router.include_router(user_properties.router) 26 | router.include_router(report.router) 27 | router.include_router(api.router) 28 | -------------------------------------------------------------------------------- /keyboards/group/join_group_keyboard.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters.callback_data import CallbackData 2 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 3 | from aiogram.utils.keyboard import InlineKeyboardBuilder 4 | 5 | 6 | class JoinGroupCallback(CallbackData, prefix="grpj"): 7 | uid: int 8 | group_id: int 9 | result: bool 10 | 11 | 12 | def get(uid: int, group_id: int) -> InlineKeyboardMarkup: 13 | kb = InlineKeyboardBuilder() 14 | 15 | kb.add(InlineKeyboardButton( 16 | text='Отклонить', 17 | callback_data=JoinGroupCallback(uid=uid, group_id=group_id, result=False).pack() 18 | )) 19 | 20 | kb.add(InlineKeyboardButton( 21 | text='Принять', 22 | callback_data=JoinGroupCallback(uid=uid, group_id=group_id, result=True).pack() 23 | )) 24 | 25 | return kb.as_markup() 26 | -------------------------------------------------------------------------------- /brocker/base.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import aiormq 4 | from aiormq.abc import AbstractConnection, AbstractChannel 5 | 6 | import config 7 | 8 | 9 | class ConnectionStorer: 10 | _connection: Optional[AbstractConnection] = None 11 | _channel: Optional[AbstractChannel] = None 12 | 13 | async def get_connection(self) -> Optional[AbstractConnection]: 14 | if self._connection is None or self._connection.is_closed: 15 | self._connection = await aiormq.connect(config.AMQP.uri) 16 | 17 | return self._connection 18 | 19 | async def get_channel(self) -> Optional[AbstractChannel]: 20 | if self._channel is None or self._channel.is_closed: 21 | self._channel = await (await self.get_connection()).channel() 22 | return self._channel 23 | 24 | 25 | storer = ConnectionStorer() 26 | -------------------------------------------------------------------------------- /db/User.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields 2 | from tortoise.models import Model 3 | 4 | from db.fields import PermissionField, AutoNowDatetimeField 5 | 6 | 7 | class User(Model): 8 | uid = fields.BigIntField(pk=True, unique=True) 9 | name = fields.CharField(max_length=129) 10 | admin = PermissionField() 11 | 12 | friends = fields.ManyToManyField('models.User', related_name='friend_with', through='friend_user') 13 | mute_friend_requests = fields.BooleanField(default=False) 14 | 15 | autoend = fields.BooleanField(default=True) 16 | autoend_time = fields.SmallIntField(default=10) 17 | 18 | created_at = AutoNowDatetimeField() 19 | 20 | 21 | class Ban(Model): 22 | uid = fields.BigIntField(pk=True, unique=True) 23 | banned_by = fields.BigIntField(null=True) 24 | reason = fields.TextField() 25 | 26 | created_at = AutoNowDatetimeField() 27 | -------------------------------------------------------------------------------- /bot_service/filters/command_mention.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot 2 | from aiogram.filters import Command, BaseFilter 3 | from aiogram.filters.command import CommandException 4 | from aiogram.types import Message 5 | 6 | import config 7 | 8 | 9 | class CommandMention(BaseFilter): 10 | def __init__(self, *args, **kwargs): 11 | self.command = Command(*args, **kwargs) 12 | 13 | async def __call__(self, message: Message, bot: Bot): 14 | ping = f'@{config.bot_me.username}' 15 | 16 | text = message.text 17 | if text is None: 18 | text = '' 19 | 20 | if text.startswith(ping): 21 | text = ' '.join(message.text.split()[1:]) 22 | 23 | try: 24 | command = await self.command.parse_command(text=text, bot=bot) 25 | 26 | except CommandException: 27 | return False 28 | 29 | return {'command': command} 30 | -------------------------------------------------------------------------------- /keyboards/friend/request_friend_keyboard.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters.callback_data import CallbackData 2 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 3 | from aiogram.utils.keyboard import InlineKeyboardBuilder 4 | 5 | 6 | class ActionRequestUserCallback(CallbackData, prefix="aru"): 7 | uid: int 8 | requested_uid: int 9 | result: bool 10 | 11 | 12 | def get(uid: int, requested_uid: int) -> InlineKeyboardMarkup: 13 | kb = InlineKeyboardBuilder() 14 | 15 | kb.add(InlineKeyboardButton( 16 | text='Отклонить', 17 | callback_data=ActionRequestUserCallback(uid=uid, requested_uid=requested_uid, result=False).pack() 18 | )) 19 | 20 | kb.add(InlineKeyboardButton( 21 | text='Принять', 22 | callback_data=ActionRequestUserCallback(uid=uid, requested_uid=requested_uid, result=True).pack() 23 | )) 24 | 25 | return kb.as_markup() 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to server 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout Code 13 | uses: actions/checkout@v2 14 | 15 | - name: Netbird Connect 16 | id: netbird 17 | uses: Alemiz112/netbird-connect@v1 18 | with: 19 | setup-key: ${{ secrets.NETBIRD_SETUP_KEY }} 20 | hostname: 'deploy-ura' 21 | management-url: 'https://api.netbird.io' 22 | 23 | - name: Deploy 24 | uses: appleboy/ssh-action@master 25 | with: 26 | host: ${{ secrets.SSH_HOST }} 27 | username: ${{ secrets.SSH_USER }} 28 | password: ${{ secrets.SSH_PASSWORD }} 29 | script: | 30 | cd ura/project-ura 31 | git fetch 32 | git pull 33 | docker compose down 34 | docker compose up -d --build -------------------------------------------------------------------------------- /bot_service/middlewares/throttling.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from datetime import datetime 3 | from typing import Any, Dict, Callable 4 | 5 | from aiogram.types import Update 6 | 7 | import config 8 | from bot_service.middlewares.util import UtilMiddleware 9 | 10 | 11 | class ThrottlingMiddleware(UtilMiddleware, ABC): 12 | async def __call__( 13 | self, 14 | handler: Callable, 15 | event: Update, 16 | data: Dict[str, Any] 17 | ) -> Any: 18 | user_id = self.get_user(event).id 19 | 20 | last_act = await config.storage.redis.get(user_id) 21 | if last_act is None: 22 | last_act = 0 23 | last_act = float(last_act) 24 | 25 | now = datetime.now().timestamp() 26 | if (now - last_act) <= config.Constants.throttling_time: 27 | return 28 | 29 | await config.storage.redis.set(user_id, now) 30 | return await handler(event, data) 31 | -------------------------------------------------------------------------------- /ura_button/esp32_device/src/IterablePreferences.h: -------------------------------------------------------------------------------- 1 | #ifndef ITERABLEPREFERENCES_H 2 | #define ITERABLEPREFERENCES_H 3 | 4 | #include 5 | #include 6 | 7 | class IterablePreferences : public Preferences { 8 | public: 9 | IterablePreferences() = default; 10 | 11 | using Callback = void(const char* key, nvs_type_t type); 12 | void foreach (const char* nvNamespace, Callback cb, const char* nvPartition = "nvs") { 13 | nvs_iterator_t it = nvs_entry_find(nvPartition, nvNamespace, NVS_TYPE_ANY); 14 | while (it) { 15 | nvs_entry_info_t info{}; 16 | nvs_entry_info(it, &info); // Can omit error check if parameters are guaranteed to be non-NULL 17 | if (cb) { 18 | cb(info.key, info.type); 19 | } 20 | it = nvs_entry_next(it); 21 | } 22 | nvs_release_iterator(it); 23 | } 24 | }; 25 | 26 | #endif ITERABLEPREFERENCES_H 27 | -------------------------------------------------------------------------------- /api_services/api_middlewares/auth.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | from starlette.middleware.base import BaseHTTPMiddleware 3 | from starlette.responses import JSONResponse 4 | 5 | from db.ApiAuth import ApiToken 6 | 7 | 8 | class AuthMiddleware(BaseHTTPMiddleware): 9 | def __init__(self, app, raise_unauthorized: bool = False): 10 | super().__init__(app) 11 | self.raise_unauthorized = raise_unauthorized 12 | 13 | async def dispatch(self, request: Request, call_next): 14 | token = await ApiToken.get_or_none(token=ApiToken.hash_token(request.headers.get('Authorization', ''))) 15 | if token is None: 16 | request.state.user = None 17 | if self.raise_unauthorized: 18 | return JSONResponse({"detail": "Failed auth by token"}, status_code=401) 19 | 20 | else: 21 | request.state.user = await token.owner 22 | 23 | response = await call_next(request) 24 | return response 25 | -------------------------------------------------------------------------------- /bot_service/handlers/user_properties/setnickname.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | from aiogram import types 3 | from aiogram.filters import Command, CommandObject 4 | 5 | from db.User import User 6 | from utils.verify_name import verify_name 7 | 8 | router = Router() 9 | 10 | 11 | @router.message(Command("setnickname")) 12 | async def setnickname(message: types.Message, command: CommandObject, user: User): 13 | if command.args is None: 14 | await message.reply('Пример:\n/setnickname <name>') 15 | return 16 | 17 | if len(command.args) > 129: 18 | await message.reply('Имя должно быть не длиннее 129 символов.') 19 | return 20 | 21 | if not verify_name(command.args.replace(' ', '')): 22 | await message.reply('Имя не должно содержать специальные символы.') 23 | return 24 | 25 | user.name = command.args 26 | await user.save() 27 | 28 | await message.reply(f'Ваш никнейм установлен на {user.name}') 29 | -------------------------------------------------------------------------------- /keyboards/notify_keyboard.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters.callback_data import CallbackData 2 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 3 | from aiogram.utils.keyboard import InlineKeyboardBuilder 4 | 5 | 6 | class Notify(CallbackData, prefix="not"): 7 | action: str 8 | 9 | 10 | def get(only_cancel: bool = False) -> InlineKeyboardMarkup: 11 | kb = InlineKeyboardBuilder() 12 | 13 | kb.add(InlineKeyboardButton( 14 | text='Отменить', 15 | callback_data=Notify(action='cancel').pack() 16 | )) 17 | 18 | if not only_cancel: 19 | kb.add(InlineKeyboardButton( 20 | text='Подтвердить', 21 | callback_data=Notify(action='submit').pack() 22 | )) 23 | 24 | return kb.as_markup() 25 | 26 | 27 | def get_update() -> InlineKeyboardMarkup: 28 | kb = InlineKeyboardBuilder() 29 | 30 | kb.add(InlineKeyboardButton( 31 | text=f'Обновить', 32 | callback_data=Notify(action='update').pack() 33 | )) 34 | 35 | return kb.as_markup() 36 | -------------------------------------------------------------------------------- /bot_service/middlewares/degrade.py: -------------------------------------------------------------------------------- 1 | import json 2 | from abc import ABC 3 | from typing import Any, Dict, Callable 4 | 5 | 6 | from aiogram.types import Update 7 | from loguru import logger 8 | from pydantic import BaseModel 9 | 10 | import config 11 | from bot_service.middlewares.util import UtilMiddleware 12 | 13 | 14 | class DegradationData(BaseModel): 15 | admin_only: bool = False 16 | enable_groups: bool = True 17 | enable_channels: bool = True 18 | 19 | 20 | class DegradationMiddleware(UtilMiddleware, ABC): 21 | async def __call__( 22 | self, 23 | handler: Callable, 24 | event: Update, 25 | data: Dict[str, Any] 26 | ) -> Any: 27 | degrade = DegradationData(**json.loads(await config.storage.redis.get('degrade'))) 28 | data['degrade'] = degrade 29 | 30 | if degrade.admin_only and not data['user'].admin: 31 | return 32 | 33 | if degrade.admin_only: 34 | logger.info('Pass admin under "admin_only" degradation') 35 | 36 | return await handler(event, data) 37 | -------------------------------------------------------------------------------- /utils/paged_keyboard.py: -------------------------------------------------------------------------------- 1 | from typing import List, Type, Tuple 2 | 3 | from aiogram.filters.callback_data import CallbackData 4 | from aiogram.types import InlineKeyboardButton 5 | 6 | 7 | class PagedCallbackData(CallbackData, prefix=''): 8 | page: int 9 | 10 | 11 | def draw_page_navigation( 12 | items: List, 13 | page: int, 14 | page_size: int, 15 | page_callback_data_class: Type[PagedCallbackData], 16 | **page_kwargs, 17 | ) -> Tuple[List[InlineKeyboardButton], int]: 18 | buttons = [] 19 | 20 | i = 0 21 | if page != 0: 22 | i += 1 23 | buttons.append(InlineKeyboardButton( 24 | text='◀', 25 | callback_data=page_callback_data_class(page=page - 1, **page_kwargs).pack() 26 | )) 27 | 28 | if len(items) == page_size + 1: 29 | i += 1 30 | buttons.append(InlineKeyboardButton( 31 | text='▶', 32 | callback_data=page_callback_data_class(page=page + 1, **page_kwargs).pack() 33 | )) 34 | items.pop() 35 | 36 | return buttons, i 37 | -------------------------------------------------------------------------------- /bot_service/handlers/admin/send.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | from aiogram import types 3 | from aiogram.filters import CommandObject 4 | 5 | import config 6 | from bot_service.filters import CommandMention 7 | from bot_service.filters import UserAuthFilter 8 | 9 | router = Router() 10 | 11 | 12 | @router.message(CommandMention("send"), UserAuthFilter(admin=True)) 13 | async def send(message: types.Message, command: CommandObject): 14 | args = command.args 15 | if args is None: 16 | args = '' 17 | args = args.split() 18 | 19 | if len(args) < 2: 20 | await message.reply('Пример:\n/send <user_id> <message>') 21 | return 22 | 23 | if not args[0].isnumeric(): 24 | await message.reply('Айди должно быть числом.') 25 | return 26 | 27 | try: 28 | await config.bot.send_message(args[0], '❗️ Сообщение от админов:\n' + ' '.join(args[1:])) 29 | await message.reply('Сообщение отправлено!') 30 | 31 | except: 32 | await message.reply('Не удалось отправить сообщение.') 33 | -------------------------------------------------------------------------------- /brocker/message_sender.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Optional 3 | 4 | import aiormq 5 | from loguru import logger 6 | 7 | from . import base 8 | 9 | 10 | async def send_message( 11 | send_to: int, 12 | forward_message_chat: int, 13 | forward_message: int, 14 | priority: int = 0, 15 | notify_id: Optional[int] = None, 16 | show_sender: bool = False 17 | ): 18 | channel = await base.storer.get_channel() 19 | 20 | body = json.dumps({ 21 | "send_to": send_to, 22 | "forward_message_chat": forward_message_chat, 23 | "forward_message": forward_message, 24 | "notify_id": notify_id, 25 | "show_sender": show_sender, 26 | 27 | }, separators=(',', ':')).encode() 28 | 29 | await channel.basic_publish( 30 | body, 31 | routing_key='send_message', 32 | properties=aiormq.spec.Basic.Properties(priority=priority) 33 | ) 34 | 35 | logger.debug( 36 | f'Booked message to {send_to} from chat {forward_message_chat} message {forward_message} prioriy {priority}.') 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Stepan Khozhempo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bot_service/middlewares/auth.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import Any, Dict, Callable 3 | 4 | from aiogram import types 5 | 6 | import config 7 | from db.User import User, Ban 8 | from bot_service.middlewares.util import UtilMiddleware 9 | 10 | 11 | first_join_message_text = '''Добро пожаловать в УРА! 12 | 13 | Команды: 14 | /guide - Гайд по боту 15 | /credits - О боте''' 16 | 17 | 18 | class AuthMiddleware(UtilMiddleware, ABC): 19 | async def __call__( 20 | self, 21 | handler: Callable, 22 | event: types.Update, 23 | data: Dict[str, Any] 24 | ) -> Any: 25 | user = self.get_user(event) 26 | 27 | if await Ban.filter(uid=user.id).exists(): 28 | return 29 | 30 | db_user = await User.filter(uid=user.id).get_or_none() 31 | 32 | if db_user is None: 33 | db_user = await User.create(uid=user.id, name=user.full_name) 34 | try: 35 | await config.bot.send_message(user.id, first_join_message_text) 36 | 37 | except: 38 | ... 39 | 40 | data['user'] = db_user 41 | 42 | return await handler(event, data) 43 | -------------------------------------------------------------------------------- /frontend_service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1 as build 2 | 3 | RUN apt-get update && apt-get install -y libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/* 4 | 5 | RUN curl -fsSL https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.6/tailwindcss-linux-x64 -o /usr/local/bin/tailwindcss 6 | RUN chmod +x /usr/local/bin/tailwindcss 7 | 8 | RUN cargo new --bin /app 9 | 10 | WORKDIR /app 11 | 12 | COPY ./Cargo.toml ./Cargo.lock ./ 13 | 14 | RUN cargo build --release 15 | 16 | RUN rm src/*.rs 17 | 18 | COPY ./src ./src 19 | COPY ./.sqlx ./.sqlx 20 | COPY ./templates ./templates 21 | COPY ./tailwind.css ./ 22 | COPY ./tailwind.config.js ./ 23 | 24 | RUN mkdir ./public 25 | 26 | RUN rm ./target/release/deps/ura_web* 27 | 28 | RUN tailwindcss -c tailwind.config.js -i ./tailwind.css -o ./public/styles.css --minify 29 | RUN cargo build --release 30 | 31 | FROM debian:stable-slim as runner 32 | 33 | WORKDIR /app 34 | 35 | RUN apt-get update && apt-get install -y libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/* 36 | 37 | COPY --from=build /app/target/release/ura-web . 38 | COPY --from=build /app/public/styles.css ./public/styles.css 39 | 40 | CMD ["./ura-web"] 41 | -------------------------------------------------------------------------------- /bot_service/middlewares/group.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import Any, Dict, Callable 3 | 4 | from aiogram import types 5 | 6 | from db.UserUnion import Group 7 | from keyboards.group import groups_keyboard 8 | from bot_service.middlewares.util import UtilMiddleware 9 | 10 | 11 | class GroupMiddleware(UtilMiddleware, ABC): 12 | async def __call__( 13 | self, 14 | handler: Callable, 15 | event: types.Update, 16 | data: Dict[str, Any] 17 | ) -> Any: 18 | try: 19 | callback = groups_keyboard.GroupCallback.unpack(event.data) 20 | if callback.group == -1: 21 | raise ModuleNotFoundError 22 | 23 | group = await Group.filter(pk=callback.group).get_or_none() 24 | if group is None: 25 | await event.answer('Группы не существует.', show_alert=True) 26 | await event.message.edit_text('Выберете группу.', 27 | reply_markup=await groups_keyboard.get_all(data['user'])) 28 | return 29 | 30 | data['group'] = group 31 | 32 | except (ModuleNotFoundError, TypeError): 33 | ... 34 | 35 | return await handler(event, data) 36 | -------------------------------------------------------------------------------- /bot_service/handlers/user_properties/export.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from aiogram import Router 4 | from aiogram import types 5 | from aiogram.filters import Command, CommandObject 6 | 7 | import config 8 | from brocker.export_info import export_info 9 | from db.User import User 10 | 11 | router = Router() 12 | 13 | 14 | @router.message(Command("export")) 15 | async def export(message: types.Message, command: CommandObject, user: User): 16 | user_id = user.uid 17 | if user.admin and command.args is not None and command.args.isnumeric(): 18 | user_id = int(command.args) 19 | 20 | if not user.admin: 21 | key = f'{user.uid}_export' 22 | last_export = await config.storage.redis.get(key) 23 | if last_export is None: 24 | last_export = 0 25 | last_export = int(last_export) 26 | 27 | now = int(datetime.now().timestamp()) 28 | 29 | if (now - last_export) < config.Constants.export_info_interval: 30 | await message.answer('Вы можете запрашивать экспорт информации не чаще чем раз в неделю.') 31 | return 32 | 33 | await config.storage.redis.set(key, now) 34 | 35 | await export_info(user.uid, user_id) 36 | await message.answer('Экспорт информации запрошен. Ожидайте.') 37 | -------------------------------------------------------------------------------- /bot_service/handlers/user_properties/autoend.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | from aiogram import types 3 | from aiogram.filters import Command, CommandObject 4 | 5 | import config 6 | from db.User import User 7 | 8 | router = Router() 9 | 10 | 11 | @router.message(Command("switchdefaultautoend")) 12 | async def switchdefaultautoend(message: types.Message, user: User): 13 | user.autoend = not user.autoend 14 | await user.save() 15 | 16 | await message.reply(f'{["Выключили", "Включили"][user.autoend]} автозавершение по умолчанию.') 17 | 18 | 19 | @router.message(Command("setautoendtime")) 20 | async def setautoendtime(message: types.Message, command: CommandObject, user: User): 21 | args = command.args 22 | if args is None: 23 | args = '' 24 | 25 | if not args.isnumeric(): 26 | await message.reply('Пример:\n/setautoendtime <time in minutes>') 27 | return 28 | 29 | if not(3 <= int(args) <= config.Constants.srat_delete_time): 30 | await message.reply(f'Время автозавершения должно быть не меньше 3 и не больше {config.Constants.srat_delete_time} минут.') 31 | return 32 | 33 | user.autoend_time = int(args) 34 | await user.save() 35 | 36 | await message.reply(f'Теперь ваше время автозавершения сранья: {args} минут!') 37 | -------------------------------------------------------------------------------- /ura_button/esp32_device/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "WifiController.h" 7 | 8 | namespace PIN { 9 | constexpr unsigned int BUTTON_1 = 33; 10 | constexpr unsigned int BUTTON_2 = 25; 11 | constexpr unsigned int BUTTON_3 = 26; 12 | 13 | constexpr unsigned int LED_1 = 12; 14 | constexpr unsigned int LED_2 = 14; 15 | constexpr unsigned int LED_3 = 27; 16 | } 17 | 18 | Bounce2::Button button1 = Bounce2::Button(); 19 | Bounce2::Button button2 = Bounce2::Button(); 20 | Bounce2::Button button3 = Bounce2::Button(); 21 | 22 | AsyncWebServer webServer(80); 23 | 24 | void setup() { 25 | pinMode(PIN::LED_1, OUTPUT); 26 | pinMode(PIN::LED_2, OUTPUT); 27 | pinMode(PIN::LED_3, OUTPUT); 28 | 29 | Serial.begin(115200); 30 | 31 | button1.attach(PIN::BUTTON_1, INPUT_PULLUP); 32 | button1.interval(5); 33 | button1.setPressedState(false); 34 | 35 | button2.attach(PIN::BUTTON_2, INPUT_PULLUP); 36 | button2.interval(5); 37 | button2.setPressedState(false); 38 | 39 | button3.attach(PIN::BUTTON_3, INPUT_PULLUP); 40 | button3.interval(5); 41 | button3.setPressedState(false); 42 | 43 | WiFiController::setup(&webServer); 44 | 45 | webServer.begin(); 46 | } 47 | 48 | void loop() { 49 | WiFiController::handle(); 50 | } -------------------------------------------------------------------------------- /keyboards/api_keyboard.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from aiogram.filters.callback_data import CallbackData 4 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 5 | from aiogram.utils.keyboard import InlineKeyboardBuilder 6 | 7 | from db.User import User 8 | 9 | 10 | class ApiCallback(CallbackData, prefix="api"): 11 | action: str 12 | token: Optional[str] = None 13 | 14 | 15 | async def get(user: User) -> InlineKeyboardMarkup: 16 | kb = InlineKeyboardBuilder() 17 | 18 | kb.row(InlineKeyboardButton( 19 | text='+ Новый токен', 20 | callback_data=ApiCallback(action='new').pack() 21 | )) 22 | 23 | async for token in user.tokens_owned.filter(valid=True): 24 | kb.row(InlineKeyboardButton( 25 | text=token.name, 26 | callback_data=ApiCallback(action='revoke', token=str(token.pk)).pack() 27 | )) 28 | 29 | return kb.as_markup() 30 | 31 | 32 | def get_revoke_submit(token_id: str) -> InlineKeyboardMarkup: 33 | kb = InlineKeyboardBuilder() 34 | 35 | kb.add(InlineKeyboardButton( 36 | text=f'Отменить', 37 | callback_data=ApiCallback(action='menu').pack() 38 | )) 39 | 40 | kb.add(InlineKeyboardButton( 41 | text=f'Подтвердить', 42 | callback_data=ApiCallback(action='revoke_submit', token=token_id).pack() 43 | )) 44 | 45 | return kb.as_markup() 46 | -------------------------------------------------------------------------------- /keyboards/guide_keyboard.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters.callback_data import CallbackData 2 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 3 | from aiogram.utils.keyboard import InlineKeyboardBuilder 4 | 5 | 6 | class GuideCallbackData(CallbackData, prefix="gud"): 7 | unit: str 8 | 9 | 10 | def get() -> InlineKeyboardMarkup: 11 | kb = InlineKeyboardBuilder() 12 | 13 | kb.row(InlineKeyboardButton( 14 | text='Группы', 15 | callback_data=GuideCallbackData(unit='groups').pack() 16 | )) 17 | 18 | kb.add(InlineKeyboardButton( 19 | text='Каналы', 20 | callback_data=GuideCallbackData(unit='channels').pack() 21 | )) 22 | 23 | kb.add(InlineKeyboardButton( 24 | text='Друзья', 25 | callback_data=GuideCallbackData(unit='friends').pack() 26 | )) 27 | 28 | kb.row(InlineKeyboardButton( 29 | text='Кастомизация', 30 | callback_data=GuideCallbackData(unit='customization').pack() 31 | )) 32 | 33 | kb.add(InlineKeyboardButton( 34 | text='Полезное', 35 | callback_data=GuideCallbackData(unit='utils').pack() 36 | )) 37 | 38 | return kb.as_markup() 39 | 40 | 41 | def get_return() -> InlineKeyboardMarkup: 42 | kb = InlineKeyboardBuilder() 43 | 44 | kb.row(InlineKeyboardButton( 45 | text='Назад', 46 | callback_data=GuideCallbackData(unit='main').pack() 47 | )) 48 | 49 | return kb.as_markup() 50 | -------------------------------------------------------------------------------- /db/ApiAuth.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import secrets 3 | import string 4 | from typing import Tuple 5 | 6 | from tortoise import fields 7 | from tortoise.exceptions import ValidationError 8 | from tortoise.models import Model 9 | from tortoise.validators import MinLengthValidator, Validator 10 | 11 | from db.fields import AutoNowDatetimeField 12 | from utils.generate_random_secret import generate_random_secret 13 | 14 | 15 | class TokenNameValidator(Validator): 16 | def __call__(self, value: str): 17 | for i in range(len(value)): 18 | char = value[i] 19 | if not char.isnumeric() and not ('a' <= char <= 'z') and char != '_': 20 | raise ValidationError(f'Unexpected char "{char}" in token name "{value}" on position {i + 1}', i) 21 | 22 | 23 | class ApiToken(Model): 24 | id = fields.UUIDField(pk=True) 25 | token = fields.CharField(unique=True, max_length=64, validators=[MinLengthValidator(64)]) 26 | owner = fields.ForeignKeyField('models.User', related_name='tokens_owned') 27 | name = fields.CharField(max_length=64, validators=[MinLengthValidator(3), TokenNameValidator()]) 28 | 29 | created_at = AutoNowDatetimeField() 30 | valid = fields.BooleanField(default=True) 31 | 32 | @classmethod 33 | def hash_token(cls, value: str): 34 | return hashlib.sha256(value.encode()).hexdigest() 35 | 36 | @classmethod 37 | def generate_token(cls) -> Tuple[str, str]: 38 | token = generate_random_secret(64) 39 | return token, cls.hash_token(token) 40 | -------------------------------------------------------------------------------- /db/UserUnion.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields 2 | from tortoise.models import Model 3 | 4 | from db.fields import AutoNowDatetimeField 5 | from utils.generate_random_secret import generate_random_secret 6 | 7 | 8 | def generate_password() -> str: 9 | return generate_random_secret(8).lower() 10 | 11 | 12 | class Group(Model): 13 | name = fields.CharField(max_length=32) 14 | owner = fields.ForeignKeyField('models.User', related_name='groups_owned') 15 | 16 | members = fields.ManyToManyField('models.User', related_name='groups_member', through='group_member') 17 | requests = fields.ManyToManyField('models.User', related_name='groups_requested', through='group_request') 18 | 19 | notify_perdish = fields.BooleanField(default=True) 20 | password = fields.CharField(default=generate_password, max_length=8) 21 | 22 | created_at = AutoNowDatetimeField() 23 | 24 | 25 | class FriendRequest(Model): 26 | user = fields.ForeignKeyField('models.User', related_name='friends_requested') 27 | requested_user = fields.ForeignKeyField('models.User', related_name='friend_requests') 28 | message_id = fields.BigIntField(null=True) 29 | 30 | 31 | class Channel(Model): 32 | channel_id = fields.BigIntField(pk=True) 33 | name = fields.CharField(max_length=128) 34 | 35 | members = fields.ManyToManyField('models.User', related_name='channels_member', through='channel_member') 36 | requests = fields.ManyToManyField('models.User', related_name='channels_requested', through='channel_request') 37 | 38 | notify_perdish = fields.BooleanField(default=True) 39 | password = fields.CharField(default=generate_password, max_length=8) 40 | 41 | created_at = AutoNowDatetimeField() 42 | -------------------------------------------------------------------------------- /bot_service/middlewares/channel.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import Any, Dict, Callable 3 | 4 | from aiogram import types 5 | 6 | from db.UserUnion import Channel 7 | from keyboards import channels_keyboard 8 | from bot_service.middlewares.util import UtilMiddleware 9 | 10 | 11 | class ChannelMiddleware(UtilMiddleware, ABC): 12 | def __init__(self, channel_menu_text): 13 | self.channel_menu_text = channel_menu_text 14 | 15 | async def __call__( 16 | self, 17 | handler: Callable, 18 | event: types.Update, 19 | data: Dict[str, Any] 20 | ) -> Any: 21 | try: 22 | prefix = event.data[:event.data.find(':')] 23 | 24 | unpack_func = None 25 | if prefix == 'chn': 26 | unpack_func = channels_keyboard.ChannelCallbackData.unpack 27 | 28 | elif prefix == 'chnp': 29 | unpack_func = channels_keyboard.ChannelPagedCallbackData.unpack 30 | 31 | elif prefix == 'chnmd': 32 | unpack_func = channels_keyboard.ChannelMemberDeleteCallbackData.unpack 33 | 34 | else: 35 | raise TypeError 36 | 37 | callback = unpack_func(event.data) 38 | 39 | if callback.channel_id == -1: 40 | raise ModuleNotFoundError 41 | 42 | channel = await Channel.filter(pk=callback.channel_id).get_or_none() 43 | if channel is None: 44 | await event.answer('Канала не существует.', show_alert=True) 45 | await event.message.edit_text(self.channel_menu_text, 46 | reply_markup=await channels_keyboard.get_menu(data['user'], 0)) 47 | return 48 | 49 | data['channel'] = channel 50 | 51 | except (ModuleNotFoundError, TypeError): 52 | ... 53 | 54 | return await handler(event, data) 55 | -------------------------------------------------------------------------------- /api_services/srat_service/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append('..') 4 | sys.path.append('../..') 5 | 6 | from typing import Optional 7 | 8 | from aiogram import Bot 9 | from fastapi import FastAPI, Request 10 | from pydantic import BaseModel 11 | from starlette.responses import JSONResponse 12 | 13 | import brocker 14 | import config 15 | import db 16 | import api_middlewares 17 | import setup_logger 18 | from db.ToiletSessions import SretSession, SretType 19 | from db.User import User 20 | from utils import send_srat_notification 21 | 22 | setup_logger.__init__("API srat") 23 | 24 | app = FastAPI(docs_url=None, redoc_url=None) 25 | api_middlewares.setup(app, True) 26 | 27 | 28 | class SratModel(BaseModel): 29 | status: Optional[SretType] 30 | 31 | 32 | @app.on_event("startup") 33 | async def startup_event(): 34 | await db.init() 35 | await brocker.init() 36 | 37 | bot = Bot( 38 | token=config.Telegram.token, 39 | parse_mode='html', 40 | ) 41 | config.bot = bot 42 | 43 | 44 | @app.get('/api/srat/', status_code=200, response_model=SratModel) 45 | async def get_srat(request: Request): 46 | user: User = request.state.user 47 | 48 | last_open_session = await SretSession.filter(user=user, end=None).get_or_none() 49 | if last_open_session is None: 50 | srat_status = None 51 | 52 | else: 53 | srat_status = last_open_session.sret_type 54 | 55 | return SratModel(status=srat_status) 56 | 57 | 58 | @app.post('/api/srat/', status_code=204) 59 | async def set_srat(request: Request, srat: SratModel): 60 | if srat.status is None: 61 | srat.status = 0 62 | 63 | user: User = request.state.user 64 | 65 | if not await send_srat_notification.verify_action(user, srat.status): 66 | return JSONResponse({"detail": "You cannot make this action"}, status_code=400) 67 | 68 | await send_srat_notification.send(user, srat.status) 69 | return 70 | -------------------------------------------------------------------------------- /bot_service/handlers/admin/degrade.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from aiogram import Router 4 | from aiogram import types 5 | from aiogram.filters import CommandObject 6 | from aiogram.utils.formatting import Text, Pre, Code 7 | from loguru import logger 8 | 9 | import config 10 | from bot_service.filters import CommandMention 11 | from bot_service.filters import UserAuthFilter 12 | from middlewares.degrade import DegradationData 13 | 14 | router = Router() 15 | 16 | 17 | def render_now_degradations(degrade_model: DegradationData): 18 | rer = degrade_model.__str__().replace(" ", "\n") 19 | return Pre(rer, language='Текущий статус деградаций') 20 | 21 | 22 | @router.message(CommandMention("degrade"), UserAuthFilter(admin=True)) 23 | async def degrade(message: types.Message, command: CommandObject): 24 | args = [''] if command.args is None else command.args.split() 25 | 26 | degrade_now = json.loads(await config.storage.redis.get('degrade')) 27 | if args[0] in DegradationData.model_fields: 28 | degrade_now[args[0]] = not degrade_now[args[0]] 29 | 30 | logger.warning(f'{message.from_user.id} SET DEGRADATION MODE {args[0]}={degrade_now[args[0]]}') 31 | 32 | degrade_model = DegradationData(**degrade_now) 33 | 34 | if args[0] not in DegradationData.model_fields: 35 | degradation_keys = [] 36 | for el in DegradationData.model_fields.keys(): 37 | degradation_keys.append(Code(el)) 38 | degradation_keys.append(', ') 39 | degradation_keys.pop() 40 | 41 | text = Text( 42 | f'Не можем найти деградацию ', Code(args[0]), '\n', 43 | f'Возможные деградации: ', *degradation_keys, '\n\n', 44 | render_now_degradations(degrade_model), 45 | ) 46 | await message.reply(**text.as_kwargs()) 47 | return 48 | 49 | await config.storage.redis.set('degrade', json.dumps(degrade_model.model_dump())) 50 | 51 | await message.reply(**Text('Успешно', render_now_degradations(degrade_model)).as_kwargs()) 52 | -------------------------------------------------------------------------------- /send_message_service/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append('..') 4 | 5 | import asyncio 6 | import json 7 | import time 8 | 9 | import aiormq 10 | from aiogram import Bot 11 | from aiormq.abc import DeliveredMessage 12 | from loguru import logger 13 | 14 | import config 15 | import db 16 | import setup_logger 17 | from db.Notify import Notify 18 | 19 | setup_logger.__init__('Send Message Service') 20 | 21 | bot: Bot 22 | 23 | update_notify_counter_sql = 'UPDATE notify SET executed_users_count = executed_users_count + 1 WHERE message_id = %d;' 24 | 25 | 26 | async def on_message(message: DeliveredMessage): 27 | body = json.loads(message.body.decode()) 28 | 29 | try: 30 | func = bot.copy_message 31 | if body.get('show_sender', False): 32 | func = bot.forward_message 33 | 34 | await func(body['send_to'], body['forward_message_chat'], body['forward_message']) 35 | 36 | logger.debug(f'Sended message to {body["send_to"]} from chat {body["forward_message_chat"]} message {body["forward_message"]}.') 37 | 38 | except Exception as e: 39 | logger.info(f'Cannnot send notify to {body["send_to"]} cause: {e}') 40 | 41 | if body['notify_id'] is not None: 42 | await Notify.raw(update_notify_counter_sql % body['notify_id']) 43 | 44 | await message.channel.basic_ack(message.delivery_tag) 45 | time.sleep(0.066) # costyl? | Message sending rate limiter 46 | 47 | 48 | async def main(): 49 | global bot 50 | 51 | await db.init() 52 | 53 | bot = Bot( 54 | token=config.Telegram.token, 55 | parse_mode='markdown', 56 | ) 57 | 58 | connection = await aiormq.connect(config.AMQP.uri) 59 | channel = await connection.channel() 60 | await channel.basic_qos(prefetch_count=1) 61 | 62 | declare = await channel.queue_declare('send_message', durable=True, arguments={"x-max-priority": 10}) 63 | await channel.basic_consume( 64 | declare.queue, on_message 65 | ) 66 | 67 | 68 | if __name__ == '__main__': 69 | loop = asyncio.get_event_loop() 70 | loop.run_until_complete(main()) 71 | loop.run_forever() 72 | -------------------------------------------------------------------------------- /bot_service/handlers/report.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, Bot 2 | from aiogram import types 3 | from aiogram.filters import Command 4 | from aiogram.fsm.context import FSMContext 5 | from aiogram.fsm.state import StatesGroup, State 6 | 7 | import config 8 | from db.User import User 9 | from keyboards import whois_keyboard 10 | 11 | router = Router() 12 | 13 | report_bot = Bot( 14 | token=config.Telegram.admin_token, 15 | parse_mode='html', 16 | ) 17 | 18 | 19 | class SendReport(StatesGroup): 20 | writing_report = State() 21 | 22 | 23 | @router.message(Command("report")) 24 | async def report(message: types.Message, state: FSMContext): 25 | text = ('Здесь вы можете написать сообщение администрации. Сообщайте о багах, о нарушении правил использования бота, о предложениях к функционалу и т. д.\n' 26 | 'Напишите следующим сообщением то, что вы хотите отправить нам.\n\n' 27 | 'Для отмены используйте команду /cancel') 28 | 29 | await message.delete() 30 | await state.set_state(SendReport.writing_report) 31 | 32 | last_msg = await message.answer(text) 33 | await state.update_data(last_msg=last_msg.message_id) 34 | 35 | 36 | @router.message(SendReport.writing_report) 37 | async def writing_report(message: types.Message, state: FSMContext, user: User): 38 | await state.clear() 39 | await message.reply('Ваша обращение зафиксировано!') 40 | 41 | await report_bot.send_message(config.Telegram.admin_group_id, 42 | f'❗️Зафикисировано обращение\n' 43 | f'От: _{user.name}_ (`{user.uid}`)', 44 | message_thread_id=config.Telegram.admin_token_report_thread, 45 | reply_markup=whois_keyboard.get(user.uid)) 46 | 47 | await report_bot.send_message(config.Telegram.admin_group_id, 48 | message.text, 49 | message_thread_id=config.Telegram.admin_token_report_thread, 50 | entities=message.entities, 51 | parse_mode=None) 52 | -------------------------------------------------------------------------------- /bot_service/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from loguru import logger 4 | 5 | sys.path.append('..') 6 | 7 | import asyncio 8 | import json 9 | 10 | from aiohttp import web 11 | 12 | from aiogram import Bot, Dispatcher 13 | from aiogram.fsm.storage.redis import RedisStorage 14 | from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application 15 | 16 | import brocker 17 | import config 18 | import db 19 | import handlers 20 | import middlewares 21 | import setup_logger 22 | from middlewares.degrade import DegradationData 23 | 24 | setup_logger.__init__('Bot Service') 25 | 26 | redis_url = f'redis://{config.REDIS.user}:{config.REDIS.password}@{config.REDIS.host}:{config.REDIS.port}/{config.REDIS.db_name}' 27 | storage = RedisStorage.from_url(redis_url) 28 | dp = Dispatcher(storage=storage) 29 | 30 | bot = Bot( 31 | token=config.Telegram.token, 32 | parse_mode='html', 33 | ) 34 | config.bot = bot 35 | 36 | 37 | async def main(): 38 | await db.init() 39 | await brocker.init() 40 | 41 | config.storage = storage 42 | await storage.redis.set('degrade', json.dumps(DegradationData().model_dump()), nx=True) 43 | 44 | config.bot_me = await bot.me() 45 | config.loop = asyncio.get_running_loop() 46 | 47 | middlewares.setup(dp) 48 | 49 | dp.include_router(handlers.router) 50 | 51 | if config.DEBUG: 52 | await bot.delete_webhook() 53 | await dp.start_polling(bot) 54 | return 55 | 56 | logger.info(f'Setting webhook to {config.Webhook.remote_host}{config.Webhook.path}') 57 | await bot.set_webhook(f"{config.Webhook.remote_host}{config.Webhook.path}", secret_token=config.Webhook.secret) 58 | 59 | 60 | if __name__ == "__main__": 61 | if config.DEBUG: 62 | asyncio.run(main()) 63 | 64 | else: 65 | dp.startup.register(main) 66 | 67 | app = web.Application() 68 | webhook_requests_handler = SimpleRequestHandler( 69 | dispatcher=dp, 70 | bot=bot, 71 | secret_token=config.Webhook.secret, 72 | ) 73 | 74 | webhook_requests_handler.register(app, path=config.Webhook.path) 75 | setup_application(app, dp, bot=bot) 76 | web.run_app(app, host=config.Webhook.host, port=config.Webhook.port) 77 | 78 | -------------------------------------------------------------------------------- /bot_service/handlers/channels/join.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, F 2 | from aiogram import types 3 | from aiogram.enums import ChatMemberStatus 4 | from aiogram.filters import Command, CommandObject, MagicData 5 | from aiogram.filters.chat_member_updated import ChatMemberUpdatedFilter, KICKED, LEFT, MEMBER, CREATOR, ADMINISTRATOR 6 | from db.User import User 7 | from db.UserUnion import Channel 8 | from handlers.channels.control import get_bot_channel 9 | 10 | router = Router() 11 | 12 | 13 | @router.message(Command("start"), MagicData(F.command.args.startswith('IC'))) 14 | async def join_channel(message: types.Message, command: CommandObject, user: User): 15 | try: 16 | channel_id, channel_password = command.args[2:].split('P')[:2] 17 | 18 | except ValueError: 19 | return 20 | 21 | if not channel_id[1:].isnumeric() and not channel_id[0].isnumeric() and channel_id[0] != '-': 22 | await message.reply('Канал не найден.') 23 | return 24 | 25 | channel_id = int(channel_id) 26 | 27 | channel = await Channel.filter(pk=channel_id, password=channel_password).get_or_none() 28 | if channel is None: 29 | await message.reply('Канал не найден.') 30 | return 31 | 32 | channel_bot = (await get_bot_channel(message.bot, channel_id))[0] 33 | if channel_bot is None: 34 | await message.reply('Канал не найден.') 35 | return 36 | 37 | member = await channel_bot.get_member(user.uid) 38 | 39 | if member.status in [ChatMemberStatus.KICKED, ChatMemberStatus.LEFT]: 40 | await message.reply('Вас нет в тг канале, вы не можете вступать в этот канал.') 41 | return 42 | 43 | if await channel.members.filter(uid=user.uid).exists(): 44 | await message.reply('Вы уже состоите в этом канале.') 45 | return 46 | 47 | await channel.members.add(user) 48 | await message.reply(f'Добро пожаловать в канал {channel.name}!') 49 | 50 | 51 | @router.chat_member(ChatMemberUpdatedFilter(member_status_changed=(KICKED | LEFT) << (MEMBER | CREATOR | ADMINISTRATOR))) 52 | async def kick_from_channel(update: types.ChatMemberUpdated, user: User): 53 | channel = await Channel.get_or_none(pk=update.chat.id) 54 | if channel is None: 55 | return 56 | 57 | await update.bot.send_message(user.uid, f'Вы исключены из канала {channel.name}') 58 | await channel.members.remove(user) 59 | -------------------------------------------------------------------------------- /autoend_service/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append('..') 4 | 5 | import asyncio 6 | from datetime import datetime, timedelta 7 | 8 | import pytz 9 | from aiogram import Bot 10 | 11 | import brocker 12 | import config 13 | import db 14 | import setup_logger 15 | from db.ToiletSessions import SretSession, SretType 16 | from utils import send_srat_notification 17 | 18 | setup_logger.__init__('Autoend Service') 19 | 20 | autoend_sql = f''' 21 | SELECT sretsession.* FROM sretsession 22 | JOIN public."user" u on u.uid = sretsession.user_id 23 | WHERE (start <= (NOW() - interval '{config.Constants.srat_delete_time} minute') OR 24 | (start <= (NOW() - interval '1 min' * u.autoend_time) AND sretsession.autoend = true)) AND 25 | sret_type in {SretType.SRET.value, SretType.DRISHET.value} AND "end" IS NULL; 26 | ''' 27 | 28 | 29 | async def end_loop(): 30 | while True: 31 | # ORDER THIS 2 LINES IMPORTANT 32 | sessions = await SretSession.raw(autoend_sql) 33 | delete_time = datetime.now(pytz.UTC) - timedelta(minutes=config.Constants.srat_delete_time) 34 | 35 | for session in sessions: 36 | user = await session.user 37 | await config.bot.edit_message_reply_markup(user.uid, session.message_id, reply_markup=None) 38 | 39 | if delete_time >= session.start: 40 | await session.delete() 41 | 42 | try: 43 | await config.bot.send_message(user.uid, 44 | 'Вы умерли в туалете!\n' 45 | 'Мы удалили вашу сессию сранья так как вы слишком долго срете, надеемся вы сейчас живы, и просто забыли завершить сранье, впредь будьте внимательнее.') 46 | except: 47 | ... 48 | 49 | continue 50 | 51 | session.end = datetime.now(pytz.UTC) 52 | await session.save() 53 | 54 | await send_srat_notification.send(user, 0) 55 | 56 | await asyncio.sleep(60) 57 | 58 | 59 | async def main(): 60 | await brocker.init() 61 | 62 | await db.init() 63 | 64 | bot = Bot( 65 | token=config.Telegram.token, 66 | parse_mode='html', 67 | ) 68 | config.bot = bot 69 | 70 | await end_loop() 71 | 72 | 73 | if __name__ == "__main__": 74 | asyncio.run(main()) 75 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | from asyncio import AbstractEventLoop 2 | from os import environ 3 | 4 | from aiogram import Bot 5 | from aiogram.fsm.storage.redis import RedisStorage 6 | from aiogram.types import User 7 | from dotenv import load_dotenv 8 | 9 | load_dotenv() 10 | 11 | DEBUG = environ.get('DEBUG') == "TRUE" 12 | 13 | bot: Bot 14 | bot_me: User 15 | storage: RedisStorage 16 | loop: AbstractEventLoop 17 | 18 | 19 | class Telegram: 20 | token = environ.get('TOKEN') 21 | admin_token = environ.get('TOKEN_ADMIN') 22 | 23 | admin_group_id = environ.get('ADMIN_GROUP_ID') 24 | global_channel_id = environ.get('GLOBAL_CHANNEL_ID') 25 | 26 | admin_token_report_thread = environ.get('ADMIN_GROUP_REPORT_THREAD') 27 | 28 | 29 | class Logger: 30 | max_file_size = 10 * 1024 ** 2 31 | 32 | 33 | class Constants: 34 | group_members_limit = 21 35 | friends_limit = 53 36 | member_group_limit = 5 37 | 38 | max_api_tokens = 50 39 | 40 | srat_delete_time = 60 * 4 # in minutes 41 | 42 | export_info_interval = 24 * 60 * 60 # in seconds 43 | 44 | throttling_time = 0.5 # in seconds 45 | throttling_time_actions = ( # in minutes 46 | (5, 2, 2), 47 | (2, 1, 1), 48 | (1, 1, 1), 49 | ) # row - last action, column - now action. See SretActions enum to get names. 50 | 51 | 52 | class DB: 53 | host = environ.get('DB_HOST') 54 | port = int(environ.get('DB_PORT')) 55 | user = environ.get('DB_USER') 56 | password = environ.get('DB_PASSWORD') 57 | db_name = environ.get('DB_NAME') 58 | 59 | 60 | class REDIS: 61 | host = environ.get('REDIS_HOST') 62 | port = int(environ.get('REDIS_PORT')) 63 | user = environ.get('REDIS_USER') 64 | password = environ.get('REDIS_PASSWORD') 65 | db_name = environ.get('REDIS_NAME') 66 | 67 | 68 | class AMQP: 69 | host = environ.get('AMQP_HOST') 70 | port = int(environ.get('AMQP_PORT')) 71 | vhost = environ.get('AMQP_VHOST') 72 | user = environ.get('AMQP_USER') 73 | password = environ.get('AMQP_PASSWORD') 74 | 75 | uri = f'amqp://{user}:{password}@{host}:{port}/{vhost}' 76 | 77 | 78 | class Webhook: 79 | host = environ.get('WEBHOOK_HOST') 80 | port = int(environ.get('WEBHOOK_PORT')) 81 | path = environ.get('WEBHOOK_PATH') 82 | secret = environ.get('WEBHOOK_SECRET') 83 | remote_host = environ.get('WEBHOOK_REMOTE_HOST') 84 | 85 | 86 | class Sentry: 87 | use_sentry = True 88 | dsn = environ.get('SENTRY_DSN') 89 | -------------------------------------------------------------------------------- /keyboards/friend/friends_keyboard.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Optional 3 | 4 | from aiogram.filters.callback_data import CallbackData 5 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 6 | from aiogram.utils.keyboard import InlineKeyboardBuilder 7 | 8 | from db.User import User 9 | 10 | 11 | class FriendUserType(Enum): 12 | friend = 'f' 13 | friend_submit = 'fs' 14 | request = 'r' 15 | request_submit = 'rs' 16 | 17 | 18 | class FriendUserCallback(CallbackData, prefix='fru'): 19 | user_id: int 20 | type: FriendUserType 21 | 22 | 23 | class FriendMenuCallback(CallbackData, prefix='frm'): 24 | unit: str 25 | 26 | 27 | async def get(user: User) -> InlineKeyboardMarkup: 28 | kb = InlineKeyboardBuilder() 29 | 30 | kb.row(InlineKeyboardButton( 31 | text='Ваши заявки', 32 | callback_data=FriendMenuCallback(unit='req').pack() 33 | )) 34 | 35 | kb.add(InlineKeyboardButton( 36 | text=f'{["За", "Раз"][user.mute_friend_requests]}мутить', 37 | callback_data=FriendMenuCallback(unit='chgmut').pack() 38 | )) 39 | 40 | async for friend in user.friends: 41 | kb.row(InlineKeyboardButton( 42 | text=friend.name, 43 | callback_data=FriendUserCallback(user_id=friend.uid, type=FriendUserType.friend).pack() 44 | )) 45 | 46 | kb.adjust(2, 3) 47 | return kb.as_markup() 48 | 49 | 50 | async def get_requests(user: User) -> InlineKeyboardMarkup: 51 | kb = InlineKeyboardBuilder() 52 | 53 | kb.row(InlineKeyboardButton( 54 | text='Назад', 55 | callback_data=FriendMenuCallback(unit='main').pack() 56 | )) 57 | 58 | async for friend_requested in user.friends_requested: 59 | user_requested = await friend_requested.requested_user 60 | kb.row(InlineKeyboardButton( 61 | text=user_requested.name, 62 | callback_data=FriendUserCallback(user_id=user_requested.uid, type=FriendUserType.request).pack() 63 | )) 64 | 65 | kb.adjust(1, 3) 66 | return kb.as_markup() 67 | 68 | 69 | def get_submit_delete(user_id: int, return_to: str, act_type: FriendUserType) -> InlineKeyboardMarkup: 70 | kb = InlineKeyboardBuilder() 71 | 72 | kb.add(InlineKeyboardButton( 73 | text='Отмена', 74 | callback_data=FriendMenuCallback(unit=return_to).pack() 75 | )) 76 | 77 | kb.add(InlineKeyboardButton( 78 | text='Удалить', 79 | callback_data=FriendUserCallback(user_id=user_id, type=act_type).pack() 80 | )) 81 | 82 | return kb.as_markup() 83 | -------------------------------------------------------------------------------- /export_info_service/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append('..') 4 | 5 | from datetime import datetime 6 | 7 | from aiogram.types import BufferedInputFile 8 | from tortoise.expressions import Q 9 | 10 | from db.ToiletSessions import SretSession 11 | 12 | import asyncio 13 | import json 14 | 15 | import aiormq 16 | from aiogram import Bot 17 | from aiormq.abc import DeliveredMessage 18 | from loguru import logger 19 | 20 | import config 21 | import db 22 | import setup_logger 23 | 24 | 25 | setup_logger.__init__('Export Info Service') 26 | 27 | bot: Bot 28 | 29 | message_text = ('Пояснение к столбцам таблице:\n' 30 | ' - start — Время начала действия, часовой пояс: UTC\n' 31 | ' - end — Время конца действия, часовой пояс: UTC\n' 32 | ' - autoend — Имеет значени 1, если действие было завершено автоматически, иначе 0\n' 33 | ' - sret_type — Тип действия. 1 - Сранье, 2 - Дристание, 3 - Пердеж\n') 34 | 35 | 36 | async def on_message(message: DeliveredMessage): 37 | body = json.loads(message.body.decode()) 38 | buffer = 'start,end,autoend,sret_type\n' 39 | 40 | async for session in SretSession.filter(user_id=body['user_id']).filter(~Q(end=None)).order_by('-start'): 41 | date_format = '%d.%m.%Y %H:%M:%S' 42 | row = f'{session.start.strftime(date_format)},{session.end.strftime(date_format)},{int(session.autoend)},{session.sret_type}\n' 43 | 44 | buffer += row 45 | 46 | io_csv = BufferedInputFile(buffer.encode(), filename=f'{body["user_id"]}_{int(datetime.now().timestamp())}.csv') 47 | try: 48 | export_message = await bot.send_document(body['send_to_user_id'], io_csv, caption='Экспортировали активность за все время.') 49 | await export_message.reply(message_text) 50 | 51 | except Exception as e: 52 | logger.info(f'Failed send exported info to {body["send_to_user_id"]} - error: {e}') 53 | 54 | await message.channel.basic_ack(message.delivery_tag) 55 | 56 | 57 | async def main(): 58 | global bot 59 | 60 | await db.init() 61 | 62 | bot = Bot( 63 | token=config.Telegram.token, 64 | parse_mode='html', 65 | ) 66 | 67 | connection = await aiormq.connect(config.AMQP.uri) 68 | channel = await connection.channel() 69 | await channel.basic_qos(prefetch_count=1) 70 | 71 | declare = await channel.queue_declare('export_info', durable=True) 72 | await channel.basic_consume( 73 | declare.queue, on_message 74 | ) 75 | 76 | 77 | if __name__ == '__main__': 78 | loop = asyncio.get_event_loop() 79 | loop.run_until_complete(main()) 80 | loop.run_forever() 81 | -------------------------------------------------------------------------------- /setup_logger.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import logging 3 | import os 4 | import platform 5 | from datetime import datetime 6 | from pathlib import Path 7 | 8 | import sentry_sdk 9 | from aiogram import Bot 10 | from aiogram.types import BufferedInputFile 11 | from loguru import logger 12 | 13 | import config 14 | 15 | project_name: str 16 | 17 | 18 | class InterceptHandler(logging.Handler): 19 | LEVELS_MAP = { 20 | logging.CRITICAL: "CRITICAL", 21 | logging.ERROR: "ERROR", 22 | logging.WARNING: "WARNING", 23 | logging.INFO: "INFO", 24 | logging.DEBUG: "DEBUG", 25 | } 26 | 27 | def _get_level(self, record): 28 | return self.LEVELS_MAP.get(record.levelno, record.levelno) 29 | 30 | def emit(self, record): 31 | try: 32 | level = logger.level(record.levelname).name 33 | except ValueError: 34 | level = record.levelno 35 | 36 | logger.opt(exception=record.exc_info).log(level, record.getMessage()) 37 | 38 | 39 | def __init__(__project_name: str): 40 | global project_name 41 | 42 | project_name = __project_name 43 | project_name_u = project_name.replace(" ", "-").lower() 44 | 45 | logs_dir_path = Path(__file__).resolve().__str__().replace('setup_logger.py', 'logs') 46 | if not os.path.exists(logs_dir_path): 47 | os.mkdir(logs_dir_path) 48 | 49 | if os.path.exists(f'{logs_dir_path}/latest-{project_name_u}.log'): 50 | os.remove(f'{logs_dir_path}/latest-{project_name_u}.log') 51 | 52 | for log_file in os.listdir(logs_dir_path): 53 | if not log_file.endswith('.log') or project_name_u not in log_file: 54 | continue 55 | 56 | with open(f'{logs_dir_path}/{log_file}', 'rb') as fp: 57 | log_bytes = fp.read() 58 | 59 | with open(f'{logs_dir_path}/{log_file}.gz', 'wb') as fp: 60 | fp.write(gzip.compress(log_bytes)) 61 | 62 | os.remove(f'{logs_dir_path}/{log_file}') 63 | 64 | logging.getLogger('aiogram').setLevel(logging.DEBUG) 65 | logging.getLogger('aiogram').addHandler(InterceptHandler()) 66 | logging.getLogger('asyncio').setLevel(logging.DEBUG) 67 | logging.getLogger('asyncio').addHandler(InterceptHandler()) 68 | 69 | logger.add(f'{logs_dir_path}/URA-{project_name_u}_' + '{time:YYYY-MM-DD_hh:mm:ss!UTC}.log', 70 | rotation='00:00', 71 | compression='gz') 72 | 73 | logger.add(f'{logs_dir_path}/latest-{project_name_u}.log') 74 | 75 | logger.info(f'URA-PROJECT | {project_name}') 76 | if config.DEBUG: 77 | logger.warning('APP IN DEBUG MODE') 78 | 79 | logger.info(f"Python version: {platform.python_version()}") 80 | logger.info(f"Running on: {platform.system()} {platform.release()} ({os.name})") 81 | 82 | if config.Sentry.use_sentry: 83 | sentry_sdk.init( 84 | dsn=config.Sentry.dsn, 85 | traces_sample_rate=1.0, 86 | profiles_sample_rate=1.0, 87 | environment=project_name_u 88 | ) 89 | -------------------------------------------------------------------------------- /bot_service/handlers/srat.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Optional 3 | 4 | import pytz 5 | from aiogram import types, Router, F 6 | from aiogram.enums import ChatType 7 | from aiogram.types import InlineQueryResultArticle, InlineQuery, InputTextMessageContent, ChosenInlineResult 8 | from tortoise import Tortoise 9 | 10 | import config 11 | from db.ToiletSessions import SretSession 12 | from db.User import User 13 | from keyboards import sret_keyboard 14 | from keyboards.srat_var_keyboard import SretActions 15 | from utils import send_srat_notification 16 | from utils.send_srat_notification import verify_action 17 | 18 | router = Router() 19 | 20 | 21 | @router.message(F.text.startswith('Я'), F.chat.type == ChatType.PRIVATE) 22 | async def send_srat(message: types.Message, user: User): 23 | sret = None 24 | for el in SretActions: 25 | if el.value[1] == message.text: 26 | sret = el.value[0] 27 | break 28 | if sret is None: 29 | return 30 | 31 | if not await verify_action(user, sret, message): 32 | return 33 | 34 | await message.delete() 35 | await send_srat_notification.send(user, sret) 36 | 37 | 38 | @router.callback_query(F.data == 'chg_aend_srat') 39 | async def switch_srat_autoend(callback: types.CallbackQuery): 40 | dbconn = Tortoise.get_connection('default') 41 | query = (f'UPDATE {config.DB.db_name}.public.sretsession SET autoend = NOT autoend ' 42 | f'WHERE message_id = {callback.message.message_id} ' 43 | f'RETURNING autoend;') 44 | now_autoend = (await dbconn.execute_query_dict(query))[0].get('autoend') 45 | 46 | await callback.answer(f'{["Выключили", "Включили"][now_autoend]} автозавершение.') 47 | await callback.message.edit_reply_markup(reply_markup=sret_keyboard.get(now_autoend)) 48 | 49 | 50 | @router.inline_query(F.query == '') 51 | async def get_sret_actions(inline_query: InlineQuery, user: User): 52 | res = [] 53 | exists = await SretSession.filter(user=user, end=None).exists() 54 | for el in SretActions: 55 | sret = int(el.value[0]) 56 | in_must = sret in send_srat_notification.must_sret 57 | inn_must = sret in send_srat_notification.must_not_sret 58 | if in_must and exists or \ 59 | inn_must and not exists or (not in_must and not inn_must): 60 | res.append(InlineQueryResultArticle( 61 | id=str(el.value[0]), 62 | title=el.value[1], 63 | input_message_content=InputTextMessageContent( 64 | message_text=send_srat_notification.get_message_text(user, el.value[0]) 65 | ) 66 | )) 67 | 68 | await inline_query.answer(res, is_personal=True, cache_time=0) 69 | 70 | 71 | @router.chosen_inline_result() 72 | async def send_srat_inline(chosen_result: ChosenInlineResult, user: User): 73 | sret = int(chosen_result.result_id) 74 | if not await verify_action(user, sret): 75 | text = 'Эй! Полегче! Вы совершили невозможное для вашей жопы действие! Вы конечно молодец, но мы не отправим уведомление об этом действии всем.' 76 | await config.bot.send_message(chosen_result.from_user.id, text) 77 | return 78 | 79 | await send_srat_notification.send(user, sret) 80 | -------------------------------------------------------------------------------- /bot_service/handlers/admin/ban.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | from aiogram import types 3 | from aiogram.filters import Command, CommandObject 4 | from aiogram.utils.formatting import Text, Bold, Italic, Pre 5 | 6 | import config 7 | from db.User import Ban, User 8 | from filters import CommandMention 9 | from filters import UserAuthFilter 10 | 11 | router = Router() 12 | 13 | 14 | @router.message(CommandMention("ban"), UserAuthFilter(admin=True)) 15 | async def ban(message: types.Message, command: CommandObject, user: User): 16 | if command.args is None: 17 | await message.reply('Пример:\n/ban ') 18 | return 19 | 20 | args = command.args.split() 21 | 22 | if len(args) == 0: 23 | await message.reply('Пожалуйста укажите айди и причину бана.') 24 | return 25 | 26 | if len(args) == 1: 27 | await message.reply('Пожалуйста укажите причину бана.') 28 | return 29 | 30 | ban_id = args[0] 31 | 32 | if not ban_id.isnumeric(): 33 | await message.reply('Айди должно быть числом.') 34 | return 35 | 36 | ban_id = int(ban_id) 37 | reason = ' '.join(args[1:]) 38 | 39 | if await Ban.filter(uid=ban_id).exists(): 40 | await message.reply('Пользователь уже забанен.') 41 | return 42 | 43 | admin_user = await User.filter(uid=ban_id, admin=True).get_or_none() 44 | if admin_user is not None: 45 | await message.reply('Админа можно забанить только через базу данных. Сообщаем всем.') 46 | 47 | text = Text( 48 | f'❗️ Админ ', Bold(user.name), ' ', Italic(user.uid), ' хочет забанить админа ', Bold(admin_user.name), ' ', Italic(admin_user.uid), '\n', 49 | Pre(reason, language='Причина:') 50 | ) 51 | 52 | async for send_to in User.filter(admin=True): 53 | await config.bot.send_message(send_to.uid, text) 54 | 55 | return 56 | 57 | await Ban.create(uid=ban_id, reason=reason, banned_by=user.uid) 58 | await message.reply(f'Пользователь `{ban_id}` забанен по причине `{reason}`') 59 | 60 | try: 61 | text = ('Вы забанены в боте! Ваши действия теперь игнорируются и не будут обрабатываться.\n' 62 | 'Если вы считаете, что произошла ошибка обратитесь к администрации.\n' 63 | 'https://www.youtube.com/watch?v=XeoS-zsGVCs') 64 | await config.bot.send_message(ban_id, text) 65 | 66 | except: 67 | ... 68 | 69 | 70 | @router.message(Command("unban"), UserAuthFilter(admin=True)) 71 | async def unban(message: types.Message, command: CommandObject): 72 | if command.args is None: 73 | await message.reply('Пример:\n/unban ') 74 | return 75 | 76 | if not command.args.isnumeric(): 77 | await message.reply('Айди должно быть числом.') 78 | return 79 | 80 | ban_id = int(command.args) 81 | 82 | ban = await Ban.filter(uid=ban_id).get_or_none() 83 | if ban is None: 84 | await message.reply('Такой пользователь не забанен.') 85 | return 86 | 87 | await ban.delete() 88 | await message.reply(f'Пользователь {ban_id} разбанен.') 89 | 90 | try: 91 | await config.bot.send_message(ban_id, 'Вы разбанены в боте.') 92 | 93 | except: 94 | ... 95 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:16 4 | restart: unless-stopped 5 | environment: 6 | POSTGRES_USER: urabot 7 | POSTGRES_PASSWORD: urabot 8 | POSTGRES_DB: urabot 9 | ports: 10 | - "51432:5432" 11 | volumes: 12 | - ../data/postgres:/var/lib/postgresql/data 13 | 14 | redis: 15 | image: redis:latest 16 | restart: unless-stopped 17 | command: redis-server /usr/local/etc/redis/redis.conf 18 | ports: 19 | - "6379:6379" 20 | volumes: 21 | - ./redis.conf:/usr/local/etc/redis/redis.conf 22 | - ../data/redis:/data 23 | 24 | rabbitmq: 25 | image: rabbitmq:latest 26 | hostname: rabbitmq 27 | restart: unless-stopped 28 | environment: 29 | RABBITMQ_DEFAULT_USER: urabot 30 | RABBITMQ_DEFAULT_PASS: urabot 31 | ports: 32 | - "15672:15672" 33 | volumes: 34 | - ../data/rabbitmq:/var/lib/rabbitmq 35 | 36 | nginx: 37 | image: nginx:latest 38 | restart: unless-stopped 39 | ports: 40 | - "58772:80" 41 | volumes: 42 | - ./nginx.conf:/etc/nginx/nginx.conf 43 | - ../logs/nginx:/var/log/nginx 44 | 45 | bot-service: 46 | build: 47 | context: ./ 48 | dockerfile: ./bot_service/Dockerfile 49 | restart: unless-stopped 50 | volumes: 51 | - ../logs/services:/app/logs 52 | depends_on: 53 | - postgres 54 | - redis 55 | - rabbitmq 56 | env_file: 57 | - integrated.env 58 | 59 | send-message-service: 60 | build: 61 | context: ./ 62 | dockerfile: ./send_message_service/Dockerfile 63 | restart: unless-stopped 64 | volumes: 65 | - ../logs/services:/app/logs 66 | depends_on: 67 | - postgres 68 | - redis 69 | - rabbitmq 70 | env_file: 71 | - integrated.env 72 | 73 | autoend-service: 74 | build: 75 | context: ./ 76 | dockerfile: ./autoend_service/Dockerfile 77 | restart: unless-stopped 78 | volumes: 79 | - ../logs/services:/app/logs 80 | depends_on: 81 | - postgres 82 | - redis 83 | - rabbitmq 84 | env_file: 85 | - integrated.env 86 | 87 | export-info-service: 88 | build: 89 | context: ./ 90 | dockerfile: ./export_info_service/Dockerfile 91 | restart: unless-stopped 92 | volumes: 93 | - ../logs/services:/app/logs 94 | depends_on: 95 | - postgres 96 | - redis 97 | - rabbitmq 98 | env_file: 99 | - integrated.env 100 | 101 | frontend-service: 102 | build: 103 | context: ./frontend_service 104 | dockerfile: ./Dockerfile 105 | restart: unless-stopped 106 | volumes: 107 | - ../logs/services:/app/logs 108 | depends_on: 109 | - postgres 110 | - redis 111 | - rabbitmq 112 | env_file: 113 | - integrated.env 114 | 115 | api-srat-service: 116 | build: 117 | context: ./ 118 | dockerfile: ./api_services/srat_service/Dockerfile 119 | restart: unless-stopped 120 | volumes: 121 | - ../logs/services:/app/logs 122 | depends_on: 123 | - postgres 124 | - redis 125 | - rabbitmq 126 | env_file: 127 | - integrated.env 128 | -------------------------------------------------------------------------------- /bot_service/handlers/admin/whois.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, F 2 | from aiogram import types 3 | from aiogram.filters import Command, MagicData, CommandObject 4 | 5 | from db.ToiletSessions import SretSession, SretType 6 | from db.User import User 7 | from bot_service.filters import UserAuthFilter 8 | from keyboards import whois_keyboard 9 | 10 | router = Router() 11 | 12 | 13 | async def whois_ans(message: types.Message, user_id: int): 14 | user = await User.all().filter(uid=user_id).get_or_none() 15 | if user is None: 16 | await message.reply(f'Пользователь с айди {user_id} не найден.') 17 | return 18 | 19 | last_session_timestamp = await SretSession.all().filter(user=user).order_by('-end').first() 20 | if last_session_timestamp is not None: 21 | if last_session_timestamp.end is None: 22 | last_session_timestamp = last_session_timestamp.start 23 | else: 24 | last_session_timestamp = last_session_timestamp.end 25 | 26 | perdezhs = await SretSession.all().filter(user=user, sret_type=SretType.PERNUL).count() 27 | toilets = await SretSession.all().filter(user=user, sret_type__in=[SretType.SRET, SretType.DRISHET]).count() 28 | 29 | text = (f'Пользователь {user.name} ({user_id})\n\n' 30 | f'Админ: {user.admin}\n' 31 | f'Аккаунт создан: {user.created_at}\n' 32 | f'Последняя активность: {last_session_timestamp}\n' 33 | f'Всего ходил раз в туалет: {toilets}\n' 34 | f'Всего пернул: {perdezhs}\n\n' 35 | f'Написать через бота: /send {user_id}\n' 36 | f'Забанить: /ban {user_id}') 37 | 38 | await message.reply(text, reply_markup=whois_keyboard.get(user_id)) 39 | 40 | 41 | async def name_to_id(message: types.Message, user_name: str): 42 | user = await User.all().filter(name=user_name).get_or_none() 43 | if user is None: 44 | await message.reply(f'Пользователь с именем {user_name} не найден.') 45 | return 46 | 47 | await whois_ans(message, user.uid) 48 | 49 | 50 | @router.message(Command("whois"), UserAuthFilter(admin=True), MagicData(~F.command.args)) 51 | async def whois_by_message(message: types.Message): 52 | if message.reply_to_message is None: 53 | await message.reply('Вы должны ссылаться на сообщение с уведомлением для получения информации.') 54 | return 55 | 56 | if 'ВНИМАНИЕ' not in message.reply_to_message.text: 57 | await message.reply('К сожалению мы не может получить информацию о пользователе из этого сообщения.') 58 | return 59 | 60 | text = message.reply_to_message.html_text 61 | i = text.find('') + 6 62 | j = text.find('') 63 | 64 | name = text[i:j] 65 | await name_to_id(message, name) 66 | 67 | 68 | @router.message(Command("whois"), UserAuthFilter(admin=True), MagicData(F.command.args.isnumeric())) 69 | async def whois_by_id(message: types.Message, command: CommandObject): 70 | await whois_ans(message, int(command.args)) 71 | 72 | 73 | @router.message(Command("whois"), UserAuthFilter(admin=True), MagicData(~F.command.args.isnumeric())) 74 | async def whois_by_name(message: types.Message, command: CommandObject): 75 | await name_to_id(message, command.args) 76 | -------------------------------------------------------------------------------- /frontend_service/src/main.rs: -------------------------------------------------------------------------------- 1 | use askama::Template; 2 | use axum::{ 3 | extract::State, 4 | response::{IntoResponse, Response}, 5 | routing::get, 6 | Router, 7 | }; 8 | use sqlx::{postgres::PgPoolOptions, PgPool}; 9 | use std::env; 10 | use tower_http::services::ServeDir; 11 | 12 | #[derive(Template)] 13 | #[template(path = "index.html")] 14 | struct IndexTemplate; 15 | 16 | #[derive(Template)] 17 | #[template(path = "anal.html")] 18 | struct AnalyticsTemplate { 19 | type1_total: i64, 20 | type2_total: i64, 21 | type3_total: i64, 22 | leaderboard1: Vec, 23 | leaderboard2: Vec, 24 | leaderboard3: Vec, 25 | } 26 | 27 | #[derive(Template)] 28 | #[template(path = "error.html")] 29 | struct ErrorTemplate; 30 | 31 | #[tokio::main] 32 | async fn main() { 33 | let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); 34 | let db_pool = PgPoolOptions::new() 35 | .max_connections(5) 36 | .connect(&database_url) 37 | .await 38 | .expect("Failed to connect to Postgres"); 39 | let app = Router::new() 40 | .route("/", get(index)) 41 | .route("/anal", get(analytics)) 42 | .with_state(db_pool) 43 | .nest_service( 44 | "/public", 45 | ServeDir::new("./public").append_index_html_on_directories(true), 46 | ); 47 | let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); 48 | axum::serve(listener, app).await.unwrap(); 49 | } 50 | 51 | async fn index() -> impl IntoResponse { 52 | IndexTemplate 53 | } 54 | 55 | async fn analytics(State(db_pool): State) -> Response { 56 | if let Some(templ) = get_analytics(db_pool).await { 57 | return templ.into_response(); 58 | } 59 | (ErrorTemplate {}).into_response() 60 | } 61 | 62 | struct LeaderboardRecord { 63 | name: String, 64 | count: Option, 65 | } 66 | 67 | async fn get_analytics(db_pool: PgPool) -> Option { 68 | let shit = sqlx::query!("SELECT sret_type, COUNT(*) count FROM sretsession GROUP BY sret_type") 69 | .fetch_all(&db_pool) 70 | .await 71 | .ok()?; 72 | let leaderboard1: Vec = sqlx::query_as!(LeaderboardRecord, "SELECT u.name, COUNT(*) count FROM sretsession as s JOIN \"user\" u ON u.uid = s.user_id GROUP BY u.name ORDER BY COUNT(*) DESC LIMIT 10").fetch_all(&db_pool).await.ok()?; 73 | let leaderboard2: Vec = sqlx::query_as!(LeaderboardRecord, "SELECT u.name, COUNT(*) count FROM sretsession as s JOIN \"user\" u on u.uid = s.user_id WHERE s.sret_type = 3 GROUP BY u.name ORDER BY COUNT(*) DESC LIMIT 10").fetch_all(&db_pool).await.ok()?; 74 | let leaderboard3: Vec = sqlx::query_as!(LeaderboardRecord, "SELECT u.name, COUNT(*) count FROM sretsession as s JOIN \"user\" u on u.uid = s.user_id WHERE s.sret_type IN (1, 2) GROUP BY u.name ORDER BY COUNT(*) DESC LIMIT 10").fetch_all(&db_pool).await.ok()?; 75 | Some(AnalyticsTemplate { 76 | type1_total: shit 77 | .iter() 78 | .find(|x| x.sret_type == 1) 79 | .and_then(|x| x.count) 80 | .unwrap_or(0), 81 | type2_total: shit 82 | .iter() 83 | .find(|x| x.sret_type == 2) 84 | .and_then(|x| x.count) 85 | .unwrap_or(0), 86 | type3_total: shit 87 | .iter() 88 | .find(|x| x.sret_type == 3) 89 | .and_then(|x| x.count) 90 | .unwrap_or(0), 91 | leaderboard1, 92 | leaderboard2, 93 | leaderboard3, 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /bot_service/handlers/user_properties/analytics.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router 2 | from aiogram import types 3 | from aiogram.filters import Command, CommandObject 4 | from asyncpg import Record 5 | from tortoise import Tortoise 6 | 7 | import config 8 | from db.ToiletSessions import SretSession 9 | from db.User import User 10 | 11 | router = Router() 12 | 13 | stat_request = '''SELECT sret_type, 14 | autoend, 15 | start >= (now() - interval '1 week') as last_week, 16 | start >= (now() - interval '1 month') as last_month, 17 | AVG(s.end - s.start), 18 | COUNT(*) 19 | FROM sretsession as s 20 | WHERE user_id = %d 21 | GROUP BY sret_type, last_week, last_month, autoend;''' 22 | 23 | 24 | def calc_avg(var, el): 25 | seconds = el['avg'].total_seconds() 26 | if var[0] is None: 27 | var = (seconds, el['count']) 28 | 29 | else: 30 | total = var[1] + el['count'] 31 | var = ((var[0] * var[1] + seconds * el['count']) / total, total) 32 | 33 | return var 34 | 35 | 36 | def render_time(val): 37 | if val[0] is None: 38 | return 'Н/Д' 39 | 40 | return f'{round(val[0] / 60, 1)} минут' 41 | 42 | 43 | @router.message(Command("anal")) 44 | async def anal(message: types.Message, command: CommandObject, user: User): 45 | user_id = user.uid 46 | if user.admin and command.args is not None and command.args.isnumeric(): 47 | user_id = int(command.args) 48 | 49 | conn = Tortoise.get_connection('default') 50 | res = await conn.execute_query(stat_request % user_id) 51 | 52 | co = [ 53 | [0, 0, 0], 54 | [0, 0, 0], 55 | [0, 0, 0], 56 | ] 57 | avg = [(None, 0), (None, 0), (None, 0)] 58 | for el in res[1]: 59 | crit = not el['autoend'] and el['sret_type'] != 3 60 | if el['last_week']: 61 | co[0][el['sret_type'] - 1] += el['count'] 62 | if crit: 63 | avg[0] = calc_avg(avg[0], el) 64 | 65 | if el['last_month']: 66 | co[1][el['sret_type'] - 1] += el['count'] 67 | if crit: 68 | avg[1] = calc_avg(avg[1], el) 69 | 70 | co[2][el['sret_type'] - 1] += el['count'] 71 | if crit: 72 | avg[2] = calc_avg(avg[2], el) 73 | 74 | pronouns = 'вы' 75 | if user.uid != user_id: 76 | pronouns = str(user_id) 77 | 78 | text = ( 79 | f'Всего за все время {pronouns}:\n' 80 | f'Раз срали: {co[2][0]}\n' 81 | f'Раз дристали: {co[2][1]}\n' 82 | f'Пернули: {co[2][2]}\n' 83 | f'Среднее время в туалете: {render_time(avg[0])}\n\n' 84 | 85 | f'За последний месяц {pronouns}:\n' 86 | f'Раз срали: {co[1][0]}\n' 87 | f'Раз дристали: {co[1][1]}\n' 88 | f'Пернули: {co[1][2]}\n' 89 | f'Среднее время в туалете: {render_time(avg[1])}\n\n' 90 | 91 | f'За последнюю неделю {pronouns}:\n' 92 | f'Раз срали: {co[0][0]}\n' 93 | f'Раз дристали: {co[0][1]}\n' 94 | f'Пернули: {co[0][2]}\n' 95 | f'Среднее время в туалете: {render_time(avg[2])}\n\n' 96 | 97 | f'Аккаунт создан: {user.created_at}\n' 98 | ) 99 | 100 | await message.reply(text) 101 | -------------------------------------------------------------------------------- /bot_service/handlers/groups/join.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, F 2 | from aiogram import types 3 | from aiogram.filters import Command, CommandObject, MagicData 4 | 5 | import config 6 | from db.User import User 7 | from db.UserUnion import Group 8 | from keyboards.group import join_group_keyboard 9 | 10 | router = Router() 11 | 12 | 13 | @router.message(Command("start"), MagicData(F.command.args.startswith('IG'))) 14 | async def join_group(message: types.Message, command: CommandObject, user: User): 15 | try: 16 | group_id, group_password = command.args[2:].split('P')[:2] 17 | 18 | except ValueError: 19 | return 20 | 21 | if not group_id.isnumeric(): 22 | await message.reply('Группа не найдена.') 23 | return 24 | 25 | group_id = int(group_id) 26 | 27 | group = await Group.filter(pk=group_id, password=group_password).get_or_none() 28 | if group is None: 29 | await message.reply('Группа не найдена.') 30 | return 31 | 32 | if await group.members.filter(uid=user.uid).exists(): 33 | await message.reply('Вы уже состоите в этой группе.') 34 | return 35 | 36 | if await group.requests.filter(uid=user.uid).exists(): 37 | await message.reply('Вы уже подавали заявку для этой группы.') 38 | return 39 | 40 | await group.requests.add(user) 41 | await config.bot.send_message((await group.owner.only('uid').get()).uid, 42 | f'Пользователь {user.name} ({user.uid}) хочет вступить к вам в группу {group.name} ({group.pk})', 43 | reply_markup=join_group_keyboard.get(user.uid, group.pk)) 44 | 45 | text = '' 46 | if (await user.groups_member.all().count() + await user.groups_requested.all().count()) > config.Constants.member_group_limit: 47 | text = '\n\nУчтите, что при принять все поданные вами заявки не получится, так как вы достигните лимит групп.' 48 | 49 | await message.reply(f'Ваша заявка на присоединение к группе {group.name} отправлена и ожидает одобрения.' + text) 50 | 51 | 52 | @router.callback_query(join_group_keyboard.JoinGroupCallback.filter()) 53 | async def join_group_decline(callback: types.CallbackQuery): 54 | join_group_data = join_group_keyboard.JoinGroupCallback.unpack(callback.data) 55 | 56 | group = await Group.filter(pk=join_group_data.group_id).get() 57 | 58 | join_user = await User.filter(pk=join_group_data.uid).get() 59 | await group.requests.remove(join_user) 60 | if join_group_data.result and (await group.members.all().count()) >= config.Constants.group_members_limit: 61 | await callback.answer('Вы достигли максимум человек в группе.', 62 | show_alert=True) 63 | return 64 | 65 | request_status = ["ОТКЛОНЕНА", "ОДОБРЕНА"][join_group_data.result] 66 | text = f'Пользователь {join_user.name} (`{join_user.uid}`) хочет вступить к вам в группу {group.name} ({group.pk})' 67 | 68 | await callback.message.edit_text(text + f'\n\n{request_status}') 69 | join_user_message = await config.bot.send_message(join_group_data.uid, f'Ваша заявкам в группу {group.name} {request_status}') 70 | 71 | if not join_group_data.result: 72 | return 73 | 74 | if not join_user.admin and await join_user.groups_member.all().count() >= config.Constants.member_group_limit: 75 | await callback.answer('Пользователь не добавлен в группу так как количество групп к котором он присоединен достигло максимума.', 76 | show_alert=True) 77 | await callback.message.edit_text(text + f'\n\n{request_status} НЕ ДОБАВЛЕН (лимит групп)') 78 | 79 | await join_user_message.reply('Вы не были в группу так как количество групп к котором вы присоединенились достигло максимума.') 80 | return 81 | 82 | await group.members.add(join_user) 83 | -------------------------------------------------------------------------------- /bot_service/handlers/friends/request.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, F, types 2 | from aiogram.filters import Command, MagicData, CommandObject 3 | 4 | import config 5 | from db.User import User 6 | from db.UserUnion import FriendRequest 7 | from keyboards.friend import request_friend_keyboard 8 | from keyboards.friend.request_friend_keyboard import ActionRequestUserCallback 9 | 10 | router = Router() 11 | 12 | 13 | @router.message(Command("start"), MagicData(F.command.args.startswith('IF'))) 14 | async def send_request(message: types.Message, command: CommandObject, user: User): 15 | user_id = str(command.args).replace('IF', '') 16 | if not user_id.isnumeric(): 17 | return 18 | user_id = int(user_id) 19 | 20 | if user_id == user.uid: 21 | await message.reply('Вы не можете подать заявку самому себе.') 22 | return 23 | 24 | request_user = await User.filter(pk=user_id).get_or_none() 25 | if request_user is None: 26 | await message.reply('Пользователь не найден.') 27 | return 28 | 29 | if await FriendRequest.filter(user=user, requested_user=request_user).exists(): 30 | await message.reply('Вы уже подавали заявку этому пользователю.') 31 | return 32 | 33 | if await request_user.friends.filter(uid=user.uid).exists(): 34 | await message.reply('Вы уже друзья.') 35 | return 36 | 37 | # contr request 38 | contr_request = await FriendRequest.filter(user=request_user, requested_user=user).get_or_none() 39 | if contr_request is not None: 40 | if contr_request.message_id is not None: 41 | await config.bot.delete_message(user.uid, contr_request.message_id) 42 | 43 | await contr_request.delete() 44 | await user.friends.add(request_user) 45 | await request_user.friends.add(user) 46 | 47 | await config.bot.send_message(request_user.uid, f'{user.name} добавил вас в друзья!') 48 | await message.answer(f'Вы добавили в друзья {request_user.name}.') 49 | return 50 | 51 | message_id = None 52 | if not request_user.mute_friend_requests: 53 | message_id = ((await config.bot.send_message(user_id, f'{user.name} хочет добавить вас в друзья.', 54 | reply_markup=request_friend_keyboard.get(user.uid, user_id))) 55 | .message_id) 56 | 57 | await FriendRequest.create(user=user, requested_user=request_user, message_id=message_id) 58 | 59 | await message.reply(f'Успешно отправили запрос на добавление в друзья пользователя {request_user.name}') 60 | 61 | 62 | @router.callback_query(ActionRequestUserCallback.filter()) 63 | async def action_request(callback: types.CallbackQuery, user: User): 64 | cb_data = ActionRequestUserCallback.unpack(callback.data) 65 | user_sender = await User.filter(pk=cb_data.uid).get() 66 | 67 | not_admin = not user.admin and not user_sender.admin 68 | if cb_data.result: 69 | if await user.friends.all().count() >= config.Constants.friends_limit and not_admin: 70 | await callback.answer('У вас максимальное количество друзей.') 71 | return 72 | 73 | if await user_sender.friends.all().count() >= config.Constants.friends_limit and not_admin: 74 | await callback.answer('У пользователя, который отправил заявку, максимальное количество друзей.') 75 | 76 | await FriendRequest.filter(user=user_sender, requested_user=user).delete() 77 | await callback.message.delete() 78 | return 79 | 80 | await FriendRequest.filter(user=user_sender, requested_user=user).delete() 81 | await callback.message.edit_text(callback.message.text + f'\n\n{["ОТКЛОНЕНА", "ОДОБРЕНА"][cb_data.result]}') 82 | 83 | if cb_data.result: 84 | await user.friends.add(user_sender) 85 | await user_sender.friends.add(user) 86 | 87 | await config.bot.send_message(user_sender.uid, f'{user.name} добавил вас в друзья!') 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | 162 | data.json 163 | .setupdb.py 164 | test.py 165 | 166 | logs/ 167 | .pio/ 168 | -------------------------------------------------------------------------------- /frontend_service/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | YPA Project 6 | 11 | 12 | 13 | 14 |
17 | Бот 22 | Новости 27 | 28 | Аналитика 33 |
34 |
37 |
38 |

YPA Project

39 |

40 | Уведомление Ректальных Активностей Бот, чтобы 41 | держать всех в курсе о ваших ректальных деяниях. 42 |

43 |
46 |
49 |
50 | 86 |
87 | 88 | 89 | -------------------------------------------------------------------------------- /keyboards/group/groups_keyboard.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters.callback_data import CallbackData 2 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 3 | from aiogram.utils.keyboard import InlineKeyboardBuilder 4 | 5 | from db.User import User 6 | from db.UserUnion import Group 7 | 8 | 9 | class GroupCallback(CallbackData, prefix="grp"): 10 | group: int 11 | action: str 12 | 13 | 14 | class DeleteGroupMemberCallback(CallbackData, prefix="grpdm"): 15 | uid: int 16 | group: int 17 | submit: bool 18 | 19 | 20 | def _return_button(text: str = 'Назад'): 21 | return InlineKeyboardButton( 22 | text=text, 23 | callback_data=GroupCallback(group=-1, action='main').pack() 24 | ) 25 | 26 | 27 | def _group_button(text: str, group_id: int): 28 | return InlineKeyboardButton( 29 | text=text, 30 | callback_data=GroupCallback(group=group_id, action='show').pack() 31 | ) 32 | 33 | 34 | def get_return(text: str = None) -> InlineKeyboardMarkup: 35 | kb = InlineKeyboardBuilder() 36 | 37 | kb.row(_return_button(text)) 38 | 39 | return kb.as_markup() 40 | 41 | 42 | def get_group_return(group_id: int, text: str = None) -> InlineKeyboardMarkup: 43 | kb = InlineKeyboardBuilder() 44 | 45 | kb.row(_group_button(text, group_id)) 46 | 47 | return kb.as_markup() 48 | 49 | 50 | async def get_all(user: User) -> InlineKeyboardMarkup: 51 | kb = InlineKeyboardBuilder() 52 | 53 | kb.row(InlineKeyboardButton( 54 | text='Создать', 55 | callback_data=GroupCallback(group=-1, action='create').pack() 56 | )) 57 | 58 | async for group in user.groups_member: 59 | text = 'O| ' * (await group.owner == user) 60 | kb.row(_group_button(text + group.name, group.pk)) 61 | 62 | return kb.as_markup() 63 | 64 | 65 | def get_group(group_id: int, owned: bool, show_leave: bool) -> InlineKeyboardMarkup: 66 | kb = InlineKeyboardBuilder() 67 | 68 | kb.row(_return_button()) 69 | 70 | if show_leave: 71 | kb.add(InlineKeyboardButton( 72 | text='Покинуть', 73 | callback_data=GroupCallback(group=group_id, action='leave').pack() 74 | )) 75 | 76 | if not owned: 77 | return kb.as_markup() 78 | 79 | kb.add(InlineKeyboardButton( 80 | text='Участники', 81 | callback_data=GroupCallback(group=group_id, action='members').pack() 82 | )) 83 | 84 | kb.row(InlineKeyboardButton( 85 | text='Изменить ссылку', 86 | callback_data=GroupCallback(group=group_id, action='password').pack() 87 | )) 88 | 89 | kb.add(InlineKeyboardButton( 90 | text='Изменить имя', 91 | callback_data=GroupCallback(group=group_id, action='name').pack() 92 | )) 93 | 94 | kb.add(InlineKeyboardButton( 95 | text='Пердежи', 96 | callback_data=GroupCallback(group=group_id, action='perdish').pack() 97 | )) 98 | 99 | kb.row(InlineKeyboardButton( 100 | text='Удалить группу', 101 | callback_data=GroupCallback(group=group_id, action='delete').pack() 102 | )) 103 | 104 | return kb.as_markup() 105 | 106 | 107 | async def get_group_members(group: Group) -> InlineKeyboardMarkup: 108 | kb = InlineKeyboardBuilder() 109 | 110 | kb.add(_group_button('Назад', group.pk)) 111 | 112 | async for member in group.members.all(): 113 | kb.add(InlineKeyboardButton( 114 | text=member.name, 115 | callback_data=DeleteGroupMemberCallback(uid=member.pk, group=group.pk, submit=False).pack() 116 | )) 117 | 118 | kb.adjust(1, 3) 119 | 120 | return kb.as_markup() 121 | 122 | 123 | def get_group_delete_member(uid: int, group_id: int) -> InlineKeyboardMarkup: 124 | kb = InlineKeyboardBuilder() 125 | 126 | kb.add(InlineKeyboardButton( 127 | text='Отмена', 128 | callback_data=GroupCallback(group=group_id, action='members').pack() 129 | )) 130 | 131 | kb.add(InlineKeyboardButton( 132 | text='Удалить', 133 | callback_data=DeleteGroupMemberCallback(uid=uid, group=group_id, submit=True).pack() 134 | )) 135 | 136 | return kb.as_markup() 137 | -------------------------------------------------------------------------------- /bot_service/handlers/friends/control.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, types, F 2 | from aiogram.filters import Command 3 | 4 | import config 5 | from db.User import User 6 | from db.UserUnion import FriendRequest 7 | from keyboards.friend import friends_keyboard 8 | from keyboards.friend.friends_keyboard import FriendMenuCallback, FriendUserCallback 9 | 10 | router = Router() 11 | 12 | 13 | def get_main_menu_text(user: User) -> str: 14 | friend_link = f'https://t.me/{config.bot_me.username}?start=IF{user.uid}' 15 | text = (f'Список ваших друзей. Для удаления нажмите на никнейм того, кого хотите удалить.\n' 16 | f'Если вы мутите входящие заявки в друзья, то для добавления в друзья вам нужно будет отправить ответную заявку.\n\n' 17 | f'Ваша ссылка для добавления вас в друзья - {friend_link}') 18 | 19 | return text 20 | 21 | 22 | @router.message(Command('friends')) 23 | async def friends_menu(message: types.Message, user: User): 24 | await message.answer(get_main_menu_text(user), reply_markup=await friends_keyboard.get(user)) 25 | 26 | 27 | @router.callback_query(FriendMenuCallback.filter(F.unit == 'main')) 28 | async def friends_menu_callback(callback: types.CallbackQuery, user: User): 29 | await callback.message.edit_text(get_main_menu_text(user), reply_markup=await friends_keyboard.get(user)) 30 | 31 | 32 | @router.callback_query(FriendMenuCallback.filter(F.unit == 'chgmut')) 33 | async def change_mute(callback: types.CallbackQuery, user: User): 34 | user.mute_friend_requests = not user.mute_friend_requests 35 | await user.save() 36 | 37 | await friends_menu_callback(callback, user) 38 | 39 | 40 | @router.callback_query(FriendUserCallback.filter(F.type == friends_keyboard.FriendUserType.friend)) 41 | async def delete_friend(callback: types.CallbackQuery): 42 | cb_data = FriendUserCallback.unpack(callback.data) 43 | user_2 = await User.filter(pk=cb_data.user_id).only('name').get() 44 | 45 | await callback.message.edit_text(f'Вы уверены, что хотите удалить {user_2.name} из друзей?', 46 | reply_markup=friends_keyboard.get_submit_delete(cb_data.user_id, 'main', friends_keyboard.FriendUserType.friend_submit)) 47 | 48 | 49 | @router.callback_query(FriendUserCallback.filter(F.type == friends_keyboard.FriendUserType.friend_submit)) 50 | async def delete_friend_submit(callback: types.CallbackQuery, user: User): 51 | cb_data = FriendUserCallback.unpack(callback.data) 52 | user_2 = await User.filter(pk=cb_data.user_id).get() 53 | 54 | await user.friends.remove(user_2) 55 | await user_2.friends.remove(user) 56 | 57 | await callback.answer(f'Удалили {user_2.name} из друзей.') 58 | await friends_menu_callback(callback, user) 59 | 60 | 61 | @router.callback_query(FriendMenuCallback.filter(F.unit == 'req')) 62 | async def friend_requests_menu(callback: types.CallbackQuery, user: User): 63 | await callback.message.edit_text('Ваши заявки в друзья. Для удаления нажмите на никнейм того, кого хотите удалить.', 64 | reply_markup=await friends_keyboard.get_requests(user)) 65 | 66 | 67 | @router.callback_query(FriendUserCallback.filter(F.type == friends_keyboard.FriendUserType.request)) 68 | async def delete_friend_request(callback: types.CallbackQuery): 69 | cb_data = FriendUserCallback.unpack(callback.data) 70 | user_2 = await User.filter(pk=cb_data.user_id).only('name').get() 71 | 72 | await callback.message.edit_text(f'Вы уверены, что хотите отозвать заявку {user_2.name} в друзья?', 73 | reply_markup=friends_keyboard.get_submit_delete(cb_data.user_id, 'req', friends_keyboard.FriendUserType.request_submit)) 74 | 75 | 76 | @router.callback_query(FriendUserCallback.filter(F.type == friends_keyboard.FriendUserType.request_submit)) 77 | async def delete_friend_request_submit(callback: types.CallbackQuery, user: User): 78 | cb_data = FriendUserCallback.unpack(callback.data) 79 | user_2 = await User.filter(pk=cb_data.user_id).get() 80 | 81 | # govnocode request 82 | request = await FriendRequest.filter(user=user, requested_user=user_2).get() 83 | await request.delete() 84 | 85 | if request.message_id is not None: 86 | try: 87 | await config.bot.delete_message(user_2.uid, request.message_id) 88 | 89 | except: 90 | await config.bot.edit_message_text('Удаленная заявка', reply_markup=None) 91 | 92 | await callback.answer(f'Отозвали заявку {user_2.name} в друзья.') 93 | await friend_requests_menu(callback, user) 94 | -------------------------------------------------------------------------------- /utils/send_srat_notification.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Optional 3 | 4 | import aiogram.exceptions 5 | import pytz 6 | from aiogram import types 7 | from loguru import logger 8 | 9 | import config 10 | from brocker import message_sender 11 | from db.ToiletSessions import SretSession, SretType 12 | from db.User import User 13 | from keyboards import sret_keyboard 14 | 15 | must_not_sret = [1, 2] 16 | must_sret = [0] 17 | 18 | 19 | async def verify_action(user: User, sret: int, message: Optional[types.Message] = None): 20 | last_session = await SretSession.filter(user=user).order_by('-message_id').first() 21 | 22 | has_opened_session = False 23 | if last_session is not None: 24 | has_opened_session = last_session.end is None 25 | 26 | if sret in must_sret and not has_opened_session: 27 | if message is not None: 28 | await message.reply('Ты что заканчивать захотел? Ты даже не срешь!') 29 | return False 30 | 31 | if sret in must_not_sret and has_opened_session: 32 | if message is not None: 33 | await message.reply('Ты прошлое свое сранье не закончил, а уже новое начинаешь?\n' 34 | 'Нет уж. Будь добр, раз начал - закончи.') 35 | return False 36 | 37 | if has_opened_session or last_session is None: 38 | return True 39 | 40 | throttling_time = timedelta(minutes=config.Constants.throttling_time_actions[last_session.sret_type - 1][sret - 1]) 41 | now = datetime.now(pytz.UTC).astimezone() 42 | if (now - last_session.end) <= throttling_time: 43 | if message is not None: 44 | wait_time = round((throttling_time - (now - last_session.end)).total_seconds()) 45 | await message.reply(f'Вы совершаете действия слишком часто!\n' 46 | f'Подождите еще {wait_time} секунд') 47 | return False 48 | 49 | return True 50 | 51 | 52 | def get_message_text(user: User, sret: int): 53 | if sret == 1: 54 | text = '⚠️ ВНИМАНИЕ ⚠️\n' \ 55 | '%s прямо сейчас пошел срать' 56 | 57 | elif sret == 0: 58 | text = '⚠️ ВНИМАНИЕ ⚠️\n' \ 59 | '%s закончил срать' 60 | 61 | elif sret == 3: 62 | text = '⚠️ ВНИМАНИЕ ⚠️\n' \ 63 | '%s просто пернул' 64 | 65 | elif sret == 2: 66 | text = '⚠️️️️⚠️⚠️ ВНИМАНИЕ ⚠️⚠️⚠️\n\n' \ 67 | '⚠️НАДВИГАЕТСЯ ГОВНОПОКАЛИПСИС⚠️\n' \ 68 | '%s прямо сейчас пошел адски дристать лютейшей струей поноса' 69 | 70 | text %= user.name 71 | 72 | return text 73 | 74 | 75 | async def send(user: User, sret: int): 76 | text = get_message_text(user, sret) 77 | 78 | # Send self 79 | self_message = await config.bot.send_message(user.uid, text, 80 | reply_markup=sret_keyboard.get(user.autoend) if sret in must_not_sret else None) 81 | 82 | # DB operations 83 | if sret in must_not_sret: 84 | await SretSession.create(message_id=self_message.message_id, user=user, 85 | sret_type=SretType.DRISHET if sret == 2 else SretType.SRET, 86 | autoend=user.autoend) 87 | 88 | else: 89 | session = await SretSession.filter(user=user, end=None).first() 90 | 91 | if session is not None: 92 | session.end = datetime.now() 93 | session.autoend = False 94 | if sret == 3: 95 | session.sret_type = SretType.PERNUL 96 | 97 | await session.save() 98 | try: 99 | await config.bot.edit_message_reply_markup(user.uid, session.message_id) 100 | 101 | except aiogram.exceptions.TelegramBadRequest: 102 | ... 103 | 104 | else: 105 | await SretSession.create(message_id=self_message.message_id, user=user, end=datetime.now(pytz.UTC), 106 | sret_type=SretType.PERNUL, autoend=False) 107 | 108 | # Send notifications 109 | users_send = set() 110 | 111 | group_query = user.groups_member 112 | if sret == 3: 113 | group_query = group_query.filter(notify_perdish=True) 114 | async for group in group_query: 115 | users_send = users_send.union(set(await group.members.all())) 116 | 117 | users_send = users_send.union(set(await user.friends.all())) 118 | 119 | try: 120 | users_send.remove(user) 121 | 122 | except KeyError: 123 | ... 124 | 125 | await message_sender.send_message(config.Telegram.global_channel_id, user.uid, self_message.message_id, 1) 126 | for send_to in users_send: 127 | await message_sender.send_message(send_to.uid, user.uid, self_message.message_id, 1) 128 | 129 | async for send_to_channel in user.channels_member.all(): 130 | await message_sender.send_message(send_to_channel.channel_id, user.uid, self_message.message_id, 1, show_sender=True) 131 | 132 | return self_message.message_id 133 | -------------------------------------------------------------------------------- /keyboards/channels_keyboard.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters.callback_data import CallbackData 2 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 3 | from aiogram.utils.keyboard import InlineKeyboardBuilder 4 | 5 | from db.User import User 6 | from db.UserUnion import Channel 7 | from utils import paged_keyboard 8 | 9 | channels_on_one_page = 9 10 | channel_members_on_one_page = 18 11 | 12 | 13 | class ChannelCallbackData(CallbackData, prefix="chn"): 14 | channel_id: int 15 | action: str 16 | 17 | 18 | class ChannelMemberDeleteCallbackData(CallbackData, prefix="chnmd"): 19 | channel_id: int 20 | user_id: int 21 | submit: bool 22 | 23 | 24 | class ChannelPagedCallbackData(paged_keyboard.PagedCallbackData, prefix="chnp"): 25 | channel_id: int = -1 26 | unit: str 27 | 28 | 29 | async def get_menu(user: User, page: int) -> InlineKeyboardMarkup: 30 | kb = InlineKeyboardBuilder() 31 | 32 | kb.add(InlineKeyboardButton( 33 | text='Создать', 34 | callback_data=ChannelCallbackData(channel_id=-1, action='create').pack() 35 | )) 36 | 37 | channels = await user.channels_member.all().offset(channels_on_one_page * page).limit(channels_on_one_page + 1) 38 | 39 | navigation, i = paged_keyboard.draw_page_navigation(channels, page, channels_on_one_page, 40 | page_callback_data_class=ChannelPagedCallbackData, unit='menu') 41 | kb.add(*navigation) 42 | 43 | for channel in channels: 44 | kb.add(InlineKeyboardButton( 45 | text=channel.name, 46 | callback_data=ChannelCallbackData(channel_id=channel.channel_id, action='channel').pack() 47 | )) 48 | 49 | kb.adjust(1 + i, 3) 50 | 51 | return kb.as_markup() 52 | 53 | 54 | def get_channel(channel_id: int, is_owner: bool) -> InlineKeyboardMarkup: 55 | kb = InlineKeyboardBuilder() 56 | 57 | kb.row(InlineKeyboardButton( 58 | text='Назад', 59 | callback_data=ChannelPagedCallbackData(unit='menu', page=0).pack() 60 | )) 61 | 62 | if not is_owner: 63 | kb.add(InlineKeyboardButton( 64 | text='Покинуть', 65 | callback_data=ChannelCallbackData(channel_id=channel_id, action='leave').pack() 66 | )) 67 | 68 | return kb.as_markup() 69 | 70 | kb.row(InlineKeyboardButton( 71 | text='Участники', 72 | callback_data=ChannelPagedCallbackData(channel_id=channel_id, unit='members', page=0).pack() 73 | )) 74 | 75 | kb.add(InlineKeyboardButton( 76 | text='Изменить ссылку', 77 | callback_data=ChannelCallbackData(channel_id=channel_id, action='password').pack() 78 | )) 79 | 80 | kb.add(InlineKeyboardButton( 81 | text='Пердежи', 82 | callback_data=ChannelCallbackData(channel_id=channel_id, action='perdish').pack() 83 | )) 84 | 85 | kb.row(InlineKeyboardButton( 86 | text='Обновить имя', 87 | callback_data=ChannelCallbackData(channel_id=channel_id, action='name').pack() 88 | )) 89 | 90 | kb.row(InlineKeyboardButton( 91 | text='Удалить канал', 92 | callback_data=ChannelCallbackData(channel_id=channel_id, action='delete').pack() 93 | )) 94 | 95 | return kb.as_markup() 96 | 97 | 98 | def get_channel_delete_submit(channel_id: int) -> InlineKeyboardMarkup: 99 | kb = InlineKeyboardBuilder() 100 | 101 | kb.add(InlineKeyboardButton( 102 | text='Отменить', 103 | callback_data=ChannelCallbackData(channel_id=channel_id, action='channel').pack() 104 | )) 105 | 106 | kb.add(InlineKeyboardButton( 107 | text='Подтвердить', 108 | callback_data=ChannelCallbackData(channel_id=channel_id, action='delete_submit').pack() 109 | )) 110 | 111 | return kb.as_markup() 112 | 113 | 114 | async def get_channel_members(channel: Channel, page: int) -> InlineKeyboardMarkup: 115 | kb = InlineKeyboardBuilder() 116 | 117 | kb.add(InlineKeyboardButton( 118 | text='К каналу', 119 | callback_data=ChannelCallbackData(channel_id=channel.channel_id, action='channel').pack() 120 | )) 121 | 122 | members = await channel.members.all().offset(channel_members_on_one_page * page).limit(channels_on_one_page + 1) 123 | 124 | navigation, i = paged_keyboard.draw_page_navigation(members, page, channels_on_one_page, 125 | page_callback_data_class=ChannelPagedCallbackData, 126 | unit='members', channel_id=channel.channel_id) 127 | kb.add(*navigation) 128 | 129 | for el in members: 130 | kb.add(InlineKeyboardButton( 131 | text=el.name, 132 | callback_data=ChannelMemberDeleteCallbackData(channel_id=channel.channel_id, user_id=el.uid, submit=False).pack() 133 | )) 134 | 135 | kb.adjust(1 + i, 3) 136 | 137 | return kb.as_markup() 138 | 139 | 140 | def get_delete_user_submit(channel_id: int, user_id: int) -> InlineKeyboardMarkup: 141 | kb = InlineKeyboardBuilder() 142 | 143 | kb.add(InlineKeyboardButton( 144 | text='Отменить', 145 | callback_data=ChannelPagedCallbackData(channel_id=channel_id, unit='members', page=0).pack() 146 | )) 147 | 148 | kb.add(InlineKeyboardButton( 149 | text='Подтвердить', 150 | callback_data=ChannelMemberDeleteCallbackData(channel_id=channel_id, user_id=user_id, submit=True).pack() 151 | )) 152 | 153 | return kb.as_markup() 154 | -------------------------------------------------------------------------------- /bot_service/handlers/admin/notify.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import aiogram 4 | from aiogram import Router, F 5 | from aiogram import types 6 | from aiogram.filters import Command, CommandObject 7 | from aiogram.fsm.context import FSMContext 8 | from aiogram.fsm.state import StatesGroup, State 9 | from tortoise.functions import Count 10 | 11 | import config 12 | from brocker import message_sender 13 | from db.User import User 14 | from db.Notify import Notify 15 | from bot_service.filters import UserAuthFilter 16 | from keyboards import notify_keyboard 17 | 18 | router = Router() 19 | 20 | 21 | class SendNotify(StatesGroup): 22 | writing_message = State() 23 | submit = State() 24 | 25 | 26 | @router.message(Command("notify"), UserAuthFilter(admin=True)) 27 | async def notify(message: types.Message, state: FSMContext): 28 | text = (f'Отправьте текст, который хотите отправить в качестве уведомления.\n\n' 29 | f'*Правила форматирования:*\n_Такое же как в обычных сообщениях_') 30 | 31 | await message.delete() 32 | 33 | await state.set_state(SendNotify.writing_message) 34 | last_msg = await message.answer(text, reply_markup=notify_keyboard.get(only_cancel=True)) 35 | await state.update_data({'last_msg': last_msg.message_id}) 36 | 37 | 38 | @router.message(SendNotify.writing_message) 39 | async def notify_get_message(message: types.Message, state: FSMContext): 40 | last_msg = (await state.get_data()).get('last_msg') 41 | 42 | await state.set_state(SendNotify.submit) 43 | await state.update_data({'message_id': message.message_id}) 44 | 45 | await config.bot.edit_message_text( 46 | f'Получили, Подтверждаем отправку?', 47 | message.chat.id, 48 | last_msg, 49 | reply_markup=notify_keyboard.get(), 50 | ) 51 | 52 | 53 | @router.callback_query(notify_keyboard.Notify.filter(F.action == 'cancel')) 54 | async def cancel_notify(callback: types.CallbackQuery, state: FSMContext): 55 | await state.clear() 56 | await callback.message.edit_text('Отменили отправку.') 57 | 58 | await asyncio.sleep(5) 59 | await callback.message.delete() 60 | 61 | 62 | @router.callback_query(SendNotify.submit, notify_keyboard.Notify.filter(F.action == 'submit')) 63 | async def submit_notify(callback: types.CallbackQuery, state: FSMContext, user: User): 64 | original_message_id = (await state.get_data()).get('message_id') 65 | 66 | users = await User.all() 67 | notify_instance = await Notify.create(message_id=original_message_id, initiated_by=user, scheduled_users_count=len(users)) 68 | 69 | for send_to in users: 70 | await message_sender.send_message(send_to.uid, user.uid, original_message_id, 0, notify_instance.pk) 71 | 72 | admin_text = (f'Отправка уведомлений начата.\n' 73 | f'Айди уведомления {notify_instance.pk}\n' 74 | f'Получить актуальную информацию о рассылке:\n/nstatus {notify_instance.pk}') 75 | await callback.message.edit_text(admin_text) 76 | 77 | await state.clear() 78 | 79 | 80 | async def get_notify_status_text(notify_id: int) -> str: 81 | notify_instance = await Notify.filter(pk=notify_id).get_or_none() 82 | if notify_instance is None: 83 | return '' 84 | 85 | if notify_instance.executed_users_count == notify_instance.scheduled_users_count: 86 | status = 'ЗАВЕРШЕНО' 87 | 88 | elif notify_instance.executed_users_count == 0: 89 | status = 'В ОЧЕРЕДИ' 90 | 91 | else: 92 | status = 'В ПРОЦЕССЕ' 93 | 94 | percent = round((notify_instance.executed_users_count / notify_instance.scheduled_users_count) * 100, 1) 95 | text = (f'Уведомление №{notify_id}\n' 96 | f'Статус: {status}\n\n' 97 | f'Исполнено: {notify_instance.executed_users_count}/{notify_instance.scheduled_users_count} - {percent}%') 98 | 99 | if notify_instance.executed_users_count != notify_instance.scheduled_users_count: 100 | text += f'\n\nВремя рассылки ~{round((notify_instance.scheduled_users_count - notify_instance.executed_users_count) * 2 / 60, 1)} мин' 101 | 102 | return text 103 | 104 | 105 | @router.message(Command("nstatus"), UserAuthFilter(admin=True)) 106 | async def nstatus(message: types.Message, command: CommandObject): 107 | if command.args is None: 108 | text = 'Очередь уведомлений:\n' 109 | 110 | notifys = Notify.all().order_by('created_at').limit(5) 111 | async for notify_o in notifys: 112 | now = f'{notify_o.executed_users_count}/{notify_o.init_queue_size}' if notify_o.executed_users_count != 0 else 'в очереди' 113 | text += f'- №{notify_o.pk}: _{now}_\n' 114 | 115 | await message.reply(text) 116 | 117 | return 118 | 119 | if not command.args.isnumeric(): 120 | await message.reply('Айди должно быть числом') 121 | return 122 | notify_id = int(command.args) 123 | 124 | text = await get_notify_status_text(notify_id) 125 | if text == '': 126 | await message.reply('Уведомление не найдено.') 127 | return 128 | 129 | await message.reply(text, reply_markup=notify_keyboard.get_update()) 130 | 131 | 132 | @router.callback_query(notify_keyboard.Notify.filter(F.action == 'update')) 133 | async def nstatus_update(callback: types.CallbackQuery): 134 | i = callback.message.text.find('№') 135 | j = callback.message.text.find('\n') 136 | notify_id = callback.message.text[i + 1:j] 137 | 138 | if not notify_id.isnumeric(): 139 | await callback.message.edit_text('Уведомление не найдено.') 140 | return 141 | 142 | text = await get_notify_status_text(int(notify_id)) 143 | if text == '': 144 | await callback.message.edit_text('Уведомление не найдено.') 145 | return 146 | 147 | try: 148 | await callback.message.edit_text(text, reply_markup=notify_keyboard.get_update()) 149 | 150 | except aiogram.exceptions.TelegramBadRequest: 151 | await callback.answer('Ничего не изменилось.') 152 | -------------------------------------------------------------------------------- /bot_service/handlers/api.py: -------------------------------------------------------------------------------- 1 | import aiogram 2 | from aiogram import Router, F, Bot 3 | from aiogram import types 4 | from aiogram.filters import Command, CommandObject 5 | from aiogram.fsm.context import FSMContext 6 | from aiogram.fsm.state import StatesGroup, State 7 | from tortoise.exceptions import ValidationError 8 | 9 | import config 10 | from db.ApiAuth import ApiToken, TokenNameValidator 11 | from db.User import User 12 | from keyboards import api_keyboard 13 | 14 | router = Router() 15 | 16 | menu_text = ('Меню управления API токенами\n' 17 | 'Для того чтобы отозвать токен нажмите на него.\n\n' 18 | 'Документация к API.') 19 | 20 | requirements_text = ('Название должно соответствовать данным условиям:\n' 21 | ' - Длина от 3 до 64 символов\n' 22 | ' - Cимволы могут быть только строчными латинскими буквами, цифрами и нижними подчеркиваниями\n') 23 | 24 | class NewTokenStates(StatesGroup): 25 | writing_name = State() 26 | 27 | 28 | @router.message(Command("api")) 29 | async def api_menu(message: types.Message, user: User, command: CommandObject): 30 | if command.args is not None: 31 | await message.delete() 32 | 33 | hashed_token = ApiToken.hash_token(command.args) 34 | token = await ApiToken.get_or_none(token=hashed_token) 35 | 36 | if token is None: 37 | await message.answer('Токен не найден.') 38 | return 39 | 40 | await message.answer(f'Токен {token.name}\n\n' 41 | f'Owner: {token.owner_id}\n' 42 | f'ID: {token.id}\n' 43 | f'Valid: {["No", "Yes"][token.valid]}\n' 44 | f'Created at: {token.created_at}') 45 | 46 | return 47 | 48 | await message.answer(menu_text, reply_markup=await api_keyboard.get(user)) 49 | 50 | 51 | @router.callback_query(api_keyboard.ApiCallback.filter(F.action == 'menu')) 52 | async def api_menu_button(callback: types.CallbackQuery, user: User): 53 | await callback.message.edit_text(menu_text, reply_markup=await api_keyboard.get(user)) 54 | 55 | 56 | @router.callback_query(api_keyboard.ApiCallback.filter(F.action == 'revoke')) 57 | async def revoke_api_token(callback: types.CallbackQuery): 58 | unpacked_data = api_keyboard.ApiCallback.unpack(callback.data) 59 | 60 | token_name = '' 61 | for button in callback.message.reply_markup.inline_keyboard: 62 | if button[0].callback_data == callback.data: 63 | token_name = button[0].text 64 | 65 | await callback.message.edit_text(f'Вы уверены, что хотите отозвать токен {token_name}?\n' 66 | f'Это действие нельзя отменить.', 67 | reply_markup=api_keyboard.get_revoke_submit(unpacked_data.token)) 68 | 69 | 70 | @router.callback_query(api_keyboard.ApiCallback.filter(F.action == 'revoke_submit')) 71 | async def revoke_api_token_submit(callback: types.CallbackQuery, user: User): 72 | unpacked_data = api_keyboard.ApiCallback.unpack(callback.data) 73 | 74 | await ApiToken.filter(pk=unpacked_data.token).update(valid=False) 75 | 76 | text = callback.message.html_text 77 | token_name = text[text.find('') + 6:text.find('')] 78 | 79 | await callback.answer(f'Токен {token_name} отозван.') 80 | await api_menu_button(callback, user) 81 | 82 | 83 | @router.callback_query(api_keyboard.ApiCallback.filter(F.action == 'new')) 84 | async def new_api_token(callback: types.CallbackQuery, user: User, state: FSMContext): 85 | if await user.tokens_owned.filter(valid=True).count() >= config.Constants.max_api_tokens: 86 | await callback.answer(f'Вы не можете создать более чем {config.Constants.max_api_tokens} токенов.', show_alert=True) 87 | return 88 | 89 | await state.set_state(NewTokenStates.writing_name) 90 | await state.update_data(last_msg=callback.message.message_id) 91 | 92 | await callback.message.edit_text('Напишите название токена.\n\n' + requirements_text) 93 | 94 | 95 | @router.message(NewTokenStates.writing_name) 96 | async def new_api_token_wrote_name(message: types.Message, user: User, state: FSMContext, bot: Bot): 97 | await message.delete() 98 | 99 | token_name = message.text 100 | last_msg = (await state.get_data()).get('last_msg') 101 | 102 | if not(3 <= len(token_name) <= 64): 103 | try: 104 | await bot.edit_message_text('Недопустимая длина названия.\n\n' + requirements_text, message.chat.id, last_msg) 105 | 106 | except aiogram.exceptions.TelegramBadRequest: 107 | await bot.edit_message_text('Недопустимая длина названия\n\n' + requirements_text, message.chat.id, last_msg) 108 | 109 | return 110 | 111 | try: 112 | TokenNameValidator()(token_name) 113 | 114 | except ValidationError as e: 115 | try: 116 | await bot.edit_message_text(f'Недопустимый символ на позиции {e.args[1] + 1}.\n\n' + requirements_text, message.chat.id, last_msg) 117 | 118 | except aiogram.exceptions.TelegramBadRequest: 119 | await bot.edit_message_text(f'Недопустимый символ на позиции {e.args[1] + 1}\n\n' + requirements_text, message.chat.id, last_msg) 120 | 121 | return 122 | 123 | await state.clear() 124 | 125 | secret, hashed_secret = ApiToken.generate_token() 126 | token = await ApiToken.create(token=hashed_secret, name=token_name, owner=user) 127 | 128 | await bot.edit_message_text(menu_text, message.chat.id, last_msg, reply_markup=await api_keyboard.get(user)) 129 | 130 | await message.answer(f'Токен {token.name}\n\n' 131 | f'ID:\n{token.id}\n' 132 | f'TOKEN:\n{secret}\n\n' 133 | f'Created at: {token.created_at}') 134 | -------------------------------------------------------------------------------- /ura_button/esp32_device/src/WifiController.cpp: -------------------------------------------------------------------------------- 1 | #include "WifiController.h" 2 | #include "IterablePreferences.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | using namespace WiFiController; 11 | 12 | std::map WiFiController::saved; 13 | String available; 14 | 15 | Preferences preferences; 16 | 17 | WiFiMulti wifiMulti; 18 | 19 | void get_available(AsyncWebServerRequest *request) { 20 | request->send(200, "application/json", available); 21 | } 22 | 23 | void get_saved(AsyncWebServerRequest *request) { 24 | JsonDocument payload; 25 | JsonArray array = payload.to(); 26 | 27 | for (const auto& [k, v] : saved) { 28 | JsonDocument now; 29 | now["ssid"] = k; 30 | now["password"] = v; 31 | array.add(now); 32 | } 33 | 34 | String data_s; 35 | serializeJson(payload, data_s); 36 | 37 | request->send(200, "application/json", data_s); 38 | } 39 | 40 | 41 | void add_saved(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { 42 | JsonDocument json; 43 | DeserializationError err = deserializeJson(json, data); 44 | if (err) { 45 | request->send(400, "application/json", R"({"detail": "Invalid json"})"); 46 | return; 47 | } 48 | 49 | if (saved.size() >= 30) { 50 | request->send(400, "application/json", R"({"detail": "Cannot add more than 30 wifi networks"})"); 51 | return; 52 | } 53 | 54 | if (not json["ssid"].is()) { 55 | request->send(400, "application/json", R"({"detail": "Invalid ssid field type"})"); 56 | return; 57 | } 58 | 59 | if (json["ssid"].as().length() > 128) { 60 | request->send(413, "application/json", R"({"detail": "Len of ssid cannot be more than 128"})"); 61 | return; 62 | } 63 | 64 | if (not json["password"].is()) { 65 | request->send(400, "application/json", R"({"detail": "Invalid password field type"})"); 66 | return; 67 | } 68 | 69 | if (json["password"].as().length() > 128) { 70 | request->send(413, "application/json", R"({"detail": "Len of pass cannot be more than 128"})"); 71 | return; 72 | } 73 | 74 | saved[json["ssid"]] = json["password"].as(); 75 | preferences.putString(json["ssid"], json["password"].as()); 76 | 77 | request->send(204); 78 | } 79 | 80 | void delete_saved(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { 81 | JsonDocument json; 82 | DeserializationError err = deserializeJson(json, data); 83 | if (err) { 84 | request->send(400, "application/json", R"({"detail": "Invalid json"})"); 85 | return; 86 | } 87 | 88 | if (not json["ssid"].is()) { 89 | request->send(400, "application/json", R"({"detail": "Invalid ssid field type"})"); 90 | return; 91 | } 92 | 93 | if (json["ssid"].as().length() > 128) { 94 | request->send(413, "application/json", R"({"detail": "Len of ssid cannot be more than 128"})"); 95 | return; 96 | } 97 | 98 | if (saved.count(json["ssid"]) == 0) { 99 | request->send(404, "application/json", R"({"detail": "Can't find saved wifi with provided ssid"})"); 100 | return; 101 | } 102 | 103 | saved.erase(json["ssid"].as()); 104 | preferences.remove(json["ssid"]); 105 | 106 | request->send(204); 107 | } 108 | 109 | 110 | void WiFiController::setup(AsyncWebServer *server) { 111 | WiFi.mode(WIFI_STA); 112 | 113 | // scan wifis 114 | int n = WiFi.scanNetworks(); 115 | 116 | JsonDocument payload; 117 | JsonArray array = payload.to(); 118 | 119 | for (int i = 0; i < n; ++i) { 120 | JsonDocument now; 121 | now["ssid"] = WiFi.SSID(i); 122 | now["rssi"] = WiFi.RSSI(i); 123 | array.add(now); 124 | } 125 | 126 | serializeJson(payload, available); 127 | 128 | // Begining preferences 129 | preferences.begin("wifi", false); 130 | 131 | IterablePreferences prefs; 132 | prefs.foreach ("wifi", [](const char* key, nvs_type_t type) { 133 | saved[key] = preferences.getString(key); 134 | }); 135 | prefs.end(); 136 | 137 | // Registering endpoints 138 | server->on("/wifi/available/", HTTP_GET, get_available); 139 | server->on("/wifi/", HTTP_GET, get_saved); 140 | server->on("/wifi/", HTTP_POST, [](AsyncWebServerRequest *){}, nullptr, add_saved); 141 | server->on("/wifi/", HTTP_DELETE, [](AsyncWebServerRequest *){}, nullptr, delete_saved); 142 | 143 | // Connecting Wifi 144 | for (const auto& [k, v] : saved) { 145 | wifiMulti.addAP(k.c_str(), v.c_str()); 146 | } 147 | 148 | connect(); 149 | } 150 | 151 | constexpr long long INF = 1e18; 152 | constexpr long long waiting_network = 30000; 153 | long long last_not_connected = INF; 154 | 155 | void WiFiController::connect() { 156 | if (wifiMulti.run() == WL_CONNECTED) { 157 | last_not_connected = INF; 158 | 159 | Serial.println("Wifi connected"); 160 | Serial.println(WiFi.SSID()); 161 | Serial.print("IP: "); 162 | Serial.println(WiFi.localIP()); 163 | 164 | } else { 165 | if (last_not_connected == INF) { 166 | last_not_connected = millis(); 167 | } 168 | 169 | if ((millis() - last_not_connected) >= waiting_network) { 170 | Serial.println("Can't connect to Wifi. Starting AP..."); 171 | 172 | WiFi.disconnect(); 173 | WiFi.mode(WIFI_AP); 174 | WiFi.softAP("URA_BUTTON", "aboba123"); 175 | 176 | IPAddress ip(192, 168, 1, 1); //setto IP Access Point same as gateway 177 | IPAddress mask(255, 255, 255, 0); 178 | WiFi.softAPConfig(ip, ip, mask); 179 | 180 | Serial.print("Set IP: "); 181 | Serial.println(ip); 182 | 183 | } else { 184 | Serial.print("Can't connect to Wifi. Starting AP in "); 185 | Serial.print(waiting_network - ((millis() - last_not_connected))); 186 | Serial.println("ms"); 187 | } 188 | } 189 | } 190 | 191 | void WiFiController::handle() { 192 | if (WiFi.getMode() != WIFI_MODE_AP and not WiFi.isConnected()) { 193 | Serial.println("Lost WiFi connection"); 194 | connect(); 195 | } 196 | } 197 | 198 | 199 | -------------------------------------------------------------------------------- /frontend_service/templates/anal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | YPA Project 6 | 11 | 12 | 13 | 14 |
17 | Бот 22 | Новости 27 | 28 | Аналитика 33 |
34 |
37 |
40 |
43 |
46 |
47 |

Всего уведомлений

48 |

За все время

49 |
50 |

51 | {{ type1_total + type2_total + type3_total }} 52 |

53 |
54 |
57 |
58 |

Всего насрано

59 |

За все время

60 |
61 |

{{ type1_total }}

62 |
63 |
66 |
67 |

Всего надристано

68 |

За все время

69 |
70 |

{{ type2_total }}

71 |
72 |
75 |
76 |

Всего напержено

77 |

За все время

78 |
79 |

{{ type3_total }}

80 |
81 |
84 |
85 |

Лучшие по активности

86 |

За все время

87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | {% for record in leaderboard1 %} 97 | 98 | 99 | 100 | 101 | {% endfor %} 102 | 103 |
Имячисло
{{ record.name }}{{ record.count.unwrap_or(0) }}
104 |
105 |
108 |
109 |

Лучшие по пердежам

110 |

За все время

111 |
112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | {% for record in leaderboard2 %} 121 | 122 | 123 | 124 | 125 | {% endfor %} 126 | 127 |
Имячисло
{{ record.name }}{{ record.count.unwrap_or(0) }}
128 |
129 |
132 |
133 |

Лучшие по сранью

134 |

За все время

135 |
136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | {% for record in leaderboard3 %} 145 | 146 | 147 | 148 | 149 | {% endfor %} 150 | 151 |
Имячисло
{{ record.name }}{{ record.count.unwrap_or(0) }}
152 |
153 |
154 | 155 | 156 | -------------------------------------------------------------------------------- /bot_service/handlers/info.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, F 2 | from aiogram import types 3 | from aiogram.filters import Command 4 | from aiogram.types import LinkPreviewOptions 5 | from aiogram.utils.formatting import Text, TextLink, Pre, Bold, Italic 6 | 7 | from keyboards import guide_keyboard 8 | from keyboards.guide_keyboard import GuideCallbackData 9 | 10 | router = Router() 11 | 12 | history = '''С давних времён, с самого зарождения цивилизаций, информация всегда была самым ценным ресурсом. Какой смысл в добыче металлов, если ты не знаешь, как изготовить из этого металла инструмент? Передача же информации была чем-то сакральным, она передавалась от отца к сыну, от кузнеца к подмастерью и всегда была на вес золота. Войны выигрывались из-за получения ключевой информации о противнике одной из сторон. 13 | 14 | Сейчас идёт век глобализации, информация стала более доступной для обычного человека, но по настоящему важная информация остаётся скрытой в архивах государств, за дверьми домов самых богатых людей мира и в памяти учёных, которые не хотят делиться своими знаниями. 15 | 16 | И вот, группа энтузиастов, которые хотят явить народу истинные знания, представляют вашему вниманию проект УРА! 17 | Уведомления Ректальной Активности откроет вам глаза на важный аспект нашей жизни, на личную жизнь жопы каждого, кто присоединился к нашему проекту. 18 | 19 | Всё началось с идеи, что каждый наш друг должен знать, что мы в безопасности, что наша жопа открыта для каждого толчка на Земле. Позже наша небольшая команда поняла, что в этом нуждается каждый житель планеты и мы начали масштабироваться. Мы сделали всё, чтобы каждый смог прикоснуться к Big Data, к которой ранее имели доступ только самые богатые корпорации и люди планеты. Сегодня наш проект является лучшим олицетворением свободы передачи информации всемирного масштаба. Мы не остановимся ни перед чем, чтобы каждый мог посрать не в одиночестве!''' 20 | 21 | credits_text = Text( 22 | Bold('УРА - Уведомление Ректальных Активностей'), '\n', 23 | 'Бот, чтобы держать всех в курсе о ваших ректальных деяниях.\n', 24 | TextLink('Новостной канал', url='https://t.me/uraproject'), '\n', 25 | '\n', 26 | Pre(history, language='История'), 27 | '\n\n', 28 | TextLink('Степан Хожемпо', url='https://github.com/teleportx/'), ' - Backend developer & Bot developer', '\n', 29 | TextLink('Максим Кузнецов', url='https://github.com/uuuuuno/'), ' - Frontend developer & DevOps engineer', '\n', 30 | TextLink('Алексей Шаблыкин', url='https://t.me/AllShabCH'), ' - Project manager', '\n', 31 | TextLink('Матвей Рябчиков', url='https://github.com/ronanru/'), ' - Frontend developer', '\n', 32 | '\n', 33 | TextLink('Source Code', url='https://github.com/teleportx/URA-project') 34 | ) 35 | 36 | guide_text_main = Text( 37 | Bold('Гайд по работе с ботом'), '\n\n', 38 | Bold('Основной функционал:'), '\n\n', 39 | Pre('Нажимаете на кнопку только тогда, когда идёте срать. Всем чат (группы, если таковая имеется и глобальный) отправляется уведомление, что вы идёте срать. Под уведомлением появляется кнопка "отменить автозавершение сранья". При её нажатии автоматическое завершение акта дефекации через 10 минут будет отключено.', language='Я иду срать'), '\n\n', 40 | Pre('Функционал аналогичен кнопке "я иду срать", но нажимаете её только тогда, когда понимаете, что вы идёте на санфаянсовый престол, с целью покакать жидко', language='Я иду ЛЮТЕЙШЕ ДРИСТАТЬ'), '\n\n', 41 | Pre('Кнопка, при нажатии на которую сессия на вашем толчке будет сразу завершена. Всем в чате отправится соответствующее уведомление. Если вы отключили автоматическое завершение сранья и не нажали кнопку "Я закончил срать", то через час ваш акт производства говна автоматически завершится, при этом сам акт засчитан в статистику не будет.', language='Я закончил срать'), '\n\n', 42 | Pre('Кнопка нажимается в двух случаях: когда вы действительно просто пернули, а так же когда вы уже нажали кнопку "Я иду срать" или "Я иду ЛЮТЕЙШЕ ДРИСТАТЬ", но при этом пастилы из вашей жопы выдавлено не было. Во втором случае, бот закроет вашу сессию, по аналогии с кнопкой "Я закончил срать".', language='Я просто пернул'), '\n\n', 43 | 'Помните, что в нашем комьюнити мы не обманываем единомышленников и прожимаем пердёж только тогда, когда пёрнули:)\n', 44 | 'Пользуйтесь кнопками под сообщением, чтобы узнать поподробнее о конкретной функции бота.', '\n\n', 45 | Italic('Удачи с держанием всех в курсе своих ректальных активностей!'), 46 | ) 47 | 48 | guide_text_groups = Text( 49 | Bold('Работа с группами'), '\n\n', 50 | Pre('Команда, вызывающая интерфейс работы с группами', language='/groups'), '\n\n', 51 | Pre('Кнопка, при нажатии на которую вас попросят ввести название новой группы, которая будет создана. При новом запуске интерфейса, с помощью команды /groups, она будет отображаться в списке. При нажатии на эту группу, вы перейдёте в интерфейс управления группой.', language='Создать'), '\n\n', 52 | Bold('Интерфейс управления группой:'), '\n\n', 53 | Pre('При нажатии кнопки открывается список участников. Удалить участника группы можно нажатием на его имя.', language='Участники'), '\n\n', 54 | Pre('Если есть подозрение, что ссылка на вашу группу попала туда, куда не следует, нажмите на эту кнопку, чтобы изменить ссылку-приглашение.', language='Изменить ссылку'), '\n\n', 55 | Pre('При нажатии на эту кнопку, надо написать новое имя вашей группы', language='Изменить имя'), '\n\n', 56 | Pre('Если группа больше не нужна, необходимо нажать на эту кнопку. Перед удалением нужно написать название группы, чтобы она была стёрта с просторов интернета.', language='Удалить группу'), '\n\n', 57 | ) 58 | 59 | guide_text_friends = Text( 60 | Bold('Друзья'), '\n\n', 61 | Pre('Открывает интерфейс управления друзьями.', language='/friends'), '\n\n', 62 | Pre('Показывает список входящих заявок в друзья.', language='Кнопка "Мои заявки"'), '\n\n', 63 | ) 64 | 65 | guide_text_channels = Text( 66 | Bold('Работа с ТГ каналами'), '\n\n', 67 | Pre('Команда для управления ботом в вашем телеграм-канале. При первом использовании отправляет инструкцию по добавлению бота в ТГК.', language='/channels'), '\n\n', 68 | Bold('Интерфейс работы с ТГ каналами'), '\n\n', 69 | Pre('Показывает всех пользователей, от которых будут приходить уведомления в ваш ТГК. Нажмите на участника, чтобы удалить его', language='Кнопка "Участники"'), '\n\n', 70 | Pre('Изменяет ссылку-приглашение', language='Кнопка "Изменить ссылку"'), '\n\n', 71 | Pre('Включает/выключает уведомления о пердежах', language='Кнопка "Пердежи"'), '\n\n', 72 | Pre('Обновляет название канала в боте на текущее название канала в тг', language='Кнопка "Обновить имя"'), '\n\n', 73 | Pre('Удаляет канал в боте (в ваш ТГК не будут приходить уведомления ректальных активностей)', language='Кнопка "Удалить канал"'), '\n\n', 74 | ) 75 | 76 | guide_text_customization = Text( 77 | Bold('Кастомизация'), '\n\n', 78 | Pre('Установить ник, который будет отображаться вместо вашего имени.', language='/setnickname'), '\n\n', 79 | Pre('Меняет активность автозавершения сранья по умолчанию.', language='/switchdefaultautoend'), '\n\n', 80 | Pre('Устанавливает время, через которое вы автоматически будете заканчивать срать (если эта функция активна)', language='/setautoendtime'), '\n\n', 81 | ) 82 | 83 | guide_text_utils = Text( 84 | Bold('Полезные команды'), '\n\n', 85 | Pre('Посмотреть свою статистику активностей за все время/месяц/неделю', language='/anal'), '\n\n', 86 | Pre('Экспортировать информацию о своих активностях за все время в таблицу.', language='/export'), '\n\n', 87 | Pre('Команда, позволяющая отправить сообщение об ошибке или о подозрительном поведении другого участника проекта', language='/report'), '\n\n', 88 | Pre('История создания проекта и ссылки на администрацию', language='/credits'), '\n\n', 89 | ) 90 | 91 | unit_text = { 92 | "main": guide_text_main, 93 | "groups": guide_text_groups, 94 | "channels": guide_text_channels, 95 | "friends": guide_text_friends, 96 | "customization": guide_text_customization, 97 | "utils": guide_text_utils, 98 | } 99 | 100 | 101 | @router.message(Command("credits")) 102 | async def credit(message: types.Message): 103 | await message.answer(**credits_text.as_kwargs(), link_preview_options=LinkPreviewOptions(is_disabled=True)) 104 | 105 | 106 | @router.message(Command("guide")) 107 | async def guide_main(message: types.Message): 108 | await message.answer(**guide_text_main.as_kwargs(), 109 | reply_markup=guide_keyboard.get(), 110 | link_preview_options=LinkPreviewOptions(is_disabled=True)) 111 | 112 | 113 | @router.callback_query(GuideCallbackData.filter()) 114 | async def guide_menu(callback: types.CallbackQuery): 115 | cb_data = GuideCallbackData.unpack(callback.data) 116 | 117 | reply_markup = guide_keyboard.get_return() 118 | if cb_data.unit == 'main': 119 | reply_markup = guide_keyboard.get() 120 | 121 | await callback.message.edit_text(**unit_text[cb_data.unit].as_kwargs(), 122 | reply_markup=reply_markup, 123 | link_preview_options=LinkPreviewOptions(is_disabled=True)) 124 | -------------------------------------------------------------------------------- /bot_service/handlers/groups/control.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from aiogram import Router, types, F 4 | from aiogram.filters import Command 5 | from aiogram.fsm.context import FSMContext 6 | from aiogram.fsm.state import StatesGroup, State 7 | 8 | import config 9 | from db import UserUnion 10 | from db.User import User 11 | from db.UserUnion import Group 12 | from keyboards.group import groups_keyboard 13 | from middlewares.group import GroupMiddleware 14 | from utils.verify_name import verify_name 15 | 16 | router = Router() 17 | router.callback_query.middleware.register(GroupMiddleware()) 18 | 19 | 20 | class NamingGroupStates(StatesGroup): 21 | writing_name = State() 22 | 23 | 24 | class DeleteGroupStates(StatesGroup): 25 | writing_name = State() 26 | 27 | 28 | group_menu_text = ('Меню управления группами.\n' 29 | 'Объединяйтесь в группы, чтобы уведомлять друг друга, когда вы идете срать! ' 30 | 'В группе всем лично придет уведомление о том, что вы идете срать.\n\n' 31 | 'Учтите вы можете состоять не более чем в 5 группах. ' 32 | 'Также в группах может состоять не более 21 человека, ' 33 | 'если вам нужно больше воспользуйтесь каналами\n(/channels)\n\n') 34 | 35 | 36 | # GROUP MAIN MENU 37 | @router.message(Command('groups')) 38 | async def groups_menu(message: types.Message, user: User): 39 | await message.answer(group_menu_text, reply_markup=await groups_keyboard.get_all(user)) 40 | 41 | 42 | @router.callback_query(groups_keyboard.GroupCallback.filter(F.action == 'main')) 43 | async def groups_menu_callback(callback: types.CallbackQuery, user: User, state: FSMContext): 44 | await state.clear() 45 | await callback.message.edit_text(group_menu_text, reply_markup=await groups_keyboard.get_all(user)) 46 | 47 | 48 | # CREATE GROUP 49 | @router.callback_query(groups_keyboard.GroupCallback.filter(F.action == 'create')) 50 | async def create_group(callback: types.CallbackQuery, state: FSMContext, user: User): 51 | if not user.admin and await user.groups_member.all().count() >= config.Constants.member_group_limit: 52 | await callback.answer(f'Вы не можете состоять более чем в {config.Constants.member_group_limit} группах.', show_alert=True) 53 | return 54 | 55 | await callback.message.edit_text( 56 | 'Напишите название для вашей группы. Оно должно быть не длиннее 32 символов и не содержать специальных символов.') 57 | 58 | await state.set_state(NamingGroupStates.writing_name) 59 | await state.update_data({'last_msg': callback.message.message_id}) 60 | 61 | 62 | @router.message(NamingGroupStates.writing_name) 63 | async def group_writing_name(message: types.Message, state: FSMContext, user: User): 64 | state_data = (await state.get_data()) 65 | last_msg = state_data.get('last_msg') 66 | await message.delete() 67 | 68 | if len(message.text) > 32: 69 | await config.bot.edit_message_text('Название должно быть не длиннее 32 символов.', user.uid, last_msg) 70 | return 71 | 72 | if not verify_name(message.text.replace(' ', '')): 73 | await config.bot.edit_message_text('Название не должно содержать специальные символы.', user.uid, last_msg) 74 | return 75 | 76 | if state_data.get('group_id') is not None: 77 | group = await Group.filter(pk=state_data.get('group_id')).get() 78 | group.name = message.text 79 | await group.save() 80 | 81 | info_message = await message.answer('Название группы успешно изменено!') 82 | 83 | await state.clear() 84 | await show_group(None, group, user, state, last_msg, user.uid) 85 | 86 | await asyncio.sleep(3) 87 | await info_message.delete() 88 | return 89 | 90 | created_group = await Group.create(name=message.text, owner=user) 91 | await created_group.members.add(user) 92 | 93 | await config.bot.edit_message_text(f'Ваша группа {message.text} успешно создана!\n', 94 | user.uid, last_msg, 95 | reply_markup=groups_keyboard.get_return('Перейти к управлению группами')) 96 | 97 | await state.clear() 98 | 99 | 100 | # GROUP CONTROL 101 | @router.callback_query(groups_keyboard.GroupCallback.filter(F.action == 'show')) 102 | async def show_group(callback: types.CallbackQuery, group: Group, user: User, state: FSMContext, message_id: int = None, 103 | chat_id: int = None): 104 | await state.clear() 105 | owner = await group.owner 106 | 107 | invite_link = f'https://t.me/{config.bot_me.username}?start=IG{group.pk}P{group.password}' 108 | text = (f'Группа {group.name} ({group.pk})\n' 109 | f'Владелец {owner.name} ({owner.uid})\n' 110 | f'Человек {await group.members.all().count()}/{config.Constants.group_members_limit}\n' 111 | f'Пердежы: {["Выключены", "Включены"][group.notify_perdish]}\n\n' 112 | f'Создана {group.created_at}\n\n' 113 | f'Ссылка-приглашение:\n{invite_link}') 114 | 115 | has_access = owner == user or user.admin 116 | if not has_access: 117 | text = '\n'.join(text.splitlines()[:3]) 118 | 119 | if callback is None: 120 | await config.bot.edit_message_text(text, chat_id, message_id, 121 | reply_markup=groups_keyboard.get_group(group.pk, has_access, owner != user)) 122 | return 123 | 124 | await callback.message.edit_text(text, 125 | reply_markup=groups_keyboard.get_group(group.pk, has_access, owner != user)) 126 | 127 | 128 | @router.callback_query(groups_keyboard.GroupCallback.filter(F.action == 'password')) 129 | async def change_group_password(callback: types.CallbackQuery, group: Group, user: User, state: FSMContext): 130 | group.password = UserUnion.generate_password() 131 | await group.save() 132 | 133 | await callback.answer('Пароль группы изменен.') 134 | await show_group(callback, group, user, state) 135 | 136 | 137 | @router.callback_query(groups_keyboard.GroupCallback.filter(F.action == 'perdish')) 138 | async def change_group_perdish(callback: types.CallbackQuery, group: Group, user: User, state: FSMContext): 139 | group.notify_perdish = not group.notify_perdish 140 | await group.save() 141 | 142 | await callback.answer(f'Пердежи {["вы", "в"][group.notify_perdish]}ключены.') 143 | await show_group(callback, group, user, state) 144 | 145 | 146 | @router.callback_query(groups_keyboard.GroupCallback.filter(F.action == 'name')) 147 | async def change_group_name(callback: types.CallbackQuery, group: Group, state: FSMContext): 148 | await callback.message.edit_text( 149 | 'Напишите новое название для вашей группы. Оно должно быть не длиннее 32 символов и не содержать специальных символов.') 150 | 151 | await state.set_state(NamingGroupStates.writing_name) 152 | await state.update_data({'last_msg': callback.message.message_id}) 153 | await state.update_data({'group_id': group.pk}) 154 | 155 | 156 | @router.callback_query(groups_keyboard.GroupCallback.filter(F.action == 'delete')) 157 | async def delete_group(callback: types.CallbackQuery, group: Group, state: FSMContext): 158 | await callback.message.edit_text(f'Вы уверены что хотите удалить группу {group.name} ({group.pk})?\n' 159 | f'Если вы уверены, тогда напишите название группы ответным сообщением.', 160 | reply_markup=groups_keyboard.get_group_return(group.pk, 'Отменить')) 161 | 162 | await state.set_state(DeleteGroupStates.writing_name) 163 | await state.update_data({'last_msg': callback.message.message_id}) 164 | await state.update_data({'group_id': group.pk}) 165 | 166 | 167 | @router.message(DeleteGroupStates.writing_name) 168 | async def delete_group_submit(message: types.Message, state: FSMContext, user: User): 169 | state_data = (await state.get_data()) 170 | last_msg = state_data.get('last_msg') 171 | group_id = state_data.get('group_id') 172 | 173 | await message.delete() 174 | 175 | group = await Group.filter(pk=group_id).get() 176 | if group.name != message.text: 177 | info_message = await message.answer('Вы написали название группы неверно.') 178 | 179 | else: 180 | await group.delete() 181 | await state.clear() 182 | 183 | info_message = await message.answer(f'Группа {group.name} удалена.') 184 | await config.bot.edit_message_text('Выберите группу.', user.uid, last_msg, 185 | reply_markup=await groups_keyboard.get_all(user)) 186 | 187 | await asyncio.sleep(3) 188 | await info_message.delete() 189 | 190 | 191 | @router.callback_query(groups_keyboard.GroupCallback.filter(F.action == 'members')) 192 | async def group_members(callback: types.CallbackQuery, group: Group): 193 | await callback.message.edit_text('Выберете участника.', 194 | reply_markup=await groups_keyboard.get_group_members(group)) 195 | 196 | 197 | @router.callback_query(groups_keyboard.DeleteGroupMemberCallback.filter(F.submit == False)) 198 | async def call_submit_delete_group_member(callback: types.CallbackQuery): 199 | group_data = groups_keyboard.DeleteGroupMemberCallback.unpack(callback.data) 200 | 201 | await callback.message.edit_text(f'Вы уверены что хотите удалить пользователя с айди {group_data.uid}?', 202 | reply_markup=groups_keyboard.get_group_delete_member(group_data.uid, group_data.group)) 203 | 204 | 205 | @router.callback_query(groups_keyboard.DeleteGroupMemberCallback.filter(F.submit == True)) 206 | async def call_submit_delete_group_member(callback: types.CallbackQuery, user: User): 207 | group_data = groups_keyboard.DeleteGroupMemberCallback.unpack(callback.data) 208 | 209 | group = await Group.filter(pk=group_data.group).get() 210 | if group.owner_id == group_data.uid: 211 | await callback.answer('Вы не можете удалить создателя из группы.') 212 | 213 | elif user.pk == group_data.uid: 214 | await callback.answer('Вы не можете удалить самого себя из группы.') 215 | 216 | else: 217 | await group.members.remove(await User.filter(pk=group_data.uid).get()) 218 | await config.bot.send_message(group_data.uid, f'Вы исключены из групппы {group.name} ({group.pk})') 219 | 220 | await group_members(callback, group) 221 | 222 | 223 | @router.callback_query(groups_keyboard.GroupCallback.filter(F.action == 'leave')) 224 | async def leave_from_group(callback: types.CallbackQuery, group: Group, user: User, state: FSMContext): 225 | await group.members.remove(user) 226 | await callback.answer('Вы успешно покинули группу.') 227 | await groups_menu_callback(callback, user, state) 228 | 229 | owner = await group.owner 230 | 231 | await config.bot.send_message(owner.uid, f'Пользователь {user.name} ({user.uid}) покинул группу {group.name} ({group.pk})') 232 | 233 | -------------------------------------------------------------------------------- /bot_service/handlers/channels/control.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, List 2 | 3 | import aiogram 4 | import tortoise 5 | from aiogram import Router, F, Bot 6 | from aiogram import types 7 | from aiogram.filters import Command 8 | from aiogram.fsm.context import FSMContext 9 | from aiogram.fsm.state import StatesGroup, State 10 | 11 | import config 12 | from db import UserUnion 13 | from db.User import User 14 | from db.UserUnion import Channel 15 | from keyboards import channels_keyboard 16 | from keyboards.channels_keyboard import ChannelCallbackData, ChannelPagedCallbackData, \ 17 | ChannelMemberDeleteCallbackData 18 | from middlewares.channel import ChannelMiddleware 19 | from utils.find_button_by_callback import find_button_by_callback_data 20 | 21 | router = Router() 22 | 23 | 24 | class CreateChannelStates(StatesGroup): 25 | send_message_from_channel = State() 26 | 27 | 28 | channel_menu_text = ('Меню управления каналами.\n' 29 | 'Присоединяйтесь в каналы, чтобы уведомлять большое число людей, когда вы идете срать! ' 30 | 'Уведомление о ректальных активностях придет в ваш тг канал.\n\n' 31 | 'Пусть ваш тг канал станет ещё информативнее, ведь с помощью бота можно добавить тех, ' 32 | 'кто вместе с вами будет уведомлять ваших подписчиков о своих кишечных потугах!') 33 | channel_members_menu_text = 'Нажмите на пользователя, которого хотите удалить.' 34 | 35 | router.callback_query.middleware.register(ChannelMiddleware(channel_menu_text)) 36 | 37 | 38 | async def get_bot_channel(bot: Bot, channel_id: int) -> Tuple[types.Chat, List[types.ChatMember]]: 39 | try: 40 | channel_bot = await bot.get_chat(channel_id) 41 | return channel_bot, await channel_bot.get_administrators() 42 | 43 | except (aiogram.exceptions.TelegramForbiddenError, aiogram.exceptions.TelegramBadRequest): 44 | 45 | await Channel.filter(pk=channel_id).delete() 46 | 47 | return None, None 48 | 49 | 50 | async def answer_channel_deleted(callback: types.CallbackQuery, user: User): 51 | await callback.message.edit_text(channel_menu_text, reply_markup=await channels_keyboard.get_menu(user, 0)) 52 | await callback.answer('Бот больше не в канале.', show_alert=True) 53 | 54 | 55 | async def is_admin_channel(channel_bot: types.Chat, user_id: int, 56 | channel_admins: List[types.ChatMember] = None) -> bool: 57 | if channel_admins is None: 58 | channel_admins = await channel_bot.get_administrators() 59 | for el in channel_admins: 60 | if el.user.id == user_id: 61 | return True 62 | 63 | return False 64 | 65 | 66 | # CHANNELS MENU 67 | @router.message(Command('channels')) 68 | async def channels_menu(message: types.Message, user: User): 69 | await message.answer(channel_menu_text, reply_markup=await channels_keyboard.get_menu(user, 0)) 70 | 71 | 72 | @router.callback_query(ChannelPagedCallbackData.filter(F.unit == "menu")) 73 | async def channels_menu_callback(callback: types.CallbackQuery, user: User): 74 | data = ChannelPagedCallbackData.unpack(callback.data) 75 | await callback.message.edit_text(channel_menu_text, reply_markup=await channels_keyboard.get_menu(user, data.page)) 76 | 77 | 78 | # CREATING CHANNEL 79 | @router.callback_query(ChannelCallbackData.filter(F.action == 'create')) 80 | async def create_channel(callback: types.CallbackQuery, state: FSMContext): 81 | await callback.message.edit_text(f'Для начала добавьте бота @{config.bot_me.username} в ваш канал.\n' 82 | f'Затем перешлите любое сообщение из этого канала сюда.\n\n' 83 | f'Для отмены пропишите /cancel') 84 | 85 | await state.set_state(CreateChannelStates.send_message_from_channel) 86 | await state.update_data(last_msg=callback.message.message_id) 87 | 88 | 89 | @router.message(CreateChannelStates.send_message_from_channel, F.forward_from_chat) 90 | async def create_channel_message_from(message: types.Message, user: User, state: FSMContext): 91 | await message.delete() 92 | last_msg = (await state.get_data()).get('last_msg') 93 | 94 | channel_id = message.forward_from_chat.id 95 | try: 96 | channel = await message.bot.get_chat(channel_id) 97 | 98 | except aiogram.exceptions.TelegramForbiddenError: 99 | await message.bot.edit_message_text('Бот не состоит в этом канале.\n\n' 100 | 'Для отмены пропишите /cancel', message.chat.id, last_msg) 101 | return 102 | 103 | if channel.type != 'channel': 104 | await message.bot.edit_message_text('Вы должны переслать сообщение именно из канала.\n\n' 105 | 'Для отмены пропишите /cancel', message.chat.id, last_msg) 106 | return 107 | 108 | try: 109 | channel_instance = await Channel.create(channel_id=channel_id, name=channel.full_name) 110 | 111 | except tortoise.exceptions.IntegrityError as err: 112 | if not err.args[0].startswith('duplicate'): 113 | raise err 114 | await message.bot.edit_message_text('Этот канал уже добавлен в бота.\n\n' 115 | 'Для отмены пропишите /cancel', message.chat.id, last_msg) 116 | return 117 | 118 | await channel_instance.members.add(user) 119 | 120 | await state.clear() 121 | await message.bot.edit_message_text('Канал успешно создан!', message.chat.id, last_msg, 122 | reply_markup=await channels_keyboard.get_menu(user, 0)) 123 | 124 | 125 | # CONTROL CHANNEL 126 | @router.callback_query(ChannelCallbackData.filter(F.action == 'channel')) 127 | async def show_channel(callback: types.CallbackQuery, user: User, channel: Channel): 128 | channel_bot, channel_admins = await get_bot_channel(callback.bot, channel.channel_id) 129 | if channel_bot is None: 130 | await answer_channel_deleted(callback, user) 131 | return 132 | 133 | # Update channel name if changed 134 | if channel.name != channel_bot.full_name: 135 | channel.name = channel_bot.full_name 136 | await channel.save() 137 | await callback.answer('У канала обновлено имя.') 138 | 139 | invite_link = f'https://t.me/{config.bot_me.username}?start=IC{channel.pk}P{channel.password}' 140 | text = (f'Канал {channel.name} ({channel.pk})\n' 141 | f'Человек {await channel.members.all().count()}\n' 142 | f'Пердежы: {["Выключены", "Включены"][channel.notify_perdish]}\n\n' 143 | f'Создан {channel.created_at}\n\n' 144 | f'Ссылка-приглашение:\n{invite_link}') 145 | 146 | is_admin = await is_admin_channel(channel_bot, user.uid, channel_admins) 147 | if not is_admin: 148 | text = '\n'.join(text.splitlines()[:3]) 149 | 150 | await callback.message.edit_text(text, reply_markup=channels_keyboard.get_channel(channel.channel_id, is_admin)) 151 | 152 | 153 | @router.callback_query(ChannelCallbackData.filter(F.action == 'leave')) 154 | async def leave_from_channel(callback: types.CallbackQuery, user: User, channel: Channel): 155 | channel_bot, channel_admins = await get_bot_channel(callback.bot, channel.channel_id) 156 | if channel_bot is None: 157 | await answer_channel_deleted(callback, user) 158 | return 159 | is_admin = await is_admin_channel(channel_bot, user.uid, channel_admins) 160 | if is_admin: 161 | await callback.answer('Вы не можете ливнуть из этого канала, тк вы админ.', show_alert=True) 162 | 163 | await channel.members.remove(user) 164 | await callback.message.edit_text(channel_menu_text, reply_markup=await channels_keyboard.get_menu(user, 0)) 165 | 166 | 167 | @router.callback_query(ChannelCallbackData.filter(F.action == 'password')) 168 | async def change_channel_password(callback: types.CallbackQuery, user: User, channel: Channel): 169 | channel.password = UserUnion.generate_password() 170 | await channel.save() 171 | await callback.answer('Пароль канала изменен.') 172 | await show_channel(callback, user, channel) 173 | 174 | 175 | @router.callback_query(ChannelCallbackData.filter(F.action == 'perdish')) 176 | async def change_channel_perdish(callback: types.CallbackQuery, user: User, channel: Channel): 177 | channel.notify_perdish = not channel.notify_perdish 178 | await channel.save() 179 | await callback.answer('Пердежи канала изменены.') 180 | await show_channel(callback, user, channel) 181 | 182 | 183 | @router.callback_query(ChannelPagedCallbackData.filter(F.unit == 'members')) 184 | async def show_channel_members(callback: types.CallbackQuery, channel: Channel): 185 | cb_data = ChannelPagedCallbackData.unpack(callback.data) 186 | await callback.message.edit_text(channel_members_menu_text, 187 | reply_markup=await channels_keyboard.get_channel_members(channel, cb_data.page)) 188 | 189 | 190 | @router.callback_query(ChannelMemberDeleteCallbackData.filter(~F.submit)) 191 | async def delete_channel_member(callback: types.CallbackQuery): 192 | clicked_button = find_button_by_callback_data(callback.message.reply_markup, callback.data) 193 | cb_data = ChannelMemberDeleteCallbackData.unpack(callback.data) 194 | 195 | await callback.message.edit_text( 196 | f'Вы уверены, что хотите удалить из канала пользователя {clicked_button.text}?', 197 | reply_markup=channels_keyboard.get_delete_user_submit(cb_data.channel_id, cb_data.user_id)) 198 | 199 | 200 | @router.callback_query(ChannelMemberDeleteCallbackData.filter(F.submit)) 201 | async def delete_channel_member_submit(callback: types.CallbackQuery, user: User, channel: Channel): 202 | channel_bot, channel_admins = await get_bot_channel(callback.bot, channel.channel_id) 203 | if channel_bot is None: 204 | await answer_channel_deleted(callback, user) 205 | return 206 | 207 | cb_data = ChannelMemberDeleteCallbackData.unpack(callback.data) 208 | 209 | if await is_admin_channel(channel_bot, cb_data.user_id, channel_admins): 210 | await callback.answer('Вы не можете удалить админа из канала.', show_alert=True) 211 | 212 | else: 213 | # GoVnOcOdE 214 | delete_user = await User.get(pk=cb_data.user_id) 215 | await channel.members.remove(delete_user) 216 | 217 | await callback.bot.send_message(cb_data.user_id, f'Вы исключены из канала {channel.name}') 218 | 219 | await callback.answer('Пользователь удален.') 220 | 221 | await callback.message.edit_text(channel_members_menu_text, 222 | reply_markup=await channels_keyboard.get_channel_members(channel, 0)) 223 | 224 | 225 | @router.callback_query(ChannelCallbackData.filter(F.action == 'delete')) 226 | async def delete_channel(callback: types.CallbackQuery, channel: Channel): 227 | await callback.message.edit_text(f'Вы уверены, что хотите удалить канал {channel.name}?', 228 | reply_markup=channels_keyboard.get_channel_delete_submit(channel.channel_id)) 229 | 230 | 231 | @router.callback_query(ChannelCallbackData.filter(F.action == 'delete_submit')) 232 | async def delete_channel_submit(callback: types.CallbackQuery, user: User, channel: Channel): 233 | await channel.delete() 234 | await callback.answer('Канал удален.') 235 | await callback.message.edit_text(channel_menu_text, reply_markup=await channels_keyboard.get_menu(user, 0)) 236 | --------------------------------------------------------------------------------