Repository: MasterGroosha/telegram-casino-bot
Branch: master
Commit: 7720d65ee908
Files: 32
Total size: 31.1 KB
Directory structure:
gitextract_t9fz8z6y/
├── .dockerignore
├── .gitignore
├── .python-version
├── Dockerfile
├── LICENSE
├── README.md
├── README.ru.md
├── bot/
│ ├── __init__.py
│ ├── __main__.py
│ ├── config_reader.py
│ ├── dice_check.py
│ ├── filters/
│ │ ├── __init__.py
│ │ └── spin_text_filter.py
│ ├── fluent_loader.py
│ ├── handlers/
│ │ ├── __init__.py
│ │ ├── default_commands.py
│ │ └── spin.py
│ ├── keyboards.py
│ ├── locale/
│ │ ├── current/
│ │ │ └── README.md
│ │ └── example/
│ │ ├── README.md
│ │ ├── README.ru.md
│ │ ├── en/
│ │ │ └── strings.ftl
│ │ └── ru/
│ │ └── strings.ftl
│ ├── logs.py
│ ├── middlewares/
│ │ ├── __init__.py
│ │ └── throttling.py
│ └── ui_commands.py
├── casino-bot.example.service
├── docker-compose.example.yml
├── pyproject.toml
├── redis.example.conf
└── settings.example.toml
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
repo_images/
redis.example.conf
settings.example.toml
.git/
.idea/
.vscode/
__pycache__
*.pyc
================================================
FILE: .gitignore
================================================
.idea/
/venv/
/.venv/
.vscode/
*.ipynb
docker-compose.yml
.env
/redis.conf
/settings.toml
================================================
FILE: .python-version
================================================
3.11
================================================
FILE: Dockerfile
================================================
## Build stage
FROM ghcr.io/astral-sh/uv:bookworm-slim AS builder
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
ENV UV_PYTHON_INSTALL_DIR=/python UV_PYTHON_PREFERENCE=only-managed
RUN uv python install 3.11
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev --no-editable
COPY ./bot /app/bot
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev --no-editable
## Final stage
FROM gcr.io/distroless/python3-debian12:nonroot
COPY --from=builder --chown=nonroot:nonroot /python /python
WORKDIR /app
COPY --from=builder --chown=nonroot:nonroot /app/ /app
ENV PYTHONPATH="/app/.venv/lib/python3.11/site-packages:$PYTHONPATH"
CMD ["-m", "bot"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020-present Aleksandr (aka MasterGroosha on GitHub)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
[
](https://t.me/DifichentoBot) (Ru)
> 🇷🇺 README на русском доступен [здесь](README.ru.md)
# Telegram Virtual Casino
In October 2020 Telegram team released [yet another update](https://telegram.org/blog/pinned-messages-locations-playlists)
with slot machine dice. Here it is:

According to [Dice type documentation](https://core.telegram.org/bots/api#dice) in Bot API, slot machine
emits values 1 to 64. In [dice_check.py](bot/dice_check.py) file you can find all the logic regarding
matching dice integer value with visual three-icons representation. There is also a test bot [@DifichentoBot](https://t.me/difichentobot)
in Russian to test how it works.
Dice are generated on Telegram server-side, you your bot cannot affect the result.
## Technology
* [aiogram](https://github.com/aiogram/aiogram) — asyncio Telegram Bot API framework;
* [redis](https://redis.io) — persistent data storage (persistency enabled separately);
* [cachetools](https://cachetools.readthedocs.io/en/stable) — for anti-flood throttling mechanism;
* [Docker](https://www.docker.com) and [Docker-Compose](https://docs.docker.com/compose) — quickly deploy bot in containers.
* Systemd
## Installation
Copy `settings.example.toml` file to `settings.toml`, open and edit it.
To change bot's language, overwrite `bot/locale/current` directory contents with a chosen language. For example, you want
to use English language in your bot. Then, in `bot/locale/current` create a directory named `en` and place your `.ftl`
files inside. Check [this directory](bot/locales/example) for samples.
Please note that only one language can be active at a time.
Finally, run the bot with `docker-compose --profile "all" up -d` command.
Alternative way: you can use Systemd services, there is also an [example](casino-bot.example.service) available.
## Credits to
* [@Tishka17](https://t.me/Tishka17) for initial inspiration
* [@svinerus](https://t.me/svinerus) for compact dice combination check (f6f42a841d3c1778f0e32)
## Note on versioning
For most of my Telegram bots, I plan to use Calendar Versioning with the following rules:
* Versions should look like `vAAAA.BB.C`, where:
* * `vAAAA` is the letter "v" followed by the 4-digit year of release, e.g., `v2025`.
* * `BB` is the 2-digit month number, e.g., `06` for June.
* * `C` is the release number for that month, not zero-padded, e.g., 1 for the first release in June.
For example, the first release to use the new versioning schema will be tagged as `v2025.06.1`.
This scheme makes it easier to understand which Bot API features might be supported in a given release and which are definitely not.
================================================
FILE: README.ru.md
================================================
[
](https://t.me/DifichentoBot)
# Виртуальное казино в Telegram
В конце октября 2020 года команда Telegram выпустила [очередное обновление](https://telegram.org/blog/pinned-messages-locations-playlists/ru?ln=a)
мессенджера с поддержкой дайса игрового автомата. Вот он:

Согласно [документации на тип Dice](https://core.telegram.org/bots/api#dice) в Bot API, слот-машина
может принимать значения от 1 до 64 включительно. В файле [dice_check.py](bot/dice_check.py) вы найдёте функции
для сопоставления значения дайса с тройкой выпавших элементов игрового автомата.
Для демонстрации создан бот [@DifichentoBot](https://t.me/difichentobot) с ведением счёта на виртуальные очки.
Важным отличием от «традиционного» казино является невозможность влиять
на выпадающие комбинации, т.к. итоговое значение генерируется на стороне Telegram.
## Технологии
* [aiogram](https://github.com/aiogram/aiogram) — работа с Telegram Bot API;
* [redis](https://redis.io) — персистентное хранение данных (персистентность включается отдельно);
* [cachetools](https://cachetools.readthedocs.io/en/stable) — реализация троттлинга для борьбы с флудом;
* [Docker](https://www.docker.com) и [Docker-Compose](https://docs.docker.com/compose) — быстрое разворачивание бота в изолированном контейнере.
* Systemd
## Установка
Скопируйте файл `settings.example.toml` под именем `settings.toml`, откройте и отредактируйте.
Для смены языка в каталоге `bot/locale/current` создайте каталог с названием нужного языка (например, `ru`),
а внутрь положите вашие `.ftl`-файлы. Примеры есть в [этом каталоге](bot/locales/example).
Учтите, что язык бота определяется именно файлами в каталоге `current`, язык пользователя не учитывается.
Наконец, запустите бота командой `docker-compose up --profile "all" -d`.
Альтернативный вариант: используйте Systemd, пример службы тоже есть в [репозитории](casino-bot.example.service).
## Благодарности
* [@Tishka17](https://t.me/Tishka17) за изначальный вектор направления
* [@svinerus](https://t.me/svinerus) за компактную реализацию определения выпавшей комбинации (f6f42a841d3c1778f0e32)
## Примечание об именовании релизов
Для большинства своих телеграм-ботов, я планирую использовать [Calendar Versioning](https://calver.org) по следующим правилам:
* Номер версии (релиза) должен иметь формат `vAAAA.BB.C`, где:
* * `vAAAA` это латинская буква v, после которой идет номер года релиза, например, `v2025`.
* * `BB` это двузначный номер месяца, например, `06` для июня.
* * `C` это порядковый номер релиза в месяце (без zero-padding'а), например, 1 для первого релиза в июне.
Проще говоря, первый релиз, который будет следовать новому формату, будет иметь вид `v2025.06.1`.
При таком подходе становится гораздо проще понять, какие фичи Bot API могут поддерживаться ботом, а какие точно нет.
================================================
FILE: bot/__init__.py
================================================
================================================
FILE: bot/__main__.py
================================================
import asyncio
import structlog
from aiogram import Bot, Dispatcher, F
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.fsm.storage.redis import RedisStorage
from structlog.typing import FilteringBoundLogger
from bot.config_reader import LogConfig, get_config, BotConfig, FSMMode, RedisConfig, GameConfig
from bot.fluent_loader import get_fluent_localization
from bot.handlers import default_commands, spin
from bot.logs import get_structlog_config
from bot.middlewares.throttling import ThrottlingMiddleware
from bot.ui_commands import set_bot_commands
async def main():
log_config = get_config(model=LogConfig, root_key="logs")
structlog.configure(**get_structlog_config(log_config))
bot_config = get_config(model=BotConfig, root_key="bot")
bot = Bot(
token=bot_config.token.get_secret_value(),
default=DefaultBotProperties(
parse_mode=ParseMode.HTML
)
)
if bot_config.fsm_mode == FSMMode.REDIS:
redis_config = get_config(model=RedisConfig, root_key="redis")
storage = RedisStorage.from_url(
url=str(redis_config.dsn),
connection_kwargs={"decode_responses": True},
)
else:
storage = MemoryStorage()
# Loading localization for bot
l10n = get_fluent_localization()
game_config = get_config(model=GameConfig, root_key="game_config")
# Creating dispatcher with some dependencies
dp = Dispatcher(
storage=storage,
l10n=l10n,
game_config=game_config,
)
# Make bot work only in PM (one-on-one chats) with bot
dp.message.filter(F.chat.type == "private")
# Register routers with handlers
dp.include_router(default_commands.router)
dp.include_router(spin.router)
# Register throttling middleware
dp.message.middleware(
ThrottlingMiddleware(game_config.throttle_time_spin, game_config.throttle_time_other)
)
# Set bot commands in the UI
await set_bot_commands(bot, l10n)
logger: FilteringBoundLogger = structlog.get_logger()
await logger.ainfo("Starting polling...")
try:
await dp.start_polling(bot)
finally:
await bot.session.close()
if __name__ == '__main__':
asyncio.run(main())
================================================
FILE: bot/config_reader.py
================================================
from enum import StrEnum, auto
from functools import lru_cache
from os import getenv
from tomllib import load
from typing import Type, TypeVar
from pydantic import BaseModel, SecretStr, field_validator, RedisDsn
ConfigType = TypeVar("ConfigType", bound=BaseModel)
class LogRenderer(StrEnum):
JSON = auto()
CONSOLE = auto()
class FSMMode(StrEnum):
MEMORY = auto()
REDIS = auto()
class BotConfig(BaseModel):
token: SecretStr
fsm_mode: FSMMode
@field_validator('fsm_mode', mode="before")
@classmethod
def fsm_mode_to_lower(cls, v: str):
return v.lower()
class LogConfig(BaseModel):
project_name: str = "my project"
show_datetime: bool
datetime_format: str
show_debug_logs: bool
time_in_utc: bool
use_colors_in_console: bool
renderer: LogRenderer
allow_third_party_logs: bool
@field_validator('renderer', mode="before")
@classmethod
def log_renderer_to_lower(cls, v: str):
return v.lower()
class RedisConfig(BaseModel):
dsn: RedisDsn
class GameConfig(BaseModel):
starting_points: int
send_gameover_sticker: bool
throttle_time_spin: int
throttle_time_other: int
@lru_cache
def parse_config_file() -> dict:
# Проверяем наличие переменной окружения, которая переопределяет путь к конфигу
file_path = getenv("CONFIG_FILE_PATH")
if file_path is None:
error = "Could not find settings file"
raise ValueError(error)
# Читаем сам файл, пытаемся его распарсить как TOML
with open(file_path, "rb") as file:
config_data = load(file)
return config_data
@lru_cache
def get_config(model: Type[ConfigType], root_key: str) -> ConfigType:
config_dict = parse_config_file()
if root_key not in config_dict:
error = f"Key {root_key} not found"
raise ValueError(error)
return model.model_validate(config_dict[root_key])
================================================
FILE: bot/dice_check.py
================================================
# Source: https://gist.github.com/MasterGroosha/963c0a82df348419788065ab229094ac
from functools import lru_cache
from typing import List
from fluent.runtime import FluentLocalization
@lru_cache(maxsize=64)
def get_score_change(dice_value: int) -> int:
"""
Checks for the winning combination
:param dice_value: dice value (1-64)
:return: user score change (integer)
"""
# three-of-a-kind (except 777)
if dice_value in (1, 22, 43):
return 7
# starting with two 7's (again, except 777)
elif dice_value in (16, 32, 48):
return 5
# jackpot (777)
elif dice_value == 64:
return 10
else:
return -1
def get_combo_parts(dice_value: int) -> List[str]:
"""
Returns exact icons from dice (bar, grapes, lemon, seven).
Do not edit these values, since they are subject to be translated
by outer code.
:param dice_value: dice value (1-64)
:return: list of icons' texts
"""
# Alternative way (credits to t.me/svinerus):
# return [casino[(dice_value - 1) // i % 4]for i in (1, 4, 16)]
# Do not edit these values; they are actually translation keys
# 0 1 2 3
values = ["bar", "grapes", "lemon", "seven"]
dice_value -= 1
result = []
for _ in range(3):
result.append(values[dice_value % 4])
dice_value //= 4
return result
@lru_cache(maxsize=64)
def get_combo_text(dice_value: int, l10n: FluentLocalization) -> str:
"""
Returns localized string with dice result
:param dice_value: dice value (1-64)
:param l10n: Fluent localization object
:return: string with localized result
"""
parts: list[str] = get_combo_parts(dice_value)
for i in range(len(parts)):
parts[i] = l10n.format_value(parts[i])
return ", ".join(parts)
================================================
FILE: bot/filters/__init__.py
================================================
from .spin_text_filter import SpinTextFilter
__all__ = [
"SpinTextFilter"
]
================================================
FILE: bot/filters/spin_text_filter.py
================================================
from aiogram.filters import BaseFilter
from aiogram.types import Message
from fluent.runtime import FluentLocalization
class SpinTextFilter(BaseFilter):
async def __call__(self, message: Message, l10n: FluentLocalization) -> bool:
return message.text == l10n.format_value("spin-button-text")
================================================
FILE: bot/fluent_loader.py
================================================
from pathlib import Path
from fluent.runtime import FluentLocalization, FluentResourceLoader
def get_fluent_localization() -> FluentLocalization:
# Access "locale/current" dir
real_locale_dir = Path(__file__).parent.joinpath("locale", "current")
# Find all subdirectories.
real_languages_subdirs: list = [d for d in real_locale_dir.iterdir() if d.is_dir()]
# There must be at least one subdirectory inside. Ideally – only one.
if len(real_languages_subdirs) == 0:
raise RuntimeError("No languages found in the 'current' directory.")
# Select the first subdir if there are multiple subdirectories inside.
selected_language_dir = real_languages_subdirs[0]
# Find all .ftl files inside the selected language directory
ftl_files = [f.name for f in selected_language_dir.iterdir() if f.is_file() and f.suffix == ".ftl"]
if len(ftl_files) == 0:
raise RuntimeError(f"No .ftl files found in the {selected_language_dir.name} directory.")
# Now form Fluent-compatible path and create Fluent objects.
locale_dir = real_locale_dir.joinpath("{locale}")
loader = FluentResourceLoader(str(locale_dir.absolute()))
return FluentLocalization([selected_language_dir.name], ftl_files, loader)
================================================
FILE: bot/handlers/__init__.py
================================================
================================================
FILE: bot/handlers/default_commands.py
================================================
from aiogram import Router
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, ReplyKeyboardRemove
from fluent.runtime import FluentLocalization
from bot.config_reader import GameConfig
from bot.keyboards import get_spin_keyboard
flags = {"throttling_key": "default"}
router = Router()
@router.message(Command("start"), flags=flags)
async def cmd_start(
message: Message,
state: FSMContext,
l10n: FluentLocalization,
game_config: GameConfig,
):
start_text = l10n.format_value("start-text", {"points": game_config.starting_points})
await state.update_data(score=game_config.starting_points)
await message.answer(start_text, reply_markup=get_spin_keyboard(l10n))
@router.message(Command("stop"), flags=flags)
async def cmd_stop(message: Message, l10n: FluentLocalization):
await message.answer(
l10n.format_value("stop-text"),
reply_markup=ReplyKeyboardRemove()
)
@router.message(Command("help"), flags=flags)
async def cmd_help(message: Message, l10n: FluentLocalization):
await message.answer(
l10n.format_value("help-text"),
disable_web_page_preview=True
)
================================================
FILE: bot/handlers/spin.py
================================================
from asyncio import sleep
from contextlib import suppress
from aiogram import Router
from aiogram.enums.dice_emoji import DiceEmoji
from aiogram.exceptions import TelegramBadRequest
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message
from fluent.runtime import FluentLocalization
from bot.config_reader import GameConfig
from bot.dice_check import get_combo_text, get_score_change
from bot.filters import SpinTextFilter
from bot.keyboards import get_spin_keyboard
flags = {"throttling_key": "spin"}
router = Router()
@router.message(Command("spin"), flags=flags)
@router.message(SpinTextFilter(), flags=flags)
async def cmd_spin(
message: Message,
state: FSMContext,
l10n: FluentLocalization,
game_config: GameConfig,
):
# Get current score
user_data = await state.get_data()
user_score = user_data.get("score", game_config.starting_points)
if user_score == 0:
if game_config.send_gameover_sticker:
# In case sticker file_id is invalid or missing
with suppress(TelegramBadRequest):
await message.answer_sticker(l10n.format_value("zero-balance-sticker"))
await message.answer(l10n.format_value("zero-balance"))
return
# Send dice to user
msg = await message.answer_dice(emoji=DiceEmoji.SLOT_MACHINE, reply_markup=get_spin_keyboard(l10n))
# Check whether user won or not
score_change = get_score_change(msg.dice.value)
if score_change < 0:
win_or_lose_text = l10n.format_value("spin-fail")
else:
win_or_lose_text = l10n.format_value("spin-success", {"score-value": score_change})
# Updating score in FSM data
new_score = user_score + score_change
await state.update_data(score=new_score)
# This delay is roughly equivalent of animation duration
# of slot machine. Depending on dice value,
# animation duration is different, but approx. 2 seconds
await sleep(2.0)
await msg.reply(
l10n.format_value(
"after-spin",
{
"combo_text": get_combo_text(msg.dice.value, l10n),
"dice_value": msg.dice.value,
"result_text": win_or_lose_text,
"score-value": new_score
}
)
)
================================================
FILE: bot/keyboards.py
================================================
from functools import cache
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton
from fluent.runtime import FluentLocalization
@cache
def get_spin_keyboard(l10n: FluentLocalization):
keyboard = [
[KeyboardButton(text=l10n.format_value("spin-button-text"))]
]
return ReplyKeyboardMarkup(keyboard=keyboard, resize_keyboard=True)
================================================
FILE: bot/locale/current/README.md
================================================
Place your locale directory with ftl-files here.
================================================
FILE: bot/locale/example/README.md
================================================
> 🇷🇺 README на русском доступен [здесь](README.ru.md)
In this directory you can find sample localization files for EN and RU languages.
To use them in your bot, create a new directory with language code, e.g. "en" in "locales/current" folder
(e.g. "bot/locales/current/en"). Then put your .ftl files in that directory (e.g. "strings.ftl").
================================================
FILE: bot/locale/example/README.ru.md
================================================
В этом каталоге располагаются образцы файлов локализации для английского и русского языков.
Чтобы использовать бота на нужном языке, создайте в каталоге "locales/current" папку с любым удобным названием
локализации, например, "ru". Затем положите туда ваши `.ftl`-файлы (или скопируйте пример из текущего каталога).
У вас должен получиться путь `bot/locales/current/ru` с `ftl`-файлами внутри.
================================================
FILE: bot/locale/example/en/strings.ftl
================================================
start-text =
Welcome to our virtual casino!
From the start, you have { $points } points. Every spin costs 1 point. Possible winning combinations are:
3-of-a-kind (except "7") — 7 points
7️⃣7️⃣▫️ — 5 points (square = anything)
7️⃣7️⃣7️⃣ — 10 points
Disclaimer: this bot was made solely for entertainment purposes, your data may be lost any time!
There are no paid options in this bot.
Remove keyboard — /stop
Show keyboard if missing — /spin
help-text =
Telegram slot machine has 4 icons: BAR, grapes, lemon and number 7. In total, there are 64 combinations.
Decoding of dice value is described here.
Source code of the bot is available on GitHub
stop-text = Keyboard removed. To start from scratch, press /start, to get keyboard and continue: /spin
bar = BAR
grapes = grapes
lemon = lemon
seven = seven
spin-button-text = 🎰 Try it!
spin-fail = You lost the bet.
spin-success =
You won {$score_change ->
[one] {$score_change} point
*[many] {$score_change} points
}!
after-spin =
Your combination: { $combo_text } (№{ $dice_value }).
{ $result_text } New score: { $new_score }.
zero-balance =
Your balance is zero. Accept your fate and get back to your business, or press /start to start over. To remove keyboard, press /stop.
# If you don't want to send sticker when balance is zero, disable feature in bot configuration
zero-balance-sticker = CAACAgIAAxkBAAEWXv5lAUAm76JOjvehtp18Gxb3if0eVQAC-hEAAknF8EuBzj23_M8x3jAE
menu-start = Restart Casino
menu-spin = Show keyboard and make a spin
menu-stop = Remove keyboard
menu-help = Information about this bot
================================================
FILE: bot/locale/example/ru/strings.ftl
================================================
start-text =
Добро пожаловать в наше виртуальное казино!
У вас на старте { $points } очков. Каждая попытка стоит 1 очко, а за выигрышные комбинации вы получите:
3 одинаковых символа (кроме семёрки) — 7 очков
7️⃣7️⃣▫️ — 5 очков (квадрат = что угодно)
7️⃣7️⃣7️⃣ — 10 очков
Внимание: бот предназначен исключительно для демонстрации, и ваши данные могут быть сброшены в любой момент!
Помните: лудомания — это болезнь, и никаких платных опций в боте нет.
Убрать клавиатуру — /stop
Показать клавиатуру, если пропала — /spin
help-text =
В казино доступно 4 элемента: BAR, виноград, лимон и цифра семь. Комбинаций, соответственно, 64.
Для распознавания комбинации используется четверичная система, а пример кода для получения комбинации по значению от Bot API можно увидеть здесь.
Исходный код бота доступен на GitHub и на GitLab."
stop-text = Клавиатура удалена. Начать заново: /start, вернуть клавиатуру и продолжить: /spin
bar = BAR
grapes = виноград
lemon = лимон
seven = семь
spin-button-text = 🎰 Испытать удачу!
score-points = {$score-value ->
[one] {$score-value} очко
[few] {$score-value} очка
*[many] {$score-value} очков
}
spin-fail =
К сожалению, вы не выиграли.
spin-success =
Вы выиграли {score-points}!
after-spin =
Ваша комбинация: {$combo_text} (№{$dice_value}).
{$result_text}
У вас осталось {score-points}.
zero-balance =
Ваш баланс равен нулю. Вы можете смириться с судьбой и продолжить жить своей жизнью, а можете нажать /start, чтобы начать всё заново. Или /stop, чтобы просто убрать клавиатуру.
# Если не хотите использовать стикер, укажите это в конфиге
zero-balance-sticker = CAACAgIAAxkBAAEFGxpfqmqG-MltYIj4zjmFl1eCBfvhZwACuwIAAuPwEwwS3zJY4LIw9B4E
menu-start = Перезапустить казино
menu-spin = Показать клавиатуру и сделать бросок
menu-stop = Убрать клавиатуру
menu-help = Справочная информация
================================================
FILE: bot/logs.py
================================================
import logging
from json import dumps
from sys import stdout
import structlog
from structlog import WriteLoggerFactory
from structlog.typing import WrappedLogger, EventDict
from bot.config_reader import LogConfig, LogRenderer
class ProjectNameProcessor:
def __init__(self, project_name: str):
self.project_name = project_name
def __call__(
self, logger: WrappedLogger, name: str, event_dict: EventDict
) -> EventDict:
event_dict["project_name"] = self.project_name
return event_dict
def get_structlog_config(log_config: LogConfig) -> dict:
if log_config.show_debug_logs is True:
min_level = logging.DEBUG
else:
min_level = logging.INFO
if log_config.allow_third_party_logs:
# Create handler for stdlib logging
standard_handler = logging.StreamHandler(stream=stdout)
standard_handler.setFormatter(
structlog.stdlib.ProcessorFormatter(
processors=get_processors(log_config)
)
)
# Configure root logger to use this handler
standard_logger = logging.getLogger()
standard_logger.addHandler(standard_handler)
standard_logger.setLevel(logging.DEBUG if log_config.show_debug_logs else logging.INFO)
return {
"processors": get_processors(log_config),
"cache_logger_on_first_use": True,
"wrapper_class": structlog.make_filtering_bound_logger(min_level),
"logger_factory": WriteLoggerFactory()
}
def get_processors(log_config: LogConfig) -> list:
def custom_json_serializer(data, *args, **kwargs):
result = dict()
# Set keys in specific order
for key in ("level", "event"):
if key in data:
result[key] = data.pop(key)
# Clean up non-native structlog logs:
if "_from_structlog" in data:
data.pop("_from_structlog")
data.pop("_record")
# Add all other fields
result.update(**data)
return dumps(result, default=str)
processors = list()
if log_config.show_datetime is True:
processors.append(structlog.processors.TimeStamper(
fmt=log_config.datetime_format,
utc=log_config.time_in_utc
)
)
processors.append(structlog.processors.add_log_level)
processors.append(ProjectNameProcessor(log_config.project_name))
if log_config.renderer == LogRenderer.JSON:
processors.append(structlog.processors.format_exc_info)
processors.append(structlog.processors.JSONRenderer(serializer=custom_json_serializer))
else:
processors.append(structlog.dev.ConsoleRenderer(
colors=log_config.use_colors_in_console,
pad_level=False
))
return processors
================================================
FILE: bot/middlewares/__init__.py
================================================
================================================
FILE: bot/middlewares/throttling.py
================================================
from typing import Any, Awaitable, Callable, Dict
from aiogram import BaseMiddleware
from aiogram.dispatcher.flags import get_flag
from aiogram.types import Message
from cachetools import TTLCache
class ThrottlingMiddleware(BaseMiddleware):
def __init__(self, throttle_time_spin: int, throttle_time_other: int):
self.caches = {
"spin": TTLCache(maxsize=10_000, ttl=throttle_time_spin),
"default": TTLCache(maxsize=10_000, ttl=throttle_time_other)
}
async def __call__(
self,
handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]],
event: Message,
data: Dict[str, Any],
) -> Any:
throttling_key = get_flag(data, "throttling_key")
if throttling_key is not None and throttling_key in self.caches:
if event.chat.id in self.caches[throttling_key]:
return
else:
self.caches[throttling_key][event.chat.id] = None
return await handler(event, data)
================================================
FILE: bot/ui_commands.py
================================================
from aiogram import Bot
from aiogram.types import BotCommand, BotCommandScopeAllPrivateChats
from fluent.runtime import FluentLocalization
async def set_bot_commands(bot: Bot, l10n: FluentLocalization):
commands = [
BotCommand(command="start", description=l10n.format_value("menu-start")),
BotCommand(command="spin", description=l10n.format_value("menu-spin")),
BotCommand(command="stop", description=l10n.format_value("menu-stop")),
BotCommand(command="help", description=l10n.format_value("menu-help"))
]
await bot.set_my_commands(commands=commands, scope=BotCommandScopeAllPrivateChats())
================================================
FILE: casino-bot.example.service
================================================
[Unit]
Description=Telegram Casino Bot
After=network.target redis.service
[Service]
Type=simple
WorkingDirectory=/home/user/casino-bot
ExecStart=/home/user/casino-bot/venv/bin/python -m bot
KillMode=process
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
================================================
FILE: docker-compose.example.yml
================================================
name: "telegram-casino-bot"
services:
bot:
build:
context: "."
dockerfile: "Dockerfile"
profiles:
- "all"
restart: "always"
stop_signal: SIGINT
volumes:
# Path to .toml file with settings
- "/home/user/casino-bot/settings.toml:/app/settings.toml"
# Path to localization directory
- "/home/user/casino-bot/locale/example/en:/app/bot/locale/current/en"
environment:
- CONFIG_FILE_PATH=/app/settings.toml
depends_on:
- redis
redis:
profiles:
- "all"
- "infra"
image: "redis:8-alpine"
restart: "always"
volumes:
# Path to redis.conf file
- "/home/user/casino-bot/redis.conf:/usr/local/etc/redis/redis.conf"
# Redis volume
- "redis_data:/data"
command: "redis-server /usr/local/etc/redis/redis.conf"
healthcheck:
test: [ "CMD", "redis-cli","ping" ]
volumes:
redis_data:
================================================
FILE: pyproject.toml
================================================
[project]
name = "telegram-casino-bot"
version = "2025.06.1"
description = "A simple casino-like bot for Telegram"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"aiogram>=3.18.0",
"async-timeout>=5.0.1",
"cachetools>=5.5.2",
"fluent-runtime>=0.4.0",
"redis>=5.2.1",
"structlog>=25.1.0",
]
================================================
FILE: redis.example.conf
================================================
port 6379
save 600 1
dbfilename redis_dump.rdb
================================================
FILE: settings.example.toml
================================================
[bot]
# Bot token. Obtain one from https://t.me/botfather
token = "1234567890:AaBbCcDdEeFfGrOoShALlMmNnOoPpQqRrSs"
# Where to store users' data. Options: "memory", "redis".
# Memory storage gets wiped upon bot restart, Redis uses persistency.
fsm_mode = "redis"
[redis]
# In case you select "Redis" fsm_mode, specify connection string here
dsn = "redis://user:pass@host:port"
[logs]
# Project name. Used in logs, useful when aggregating logs from different bots.
project_name = "casino_bot"
# If true, logs will include date and time of the event
show_datetime = true
# Datetime format in logs
datetime_format = "%Y-%m-%d %H:%M:%S"
# If true, logs with DEBUG level will be shown. Otherwise, only INFO+
show_debug_logs = false
# If true, logs will use UTC time instead of server's local time
time_in_utc = false
# How to show logs. Options: "console", "json"
renderer = "json"
# If true, "console" renderer will use colors when rendering logs
use_colors_in_console = false
# If true, logs from other libraries (e.g. aiogram) will be shown
allow_third_party_logs = true
[game_config]
# Starting points for all new games
starting_points = 50
# Whether to send a special "game over" sticker or not. Specify sticker's file id in localization file
send_gameover_sticker = true
# Throttling time for spins. Casino dice animation is variable, on average ~2 seconds long
throttle_time_spin = 2
# Throttling time for all other actions.
throttle_time_other = 1