. * * @author Daniil Gentili * @copyright 2016-2023 Daniil Gentili * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 * @link https://docs.madelineproto.xyz MadelineProto documentation */ namespace danog\MadelineProto\VoIP; use Amp\ByteStream\ReadableStream; use Amp\DeferredFuture; use danog\MadelineProto\LocalFile; use danog\MadelineProto\Logger; use danog\MadelineProto\Magic; use danog\MadelineProto\MTProtoTools\Crypt; use danog\MadelineProto\PeerNotInDbException; use danog\MadelineProto\RemoteUrl; use danog\MadelineProto\VoIP; use danog\MadelineProto\VoIPController; use phpseclib3\Math\BigInteger; use Throwable; use const STR_PAD_LEFT; /** * Manages the creation of the authorization key. * * https://core.telegram.org/mtproto/auth_key * https://core.telegram.org/mtproto/samples-auth_key * * @internal */ trait AuthKeyHandler { /** @var array */ private array $calls = []; /** @var array */ private array $callsByPeer = []; private array $pendingCalls = []; /** * Request VoIP call. * * @param mixed $user User */ public function requestCall(mixed $user): VoIP { $user = ($this->getInfo($user)); if (!isset($user['InputUser']) || $user['InputUser']['_'] === 'inputUserSelf') { throw new PeerNotInDbException(); } $user = $user['bot_api_id']; if (isset($this->pendingCalls[$user])) { return $this->pendingCalls[$user]->await(); } $deferred = new DeferredFuture; $this->pendingCalls[$user] = $deferred->getFuture(); try { $this->logger->logger(\sprintf('Calling %s...', $user), Logger::VERBOSE); $dh_config = ($this->getDhConfig()); $this->logger->logger('Generating a...', Logger::VERBOSE); $a = BigInteger::randomRange(Magic::$two, $dh_config['p']->subtract(Magic::$two)); $this->logger->logger('Generating g_a...', Logger::VERBOSE); $g_a = $dh_config['g']->powMod($a, $dh_config['p']); Crypt::checkG($g_a, $dh_config['p']); $res = $this->methodCallAsyncRead('phone.requestCall', ['user_id' => $user, 'g_a_hash' => \hash('sha256', $g_a->toBytes(), true), 'protocol' => ['_' => 'phoneCallProtocol', 'udp_p2p' => true, 'udp_reflector' => true, 'min_layer' => 65, 'max_layer' => 92]])['phone_call']; $res['a'] = $a; $res['g_a'] = \str_pad($g_a->toBytes(), 256, \chr(0), STR_PAD_LEFT); $this->calls[$res['id']] = $controller = new VoIPController($this, $res); $this->callsByPeer[$controller->public->otherID] = $controller; unset($this->pendingCalls[$user]); $deferred->complete($controller->public); } catch (Throwable $e) { unset($this->pendingCalls[$user]); $deferred->error($e); } return $deferred->getFuture()->await(); } /** @internal */ public function cleanupCall(int $id): void { if (isset($this->calls[$id])) { $call = $this->calls[$id]; unset($this->callsByPeer[$call->public->otherID], $this->calls[$id]); } } /** * Get call emojis (will return null if the call is not inited yet). * * @internal * * @return ?list{string, string, string, string} */ public function getCallVisualization(int $id): ?array { return ($this->calls[$id] ?? null)?->getVisualization(); } /** * Accept call. */ public function acceptCall(int $id): void { ($this->calls[$id] ?? null)?->accept(); } /** * Discard call. * * @param int<1, 5> $rating Call rating in stars * @param string $comment Additional comment on call quality. */ public function discardCall(int $id, DiscardReason $reason = DiscardReason::HANGUP, ?int $rating = null, ?string $comment = null): void { ($this->calls[$id] ?? null)?->discard($reason, $rating, $comment); } /** * Get the phone call with the specified user ID. */ public function getCallByPeer(int $userId): ?VoIP { return ($this->callsByPeer[$userId] ?? null)?->public; } /** * Get all pending and running calls, indexed by user ID. * * @return array */ public function getAllCalls(): array { return \array_map(fn (VoIPController $v): VoIP => $v->public, $this->callsByPeer); } /** * Get phone call information. */ public function getCall(int $id): ?VoIP { return ($this->calls[$id] ?? null)?->public; } /** * Play file in call. */ public function callPlay(int $id, LocalFile|RemoteUrl|ReadableStream $file): void { ($this->calls[$id] ?? null)?->play($file); } /** * Play file in call, blocking until the file has finished playing if a stream is provided. * * @internal */ public function callPlayBlocking(int $id, LocalFile|RemoteUrl|ReadableStream $file): void { if (!isset($this->calls[$id])) { return; } $this->calls[$id]->play($file); if ($file instanceof ReadableStream) { $deferred = new DeferredFuture; $file->onClose($deferred->complete(...)); $deferred->getFuture()->await(); } } /** * When called, skips to the next file in the playlist. */ public function skipPlay(int $id): void { ($this->calls[$id] ?? null)?->skip(); } /** * Stops playing all files in the call, clears the main and the hold playlist. */ public function stopPlay(int $id): void { ($this->calls[$id] ?? null)?->stop(); } /** * Pauses playback of the current audio file in the call. */ public function pausePlay(int $id): void { ($this->calls[$id] ?? null)?->pause(); } /** * Resumes playback of the current audio file in the call. */ public function resumePlay(int $id): void { ($this->calls[$id] ?? null)?->resume(); } /** * Whether the currently playing audio file is paused. */ public function isPlayPaused(int $id): bool { return ($this->calls[$id] ?? null)?->isPaused() ?? false; } /** * Play files on hold in call. */ public function callPlayOnHold(int $id, LocalFile|RemoteUrl|ReadableStream ...$files): void { ($this->calls[$id] ?? null)?->playOnHold(...$files); } /** * Play files on hold in call. * * @internal */ public function callPlayOnHoldBlocking(int $id, LocalFile|RemoteUrl|ReadableStream ...$files): void { if (!isset($this->calls[$id])) { return; } $this->calls[$id]->playOnHold(...$files); foreach ($files as $file) { if ($file instanceof ReadableStream) { $deferred = new DeferredFuture; $file->onClose($deferred->complete(...)); $deferred->getFuture()->await(); } } } /** * Get the file that is currently being played. * * Will return a string with the object ID of the stream if we're currently playing a stream, otherwise returns the related LocalFile or RemoteUrl. */ public function callGetCurrent(int $id): RemoteUrl|LocalFile|string|null { return ($this->calls[$id] ?? null)?->getCurrent(); } /** * Get call state. */ public function getCallState(int $id): ?CallState { return ($this->calls[$id] ?? null)?->getCallState(); } }