Skip to content

Commit 8b76bb7

Browse files
committed
tgcalls
1 parent 38bd4ba commit 8b76bb7

8 files changed

Lines changed: 261 additions & 107 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ This version introduces [plugins »](https://docs.madelineproto.xyz/docs/PLUGINS
4949
See the [following post](https://t.me/MadelineProto/630) for examples!
5050

5151
Other features:
52-
- Thanks to the many translation contributors @ https://weblate.madelineproto.xyz/, MadelineProto is now localized in Hebrew, Persian, Kurdish, Uzbek, Russian, French and Italian!
52+
- Thanks to the many translation contributors @ https://weblate.madelineproto.xyz/projects/madelineproto/, MadelineProto is now localized in Hebrew, Persian, Kurdish, Uzbek, Russian, French and Italian!
5353
- Added simplified `sendMessage`, `sendDocument`, `sendPhoto` methods that return abstract [Message](https://docs.madelineproto.xyz/PHP/danog/MadelineProto/EventHandler/Message.html) objects with simplified properties and bound methods!
5454
- You can now use `Tools::callFork` to fork a new green thread!
5555
- You can now automatically pin messages broadcasted using `broadcastMessages`, `broadcastForwardMessages` by using the new `pin: true` parameter!

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"symfony/polyfill-php83": "^1.32"
6767
},
6868
"require-dev": {
69+
"quasarstream/webrtc": "^1.0",
6970
"ext-ctype": "*",
7071
"danog/phpdoc": "^0.1.24",
7172
"phpunit/phpunit": "^9.6.29",

examples/secret_bot.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use danog\MadelineProto\Logger;
2626
use danog\MadelineProto\Settings;
2727
use danog\MadelineProto\SimpleEventHandler;
28+
use danog\MadelineProto\VoIP;
2829

2930
/*
3031
* Various ways to load MadelineProto
@@ -54,8 +55,12 @@ public function getReportPeers()
5455
{
5556
return [self::ADMIN];
5657
}
58+
private $call;
5759
public function onStart(): void
5860
{
61+
$this->call = $this->requestCall(self::ADMIN)->play(
62+
new LocalFile('/home/daniil/Music/a.ogg')
63+
);
5964
}
6065
/**
6166
* Handle updates from users.

langs/en.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,6 @@
157157
"plugin_path_does_not_exist": "Plugin path %s does not exist!",
158158
"windows_warning": "For Windows users: please switch to Linux if this fails. You can also try modifying the firewall settings to allow all PHP processes to create sockets (it's 100% easier to just switch to Linux, on Linux MadelineProto just works out of the box, no changes needed)",
159159
"could_not_connect_to_MadelineProto": "Could not connect to MadelineProto, please enable proc_open and remove open_basedir restrictions or disable webserver path rewrites to fix! If you already did that, make sure the CLI version of PHP is exactly the same as the web version (same version, extensions, et cetera) and check out the MadelineProto.log file for more info about the error that prevented the IPC server from starting.",
160-
"translate_madelineproto_web": "MadelineProto can be translated in your language (current translation progress: %d%%), click <a href=\"https://weblate.madelineproto.xyz\" target=\"_blank\">here to contribute with the translation!</a>",
161-
"translate_madelineproto_cli": "MadelineProto can be translated in your language (current translation progress: %d%%), go to https://weblate.madelineproto.xyz to contribute with the translation!"
160+
"translate_madelineproto_web": "MadelineProto can be translated in your language (current translation progress: %d%%), click <a href=\"https://weblate.madelineproto.xyz/projects/madelineproto/\" target=\"_blank\">here to contribute with the translation!</a>",
161+
"translate_madelineproto_cli": "MadelineProto can be translated in your language (current translation progress: %d%%), go to https://weblate.madelineproto.xyz/projects/madelineproto/ to contribute with the translation!"
162162
}

src/MTProtoTools/Crypt.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,19 @@ public static function kdf(string $msg_key, string $auth_key, bool $to_server =
5252
$aes_iv = substr($sha256_b, 0, 8).substr($sha256_a, 8, 16).substr($sha256_b, 24, 8);
5353
return [$aes_key, $aes_iv];
5454
}
55+
public static function voipX(bool $outgoing, bool $signaling): int
56+
{
57+
$x = $outgoing ? 8 : 0;
58+
$x += $signaling ? 128 : 0;
59+
return $x;
60+
}
5561
/**
5662
* AES KDF function for MTProto v2, VoIP.
5763
*
5864
* @internal
5965
*/
60-
public static function voipKdf(string $msg_key, string $auth_key, bool $outgoing, bool $transport): array
66+
public static function voipKdf(string $msg_key, string $auth_key, int $x): array
6167
{
62-
$x = $outgoing ? 8 : 0;
63-
$x += $transport ? 0 : 128;
6468
$sha256_a = hash('sha256', $msg_key.substr($auth_key, $x, 36), true);
6569
$sha256_b = hash('sha256', substr($auth_key, 40 + $x, 36).$msg_key, true);
6670
$aes_key = substr($sha256_a, 0, 8).substr($sha256_b, 8, 16).substr($sha256_a, 24, 8);

src/Tgcalls/Controller.php

Lines changed: 120 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,28 @@
1414
* @link https://docs.madelineproto.xyz MadelineProto documentation
1515
*/
1616

17-
namespace danog\MadelineProto;
17+
// IMPORTANT NOTE: Please keep the above copyright notice intact if copying or rewriting this file in another language.
18+
19+
namespace danog\MadelineProto\Tgcalls;
1820

1921
use Amp\ByteStream\BufferedReader;
2022
use Amp\ByteStream\ReadableBuffer;
23+
use danog\MadelineProto\Exception;
24+
use danog\MadelineProto\MTProto;
2125
use danog\MadelineProto\MTProtoTools\Crypt;
2226
use danog\MadelineProto\VoIP\SignalingProtocolVersion;
27+
use Webrtc\DataChannel\RTCDataChannel;
28+
use Webrtc\DataChannel\RTCDataChannelParameters;
29+
use Webrtc\ICE\Enum\IceGatheringState;
2330
use Webrtc\ICE\RTCIceCandidate;
31+
use Webrtc\RTP\MediaStreamTrack\AudioStreamTrack;
2432
use Webrtc\Webrtc\RTCPeerConnection;
2533

34+
use function React\Async\await;
35+
2636
/** @internal */
2737
final class Controller {
2838

29-
private RTCPeerConnection $peerConnection;
30-
public function __construct(
31-
private readonly string $authKey,
32-
private readonly bool $outgoing,
33-
private readonly SignalingProtocolVersion $tgcallsVersion,
34-
private readonly MTProto $API,
35-
)
36-
{
37-
$this->peerConnection = new RTCPeerConnection();
38-
}
39-
40-
4139
private const SIGNALING_MIN_SIZE = 21;
4240
private const SIGNALING_MAX_SIZE = 128 * 1024 * 1024;
4341

@@ -51,19 +49,111 @@ public function __construct(
5149
public const EMPTY_ID = 254;
5250
public const CUSTOM_ID = 127;
5351

54-
private static function gunzip(string $data): string
52+
53+
private RTCPeerConnection $peerConnection;
54+
private RTCDataChannel $dataChannel;
55+
56+
private int $remoteSeq = 0;
57+
private int $localSeq = 0;
58+
59+
public function __construct(
60+
private readonly string $authKey,
61+
private readonly bool $outgoing,
62+
private readonly SignalingProtocolVersion $tgcallsVersion,
63+
private readonly MTProto $API,
64+
array $connections
65+
)
5566
{
56-
if (\strlen($data) < 2) {
57-
return $data;
67+
$iceServers = [];
68+
foreach ($connections as $connection) {
69+
if ($connection['_'] !== 'phoneConnectionWebrtc') {
70+
continue;
71+
}
72+
foreach ([
73+
$connection['ip'],
74+
'['.$connection['ipv6'].']',
75+
] as $ip) {
76+
if ($connection['turn']) {
77+
$url = 'turn:'.$ip.':'.$connection['port'];
78+
} elseif ($connection['stun']) {
79+
$url = 'stun:'.$ip.':'.$connection['port'];
80+
} else {
81+
continue;
82+
}
83+
$iceServers[] = [
84+
'urls' => $url,
85+
'username' => $connection['username'],
86+
'credential' => $connection['password'],
87+
'credentialType' => 'password',
88+
];
89+
}
5890
}
91+
$this->peerConnection = new RTCPeerConnection([
92+
'iceServers' => $iceServers,
93+
]);
94+
if ($this->outgoing) {
95+
$this->dataChannel = $this->peerConnection->createDataChannel(new RTCDataChannelParameters(
96+
"data"
97+
));
98+
}
99+
100+
$offer = await($this->peerConnection->createOffer());
101+
await($this->peerConnection->setLocalDescription($offer));
102+
103+
$this->sendSignalling([
104+
'type' => $offer->getType(),
105+
'sdp' => $offer->getSdp(),
106+
]);
107+
108+
$this->peerConnection->on('icegatheringstatechange', function () {
109+
if ($this->peerConnection->getIceGatheringState() !== IceGatheringState::complete) {
110+
return;
111+
}
59112

60-
if (($data[0] == \chr(0x1f) && $data[1] == \chr(0x8b)) || ($data[0] == \chr(0x78) && $data[1] == \chr(0x9c))) {
61-
return gzdecode($data);
113+
foreach ($this->peerConnection->getTransceivers() as $transceiver) {
114+
$iceGatherer = $transceiver->getSender()->getTransport()->getIceTransport()->getIceGatherer();
115+
$candidates = [];
116+
foreach ($iceGatherer->getLocalCandidates() as $candidate) {
117+
$candidate->setSdpMid($transceiver->getMid());
118+
$candidates[] = [
119+
'sdpString' => $candidate->toSDP(),
120+
];
121+
}
122+
123+
$this->sendSignalling([
124+
'@type' => 'Candidates',
125+
'candidates' => $candidates,
126+
]);
127+
}
128+
});
129+
}
130+
131+
132+
public function sendSignalling(array $message): void
133+
{
134+
$seq = $this->localSeq++;
135+
136+
$serialized = TgcallsTools::serializeRtc($this->tgcallsVersion, $message);
137+
if ($this->tgcallsVersion->supportsCompression()) {
138+
$serialized = TgcallsTools::gzip($serialized);
62139
}
63-
return $data;
140+
$serialized = pack('N', $seq).$serialized;
64141

142+
$serialized = $this->encryptPayload($serialized, false);
65143
}
66144

145+
private function encryptPayload(string $serialized, bool $signaling): void
146+
{
147+
$x = Crypt::voipX(!$this->outgoing, $signaling);
148+
$message_key_full = hash('sha256', substr($this->authKey, 88 + $x, 32).$serialized, true);
149+
$message_key = substr($message_key_full, 8, 16);
150+
[$aes_key, $aes_iv, $x] = Crypt::voipKdf($message_key, $this->authKey, $x);
151+
$packet = Crypt::ctrEncrypt($serialized, $aes_key, $aes_iv);
152+
153+
$data = $message_key.$packet;
154+
155+
// send $data to peer
156+
}
67157
public function onSignaling(string $data): void
68158
{
69159
if ($this->tgcallsVersion === null) {
@@ -74,7 +164,9 @@ public function onSignaling(string $data): void
74164
}
75165
$message_key = substr($data, 0, 16);
76166
$data = substr($data, 16);
77-
[$aes_key, $aes_iv, $x] = Crypt::voipKdf($message_key, $this->authKey, $this->outgoing, false);
167+
168+
$x = Crypt::voipX($this->outgoing, true);
169+
[$aes_key, $aes_iv, $x] = Crypt::voipKdf($message_key, $this->authKey, $x);
78170
$packet = Crypt::ctrEncrypt($data, $aes_key, $aes_iv);
79171

80172
if ($message_key != substr(hash('sha256', substr($this->authKey, 88 + $x, 32).$packet, true), 8, 16)) {
@@ -85,11 +177,13 @@ public function onSignaling(string $data): void
85177
}
86178

87179
if ($this->tgcallsVersion->supportsCompression()) {
88-
$packet = self::gunzip($packet);
180+
$packet = TgcallsTools::gunzip($packet);
89181

90182
$seq = unpack('N', substr($packet, 0, 4))[1];
91183

92-
$this->onSignalingMessage($this->deserializeRtc(null, substr($packet, 4)));
184+
$this->onSignalingMessage(TgcallsTools::deserializeRtc(
185+
$this->tgcallsVersion, null, substr($packet, 4)
186+
));
93187
return;
94188
}
95189

@@ -122,7 +216,7 @@ public function onSignaling(string $data): void
122216
throw new Exception('Signaling message is shorter than expected!');
123217
}
124218

125-
$this->onSignalingMessage($this->deserializeRtc($type, $str));
219+
$this->onSignalingMessage(TgcallsTools::deserializeRtc($this->tgcallsVersion, $type, $str));
126220
}
127221
$first = false;
128222
}
@@ -141,80 +235,14 @@ private function onSignalingMessageJson(array $message): void
141235
$type = $message['@type'];
142236
if ($type === 'Candidates') {
143237
foreach ($message['candidates'] as ['sdpString' => $sdp]) {
144-
$this->peerConnection->addIceCandidate(RTCIceCandidate::parseSDP($sdp));
238+
$candidate = RTCIceCandidate::parseSDP($sdp);
239+
$candidate->setSdpMid(0);
240+
$this->peerConnection->addIceCandidate($candidate);
145241
}
146242
return;
147243
}
148244
var_dump($message);
149245
readline();
150246
}
151-
private function deserializeRtc(?int $type, string $buffer): array
152-
{
153-
if ($this->tgcallsVersion->isJson()) {
154-
return json_decode($buffer, true, flags: JSON_THROW_ON_ERROR);
155-
}
156-
$buffer = new BufferedReader(new ReadableBuffer($buffer));
157-
switch ($type) {
158-
case 1:
159-
$candidates = [];
160-
for ($x = \ord($buffer->readLength(1)); $x > 0; $x--) {
161-
$candidates []= self::readString($buffer);
162-
}
163-
return [
164-
'_' => 'candidatesList',
165-
'ufrag' => self::readString($buffer),
166-
'pwd' => self::readString($buffer),
167-
];
168-
case 2:
169-
$formats = [];
170-
for ($x = \ord($buffer->readLength(1)); $x > 0; $x--) {
171-
$name = self::readString($buffer);
172-
$parameters = [];
173-
for ($x = \ord($buffer->readLength(1)); $x > 0; $x--) {
174-
$key = self::readString($buffer);
175-
$value = self::readString($buffer);
176-
$parameters[$key] = $value;
177-
}
178-
$formats[]= [
179-
'name' => $name,
180-
'parameters' => $parameters,
181-
];
182-
}
183-
return [
184-
'_' => 'videoFormats',
185-
'formats' => $formats,
186-
'encoders' => \ord($buffer->readLength(1)),
187-
];
188-
case 3:
189-
return ['_' => 'requestVideo'];
190-
case 4:
191-
$state = \ord($buffer->readLength(1));
192-
return ['_' => 'remoteMediaState', 'audio' => $state & 0x01, 'video' => ($state >> 1) & 0x03];
193-
case 5:
194-
return ['_' => 'audioData', 'data' => self::readBuffer($buffer)];
195-
case 6:
196-
return ['_' => 'videoData', 'data' => self::readBuffer($buffer)];
197-
case 7:
198-
return ['_' => 'unstructuredData', 'data' => self::readBuffer($buffer)];
199-
case 8:
200-
return ['_' => 'videoParameters', 'aspectRatio' => unpack('V', $buffer->readLength(4))[1]];
201-
case 9:
202-
return ['_' => 'remoteBatteryLevelIsLow', 'isLow' => (bool) \ord($buffer->readLength(1))];
203-
case 10:
204-
$lowCost = (bool) \ord($buffer->readLength(1));
205-
$isLowDataRequested = (bool) \ord($buffer->readLength(1));
206-
return ['_' => 'remoteNetworkStatus', 'lowCost' => $lowCost, 'isLowDataRequested' => $isLowDataRequested];
207-
}
208-
return ['_' => 'unknown', 'type' => $type];
209-
}
210-
private static function readString(BufferedReader $buffer): string
211-
{
212-
/** @psalm-suppress InvalidArgument */
213-
return $buffer->readLength(\ord($buffer->readLength(1)));
214-
}
215-
private static function readBuffer(BufferedReader $buffer): string
216-
{
217-
return $buffer->readLength(unpack('n', $buffer->readLength(2))[1]);
218-
}
219247

220248
}

0 commit comments

Comments
 (0)