From 42d331408c6e0ea79999f1169c3d78e264e74faa Mon Sep 17 00:00:00 2001 From: Netkas Date: Fri, 23 Jun 2023 00:28:30 -0400 Subject: [PATCH] Implemented concept peer association manager --- src/FederationLib/Classes/Configuration.php | 5 + src/FederationLib/Enums/DatabaseTables.php | 4 +- .../Enums/Standard/ErrorCodes.php | 12 +- .../Enums/Standard/PeerAssociationType.php | 8 +- .../PeerAssociationNotFoundException.php | 19 ++ .../InvalidPeerAssociationTypeException.php | 19 ++ src/FederationLib/FederationLib.php | 3 +- .../Managers/AssociationManager.php | 307 ++++++++++++++++++ .../Objects/PeerAssociationRecord.php | 119 +++++++ .../PeerMetadata/TelegramChatMetadata.php | 2 +- .../PeerMetadata/TelegramUserMetadata.php | 2 +- 11 files changed, 486 insertions(+), 14 deletions(-) create mode 100644 src/FederationLib/Exceptions/PeerAssociationNotFoundException.php create mode 100644 src/FederationLib/Exceptions/Standard/InvalidPeerAssociationTypeException.php create mode 100644 src/FederationLib/Managers/AssociationManager.php create mode 100644 src/FederationLib/Objects/PeerAssociationRecord.php diff --git a/src/FederationLib/Classes/Configuration.php b/src/FederationLib/Classes/Configuration.php index 30f33ea..989cdd6 100644 --- a/src/FederationLib/Classes/Configuration.php +++ b/src/FederationLib/Classes/Configuration.php @@ -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); diff --git a/src/FederationLib/Enums/DatabaseTables.php b/src/FederationLib/Enums/DatabaseTables.php index 1b2f7a2..e33a007 100644 --- a/src/FederationLib/Enums/DatabaseTables.php +++ b/src/FederationLib/Enums/DatabaseTables.php @@ -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, diff --git a/src/FederationLib/Enums/Standard/ErrorCodes.php b/src/FederationLib/Enums/Standard/ErrorCodes.php index bd695a5..2c67447 100644 --- a/src/FederationLib/Enums/Standard/ErrorCodes.php +++ b/src/FederationLib/Enums/Standard/ErrorCodes.php @@ -1,4 +1,4 @@ -checkPermission(Methods::CHANGE_CLIENT_NAME, $this->resolveIdentity($identity))) { @@ -263,5 +263,4 @@ return true; } - } \ No newline at end of file diff --git a/src/FederationLib/Managers/AssociationManager.php b/src/FederationLib/Managers/AssociationManager.php new file mode 100644 index 0000000..a808302 --- /dev/null +++ b/src/FederationLib/Managers/AssociationManager.php @@ -0,0 +1,307 @@ +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)); + } + } \ No newline at end of file diff --git a/src/FederationLib/Objects/PeerAssociationRecord.php b/src/FederationLib/Objects/PeerAssociationRecord.php new file mode 100644 index 0000000..0d639ec --- /dev/null +++ b/src/FederationLib/Objects/PeerAssociationRecord.php @@ -0,0 +1,119 @@ +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; + } + + } \ No newline at end of file diff --git a/src/FederationLib/Objects/Standard/PeerMetadata/TelegramChatMetadata.php b/src/FederationLib/Objects/Standard/PeerMetadata/TelegramChatMetadata.php index ddb0687..874ce3d 100644 --- a/src/FederationLib/Objects/Standard/PeerMetadata/TelegramChatMetadata.php +++ b/src/FederationLib/Objects/Standard/PeerMetadata/TelegramChatMetadata.php @@ -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'])) { diff --git a/src/FederationLib/Objects/Standard/PeerMetadata/TelegramUserMetadata.php b/src/FederationLib/Objects/Standard/PeerMetadata/TelegramUserMetadata.php index 72f20ea..94881e1 100644 --- a/src/FederationLib/Objects/Standard/PeerMetadata/TelegramUserMetadata.php +++ b/src/FederationLib/Objects/Standard/PeerMetadata/TelegramUserMetadata.php @@ -78,7 +78,7 @@ 'language_code' => 'string', ]; - Validate::validateMetadata($this->toArray(), $required_properties, $optional_properties); + Validate::metadata($this->toArray(), $required_properties, $optional_properties); } /**