Implemented concept peer association manager
This commit is contained in:
parent
a2f4b2b685
commit
42d331408c
11 changed files with 486 additions and 14 deletions
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
];
|
||||
}
|
|
@ -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
|
||||
];
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
307
src/FederationLib/Managers/AssociationManager.php
Normal file
307
src/FederationLib/Managers/AssociationManager.php
Normal 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));
|
||||
}
|
||||
}
|
119
src/FederationLib/Objects/PeerAssociationRecord.php
Normal file
119
src/FederationLib/Objects/PeerAssociationRecord.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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']))
|
||||
{
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
'language_code' => 'string',
|
||||
];
|
||||
|
||||
Validate::validateMetadata($this->toArray(), $required_properties, $optional_properties);
|
||||
Validate::metadata($this->toArray(), $required_properties, $optional_properties);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Reference in a new issue