Pandabot/vendor/danog/madelineproto/src/MTProtoTools/ReferenceDatabase.php

577 lines
25 KiB
PHP
Executable file

<?php
declare(strict_types=1);
/**
* Files module.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto\MTProtoTools;
use Amp\Sync\LocalKeyedMutex;
use danog\AsyncOrm\Annotations\OrmMappedArray;
use danog\AsyncOrm\DbArray;
use danog\AsyncOrm\KeyType;
use danog\AsyncOrm\ValueType;
use danog\MadelineProto\Exception;
use danog\MadelineProto\LegacyMigrator;
use danog\MadelineProto\Logger;
use danog\MadelineProto\MTProto;
use danog\MadelineProto\MTProto\MTProtoOutgoingMessage;
use danog\MadelineProto\TL\TLCallback;
use danog\MadelineProto\Tools;
use Revolt\EventLoop;
use Webmozart\Assert\Assert;
/**
* Manages upload and download of files.
*
* @internal
*/
final class ReferenceDatabase implements TLCallback
{
use LegacyMigrator;
// Reference from a document
public const DOCUMENT_LOCATION = 0;
// Reference from a photo
public const PHOTO_LOCATION = 1;
// Reference from a photo location (can only be photo location)
public const PHOTO_LOCATION_LOCATION = 2;
// Peer + photo ID
public const USER_PHOTO_ORIGIN = 0;
// Peer (default photo ID)
public const PEER_PHOTO_ORIGIN = 1;
// set ID
public const STICKER_SET_ID_ORIGIN = 2;
// Peer + msg ID
public const MESSAGE_ORIGIN = 3;
public const SAVED_GIFS_ORIGIN = 4;
public const STICKER_SET_RECENT_ORIGIN = 5;
public const STICKER_SET_FAVED_ORIGIN = 6;
// emoticon
public const STICKER_SET_EMOTICON_ORIGIN = 8;
public const WALLPAPER_ORIGIN = 9;
public const LOCATION_CONTEXT = [
//'inputFileLocation' => self::PHOTO_LOCATION_LOCATION, // DEPRECATED
'inputDocumentFileLocation' => self::DOCUMENT_LOCATION,
'inputPhotoFileLocation' => self::PHOTO_LOCATION,
'inputPhoto' => self::PHOTO_LOCATION,
'inputDocument' => self::DOCUMENT_LOCATION,
];
public const METHOD_CONTEXT = ['photos.updateProfilePhoto' => self::USER_PHOTO_ORIGIN, 'photos.getUserPhotos' => self::USER_PHOTO_ORIGIN, 'photos.uploadProfilePhoto' => self::USER_PHOTO_ORIGIN, 'messages.getStickers' => self::STICKER_SET_EMOTICON_ORIGIN];
public const CONSTRUCTOR_CONTEXT = ['message' => self::MESSAGE_ORIGIN, 'messageService' => self::MESSAGE_ORIGIN, 'chatFull' => self::PEER_PHOTO_ORIGIN, 'channelFull' => self::PEER_PHOTO_ORIGIN, 'chat' => self::PEER_PHOTO_ORIGIN, 'channel' => self::PEER_PHOTO_ORIGIN, 'updateUserPhoto' => self::USER_PHOTO_ORIGIN, 'user' => self::USER_PHOTO_ORIGIN, 'userFull' => self::USER_PHOTO_ORIGIN, 'wallPaper' => self::WALLPAPER_ORIGIN, 'messages.savedGifs' => self::SAVED_GIFS_ORIGIN, 'messages.recentStickers' => self::STICKER_SET_RECENT_ORIGIN, 'messages.favedStickers' => self::STICKER_SET_FAVED_ORIGIN, 'messages.stickerSet' => self::STICKER_SET_ID_ORIGIN, 'document' => self::STICKER_SET_ID_ORIGIN];
private const V = 1;
/**
* References indexed by location.
* @var DbArray<string, array>
*/
#[OrmMappedArray(KeyType::STRING, ValueType::SCALAR)]
private $db;
/**
* @var array<string, list{string, int, array}>
*/
private array $pendingDb = [];
private array $cache = [];
private array $cacheContexts = [];
private array $refreshed = [];
private bool $refresh = false;
private int $refreshCount = 0;
private int $v = 0;
private LocalKeyedMutex $flushMutex;
public function __construct(private MTProto $API)
{
$this->flushMutex = new LocalKeyedMutex;
$this->v = self::V;
}
public function __sleep()
{
return ['db', 'pendingDb', 'API', 'v'];
}
public function __wakeup(): void
{
$this->flushMutex = new LocalKeyedMutex;
}
public function init(): void
{
$this->initDbProperties($this->API->getDbSettings(), $this->API->getDbPrefix().'_ReferenceDatabase_');
if ($this->v === 0) {
$this->db->clear();
$this->pendingDb = [];
$this->v = self::V;
}
foreach ($this->pendingDb as $key => $_) {
EventLoop::queue($this->flush(...), $key);
}
}
private function flush(string $location): void
{
if (!isset($this->pendingDb[$location])) {
return;
}
$lock = $this->flushMutex->acquire($location);
try {
if (!isset($this->pendingDb[$location])) {
return;
}
[
$reference,
$originType,
$origin
] = $this->pendingDb[$location];
$locationValue = $this->db[$location];
if (!$locationValue) {
$locationValue = ['origins' => []];
}
$locationValue['reference'] = $reference;
$locationValue['origins'][$originType] = $origin;
ksort($locationValue['origins']);
$this->db[$location] = $locationValue;
} finally {
unset($this->pendingDb[$location]);
EventLoop::queue($lock->release(...));
}
}
public function getMethodAfterResponseDeserializationCallbacks(): array
{
return array_fill_keys(array_keys(self::METHOD_CONTEXT), [$this->addOriginMethod(...)]);
}
public function getMethodBeforeResponseDeserializationCallbacks(): array
{
return array_fill_keys(array_keys(self::METHOD_CONTEXT), [$this->addOriginMethodContext(...)]);
}
public function getConstructorAfterDeserializationCallbacks(): array
{
return array_merge(
array_fill_keys(['document', 'photo', 'fileLocation'], [$this->addReference(...)]),
array_fill_keys(array_keys(self::CONSTRUCTOR_CONTEXT), [$this->addOrigin(...)]),
['document' => [$this->addReference(...), $this->addOrigin(...)]]
);
}
public function getConstructorBeforeDeserializationCallbacks(): array
{
return array_fill_keys(array_keys(self::CONSTRUCTOR_CONTEXT), [$this->addOriginContext(...)]);
}
public function getConstructorBeforeSerializationCallbacks(): array
{
return array_fill_keys(array_keys(self::LOCATION_CONTEXT), $this->populateReference(...));
}
public function getTypeMismatchCallbacks(): array
{
return [];
}
public function reset(): void
{
if ($this->cache) {
$this->API->logger('Found '.\count($this->cache).' pending contexts', Logger::ERROR);
$this->cache = [];
}
if ($this->cacheContexts) {
$this->API->logger('Found '.\count($this->cacheContexts).' pending contexts', Logger::ERROR);
$this->cacheContexts = [];
}
}
public function addReference(array $location): bool
{
if (!$this->cacheContexts) {
$this->API->logger('Trying to add reference out of context, report the following message to @danogentili!', Logger::ERROR);
$frames = [];
$previous = '';
foreach (debug_backtrace(0) as $k => $frame) {
if (isset($frame['function']) && $frame['function'] === 'deserialize') {
if (isset($frame['args'][1]['subtype'])) {
if ($frame['args'][1]['subtype'] === $previous) {
continue;
}
$frames[] = $frame['args'][1]['subtype'];
$previous = $frame['args'][1]['subtype'];
} elseif (isset($frame['args'][1]['type'])) {
if ($frame['args'][1]['type'] === '') {
break;
}
if ($frame['args'][1]['type'] === $previous) {
continue;
}
$frames[] = $frame['args'][1]['type'];
$previous = $frame['args'][1]['type'];
}
}
}
$frames = array_reverse($frames);
$tlTrace = array_shift($frames);
foreach ($frames as $frame) {
$tlTrace .= "['".$frame."']";
}
$this->API->logger($tlTrace, Logger::ERROR);
return false;
}
if (!isset($location['file_reference'])) {
$this->API->logger("Object {$location['_']} does not have reference", Logger::ERROR);
return false;
}
$key = \count($this->cacheContexts) - 1;
switch ($location['_']) {
case 'document':
$locationType = self::DOCUMENT_LOCATION;
break;
case 'photo':
$locationType = self::PHOTO_LOCATION;
break;
case 'fileLocation':
$locationType = self::PHOTO_LOCATION_LOCATION;
break;
default:
throw new Exception('Unknown location type provided: '.$location['_']);
}
$this->API->logger("Caching reference from location of type {$locationType} from {$location['_']}", Logger::ULTRA_VERBOSE);
if (!isset($this->cache[$key])) {
$this->cache[$key] = [];
}
$this->cache[$key][self::serializeLocation($locationType, $location)] = (string) $location['file_reference'];
return true;
}
public function addOriginContext(string $type): void
{
if (!isset(self::CONSTRUCTOR_CONTEXT[$type])) {
throw new Exception("Unknown origin type provided: {$type}");
}
$originContext = self::CONSTRUCTOR_CONTEXT[$type];
//$this->API->logger("Adding origin context {$originContext} for {$type}!", \danog\MadelineProto\Logger::ULTRA_VERBOSE);
$this->cacheContexts[] = $originContext;
}
public function addOrigin(array $data = []): void
{
$key = \count($this->cacheContexts) - 1;
if ($key === -1) {
throw new Exception("Trying to add origin to constructor {$data['_']} with no origin context set");
}
$originType = array_pop($this->cacheContexts);
if (!isset($this->cache[$key])) {
//$this->API->logger("Removing origin context {$originType} for {$data['_']}, nothing in the reference cache!", \danog\MadelineProto\Logger::ULTRA_VERBOSE);
return;
}
$cache = $this->cache[$key];
unset($this->cache[$key]);
$origin = [];
switch ($data['_']) {
case 'message':
case 'messageService':
$origin['peer'] = $this->API->getIdInternal($data);
$origin['msg_id'] = $data['id'];
break;
case 'messages.savedGifs':
case 'messages.recentStickers':
case 'messages.favedStickers':
case 'wallPaper':
break;
case 'user':
$origin['max_id'] = $data['photo']['photo_id'];
$origin['offset'] = -1;
$origin['limit'] = 1;
$origin['user_id'] = $data['id'];
break;
case 'updateUserPhoto':
$origin['max_id'] = $data['photo']['photo_id'];
$origin['offset'] = -1;
$origin['limit'] = 1;
$origin['user_id'] = $data['user_id'];
break;
case 'userFull':
if (!isset($data['profile_photo'])) {
$key = \count($this->cacheContexts) - 1;
if (!isset($this->cache[$key])) {
$this->cache[$key] = [];
}
foreach ($cache as $location => $reference) {
$this->cache[$key][$location] = $reference;
}
$this->API->logger("Skipped origin {$originType} ({$data['_']}) for ".\count($cache).' references', Logger::ULTRA_VERBOSE);
return;
}
$origin['max_id'] = $data['profile_photo']['id'];
$origin['offset'] = -1;
$origin['limit'] = 1;
$origin['user_id'] = $data['id'];
break;
case 'chatFull':
case 'chat':
$origin['peer'] = $data['id'];
break;
case 'channelFull':
case 'channel':
$origin['peer'] = $data['id'];
break;
case 'document':
foreach ($data['attributes'] as $attribute) {
if ($attribute['_'] === 'documentAttributeSticker' && $attribute['stickerset']['_'] !== 'inputStickerSetEmpty') {
$origin['stickerset'] = $attribute['stickerset'];
}
}
if (!isset($origin['stickerset'])) {
$key = \count($this->cacheContexts) - 1;
if (!isset($this->cache[$key])) {
$this->cache[$key] = [];
}
foreach ($cache as $location => $reference) {
$this->cache[$key][$location] = $reference;
}
$this->API->logger("Skipped origin {$originType} ({$data['_']}) for ".\count($cache).' references', Logger::ULTRA_VERBOSE);
return;
}
break;
case 'messages.stickerSet':
$origin['stickerset'] = ['_' => 'inputStickerSetID', 'id' => $data['set']['id'], 'access_hash' => $data['set']['access_hash']];
break;
default:
throw new Exception("Unknown origin type provided: {$data['_']}");
}
foreach ($cache as $location => $reference) {
$this->storeReference($location, $reference, $originType, $origin);
}
$this->API->logger("Added origin {$originType} ({$data['_']}) to ".\count($cache).' references', Logger::ULTRA_VERBOSE);
}
public function addOriginMethodContext(string $type): void
{
if (!isset(self::METHOD_CONTEXT[$type])) {
throw new Exception("Unknown origin type provided: {$type}");
}
$originContext = self::METHOD_CONTEXT[$type];
//$this->API->logger("Adding origin context {$originContext} for {$type}!", Logger::ULTRA_VERBOSE);
$this->cacheContexts[] = $originContext;
}
public function addOriginMethod(MTProtoOutgoingMessage $data, array $res): void
{
$key = \count($this->cacheContexts) - 1;
$constructor = $data->constructor;
if ($key === -1) {
throw new Exception("Trying to add origin to method $constructor with no origin context set");
}
$originType = array_pop($this->cacheContexts);
if (!isset($this->cache[$key])) {
//$this->API->logger("Removing origin context {$originType} for {$constructor}, nothing in the reference cache!", Logger::ULTRA_VERBOSE);
return;
}
$cache = $this->cache[$key];
unset($this->cache[$key]);
$origin = [];
switch ($data->constructor) {
case 'photos.updateProfilePhoto':
$origin['max_id'] = $res['photo_id'] ?? 0;
$origin['offset'] = -1;
$origin['limit'] = 1;
$origin['user_id'] = $this->API->authorization['user']['id'];
break;
case 'photos.uploadProfilePhoto':
$origin['max_id'] = $res['photo']['id'];
$origin['offset'] = -1;
$origin['limit'] = 1;
$origin['user_id'] = $this->API->authorization['user']['id'];
break;
case 'photos.getUserPhotos':
$origin['user_id'] = $data->getBodyOrEmpty()['user_id'];
$origin['offset'] = -1;
$origin['limit'] = 1;
$count = 0;
foreach ($res['photos'] as $photo) {
$origin['max_id'] = $photo['id'];
$dc_id = $photo['dc_id'];
$location = self::serializeLocation(self::PHOTO_LOCATION, $photo);
if (isset($cache[$location])) {
$reference = $cache[$location];
unset($cache[$location]);
$this->storeReference($location, $reference, $originType, $origin);
$count++;
}
if (isset($photo['sizes'])) {
foreach ($photo['sizes'] as $size) {
if (isset($size['location'])) {
$size['location']['dc_id'] = $dc_id;
$location = self::serializeLocation(self::PHOTO_LOCATION_LOCATION, $size['location']);
if (isset($cache[$location])) {
$reference = $cache[$location];
unset($cache[$location]);
$this->storeReference($location, $reference, $originType, $origin);
$count++;
}
}
}
}
}
$this->API->logger("Added origin {$originType} ($constructor) to {$count} references", Logger::ULTRA_VERBOSE);
return;
case 'messages.getStickers':
$origin['emoticon'] = $data->getBodyOrEmpty()['emoticon'];
break;
default:
throw new Exception("Unknown origin type provided: {$constructor}");
}
foreach ($cache as $location => $reference) {
$this->storeReference($location, $reference, $originType, $origin);
}
$this->API->logger("Added origin {$originType} ({$constructor}) to ".\count($cache).' references', Logger::ULTRA_VERBOSE);
}
private function storeReference(string $location, string $reference, int $originType, array $origin): void
{
$this->pendingDb[$location] = [
$reference,
$originType,
$origin,
];
if ($this->refresh) {
$this->refreshed[$location] = true;
}
$key = \count($this->cacheContexts) - 1;
if ($key >= 0) {
$this->cache[$key][$location] = $reference;
}
EventLoop::queue($this->flush(...), $location);
}
public function refreshNextEnable(): void
{
if ($this->refreshCount === 0) {
$this->refreshed = [];
$this->refreshCount++;
$this->refresh = true;
} else {
$this->refreshCount++;
}
}
public function refreshNextDisable(): void
{
if ($this->refreshCount === 1) {
$this->refreshed = [];
$this->refreshCount--;
$this->refresh = false;
} elseif ($this->refreshCount === 0) {
} else {
$this->refreshCount--;
}
}
private function populateReference(array $object): array
{
$object['file_reference'] = $this->getReference(self::LOCATION_CONTEXT[$object['_']], $object);
return $object;
}
private function getDb(string $location): ?array
{
while (isset($this->pendingDb[$location])) {
$this->flush($location);
}
return $this->db[$location];
}
public function getReference(int $locationType, array $location): string
{
$locationString = self::serializeLocation($locationType, $location);
$res = $this->getDb($locationString);
if (!isset($res['reference'])) {
if (isset($location['file_reference'])) {
$this->API->logger("Using outdated file reference for location of type {$locationType} object {$location['_']}", Logger::ULTRA_VERBOSE);
if (\is_array($location['file_reference'])) {
Assert::eq($location['file_reference']['_'], 'bytes');
return base64_decode($location['file_reference']['bytes'], true);
}
return (string) $location['file_reference'];
}
if (!$this->refresh) {
$this->API->logger("Using null file reference for location of type {$locationType} object {$location['_']}", Logger::ULTRA_VERBOSE);
return '';
}
throw new Exception("Could not find file reference for location of type {$locationType} object {$location['_']}");
}
$this->API->logger("Getting file reference for location of type {$locationType} object {$location['_']}", Logger::ULTRA_VERBOSE);
if ($this->refresh) {
if (isset($this->refreshed[$locationString])) {
$this->API->logger('Reference already refreshed!', Logger::VERBOSE);
return (string) $this->getDb($locationString)['reference'];
}
$count = 0;
foreach ($this->getDb($locationString)['origins'] as $originType => $origin) {
$count++;
$this->API->logger("Try {$count} refreshing file reference with origin type {$originType}", Logger::VERBOSE);
switch ($originType) {
// Peer + msg ID
case self::MESSAGE_ORIGIN:
if (\is_array($origin['peer'])) {
$origin['peer'] = $this->API->getIdInternal($origin['peer']);
}
if ($origin['peer'] < 0) {
$this->API->methodCallAsyncRead('channels.getMessages', ['channel' => $origin['peer'], 'id' => [$origin['msg_id']]]);
break;
}
$this->API->methodCallAsyncRead('messages.getMessages', ['id' => [$origin['msg_id']]]);
break;
// Peer + photo ID
case self::PEER_PHOTO_ORIGIN:
$this->API->peerDatabase->expireFull($origin['peer']);
$this->API->getFullInfo($origin['peer']);
break;
// Peer (default photo ID)
case self::USER_PHOTO_ORIGIN:
$this->API->methodCallAsyncRead('photos.getUserPhotos', $origin);
break;
case self::SAVED_GIFS_ORIGIN:
$this->API->methodCallAsyncRead('messages.getSavedGifs', $origin);
break;
case self::STICKER_SET_ID_ORIGIN:
$this->API->methodCallAsyncRead('messages.getStickerSet', $origin);
break;
case self::STICKER_SET_RECENT_ORIGIN:
$this->API->methodCallAsyncRead('messages.getRecentStickers', $origin);
break;
case self::STICKER_SET_FAVED_ORIGIN:
$this->API->methodCallAsyncRead('messages.getFavedStickers', $origin);
break;
case self::STICKER_SET_EMOTICON_ORIGIN:
$this->API->methodCallAsyncRead('messages.getStickers', $origin);
break;
case self::WALLPAPER_ORIGIN:
$this->API->methodCallAsyncRead('account.getWallPapers', $origin);
break;
default:
throw new Exception("Unknown origin type {$originType}");
}
if (isset($this->refreshed[$locationString])) {
return (string) $this->getDb($locationString)['reference'];
}
}
throw new Exception('Did not refresh reference');
}
return (string) $this->getDb($locationString)['reference'];
}
private static function serializeLocation(int $locationType, array $location): string
{
switch ($locationType) {
case self::DOCUMENT_LOCATION:
case self::PHOTO_LOCATION:
return $locationType.bin2hex(Tools::packSignedLong($location['id']));
case self::PHOTO_LOCATION_LOCATION:
$dc_id = Tools::packSignedInt($location['dc_id']);
$volume_id = Tools::packSignedLong($location['volume_id']);
$local_id = Tools::packSignedInt($location['local_id']);
return $locationType.bin2hex($dc_id.$volume_id.$local_id);
}
throw new Exception('Invalid location type specified!');
}
public function __debugInfo()
{
return ['ReferenceDatabase instance '.spl_object_hash($this)];
}
}