Browse Source

implement comission charge for publications

main
ghost 9 months ago
parent
commit
ab546df14d
  1. 27
      .env
  2. 20
      README.md
  3. 8
      compose.override.yaml
  4. 21
      compose.yaml
  5. 9
      composer.json
  6. 3
      config/bundles.php
  7. 50
      config/packages/doctrine.yaml
  8. 6
      config/packages/doctrine_migrations.yaml
  9. 5
      config/services.yaml
  10. 0
      migrations/.gitignore
  11. 7
      public/css/default.css
  12. 141
      src/Controller/CrontabController.php
  13. 4
      src/Controller/ModuleController.php
  14. 210
      src/Controller/RoomController.php
  15. 83
      src/Controller/UserController.php
  16. 0
      src/Entity/.gitignore
  17. 141
      src/Entity/Pool.php
  18. 0
      src/Repository/.gitignore
  19. 23
      src/Repository/PoolRepository.php
  20. 78
      src/Twig/AppExtension.php
  21. 36
      symfony.lock
  22. 3
      templates/default/module/post.html.twig
  23. 10
      templates/default/module/room.html.twig
  24. 12
      templates/default/room/index.html.twig
  25. 6
      templates/default/user/join.html.twig

27
.env

@ -19,7 +19,7 @@ APP_ENV=dev
APP_SECRET=EDIT_ME APP_SECRET=EDIT_ME
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###
APP_VERSION=1.8.4 APP_VERSION=1.9.0
APP_NAME=KevaChat APP_NAME=KevaChat
@ -102,3 +102,28 @@ APP_ADD_POST_KEY_REGEX=/^([\d]+)@([A-z0-9\.\:\[\]]+)$/
# Post content rules (for kevacoin value, max length is 3072) # Post content rules (for kevacoin value, max length is 3072)
APP_ADD_POST_VALUE_REGEX=/.*/ui 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 ###

20
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. 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. 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 ## 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 `cd webapp`
`git pull`
* `git clone https://github.com/kevachat/webapp.git KevaChat` `composer update`
* `cd KevaChat` `php bin/console doctrine:migrations:migrate`
* `composer install` `APP_ENV=prod APP_DEBUG=0 php bin/console cache:clear`
## Setup ## Setup

8
compose.override.yaml

@ -0,0 +1,8 @@
version: '3'
services:
###> doctrine/doctrine-bundle ###
database:
ports:
- "5432"
###< doctrine/doctrine-bundle ###

21
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 ###

9
composer.json

@ -10,8 +10,11 @@
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*", "ext-iconv": "*",
"clitor-is-protocol/kevacoin": "^1.0", "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", "jdenticon/jdenticon": "^1.0",
"kevachat/kevacoin": "^1.0", "kevachat/kevacoin": "^1.7",
"league/commonmark": "^2.4", "league/commonmark": "^2.4",
"symfony/console": "7.0.*", "symfony/console": "7.0.*",
"symfony/dotenv": "7.0.*", "symfony/dotenv": "7.0.*",
@ -23,6 +26,7 @@
"symfony/twig-bundle": "7.0.*", "symfony/twig-bundle": "7.0.*",
"symfony/yaml": "7.0.*", "symfony/yaml": "7.0.*",
"twig/extra-bundle": "^3.8", "twig/extra-bundle": "^3.8",
"twig/intl-extra": "^3.8",
"twig/markdown-extra": "^3.8" "twig/markdown-extra": "^3.8"
}, },
"config": { "config": {
@ -73,5 +77,8 @@
"allow-contrib": false, "allow-contrib": false,
"require": "7.0.*" "require": "7.0.*"
} }
},
"require-dev": {
"symfony/maker-bundle": "^1.54"
} }
} }

3
config/bundles.php

@ -5,4 +5,7 @@ return [
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::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],
]; ];

50
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

6
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

5
config/services.yaml

@ -36,6 +36,11 @@ parameters:
app.add.post.remote.ip.denied: '%env(APP_ADD_POST_REMOTE_IP_DENIED)%' 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.key.regex: '%env(APP_ADD_POST_KEY_REGEX)%'
app.add.post.value.regex: '%env(APP_ADD_POST_VALUE_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)%' app.moderator.remote.ip: '%env(APP_MODERATOR_REMOTE_IP)%'
services: services:

0
migrations/.gitignore vendored

7
public/css/default.css

@ -44,6 +44,13 @@ body
font-size: 12px; font-size: 12px;
} }
form > span
{
float: right;
line-height: 24px;
padding: 0 8px;
}
/* header */ /* header */
header header

141
src/Controller/CrontabController.php

@ -0,0 +1,141 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
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 CrontabController extends AbstractController
{
#[Route(
'/crontab/pool',
name: 'crontab_pool',
methods:
[
'GET'
]
)]
public function pool(
Request $request,
EntityManagerInterface $entity
): Response
{
// Connect kevacoin
$client = new \Kevachat\Kevacoin\Client(
$this->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
}
}

4
src/Controller/ModuleController.php

@ -204,6 +204,7 @@ class ModuleController extends AbstractController
'sign' => $sign, 'sign' => $sign,
'message' => $message, 'message' => $message,
'username' => $username, 'username' => $username,
'cost' => $this->getParameter('app.add.post.cost.kva'),
'enabled' => 'enabled' =>
( (
!in_array( !in_array(
@ -224,7 +225,8 @@ class ModuleController extends AbstractController
return $this->render( return $this->render(
'default/module/room.html.twig', 'default/module/room.html.twig',
[ [
'request' => $request 'request' => $request,
'cost' => $this->getParameter('app.add.room.cost.kva')
] ]
); );
} }

210
src/Controller/RoomController.php

@ -9,6 +9,10 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Pool;
class RoomController extends AbstractController class RoomController extends AbstractController
{ {
#[Route( #[Route(
@ -160,7 +164,8 @@ class RoomController extends AbstractController
] ]
)] )]
public function room( public function room(
Request $request Request $request,
EntityManagerInterface $entity
): Response ): Response
{ {
// Connect kevacoin // Connect kevacoin
@ -175,6 +180,54 @@ class RoomController extends AbstractController
// Get room feed // Get room feed
$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 // Get pending paradise
foreach ((array) $client->kevaPending() as $pending) // @TODO relate to this room foreach ((array) $client->kevaPending() as $pending) // @TODO relate to this room
{ {
@ -318,7 +371,8 @@ class RoomController extends AbstractController
)] )]
public function post( public function post(
Request $request, Request $request,
TranslatorInterface $translator TranslatorInterface $translator,
EntityManagerInterface $entity
): Response ): Response
{ {
// Check maintenance mode disabled // Check maintenance mode disabled
@ -550,7 +604,79 @@ class RoomController extends AbstractController
); );
} }
// Send message to DHT // 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,
$username
)
);
$pool->setValue(
$request->get('message')
);
$entity->persist(
$pool
);
$entity->flush();
// 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'
]
);
}
// Post has zero cost, send message to DHT
else
{
if ( if (
$client->kevaPut( $client->kevaPut(
$request->get('namespace'), $request->get('namespace'),
@ -583,6 +709,7 @@ class RoomController extends AbstractController
] ]
); );
} }
}
// Something went wrong, return error message // Something went wrong, return error message
return $this->redirectToRoute( return $this->redirectToRoute(
@ -608,7 +735,8 @@ class RoomController extends AbstractController
)] )]
public function add( public function add(
Request $request, Request $request,
TranslatorInterface $translator TranslatorInterface $translator,
EntityManagerInterface $entity
): Response ): Response
{ {
// Check maintenance mode disabled // 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 // Send message to DHT
if ($namespace = $client->kevaNamespace($name)) if ($namespace = $client->kevaNamespace($name))
{ {

83
src/Controller/UserController.php

@ -9,6 +9,10 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Pool;
class UserController extends AbstractController class UserController extends AbstractController
{ {
private $_algorithm = PASSWORD_BCRYPT; private $_algorithm = PASSWORD_BCRYPT;
@ -167,7 +171,8 @@ class UserController extends AbstractController
return $this->render( return $this->render(
'default/user/join.html.twig', '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( public function add(
Request $request, Request $request,
TranslatorInterface $translator TranslatorInterface $translator,
EntityManagerInterface $entity
): Response ): Response
{ {
// Check maintenance mode disabled // 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 // Auth success, add user to DB
if (!$this->_add($client, $namespace, $username, $hash)) if (!$this->_add($client, $namespace, $username, $hash))
{ {

0
src/Entity/.gitignore vendored

141
src/Entity/Pool.php

@ -0,0 +1,141 @@
<?php
namespace App\Entity;
use App\Repository\PoolRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: PoolRepository::class)]
class Pool
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?int $time = null;
#[ORM\Column]
private ?int $sent = null;
#[ORM\Column]
private ?int $expired = null;
#[ORM\Column]
private ?float $cost = null;
#[ORM\Column(length: 255)]
private ?string $address = null;
#[ORM\Column(length: 255)]
private ?string $namespace = null;
#[ORM\Column(length: 255)]
private ?string $key = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $value = null;
public function getId(): ?int
{
return $this->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;
}
}

0
src/Repository/.gitignore vendored

23
src/Repository/PoolRepository.php

@ -0,0 +1,23 @@
<?php
namespace App\Repository;
use App\Entity\Pool;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Pool>
*
* @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);
}
}

78
src/Twig/AppExtension.php

@ -31,6 +31,13 @@ class AppExtension extends AbstractExtension
'formatAgo' 'formatAgo'
] ]
), ),
new TwigFilter(
'format_expire',
[
$this,
'formatExpire'
]
),
new TwigFilter( new TwigFilter(
'format_bytes', '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( public function formatBytes(
int $bytes, int $bytes,
int $precision = 2 int $precision = 2

36
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": { "symfony/console": {
"version": "7.0", "version": "7.0",
"recipe": { "recipe": {
@ -42,6 +69,15 @@
"src/Kernel.php" "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": { "symfony/monolog-bundle": {
"version": "3.10", "version": "3.10",
"recipe": { "recipe": {

3
templates/default/module/post.html.twig

@ -28,5 +28,8 @@
<a href="{{ path('user_login') }}">{{ 'login' | trans }}</a> <a href="{{ path('user_login') }}">{{ 'login' | trans }}</a>
{% endif %} {% endif %}
<button type="submit">{{ 'send' | trans }}</button> <button type="submit">{{ 'send' | trans }}</button>
{% if cost %}
<span>{{ 'cost: %s KVA' | format(cost) | trans }}</span>
{% endif %}
</form> </form>
{% endif %} {% endif %}

10
templates/default/module/room.html.twig

@ -1,7 +1,13 @@
<form name="room" action="{{ path('room_add', { mode : request.get('mode') }) }}" method="post"> <form name="room" action="{{ path('room_add', { mode : request.get('mode') }) }}" method="post">
{% if request.get('error') %} {% if request.get('error') %}
<output name="error" for="form-room-name">{{ request.get('error') }}</output> <output name="error" for="form-room-name">{{ request.get('error') }}</output>
{% endif %} {% endif %}
{% if request.get('warning') %}
<output name="warning" for="form-room-name">{{ request.get('warning') }}</output>
{% endif %}
<input type="text" name="name" id="form-room-name" value="{{ request.get('name') }}" placeholder="{{ 'enter new room name...' | trans }}" /> <input type="text" name="name" id="form-room-name" value="{{ request.get('name') }}" placeholder="{{ 'enter new room name...' | trans }}" />
<button type="submit">{{ 'add' | trans }}</button> <button type="submit">{{ 'add' | trans }}</button>
</form> {% if cost %}
<span>{{ 'cost: %s KVA' | format(cost) | trans }}</span>
{% endif %}
</form>

12
templates/default/room/index.html.twig

@ -17,15 +17,21 @@
</strong> </strong>
{% endif %} {% endif %}
&bull; &bull;
<a rel="nofollow" href="{{ path('room_namespace', { mode : mode, namespace : namespace, _fragment : post.id }) }}" title="{{ post.time | date('c') }}">{{ post.time | format_ago }}</a>
&bull;
<a rel="nofollow" href="{{ path('room_namespace', { mode : mode, namespace : namespace, txid : post.id, _fragment : post.id }) }}">{{ 'reply' | trans }}</a>
{% if post.pending %} {% if post.pending %}
{{ post.time | format_ago }}
{% if post.pool %}
&bull;
{{ 'pending %s KVA to %s (%s)' | trans | format(post.pool.cost, post.pool.address, post.pool.expires | format_expire ) }}
{% endif %}
<span title="{{ 'pending in pool' | trans }}"> <span title="{{ 'pending in pool' | trans }}">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71z"/> <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71z"/>
</svg> </svg>
</span> </span>
{% else %}
<a rel="nofollow" href="{{ path('room_namespace', { mode : mode, namespace : namespace, _fragment : post.id }) }}" title="{{ post.time | date('c') }}">{{ post.time | format_ago }}</a>
&bull;
<a rel="nofollow" href="{{ path('room_namespace', { mode : mode, namespace : namespace, txid : post.id, _fragment : post.id }) }}">{{ 'reply' | trans }}</a>
{% endif %} {% endif %}
{# apply markdown whitelist filters only to prevent ping from remote includes #} {# apply markdown whitelist filters only to prevent ping from remote includes #}
{{ {{

6
templates/default/user/join.html.twig

@ -5,6 +5,9 @@
{% if request.get('error') %} {% if request.get('error') %}
<output name="error">{{ request.get('error') }}</output> <output name="error">{{ request.get('error') }}</output>
{% endif %} {% endif %}
{% if request.get('warning') %}
<output name="warning">{{ request.get('warning') }}</output>
{% endif %}
<label for="username">{{ 'Username' | trans }}</label> <label for="username">{{ 'Username' | trans }}</label>
<input type="text" name="username" id="username" value="{{ request.get('username') }}" placeholder="{{ 'Your public identity for this instance, permanently stored in blockchain' | trans }}" /> <input type="text" name="username" id="username" value="{{ request.get('username') }}" placeholder="{{ 'Your public identity for this instance, permanently stored in blockchain' | trans }}" />
<label for="password">{{ 'Password' | trans }}</label> <label for="password">{{ 'Password' | trans }}</label>
@ -13,5 +16,8 @@
<input type="password" name="repeat" id="repeat" value="" placeholder="{{ 'Make sure your password is correct, you can not reset it later!' | trans }}" /> <input type="password" name="repeat" id="repeat" value="" placeholder="{{ 'Make sure your password is correct, you can not reset it later!' | trans }}" />
<a href="{{ path('user_login') }}">{{ 'I have account' | trans }}</a> <a href="{{ path('user_login') }}">{{ 'I have account' | trans }}</a>
<button type="submit">{{ 'join' | trans }}</button> <button type="submit">{{ 'join' | trans }}</button>
{% if cost %}
<span>{{ 'cost: %s KVA' | format(cost) | trans }}</span>
{% endif %}
</form> </form>
{% endblock %} {% endblock %}
Loading…
Cancel
Save