├── WebCore ├── __init__.py ├── ui.xd ├── static │ ├── css │ │ ├── bets.css │ │ ├── giveaway-main.css │ │ ├── swal.css │ │ ├── giveaway-draws-of-prizes.css │ │ ├── collapsible.css │ │ ├── giveaway-header.css │ │ └── detailed-draw.css │ ├── js │ │ ├── colapsible.js │ │ └── main.js │ └── assets │ │ └── arrow_for_collapsible.svg ├── templates │ └── app.html └── routes.py ├── BotCore ├── middlewares │ ├── __init__.py │ └── add_users.py ├── utils │ ├── __init__.py │ ├── photos_manager.py │ └── payment.py ├── filters │ ├── __init__.py │ ├── is_admin.py │ └── callback_filters.py ├── keyboards │ ├── __init__.py │ ├── rkb.py │ └── ikb.py └── handlers │ ├── __init__.py │ ├── other.py │ ├── profile.py │ ├── start.py │ ├── payment.py │ └── create_ruffle_prizes.py ├── General ├── other │ ├── __init__.py │ └── CustomStorage.py ├── __init__.py ├── assets │ ├── main.mp4 │ ├── main.png │ └── start.png ├── loader.py ├── config.py ├── db_settings.py └── db.py ├── requirements.txt ├── .envexample ├── app.py └── .gitignore /WebCore/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /BotCore/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /BotCore/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from . import photos_manager 2 | -------------------------------------------------------------------------------- /General/other/__init__.py: -------------------------------------------------------------------------------- 1 | from . import CustomStorage 2 | -------------------------------------------------------------------------------- /BotCore/filters/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | is_admin 3 | ) 4 | -------------------------------------------------------------------------------- /General/__init__.py: -------------------------------------------------------------------------------- 1 | from . import config 2 | from . import loader 3 | -------------------------------------------------------------------------------- /BotCore/keyboards/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | ikb, 3 | rkb 4 | ) -------------------------------------------------------------------------------- /WebCore/ui.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DIMFLIX/SatsMarket/HEAD/WebCore/ui.xd -------------------------------------------------------------------------------- /BotCore/keyboards/rkb.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import ReplyKeyboardMarkup, InlineKeyboardButton 2 | -------------------------------------------------------------------------------- /General/assets/main.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DIMFLIX/SatsMarket/HEAD/General/assets/main.mp4 -------------------------------------------------------------------------------- /General/assets/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DIMFLIX/SatsMarket/HEAD/General/assets/main.png -------------------------------------------------------------------------------- /General/assets/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DIMFLIX/SatsMarket/HEAD/General/assets/start.png -------------------------------------------------------------------------------- /BotCore/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from . import start 2 | from . import profile 3 | from . import payment 4 | from . import create_ruffle_prizes 5 | from . import other 6 | -------------------------------------------------------------------------------- /BotCore/handlers/other.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from General.loader import bot, dp 3 | 4 | @dp.message() 5 | async def other_way(message: types.Message) -> None: 6 | await bot.delete_message(message.from_user.id, message.message_id) 7 | -------------------------------------------------------------------------------- /WebCore/static/css/bets.css: -------------------------------------------------------------------------------- 1 | #ruffle-prizes-bets-page { 2 | width: 100%; 3 | height: 100%; 4 | background: var(--bg-color); 5 | display: none; 6 | overflow-y: scroll; 7 | padding: 0 10px 10px 10px; 8 | box-sizing: border-box; 9 | } 10 | -------------------------------------------------------------------------------- /WebCore/static/js/colapsible.js: -------------------------------------------------------------------------------- 1 | function collapsible_event(event) { 2 | event.classList.toggle("collapsible-active"); 3 | let content = event.nextElementSibling; 4 | if (content.style.maxHeight) {content.style.maxHeight = null} 5 | else {content.style.maxHeight = content.scrollHeight + "px"} 6 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram==3.0.0 2 | asyncpg 3 | greenlet 4 | loguru~=0.7.1 5 | pyngrok~=6.0.0 6 | aiohttp~=3.8.5 7 | SQLAlchemy~=2.0.20 8 | alembic~=1.12.0 9 | aiofiles~=23.1.0 10 | environs~=8.0.0 11 | Jinja2~=3.1.2 12 | jsonpickle 13 | async_lru 14 | aiogram_media_group 15 | aiohttp_jinja2 16 | pillow -------------------------------------------------------------------------------- /.envexample: -------------------------------------------------------------------------------- 1 | # BOT SETTINGS 2 | BOT_TOKEN= 3 | ADMIN_ID= 4 | 5 | 6 | # DATABASE SETTINGS 7 | DB_HOST=localhost 8 | DB_PORT=5432 9 | DB_USERNAME= 10 | DB_PASSWORD= 11 | DB_NAME= 12 | 13 | 14 | # WEB SETTINGS 15 | WEBHOOK_HOST=localhost 16 | WEBHOOK_PORT=5000 17 | WEBHOOK_PATH=/webhook/{bot_token} 18 | WEB_URL= 19 | AUTO_URL=False -------------------------------------------------------------------------------- /WebCore/static/css/giveaway-main.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@600;700&display=swap'); 2 | 3 | * { 4 | outline: none; 5 | padding: 0; 6 | margin: 0; 7 | font-family: Inter, serif; 8 | } 9 | 10 | body { 11 | background: var(--bg-color); 12 | overflow: scroll; 13 | } 14 | -------------------------------------------------------------------------------- /BotCore/filters/is_admin.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from aiogram.filters import Filter 3 | 4 | from General import config as cfg 5 | 6 | 7 | class MyFilter(Filter): 8 | def __init__(self) -> None: ... 9 | 10 | async def __call__(self, message: types.Message) -> bool: 11 | return message.from_user.id == cfg.ADMIN_ID 12 | 13 | -------------------------------------------------------------------------------- /WebCore/static/assets/arrow_for_collapsible.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /General/loader.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from loguru import logger 4 | from aiogram import Bot, Dispatcher 5 | from aiogram.fsm.storage.memory import MemoryStorage 6 | 7 | from General import config as cfg 8 | from General.db import Database 9 | from .other.CustomStorage import PGStorage 10 | 11 | 12 | db = Database(cfg.DB_HOST, cfg.DB_PORT, cfg.DB_USERNAME, cfg.DB_PASSWORD, cfg.DB_NAME) 13 | bot: Bot = Bot(token=cfg.BOT_TOKEN, parse_mode="HTML") 14 | mem_storage = PGStorage() 15 | dp: Dispatcher = Dispatcher(storage=mem_storage) 16 | 17 | logger.remove() 18 | logger.add(sys.stderr, level="DEBUG" if cfg.DEBUG_LOGGING else "INFO") 19 | -------------------------------------------------------------------------------- /WebCore/static/css/swal.css: -------------------------------------------------------------------------------- 1 | .popup-add-dialog-container-html { 2 | padding-bottom: 0 !important; 3 | } 4 | .swal2-container.swal2-center>.swal2-popup { 5 | background-color: var(--bg-color) !important; 6 | border-radius: 20px !important; 7 | width: 90% !important; 8 | max-width: 300px !important; 9 | } 10 | 11 | #swal2-html-container { 12 | overflow: unset !important; 13 | } 14 | 15 | .swal2-html-container, .swal2-title{ 16 | color: var(--text-color) !important; 17 | font-size: 1.3em !important; 18 | } 19 | 20 | .swal2-html-container { 21 | font-size: 0.8em !important; 22 | } 23 | 24 | .swal2-backdrop { 25 | opacity: 0.2 !important; 26 | } 27 | 28 | .swal2-confirm:focus { 29 | outline: none !important; 30 | } 31 | 32 | .swal2-popup .swal2-styled:focus { 33 | box-shadow: none !important; 34 | } -------------------------------------------------------------------------------- /General/config.py: -------------------------------------------------------------------------------- 1 | from pyngrok import ngrok 2 | from environs import Env 3 | 4 | env = Env() 5 | env.read_env() 6 | 7 | BOT_TOKEN: str = env.str('BOT_TOKEN') 8 | ADMIN_ID: int = env.int('ADMIN_ID') 9 | 10 | PAYMENT_MERCHANT_ID: str = env.str('PAYMENT_MERCHANT_ID') 11 | PAYMENT_API_KEY: str = env.str("PAYMENT_API_KEY") 12 | PAYMENT_SECRET_1: str = env.str("PAYMENT_SECRET_1") 13 | PAYMENT_SECRET_2: str = env.str("PAYMENT_SECRET_2") 14 | PAYMENT_CURRENCY: str = "UAH" 15 | PAYMENT_FORM_LANG: str = "ua" 16 | 17 | DB_HOST: str = env.str("DB_HOST") 18 | DB_PORT: int = env.int("DB_PORT") 19 | DB_USERNAME: str = env.str("DB_USERNAME") 20 | DB_PASSWORD: str = env.str("DB_PASSWORD") 21 | DB_NAME: str = env.str("DB_NAME") 22 | 23 | WEBHOOK_HOST: str = env.str("WEBHOOK_HOST", default='localhost') 24 | WEBHOOK_PORT: int = env.int("WEBHOOK_PORT", default=5000) 25 | WEBHOOK_PATH: str = env.str("WEBHOOK_PATH") 26 | WEB_URL: str = ngrok.connect(WEBHOOK_PORT).public_url if env.bool("AUTO_URL", default=False) else env.str("WEB_URL") 27 | 28 | DEBUG_LOGGING: bool = env.bool("DEBUG_LOGGING", default=True) 29 | -------------------------------------------------------------------------------- /BotCore/keyboards/ikb.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo 3 | from aiogram.utils.keyboard import KeyboardBuilder 4 | 5 | from General import config as cfg 6 | 7 | start_menu = InlineKeyboardMarkup( 8 | inline_keyboard=[ 9 | [InlineKeyboardButton(text="Товары", web_app=WebAppInfo(url=cfg.WEB_URL + "/giveaway"))], 10 | [ 11 | InlineKeyboardButton(text="Поддержка", url="https://t.me/breitenbuecher"), 12 | InlineKeyboardButton(text="Профиль", callback_data="profile") 13 | ] 14 | ] 15 | ) 16 | 17 | back_to_start = InlineKeyboardMarkup( 18 | inline_keyboard=[ 19 | [InlineKeyboardButton(text="В главное меню", callback_data="back_to_start")] 20 | ] 21 | ) 22 | 23 | 24 | def profile_kb(is_admin: bool): 25 | kb = KeyboardBuilder(types.InlineKeyboardButton) 26 | 27 | if is_admin: 28 | kb.row(InlineKeyboardButton(text="Создать розыгрыш", callback_data="create_ruffle_prizes")) 29 | 30 | kb.row(InlineKeyboardButton(text="Пополнить баланс", callback_data="balance_top_up")) 31 | kb.row(InlineKeyboardButton(text="Назад", callback_data="back_to_start")) 32 | return kb.as_markup() 33 | 34 | -------------------------------------------------------------------------------- /BotCore/handlers/profile.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | 3 | from BotCore.utils.photos_manager import Photo 4 | from General.loader import bot, dp, db 5 | from General import config as cfg 6 | from BotCore.keyboards import ikb 7 | from BotCore.filters.callback_filters import CData 8 | 9 | 10 | @dp.callback_query(CData("profile")) 11 | async def start_handler(callback: types.CallbackQuery) -> None: 12 | balance = (await db.get_user(callback.from_user.id))["balance"] 13 | user_chat = await bot.get_chat(callback.from_user.id) 14 | text = f"Ваш ID: {callback.from_user.id}\nВаше имя: {callback.from_user.full_name}\nВаш баланс: {balance}" 15 | kb = ikb.profile_kb(callback.from_user.id == cfg.ADMIN_ID) 16 | 17 | if user_chat.photo is None: 18 | await bot.edit_message_caption( 19 | chat_id=callback.from_user.id, 20 | message_id=callback.message.message_id, 21 | caption=text, 22 | reply_markup=kb 23 | ) 24 | else: 25 | await bot.send_photo( 26 | chat_id=callback.from_user.id, 27 | photo=await Photo.avatar(bot, callback.from_user.id), 28 | caption=text, 29 | reply_markup=kb 30 | ) 31 | await bot.delete_message(callback.from_user.id, callback.message.message_id) 32 | -------------------------------------------------------------------------------- /BotCore/middlewares/add_users.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Callable, Dict, Any, Awaitable 3 | 4 | from aiogram import BaseMiddleware, types 5 | 6 | from General.loader import db 7 | 8 | 9 | class AddUserMiddleware(BaseMiddleware): 10 | async def __call__( 11 | self, 12 | handler: Callable[[types.CallbackQuery, Dict[str, Any]], Awaitable[Any]], 13 | event: types.Update, 14 | data: Dict[str, Any] 15 | ) -> Any: 16 | if isinstance(event.message, types.Message): 17 | user_id = event.message.from_user.id 18 | username = event.message.from_user.username 19 | elif isinstance(event.callback_query, types.CallbackQuery): 20 | user_id = event.callback_query.from_user.id 21 | username = event.callback_query.from_user.username 22 | elif isinstance(event.inline_query, types.InlineQuery): 23 | user_id = event.inline_query.from_user.id 24 | username = event.inline_query.from_user.username 25 | else: 26 | return 27 | 28 | if await db.get_user(user_id) is None: 29 | await db.add_user(user_id, username) 30 | else: 31 | await db.update_user_online(user_id, latest_online=datetime.datetime.now()) 32 | 33 | return await handler(event, data) 34 | -------------------------------------------------------------------------------- /WebCore/static/css/giveaway-draws-of-prizes.css: -------------------------------------------------------------------------------- 1 | #all-draws-page { 2 | width: 100%; 3 | height: 100%; 4 | background: var(--bg-color); 5 | overflow: hidden; 6 | display: unset; 7 | } 8 | 9 | .all-draws-of-prizes { 10 | height: 100%; 11 | display: grid; 12 | grid-template-columns: repeat(2, 1fr); 13 | grid-row-gap: 20px; 14 | justify-items: center; 15 | margin: 20px 0; 16 | list-style: none; 17 | } 18 | 19 | .all-draws-of-prizes li { 20 | display: grid; 21 | text-align: center; 22 | justify-items: center; 23 | } 24 | 25 | .all-draws-of-prizes-background { 26 | display: grid; 27 | justify-items: center; 28 | align-items: center; 29 | width: calc(100% - 20px); 30 | height: 160px; 31 | background: var(--secondary-bg-color); 32 | border-radius: 15px; 33 | grid-template-rows: 4fr 1fr; 34 | } 35 | 36 | .all-draws-of-prizes-background img { 37 | min-width: 65%; 38 | max-width: 65%; 39 | } 40 | 41 | .all-draws-of-prizes li p { 42 | font-size: 15px; 43 | color: #FFFFFF; 44 | font-weight: 600; 45 | } 46 | 47 | .all-draws-of-prizes li .collected { 48 | background-image: linear-gradient(-130deg, #b578f2 20%, #98b0ff 100%); 49 | width: calc(100% - 20px); 50 | height: 35px; 51 | border-radius: 5px; 52 | border: none; 53 | outline: none; 54 | margin-top: 10px; 55 | color: var(--text-color); 56 | } -------------------------------------------------------------------------------- /WebCore/static/css/collapsible.css: -------------------------------------------------------------------------------- 1 | .collapsible_background { 2 | margin-top: 10px; 3 | background-color: var(--secondary-bg-color-30); 4 | border-radius: 10px; 5 | overflow: auto; 6 | } 7 | 8 | .collapsible { 9 | -webkit-tap-highlight-color: transparent; 10 | display:inline-flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | text-align: left; 14 | background-color: var(--secondary-bg-color-30); 15 | color: var(--text-color); 16 | cursor: pointer; 17 | padding-left: 15px; 18 | padding-right: 15px; 19 | width: 100%; 20 | height: 50px; 21 | border: none; 22 | outline: none; 23 | font-size: 16px; 24 | font-weight: 600; 25 | } 26 | .collapsible:after { 27 | content: url('/static/assets/arrow_for_collapsible.svg'); 28 | width: 24px; 29 | height: 24px; 30 | margin-left: 5px; 31 | transition: 0.2s ease; 32 | } 33 | .active:after { 34 | transform: rotate(-180deg); 35 | transition: 0.2s ease; 36 | color: #FFF; 37 | } 38 | 39 | .content { 40 | padding-left: 15px; 41 | padding-right: 15px; 42 | max-height: 0; 43 | overflow: hidden; 44 | transition: max-height 0.2s ease-out; 45 | background-color: var(--secondary-bg-color-30); 46 | } 47 | 48 | .content p { 49 | margin-top: -2px; 50 | font-size: 14px; 51 | overflow-wrap: break-word; 52 | margin-bottom: 10px; 53 | color: var(--text-color); 54 | } -------------------------------------------------------------------------------- /BotCore/handlers/start.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from aiogram.filters import Command 3 | from aiogram.fsm.context import FSMContext 4 | 5 | from BotCore.filters.callback_filters import CData 6 | from BotCore.utils.photos_manager import Photo 7 | from General.loader import bot, dp 8 | from BotCore.keyboards import ikb 9 | 10 | 11 | @dp.message(Command("start")) 12 | async def start_handler(message: types.Message, state: FSMContext): 13 | await state.clear() 14 | await bot.send_photo( 15 | chat_id=message.from_user.id, 16 | photo=await Photo.file(), 17 | caption="Приветствую, рад тебя видеть, " + message.from_user.first_name, 18 | reply_markup=ikb.start_menu 19 | ) 20 | await bot.delete_message(message.from_user.id, message.message_id) 21 | 22 | 23 | @dp.callback_query(CData("back_to_start")) 24 | async def callback_start_handler(callback: types.CallbackQuery, state: FSMContext): 25 | await state.clear() 26 | await bot.edit_message_caption( 27 | chat_id=callback.from_user.id, 28 | message_id=callback.message.message_id, 29 | caption="Приветствую, рад тебя видеть, " + callback.from_user.first_name, 30 | reply_markup=ikb.start_menu 31 | ) 32 | 33 | 34 | @dp.callback_query(CData("hide_message")) 35 | async def hide_message(callback: types.CallbackQuery): 36 | await bot.delete_message(chat_id=callback.from_user.id, message_id=callback.message.message_id) 37 | 38 | 39 | -------------------------------------------------------------------------------- /BotCore/utils/photos_manager.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from io import BytesIO 3 | 4 | import aiofiles 5 | from PIL import Image 6 | from aiogram import Bot 7 | from aiogram.types import BufferedInputFile 8 | from async_lru import alru_cache 9 | 10 | 11 | class Photo: 12 | @classmethod 13 | @alru_cache 14 | async def file(cls, filename: str = "main.png") -> BufferedInputFile: 15 | async with aiofiles.open("General/assets/" + filename, 'rb') as f: 16 | photo = await f.read() 17 | 18 | return BufferedInputFile(photo, filename) 19 | 20 | @classmethod 21 | async def avatar(cls, bot: Bot, user_id: int) -> BufferedInputFile: 22 | user_chat = await bot.get_chat(user_id) 23 | 24 | if user_chat.photo is not None: 25 | user_photo_id = await bot.get_file(user_chat.photo.big_file_id) 26 | user_photo = await bot.download_file(user_photo_id.file_path) 27 | return BufferedInputFile(user_photo.read(), "LunarLegacy.png") 28 | 29 | return await cls.file() 30 | 31 | @classmethod 32 | def compress_img(cls, photo_bytes, *, quality, width, height, format="JPEG") -> str: 33 | photo_bytes_io = BytesIO(photo_bytes) 34 | img = Image.open(photo_bytes_io) 35 | img.thumbnail((width, height)) 36 | 37 | buffered = BytesIO() 38 | img.save(buffered, format=format, optimize=True, quality=quality) 39 | return base64.b64encode(buffered.getvalue()).decode('utf-8') 40 | 41 | -------------------------------------------------------------------------------- /BotCore/filters/callback_filters.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters import BaseFilter 2 | from aiogram.types import CallbackQuery 3 | from typing import Union, List 4 | 5 | 6 | class CData(BaseFilter): 7 | def __init__(self, cdata: Union[str, List[str]]): 8 | self.cdata = cdata 9 | 10 | async def __call__(self, callback: CallbackQuery) -> bool: 11 | if type(self.cdata) == str: 12 | return callback.data == self.cdata 13 | 14 | elif type(self.cdata) == list: 15 | for i in self.cdata: 16 | if callback.data == i: 17 | return True 18 | 19 | return False 20 | 21 | 22 | class CDataStart(BaseFilter): 23 | def __init__(self, cdata_start: Union[str, List[str]]): 24 | self.cdata_start = cdata_start 25 | 26 | async def __call__(self, callback: CallbackQuery) -> bool: 27 | if type(self.cdata_start) == str: 28 | return callback.data.endswith(self.cdata_start) 29 | 30 | elif type(self.cdata_start) == list: 31 | for i in self.cdata_start: 32 | if callback.data.startswith(i): 33 | return True 34 | 35 | return False 36 | 37 | 38 | class CDataEnd(BaseFilter): 39 | def __init__(self, cdata_end: Union[str, List[str]]): 40 | self.cdata_end = cdata_end 41 | 42 | async def __call__(self, callback: CallbackQuery) -> bool: 43 | if type(self.cdata_end) == str: 44 | return callback.data.endswith(self.cdata_end) 45 | 46 | elif type(self.cdata_end) == list: 47 | for i in self.cdata_end: 48 | if callback.data.endswith(i): 49 | return True 50 | 51 | return False 52 | -------------------------------------------------------------------------------- /General/db_settings.py: -------------------------------------------------------------------------------- 1 | import asyncpg 2 | 3 | 4 | class DictRecord(asyncpg.Record): 5 | def __getitem__(self, key): 6 | value = super().__getitem__(key) 7 | if isinstance(value, asyncpg.Record): 8 | return dict(value.items()) 9 | return value 10 | 11 | def to_dict(self): 12 | return dict(super().items()) 13 | 14 | def __repr__(self): 15 | return str(dict(super().items())) 16 | 17 | 18 | class DbSettings: 19 | @staticmethod 20 | async def create_tables(db: asyncpg.Connection) -> None: 21 | await db.execute("""CREATE TABLE IF NOT EXISTS \"users\"( 22 | id BIGINT NOT NULL PRIMARY KEY, 23 | username TEXT, 24 | balance BIGINT NOT NULL DEFAULT 0, 25 | latest_online TIMESTAMP NOT NULL DEFAULT now(), 26 | registration_date TIMESTAMP NOT NULL DEFAULT now())""") 27 | await db.execute("""CREATE TABLE IF NOT EXISTS \"bets\"( 28 | id BIGSERIAL NOT NULL PRIMARY KEY, 29 | user_id BIGINT NOT NULL, 30 | amount BIGINT NOT NULL, 31 | bet_time TIMESTAMP NOT NULL DEFAULT now(), 32 | ruffle_prizes_id INTEGER NOT NULL)""") 33 | await db.execute("""CREATE TABLE IF NOT EXISTS \"ruffle_prizes\"( 34 | id BIGSERIAL NOT NULL PRIMARY KEY, 35 | title TEXT NOT NULL, 36 | description TEXT default 'Описание отсутствует...', 37 | low_quality_photos JSONB, 38 | photos JSONB, 39 | menu_icon TEXT NOT NULL, 40 | money_collected BIGINT NOT NULL DEFAULT 0, 41 | money_needed BIGINT NOT NULL, 42 | countdown_hours BIGINT NOT NULL, 43 | countdown_start_time TIMESTAMP, 44 | winner_id BIGINT, 45 | is_over BOOLEAN NOT NULL DEFAULT FALSE)""") 46 | await db.execute("""CREATE TABLE IF NOT EXISTS \"payment\"( 47 | id BIGSERIAL NOT NULL PRIMARY KEY, 48 | user_id BIGINT NOT NULL, 49 | amount BIGINT NOT NULL, 50 | currency TEXT NOT NULL, 51 | date TIMESTAMP NOT NULL DEFAULT now(), 52 | is_payed BOOLEAN NOT NULL DEFAULT False)""") 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /WebCore/static/css/giveaway-header.css: -------------------------------------------------------------------------------- 1 | 2 | .header { 3 | display: inline-block; 4 | width: 100%; 5 | height: auto; 6 | background-image: linear-gradient(220deg, #b578f2 20%, #98b0ff 100%); 7 | border-radius: 0 0 30px 30px; 8 | padding-bottom: 20px; 9 | } 10 | 11 | .top { 12 | display: flex; 13 | justify-content: space-between; 14 | height: 50px; 15 | margin-top: 10px; 16 | width: 100%; 17 | } 18 | 19 | .avatar { 20 | width: 50px; 21 | height: 50px; 22 | border-radius: 25px; 23 | background: white; 24 | } 25 | 26 | .greetings { 27 | margin-left: 10px; 28 | overflow: hidden; 29 | box-shadow: inset 0 0 10px rgba(0, 0, 0, 0); 30 | } 31 | .greetings p { 32 | color: var(--text-color); 33 | font-weight: bold; 34 | display: inline-block; 35 | } 36 | 37 | #firstname { 38 | font-size: 20px; 39 | overflow: hidden; 40 | color: var(--text-color); 41 | font-weight: bold; 42 | display: inline-block; 43 | width: 90%; 44 | white-space: nowrap; 45 | } 46 | 47 | .balance { 48 | font-size: 19px; 49 | color: var(--text-color); 50 | margin: auto 0 auto 10px; 51 | font-weight: bold; 52 | position: relative; 53 | } 54 | 55 | .balance-icon { 56 | position: relative; 57 | color: var(--text-color); 58 | font-size: 30px; 59 | margin: auto 0; 60 | } 61 | 62 | .title-draws-of-prizes { 63 | width: 100%; 64 | text-align: center; 65 | color: var(--text-color); 66 | font-size: 25px; 67 | font-weight: bold; 68 | margin-top: 20px; 69 | } 70 | 71 | .draws-of-prizes-statistic-box { 72 | width: 90px; 73 | height: 90px; 74 | background: #FFFFFF20; 75 | border: 2px solid #FFFFFF; 76 | border-radius: 15px; 77 | display: flex; 78 | justify-content: center; 79 | align-items: center; 80 | font-size: 30px; 81 | color: var(--text-color); 82 | } 83 | 84 | .draws-of-prizes-statistic-box-signature { 85 | color: var(--text-color); 86 | font-size: 20px; 87 | font-weight: bold; 88 | margin-top: 10px; 89 | } -------------------------------------------------------------------------------- /BotCore/utils/payment.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import hashlib 3 | from urllib.parse import urlencode 4 | from loguru import logger 5 | 6 | from General.loader import db 7 | from General.config import ( 8 | PAYMENT_SECRET_1, PAYMENT_SECRET_2, PAYMENT_API_KEY, 9 | PAYMENT_MERCHANT_ID, PAYMENT_CURRENCY, PAYMENT_FORM_LANG 10 | ) 11 | 12 | 13 | class Payment: 14 | __merchant_id: str = PAYMENT_MERCHANT_ID 15 | __secret_1: str = PAYMENT_SECRET_1 16 | __secret_2: str = PAYMENT_SECRET_2 17 | __api_key: str = PAYMENT_API_KEY 18 | _currency: str = PAYMENT_CURRENCY 19 | _form_lang: str = PAYMENT_FORM_LANG 20 | _invoice_description: str = "Order Payment" 21 | 22 | @classmethod 23 | async def get_invoice(cls, user_id: int, amount: float) -> dict: 24 | order_id: int = await db.create_order_in_payment(user_id, amount, cls._currency) 25 | sign = ":".join([cls.__merchant_id, str(amount), cls._currency, cls.__secret_1, str(order_id)]) 26 | 27 | params = { 28 | 'merchant_id': cls.__merchant_id, 29 | 'amount': amount, 30 | 'currency': cls._currency, 31 | 'order_id': order_id, 32 | 'sign': hashlib.sha256(sign.encode('utf-8')).hexdigest(), 33 | 'desc': cls._invoice_description, 34 | 'lang': 'ru' 35 | } 36 | 37 | logger.info(f"Создан новый счёт на оплату с ID - {order_id}") 38 | 39 | return { 40 | "url": "https://aaio.io/merchant/pay?" + urlencode(params), 41 | "order_id": order_id, 42 | "amount": amount, 43 | "currency": cls._currency, 44 | "user_id": user_id 45 | } 46 | 47 | @classmethod 48 | async def get_order_info(cls, order_id: int) -> dict: 49 | 50 | params = dict(merchant_id=cls.__merchant_id, order_id=order_id) 51 | headers = { 52 | 'Accept': 'application/json', 53 | 'X-Api-Key': cls.__api_key 54 | } 55 | 56 | async with aiohttp.ClientSession() as s: 57 | response = await s.post( 58 | 'https://aaio.io/api/info-pay', 59 | data=params, headers=headers 60 | ) 61 | 62 | response_json = await response.json() 63 | return response_json 64 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | 4 | from aiogram.webhook.aiohttp_server import TokenBasedRequestHandler 5 | from aiohttp import web 6 | from loguru import logger 7 | 8 | from BotCore import handlers 9 | from BotCore.middlewares.add_users import AddUserMiddleware 10 | from General import config as cfg 11 | from General.loader import bot, dp, db, mem_storage 12 | from WebCore.routes import WebRoutes 13 | 14 | 15 | async def tracing_the_end_of_draws(): 16 | while True: 17 | draws = await db.db.fetch(""" 18 | UPDATE ruffle_prizes 19 | SET is_over = TRUE 20 | WHERE is_over = FALSE 21 | AND EXTRACT(EPOCH FROM (NOW() - countdown_start_time))/3600 >= countdown_hours 22 | RETURNING id; 23 | """) 24 | 25 | for draw in draws: 26 | draw_id = draw["id"] 27 | bets = await db.db.fetch( 28 | "SELECT user_id, SUM(amount) AS amount FROM bets WHERE ruffle_prizes_id=$1 GROUP BY user_id", 29 | draw_id 30 | ) 31 | 32 | user_bets_result = {i["user_id"]: int(i["amount"]) for i in bets} 33 | ids = list(user_bets_result.keys()) 34 | weights = list(user_bets_result.values()) 35 | winner_id = random.choices(ids, weights=weights)[0] 36 | await db.db.execute("UPDATE ruffle_prizes SET winner_id=$1 WHERE id=$2", winner_id, draw_id) 37 | 38 | await asyncio.sleep(2) 39 | 40 | 41 | async def on_startup(_): 42 | await db.create_connection() 43 | await mem_storage.create_connection_and_tables(db.db) 44 | dp.update.middleware(AddUserMiddleware()) 45 | TokenBasedRequestHandler(dp).register(webapp, cfg.WEBHOOK_PATH) 46 | await bot.set_webhook(url=cfg.WEB_URL + cfg.WEBHOOK_PATH.format(bot_token=cfg.BOT_TOKEN)) 47 | asyncio.create_task(tracing_the_end_of_draws()) 48 | logger.success("Bot started succesfully") 49 | logger.warning("Debug logging enabled") 50 | 51 | 52 | async def on_shutdown(_): 53 | await bot.delete_webhook() 54 | await dp.storage.close() 55 | logger.warning("Bot turned off") 56 | 57 | 58 | if __name__ == '__main__': 59 | webapp = web.Application() 60 | webapp.on_startup.append(on_startup) 61 | webapp.on_shutdown.append(on_shutdown) 62 | WebRoutes(webapp) 63 | web.run_app(webapp, host=cfg.WEBHOOK_HOST, port=cfg.WEBHOOK_PORT, print=logger.success("Server started")) 64 | -------------------------------------------------------------------------------- /General/other/CustomStorage.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pickle 3 | 4 | import asyncpg 5 | import jsonpickle as jsonpickle 6 | from asyncio import AbstractEventLoop 7 | 8 | from aiogram.fsm.state import State 9 | from aiogram.fsm.storage.base import BaseStorage, StorageKey, StateType 10 | from aiogram.fsm.storage.memory import MemoryStorage 11 | from typing import Dict, Optional, Any 12 | from General.db import DictRecord 13 | 14 | class PGStorage(BaseStorage): 15 | __slots__ = ("host", "port", "username", "password", "database", "dsn", "loop") 16 | 17 | def __init__(self) -> None: 18 | self._db = None 19 | 20 | async def create_connection_and_tables(self, db: asyncpg.Connection) -> None: 21 | await db.execute("""CREATE TABLE IF NOT EXISTS aiogram_state( 22 | "key" TEXT NOT NULL PRIMARY KEY, 23 | "state" TEXT)""") 24 | await db.execute("""CREATE TABLE IF NOT EXISTS aiogram_data( 25 | "key" TEXT NOT NULL PRIMARY KEY, 26 | "data" TEXT)""") 27 | 28 | self._db = db 29 | 30 | async def set_state(self, key: StorageKey, state: StateType = None) -> None: 31 | state = state.state if isinstance(state, State) else state 32 | await self._db.execute( 33 | """ 34 | INSERT INTO aiogram_state VALUES($1, $2) 35 | ON CONFLICT (key) DO UPDATE SET state = $2 36 | """, 37 | str(key.user_id), state 38 | ) 39 | 40 | async def get_state(self, key: StorageKey) -> Optional[str]: 41 | response = await self._db.fetchval("SELECT state FROM aiogram_state WHERE key=$1", str(key.user_id)) 42 | return response 43 | 44 | async def set_data(self, key: StorageKey, data: Dict[str, Any]) -> None: 45 | print("Set data ", data) 46 | await self._db.execute( 47 | """ 48 | INSERT INTO aiogram_data VALUES($1, $2) 49 | ON CONFLICT (key) DO UPDATE SET data = $2 50 | """, 51 | str(key.user_id), jsonpickle.dumps(data) 52 | ) 53 | 54 | async def get_data(self, key: StorageKey) -> Dict[str, Any]: 55 | response = await self._db.fetchval("SELECT data FROM aiogram_data WHERE key=$1", str(key.user_id)) 56 | print("Get data", response) 57 | return jsonpickle.loads(response) 58 | 59 | async def close(self) -> None: 60 | pass 61 | -------------------------------------------------------------------------------- /WebCore/static/css/detailed-draw.css: -------------------------------------------------------------------------------- 1 | #detailed-draw-page { 2 | width: 100%; 3 | height: 100%; 4 | background: var(--bg-color); 5 | display: none; 6 | overflow-y: scroll; 7 | padding-bottom: 10px; 8 | } 9 | 10 | .detailed-draw-header { 11 | width: 100%; 12 | height: 50px; 13 | display: flex; 14 | justify-items: center; 15 | align-items: center; 16 | } 17 | .detailed-draw-header i { 18 | color: var(--text-color); 19 | font-size: 30px; 20 | margin-left: 10px; 21 | } 22 | 23 | .detailed-draw-header p { 24 | color: var(--text-color); 25 | font-weight: bold; 26 | font-size: 20px; 27 | margin-left: 10px; 28 | } 29 | 30 | 31 | #detailed-draw-photos { 32 | margin-top: 20px; 33 | } 34 | 35 | #detailed-draw-photos .slide { 36 | display: flex; 37 | justify-content: center; 38 | align-items: center; 39 | justify-items: center; 40 | } 41 | 42 | #detailed-draw-photos .slide * { 43 | position: relative; 44 | width: 90%; 45 | background: var(--button-color); 46 | border-radius: 15px; 47 | margin: auto; 48 | } 49 | 50 | .detailed-draw-parameters { 51 | color: var(--text-color); 52 | font-size: 15px; 53 | height: 45px; 54 | padding-left: 15px; 55 | display: flex; 56 | align-items: center; 57 | font-weight: bold; 58 | margin-top: 10px; 59 | background: var(--secondary-bg-color-50); 60 | border-radius: 10px; 61 | } 62 | 63 | #participate-button { 64 | width: 100%; 65 | height: 50px; 66 | background-image: linear-gradient(220deg, #b578f2 20%, #98b0ff 100%); 67 | border-radius: 10px; 68 | color: var(--text-color); 69 | outline: none; 70 | border: none; 71 | font-size: 20px; 72 | margin-top: 10px; 73 | } 74 | 75 | #input_draw_prizes_bet { 76 | width: 100%; 77 | height: 40px; 78 | background: var(--secondary-bg-color); 79 | border-radius: 10px; 80 | border: 1px solid var(--button-color); 81 | color: var(--text-color); 82 | font-size: 16px; 83 | font-weight: bold; 84 | padding-left: 10px; 85 | box-sizing: border-box; 86 | -moz-appearance: textfield; 87 | } 88 | #input_draw_prizes_bet::-webkit-inner-spin-button, 89 | #input_draw_prizes_bet::-webkit-outer-spin-button { 90 | -webkit-appearance: none; 91 | margin: 0; 92 | } 93 | 94 | #input_draw_prizes_bet::placeholder { 95 | color: var(--button-color); 96 | } 97 | 98 | #send_draw_prizes_bet { 99 | width: 100%; 100 | height: 50px; 101 | background-image: linear-gradient(220deg, #b578f2 20%, #98b0ff 100%); 102 | border-radius: 10px; 103 | color: var(--text-color); 104 | outline: none; 105 | border: none; 106 | font-size: 20px; 107 | margin-top: 10px; 108 | } -------------------------------------------------------------------------------- /BotCore/handlers/payment.py: -------------------------------------------------------------------------------- 1 | from aiogram import types, F 2 | from aiogram.fsm.context import FSMContext 3 | from aiogram.fsm.state import StatesGroup, State 4 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 5 | 6 | from BotCore.handlers import start 7 | from General.loader import bot, dp, db 8 | from BotCore.keyboards import ikb 9 | from BotCore.filters.callback_filters import CData 10 | from BotCore.utils.payment import Payment 11 | 12 | 13 | class PaySt(StatesGroup): 14 | amount = State() 15 | check_pay = State() 16 | 17 | 18 | @dp.callback_query(CData("balance_top_up")) 19 | async def start_handler(callback: types.CallbackQuery, state: FSMContext) -> None: 20 | last_bot_msg = await bot.edit_message_caption( 21 | chat_id=callback.from_user.id, 22 | message_id=callback.message.message_id, 23 | caption="Ok, send me amount UAH", 24 | reply_markup=ikb.back_to_start 25 | ) 26 | await state.set_state(PaySt.amount) 27 | await state.update_data(last_bot_msg_id=last_bot_msg.message_id) 28 | 29 | 30 | @dp.message(F.text.isdigit(), PaySt.amount) 31 | async def send_invoice(message: types.Message, state: FSMContext) -> None: 32 | sd = await state.get_data() 33 | payment_data = await Payment.get_invoice(message.from_user.id, float(message.text)) 34 | 35 | kb = InlineKeyboardMarkup(inline_keyboard=[ 36 | [ 37 | InlineKeyboardButton(text="Проверить платёж", callback_data="check_pay"), 38 | InlineKeyboardButton(text="Оплатить", url=payment_data["url"]) 39 | ], 40 | [InlineKeyboardButton(text="В главное меню", callback_data="back_to_start")] 41 | ]) 42 | 43 | last_bot_msg = await bot.edit_message_caption( 44 | chat_id=message.from_user.id, 45 | message_id=sd["last_bot_msg_id"], 46 | caption="Вот ваш инвойс, прошу, оплачивайте!", 47 | reply_markup=kb 48 | ) 49 | await bot.delete_message(chat_id=message.from_user.id, message_id=message.message_id) 50 | await state.update_data(payment_data=payment_data, last_bot_msg_id=last_bot_msg.message_id) 51 | await state.set_state(PaySt.check_pay) 52 | 53 | 54 | @dp.callback_query(CData("check_pay"), PaySt.check_pay) 55 | async def check_pay(callback: types.CallbackQuery, state: FSMContext) -> None: 56 | sd = await state.get_data() 57 | order_id = sd["payment_data"]["order_id"] 58 | order_info = await Payment.get_order_info(order_id) 59 | 60 | if order_info["type"] == "success": 61 | if order_info["status"] in ["success", "hold"]: 62 | await bot.answer_callback_query( 63 | callback_query_id=callback.id, 64 | text=f"🟢 Баланс пополнен на {order_info['amount']} {order_info['currency']}" 65 | ) 66 | await db.update_payment_order_when_completed(order_id) 67 | await start.callback_start_handler(callback, state) 68 | return 69 | 70 | else: 71 | await bot.answer_callback_query( 72 | callback_query_id=callback.id, 73 | text=f"🔴 Вы еще не оплатили счёт" 74 | ) 75 | 76 | else: 77 | await bot.answer_callback_query( 78 | callback_query_id=callback.id, 79 | text=f"🔴 Не удалось получить информацию по счёту" 80 | ) 81 | 82 | await state.set_state(PaySt.check_pay) 83 | -------------------------------------------------------------------------------- /WebCore/templates/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Title 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 | 24 |
25 |

Hello,

26 | 27 |
28 |
29 |
30 | 35 |
36 |
37 |
38 |

Draws of prizes

39 |
40 |
41 |

0

42 |

Active

43 |
44 |
45 |

0

46 |

Participate

47 |
48 |
49 |

0

50 |

Closed

51 |
52 |
53 |
54 |
55 | 56 |
57 | 58 | 59 |
60 | 61 |

62 |
63 | 64 |
65 | 66 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | venv/* 3 | .idea/*### venv template 4 | # Virtualenv 5 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 6 | .Python 7 | [Bb]in 8 | [Ii]nclude 9 | [Ll]ib 10 | [Ll]ib64 11 | [Ll]ocal 12 | [Ss]cripts 13 | pyvenv.cfg 14 | .venv 15 | pip-selfcheck.json 16 | 17 | ### VirtualEnv template 18 | # Virtualenv 19 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 20 | .Python 21 | [Bb]in 22 | [Ii]nclude 23 | [Ll]ib 24 | [Ll]ib64 25 | [Ll]ocal 26 | [Ss]cripts 27 | pyvenv.cfg 28 | .venv 29 | pip-selfcheck.json 30 | 31 | ### Python template 32 | # Byte-compiled / optimized / DLL files 33 | __pycache__/ 34 | *.py[cod] 35 | *$py.class 36 | 37 | # C extensions 38 | *.so 39 | 40 | # Distribution / packaging 41 | .Python 42 | build/ 43 | develop-eggs/ 44 | dist/ 45 | downloads/ 46 | eggs/ 47 | .eggs/ 48 | lib/ 49 | lib64/ 50 | parts/ 51 | sdist/ 52 | var/ 53 | wheels/ 54 | share/python-wheels/ 55 | *.egg-info/ 56 | .installed.cfg 57 | *.egg 58 | MANIFEST 59 | 60 | # PyInstaller 61 | # Usually these files are written by a python script from a template 62 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 63 | *.manifest 64 | *.spec 65 | 66 | # Installer logs 67 | pip-log.txt 68 | pip-delete-this-directory.txt 69 | 70 | # Unit test / coverage reports 71 | htmlcov/ 72 | .tox/ 73 | .nox/ 74 | .coverage 75 | .coverage.* 76 | .cache 77 | nosetests.xml 78 | coverage.xml 79 | *.cover 80 | *.py,cover 81 | .hypothesis/ 82 | .pytest_cache/ 83 | cover/ 84 | 85 | # Translations 86 | *.mo 87 | *.pot 88 | 89 | # Django stuff: 90 | *.log 91 | local_settings.py 92 | db.sqlite3 93 | db.sqlite3-journal 94 | 95 | # Flask stuff: 96 | instance/ 97 | .webassets-cache 98 | 99 | # Scrapy stuff: 100 | .scrapy 101 | 102 | # Sphinx documentation 103 | docs/_build/ 104 | 105 | # PyBuilder 106 | .pybuilder/ 107 | target/ 108 | 109 | # Jupyter Notebook 110 | .ipynb_checkpoints 111 | 112 | # IPython 113 | profile_default/ 114 | ipython_config.py 115 | 116 | # pyenv 117 | # For a library or package, you might want to ignore these files since the code is 118 | # intended to run in multiple environments; otherwise, check them in: 119 | # .python-version 120 | 121 | # pipenv 122 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 123 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 124 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 125 | # install all needed dependencies. 126 | #Pipfile.lock 127 | 128 | # poetry 129 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 130 | # This is especially recommended for binary packages to ensure reproducibility, and is more 131 | # commonly ignored for libraries. 132 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 133 | #poetry.lock 134 | 135 | # pdm 136 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 137 | #pdm.lock 138 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 139 | # in version control. 140 | # https://pdm.fming.dev/#use-with-ide 141 | .pdm.toml 142 | 143 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 144 | __pypackages__/ 145 | 146 | # Celery stuff 147 | celerybeat-schedule 148 | celerybeat.pid 149 | 150 | # SageMath parsed files 151 | *.sage.py 152 | 153 | # Environments 154 | .env 155 | .venv 156 | env/ 157 | venv/ 158 | ENV/ 159 | env.bak/ 160 | venv.bak/ 161 | 162 | # Spyder project settings 163 | .spyderproject 164 | .spyproject 165 | 166 | # Rope project settings 167 | .ropeproject 168 | 169 | # mkdocs documentation 170 | /site 171 | 172 | # mypy 173 | .mypy_cache/ 174 | .dmypy.json 175 | dmypy.json 176 | 177 | # Pyre type checker 178 | .pyre/ 179 | 180 | # pytype static type analyzer 181 | .pytype/ 182 | 183 | # Cython debug symbols 184 | cython_debug/ 185 | 186 | # PyCharm 187 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 188 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 189 | # and can be added to the global gitignore or merged into this file. For a more nuclear 190 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 191 | #.idea/ 192 | 193 | ### PyCharm+all template 194 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 195 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 196 | 197 | # User-specific stuff 198 | .idea/**/workspace.xml 199 | .idea/**/tasks.xml 200 | .idea/**/usage.statistics.xml 201 | .idea/**/dictionaries 202 | .idea/**/shelf 203 | 204 | # AWS User-specific 205 | .idea/**/aws.xml 206 | 207 | # Generated files 208 | .idea/**/contentModel.xml 209 | 210 | # Sensitive or high-churn files 211 | .idea/**/dataSources/ 212 | .idea/**/dataSources.ids 213 | .idea/**/dataSources.local.xml 214 | .idea/**/sqlDataSources.xml 215 | .idea/**/dynamic.xml 216 | .idea/**/uiDesigner.xml 217 | .idea/**/dbnavigator.xml 218 | 219 | # Gradle 220 | .idea/**/gradle.xml 221 | .idea/**/libraries 222 | 223 | # Gradle and Maven with auto-import 224 | # When using Gradle or Maven with auto-import, you should exclude module files, 225 | # since they will be recreated, and may cause churn. Uncomment if using 226 | # auto-import. 227 | # .idea/artifacts 228 | # .idea/compiler.xml 229 | # .idea/jarRepositories.xml 230 | # .idea/modules.xml 231 | # .idea/*.iml 232 | # .idea/modules 233 | # *.iml 234 | # *.ipr 235 | 236 | # CMake 237 | cmake-build-*/ 238 | 239 | # Mongo Explorer plugin 240 | .idea/**/mongoSettings.xml 241 | 242 | # File-based project format 243 | *.iws 244 | 245 | # IntelliJ 246 | out/ 247 | 248 | # mpeltonen/sbt-idea plugin 249 | .idea_modules/ 250 | 251 | # JIRA plugin 252 | atlassian-ide-plugin.xml 253 | 254 | # Cursive Clojure plugin 255 | .idea/replstate.xml 256 | 257 | # SonarLint plugin 258 | .idea/sonarlint/ 259 | 260 | # Crashlytics plugin (for Android Studio and IntelliJ) 261 | com_crashlytics_export_strings.xml 262 | crashlytics.properties 263 | crashlytics-build.properties 264 | fabric.properties 265 | 266 | # Editor-based Rest Client 267 | .idea/httpRequests 268 | 269 | # Android studio 3.1+ serialized cache file 270 | .idea/caches/build_file_checksums.ser 271 | 272 | -------------------------------------------------------------------------------- /General/db.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import datetime 3 | 4 | import asyncpg 5 | from typing import List 6 | 7 | import jsonpickle 8 | from loguru import logger 9 | 10 | from .db_settings import DbSettings, DictRecord 11 | 12 | 13 | class Database(DbSettings): 14 | def __init__(self, host: str, port: int, username: str, password: str, db_name: str): 15 | self.db_auth_data = dict(host=host, port=port, user=username, password=password) 16 | self.db_name = db_name 17 | self.db: asyncpg.Pool = None 18 | 19 | async def create_connection(self) -> asyncpg.Pool: 20 | try: 21 | self.db = await asyncpg.create_pool(**self.db_auth_data, database=self.db_name, record_class=DictRecord) 22 | except asyncpg.exceptions.InvalidCatalogNameError: 23 | async with asyncpg.create_pool(**self.db_auth_data) as conn: 24 | await conn.execute(f"CREATE DATABASE \"{self.db_name}\"") 25 | self.db = await asyncpg.create_pool(**self.db_auth_data, database=self.db_name, record_class=DictRecord) 26 | 27 | await self.create_tables(self.db) 28 | 29 | async def get_user(self, user_id: int) -> dict: 30 | return await self.db.fetchrow("SELECT * FROM users WHERE id=$1", user_id) 31 | 32 | async def add_user(self, user_id: int, username: str) -> dict: 33 | user = await self.db.fetchrow("INSERT INTO users(id, username) VALUES ($1, $2) RETURNING *", user_id, username) 34 | logger.info(f"New User added - {user_id} | {username}") 35 | return user 36 | 37 | async def update_user_online(self, user_id: int, latest_online: datetime) -> dict: 38 | user = await self.db.execute("UPDATE users SET latest_online=$1 WHERE id=$2 RETURNING *", latest_online, user_id) 39 | return user 40 | 41 | async def count_users(self) -> int: 42 | response = await self.db.fetchval("SELECT COUNT(*) FROM users") 43 | return response 44 | 45 | async def create_ruffle_prizes( 46 | self, title: str, money_needed: int, countdown_hours: int, 47 | description: str = None, photos: List[str] = None, low_quality_photos: List[str] = None, 48 | menu_icon: str = None) -> None: 49 | photos = None if photos is None else jsonpickle.dumps(photos) 50 | low_quality_photos = None if low_quality_photos is None else jsonpickle.dumps(low_quality_photos) 51 | description = "Описание отсутствует..." if description is None else description 52 | await self.db.execute(""" 53 | INSERT INTO ruffle_prizes (title, description, money_needed, countdown_hours, 54 | photos, low_quality_photos, menu_icon) VALUES ($1, $2, $3, $4, $5, $6, $7) 55 | """, title, description, money_needed, countdown_hours, photos, low_quality_photos, menu_icon) 56 | 57 | async def get_prize_draws(self) -> List[dict]: 58 | return await self.db.fetch(f""" 59 | SELECT RP.id, title, description, COALESCE(SUM(B.amount), 0) AS money_collected, money_needed, 60 | low_quality_photos, NULL as photos, menu_icon, countdown_hours, countdown_start_time, winner_id, is_over 61 | FROM ruffle_prizes RP, bets B 62 | WHERE RP.id=B.ruffle_prizes_id 63 | GROUP BY RP.id 64 | """) 65 | 66 | async def get_high_quality_photos(self): 67 | return await self.db.fetch("SELECT id, photos FROM ruffle_prizes") 68 | 69 | async def get_active_prize_draws(self) -> List[dict]: 70 | return await self.db.fetch(""" 71 | SELECT RP.id, title, description, COALESCE(SUM(B.amount), 0) AS money_collected, money_needed, 72 | low_quality_photos, NULL as photos, menu_icon, countdown_hours, countdown_start_time, winner_id, is_over 73 | FROM ruffle_prizes RP 74 | LEFT JOIN bets B ON RP.id=B.ruffle_prizes_id 75 | WHERE RP.is_over=false 76 | GROUP BY RP.id 77 | """) 78 | 79 | async def get_participate_prize_draws(self, user_id: int) -> List[dict]: 80 | return await self.db.fetch(""" 81 | SELECT RP.id, title, description, COALESCE(SUM(B.amount), 0) AS money_collected, money_needed, 82 | low_quality_photos, NULL as photos, menu_icon, countdown_hours, countdown_start_time, winner_id, is_over 83 | FROM ruffle_prizes RP 84 | LEFT JOIN bets B ON RP.id=B.ruffle_prizes_id 85 | WHERE RP.is_over=false 86 | AND RP.id IN (SELECT ruffle_prizes_id FROM bets WHERE user_id=$1) 87 | GROUP BY RP.id 88 | """, user_id) 89 | 90 | async def get_closed_prize_draws(self) -> List[dict]: 91 | return await self.db.fetch(""" 92 | SELECT 93 | RP.id, title, description, COALESCE(SUM(B.amount), 0) AS money_collected, 94 | money_needed, low_quality_photos, NULL as photos, menu_icon, countdown_hours, 95 | countdown_start_time, winner_id, is_over, 96 | CASE WHEN EXISTS (SELECT 1 FROM bets WHERE ruffle_prizes_id = RP.id) THEN ( 97 | SELECT jsonb_object_agg(B.user_id, B.amount) FROM ( 98 | SELECT user_id, SUM(amount) AS amount 99 | FROM bets 100 | WHERE ruffle_prizes_id = RP.id 101 | GROUP BY user_id 102 | ) AS B 103 | ) ELSE NULL END AS users_bets 104 | FROM ruffle_prizes RP 105 | LEFT JOIN bets B ON RP.id=B.ruffle_prizes_id 106 | WHERE RP.is_over=true 107 | GROUP BY RP.id 108 | """) 109 | 110 | async def if_active_draw_exists(self, draw_id): 111 | return await self.db.fetchval("SELECT EXISTS (SELECT 1 FROM ruffle_prizes WHERE id=$1)", draw_id) 112 | 113 | async def create_bet(self, user_id, amount, ruffle_prizes_id) -> dict: 114 | new_bet = await self.db.fetchrow(""" 115 | INSERT INTO bets(user_id, amount, ruffle_prizes_id) VALUES ($1, $2, $3) 116 | RETURNING id as bet_id, amount as bet_amount, bet_time, 117 | ruffle_prizes_id, (SELECT title as ruffle_prizes_title FROM ruffle_prizes WHERE id=$3) 118 | """, user_id, amount, ruffle_prizes_id 119 | ) 120 | asyncio.create_task(self.db.execute(""" 121 | UPDATE ruffle_prizes SET countdown_start_time=NOW() 122 | WHERE id=$1 123 | AND (SELECT SUM(amount) FROM bets WHERE ruffle_prizes_id=$1) >= money_needed 124 | AND countdown_start_time IS NULL 125 | """, ruffle_prizes_id 126 | )) 127 | 128 | return new_bet 129 | 130 | async def get_user_bets(self, user_id) -> List[dict]: 131 | return await self.db.fetch(""" 132 | SELECT B.id as bet_id, b.amount as bet_amount, 133 | b.bet_time, B.ruffle_prizes_id, 134 | RP.title AS ruffle_prizes_title 135 | FROM bets B, ruffle_prizes RP WHERE B.ruffle_prizes_id=RP.id AND B.user_id=$1 136 | """, user_id) 137 | 138 | async def change_balance(self, user_id: int, operation: str, amount: int) -> bool: 139 | balance = await self.db.fetchval("SELECT balance FROM users WHERE id=$1", user_id) 140 | 141 | match operation: 142 | case "+": 143 | balance += amount, 144 | case "-": 145 | balance = balance - amount, 146 | case "=": 147 | balance = amount 148 | case _: 149 | return False 150 | 151 | await self.db.execute("UPDATE users SET balance=$1 WHERE id=$2", balance[0], user_id) 152 | return True 153 | 154 | async def create_order_in_payment(self, user_id: int, amount: int, currency: str) -> int: 155 | order_id = await self.db.fetchval( 156 | "INSERT INTO payment(user_id, amount, currency) VALUES($1, $2, $3) RETURNING id", 157 | user_id, amount, currency 158 | ) 159 | return order_id 160 | 161 | async def update_payment_order_when_completed(self, order_id: int) -> None: 162 | await self.db.execute(""" 163 | WITH updated_users AS ( 164 | UPDATE users 165 | SET balance = balance + payment.amount, latest_online = now() 166 | FROM payment 167 | WHERE payment.user_id = users.id AND payment.id = $1 AND payment.is_payed = false 168 | AND NOT EXISTS (SELECT 1 FROM payment WHERE payment.user_id = users.id AND payment.id = $1 AND payment.is_payed = true) 169 | RETURNING users.id 170 | ) 171 | UPDATE payment SET is_payed = true 172 | WHERE payment.user_id IN (SELECT id FROM updated_users) AND payment.id = $1 AND payment.is_payed = false; 173 | """, order_id) 174 | 175 | async def get_payment_order(self, order_id: int) -> dict: 176 | response = await self.db.fetchrow("SELECT * FROM payment WHERE id=$1", order_id) 177 | return response 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /WebCore/routes.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import decimal 3 | import hashlib 4 | import hmac 5 | import json 6 | from typing import Union 7 | 8 | from aiogram.client.session import aiohttp 9 | from aiogram.utils.web_app import safe_parse_webapp_init_data, WebAppInitData 10 | from aiohttp import web, WSMessage 11 | from aiohttp.web_request import Request 12 | import aiohttp_jinja2 13 | import jinja2 14 | from loguru import logger 15 | 16 | from BotCore.utils.photos_manager import Photo 17 | from General import config as cfg 18 | from General.db_settings import DictRecord 19 | from General.loader import bot, db 20 | 21 | 22 | class WebRoutes: 23 | def __init__(self, webapp: web.Application) -> None: 24 | self.webapp = webapp 25 | aiohttp_jinja2.setup(webapp, enable_async=True, loader=jinja2.FileSystemLoader('WebCore/templates')) 26 | self.webapp.router.add_static('/static/', path='WebCore/static', name='static') 27 | webapp.router.add_post("/payment_notify", self.payment_notify) 28 | webapp.router.add_get("/giveaway", self.giveaway_get) 29 | webapp.router.add_post("/giveaway", self.giveaway_post) 30 | webapp.router.add_get("/giveaway/ws", self.websocket) 31 | 32 | async def payment_notify(self, request: web.Request) -> None: 33 | data = await request.post() 34 | logger.info(f"Получено уведомление об оплате с данными {data}") 35 | 36 | ##==> Проверка на ip address 37 | ################################################ 38 | async with aiohttp.ClientSession() as s: 39 | response = await s.get('https://aaio.io/api/public/ips', timeout=10) 40 | good_ip_list = (await response.json())["list"] 41 | user_ip = request.headers["X-Forwarded-For"] 42 | 43 | if user_ip not in good_ip_list: 44 | logger.error(f"{user_ip} не входит в {good_ip_list}") 45 | return 46 | 47 | ##==> Проверка на подпись 48 | ################################################ 49 | for i in ["order_id", "sign", "amount", "currency"]: 50 | if i not in data: 51 | logger.error(f"В данных не хватает поля {i}") 52 | return 53 | 54 | order = await db.get_payment_order(int(data['order_id'])) 55 | logger.info(f"Достали order из бд: {order}") 56 | if order is None: return 57 | sign = hashlib.sha256(':'.join([ 58 | str(cfg.PAYMENT_MERCHANT_ID), 59 | "{0:.2f}".format(float(order['amount'])), 60 | str(order['currency']), 61 | str(cfg.PAYMENT_SECRET_2), 62 | str(order['id']) 63 | ]).encode('utf-8')).hexdigest() 64 | 65 | logger.info(f"Получили подпись {sign}") 66 | if not hmac.compare_digest(data['sign'], sign): 67 | logger.error(f"Подпись из уведомления ({data['sign']}) не совпадает с нашей") 68 | return 69 | 70 | await db.update_payment_order_when_completed(order["id"]) 71 | 72 | @staticmethod 73 | async def check_auth(auth_data: str) -> Union[WebAppInitData, None]: 74 | try: 75 | return safe_parse_webapp_init_data(token=cfg.BOT_TOKEN, init_data=auth_data) 76 | except ValueError: 77 | return None 78 | 79 | @staticmethod 80 | async def render_template(path: str, request: Request, **kwargs): 81 | return await aiohttp_jinja2.render_template_async(path, request, context=kwargs) 82 | 83 | @staticmethod 84 | def post_wrapper(func): 85 | async def inner(*args, **kwargs): 86 | 87 | def json_encoder(obj): 88 | if isinstance(obj, datetime.datetime): 89 | return obj.isoformat() 90 | elif isinstance(obj, DictRecord): 91 | return obj.to_dict() 92 | elif isinstance(obj, decimal.Decimal): 93 | return int(obj) 94 | 95 | raise TypeError(f"{obj} is not JSON serializable") 96 | 97 | result = await func(*args, **kwargs) 98 | return web.json_response(result, dumps=lambda obj: json.dumps(obj, default=json_encoder)) 99 | 100 | return inner 101 | 102 | @post_wrapper 103 | async def giveaway_post(self, request: web.Request) -> dict: 104 | data = await request.json() 105 | user_data = await self.check_auth(data["Authorization"]) if "Authorization" in data else None 106 | 107 | if "method" not in data: 108 | return dict(ok=False, error="Параметра method не существует!") 109 | 110 | match data["method"]: 111 | case "get_user_data": 112 | if user_data is None: 113 | return dict(ok=False, error="Такого пользователя не существует!") 114 | 115 | user_data: WebAppInitData 116 | user_info = await db.get_user(user_data.user.id) 117 | user_photo = await Photo.avatar(bot, user_data.user.id) 118 | user_photo = Photo.compress_img(user_photo.data, quality=70, width=100, height=100) 119 | return dict( 120 | ok=True, 121 | firstname=user_data.user.first_name, 122 | balance=user_info["balance"], 123 | photo=user_photo 124 | ) 125 | 126 | case "get_prize_draws": 127 | if "type" in data: 128 | if data["type"] == "active": 129 | prize_draws = await db.get_active_prize_draws() 130 | elif data["type"] == "participate": 131 | prize_draws = await db.get_participate_prize_draws(user_data.user.id) 132 | elif data["type"] == "closed": 133 | prize_draws = await db.get_closed_prize_draws() 134 | else: 135 | prize_draws = await db.get_prize_draws() 136 | else: 137 | prize_draws = await db.get_prize_draws() 138 | 139 | return dict(ok=True, prize_draws=prize_draws) 140 | 141 | case "load_high_quality_photos": 142 | prize_draws = await db.get_high_quality_photos() 143 | return dict(ok=True, prize_draws=prize_draws) 144 | 145 | case "get_user_bets": 146 | if user_data is None: 147 | return dict(ok=False, error="Такого пользователя не существует!") 148 | 149 | bets = await db.get_user_bets(user_data.user.id) 150 | return dict(ok=True, bets=None if bets == [] else bets) 151 | 152 | case "create_draw_prizes_bet": 153 | if user_data is None: 154 | return dict(ok=False, error="Такого пользователя не существует!") 155 | 156 | if "bet" not in data or "draw_id" not in data: 157 | return dict(ok=False, error="Поля \"bet\" и \"draw_id\" должны быть заполнены!") 158 | else: 159 | if not str(data["draw_id"]).isdigit() or not str(data["bet"]).isdigit(): 160 | return dict(ok=False, error="Поля \"bet\" и \"draw_id\" должны быть числами!") 161 | else: 162 | draw_id = int(data["draw_id"]) 163 | bet = int(data["bet"]) 164 | 165 | if_draw_exists: bool = await db.if_active_draw_exists(draw_id) 166 | 167 | if if_draw_exists: 168 | user_balance: int = (await db.get_user(user_data.user.id))["balance"] 169 | if user_balance >= bet: 170 | await db.change_balance(user_data.user.id, "-", bet) 171 | created_bet = await db.create_bet(user_data.user.id, bet, draw_id) 172 | return dict(ok=True, new_balance=user_balance - bet, bet=created_bet) 173 | else: 174 | return dict(ok=False, error="Недостаточно баланса для совершения данной ставки!") 175 | else: 176 | return dict(ok=False, error="Такого розыгрыша не существует!") 177 | 178 | case _: 179 | return dict(ok=False, error=f"Метода {data['method']} не существует!") 180 | 181 | async def giveaway_get(self, request): 182 | return await self.render_template('app.html', request) 183 | 184 | async def websocket(self, request: Request): 185 | ws = web.WebSocketResponse() 186 | await ws.prepare(request) 187 | 188 | try: 189 | async for msg in ws: 190 | msg: WSMessage 191 | data_message = json.loads(msg.data) 192 | 193 | if msg.type == aiohttp.WSMsgType.TEXT: 194 | ... 195 | 196 | elif msg.type == aiohttp.WSMsgType.ERROR: 197 | print('ws connection closed with exception %s' % ws.exception()) 198 | 199 | finally: 200 | pass 201 | 202 | return ws 203 | -------------------------------------------------------------------------------- /BotCore/handlers/create_ruffle_prizes.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import base64 3 | from typing import Union, List 4 | from PIL import Image 5 | from io import BytesIO 6 | 7 | from aiogram import types, F 8 | from aiogram.fsm.context import FSMContext 9 | from aiogram.fsm.state import StatesGroup, State 10 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, PhotoSize 11 | from aiogram_media_group import media_group_handler 12 | 13 | from BotCore.utils.photos_manager import Photo 14 | from General.loader import bot, dp, db 15 | from BotCore.keyboards import ikb 16 | from BotCore.filters.callback_filters import CData 17 | 18 | 19 | class AddRufflePrizes(StatesGroup): 20 | title = State() 21 | description = State() 22 | money_needed = State() 23 | countdown_hours = State() 24 | photos = State() 25 | menu_icon = State() 26 | confirm = State() 27 | 28 | 29 | @dp.callback_query(CData("create_ruffle_prizes")) 30 | async def start_handler(callback: types.CallbackQuery, state: FSMContext) -> None: 31 | bot_message = await bot.edit_message_caption( 32 | chat_id=callback.from_user.id, 33 | message_id=callback.message.message_id, 34 | caption="Окей, приступим!\nВведите название розыгрыша", 35 | reply_markup=ikb.back_to_start 36 | ) 37 | 38 | await state.set_state(AddRufflePrizes.title) 39 | await state.update_data(last_bot_msg_id=bot_message.message_id) 40 | 41 | 42 | @dp.message(AddRufflePrizes.title) 43 | async def input_title(message: types.Message, state: FSMContext) -> None: 44 | state_data = await state.get_data() 45 | 46 | kb = InlineKeyboardMarkup( 47 | inline_keyboard=[ 48 | [InlineKeyboardButton(text="Пропустить", callback_data="ruffle_prizes_skip_description")], 49 | [InlineKeyboardButton(text="В главное меню", callback_data="back_to_start")] 50 | ] 51 | ) 52 | 53 | bot_message = await bot.edit_message_caption( 54 | chat_id=message.from_user.id, 55 | message_id=state_data["last_bot_msg_id"], 56 | caption="Отлично, теперь введите описание для товара.", 57 | reply_markup=kb, 58 | ) 59 | await bot.delete_message(message.from_user.id, message.message_id) 60 | await state.update_data(last_bot_msg_id=bot_message.message_id, ruffle_prizes_title=message.text) 61 | await state.set_state(AddRufflePrizes.description) 62 | 63 | 64 | @dp.message(AddRufflePrizes.description) 65 | @dp.callback_query(CData("ruffle_prizes_skip_description"), AddRufflePrizes.description) 66 | async def input_description(obj: Union[types.Message, types.CallbackQuery], state: FSMContext): 67 | state_data = await state.get_data() 68 | 69 | bot_message = await bot.edit_message_caption( 70 | chat_id=obj.from_user.id, 71 | message_id=state_data["last_bot_msg_id"], 72 | caption="Хорошо, теперь мне нужно знать стоимость этого розыгрыша.", 73 | reply_markup=ikb.back_to_start 74 | ) 75 | 76 | ruffle_prizes_description = None 77 | if isinstance(obj, types.Message): 78 | ruffle_prizes_description = obj.text 79 | await bot.delete_message(obj.from_user.id, obj.message_id) 80 | 81 | await state.update_data(last_bot_msg_id=bot_message.message_id, ruffle_prizes_description=ruffle_prizes_description) 82 | await state.set_state(AddRufflePrizes.money_needed) 83 | 84 | 85 | @dp.message(F.text.isdigit(), AddRufflePrizes.money_needed) 86 | async def input_money_needed(message: types.Message, state: FSMContext) -> None: 87 | state_data = await state.get_data() 88 | 89 | kb = InlineKeyboardMarkup( 90 | inline_keyboard=[ 91 | [InlineKeyboardButton(text=i, callback_data=f"ruffle_prizes_time_{i}") for i in ["3", "6", "12", "24"]], 92 | [InlineKeyboardButton(text="В главное меню", callback_data="back_to_start")] 93 | ] 94 | ) 95 | 96 | bot_message = await bot.edit_message_caption( 97 | chat_id=message.from_user.id, 98 | message_id=state_data["last_bot_msg_id"], 99 | caption="Теперь выберите время отсчета. (Можно написть вручную количество часов)", 100 | reply_markup=kb, 101 | ) 102 | 103 | await bot.delete_message(message.from_user.id, message.message_id) 104 | await state.update_data(last_bot_msg_id=bot_message.message_id, ruffle_prizes_money_needed=int(message.text)) 105 | await state.set_state(AddRufflePrizes.countdown_hours) 106 | 107 | 108 | @dp.callback_query(F.data.startswith("ruffle_prizes_time")) 109 | @dp.message(F.text.isdigit(), AddRufflePrizes.countdown_hours) 110 | async def input_time_start(obj: Union[types.Message, types.CallbackQuery], state: FSMContext) -> None: 111 | state_data = await state.get_data() 112 | 113 | if isinstance(obj, types.Message) and obj.text.isdigit(): 114 | countdown_hours = int(obj.text) 115 | elif isinstance(obj, types.CallbackQuery): 116 | countdown_hours = int(obj.data.split("_")[-1]) 117 | else: 118 | return 119 | 120 | kb = InlineKeyboardMarkup( 121 | inline_keyboard=[ 122 | [InlineKeyboardButton(text="Пропустить", callback_data="ruffle_prizes_photos_skip")], 123 | [InlineKeyboardButton(text="В главное меню", callback_data="back_to_start")] 124 | ] 125 | ) 126 | 127 | bot_message = await bot.edit_message_caption( 128 | chat_id=obj.from_user.id, 129 | message_id=state_data["last_bot_msg_id"], 130 | caption="Загрузите фотографии (можно несколько)", 131 | reply_markup=kb, 132 | ) 133 | 134 | if isinstance(obj, types.Message): 135 | await bot.delete_message(obj.from_user.id, obj.message_id) 136 | 137 | await state.update_data(last_bot_msg_id=bot_message.message_id, ruffle_prizes_countdown_hours=int(countdown_hours)) 138 | await state.set_state(AddRufflePrizes.photos) 139 | 140 | 141 | @dp.message(F.photo, AddRufflePrizes.photos) 142 | @media_group_handler(only_album=False, receive_timeout=2) 143 | async def get_photos(messages: List[types.Message], state: FSMContext) -> None: 144 | all_photos = [] 145 | 146 | for message in messages: 147 | all_photos.append(message.photo[-1]) 148 | await bot.delete_message(message.from_user.id, message.message_id) 149 | 150 | await state.update_data(ruffle_prizes_photos=all_photos) 151 | await get_menu_icon(messages[-1], state) 152 | 153 | 154 | @dp.callback_query(CData("ruffle_prizes_photos_skip"), AddRufflePrizes.photos) 155 | async def get_menu_icon(obj: Union[types.Message, types.CallbackQuery], state: FSMContext) -> None: 156 | state_data = await state.get_data() 157 | 158 | if isinstance(obj, types.CallbackQuery): 159 | await state.update_data(ruffle_prizes_photos=None) 160 | 161 | bot_message = await bot.edit_message_caption( 162 | chat_id=obj.from_user.id, 163 | message_id=state_data["last_bot_msg_id"], 164 | caption=f"Отправьте стикер для иконки в меню", 165 | reply_markup=ikb.back_to_start, 166 | ) 167 | await state.update_data(last_bot_msg_id=bot_message.message_id) 168 | await state.set_state(AddRufflePrizes.menu_icon) 169 | 170 | 171 | @dp.message(lambda m: m.sticker is not None and m.sticker.is_animated is False, AddRufflePrizes.menu_icon) 172 | async def view_final_ruffle_prizes(message: types.Message, state: FSMContext) -> None: 173 | state_data = await state.get_data() 174 | get_file = await bot.get_file(message.sticker.file_id) 175 | menu_icon = await bot.download_file(get_file.file_path) 176 | 177 | kb = InlineKeyboardMarkup( 178 | inline_keyboard=[ 179 | [InlineKeyboardButton(text="Подтвердить", callback_data="confirm_create_ruffle_prizes")], 180 | [InlineKeyboardButton(text="В главное меню", callback_data="back_to_start")] 181 | ] 182 | ) 183 | 184 | await bot.edit_message_caption( 185 | chat_id=message.from_user.id, 186 | message_id=state_data["last_bot_msg_id"], 187 | caption=f"Вы подтверждаете создание розыгрыша {state_data['ruffle_prizes_title']}?", 188 | reply_markup=kb, 189 | ) 190 | await bot.delete_message(message.from_user.id, message.message_id) 191 | img = Photo.compress_img(menu_icon.read(), width=300, height=300, format="PNG", quality=50) 192 | await state.update_data(ruffle_prizes_menu_icon=img) 193 | await state.set_state(AddRufflePrizes.confirm) 194 | 195 | 196 | async def task_create_ruffle_prizes( 197 | title: str, money_needed: int, countdown_hours: int, 198 | description: str = None, photos: List[PhotoSize] = None, 199 | menu_icon: PhotoSize = None 200 | ) -> None: 201 | 202 | success_photos = [] 203 | success_low_quality_photos = [] 204 | if photos is not None: 205 | for photo in photos: 206 | photo_file = await bot.get_file(photo.file_id) 207 | downloaded_photo = await bot.download_file(photo_file.file_path) 208 | photo = downloaded_photo.read() 209 | 210 | s_photo = Photo.compress_img(photo, width=800, height=600, quality=70) 211 | l_photo = Photo.compress_img(photo, width=40, height=30, quality=10) 212 | success_photos.append(s_photo) 213 | success_low_quality_photos.append(l_photo) 214 | else: 215 | success_photos = None 216 | 217 | await db.create_ruffle_prizes( 218 | title=title, 219 | description=description, 220 | money_needed=money_needed, 221 | countdown_hours=countdown_hours, 222 | photos=success_photos, 223 | low_quality_photos=success_low_quality_photos, 224 | menu_icon=menu_icon 225 | ) 226 | 227 | 228 | @dp.callback_query(CData("confirm_create_ruffle_prizes")) 229 | async def confirm_create_ruffle_prizes(callback: types.CallbackQuery, state: FSMContext) -> None: 230 | state_data = await state.get_data() 231 | 232 | await bot.edit_message_caption( 233 | chat_id=callback.from_user.id, 234 | message_id=callback.message.message_id, 235 | caption=f"Розыгрыш \"{state_data['ruffle_prizes_title']}\" создан успешно!", 236 | reply_markup=ikb.back_to_start 237 | ) 238 | asyncio.create_task( 239 | task_create_ruffle_prizes( 240 | title=state_data["ruffle_prizes_title"], 241 | description=state_data["ruffle_prizes_description"], 242 | money_needed=state_data["ruffle_prizes_money_needed"], 243 | countdown_hours=state_data["ruffle_prizes_countdown_hours"], 244 | photos=state_data["ruffle_prizes_photos"], 245 | menu_icon=state_data["ruffle_prizes_menu_icon"] 246 | ) 247 | ) 248 | await state.clear() 249 | -------------------------------------------------------------------------------- /WebCore/static/js/main.js: -------------------------------------------------------------------------------- 1 | 2 | class Utils { 3 | post(data, callback) { 4 | let xhr = new XMLHttpRequest(); 5 | xhr.open("POST", "", true); 6 | xhr.setRequestHeader('Content-Type', 'application/json'); 7 | xhr.onload = function (res) { callback(JSON.parse(res.currentTarget.response)) }; 8 | data["Authorization"] = tg.initData; 9 | xhr.send(JSON.stringify(data)); 10 | } 11 | } 12 | 13 | class RufflePrizesBetsPage { 14 | open_page() { 15 | document.querySelector("#all-draws-page").style.display = "none"; 16 | document.querySelector("#detailed-draw-page").style.display = "none"; 17 | document.querySelector("#ruffle-prizes-bets-page").style.display = "block"; 18 | 19 | tg.BackButton.show() 20 | tg.BackButton.onClick(all_draws.open_page) 21 | } 22 | 23 | getAndAddBets() { 24 | utils.post({"method": "get_user_bets"}, (response)=>{ 25 | if (response["bets"] === null) {return} 26 | response["bets"].reverse().forEach((i)=>{ 27 | this.add_bet(i["ruffle_prizes_title"], i["bet_amount"],i["bet_time"]) 28 | }) 29 | }); 30 | } 31 | 32 | add_bet(draw_name, amount, time) { 33 | const date = new Date(time); 34 | const day = date.getDate().toString().padStart(2, "0"); 35 | const month = (date.getMonth() + 1).toString().padStart(2, "0"); 36 | const year = date.getFullYear(); 37 | const hour = date.getHours().toString().padStart(2, "0"); 38 | const minute = date.getMinutes().toString().padStart(2, "0"); 39 | const formattedDate = `${day}-${month}-${year} ${hour}:${minute}`; 40 | 41 | let el = document.querySelector("#ruffle-prizes-bets-page"); 42 | el.innerHTML = ` 43 |
44 | 45 |
46 |

Розыгрыш: ${draw_name}
Ставка: ${amount}
Время: ${formattedDate}

47 |
48 |
49 | ` + el.innerHTML 50 | } 51 | } 52 | 53 | class DetailedDrawPage { 54 | constructor() { 55 | this.opened_page_draw_id = null 56 | } 57 | open_page(draw_id) { 58 | let draw = all_draws.get_all_draws()[draw_id] 59 | 60 | // Заполнение фотографий 61 | let photos = (draw["photos"] === null) ? JSON.parse(draw["low_quality_photos"]) : JSON.parse(draw["photos"]) 62 | let img_style = (draw["photos"] === null) ? "filter: blur(20px); box_shadow: 0 0 10px 5px transparent;" : "filter: none; box_shadow: none;" 63 | let photos_html = ``; 64 | if (photos !== null) { 65 | photos.forEach(photo => { 66 | photos_html += ` 67 | 68 | `}); 69 | } else { 70 | photos_html += `
` 71 | } 72 | 73 | // Ставить кнопку 'Сделать ставку', или список победивших 74 | let end_html; 75 | if ("users_bets" in draw) { 76 | let users_bets_html = `` 77 | let users_bets = JSON.parse(draw["users_bets"]) 78 | for (let i in users_bets) { users_bets_html += `

${i} - ${users_bets[i]}Победитель: ${draw['winner_id']}

81 |
82 | 83 |
84 | ${users_bets_html} 85 |
86 |
87 | ` 88 | } else { 89 | end_html = `` 90 | } 91 | 92 | // Узнаем оставшееся время до подведения розыгрыша 93 | if (draw["countdown_start_time"] !== null && !(draw["is_over"])) { 94 | let start_time = new Date(draw["countdown_start_time"]); 95 | let current_time = new Date(); 96 | let timeDiff = current_time.getTime() - start_time.getTime(); 97 | console.log(start_time) 98 | console.log(current_time) 99 | var time_left = `Осталось времени: ${draw["countdown_hours"] - Math.floor(timeDiff / (1000 * 60 * 60))}ч`; 100 | } else if (draw["is_over"]) { 101 | var time_left = "Отсчет времени завершился!" 102 | } else { 103 | var time_left = "Отсчет времени еще не начался!" 104 | } 105 | 106 | document.querySelector("#detailed-draw-page").innerHTML = ` 107 | ${photos_html} 108 |
109 |

${draw['title']}

110 |

Собрано: ${draw['money_collected']}/${draw['money_needed']}

111 |

${time_left}

112 |
113 | 114 |
115 |

${draw['description']}

116 |
117 |
118 | ${end_html} 119 |
120 | ` 121 | 122 | document.querySelector("#all-draws-page").style.display = "none"; 123 | document.querySelector("#detailed-draw-page").style.display = "block"; 124 | document.querySelector("#ruffle-prizes-bets-page").style.display = "none"; 125 | 126 | tg.BackButton.show() 127 | tg.BackButton.onClick(all_draws.open_page) 128 | this.opened_page_draw_id = draw_id 129 | } 130 | 131 | draw_prizes_bet_popup(draw_id) { 132 | Swal.fire({ 133 | html: ` 134 |

Ставка

135 |

Ваш баланс: ${user_information["balance"]}₴

136 | 137 | `, 138 | showConfirmButton: false, 139 | showCancelButton: false, 140 | heightAuto: false, 141 | customClass: { 142 | htmlContainer: "popup-add-dialog-container-html", 143 | popup: "popup-add-dialog-popup" 144 | } 145 | }); 146 | } 147 | 148 | try_create_bet(draw_id) { 149 | let bet = document.querySelector("#input_draw_prizes_bet") 150 | if (bet.value !== "") { 151 | utils.post( 152 | {method: "create_draw_prizes_bet", draw_id: draw_id, bet: bet.value}, 153 | (response)=>{ 154 | if (response["ok"] === true) { 155 | user_information["balance"] = response["new_balance"] 156 | bets.add_bet(response["bet"]["ruffle_prizes_title"], response["bet"]["bet_amount"], response["bet"]["bet_time"]) 157 | 158 | let ruffle_id = response["bet"]["ruffle_prizes_id"] 159 | let list_of_draws = all_draws.get_all_draws() 160 | if (ruffle_id in list_of_draws) { 161 | list_of_draws[ruffle_id]["money_collected"] += response["bet"]["bet_amount"] 162 | let money_collected = list_of_draws[ruffle_id]["money_collected"] 163 | let money_needed = list_of_draws[ruffle_id]["money_needed"] 164 | document.querySelector("#detailed-draw-money-collected") 165 | .innerHTML = `Собрано: ${money_collected}/${money_needed}` 166 | document.querySelector(`#ruffle-prizes-${ruffle_id} button`) 167 | .innerHTML = `${money_collected}/${money_needed}` 168 | } 169 | 170 | if (!(ruffle_id in all_draws.draws.get("participate"))) { 171 | all_draws.draws.get("participate")[ruffle_id] = list_of_draws[ruffle_id] 172 | all_draws.reset_draws_statistic("*") 173 | } 174 | 175 | Swal.fire({ 176 | title: "Запрос принят!", 177 | text: "Ставка сделана! Удачи в розыгрыше :)", 178 | showConfirmButton: false, 179 | showCancelButton: false, 180 | }); 181 | } else { 182 | Swal.fire({ 183 | title: "Ошибка!", 184 | text: response["error"], 185 | showConfirmButton: false, 186 | showCancelButton: false, 187 | }); 188 | } 189 | } 190 | ); 191 | } 192 | bet.value = "" 193 | } 194 | } 195 | 196 | 197 | 198 | class AllDrawsPage { 199 | constructor() { 200 | this.draws = new Map(); 201 | 202 | // Переключение фильтров для показа списка розыгрышей 203 | /////////////////////////////////////////////////////////////// 204 | let set_color_for_statistic_boxes = (active, participate, closed) => { 205 | document.querySelector("#statistic_active").style.background = `#FFFFFF${active}` 206 | document.querySelector("#statistic_participate").style.background = `#FFFFFF${participate}` 207 | document.querySelector("#statistic_closed").style.background = `#FFFFFF${closed}` 208 | } 209 | set_color_for_statistic_boxes(80, 20, 20) 210 | document.querySelector("#statistic_active").addEventListener("click", ()=>{ 211 | set_color_for_statistic_boxes(80, 20, 20) 212 | this.show_filtered_draws("active") 213 | }); 214 | document.querySelector("#statistic_participate").addEventListener("click", ()=>{ 215 | set_color_for_statistic_boxes(20, 80, 20) 216 | this.show_filtered_draws("participate") 217 | }); 218 | document.querySelector("#statistic_closed").addEventListener("click", ()=>{ 219 | set_color_for_statistic_boxes(20, 20, 80) 220 | this.show_filtered_draws("closed") 221 | }); 222 | } 223 | 224 | load_high_quality_photos() { 225 | utils.post( 226 | {method: "load_high_quality_photos"}, 227 | (response) => { 228 | response["prize_draws"].forEach(draw => { 229 | if (draw["id"] in this.draws.get("active")) {this.draws.get("active")[draw["id"]]["photos"] = draw["photos"]} 230 | if (draw["id"] in this.draws.get("participate")) { this.draws.get("participate")[draw["id"]]["photos"] = draw["photos"]} 231 | if (draw["id"] in this.draws.get("closed")) { this.draws.get("closed")[draw["id"]]["photos"] = draw["photos"]} 232 | 233 | // Обновляем страницу с детальным описанием, если она открыта 234 | if (detailed_draws.opened_page_draw_id === draw["id"]) {detailed_draws.open_page(draw["id"])} 235 | }) 236 | } 237 | ) 238 | } 239 | 240 | open_page() { 241 | tg.BackButton.hide() 242 | document.querySelector("#all-draws-page").style.display = "block"; 243 | document.querySelector("#detailed-draw-page").style.display = "none"; 244 | document.querySelector("#ruffle-prizes-bets-page").style.display = "none"; 245 | } 246 | 247 | show_filtered_draws(filter_name) { 248 | let parentElement = document.querySelector("#list-draws-of-prizes") 249 | while (parentElement.firstChild) { 250 | parentElement.removeChild(parentElement.firstChild); 251 | } 252 | for (let key in this.draws.get(filter_name)) { 253 | this.add_draw(this.draws.get(filter_name)[key]) 254 | } 255 | } 256 | 257 | get_all_draws() { 258 | return Object.assign({}, this.draws.get("active"), this.draws.get("participate"), this.draws.get("closed")); 259 | } 260 | 261 | reset_draws_statistic(filter_name="*") { 262 | if (filter_name==="active" || filter_name==="*") { 263 | document.querySelector("#statistic_active").innerHTML = Object.keys(this.draws.get("active")).length; 264 | } 265 | if (filter_name==="participate" || filter_name==="*") { 266 | document.querySelector("#statistic_participate").innerHTML = Object.keys(this.draws.get("participate")).length; 267 | } 268 | if (filter_name==="closed" || filter_name==="*") { 269 | document.querySelector("#statistic_closed").innerHTML = Object.keys(this.draws.get("closed")).length; 270 | } 271 | } 272 | 273 | reset_prize_draws_list() { 274 | this.clear_all_draws() 275 | this.draws.set("active", {}) 276 | this.draws.set("participate", {}) 277 | this.draws.set("closed", {}) 278 | 279 | let set = (filter, prize_draws) => { 280 | prize_draws.forEach((value, _) => { 281 | this.draws.get(filter)[value["id"]] = value 282 | }) 283 | } 284 | utils.post({method: "get_prize_draws", type: "active"}, (response)=>{ 285 | set("active", response["prize_draws"]) 286 | this.reset_draws_statistic("active") 287 | this.show_filtered_draws("active") 288 | }); 289 | utils.post({method: "get_prize_draws", type: "participate"}, (response)=>{ 290 | set("participate", response["prize_draws"]) 291 | this.reset_draws_statistic("participate") 292 | }); 293 | utils.post({method: "get_prize_draws", type: "closed"}, (response)=>{ 294 | set("closed", response["prize_draws"]) 295 | this.reset_draws_statistic("closed") 296 | }); 297 | 298 | this.load_high_quality_photos(); 299 | } 300 | 301 | add_draw(draw) { 302 | document.querySelector("#list-draws-of-prizes").innerHTML += ` 303 |
  • 304 |
    305 | 306 |

    ${draw["title"]}

    307 |
    308 | 309 |
  • 310 | `; 311 | } 312 | 313 | clear_all_draws() { 314 | this.draws.clear() 315 | document.querySelector("#list-draws-of-prizes").innerHTML = ""; 316 | } 317 | } 318 | 319 | 320 | const tg = Telegram.WebApp; 321 | const utils = new Utils(); 322 | const all_draws = new AllDrawsPage(); 323 | const detailed_draws = new DetailedDrawPage(); 324 | const bets = new RufflePrizesBetsPage(); 325 | 326 | // Устанавливаем цвета из Telegram 327 | //////////////////////////////////////////// 328 | tg.themeParams.bg_color = "#222139" 329 | tg.themeParams.secondary_bg_color = "#37355E" 330 | tg.themeParams.text_color = "#FFFFFF" 331 | 332 | // Расширяем цветовую палитру 333 | ////////////////////////////////////////////// 334 | for (const [key, value] of Object.entries(tg.themeParams)) { 335 | const cssKey = `--${key.replaceAll("_", "-")}`; 336 | document.documentElement.style.setProperty(cssKey, value); 337 | 338 | for (let i = 10; i < 100; i += 10) { 339 | document.documentElement.style.setProperty(`${cssKey}-${i}`, `${value}${i}`); 340 | } 341 | } 342 | 343 | // Делаем WEBAPP на весь экран 344 | //////////////////////////////////// 345 | tg.expand(); 346 | 347 | // Загружаем информацию о пользователе 348 | //////////////////////////////////// 349 | let user_information; 350 | utils.post({"method": "get_user_data"}, (user_data)=>{ 351 | user_information = user_data 352 | let firstname_el = document.querySelector("#firstname"); 353 | let user_photo_el = document.querySelector("#user_avatar"); 354 | if("photo" in user_data) {user_photo_el.src = "data:image/png;base64, " + user_data["photo"]} 355 | if("firstname" in user_data) {firstname_el.innerHTML = user_data["firstname"]} 356 | else {firstname_el.innerHTML = "Anonymous"} 357 | }); 358 | 359 | // Подгружаем розыгрыши 360 | all_draws.reset_prize_draws_list() 361 | bets.getAndAddBets() 362 | --------------------------------------------------------------------------------