1
0
mirror of https://github.com/danog/MadelineProto.git synced 2024-11-30 07:58:58 +01:00

Implement hold logic

This commit is contained in:
Daniil Gentili 2023-08-12 21:47:09 +02:00
parent 2c95ca4415
commit 8678bba3bb
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
10 changed files with 262 additions and 268 deletions

View File

@ -257,7 +257,7 @@ Want to add your own open-source project to this list? [Click here!](https://doc
* [Create a secret chat bot](https://docs.madelineproto.xyz/docs/SECRET_CHATS.html)
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.declinePasswordReset.html" name="account.declinePasswordReset">Abort a pending 2FA password reset, see here for more info »: account.declinePasswordReset</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/auth.acceptLoginToken.html" name="auth.acceptLoginToken">Accept QR code login token, logging in the app that generated it: auth.acceptLoginToken</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#acceptcall-array-call-bool" name="acceptCall">Accept call: acceptCall</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#acceptcall-int-id-void" name="acceptCall">Accept call: acceptCall</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#acceptsecretchat-array-params-void" name="acceptSecretChat">Accept secret chat: acceptSecretChat</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.acceptTermsOfService.html" name="help.acceptTermsOfService">Accept the new terms of service: help.acceptTermsOfService</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/bots.toggleUsername.html" name="bots.toggleUsername">Activate or deactivate a purchased fragment.com username associated to a bot we own: bots.toggleUsername</a>
@ -326,11 +326,9 @@ Want to add your own open-source project to this list? [Click here!](https://doc
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#closeconnection-string-message-void" name="closeConnection">Close connection with client, connected via web: closeConnection</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#completephonelogin-string-code-mixed" name="completePhoneLogin">Complet user login using login code: completePhoneLogin</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#complete2falogin-string-password-array" name="complete2faLogin">Complete 2FA login: complete2faLogin</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#completecall-array-params-mixed" name="completeCall">Complete call handshake: completeCall</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#completesignup-string-first_name-string-last_name-array" name="completeSignup">Complete signup to Telegram: completeSignup</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.startHistoryImport.html" name="messages.startHistoryImport">Complete the history import process, importing all messages into the chat. : messages.startHistoryImport</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.confirmPhone.html" name="account.confirmPhone">Confirm a phone number to cancel account deletion, for more info click here »: account.confirmPhone</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#confirmcall-array-params-mixed" name="confirmCall">Confirm call: confirmCall</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.receivedMessages.html" name="messages.receivedMessages">Confirms receipt of messages by a client, cancels PUSH-notification sending: messages.receivedMessages</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#tosupergroup-int-id-int" name="toSupergroup">Convert MTProto channel ID to bot API channel ID: toSupergroup</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#mtprototobotapi-array-data-array" name="MTProtoToBotAPI">Convert MTProto parameters to bot API parameters: MTProtoToBotAPI</a>
@ -383,7 +381,7 @@ Want to add your own open-source project to this list? [Click here!](https://doc
* <a href="https://docs.madelineproto.xyz/API_docs/methods/contacts.deleteContacts.html" name="contacts.deleteContacts">Deletes several contacts from the list: contacts.deleteContacts</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/contacts.unblock.html" name="contacts.unblock">Deletes the user from the blacklist: contacts.unblock</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.deactivateAllUsernames.html" name="channels.deactivateAllUsernames">Disable all purchased usernames of a supergroup or channel: channels.deactivateAllUsernames</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#discardcall-array-call-array-reason-array-rating-bool-need_debug-true-danog-madelineproto-voip" name="discardCall">Discard call: discardCall</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#discardcall-int-id-danog-madelineproto-voip-discardreason-reason-danog-madelineproto-voip-discardreason-hangup-int-1-5-rating-null-string-comment-null-void" name="discardCall">Discard call: discardCall</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#discardsecretchat-int-chat-void" name="discardSecretChat">Discard secret chat: discardSecretChat</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.dismissSuggestion.html" name="help.dismissSuggestion">Dismiss a suggestion, see here for more info »: help.dismissSuggestion</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/chatlists.hideChatlistUpdates.html" name="chatlists.hideChatlistUpdates">Dismiss new pending peers recently added to a chat folder deep link »: chatlists.hideChatlistUpdates</a>
@ -471,8 +469,8 @@ Want to add your own open-source project to this list? [Click here!](https://doc
* <a href="https://docs.madelineproto.xyz/API_docs/methods/account.getAutoSaveSettings.html" name="account.getAutoSaveSettings">Get autosave settings: account.getAutoSaveSettings</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#getconfig-array-config-array" name="getConfig">Get cached (or eventually re-fetch) server-side config: getConfig</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#getcachedconfig-array" name="getCachedConfig">Get cached server-side config: getCachedConfig</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#getcall-int-call-array" name="getCall">Get call info: getCall</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#callstatus-int-id-int" name="callStatus">Get call status: callStatus</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#getcallstate-int-id-danog-madelineproto-voip-callstate" name="getCallState">Get call state: getCallState</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#callstatus-int-id-danog-madelineproto-voip-callstate" name="callStatus">Get call status: callStatus</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getEmojiKeywordsDifference.html" name="messages.getEmojiKeywordsDifference">Get changed emoji keywords »: messages.getEmojiKeywordsDifference</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/help.getAppChangelog.html" name="help.getAppChangelog">Get changelog of current app. : help.getAppChangelog</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/stats.getBroadcastStats.html" name="stats.getBroadcastStats">Get channel statistics: stats.getBroadcastStats</a>
@ -699,6 +697,8 @@ Want to add your own open-source project to this list? [Click here!](https://doc
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.updatePinnedMessage.html" name="messages.updatePinnedMessage">Pin a message: messages.updatePinnedMessage</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.updatePinnedForumTopic.html" name="channels.updatePinnedForumTopic">Pin or unpin forum topics: channels.updatePinnedForumTopic</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.toggleDialogPin.html" name="messages.toggleDialogPin">Pin/unpin a dialog: messages.toggleDialogPin</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#callplay-int-id-string-file-void" name="callPlay">Play file in call: callPlay</a>
* <a href="https://docs.madelineproto.xyz/PHP/danog/MadelineProto/API.html#callplayonhold-int-id-string-files-void" name="callPlayOnHold">Play files on hold in call: callPlayOnHold</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/channels.getMessages.html" name="channels.getMessages">Please use the event handler: channels.getMessages</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getHistory.html" name="messages.getHistory">Please use the event handler: messages.getHistory</a>
* <a href="https://docs.madelineproto.xyz/API_docs/methods/messages.getMessages.html" name="messages.getMessages">Please use the event handler: messages.getMessages</a>

View File

@ -747,9 +747,7 @@
<code>MTProtoToTdcli</code>
<code>all</code>
<code>any</code>
<code>completeCall</code>
<code>completePhoneLogin</code>
<code>confirmCall</code>
<code>downloadToCallable</code>
<code>downloadToDir</code>
<code>downloadToStream</code>
@ -1276,7 +1274,6 @@
<PossiblyNullReference>
<code>logger</code>
<code>start</code>
<code>start</code>
</PossiblyNullReference>
<PropertyNotSetInConstructor>
<code>$channelParticipants</code>
@ -1300,6 +1297,7 @@
<RedundantCondition>
<code><![CDATA[$this->minDatabase?->sync()]]></code>
<code><![CDATA[isset($this->wrapper) && $this->isInited()]]></code>
<code><![CDATA[isset($this->wrapper) && isset(self::$references[$this->getSessionName()])]]></code>
</RedundantCondition>
<RedundantConditionGivenDocblockType>
<code><![CDATA[$this->authorized === API::LOGGED_IN && \is_int($dc_id)]]></code>
@ -2009,39 +2007,6 @@
<code>$stream</code>
</MissingPropertyType>
</file>
<file src="src/Stream/Ogg/Ogg.php">
<MismatchingDocblockReturnType>
<code><![CDATA[iterable<string>]]></code>
</MismatchingDocblockReturnType>
<PossiblyInvalidArgument>
<code>$offset</code>
<code>$offset</code>
<code>$offset</code>
<code>$offset</code>
<code>$offset</code>
<code>$offset</code>
<code>$size</code>
</PossiblyInvalidArgument>
<PossiblyInvalidPropertyAssignmentValue>
<code><![CDATA[$this->currentDuration += $frameDuration]]></code>
<code><![CDATA[$this->currentDuration += $totalDuration]]></code>
</PossiblyInvalidPropertyAssignmentValue>
<PropertyNotSetInConstructor>
<code>$emitter</code>
<code>$packFormat</code>
<code>$stream</code>
<code>$streamCount</code>
</PropertyNotSetInConstructor>
<PropertyTypeCoercion>
<code><![CDATA[$stream->getReadBuffer($l)]]></code>
</PropertyTypeCoercion>
<ReferenceConstraintViolation>
<code>$offset</code>
<code>$offset</code>
<code>$offset</code>
<code>$offset</code>
</ReferenceConstraintViolation>
</file>
<file src="src/Stream/Proxy/HttpProxy.php">
<MissingConstructor>
<code>$stream</code>
@ -2341,98 +2306,61 @@
</UnsupportedReferenceUsage>
</file>
<file src="src/VoIP.php">
<ArgumentTypeCoercion>
<code>$stream</code>
</ArgumentTypeCoercion>
<MissingPropertyType>
<code>$TLID_DECRYPTED_AUDIO_BLOCK</code>
<code>$TLID_REFLECTOR_PEER_INFO</code>
<code>$TLID_REFLECTOR_SELF_INFO</code>
<code>$TLID_SIMPLE_AUDIO_BLOCK</code>
<code>$callID</code>
<code>$configuration</code>
<code>$creatorID</code>
<code>$holdFiles</code>
<code>$inputFiles</code>
<code>$internalStorage</code>
<code>$isPlaying</code>
<code>$otherID</code>
<code>$outputFile</code>
<code>$protocol</code>
<code>$received_timestamp_map</code>
<code>$remote_ack_timestamp_map</code>
<code>$session_in_seq_no</code>
<code>$session_out_seq_no</code>
<code>$signal</code>
<code>$storage</code>
<code>$visualization</code>
<code>$voip_state</code>
</MissingPropertyType>
<MissingReturnType>
<code>openFile</code>
</MissingReturnType>
<PropertyNotSetInConstructor>
<code>$authKey</code>
</PropertyNotSetInConstructor>
</file>
<file src="src/VoIP/AckHandler.php">
<MissingParamType>
<code>$ack_mask</code>
<code>$last_ack_id</code>
<code>$packet_seq_no</code>
<code>$s1</code>
<code>$s2</code>
</MissingParamType>
<MissingReturnType>
<code>received_packet</code>
<code>seqgt</code>
</MissingReturnType>
<PossiblyInvalidArrayAccess>
<code><![CDATA[$API->getSelf()['id']]]></code>
</PossiblyInvalidArrayAccess>
</file>
<file src="src/VoIP/AuthKeyHandler.php">
<MissingClosureParamType>
<code>$controller</code>
<code>$id</code>
</MissingClosureParamType>
<MissingReturnType>
<code>completeCall</code>
<code>confirmCall</code>
<code>requestCall</code>
</MissingReturnType>
<PossiblyUndefinedArrayOffset>
<code><![CDATA[$call['id']]]></code>
<code><![CDATA[$call['id']]]></code>
<code><![CDATA[$call['id']]]></code>
<code><![CDATA[$call['id']]]></code>
<code><![CDATA[$user['user_id']]]></code>
</PossiblyUndefinedArrayOffset>
</file>
<file src="src/VoIP/Endpoint.php">
<MissingReturnType>
<code>read</code>
</MissingReturnType>
<MissingParamType>
<code>$stream</code>
</MissingParamType>
<PossiblyInvalidArrayOffset>
<code><![CDATA[$result[$x]['data']]]></code>
<code><![CDATA[$result[$x]['data']]]></code>
<code><![CDATA[$result[$x]['has_more_flags']]]></code>
<code><![CDATA[$result[$x]['has_more_flags']]]></code>
<code><![CDATA[$result[$x]['stream_id']]]></code>
<code><![CDATA[$result[$x]['stream_id']]]></code>
<code><![CDATA[$result[$x]['timestamp']]]></code>
<code><![CDATA[$result[$x]['timestamp']]]></code>
</PossiblyInvalidArrayOffset>
<PossiblyNullReference>
<code>read</code>
</PossiblyNullReference>
</file>
<file src="src/VoIP/MessageHandler.php">
<MissingParamType>
<code>$args</code>
<code>$datacenter</code>
<code>$object</code>
<code>$stream</code>
</MissingParamType>
<MissingReturnType>
<code>pack_string</code>
<code>recv_message</code>
<code>send_message</code>
<code>unpack_string</code>
</MissingReturnType>
<PossiblyUndefinedVariable>
<code>$ack_mask</code>
<code>$in_seq_no</code>
<code>$out_seq_no</code>
</PossiblyUndefinedVariable>
</file>
<file src="src/VoIPController.php">
<ArgumentTypeCoercion>
<code>$stream</code>
</ArgumentTypeCoercion>
<InaccessibleProperty>
<code><![CDATA[$this->authKey]]></code>
<code><![CDATA[$this->authKey]]></code>
</InaccessibleProperty>
<InvalidArgument>
<code><![CDATA[$this->call]]></code>
</InvalidArgument>
<InvalidPropertyAssignmentValue>
<code>$visualization</code>
<code>$visualization</code>
</InvalidPropertyAssignmentValue>
<PossiblyUndefinedArrayOffset>
<code><![CDATA[$this->call['g_a']]]></code>
</PossiblyUndefinedArrayOffset>
<PropertyNotSetInConstructor>
<code>$authKey</code>
<code>$bestEndpoint</code>
<code>$messageHandler</code>
</PropertyNotSetInConstructor>
</file>
<file src="src/Wrappers/DialogHandler.php">
<InvalidReturnType>
<code><![CDATA[array<int, array>]]></code>

View File

@ -335,8 +335,7 @@ abstract class InternalDoc
}
/**
* Play files on hold in call.
*
* @param list<string> $files
* @param array<string> $files
*/
public function callPlayOnHold(int $id, array $files): void
{
@ -399,13 +398,13 @@ abstract class InternalDoc
}
/**
* Discard call.
*
* @param int<1, 5> $rating Call rating in stars
* @param string $comment Additional comment on call quality.
*/
public function discardCall(int $id, array $reason = [
'_' => 'phoneCallDiscardReasonDisconnect',
], array $rating = [
]): void
public function discardCall(int $id, \danog\MadelineProto\VoIP\DiscardReason $reason = \danog\MadelineProto\VoIP\DiscardReason::HANGUP, ?int $rating = null, ?string $comment = null): void
{
$this->wrapper->getAPI()->discardCall($id, $reason, $rating);
$this->wrapper->getAPI()->discardCall($id, $reason, $rating, $comment);
}
/**
* Discard secret chat.
@ -695,6 +694,13 @@ abstract class InternalDoc
{
return $this->wrapper->getAPI()->getCachedConfig();
}
/**
* Get the call with the specified user ID.
*/
public function getCallByPeer(int $userId): ?\danog\MadelineProto\VoIP
{
return $this->wrapper->getAPI()->getCallByPeer($userId);
}
/**
* Get call state.
*/

View File

@ -670,6 +670,7 @@ final class MTProto implements TLCallback, LoggerGetter
'reportDest',
'calls',
'callsByPeer',
'snitch',
];
}

View File

@ -57,7 +57,6 @@ use danog\MadelineProto\TL\TL;
use danog\MadelineProto\TL\Types\Button;
use danog\MadelineProto\Tools;
use danog\MadelineProto\UpdateHandlerType;
use danog\MadelineProto\VoIP;
use danog\MadelineProto\VoIPController;
use Revolt\EventLoop;
use Throwable;
@ -753,14 +752,16 @@ trait UpdateHandler
if ($update['_'] === 'updatePhoneCall') {
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'],
);
if (isset($this->calls[$update['phone_call']['id']])) {
return;
}
break;*/
$this->calls[$update['phone_call']['id']] = $controller = new VoIPController(
$this,
$update['phone_call'],
);
$this->callsByPeer[$controller->public->otherID] = $controller;
$update['phone_call'] = $controller->public;
break;
case 'phoneCallWaiting':
if (isset($this->calls[$update['phone_call']['id']])) {
return;
@ -769,6 +770,7 @@ trait UpdateHandler
$this,
$update['phone_call']
);
$this->callsByPeer[$controller->public->otherID] = $controller;
$update['phone_call'] = $controller->public;
break;
case 'phoneCallAccepted':

View File

@ -15,10 +15,12 @@ If not, see <http://www.gnu.org/licenses/>.
namespace danog\MadelineProto;
use danog\MadelineProto\EventHandler\SimpleFilters;
use danog\MadelineProto\EventHandler\Update;
use danog\MadelineProto\VoIP\CallState;
use danog\MadelineProto\VoIP\DiscardReason;
final class VoIP extends Update
final class VoIP extends Update implements SimpleFilters
{
/** Phone call ID */
public readonly int $callID;
@ -26,8 +28,6 @@ final class VoIP extends Update
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;
@ -44,14 +44,12 @@ final class VoIP extends Update
$call['_'] = 'inputPhoneCall';
$this->date = $call['date'];
$this->callID = $call['id'];
if ($call['_'] === 'phoneCallWaiting') {
$this->outgoing = false;
$this->otherID = $call['participant_id'];
$this->creatorID = $call['admin_id'];
} else {
if ($call['admin_id'] === $API->getSelf()['id']) {
$this->outgoing = true;
$this->otherID = $call['participant_id'];
} else {
$this->outgoing = false;
$this->otherID = $call['admin_id'];
$this->creatorID = $call['participant_id'];
}
}
@ -65,10 +63,13 @@ final class VoIP extends Update
}
/**
* Discard call.
*
* @param int<1, 5> $rating Call rating in stars
* @param string $comment Additional comment on call quality.
*/
public function discard(array $reason = ['_' => 'phoneCallDiscardReasonDisconnect'], array $rating = []): self
public function discard(DiscardReason $reason = DiscardReason::HANGUP, ?int $rating = null, ?string $comment = null): self
{
$this->getClient()->discardCall($this->callID, $reason, $rating);
$this->getClient()->discardCall($this->callID, $reason, $rating, $comment);
return $this;
}
@ -104,6 +105,7 @@ final class VoIP extends Update
/**
* Files to play on hold.
* @param array<string> $files
*/
public function playOnHold(array $files): self
{

View File

@ -44,6 +44,8 @@ trait AuthKeyHandler
{
/** @var array<int, VoIPController> */
private array $calls = [];
/** @var array<int, VoIPController> */
private array $callsByPeer = [];
private array $pendingCalls = [];
/**
* Request VoIP call.
@ -75,6 +77,7 @@ trait AuthKeyHandler
$res['a'] = $a;
$res['g_a'] = \str_pad($g_a->toBytes(), 256, \chr(0), STR_PAD_LEFT);
$this->calls[$res['id']] = $controller = new VoIPController($this, $res);
$this->callsByPeer[$controller->public->otherID] = $controller;
unset($this->pendingCalls[$user]);
$deferred->complete($controller->public);
} catch (Throwable $e) {
@ -124,10 +127,21 @@ trait AuthKeyHandler
/**
* Discard call.
*
* @param int<1, 5> $rating Call rating in stars
* @param string $comment Additional comment on call quality.
*/
public function discardCall(int $id, array $reason = ['_' => 'phoneCallDiscardReasonDisconnect'], array $rating = []): void
public function discardCall(int $id, DiscardReason $reason = DiscardReason::HANGUP, ?int $rating = null, ?string $comment = null): void
{
($this->calls[$id] ?? null)?->discard($reason, $rating);
($this->calls[$id] ?? null)?->discard($reason, $rating, $comment);
}
/**
* Get the call with the specified user ID.
*/
public function getCallByPeer(int $userId): ?VoIP
{
return $this->callsByPeer[$userId] ?? null;
}
/**
@ -140,8 +154,7 @@ trait AuthKeyHandler
/**
* Play files on hold in call.
*
* @param list<string> $files
* @param array<string> $files
*/
public function callPlayOnHold(int $id, array $files): void
{

View File

@ -12,6 +12,7 @@ use danog\MadelineProto\Tools;
use danog\MadelineProto\VoIPController;
use Exception;
use function Amp\delay;
use function Amp\Socket\connect;
final class Endpoint
@ -42,7 +43,7 @@ final class Endpoint
{
$vars = \get_object_vars($this);
unset($vars['socket']);
return array_keys($vars);
return \array_keys($vars);
}
public function __toString(): string
@ -104,6 +105,7 @@ final class Endpoint
}
$pos = 16;
}
$result = [];
if (\fread($payload, 12) === "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF") {
switch ($crc = \fread($payload, 4)) {
case VoIPController::TLID_REFLECTOR_SELF_INFO:
@ -222,7 +224,7 @@ final class Endpoint
continue 2;
}
}
if (!$this->handler->shouldSkip($in_seq_no, $out_seq_no, $ack_mask)) {
if (isset($in_seq_no) && isset($out_seq_no) && !$this->handler->shouldSkip($in_seq_no, $out_seq_no, $ack_mask)) {
continue;
}
switch ($result['_']) {
@ -331,6 +333,20 @@ final class Endpoint
return $result;
} while (true);
}
public function writeReliably(array $data): bool
{
do {
$payload = $this->handler->encryptPacket($data);
$seqno = $this->handler->getLastSentSeq();
if (!$this->write($payload)) {
return false;
}
delay(0.2);
if ($this->handler->acked($seqno)) {
return true;
}
} while (true);
}
/**
* Write data.
*/

View File

@ -15,9 +15,9 @@ If not, see <http://www.gnu.org/licenses/>.
namespace danog\MadelineProto\VoIP;
use AssertionError;
use danog\MadelineProto\Logger;
use danog\MadelineProto\Tools;
use danog\MadelineProto\VoIP;
use danog\MadelineProto\VoIPController;
/**
@ -27,11 +27,11 @@ use danog\MadelineProto\VoIPController;
*/
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 $outSeqNo = 0;
private int $inSeqNo = 0;
private int $session_out_seq_no = 0;
private int $session_in_seq_no = 0;
private int $acksForSent = 0;
private int $acksForReceived = 0;
public int $peerVersion = 0;
@ -40,6 +40,10 @@ final class MessageHandler
public readonly string $callID
) {
}
public function getLastSentSeq(): int
{
return $this->outSeqNo-1;
}
private static function pack_string(string $object): string
{
$l = \strlen($object);
@ -99,7 +103,7 @@ final class MessageHandler
// packetStreamState#3 state:streamTypeState = Packet;
case VoIPController::PKT_STREAM_STATE:
$message .= \chr($args['id']);
$message .= \chr($args['enabled']);
$message .= \chr($args['enabled'] ? 1 : 0);
break;
// streamData flags:int2 stream_id:int6 has_more_flags:flags.1?true length:(flags.0?int16:int8) timestamp:int data:byteArray = StreamData;
// packetStreamData#4 stream_data:streamData = Packet;
@ -169,21 +173,11 @@ final class MessageHandler
break;*/
}
$ack_mask = 0;
for ($x=0; $x<32; $x++) {
if ($this->received_timestamp_map[$x]>0) {
$ack_mask|=1;
}
if ($x<31) {
$ack_mask<<=1;
}
}
if ($this->peerVersion >= 8 || (!$this->peerVersion)) {
$payload = \chr($args['_']);
$payload .= Tools::packUnsignedInt($this->session_in_seq_no);
$payload .= Tools::packUnsignedInt($init ? 0 : $this->session_out_seq_no);
$payload .= Tools::packUnsignedInt($ack_mask);
$payload .= Tools::packUnsignedInt($this->inSeqNo);
$payload .= Tools::packUnsignedInt($init ? 0 : $this->outSeqNo);
$payload .= Tools::packUnsignedInt($this->acksForReceived);
$payload .= \chr(0);
$payload .= $message;
} elseif (\in_array($this->instance->getVoIPState(), [VoIPState::WAIT_INIT, VoIPState::WAIT_INIT_ACK], true)) {
@ -201,9 +195,9 @@ final class MessageHandler
$flags = $flags | ($args['_'] << 24);
$payload .= Tools::packUnsignedInt($flags);
$payload .= $this->callID;
$payload .= Tools::packUnsignedInt($this->session_in_seq_no);
$payload .= Tools::packUnsignedInt($init ? 0 : $this->session_out_seq_no);
$payload .= Tools::packUnsignedInt($ack_mask);
$payload .= Tools::packUnsignedInt($this->inSeqNo);
$payload .= Tools::packUnsignedInt($init ? 0 : $this->outSeqNo);
$payload .= Tools::packUnsignedInt($this->acksForReceived);
$payload .= VoIPController::PROTO_ID;
if ($flags & 2) {
$payload .= $this->pack_string($args['extra']);
@ -216,12 +210,12 @@ final class MessageHandler
$payload .= Tools::random(8);
$payload .= \chr(7);
$payload .= Tools::random(7);
$message = \chr($args['_']).Tools::packUnsignedInt($this->session_in_seq_no).Tools::packUnsignedInt($init ? 0 : $this->session_out_seq_no).Tools::packUnsignedInt($ack_mask).$message;
$message = \chr($args['_']).Tools::packUnsignedInt($this->inSeqNo).Tools::packUnsignedInt($init ? 0 : $this->outSeqNo).Tools::packUnsignedInt($this->acksForReceived).$message;
$payload .= $this->pack_string($message);
}
if (!$init) {
$this->session_out_seq_no++;
$this->outSeqNo++;
}
return $payload;
@ -229,53 +223,35 @@ final class MessageHandler
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 ($packet_seq_no > $this->inSeqNo) {
$diff = $packet_seq_no - $this->inSeqNo;
if ($diff > 31) {
$this->received_timestamp_map = \array_fill(0, 32, 0);
$this->acksForReceived = 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->acksForReceived = (($this->acksForReceived << ($diff+1)) & 0xFFFF_FFFF) | 1;
}
$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]) {
$this->inSeqNo = $packet_seq_no;
} elseif (($diff = $this->inSeqNo - $packet_seq_no) < 32) {
if ($this->acksForReceived & (1 << ($diff+1))) {
Logger::log("Got duplicate $packet_seq_no");
return false;
}
$this->received_timestamp_map[$diff] = \microtime(true);
$this->acksForReceived |= 1 << ($diff+1);
} 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);
}
}
}
$this->inSeqNo = $packet_seq_no;
$this->acksForSent = $ack_mask;
return true;
}
public function acked(int $seq): bool
{
$diff = $this->outSeqNo - $seq;
if ($diff > 31) {
throw new AssertionError("Already forgot about packet!");
}
return (bool) ($this->acksForReceived & (1 << ($diff+1)));
}
}

View File

@ -19,6 +19,7 @@ use danog\MadelineProto\MTProtoTools\Crypt;
use danog\MadelineProto\Stream\Common\FileBufferedStream;
use danog\MadelineProto\Stream\ConnectionContext;
use danog\MadelineProto\VoIP\CallState;
use danog\MadelineProto\VoIP\DiscardReason;
use danog\MadelineProto\VoIP\Endpoint;
use danog\MadelineProto\VoIP\MessageHandler;
use danog\MadelineProto\VoIP\VoIPState;
@ -96,23 +97,23 @@ final class VoIPController
private array $call;
/** @var list<string> */
/** @var array<string> */
private array $holdFiles = [];
/** @var list<string> */
private array $inputFiles = [];
private int $holdIndex = 0;
/**
* @var array<Endpoint>
*/
private array $sockets = [];
private ?Endpoint $bestEndpoint = null;
private Endpoint $bestEndpoint;
private bool $pendingPing = true;
private ?string $timeoutWatcher = null;
private float $lastIncomingTimestamp = 0.0;
private float $lastOutgoingTimestamp = 0.0;
private int $opusTimestamp = 0;
private SplQueue $packetQueue;
private array $tempHoldFiles = [];
/** Auth key */
private readonly string $authKey;
@ -134,10 +135,10 @@ final class VoIPController
$call['_'] = 'inputPhoneCall';
$this->packetQueue = new SplQueue;
$this->call = $call;
if ($call['_'] === 'phoneCallWaiting') {
$this->callState = CallState::INCOMING;
} else {
if ($this->public->outgoing) {
$this->callState = CallState::REQUESTED;
} else {
$this->callState = CallState::INCOMING;
}
}
@ -165,7 +166,7 @@ final class VoIPController
}
if ($e->rpc === 'CALL_ALREADY_DECLINED') {
$this->API->logger->logger(Lang::$current_lang['call_already_declined']);
$this->discard(['_' => 'phoneCallDiscardReasonHangup']);
$this->discard(DiscardReason::HANGUP);
return false;
}
throw $e;
@ -202,6 +203,8 @@ final class VoIPController
$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']);
$this->callState = CallState::ACCEPTED;
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) {
@ -211,15 +214,13 @@ final class VoIPController
}
if ($e->rpc === 'CALL_ALREADY_DECLINED') {
$this->API->logger->logger(Lang::$current_lang['call_already_declined']);
$this->discard(['_' => 'phoneCallDiscardReasonHangup']);
$this->discard(DiscardReason::HANGUP);
return $this;
}
throw $e;
}
$this->call['b'] = $b;
$this->callState = CallState::ACCEPTED;
return $this;
}
@ -236,7 +237,7 @@ final class VoIPController
}
$this->API->logger->logger(\sprintf(Lang::$current_lang['call_completing'], $this->public->otherID), Logger::VERBOSE);
$dh_config = $this->API->getDhConfig();
if (\hash('sha256', $params['g_a_or_b'], true) != $this->call['g_a_hash']) {
if (\hash('sha256', (string) $params['g_a_or_b'], true) !== (string) $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);
@ -267,13 +268,11 @@ final class VoIPController
public function __wakeup(): void
{
if ($this->callState === CallState::RUNNING) {
$this->lastIncomingTimestamp = microtime(true);
$this->lastIncomingTimestamp = \microtime(true);
$this->startReadLoop();
if ($this->voipState === VoIPState::ESTABLISHED) {
$diff = (int) ((microtime(true) - $this->lastOutgoingTimestamp) * 1000);
Logger::log("Missed $diff milliseconds of audio due to script shutdown...");
$this->opusTimestamp += $diff;
$this->opusTimestamp -= ($this->opusTimestamp) % 60;
$diff = (int) ((\microtime(true) - $this->lastOutgoingTimestamp) * 1000);
$this->opusTimestamp += $diff - ($diff % 60);
EventLoop::queue($this->startWriteLoop(...));
}
}
@ -291,12 +290,17 @@ final class VoIPController
/**
* Discard call.
*
* @param int<1, 5> $rating Call rating in stars
* @param string $comment Additional comment on call quality.
*/
public function discard(array $reason = ['_' => 'phoneCallDiscardReasonDisconnect'], array $rating = []): self
public function discard(DiscardReason $reason = DiscardReason::HANGUP, ?int $rating = null, ?string $comment = null): self
{
if ($this->callState === CallState::ENDED) {
return $this;
}
$this->API->cleanupCall($this->public->callID);
Logger::log("Now closing $this");
if (isset($this->timeoutWatcher)) {
EventLoop::cancel($this->timeoutWatcher);
@ -306,21 +310,26 @@ final class VoIPController
foreach ($this->sockets as $socket) {
$socket->disconnect();
}
$this->packetQueue = new SplQueue;
Logger::log("Closed all sockets, discarding $this");
$this->API->logger->logger(\sprintf(Lang::$current_lang['call_discarding'], $this->public->callID), Logger::VERBOSE);
try {
$this->API->methodCallAsyncRead('phone.discardCall', ['peer' => $this->call, 'duration' => \time() - $this->public->date, 'connection_id' => 0, 'reason' => $reason]);
$this->API->methodCallAsyncRead('phone.discardCall', ['peer' => $this->call, 'duration' => \time() - $this->public->date, 'connection_id' => 0, 'reason' => ['_' => match ($reason) {
DiscardReason::BUSY => 'phoneCallDiscardReasonBusy',
DiscardReason::HANGUP => 'phoneCallDiscardReasonHangup',
DiscardReason::DISCONNECTED => 'phoneCallDiscardReasonDisconnect',
DiscardReason::MISSED => 'phoneCallDiscardReasonMissed'
}]]);
} catch (RPCErrorException $e) {
if (!\in_array($e->rpc, ['CALL_ALREADY_DECLINED', 'CALL_ALREADY_ACCEPTED'], true)) {
throw $e;
}
}
if (!empty($rating)) {
if ($rating !== null) {
$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->methodCallAsyncRead('phone.setCallRating', ['peer' => $this->call, 'rating' => $rating, 'comment' => $comment]);
}
$this->API->cleanupCall($this->public->callID);
$this->callState = CallState::ENDED;
return $this;
}
@ -395,7 +404,7 @@ final class VoIPController
break;
case self::PKT_INIT_ACK:
if (!$this->bestEndpoint) {
if (!isset($this->bestEndpoint)) {
$this->bestEndpoint = $socket;
$this->startWriteLoop();
}
@ -436,52 +445,90 @@ final class VoIPController
}
$this->timeoutWatcher = EventLoop::repeat(10, function (): void {
if (\microtime(true) - $this->lastIncomingTimestamp > 10) {
$this->discard(['_' => 'phoneCallDiscardReasonDisconnect']);
$this->discard(DiscardReason::DISCONNECTED);
}
});
}
private bool $muted = false;
private bool $playingHold = false;
private function pullPacket(): bool
{
$file = \array_shift($this->inputFiles);
if ($file) {
$this->playingHold = false;
} else {
$this->playingHold = true;
if (!$this->holdFiles) {
return false;
}
$file = $this->holdFiles[($this->holdIndex++) % \count($this->holdFiles)];
}
$it = $this->openFile($file);
foreach ($it->opusPackets as $packet) {
$this->packetQueue->enqueue($packet);
}
return false;
}
/**
* Start write loop.
*/
private function startWriteLoop(): void
{
$this->voipState = VoIPState::ESTABLISHED;
Logger::log("Call established, sending OPUS data!");
Logger::log("Call established in $this, sending OPUS data!");
$this->tempHoldFiles = [];
$delay = $this->muted ? 0.2 : 0.06;
$t = \microtime(true) + $delay;
while (true) {
if ($this->packetQueue->isEmpty()) {
$file = \array_shift($this->inputFiles);
if (!$file) {
if (empty($this->tempHoldFiles)) {
$this->tempHoldFiles = $this->holdFiles;
}
if (empty($this->tempHoldFiles)) {
return;
}
$file = \array_shift($this->tempHoldFiles);
if ($this->packetQueue->isEmpty() && !$this->pullPacket()) {
if (!$this->muted) {
$this->bestEndpoint->writeReliably([
'_' => self::PKT_STREAM_STATE,
'id' => 0,
'enabled' => false
]);
$this->muted = true;
$delay = 0.2;
}
$it = $this->openFile($file);
foreach ($it->opusPackets as $packet) {
$this->packetQueue->enqueue($packet);
$packet = $this->messageHandler->encryptPacket([
'_' => self::PKT_NOP
]);
} else {
if ($this->muted) {
$this->bestEndpoint->writeReliably([
'_' => self::PKT_STREAM_STATE,
'id' => 0,
'enabled' => true
]);
$this->muted = false;
$delay = 0.06;
$this->opusTimestamp = 0;
}
}
$t = \microtime(true) + 0.060;
while (!$this->packetQueue->isEmpty()) {
$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!");
$cur = \microtime(true);
$diff = $t - $cur;
if ($diff > 0) {
delay($diff);
}
$this->bestEndpoint->write($packet);
$this->lastOutgoingTimestamp = $cur;
$t += 0.060;
$packet = $this->messageHandler->encryptPacket([
'_' => self::PKT_STREAM_DATA,
'stream_id' => 0,
'data' => $this->packetQueue->dequeue(),
'timestamp' => $this->opusTimestamp
]);
$this->opusTimestamp += 60;
}
//Logger::log("Writing {$this->opusTimestamp} in $this!");
$cur = \microtime(true);
$diff = $t - $cur;
if ($diff > 0) {
delay($diff);
}
$this->bestEndpoint->write($packet);
if ($diff > 0) {
$cur += $diff;
} else {
Logger::log("We're late while sending audio data!");
}
$this->lastOutgoingTimestamp = $cur;
$t += $delay;
}
}
/**
@ -500,6 +547,9 @@ final class VoIPController
public function play(string $file): self
{
$this->inputFiles[] = $file;
if ($this->playingHold) {
$this->packetQueue = new SplQueue;
}
return $this;
}
@ -509,13 +559,13 @@ final class VoIPController
*/
public function then(string $file): self
{
$this->inputFiles[] = $file;
return $this;
return $this->play($file);
}
/**
* Files to play on hold.
*
* @param array<string> $files
*/
public function playOnHold(array $files): self
{