. * * @author Daniil Gentili * @copyright 2016-2020 Daniil Gentili * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 * * @link https://docs.madelineproto.xyz MadelineProto documentation */ namespace danog\MadelineProto\MTProtoTools; use Amp\Http\Client\Request; use danog\Decoder\FileId; use danog\Decoder\PhotoSizeSource\PhotoSizeSourceDialogPhoto; use danog\MadelineProto\Db\DbArray; use danog\MadelineProto\Settings; use danog\MadelineProto\Tools; use const danog\Decoder\PROFILE_PHOTO; /** * Manages peers. * * @property Settings $settings Settings */ trait PeerHandler { public $caching_simple = []; public $caching_simple_username = []; public $caching_possible_username = []; public $caching_full_info = []; /** * Convert MTProto channel ID to bot API channel ID. * * @param int $id MTProto channel ID * * @return float|int */ public static function toSupergroup($id) { return -($id + \pow(10, (int) \floor(\log($id, 10) + 3))); } /** * Convert bot API channel ID to MTProto channel ID. * * @param int $id Bot API channel ID * * @return float|int */ public static function fromSupergroup($id) { return -$id - \pow(10, (int) \floor(\log(-$id, 10))); } /** * Check whether provided bot API ID is a channel. * * @param int $id Bot API ID * * @return boolean */ public static function isSupergroup($id): bool { $log = \log(-$id, 10); return ($log - \intval($log)) * 1000 < 10; } /** * Set support info. * * @param array $support Support info * * @internal * * @return void */ public function addSupport(array $support): void { $this->supportUser = $support['user']['id']; } /** * Add user info. * * @param array $user User info * * @return \Generator * @throws \danog\MadelineProto\Exception */ public function addUser(array $user): \Generator { $existingChat = yield $this->chats[$user['id']]; if ($existingChat) { $this->cacheChatUsername($user['id'], $user); } if (!isset($user['access_hash']) && !($user['min'] ?? false)) { if (!empty($existingChat['access_hash'])) { $this->logger->logger("No access hash with user {$user['id']}, using backup"); $user['access_hash'] = $existingChat['access_hash']; } elseif (!isset($this->caching_simple[$user['id']]) && !(isset($user['username']) && isset($this->caching_simple_username[$user['username']]))) { $this->logger->logger("No access hash with user {$user['id']}, trying to fetch by ID..."); if (isset($user['username']) && !isset($this->caching_simple_username[$user['username']])) { $this->caching_possible_username[$user['id']] = $user['username']; } $this->cachePwrChat($user['id'], false, true); } elseif (isset($user['username']) && !$existingChat && !isset($this->caching_simple_username[$user['username']])) { $this->logger->logger("No access hash with user {$user['id']}, trying to fetch by username..."); $this->cachePwrChat($user['username'], false, true); } else { $this->logger->logger("No access hash with user {$user['id']}, tried and failed to fetch data..."); } return; } switch ($user['_']) { case 'user': if (!$existingChat || $existingChat !== $user) { $this->logger->logger("Updated user {$user['id']}", \danog\MadelineProto\Logger::ULTRA_VERBOSE); if (($user['min'] ?? false) && !($existingChat['min'] ?? false)) { $this->logger->logger("{$user['id']} is min, filling missing fields", \danog\MadelineProto\Logger::ULTRA_VERBOSE); if (isset($existingChat['access_hash'])) { $user['min'] = false; $user['access_hash'] = $existingChat['access_hash']; } } yield $this->chats->offsetSet($user['id'], $user); $this->cachePwrChat($user['id'], false, true); } $this->cacheChatUsername($user['id'], $user); break; case 'userEmpty': break; default: throw new \danog\MadelineProto\Exception('Invalid user provided', $user); } } /** * Add chat to database. * * @param array $chat Chat * * @internal * * @return \Generator * * @psalm-return \Generator */ public function addChat($chat): \Generator { switch ($chat['_']) { case 'chat': case 'chatEmpty': case 'chatForbidden': $existingChat = yield $this->chats[-$chat['id']]; if (!$existingChat || $existingChat !== $chat) { $this->logger->logger("Updated chat -{$chat['id']}", \danog\MadelineProto\Logger::ULTRA_VERBOSE); yield $this->chats->offsetSet(-$chat['id'], $chat); $this->cachePwrChat(-$chat['id'], $this->getSettings()->getPeer()->getFullFetch(), true); } $this->cacheChatUsername(-$chat['id'], $chat); break; case 'channelEmpty': break; case 'channel': case 'channelForbidden': $bot_api_id = $this->toSupergroup($chat['id']); if (!isset($chat['access_hash'])) { if (!isset($this->caching_simple[$bot_api_id]) && !(isset($chat['username']) && isset($this->caching_simple_username[$chat['username']]))) { $this->logger->logger("No access hash with {$chat['_']} {$bot_api_id}, trying to fetch by ID..."); if (isset($chat['username']) && !isset($this->caching_simple_username[$chat['username']])) { $this->caching_possible_username[$bot_api_id] = $chat['username']; } $this->cachePwrChat($bot_api_id, false, true); } elseif (isset($chat['username']) && !(yield $this->chats[$bot_api_id]) && !isset($this->caching_simple_username[$chat['username']])) { $this->logger->logger("No access hash with {$chat['_']} {$bot_api_id}, trying to fetch by username..."); $this->cachePwrChat($chat['username'], false, true); } else { $this->logger->logger("No access hash with {$chat['_']} {$bot_api_id}, tried and failed to fetch data..."); } return; } $existingChat = yield $this->chats[$bot_api_id]; if (!$existingChat || $existingChat !== $chat) { $this->logger->logger("Updated chat {$bot_api_id}", \danog\MadelineProto\Logger::ULTRA_VERBOSE); if (($chat['min'] ?? false) && $existingChat && !($existingChat['min'] ?? false)) { $this->logger->logger("{$bot_api_id} is min, filling missing fields", \danog\MadelineProto\Logger::ULTRA_VERBOSE); $newchat = $existingChat; foreach (['title', 'username', 'photo', 'banned_rights', 'megagroup', 'verified'] as $field) { if (isset($chat[$field])) { $newchat[$field] = $chat[$field]; } } $chat = $newchat; } yield $this->chats->offsetSet($bot_api_id, $chat); $fullChat = yield $this->full_chats[$bot_api_id]; if ($this->getSettings()->getPeer()->getFullFetch() && (!$fullChat || $fullChat['full']['participants_count'] !== (yield from $this->getFullInfo($bot_api_id))['full']['participants_count'])) { $this->cachePwrChat($bot_api_id, $this->getSettings()->getPeer()->getFullFetch(), true); } } $this->cacheChatUsername($bot_api_id, $chat); break; } } private function cacheChatUsername($id, array $chat): void { if ($id && !empty($chat['username'])) { $this->usernames[\strtolower($chat['username'])] = $id; } } private function cachePwrChat($id, $full_fetch, $send): void { \danog\MadelineProto\Tools::callFork((function () use ($id, $full_fetch, $send): \Generator { try { yield from $this->getPwrChat($id, $full_fetch, $send); } catch (\danog\MadelineProto\Exception $e) { $this->logger->logger('While caching: '.$e->getMessage(), \danog\MadelineProto\Logger::WARNING); } catch (\danog\MadelineProto\RPCErrorException $e) { $this->logger->logger('While caching: '.$e->getMessage(), \danog\MadelineProto\Logger::WARNING); } })()); } /** * Check if peer is present in internal peer database. * * @param mixed $id Peer * * @return \Generator * * @psalm-return \Generator */ public function peerIsset($id): \Generator { try { $info = yield from $this->getInfo($id); $chatId = $info['bot_api_id']; return (yield $this->chats[$chatId]) !== null; } catch (\danog\MadelineProto\Exception $e) { return false; } catch (\danog\MadelineProto\RPCErrorException $e) { if ($e->rpc === 'CHAT_FORBIDDEN') { return true; } if ($e->rpc === 'CHANNEL_PRIVATE') { return true; } return false; } } /** * Check if all peer entities are in db. * * @param array $entities Entity list * * @internal * * @return \Generator */ public function entitiesPeerIsset(array $entities): \Generator { try { foreach ($entities as $entity) { if ($entity['_'] === 'messageEntityMentionName' || $entity['_'] === 'inputMessageEntityMentionName') { if (!(yield from $this->peerIsset($entity['user_id']))) { return false; } } } } catch (\danog\MadelineProto\Exception $e) { return false; } return true; } /** * Check if fwd peer is set. * * @param array $fwd Forward info * * @internal * * @return \Generator */ public function fwdPeerIsset(array $fwd): \Generator { try { if (isset($fwd['user_id']) && !(yield from $this->peerIsset($fwd['user_id']))) { return false; } if (isset($fwd['channel_id']) && !(yield from $this->peerIsset($this->toSupergroup($fwd['channel_id'])))) { return false; } } catch (\danog\MadelineProto\Exception $e) { return false; } return true; } /** * Get folder ID from object. * * @param mixed $id Object * * @return ?int */ public static function getFolderId($id): ?int { if (!\is_array($id)) { return null; } if (!isset($id['folder_id'])) { return null; } return $id['folder_id']; } /** * Get bot API ID from peer object. * * @param mixed $id Peer * * @return int */ public function getId($id) { if (\is_array($id)) { switch ($id['_']) { case 'updateDialogPinned': case 'updateDialogUnreadMark': case 'updateNotifySettings': $id = $id['peer']; // no break case 'updateDraftMessage': case 'inputDialogPeer': case 'dialogPeer': case 'inputNotifyPeer': case 'notifyPeer': case 'dialog': case 'help.proxyDataPromo': case 'updateChatDefaultBannedRights': case 'folderPeer': case 'inputFolderPeer': return $this->getId($id['peer']); case 'inputUserFromMessage': case 'inputPeerUserFromMessage': return $id['user_id']; case 'inputChannelFromMessage': case 'inputPeerChannelFromMessage': return $this->toSupergroup($id['channel_id']); case 'inputUserSelf': case 'inputPeerSelf': return $this->authorization['user']['id']; case 'user': return $id['id']; case 'userFull': return $id['user']['id']; case 'inputPeerUser': case 'inputUser': case 'peerUser': case 'messageEntityMentionName': case 'messageActionChatDeleteUser': return $id['user_id']; case 'messageActionChatJoinedByLink': return $id['inviter_id']; case 'chat': case 'chatForbidden': case 'chatFull': return -$id['id']; case 'inputPeerChat': case 'peerChat': return -$id['chat_id']; case 'channelForbidden': case 'channel': case 'channelFull': return $this->toSupergroup($id['id']); case 'inputPeerChannel': case 'inputChannel': case 'peerChannel': return $this->toSupergroup($id['channel_id']); case 'message': case 'messageService': if (!isset($id['from_id']) // No other option // It's a channel/chat, 100% what we need || $id['peer_id']['_'] !== 'peerUser' // It is a user, and it's not ourselves || $id['peer_id']['user_id'] !== $this->authorization['user']['id'] ) { return $this->getId($id['peer_id']); } return $this->getId($id['from_id']); case 'updateChannelReadMessagesContents': case 'updateChannelAvailableMessages': case 'updateChannel': case 'updateChannelWebPage': case 'updateChannelMessageViews': case 'updateReadChannelInbox': case 'updateReadChannelOutbox': case 'updateDeleteChannelMessages': case 'updateChannelPinnedMessage': case 'updateChannelTooLong': return $this->toSupergroup($id['channel_id']); case 'updateChatParticipants': $id = $id['participants']; // no break case 'updateChatUserTyping': case 'updateChatParticipantAdd': case 'updateChatParticipantDelete': case 'updateChatParticipantAdmin': case 'updateChatAdmins': case 'updateChatPinnedMessage': return -$id['chat_id']; case 'updateUserTyping': case 'updateUserStatus': case 'updateUserName': case 'updateUserPhoto': case 'updateUserPhone': case 'updateUserBlocked': case 'updateContactRegistered': case 'updateContactLink': case 'updateBotInlineQuery': case 'updateInlineBotCallbackQuery': case 'updateBotInlineSend': case 'updateBotCallbackQuery': case 'updateBotPrecheckoutQuery': case 'updateBotShippingQuery': case 'updateUserPinnedMessage': case 'contact': return $id['user_id']; case 'updatePhoneCall': return $id['phone_call']->getOtherID(); case 'updateReadHistoryInbox': case 'updateReadHistoryOutbox': return $this->getId($id['peer']); case 'updateNewMessage': case 'updateNewChannelMessage': case 'updateEditMessage': case 'updateEditChannelMessage': return $this->getId($id['message']); default: throw new \danog\MadelineProto\Exception('Invalid constructor given '.\var_export($id, true)); } } if (\is_string($id)) { if (\strpos($id, '#') !== false) { if (\preg_match('/^channel#(\\d*)/', $id, $matches)) { return $this->toSupergroup($matches[1]); } if (\preg_match('/^chat#(\\d*)/', $id, $matches)) { $id = '-'.$matches[1]; } if (\preg_match('/^user#(\\d*)/', $id, $matches)) { return $matches[1]; } } } if (\is_numeric($id)) { if (\is_string($id)) { $id = \danog\MadelineProto\Magic::$bigint ? (float) $id : (int) $id; } return $id; } return false; } /** * Get info about peer, returns an Info object. * * @param mixed $id Peer * @param boolean $recursive Internal * * @see https://docs.madelineproto.xyz/Info.html * * @return \Generator Info object * * @psalm-return \Generator|array, mixed, array{ * InputPeer: array{_: string, user_id?: mixed, access_hash?: mixed, min?: mixed, chat_id?: mixed, channel_id?: mixed}, * Peer: array{_: string, user_id?: mixed, chat_id?: mixed, channel_id?: mixed}, * DialogPeer: array{_: string, peer: array{_: string, user_id?: mixed, chat_id?: mixed, channel_id?: mixed}}, * NotifyPeer: array{_: string, peer: array{_: string, user_id?: mixed, chat_id?: mixed, channel_id?: mixed}}, * InputDialogPeer: array{_: string, peer: array{_: string, user_id?: mixed, access_hash?: mixed, min?: mixed, chat_id?: mixed, channel_id?: mixed}}, * InputNotifyPeer: array{_: string, peer: array{_: string, user_id?: mixed, access_hash?: mixed, min?: mixed, chat_id?: mixed, channel_id?: mixed}}, * bot_api_id: int|string, * user_id?: int, * chat_id?: int, * channel_id?: int, * InputUser?: array{_: string, user_id?: int, access_hash?: mixed, min?: bool}, * InputChannel?: array{_: string, channel_id: int, access_hash: mixed, min: bool}, * type: string * }> */ public function getInfo($id, $recursive = true): \Generator { if (\is_array($id)) { switch ($id['_']) { case 'updateEncryption': return $this->getSecretChat($id['chat']['id']); case 'inputEncryptedChat': case 'updateEncryptedChatTyping': case 'updateEncryptedMessagesRead': return $this->getSecretChat($id['chat_id']); case 'updateNewEncryptedMessage': $id = $id['message']; // no break case 'encryptedMessage': case 'encryptedMessageService': $id = $id['chat_id']; if (!isset($this->secret_chats[$id])) { throw new \danog\MadelineProto\Exception(\danog\MadelineProto\Lang::$current_lang['sec_peer_not_in_db']); } return $this->secret_chats[$id]; } } $folder_id = $this->getFolderId($id); $try_id = $this->getId($id); if ($try_id !== false) { $id = $try_id; } $tried_simple = false; if (\is_numeric($id)) { if (! yield $this->chats[$id]) { try { $this->logger->logger("Try fetching {$id} with access hash 0"); $this->caching_simple[$id] = true; if ($id < 0) { if ($this->isSupergroup($id)) { yield from $this->methodCallAsyncRead('channels.getChannels', ['id' => [['access_hash' => 0, 'channel_id' => $this->fromSupergroup($id), '_' => 'inputChannel']]], ['datacenter' => $this->datacenter->curdc]); } else { yield from $this->methodCallAsyncRead('messages.getFullChat', ['chat_id' => -$id], ['datacenter' => $this->datacenter->curdc]); } } else { yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [['access_hash' => 0, 'user_id' => $id, '_' => 'inputUser']]], ['datacenter' => $this->datacenter->curdc]); } } catch (\danog\MadelineProto\Exception $e) { $this->logger->logger($e->getMessage(), \danog\MadelineProto\Logger::WARNING); } catch (\danog\MadelineProto\RPCErrorException $e) { $this->logger->logger($e->getMessage(), \danog\MadelineProto\Logger::WARNING); } finally { if (isset($this->caching_simple[$id])) { unset($this->caching_simple[$id]); } $tried_simple = true; } } if (yield $this->chats[$id]) { if (((yield $this->chats[$id])['min'] ?? false) && yield $this->minDatabase->hasPeer($id) && !isset($this->caching_full_info[$id])) { $this->caching_full_info[$id] = true; $this->logger->logger("Only have min peer for {$id} in database, trying to fetch full info"); try { if ($id < 0) { yield from $this->methodCallAsyncRead('channels.getChannels', ['id' => [$this->genAll(yield $this->chats[$id], $folder_id)['InputChannel']]], ['datacenter' => $this->datacenter->curdc]); } else { yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [$this->genAll(yield $this->chats[$id], $folder_id)['InputUser']]], ['datacenter' => $this->datacenter->curdc]); } } catch (\danog\MadelineProto\Exception $e) { $this->logger->logger($e->getMessage(), \danog\MadelineProto\Logger::WARNING); } catch (\danog\MadelineProto\RPCErrorException $e) { $this->logger->logger($e->getMessage(), \danog\MadelineProto\Logger::WARNING); } finally { unset($this->caching_full_info[$id]); } } try { return $this->genAll(yield $this->chats[$id], $folder_id); } catch (\danog\MadelineProto\Exception $e) { if ($e->getMessage() === 'This peer is not present in the internal peer database') { yield $this->chats->offsetUnset($id);/** @uses DbArray::offsetUnset() */ } else { throw $e; } } } if ($this->settings->getPwr()->getRequests() && $recursive) { $dbres = []; try { $dbres = \json_decode(yield from $this->datacenter->fileGetContents('https://id.pwrtelegram.xyz/db/getusername?id='.$id), true); } catch (\Throwable $e) { $this->logger->logger($e); } if (isset($dbres['ok']) && $dbres['ok']) { yield from $this->resolveUsername('@'.$dbres['result']); return yield from $this->getInfo($id, false); } } if ($tried_simple && isset($this->caching_possible_username[$id])) { $this->logger->logger("No access hash with {$id}, trying to fetch by username..."); $user = $this->caching_possible_username[$id]; unset($this->caching_possible_username[$id]); return yield from $this->getInfo($user); } throw new \danog\MadelineProto\Exception('This peer is not present in the internal peer database'); } if (\preg_match('@(?:t|telegram)\\.(?:me|dog)/(joinchat/)?([a-z0-9_-]*)@i', $id, $matches)) { if ($matches[1] === '') { $id = $matches[2]; } else { $invite = yield from $this->methodCallAsyncRead('messages.checkChatInvite', ['hash' => $matches[2]], ['datacenter' => $this->datacenter->curdc]); if (isset($invite['chat'])) { return yield from $this->getInfo($invite['chat']); } throw new \danog\MadelineProto\Exception('You have not joined this chat'); } } $id = \strtolower(\str_replace('@', '', $id)); if ($id === 'me') { return yield from $this->getInfo($this->authorization['user']['id']); } if ($id === 'support') { if (!$this->supportUser) { yield from $this->methodCallAsyncRead('help.getSupport', [], $this->settings->getDefaultDcParams()); } return yield from $this->getInfo($this->supportUser); } if ($bot_api_id = yield $this->usernames[$id]) { $chat = yield $this->chats[$bot_api_id]; if (empty($chat['username']) || \strtolower($chat['username']) !== $id) { yield $this->usernames->offsetUnset($id); /** @uses DbArray::offsetUnset() */ } if (isset($chat['username']) && \strtolower($chat['username']) === $id) { if ($chat['min'] ?? false && !isset($this->caching_full_info[$bot_api_id])) { $this->caching_full_info[$bot_api_id] = true; $this->logger->logger("Only have min peer for {$bot_api_id} in database, trying to fetch full info"); try { if ($bot_api_id < 0) { yield from $this->methodCallAsyncRead('channels.getChannels', ['id' => [$this->genAll(yield $this->chats[$bot_api_id], $folder_id)['InputChannel']]], ['datacenter' => $this->datacenter->curdc]); } else { yield from $this->methodCallAsyncRead('users.getUsers', ['id' => [$this->genAll(yield $this->chats[$bot_api_id], $folder_id)['InputUser']]], ['datacenter' => $this->datacenter->curdc]); } } catch (\danog\MadelineProto\Exception $e) { $this->logger->logger($e->getMessage(), \danog\MadelineProto\Logger::WARNING); } catch (\danog\MadelineProto\RPCErrorException $e) { $this->logger->logger($e->getMessage(), \danog\MadelineProto\Logger::WARNING); } finally { unset($this->caching_full_info[$bot_api_id]); } } return $this->genAll(yield $this->chats[$bot_api_id], $folder_id); } } if ($recursive) { yield from $this->resolveUsername($id); return yield from $this->getInfo($id, false); } throw new \danog\MadelineProto\Exception('This peer is not present in the internal peer database'); } /** * @return (((mixed|string)[]|mixed|string)[]|int|mixed|string)[] * * @psalm-return array{ * InputPeer: array{_: string, user_id?: mixed, access_hash?: mixed, min?: mixed, chat_id?: mixed, channel_id?: mixed}, * Peer: array{_: string, user_id?: mixed, chat_id?: mixed, channel_id?: mixed}, * DialogPeer: array{_: string, peer: array{_: string, user_id?: mixed, chat_id?: mixed, channel_id?: mixed}}, * NotifyPeer: array{_: string, peer: array{_: string, user_id?: mixed, chat_id?: mixed, channel_id?: mixed}}, * InputDialogPeer: array{_: string, peer: array{_: string, user_id?: mixed, access_hash?: mixed, min?: mixed, chat_id?: mixed, channel_id?: mixed}}, * InputNotifyPeer: array{_: string, peer: array{_: string, user_id?: mixed, access_hash?: mixed, min?: mixed, chat_id?: mixed, channel_id?: mixed}}, * bot_api_id: int|string, * user_id?: int, * chat_id?: int, * channel_id?: int, * InputUser?: {_: string, user_id?: int, access_hash?: mixed, min?: bool}, * InputChannel?: {_: string, channel_id: int, access_hash: mixed, min: bool}, * type: string * } */ private function genAll($constructor, $folder_id = null): array { $res = [$this->TL->getConstructors()->findByPredicate($constructor['_'])['type'] => $constructor]; switch ($constructor['_']) { case 'user': if ($constructor['self'] ?? false) { $res['InputPeer'] = ['_' => 'inputPeerSelf']; $res['InputUser'] = ['_' => 'inputUserSelf']; } elseif (isset($constructor['access_hash'])) { $res['InputPeer'] = ['_' => 'inputPeerUser', 'user_id' => $constructor['id'], 'access_hash' => $constructor['access_hash'], 'min' => $constructor['min']]; $res['InputUser'] = ['_' => 'inputUser', 'user_id' => $constructor['id'], 'access_hash' => $constructor['access_hash'], 'min' => $constructor['min']]; } else { throw new \danog\MadelineProto\Exception('This peer is not present in the internal peer database'); } $res['Peer'] = ['_' => 'peerUser', 'user_id' => $constructor['id']]; $res['DialogPeer'] = ['_' => 'dialogPeer', 'peer' => $res['Peer']]; $res['NotifyPeer'] = ['_' => 'notifyPeer', 'peer' => $res['Peer']]; $res['InputDialogPeer'] = ['_' => 'inputDialogPeer', 'peer' => $res['InputPeer']]; $res['InputNotifyPeer'] = ['_' => 'inputNotifyPeer', 'peer' => $res['InputPeer']]; $res['user_id'] = $constructor['id']; $res['bot_api_id'] = $constructor['id']; $res['type'] = $constructor['bot'] ?? false ? 'bot' : 'user'; break; case 'chat': case 'chatForbidden': $res['InputPeer'] = ['_' => 'inputPeerChat', 'chat_id' => $constructor['id']]; $res['Peer'] = ['_' => 'peerChat', 'chat_id' => $constructor['id']]; $res['DialogPeer'] = ['_' => 'dialogPeer', 'peer' => $res['Peer']]; $res['NotifyPeer'] = ['_' => 'notifyPeer', 'peer' => $res['Peer']]; $res['InputDialogPeer'] = ['_' => 'inputDialogPeer', 'peer' => $res['InputPeer']]; $res['InputNotifyPeer'] = ['_' => 'inputNotifyPeer', 'peer' => $res['InputPeer']]; $res['chat_id'] = $constructor['id']; $res['bot_api_id'] = -$constructor['id']; $res['type'] = 'chat'; break; case 'channel': if (!isset($constructor['access_hash'])) { throw new \danog\MadelineProto\Exception('This peer is not present in the internal peer database'); } $res['InputPeer'] = ['_' => 'inputPeerChannel', 'channel_id' => $constructor['id'], 'access_hash' => $constructor['access_hash'], 'min' => $constructor['min']]; $res['Peer'] = ['_' => 'peerChannel', 'channel_id' => $constructor['id']]; $res['DialogPeer'] = ['_' => 'dialogPeer', 'peer' => $res['Peer']]; $res['NotifyPeer'] = ['_' => 'notifyPeer', 'peer' => $res['Peer']]; $res['InputDialogPeer'] = ['_' => 'inputDialogPeer', 'peer' => $res['InputPeer']]; $res['InputNotifyPeer'] = ['_' => 'inputNotifyPeer', 'peer' => $res['InputPeer']]; $res['InputChannel'] = ['_' => 'inputChannel', 'channel_id' => $constructor['id'], 'access_hash' => $constructor['access_hash'], 'min' => $constructor['min']]; $res['channel_id'] = $constructor['id']; $res['bot_api_id'] = $this->toSupergroup($constructor['id']); $res['type'] = $constructor['megagroup'] ?? false ? 'supergroup' : 'channel'; break; case 'channelForbidden': throw new \danog\MadelineProto\Exception('This peer is not present in the internal peer database'); default: throw new \danog\MadelineProto\Exception('Invalid constructor given '.\var_export($constructor, true)); } if ($folder_id) { $res['FolderPeer'] = ['_' => 'folderPeer', 'peer' => $res['Peer'], 'folder_id' => $folder_id]; $res['InputFolderPeer'] = ['_' => 'inputFolderPeer', 'peer' => $res['InputPeer'], 'folder_id' => $folder_id]; } return $res; } /** * When were full info for this chat last cached. * * @param mixed $id Chat ID * * @return \Generator */ public function fullChatLastUpdated($id): \Generator { return (yield $this->full_chats[$id])['last_update'] ?? 0; } /** * Get full info about peer, returns an FullInfo object. * * @param mixed $id Peer * * @see https://docs.madelineproto.xyz/FullInfo.html * * @return \Generator FullInfo object * * @psalm-return \Generator */ public function getFullInfo($id): \Generator { $partial = (yield from $this->getInfo($id)); if (\time() - (yield from $this->fullChatLastUpdated($partial['bot_api_id'])) < $this->getSettings()->getPeer()->getFullInfoCacheTime()) { return \array_merge($partial, yield $this->full_chats[$partial['bot_api_id']]); } $full = null; switch ($partial['type']) { case 'user': case 'bot': $full = yield from $this->methodCallAsyncRead('users.getFullUser', ['id' => $partial['InputUser']], ['datacenter' => $this->datacenter->curdc]); break; case 'chat': $full = (yield from $this->methodCallAsyncRead('messages.getFullChat', $partial, ['datacenter' => $this->datacenter->curdc]))['full_chat']; break; case 'channel': case 'supergroup': $full = (yield from $this->methodCallAsyncRead('channels.getFullChannel', ['channel' => $partial['InputChannel']], ['datacenter' => $this->datacenter->curdc]))['full_chat']; break; } $res = []; $res['full'] = $full; $res['last_update'] = \time(); $this->full_chats[$partial['bot_api_id']] = $res; $partial = (yield from $this->getInfo($id)); return \array_merge($partial, $res); } /** * Get full info about peer (including full list of channel members), returns a Chat object. * * @param mixed $id Peer * * @see https://docs.madelineproto.xyz/Chat.html * * @return \Generator Chat object */ public function getPwrChat($id, bool $fullfetch = true, bool $send = true): \Generator { $full = $fullfetch ? yield from $this->getFullInfo($id) : (yield from $this->getInfo($id)); $res = ['id' => $full['bot_api_id'], 'type' => $full['type']]; switch ($full['type']) { case 'user': case 'bot': foreach (['first_name', 'last_name', 'username', 'verified', 'restricted', 'restriction_reason', 'status', 'bot_inline_placeholder', 'access_hash', 'phone', 'lang_code', 'bot_nochats'] as $key) { if (isset($full['User'][$key])) { $res[$key] = $full['User'][$key]; } } foreach (['about', 'bot_info', 'phone_calls_available', 'phone_calls_private', 'common_chats_count', 'can_pin_message', 'pinned_msg_id', 'notify_settings'] as $key) { if (isset($full['full'][$key])) { $res[$key] = $full['full'][$key]; } } if (isset($full['full']['profile_photo']['sizes'])) { $res['photo'] = $full['full']['profile_photo']; } break; case 'chat': foreach (['title', 'participants_count', 'admin', 'admins_enabled'] as $key) { if (isset($full['Chat'][$key])) { $res[$key] = $full['Chat'][$key]; } } foreach (['bot_info', 'pinned_msg_id', 'notify_settings'] as $key) { if (isset($full['full'][$key])) { $res[$key] = $full['full'][$key]; } } if (isset($res['admins_enabled'])) { $res['all_members_are_administrators'] = !$res['admins_enabled']; } if (isset($full['full']['chat_photo']['sizes'])) { $res['photo'] = $full['full']['chat_photo']; } if (isset($full['full']['exported_invite']['link'])) { $res['invite'] = $full['full']['exported_invite']['link']; } if (isset($full['full']['participants']['participants'])) { $res['participants'] = $full['full']['participants']['participants']; } break; case 'channel': case 'supergroup': foreach (['title', 'democracy', 'restricted', 'restriction_reason', 'access_hash', 'username', 'signatures'] as $key) { if (isset($full['Chat'][$key])) { $res[$key] = $full['Chat'][$key]; } } foreach (['read_inbox_max_id', 'read_outbox_max_id', 'hidden_prehistory', 'bot_info', 'notify_settings', 'can_set_stickers', 'stickerset', 'can_view_participants', 'can_set_username', 'participants_count', 'admins_count', 'kicked_count', 'banned_count', 'migrated_from_chat_id', 'migrated_from_max_id', 'pinned_msg_id', 'about', 'hidden_prehistory', 'available_min_id', 'can_view_stats', 'online_count'] as $key) { if (isset($full['full'][$key])) { $res[$key] = $full['full'][$key]; } } if (isset($full['full']['chat_photo']['sizes'])) { $res['photo'] = $full['full']['chat_photo']; } if (isset($full['full']['exported_invite']['link'])) { $res['invite'] = $full['full']['exported_invite']['link']; } if (isset($full['full']['participants']['participants'])) { $res['participants'] = $full['full']['participants']['participants']; } break; } if (isset($res['participants']) && $fullfetch) { foreach ($res['participants'] as $key => $participant) { $newres = []; $newres['user'] = (yield from $this->getPwrChat($participant['user_id'], false, true)); if (isset($participant['inviter_id'])) { $newres['inviter'] = (yield from $this->getPwrChat($participant['inviter_id'], false, true)); } if (isset($participant['promoted_by'])) { $newres['promoted_by'] = (yield from $this->getPwrChat($participant['promoted_by'], false, true)); } if (isset($participant['kicked_by'])) { $newres['kicked_by'] = (yield from $this->getPwrChat($participant['kicked_by'], false, true)); } if (isset($participant['date'])) { $newres['date'] = $participant['date']; } if (isset($participant['admin_rights'])) { $newres['admin_rights'] = $participant['admin_rights']; } if (isset($participant['banned_rights'])) { $newres['banned_rights'] = $participant['banned_rights']; } if (isset($participant['can_edit'])) { $newres['can_edit'] = $participant['can_edit']; } if (isset($participant['left'])) { $newres['left'] = $participant['left']; } switch ($participant['_']) { case 'chatParticipant': $newres['role'] = 'user'; break; case 'chatParticipantAdmin': $newres['role'] = 'admin'; break; case 'chatParticipantCreator': $newres['role'] = 'creator'; break; } $res['participants'][$key] = $newres; } } if (!isset($res['participants']) && $fullfetch && \in_array($res['type'], ['supergroup', 'channel'])) { $total_count = (isset($res['participants_count']) ? $res['participants_count'] : 0) + (isset($res['admins_count']) ? $res['admins_count'] : 0) + (isset($res['kicked_count']) ? $res['kicked_count'] : 0) + (isset($res['banned_count']) ? $res['banned_count'] : 0); $res['participants'] = []; $limit = 200; $filters = ['channelParticipantsAdmins', 'channelParticipantsBots']; foreach ($filters as $filter) { yield from $this->fetchParticipants($full['InputChannel'], $filter, '', $total_count, $res); } $q = ''; $filters = ['channelParticipantsSearch', 'channelParticipantsKicked', 'channelParticipantsBanned']; foreach ($filters as $filter) { yield from $this->recurseAlphabetSearchParticipants($full['InputChannel'], $filter, $q, $total_count, $res); } $this->logger->logger('Fetched '.\count($res['participants'])." out of {$total_count}"); $res['participants'] = \array_values($res['participants']); } if (!$fullfetch) { unset($res['participants']); } if ($fullfetch || $send) { $this->storeDb($res); } if (isset($res['photo'])) { $photo = []; foreach ([ 'small' => $res['photo']['sizes'][0], 'big' => Tools::maxSize($res['photo']['sizes']), ] as $type => $size) { $fileId = new FileId; $fileId->setId($res['photo']['id'] ?? 0); $fileId->setAccessHash($res['photo']['access_hash'] ?? 0); $fileId->setFileReference($res['photo']['file_reference'] ?? ''); $fileId->setDcId($res['photo']['dc_id']); $fileId->setType(PROFILE_PHOTO); $fileId->setLocalId($size['location']['local_id']); $fileId->setVolumeId($size['location']['volume_id']); $photoSize = new PhotoSizeSourceDialogPhoto; $photoSize->setDialogId($res['id']); $photoSize->setDialogPhotoSmall($type === 'small'); $photoSize->setDialogAccessHash($res['access_hash'] ?? 0); $fileId->setPhotoSizeSource($photoSize); $photo[$type.'_file_id'] = (string) $fileId; $photo[$type.'_file_unique_id'] = $fileId->getUniqueBotAPI(); } $res['photo'] += $photo; } return $res; } private function recurseAlphabetSearchParticipants($channel, $filter, $q, $total_count, &$res): \Generator { if (!(yield from $this->fetchParticipants($channel, $filter, $q, $total_count, $res))) { return false; } for ($x = 'a'; $x !== 'aa' && $total_count > \count($res['participants']); $x++) { yield from $this->recurseAlphabetSearchParticipants($channel, $filter, $q.$x, $total_count, $res); } } private function fetchParticipants($channel, $filter, $q, $total_count, &$res): \Generator { $offset = 0; $limit = 200; $has_more = false; $cached = false; $last_count = -1; do { try { $gres = yield from $this->methodCallAsyncRead('channels.getParticipants', ['channel' => $channel, 'filter' => ['_' => $filter, 'q' => $q], 'offset' => $offset, 'limit' => $limit, 'hash' => $hash = yield from $this->getParticipantsHash($channel, $filter, $q, $offset, $limit)], ['datacenter' => $this->datacenter->curdc, 'heavy' => true]); } catch (\danog\MadelineProto\RPCErrorException $e) { if ($e->rpc === 'CHAT_ADMIN_REQUIRED') { $this->logger->logger($e->rpc); return $has_more; } throw $e; } if ($cached = ($gres['_'] === 'channels.channelParticipantsNotModified')) { $gres = yield from $this->fetchParticipantsCache($channel, $filter, $q, $offset, $limit); } else { yield from $this->storeParticipantsCache($gres, $channel, $filter, $q, $offset, $limit); } if ($last_count !== -1 && $last_count !== $gres['count']) { $has_more = true; } else { $last_count = $gres['count']; } foreach ($gres['participants'] as $participant) { $newres = []; $newres['user'] = (yield from $this->getPwrChat($participant['user_id'], false, true)); if (isset($participant['inviter_id'])) { $newres['inviter'] = (yield from $this->getPwrChat($participant['inviter_id'], false, true)); } if (isset($participant['kicked_by'])) { $newres['kicked_by'] = (yield from $this->getPwrChat($participant['kicked_by'], false, true)); } if (isset($participant['promoted_by'])) { $newres['promoted_by'] = (yield from $this->getPwrChat($participant['promoted_by'], false, true)); } if (isset($participant['date'])) { $newres['date'] = $participant['date']; } if (isset($participant['rank'])) { $newres['rank'] = $participant['rank']; } if (isset($participant['admin_rights'])) { $newres['admin_rights'] = $participant['admin_rights']; } if (isset($participant['banned_rights'])) { $newres['banned_rights'] = $participant['banned_rights']; } switch ($participant['_']) { case 'channelParticipantSelf': $newres['role'] = 'user'; break; case 'channelParticipant': $newres['role'] = 'user'; break; case 'channelParticipantCreator': $newres['role'] = 'creator'; break; case 'channelParticipantAdmin': $newres['role'] = 'admin'; break; case 'channelParticipantBanned': $newres['role'] = 'banned'; break; } $res['participants'][$participant['user_id']] = $newres; } $this->logger->logger('Fetched '.\count($gres['participants'])." channel participants with filter {$filter}, query {$q}, offset {$offset}, limit {$limit}, hash {$hash}: ".($cached ? 'cached' : 'not cached').', '.($offset + \count($gres['participants'])).' participants out of '.$gres['count'].', in total fetched '.\count($res['participants']).' out of '.$total_count); $offset += \count($gres['participants']); } while (\count($gres['participants'])); if ($offset === $limit) { return true; } return $has_more; } private function fetchParticipantsCache($channel, $filter, $q, $offset, $limit): \Generator { return (yield $this->channel_participants[$channel['channel_id']])[$filter][$q][$offset][$limit]; } private function storeParticipantsCache($gres, $channel, $filter, $q, $offset, $limit): \Generator { unset($gres['users']); $ids = []; foreach ($gres['participants'] as $participant) { $ids[] = $participant['user_id']; } \sort($ids, SORT_NUMERIC); $gres['hash'] = \danog\MadelineProto\Tools::genVectorHash($ids); $participant = yield $this->channel_participants[$channel['channel_id']]; $participant[$filter][$q][$offset][$limit] = $gres; $this->channel_participants[$channel['channel_id']] = $participant; } private function getParticipantsHash($channel, $filter, $q, $offset, $limit): \Generator { return (yield $this->channel_participants[$channel['channel_id']])[$filter][$q][$offset][$limit]['hash'] ?? 0; } private function storeDb($res, $force = false): \Generator { if (!$this->settings->getPwr()->getDbToken() || $this->settings->getConnection()->getTestMode()) { return; } if (!empty($res)) { if (isset($res['participants'])) { unset($res['participants']); } $this->qres[] = $res; } if ($this->last_stored > \time() && !$force) { //$this->logger->logger("========== WILL SERIALIZE IN ".($this->last_stored - time())." ============="); return false; } if (empty($this->qres)) { return false; } try { $payload = \json_encode($this->qres); //$path = '/tmp/ids'.hash('sha256', $payload); //file_put_contents($path, $payload); $id = isset($this->authorization['user']['username']) ? $this->authorization['user']['username'] : $this->authorization['user']['id']; $request = new Request('https://id.pwrtelegram.xyz/db'.$this->settings->getPwr()->getDbToken().'/addnewmadeline?d=pls&from='.$id, 'POST'); $request->setHeader('content-type', 'application/json'); $request->setBody($payload); $result = yield (yield $this->datacenter->getHTTPClient()->request($request))->getBody()->buffer(); $this->logger->logger("============ {$result} =============", \danog\MadelineProto\Logger::VERBOSE); $this->qres = []; $this->last_stored = \time() + 10; } catch (\danog\MadelineProto\Exception $e) { $this->logger->logger('======= COULD NOT STORE IN DB DUE TO '.$e->getMessage().' =============', \danog\MadelineProto\Logger::VERBOSE); } } /** * Resolve username (use getInfo instead). * * @param string $username Username * * @return \Generator */ public function resolveUsername(string $username): \Generator { $username = \str_replace('@', '', $username); if (!$username) { return false; } try { $this->caching_simple_username[$username] = true; $res = yield from $this->methodCallAsyncRead('contacts.resolveUsername', ['username' => $username], ['datacenter' => $this->datacenter->curdc]); } catch (\danog\MadelineProto\RPCErrorException $e) { $this->logger->logger('Username resolution failed with error '.$e->getMessage(), \danog\MadelineProto\Logger::ERROR); if (\strpos($e->rpc, 'FLOOD_WAIT_') === 0 || $e->rpc === 'AUTH_KEY_UNREGISTERED' || $e->rpc === 'USERNAME_INVALID') { throw $e; } return false; } finally { if (isset($this->caching_simple_username[$username])) { unset($this->caching_simple_username[$username]); } } if ($res['_'] === 'contacts.resolvedPeer') { foreach ($res['chats'] as $chat) { yield from $this->addChat($chat); } foreach ($res['users'] as $user) { yield from $this->addUser($user); } return $res; } return false; } }