Skip to content

Commit

Permalink
Feature: Customizable texts via Project Fluent (#9)
Browse files Browse the repository at this point in the history
* Updated all dependencies to their actual versions

* Updated pydantic to v2 along with config

* Added Project Fluent library and its loader

* Updates field validators

* Added SpinTextFilter to handle localized spin text

* Make localized spin keyboard

* Cache created keyboard, since it's always the same until restart

* Pass config through dispatcher

* Use localized texts for handlers

* Greatly speed up getting dice combo text

* Optionally send zero balance sticker (easter egg?)

* Added more config parameters

* Deleted const.py file

* Use throttle time from config

* Updated LICENSE

* Translated dice_check.py to English

* Localizable bot commands

* More comments in spin.py

* Added sample localization files for RU and EN

* Hardcode sleep after slot machine spin

* Updated env_example

* Added README.md in English, updated README.ru.md in Russian

* Removed .gitlab-ci.yml

* Updated docker-compose.example.yml
  • Loading branch information
MasterGroosha authored Sep 13, 2023
1 parent 09ef66c commit 389cbc6
Show file tree
Hide file tree
Showing 23 changed files with 402 additions and 211 deletions.
40 changes: 0 additions & 40 deletions .gitlab-ci.yml

This file was deleted.

2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2020-2022 Groosha
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
Expand Down
50 changes: 25 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
[<img src="https://img.shields.io/badge/Telegram-%40DifichentoBot-blue">](https://t.me/DifichentoBot)
[<img src="https://img.shields.io/badge/Telegram-%40DifichentoBot-blue">](https://t.me/DifichentoBot) (Ru)

# Виртуальное казино в Telegram
> 🇷🇺 README на русском доступен [здесь](README.ru.md)
В конце октября 2020 года команда Telegram выпустила [очередное обновление](https://telegram.org/blog/pinned-messages-locations-playlists/ru?ln=a)
мессенджера с поддержкой дайса игрового автомата. Вот он:
# Telegram Virtual Casino

![игровой автомат](repo_images/slot_machine.png)
In October 2020 Telegram team released [yet another update](https://telegram.org/blog/pinned-messages-locations-playlists)
with slot machine dice. Here it is:

Согласно [документации на тип Dice](https://core.telegram.org/bots/api#dice) в Bot API, слот-машина
может принимать значения от 1 до 64 включительно. В файле `casino.py` вы найдёте функции для сопоставления значения дайса
с тройкой выпавших элементов игрового автомата. Для демонстрации создан бот [@DifichentoBot](https://t.me/difichentobot) с
ведением счёта на виртуальные очки, начиная с 50.
Важным отличием от «традиционного» казино является невозможность влиять
на выпадающие комбинации, т.к. итоговое значение генерируется на стороне Telegram.
![slot machine dice](repo_images/slot_machine.png)

## Технологии
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.

* [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) — быстрое разворачивание бота \
в изолированном контейнере.
## 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

Скопируйте файл `env_example` как `.env` (с точкой в начале), откройте и отредактируйте содержимое. Создайте каталоги
`redis_data` и `redis_config`, в последний подложите свой конфиг `redis.conf` (в репозитории есть пример). \
Запустите бота командой `docker-compose up -d`.
Copy `env_example` file to `.env` (with leading dot), open and edit it. Create `redis_data` and `redis_config`
directories, put `redis.conf` file into the latter (there is [example](redis.example.conf) in this repo).
Run the bot with `docker-compose up -d` command.

Альтернативный вариант: используйте Systemd, пример службы тоже есть в репозитории.
Alternative way: you can use Systemd services, there is also an [example](casino-bot.example.service) available.

## Благодарности
## Credits to

* [@Tishka17](https://t.me/Tishka17) за изначальный вектор направления
* [@svinerus](https://t.me/svinerus) за компактную реализацию определения выпавшей комбинации (f6f42a841d3c1778f0e32)
* [@Tishka17](https://t.me/Tishka17) for initial inspiration
* [@svinerus](https://t.me/svinerus) for compact dice combination check (f6f42a841d3c1778f0e32)
36 changes: 36 additions & 0 deletions README.ru.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[<img src="https://img.shields.io/badge/Telegram-%40DifichentoBot-blue">](https://t.me/DifichentoBot)

# Виртуальное казино в Telegram

В конце октября 2020 года команда Telegram выпустила [очередное обновление](https://telegram.org/blog/pinned-messages-locations-playlists/ru?ln=a)
мессенджера с поддержкой дайса игрового автомата. Вот он:

![игровой автомат](repo_images/slot_machine.png)

Согласно [документации на тип 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

## Установка

Скопируйте файл `env_example` как `.env` (с точкой в начале), откройте и отредактируйте содержимое. Создайте каталоги
`redis_data` и `redis_config`, в последний подложите свой конфиг `redis.conf`
(в репозитории есть [пример](redis.example.conf)). Запустите бота командой `docker-compose up -d`.

Альтернативный вариант: используйте Systemd, пример службы тоже есть в [репозитории](casino-bot.example.service).

## Благодарности

* [@Tishka17](https://t.me/Tishka17) за изначальный вектор направления
* [@svinerus](https://t.me/svinerus) за компактную реализацию определения выпавшей комбинации (f6f42a841d3c1778f0e32)
15 changes: 10 additions & 5 deletions bot/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.fsm.storage.redis import RedisStorage

from bot.config_reader import config
from bot.config_reader import Settings
from bot.fluent_loader import get_fluent_localization
from bot.handlers import default_commands, spin
from bot.middlewares.throttling import ThrottlingMiddleware
from bot.ui_commands import set_bot_commands


async def main():
logging.basicConfig(level=logging.WARNING)
config = Settings()

bot = Bot(config.bot_token.get_secret_value(), parse_mode="HTML")

Expand All @@ -25,8 +27,11 @@ async def main():
else:
storage = MemoryStorage()

# Loading localization for bot
l10n = get_fluent_localization(config.bot_language)

# Создание диспетчера
dp = Dispatcher(storage=storage)
dp = Dispatcher(storage=storage, l10n=l10n, config=config)
# Принудительно настраиваем фильтр на работу только в чатах один-на-один с ботом
dp.message.filter(F.chat.type == "private")

Expand All @@ -35,10 +40,10 @@ async def main():
dp.include_router(spin.router)

# Регистрация мидлвари для троттлинга
dp.message.middleware(ThrottlingMiddleware())
dp.message.middleware(ThrottlingMiddleware(config.throttle_time_spin, config.throttle_time_other))

# Установка команд в интерфейсе
await set_bot_commands(bot)
# Set bot commands in the UI
await set_bot_commands(bot, l10n)

try:
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
Expand Down
45 changes: 24 additions & 21 deletions bot/config_reader.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
from enum import Enum
from typing import Optional

from pydantic import BaseSettings, validator, SecretStr, RedisDsn
from pydantic import field_validator, SecretStr, RedisDsn, FieldValidationInfo
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
bot_token: SecretStr
fsm_mode: str
redis: Optional[RedisDsn]
class FSMMode(str, Enum):
MEMORY = "memory"
REDIS = "redis"

@validator("fsm_mode")
def fsm_type_check(cls, v):
if v not in ("memory", "redis"):
raise ValueError("Incorrect fsm_mode. Must be one of: memory, redis")
return v

@validator("redis")
def skip_validating_redis(cls, v, values):
if values["fsm_mode"] == "redis" and v is None:
raise ValueError("Redis config is missing, though fsm_type is 'redis'")
class Settings(BaseSettings):
bot_token: SecretStr
fsm_mode: FSMMode
redis: Optional[RedisDsn] = None
bot_language: str
starting_points: int = 50
send_gameover_sticker: bool = False
throttle_time_spin: int = 2
throttle_time_other: int = 1

model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8')

@field_validator("redis", mode="after")
@classmethod
def skip_validating_redis(cls, v: Optional[RedisDsn], info: FieldValidationInfo):
if info.data.get("fsm_mode") == FSMMode.REDIS and v is None:
err = 'FSM Mode is set to "Redis", but Redis DNS is missing!'
raise ValueError(err)
return v

class Config:
env_file = '.env'
env_file_encoding = 'utf-8'


config = Settings()
5 changes: 0 additions & 5 deletions bot/const.py

This file was deleted.

57 changes: 33 additions & 24 deletions bot/dice_check.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,48 @@
# Source: https://gist.github.com/MasterGroosha/963c0a82df348419788065ab229094ac

from typing import List, Tuple
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: значение дайса (число)
:return: изменение счёта игрока (число)
:param dice_value: dice value (1-64)
:return: user score change (integer)
"""

# Совпадающие значения (кроме 777)
# three-of-a-kind (except 777)
if dice_value in (1, 22, 43):
return 7
# Начинающиеся с двух семёрок (опять же, не учитываем 777)
# 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_text(dice_value: int) -> List[str]:
def get_combo_parts(dice_value: int) -> List[str]:
"""
Возвращает то, что было на конкретном дайсе-казино
:param dice_value: значение дайса (число)
:return: массив строк, содержащий все выпавшие элементы в виде текста
Альтернативный вариант (ещё раз спасибо t.me/svinerus):
return [casino[(dice_value - 1) // i % 4]for i in (1, 4, 16)]
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", "виноград", "лимон", "семь"]
values = ["bar", "grapes", "lemon", "seven"]

dice_value -= 1
result = []
Expand All @@ -44,14 +52,15 @@ def get_combo_text(dice_value: int) -> List[str]:
return result


def get_combo_data(dice_value: int) -> Tuple[int, str]:
@lru_cache(maxsize=64)
def get_combo_text(dice_value: int, l10n: FluentLocalization) -> str:
"""
Возвращает все необходимые для показа информации о комбинации данные
:param dice_value: значение дайса (число)
:return: Пара ("изменение счёта", "список выпавших элементов")
Returns localized string with dice result
:param dice_value: dice value (1-64)
:param l10n: Fluent localization object
:return: string with localized result
"""
return (
get_score_change(dice_value),
', '.join(get_combo_text(dice_value))
)
parts: list[str] = get_combo_parts(dice_value)
for i in range(len(parts)):
parts[i] = l10n.format_value(parts[i])
return ", ".join(parts)
5 changes: 5 additions & 0 deletions bot/filters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .spin_text_filter import SpinTextFilter

__all__ = [
"SpinTextFilter"
]
8 changes: 8 additions & 0 deletions bot/filters/spin_text_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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")
38 changes: 38 additions & 0 deletions bot/fluent_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from pathlib import Path

from fluent.runtime import FluentLocalization, FluentResourceLoader


def get_fluent_localization(language: str) -> FluentLocalization:
"""
Loads FTL files for chosen language
:param language: language name, as passed from configuration outside
:return: FluentLocalization object with loaded FTL files for chosen language
"""

# Check "locales" directory on the same level as this file
locales_dir = Path(__file__).parent.joinpath("locales")
if not locales_dir.exists():
err = '"locales" directory does not exist'
raise FileNotFoundError(err)
if not locales_dir.is_dir():
err = '"locales" is not a directory'
raise NotADirectoryError(err)

locales_dir = locales_dir.absolute()
locale_dir_found = False
for directory in Path.iterdir(locales_dir):
if directory.stem == language:
locale_dir_found = True
break
if not locale_dir_found:
err = f'Directory for "{language}" locale not found'
raise FileNotFoundError(err)

locale_files = list()
for file in Path.iterdir(Path.joinpath(locales_dir, language)):
if file.suffix == ".ftl":
locale_files.append(str(file.absolute()))
l10n_loader = FluentResourceLoader(str(Path.joinpath(locales_dir, "{locale}")))

return FluentLocalization([language], locale_files, l10n_loader)
Loading

0 comments on commit 389cbc6

Please sign in to comment.