From ab546df14dad97a80de082d065be7bb05488def9 Mon Sep 17 00:00:00 2001 From: ghost Date: Thu, 15 Feb 2024 23:15:00 +0200 Subject: [PATCH] implement comission charge for publications --- .env | 29 ++- README.md | 20 +- compose.override.yaml | 8 + compose.yaml | 21 +++ composer.json | 9 +- config/bundles.php | 3 + config/packages/doctrine.yaml | 50 +++++ config/packages/doctrine_migrations.yaml | 6 + config/services.yaml | 5 + migrations/.gitignore | 0 public/css/default.css | 7 + src/Controller/CrontabController.php | 141 ++++++++++++++ src/Controller/ModuleController.php | 4 +- src/Controller/RoomController.php | 226 +++++++++++++++++++++-- src/Controller/UserController.php | 83 ++++++++- src/Entity/.gitignore | 0 src/Entity/Pool.php | 141 ++++++++++++++ src/Repository/.gitignore | 0 src/Repository/PoolRepository.php | 23 +++ src/Twig/AppExtension.php | 78 ++++++++ symfony.lock | 36 ++++ templates/default/module/post.html.twig | 3 + templates/default/module/room.html.twig | 20 +- templates/default/room/index.html.twig | 12 +- templates/default/user/join.html.twig | 6 + 25 files changed, 895 insertions(+), 36 deletions(-) create mode 100644 compose.override.yaml create mode 100644 compose.yaml create mode 100644 config/packages/doctrine.yaml create mode 100644 config/packages/doctrine_migrations.yaml create mode 100644 migrations/.gitignore create mode 100644 src/Controller/CrontabController.php create mode 100644 src/Entity/.gitignore create mode 100644 src/Entity/Pool.php create mode 100644 src/Repository/.gitignore create mode 100644 src/Repository/PoolRepository.php diff --git a/.env b/.env index fd1ff9d..febaba2 100644 --- a/.env +++ b/.env @@ -19,7 +19,7 @@ APP_ENV=dev APP_SECRET=EDIT_ME ###< symfony/framework-bundle ### -APP_VERSION=1.8.4 +APP_VERSION=1.9.0 APP_NAME=KevaChat @@ -101,4 +101,29 @@ APP_ADD_POST_REMOTE_IP_DENIED= APP_ADD_POST_KEY_REGEX=/^([\d]+)@([A-z0-9\.\:\[\]]+)$/ # Post content rules (for kevacoin value, max length is 3072) -APP_ADD_POST_VALUE_REGEX=/.*/ui \ No newline at end of file +APP_ADD_POST_VALUE_REGEX=/.*/ui + +# Post cost (set 0 for free publications) +APP_ADD_POST_COST_KVA=1 + +# Room cost (set 0 for free publications) +APP_ADD_ROOM_COST_KVA=100 + +# User cost (set 0 for free registration) +APP_ADD_USER_COST_KVA=100 + +# Quantity of payment confirmations to send message to blockchain +APP_POOL_CONFIRMATIONS=1 + +# Cleanup abandoned messages without payment after seconds timeout +APP_POOL_TIMEOUT=3600 + +###> doctrine/doctrine-bundle ### +# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml +# +DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" +# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4" +# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" +# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" +###< doctrine/doctrine-bundle ### diff --git a/README.md b/README.md index 2ed265b..8af8aa1 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Instance require connection to the [Kevacoin](https://github.com/kevacoin-projec KevaChat following open wallet model, where community boost shared ballance for talks. -* In another way, node administrators able to provide unique payment addresses to each message sent and charge commission for instance monetization. Currently, this model not implemented because of about zero conversation cost in KevaCoin network. +* In another way, node administrators able to provide unique payment addresses to each message sent and charge commission for instance monetization. Administrators have flexible settings of access levels explained in the `.env` file: read-only rooms, connection and post limits, etc. @@ -30,15 +30,19 @@ All messages related to their room `namespaces`. ## Install -### Production +* `git clone https://github.com/kevachat/webapp.git` +* `cd webapp` +* `composer update` +* `php bin/console doctrine:schema:update --force` +* `* * * * * php crontab/pool` -`composer create-project kevachat/webapp KevaChat` +## Update -### Development - -* `git clone https://github.com/kevachat/webapp.git KevaChat` -* `cd KevaChat` -* `composer install` +`cd webapp` +`git pull` +`composer update` +`php bin/console doctrine:migrations:migrate` +`APP_ENV=prod APP_DEBUG=0 php bin/console cache:clear` ## Setup diff --git a/compose.override.yaml b/compose.override.yaml new file mode 100644 index 0000000..f2247d5 --- /dev/null +++ b/compose.override.yaml @@ -0,0 +1,8 @@ +version: '3' + +services: +###> doctrine/doctrine-bundle ### + database: + ports: + - "5432" +###< doctrine/doctrine-bundle ### diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..1abf6c6 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,21 @@ +version: '3' + +services: +###> doctrine/doctrine-bundle ### + database: + image: postgres:${POSTGRES_VERSION:-16}-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-app} + # You should definitely change the password in production + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!} + POSTGRES_USER: ${POSTGRES_USER:-app} + volumes: + - database_data:/var/lib/postgresql/data:rw + # You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data! + # - ./docker/db/data:/var/lib/postgresql/data:rw +###< doctrine/doctrine-bundle ### + +volumes: +###> doctrine/doctrine-bundle ### + database_data: +###< doctrine/doctrine-bundle ### diff --git a/composer.json b/composer.json index c16a785..58d594a 100644 --- a/composer.json +++ b/composer.json @@ -10,8 +10,11 @@ "ext-ctype": "*", "ext-iconv": "*", "clitor-is-protocol/kevacoin": "^1.0", + "doctrine/doctrine-bundle": "^2.11", + "doctrine/doctrine-migrations-bundle": "^3.3", + "doctrine/orm": "^3.0", "jdenticon/jdenticon": "^1.0", - "kevachat/kevacoin": "^1.0", + "kevachat/kevacoin": "^1.7", "league/commonmark": "^2.4", "symfony/console": "7.0.*", "symfony/dotenv": "7.0.*", @@ -23,6 +26,7 @@ "symfony/twig-bundle": "7.0.*", "symfony/yaml": "7.0.*", "twig/extra-bundle": "^3.8", + "twig/intl-extra": "^3.8", "twig/markdown-extra": "^3.8" }, "config": { @@ -73,5 +77,8 @@ "allow-contrib": false, "require": "7.0.*" } + }, + "require-dev": { + "symfony/maker-bundle": "^1.54" } } diff --git a/config/bundles.php b/config/bundles.php index 70982cc..7de78a1 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -5,4 +5,7 @@ return [ Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], ]; diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml new file mode 100644 index 0000000..d42c52d --- /dev/null +++ b/config/packages/doctrine.yaml @@ -0,0 +1,50 @@ +doctrine: + dbal: + url: '%env(resolve:DATABASE_URL)%' + + # IMPORTANT: You MUST configure your server version, + # either here or in the DATABASE_URL env var (see .env file) + #server_version: '16' + + profiling_collect_backtrace: '%kernel.debug%' + use_savepoints: true + orm: + auto_generate_proxy_classes: true + enable_lazy_ghost_objects: true + report_fields_where_declared: true + validate_xml_mapping: true + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + auto_mapping: true + mappings: + App: + type: attribute + is_bundle: false + dir: '%kernel.project_dir%/src/Entity' + prefix: 'App\Entity' + alias: App + +when@test: + doctrine: + dbal: + # "TEST_TOKEN" is typically set by ParaTest + dbname_suffix: '_test%env(default::TEST_TOKEN)%' + +when@prod: + doctrine: + orm: + auto_generate_proxy_classes: false + proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + + framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system diff --git a/config/packages/doctrine_migrations.yaml b/config/packages/doctrine_migrations.yaml new file mode 100644 index 0000000..29231d9 --- /dev/null +++ b/config/packages/doctrine_migrations.yaml @@ -0,0 +1,6 @@ +doctrine_migrations: + migrations_paths: + # namespace is arbitrary but should be different from App\Migrations + # as migrations classes should NOT be autoloaded + 'DoctrineMigrations': '%kernel.project_dir%/migrations' + enable_profiler: false diff --git a/config/services.yaml b/config/services.yaml index 1b6eb0c..9972146 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -36,6 +36,11 @@ parameters: app.add.post.remote.ip.denied: '%env(APP_ADD_POST_REMOTE_IP_DENIED)%' app.add.post.key.regex: '%env(APP_ADD_POST_KEY_REGEX)%' app.add.post.value.regex: '%env(APP_ADD_POST_VALUE_REGEX)%' + app.add.post.cost.kva: '%env(APP_ADD_POST_COST_KVA)%' + app.add.room.cost.kva: '%env(APP_ADD_ROOM_COST_KVA)%' + app.add.user.cost.kva: '%env(APP_ADD_USER_COST_KVA)%' + app.pool.confirmations: '%env(APP_POOL_CONFIRMATIONS)%' + app.pool.timeout: '%env(APP_POOL_TIMEOUT)%' app.moderator.remote.ip: '%env(APP_MODERATOR_REMOTE_IP)%' services: diff --git a/migrations/.gitignore b/migrations/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/public/css/default.css b/public/css/default.css index b74ddbf..b583858 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -44,6 +44,13 @@ body font-size: 12px; } +form > span +{ + float: right; + line-height: 24px; + padding: 0 8px; +} + /* header */ header diff --git a/src/Controller/CrontabController.php b/src/Controller/CrontabController.php new file mode 100644 index 0000000..ac3dcc2 --- /dev/null +++ b/src/Controller/CrontabController.php @@ -0,0 +1,141 @@ +getParameter('app.kevacoin.protocol'), + $this->getParameter('app.kevacoin.host'), + $this->getParameter('app.kevacoin.port'), + $this->getParameter('app.kevacoin.username'), + $this->getParameter('app.kevacoin.password') + ); + + // Get room list + $rooms = []; + + foreach ((array) $client->kevaListNamespaces() as $value) + { + $rooms[$value['namespaceId']] = mb_strtolower($value['displayName']); + } + + // Skip room lock events + if (empty($rooms)) + { + return new Response(); // @TODO + } + + // Get pending from payment pool + foreach ($entity->getRepository(Pool::class)->findBy( + [ + 'sent' => 0, + 'expired' => 0 + ] + ) as $pool) + { + // Payment received, send to blockchain + if ($client->getReceivedByAddress($pool->getAddress(), $this->getParameter('app.pool.confirmations')) >= $pool->getCost()) + { + // Check physical wallet balance + if ($client->getBalance() <= $pool->getCost()) + { + break; // @TODO exception + } + + // Is room request + else if ('_KEVA_NS_' == $pool->getKey()) + { + // Check room name not taken + if (in_array(mb_strtolower($pool->getValue()), $rooms)) + { + continue; // @TODO exception + } + + // Create new room record + if ($client->kevaNamespace($pool->getValue())) + { + // Update status + $pool->setSent( + time() + ); + + $entity->persist( + $pool + ); + + $entity->flush(); + } + } + + // Is regular key/value request + else + { + // Check namespace is valid + if (!isset($rooms[$pool->getNamespace()])) + { + continue; // @TODO exception + } + + if ($client->kevaPut($pool->getNamespace(), $pool->getKey(), $pool->getValue())) + { + // Update status + $pool->setSent( + time() + ); + + $entity->persist( + $pool + ); + + $entity->flush(); + } + } + } + + // Record expired + else + { + if ($pool->getTime() + $this->getParameter('app.pool.timeout') >= time()) + { + // Update status + $pool->setExpired( + time() + ); + + $entity->persist( + $pool + ); + + $entity->flush(); + } + } + } + + return new Response(); // @TODO + } +} \ No newline at end of file diff --git a/src/Controller/ModuleController.php b/src/Controller/ModuleController.php index 6d8d5cc..005f123 100644 --- a/src/Controller/ModuleController.php +++ b/src/Controller/ModuleController.php @@ -204,6 +204,7 @@ class ModuleController extends AbstractController 'sign' => $sign, 'message' => $message, 'username' => $username, + 'cost' => $this->getParameter('app.add.post.cost.kva'), 'enabled' => ( !in_array( @@ -224,7 +225,8 @@ class ModuleController extends AbstractController return $this->render( 'default/module/room.html.twig', [ - 'request' => $request + 'request' => $request, + 'cost' => $this->getParameter('app.add.room.cost.kva') ] ); } diff --git a/src/Controller/RoomController.php b/src/Controller/RoomController.php index 89c80e6..2dbf5d7 100644 --- a/src/Controller/RoomController.php +++ b/src/Controller/RoomController.php @@ -9,6 +9,10 @@ use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; +use Doctrine\ORM\EntityManagerInterface; + +use App\Entity\Pool; + class RoomController extends AbstractController { #[Route( @@ -160,7 +164,8 @@ class RoomController extends AbstractController ] )] public function room( - Request $request + Request $request, + EntityManagerInterface $entity ): Response { // Connect kevacoin @@ -175,6 +180,54 @@ class RoomController extends AbstractController // Get room feed $feed = []; + // Get pending from payment pool + foreach ($entity->getRepository(Pool::class)->findBy( + [ + 'namespace' => $request->get('namespace'), + 'sent' => 0, + 'expired' => 0 + ] + ) as $pending) + { + // Require valid kevachat meta + if ($data = $this->_post( + [ + 'key' => $pending->getKey(), + 'value' => $pending->getValue(), + 'txid' => hash( // @TODO tmp solution as required for tree building + 'sha256', + rand() + ) + ] + )) + { + // Detect parent post + preg_match('/^@([A-z0-9]{64})\s/i', $data->message, $mention); + $feed[$data->id] = + [ + 'id' => $data->id, + 'user' => $data->user, + 'icon' => $data->icon, + 'time' => $data->time, + 'parent' => isset($mention[1]) ? $mention[1] : null, + 'message' => trim( + preg_replace( // remove mention from folded message + '/^@([A-z0-9]{64})\s/i', + '', + $data->message + ) + ), + 'pending' => true, + 'pool' => + [ + 'cost' => $pending->getCost(), + 'address' => $pending->getAddress(), + 'expires' => $pending->getTime() + $this->getParameter('app.pool.timeout') + ] + ]; + } + } + // Get pending paradise foreach ((array) $client->kevaPending() as $pending) // @TODO relate to this room { @@ -318,7 +371,8 @@ class RoomController extends AbstractController )] public function post( Request $request, - TranslatorInterface $translator + TranslatorInterface $translator, + EntityManagerInterface $entity ): Response { // Check maintenance mode disabled @@ -550,19 +604,55 @@ class RoomController extends AbstractController ); } - // Send message to DHT - if ( - $client->kevaPut( - $request->get('namespace'), + // Post has commission cost, send message to pending payment pool + if ($this->getParameter('app.add.post.cost.kva') > 0) + { + $time = time(); + + $pool = new Pool(); + + $pool->setTime( + $time + ); + + $pool->setSent( + 0 + ); + + $pool->setExpired( + 0 + ); + + $pool->setCost( + $this->getParameter('app.add.post.cost.kva') + ); + + $pool->setAddress( + $client->getNewAddress() + ); + + $pool->setNamespace( + $request->get('namespace') + ); + + $pool->setKey( sprintf( '%s@%s', - time(), // @TODO save timestamp as part of key to keep timing actual for the chat feature + $time, $username - ), + ) + ); + + $pool->setValue( $request->get('message') - ) - ) - { + ); + + $entity->persist( + $pool + ); + + $entity->flush(); + // Register event time $memcached->set( $memory, @@ -584,6 +674,43 @@ class RoomController extends AbstractController ); } + // Post has zero cost, send message to DHT + else + { + if ( + $client->kevaPut( + $request->get('namespace'), + sprintf( + '%s@%s', + time(), // @TODO save timestamp as part of key to keep timing actual for the chat feature + $username + ), + $request->get('message') + ) + ) + { + // Register event time + $memcached->set( + $memory, + time(), + (int) $this->getParameter('app.add.post.remote.ip.delay') // auto remove on cache expire + ); + + // Redirect back to room + return $this->redirectToRoute( + 'room_namespace', + [ + 'mode' => $request->get('mode'), + 'namespace' => $request->get('namespace'), + 'sign' => $request->get('sign'), + 'error' => null, + 'message' => null, + '_fragment' => 'latest' + ] + ); + } + } + // Something went wrong, return error message return $this->redirectToRoute( 'room_namespace', @@ -608,7 +735,8 @@ class RoomController extends AbstractController )] public function add( Request $request, - TranslatorInterface $translator + TranslatorInterface $translator, + EntityManagerInterface $entity ): Response { // Check maintenance mode disabled @@ -782,6 +910,80 @@ class RoomController extends AbstractController ); } + // Room registration has commission cost, send to pending payment pool + if ($this->getParameter('app.add.room.cost.kva') > 0) + { + if ($address = $client->getNewAddress()) + { + $time = time(); + + $pool = new Pool(); + + $pool->setTime( + $time + ); + + $pool->setSent( + 0 + ); + + $pool->setExpired( + 0 + ); + + $pool->setCost( + $this->getParameter('app.add.room.cost.kva') + ); + + $pool->setAddress( + $address + ); + + $pool->setNamespace( + '' + ); + + $pool->setKey( + '_KEVA_NS_' + ); + + $pool->setValue( + $name + ); + + $entity->persist( + $pool + ); + + $entity->flush(); + + // Redirect back to room + return $this->redirectToRoute( + 'room_list', + [ + 'mode' => $request->get('mode'), + 'name' => $name, + 'warning' => sprintf( + $translator->trans('To complete, send %s KVA to %s'), + $this->getParameter('app.add.room.cost.kva'), + $address + ) + ] + ); + } + + else + { + return $this->redirectToRoute( + 'room_list', + [ + 'username' => $request->get('username'), + 'error' => $translator->trans('Could not init registration address!') + ] + ); + } + } + // Send message to DHT if ($namespace = $client->kevaNamespace($name)) { diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 0c67cd3..e11768f 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -9,6 +9,10 @@ use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; +use Doctrine\ORM\EntityManagerInterface; + +use App\Entity\Pool; + class UserController extends AbstractController { private $_algorithm = PASSWORD_BCRYPT; @@ -167,7 +171,8 @@ class UserController extends AbstractController return $this->render( 'default/user/join.html.twig', [ - 'request' => $request + 'request' => $request, + 'cost' => $this->getParameter('app.add.user.cost.kva') ] ); } @@ -247,7 +252,8 @@ class UserController extends AbstractController )] public function add( Request $request, - TranslatorInterface $translator + TranslatorInterface $translator, + EntityManagerInterface $entity ): Response { // Check maintenance mode disabled @@ -442,6 +448,79 @@ class UserController extends AbstractController ); } + // User registration has commission cost, send message to pending payment pool + if ($this->getParameter('app.add.user.cost.kva') > 0) + { + if ($address = $client->getNewAddress()) + { + $time = time(); + + $pool = new Pool(); + + $pool->setTime( + $time + ); + + $pool->setSent( + 0 + ); + + $pool->setExpired( + 0 + ); + + $pool->setCost( + $this->getParameter('app.add.user.cost.kva') + ); + + $pool->setAddress( + $address + ); + + $pool->setNamespace( + $namespace + ); + + $pool->setKey( + $username + ); + + $pool->setValue( + $hash + ); + + $entity->persist( + $pool + ); + + $entity->flush(); + + // Redirect back to room + return $this->redirectToRoute( + 'user_add', + [ + 'username' => $request->get('username'), + 'warning' => sprintf( + $translator->trans('To complete registration, send %s KVA to %s'), + $this->getParameter('app.add.user.cost.kva'), + $address + ) + ] + ); + } + + else + { + return $this->redirectToRoute( + 'user_add', + [ + 'username' => $request->get('username'), + 'error' => $translator->trans('Could not init registration address!') + ] + ); + } + } + // Auth success, add user to DB if (!$this->_add($client, $namespace, $username, $hash)) { diff --git a/src/Entity/.gitignore b/src/Entity/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/Entity/Pool.php b/src/Entity/Pool.php new file mode 100644 index 0000000..2a3bafd --- /dev/null +++ b/src/Entity/Pool.php @@ -0,0 +1,141 @@ +id; + } + + public function getTime(): ?int + { + return $this->time; + } + + public function setTime(int $time): static + { + $this->time = $time; + + return $this; + } + + public function getSent(): ?int + { + return $this->sent; + } + + public function setSent(int $sent): static + { + $this->sent = $sent; + + return $this; + } + + public function getExpired(): ?int + { + return $this->expired; + } + + public function setExpired(int $expired): static + { + $this->expired = $expired; + + return $this; + } + + public function getCost(): ?float + { + return $this->cost; + } + + public function setCost(float $cost): static + { + $this->cost = $cost; + + return $this; + } + + public function getAddress(): ?string + { + return $this->address; + } + + public function setAddress(string $address): static + { + $this->address = $address; + + return $this; + } + + public function getNamespace(): ?string + { + return $this->namespace; + } + + public function setNamespace(string $namespace): static + { + $this->namespace = $namespace; + + return $this; + } + + public function getKey(): ?string + { + return $this->key; + } + + public function setKey(string $key): static + { + $this->key = $key; + + return $this; + } + + public function getValue(): ?string + { + return $this->value; + } + + public function setValue(string $value): static + { + $this->value = $value; + + return $this; + } +} diff --git a/src/Repository/.gitignore b/src/Repository/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/Repository/PoolRepository.php b/src/Repository/PoolRepository.php new file mode 100644 index 0000000..2d6c9fa --- /dev/null +++ b/src/Repository/PoolRepository.php @@ -0,0 +1,23 @@ + + * + * @method Pool|null find($id, $lockMode = null, $lockVersion = null) + * @method Pool|null findOneBy(array $criteria, array $orderBy = null) + * @method Pool[] findAll() + * @method Pool[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class PoolRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Pool::class); + } +} diff --git a/src/Twig/AppExtension.php b/src/Twig/AppExtension.php index c5e923b..b1c9111 100644 --- a/src/Twig/AppExtension.php +++ b/src/Twig/AppExtension.php @@ -31,6 +31,13 @@ class AppExtension extends AbstractExtension 'formatAgo' ] ), + new TwigFilter( + 'format_expire', + [ + $this, + 'formatExpire' + ] + ), new TwigFilter( 'format_bytes', [ @@ -147,6 +154,77 @@ class AppExtension extends AbstractExtension } } + public function formatExpire( + int $time, + ): string + { + $diff = $time - time(); + + if ($diff < 1) + { + return $this->translator->trans('expired'); + } + + $values = + [ + 365 * 24 * 60 * 60 => + [ + $this->translator->trans('year to expire'), + $this->translator->trans('years to expire'), + $this->translator->trans(' years to expire') + ], + 30 * 24 * 60 * 60 => + [ + $this->translator->trans('month to expire'), + $this->translator->trans('months to expire'), + $this->translator->trans(' months to expire') + ], + 24 * 60 * 60 => + [ + $this->translator->trans('day to expire'), + $this->translator->trans('days to expire'), + $this->translator->trans(' days to expire') + ], + 60 * 60 => + [ + $this->translator->trans('hour to expire'), + $this->translator->trans('hours to expire'), + $this->translator->trans(' hours to expire') + ], + 60 => + [ + $this->translator->trans('minute to expire'), + $this->translator->trans('minutes to expire'), + $this->translator->trans(' minutes to expire') + ], + 1 => + [ + $this->translator->trans('second to expire'), + $this->translator->trans('seconds to expire'), + $this->translator->trans(' seconds to expire') + ] + ]; + + foreach ($values as $key => $value) + { + $result = $diff / $key; + + if ($result >= 1) + { + $round = round($result); + + return sprintf( + '%s %s', + $round, + $this->_plural( + $round, + $value + ) + ); + } + } + } + public function formatBytes( int $bytes, int $precision = 2 diff --git a/symfony.lock b/symfony.lock index f8d6890..8c89036 100644 --- a/symfony.lock +++ b/symfony.lock @@ -1,4 +1,31 @@ { + "doctrine/doctrine-bundle": { + "version": "2.11", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.10", + "ref": "c170ded8fc587d6bd670550c43dafcf093762245" + }, + "files": [ + "config/packages/doctrine.yaml", + "src/Entity/.gitignore", + "src/Repository/.gitignore" + ] + }, + "doctrine/doctrine-migrations-bundle": { + "version": "3.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.1", + "ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33" + }, + "files": [ + "config/packages/doctrine_migrations.yaml", + "migrations/.gitignore" + ] + }, "symfony/console": { "version": "7.0", "recipe": { @@ -42,6 +69,15 @@ "src/Kernel.php" ] }, + "symfony/maker-bundle": { + "version": "1.54", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" + } + }, "symfony/monolog-bundle": { "version": "3.10", "recipe": { diff --git a/templates/default/module/post.html.twig b/templates/default/module/post.html.twig index cc92f3c..615cba7 100644 --- a/templates/default/module/post.html.twig +++ b/templates/default/module/post.html.twig @@ -28,5 +28,8 @@ {{ 'login' | trans }} {% endif %} + {% if cost %} + {{ 'cost: %s KVA' | format(cost) | trans }} + {% endif %} {% endif %} \ No newline at end of file diff --git a/templates/default/module/room.html.twig b/templates/default/module/room.html.twig index 1abcda8..cc8e0f4 100644 --- a/templates/default/module/room.html.twig +++ b/templates/default/module/room.html.twig @@ -1,7 +1,13 @@ -
- {% if request.get('error') %} - {{ request.get('error') }} - {% endif %} - - -
\ No newline at end of file +
+ {% if request.get('error') %} + {{ request.get('error') }} + {% endif %} + {% if request.get('warning') %} + {{ request.get('warning') }} + {% endif %} + + + {% if cost %} + {{ 'cost: %s KVA' | format(cost) | trans }} + {% endif %} +
\ No newline at end of file diff --git a/templates/default/room/index.html.twig b/templates/default/room/index.html.twig index 470dfcc..918e5e9 100644 --- a/templates/default/room/index.html.twig +++ b/templates/default/room/index.html.twig @@ -17,15 +17,21 @@ {% endif %} • - {{ post.time | format_ago }} - • - {{ 'reply' | trans }} {% if post.pending %} + {{ post.time | format_ago }} + {% if post.pool %} + • + {{ 'pending %s KVA to %s (%s)' | trans | format(post.pool.cost, post.pool.address, post.pool.expires | format_expire ) }} + {% endif %} + {% else %} + {{ post.time | format_ago }} + • + {{ 'reply' | trans }} {% endif %} {# apply markdown whitelist filters only to prevent ping from remote includes #} {{ diff --git a/templates/default/user/join.html.twig b/templates/default/user/join.html.twig index 32b212b..6437b64 100644 --- a/templates/default/user/join.html.twig +++ b/templates/default/user/join.html.twig @@ -5,6 +5,9 @@ {% if request.get('error') %} {{ request.get('error') }} {% endif %} + {% if request.get('warning') %} + {{ request.get('warning') }} + {% endif %} @@ -13,5 +16,8 @@ {{ 'I have account' | trans }} + {% if cost %} + {{ 'cost: %s KVA' | format(cost) | trans }} + {% endif %} {% endblock %} \ No newline at end of file