Розыгрыш: ${draw_name}
Ставка: ${amount}
Время: ${formattedDate}
├── 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 |Hello,
26 | 27 |Draws of prizes
39 |0
42 |Active
43 |0
46 |Participate
47 |0
50 |Closed
51 |Розыгрыш: ${draw_name}
Ставка: ${amount}
Время: ${formattedDate}
${i} - ${users_bets[i]}
Победитель: ${draw['winner_id']} 81 |${draw['title']}
110 |Собрано: ${draw['money_collected']}/${draw['money_needed']}
111 |${time_left}
112 |${draw['description']}
116 |Ваш баланс: ${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 |${draw["title"]}
307 |