Implemented PeerMetadata Manager & Telegram User Metadata manager

This commit is contained in:
Netkas 2023-06-20 16:56:49 -04:00
parent a322c54a69
commit 8e2e1eaba1
No known key found for this signature in database
GPG key ID: 5DAF58535614062B
11 changed files with 841 additions and 24 deletions

View file

@ -6,6 +6,7 @@
use Exception;
use FederationLib\Enums\SerializationMethod;
use FederationLib\Interfaces\PeerMetadataInterface;
use FederationLib\Interfaces\SerializableObjectInterface;
use InvalidArgumentException;
use LogLib\Log;
@ -182,10 +183,14 @@
return $outliers;
}
public static function weightedRandomPick( array $data): string
/**
* @param array $data
* @return string
*/
public static function weightedRandomPick(array $data): string
{
$totalWeight = array_sum($data);
if($totalWeight == 0)
if($totalWeight === 0)
{
throw new InvalidArgumentException('Total weight cannot be 0');
}
@ -210,5 +215,37 @@
return $item;
}
}
// Select a random item if the cumulative weight is 0
return array_rand($data);
}
/**
* This function returns an array showing the changed values comparing from $a to $b
* if $b contains changes that is different from $a, it will be returned in the array
*
* @param PeerMetadataInterface $a
* @param PeerMetadataInterface $b
* @return array
*/
public static function metadataDifferences(PeerMetadataInterface $a, PeerMetadataInterface $b, array $filter=[]): array
{
$differences = [];
$a_array = $a->toArray();
$b_array = $b->toArray();
foreach ($a_array as $key => $value)
{
if(in_array($key, $filter, true))
{
continue;
}
if($value !== $b_array[$key])
{
$differences[$key] = $b_array[$key];
}
}
return $differences;
}
}

View file

@ -47,13 +47,25 @@
public const CLIENT_DISABLED = 1004;
/**
* The given peer metadata is invalid or malformed.
*/
public const INVALID_PEER_METADATA = 2000;
public const PEER_METADATA_NOT_FOUND = 2001;
public const ALL = [
self::INTERNAL_SERVER_ERROR,
self::ACCESS_DENIED,
self::CLIENT_NOT_FOUND,
self::INVALID_CLIENT_NAME,
self::INVALID_CLIENT_DESCRIPTION,
self::SIGNATURE_VERIFICATION_FAILED,
self::CLIENT_DISABLED
self::CLIENT_DISABLED,
self::INVALID_PEER_METADATA,
];
}

View file

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

View file

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

View file

@ -1,20 +0,0 @@
<?php
namespace FederationLib\Interfaces;
interface EntityObjectInterface extends SerializableObjectInterface
{
/**
* Validates the given object and returns true if it is valid.
*
* @return bool
*/
public function validate(): bool;
/**
* Returns the standard federated address of the entity.
*
* @return string
*/
public function getFederatedAddress(): string;
}

View file

@ -0,0 +1,91 @@
<?php
namespace FederationLib\Interfaces;
use FederationLib\Exceptions\Standard\InvalidPeerMetadataException;
interface PeerMetadataInterface extends SerializableObjectInterface
{
/**
* Validates if the metadata is valid or not
* throws InvalidPeerMetadataException if the metadata is invalid
*
* @throws InvalidPeerMetadataException
*/
public function validate(): void;
/**
* Returns True if the metadata indicates that the peer is a bot
* Returns False if there's no indication that the peer is a bot
*
* @return bool
*/
public function isAutomated(): bool;
/**
* Returns the platform-specific ID of the peer (e.g. Telegram Chat ID)
* If no ID is available, the method will try to find a unique ID in the metadata
* that can uniquely identify the peer
*
* @return string
*/
public function getPlatformId(): string;
/**
* Returns the calculated standard federated address of the peer
*
* @return string
*/
public function getFederatedAddress(): string;
/**
* Sets the timestamp of when the peer record was last updated (this is usually used internally)
*
* @param int|null $timestamp
* @return void
*/
public function setLastUpdatedTimestamp(?int $timestamp): void;
/**
* Returns the timestamp of when the peer record was last updated
*
* @return int|null
*/
public function getLastUpdatedTimestamp(): ?int;
/**
* Sets the Client's UUID that last updated the peer record (this is usually used internally)
*
* @param string|null $client
* @return void
*/
public function setUpdatedClient(?string $client): void;
/**
* Returns the Client's UUID that last updated the peer record
*
* @return string|null
*/
public function getUpdatedClient(): ?string;
/**
* Returns an array representation of the object, if $raw is true, the array will contain
* additional information that is not part of the standard metadata such as the client UUID
* and the last updated timestamp
*
* @param bool $raw
* @return array
*/
public function toArray(bool $raw=false): array;
/**
* Constructs object from an array representation, if $raw is enabled it will accept
* raw data that is not part of the standard metadata, this should be disabled if
* the data is coming from an untrusted source such as a user
*
* @param array $array
* @param bool $raw
* @return PeerMetadataInterface
*/
public static function fromArray(array $array, bool $raw=false): PeerMetadataInterface;
}

View file

@ -0,0 +1,38 @@
<?php
namespace FederationLib\Interfaces;
use FederationLib\Objects\Client;
use FederationLib\Objects\Peer;
use FederationLib\Objects\Standard\PeerMetadata\TelegramUserMetadata;
interface PeerMetadataManagerInterface
{
/**
* Intelligently syncs the metadata of the given federated address, this will ignore raw metadata & use
* data from the given $client_uuid
*
* @param Client|string $client_uuid
* @param TelegramUserMetadata $telegram_user_metadata
* @return string
*/
public function syncMetadata(Client|string $client_uuid, TelegramUserMetadata $telegram_user_metadata): string;
/**
* Registers the metadata of the given federated address, this will ignore raw metadata & use data from the
* given $client_uuid
*
* @param Client|string $client_uuid
* @param TelegramUserMetadata $telegram_user_metadata
* @return string
*/
public function registerMetadata(Client|string $client_uuid, TelegramUserMetadata $telegram_user_metadata): string;
/**
* Returns the metadata of the given federated address, this returns the full raw metadata
*
* @param Peer|string $federated_address
* @return TelegramUserMetadata
*/
public function getMetadata(Peer|string $federated_address): TelegramUserMetadata;
}

View file

@ -0,0 +1,284 @@
<?php
namespace FederationLib\Managers\MetadataManagers;
use Doctrine\DBAL\ParameterType;
use Exception;
use FederationLib\Classes\Configuration;
use FederationLib\Classes\Database;
use FederationLib\Classes\Utilities;
use FederationLib\Enums\DatabaseTables;
use FederationLib\Enums\Misc;
use FederationLib\Exceptions\DatabaseException;
use FederationLib\Exceptions\Standard\InvalidPeerMetadataException;
use FederationLib\Exceptions\Standard\PeerMetadataNotFoundException;
use FederationLib\Interfaces\PeerMetadataManagerInterface;
use FederationLib\Managers\RedisConnectionManager;
use FederationLib\Objects\Client;
use FederationLib\Objects\Peer;
use FederationLib\Objects\Standard\PeerMetadata\TelegramUserMetadata;
use LogLib\Log;
class TelegramUserManager implements PeerMetadataManagerInterface
{
/**
* @param Client|string $client_uuid
* @param TelegramUserMetadata $telegram_user_metadata
* @return string
* @throws DatabaseException
* @throws InvalidPeerMetadataException
*/
public function syncMetadata(Client|string $client_uuid, TelegramUserMetadata $telegram_user_metadata): string
{
if($client_uuid instanceof Client)
{
$client_uuid = $client_uuid->getUuid();
}
$telegram_user_metadata->validate();
$cache_key = sprintf('telegram_user_metadata:%s', $telegram_user_metadata->getFederatedAddress());
$redis_client = null;
if(Configuration::isCacheSystemEnabled() && Configuration::getObjectCacheEnabled('peer_objects'))
{
try
{
$redis_client = RedisConnectionManager::getConnectionFromConfig('peer_objects');
if($redis_client->exists($cache_key))
{
$redis_client->del($cache_key);
}
}
catch(Exception $e)
{
Log::warning(Misc::FEDERATIONLIB, sprintf('There was an error with the cache system while tyring to sync peer metadata with the Redis server: %s', $e->getMessage()), $e);
}
}
try
{
$old_metadata = $this->getMetadata($telegram_user_metadata->getFederatedAddress());
}
catch(PeerMetadataNotFoundException $e)
{
unset($e);
// If it doesn't exist, we create it instead of updating it
return $this->registerMetadata($client_uuid, $telegram_user_metadata);
}
// Take the old metadata and get the differences
$differences = Utilities::metadataDifferences($old_metadata, $telegram_user_metadata);
try
{
// If there are no differences, then we don't need to update the database
if(count($differences) === 0)
{
if($redis_client !== null && Configuration::getObjectCacheTtl('peer_objects') > 0)
{
// Reset the TTL since we just accessed it
$redis_client->expire($cache_key, Configuration::getObjectCacheTtl('peer_objects'));
}
return $telegram_user_metadata->getFederatedAddress();
}
$qb = Database::getConnection()->createQueryBuilder();
$qb->update(DatabaseTables::PEERS_TELEGRAM_USER);
$qb->where('federated_address = :federated_address');
$qb->setParameter('federated_address', $telegram_user_metadata->getFederatedAddress());
$qb->set('updated_timestamp', $qb->createNamedParameter(time(), ParameterType::INTEGER));
$qb->set('updated_client', $qb->createNamedParameter($client_uuid));
$qb->setMaxResults(1);
foreach($differences as $key => $value)
{
$qb->set($key, $qb->createNamedParameter($value));
}
$qb->executeQuery();
}
catch(Exception $e)
{
throw new DatabaseException(sprintf('There was a database error while trying to sync peer metadata: %s', $e->getMessage()), $e);
}
if($redis_client !== null && count($differences) > 0)
{
try
{
foreach($differences as $key => $value)
{
$redis_client->hSet($cache_key, $key, $value);
}
}
catch(Exception $e)
{
Log::warning(Misc::FEDERATIONLIB, sprintf('There was an error with the cache system while tyring to sync peer metadata with the Redis server: %s', $e->getMessage()), $e);
}
}
return $telegram_user_metadata->getFederatedAddress();
}
/**
* Registers the client's metadata with the database
*
* @param Client|string $client_uuid
* @param TelegramUserMetadata $telegram_user_metadata
* @return string
* @throws DatabaseException
*/
public function registerMetadata(Client|string $client_uuid, TelegramUserMetadata $telegram_user_metadata): string
{
if($client_uuid instanceof Client)
{
$client_uuid = $client_uuid->getUuid();
}
try
{
$telegram_user_metadata->validate();
$qb = Database::getConnection()->createQueryBuilder();
$qb->insert(DatabaseTables::PEERS_TELEGRAM_USER);
$qb->setValue('federated_address', $qb->createNamedParameter($telegram_user_metadata->getFederatedAddress()));
$qb->setValue('updated_timestamp', $qb->createNamedParameter(time(), ParameterType::INTEGER));
$qb->setValue('updated_client', $qb->createNamedParameter($client_uuid));
foreach($telegram_user_metadata->toArray() as $key => $value)
{
switch(gettype($value))
{
case 'array':
$qb->setValue($key, $qb->createNamedParameter(implode(',', $value)));
break;
case 'NULL':
$qb->setValue($key, $qb->createNamedParameter(null, ParameterType::NULL));
break;
case 'boolean':
$qb->setValue($key, $qb->createNamedParameter((int)$value, ParameterType::INTEGER));
break;
case 'integer':
$qb->setValue($key, $qb->createNamedParameter($value, ParameterType::INTEGER));
break;
default:
$qb->setValue($key, $qb->createNamedParameter($value));
break;
}
}
$qb->executeQuery();
}
catch(Exception $e)
{
throw new DatabaseException(sprintf('There was a database error while trying to register peer metadata: %s', $e->getMessage()), $e);
}
return $telegram_user_metadata->getFederatedAddress();
}
/**
* @param Peer|string $federated_address
* @throws PeerMetadataNotFoundException
* @throws DatabaseException
* @return TelegramUserMetadata
*/
public function getMetadata(Peer|string $federated_address): TelegramUserMetadata
{
if($federated_address instanceof Peer)
{
$federated_address = $federated_address->getFederatedAddress();
}
if(Configuration::isCacheSystemEnabled() && Configuration::getObjectCacheEnabled('peer_objects'))
{
try
{
$redis = RedisConnectionManager::getConnectionFromConfig('peer_objects');
$key = sprintf('telegram_user_metadata:%s', $federated_address);
if($redis->exists($key))
{
$telegram_user_metadata = TelegramUserMetadata::fromArray($redis->hGetAll($key), true);
if(Configuration::getObjectCacheTTL('peer_objects') > 0)
{
$redis->expire($key, Configuration::getObjectCacheTTL('peer_objects'));
}
Log::debug(Misc::FEDERATIONLIB, sprintf('Loaded peer metadata object %s from cache', $federated_address));
return $telegram_user_metadata;
}
}
catch(Exception $e)
{
Log::warning(Misc::FEDERATIONLIB, sprintf('Failed to load peer metadata object %s from cache: %s', $federated_address, $e->getMessage()));
}
}
$qb = Database::getConnection()->createQueryBuilder();
$qb->select(
'id',
'type',
'title',
'username',
'first_name',
'last_name',
'is_forum',
'updated_timestamp',
'updated_client'
);
$qb->from(DatabaseTables::PEERS_TELEGRAM_USER);
$qb->where('federated_address = :federated_address');
$qb->setParameter('federated_address', $federated_address);
$qb->setMaxResults(1);
try
{
$result = $qb->executeQuery();
if($result->rowCount() === 0)
{
throw new PeerMetadataNotFoundException(sprintf('Peer metadata not found for federated address %s', $federated_address));
}
$telegram_user_metadata = TelegramUserMetadata::fromArray($result->fetchAssociative(), true);
}
catch(PeerMetadataNotFoundException $e)
{
throw $e;
}
catch(Exception $e)
{
throw new DatabaseException(sprintf('Failed to get peer metadata for federated address %s', $federated_address), $e);
}
if(Configuration::isCacheSystemEnabled() && Configuration::getObjectCacheEnabled('peer_objects'))
{
try
{
$redis = RedisConnectionManager::getConnectionFromConfig('peer_objects');
$key = sprintf('telegram_user_metadata:%s', $federated_address);
$redis->hMSet($key, $telegram_user_metadata->toArray());
if(Configuration::getObjectCacheTTL('peer_objects') > 0)
{
$redis->expire($key, Configuration::getObjectCacheTTL('peer_objects'));
}
Log::debug(Misc::FEDERATIONLIB, sprintf('Cached peer metadata object %s', $federated_address));
}
catch(Exception $e)
{
Log::warning(Misc::FEDERATIONLIB, sprintf('Failed to cache peer metadata object %s: %s', $federated_address, $e->getMessage()));
}
}
return $telegram_user_metadata;
}
}

View file

@ -2,14 +2,22 @@
namespace FederationLib\Managers;
use FederationLib\Enums\Standard\UserPeerType;
use FederationLib\FederationLib;
use FederationLib\Interfaces\PeerMetadataManagerInterface;
use FederationLib\Objects\Standard\PeerMetadata\TelegramUserMetadata;
class PeerManager
{
/**
* @var FederationLib
*/
private FederationLib $federationLib;
private $federationLib;
/**
* @var PeerMetadataManagerInterface[]
*/
private $metadata_managers;
/**
* PeerManager constructor.
@ -19,5 +27,6 @@
public function __construct(FederationLib $federationLib)
{
$this->federationLib = $federationLib;
$this->metadata_managers[UserPeerType::TELEGRAM_USER] = new TelegramUserMetadata();
}
}

View file

@ -17,6 +17,19 @@
*/
private static $connections;
/**
* @param string $name
* @return \Redis
* @throws CacheConnectionException
*/
public static function getConnectionFromConfig(string $name): \Redis
{
return self::getConnection(
Configuration::getObjectCacheServerPreference($name),
Configuration::getObjectCacheServerFallback($name)
);
}
/**
* Returns the requested redis connection to use
*

View file

@ -0,0 +1,315 @@
<?php
/** @noinspection PhpUnused */
/** @noinspection PhpMissingFieldTypeInspection */
namespace FederationLib\Objects\Standard\PeerMetadata;
use FederationLib\Enums\Standard\UserPeerType;
use FederationLib\Exceptions\Standard\InvalidPeerMetadataException;
use FederationLib\Interfaces\PeerMetadataInterface;
class TelegramUserMetadata implements PeerMetadataInterface
{
/**
* @var int
*/
private $id;
/**
* @var bool
*/
private $is_bot;
/**
* @var string
*/
private $first_name;
/**
* @var string|null
*/
private $last_name;
/**
* @var string|null
*/
private $username;
/**
* @var string|null
*/
private $language_code;
/**
* @var bool
*/
private $is_premium;
/**
* @var int|null
*/
private $updated_timestamp;
/**
* @var string|null
*/
private $updated_client;
/**
* Validates the given metadata and throws an exception if the metadata is invalid
*
* @throws InvalidPeerMetadataException
*/
public function validate(): void
{
if($this->id === null)
{
throw new InvalidPeerMetadataException(sprintf('The property "id" is required in the metadata for peer type %s', UserPeerType::TELEGRAM_USER));
}
if(!is_int($this->id))
{
throw new InvalidPeerMetadataException(sprintf('The property "id" must be an integer in metadata for peer type %s, got %s', UserPeerType::TELEGRAM_USER, gettype($this->id)));
}
if(!is_bool($this->is_bot))
{
throw new InvalidPeerMetadataException(sprintf('The property "is_bot" must be a boolean in metadata for peer type %s, got %s', UserPeerType::TELEGRAM_USER, gettype($this->is_bot)));
}
if($this->first_name === null)
{
throw new InvalidPeerMetadataException(sprintf('The property "first_name" is required in the metadata for peer type %s', UserPeerType::TELEGRAM_USER));
}
if(!is_string($this->first_name))
{
throw new InvalidPeerMetadataException(sprintf('The property "first_name" must be a string in metadata for peer type %s, got %s', UserPeerType::TELEGRAM_USER, gettype($this->first_name)));
}
if($this->last_name !== null && !is_string($this->last_name))
{
throw new InvalidPeerMetadataException(sprintf('The property "last_name" must be a string in metadata for peer type %s, got %s', UserPeerType::TELEGRAM_USER, gettype($this->last_name)));
}
if($this->username !== null && !is_string($this->username))
{
throw new InvalidPeerMetadataException(sprintf('The property "username" must be a string in metadata for peer type %s, got %s', UserPeerType::TELEGRAM_USER, gettype($this->username)));
}
if($this->language_code !== null && !is_string($this->language_code))
{
throw new InvalidPeerMetadataException(sprintf('The property "language_code" must be a string in metadata for peer type %s, got %s', UserPeerType::TELEGRAM_USER, gettype($this->language_code)));
}
if(!is_bool($this->is_premium))
{
throw new InvalidPeerMetadataException(sprintf('The property "is_premium" must be a boolean in metadata for peer type %s, got %s', UserPeerType::TELEGRAM_USER, gettype($this->is_premium)));
}
}
/**
* Returns True if the peer is a bot, False otherwise
*
* @return bool
*/
public function isAutomated(): bool
{
return $this->is_bot;
}
/**
* Returns the platform-specific ID of the peer
*
* @return string
*/
public function getPlatformId(): string
{
return $this->id;
}
/**
* Returns the standard federated address of the peer
* Format: telegram.user:<id>
*
* @return string
*/
public function getFederatedAddress(): string
{
return sprintf('%s:%s', UserPeerType::TELEGRAM_USER, $this->id);
}
/**
* Sets the client UUID that last updated the record
*
* @param string|null $client
* @return void
*/
public function setUpdatedClient(?string $client): void
{
$this->updated_client = $client;
}
/**
* Returns the Timestamp for when the record was last updated
*
* @return int
*/
public function getLastUpdatedTimestamp(): int
{
return $this->updated_timestamp;
}
/**
* Sets the Timestamp for when the record was last updated
*
* @param int|null $timestamp
* @return void
*/
public function setLastUpdatedTimestamp(?int $timestamp): void
{
$this->updated_timestamp = $timestamp;
}
/**
* Returns the Client UUID that last updated the record
*
* @return ?string
*/
public function getUpdatedClient(): ?string
{
return $this->updated_client;
}
/**
* Unique identifier for this user or bot. This number may have more than 32 significant bits and some
* programming languages may have difficulty/silent defects in interpreting it. But it has at most 52
* significant bits, so a 64-bit integer or double-precision float type are safe for storing this identifier.
*
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* True, if this user is a bot
*
* @return bool
*/
public function isBot(): bool
{
return $this->is_bot;
}
/**
* User's or bot's first name
*
* @return string
*/
public function getFirstName(): string
{
return $this->first_name;
}
/**
* Optional. User's or bot's last name
*
* @return string|null
*/
public function getLastName(): ?string
{
return $this->last_name;
}
/**
* Optional. User's or bot's username
*
* @return string|null
*/
public function getUsername(): ?string
{
return $this->username;
}
/**
* Optional. IETF language tag of the user's language
*
* @return string|null
*/
public function getLanguageCode(): ?string
{
return $this->language_code;
}
/**
* True, if this user is a Telegram Premium user
*
* @return bool
*/
public function isPremium(): bool
{
return $this->is_premium;
}
/**
* Returns an array representation of the object.
*
* @param bool $raw
* @return array
*/
public function toArray(bool $raw = false): array
{
$return = [
'id' => $this->id,
'is_bot' => $this->is_bot,
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'username' => $this->username,
'language_code' => $this->language_code,
'is_premium' => $this->is_premium
];
if(!$raw)
{
return $return;
}
return array_merge($return, [
'updated_timestamp' => $this->updated_timestamp,
'updated_client' => $this->updated_client
]);
}
/**
* Constructs object from an array representation.
*
* @param array $array
* @param bool $raw
* @return TelegramUserMetadata
*/
public static function fromArray(array $array, bool $raw = false): TelegramUserMetadata
{
$object = new self();
$object->id = $array['id'];
$object->is_bot = $array['is_bot'] ?? false;
$object->first_name = $array['first_name'];
$object->last_name = $array['last_name'] ?? null;
$object->username = $array['username'] ?? null;
$object->language_code = $array['language_code'] ?? null;
$object->is_premium = $array['is_premium'] ?? false;
if(!$raw)
{
return $object;
}
$object->updated_timestamp = $array['updated_timestamp'] ?? null;
$object->updated_client = $array['updated_client'] ?? null;
return $object;
}
}