├── .dockerignore
├── .editorconfig
├── .env.example
├── .gcloudignore
├── .github
└── workflows
│ ├── check.yml
│ └── deploy.yml
├── .gitignore
├── .php.ini.d
└── php.ini
├── .slugignore
├── CONTRIBUTING.md
├── LICENSE
├── Procfile
├── README.md
├── app.json
├── app.yaml
├── bin
└── console
├── composer.json
├── composer.lock
├── config.example.php
├── cron.yaml
├── crontab
├── dbcleanup.php
├── env_variables.example.yaml
├── fly.toml
├── logo.png
├── logo.xcf
├── nginx.inc.conf
├── php.ini
├── phpcs.xml.dist
├── public
├── .user.ini
└── index.php
├── src
├── BotCore.php
├── Command
│ ├── Admin
│ │ ├── CleansessionsCommand.php
│ │ └── StatsCommand.php
│ ├── System
│ │ ├── CallbackqueryCommand.php
│ │ ├── ChoseninlineresultCommand.php
│ │ ├── GenericmessageCommand.php
│ │ └── InlinequeryCommand.php
│ └── User
│ │ └── StartCommand.php
├── Entity
│ ├── Game.php
│ ├── Game
│ │ ├── Checkers.php
│ │ ├── Connectfour.php
│ │ ├── Elephantxo.php
│ │ ├── Poolcheckers.php
│ │ ├── Rockpaperscissors.php
│ │ ├── Rockpaperscissorslizardspock.php
│ │ ├── Russianroulette.php
│ │ ├── Tictacfour.php
│ │ └── Tictactoe.php
│ └── TempFile.php
├── Exception
│ ├── BotException.php
│ ├── StorageException.php
│ └── TelegramApiException.php
├── GameCore.php
├── Helper
│ ├── Language.php
│ └── Utilities.php
├── Storage
│ ├── Driver
│ │ ├── BotDB.php
│ │ ├── File.php
│ │ ├── Memcache.php
│ │ ├── MySQL.php
│ │ └── PostgreSQL.php
│ └── Storage.php
└── TelegramBot.php
├── start.sh
├── translations
└── messages.pot
└── worker.sh
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Ignore everything by default
2 | **
3 |
4 | # Uningnore only what we need
5 | !.php.ini.d/*
6 | !bin/*
7 | !public/.user.ini
8 | !public/index.php
9 | !src/**
10 | !translations/*.pot
11 | !translations/*.po
12 | !vendor/
13 | !config.php
14 | !composer.json
15 | !composer.lock
16 | !nginx.inc.conf
17 | !Procfile
18 | !start.sh
19 | !worker.sh
20 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = LF
5 | insert_final_newline = true
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 |
9 | [*.php]
10 | indent_style = space
11 | indent_size = 4
12 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Bot API token obtained from @BotFather
2 | BOT_TOKEN=""
3 |
4 | # Bot username (without '@' symbol)
5 | BOT_USERNAME=""
6 |
7 | # Webhook URL
8 | BOT_WEBHOOK=""
9 |
10 | # Secret variable used to secure the web hook
11 | BOT_SECRET=""
12 |
13 | # Optional: Admin's Telegram ID
14 | #BOT_ADMIN=""
15 |
16 | # Optional: Database connection DSN string
17 | #DATABASE_URL=""
18 |
19 | # Optional: php-telegram-bot's DB connection
20 | #DB_HOST=""
21 | #DB_USER=""
22 | #DB_PASS=""
23 | #DB_NAME=""
24 |
25 | # Optional: Bot data storage path (absolute)
26 | #DATA_PATH=""
27 |
28 | # Optional: Storage class to use
29 | #STORAGE_CLASS=""
30 |
31 | # Optional: Debug mode (will output a lot of data to logs/console)
32 | #DEBUG=true
33 |
34 | # Optional: Default language (translation file must exist - translations/messages.xx.po)
35 | #DEFAULT_LANGUAGE=en
36 |
--------------------------------------------------------------------------------
/.gcloudignore:
--------------------------------------------------------------------------------
1 | #!include:.gitignore
2 | .*
3 | /logo.*
4 |
5 | # Unignore some files
6 | !/config.php
7 |
8 | # Files created by Github actions
9 | gha-creds-*.json
10 | DOCKER_ENV
11 | docker_tag
12 | *.log
13 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: Check code
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'develop'
7 | pull_request:
8 | workflow_dispatch:
9 |
10 | jobs:
11 | check:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout the repository
15 | uses: actions/checkout@v3
16 |
17 | - name: Cache Composer dependencies
18 | uses: actions/cache@v2
19 | with:
20 | path: /tmp/composer-cache
21 | key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
22 |
23 | - name: Install PHP dependencies through Composer
24 | uses: php-actions/composer@v6
25 | with:
26 | version: 2
27 | php_version: 7.4
28 | args: --ignore-platform-reqs
29 |
30 | - name: Check code using PHP_CodeSniffer
31 | run: composer check-code
32 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'master'
7 | workflow_dispatch:
8 |
9 | jobs:
10 | deploy:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Invoke deployment hook
14 | uses: distributhor/workflow-webhook@v2
15 | env:
16 | webhook_url: ${{ secrets.WEBHOOK_URL }}
17 | webhook_secret: ${{ secrets.WEBHOOK_SECRET }}
18 | webhook_auth: ${{ secrets.WEBHOOK_AUTH }}
19 | data: ${{ secrets.WEBHOOK_DATA }}
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Composer
2 | /vendor/
3 |
4 | # Configuration files
5 | /config.php
6 | /.env
7 | /env_variables.yaml
8 |
9 | # Data directory
10 | /data/
11 |
12 | # Other
13 | /translations/*.mo
14 |
--------------------------------------------------------------------------------
/.php.ini.d/php.ini:
--------------------------------------------------------------------------------
1 | memory_limit = 16M
2 |
3 | extension=curl
4 | extension=gettext
5 | extension=intl
6 | extension=mbstring
7 | extension=pdo
8 |
--------------------------------------------------------------------------------
/.slugignore:
--------------------------------------------------------------------------------
1 | /logo.*
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing
2 | -------------
3 |
4 | First, [fork](https://github.com/jacklul/inlinegamesbot/fork) this repository, checkout it locally and then install project dependencies with Composer - `composer install`.
5 |
6 | Now make all your changes and test them.
7 |
8 | To test the changes you will obviously need a bot, assuming you already have one - put the token and bot username in `.env` file for local development.
9 |
10 | The easiest way to test your changes is to run the bot with `getUpdates` method - use `php bin/console loop` command.
11 |
12 | Make sure your code is following PSR-2 coding standard - run [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) with `composer check-code` command.
13 |
14 | Now when all seems to be good push you changes to a new branch in your fork and then [create a pull request](https://github.com/jacklul/inlinegamesbot/compare) explaining all the changes.
15 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | # For Heroku:
2 | web: vendor/bin/heroku-php-nginx -C nginx.inc.conf public/
3 |
4 | # For fly.io through Heroku builder (with worker instead of cron)
5 | #web: ./start.sh
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Inline Games [](https://github.com/jacklul/inlinegamesbot/blob/master/LICENSE) [](https://telegram.me/inlinegamesbot)
2 |
3 | A Telegram bot that provides real-time multiplayer games that can be played in any chat.
4 |
5 | You can see the bot in action by messaging [@inlinegamesbot](https://telegram.me/inlinegamesbot).
6 |
7 | The bot is currently hosted at [DOM Cloud](https://domcloud.co).
8 |
9 | #### Currently available games:
10 |
11 | - Tic-Tac-Toe
12 | - Tic-Tac-Four ([@DO97](https://github.com/DO97))
13 | - Elephant XO ([@DO97](https://github.com/DO97))
14 | - Connect Four
15 | - Rock-Paper-Scissors
16 | - Rock-Paper-Scissors-Lizard-Spock ([@DO97](https://github.com/DO97))
17 | - Russian Roulette
18 | - Checkers
19 | - Pool Checkers
20 |
21 | ## Deploying
22 |
23 | ### Heroku
24 |
25 |
26 | Instructions
27 |
28 | Use this button to begin deployment:
29 | [](https://heroku.com/deploy?template=https://github.com/jacklul/inlinegamesbot)
30 |
31 | Assuming everything was entered correctly your bot should be instantly working - if it's not you should try running `php bin/console post-install` inside the app.
32 |
33 | You will also want to add **Heroku Scheduler** addon and set up a hourly task to run the following command to clean up expired games from the database:
34 | - `php bin/console cron`
35 |
36 | _If this command times out too fast try using something like this instead: `php -d max_execution_time=2700 bin/console cron`_
37 |
38 |
39 |
40 | ### Google Cloud Platform
41 |
42 |
43 | Instructions
44 |
45 | - Install dependencies with `composer install`
46 | - Copy `env_variables.example.yaml` into `env_variables.yaml` and fill out the details
47 | - Run the deployment command: `gcloud app deploy --project YOUR-PROJECT-NAME-HERE app.yaml cron.yaml`
48 | - Visit `https://YOUR-PROJECT-NAME-HERE.appspot.com/admin?a=post-install` to perform post-install tasks
49 |
50 |
51 |
52 | ### Fly.io
53 |
54 |
55 | Instructions
56 |
57 | - `flyctl apps create`
58 | - `flyctl volumes create data --size=1`
59 | - `flyctl secrets set BOT_TOKEN=`
60 | - `flyctl secrets set BOT_USERNAME=`
61 | - `flyctl secrets set BOT_WEBHOOK=YOUR-APP-NAME.fly.dev`
62 | - `flyctl secrets set BOT_SECRET=`
63 | - If you want to use web+worker setup you have to replace `web:` line in `Procfile`
64 | - `flyctl deploy`
65 |
66 |
67 |
68 | ### DOM Cloud
69 |
70 |
71 | Instructions
72 |
73 | - Copy `.env.example` into `.env` and fill out the details
74 | - Upload `.env` and `crontab` to `/home//config` directory on the FTP
75 | - `crontab` will require modifications - use full paths to the script - e.g.: `/home//public_html/bin/console`
76 | - Run this deployment task:
77 | ```
78 | source: 'https://github.com/jacklul/inlinegamesbot'
79 | commands:
80 | - 'test -f ../config/.env && cp -f ../config/.env .'
81 | - 'test -f ../config/config.php && cp -f ../config/config.php . || exit 0'
82 | - 'composer install --no-dev --optimize-autoloader --ignore-platform-reqs'
83 | - 'php bin/console install'
84 | - 'php bin/console set'
85 | - 'test -f ../config/crontab && cat ../config/crontab | crontab - || exit 0'
86 | features:
87 | - ssl
88 | - 'php 7.4'
89 | ```
90 |
91 |
92 |
93 | ## Note on translations
94 |
95 | Translations support is implemented but it is not used mainly because translated text would be displayed to both players - this could be problematic in "gaming" groups - people setting language that other player can't understand!
96 |
97 | ## Contributing
98 |
99 | See [CONTRIBUTING](CONTRIBUTING.md) for more information.
100 |
101 | ## License
102 |
103 | See [LICENSE](LICENSE).
104 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Inline Games",
3 | "description": "A Telegram bot providing games that can be played in any chat.",
4 | "repository": "https://github.com/jacklul/inlinegamesbot",
5 | "logo": "https://raw.githubusercontent.com/jacklul/inlinegamesbot/master/logo.png",
6 | "stack": "heroku-20",
7 | "buildpacks": [
8 | {
9 | "url": "https://github.com/heroku/heroku-buildpack-php"
10 | }
11 | ],
12 | "addons": [
13 | "heroku-postgresql"
14 | ],
15 | "env": {
16 | "BOT_TOKEN": {
17 | "description": "Bot API token obtained from @BotFather",
18 | "value": ""
19 | },
20 | "BOT_USERNAME": {
21 | "description": "Bot username (without '@' symbol)",
22 | "value": ""
23 | },
24 | "BOT_WEBHOOK": {
25 | "description": "Webhook URL (YOURAPPNAME must match app name entered earlier)",
26 | "value": "https://YOURAPPNAME.herokuapp.com"
27 | },
28 | "BOT_SECRET": {
29 | "description": "Secret variable used to secure the web hook",
30 | "generator": "secret"
31 | },
32 | "BOT_ADMIN": {
33 | "description": "Admin's Telegram ID",
34 | "value": "",
35 | "required": false
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: php74
2 |
3 | entrypoint: serve --workers=15 public/index.php
4 |
5 | automatic_scaling:
6 | max_instances: 1
7 | max_idle_instances: 1
8 | max_concurrent_requests: 15
9 |
10 | # You might want to create an override config.php and set webhook.max_connections to match max_concurrent_requests & --workers value
11 |
12 | default_expiration: "1d"
13 |
14 | handlers:
15 | - url: /admin.*
16 | script: auto
17 | login: admin
18 | secure: always
19 |
20 | - url: /.*
21 | script: auto
22 | secure: always
23 |
24 | includes:
25 | - env_variables.yaml
26 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | use Bot\BotCore;
13 |
14 | /**
15 | * Composer autoloader
16 | */
17 | require_once __DIR__ . '/../vendor/autoload.php';
18 |
19 | /**
20 | * Run console interface
21 | */
22 | try {
23 | $app = new BotCore();
24 | $app->run();
25 | } catch (\Throwable $e) {
26 | print $e;
27 | }
28 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jacklul/inlinegamesbot",
3 | "type": "project",
4 | "description": "A Telegram bot providing games that can be played in any chat.",
5 | "license": "AGPL-3.0",
6 | "keywords": [
7 | "telegram",
8 | "bot",
9 | "games"
10 | ],
11 | "authors": [
12 | {
13 | "name": "Jack'lul",
14 | "email": "jacklulcat@gmail.com",
15 | "homepage": "https://jacklul.github.io",
16 | "role": "Developer"
17 | }
18 | ],
19 | "require": {
20 | "php": "^7.4",
21 | "ext-curl": "*",
22 | "ext-gettext": "*",
23 | "ext-intl": "*",
24 | "ext-json": "*",
25 | "ext-mbstring": "*",
26 | "ext-pdo": "*",
27 | "gettext/gettext": "^4.3",
28 | "jacklul/monolog-telegram": "^2.0",
29 | "longman/telegram-bot": "^0.78.0",
30 | "memcachier/php-memcache-sasl": "^1.0",
31 | "spatie/emoji": "^2.0",
32 | "vlucas/phpdotenv": "^5.0"
33 | },
34 | "require-dev": {
35 | "squizlabs/php_codesniffer": "^3.2"
36 | },
37 | "autoload": {
38 | "psr-4": {
39 | "Bot\\": "src/"
40 | }
41 | },
42 | "config": {
43 | "sort-packages": true
44 | },
45 | "scripts": {
46 | "check-code": [
47 | "\"vendor/bin/phpcs\" -snp --standard=PSR2 --encoding=utf-8 --report-width=150 src/ bin/ public/"
48 | ],
49 | "post-install-cmd": [
50 | "@php bin/console post-install"
51 | ]
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/config.example.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | /**
12 | * Custom configuration
13 | */
14 |
15 | return [
16 | 'commands' => [
17 | 'configs' => [
18 | 'cleansessions' => [
19 | 'clean_interval' => 3600,
20 | ],
21 | ],
22 | ],
23 | 'webhook' => [
24 | 'max_connections' => 5,
25 | ],
26 | 'logging' => [
27 | 'error' => DATA_PATH . '/logs/Error.log',
28 | ],
29 | ];
30 |
--------------------------------------------------------------------------------
/cron.yaml:
--------------------------------------------------------------------------------
1 | cron:
2 | - description: Run cleanup task
3 | url: /admin?a=cron
4 | schedule: every hour
5 |
--------------------------------------------------------------------------------
/crontab:
--------------------------------------------------------------------------------
1 | # Clean up older games hourly
2 | 0 * * * * php -d max_execution_time=2700 bin/console cron
3 |
4 | # Alternatively: just remove the records from the database (faster and lighter)
5 | #0 * * * * php dbcleanup.php
6 |
--------------------------------------------------------------------------------
/dbcleanup.php:
--------------------------------------------------------------------------------
1 | load();
8 | }
9 |
10 | $interval = 86400;
11 |
12 | if (file_exists(__DIR__ . '/config.php')) {
13 | $config = include_once __DIR__ . '/config.php';
14 | $interval = $config['commands']['configs']['cleansessions']['clean_interval'] ?? $interval;
15 | }
16 |
17 | $dsn = parse_url(getenv('DATABASE_URL'));
18 |
19 | if (isset($dsn['host'])) {
20 | $pdo = new PDO('mysql:' . 'host=' . $dsn['host'] . ';port=' . $dsn['port'] . ';dbname=' . ltrim($dsn['path'], '/'), $dsn['user'], $dsn['pass']);
21 | $query = $pdo->query('DELETE FROM game WHERE updated_at < NOW() - INTERVAL ' . $interval . ' SECOND;');
22 | echo $query->rowCount() . PHP_EOL;
23 | } else {
24 | die('DATABASE_URL is not set or is not a valid DSN string!' . PHP_EOL);
25 | }
26 |
--------------------------------------------------------------------------------
/env_variables.example.yaml:
--------------------------------------------------------------------------------
1 | env_variables:
2 | DATABASE_URL: 'postgres://user:password@server:5432/database'
3 | BOT_TOKEN: '12345678:vS6dfn78gnfsb6Ssdfuy7d6s5d'
4 | BOT_USERNAME: 'yourawesomebot'
5 | BOT_WEBHOOK: 'https://APPNAME.appspot.com'
6 | BOT_SECRET: ''
7 | #BOT_ADMIN: '123456789'
8 |
--------------------------------------------------------------------------------
/fly.toml:
--------------------------------------------------------------------------------
1 | app = "inlinegamesbot"
2 | kill_signal = "SIGINT"
3 | kill_timeout = 5
4 | processes = []
5 |
6 | [build]
7 | builder = "heroku/buildpacks:20"
8 | #builder = "paketobuildpacks/builder:full"
9 | #buildpacks = ["gcr.io/paketo-buildpacks/php"]
10 | #[build.args]
11 | # BP_PHP_VERSION = "8.0"
12 |
13 | [deploy]
14 | release_command = "php bin/console post-install"
15 |
16 | [env]
17 | STORAGE_CLASS="Bot\\Storage\\Driver\\File"
18 | DATA_PATH="/data"
19 | LOG_NO_DATE_PREFIX=true
20 | WORKER_INTERVAL=60
21 | WORKER_MEMORY_LIMIT="64M"
22 | LIST_MEMORY_LIMIT="64M"
23 | # The following are required by Heroku builder
24 | PORT=8080
25 | DOCUMENT_ROOT="public/"
26 | WEB_CONCURRENCY=10
27 |
28 | [mounts]
29 | source="data"
30 | destination="/data"
31 |
32 | #[processes]
33 | # app = "php -S 0.0.0.0:8080 -t /workspace/public"
34 | # worker_app = "php bin/console worker"
35 |
36 | [experimental]
37 | allowed_public_ports = []
38 | auto_rollback = true
39 |
40 | [[services]]
41 | http_checks = []
42 | internal_port = 8080
43 | processes = ["app"]
44 | protocol = "tcp"
45 | script_checks = []
46 | [services.concurrency]
47 | hard_limit = 25
48 | soft_limit = 20
49 | type = "connections"
50 |
51 | [[services.ports]]
52 | force_https = true
53 | handlers = ["http"]
54 | port = 80
55 |
56 | [[services.ports]]
57 | handlers = ["tls", "http"]
58 | port = 443
59 |
60 | [[services.tcp_checks]]
61 | grace_period = "1s"
62 | interval = "15s"
63 | restart_limit = 0
64 | timeout = "2s"
65 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jacklul/inlinegamesbot/e815617fcd3e45898c115127a6eac1f4fca34aa0/logo.png
--------------------------------------------------------------------------------
/logo.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jacklul/inlinegamesbot/e815617fcd3e45898c115127a6eac1f4fca34aa0/logo.xcf
--------------------------------------------------------------------------------
/nginx.inc.conf:
--------------------------------------------------------------------------------
1 | location / {
2 | try_files = /index.php?$args;
3 | }
4 |
--------------------------------------------------------------------------------
/php.ini:
--------------------------------------------------------------------------------
1 | memory_limit = 16M
2 |
--------------------------------------------------------------------------------
/phpcs.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 | PSR2 without line length warning
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/public/.user.ini:
--------------------------------------------------------------------------------
1 | memory_limit = 16M
2 |
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | use Bot\BotCore;
12 |
13 | /**
14 | * Handle webhook request only when it's a POST request
15 | */
16 | if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
17 | require_once __DIR__ . ' /../vendor/autoload.php';
18 |
19 | try {
20 | $app = new BotCore();
21 | $app->run(true);
22 | } catch (\Throwable $e) {
23 | // Prevent Telegram from retrying
24 | }
25 | } elseif (isset($_SERVER['GAE_VERSION']) && $_SERVER['PATH_INFO'] === '/admin') {
26 | require_once __DIR__.'/../vendor/autoload.php';
27 |
28 | $app = new BotCore();
29 | $app->run();
30 | } else {
31 | header("Location: https://github.com/jacklul/inlinegamesbot"); // Redirect non-POST requests to Github repository
32 | }
33 |
--------------------------------------------------------------------------------
/src/BotCore.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot;
12 |
13 | use Bot\Entity\TempFile;
14 | use Bot\Exception\BotException;
15 | use Bot\Exception\StorageException;
16 | use Bot\Helper\Utilities;
17 | use Bot\Storage\Driver\File;
18 | use Bot\Storage\Storage;
19 | use Dotenv\Dotenv;
20 | use Exception;
21 | use Gettext\Translator;
22 | use GuzzleHttp\Client;
23 | use InvalidArgumentException;
24 | use Longman\TelegramBot\Entities\ServerResponse;
25 | use Longman\TelegramBot\Exception\TelegramException;
26 | use Longman\TelegramBot\Exception\TelegramLogException;
27 | use Longman\TelegramBot\Request;
28 | use Longman\TelegramBot\TelegramLog;
29 | use Monolog\Formatter\LineFormatter;
30 | use Monolog\Handler\DeduplicationHandler;
31 | use Monolog\Handler\FingersCrossedHandler;
32 | use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy;
33 | use Monolog\Handler\StreamHandler;
34 | use Monolog\Logger;
35 | use Throwable;
36 | use jacklul\MonologTelegramHandler\TelegramFormatter;
37 | use jacklul\MonologTelegramHandler\TelegramHandler;
38 |
39 | define("ROOT_PATH", realpath(dirname(__DIR__)));
40 | define("APP_PATH", ROOT_PATH . '/bot');
41 | define("SRC_PATH", ROOT_PATH . '/src');
42 |
43 | /**
44 | * This is the master loader class, contains console commands and essential code for bootstrapping the bot
45 | */
46 | class BotCore
47 | {
48 | /**
49 | * Commands
50 | *
51 | * @var array
52 | */
53 | private static $commands = [
54 | 'help' => [
55 | 'function' => 'showHelp',
56 | 'description' => 'Shows this help message',
57 | ],
58 | 'set' => [
59 | 'function' => 'setWebhook',
60 | 'description' => 'Set the webhook',
61 | 'require_telegram' => true,
62 | ],
63 | 'unset' => [
64 | 'function' => 'deleteWebhook',
65 | 'description' => 'Delete the webhook',
66 | 'require_telegram' => true,
67 | ],
68 | 'info' => [
69 | 'function' => 'webhookInfo',
70 | 'description' => 'Print webhookInfo request result',
71 | 'require_telegram' => true,
72 | ],
73 | 'install' => [
74 | 'function' => 'installDb',
75 | 'description' => 'Execute database creation script',
76 | ],
77 | 'handle' => [
78 | 'function' => 'handleWebhook',
79 | 'description' => 'Handle incoming webhook update',
80 | 'require_telegram' => true,
81 | ],
82 | 'run' => [
83 | 'function' => 'handleLongPolling',
84 | 'description' => 'Run the bot using getUpdates in a loop',
85 | 'require_telegram' => true,
86 | ],
87 | 'cron' => [
88 | 'function' => 'handleCron',
89 | 'description' => 'Run scheduled commands once',
90 | 'require_telegram' => true,
91 | ],
92 | 'worker' => [
93 | 'function' => 'handleWorker',
94 | 'description' => 'Run scheduled commands every minute',
95 | 'require_telegram' => true,
96 | ],
97 | 'post-install' => [
98 | 'function' => 'postComposerInstall',
99 | 'description' => 'Execute commands after composer install runs',
100 | 'hidden' => true,
101 | ],
102 | ];
103 |
104 | /**
105 | * Config array
106 | *
107 | * @var array
108 | */
109 | private $config = [];
110 |
111 | /**
112 | * TelegramBot object
113 | *
114 | * @var TelegramBot
115 | */
116 | private $telegram;
117 |
118 | /**
119 | * Bot constructor
120 | *
121 | * @throws BotException
122 | */
123 | public function __construct()
124 | {
125 | if (!defined('ROOT_PATH')) {
126 | throw new BotException('Root path not defined!');
127 | }
128 |
129 | // Load environment variables from file if it exists
130 | if (class_exists(Dotenv::class) && file_exists(ROOT_PATH . '/.env')) {
131 | $dotenv = Dotenv::createUnsafeImmutable(ROOT_PATH);
132 | $dotenv->load();
133 | }
134 |
135 | // Debug mode
136 | if (getenv('DEBUG')) {
137 | Utilities::setDebugPrint();
138 |
139 | /*$logger = new Logger('console');
140 | $logger->pushHandler((new ErrorLogHandler())->setFormatter(new LineFormatter('[%datetime%] %message% %context% %extra%', 'Y-m-d H:i:s', false, true)));
141 |
142 | Utilities::setDebugPrintLogger($logger);*/
143 | }
144 |
145 | // Do not display errors by default
146 | ini_set('display_errors', (getenv('DEBUG') ? 1 : 0));
147 |
148 | // Set timezone
149 | date_default_timezone_set(getenv('TIMEZONE') ?: 'UTC');
150 |
151 | // Set custom data path if variable exists, otherwise do not use anything
152 | if (!empty($data_path = getenv('DATA_PATH'))) {
153 | define('DATA_PATH', str_replace('"', '', str_replace('./', ROOT_PATH . '/', $data_path)));
154 | }
155 |
156 | // gettext '__()' function must be initialized as all public messages are using it
157 | (new Translator())->register();
158 |
159 | // Load DEFAULT config
160 | $this->loadDefaultConfig();
161 |
162 | // Merge default config with user config
163 | $config_file = ROOT_PATH . '/config.php';
164 | if (file_exists($config_file)) {
165 | /** @noinspection PhpIncludeInspection */
166 | $config = include $config_file;
167 |
168 | if (is_array($config)) {
169 | $this->config = array_replace_recursive($this->config, $config);
170 | }
171 | }
172 | }
173 |
174 | /**
175 | * Load default config values
176 | */
177 | private function loadDefaultConfig(): void
178 | {
179 | $this->config = [
180 | 'api_key' => getenv('BOT_TOKEN'),
181 | 'bot_username' => getenv('BOT_USERNAME'),
182 | 'admins' => [(int)getenv('BOT_ADMIN') ?: 0],
183 | 'commands' => [
184 | 'paths' => [
185 | SRC_PATH . '/Command',
186 | ],
187 | 'configs' => [
188 | 'cleansessions' => [
189 | 'clean_interval' => 86400,
190 | ],
191 | ],
192 | ],
193 | 'webhook' => [
194 | 'url' => getenv('BOT_WEBHOOK'),
195 | 'max_connections' => 100,
196 | 'allowed_updates' => [
197 | 'message',
198 | 'inline_query',
199 | 'chosen_inline_result',
200 | 'callback_query',
201 | ],
202 | 'secret_token' => getenv('BOT_SECRET'),
203 | ],
204 | 'mysql' => [
205 | 'host' => getenv('DB_HOST'),
206 | 'user' => getenv('DB_USER'),
207 | 'password' => getenv('DB_PASS'),
208 | 'database' => getenv('DB_NAME'),
209 | ],
210 | 'cron' => [
211 | 'groups' => [
212 | 'default' => [
213 | '/cleansessions',
214 | ],
215 | ],
216 | ],
217 | ];
218 | }
219 |
220 | /**
221 | * Run the bot
222 | *
223 | * @param bool $webhook
224 | *
225 | * @throws Throwable
226 | */
227 | public function run(bool $webhook = false): void
228 | {
229 | if (is_bool($webhook) && $webhook === true) {
230 | $arg = 'handle'; // from webspace allow only handling webhook
231 | } elseif (isset($_SERVER['argv'][1])) {
232 | $arg = strtolower(trim($_SERVER['argv'][1]));
233 | } elseif (isset($_GET['a'])) {
234 | $arg = strtolower(trim($_GET['a']));
235 | }
236 |
237 | try {
238 | if (!empty($arg) && isset(self::$commands[$arg]['function'])) {
239 | if (!$this->telegram instanceof TelegramBot && isset(self::$commands[$arg]['require_telegram']) && self::$commands[$arg]['require_telegram'] === true) {
240 | $this->initialize();
241 | }
242 |
243 | $function = self::$commands[$arg]['function'];
244 | $this->$function();
245 | } else {
246 | $this->showHelp();
247 |
248 | if (!empty($arg)) {
249 | print PHP_EOL . 'Invalid parameter specified!' . PHP_EOL;
250 | } else {
251 | print PHP_EOL . 'No parameter specified!' . PHP_EOL;
252 | }
253 | }
254 | } catch (Throwable $e) {
255 | $ignored_errors = getenv('IGNORED_ERRORS');
256 |
257 | if (!empty($ignored_errors) && !Utilities::isDebugPrintEnabled()) {
258 | $ignored_errors = explode(';', $ignored_errors);
259 | $ignored_errors = array_map('trim', $ignored_errors);
260 |
261 | foreach ($ignored_errors as $ignored_error) {
262 | if (strpos($e->getMessage(), $ignored_error) !== false) {
263 | return;
264 | }
265 | }
266 | }
267 |
268 | TelegramLog::error($e);
269 | throw $e;
270 | }
271 | }
272 |
273 | /**
274 | * Initialize Telegram object
275 | *
276 | * @throws TelegramException
277 | * @throws TelegramLogException
278 | * @throws InvalidArgumentException
279 | * @throws Exception
280 | */
281 | private function initialize(): void
282 | {
283 | if ($this->telegram instanceof TelegramBot) {
284 | return;
285 | }
286 |
287 | Utilities::debugPrint('DEBUG MODE ACTIVE');
288 |
289 | $this->telegram = new TelegramBot($this->config['api_key'], $this->config['bot_username']);
290 | $monolog = new Logger($this->config['bot_username']);
291 |
292 | if (isset($this->config['logging']['error'])) {
293 | $monolog->pushHandler((new StreamHandler($this->config['logging']['error'], Logger::ERROR))->setFormatter(new LineFormatter(null, null, true)));
294 | }
295 |
296 | if (isset($this->config['logging']['debug'])) {
297 | $monolog->pushHandler((new StreamHandler($this->config['logging']['debug'], Logger::ERROR))->setFormatter(new LineFormatter(null, null, true)));
298 | }
299 |
300 | if (isset($this->config['logging']['update'])) {
301 | $update_logger = new Logger(
302 | $this->config['bot_username'] . '_update',
303 | [
304 | (new StreamHandler($this->config['logging']['update'], Logger::INFO))->setFormatter(new LineFormatter('%message%' . PHP_EOL)),
305 | ]
306 | );
307 | }
308 |
309 | if (file_exists(ROOT_PATH . '/vendor/bin/heroku-php-nginx')) {
310 | $monolog->pushHandler(
311 | new FingersCrossedHandler(
312 | new StreamHandler('php://stderr'),
313 | new ErrorLevelActivationStrategy(Logger::WARNING)
314 | )
315 | );
316 | }
317 |
318 | if (isset($this->config['admins']) && !empty($this->config['admins'][0])) {
319 | $this->telegram->enableAdmins($this->config['admins']);
320 |
321 | $handler = new TelegramHandler($this->config['api_key'], (int)$this->config['admins'][0], Logger::ERROR);
322 | $handler->setFormatter(new TelegramFormatter());
323 |
324 | $handler = new DeduplicationHandler($handler);
325 | $handler->setLevel(Utilities::isDebugPrintEnabled() ? Logger::DEBUG : Logger::ERROR);
326 |
327 | $monolog->pushHandler($handler);
328 | }
329 |
330 | if (!empty($monolog->getHandlers())) {
331 | TelegramLog::initialize($monolog, $update_logger ?? null);
332 | }
333 |
334 | if (isset($this->config['custom_http_client'])) {
335 | Request::setClient(new Client($this->config['custom_http_client']));
336 | }
337 |
338 | if (isset($this->config['commands']['paths'])) {
339 | $this->telegram->addCommandsPaths($this->config['commands']['paths']);
340 | }
341 |
342 | if (isset($this->config['mysql']['host']) && !empty($this->config['mysql']['host'])) {
343 | $this->telegram->enableMySql($this->config['mysql']);
344 | }
345 |
346 | if (isset($this->config['paths']['download'])) {
347 | $this->telegram->setDownloadPath($this->config['paths']['download']);
348 | }
349 |
350 | if (isset($this->config['paths']['upload'])) {
351 | $this->telegram->setDownloadPath($this->config['paths']['upload']);
352 | }
353 |
354 | if (isset($this->config['commands']['configs'])) {
355 | foreach ($this->config['commands']['configs'] as $command => $config) {
356 | $this->telegram->setCommandConfig($command, $config);
357 | }
358 | }
359 |
360 | if (!empty($this->config['limiter']['enabled'])) {
361 | if (!empty($this->config['limiter']['options'])) {
362 | $this->telegram->enableLimiter($this->config['limiter']['options']);
363 | } else {
364 | $this->telegram->enableLimiter();
365 | }
366 | }
367 | }
368 |
369 | /**
370 | * Display usage help
371 | */
372 | private function showHelp(): void
373 | {
374 | if (PHP_SAPI !== 'cli') {
375 | print '
';
376 | }
377 |
378 | print 'Bot Console' . ($this->config['bot_username'] ? ' (@' . $this->config['bot_username'] . ')' : '') . PHP_EOL . PHP_EOL;
379 | print 'Available commands:' . PHP_EOL;
380 |
381 | $commands = '';
382 | foreach (self::$commands as $command => $data) {
383 | if (isset($data['hidden']) && $data['hidden'] === true) {
384 | continue;
385 | }
386 |
387 | if (!empty($commands)) {
388 | $commands .= PHP_EOL;
389 | }
390 |
391 | if (!isset($data['description'])) {
392 | $data['description'] = 'No description available';
393 | }
394 |
395 | $commands .= ' ' . $command . str_repeat(' ', 10 - strlen($command)) . '- ' . trim($data['description']);
396 | }
397 |
398 | print $commands . PHP_EOL;
399 |
400 | if (PHP_SAPI !== 'cli') {
401 | print '
';
402 | }
403 | }
404 |
405 | /**
406 | * Handle webhook method request
407 | *
408 | * @noinspection PhpUnusedPrivateMethodInspection
409 | *
410 | * @throws TelegramException
411 | */
412 | private function handleWebhook(): void
413 | {
414 | if ($this->validateRequest()) {
415 | try {
416 | $this->telegram->handle();
417 | } catch (TelegramException $e) {
418 | if (strpos($e->getMessage(), 'Telegram returned an invalid response') === false) {
419 | throw $e;
420 | }
421 | }
422 | }
423 | }
424 |
425 | /**
426 | * Validate request to check if it comes from the Telegram servers
427 | * and also does it contain a secret string
428 | *
429 | * @return bool
430 | */
431 | private function validateRequest(): bool
432 | {
433 | if (PHP_SAPI !== 'cli') {
434 | $header_secret = null;
435 | foreach (getallheaders() as $name => $value) {
436 | if (stripos($name, 'X-Telegram-Bot-Api-Secret-Token') !== false) {
437 | $header_secret = $value;
438 | break;
439 | }
440 | }
441 |
442 | $secret = getenv('BOT_SECRET');
443 |
444 | if (!isset($secret, $header_secret) || $secret !== $header_secret) {
445 | return false;
446 | }
447 | }
448 |
449 | return true;
450 | }
451 |
452 | /**
453 | * Set webhook
454 | *
455 | * @noinspection PhpUnusedPrivateMethodInspection
456 | *
457 | * @throws BotException
458 | * @throws TelegramException
459 | */
460 | private function setWebhook(): void
461 | {
462 | if (empty($this->config['webhook']['url'])) {
463 | throw new BotException('Webhook URL is empty!');
464 | }
465 |
466 | if (!isset($this->config['webhook']['secret_token'])) {
467 | throw new BotException('Secret is empty!');
468 | }
469 |
470 | $result = $this->telegram->setWebhook($this->config['webhook']['url'], $this->config['webhook']);
471 |
472 | if ($result->isOk()) {
473 | print 'Webhook URL: ' . $this->config['webhook']['url'] . PHP_EOL;
474 | print $result->getDescription() . PHP_EOL;
475 | } else {
476 | print 'Request failed: ' . $result->getDescription() . PHP_EOL;
477 | }
478 | }
479 |
480 | /**
481 | * Delete webhook
482 | *
483 | * @noinspection PhpUnusedPrivateMethodInspection
484 | *
485 | * @throws TelegramException
486 | */
487 | private function deleteWebhook(): void
488 | {
489 | $result = $this->telegram->deleteWebhook();
490 |
491 | if ($result->isOk()) {
492 | print $result->getDescription() . PHP_EOL;
493 | } else {
494 | print 'Request failed: ' . $result->getDescription();
495 | }
496 | }
497 |
498 | /**
499 | * Get webhook info
500 | *
501 | * @noinspection PhpUnusedPrivateMethodInspection
502 | */
503 | private function webhookInfo(): void
504 | {
505 | $result = Request::getWebhookInfo();
506 |
507 | if ($result->isOk()) {
508 | if (PHP_SAPI !== 'cli') {
509 | print '' . print_r($result->getResult(), true) . '
' . PHP_EOL;
510 | } else {
511 | print print_r($result->getResult(), true) . PHP_EOL;
512 | }
513 | } else {
514 | print 'Request failed: ' . $result->getDescription() . PHP_EOL;
515 | }
516 | }
517 |
518 | /**
519 | * Handle getUpdates method
520 | *
521 | * @noinspection PhpUnusedPrivateMethodInspection
522 | *
523 | * @throws TelegramException
524 | */
525 | private function handleLongPolling(): void
526 | {
527 | if (PHP_SAPI !== 'cli') {
528 | print 'Cannot run this from the webspace!' . PHP_EOL;
529 |
530 | return;
531 | }
532 |
533 | if (!isset($this->config['mysql']['host']) || empty($this->config['mysql']['host'])) {
534 | $this->telegram->useGetUpdatesWithoutDatabase();
535 | }
536 |
537 | $dateTimePrefix = static function () {
538 | if (getenv('LOG_NO_DATE_PREFIX')) {
539 | return;
540 | }
541 |
542 | return '[' . date('Y-m-d H:i:s') . '] ';
543 | };
544 |
545 | print $dateTimePrefix() . 'Running with getUpdates method...' . PHP_EOL;
546 | while (true) {
547 | set_time_limit(0);
548 |
549 | try {
550 | $server_response = $this->telegram->handleGetUpdates();
551 | } catch (TelegramException $e) {
552 | if (strpos($e->getMessage(), 'Telegram returned an invalid response') !== false) {
553 | $server_response = new ServerResponse(['ok' => false, 'description' => 'Telegram returned an invalid response'], '');
554 | } else {
555 | throw $e;
556 | }
557 | }
558 |
559 | if ($server_response->isOk()) {
560 | $update_count = count($server_response->getResult());
561 |
562 | if ($update_count > 0) {
563 | print $dateTimePrefix() . 'Processed ' . $update_count . ' updates!' . ' (peak memory usage: ' . Utilities::formatBytes(memory_get_peak_usage()) . ')' . PHP_EOL;
564 | }
565 | } else {
566 | print $dateTimePrefix() . 'Failed to process updates!' . PHP_EOL;
567 | print 'Error: ' . $server_response->getDescription() . PHP_EOL;
568 | }
569 |
570 | if (function_exists('gc_collect_cycles')) {
571 | gc_collect_cycles();
572 | }
573 |
574 | usleep(333333);
575 | }
576 | }
577 |
578 | /**
579 | * Handle worker process
580 | *
581 | * @noinspection PhpUnusedPrivateMethodInspection
582 | *
583 | * @throws BotException
584 | * @throws TelegramException
585 | */
586 | private function handleWorker(): void
587 | {
588 | if (PHP_SAPI !== 'cli') {
589 | print 'Cannot run this from the webspace!' . PHP_EOL;
590 |
591 | return;
592 | }
593 |
594 | $dateTimePrefix = static function () {
595 | if (getenv('LOG_NO_DATE_PREFIX')) {
596 | return;
597 | }
598 |
599 | return '[' . date('Y-m-d H:i:s') . '] ';
600 | };
601 |
602 | print $dateTimePrefix() . 'Initializing worker...' . PHP_EOL;
603 |
604 | $interval = 60;
605 | if (!empty($interval_user = getenv('WORKER_INTERVAL'))) {
606 | $interval = $interval_user;
607 | }
608 |
609 | if (!empty($memory_limit = getenv('WORKER_MEMORY_LIMIT'))) {
610 | ini_set('memory_limit', $memory_limit);
611 | }
612 |
613 | define('IN_WORKER', time());
614 |
615 | $sleep_time = ceil($interval / 3);
616 | $last_run = time();
617 |
618 | while (true) {
619 | set_time_limit(0);
620 |
621 | if (time() < $last_run + $interval) {
622 | $next_run = $last_run + $interval - time();
623 | $sleep_time_this = $sleep_time;
624 |
625 | if ($next_run < $sleep_time_this) {
626 | $sleep_time_this = $next_run;
627 | }
628 |
629 | print $dateTimePrefix() . 'Next scheduled run in ' . $next_run . ' seconds, sleeping for ' . $sleep_time_this . ' seconds...' . PHP_EOL;
630 |
631 | if (function_exists('gc_collect_cycles')) {
632 | gc_collect_cycles();
633 | }
634 |
635 | sleep($sleep_time_this);
636 |
637 | continue;
638 | }
639 |
640 | print $dateTimePrefix() . 'Running scheduled commands...' . PHP_EOL;
641 |
642 | try {
643 | $this->handleCron();
644 | } catch (\Exception $e) {
645 | print $dateTimePrefix() . 'Caught exception: ' . $e->getMessage() . PHP_EOL;
646 | }
647 |
648 | $last_run = time();
649 | }
650 | }
651 |
652 | /**
653 | * Run scheduled commands
654 | *
655 | * @throws BotException
656 | * @throws TelegramException
657 | */
658 | private function handleCron(): void
659 | {
660 | $commands = [];
661 |
662 | $cronlock = new TempFile('cron');
663 | if ($cronlock->getFile() === null) {
664 | exit("Couldn't obtain lockfile!" . PHP_EOL);
665 | }
666 |
667 | $file = $cronlock->getFile()->getPathname();
668 |
669 | $fh = fopen($file, 'wb');
670 | if (!$fh || !flock($fh, LOCK_EX | LOCK_NB)) {
671 | if (PHP_SAPI === 'cli') {
672 | print "There is already another cron task running in the background!" . PHP_EOL;
673 | }
674 |
675 | exit;
676 | }
677 |
678 | if (!empty($this->config['cron']['groups'])) {
679 | foreach ($this->config['cron']['groups'] as $command_group => $commands_in_group) {
680 | foreach ($commands_in_group as $command) {
681 | $commands[] = $command;
682 | }
683 | }
684 | }
685 |
686 | if (empty($commands)) {
687 | throw new BotException('No commands to run!');
688 | }
689 |
690 | $this->telegram->runCommands($commands);
691 |
692 | if (flock($fh, LOCK_UN)) {
693 | fclose($fh);
694 | unlink($file);
695 | }
696 | }
697 |
698 | /**
699 | * Handle installing database structure
700 | *
701 | * @noinspection PhpUnusedPrivateMethodInspection
702 | *
703 | * @throws StorageException
704 | */
705 | private function installDb(): void
706 | {
707 | /** @var File $storage_class */
708 | $storage_class = Storage::getClass();
709 | $storage_class_name = explode('\\', (string) $storage_class);
710 |
711 | print 'Installing storage structure (' . end($storage_class_name) . ')...' . PHP_EOL;
712 |
713 | if ($storage_class::createStructure()) {
714 | print 'Ok!' . PHP_EOL;
715 | } else {
716 | print 'Error!' . PHP_EOL;
717 | }
718 | }
719 |
720 | /**
721 | * Run some tasks after composer install
722 | *
723 | * @return void
724 | */
725 | private function postComposerInstall(): void
726 | {
727 | if (!empty(getenv('STORAGE_CLASS')) || !empty(getenv('DATABASE_URL')) || !empty(getenv('DATA_PATH'))) {
728 | $this->installDb();
729 | }
730 |
731 | if (!empty($this->config['api_key']) && !empty($this->config['bot_username']) && !empty($this->config['webhook']['url']) && !empty($this->config['webhook']['secret_token'])) {
732 | if (!$this->telegram instanceof TelegramBot) {
733 | $this->initialize();
734 | }
735 |
736 | $this->setWebhook();
737 | }
738 | }
739 | }
740 |
--------------------------------------------------------------------------------
/src/Command/Admin/CleansessionsCommand.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Command\Admin;
12 |
13 | use Bot\Exception\BotException;
14 | use Bot\Exception\StorageException;
15 | use Bot\GameCore;
16 | use Bot\Helper\Utilities;
17 | use Bot\Storage\Driver\File;
18 | use Bot\Storage\Storage;
19 | use Longman\TelegramBot\Commands\AdminCommand;
20 | use Longman\TelegramBot\Entities\InlineKeyboard;
21 | use Longman\TelegramBot\Entities\InlineKeyboardButton;
22 | use Longman\TelegramBot\Entities\ServerResponse;
23 | use Longman\TelegramBot\Exception\TelegramException;
24 | use Longman\TelegramBot\Request;
25 |
26 | /**
27 | * This command will clean games that were inactive for a longer period and
28 | * edit their message to indicate that they expired
29 | */
30 | class CleansessionsCommand extends AdminCommand
31 | {
32 | protected $name = 'cleansessions';
33 | protected $description = 'Clean old game messages and set them as empty';
34 | protected $usage = '/cleansessions';
35 |
36 | /**
37 | * @return ServerResponse
38 | *
39 | * @throws BotException
40 | * @throws StorageException
41 | * @throws TelegramException
42 | */
43 | public function execute(): ServerResponse
44 | {
45 | $message = $this->getMessage();
46 | $edited_message = $this->getUpdate()->getEditedMessage();
47 |
48 | if ($edited_message) {
49 | $message = $edited_message;
50 | }
51 |
52 | $chat_id = $message->getFrom()->getId();
53 | $bot_id = $this->getTelegram()->getBotId();
54 | $text = trim($message->getText(true));
55 |
56 | $data = [];
57 | $data['chat_id'] = $chat_id;
58 |
59 | $cleanInterval = $this->getConfig('clean_interval');
60 |
61 | if (isset($text) && is_numeric($text) && $text > 0) {
62 | $cleanInterval = $text;
63 | }
64 |
65 | if (empty($cleanInterval)) {
66 | $cleanInterval = 86400; // 86400 seconds = 1 day
67 | }
68 |
69 | // Bug workaround: When run from the webhook and script just keeps going for too long Bot API will resend the update triggering the command again... and again...
70 | if (PHP_SAPI !== 'cli') {
71 | set_time_limit(10);
72 | }
73 |
74 | /** @var string $storage_class */
75 | $storage_class = Storage::getClass();
76 |
77 | if (class_exists($storage_class)) {
78 | $timeout = 10;
79 | do {
80 | try {
81 | $storage_init = $storage_class::initializeStorage();
82 | } catch (StorageException $e) {
83 | if ($timeout < 0 || strpos($e, 'too many connections') === false) {
84 | throw $e;
85 | }
86 | }
87 |
88 | $timeout--;
89 | sleep(1);
90 | } while (!isset($storage_init));
91 |
92 | if (!defined('IN_WORKER') && !empty($memory_limit = getenv('LIST_MEMORY_LIMIT'))) {
93 | ini_set('memory_limit', $memory_limit);
94 | }
95 |
96 | $inactive = $storage_class::listFromGame($cleanInterval);
97 |
98 | if (is_array($inactive) && count($inactive) > 0) {
99 | $chat_action_start = 0;
100 | $last_request_time = 0;
101 | $timelimit = ini_get('max_execution_time') > 0 ? ini_get('max_execution_time') : 59;
102 | $start_time = time();
103 |
104 | $hours = floor($cleanInterval / 3600);
105 | $minutes = floor(($cleanInterval / 60) % 60);
106 | $seconds = $cleanInterval % 60;
107 |
108 | $data['text'] = 'Cleaning games older than ' . $hours . 'h ' . $minutes . 'm ' . $seconds . 's' . '... (time limit: ' . $timelimit . ' seconds)';
109 |
110 | if ($chat_id != $bot_id) {
111 | Request::sendMessage($data);
112 | } elseif (PHP_SAPI === 'cli') {
113 | print $data['text'] . PHP_EOL;
114 | }
115 |
116 | $cleaned = 0;
117 | $edited = 0;
118 |
119 | foreach ($inactive as $inactive_game) {
120 | if (time() >= $start_time + $timelimit - 1) {
121 | Utilities::debugPrint('Time limit reached');
122 | break;
123 | }
124 |
125 | if ($chat_id != $bot_id && $chat_action_start < strtotime('-5 seconds')) {
126 | Request::sendChatAction(['chat_id' => $chat_id, 'action' => 'typing']);
127 | $chat_action_start = time();
128 | }
129 |
130 | $inactive_game['id'] = trim($inactive_game['id']);
131 |
132 | if (PHP_SAPI === 'cli' && $chat_id == $bot_id) {
133 | print 'Cleaning: \'' . $inactive_game['id'] . '\'' . PHP_EOL;
134 | }
135 |
136 | $game_data = $storage_class::selectFromGame($inactive_game['id']);
137 |
138 | if (isset($game_data['updated_at']) && $game_data['updated_at'] + $cleanInterval > time()) {
139 | continue;
140 | }
141 |
142 | if (isset($game_data['game_code'])) {
143 | $game = new GameCore($inactive_game['id'], $game_data['game_code'], $this);
144 |
145 | if ($game->canRun()) {
146 | while (time() <= $last_request_time) {
147 | Utilities::debugPrint('Delaying next request');
148 | sleep(1);
149 | }
150 |
151 | $game_class = $game->getGame();
152 |
153 | if (isset($game_data['game_data']['current_turn']) && $game_data['game_data']['current_turn'] === 'E') {
154 | $result = Request::editMessageReplyMarkup(
155 | [
156 | 'inline_message_id' => $inactive_game['id'],
157 | 'reply_markup' => $this->createInlineKeyboard($game_data['game_code'], __('Create New Session')),
158 | ]
159 | );
160 | } else {
161 | $result = Request::editMessageText(
162 | [
163 | 'inline_message_id' => $inactive_game['id'],
164 | 'text' => '' . $game_class::getTitle() . '' . PHP_EOL . PHP_EOL . '' . __("This game session has expired.") . '',
165 | 'reply_markup' => $this->createInlineKeyboard($game_data['game_code']),
166 | 'parse_mode' => 'HTML',
167 | 'disable_web_page_preview' => true,
168 | ]
169 | );
170 | }
171 |
172 | $last_request_time = time();
173 |
174 | if (isset($result) && $result->isOk()) {
175 | $edited++;
176 | Utilities::debugPrint('Message edited successfully');
177 | } else {
178 | Utilities::debugPrint('Failed to edit message: ' . (isset($result) ? $result->getDescription() : ''));
179 | }
180 | }
181 | }
182 |
183 | if ($storage_class::deleteFromGame($inactive_game['id'])) {
184 | $cleaned++;
185 | Utilities::debugPrint('Record removed from the database');
186 | }
187 | }
188 |
189 | $data['text'] = 'Cleaned ' . $cleaned . ' games (edited ' . $edited . ' messages).';
190 | } else {
191 | $data['text'] = 'Nothing to clean!';
192 | }
193 | } else {
194 | $data['text'] = 'Error!';
195 | }
196 |
197 | if ($chat_id != $bot_id) {
198 | return Request::sendMessage($data);
199 | } elseif (PHP_SAPI === 'cli') {
200 | print $data['text'] . PHP_EOL;
201 | }
202 |
203 | return Request::emptyResponse();
204 | }
205 |
206 | /**
207 | * Create inline keyboard with button that creates the game session
208 | *
209 | * @param string $game_code
210 | * @param string|null $text
211 | *
212 | * @return InlineKeyboard
213 | * @throws TelegramException
214 | */
215 | private function createInlineKeyboard(string $game_code, $text = null): InlineKeyboard
216 | {
217 | $inline_keyboard = [
218 | [
219 | new InlineKeyboardButton(
220 | [
221 | 'text' => $text ?: __('Create'),
222 | 'callback_data' => $game_code . ';new',
223 | ]
224 | ),
225 | ],
226 | ];
227 |
228 | return new InlineKeyboard(...$inline_keyboard);
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/src/Command/Admin/StatsCommand.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Command\Admin;
12 |
13 | use Bot\Exception\BotException;
14 | use Bot\Exception\StorageException;
15 | use Bot\GameCore;
16 | use Bot\Storage\Driver\File;
17 | use Bot\Storage\Storage;
18 | use Longman\TelegramBot\Commands\AdminCommand;
19 | use Longman\TelegramBot\Entities\InlineKeyboard;
20 | use Longman\TelegramBot\Entities\InlineKeyboardButton;
21 | use Longman\TelegramBot\Entities\ServerResponse;
22 | use Longman\TelegramBot\Exception\TelegramException;
23 | use Longman\TelegramBot\Request;
24 |
25 | /**
26 | * Extremely simple stats command
27 | */
28 | class StatsCommand extends AdminCommand
29 | {
30 | protected $name = 'stats';
31 | protected $description = 'Display stats';
32 | protected $usage = '/stats';
33 |
34 | /**
35 | * @return ServerResponse
36 | *
37 | * @throws BotException
38 | * @throws StorageException
39 | * @throws TelegramException
40 | */
41 | public function execute(): ServerResponse
42 | {
43 | $message = $this->getMessage();
44 | $edited_message = $this->getUpdate()->getEditedMessage();
45 | $callback_query = $this->getUpdate()->getCallbackQuery();
46 |
47 | if ($edited_message) {
48 | $message = $edited_message;
49 | }
50 |
51 | $chat_id = null;
52 | $data_query = null;
53 |
54 | if ($message) {
55 | $chat_id = $message->getChat()->getId();
56 | } elseif ($callback_query) {
57 | $chat_id = $callback_query->getMessage()->getChat()->getId();
58 |
59 | $data_query = [];
60 | $data_query['callback_query_id'] = $callback_query->getId();
61 | }
62 |
63 | /** @var File $storage_class */
64 | $storage_class = Storage::getClass();
65 | $storage_class::initializeStorage();
66 |
67 | if (!empty($memory_limit = getenv('LIST_MEMORY_LIMIT'))) {
68 | ini_set('memory_limit', $memory_limit);
69 | }
70 |
71 | $games = $storage_class::listFromGame();
72 | $stats = [
73 | 'games' => [],
74 | 'games_5min' => [],
75 | 'total' => 0,
76 | '5min' => 0,
77 | ];
78 |
79 | foreach ($games as $game) {
80 | $data = json_decode($game['data'], true);
81 |
82 | if (empty($data['game_code'])) {
83 | continue;
84 | }
85 |
86 | $game_obj = new GameCore($game['id'], $data['game_code'], $this);
87 | $game_class = $game_obj->getGame();
88 | $game_title = $game_class::getTitle();
89 |
90 | if (isset($stats['games'][$game_title])) {
91 | $stats['games'][$game_title] = $stats['games'][$game_title] + 1;
92 | } else {
93 | $stats['games'][$game_title] = 1;
94 | }
95 |
96 | if (!isset($stats['games_5min'][$game_title])) {
97 | $stats['games_5min'][$game_title] = 0;
98 | }
99 |
100 | if (strtotime($game['updated_at']) >= strtotime('-5 minutes')) {
101 | $stats['games_5min'][$game_title] = $stats['games_5min'][$game_title] + 1;
102 | $stats['5min']++;
103 | }
104 |
105 | $stats['total']++;
106 | }
107 |
108 | $stats['games']['All'] = $stats['total'];
109 | $stats['games_5min']['All'] = $stats['5min'];
110 |
111 | $output = '*Active sessions:*' . PHP_EOL;
112 |
113 | arsort($stats['games_5min']);
114 |
115 | foreach ($stats['games_5min'] as $game => $value) {
116 | $output .= ' ' . $game . ' – *' . ($stats['games_5min'][$game] ?? 0) . '* (*' . $stats['games'][$game] . '* total)' . PHP_EOL;
117 | }
118 |
119 | $data = [];
120 | $data['chat_id'] = $chat_id;
121 | $data['text'] = $output;
122 | $data['reply_markup'] = $this->createInlineKeyboard();
123 | $data['parse_mode'] = 'Markdown';
124 |
125 | if ($message) {
126 | return Request::sendMessage($data);
127 | } elseif ($callback_query) {
128 | $data['message_id'] = $callback_query->getMessage()->getMessageId();
129 | $result = Request::editMessageText($data);
130 |
131 | if (!$result->isOk()) {
132 | $data_query['show_alert'] = true;
133 | $data_query['text'] = substr($result->getDescription(), 0, 200);
134 | }
135 |
136 | return Request::answerCallbackQuery($data_query);
137 | }
138 |
139 | return Request::emptyResponse();
140 | }
141 |
142 | /**
143 | * Create inline keyboard that will refresh this message
144 | *
145 | * @return InlineKeyboard
146 | * @throws TelegramException
147 | */
148 | private function createInlineKeyboard(): InlineKeyboard
149 | {
150 | $inline_keyboard = [
151 | [
152 | new InlineKeyboardButton(
153 | [
154 | 'text' => 'Refresh',
155 | 'callback_data' => 'stats;refresh',
156 | ]
157 | ),
158 | ],
159 | ];
160 |
161 | return new InlineKeyboard(...$inline_keyboard);
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/Command/System/CallbackqueryCommand.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Command\System;
12 |
13 | use Bot\Exception\BotException;
14 | use Bot\Exception\StorageException;
15 | use Bot\Exception\TelegramApiException;
16 | use Bot\GameCore;
17 | use Bot\Helper\Utilities;
18 | use Longman\TelegramBot\Commands\SystemCommand;
19 | use Longman\TelegramBot\Entities\ServerResponse;
20 | use Longman\TelegramBot\Exception\TelegramException;
21 | use Longman\TelegramBot\Request;
22 | use Throwable;
23 |
24 | /**
25 | * Handle button presses
26 | *
27 | * @noinspection PhpUndefinedClassInspection
28 | */
29 | class CallbackqueryCommand extends SystemCommand
30 | {
31 | /**
32 | * Callback data before first ';' symbol -> command bind
33 | *
34 | * @var array
35 | */
36 | private $aliases = [
37 | 'stats' => 'stats',
38 | ];
39 |
40 | /**
41 | * @return bool|ServerResponse|mixed
42 | *
43 | * @throws TelegramException
44 | * @throws BotException
45 | * @throws StorageException
46 | * @throws TelegramApiException
47 | * @throws Throwable
48 | */
49 | public function execute(): ServerResponse
50 | {
51 | $callback_query = $this->getUpdate()->getCallbackQuery();
52 | $data = $callback_query->getData();
53 |
54 | Utilities::debugPrint('Data: ' . $data);
55 |
56 | $command = explode(';', $data)[0];
57 |
58 | if (isset($this->aliases[$command]) && $this->getTelegram()->getCommandObject($this->aliases[$command])) {
59 | return $this->getTelegram()->executeCommand($this->aliases[$command]);
60 | }
61 |
62 | if (($inline_message_id = $callback_query->getInlineMessageId()) && $this->isDataValid($data)) {
63 | $game = new GameCore($inline_message_id, explode(';', $data)[0], $this);
64 |
65 | if ($game->canRun()) {
66 | return $game->run();
67 | }
68 | }
69 |
70 | return Request::answerCallbackQuery(
71 | [
72 | 'callback_query_id' => $callback_query->getId(),
73 | 'text' => __("Bad request!"),
74 | 'show_alert' => true,
75 | ]
76 | );
77 | }
78 |
79 | /**
80 | * Validate callback data
81 | *
82 | * @param string $data
83 | *
84 | * @return bool
85 | */
86 | private function isDataValid(string $data): bool
87 | {
88 | /** @noinspection CallableParameterUseCaseInTypeContextInspection */
89 | $data = explode(';', $data);
90 |
91 | if (count($data) >= 2) {
92 | return true;
93 | }
94 |
95 | return false;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/Command/System/ChoseninlineresultCommand.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Command\System;
12 |
13 | use Bot\Exception\BotException;
14 | use Bot\Exception\StorageException;
15 | use Bot\Exception\TelegramApiException;
16 | use Bot\GameCore;
17 | use Bot\Helper\Utilities;
18 | use Longman\TelegramBot\Commands\SystemCommand;
19 | use Longman\TelegramBot\Entities\ServerResponse;
20 | use Longman\TelegramBot\Exception\TelegramException;
21 | use Throwable;
22 |
23 | /**
24 | * Handle event when inline message is pasted into chat, instantly put a player into game
25 | */
26 | class ChoseninlineresultCommand extends SystemCommand
27 | {
28 | /**
29 | * @return bool|ServerResponse
30 | *
31 | * @throws TelegramException
32 | * @throws BotException
33 | * @throws StorageException
34 | * @throws TelegramApiException
35 | * @throws Throwable
36 | */
37 | public function execute(): ServerResponse
38 | {
39 | $chosen_inline_result = $this->getUpdate()->getChosenInlineResult();
40 |
41 | if ($inline_message_id = $chosen_inline_result->getInlineMessageId()) {
42 | Utilities::debugPrint('Data: ' . $chosen_inline_result->getResultId());
43 |
44 | $game = new GameCore($inline_message_id, $chosen_inline_result->getResultId(), $this);
45 |
46 | if ($game->canRun()) {
47 | return $game->run();
48 | }
49 | }
50 |
51 | return parent::execute();
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Command/System/GenericmessageCommand.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Command\System;
12 |
13 | use Longman\TelegramBot\Commands\SystemCommand;
14 | use Longman\TelegramBot\Conversation;
15 | use Longman\TelegramBot\Entities\ServerResponse;
16 | use Longman\TelegramBot\Entities\User;
17 | use Longman\TelegramBot\Exception\TelegramException;
18 | use Longman\TelegramBot\Request;
19 | use Throwable;
20 |
21 | /**
22 | * Handle text messages
23 | *
24 | * @noinspection PhpUndefinedClassInspection
25 | */
26 | class GenericmessageCommand extends SystemCommand
27 | {
28 | /**
29 | * @return mixed
30 | *
31 | * @throws TelegramException
32 | * @throws Throwable
33 | */
34 | public function execute(): ServerResponse
35 | {
36 | $this->leaveGroupChat();
37 |
38 | if ($this->getMessage()->getViaBot() instanceof User && $this->getMessage()->getViaBot()->getId() === $this->getTelegram()->getBotId()) {
39 | return Request::emptyResponse();
40 | }
41 |
42 | $conversation = new Conversation(
43 | $this->getMessage()->getFrom()->getId(),
44 | $this->getMessage()->getChat()->getId()
45 | );
46 |
47 | if ($conversation->exists() && ($command = $conversation->getCommand())) {
48 | return $this->telegram->executeCommand($command);
49 | }
50 |
51 | return $this->executeNoDb();
52 | }
53 |
54 | /**
55 | * Leave group chats
56 | *
57 | * @return ServerResponse|null
58 | */
59 | private function leaveGroupChat(): ?ServerResponse
60 | {
61 | if (getenv('DEBUG')) {
62 | return null;
63 | }
64 |
65 | if (!$this->getMessage()->getChat()->isPrivateChat()) {
66 | return Request::leaveChat(
67 | [
68 | 'chat_id' => $this->getMessage()->getChat()->getId(),
69 | ]
70 | );
71 | }
72 |
73 | return null;
74 | }
75 |
76 | /**
77 | * @return ServerResponse|void
78 | * @throws TelegramException
79 | */
80 | public function executeNoDb(): ServerResponse
81 | {
82 | return Request::emptyResponse();
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Command/System/InlinequeryCommand.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Command\System;
12 |
13 | use Bot\Entity\Game;
14 | use DirectoryIterator;
15 | use Longman\TelegramBot\Commands\SystemCommand;
16 | use Longman\TelegramBot\Entities\InlineKeyboard;
17 | use Longman\TelegramBot\Entities\InlineKeyboardButton;
18 | use Longman\TelegramBot\Entities\InlineQuery\InlineQueryResultArticle;
19 | use Longman\TelegramBot\Entities\InputMessageContent\InputTextMessageContent;
20 | use Longman\TelegramBot\Entities\ServerResponse;
21 | use Longman\TelegramBot\Exception\TelegramException;
22 | use Longman\TelegramBot\Request;
23 |
24 | /**
25 | * Handle incoming inline queries, shows game list no matter what user enters
26 | *
27 | * @noinspection PhpUndefinedClassInspection
28 | */
29 | class InlinequeryCommand extends SystemCommand
30 | {
31 | /**
32 | * @return ServerResponse
33 | *
34 | * @throws TelegramException
35 | */
36 | public function execute(): ServerResponse
37 | {
38 | $articles = [];
39 |
40 | foreach ($this->getGamesList() as $game) {
41 | /** @var Game $game_class */
42 | if (class_exists($game_class = $game['class'])) {
43 | $articles[] = [
44 | 'id' => $game_class::getCode(),
45 | 'title' => $game_class::getTitle() . (method_exists($game_class, 'getTitleExtra') ? ' ' . $game_class::getTitleExtra() : ''),
46 | 'description' => $game_class::getDescription(),
47 | 'input_message_content' => new InputTextMessageContent(
48 | [
49 | 'message_text' => '' . $game_class::getTitle() . '' . PHP_EOL . PHP_EOL . '' . __('This game session is empty.') . '',
50 | 'parse_mode' => 'HTML',
51 | 'disable_web_page_preview' => true,
52 | ]
53 | ),
54 | 'reply_markup' => $this->createInlineKeyboard($game_class::getCode()),
55 | 'thumb_url' => $game_class::getImage(),
56 | ];
57 | }
58 | }
59 |
60 | $array_article = [];
61 | foreach ($articles as $article) {
62 | $array_article[] = new InlineQueryResultArticle($article);
63 | }
64 |
65 | $result = Request::answerInlineQuery(
66 | [
67 | 'inline_query_id' => $this->getUpdate()->getInlineQuery()->getId(),
68 | 'cache_time' => 60,
69 | 'results' => '[' . implode(',', $array_article) . ']',
70 | 'switch_pm_text' => 'Help',
71 | 'switch_pm_parameter' => 'start',
72 | ]
73 | );
74 |
75 | return $result;
76 | }
77 |
78 | /**
79 | * Get games list
80 | *
81 | * @return array
82 | */
83 | private function getGamesList(): array
84 | {
85 | $games = [];
86 | if (is_dir(SRC_PATH . '/Entity/Game')) {
87 | foreach (new DirectoryIterator(SRC_PATH . '/Entity/Game') as $file) {
88 | if (!$file->isDir() && !$file->isDot() && $file->getExtension() === 'php') {
89 | /** @var Game $game_class */
90 | $game_class = '\Bot\Entity\Game\\' . basename($file->getFilename(), '.php');
91 |
92 | $games[] = [
93 | 'class' => $game_class,
94 | 'order' => $game_class::getOrder(),
95 | ];
96 | }
97 | }
98 | }
99 |
100 | usort(
101 | $games,
102 | static function ($item1, $item2) {
103 | return $item1['order'] <=> $item2['order'];
104 | }
105 | );
106 |
107 | return $games;
108 | }
109 |
110 | /**
111 | * Create inline keyboard with button that creates the game session
112 | *
113 | * @param string $game_code
114 | *
115 | * @return InlineKeyboard
116 | * @throws TelegramException
117 | */
118 | private function createInlineKeyboard(string $game_code): InlineKeyboard
119 | {
120 | $inline_keyboard = [
121 | [
122 | new InlineKeyboardButton(
123 | [
124 | 'text' => __('Create'),
125 | 'callback_data' => $game_code . ';new',
126 | ]
127 | ),
128 | ],
129 | ];
130 |
131 | return new InlineKeyboard(...$inline_keyboard);
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/Command/User/StartCommand.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Command\User;
12 |
13 | use Longman\TelegramBot\Commands\UserCommand;
14 | use Longman\TelegramBot\Entities\InlineKeyboard;
15 | use Longman\TelegramBot\Entities\InlineKeyboardButton;
16 | use Longman\TelegramBot\Entities\ServerResponse;
17 | use Longman\TelegramBot\Exception\TelegramException;
18 | use Longman\TelegramBot\Request;
19 | use Spatie\Emoji\Emoji;
20 |
21 | /**
22 | * Start command...
23 | *
24 | * @noinspection PhpUndefinedClassInspection
25 | */
26 | class StartCommand extends UserCommand
27 | {
28 | /**
29 | * @return mixed
30 | *
31 | * @throws TelegramException
32 | */
33 | public function execute(): ServerResponse
34 | {
35 | $message = $this->getUpdate()->getMessage();
36 | $edited_message = $this->getUpdate()->getEditedMessage();
37 | $callback_query = $this->getUpdate()->getCallbackQuery();
38 |
39 | if ($edited_message) {
40 | $message = $edited_message;
41 | }
42 |
43 | $chat_id = null;
44 | $data_query = null;
45 |
46 | if ($message) {
47 | if (!$message->getChat()->isPrivateChat()) {
48 | return Request::emptyResponse();
49 | }
50 |
51 | $chat_id = $message->getChat()->getId();
52 | } elseif ($callback_query) {
53 | $chat_id = $callback_query->getMessage()->getChat()->getId();
54 |
55 | $data_query = [];
56 | $data_query['callback_query_id'] = $callback_query->getId();
57 | }
58 |
59 | $text = Emoji::wavingHand() . ' ';
60 | $text .= '' . __('Hi!') . '' . PHP_EOL;
61 | $text .= __('To begin, start a message with {USAGE} in any of your chats or click the {BUTTON} button and then select a chat to play in.', ['{USAGE}' => '\'@' . $this->getTelegram()->getBotUsername() . ' ...\'', '{BUTTON}' => '\'' . __('Play') . '\'']);
62 |
63 | $data = [
64 | 'chat_id' => $chat_id,
65 | 'text' => $text,
66 | 'parse_mode' => 'HTML',
67 | 'disable_web_page_preview' => true,
68 | 'reply_markup' => new InlineKeyboard(
69 | [
70 | new InlineKeyboardButton(
71 | [
72 | 'text' => __('Play') . ' ' . Emoji::gameDie(),
73 | 'switch_inline_query' => Emoji::gameDie(),
74 | ]
75 | ),
76 | ]
77 | ),
78 | ];
79 |
80 | if ($message) {
81 | return Request::sendMessage($data);
82 | } elseif ($callback_query) {
83 | $data['message_id'] = $callback_query->getMessage()->getMessageId();
84 | $result = Request::editMessageText($data);
85 | Request::answerCallbackQuery($data_query);
86 |
87 | return $result;
88 | }
89 |
90 | return Request::emptyResponse();
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Entity/Game/Connectfour.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Entity\Game;
12 |
13 | use Bot\Entity\Game;
14 | use Bot\Exception\BotException;
15 | use Bot\Exception\StorageException;
16 | use Bot\Helper\Utilities;
17 | use Longman\TelegramBot\Entities\ServerResponse;
18 | use Longman\TelegramBot\Exception\TelegramException;
19 | use Spatie\Emoji\Emoji;
20 |
21 | /**
22 | * Connect Four
23 | */
24 | class Connectfour extends Game
25 | {
26 | /**
27 | * Game unique ID
28 | *
29 | * @var string
30 | */
31 | protected static $code = 'c4';
32 |
33 | /**
34 | * Game name / title
35 | *
36 | * @var string
37 | */
38 | protected static $title = 'Connect Four';
39 |
40 | /**
41 | * Game description
42 | *
43 | * @var string
44 | */
45 | protected static $description = 'Connect Four is a connection game in which the players take turns dropping colored discs from the top into a seven-column, six-row vertically suspended grid.';
46 |
47 | /**
48 | * Game thumbnail image
49 | *
50 | * @var string
51 | */
52 | protected static $image = 'http://i.imgur.com/KgH8blx.jpg';
53 |
54 | /**
55 | * Order on the games list
56 | *
57 | * @var int
58 | */
59 | protected static $order = 10;
60 |
61 | /**
62 | * Base starting board
63 | *
64 | * @var array
65 | */
66 | protected static $board = [
67 | ['', '', '', '', '', '', ''],
68 | ['', '', '', '', '', '', ''],
69 | ['', '', '', '', '', '', ''],
70 | ['', '', '', '', '', '', ''],
71 | ['', '', '', '', '', '', ''],
72 | ['', '', '', '', '', '', ''],
73 | ];
74 |
75 | /**
76 | * Game handler
77 | *
78 | * @return ServerResponse
79 | *
80 | * @throws BotException
81 | * @throws TelegramException
82 | * @throws StorageException
83 | */
84 | protected function gameAction(): ServerResponse
85 | {
86 | if ($this->getCurrentUserId() !== $this->getUserId('host') && $this->getCurrentUserId() !== $this->getUserId('guest')) {
87 | return $this->answerCallbackQuery(__("You're not in this game!"), true);
88 | }
89 |
90 | $data = &$this->data['game_data'];
91 |
92 | $this->defineSymbols();
93 |
94 | $callbackquery_data = $this->manager->getUpdate()->getCallbackQuery()->getData();
95 | $callbackquery_data = explode(';', $callbackquery_data);
96 |
97 | $command = $callbackquery_data[1];
98 |
99 | if (isset($callbackquery_data[2])) {
100 | $args = explode('-', $callbackquery_data[2]);
101 | }
102 |
103 | if ($command === 'start') {
104 | if (isset($data['settings']) && $data['settings']['X'] == 'host') {
105 | $data['settings']['X'] = 'guest';
106 | $data['settings']['O'] = 'host';
107 | } else {
108 | $data['settings']['X'] = 'host';
109 | $data['settings']['O'] = 'guest';
110 | }
111 |
112 | $data['current_turn'] = 'X';
113 | $data['board'] = static::$board;
114 |
115 | Utilities::debugPrint('Game initialization');
116 | } elseif (!isset($args)) {
117 | Utilities::debugPrint('No move data received');
118 | }
119 |
120 | if (isset($data['current_turn']) && $data['current_turn'] == 'E') {
121 | return $this->answerCallbackQuery(__("This game has ended!", true));
122 | }
123 |
124 | if ($this->getCurrentUserId() !== $this->getUserId($data['settings'][$data['current_turn']]) && $command !== 'start') {
125 | return $this->answerCallbackQuery(__("It's not your turn!"), true);
126 | }
127 |
128 | $this->max_y = count($data['board']);
129 | $this->max_x = count($data['board'][0]);
130 |
131 | Utilities::isDebugPrintEnabled() && Utilities::debugPrint('BOARD: ' . $this->max_x . ' - ' . $this->max_y);
132 |
133 | if (isset($args)) {
134 | for ($y = $this->max_y - 1; $y >= 0; $y--) {
135 | if (isset($data['board'][$y][$args[1]])) {
136 | if ($data['board'][$y][$args[1]] === '') {
137 | $data['board'][$y][$args[1]] = $data['current_turn'];
138 |
139 | if ($data['current_turn'] == 'X') {
140 | $data['current_turn'] = 'O';
141 | } elseif ($data['current_turn'] == 'O') {
142 | $data['current_turn'] = 'X';
143 | }
144 |
145 | break;
146 | }
147 |
148 | if ($y === 0) {
149 | return $this->answerCallbackQuery(__("Invalid move!"), true);
150 | }
151 | } else {
152 | Utilities::debugPrint('Invalid move data: ' . ($args[0]) . ' - ' . ($y));
153 |
154 | return $this->answerCallbackQuery(__("Invalid move!"), true);
155 | }
156 | }
157 |
158 | Utilities::debugPrint($data['current_turn'] . ' placed at ' . ($args[1]) . ' - ' . ($y));
159 | }
160 |
161 | $isOver = $this->isGameOver($data['board']);
162 | $gameOutput = '';
163 |
164 | if (!empty($isOver) && in_array($isOver, ['X', 'O'])) {
165 | $gameOutput = Emoji::trophy() . ' ' . __("{PLAYER} won!", ['{PLAYER}' => '' . $this->getUserMention($data['settings'][$isOver]) . '']) . '';
166 | } elseif ($isOver == 'T') {
167 | $gameOutput = Emoji::chequeredFlag() . ' ' . __("Game ended with a draw!") . '';
168 | }
169 |
170 | if (!empty($isOver) && in_array($isOver, ['X', 'O', 'T'])) {
171 | $data['current_turn'] = 'E';
172 | } else {
173 | $gameOutput = Emoji::playButton() . ' ' . $this->getUserMention($data['settings'][$data['current_turn']]) . ' (' . $this->symbols[$data['current_turn']] . ')';
174 | }
175 |
176 | if ($this->saveData($this->data)) {
177 | return $this->editMessage(
178 | $this->getUserMention('host') . ' (' . (($data['settings']['X'] == 'host') ? $this->symbols['X'] : $this->symbols['O']) . ')' . ' vs. ' . $this->getUserMention('guest') . ' (' . (($data['settings']['O'] == 'guest') ? $this->symbols['O'] : $this->symbols['X']) . ')' . PHP_EOL . PHP_EOL . $gameOutput,
179 | $this->gameKeyboard($data['board'], $isOver)
180 | );
181 | }
182 |
183 | return parent::gameAction();
184 | }
185 |
186 | /**
187 | * Define game symbols
188 | */
189 | protected function defineSymbols(): void
190 | {
191 | $this->symbols['empty'] = Emoji::whiteCircle();
192 |
193 | $this->symbols['X'] = Emoji::blueCircle();
194 | $this->symbols['O'] = Emoji::redCircle();
195 |
196 | $this->symbols['X_won'] = Emoji::largeBlueDiamond();
197 | $this->symbols['O_won'] = Emoji::largeOrangeDiamond();
198 |
199 | $this->symbols['X_lost'] = Emoji::blackCircle();
200 | $this->symbols['O_lost'] = Emoji::blackCircle();
201 | }
202 |
203 | /**
204 | * Check whenever game is over
205 | *
206 | * @param array $board
207 | *
208 | * @return string
209 | */
210 | protected function isGameOver(array &$board): ?string
211 | {
212 | $empty = 0;
213 | for ($x = 0; $x <= $this->max_x; $x++) {
214 | for ($y = 0; $y <= $this->max_y; $y++) {
215 | if (isset($board[$x][$y]) && $board[$x][$y] == '') {
216 | $empty++;
217 | }
218 |
219 | if (isset($board[$x][$y]) && isset($board[$x][$y + 1]) && isset($board[$x][$y + 2]) && isset($board[$x][$y + 3])) {
220 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x][$y + 1] && $board[$x][$y] == $board[$x][$y + 2] && $board[$x][$y] == $board[$x][$y + 3]) {
221 | $winner = $board[$x][$y];
222 | $board[$x][$y + 1] = $board[$x][$y] . '_won';
223 | $board[$x][$y + 2] = $board[$x][$y] . '_won';
224 | $board[$x][$y + 3] = $board[$x][$y] . '_won';
225 | $board[$x][$y] = $board[$x][$y] . '_won';
226 |
227 | return $winner;
228 | }
229 | }
230 |
231 | if (isset($board[$x][$y]) && isset($board[$x + 1][$y]) && isset($board[$x + 2][$y]) && isset($board[$x + 3][$y])) {
232 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x + 1][$y] && $board[$x][$y] == $board[$x + 2][$y] && $board[$x][$y] == $board[$x + 3][$y]) {
233 | $winner = $board[$x][$y];
234 | $board[$x + 1][$y] = $board[$x][$y] . '_won';
235 | $board[$x + 2][$y] = $board[$x][$y] . '_won';
236 | $board[$x + 3][$y] = $board[$x][$y] . '_won';
237 | $board[$x][$y] = $board[$x][$y] . '_won';
238 |
239 | return $winner;
240 | }
241 | }
242 |
243 | if (isset($board[$x][$y]) && isset($board[$x + 1][$y + 1]) && isset($board[$x + 2][$y + 2]) && isset($board[$x + 3][$y + 3])) {
244 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x + 1][$y + 1] && $board[$x][$y] == $board[$x + 2][$y + 2] && $board[$x][$y] == $board[$x + 3][$y + 3]) {
245 | $winner = $board[$x][$y];
246 | $board[$x + 1][$y + 1] = $board[$x][$y] . '_won';
247 | $board[$x + 2][$y + 2] = $board[$x][$y] . '_won';
248 | $board[$x + 3][$y + 3] = $board[$x][$y] . '_won';
249 | $board[$x][$y] = $board[$x][$y] . '_won';
250 |
251 | return $winner;
252 | }
253 | }
254 |
255 | if (isset($board[$x][$y]) && isset($board[$x - 1][$y + 1]) && isset($board[$x - 2][$y + 2]) && isset($board[$x - 3][$y + 3])) {
256 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x - 1][$y + 1] && $board[$x][$y] == $board[$x - 2][$y + 2] && $board[$x][$y] == $board[$x - 3][$y + 3]) {
257 | $winner = $board[$x][$y];
258 | $board[$x - 1][$y + 1] = $board[$x][$y] . '_won';
259 | $board[$x - 2][$y + 2] = $board[$x][$y] . '_won';
260 | $board[$x - 3][$y + 3] = $board[$x][$y] . '_won';
261 | $board[$x][$y] = $board[$x][$y] . '_won';
262 |
263 | return $winner;
264 | }
265 | }
266 | }
267 | }
268 |
269 | if ($empty == 0) {
270 | return 'T';
271 | }
272 |
273 | return null;
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/src/Entity/Game/Elephantxo.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Entity\Game;
12 |
13 | /**
14 | * Elephant XO
15 | */
16 | class Elephantxo extends Tictactoe
17 | {
18 | /**
19 | * Game unique ID
20 | *
21 | * @var string
22 | */
23 | protected static $code = 'exo';
24 |
25 | /**
26 | * Game name / title
27 | *
28 | * @var string
29 | */
30 | protected static $title = 'Elephant XO';
31 |
32 | /**
33 | * Game description
34 | *
35 | * @var string
36 | */
37 | protected static $description = 'Elephant XO is a game for two players, X and O, who take turns marking the spaces in a 8×8 grid.';
38 |
39 | /**
40 | * Game thumbnail image
41 | *
42 | * @var string
43 | */
44 | protected static $image = 'https://i.imgur.com/tv2dmtq.png';
45 |
46 | /**
47 | * Order on the games list
48 | *
49 | * @var int
50 | */
51 | protected static $order = 3;
52 |
53 | /**
54 | * Base starting board
55 | *
56 | * @var array
57 | */
58 | protected static $board = [
59 | ['', '', '', '', '', '', '', ''],
60 | ['', '', '', '', '', '', '', ''],
61 | ['', '', '', '', '', '', '', ''],
62 | ['', '', '', '', '', '', '', ''],
63 | ['', '', '', '', '', '', '', ''],
64 | ['', '', '', '', '', '', '', ''],
65 | ['', '', '', '', '', '', '', ''],
66 | ['', '', '', '', '', '', '', ''],
67 | ];
68 |
69 | /**
70 | * Check whenever game is over
71 | *
72 | * @param array $board
73 | *
74 | * @return string
75 | */
76 | protected function isGameOver(array &$board): ?string
77 | {
78 | $empty = 0;
79 | for ($x = 0; $x <= $this->max_x; $x++) {
80 | for ($y = 0; $y <= $this->max_y; $y++) {
81 | if (isset($board[$x][$y]) && $board[$x][$y] == '') {
82 | $empty++;
83 | }
84 |
85 | if (isset($board[$x][$y]) && isset($board[$x][$y + 1]) && isset($board[$x][$y + 2]) && isset($board[$x][$y + 3]) && isset($board[$x][$y + 4])) {
86 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x][$y + 1] && $board[$x][$y] == $board[$x][$y + 2] && $board[$x][$y] == $board[$x][$y + 3] && $board[$x][$y] == $board[$x][$y + 4]) {
87 | $winner = $board[$x][$y];
88 | $board[$x][$y + 1] = $board[$x][$y] . '_won';
89 | $board[$x][$y + 2] = $board[$x][$y] . '_won';
90 | $board[$x][$y + 3] = $board[$x][$y] . '_won';
91 | $board[$x][$y + 4] = $board[$x][$y] . '_won';
92 | $board[$x][$y] = $board[$x][$y] . '_won';
93 |
94 | return $winner;
95 | }
96 | }
97 |
98 | if (isset($board[$x][$y]) && isset($board[$x + 1][$y]) && isset($board[$x + 2][$y]) && isset($board[$x + 3][$y]) && isset($board[$x + 4][$y])) {
99 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x + 1][$y] && $board[$x][$y] == $board[$x + 2][$y] && $board[$x][$y] == $board[$x + 3][$y] && $board[$x][$y] == $board[$x + 4][$y]) {
100 | $winner = $board[$x][$y];
101 | $board[$x + 1][$y] = $board[$x][$y] . '_won';
102 | $board[$x + 2][$y] = $board[$x][$y] . '_won';
103 | $board[$x + 3][$y] = $board[$x][$y] . '_won';
104 | $board[$x + 4][$y] = $board[$x][$y] . '_won';
105 | $board[$x][$y] = $board[$x][$y] . '_won';
106 |
107 | return $winner;
108 | }
109 | }
110 |
111 | if (isset($board[$x][$y]) && isset($board[$x + 1][$y + 1]) && isset($board[$x + 2][$y + 2]) && isset($board[$x + 3][$y + 3]) && isset($board[$x + 4][$y + 4])) {
112 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x + 1][$y + 1] && $board[$x][$y] == $board[$x + 2][$y + 2] && $board[$x][$y] == $board[$x + 3][$y + 3] && $board[$x][$y] == $board[$x + 4][$y + 4]) {
113 | $winner = $board[$x][$y];
114 | $board[$x + 1][$y + 1] = $board[$x][$y] . '_won';
115 | $board[$x + 2][$y + 2] = $board[$x][$y] . '_won';
116 | $board[$x + 3][$y + 3] = $board[$x][$y] . '_won';
117 | $board[$x + 4][$y + 4] = $board[$x][$y] . '_won';
118 | $board[$x][$y] = $board[$x][$y] . '_won';
119 |
120 | return $winner;
121 | }
122 | }
123 |
124 | if (isset($board[$x][$y]) && isset($board[$x - 1][$y + 1]) && isset($board[$x - 2][$y + 2]) && isset($board[$x - 3][$y + 3]) && isset($board[$x - 4][$y + 4])) {
125 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x - 1][$y + 1] && $board[$x][$y] == $board[$x - 2][$y + 2] && $board[$x][$y] == $board[$x - 3][$y + 3] && $board[$x][$y] == $board[$x - 4][$y + 4]) {
126 | $winner = $board[$x][$y];
127 | $board[$x - 1][$y + 1] = $board[$x][$y] . '_won';
128 | $board[$x - 2][$y + 2] = $board[$x][$y] . '_won';
129 | $board[$x - 3][$y + 3] = $board[$x][$y] . '_won';
130 | $board[$x - 4][$y + 4] = $board[$x][$y] . '_won';
131 | $board[$x][$y] = $board[$x][$y] . '_won';
132 |
133 | return $winner;
134 | }
135 | }
136 | }
137 | }
138 |
139 | if ($empty == 0) {
140 | return 'T';
141 | }
142 |
143 | return null;
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/Entity/Game/Rockpaperscissors.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Entity\Game;
12 |
13 | use Bot\Entity\Game;
14 | use Bot\Exception\BotException;
15 | use Bot\Exception\StorageException;
16 | use Bot\Helper\Utilities;
17 | use Longman\TelegramBot\Entities\InlineKeyboard;
18 | use Longman\TelegramBot\Entities\InlineKeyboardButton;
19 | use Longman\TelegramBot\Entities\ServerResponse;
20 | use Longman\TelegramBot\Exception\TelegramException;
21 | use Spatie\Emoji\Emoji;
22 |
23 | /**
24 | * Rock-Paper-Scissors
25 | */
26 | class Rockpaperscissors extends Game
27 | {
28 | /**
29 | * Game unique ID
30 | *
31 | * @var string
32 | */
33 | protected static $code = 'rps';
34 |
35 | /**
36 | * Game name / title
37 | *
38 | * @var string
39 | */
40 | protected static $title = 'Rock-Paper-Scissors';
41 |
42 | /**
43 | * Game description
44 | *
45 | * @var string
46 | */
47 | protected static $description = 'Rock-paper-scissors is game in which each player simultaneously forms one of three shapes with an outstretched hand.';
48 |
49 | /**
50 | * Game thumbnail image
51 | *
52 | * @var string
53 | */
54 | protected static $image = 'https://i.imgur.com/1H8HI7n.png';
55 |
56 | /**
57 | * Order on the games list
58 | *
59 | * @var int
60 | */
61 | protected static $order = 20;
62 |
63 | /**
64 | * Game handler
65 | *
66 | * @return ServerResponse
67 | *
68 | * @throws BotException
69 | * @throws TelegramException
70 | * @throws StorageException
71 | */
72 | protected function gameAction(): ServerResponse
73 | {
74 | if ($this->getCurrentUserId() !== $this->getUserId('host') && $this->getCurrentUserId() !== $this->getUserId('guest')) {
75 | return $this->answerCallbackQuery(__("You're not in this game!"), true);
76 | }
77 |
78 | $data = &$this->data['game_data'];
79 |
80 | $this->defineSymbols();
81 |
82 | $callbackquery_data = $this->manager->getUpdate()->getCallbackQuery()->getData();
83 | $callbackquery_data = explode(';', $callbackquery_data);
84 |
85 | $command = $callbackquery_data[1];
86 |
87 | $arg = $callbackquery_data[2] ?? null;
88 |
89 | if ($command === 'start') {
90 | $data['host_pick'] = '';
91 | $data['guest_pick'] = '';
92 | $data['host_wins'] = 0;
93 | $data['guest_wins'] = 0;
94 | $data['round'] = 1;
95 | $data['current_turn'] = '';
96 |
97 | Utilities::debugPrint('Game initialization');
98 | } elseif ($arg === null) {
99 | Utilities::debugPrint('No move data received');
100 | }
101 |
102 | if (isset($data['current_turn']) && $data['current_turn'] == 'E') {
103 | return $this->answerCallbackQuery(__("This game has ended!"), true);
104 | }
105 |
106 | Utilities::debugPrint('Argument: ' . $arg);
107 |
108 | if (isset($arg)) {
109 | if (in_array($arg, $this->symbols['valid'])) {
110 | if ($this->getCurrentUserId() === $this->getUserId('host') && $data['host_pick'] == '') {
111 | $data['host_pick'] = $arg;
112 | } elseif ($this->getCurrentUserId() === $this->getUserId('guest') && $data['guest_pick'] == '') {
113 | $data['guest_pick'] = $arg;
114 | }
115 |
116 | if ($this->saveData($this->data)) {
117 | Utilities::isDebugPrintEnabled() && Utilities::debugPrint($this->getCurrentUserMention() . ' picked ' . $arg);
118 | }
119 | } else {
120 | Utilities::debugPrint('Invalid move data: ' . $arg);
121 |
122 | return $this->answerCallbackQuery(__("Invalid move!"), true);
123 | }
124 | }
125 |
126 | $isOver = false;
127 | $gameOutput = '';
128 | $hostPick = '';
129 | $guestPick = '';
130 |
131 | if ($data['host_pick'] != '' && $data['guest_pick'] != '') {
132 | $isOver = $this->isGameOver($data['host_pick'], $data['guest_pick']);
133 |
134 | if (in_array($isOver, ['X', 'O', 'T'])) {
135 | $data['round'] += 1;
136 |
137 | if ($isOver == 'X') {
138 | $data['host_wins'] = $data['host_wins'] + 1;
139 |
140 | $gameOutput = Emoji::sportsMedal() . ' ' . __("{PLAYER} won this round!", ['{PLAYER}' => '' . $this->getUserMention('host') . '']) . '' . PHP_EOL;
141 | } elseif ($isOver == 'O') {
142 | $data['guest_wins'] = $data['guest_wins'] + 1;
143 |
144 | $gameOutput = Emoji::sportsMedal() . ' ' . __("{PLAYER} won this round!", ['{PLAYER}' => '' . $this->getUserMention('guest') . '']) . '' . PHP_EOL;
145 | } else {
146 | $gameOutput = Emoji::chequeredFlag() . ' ' . __("This round ended with a draw!") . '' . PHP_EOL;
147 | }
148 | }
149 |
150 | $hostPick = ' (' . $this->symbols[$data['host_pick'] . '_short'] . ')';
151 | $guestPick = ' (' . $this->symbols[$data['guest_pick'] . '_short'] . ')';
152 | }
153 |
154 | if (($data['host_wins'] >= 3 && $data['host_wins'] > $data['guest_wins']) || $data['host_wins'] >= $data['guest_wins'] + 3 || ($data['round'] > 5 && $data['host_wins'] > $data['guest_wins'])) {
155 | $gameOutput = Emoji::trophy() . ' ' . __("{PLAYER} won the game!", ['{PLAYER}' => '' . $this->getUserMention('host') . '']) . '';
156 |
157 | $data['current_turn'] = 'E';
158 | } elseif (($data['guest_wins'] >= 3 && $data['guest_wins'] > $data['host_wins']) || $data['guest_wins'] >= $data['host_wins'] + 3 || ($data['round'] > 5 && $data['guest_wins'] > $data['host_wins'])) {
159 | $gameOutput = Emoji::trophy() . ' ' . __("{PLAYER} won the game!", ['{PLAYER}' => '' . $this->getUserMention('guest') . '']) . '';
160 |
161 | $data['current_turn'] = 'E';
162 | } else {
163 | $gameOutput .= '' . __("Round {ROUND} - make your picks!", ['{ROUND}' => $data['round']]) . '';
164 |
165 | if ($data['host_pick'] != '' && $data['guest_pick'] === '') {
166 | $gameOutput .= PHP_EOL . '' . __("Waiting for:") . ' ' . $this->getUserMention('guest');
167 | } elseif ($data['guest_pick'] != '' && $data['host_pick'] === '') {
168 | $gameOutput .= PHP_EOL . '' . __("Waiting for:") . ' ' . $this->getUserMention('host');
169 | } else {
170 | $data['host_pick'] = '';
171 | $data['guest_pick'] = '';
172 | }
173 |
174 | $isOver = false;
175 | }
176 |
177 | if ($this->saveData($this->data)) {
178 | return $this->editMessage(
179 | $this->getUserMention('host') . (($data['host_wins'] > 0 || $data['guest_wins'] > 0) ? ' (' . $data['host_wins'] . ')' : '') . $hostPick . ' vs. ' . $this->getUserMention('guest') . (($data['guest_wins'] > 0 || $data['host_wins'] > 0) ? ' (' . $data['guest_wins'] . ')' : '') . $guestPick . PHP_EOL . PHP_EOL . $gameOutput,
180 | $this->customGameKeyboard($isOver)
181 | );
182 | }
183 |
184 | return parent::gameAction();
185 | }
186 |
187 | /**
188 | * Define game symbols (emojis)
189 | */
190 | protected function defineSymbols(): void
191 | {
192 | $this->symbols['R'] = 'ROCK';
193 | $this->symbols['R_short'] = Emoji::raisedFist();
194 | $this->symbols['P'] = 'PAPER';
195 | $this->symbols['P_short'] = Emoji::raisedHand();
196 | $this->symbols['S'] = 'SCISSORS';
197 | $this->symbols['S_short'] = Emoji::victoryHand();
198 | $this->symbols['valid'] = ['R', 'P', 'S'];
199 | }
200 |
201 | /**
202 | * Check whenever game is over
203 | *
204 | * @param string $x
205 | * @param string $y
206 | *
207 | * @return string
208 | */
209 | protected function isGameOver(string $x, string $y): ?string
210 | {
211 | if ($x == 'P' && $y == 'R') {
212 | return 'X';
213 | }
214 |
215 | if ($y == 'P' && $x == 'R') {
216 | return 'O';
217 | }
218 |
219 | if ($x == 'R' && $y == 'S') {
220 | return 'X';
221 | }
222 |
223 | if ($y == 'R' && $x == 'S') {
224 | return 'O';
225 | }
226 |
227 | if ($x == 'S' && $y == 'P') {
228 | return 'X';
229 | }
230 |
231 | if ($y == 'S' && $x == 'P') {
232 | return 'O';
233 | }
234 |
235 | if ($y == $x) {
236 | return 'T';
237 | }
238 |
239 | return null;
240 | }
241 |
242 | /**
243 | * Keyboard for game in progress
244 | *
245 | * @param bool $isOver
246 | *
247 | * @return InlineKeyboard
248 | * @throws BotException
249 | */
250 | protected function customGameKeyboard(bool $isOver = false): InlineKeyboard
251 | {
252 | if (!$isOver) {
253 | $inline_keyboard[] = [
254 | new InlineKeyboardButton(
255 | [
256 | 'text' => $this->symbols['R'] . ' ' . $this->symbols['R_short'],
257 | 'callback_data' => self::getCode() . ';game;R',
258 | ]
259 | ),
260 | new InlineKeyboardButton(
261 | [
262 | 'text' => $this->symbols['P'] . ' ' . $this->symbols['P_short'],
263 | 'callback_data' => self::getCode() . ';game;P',
264 | ]
265 | ),
266 | new InlineKeyboardButton(
267 | [
268 | 'text' => $this->symbols['S'] . ' ' . $this->symbols['S_short'],
269 | 'callback_data' => self::getCode() . ';game;S',
270 | ]
271 | ),
272 | ];
273 | } else {
274 | $inline_keyboard[] = [
275 | new InlineKeyboardButton(
276 | [
277 | 'text' => __('Play again!'),
278 | 'callback_data' => self::getCode() . ';start',
279 | ]
280 | ),
281 | ];
282 | }
283 |
284 | if (getenv('DEBUG') && $this->getCurrentUserId() == getenv('BOT_ADMIN')) {
285 | $inline_keyboard[] = [
286 | new InlineKeyboardButton(
287 | [
288 | 'text' => 'DEBUG: ' . 'Restart',
289 | 'callback_data' => self::getCode() . ';start',
290 | ]
291 | ),
292 | ];
293 | }
294 |
295 | $inline_keyboard[] = [
296 | new InlineKeyboardButton(
297 | [
298 | 'text' => __('Quit'),
299 | 'callback_data' => self::getCode() . ';quit',
300 | ]
301 | ),
302 | new InlineKeyboardButton(
303 | [
304 | 'text' => __('Kick'),
305 | 'callback_data' => self::getCode() . ';kick',
306 | ]
307 | ),
308 | ];
309 |
310 | return new InlineKeyboard(...$inline_keyboard);
311 | }
312 | }
313 |
--------------------------------------------------------------------------------
/src/Entity/Game/Rockpaperscissorslizardspock.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Entity\Game;
12 |
13 | use Bot\Exception\BotException;
14 | use Longman\TelegramBot\Entities\InlineKeyboard;
15 | use Longman\TelegramBot\Entities\InlineKeyboardButton;
16 | use Spatie\Emoji\Emoji;
17 |
18 | /**
19 | * Rock-Paper-Scissors-Lizard-Spock
20 | */
21 | class Rockpaperscissorslizardspock extends Rockpaperscissors
22 | {
23 | /**
24 | * Game unique ID
25 | *
26 | * @var string
27 | */
28 | protected static $code = 'rpsls';
29 |
30 | /**
31 | * Game name / title
32 | *
33 | * @var string
34 | */
35 | protected static $title = 'Rock-Paper-Scissors-Lizard-Spock';
36 |
37 | /**
38 | * Game description
39 | *
40 | * @var string
41 | */
42 | protected static $description = 'Rock-paper-scissors-lizard-spock is a game in which each player simultaneously forms one of five shapes with an outstretched hand.';
43 |
44 | /**
45 | * Game thumbnail image
46 | *
47 | * @var string
48 | */
49 | protected static $image = 'https://i.imgur.com/vSMnT88.png';
50 |
51 | /**
52 | * Order on the games list
53 | *
54 | * @var int
55 | */
56 | protected static $order = 21;
57 |
58 | /**
59 | * Define game symbols (emojis)
60 | */
61 | protected function defineSymbols(): void
62 | {
63 | $this->symbols['R'] = 'ROCK';
64 | $this->symbols['R_short'] = Emoji::raisedFist();
65 | $this->symbols['P'] = 'PAPER';
66 | $this->symbols['P_short'] = Emoji::raisedHand();
67 | $this->symbols['S'] = 'SCISSORS';
68 | $this->symbols['S_short'] = Emoji::victoryHand();
69 | $this->symbols['L'] = 'LIZARD';
70 | $this->symbols['L_short'] = Emoji::okHand();
71 | $this->symbols['A'] = 'SPOCK';
72 | $this->symbols['A_short'] = Emoji::vulcanSalute();
73 | $this->symbols['valid'] = ['R', 'P', 'S', 'L', 'A'];
74 | }
75 |
76 | /**
77 | * Check whenever game is over
78 | *
79 | * @param string $x
80 | * @param string $y
81 | *
82 | * @return string
83 | */
84 | protected function isGameOver(string $x, string $y): ?string
85 | {
86 | if ($x == 'P' && $y == 'R') {
87 | return 'X';
88 | }
89 |
90 | if ($y == 'P' && $x == 'R') {
91 | return 'O';
92 | }
93 |
94 | if ($x == 'P' && $y == 'A') {
95 | return 'X';
96 | }
97 |
98 | if ($y == 'P' && $x == 'A') {
99 | return 'O';
100 | }
101 |
102 | if ($x == 'R' && $y == 'S') {
103 | return 'X';
104 | }
105 |
106 | if ($y == 'R' && $x == 'S') {
107 | return 'O';
108 | }
109 |
110 | if ($x == 'R' && $y == 'L') {
111 | return 'X';
112 | }
113 |
114 | if ($y == 'R' && $x == 'L') {
115 | return 'O';
116 | }
117 |
118 | if ($x == 'S' && $y == 'P') {
119 | return 'X';
120 | }
121 |
122 | if ($y == 'S' && $x == 'P') {
123 | return 'O';
124 | }
125 |
126 | if ($x == 'S' && $y == 'L') {
127 | return 'X';
128 | }
129 |
130 | if ($y == 'S' && $x == 'L') {
131 | return 'O';
132 | }
133 |
134 | if ($x == 'L' && $y == 'P') {
135 | return 'X';
136 | }
137 |
138 | if ($y == 'L' && $x == 'P') {
139 | return 'O';
140 | }
141 |
142 | if ($x == 'L' && $y == 'A') {
143 | return 'X';
144 | }
145 |
146 | if ($y == 'L' && $x == 'A') {
147 | return 'O';
148 | }
149 |
150 | if ($x == 'A' && $y == 'S') {
151 | return 'X';
152 | }
153 |
154 | if ($y == 'A' && $x == 'S') {
155 | return 'O';
156 | }
157 |
158 | if ($x == 'A' && $y == 'R') {
159 | return 'X';
160 | }
161 |
162 | if ($y == 'A' && $x == 'R') {
163 | return 'O';
164 | }
165 |
166 | if ($y == $x) {
167 | return 'T';
168 | }
169 |
170 | return null;
171 | }
172 |
173 | /**
174 | * Keyboard for game in progress
175 | *
176 | * @param bool $isOver
177 | *
178 | * @return InlineKeyboard
179 | * @throws BotException
180 | */
181 | protected function customGameKeyboard(bool $isOver = false): InlineKeyboard
182 | {
183 | if (!$isOver) {
184 | $inline_keyboard[] = [
185 | new InlineKeyboardButton(
186 | [
187 | 'text' => $this->symbols['R'] . ' ' . $this->symbols['R_short'],
188 | 'callback_data' => self::getCode() . ';game;R',
189 | ]
190 | ),
191 | new InlineKeyboardButton(
192 | [
193 | 'text' => $this->symbols['P'] . ' ' . $this->symbols['P_short'],
194 | 'callback_data' => self::getCode() . ';game;P',
195 | ]
196 | ),
197 | new InlineKeyboardButton(
198 | [
199 | 'text' => $this->symbols['S'] . ' ' . $this->symbols['S_short'],
200 | 'callback_data' => self::getCode() . ';game;S',
201 | ]
202 | ),
203 | ];
204 | $inline_keyboard[] = [
205 | new InlineKeyboardButton(
206 | [
207 | 'text' => $this->symbols['L'] . ' ' . $this->symbols['L_short'],
208 | 'callback_data' => self::getCode() . ';game;L',
209 | ]
210 | ),
211 | new InlineKeyboardButton(
212 | [
213 | 'text' => $this->symbols['A'] . ' ' . $this->symbols['A_short'],
214 | 'callback_data' => self::getCode() . ';game;A',
215 | ]
216 | ),
217 | ];
218 | } else {
219 | $inline_keyboard[] = [
220 | new InlineKeyboardButton(
221 | [
222 | 'text' => __('Play again!'),
223 | 'callback_data' => self::getCode() . ';start',
224 | ]
225 | ),
226 | ];
227 | }
228 |
229 | if (getenv('DEBUG') && $this->getCurrentUserId() == getenv('BOT_ADMIN')) {
230 | $inline_keyboard[] = [
231 | new InlineKeyboardButton(
232 | [
233 | 'text' => 'DEBUG: ' . 'Restart',
234 | 'callback_data' => self::getCode() . ';start',
235 | ]
236 | ),
237 | ];
238 | }
239 |
240 | $inline_keyboard[] = [
241 | new InlineKeyboardButton(
242 | [
243 | 'text' => __('Quit'),
244 | 'callback_data' => self::getCode() . ';quit',
245 | ]
246 | ),
247 | new InlineKeyboardButton(
248 | [
249 | 'text' => __('Kick'),
250 | 'callback_data' => self::getCode() . ';kick',
251 | ]
252 | ),
253 | ];
254 |
255 | return new InlineKeyboard(...$inline_keyboard);
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/src/Entity/Game/Russianroulette.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Entity\Game;
12 |
13 | use Bot\Entity\Game;
14 | use Bot\Exception\BotException;
15 | use Bot\Exception\StorageException;
16 | use Bot\Helper\Utilities;
17 | use Longman\TelegramBot\Entities\InlineKeyboard;
18 | use Longman\TelegramBot\Entities\InlineKeyboardButton;
19 | use Longman\TelegramBot\Entities\ServerResponse;
20 | use Longman\TelegramBot\Exception\TelegramException;
21 | use Spatie\Emoji\Emoji;
22 |
23 | /**
24 | * Russian Roulette
25 | */
26 | class Russianroulette extends Game
27 | {
28 | /**
29 | * Game unique ID
30 | *
31 | * @var string
32 | */
33 | protected static $code = 'rr';
34 |
35 | /**
36 | * Game name / title
37 | *
38 | * @var string
39 | */
40 | protected static $title = 'Russian Roulette';
41 |
42 | /**
43 | * Game description
44 | *
45 | * @var string
46 | */
47 | protected static $description = 'Russian roulette is a game of chance in which a player places a single round in a revolver, spins the cylinder, places the muzzle against their head, and pulls the trigger.';
48 |
49 | /**
50 | * Game thumbnail image
51 | *
52 | * @var string
53 | */
54 | protected static $image = 'https://i.imgur.com/LffxQLK.jpg';
55 |
56 | /**
57 | * Order on the games list
58 | *
59 | * @var int
60 | */
61 | protected static $order = 30;
62 |
63 | /**
64 | * Game handler
65 | *
66 | * @return ServerResponse
67 | *
68 | * @throws BotException
69 | * @throws TelegramException
70 | * @throws StorageException
71 | */
72 | protected function gameAction(): ServerResponse
73 | {
74 | if ($this->getCurrentUserId() !== $this->getUserId('host') && $this->getCurrentUserId() !== $this->getUserId('guest')) {
75 | return $this->answerCallbackQuery(__("You're not in this game!"), true);
76 | }
77 |
78 | $data = &$this->data['game_data'];
79 |
80 | $this->defineSymbols();
81 |
82 | $callbackquery_data = $this->manager->getUpdate()->getCallbackQuery()->getData();
83 | $callbackquery_data = explode(';', $callbackquery_data);
84 |
85 | $command = $callbackquery_data[1];
86 |
87 | $arg = $callbackquery_data[2] ?? null;
88 |
89 | if ($command === 'start') {
90 | if (isset($data['settings']) && $data['settings']['X'] == 'host') {
91 | $data['settings']['X'] = 'guest';
92 | $data['settings']['O'] = 'host';
93 | } else {
94 | $data['settings']['X'] = 'host';
95 | $data['settings']['O'] = 'guest';
96 | }
97 |
98 | $data['current_turn'] = 'X';
99 | $data['cylinder'] = ['', '', '', '', '', ''];
100 | /** @noinspection RandomApiMigrationInspection */
101 | $data['cylinder'][mt_rand(0, 5)] = 'X';
102 |
103 | Utilities::debugPrint('Game initialization');
104 | } elseif ($arg === null) {
105 | Utilities::debugPrint('No move data received');
106 | }
107 |
108 | if (isset($data['current_turn']) && $data['current_turn'] == 'E') {
109 | return $this->answerCallbackQuery(__("This game has ended!"), true);
110 | }
111 |
112 | if ($this->getCurrentUserId() !== $this->getUserId($data['settings'][$data['current_turn']]) && $command !== 'start') {
113 | return $this->answerCallbackQuery(__("It's not your turn!"), true);
114 | }
115 |
116 | $hit = '';
117 | $gameOutput = '';
118 |
119 | if (isset($arg)) {
120 | if ($arg === 'null') {
121 | return $this->answerCallbackQuery();
122 | }
123 |
124 | if (!isset($data['cylinder'][$arg - 1])) {
125 | Utilities::debugPrint('Bad move data received: ' . $arg);
126 |
127 | return $this->answerCallbackQuery(__("Invalid move!"), true);
128 | }
129 |
130 | Utilities::debugPrint('Chamber selected: ' . $arg);
131 |
132 | if ($data['cylinder'][$arg - 1] === 'X') {
133 | Utilities::debugPrint('Chamber contains bullet, player is dead');
134 |
135 | if ($data['current_turn'] == 'X') {
136 | $gameOutput = Emoji::skull() . ' ' . __("{PLAYER} died! (kicked)", ['{PLAYER}' => '' . $this->getUserMention($data['settings']['X']) . '']) . '' . PHP_EOL;
137 | $gameOutput .= Emoji::trophy() . ' ' . __("{PLAYER} won!", ['{PLAYER}' => '' . $this->getUserMention($data['settings']['O']) . '']) . '' . PHP_EOL;
138 |
139 |
140 | if ($data['settings']['X'] === 'host') {
141 | $this->data['players']['host'] = $this->data['players']['guest'];
142 | $this->data['players']['guest'] = null;
143 | } else {
144 | $this->data['players']['guest'] = null;
145 | }
146 |
147 | $data['current_turn'] = 'E';
148 | } elseif ($data['current_turn'] == 'O') {
149 | $gameOutput = Emoji::skull() . ' ' . __("{PLAYER} died! (kicked)", ['{PLAYER}' => '' . $this->getUserMention($data['settings']['O']) . '']) . '' . PHP_EOL;
150 | $gameOutput .= Emoji::trophy() . ' ' . __("{PLAYER} won!", ['{PLAYER}' => '' . $this->getUserMention($data['settings']['X']) . '']) . '';
151 |
152 |
153 | if ($data['settings']['O'] === 'host') {
154 | $this->data['players']['host'] = $this->data['players']['guest'];
155 | $this->data['players']['guest'] = null;
156 | } else {
157 | $this->data['players']['guest'] = null;
158 | }
159 |
160 | $data['current_turn'] = 'E';
161 | }
162 |
163 | $hit = $arg;
164 |
165 | if ($this->saveData($this->data)) {
166 | return $this->editMessage($gameOutput . PHP_EOL . PHP_EOL . __('{PLAYER_HOST} is waiting for opponent to join...', ['{PLAYER_HOST}' => $this->getUserMention('host')]) . PHP_EOL . __('Press {BUTTON} button to join.', ['{BUTTON}' => '\'' . __('Join') . '\'']), $this->customGameKeyboard($hit));
167 | }
168 | }
169 |
170 | $gameOutput = Emoji::smilingFaceWithSunglasses() . ' ' . __("{PLAYER} survived!", ['{PLAYER}' => '' . $this->getCurrentUserMention() . '']) . '' . PHP_EOL;
171 |
172 | if ($data['current_turn'] == 'X') {
173 | $data['current_turn'] = 'O';
174 | } elseif ($data['current_turn'] == 'O') {
175 | $data['current_turn'] = 'X';
176 | }
177 |
178 | $data['cylinder'] = ['', '', '', '', '', ''];
179 | /** @noinspection RandomApiMigrationInspection */
180 | $data['cylinder'][mt_rand(0, 5)] = 'X';
181 | }
182 |
183 | $gameOutput .= Emoji::playButton() . ' ' . $this->getUserMention($data['settings'][$data['current_turn']]);
184 |
185 | Utilities::isDebugPrintEnabled() && Utilities::debugPrint('Cylinder: |' . implode('|', $data['cylinder']) . '|');
186 |
187 | if ($this->saveData($this->data)) {
188 | return $this->editMessage(
189 | $this->getUserMention('host') . ' vs. ' . $this->getUserMention('guest') . PHP_EOL . PHP_EOL . $gameOutput,
190 | $this->customGameKeyboard($hit)
191 | );
192 | }
193 |
194 | return parent::gameAction();
195 | }
196 |
197 | /**
198 | * Define game symbols (emojis)
199 | */
200 | protected function defineSymbols(): void
201 | {
202 | $this->symbols['empty'] = '.';
203 |
204 | $this->symbols['chamber'] = Emoji::radioButton();
205 | $this->symbols['chamber_hit'] = Emoji::redCircle();
206 | }
207 |
208 | /**
209 | * Keyboard for game in progress
210 | *
211 | * @param string $hit
212 | *
213 | * @return InlineKeyboard
214 | * @throws BotException
215 | */
216 | protected function customGameKeyboard(string $hit = null): InlineKeyboard
217 | {
218 | $inline_keyboard[] = [
219 | new InlineKeyboardButton(
220 | [
221 | 'text' => $this->symbols['empty'],
222 | 'callback_data' => self::getCode() . ';game;null',
223 | ]
224 | ),
225 | new InlineKeyboardButton(
226 | [
227 | 'text' => ($hit == 1) ? $this->symbols['chamber_hit'] : $this->symbols['chamber'],
228 | 'callback_data' => self::getCode() . ';game;1',
229 | ]
230 | ),
231 | new InlineKeyboardButton(
232 | [
233 | 'text' => ($hit == 2) ? $this->symbols['chamber_hit'] : $this->symbols['chamber'],
234 | 'callback_data' => self::getCode() . ';game;2',
235 | ]
236 | ),
237 | new InlineKeyboardButton(
238 | [
239 | 'text' => $this->symbols['empty'],
240 | 'callback_data' => self::getCode() . ';game;null',
241 | ]
242 | ),
243 | ];
244 |
245 | $inline_keyboard[] = [
246 | new InlineKeyboardButton(
247 | [
248 | 'text' => ($hit == 6) ? $this->symbols['chamber_hit'] : $this->symbols['chamber'],
249 | 'callback_data' => self::getCode() . ';game;6',
250 | ]
251 | ),
252 | new InlineKeyboardButton(
253 | [
254 | 'text' => $this->symbols['empty'],
255 | 'callback_data' => self::getCode() . ';game;null',
256 | ]
257 | ),
258 | new InlineKeyboardButton(
259 | [
260 | 'text' => ($hit == 3) ? $this->symbols['chamber_hit'] : $this->symbols['chamber'],
261 | 'callback_data' => self::getCode() . ';game;3',
262 | ]
263 | ),
264 | ];
265 |
266 | $inline_keyboard[] = [
267 | new InlineKeyboardButton(
268 | [
269 | 'text' => $this->symbols['empty'],
270 | 'callback_data' => self::getCode() . ';game;null',
271 | ]
272 | ),
273 | new InlineKeyboardButton(
274 | [
275 | 'text' => ($hit == 5) ? $this->symbols['chamber_hit'] : $this->symbols['chamber'],
276 | 'callback_data' => self::getCode() . ';game;5',
277 | ]
278 | ),
279 | new InlineKeyboardButton(
280 | [
281 | 'text' => ($hit == 4) ? $this->symbols['chamber_hit'] : $this->symbols['chamber'],
282 | 'callback_data' => self::getCode() . ';game;4',
283 | ]
284 | ),
285 | new InlineKeyboardButton(
286 | [
287 | 'text' => $this->symbols['empty'],
288 | 'callback_data' => self::getCode() . ';game;null',
289 | ]
290 | ),
291 | ];
292 |
293 | if (!is_numeric($hit)) {
294 | $inline_keyboard[] = [
295 | new InlineKeyboardButton(
296 | [
297 | 'text' => __('Quit'),
298 | 'callback_data' => self::getCode() . ';quit',
299 | ]
300 | ),
301 | new InlineKeyboardButton(
302 | [
303 | 'text' => __('Kick'),
304 | 'callback_data' => self::getCode() . ';kick',
305 | ]
306 | ),
307 | ];
308 | } else {
309 | $inline_keyboard[] = [
310 | new InlineKeyboardButton(
311 | [
312 | 'text' => __('Quit'),
313 | 'callback_data' => self::getCode() . ';quit',
314 | ]
315 | ),
316 | new InlineKeyboardButton(
317 | [
318 | 'text' => __('Join'),
319 | 'callback_data' => self::getCode() . ';join',
320 | ]
321 | ),
322 | ];
323 | }
324 |
325 | if (getenv('DEBUG') && $this->getCurrentUserId() == getenv('BOT_ADMIN')) {
326 | $inline_keyboard[] = [
327 | new InlineKeyboardButton(
328 | [
329 | 'text' => 'DEBUG: ' . 'Restart',
330 | 'callback_data' => self::getCode() . ';start',
331 | ]
332 | ),
333 | ];
334 | }
335 |
336 | return new InlineKeyboard(...$inline_keyboard);
337 | }
338 | }
339 |
--------------------------------------------------------------------------------
/src/Entity/Game/Tictacfour.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Entity\Game;
12 |
13 | /**
14 | * Tic-Tac-Four
15 | */
16 | class Tictacfour extends Tictactoe
17 | {
18 | /**
19 | * Game unique ID
20 | *
21 | * @var string
22 | */
23 | protected static $code = 'ttf';
24 |
25 | /**
26 | * Game name / title
27 | *
28 | * @var string
29 | */
30 | protected static $title = 'Tic-Tac-Four';
31 |
32 | /**
33 | * Game description
34 | *
35 | * @var string
36 | */
37 | protected static $description = 'Tic-tac-four is a game for two players, X and O, who take turns marking the spaces in a 7×6 grid.';
38 |
39 | /**
40 | * Game thumbnail image
41 | *
42 | * @var string
43 | */
44 | protected static $image = 'https://i.imgur.com/DSB90oa.png';
45 |
46 | /**
47 | * Order on the games list
48 | *
49 | * @var int
50 | */
51 | protected static $order = 2;
52 |
53 | /**
54 | * Base starting board
55 | *
56 | * @var array
57 | */
58 | protected static $board = [
59 | ['', '', '', '', '', '', ''],
60 | ['', '', '', '', '', '', ''],
61 | ['', '', '', '', '', '', ''],
62 | ['', '', '', '', '', '', ''],
63 | ['', '', '', '', '', '', ''],
64 | ['', '', '', '', '', '', ''],
65 | ];
66 |
67 | /**
68 | * Check whenever game is over
69 | *
70 | * @param array $board
71 | *
72 | * @return string
73 | */
74 | protected function isGameOver(array &$board): ?string
75 | {
76 | $empty = 0;
77 | for ($x = 0; $x <= $this->max_x; $x++) {
78 | for ($y = 0; $y <= $this->max_y; $y++) {
79 | if (isset($board[$x][$y]) && $board[$x][$y] == '') {
80 | $empty++;
81 | }
82 |
83 | if (isset($board[$x][$y]) && isset($board[$x][$y + 1]) && isset($board[$x][$y + 2]) && isset($board[$x][$y + 3])) {
84 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x][$y + 1] && $board[$x][$y] == $board[$x][$y + 2] && $board[$x][$y] == $board[$x][$y + 3]) {
85 | $winner = $board[$x][$y];
86 | $board[$x][$y + 1] = $board[$x][$y] . '_won';
87 | $board[$x][$y + 2] = $board[$x][$y] . '_won';
88 | $board[$x][$y + 3] = $board[$x][$y] . '_won';
89 | $board[$x][$y] = $board[$x][$y] . '_won';
90 |
91 | return $winner;
92 | }
93 | }
94 |
95 | if (isset($board[$x][$y]) && isset($board[$x + 1][$y]) && isset($board[$x + 2][$y]) && isset($board[$x + 3][$y])) {
96 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x + 1][$y] && $board[$x][$y] == $board[$x + 2][$y] && $board[$x][$y] == $board[$x + 3][$y]) {
97 | $winner = $board[$x][$y];
98 | $board[$x + 1][$y] = $board[$x][$y] . '_won';
99 | $board[$x + 2][$y] = $board[$x][$y] . '_won';
100 | $board[$x + 3][$y] = $board[$x][$y] . '_won';
101 | $board[$x][$y] = $board[$x][$y] . '_won';
102 |
103 | return $winner;
104 | }
105 | }
106 |
107 | if (isset($board[$x][$y]) && isset($board[$x + 1][$y + 1]) && isset($board[$x + 2][$y + 2]) && isset($board[$x + 3][$y + 3])) {
108 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x + 1][$y + 1] && $board[$x][$y] == $board[$x + 2][$y + 2] && $board[$x][$y] == $board[$x + 3][$y + 3]) {
109 | $winner = $board[$x][$y];
110 | $board[$x + 1][$y + 1] = $board[$x][$y] . '_won';
111 | $board[$x + 2][$y + 2] = $board[$x][$y] . '_won';
112 | $board[$x + 3][$y + 3] = $board[$x][$y] . '_won';
113 | $board[$x][$y] = $board[$x][$y] . '_won';
114 |
115 | return $winner;
116 | }
117 | }
118 |
119 | if (isset($board[$x][$y]) && isset($board[$x - 1][$y + 1]) && isset($board[$x - 2][$y + 2]) && isset($board[$x - 3][$y + 3])) {
120 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x - 1][$y + 1] && $board[$x][$y] == $board[$x - 2][$y + 2] && $board[$x][$y] == $board[$x - 3][$y + 3]) {
121 | $winner = $board[$x][$y];
122 | $board[$x - 1][$y + 1] = $board[$x][$y] . '_won';
123 | $board[$x - 2][$y + 2] = $board[$x][$y] . '_won';
124 | $board[$x - 3][$y + 3] = $board[$x][$y] . '_won';
125 | $board[$x][$y] = $board[$x][$y] . '_won';
126 |
127 | return $winner;
128 | }
129 | }
130 | }
131 | }
132 |
133 | if ($empty == 0) {
134 | return 'T';
135 | }
136 |
137 | return null;
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/Entity/Game/Tictactoe.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Entity\Game;
12 |
13 | use Bot\Entity\Game;
14 | use Bot\Exception\BotException;
15 | use Bot\Exception\StorageException;
16 | use Bot\Helper\Utilities;
17 | use Longman\TelegramBot\Entities\ServerResponse;
18 | use Longman\TelegramBot\Exception\TelegramException;
19 | use Spatie\Emoji\Emoji;
20 |
21 | /**
22 | * Tic-Tac-Toe
23 | */
24 | class Tictactoe extends Game
25 | {
26 | /**
27 | * Game unique ID
28 | *
29 | * @var string
30 | */
31 | protected static $code = 'xo';
32 |
33 | /**
34 | * Game name / title
35 | *
36 | * @var string
37 | */
38 | protected static $title = 'Tic-Tac-Toe';
39 |
40 | /**
41 | * Game description
42 | *
43 | * @var string
44 | */
45 | protected static $description = 'Tic-tac-toe is a game for two players, X and O, who take turns marking the spaces in a 3×3 grid.';
46 |
47 | /**
48 | * Game thumbnail image
49 | *
50 | * @var string
51 | */
52 | protected static $image = 'http://i.imgur.com/yU2uexr.png';
53 |
54 | /**
55 | * Order on the games list
56 | *
57 | * @var int
58 | */
59 | protected static $order = 1;
60 |
61 | /**
62 | * Base starting board
63 | *
64 | * @var array
65 | */
66 | protected static $board = [
67 | ['', '', ''],
68 | ['', '', ''],
69 | ['', '', ''],
70 | ];
71 |
72 | /**
73 | * Game handler
74 | *
75 | * @return ServerResponse
76 | *
77 | * @throws BotException
78 | * @throws TelegramException
79 | * @throws StorageException
80 | */
81 | protected function gameAction(): ServerResponse
82 | {
83 | if ($this->getCurrentUserId() !== $this->getUserId('host') && $this->getCurrentUserId() !== $this->getUserId('guest')) {
84 | return $this->answerCallbackQuery(__("You're not in this game!"), true);
85 | }
86 |
87 | $data = &$this->data['game_data'];
88 |
89 | $this->defineSymbols();
90 |
91 | $callbackquery_data = $this->manager->getUpdate()->getCallbackQuery()->getData();
92 | $callbackquery_data = explode(';', $callbackquery_data);
93 |
94 | $command = $callbackquery_data[1];
95 |
96 | $args = null;
97 | if (isset($callbackquery_data[2])) {
98 | $args = explode('-', $callbackquery_data[2]);
99 | }
100 |
101 | if ($command === 'start') {
102 | if (isset($data['settings']) && $data['settings']['X'] == 'host') {
103 | $data['settings']['X'] = 'guest';
104 | $data['settings']['O'] = 'host';
105 | } else {
106 | $data['settings']['X'] = 'host';
107 | $data['settings']['O'] = 'guest';
108 | }
109 |
110 | $data['current_turn'] = 'X';
111 | $data['board'] = static::$board;
112 |
113 | Utilities::debugPrint('Game initialization');
114 | } elseif ($args === null) {
115 | Utilities::debugPrint('No move data received');
116 | }
117 |
118 | if (isset($data['current_turn']) && $data['current_turn'] == 'E') {
119 | return $this->answerCallbackQuery(__("This game has ended!"), true);
120 | }
121 |
122 | if ($this->getCurrentUserId() !== $this->getUserId($data['settings'][$data['current_turn']]) && $command !== 'start') {
123 | return $this->answerCallbackQuery(__("It's not your turn!"), true);
124 | }
125 |
126 | if (isset($args) && isset($data['board'][$args[0]][$args[1]]) && $data['board'][$args[0]][$args[1]] !== '') {
127 | return $this->answerCallbackQuery(__("Invalid move!"), true);
128 | }
129 |
130 | $this->max_y = count($data['board']);
131 | $this->max_x = count($data['board'][0]);
132 |
133 | if (isset($args)) {
134 | if ($data['current_turn'] == 'X') {
135 | $data['board'][$args[0]][$args[1]] = 'X';
136 | $data['current_turn'] = 'O';
137 | } elseif ($data['current_turn'] == 'O') {
138 | $data['board'][$args[0]][$args[1]] = 'O';
139 | $data['current_turn'] = 'X';
140 | } else {
141 | Utilities::debugPrint('Invalid move data: ' . ($args[0]) . ' - ' . ($args[1]));
142 |
143 | return $this->answerCallbackQuery(__("Invalid move!"), true);
144 | }
145 |
146 | Utilities::debugPrint($data['current_turn'] . ' placed at ' . ($args[1]) . ' - ' . ($args[0]));
147 | }
148 |
149 | $isOver = $this->isGameOver($data['board']);
150 | $gameOutput = '';
151 |
152 | if (!empty($isOver) && in_array($isOver, ['X', 'O'])) {
153 | $gameOutput = Emoji::trophy() . ' ' . __("{PLAYER} won!", ['{PLAYER}' => '' . $this->getUserMention($data['settings'][$isOver]) . '']) . '';
154 | } elseif ($isOver == 'T') {
155 | $gameOutput = Emoji::chequeredFlag() . ' ' . __("Game ended with a draw!") . '';
156 | }
157 |
158 | if (!empty($isOver) && in_array($isOver, ['X', 'O', 'T'])) {
159 | $data['current_turn'] = 'E';
160 | } else {
161 | $gameOutput = Emoji::playButton() . ' ' . $this->getUserMention($data['settings'][$data['current_turn']]) . ' (' . $this->symbols[$data['current_turn']] . ')';
162 | }
163 |
164 | if ($this->saveData($this->data)) {
165 | return $this->editMessage(
166 | $this->getUserMention('host') . ' (' . (($data['settings']['X'] == 'host') ? $this->symbols['X'] : $this->symbols['O']) . ')' . ' vs. ' . $this->getUserMention('guest') . ' (' . (($data['settings']['O'] == 'guest') ? $this->symbols['O'] : $this->symbols['X']) . ')' . PHP_EOL . PHP_EOL . $gameOutput,
167 | $this->gameKeyboard($data['board'], $isOver)
168 | );
169 | }
170 |
171 | return parent::gameAction();
172 | }
173 |
174 | /**
175 | * Define game symbols (emojis)
176 | */
177 | protected function defineSymbols(): void
178 | {
179 | $this->symbols['empty'] = '.';
180 |
181 | $this->symbols['X'] = Emoji::crossMark();
182 | $this->symbols['O'] = Emoji::hollowRedCircle();
183 |
184 | $this->symbols['X_won'] = $this->symbols['X'];
185 | $this->symbols['O_won'] = $this->symbols['O'];
186 |
187 | $this->symbols['X_lost'] = Emoji::multiply();
188 | $this->symbols['O_lost'] = Emoji::radioButton();
189 | }
190 |
191 | /**
192 | * Check whenever game is over
193 | *
194 | * @param array $board
195 | *
196 | * @return string
197 | */
198 | protected function isGameOver(array &$board): ?string
199 | {
200 | $empty = 0;
201 | for ($x = 0; $x < $this->max_x; $x++) {
202 | for ($y = 0; $y < $this->max_y; $y++) {
203 | if ($board[$x][$y] == '') {
204 | $empty++;
205 | }
206 |
207 | if (isset($board[$x][$y]) && isset($board[$x][$y + 1]) && isset($board[$x][$y + 2])) {
208 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x][$y + 1] && $board[$x][$y] == $board[$x][$y + 2]) {
209 | $winner = $board[$x][$y];
210 | $board[$x][$y + 1] = $board[$x][$y] . '_won';
211 | $board[$x][$y + 2] = $board[$x][$y] . '_won';
212 | $board[$x][$y] = $board[$x][$y] . '_won';
213 |
214 | return $winner;
215 | }
216 | }
217 |
218 | if (isset($board[$x][$y]) && isset($board[$x + 1][$y]) && isset($board[$x + 2][$y])) {
219 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x + 1][$y] && $board[$x][$y] == $board[$x + 2][$y]) {
220 | $winner = $board[$x][$y];
221 |
222 | $board[$x + 1][$y] = $board[$x][$y] . '_won';
223 | $board[$x + 2][$y] = $board[$x][$y] . '_won';
224 | $board[$x][$y] = $board[$x][$y] . '_won';
225 |
226 | return $winner;
227 | }
228 | }
229 |
230 | if (isset($board[$x][$y]) && isset($board[$x + 1][$y + 1]) && isset($board[$x + 2][$y + 2])) {
231 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x + 1][$y + 1] && $board[$x][$y] == $board[$x + 2][$y + 2]) {
232 | $winner = $board[$x][$y];
233 |
234 | $board[$x + 1][$y + 1] = $board[$x][$y] . '_won';
235 | $board[$x + 2][$y + 2] = $board[$x][$y] . '_won';
236 | $board[$x][$y] = $board[$x][$y] . '_won';
237 |
238 | return $winner;
239 | }
240 | }
241 |
242 | if (isset($board[$x][$y]) && isset($board[$x - 1][$y + 1]) && isset($board[$x - 2][$y + 2])) {
243 | if ($board[$x][$y] != '' && $board[$x][$y] == $board[$x - 1][$y + 1] && $board[$x][$y] == $board[$x - 2][$y + 2]) {
244 | $winner = $board[$x][$y];
245 | $board[$x - 1][$y + 1] = $board[$x][$y] . '_won';
246 | $board[$x - 2][$y + 2] = $board[$x][$y] . '_won';
247 | $board[$x][$y] = $board[$x][$y] . '_won';
248 |
249 | return $winner;
250 | }
251 | }
252 | }
253 | }
254 |
255 | if ($empty == 0) {
256 | return 'T';
257 | }
258 |
259 | return null;
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/src/Entity/TempFile.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Entity;
12 |
13 | use Bot\Exception\BotException;
14 | use Bot\Helper\Utilities;
15 | use RuntimeException;
16 | use SplFileInfo;
17 |
18 | /**
19 | * A temporary file handler, with removal after it's not used
20 | */
21 | class TempFile
22 | {
23 | /**
24 | * The temporary file, or false
25 | *
26 | * @var null|SplFileInfo
27 | */
28 | private $file;
29 |
30 | /**
31 | * Should the file be delete after script ends
32 | *
33 | * @var bool
34 | */
35 | private $delete;
36 |
37 | /**
38 | * TempFile constructor
39 | *
40 | * @param string $name
41 | * @param bool $delete
42 | *
43 | * @throws BotException
44 | */
45 | public function __construct($name, $delete = true)
46 | {
47 | $this->delete = $delete;
48 |
49 | if (defined('DATA_PATH')) {
50 | $this->file = DATA_PATH . '/tmp/' . $name . '.tmp';
51 | } else {
52 | $this->file = sys_get_temp_dir() . '/' . md5(__DIR__) . '/' . $name . '.tmp';
53 | }
54 |
55 | if (!is_dir(dirname($this->file)) && !mkdir($concurrentDirectory = dirname($this->file), 0755, true) && !is_dir($concurrentDirectory)) {
56 | throw new RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
57 | }
58 |
59 | if (!is_writable(dirname($this->file))) {
60 | throw new BotException('Destination path is not writable: ' . dirname($this->file));
61 | }
62 |
63 | touch($this->file);
64 | $this->file = new SplFileInfo($this->file);
65 |
66 | Utilities::isDebugPrintEnabled() && Utilities::debugPrint('File: ' . realpath($this->file));
67 | }
68 |
69 | /**
70 | * Delete the file when script ends (unless specified to not)
71 | */
72 | public function __destruct()
73 | {
74 | if ($this->delete && $this->file !== null && file_exists($this->file)) {
75 | @unlink($this->file);
76 | }
77 | }
78 |
79 | /**
80 | * Get the file path or false
81 | *
82 | * @return null|SplFileInfo
83 | */
84 | public function getFile(): ?SplFileInfo
85 | {
86 | return $this->file;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Exception/BotException.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Exception;
12 |
13 | use Exception;
14 |
15 | /**
16 | * Exception class used for bot logic related exception handling
17 | */
18 | class BotException extends Exception
19 | {
20 | }
21 |
--------------------------------------------------------------------------------
/src/Exception/StorageException.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Exception;
12 |
13 | use Bot\Storage\Storage;
14 | use Exception;
15 | use Throwable;
16 |
17 | /**
18 | * Exception class used for bot storage related exception handling
19 | */
20 | class StorageException extends BotException
21 | {
22 | /**
23 | * @param string $message
24 | * @param int $code
25 | * @param Throwable|null $previous
26 | */
27 | public function __construct($message = "", $code = 0, Throwable $previous = null)
28 | {
29 | try {
30 | parent::__construct($message . ' [' . Storage::getClass() . ']', $code, $previous);
31 | } catch (Exception $e) {
32 | parent::__construct($message, $code, $previous);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Exception/TelegramApiException.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Exception;
12 |
13 | use Longman\TelegramBot\Entities\ServerResponse;
14 |
15 | /**
16 | * Exception class used for Telegram API related exception handling
17 | */
18 | class TelegramApiException extends BotException
19 | {
20 | /**
21 | * @param string $message
22 | * @param int $code
23 | * @param ServerResponse|null $result
24 | */
25 | public function __construct($message = "", $code = 0, ServerResponse $result = null)
26 | {
27 | parent::__construct($message, $code, $result);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/GameCore.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot;
12 |
13 | use Bot\Entity\Game;
14 | use Bot\Exception\BotException;
15 | use Bot\Exception\StorageException;
16 | use Bot\Exception\TelegramApiException;
17 | use Bot\Helper\Language;
18 | use Bot\Helper\Utilities;
19 | use Bot\Storage\Driver\File;
20 | use Bot\Storage\Storage;
21 | use DirectoryIterator;
22 | use Longman\TelegramBot\Commands\Command;
23 | use Longman\TelegramBot\Entities\ServerResponse;
24 | use Longman\TelegramBot\Entities\Update;
25 | use Longman\TelegramBot\Exception\TelegramException;
26 | use Longman\TelegramBot\Request;
27 | use Throwable;
28 |
29 | /**
30 | * This is the 'manager', it does everything what's required before running a game
31 | */
32 | class GameCore
33 | {
34 | /**
35 | * Game session ID (inline_message_id)
36 | *
37 | * @var string
38 | */
39 | private $id;
40 |
41 | /**
42 | * Current game object
43 | *
44 | * @var Game
45 | */
46 | private $game;
47 |
48 | /**
49 | * Currently used storage class name
50 | *
51 | * @var string|File
52 | */
53 | private $storage;
54 |
55 | /**
56 | * Telegram Update object
57 | *
58 | * @var Update
59 | */
60 | private $update;
61 |
62 | /**
63 | * GameManager constructor
64 | *
65 | * @param string $id
66 | * @param string $game_code
67 | * @param Command $command
68 | *
69 | * @throws BotException
70 | * @throws StorageException
71 | * @throws TelegramException
72 | */
73 | public function __construct(string $id, string $game_code, Command $command)
74 | {
75 | if (empty($id)) {
76 | throw new BotException('Id is empty!');
77 | }
78 |
79 | if (empty($game_code)) {
80 | throw new BotException('Game code is empty!');
81 | }
82 |
83 | Utilities::debugPrint('ID: ' . $id);
84 |
85 | $this->id = $id;
86 | $this->update = $command->getUpdate();
87 |
88 | try {
89 | /** @var File $storage_class */
90 | $this->storage = $storage_class = Storage::getClass();
91 | $storage_class::initializeStorage();
92 | } catch (StorageException $e) {
93 | $this->notifyAboutStorageFailure();
94 |
95 | if (strpos($e->getMessage(), 'too many connections') !== false) {
96 | return;
97 | }
98 |
99 | throw $e;
100 | }
101 |
102 | if ($game = $this->findGame($game_code)) {
103 | /** @var Game $game_class */
104 | $this->game = $game_class = $game;
105 |
106 | Utilities::debugPrint('Game: ' . $game_class::getTitle());
107 | } else {
108 | Utilities::debugPrint('Game not found');
109 | }
110 | }
111 |
112 | /**
113 | * Show information to user that storage is not accessible
114 | *
115 | * @return ServerResponse
116 | *
117 | * @throws TelegramException
118 | */
119 | protected function notifyAboutStorageFailure(): ServerResponse
120 | {
121 | Utilities::debugPrint('Database error');
122 |
123 | if ($callback_query = $this->update->getCallbackQuery()) {
124 | return Request::answerCallbackQuery(
125 | [
126 | 'callback_query_id' => $callback_query->getId(),
127 | 'text' => __('Database error!') . PHP_EOL . PHP_EOL . __("Try again in a few seconds."),
128 | 'show_alert' => true,
129 | ]
130 | );
131 | }
132 |
133 | return Request::emptyResponse();
134 | }
135 |
136 | /**
137 | * Find game class based on game code
138 | *
139 | * @param string $game_code
140 | *
141 | * @return Game|bool
142 | */
143 | private function findGame(string $game_code)
144 | {
145 | if (is_dir(SRC_PATH . '/Entity/Game')) {
146 | foreach (new DirectoryIterator(SRC_PATH . '/Entity/Game') as $file) {
147 | if (!$file->isDir() && !$file->isDot() && $file->getExtension() === 'php') {
148 | $game_class = '\Bot\Entity\Game\\' . basename($file->getFilename(), '.php');
149 |
150 | /** @var Game $game_class */
151 | if ($game_class::getCode() == $game_code) {
152 | return new $game_class($this);
153 | }
154 | }
155 | }
156 | }
157 |
158 | return false;
159 | }
160 |
161 | /**
162 | * Returns whenever the game can run (game is valid and loaded)
163 | *
164 | * @return bool
165 | */
166 | public function canRun(): bool
167 | {
168 | if ($this->game instanceof Game) {
169 | return true;
170 | }
171 |
172 | return false;
173 | }
174 |
175 | /**
176 | * Run the game class
177 | *
178 | * @return ServerResponse
179 | *
180 | * @throws TelegramApiException
181 | * @throws BotException
182 | * @throws StorageException
183 | * @throws TelegramException
184 | * @throws Throwable
185 | */
186 | public function run(): ServerResponse
187 | {
188 | $callback_query = $this->getUpdate()->getCallbackQuery();
189 | $chosen_inline_result = $this->getUpdate()->getChosenInlineResult();
190 | $storage_class = $this->storage;
191 |
192 | if (!$storage_class::lockGame($this->id)) {
193 | return $this->notifyAboutStorageLock();
194 | }
195 |
196 | Utilities::debugPrint('BEGIN HANDLING THE GAME');
197 |
198 | try {
199 | if ($callback_query) {
200 | $result = $this->game->handleAction(explode(';', $callback_query->getData())[1]);
201 | $storage_class::unlockGame($this->id);
202 | } elseif ($chosen_inline_result) {
203 | $result = $this->game->handleAction('new');
204 | $storage_class::unlockGame($this->id);
205 | } else {
206 | throw new BotException('Unknown update received!');
207 | }
208 | } catch (TelegramApiException | TelegramException $e) {
209 | $this->notifyAboutTelegramApiFailure();
210 |
211 | if (strpos($e->getMessage(), 'Telegram returned an invalid response') !== false) {
212 | return new ServerResponse(['ok' => false, 'description' => 'Telegram returned an invalid response'], '');
213 | }
214 |
215 | throw $e;
216 | } catch (StorageException $e) {
217 | $this->notifyAboutStorageFailure();
218 | throw $e;
219 | } catch (BotException $e) {
220 | $this->notifyAboutBotFailure();
221 | throw $e;
222 | } catch (Throwable $e) {
223 | $this->notifyAboutUnknownFailure();
224 | throw $e;
225 | }
226 |
227 | Utilities::debugPrint('GAME HANDLED');
228 |
229 | return $result;
230 | }
231 |
232 | /**
233 | * Return Update object
234 | *
235 | * @return Update
236 | */
237 | public function getUpdate(): Update
238 | {
239 | return $this->update;
240 | }
241 |
242 | /**
243 | * Returns notice about storage lock
244 | *
245 | * @return ServerResponse
246 | *
247 | * @throws TelegramException
248 | */
249 | protected function notifyAboutStorageLock(): ServerResponse
250 | {
251 | Utilities::debugPrint('Storage is locked');
252 |
253 | if ($callback_query = $this->update->getCallbackQuery()) {
254 | return Request::answerCallbackQuery(
255 | [
256 | 'callback_query_id' => $callback_query->getId(),
257 | 'text' => __('Process for this game is busy!') . PHP_EOL . PHP_EOL . __("Try again in a few seconds."),
258 | 'show_alert' => true,
259 | ]
260 | );
261 | }
262 |
263 | return Request::emptyResponse();
264 | }
265 |
266 | /**
267 | * Returns notice about bot failure
268 | *
269 | * @return ServerResponse
270 | *
271 | * @throws TelegramException
272 | */
273 | protected function notifyAboutTelegramApiFailure(): ServerResponse
274 | {
275 | Utilities::debugPrint('Telegram API error');
276 |
277 | if ($callback_query = $this->update->getCallbackQuery()) {
278 | return Request::answerCallbackQuery(
279 | [
280 | 'callback_query_id' => $callback_query->getId(),
281 | 'text' => __('Telegram API error!') . PHP_EOL . PHP_EOL . __("Try again in a few seconds."),
282 | 'show_alert' => true,
283 | ]
284 | );
285 | }
286 |
287 | return Request::emptyResponse();
288 | }
289 |
290 | /**
291 | * Returns notice about bot failure
292 | *
293 | * @return ServerResponse
294 | *
295 | * @throws TelegramException
296 | */
297 | protected function notifyAboutBotFailure(): ServerResponse
298 | {
299 | Utilities::debugPrint('Bot error');
300 |
301 | if ($callback_query = $this->update->getCallbackQuery()) {
302 | return Request::answerCallbackQuery(
303 | [
304 | 'callback_query_id' => $callback_query->getId(),
305 | 'text' => __('Bot error!') . PHP_EOL . PHP_EOL . __("Try again in a few seconds."),
306 | 'show_alert' => true,
307 | ]
308 | );
309 | }
310 |
311 | return Request::emptyResponse();
312 | }
313 |
314 | /**
315 | * Returns notice about unknown failure
316 | *
317 | * @return ServerResponse
318 | *
319 | * @throws TelegramException
320 | */
321 | protected function notifyAboutUnknownFailure(): ServerResponse
322 | {
323 | Utilities::debugPrint('Unknown error');
324 |
325 | if ($callback_query = $this->update->getCallbackQuery()) {
326 | return Request::answerCallbackQuery(
327 | [
328 | 'callback_query_id' => $callback_query->getId(),
329 | 'text' => __('Unhandled error!') . PHP_EOL . PHP_EOL . __("Try again in a few seconds."),
330 | 'show_alert' => true,
331 | ]
332 | );
333 | }
334 |
335 | return Request::emptyResponse();
336 | }
337 |
338 | /**
339 | * Get game id
340 | *
341 | * @return string
342 | */
343 | public function getId(): string
344 | {
345 | return $this->id;
346 | }
347 |
348 | /**
349 | * Get game object
350 | *
351 | * @return Game|mixed
352 | */
353 | public function getGame(): Game
354 | {
355 | return $this->game;
356 | }
357 |
358 | /**
359 | * Get storage class
360 | *
361 | * @return string
362 | */
363 | public function getStorage(): string
364 | {
365 | return $this->storage;
366 | }
367 | }
368 |
--------------------------------------------------------------------------------
/src/Helper/Language.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Helper;
12 |
13 | use DirectoryIterator;
14 | use Gettext\Translations;
15 | use Gettext\Translator;
16 | use RuntimeException;
17 |
18 | /**
19 | * Simple localization class
20 | */
21 | class Language
22 | {
23 | /**
24 | * Default strings language
25 | *
26 | * @var string
27 | */
28 | private static $default_language = 'en';
29 |
30 | /**
31 | * Current language
32 | *
33 | * @var string
34 | */
35 | private static $current_language = '';
36 |
37 | /**
38 | * Return language list
39 | *
40 | * @return array
41 | */
42 | public static function list(): array
43 | {
44 | $languages = [self::$default_language];
45 |
46 | if (is_dir(ROOT_PATH . '/language')) {
47 | foreach (new DirectoryIterator(ROOT_PATH . '/language') as $fileInfo) {
48 | if (!$fileInfo->isDir() && !$fileInfo->isDot()) {
49 | $language = explode('.', $fileInfo->getFilename());
50 |
51 | if ($language[1] != self::getDefaultlanguage() && isset($language[2]) && $language[2] == 'po') {
52 | $languages[] = $language[1];
53 | }
54 | }
55 | }
56 | }
57 |
58 | return $languages;
59 | }
60 |
61 | /**
62 | * Get default language
63 | *
64 | * @return string
65 | */
66 | public static function getDefaultLanguage(): string
67 | {
68 | if (!empty($default_language = getenv('DEFAULT_LANGUAGE'))) {
69 | return $default_language;
70 | }
71 |
72 | return self::$default_language;
73 | }
74 |
75 | /**
76 | * Set default language
77 | *
78 | * @param string $default_language
79 | */
80 | public static function setDefaultLanguage(string $default_language): void
81 | {
82 | self::$default_language = $default_language;
83 | self::set($default_language);
84 | }
85 |
86 | /**
87 | * Set the language and load translation
88 | *
89 | * @param string $language
90 | */
91 | public static function set(string $language = ''): void
92 | {
93 | $t = new Translator();
94 |
95 | if (file_exists(ROOT_PATH . '/translations/messages.' . $language . '.po')) {
96 | if (defined('DATA_PATH')) {
97 | if (!file_exists(ROOT_PATH . '/translations/messages.' . $language . '.cache') || md5_file(ROOT_PATH . '/translations/messages.' . $language . '.po') != file_get_contents(DATA_PATH . '/translations/messages.' . $language . '.cache')) {
98 | if (!self::compileToArray($language)) {
99 | Utilities::debugPrint('Language compilation to PHP array failed!');
100 | }
101 | }
102 |
103 | if (file_exists(DATA_PATH . '/translations/messages.' . $language . '.php')) {
104 | $t->loadTranslations(DATA_PATH . '/translations/messages.' . $language . '.php');
105 | }
106 | } else {
107 | $translations = Translations::fromPoFile(ROOT_PATH . '/translations/messages.' . $language . '.po');
108 | $t->loadTranslations($translations);
109 | }
110 |
111 | self::$current_language = $language;
112 | } else {
113 | self::$current_language = self::$default_language;
114 | }
115 |
116 | $t->register();
117 | }
118 |
119 | /**
120 | * Compile .po file into .php array file
121 | *
122 | * @param string $language
123 | *
124 | * @return bool
125 | */
126 | private static function compileToArray(string $language): bool
127 | {
128 | if (defined('DATA_PATH') && !file_exists(ROOT_PATH . '/translations/messages.' . $language . '.php')) {
129 | $translation = Translations::fromPoFile(ROOT_PATH . '/translations/messages.' . $language . '.po');
130 |
131 | if (!is_dir(DATA_PATH . '/translations/') && !mkdir($concurrentDirectory = DATA_PATH . '/translations/', 0755, true) && !is_dir($concurrentDirectory)) {
132 | throw new RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
133 | }
134 |
135 | $translation->toPhpArrayFile(DATA_PATH . '/translations/messages.' . $language . '.php');
136 |
137 | return file_put_contents(DATA_PATH . '/translations/messages.' . $language . '.cache', md5_file(ROOT_PATH . '/translations/messages.' . $language . '.po'));
138 | }
139 |
140 | return false;
141 | }
142 |
143 | /**
144 | * Get current language
145 | *
146 | * @return string
147 | */
148 | public static function getCurrentLanguage(): string
149 | {
150 | return self::$current_language ?: self::$default_language;
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/Helper/Utilities.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Helper;
12 |
13 | use Longman\TelegramBot\Entities\Update;
14 | use Longman\TelegramBot\TelegramLog;
15 | use Psr\Log\LoggerInterface;
16 |
17 | /**
18 | * Extra functions
19 | */
20 | class Utilities
21 | {
22 | /**
23 | * Is debug print enabled?
24 | *
25 | * @var bool
26 | */
27 | private static $debug_print_enabled = false;
28 |
29 | /**
30 | * Logger instance
31 | *
32 | * @var LoggerInterface
33 | */
34 | private static $debug_print_logger = null;
35 |
36 | /**
37 | * Show debug message and (if enabled) write to debug log
38 | *
39 | * @param string $text
40 | * @param array $context
41 | */
42 | public static function debugPrint(string $text, array $context = []): void
43 | {
44 | if (PHP_SAPI === 'cli' && self::$debug_print_enabled) {
45 | if ($text === '') {
46 | return;
47 | }
48 |
49 | $prefix = '';
50 | $backtrace = debug_backtrace();
51 |
52 | if (isset($backtrace[1]['class'])) {
53 | $prefix = $backtrace[1]['class'] . '\\' . $backtrace[1]['function'];
54 | }
55 |
56 | TelegramLog::debug('[' . $prefix . '] ' . $text . ' ' . json_encode($context));
57 |
58 | if (self::$debug_print_logger !== null) {
59 | if (strpos($text, PHP_EOL) !== false) {
60 | $text = explode(PHP_EOL, trim($text));
61 |
62 | foreach ($text as $line) {
63 | self::$debug_print_logger->debug('[' . $prefix . '] ' . trim($line), $context ?? []);
64 | }
65 | } else {
66 | self::$debug_print_logger->debug('[' . $prefix . '] ' . trim($text), $context ?? []);
67 | }
68 | } else {
69 | $prefix = '[' . date('Y-m-d H:i:s') . '] ' . $prefix . ': ';
70 | $message = $prefix . trim($text);
71 |
72 | if (!empty($context)) {
73 | $message .= ' ' . \json_encode($context);
74 | }
75 |
76 | $message = preg_replace('~[\r\n]+~', PHP_EOL . $prefix, $message);
77 |
78 | print $message . PHP_EOL;
79 | }
80 | }
81 | }
82 |
83 | /**
84 | * Enable/disable debug print
85 | *
86 | * @param bool $enabled
87 | */
88 | public static function setDebugPrint(bool $enabled = true): void
89 | {
90 | self::$debug_print_enabled = $enabled;
91 | }
92 |
93 | /**
94 | * Inject logger into the debug print function
95 | *
96 | * @param LoggerInterface $logger
97 | */
98 | public static function setDebugPrintLogger(LoggerInterface $logger): void
99 | {
100 | self::$debug_print_logger = $logger;
101 | }
102 |
103 | /**
104 | * Check if debug print is enabled
105 | */
106 | public static function isDebugPrintEnabled(): bool
107 | {
108 | if (PHP_SAPI === 'cli') {
109 | return self::$debug_print_enabled;
110 | }
111 |
112 | return false;
113 | }
114 |
115 | /**
116 | * Make a debug dump from array of debug data
117 | *
118 | * @param string $message
119 | * @param array $data
120 | *
121 | * @return string
122 | */
123 | public static function debugDump(string $message = '', array $data = []): string
124 | {
125 | if (!empty($message)) {
126 | $output = $message . PHP_EOL;
127 | } else {
128 | $output = PHP_EOL;
129 | }
130 |
131 | foreach ($data as $var => $val) {
132 | /** @noinspection NestedTernaryOperatorInspection */
133 | $output .= $var . ': ' . (is_array($val) ? print_r($val, true) : (is_bool($val) ? ($val ? 'true' : 'false') : $val)) . PHP_EOL;
134 | }
135 |
136 | return $output;
137 | }
138 |
139 | /**
140 | * strpos() with array needle
141 | * https://stackoverflow.com/a/9220624
142 | *
143 | * @param string $haystack
144 | * @param array $needle
145 | * @param int $offset
146 | *
147 | * @return bool|mixed
148 | */
149 | public static function strposa(string $haystack, array $needle, int $offset = 0)
150 | {
151 | if (!is_array($needle)) {
152 | $needle = [$needle];
153 | }
154 | foreach ($needle as $query) {
155 | if (strpos($haystack, $query, $offset) !== false) {
156 | return true;
157 | }
158 | }
159 |
160 | return false;
161 | }
162 |
163 | /**
164 | * Convert update object to array then remove 'raw_data' and 'bot_username' from it
165 | *
166 | * @param Update $update
167 | *
168 | * @return array
169 | */
170 | public static function updateToArray(Update $update): array
171 | {
172 | $update_array = json_decode(json_encode($update), true);
173 | unset($update_array['raw_data']);
174 | unset($update_array['bot_username']);
175 |
176 | return $update_array;
177 | }
178 |
179 | /**
180 | * Formats bytes to human readable format
181 | * https://stackoverflow.com/a/2510459
182 | *
183 | * @param int $bytes
184 | * @param int $precision
185 | *
186 | * @return string
187 | */
188 | public static function formatBytes(int $bytes, int $precision = 2): string
189 | {
190 | $units = ['B', 'KB', 'MB', 'GB', 'TB'];
191 |
192 | $bytes = max($bytes, 0);
193 | $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
194 | $pow = min($pow, count($units) - 1);
195 | $bytes /= 1024 ** $pow;
196 |
197 | return round($bytes, $precision) . ' ' . $units[$pow];
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/src/Storage/Driver/BotDB.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Storage\Driver;
12 |
13 | use Bot\Exception\BotException;
14 | use Bot\Exception\StorageException;
15 | use Longman\TelegramBot\DB;
16 |
17 | /**
18 | * Pulls PDO connection from \Longman\TelegramBot\DB and redirects all calls to \Bot\Storage\MySQL
19 | */
20 | class BotDB extends DB
21 | {
22 | /**
23 | * Create table structure
24 | *
25 | * @return bool
26 | *
27 | * @throws StorageException
28 | */
29 | public static function createStructure(): bool
30 | {
31 | self::initializeStorage();
32 |
33 | return MySQL::createStructure();
34 | }
35 |
36 | /**
37 | * Initialize PDO connection
38 | *
39 | * @return bool
40 | *
41 | * @throws StorageException
42 | */
43 | public static function initializeStorage(): bool
44 | {
45 | return MySQL::initializeStorage(self::$pdo);
46 | }
47 |
48 | /**
49 | * Select data from database
50 | *
51 | * @param string $id
52 | *
53 | * @return array|bool
54 | *
55 | * @throws StorageException
56 | */
57 | public static function selectFromGame(string $id)
58 | {
59 | return MySQL::selectFromGame($id);
60 | }
61 |
62 | /**
63 | * Insert data to database
64 | *
65 | * @param string $id
66 | * @param array $data
67 | *
68 | * @return bool
69 | *
70 | * @throws StorageException
71 | */
72 | public static function insertToGame(string $id, array $data): bool
73 | {
74 | return MySQL::insertToGame($id, $data);
75 | }
76 |
77 | /**
78 | * Delete data from storage
79 | *
80 | * @param string $id
81 | *
82 | * @return bool
83 | *
84 | * @throws StorageException
85 | */
86 | public static function deleteFromGame(string $id): bool
87 | {
88 | return MySQL::deleteFromGame($id);
89 | }
90 |
91 | /**
92 | * Lock the row to prevent another process modifying it
93 | *
94 | * @param string $id
95 | *
96 | * @return bool
97 | *
98 | * @throws BotException
99 | * @throws StorageException
100 | */
101 | public static function lockGame(string $id): bool
102 | {
103 | return MySQL::lockGame($id);
104 | }
105 |
106 | /**
107 | * Unlock the row after
108 | *
109 | * @param string $id
110 | *
111 | * @return bool
112 | *
113 | * @throws StorageException
114 | */
115 | public static function unlockGame(string $id): bool
116 | {
117 | return MySQL::unlockGame($id);
118 | }
119 |
120 | /**
121 | * Select multiple data from the database
122 | *
123 | * @param int $time
124 | *
125 | * @return array|bool
126 | *
127 | * @throws StorageException
128 | */
129 | public static function listFromGame(int $time = 0)
130 | {
131 | return MySQL::listFromGame($time);
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/Storage/Driver/File.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Storage\Driver;
12 |
13 | use Bot\Entity\TempFile;
14 | use Bot\Exception\StorageException;
15 | use DirectoryIterator;
16 | use RuntimeException;
17 |
18 | /**
19 | * Stores data in json formatted text files
20 | */
21 | class File
22 | {
23 | /**
24 | * Lock file object
25 | *
26 | * @var TempFile
27 | */
28 | private static $lock;
29 |
30 | /**
31 | * Initialize - define paths
32 | *
33 | * @throws StorageException
34 | */
35 | public static function initializeStorage(): bool
36 | {
37 | if (!defined('STORAGE_GAME_PATH')) {
38 | if (!defined('DATA_PATH')) {
39 | throw new StorageException('Data path is not set!');
40 | }
41 |
42 | define('STORAGE_GAME_PATH', DATA_PATH . '/game');
43 |
44 | if (!is_dir(STORAGE_GAME_PATH) && !mkdir($concurrentDirectory = STORAGE_GAME_PATH, 0755, true) && !is_dir($concurrentDirectory)) {
45 | throw new RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
46 | }
47 | }
48 |
49 | return true;
50 | }
51 |
52 | /**
53 | * Dummy function
54 | *
55 | * @return bool
56 | */
57 | public static function createStructure(): bool
58 | {
59 | return true;
60 | }
61 |
62 | /**
63 | * Read data from the file
64 | *
65 | * @param string $id
66 | *
67 | * @return array|bool
68 | * @throws StorageException
69 | */
70 | public static function selectFromGame(string $id)
71 | {
72 | if (empty($id)) {
73 | throw new StorageException('Id is empty!');
74 | }
75 |
76 | if (file_exists(STORAGE_GAME_PATH . '/' . $id . '.json')) {
77 | return json_decode(file_get_contents(STORAGE_GAME_PATH . '/' . $id . '.json'), true) ?? [];
78 | }
79 |
80 | return [];
81 | }
82 |
83 | /**
84 | * Place data to the file
85 | *
86 | * @param string $id
87 | * @param array $data
88 | *
89 | * @return bool
90 | * @throws StorageException
91 | */
92 | public static function insertToGame(string $id, array $data): bool
93 | {
94 | if (empty($id)) {
95 | throw new StorageException('Id is empty!');
96 | }
97 |
98 | $data['updated_at'] = time();
99 |
100 | if (!isset($data['created_at'])) {
101 | $data['created_at'] = $data['updated_at'];
102 | }
103 |
104 | if (file_exists(STORAGE_GAME_PATH . '/' . $id . '.json')) {
105 | return file_put_contents(STORAGE_GAME_PATH . '/' . $id . '.json', json_encode($data));
106 | }
107 |
108 | return false;
109 | }
110 |
111 | /**
112 | * Remove data file
113 | *
114 | * @param string $id
115 | *
116 | * @return bool
117 | * @throws StorageException
118 | */
119 | public static function deleteFromGame(string $id): bool
120 | {
121 | if (empty($id)) {
122 | throw new StorageException('Id is empty!');
123 | }
124 |
125 | if (file_exists(STORAGE_GAME_PATH . '/' . $id . '.json')) {
126 | return unlink(STORAGE_GAME_PATH . '/' . $id . '.json');
127 | }
128 |
129 | return false;
130 | }
131 |
132 | /**
133 | * Lock the file to prevent another process modifying it
134 | *
135 | * @param string $id
136 | *
137 | * @return bool
138 | * @throws StorageException
139 | */
140 | public static function lockGame(string $id): bool
141 | {
142 | if (empty($id)) {
143 | throw new StorageException('Id is empty!');
144 | }
145 |
146 | self::$lock = new TempFile($id);
147 | if (self::$lock->getFile() === null) {
148 | return false;
149 | }
150 |
151 | if (flock(fopen(self::$lock->getFile()->getPathname(), 'ab+'), LOCK_EX)) {
152 | if (!file_exists(STORAGE_GAME_PATH . '/' . $id . '.json')) {
153 | file_put_contents(STORAGE_GAME_PATH . '/' . $id . '.json', json_encode([]));
154 | }
155 |
156 | return true;
157 | }
158 |
159 | return false;
160 | }
161 |
162 | /**
163 | * Unlock the file after
164 | *
165 | * @param string $id
166 | *
167 | * @return bool
168 | * @throws StorageException
169 | */
170 | public static function unlockGame(string $id): bool
171 | {
172 | if (empty($id)) {
173 | throw new StorageException('Id is empty!');
174 | }
175 |
176 | if (self::$lock === null) {
177 | throw new StorageException('No lock file object!');
178 | }
179 |
180 | if (self::$lock->getFile() === null) {
181 | return false;
182 | }
183 |
184 | return flock(fopen(self::$lock->getFile()->getPathname(), 'ab+'), LOCK_UN);
185 | }
186 |
187 | /**
188 | * Select inactive data fields from database
189 | *
190 | * @param int $time
191 | *
192 | * @return array
193 | * @throws StorageException
194 | */
195 | public static function listFromGame(int $time = 0): array
196 | {
197 | if (!is_numeric($time)) {
198 | throw new StorageException('Time must be a number!');
199 | }
200 |
201 | $ids = [];
202 | foreach (new DirectoryIterator(STORAGE_GAME_PATH) as $file) {
203 | if (!$file->isDir() && !$file->isDot() && $file->getMTime() + $time < time()) {
204 | $data = file_get_contents($file->getPathname());
205 | $json = json_decode($data, true);
206 | $data_stripped = json_encode(['game_code' => $json['game_code'] ?? null]);
207 |
208 | $ids[] = ['id' => trim(basename($file->getFilename(), '.json')), 'data' => $data_stripped, 'updated_at' => date('H:i:s d-m-Y', filemtime($file->getPathname()))];
209 | }
210 | }
211 |
212 | return $ids;
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/src/Storage/Driver/Memcache.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Storage\Driver;
12 |
13 | use Bot\Entity\TempFile;
14 | use Bot\Exception\BotException;
15 | use Bot\Exception\StorageException;
16 | use MemCachier\MemcacheSASL;
17 | use Memcache as MemcacheCore;
18 | use Memcached as MemcachedCore;
19 |
20 | /**
21 | * Class Memcache
22 | */
23 | class Memcache
24 | {
25 | /**
26 | * Memcache object
27 | *
28 | * @var MemcachedCore|MemcacheCore
29 | */
30 | private static $memcache;
31 |
32 | /**
33 | * Lock file object
34 | *
35 | * @var TempFile
36 | */
37 | private static $lock;
38 |
39 | /**
40 | * Create table structure
41 | *
42 | * @return bool
43 | * @throws StorageException
44 | */
45 | public static function createStructure(): bool
46 | {
47 | return true;
48 | }
49 |
50 | /**
51 | * Check if database connection has been created
52 | *
53 | * @return bool
54 | */
55 | public static function isDbConnected(): bool
56 | {
57 | return self::$memcache !== null;
58 | }
59 |
60 | /**
61 | * Initialize connection
62 | *
63 | * @return bool
64 | *
65 | * @throws StorageException
66 | */
67 | public static function initializeStorage(): bool
68 | {
69 | if (self::isDbConnected()) {
70 | return true;
71 | }
72 |
73 | try {
74 | $dsn = parse_url(getenv('DATABASE_URL'));
75 |
76 | if (class_exists(MemcacheSASL::class) && stripos($dsn['host'], 'memcachier') !== false) {
77 | $memcache = new MemcacheSASL();
78 |
79 | $memcache->setOption(MemcacheSASL::OPT_COMPRESSION, true);
80 | $memcache->setOption(MemcacheSASL::OPT_CONNECT_TIMEOUT, 1000);
81 | $memcache->setOption(MemcacheSASL::OPT_RECV_TIMEOUT, 10000);
82 | $memcache->setOption(MemcacheSASL::OPT_SEND_TIMEOUT, 10000);
83 | } elseif (class_exists(MemcachedCore::class)) {
84 | $memcache = new MemcachedCore($persistent_id ?? null);
85 |
86 | $memcache->setOption(MemcachedCore::OPT_BINARY_PROTOCOL, true);
87 | $memcache->setOption(MemcachedCore::OPT_NO_BLOCK, true);
88 | $memcache->setOption(MemcachedCore::OPT_COMPRESSION, true);
89 | $memcache->setOption(MemcachedCore::OPT_CONNECT_TIMEOUT, 1000);
90 | $memcache->setOption(MemcachedCore::OPT_RECV_TIMEOUT, 10000);
91 | $memcache->setOption(MemcachedCore::OPT_SEND_TIMEOUT, 10000);
92 | } elseif (class_exists(MemcacheCore::class)) {
93 | $memcache = new MemcacheCore($persistent_id ?? null);
94 | } else {
95 | throw new StorageException('Unsupported database type!');
96 | }
97 |
98 | $memcache->addServer($dsn['host'], $dsn['port']);
99 |
100 | if (isset($dsn['user'], $dsn['pass'])) {
101 | if (!method_exists($memcache, 'setSaslAuthData')) {
102 | throw new \RuntimeException('Memcached extension was not build with SASL support');
103 | }
104 |
105 | $memcache->setSaslAuthData($dsn['user'], $dsn['pass']);
106 | }
107 |
108 | self::$memcache = $memcache;
109 | } catch (\Exception $e) {
110 | throw new StorageException('Connection to the memcached server failed: ' . $e->getMessage());
111 | }
112 |
113 | return true;
114 | }
115 |
116 | /**
117 | * Select data from database
118 | *
119 | * @param string $id
120 | *
121 | * @return array|bool
122 | * @throws StorageException
123 | */
124 | public static function selectFromGame(string $id)
125 | {
126 | if (!self::isDbConnected()) {
127 | return false;
128 | }
129 |
130 | if (empty($id)) {
131 | throw new StorageException('Id is empty!');
132 | }
133 |
134 | $data = self::$memcache->get('game_' . sha1($id));
135 |
136 | if ($data !== null) {
137 | return json_decode($data, true) ?? [];
138 | }
139 |
140 | return [];
141 | }
142 |
143 | /**
144 | * Insert data to database
145 | *
146 | * @param string $id
147 | * @param array $data
148 | *
149 | * @return bool
150 | * @throws StorageException
151 | */
152 | public static function insertToGame(string $id, array $data): bool
153 | {
154 | if (!self::isDbConnected()) {
155 | return false;
156 | }
157 |
158 | if (empty($id)) {
159 | throw new StorageException('Id is empty!');
160 | }
161 |
162 | if (empty($data)) {
163 | throw new StorageException('Data is empty!');
164 | }
165 |
166 | return self::$memcache->set('game_' . sha1($id), json_encode($data));
167 | }
168 |
169 | /**
170 | * Delete data from storage
171 | *
172 | * @param string $id
173 | *
174 | * @return bool
175 | * @throws StorageException
176 | */
177 | public static function deleteFromGame(string $id): bool
178 | {
179 | if (!self::isDbConnected()) {
180 | return false;
181 | }
182 |
183 | if (empty($id)) {
184 | throw new StorageException('Id is empty!');
185 | }
186 |
187 | return self::$memcache->delete('game_' . sha1($id));
188 | }
189 |
190 | /**
191 | * Basic file-powered lock to prevent other process accessing same game
192 | *
193 | * @param string $id
194 | *
195 | * @return bool
196 | *
197 | * @throws StorageException
198 | * @throws BotException
199 | */
200 | public static function lockGame(string $id): bool
201 | {
202 | if (!self::isDbConnected()) {
203 | return false;
204 | }
205 |
206 | if (empty($id)) {
207 | throw new StorageException('Id is empty!');
208 | }
209 |
210 | self::$lock = new TempFile($id);
211 | if (self::$lock->getFile() === null) {
212 | return false;
213 | }
214 |
215 | return flock(fopen(self::$lock->getFile()->getPathname(), 'ab+'), LOCK_EX);
216 | }
217 |
218 | /**
219 | * Unlock the game to allow access from other processes
220 | *
221 | * @param string $id
222 | *
223 | * @return bool
224 | *
225 | * @throws StorageException
226 | */
227 | public static function unlockGame(string $id): bool
228 | {
229 | if (!self::isDbConnected()) {
230 | return false;
231 | }
232 |
233 | if (empty($id)) {
234 | throw new StorageException('Id is empty!');
235 | }
236 |
237 | if (self::$lock === null) {
238 | throw new StorageException('No lock file object!');
239 | }
240 |
241 | if (self::$lock->getFile() === null) {
242 | return false;
243 | }
244 |
245 | return flock(fopen(self::$lock->getFile()->getPathname(), 'ab+'), LOCK_UN);
246 | }
247 |
248 | /**
249 | * Select multiple data from the database
250 | *
251 | * @param int $time
252 | *
253 | * @return array|bool
254 | * @throws StorageException
255 | */
256 | public static function listFromGame(int $time = 0)
257 | {
258 | if (!self::isDbConnected()) {
259 | return false;
260 | }
261 |
262 | return [];
263 | }
264 | }
265 |
--------------------------------------------------------------------------------
/src/Storage/Driver/MySQL.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Storage\Driver;
12 |
13 | use Bot\Entity\TempFile;
14 | use Bot\Exception\BotException;
15 | use Bot\Exception\StorageException;
16 | use PDO;
17 | use PDOException;
18 |
19 | /**
20 | * Class MySQL
21 | */
22 | class MySQL
23 | {
24 | /**
25 | * PDO object
26 | *
27 | * @var PDO
28 | */
29 | private static $pdo;
30 |
31 | /**
32 | * Lock file object
33 | *
34 | * @var TempFile
35 | */
36 | private static $lock;
37 |
38 | /**
39 | * SQL to create database structure
40 | *
41 | * @var string
42 | */
43 | private static $structure = 'CREATE TABLE IF NOT EXISTS `game` (
44 | `id` CHAR(255) COMMENT "Unique identifier for this entry",
45 | `data` TEXT NOT NULL COMMENT "Stored data",
46 | `created_at` timestamp NULL DEFAULT NULL COMMENT "Entry creation date",
47 | `updated_at` timestamp NULL DEFAULT NULL COMMENT "Entry update date",
48 |
49 | PRIMARY KEY (`id`)
50 | );';
51 |
52 | /**
53 | * Create table structure
54 | *
55 | * @return bool
56 | * @throws StorageException
57 | */
58 | public static function createStructure(): bool
59 | {
60 | if (!self::isDbConnected()) {
61 | self::initializeStorage();
62 | }
63 |
64 | if (!self::$pdo->query(self::$structure)) {
65 | throw new StorageException('Failed to create DB structure!');
66 | }
67 |
68 | return true;
69 | }
70 |
71 | /**
72 | * Check if database connection has been created
73 | *
74 | * @return bool
75 | */
76 | public static function isDbConnected(): bool
77 | {
78 | return self::$pdo !== null;
79 | }
80 |
81 | /**
82 | * Initialize PDO connection
83 | *
84 | * @param $pdo
85 | *
86 | * @return bool
87 | *
88 | * @throws StorageException
89 | */
90 | public static function initializeStorage($pdo = null): bool
91 | {
92 | if (self::isDbConnected()) {
93 | return true;
94 | }
95 |
96 | if (!defined('TB_GAME')) {
97 | define('TB_GAME', 'game');
98 | }
99 |
100 | if ($pdo === null) {
101 | try {
102 | $dsn = parse_url(getenv('DATABASE_URL'));
103 |
104 | self::$pdo = new PDO('mysql:' . 'host=' . $dsn['host'] . ';port=' . $dsn['port'] . ';dbname=' . ltrim($dsn['path'], '/'), $dsn['user'], $dsn['pass']);
105 | self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
106 | } catch (PDOException $e) {
107 | throw new StorageException('Connection to the database failed: ' . $e->getMessage());
108 | }
109 | } else {
110 | self::$pdo = $pdo;
111 | }
112 |
113 | return true;
114 | }
115 |
116 | /**
117 | * Select data from database
118 | *
119 | * @param string $id
120 | *
121 | * @return array|bool
122 | * @throws StorageException
123 | */
124 | public static function selectFromGame(string $id)
125 | {
126 | if (!self::isDbConnected()) {
127 | return false;
128 | }
129 |
130 | if (empty($id)) {
131 | throw new StorageException('Id is empty!');
132 | }
133 |
134 | try {
135 | $sth = self::$pdo->prepare(
136 | '
137 | SELECT * FROM `' . TB_GAME . '`
138 | WHERE `id` = :id
139 | '
140 | );
141 |
142 | $sth->bindParam(':id', $id);
143 |
144 | if ($result = $sth->execute()) {
145 | $result = $sth->fetchAll(PDO::FETCH_ASSOC);
146 |
147 | return isset($result[0]) ? json_decode($result[0]['data'], true) : [];
148 | }
149 |
150 | return false;
151 | } catch (PDOException $e) {
152 | throw new StorageException($e->getMessage());
153 | }
154 | }
155 |
156 | /**
157 | * Insert data to database
158 | *
159 | * @param string $id
160 | * @param array $data
161 | *
162 | * @return bool
163 | * @throws StorageException
164 | */
165 | public static function insertToGame(string $id, array $data): bool
166 | {
167 | if (!self::isDbConnected()) {
168 | return false;
169 | }
170 |
171 | if (empty($id)) {
172 | throw new StorageException('Id is empty!');
173 | }
174 |
175 | if (empty($data)) {
176 | throw new StorageException('Data is empty!');
177 | }
178 |
179 | try {
180 | $sth = self::$pdo->prepare(
181 | '
182 | INSERT INTO `' . TB_GAME . '`
183 | (`id`, `data`, `created_at`, `updated_at`)
184 | VALUES
185 | (:id, :data, :date, :date)
186 | ON DUPLICATE KEY UPDATE
187 | `data` = VALUES(`data`),
188 | `updated_at` = VALUES(`updated_at`)
189 | '
190 | );
191 |
192 | /** @noinspection CallableParameterUseCaseInTypeContextInspection */
193 | $data = json_encode($data);
194 | $date = date('Y-m-d H:i:s');
195 |
196 | $sth->bindParam(':id', $id);
197 | $sth->bindParam(':data', $data);
198 | $sth->bindParam(':date', $date);
199 |
200 | return $sth->execute();
201 | } catch (PDOException $e) {
202 | throw new StorageException($e->getMessage());
203 | }
204 | }
205 |
206 | /**
207 | * Delete data from storage
208 | *
209 | * @param string $id
210 | *
211 | * @return bool
212 | * @throws StorageException
213 | */
214 | public static function deleteFromGame(string $id): bool
215 | {
216 | if (!self::isDbConnected()) {
217 | return false;
218 | }
219 |
220 | if (empty($id)) {
221 | throw new StorageException('Id is empty!');
222 | }
223 |
224 | try {
225 | $sth = self::$pdo->prepare(
226 | '
227 | DELETE FROM `' . TB_GAME . '`
228 | WHERE `id` = :id
229 | '
230 | );
231 |
232 | $sth->bindParam(':id', $id);
233 |
234 | return $sth->execute();
235 | } catch (PDOException $e) {
236 | throw new StorageException($e->getMessage());
237 | }
238 | }
239 |
240 | /**
241 | * Basic file-powered lock to prevent other process accessing same game
242 | *
243 | * @param string $id
244 | *
245 | * @return bool
246 | *
247 | * @throws StorageException
248 | * @throws BotException
249 | */
250 | public static function lockGame(string $id): bool
251 | {
252 | if (!self::isDbConnected()) {
253 | return false;
254 | }
255 |
256 | if (empty($id)) {
257 | throw new StorageException('Id is empty!');
258 | }
259 |
260 | self::$lock = new TempFile($id);
261 | if (self::$lock->getFile() === null) {
262 | return false;
263 | }
264 |
265 | return flock(fopen(self::$lock->getFile()->getPathname(), 'ab+'), LOCK_EX);
266 | }
267 |
268 | /**
269 | * Unlock the game to allow access from other processes
270 | *
271 | * @param string $id
272 | *
273 | * @return bool
274 | *
275 | * @throws StorageException
276 | */
277 | public static function unlockGame(string $id): bool
278 | {
279 | if (!self::isDbConnected()) {
280 | return false;
281 | }
282 |
283 | if (empty($id)) {
284 | throw new StorageException('Id is empty!');
285 | }
286 |
287 | if (self::$lock === null) {
288 | throw new StorageException('No lock file object!');
289 | }
290 |
291 | if (self::$lock->getFile() === null) {
292 | return false;
293 | }
294 |
295 | return flock(fopen(self::$lock->getFile()->getPathname(), 'ab+'), LOCK_UN);
296 | }
297 |
298 | /**
299 | * Select multiple data from the database
300 | *
301 | * @param int $time
302 | *
303 | * @return array|bool
304 | * @throws StorageException
305 | */
306 | public static function listFromGame(int $time = 0)
307 | {
308 | if (!self::isDbConnected()) {
309 | return false;
310 | }
311 |
312 | if (!is_numeric($time)) {
313 | throw new StorageException('Time must be a number!');
314 | }
315 |
316 | if ($time >= 0) {
317 | $compare_sign = '<=';
318 | } else {
319 | $compare_sign = '>';
320 | }
321 |
322 | try {
323 | $sth = self::$pdo->prepare(
324 | '
325 | SELECT * FROM `' . TB_GAME . '`
326 | WHERE `updated_at` ' . $compare_sign . ' :date
327 | ORDER BY `updated_at` ASC
328 | '
329 | );
330 |
331 | $date = date('Y-m-d H:i:s', strtotime('-' . abs($time) . ' seconds'));
332 | $sth->bindParam(':date', $date);
333 |
334 | if ($sth->execute()) {
335 | //return $sth->fetchAll(PDO::FETCH_ASSOC);
336 |
337 | $results = [];
338 | while ($entry = $sth->fetch(PDO::FETCH_ASSOC)) {
339 | $json = json_decode($entry['data'], true);
340 | $data_stripped = json_encode(['game_code' => $json['game_code'] ?? null]);
341 |
342 | $results[] = [
343 | 'id' => $entry['id'],
344 | 'data' => $data_stripped,
345 | 'updated_at' => $entry['updated_at'],
346 | ];
347 | }
348 |
349 | return $results;
350 | }
351 |
352 | return false;
353 | } catch (PDOException $e) {
354 | throw new StorageException($e->getMessage());
355 | }
356 | }
357 | }
358 |
--------------------------------------------------------------------------------
/src/Storage/Driver/PostgreSQL.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Storage\Driver;
12 |
13 | use Bot\Entity\TempFile;
14 | use Bot\Exception\BotException;
15 | use Bot\Exception\StorageException;
16 | use PDO;
17 | use PDOException;
18 |
19 | /**
20 | * Class PostgreSQL
21 | */
22 | class PostgreSQL
23 | {
24 | /**
25 | * PDO object
26 | *
27 | * @var PDO
28 | */
29 | private static $pdo;
30 |
31 | /**
32 | * Lock file object
33 | *
34 | * @var TempFile
35 | */
36 | private static $lock;
37 |
38 | /**
39 | * SQL to create database structure
40 | *
41 | * @var string
42 | */
43 | private static $structure = 'CREATE TABLE IF NOT EXISTS game (
44 | id CHAR(255),
45 | data TEXT NOT NULL,
46 | created_at timestamp NULL DEFAULT NULL,
47 | updated_at timestamp NULL DEFAULT NULL,
48 |
49 | PRIMARY KEY (id)
50 | );';
51 |
52 | /**
53 | * Create table structure
54 | *
55 | * @return bool
56 | * @throws StorageException
57 | */
58 | public static function createStructure(): bool
59 | {
60 | if (!self::isDbConnected()) {
61 | self::initializeStorage();
62 | }
63 |
64 | if (!self::$pdo->query(self::$structure)) {
65 | throw new StorageException('Failed to create DB structure!');
66 | }
67 |
68 | return true;
69 | }
70 |
71 | /**
72 | * Check if database connection has been created
73 | *
74 | * @return bool
75 | */
76 | public static function isDbConnected(): bool
77 | {
78 | return self::$pdo !== null;
79 | }
80 |
81 | /**
82 | * Initialize PDO connection
83 | *
84 | * @throws StorageException
85 | */
86 | public static function initializeStorage(): bool
87 | {
88 | if (self::isDbConnected()) {
89 | return true;
90 | }
91 |
92 | if (!defined('TB_GAME')) {
93 | define('TB_GAME', 'game');
94 | }
95 |
96 | try {
97 | $dsn = parse_url(getenv('DATABASE_URL'));
98 |
99 | self::$pdo = new PDO('pgsql:' . 'host=' . $dsn['host'] . ';port=' . $dsn['port'] . ';dbname=' . ltrim($dsn['path'], '/'), $dsn['user'], $dsn['pass']);
100 | self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
101 | } catch (PDOException $e) {
102 | throw new StorageException('Connection to the database failed: ' . $e->getMessage());
103 | }
104 |
105 | return true;
106 | }
107 |
108 | /**
109 | * Select data from database
110 | *
111 | * @param string $id
112 | *
113 | * @return array|bool
114 | * @throws StorageException
115 | */
116 | public static function selectFromGame(string $id)
117 | {
118 | if (!self::isDbConnected()) {
119 | return false;
120 | }
121 |
122 | if (empty($id)) {
123 | throw new StorageException('Id is empty!');
124 | }
125 |
126 | try {
127 | $sth = self::$pdo->prepare(
128 | '
129 | SELECT * FROM ' . TB_GAME . '
130 | WHERE id = :id
131 | '
132 | );
133 |
134 | $sth->bindParam(':id', $id);
135 |
136 | if ($result = $sth->execute()) {
137 | $result = $sth->fetchAll(PDO::FETCH_ASSOC);
138 |
139 | return isset($result[0]) ? json_decode($result[0]['data'], true) : [];
140 | }
141 |
142 | return false;
143 | } catch (PDOException $e) {
144 | throw new StorageException($e->getMessage());
145 | }
146 | }
147 |
148 | /**
149 | * Insert data to database
150 | *
151 | * @param string $id
152 | * @param array $data
153 | *
154 | * @return bool
155 | * @throws StorageException
156 | */
157 | public static function insertToGame(string $id, array $data): bool
158 | {
159 | if (!self::isDbConnected()) {
160 | return false;
161 | }
162 |
163 | if (empty($id)) {
164 | throw new StorageException('Id is empty!');
165 | }
166 |
167 | if (empty($data)) {
168 | throw new StorageException('Data is empty!');
169 | }
170 |
171 | try {
172 | $sth = self::$pdo->prepare(
173 | '
174 | INSERT INTO ' . TB_GAME . '
175 | (id, data, created_at, updated_at)
176 | VALUES
177 | (:id, :data, :date, :date)
178 | ON CONFLICT (id) DO UPDATE
179 | SET data = :data,
180 | updated_at = :date
181 | '
182 | );
183 |
184 | /** @noinspection CallableParameterUseCaseInTypeContextInspection */
185 | $data = json_encode($data);
186 | $date = date('Y-m-d H:i:s');
187 |
188 | $sth->bindParam(':id', $id);
189 | $sth->bindParam(':data', $data);
190 | $sth->bindParam(':date', $date);
191 |
192 | return $sth->execute();
193 | } catch (PDOException $e) {
194 | throw new StorageException($e->getMessage());
195 | }
196 | }
197 |
198 | /**
199 | * Delete data from storage
200 | *
201 | * @param string $id
202 | *
203 | * @return array|bool|mixed
204 | * @throws StorageException
205 | */
206 | public static function deleteFromGame(string $id): bool
207 | {
208 | if (!self::isDbConnected()) {
209 | return false;
210 | }
211 |
212 | if (empty($id)) {
213 | throw new StorageException('Id is empty!');
214 | }
215 |
216 | try {
217 | $sth = self::$pdo->prepare(
218 | '
219 | DELETE FROM ' . TB_GAME . '
220 | WHERE id = :id
221 | '
222 | );
223 |
224 | $sth->bindParam(':id', $id);
225 |
226 | return $sth->execute();
227 | } catch (PDOException $e) {
228 | throw new StorageException($e->getMessage());
229 | }
230 | }
231 |
232 | /**
233 | * Basic file-powered lock to prevent other process accessing same game
234 | *
235 | * @param string $id
236 | *
237 | * @return bool
238 | *
239 | * @throws StorageException
240 | * @throws BotException
241 | */
242 | public static function lockGame(string $id): bool
243 | {
244 | if (!self::isDbConnected()) {
245 | return false;
246 | }
247 |
248 | if (empty($id)) {
249 | throw new StorageException('Id is empty!');
250 | }
251 |
252 | self::$lock = new TempFile($id);
253 | if (self::$lock->getFile() === null) {
254 | return false;
255 | }
256 |
257 | return flock(fopen(self::$lock->getFile()->getPathname(), 'ab+'), LOCK_EX);
258 | }
259 |
260 | /**
261 | * Unlock the game to allow access from other processes
262 | *
263 | * @param string $id
264 | *
265 | * @return bool
266 | *
267 | * @throws StorageException
268 | */
269 | public static function unlockGame(string $id): bool
270 | {
271 | if (!self::isDbConnected()) {
272 | return false;
273 | }
274 |
275 | if (empty($id)) {
276 | throw new StorageException('Id is empty!');
277 | }
278 |
279 | if (self::$lock === null) {
280 | throw new StorageException('No lock file object!');
281 | }
282 |
283 | if (self::$lock->getFile() === null) {
284 | return false;
285 | }
286 |
287 | return flock(fopen(self::$lock->getFile()->getPathname(), 'ab+'), LOCK_UN);
288 | }
289 |
290 | /**
291 | * Select multiple data from the database
292 | *
293 | * @param int $time
294 | *
295 | * @return array|bool
296 | * @throws StorageException
297 | */
298 | public static function listFromGame(int $time = 0)
299 | {
300 | if (!self::isDbConnected()) {
301 | return false;
302 | }
303 |
304 | if (!is_numeric($time)) {
305 | throw new StorageException('Time must be a number!');
306 | }
307 |
308 | if ($time >= 0) {
309 | $compare_sign = '<=';
310 | } else {
311 | $compare_sign = '>';
312 | }
313 |
314 | try {
315 | $sth = self::$pdo->prepare(
316 | '
317 | SELECT * FROM ' . TB_GAME . '
318 | WHERE updated_at ' . $compare_sign . ' :date
319 | ORDER BY updated_at ASC
320 | '
321 | );
322 |
323 | $date = date('Y-m-d H:i:s', strtotime('-' . abs($time) . ' seconds'));
324 | $sth->bindParam(':date', $date);
325 |
326 | if ($sth->execute()) {
327 | //return $sth->fetchAll(PDO::FETCH_ASSOC);
328 |
329 | $results = [];
330 | while ($entry = $sth->fetch(PDO::FETCH_ASSOC)) {
331 | $json = json_decode($entry['data'], true);
332 | $data_stripped = json_encode(['game_code' => $json['game_code'] ?? null]);
333 |
334 | $results[] = [
335 | 'id' => $entry['id'],
336 | 'data' => $data_stripped,
337 | 'updated_at' => $entry['updated_at'],
338 | ];
339 | }
340 |
341 | return $results;
342 | }
343 |
344 | return false;
345 | } catch (PDOException $e) {
346 | throw new StorageException($e->getMessage());
347 | }
348 | }
349 | }
350 |
--------------------------------------------------------------------------------
/src/Storage/Storage.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot\Storage;
12 |
13 | use Bot\Exception\StorageException;
14 | use Bot\Helper\Utilities;
15 | use Bot\Storage\Driver\BotDB;
16 | use Bot\Storage\Driver\File;
17 | use Longman\TelegramBot\DB;
18 |
19 | /**
20 | * Picks the best storage driver available
21 | */
22 | class Storage
23 | {
24 | /**
25 | * Supported database engines
26 | */
27 | private static $storage_drivers = [
28 | 'mysql' => 'MySQL',
29 | 'pgsql' => 'PostgreSQL',
30 | 'postgres' => 'PostgreSQL',
31 | 'memcache' => 'Memcache',
32 | 'memcached' => 'Memcache',
33 | ];
34 |
35 | /**
36 | * Return which driver class to use
37 | *
38 | * @return string
39 | *
40 | * @throws StorageException
41 | */
42 | public static function getClass(): string
43 | {
44 | if (!empty($debug_storage = getenv('STORAGE_CLASS'))) {
45 | $storage = str_replace('"', '', $debug_storage);
46 | Utilities::debugPrint('Forcing storage: \'' . $storage . '\'');
47 | } elseif (DB::isDbConnected()) {
48 | $storage = BotDB::class;
49 | } elseif (getenv('DATABASE_URL')) {
50 | $dsn = parse_url(getenv('DATABASE_URL'));
51 |
52 | if (!isset(self::$storage_drivers[$dsn['scheme']])) {
53 | throw new StorageException('Unsupported database type!');
54 | }
55 |
56 | $storage = 'Bot\Storage\Driver\\' . (self::$storage_drivers[$dsn['scheme']] ?: '');
57 | } elseif (defined('DATA_PATH')) {
58 | $storage = File::class;
59 | }
60 |
61 | if (empty($storage)) {
62 | /** @noinspection PhpUndefinedVariableInspection */
63 | throw new StorageException('Storage class not provided');
64 | }
65 |
66 | if (!class_exists($storage)) {
67 | /** @noinspection PhpUndefinedVariableInspection */
68 | throw new StorageException('Storage class doesn\'t exist: ' . $storage);
69 | }
70 |
71 | Utilities::debugPrint('Using storage: \'' . $storage . '\'');
72 |
73 | return $storage;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/TelegramBot.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Bot;
12 |
13 | use Bot\Helper\Language;
14 | use Longman\TelegramBot\Entities\ServerResponse;
15 | use Longman\TelegramBot\Entities\Update;
16 | use Longman\TelegramBot\Telegram;
17 |
18 | class TelegramBot extends Telegram
19 | {
20 | /**
21 | * @param Update $update
22 | *
23 | * @return ServerResponse
24 | */
25 | public function processUpdate(Update $update): ServerResponse
26 | {
27 | Language::set(Language::getDefaultLanguage()); // Set default language before handling each update
28 |
29 | return parent::processUpdate($update);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # This should be started through Procfile on Fly.io
3 |
4 | echo "Starting worker process in the background..."
5 | ./worker.sh &
6 |
7 | echo "Starting web process..."
8 | vendor/bin/heroku-php-nginx -C nginx.inc.conf public/
9 |
--------------------------------------------------------------------------------
/translations/messages.pot:
--------------------------------------------------------------------------------
1 | #, fuzzy
2 | msgid ""
3 | msgstr ""
4 | "Project-Id-Version: \n"
5 | "Report-Msgid-Bugs-To: \n"
6 | "Last-Translator: @jacklul\n"
7 | "Language-Team: \n"
8 | "MIME-Version: 1.0\n"
9 | "Content-Type: text/plain; charset=UTF-8\n"
10 | "Content-Transfer-Encoding: 8bit\n"
11 | "POT-Creation-Date: 2021-09-16 16:00+0200\n"
12 | "PO-Revision-Date: 2017-07-13T17:47:54+00:00\n"
13 | "X-Poedit-Basepath: ../src\n"
14 | "X-Poedit-KeywordsList: __\n"
15 | "X-Generator: Poedit 2.2.4\n"
16 | "X-Poedit-SearchPath-0: .\n"
17 |
18 | #: Command/Admin/CleansessionsCommand.php:149
19 | msgid "Create New Session"
20 | msgstr ""
21 |
22 | #: Command/Admin/CleansessionsCommand.php:156
23 | msgid "This game session has expired."
24 | msgstr ""
25 |
26 | #: Command/Admin/CleansessionsCommand.php:213
27 | #: Command/System/InlinequeryCommand.php:124 Entity/Game.php:805
28 | msgid "Create"
29 | msgstr ""
30 |
31 | #: Command/System/CallbackqueryCommand.php:73
32 | msgid "Bad request!"
33 | msgstr ""
34 |
35 | #: Command/System/InlinequeryCommand.php:49 Entity/Game.php:634
36 | #: Entity/Game.php:708
37 | msgid "This game session is empty."
38 | msgstr ""
39 |
40 | #: Command/User/StartCommand.php:60
41 | msgid "Hi!"
42 | msgstr ""
43 |
44 | #: Command/User/StartCommand.php:61 Command/User/StartCommand.php:72
45 | #: Entity/Game.php:558 Entity/Game.php:787 Entity/Game.php:865
46 | msgid "Play"
47 | msgstr ""
48 |
49 | #: Command/User/StartCommand.php:61
50 | msgid "To begin, start a message with {USAGE} in any of your chats or click the {BUTTON} button and then select a chat to play in."
51 | msgstr ""
52 |
53 | #: Entity/Game.php:232
54 | msgid "This game session has crashed."
55 | msgstr ""
56 |
57 | #: Entity/Game.php:464
58 | msgid "This game is already created!"
59 | msgstr ""
60 |
61 | #: Entity/Game.php:477 Entity/Game.php:545 Entity/Game.php:623
62 | #: Entity/Game.php:648 Entity/Game.php:685 Entity/Game.php:790
63 | #: Entity/Game/Russianroulette.php:166
64 | msgid "{PLAYER_HOST} is waiting for opponent to join..."
65 | msgstr ""
66 |
67 | #: Entity/Game.php:477 Entity/Game.php:545 Entity/Game.php:623
68 | #: Entity/Game.php:648 Entity/Game.php:685 Entity/Game.php:790
69 | #: Entity/Game.php:843 Entity/Game/Russianroulette.php:166
70 | #: Entity/Game/Russianroulette.php:318
71 | msgid "Join"
72 | msgstr ""
73 |
74 | #: Entity/Game.php:477 Entity/Game.php:545 Entity/Game.php:623
75 | #: Entity/Game.php:648 Entity/Game.php:685 Entity/Game.php:790
76 | #: Entity/Game/Russianroulette.php:166
77 | msgid "Press {BUTTON} button to join."
78 | msgstr ""
79 |
80 | #: Entity/Game.php:558 Entity/Game.php:787
81 | msgid "{PLAYER_GUEST} joined..."
82 | msgstr ""
83 |
84 | #: Entity/Game.php:558 Entity/Game.php:787
85 | msgid "Waiting for {PLAYER} to start..."
86 | msgstr ""
87 |
88 | #: Entity/Game.php:558 Entity/Game.php:787
89 | msgid "Press {BUTTON} button to start."
90 | msgstr ""
91 |
92 | #: Entity/Game.php:564
93 | msgid "You cannot play with yourself!"
94 | msgstr ""
95 |
96 | #: Entity/Game.php:567
97 | msgid "This game is full!"
98 | msgstr ""
99 |
100 | #: Entity/Game.php:610 Entity/Game.php:712 Entity/Game.php:743
101 | #: Entity/Game/Checkers.php:111 Entity/Game/Checkers.php:372
102 | #: Entity/Game/Checkers.php:421 Entity/Game/Connectfour.php:87
103 | #: Entity/Game/Poolcheckers.php:80 Entity/Game/Poolcheckers.php:133
104 | #: Entity/Game/Rockpaperscissors.php:75 Entity/Game/Russianroulette.php:75
105 | #: Entity/Game/Tictactoe.php:84
106 | msgid "You're not in this game!"
107 | msgstr ""
108 |
109 | #: Entity/Game.php:623 Entity/Game.php:648
110 | msgid "{PLAYER} quit..."
111 | msgstr ""
112 |
113 | #: Entity/Game.php:623
114 | msgid "{PLAYER_HOST} is now the host."
115 | msgstr ""
116 |
117 | #: Entity/Game.php:671 Entity/Game.php:716 Entity/Game.php:761
118 | msgid "You're not the host!"
119 | msgstr ""
120 |
121 | #: Entity/Game.php:675
122 | msgid "There is no player to kick!"
123 | msgstr ""
124 |
125 | #: Entity/Game.php:685
126 | msgid "{PLAYER_GUEST} was kicked..."
127 | msgstr ""
128 |
129 | #: Entity/Game.php:837 Entity/Game.php:885 Entity/Game.php:961
130 | #: Entity/Game/Checkers.php:278 Entity/Game/Poolcheckers.php:820
131 | #: Entity/Game/Rockpaperscissors.php:298
132 | #: Entity/Game/Rockpaperscissorslizardspock.php:243
133 | #: Entity/Game/Russianroulette.php:297 Entity/Game/Russianroulette.php:312
134 | msgid "Quit"
135 | msgstr ""
136 |
137 | #: Entity/Game.php:891 Entity/Game.php:967 Entity/Game/Checkers.php:284
138 | #: Entity/Game/Poolcheckers.php:826 Entity/Game/Rockpaperscissors.php:304
139 | #: Entity/Game/Rockpaperscissorslizardspock.php:249
140 | #: Entity/Game/Russianroulette.php:303
141 | msgid "Kick"
142 | msgstr ""
143 |
144 | #: Entity/Game.php:951 Entity/Game/Checkers.php:248
145 | #: Entity/Game/Poolcheckers.php:790 Entity/Game/Rockpaperscissors.php:277
146 | #: Entity/Game/Rockpaperscissorslizardspock.php:222
147 | msgid "Play again!"
148 | msgstr ""
149 |
150 | #: Entity/Game.php:1037
151 | msgid "Game session not found or expired."
152 | msgstr ""
153 |
154 | #: Entity/Game/Checkers.php:117 Entity/Game/Checkers.php:378
155 | #: Entity/Game/Checkers.php:464 Entity/Game/Connectfour.php:121
156 | #: Entity/Game/Poolcheckers.php:86 Entity/Game/Poolcheckers.php:176
157 | #: Entity/Game/Rockpaperscissors.php:103 Entity/Game/Russianroulette.php:109
158 | #: Entity/Game/Tictactoe.php:119
159 | msgid "This game has ended!"
160 | msgstr ""
161 |
162 | #: Entity/Game/Checkers.php:129 Entity/Game/Checkers.php:154
163 | #: Entity/Game/Checkers.php:590 Entity/Game/Checkers.php:592
164 | #: Entity/Game/Connectfour.php:165 Entity/Game/Poolcheckers.php:304
165 | #: Entity/Game/Poolcheckers.php:306 Entity/Game/Russianroulette.php:137
166 | #: Entity/Game/Russianroulette.php:150 Entity/Game/Tictactoe.php:153
167 | msgid "{PLAYER} won!"
168 | msgstr ""
169 |
170 | #: Entity/Game/Checkers.php:130 Entity/Game/Checkers.php:155
171 | msgid "{PLAYER} surrendered!"
172 | msgstr ""
173 |
174 | #: Entity/Game/Checkers.php:146 Entity/Game/Checkers.php:171
175 | msgid "Press the button again to surrender!"
176 | msgstr ""
177 |
178 | #: Entity/Game/Checkers.php:258 Entity/Game/Poolcheckers.php:800
179 | msgid "Surrender"
180 | msgstr ""
181 |
182 | #: Entity/Game/Checkers.php:267 Entity/Game/Poolcheckers.php:809
183 | msgid "Vote to draw"
184 | msgstr ""
185 |
186 | #: Entity/Game/Checkers.php:406 Entity/Game/Poolcheckers.php:118
187 | msgid "You already voted!"
188 | msgstr ""
189 |
190 | #: Entity/Game/Checkers.php:486 Entity/Game/Checkers.php:509
191 | #: Entity/Game/Checkers.php:557 Entity/Game/Poolcheckers.php:197
192 | #: Entity/Game/Poolcheckers.php:220 Entity/Game/Poolcheckers.php:271
193 | msgid "You must make a jump when possible!"
194 | msgstr ""
195 |
196 | #: Entity/Game/Checkers.php:561 Entity/Game/Checkers.php:571
197 | #: Entity/Game/Connectfour.php:149 Entity/Game/Connectfour.php:154
198 | #: Entity/Game/Poolcheckers.php:275 Entity/Game/Poolcheckers.php:285
199 | #: Entity/Game/Rockpaperscissors.php:122 Entity/Game/Russianroulette.php:127
200 | #: Entity/Game/Tictactoe.php:127 Entity/Game/Tictactoe.php:143
201 | msgid "Invalid move!"
202 | msgstr ""
203 |
204 | #: Entity/Game/Checkers.php:569 Entity/Game/Poolcheckers.php:283
205 | msgid "Invalid selection!"
206 | msgstr ""
207 |
208 | #: Entity/Game/Checkers.php:575 Entity/Game/Connectfour.php:125
209 | #: Entity/Game/Poolcheckers.php:289 Entity/Game/Russianroulette.php:113
210 | #: Entity/Game/Tictactoe.php:123
211 | msgid "It's not your turn!"
212 | msgstr ""
213 |
214 | #: Entity/Game/Checkers.php:594 Entity/Game/Connectfour.php:167
215 | #: Entity/Game/Poolcheckers.php:308 Entity/Game/Tictactoe.php:155
216 | msgid "Game ended with a draw!"
217 | msgstr ""
218 |
219 | #: Entity/Game/Checkers.php:605 Entity/Game/Checkers.php:607
220 | #: Entity/Game/Poolcheckers.php:319 Entity/Game/Poolcheckers.php:321
221 | msgid "{PLAYER} voted to draw!"
222 | msgstr ""
223 |
224 | #: Entity/Game/Checkers.php:613 Entity/Game/Poolcheckers.php:327
225 | msgid "(Select the piece you want to move)"
226 | msgstr ""
227 |
228 | #: Entity/Game/Checkers.php:615 Entity/Game/Poolcheckers.php:329
229 | msgid "(Selected: {COORDINATES})"
230 | msgstr ""
231 |
232 | #: Entity/Game/Checkers.php:618 Entity/Game/Poolcheckers.php:332
233 | msgid "(Make your move or select different piece)"
234 | msgstr ""
235 |
236 | #: Entity/Game/Checkers.php:620 Entity/Game/Poolcheckers.php:334
237 | msgid "(Your move must continue)"
238 | msgstr ""
239 |
240 | #: Entity/Game/Rockpaperscissors.php:140 Entity/Game/Rockpaperscissors.php:144
241 | msgid "{PLAYER} won this round!"
242 | msgstr ""
243 |
244 | #: Entity/Game/Rockpaperscissors.php:146
245 | msgid "This round ended with a draw!"
246 | msgstr ""
247 |
248 | #: Entity/Game/Rockpaperscissors.php:155 Entity/Game/Rockpaperscissors.php:159
249 | msgid "{PLAYER} won the game!"
250 | msgstr ""
251 |
252 | #: Entity/Game/Rockpaperscissors.php:163
253 | msgid "Round {ROUND} - make your picks!"
254 | msgstr ""
255 |
256 | #: Entity/Game/Rockpaperscissors.php:166 Entity/Game/Rockpaperscissors.php:168
257 | msgid "Waiting for:"
258 | msgstr ""
259 |
260 | #: Entity/Game/Russianroulette.php:136 Entity/Game/Russianroulette.php:149
261 | msgid "{PLAYER} died! (kicked)"
262 | msgstr ""
263 |
264 | #: Entity/Game/Russianroulette.php:170
265 | msgid "{PLAYER} survived!"
266 | msgstr ""
267 |
268 | #: GameCore.php:127
269 | msgid "Database error!"
270 | msgstr ""
271 |
272 | #: GameCore.php:127 GameCore.php:257 GameCore.php:281 GameCore.php:305
273 | #: GameCore.php:329
274 | msgid "Try again in a few seconds."
275 | msgstr ""
276 |
277 | #: GameCore.php:257
278 | msgid "Process for this game is busy!"
279 | msgstr ""
280 |
281 | #: GameCore.php:281
282 | msgid "Telegram API error!"
283 | msgstr ""
284 |
285 | #: GameCore.php:305
286 | msgid "Bot error!"
287 | msgstr ""
288 |
289 | #: GameCore.php:329
290 | msgid "Unhandled error!"
291 | msgstr ""
292 |
--------------------------------------------------------------------------------
/worker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # This runs build-in worker to periodically clean up older games
3 |
4 | # In case process crashes we it will restart indefinitely
5 | while true; do
6 | php bin/console worker
7 |
8 | echo "Worker process crashed, restarting in 60 seconds..."
9 | sleep 60
10 | done
11 |
--------------------------------------------------------------------------------