diff --git a/SECURITY.md b/SECURITY.md
index d56568498..abf462aab 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -5,8 +5,7 @@
| Version | Supported |
| ------- | ------------------ |
| 8.x | :white_check_mark: |
-| 7.x | :white_check_mark: |
-| < 7.x | :x: |
+| < 8.x | :x: |
## Reporting a Vulnerability
diff --git a/examples/bot.php b/examples/bot.php
index fa1a32515..a9cb70642 100755
--- a/examples/bot.php
+++ b/examples/bot.php
@@ -40,6 +40,7 @@ use danog\MadelineProto\Settings\Database\Mysql;
use danog\MadelineProto\Settings\Database\Postgres;
use danog\MadelineProto\Settings\Database\Redis;
use danog\MadelineProto\SimpleEventHandler;
+use danog\MadelineProto\VoIP;
// MadelineProto is already loaded
if (class_exists(API::class)) {
@@ -56,6 +57,9 @@ if (class_exists(API::class)) {
/**
* Event handler class.
*
+ * NOTE: ALL of the following methods are OPTIONAL.
+ * You can even provide an empty event handler if you want.
+ *
* All properties returned by __sleep are automatically stored in the database.
*/
class MyEventHandler extends SimpleEventHandler
@@ -228,7 +232,7 @@ class MyEventHandler extends SimpleEventHandler
$message->reply($args[0] ?? '');
}
- #[FilterRegex('/.*(mt?proto).*/i')]
+ #[FilterRegex('/.*(mt?proto)[^.]?.*/i')]
public function testRegex(Incoming & Message $message): void
{
$message->reply("Did you mean to write MadelineProto instead of ".$message->matches[1].'?');
@@ -288,6 +292,18 @@ class MyEventHandler extends SimpleEventHandler
$reply->reply("Download link: ".$reply->media->getDownloadLink());
}
+ #[FilterCommand('call')]
+ public function callVoip(Incoming&Message $message): void
+ {
+ $this->requestCall($message->senderId)->play(__DIR__.'/../music.ogg');
+ }
+
+ #[Handler]
+ public function handleIncomingCall(VoIP $call): void
+ {
+ $call->accept()->play(__DIR__.'/../music.ogg');
+ }
+
public static function getPluginPaths(): string|array|null
{
return 'plugins/';
diff --git a/src/EventHandler/Filter/FilterIncoming.php b/src/EventHandler/Filter/FilterIncoming.php
index ba2beb12b..53ab9bc6a 100644
--- a/src/EventHandler/Filter/FilterIncoming.php
+++ b/src/EventHandler/Filter/FilterIncoming.php
@@ -5,6 +5,8 @@ namespace danog\MadelineProto\EventHandler\Filter;
use Attribute;
use danog\MadelineProto\EventHandler\AbstractMessage;
use danog\MadelineProto\EventHandler\Update;
+use danog\MadelineProto\VoIP;
+use danog\MadelineProto\VoIP\CallState;
/**
* Allow only incoming messages.
@@ -14,6 +16,8 @@ final class FilterIncoming extends Filter
{
public function apply(Update $update): bool
{
- return $update instanceof AbstractMessage && !$update->out;
+ return ($update instanceof AbstractMessage && !$update->out)
+ || ($update instanceof VoIP && $update->getCallState() === CallState::INCOMING)
+ ;
}
}
diff --git a/src/Loop/Connection/CheckLoop.php b/src/Loop/Connection/CheckLoop.php
index cf70c2525..a9b2500f4 100644
--- a/src/Loop/Connection/CheckLoop.php
+++ b/src/Loop/Connection/CheckLoop.php
@@ -126,6 +126,7 @@ final class CheckLoop extends Loop
$list = '';
// Don't edit this here pls
foreach ($message_ids as $message_id) {
+ if (!isset($this->connection->outgoing_messages[$message_id])) continue;
$list .= $this->connection->outgoing_messages[$message_id]->getConstructor().', ';
}
$this->logger->logger("Still missing {$list} on DC {$this->datacenter}, sending state request", Logger::ERROR);
diff --git a/src/MTProto.php b/src/MTProto.php
index 55845b5bf..0384bfca9 100644
--- a/src/MTProto.php
+++ b/src/MTProto.php
@@ -59,6 +59,7 @@ use danog\MadelineProto\TL\TL;
use danog\MadelineProto\TL\TLCallback;
use danog\MadelineProto\TL\TLInterface;
use danog\MadelineProto\TL\Types\LoginQrCode;
+use danog\MadelineProto\VoIP\CallState;
use danog\MadelineProto\Wrappers\Ads;
use danog\MadelineProto\Wrappers\Button;
use danog\MadelineProto\Wrappers\DialogHandler;
@@ -293,10 +294,6 @@ final class MTProto implements TLCallback, LoggerGetter
* Config loop.
*/
public ?PeriodicLoopInternal $configLoop = null;
- /**
- * Call checker loop.
- */
- private ?PeriodicLoopInternal $callCheckerLoop = null;
/**
* Autoserialization loop.
*/
@@ -820,7 +817,6 @@ final class MTProto implements TLCallback, LoggerGetter
*/
private function startLoops(): void
{
- $this->callCheckerLoop ??= new PeriodicLoopInternal($this, $this->checkCalls(...), 'call check', 10);
$this->serializeLoop ??= new PeriodicLoopInternal($this, $this->serialize(...), 'serialize', $this->settings->getSerialization()->getInterval());
$this->phoneConfigLoop ??= new PeriodicLoopInternal($this, $this->getPhoneConfig(...), 'phone config', 3600);
$this->configLoop ??= new PeriodicLoopInternal($this, $this->getConfig(...), 'config', 3600);
@@ -834,7 +830,6 @@ final class MTProto implements TLCallback, LoggerGetter
$this->logger->logger("Error while starting IPC server: $e", Logger::FATAL_ERROR);
}
}
- $this->callCheckerLoop->start();
$this->serializeLoop->start();
$this->phoneConfigLoop->start();
$this->configLoop->start();
@@ -852,10 +847,6 @@ final class MTProto implements TLCallback, LoggerGetter
*/
private function stopLoops(): void
{
- if ($this->callCheckerLoop) {
- $this->callCheckerLoop->stop();
- $this->callCheckerLoop = null;
- }
if ($this->serializeLoop) {
$this->serializeLoop->stop();
$this->serializeLoop = null;
@@ -1006,18 +997,6 @@ final class MTProto implements TLCallback, LoggerGetter
$this->TL->updateCallbacks($callbacks);
- // Clean up phone call array
- foreach ($this->calls as $id => $controller) {
- if (!\is_object($controller)) {
- unset($this->calls[$id]);
- } elseif ($controller->getCallState() === VoIP::CALL_STATE_ENDED) {
- $controller->setMadeline($this);
- $controller->discard();
- } else {
- $controller->setMadeline($this);
- }
- }
-
$this->settings->getConnection()->init();
// Setup logger
$this->setupLogger();
diff --git a/src/MTProtoTools/UpdateHandler.php b/src/MTProtoTools/UpdateHandler.php
index d8541340e..0626585f5 100644
--- a/src/MTProtoTools/UpdateHandler.php
+++ b/src/MTProtoTools/UpdateHandler.php
@@ -58,6 +58,7 @@ use danog\MadelineProto\TL\Types\Button;
use danog\MadelineProto\Tools;
use danog\MadelineProto\UpdateHandlerType;
use danog\MadelineProto\VoIP;
+use danog\MadelineProto\VoIP\CallState;
use Revolt\EventLoop;
use Throwable;
use Webmozart\Assert\Assert;
@@ -344,6 +345,7 @@ trait UpdateHandler
'updateInlineBotCallbackQuery' => isset($update['game_short_name'])
? new InlineGameQuery($this, $update)
: new InlineButtonQuery($this, $update),
+ 'updatePhoneCall' => $update['phone_call'],
default => null
};
}
@@ -749,38 +751,47 @@ trait UpdateHandler
return;
}
if ($update['_'] === 'updatePhoneCall') {
- if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
- $this->logger->logger('The php-libtgvoip extension is required to accept and manage calls. See daniil.it/MadelineProto for more info.', Logger::WARNING);
- return;
- }
switch ($update['phone_call']['_']) {
case 'phoneCallRequested':
+ return;
+ /*if (!isset($this->calls[$update['phone_call']['id']])) {
+ $update['phone_call'] = $this->calls[$update['phone_call']['id']] = new VoIP(
+ $this,
+ $update['phone_call'],
+ );
+ }
+ break;*/
+ case 'phoneCallWaiting':
if (isset($this->calls[$update['phone_call']['id']])) {
return;
}
- $controller = new VoIP(false, $update['phone_call']['admin_id'], $this, VoIP::CALL_STATE_INCOMING);
- $controller->setCall($update['phone_call']);
- $controller->storage = ['g_a_hash' => $update['phone_call']['g_a_hash']];
- $controller->storage['video'] = $update['phone_call']['video'] ?? false;
- $update['phone_call'] = $this->calls[$update['phone_call']['id']] = $controller;
+ $update['phone_call'] = $this->calls[$update['phone_call']['id']] = new VoIP(
+ $this,
+ $update['phone_call']
+ );
break;
case 'phoneCallAccepted':
- if (!($this->confirmCall($update['phone_call']))) {
+ if (!isset($this->calls[$update['phone_call']['id']])) {
return;
}
- $update['phone_call'] = $this->calls[$update['phone_call']['id']];
+ $controller = $this->calls[$update['phone_call']['id']];
+ $controller->confirm($update['phone_call']);
+ $update['phone_call'] = $controller;
break;
case 'phoneCall':
- if (!($this->completeCall($update['phone_call']))) {
+ if (!isset($this->calls[$update['phone_call']['id']])) {
return;
}
- $update['phone_call'] = $this->calls[$update['phone_call']['id']];
+ $controller = $this->calls[$update['phone_call']['id']];
+ $controller->complete($update['phone_call']);
+ $update['phone_call'] = $controller;
break;
case 'phoneCallDiscarded':
if (!isset($this->calls[$update['phone_call']['id']])) {
return;
}
- $update['phone_call'] = $this->calls[$update['phone_call']['id']]->discard($update['phone_call']['reason'] ?? ['_' => 'phoneCallDiscardReasonDisconnect'], [], $update['phone_call']['need_debug'] ?? false);
+ $update['phone_call'] = $controller = $this->calls[$update['phone_call']['id']];
+ $controller->discard();
break;
}
}
diff --git a/src/Magic.php b/src/Magic.php
index 2d66ea6fc..e07b60e8b 100644
--- a/src/Magic.php
+++ b/src/Magic.php
@@ -303,11 +303,6 @@ final class Magic
}
}
self::$BIG_ENDIAN = \pack('L', 1) === \pack('N', 1);
- if (\class_exists('\\danog\\MadelineProto\\VoIP')) {
- if (!\defined('\\danog\\MadelineProto\\VoIP::PHP_LIBTGVOIP_VERSION') || !\in_array(VoIP::PHP_LIBTGVOIP_VERSION, ['1.5.0'], true)) {
- throw new Exception(\hex2bin(Lang::$current_lang['v_tgerror']), 0, null, 'MadelineProto', 1);
- }
- }
self::$hasOpenssl = \extension_loaded('openssl');
self::$emojis = \json_decode(self::JSON_EMOJIS);
self::$zero = new BigInteger(0);
diff --git a/src/Ogg.php b/src/Ogg.php
index 584ecd6dd..7b027610f 100644
--- a/src/Ogg.php
+++ b/src/Ogg.php
@@ -600,7 +600,7 @@ final class Ogg
$writeTag("MadelineProto ".API::RELEASE.", ".$opus->opus_get_version_string());
$tags .= \pack('V', 2);
$writeTag("ENCODER=MadelineProto ".API::RELEASE." with ".$opus->opus_get_version_string());
- $writeTag('See https://docs.madelineproto.xyz/docs/VOIP.html for more info');
+ $writeTag('See https://docs.madelineproto.xyz/docs/CALLS.html for more info');
$writePage(
0,
0,
diff --git a/src/VoIP.php b/src/VoIP.php
index b88a4c566..235025a96 100644
--- a/src/VoIP.php
+++ b/src/VoIP.php
@@ -15,41 +15,28 @@ If not, see .
namespace danog\MadelineProto;
+use AssertionError;
+use danog\MadelineProto\EventHandler\Update;
use danog\MadelineProto\MTProto\PermAuthKey;
+use danog\MadelineProto\MTProtoTools\Crypt;
use danog\MadelineProto\Stream\Common\FileBufferedStream;
use danog\MadelineProto\Stream\ConnectionContext;
use danog\MadelineProto\VoIP\AckHandler;
+use danog\MadelineProto\VoIP\CallState;
use danog\MadelineProto\VoIP\Endpoint;
use danog\MadelineProto\VoIP\MessageHandler;
+use danog\MadelineProto\VoIP\VoIPState;
+use phpseclib3\Math\BigInteger;
use Revolt\EventLoop;
use SplQueue;
use Throwable;
+use Webmozart\Assert\Assert;
use function Amp\delay;
use function Amp\File\openFile;
-if (\extension_loaded('php-libtgvoip')) {
- return;
-}
-
-final class VoIP
+final class VoIP extends Update
{
- use MessageHandler;
- use AckHandler;
-
- const PHP_LIBTGVOIP_VERSION = '1.5.0';
- const STATE_CREATED = 0;
- const STATE_WAIT_INIT = 1;
- const STATE_WAIT_INIT_ACK = 2;
- const STATE_ESTABLISHED = 3;
- const STATE_FAILED = 4;
- const STATE_RECONNECTING = 5;
-
- const TGVOIP_ERROR_UNKNOWN = 0;
- const TGVOIP_ERROR_INCOMPATIBLE = 1;
- const TGVOIP_ERROR_TIMEOUT = 2;
- const TGVOIP_ERROR_AUDIO_IO = 3;
-
const NET_TYPE_UNKNOWN = 0;
const NET_TYPE_GPRS = 1;
const NET_TYPE_EDGE = 2;
@@ -75,194 +62,276 @@ final class VoIP
const AUDIO_STATE_CONFIGURED = 1;
const AUDIO_STATE_RUNNING = 2;
- const CALL_STATE_NONE = -1;
- const CALL_STATE_REQUESTED = 0;
- const CALL_STATE_INCOMING = 1;
- const CALL_STATE_ACCEPTED = 2;
- const CALL_STATE_CONFIRMED = 3;
- const CALL_STATE_READY = 4;
- const CALL_STATE_ENDED = 5;
-
+ /** @internal */
const PKT_INIT = 1;
+ /** @internal */
const PKT_INIT_ACK = 2;
+ /** @internal */
const PKT_STREAM_STATE = 3;
+ /** @internal */
const PKT_STREAM_DATA = 4;
+ /** @internal */
const PKT_UPDATE_STREAMS = 5;
+ /** @internal */
const PKT_PING = 6;
+ /** @internal */
const PKT_PONG = 7;
+ /** @internal */
const PKT_STREAM_DATA_X2 = 8;
+ /** @internal */
const PKT_STREAM_DATA_X3 = 9;
+ /** @internal */
const PKT_LAN_ENDPOINT = 10;
+ /** @internal */
const PKT_NETWORK_CHANGED = 11;
+ /** @internal */
const PKT_SWITCH_PREF_RELAY = 12;
+ /** @internal */
const PKT_SWITCH_TO_P2P = 13;
+ /** @internal */
const PKT_NOP = 14;
+ /** @internal */
const TLID_DECRYPTED_AUDIO_BLOCK = "\xc1\xdb\xf9\x48";
+ /** @internal */
const TLID_SIMPLE_AUDIO_BLOCK = "\x0d\x0e\x76\xcc";
+ /** @internal */
const TLID_REFLECTOR_SELF_INFO = "\xC7\x72\x15\xc0";
+ /** @internal */
const TLID_REFLECTOR_PEER_INFO = "\x1C\x37\xD9\x27";
+ /** @internal */
const PROTO_ID = 'GrVP';
+ /** @internal */
const PROTOCOL_VERSION = 9;
+ /** @internal */
const MIN_PROTOCOL_VERSION = 9;
+ /** @internal */
const STREAM_TYPE_AUDIO = 1;
+ /** @internal */
const STREAM_TYPE_VIDEO = 2;
+ /** @internal */
const CODEC_OPUS = 'SUPO';
- private MTProto $MadelineProto;
- public array $received_timestamp_map = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
- public array $remote_ack_timestamp_map = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
- public int $session_out_seq_no = 0;
- public int $session_in_seq_no = 0;
- public int $voip_state = 0;
- public array $configuration = ['endpoints' => [], 'shared_config' => []];
- public array $storage = [];
- public array $internalStorage = [];
- private $signal = 0;
- private ?int $callState = null;
- private $callID;
- private $creatorID;
- private $otherID;
- private $protocol;
- private $visualization;
- private $holdFiles = [];
- private $inputFiles = [];
- private $outputFile;
- private $isPlaying = false;
+ protected MessageHandler $messageHandler;
+ protected VoIPState $voipState = VoIPState::CREATED;
+ protected CallState $callState;
- private bool $creator;
+ protected readonly array $call;
- private PermAuthKey $authKey;
- private int $peerVersion = 0;
+ /** @var list */
+ protected array $holdFiles = [];
+ /** @var list */
+ protected array $inputFiles = [];
/**
* @var array
*/
- private array $sockets = [];
+ protected array $sockets = [];
+ protected ?Endpoint $bestEndpoint = null;
+ protected bool $pendingPing = true;
+ protected ?string $timeoutWatcher = null;
+ protected ?string $pingWatcher = null;
+ protected float $lastIncomingTimestamp = 0.0;
+ protected int $opusTimestamp = 0;
+ protected SplQueue $packetQueue;
+ protected array $tempHoldFiles = [];
- private ?Endpoint $bestEndpoint = null;
+ /** Auth key */
+ protected readonly string $authKey;
+ /** Protocol call ID */
+ protected readonly string $protocolCallID;
- private bool $pendingPing = true;
- /**
- * Timeout watcher.
- */
- private ?string $timeoutWatcher = null;
- /**
- * Ping watcher.
- */
- private ?string $pingWatcher = null;
+ /** Phone call ID */
+ public readonly int $callID;
+ /** Whether the call is an outgoing call */
+ public readonly bool $outgoing;
+ /** ID of the other user in the call */
+ public readonly int $otherID;
+ /** ID of the creator of the call */
+ public readonly int $creatorID;
+ /** When was the call created */
+ public readonly int $date;
+ /** @var ?list{string, string, string, string} */
+ protected ?array $visualization = null;
/**
- * Last incoming timestamp.
- *
+ * Constructor.
+ *
+ * @internal
*/
- private float $lastIncomingTimestamp = 0.0;
- /**
- * The outgoing timestamp.
- *
- */
- private int $timestamp = 0;
- /**
- * Packet queue.
- *
- */
- private SplQueue $packetQueue;
- /**
- * Temporary holdfile array.
- */
- private array $tempHoldFiles = [];
- /**
- * Sleep function.
- */
- public function __sleep(): array
+ public function __construct(
+ protected readonly MTProto $API,
+ array $call
+ )
{
- $vars = \get_object_vars($this);
- unset($vars['sockets'], $vars['bestEndpoint'], $vars['timeoutWatcher']);
+ $call['_'] = 'inputPhoneCall';
+ $this->packetQueue = new SplQueue;
+ $this->call = $call;
+ $this->date = $call['date'];
+ $this->callID = $call['id'];
+ if ($call['_'] === 'phoneCallWaiting') {
+ $this->outgoing = false;
+ $this->otherID = $call['participant_id'];
+ $this->creatorID = $call['admin_id'];
+ $this->callState = CallState::INCOMING;
+ } else {
+ $this->outgoing = true;
+ $this->otherID = $call['admin_id'];
+ $this->creatorID = $call['participant_id'];
+ $this->callState = CallState::REQUESTED;
+ }
+ }
- return \array_keys($vars);
+ /**
+ * Confirm requested call.
+ * @internal
+ */
+ public function confirm(array $params): bool
+ {
+ if ($this->callState !== CallState::REQUESTED) {
+ $this->API->logger->logger(\sprintf(Lang::$current_lang['call_error_2'], $this->callID));
+ return false;
+ }
+ $this->API->logger->logger(\sprintf(Lang::$current_lang['call_confirming'], $this->otherID), Logger::VERBOSE);
+ $dh_config = $this->API->getDhConfig();
+ $params['g_b'] = new BigInteger((string) $params['g_b'], 256);
+ Crypt::checkG($params['g_b'], $dh_config['p']);
+ $key = \str_pad($params['g_b']->powMod($this->call['a'], $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT);
+ try {
+ $res = ($this->API->methodCallAsyncRead('phone.confirmCall', ['key_fingerprint' => \substr(\sha1($key, true), -8), 'peer' => ['id' => $params['id'], 'access_hash' => $params['access_hash'], '_' => 'inputPhoneCall'], 'g_a' => $this->call['g_a'], 'protocol' => ['_' => 'phoneCallProtocol', 'udp_reflector' => true, 'min_layer' => 65, 'max_layer' => 92]]))['phone_call'];
+ } catch (RPCErrorException $e) {
+ if ($e->rpc === 'CALL_ALREADY_ACCEPTED') {
+ $this->API->logger->logger(\sprintf(Lang::$current_lang['call_already_accepted'], $params['id']));
+ return true;
+ }
+ if ($e->rpc === 'CALL_ALREADY_DECLINED') {
+ $this->API->logger->logger(Lang::$current_lang['call_already_declined']);
+ $this->discard(['_' => 'phoneCallDiscardReasonHangup']);
+ return false;
+ }
+ throw $e;
+ }
+ $visualization = [];
+ $length = new BigInteger(\count(Magic::$emojis));
+ foreach (\str_split(\hash('sha256', $key.\str_pad($this->call['g_a'], 256, \chr(0), STR_PAD_LEFT), true), 8) as $number) {
+ $number[0] = \chr(\ord($number[0]) & 0x7f);
+ $visualization[] = Magic::$emojis[(int) (new BigInteger($number, 256))->divide($length)[1]->toString()];
+ }
+ $this->visualization = $visualization;
+ $this->authKey = $key;
+ $this->callState = CallState::RUNNING;
+ $this->protocolCallID = \substr(\hash('sha256', $key, true), -16);
+ $this->initialize($res['connections']);
+ return true;
+ }
+ /**
+ * Accept incoming call.
+ */
+ public function accept(): self
+ {
+ if ($this->callState === CallState::RUNNING || $this->callState === CallState::ENDED) {
+ return $this;
+ }
+ Assert::eq($this->callState->name, CallState::INCOMING->name);
+
+ $this->API->logger->logger(\sprintf(Lang::$current_lang['accepting_call'], $this->otherID), Logger::VERBOSE);
+ $dh_config = $this->API->getDhConfig();
+ $this->API->logger->logger('Generating b...', Logger::VERBOSE);
+ $b = BigInteger::randomRange(Magic::$two, $dh_config['p']->subtract(Magic::$two));
+ $g_b = $dh_config['g']->powMod($b, $dh_config['p']);
+ Crypt::checkG($g_b, $dh_config['p']);
+ try {
+ $this->API->methodCallAsyncRead('phone.acceptCall', ['peer' => ['id' => $this->call['id'], 'access_hash' => $this->call['access_hash'], '_' => 'inputPhoneCall'], 'g_b' => $g_b->toBytes(), 'protocol' => ['_' => 'phoneCallProtocol', 'udp_reflector' => true, 'udp_p2p' => true, 'min_layer' => 65, 'max_layer' => 92]]);
+ } catch (RPCErrorException $e) {
+ if ($e->rpc === 'CALL_ALREADY_ACCEPTED') {
+ $this->API->logger->logger(\sprintf(Lang::$current_lang['call_already_accepted'], $this->callID));
+ return $this;
+ }
+ if ($e->rpc === 'CALL_ALREADY_DECLINED') {
+ $this->API->logger->logger(Lang::$current_lang['call_already_declined']);
+ $this->discard(['_' => 'phoneCallDiscardReasonHangup']);
+ return $this;
+ }
+ throw $e;
+ }
+ $this->call['b'] = $b;
+
+ $this->callState = CallState::ACCEPTED;
+
+ return $this;
+ }
+
+ /**
+ * Complete call handshake.
+ *
+ * @internal
+ */
+ public function complete(array $params): bool
+ {
+ if ($this->callState !== CallState::ACCEPTED) {
+ $this->API->logger->logger(\sprintf(Lang::$current_lang['call_error_3'], $params['id']));
+ return false;
+ }
+ $this->API->logger->logger(\sprintf(Lang::$current_lang['call_completing'], $this->otherID), Logger::VERBOSE);
+ $dh_config = $this->API->getDhConfig();
+ if (\hash('sha256', $params['g_a_or_b'], true) != $this->call['g_a_hash']) {
+ throw new SecurityException('Invalid g_a!');
+ }
+ $params['g_a_or_b'] = new BigInteger((string) $params['g_a_or_b'], 256);
+ Crypt::checkG($params['g_a_or_b'], $dh_config['p']);
+ $key = \str_pad($params['g_a_or_b']->powMod($this->call['b'], $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT);
+ if (\substr(\sha1($key, true), -8) != $params['key_fingerprint']) {
+ throw new SecurityException(Lang::$current_lang['fingerprint_invalid']);
+ }
+ $visualization = [];
+ $length = new BigInteger(\count(Magic::$emojis));
+ foreach (\str_split(\hash('sha256', $key.\str_pad($params['g_a_or_b']->toBytes(), 256, \chr(0), STR_PAD_LEFT), true), 8) as $number) {
+ $number[0] = \chr(\ord($number[0]) & 0x7f);
+ $visualization[] = Magic::$emojis[(int) (new BigInteger($number, 256))->divide($length)[1]->toString()];
+ }
+ $this->visualization = $visualization;
+ $this->authKey = $key;
+ $this->callState = CallState::RUNNING;
+ $this->protocolCallID = \substr(\hash('sha256', $key, true), -16);
+ $this->initialize($params['connections']);
+ return true;
}
/**
* Wakeup function.
*/
public function __wakeup(): void
{
- if ($this->voip_state === self::STATE_ESTABLISHED) {
- $this->voip_state = self::STATE_CREATED;
- $this->startTheMagic();
+ if ($this->callState === CallState::RUNNING) {
+ $this->startReadLoop();
+ if ($this->voipState === VoIPState::ESTABLISHED) {
+ $this->startWriteLoop();
+ }
}
}
- /**
- * Constructor.
- */
- public function __construct(bool $creator, int $otherID, MTProto $MadelineProto, int $callState)
- {
- $this->creator = $creator;
- $this->otherID = $otherID;
- $this->MadelineProto = $MadelineProto;
- $this->callState = $callState;
- $this->packetQueue = new SplQueue;
- }
/**
- * Get max layer.
+ * Get call emojis (will return null if the call is not inited yet).
+ *
+ * @return ?list{string, string, string, string}
*/
- public static function getConnectionMaxLayer(): int
- {
- return 92;
- }
-
- /**
- * Get debug string.
- */
- public function getDebugString(): string
- {
- return '';
- }
-
- /**
- * Set call constructor.
- */
- public function setCall(array $callID): void
- {
- $this->protocol = $callID['protocol'];
- $this->callID = [
- '_' => 'inputPhoneCall',
- 'id' => $callID['id'],
- 'access_hash' => $callID['access_hash'],
- ];
- }
-
- /**
- * Set emojis.
- */
- public function setVisualization(array $visualization): void
- {
- $this->visualization = $visualization;
- }
-
- /**
- * Get emojis.
- */
- public function getVisualization(): array
+ public function getVisualization(): ?array
{
return $this->visualization;
}
/**
* Discard call.
- *
*/
- public function discard(array $reason = ['_' => 'phoneCallDiscardReasonDisconnect'], array $rating = [], bool $debug = false): self
+ public function discard(array $reason = ['_' => 'phoneCallDiscardReasonDisconnect'], array $rating = []): self
{
- if (($this->callState ?? self::CALL_STATE_ENDED) === self::CALL_STATE_ENDED || empty($this->configuration)) {
+ if ($this->callState === CallState::ENDED) {
return $this;
}
- $this->callState = self::CALL_STATE_ENDED;
Logger::log("Now closing $this");
if (isset($this->timeoutWatcher)) {
EventLoop::cancel($this->timeoutWatcher);
@@ -274,80 +343,67 @@ final class VoIP
}
Logger::log("Closed all sockets, discarding $this");
- $this->MadelineProto->discardCall($this->callID, $reason, $rating, $debug);
- return $this;
- }
-
- public function __destruct()
- {
- EventLoop::queue($this->discard(...), ['_' => 'phoneCallDiscardReasonDisconnect']);
- }
- /**
- * Accept call.
- *
- */
- public function accept(): self|false
- {
- if ($this->callState !== self::CALL_STATE_INCOMING) {
- return false;
+ $this->API->logger->logger(\sprintf(Lang::$current_lang['call_discarding'], $this->callID), Logger::VERBOSE);
+ try {
+ $this->API->methodCallAsyncRead('phone.discardCall', ['peer' => $this->call, 'duration' => \time() - $this->calls[$call['id']]->whenCreated(), 'connection_id' => $this->calls[$call['id']]->getPreferredRelayID(), 'reason' => $reason]);
+ } catch (RPCErrorException $e) {
+ if (!\in_array($e->rpc, ['CALL_ALREADY_DECLINED', 'CALL_ALREADY_ACCEPTED'], true)) {
+ throw $e;
+ }
}
- $this->callState = self::CALL_STATE_ACCEPTED;
-
- $res = $this->MadelineProto->acceptCall($this->callID);
-
- if (!$res) {
- $this->discard(['_' => 'phoneCallDiscardReasonDisconnect']);
+ if (!empty($rating)) {
+ $this->API->logger->logger(\sprintf('Setting rating for call %s...', $this->call), Logger::VERBOSE);
+ $this->API->methodCallAsyncRead('phone.setCallRating', ['peer' => $this->call, 'rating' => $rating['rating'], 'comment' => $rating['comment']]);
}
-
+ $this->API->cleanupCall($this->callID);
+ $this->callState = CallState::ENDED;
return $this;
}
/**
- * Start the actual call.
+ * Connect to the specified endpoints.
*/
- public function startTheMagic(): self
+ private function initialize(array $endpoints): void
{
- if ($this->voip_state !== self::STATE_CREATED) {
- return $this;
+ foreach ($endpoints as $endpoint) {
+ try {
+ $this->sockets['v6 '.$endpoint['id']] = new Endpoint(
+ '['.$endpoint['ipv6'].']',
+ $endpoint['port'],
+ $endpoint['peer_tag'],
+ true,
+ $this->outgoing,
+ $this->authKey,
+ $this->protocolCallID,
+ $this->messageHandler
+ );
+ } catch (Throwable) {
+ }
+ try {
+ $this->sockets['v4 '.$endpoint['id']] = new Endpoint(
+ $endpoint['ip'],
+ $endpoint['port'],
+ $endpoint['peer_tag'],
+ true,
+ $this->outgoing,
+ $this->authKey,
+ $this->protocolCallID,
+ $this->messageHandler
+ );
+ } catch (Throwable) {
+ }
+ }
+ $this->voipState = VoIPState::WAIT_INIT;
+ $this->startReadLoop();
+ foreach ($this->sockets as $socket) {
+ $socket->write($this->messageHandler->encryptPacket([
+ '_' => self::PKT_INIT,
+ 'protocol' => self::PROTOCOL_VERSION,
+ 'min_protocol' => self::MIN_PROTOCOL_VERSION,
+ 'audio_streams' => [self::CODEC_OPUS],
+ 'video_streams' => []
+ ], true));
}
- $this->voip_state = self::STATE_WAIT_INIT;
- $this->timeoutWatcher = EventLoop::repeat(10, function (): void {
- if (\microtime(true) - $this->lastIncomingTimestamp > 10) {
- $this->discard(['_' => 'phoneCallDiscardReasonDisconnect']);
- }
- });
- EventLoop::queue(function (): void {
- $this->authKey = new PermAuthKey();
- $this->authKey->setAuthKey($this->configuration['auth_key']);
-
- foreach ($this->configuration['endpoints'] as $endpoint) {
- try {
- $this->sockets['v6 '.$endpoint['id']] = new Endpoint('['.$endpoint['ipv6'].']', $endpoint['port'], $endpoint['peer_tag'], true, $this);
- } catch (Throwable) {
- }
- try {
- $this->sockets['v4 '.$endpoint['id']] = new Endpoint($endpoint['ip'], $endpoint['port'], $endpoint['peer_tag'], true, $this);
- } catch (Throwable) {
- }
- }
- foreach ($this->sockets as $socket) {
- $socket->write($this->encryptPacket([
- '_' => self::PKT_INIT,
- 'protocol' => self::PROTOCOL_VERSION,
- 'min_protocol' => self::MIN_PROTOCOL_VERSION,
- 'audio_streams' => [self::CODEC_OPUS],
- 'video_streams' => []
- ], false));
- EventLoop::queue(function () use ($socket): void {
- while ($payload = $socket->read()) {
- $this->lastIncomingTimestamp = \microtime(true);
- EventLoop::queue($this->handlePacket(...), $socket, $payload);
- }
- Logger::log("Exiting VoIP read loop in $this!");
- });
- }
- });
- return $this;
}
/**
* Handle incoming packet.
@@ -356,28 +412,32 @@ final class VoIP
{
switch ($packet['_']) {
case self::PKT_INIT:
- //$this->voip_state = self::STATE_WAIT_INIT_ACK;
- $this->peerVersion = $packet['protocol'];
- $socket->write(
- $this->encryptPacket([
- '_' => self::PKT_INIT_ACK,
- 'protocol' => self::PROTOCOL_VERSION,
- 'min_protocol' => self::MIN_PROTOCOL_VERSION,
- 'all_streams' => [
- ['id' => 0, 'type' => self::STREAM_TYPE_AUDIO, 'codec' => self::CODEC_OPUS, 'frame_duration' => 60, 'enabled' => 1]
- ]
- ])
- );
+ //$this->voipState = VoIPState::WAIT_INIT_ACK;
+ $socket->write($this->messageHandler->encryptPacket([
+ '_' => self::PKT_INIT_ACK,
+ 'protocol' => self::PROTOCOL_VERSION,
+ 'min_protocol' => self::MIN_PROTOCOL_VERSION,
+ 'all_streams' => [
+ ['id' => 0, 'type' => self::STREAM_TYPE_AUDIO, 'codec' => self::CODEC_OPUS, 'frame_duration' => 60, 'enabled' => 1]
+ ]
+ ]));
+ $socket->write($this->messageHandler->encryptPacket([
+ '_' => self::PKT_INIT,
+ 'protocol' => self::PROTOCOL_VERSION,
+ 'min_protocol' => self::MIN_PROTOCOL_VERSION,
+ 'audio_streams' => [self::CODEC_OPUS],
+ 'video_streams' => []
+ ]));
+ break;
- // no break
case self::PKT_INIT_ACK:
if (!$this->bestEndpoint) {
$this->bestEndpoint = $socket;
$this->pingWatcher = EventLoop::delay(1.0, function (): void {
$this->pendingPing = true;
foreach ($this->sockets as $socket) {
- //$socket->udpPing();
- $packet = $this->encryptPacket(['_' => self::PKT_PING]);
+ $socket->udpPing();
+ $packet = $this->messageHandler->encryptPacket(['_' => self::PKT_PING]);
EventLoop::queue(fn () => $socket->write($packet));
}
});
@@ -393,6 +453,9 @@ final class VoIP
case self::PKT_STREAM_DATA_X3:
$cnt = 3;
break;
+ case self::PKT_PING:
+ $socket->write($this->messageHandler->encryptPacket(['_' => self::PKT_PONG, 'out_seq_no' => $packet['out_seq_no']]));
+ break;
case self::PKT_PONG:
if ($this->pendingPing) {
$this->pendingPing = false;
@@ -402,16 +465,31 @@ final class VoIP
}
}
break;
- default:
- \var_dump($packet);
}
}
+ private function startReadLoop(): void
+ {
+ foreach ($this->sockets as $socket) {
+ EventLoop::queue(function () use ($socket): void {
+ while ($payload = $socket->read()) {
+ $this->lastIncomingTimestamp = \microtime(true);
+ EventLoop::queue($this->handlePacket(...), $socket, $payload);
+ }
+ Logger::log("Exiting VoIP read loop in $this!");
+ });
+ }
+ $this->timeoutWatcher = EventLoop::repeat(10, function (): void {
+ if (\microtime(true) - $this->lastIncomingTimestamp > 10) {
+ $this->discard(['_' => 'phoneCallDiscardReasonDisconnect']);
+ }
+ });
+ }
/**
* Start write loop.
*/
private function startWriteLoop(): void
{
- $this->voip_state = self::STATE_ESTABLISHED;
+ $this->voipState = VoIPState::ESTABLISHED;
Logger::log("Call established, sending OPUS data!");
$this->tempHoldFiles = [];
@@ -432,7 +510,7 @@ final class VoIP
}
$t = \microtime(true) + 0.060;
while (!$this->packetQueue->isEmpty()) {
- $packet = $this->encryptPacket(['_' => self::PKT_STREAM_DATA, 'stream_id' => 0, 'data' => $this->packetQueue->dequeue(), 'timestamp' => $this->timestamp]);
+ $packet = $this->messageHandler->encryptPacket(['_' => self::PKT_STREAM_DATA, 'stream_id' => 0, 'data' => $this->packetQueue->dequeue(), 'timestamp' => $this->opusTimestamp]);
//Logger::log("Writing {$this->timestamp} in $this!");
$diff = $t - \microtime(true);
@@ -442,7 +520,7 @@ final class VoIP
$this->bestEndpoint->write($packet);
$t += 0.060;
- $this->timestamp += 60;
+ $this->opusTimestamp += 60;
}
}
}
@@ -506,125 +584,19 @@ final class VoIP
return $this;
}
- /**
- * Set MadelineProto instance.
- */
- public function setMadeline(MTProto $MadelineProto): void
- {
- $this->MadelineProto = $MadelineProto;
- }
-
- /**
- * Get call protocol.
- */
- public function getProtocol(): array
- {
- return $this->protocol;
- }
-
- /**
- * Get ID of other user.
- */
- public function getOtherID(): int
- {
- return $this->otherID;
- }
-
- /**
- * Get call ID.
- *
- */
- public function getCallID(): string|int
- {
- return $this->callID;
- }
-
- /**
- * Get creation date.
- *
- */
- public function whenCreated(): int|bool
- {
- return $this->internalStorage['created'] ?? false;
- }
-
- /**
- * Parse config.
- */
- public function parseConfig(): void
- {
- }
-
/**
* Get call state.
*/
- public function getCallState(): int
+ public function getCallState(): CallState
{
- return $this->callState ?? self::CALL_STATE_ENDED;
+ return $this->callState;
}
-
/**
- * Get library version.
+ * Get VoIP state.
*/
- public function getVersion(): string
+ public function getVoIPState(): VoIPState
{
- return 'libponyvoip-1.0';
- }
-
- /**
- * Get preferred relay ID.
- */
- public function getPreferredRelayID(): int
- {
- return 0;
- }
-
- /**
- * Get last error.
- */
- public function getLastError(): string
- {
- return '';
- }
-
- /**
- * Get debug log.
- */
- public function getDebugLog(): string
- {
- return '';
- }
-
- /**
- * Get signal bar count.
- */
- public function getSignalBarsCount(): int
- {
- return $this->signal;
- }
-
- /**
- * Get the value of creator.
- */
- public function isCreator(): bool
- {
- return $this->creator;
- }
-
- /**
- * Get the value of authKey.
- */
- public function getAuthKey(): PermAuthKey
- {
- return $this->authKey;
- }
-
- /**
- * Get the value of peerVersion.
- */
- public function getPeerVersion(): int
- {
- return $this->peerVersion;
+ return $this->voipState;
}
/**
@@ -632,7 +604,6 @@ final class VoIP
*/
public function __toString(): string
{
- $id = $this->callID['id'];
- return "call {$id} with {$this->otherID}";
+ return "call {$this->callID} with {$this->otherID}";
}
}
diff --git a/src/VoIP/AckHandler.php b/src/VoIP/AckHandler.php
deleted file mode 100644
index 751971500..000000000
--- a/src/VoIP/AckHandler.php
+++ /dev/null
@@ -1,80 +0,0 @@
-.
-*/
-
-namespace danog\MadelineProto\VoIP;
-
-use danog\MadelineProto\Logger;
-
-/**
- * @internal
- */
-trait AckHandler
-{
- private function seqgt(int $s1, int $s2): bool
- {
- return $s1 > $s2;
- }
- public function received_packet(int $last_ack_id, int $packet_seq_no, int $ack_mask): bool
- {
- if ($this->seqgt($packet_seq_no, $this->session_in_seq_no)) {
- $diff = $packet_seq_no - $this->session_in_seq_no;
- if ($diff > 31) {
- $this->received_timestamp_map = \array_fill(0, 32, 0);
- } else {
- $remaining = 32-$diff;
- for ($x = 0; $x < $remaining; $x++) {
- $this->received_timestamp_map[$diff+$x] = $this->received_timestamp_map[$x];
- }
- for ($x = 1; $x < $diff; $x++) {
- $this->received_timestamp_map[$x] = 0;
- }
- $this->received_timestamp_map[0] = \microtime(true);
- }
- $this->session_in_seq_no = $packet_seq_no;
- } elseif (($diff = $this->session_in_seq_no - $packet_seq_no) < 32) {
- if (!$this->received_timestamp_map[$diff]) {
- Logger::log("Got duplicate $packet_seq_no");
- return false;
- }
- $this->received_timestamp_map[$diff] = \microtime(true);
- } else {
- Logger::log("Packet $packet_seq_no is out of order and too late");
- return false;
- }
- if ($this->seqgt($last_ack_id, $this->session_out_seq_no)) {
- $diff = $last_ack_id - $this->session_out_seq_no;
- if ($diff > 31) {
- $this->remote_ack_timestamp_map = \array_fill(0, 32, 0);
- } else {
- $remaining = 32-$diff;
- for ($x = 0; $x < $remaining; $x++) {
- $this->remote_ack_timestamp_map[$diff+$x] = $this->remote_ack_timestamp_map[$x];
- }
- for ($x = 1; $x < $diff; $x++) {
- $this->remote_ack_timestamp_map[$x] = 0;
- }
- $this->remote_ack_timestamp_map[0] = \microtime(true);
- }
- $this->session_out_seq_no = $last_ack_id;
-
- for ($x = 1; $x < 32; $x++) {
- if (!$this->remote_ack_timestamp_map[$x] && ($ack_mask >> 32-$x) & 1) {
- $this->remote_ack_timestamp_map[$x] = \microtime(true);
- }
- }
- }
- return true;
- }
-}
diff --git a/src/VoIP/AuthKeyHandler.php b/src/VoIP/AuthKeyHandler.php
index cd0c02466..75332b43e 100644
--- a/src/VoIP/AuthKeyHandler.php
+++ b/src/VoIP/AuthKeyHandler.php
@@ -20,6 +20,7 @@ declare(strict_types=1);
namespace danog\MadelineProto\VoIP;
+use Amp\DeferredFuture;
use danog\MadelineProto\Exception;
use danog\MadelineProto\Lang;
use danog\MadelineProto\Logger;
@@ -31,6 +32,7 @@ use danog\MadelineProto\RPCErrorException;
use danog\MadelineProto\SecurityException;
use danog\MadelineProto\VoIP;
use phpseclib3\Math\BigInteger;
+use Throwable;
use const STR_PAD_LEFT;
@@ -44,35 +46,9 @@ use const STR_PAD_LEFT;
*/
trait AuthKeyHandler
{
+ /** @var array */
private array $calls = [];
- /**
- * Accept call from VoIP instance.
- *
- * @param VoIP $instance Call instance
- * @param array $user User
- * @internal
- */
- public function acceptCallFrom(VoIP $instance, array $user): ?VoIP
- {
- if (!$this->acceptCall($user)) {
- $instance->discard();
- return null;
- }
- return $instance;
- }
- /**
- * @param VoIP $instance Call instance
- * @param array $call Call info
- * @param array $reason Discard reason
- * @param array $rating Rating
- * @param boolean $need_debug Needs debug?
- * @internal
- */
- public function discardCallFrom(VoIP $instance, array $call, array $reason, array $rating = [], bool $need_debug = true): VoIP
- {
- $this->discardCall($call, $reason, $rating, $need_debug);
- return $instance;
- }
+ private array $pendingCalls = [];
/**
* Request VoIP call.
*
@@ -80,228 +56,52 @@ trait AuthKeyHandler
*/
public function requestCall(mixed $user)
{
- if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
- throw Exception::extension('libtgvoip');
- }
$user = ($this->getInfo($user));
if (!isset($user['InputUser']) || $user['InputUser']['_'] === 'inputUserSelf') {
throw new PeerNotInDbException();
}
- $user = $user['InputUser'];
- $this->logger->logger(\sprintf('Calling %s...', $user['user_id']), 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']);
- $controller = new VoIP(true, $user['user_id'], $this, VoIP::CALL_STATE_REQUESTED);
- $controller->storage = ['a' => $a, 'g_a' => \str_pad($g_a->toBytes(), 256, \chr(0), STR_PAD_LEFT)];
- $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' => VoIP::getConnectionMaxLayer()]]);
- $controller->setCall($res['phone_call']);
- $this->calls[$res['phone_call']['id']] = $controller;
- $this->updaters[UpdateLoop::GENERIC]->resume();
- return $controller;
- }
- /**
- * Accept call.
- *
- * @param array $call Call
- */
- public function acceptCall(array $call): bool
- {
- if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
- throw new Exception();
+ $user = $user['bot_api_id'];
+ if (isset($this->pendingCalls[$user])) {
+ return $this->pendingCalls[$user]->await();
}
- if ($this->callStatus($call['id']) !== VoIP::CALL_STATE_ACCEPTED) {
- $this->logger->logger(\sprintf(Lang::$current_lang['call_error_1'], $call['id']));
- return false;
- }
- $this->logger->logger(\sprintf(Lang::$current_lang['accepting_call'], $this->calls[$call['id']]->getOtherID()), Logger::VERBOSE);
- $dh_config = ($this->getDhConfig());
- $this->logger->logger('Generating b...', Logger::VERBOSE);
- $b = BigInteger::randomRange(Magic::$two, $dh_config['p']->subtract(Magic::$two));
- $g_b = $dh_config['g']->powMod($b, $dh_config['p']);
- Crypt::checkG($g_b, $dh_config['p']);
+ $deferred = new DeferredFuture;
+ $this->pendingCalls[$user] = $deferred->getFuture();
+
try {
- $res = $this->methodCallAsyncRead('phone.acceptCall', ['peer' => ['id' => $call['id'], 'access_hash' => $call['access_hash'], '_' => 'inputPhoneCall'], 'g_b' => $g_b->toBytes(), 'protocol' => ['_' => 'phoneCallProtocol', 'udp_reflector' => true, 'udp_p2p' => true, 'min_layer' => 65, 'max_layer' => VoIP::getConnectionMaxLayer()]]);
- } catch (RPCErrorException $e) {
- if ($e->rpc === 'CALL_ALREADY_ACCEPTED') {
- $this->logger->logger(\sprintf(Lang::$current_lang['call_already_accepted'], $call['id']));
- return true;
- }
- if ($e->rpc === 'CALL_ALREADY_DECLINED') {
- $this->logger->logger(Lang::$current_lang['call_already_declined']);
- $this->discardCall($call['id'], ['_' => 'phoneCallDiscardReasonHangup']);
- return false;
- }
- throw $e;
+ $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 VoIP($this, $res);
+ unset($this->pendingCalls[$user]);
+ $deferred->complete($controller);
+ } catch (Throwable $e) {
+ unset($this->pendingCalls[$user]);
+ $deferred->error($e);
}
- $this->calls[$res['phone_call']['id']]->storage['b'] = $b;
- $this->updaters[UpdateLoop::GENERIC]->resume();
- return true;
- }
- /**
- * Confirm call.
- *
- * @param array $params Params
- */
- public function confirmCall(array $params)
- {
- if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
- throw Exception::extension('libtgvoip');
- }
- if ($this->callStatus($params['id']) !== VoIP::CALL_STATE_REQUESTED) {
- $this->logger->logger(\sprintf(Lang::$current_lang['call_error_2'], $params['id']));
- return false;
- }
- $this->logger->logger(\sprintf(Lang::$current_lang['call_confirming'], $this->calls[$params['id']]->getOtherID()), Logger::VERBOSE);
- $dh_config = ($this->getDhConfig());
- $params['g_b'] = new BigInteger((string) $params['g_b'], 256);
- Crypt::checkG($params['g_b'], $dh_config['p']);
- $key = \str_pad($params['g_b']->powMod($this->calls[$params['id']]->storage['a'], $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT);
- try {
- $res = ($this->methodCallAsyncRead('phone.confirmCall', ['key_fingerprint' => \substr(\sha1($key, true), -8), 'peer' => ['id' => $params['id'], 'access_hash' => $params['access_hash'], '_' => 'inputPhoneCall'], 'g_a' => $this->calls[$params['id']]->storage['g_a'], 'protocol' => ['_' => 'phoneCallProtocol', 'udp_reflector' => true, 'min_layer' => 65, 'max_layer' => VoIP::getConnectionMaxLayer()]]))['phone_call'];
- } catch (RPCErrorException $e) {
- if ($e->rpc === 'CALL_ALREADY_ACCEPTED') {
- $this->logger->logger(\sprintf(Lang::$current_lang['call_already_accepted'], $params['id']));
- return true;
- }
- if ($e->rpc === 'CALL_ALREADY_DECLINED') {
- $this->logger->logger(Lang::$current_lang['call_already_declined']);
- $this->discardCall($params['id'], ['_' => 'phoneCallDiscardReasonHangup']);
- return false;
- }
- throw $e;
- }
- $this->calls[$params['id']]->setCall($res);
- $visualization = [];
- $length = new BigInteger(\count(Magic::$emojis));
- foreach (\str_split(\hash('sha256', $key.\str_pad($this->calls[$params['id']]->storage['g_a'], 256, \chr(0), STR_PAD_LEFT), true), 8) as $number) {
- $number[0] = \chr(\ord($number[0]) & 0x7f);
- $visualization[] = Magic::$emojis[(int) (new BigInteger($number, 256))->divide($length)[1]->toString()];
- }
- $this->calls[$params['id']]->setVisualization($visualization);
- $this->calls[$params['id']]->configuration['endpoints'] = \array_merge($res['connections'], $this->calls[$params['id']]->configuration['endpoints']);
- $this->calls[$params['id']]->configuration = \array_merge(['recv_timeout' => $this->config['call_receive_timeout_ms'] / 1000, 'init_timeout' => $this->config['call_connect_timeout_ms'] / 1000, 'data_saving' => VoIP::DATA_SAVING_NEVER, 'enable_NS' => true, 'enable_AEC' => true, 'enable_AGC' => true, 'auth_key' => $key, 'auth_key_id' => \substr(\sha1($key, true), -8), 'call_id' => \substr(\hash('sha256', $key, true), -16), 'network_type' => VoIP::NET_TYPE_ETHERNET], $this->calls[$params['id']]->configuration);
- $this->calls[$params['id']]->parseConfig();
- return $this->calls[$params['id']]->startTheMagic();
- }
- /**
- * Complete call handshake.
- *
- * @param array $params Params
- */
- public function completeCall(array $params)
- {
- if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
- throw Exception::extension('libtgvoip');
- }
- if ($this->callStatus($params['id']) !== VoIP::CALL_STATE_ACCEPTED || !isset($this->calls[$params['id']]->storage['b'])) {
- $this->logger->logger(\sprintf(Lang::$current_lang['call_error_3'], $params['id']));
- return false;
- }
- $this->logger->logger(\sprintf(Lang::$current_lang['call_completing'], $this->calls[$params['id']]->getOtherID()), Logger::VERBOSE);
- $dh_config = ($this->getDhConfig());
- if (\hash('sha256', $params['g_a_or_b'], true) != $this->calls[$params['id']]->storage['g_a_hash']) {
- throw new SecurityException('Invalid g_a!');
- }
- $params['g_a_or_b'] = new BigInteger((string) $params['g_a_or_b'], 256);
- Crypt::checkG($params['g_a_or_b'], $dh_config['p']);
- $key = \str_pad($params['g_a_or_b']->powMod($this->calls[$params['id']]->storage['b'], $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT);
- if (\substr(\sha1($key, true), -8) != $params['key_fingerprint']) {
- throw new SecurityException(Lang::$current_lang['fingerprint_invalid']);
- }
- $visualization = [];
- $length = new BigInteger(\count(Magic::$emojis));
- foreach (\str_split(\hash('sha256', $key.\str_pad($params['g_a_or_b']->toBytes(), 256, \chr(0), STR_PAD_LEFT), true), 8) as $number) {
- $number[0] = \chr(\ord($number[0]) & 0x7f);
- $visualization[] = Magic::$emojis[(int) (new BigInteger($number, 256))->divide($length)[1]->toString()];
- }
- $this->calls[$params['id']]->setVisualization($visualization);
- $this->calls[$params['id']]->configuration['endpoints'] = \array_merge($params['connections'], $this->calls[$params['id']]->configuration['endpoints']);
- $this->calls[$params['id']]->configuration = \array_merge(['recv_timeout' => $this->config['call_receive_timeout_ms'] / 1000, 'init_timeout' => $this->config['call_connect_timeout_ms'] / 1000, 'data_saving' => VoIP::DATA_SAVING_NEVER, 'enable_NS' => true, 'enable_AEC' => true, 'enable_AGC' => true, 'auth_key' => $key, 'auth_key_id' => \substr(\sha1($key, true), -8), 'call_id' => \substr(\hash('sha256', $key, true), -16), 'network_type' => VoIP::NET_TYPE_ETHERNET], $this->calls[$params['id']]->configuration);
- $this->calls[$params['id']]->parseConfig();
- return $this->calls[$params['id']]->startTheMagic();
+ return $deferred->getFuture()->await();
}
/**
* Get call status.
*
* @param int $id Call ID
*/
- public function callStatus(int $id): int
+ public function callStatus(int $id): ?CallState
{
- if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
- throw Exception::extension('libtgvoip');
- }
if (isset($this->calls[$id])) {
return $this->calls[$id]->getCallState();
}
- return VoIP::CALL_STATE_NONE;
+ return null;
}
- /**
- * Get call info.
- *
- * @param int $call Call ID
- */
- public function getCall(int $call): array
- {
- if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
- throw Exception::extension('libtgvoip');
- }
- return $this->calls[$call];
- }
- /**
- * Discard call.
- *
- * @param array $call Call
- * @param array $rating Rating
- * @param boolean $need_debug Need debug?
- */
- public function discardCall(array $call, array $reason, array $rating = [], bool $need_debug = true): ?VoIP
- {
- if (!\class_exists('\\danog\\MadelineProto\\VoIP')) {
- throw Exception::extension('libtgvoip');
- }
- if (!isset($this->calls[$call['id']])) {
- return null;
- }
- $this->logger->logger(\sprintf(Lang::$current_lang['call_discarding'], $call['id']), Logger::VERBOSE);
- try {
- $res = $this->methodCallAsyncRead('phone.discardCall', ['peer' => $call, 'duration' => \time() - $this->calls[$call['id']]->whenCreated(), 'connection_id' => $this->calls[$call['id']]->getPreferredRelayID(), 'reason' => $reason]);
- } catch (RPCErrorException $e) {
- if (!\in_array($e->rpc, ['CALL_ALREADY_DECLINED', 'CALL_ALREADY_ACCEPTED'], true)) {
- throw $e;
- }
- }
- if (!empty($rating)) {
- $this->logger->logger(\sprintf('Setting rating for call %s...', $call['id']), Logger::VERBOSE);
- $this->methodCallAsyncRead('phone.setCallRating', ['peer' => $call, 'rating' => $rating['rating'], 'comment' => $rating['comment']]);
- }
- if ($need_debug && isset($this->calls[$call['id']])) {
- $this->logger->logger(\sprintf('Saving debug data for call %s...', $call['id']), Logger::VERBOSE);
- $this->methodCallAsyncRead('phone.saveCallDebug', ['peer' => $call, 'debug' => $this->calls[$call['id']]->getDebugLog()]);
- }
- if (!isset($this->calls[$call['id']])) {
- return null;
- }
- $c = $this->calls[$call['id']];
- unset($this->calls[$call['id']]);
- return $c;
- }
- /**
- * Check state of calls.
- *
- * @internal
- */
- public function checkCalls(): void
- {
- \array_walk($this->calls, function ($controller, $id): void {
- if ($controller->getCallState() === VoIP::CALL_STATE_ENDED) {
- $this->logger('Discarding ended call...');
- $controller->discard();
- unset($this->calls[$id]);
- }
- });
+
+ /** @internal */
+ public function cleanupCall(int $id): void {
+ unset($this->calls[$id]);
}
}
diff --git a/src/VoIP/CallState.php b/src/VoIP/CallState.php
new file mode 100644
index 000000000..17a263fe5
--- /dev/null
+++ b/src/VoIP/CallState.php
@@ -0,0 +1,37 @@
+.
+ *
+ * @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;
+
+
+enum CallState {
+ /** The call was requested */
+ case REQUESTED;
+ /** An incoming call */
+ case INCOMING;
+ /** The call was accepted */
+ case ACCEPTED;
+ /** The call was confirmed */
+ case CONFIRMED;
+ /** The call is ongoing */
+ case RUNNING;
+ /** The call has ended */
+ case ENDED;
+}
\ No newline at end of file
diff --git a/src/VoIP/Endpoint.php b/src/VoIP/Endpoint.php
index 37a1ff8dc..9c8392947 100644
--- a/src/VoIP/Endpoint.php
+++ b/src/VoIP/Endpoint.php
@@ -5,67 +5,48 @@ declare(strict_types=1);
namespace danog\MadelineProto\VoIP;
use Amp\Socket\Socket;
+use danog\MadelineProto\Lang;
use danog\MadelineProto\Logger;
use danog\MadelineProto\MTProto\PermAuthKey;
use danog\MadelineProto\MTProtoTools\Crypt;
use danog\MadelineProto\Tools;
use danog\MadelineProto\VoIP;
+use Exception;
use function Amp\Socket\connect;
final class Endpoint
{
- /**
- * IP address.
- */
- private string $ip;
- /**
- * Port.
- */
- private int $port;
- /**
- * Peer tag.
- */
- private string $peerTag;
-
- /**
- * Whether we're a reflector.
- */
- private bool $reflector;
-
- /**
- * Call instance.
- */
- private VoIP $instance;
/**
* The socket.
*/
private ?Socket $socket = null;
-
- /**
- * Whether we're the creator.
- */
- private bool $creator;
-
- /**
- * The auth key.
- */
- private PermAuthKey $authKey;
-
/**
* Create endpoint.
*/
- public function __construct(string $ip, int $port, string $peerTag, bool $reflector, VoIP $instance)
+ public function __construct(
+ private readonly string $ip,
+ private readonly int $port,
+ private readonly string $peerTag,
+ private readonly bool $reflector,
+ private readonly bool $creator,
+ private readonly string $authKey,
+ private readonly string $callID,
+ private readonly MessageHandler $handler
+ )
{
- $this->ip = $ip;
- $this->port = $port;
- $this->peerTag = $peerTag;
- $this->reflector = $reflector;
- $this->instance = $instance;
- $this->creator = $instance->isCreator();
- $this->authKey = $instance->getAuthKey();
$this->socket = connect("udp://{$this->ip}:{$this->port}");
}
+ public function __wakeup()
+ {
+ $this->socket = connect("udp://{$this->ip}:{$this->port}");
+ }
+ public function __sleep(): array
+ {
+ $vars = get_object_vars($this);
+ unset($vars['socket']);
+ return $vars;
+ }
public function __toString(): string
{
@@ -109,21 +90,21 @@ final class Endpoint
*/
public function read(): ?array
{
- $packet = $this->socket->read();
- if ($packet === null) {
- return null;
- }
-
do {
+ $packet = $this->socket->read();
+ if ($packet === null) {
+ return null;
+ }
+
$payload = \fopen('php://memory', 'rw+b');
\fwrite($payload, $packet);
\fseek($payload, 0);
$pos = 0;
- if ($this->instance->getPeerVersion() < 9 || $this->reflector) {
- /*if (\fread($payload, 16) !== $this->peerTag) {
+ if ($this->handler->peerVersion < 9 || $this->reflector) {
+ if (\fread($payload, 16) !== $this->peerTag) {
Logger::log('Received packet has wrong peer tag', Logger::ERROR);
continue;
- }*/
+ }
$pos = 16;
}
if (\fread($payload, 12) === "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF") {
@@ -149,11 +130,11 @@ final class Endpoint
} else {
fseek($payload, $pos);
$message_key = \fread($payload, 16);
- [$aes_key, $aes_iv] = Crypt::aesCalculate($message_key, $this->authKey->getAuthKey(), !$this->creator);
+ [$aes_key, $aes_iv] = Crypt::aesCalculate($message_key, $this->authKey, !$this->creator);
$encrypted_data = \stream_get_contents($payload);
$packet = Crypt::igeDecrypt($encrypted_data, $aes_key, $aes_iv);
- if ($message_key != \substr(\hash('sha256', \substr($this->authKey->getAuthKey(), 88 + ($this->creator ? 8 : 0), 32).$packet, true), 8, 16)) {
+ if ($message_key != \substr(\hash('sha256', \substr($this->authKey, 88 + ($this->creator ? 8 : 0), 32).$packet, true), 8, 16)) {
Logger::log('msg_key mismatch!', Logger::ERROR);
continue;
}
@@ -177,7 +158,7 @@ final class Endpoint
$flags = \unpack('V', \stream_get_contents($payload, 4))[1];
$result['_'] = $flags >> 24;
if ($flags & 4) {
- if (\stream_get_contents($payload, 16) !== $this->instance->configuration['call_id']) {
+ if (\stream_get_contents($payload, 16) !== $this->callID) {
Logger::log('Call ID mismatch', Logger::ERROR);
continue 2;
}
@@ -220,8 +201,8 @@ final class Endpoint
break;
default:
- if ($this->instance->getPeerVersion() >= 8 || (!$this->instance->getPeerVersion())) {
- \fseek($payload, -4, SEEK_CUR);
+ if ($this->handler->peerVersion >= 8 || (!$this->handler->peerVersion)) {
+ \fseek($payload, 0);
$result['_'] = \ord(\stream_get_contents($payload, 1));
$in_seq_no = \unpack('V', \stream_get_contents($payload, 4))[1];
$out_seq_no = \unpack('V', \stream_get_contents($payload, 4))[1];
@@ -244,8 +225,8 @@ final class Endpoint
continue 2;
}
}
- if (!$this->instance->received_packet($in_seq_no, $out_seq_no, $ack_mask)) {
- return $this->read();
+ if (!$this->handler->shouldSkip($in_seq_no, $out_seq_no, $ack_mask)) {
+ continue;
}
switch ($result['_']) {
// streamTypeSimple codec:int8 = StreamType;
@@ -261,6 +242,7 @@ final class Endpoint
for ($x = 0; $x < $length; $x++) {
$result['audio_streams'][$x] = \stream_get_contents($message, 4);
}
+ $this->handler->peerVersion = $result['protocol'];
break;
// streamType id:int8 type:int8 codec:int8 frame_duration:int16 enabled:int8 = StreamType;
//
@@ -296,10 +278,11 @@ final class Endpoint
$result['timestamp'] = \unpack('V', \stream_get_contents($message, 4))[1];
$result['data'] = \stream_get_contents($message, $length);
break;
- /*case \danog\MadelineProto\VoIP::PKT_UPDATE_STREAMS:
- break;
- case \danog\MadelineProto\VoIP::PKT_PING:
- break;*/
+ case \danog\MadelineProto\VoIP::PKT_UPDATE_STREAMS:
+ continue 2;
+ case \danog\MadelineProto\VoIP::PKT_PING:
+ $result['out_seq_no'] = $out_seq_no;
+ break;
case VoIP::PKT_PONG:
if (\fstat($payload)['size'] - \ftell($payload)) {
$result['out_seq_no'] = \unpack('V', \stream_get_contents($payload, 4))[1];
@@ -365,11 +348,11 @@ final class Endpoint
$padding += 16;
}
$plaintext .= Tools::random($padding);
- $message_key = \substr(\hash('sha256', \substr($this->authKey->getAuthKey(), 88 + ($this->creator ? 0 : 8), 32).$plaintext, true), 8, 16);
- [$aes_key, $aes_iv] = Crypt::aesCalculate($message_key, $this->authKey->getAuthKey(), $this->creator);
+ $message_key = \substr(\hash('sha256', \substr($this->authKey, 88 + ($this->creator ? 0 : 8), 32).$plaintext, true), 8, 16);
+ [$aes_key, $aes_iv] = Crypt::aesCalculate($message_key, $this->authKey, $this->creator);
$payload = $message_key.Crypt::igeEncrypt($plaintext, $aes_key, $aes_iv);
- if ($this->instance->getPeerVersion() < 9 || $this->reflector) {
+ if ($this->handler->peerVersion < 9 || $this->reflector) {
$payload = $this->peerTag.$payload;
}
@@ -384,11 +367,4 @@ final class Endpoint
$this->socket->write($this->peerTag.Tools::packSignedLong(-1).Tools::packSignedInt(-1).Tools::packSignedInt(-2).Tools::random(8));
return true;
}
- /**
- * Get peer tag.
- */
- public function getPeerTag(): string
- {
- return $this->peerTag;
- }
}
diff --git a/src/VoIP/MessageHandler.php b/src/VoIP/MessageHandler.php
index 579d52d85..03cd0d3c8 100644
--- a/src/VoIP/MessageHandler.php
+++ b/src/VoIP/MessageHandler.php
@@ -15,6 +15,7 @@ If not, see .
namespace danog\MadelineProto\VoIP;
+use danog\MadelineProto\Logger;
use danog\MadelineProto\Tools;
use danog\MadelineProto\VoIP;
@@ -23,8 +24,22 @@ use danog\MadelineProto\VoIP;
*
* @internal
*/
-trait MessageHandler
+final class MessageHandler
{
+ private array $received_timestamp_map = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+ private array $remote_ack_timestamp_map = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+
+ private int $session_out_seq_no = 0;
+ private int $session_in_seq_no = 0;
+
+ public int $peerVersion = 0;
+
+ public function __construct(
+ private readonly VoIP $instance,
+ private readonly string $callID
+ )
+ {
+ }
private static function pack_string(string $object): string
{
$l = \strlen($object);
@@ -42,7 +57,7 @@ trait MessageHandler
return $concat;
}
- private function encryptPacket(array $args, bool $increase_seqno = true): string
+ public function encryptPacket(array $args, bool $init = false): string
{
$message = '';
switch ($args['_']) {
@@ -167,11 +182,11 @@ trait MessageHandler
if ($this->peerVersion >= 8 || (!$this->peerVersion)) {
$payload = \chr($args['_']);
$payload .= Tools::packUnsignedInt($this->session_in_seq_no);
- $payload .= Tools::packUnsignedInt($this->session_out_seq_no);
+ $payload .= Tools::packUnsignedInt($init ? 0 : $this->session_out_seq_no);
$payload .= Tools::packUnsignedInt($ack_mask);
$payload .= \chr(0);
$payload .= $message;
- } elseif (\in_array($this->voip_state, [VoIP::STATE_WAIT_INIT, VoIP::STATE_WAIT_INIT_ACK], true)) {
+ } elseif (\in_array($this->instance->getVoIPState(), [VoIPState::WAIT_INIT, VoIPState::WAIT_INIT_ACK], true)) {
$payload = VoIP::TLID_DECRYPTED_AUDIO_BLOCK;
$payload .= Tools::random(8);
$payload .= \chr(7);
@@ -185,9 +200,9 @@ trait MessageHandler
$flags = \strlen($message) ? $flags | 1 : $flags & ~1; // raw_data
$flags = $flags | ($args['_'] << 24);
$payload .= Tools::packUnsignedInt($flags);
- $payload .= $this->configuration['call_id'];
+ $payload .= $this->callID;
$payload .= Tools::packUnsignedInt($this->session_in_seq_no);
- $payload .= Tools::packUnsignedInt($this->session_out_seq_no);
+ $payload .= Tools::packUnsignedInt($init ? 0 : $this->session_out_seq_no);
$payload .= Tools::packUnsignedInt($ack_mask);
$payload .= VoIP::PROTO_ID;
if ($flags & 2) {
@@ -201,14 +216,67 @@ trait MessageHandler
$payload .= Tools::random(8);
$payload .= \chr(7);
$payload .= Tools::random(7);
- $message = \chr($args['_']).Tools::packUnsignedInt($this->session_in_seq_no).Tools::packUnsignedInt($this->session_out_seq_no).Tools::packUnsignedInt($ack_mask).$message;
+ $message = \chr($args['_']).Tools::packUnsignedInt($this->session_in_seq_no).Tools::packUnsignedInt($init ? 0 : $this->session_out_seq_no).Tools::packUnsignedInt($ack_mask).$message;
$payload .= $this->pack_string($message);
}
- if ($increase_seqno) {
+ if (!$init) {
$this->session_out_seq_no++;
}
return $payload;
}
+
+
+ public function shouldSkip(int $last_ack_id, int $packet_seq_no, int $ack_mask): bool
+ {
+ if ($packet_seq_no > $this->session_in_seq_no) {
+ $diff = $packet_seq_no - $this->session_in_seq_no;
+ if ($diff > 31) {
+ $this->received_timestamp_map = \array_fill(0, 32, 0);
+ } else {
+ $remaining = 32-$diff;
+ for ($x = 0; $x < $remaining; $x++) {
+ $this->received_timestamp_map[$diff+$x] = $this->received_timestamp_map[$x];
+ }
+ for ($x = 1; $x < $diff; $x++) {
+ $this->received_timestamp_map[$x] = 0;
+ }
+ $this->received_timestamp_map[0] = \microtime(true);
+ }
+ $this->session_in_seq_no = $packet_seq_no;
+ } elseif (($diff = $this->session_in_seq_no - $packet_seq_no) < 32) {
+ if (!$this->received_timestamp_map[$diff]) {
+ Logger::log("Got duplicate $packet_seq_no");
+ return false;
+ }
+ $this->received_timestamp_map[$diff] = \microtime(true);
+ } else {
+ Logger::log("Packet $packet_seq_no is out of order and too late");
+ return false;
+ }
+ if ($last_ack_id > $this->session_out_seq_no) {
+ $diff = $last_ack_id - $this->session_out_seq_no;
+ if ($diff > 31) {
+ $this->remote_ack_timestamp_map = \array_fill(0, 32, 0);
+ } else {
+ $remaining = 32-$diff;
+ for ($x = 0; $x < $remaining; $x++) {
+ $this->remote_ack_timestamp_map[$diff+$x] = $this->remote_ack_timestamp_map[$x];
+ }
+ for ($x = 1; $x < $diff; $x++) {
+ $this->remote_ack_timestamp_map[$x] = 0;
+ }
+ $this->remote_ack_timestamp_map[0] = \microtime(true);
+ }
+ $this->session_out_seq_no = $last_ack_id;
+
+ for ($x = 1; $x < 32; $x++) {
+ if (!$this->remote_ack_timestamp_map[$x] && ($ack_mask >> 32-$x) & 1) {
+ $this->remote_ack_timestamp_map[$x] = \microtime(true);
+ }
+ }
+ }
+ return true;
+ }
}
diff --git a/src/VoIP/VoIPState.php b/src/VoIP/VoIPState.php
new file mode 100644
index 000000000..4b415156a
--- /dev/null
+++ b/src/VoIP/VoIPState.php
@@ -0,0 +1,33 @@
+.
+ *
+ * @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;
+
+/**
+ * VoIP protcol state.
+ */
+enum VoIPState {
+ case CREATED;
+ case WAIT_INIT;
+ case WAIT_INIT_ACK;
+ case ESTABLISHED;
+ case FAILED;
+ case RECONNECTING;
+}
\ No newline at end of file