Implemented concept peer association manager

This commit is contained in:
Netkas 2023-06-23 00:28:30 -04:00
parent a2f4b2b685
commit 42d331408c
No known key found for this signature in database
GPG key ID: 5DAF58535614062B
11 changed files with 486 additions and 14 deletions

View file

@ -59,6 +59,11 @@
self::$configuration->setDefault('cache_system.cache.peer_objects_ttl', 200);
self::$configuration->setDefault('cache_system.cache.peer_objects_server_preference', 'redis_master');
self::$configuration->setDefault('cache_system.cache.peer_objects_server_fallback', 'redis_slave');
// PeerAssociationRecord Objects
self::$configuration->setDefault('cache_system.cache.peer_association_objects_enabled', true);
self::$configuration->setDefault('cache_system.cache.peer_association_objects_ttl', 200);
self::$configuration->setDefault('cache_system.cache.peer_association_objects_server_preference', 'redis_master');
self::$configuration->setDefault('cache_system.cache.peer_association_objects_server_fallback', 'redis_slave');
/** Multi-Cache Server Configuration */
// Redis Master Configuration
self::$configuration->setDefault('cache_system.servers.redis_master.enabled', true);

View file

@ -5,10 +5,10 @@
final class DatabaseTables
{
public const ANOMALY_TRACKING = 'anomaly_tracking';
public const ASSOCIATIONS = 'associations';
public const CLIENTS = 'clients';
public const EVENTS = 'events';
public const PEERS = 'peers';
public const PEERS_ASSOCIATIONS = 'peers_associations';
public const PEERS_TELEGRAM_CHAT = 'peers_telegram_chat';
public const PEERS_TELEGRAM_USER = 'peers_telegram_user';
public const QUERY_DOCUMENTS = 'query_documents';
@ -17,10 +17,10 @@
public const ALL = [
self::ANOMALY_TRACKING,
self::ASSOCIATIONS,
self::CLIENTS,
self::EVENTS,
self::PEERS,
self::PEERS_ASSOCIATIONS,
self::PEERS_TELEGRAM_CHAT,
self::PEERS_TELEGRAM_USER,
self::QUERY_DOCUMENTS,

View file

@ -1,4 +1,4 @@
<?php
<?php // TODO: Fix the error-codes once it's been decided what they should be.
namespace FederationLib\Enums\Standard;
@ -14,11 +14,6 @@
*/
public const ACCESS_DENIED = -1001;
/**
* The requested method is disabled.
*/
public const METHOD_DISABLED = -1002;
/**
@ -56,6 +51,8 @@
public const INVALID_FEDERATED_ADDRESS = 2002;
public const INVALID_PEER_ASSOCIATION_TYPE = 2003;
public const ALL = [
self::INTERNAL_SERVER_ERROR,
@ -69,6 +66,7 @@
self::INVALID_PEER_METADATA,
self::PEER_METADATA_NOT_FOUND,
self::INVALID_FEDERATED_ADDRESS
self::INVALID_FEDERATED_ADDRESS,
self::INVALID_PEER_ASSOCIATION_TYPE
];
}

View file

@ -34,6 +34,11 @@
*/
public const ALTERNATIVE = 'alternative';
/**
* Indicates the parent peer is a subscriber of the child peer.
*/
public const SUBSCRIBER = 'subscriber';
/**
* An array of all peer association types.
*/
@ -43,6 +48,7 @@
self::MODERATOR,
self::MEMBER,
self::BANNED,
self::ALTERNATIVE
self::ALTERNATIVE,
self::SUBSCRIBER
];
}

View file

@ -0,0 +1,19 @@
<?php
namespace FederationLib\Exceptions;
use Exception;
use Throwable;
class PeerAssociationNotFoundException extends Exception
{
/**
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace FederationLib\Exceptions\Standard;
use Exception;
use FederationLib\Enums\Standard\ErrorCodes;
use Throwable;
class InvalidPeerAssociationTypeException extends Exception
{
/**
* @param string $message
* @param Throwable|null $previous
*/
public function __construct(string $message = "", ?Throwable $previous = null)
{
parent::__construct($message, ErrorCodes::INVALID_PEER_ASSOCIATION_TYPE, $previous);
}
}

View file

@ -240,7 +240,7 @@
* @throws InternalServerException
* @throws InvalidClientNameException
*/
public function changeClientName(?ClientIdentity $identity, string $client_uuid, string $new_name): bool
public function changeClientName(?ClientIdentity $identity, string $client_uuid, ?string $new_name): bool
{
if(!$this->checkPermission(Methods::CHANGE_CLIENT_NAME, $this->resolveIdentity($identity)))
{
@ -263,5 +263,4 @@
return true;
}
}

View file

@ -0,0 +1,307 @@
<?php
namespace FederationLib\Managers;
use Doctrine\DBAL\ParameterType;
use Exception;
use FederationLib\Classes\Configuration;
use FederationLib\Classes\Database;
use FederationLib\Classes\Validate;
use FederationLib\Enums\DatabaseTables;
use FederationLib\Enums\Misc;
use FederationLib\Exceptions\DatabaseException;
use FederationLib\Exceptions\PeerAssociationNotFoundException;
use FederationLib\Exceptions\Standard\InvalidPeerAssociationTypeException;
use FederationLib\FederationLib;
use FederationLib\Objects\ClientRecord;
use FederationLib\Objects\ParsedFederatedAddress;
use FederationLib\Objects\PeerAssociationRecord;
use LogLib\Log;
class AssociationManager
{
/**
* @var FederationLib
*/
private FederationLib $federationLib;
/**
* AssociationManager constructor.
*
* @param FederationLib $federationLib
*/
public function __construct(FederationLib $federationLib)
{
$this->federationLib = $federationLib;
}
/**
* Associates a child peer with a parent peer.
*
* @param ClientRecord|string $client_uuid
* @param ParsedFederatedAddress|string $parent
* @param ParsedFederatedAddress|string $child
* @param string $type
* @return void
* @throws DatabaseException
* @throws InvalidPeerAssociationTypeException
*/
public function associate(ClientRecord|string $client_uuid, ParsedFederatedAddress|string $parent, ParsedFederatedAddress|string $child, string $type): void
{
if(!Validate::peerAssociationType($type))
{
throw new InvalidPeerAssociationTypeException(sprintf('Invalid peer association type: %s', $type));
}
if($client_uuid instanceof ClientRecord)
{
$client_uuid = $client_uuid->getUuid();
}
if(is_string($parent))
{
$parent = new ParsedFederatedAddress($parent);
}
if(is_string($child))
{
$child = new ParsedFederatedAddress($child);
}
try
{
// Try updating the association if it already exists, but only if the association type is different
$peer_association = $this->getAssociation($parent, $child);
if($peer_association->getAssociationType() === $type)
{
return;
}
$this->updateAssociation($client_uuid, $parent, $child, $type);
return;
}
catch(PeerAssociationNotFoundException $e)
{
// This is fine, we'll just create a new association
unset($e);
}
$qb = Database::getConnection()->createQueryBuilder();
$qb->insert(DatabaseTables::PEERS_ASSOCIATIONS);
$qb->setValue('child_peer', $qb->createNamedParameter($child->getAddress()));
$qb->setValue('parent_peer', $qb->createNamedParameter($parent->getAddress()));
$qb->setValue('association_type', $qb->createNamedParameter($type));
$qb->setValue('client_uuid', $qb->createNamedParameter($client_uuid));
$qb->setValue('timestamp', $qb->createNamedParameter(time(), ParameterType::INTEGER));
try
{
$qb->executeStatement();
}
catch(Exception $e)
{
throw new DatabaseException('Failed to register peer association: ' . $e->getMessage(), $e);
}
Log::info(Misc::FEDERATIONLIB, sprintf('Associated %s with %s as %s', $child->getAddress(), $parent->getAddress(), $type));
}
/**
* Returns the association record for the child peer and parent peer.
*
* @param ParsedFederatedAddress|string $parent
* @param ParsedFederatedAddress|string $child
* @return PeerAssociationRecord
* @throws DatabaseException
* @throws PeerAssociationNotFoundException
*/
public function getAssociation(ParsedFederatedAddress|string $parent, ParsedFederatedAddress|string $child): PeerAssociationRecord
{
if(is_string($parent))
{
$parent = new ParsedFederatedAddress($parent);
}
if(is_string($child))
{
$child = new ParsedFederatedAddress($child);
}
$cache_key = sprintf('peer_association_%s_%s', $parent->getAddress(), $child->getAddress());
if(Configuration::isCacheSystemEnabled() && Configuration::getObjectCacheEnabled('peer_association_objects'))
{
try
{
$redis = RedisConnectionManager::getConnectionFromConfig('peer_association_objects');
if($redis->exists($cache_key))
{
$peer_association = PeerAssociationRecord::fromArray($redis->hGetAll($cache_key));
Log::debug(Misc::FEDERATIONLIB, sprintf('Loaded peer association object %s from cache', $cache_key));
return $peer_association;
}
}
catch(Exception $e)
{
Log::warning(Misc::FEDERATIONLIB, sprintf('Failed to load peer association %s from cache: %s', $cache_key, $e->getMessage()));
}
}
$qb = Database::getConnection()->createQueryBuilder();
$qb->select(
'child_peer',
'parent_peer',
'association_type',
'client_uuid',
'timestamp'
);
$qb->from(DatabaseTables::PEERS_ASSOCIATIONS);
$qb->where('child_peer = :child_peer');
$qb->andWhere('parent_peer = :parent_peer');
$qb->setParameter('child_peer', $child->getAddress());
$qb->setParameter('parent_peer', $parent->getAddress());
$qb->setMaxResults(1);
try
{
$result = $qb->executeQuery();
if($result->rowCount() === 0)
{
throw new PeerAssociationNotFoundException(sprintf('Peer association not found: %s -> %s', $child->getAddress(), $parent->getAddress()));
}
$peer_association = PeerAssociationRecord::fromArray($result->fetchAssociative());
}
catch(PeerAssociationNotFoundException $e)
{
throw $e;
}
catch(Exception $e)
{
throw new DatabaseException('Failed to get peer association: ' . $e->getMessage(), $e);
}
if(Configuration::isCacheSystemEnabled() && Configuration::getObjectCacheEnabled('peer_association_objects'))
{
try
{
if(!isset($redis))
{
$redis = RedisConnectionManager::getConnectionFromConfig('peer_association_objects');
}
$redis->hMSet($cache_key, $peer_association->toArray());
if(Configuration::getObjectCacheTtl('peer_association_objects') > 0)
{
$redis->expire($cache_key, Configuration::getObjectCacheTtl('peer_association_objects'));
}
Log::debug(Misc::FEDERATIONLIB, sprintf('Cached peer association object %s', $cache_key));
}
catch(Exception $e)
{
Log::warning(Misc::FEDERATIONLIB, sprintf('Failed to cache peer association %s: %s', $cache_key, $e->getMessage()));
}
}
return $peer_association;
}
/**
* Updates the association record for the child peer and parent peer.
* If the type remains the same, nothing will be done.
*
* @param ClientRecord|string $client_uuid
* @param ParsedFederatedAddress|string $parent
* @param ParsedFederatedAddress|string $child
* @param string $type
* @return void
* @throws DatabaseException
* @throws InvalidPeerAssociationTypeException
* @throws PeerAssociationNotFoundException
*/
public function updateAssociation(ClientRecord|string $client_uuid, ParsedFederatedAddress|string $parent, ParsedFederatedAddress|string $child, string $type): void
{
if(!Validate::peerAssociationType($type))
{
throw new InvalidPeerAssociationTypeException(sprintf('Invalid peer association type: %s', $type));
}
if($client_uuid instanceof ClientRecord)
{
$client_uuid = $client_uuid->getUuid();
}
if(is_string($parent))
{
$parent = new ParsedFederatedAddress($parent);
}
if(is_string($child))
{
$child = new ParsedFederatedAddress($child);
}
$peer_association = $this->getAssociation($parent, $child);
if($peer_association->getAssociationType() === $type)
{
return;
}
$qb = Database::getConnection()->createQueryBuilder();
$qb->update(DatabaseTables::PEERS_ASSOCIATIONS);
$qb->set('association_type', $qb->createNamedParameter($type));
$qb->set('client_uuid', $qb->createNamedParameter($client_uuid));
$qb->set('timestamp', $qb->createNamedParameter(time(), ParameterType::INTEGER));
$qb->where('child_peer = :child_peer');
$qb->andWhere('parent_peer = :parent_peer');
$qb->setParameter('child_peer', $child->getAddress());
$qb->setParameter('parent_peer', $parent->getAddress());
try
{
$qb->executeStatement();
}
catch(Exception $e)
{
throw new DatabaseException('Failed to update peer association: ' . $e->getMessage(), $e);
}
$cache_key = sprintf('peer_association_%s_%s', $parent->getAddress(), $child->getAddress());
if(Configuration::isCacheSystemEnabled() && Configuration::getObjectCacheEnabled('peer_association_objects'))
{
try
{
$redis = RedisConnectionManager::getConnectionFromConfig('peer_association_objects');
if($redis->exists($cache_key))
{
$redis->hMSet($cache_key, [
'association_type' => $type,
'client_uuid' => $client_uuid,
'timestamp' => time()
]);
if(Configuration::getObjectCacheTtl('peer_association_objects') > 0)
{
$redis->expire($cache_key, Configuration::getObjectCacheTtl('peer_association_objects'));
}
Log::debug(Misc::FEDERATIONLIB, sprintf('Updated cached peer association object %s', $cache_key));
}
}
catch(Exception $e)
{
Log::warning(Misc::FEDERATIONLIB, sprintf('Failed to update cached peer association %s: %s', $cache_key, $e->getMessage()));
}
}
Log::debug(Misc::FEDERATIONLIB, sprintf('Updated peer association %s -> %s: %s', $child->getAddress(), $parent->getAddress(), $type));
}
}

View file

@ -0,0 +1,119 @@
<?php
namespace FederationLib\Objects;
use FederationLib\Interfaces\SerializableObjectInterface;
class PeerAssociationRecord implements SerializableObjectInterface
{
/**
* @var string
*/
private $child_peer;
/**
* @var string
*/
private $parent_peer;
/**
* @var string
*/
private $association_type;
/**
* @var string
*/
private $client_uuid;
/**
* @var int
*/
private $timestamp;
/**
* Returns the child peer that is associated with the parent peer.
*
* @return string
*/
public function getChildPeer(): string
{
return $this->child_peer;
}
/**
* Returns the parent peer that is associated with the child peer.
*
* @return string
*/
public function getParentPeer(): string
{
return $this->parent_peer;
}
/**
* Returns the association type from the child peer to the parent peer.
*
* @return string
*/
public function getAssociationType(): string
{
return $this->association_type;
}
/**
* Returns the Client UUID that made this association.
*
* @return string
*/
public function getClientUuid(): string
{
return $this->client_uuid;
}
/**
* Returns the Unix Timestamp of when this association was made.
*
* @return int
*/
public function getTimestamp(): int
{
return $this->timestamp;
}
/**
* Returns an array representation the object.
*
* @return array
*/
public function toArray(): array
{
return [
'child_peer' => $this->child_peer,
'parent_peer' => $this->parent_peer,
'association_type' => $this->association_type,
'client_uuid' => $this->client_uuid,
'timestamp' => $this->timestamp
];
}
/**
* Constructs object from an array representation
*
* @param array $array
* @return PeerAssociationRecord
*/
public static function fromArray(array $array): PeerAssociationRecord
{
$object = new self();
$object->child_peer = $array['child_peer'];
$object->parent_peer = $array['parent_peer'];
$object->association_type = $array['association_type'];
$object->client_uuid = $array['client_uuid'];
$object->timestamp = $array['timestamp'];
return $object;
}
}

View file

@ -78,7 +78,7 @@
'is_forum' => 'boolean'
];
Validate::validateMetadata($this->toArray(), $required_properties, $optional_properties);
Validate::metadata($this->toArray(), $required_properties, $optional_properties);
if(!in_array(strtolower($this->type), ['private', 'group', 'supergroup', 'channel']))
{

View file

@ -78,7 +78,7 @@
'language_code' => 'string',
];
Validate::validateMetadata($this->toArray(), $required_properties, $optional_properties);
Validate::metadata($this->toArray(), $required_properties, $optional_properties);
}
/**