mirror of
https://github.com/danog/MadelineProto.git
synced 2024-11-26 15:44:40 +01:00
Refactor VoIP API
This commit is contained in:
parent
a26f906c5b
commit
b2af64d84e
@ -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
|
||||
|
||||
|
@ -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/';
|
||||
|
@ -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)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
655
src/VoIP.php
655
src/VoIP.php
@ -15,41 +15,28 @@ If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<string> */
|
||||
protected array $holdFiles = [];
|
||||
/** @var list<string> */
|
||||
protected array $inputFiles = [];
|
||||
|
||||
/**
|
||||
* @var array<Endpoint>
|
||||
*/
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
Copyright 2016-2018 Daniil Gentili
|
||||
(https://daniil.it)
|
||||
This file is part of MadelineProto.
|
||||
MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU Affero General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License along with MadelineProto.
|
||||
If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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<int, VoIP> */
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
37
src/VoIP/CallState.php
Normal file
37
src/VoIP/CallState.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Call state module.
|
||||
*
|
||||
* This file is part of MadelineProto.
|
||||
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU Affero General Public License for more details.
|
||||
* You should have received a copy of the GNU General Public License along with MadelineProto.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
|
||||
* @link https://docs.madelineproto.xyz MadelineProto documentation
|
||||
*/
|
||||
|
||||
namespace danog\MadelineProto\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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
33
src/VoIP/VoIPState.php
Normal file
33
src/VoIP/VoIPState.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Call state module.
|
||||
*
|
||||
* This file is part of MadelineProto.
|
||||
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU Affero General Public License for more details.
|
||||
* You should have received a copy of the GNU General Public License along with MadelineProto.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
|
||||
* @link https://docs.madelineproto.xyz MadelineProto documentation
|
||||
*/
|
||||
|
||||
namespace danog\MadelineProto\VoIP;
|
||||
|
||||
/**
|
||||
* VoIP protcol state.
|
||||
*/
|
||||
enum VoIPState {
|
||||
case CREATED;
|
||||
case WAIT_INIT;
|
||||
case WAIT_INIT_ACK;
|
||||
case ESTABLISHED;
|
||||
case FAILED;
|
||||
case RECONNECTING;
|
||||
}
|
Loading…
Reference in New Issue
Block a user