webapp/src/Controller/RoomController.php
2023-12-06 04:09:43 +02:00

433 lines
13 KiB
PHP

<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
class RoomController extends AbstractController
{
#[Route(
'/',
name: 'room_index',
methods:
[
'GET'
]
)]
public function index(
?Request $request
): Response
{
return $this->redirectToRoute(
'room_namespace',
[
'namespace' => $request->get('namespace') ? $request->get('namespace') : $this->getParameter('app.kevacoin.room.namespace.default')
]
);
}
#[Route(
'/room/{namespace}/{txid}',
name: 'room_namespace',
requirements:
[
'namespace' => '^[A-z0-9]{34}$',
'txid' => '^[A-z0-9]{64}$',
],
defaults:
[
'txid' => null,
],
methods:
[
'GET'
]
)]
public function room(
Request $request
): 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')
);
// Set title
$name = $request->get('namespace');
foreach ((array) $client->kevaListNamespaces() as $namespace)
{
// Get current room namespace (could be third-party)
if ($namespace['namespaceId'] == $request->get('namespace'))
{
$name = $namespace['displayName'];
break;
}
}
// Get room feed
$feed = [];
// Get pending paradise
foreach ((array) $client->kevaPending() as $pending)
{
// Ignore pending posts from other rooms
if ($pending['namespace'] !== $request->get('namespace'))
{
continue;
}
// Ignore everything in pending queue but keva_put nodes
if ($pending['op'] !== 'keva_put')
{
continue;
}
// Skip values with meta keys
if (false !== stripos($pending['key'], '_KEVA_'))
{
continue;
}
// Require valid kevachat meta
if ($data = $this->_post($pending))
{
$feed[] =
[
'id' => $data->id,
'user' => $data->user,
'icon' => $data->icon,
'message' => $data->message,
'timestamp' => $data->time,
'time' => date('c', $data->time),
'pending' => true
];
}
}
// Get regular posts
foreach ((array) $client->kevaFilter($request->get('namespace')) as $post)
{
// Skip values with meta keys
if (false !== stripos($post['key'], '_KEVA_'))
{
continue;
}
// Require valid kevachat meta
if ($data = $this->_post($post))
{
$feed[] =
[
'id' => $data->id,
'user' => $data->user,
'icon' => $data->icon,
'message' => $data->message,
'timestamp' => $data->time,
'time' => date('c', $data->time),
'pending' => false
];
}
}
// Sort posts by newest on top
array_multisort(
array_column(
$feed,
'time'
),
SORT_DESC,
$feed
);
// Return result
return $this->render(
'default/room/index.html.twig',
[
'name' => $name,
'feed' => $feed,
'request' => $request
]
);
}
#[Route(
'/room/{namespace}',
name: 'room_post',
requirements:
[
'namespace' => '^[A-z0-9]{34}$',
],
methods:
[
'POST'
]
)]
public function post(
Request $request,
TranslatorInterface $translator
): Response
{
// Connect memcached
$memcached = new \Memcached();
$memcached->addServer(
$this->getParameter('app.memcached.host'),
$this->getParameter('app.memcached.port')
);
$memory = [
'app.add.post.remote.ip.delay' => md5(
sprintf(
'kevachat.app.add.post.remote.ip.delay:%s.%s',
$this->getParameter('app.name'),
$request->getClientIp(),
),
),
];
// 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 local namespaces
$namespaces = [];
foreach ((array) $client->kevaListNamespaces() as $value)
{
$namespaces[] = $value['namespaceId'];
}
// Check namespace exist for this wallet
if (!in_array($request->get('namespace'), $namespaces))
{
return $this->redirectToRoute(
'room_namespace',
[
'namespace' => $request->get('namespace'),
'message' => $request->get('message'),
'error' => $translator->trans('Namespace not related with this node!')
]
);
}
// Check namespace writable
if (!in_array($request->get('namespace'), (array) explode('|', $this->getParameter('app.kevacoin.room.namespaces'))))
{
return $this->redirectToRoute(
'room_namespace',
[
'namespace' => $request->get('namespace'),
'message' => $request->get('message'),
'error' => $translator->trans('Namespace not listed in settings!')
]
);
}
// Validate access to the room namespace
if
(
// Ignore this rule for is moderators
!in_array(
$request->getClientIp(),
(array) explode('|', $this->getParameter('app.add.post.remote.ip.moderators'))
) &&
// Check namespace writable or user is moderator
in_array(
$request->get('namespace'),
(array) explode('|', $this->getParameter('app.kevacoin.room.namespaces.readonly'))
)
)
{
return $this->redirectToRoute(
'room_namespace',
[
'namespace' => $request->get('namespace'),
'message' => $request->get('message'),
'error' => $translator->trans('Namespace for read only!')
]
);
}
// Deny requests from banned remote hosts
if (in_array($request->getClientIp(), (array) explode('|', $this->getParameter('app.add.post.remote.ip.denied'))))
{
return $this->redirectToRoute(
'room_namespace',
[
'namespace' => $request->get('namespace'),
'message' => $request->get('message'),
'error' => $translator->trans('Access denied for this IP!')
]
);
}
// Validate remote IP regex
if (!preg_match($this->getParameter('app.add.post.remote.ip.regex'), $request->getClientIp()))
{
return $this->redirectToRoute(
'room_namespace',
[
'namespace' => $request->get('namespace'),
'message' => $request->get('message'),
'error' => $translator->trans('Access not allowed for this IP!')
]
);
}
// Validate message regex
if (!preg_match($this->getParameter('app.add.post.value.regex'), $request->get('message')))
{
return $this->redirectToRoute(
'room_namespace',
[
'namespace' => $request->get('namespace'),
'message' => $request->get('message'),
'error' => sprintf(
$translator->trans('Message does not match node requirements: %s'),
$this->getParameter('app.add.post.value.regex')
)
]
);
}
/// Validate remote IP limits
if ($delay = (int) $memcached->get($memory['app.add.post.remote.ip.delay']))
{
// Error
return $this->redirectToRoute(
'room_namespace',
[
'namespace' => $request->get('namespace'),
'message' => $request->get('message'),
'error' => sprintf(
$translator->trans('Please wait for %s seconds before post new message!'),
(int) $this->getParameter('app.add.post.remote.ip.delay') - (time() - $delay)
)
]
);
}
/// Validate funds available yet
if (1 > $client->getBalance())
{
return $this->redirectToRoute(
'room_namespace',
[
'namespace' => $request->get('namespace'),
'message' => $request->get('message'),
'error' => sprintf(
$translator->trans('Insufficient funds, wallet: %s'),
$this->getParameter('app.kevacoin.mine.address')
)
]
);
}
// Send message to DHT
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
$request->get('user') === 'ip' ? $request->getClientIp() : 'guest'
),
$request->get('message')
)
)
{
// Register event time
$memcached->set(
$memory['app.add.post.remote.ip.delay'],
time(),
(int) $this->getParameter('app.add.post.remote.ip.delay')
);
// Redirect back to room
return $this->redirectToRoute(
'room_namespace',
[
'namespace' => $request->get('namespace'),
'error' => null,
'message' => null
]
);
}
// Something went wrong, return error message
return $this->redirectToRoute(
'room_namespace',
[
'namespace' => $request->get('namespace'),
'message' => $request->get('message'),
'error' => $translator->trans('Internal error! Please feedback')
]
);
}
private function _post(array $data): ?object
{
if (false === preg_match('/^([\d]+)@(.*)$/', $data['key'], $matches))
{
return null;
}
if (empty($matches[1]))
{
return null;
}
if (empty($matches[2]))
{
return null;
}
return (object)
[
'id' => $data['txid'],
'time' => $matches[1],
'user' => $matches[2],
'icon' => $this->_identicon($matches[2]),
'message' => $data['value']
];
}
private function _identicon(mixed $value): ?string
{
if ($value === 'guest')
{
return null;
}
$identicon = new \Jdenticon\Identicon();
$identicon->setValue($value);
$identicon->setSize(12);
$identicon->setStyle(
[
'backgroundColor' => 'rgba(255, 255, 255, 0)',
'padding' => 0
]
);
return $identicon->getImageDataUri('webp');
}
}