Multiple async and VoIP improvements
This commit is contained in:
parent
e5798a41db
commit
7005cdf10a
2
docs
2
docs
@ -1 +1 @@
|
||||
Subproject commit 3dbba4bc60dc830c08d5343e438bcd003ba9d1c5
|
||||
Subproject commit f0229af1baa48d7529f90d7c3dcfbf9d5f54187b
|
87
magna.php
87
magna.php
@ -9,11 +9,11 @@ MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
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/>.
|
||||
*/
|
||||
set_include_path(get_include_path().':'.realpath(dirname(__FILE__).'/MadelineProto/'));
|
||||
*/
|
||||
set_include_path(get_include_path() . ':' . realpath(dirname(__FILE__) . '/MadelineProto/'));
|
||||
|
||||
if (!file_exists(__DIR__.'/vendor/autoload.php')) {
|
||||
echo 'You did not run composer update, using madeline.php'.PHP_EOL;
|
||||
if (!file_exists(__DIR__ . '/vendor/autoload.php')) {
|
||||
echo 'You did not run composer update, using madeline.php' . PHP_EOL;
|
||||
if (!file_exists('madeline.php')) {
|
||||
copy('https://phar.madelineproto.xyz/madeline.php', 'madeline.php');
|
||||
}
|
||||
@ -26,15 +26,15 @@ if (!file_exists('songs.php')) {
|
||||
copy('https://github.com/danog/MadelineProto/raw/master/songs.php', 'songs.php');
|
||||
}
|
||||
|
||||
echo 'Deserializing MadelineProto from session.madeline...'.PHP_EOL;
|
||||
echo 'Deserializing MadelineProto from session.madeline...' . PHP_EOL;
|
||||
|
||||
/*if (!isset($MadelineProto->inputEncryptedFilePhoto) && false) {
|
||||
$MadelineProto->inputEncryptedFilePhoto = $MadelineProto->upload_encrypted('tests/faust.jpg', 'fausticorn.jpg'); // This gets an inputFile object with file name magic
|
||||
$MadelineProto->inputEncryptedFileGif = $MadelineProto->upload_encrypted('tests/pony.mp4');
|
||||
$MadelineProto->inputEncryptedFileSticker = $MadelineProto->upload_encrypted('tests/lel.webp');
|
||||
$MadelineProto->inputEncryptedFileDocument = $MadelineProto->upload_encrypted('tests/60', 'magic'); // This gets an inputFile object with file name magic
|
||||
$MadelineProto->inputEncryptedFileVideo = $MadelineProto->upload_encrypted('tests/swing.mp4');
|
||||
$MadelineProto->inputEncryptedFileAudio = $MadelineProto->upload_encrypted('tests/mosconi.mp3');
|
||||
$MadelineProto->inputEncryptedFilePhoto = $MadelineProto->upload_encrypted('tests/faust.jpg', 'fausticorn.jpg'); // This gets an inputFile object with file name magic
|
||||
$MadelineProto->inputEncryptedFileGif = $MadelineProto->upload_encrypted('tests/pony.mp4');
|
||||
$MadelineProto->inputEncryptedFileSticker = $MadelineProto->upload_encrypted('tests/lel.webp');
|
||||
$MadelineProto->inputEncryptedFileDocument = $MadelineProto->upload_encrypted('tests/60', 'magic'); // This gets an inputFile object with file name magic
|
||||
$MadelineProto->inputEncryptedFileVideo = $MadelineProto->upload_encrypted('tests/swing.mp4');
|
||||
$MadelineProto->inputEncryptedFileAudio = $MadelineProto->upload_encrypted('tests/mosconi.mp3');
|
||||
}*/
|
||||
|
||||
class EventHandler extends \danog\MadelineProto\EventHandler
|
||||
@ -45,15 +45,7 @@ class EventHandler extends \danog\MadelineProto\EventHandler
|
||||
$call->configuration['enable_NS'] = false;
|
||||
$call->configuration['enable_AGC'] = false;
|
||||
$call->configuration['enable_AEC'] = false;
|
||||
$call->configuration['shared_config'] = [
|
||||
'audio_init_bitrate' => 100 * 1000,
|
||||
'audio_max_bitrate' => 100 * 1000,
|
||||
'audio_min_bitrate' => 10 * 1000,
|
||||
'audio_congestion_window' => 4 * 1024,
|
||||
//'audio_bitrate_step_decr' => 0,
|
||||
//'audio_bitrate_step_incr' => 2000,
|
||||
];
|
||||
$call->configuration['log_file_path'] = '/tmp/logs'.$call->getCallID()['id'].'.log'; // Default is /dev/null
|
||||
$call->configuration['log_file_path'] = '/tmp/logs' . $call->getCallID()['id'] . '.log'; // Default is /dev/null
|
||||
//$call->configuration["stats_dump_file_path"] = "/tmp/stats".$call->getCallID()['id'].".txt"; // Default is /dev/null
|
||||
$call->parseConfig();
|
||||
$call->playOnHold($songs);
|
||||
@ -91,7 +83,7 @@ Propic art by @magnaluna on [deviantart](https://magnaluna.deviantart.com).", 'p
|
||||
$this->configureCall($call);
|
||||
if ($call->getCallState() !== \danog\MadelineProto\VoIP::CALL_STATE_ENDED) {
|
||||
$this->calls[$call->getOtherID()] = $call;
|
||||
$this->times[$call->getOtherID()] = [time(), $this->messages->sendMessage(['peer' => $call->getOtherID(), 'message' => 'Total running calls: '.count($this->calls).PHP_EOL.PHP_EOL.$call->getDebugString()])['id']];
|
||||
$this->times[$call->getOtherID()] = [time(), $this->messages->sendMessage(['peer' => $call->getOtherID(), 'message' => 'Total running calls: ' . count($this->calls) . PHP_EOL . PHP_EOL . $call->getDebugString()])['id']];
|
||||
}
|
||||
}
|
||||
if (strpos($message, '/program') === 0) {
|
||||
@ -181,7 +173,7 @@ Propic art by @magnaluna on [deviantart](https://magnaluna.deviantart.com).", 'p
|
||||
$this->calls[$update['phone_call']->getOtherID()] = $update['phone_call'];
|
||||
|
||||
try {
|
||||
$this->times[$update['phone_call']->getOtherID()] = [time(), $this->messages->sendMessage(['peer' => $update['phone_call']->getOtherID(), 'message' => 'Total running calls: '.count($this->calls).PHP_EOL.PHP_EOL])['id']];
|
||||
$this->times[$update['phone_call']->getOtherID()] = [time(), $this->messages->sendMessage(['peer' => $update['phone_call']->getOtherID(), 'message' => 'Total running calls: ' . count($this->calls) . PHP_EOL . PHP_EOL])['id']];
|
||||
} catch (\danog\MadelineProto\RPCErrorException $e) {
|
||||
}
|
||||
}
|
||||
@ -203,7 +195,7 @@ Propic art by @magnaluna on [deviantart](https://magnaluna.deviantart.com).", 'p
|
||||
$this->configureCall($call);
|
||||
if ($call->getCallState() !== \danog\MadelineProto\VoIP::CALL_STATE_ENDED) {
|
||||
$this->calls[$call->getOtherID()] = $call;
|
||||
$this->times[$call->getOtherID()] = [time(), $this->messages->sendMessage(['peer' => $call->getOtherID(), 'message' => 'Total running calls: '.count($this->calls).PHP_EOL.PHP_EOL.$call->getDebugString()])['id']];
|
||||
$this->times[$call->getOtherID()] = [time(), $this->messages->sendMessage(['peer' => $call->getOtherID(), 'message' => 'Total running calls: ' . count($this->calls) . PHP_EOL . PHP_EOL . $call->getDebugString()])['id']];
|
||||
}
|
||||
} catch (\danog\MadelineProto\RPCErrorException $e) {
|
||||
try {
|
||||
@ -239,7 +231,7 @@ Propic art by @magnaluna on [deviantart](https://magnaluna.deviantart.com).", 'p
|
||||
}
|
||||
break;
|
||||
}
|
||||
\danog\MadelineProto\Logger::log(count($this->calls).' calls running!');
|
||||
\danog\MadelineProto\Logger::log(count($this->calls) . ' calls running!');
|
||||
foreach ($this->calls as $key => $call) {
|
||||
if ($call->getCallState() === \danog\MadelineProto\VoIP::CALL_STATE_ENDED) {
|
||||
try {
|
||||
@ -248,24 +240,24 @@ Propic art by @magnaluna on [deviantart](https://magnaluna.deviantart.com).", 'p
|
||||
'reply_to_msg_id' => $this->times[$call->getOtherID()][1],
|
||||
'peer' => $call->getOtherID(), 'message' => 'Call statistics by @magnaluna',
|
||||
'media' => [
|
||||
'_' => 'inputMediaUploadedDocument',
|
||||
'file' => "/tmp/stats".$call->getCallID()['id'].".txt",
|
||||
'attributes' => [
|
||||
['_' => 'documentAttributeFilename', 'file_name' => "stats".$call->getCallID()['id'].".txt"]
|
||||
]
|
||||
'_' => 'inputMediaUploadedDocument',
|
||||
'file' => "/tmp/stats".$call->getCallID()['id'].".txt",
|
||||
'attributes' => [
|
||||
['_' => 'documentAttributeFilename', 'file_name' => "stats".$call->getCallID()['id'].".txt"]
|
||||
]
|
||||
],
|
||||
]);*/
|
||||
$this->messages->sendMedia([
|
||||
'reply_to_msg_id' => $this->times[$call->getOtherID()][1],
|
||||
'peer' => $call->getOtherID(), 'message' => 'Debug info by @magnaluna',
|
||||
'media' => [
|
||||
'_' => 'inputMediaUploadedDocument',
|
||||
'file' => '/tmp/logs'.$call->getCallID()['id'].'.log',
|
||||
'attributes' => [
|
||||
['_' => 'documentAttributeFilename', 'file_name' => 'logs'.$call->getCallID()['id'].'.log'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
'reply_to_msg_id' => $this->times[$call->getOtherID()][1],
|
||||
'peer' => $call->getOtherID(), 'message' => 'Debug info by @magnaluna',
|
||||
'media' => [
|
||||
'_' => 'inputMediaUploadedDocument',
|
||||
'file' => '/tmp/logs' . $call->getCallID()['id'] . '.log',
|
||||
'attributes' => [
|
||||
['_' => 'documentAttributeFilename', 'file_name' => 'logs' . $call->getCallID()['id'] . '.log'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
} catch (\danog\MadelineProto\Exception $e) {
|
||||
echo $e;
|
||||
@ -274,14 +266,14 @@ Propic art by @magnaluna on [deviantart](https://magnaluna.deviantart.com).", 'p
|
||||
} catch (\danog\MadelineProto\Exception $e) {
|
||||
echo $e;
|
||||
}
|
||||
@unlink('/tmp/logs'.$call->getCallID()['id'].'.log');
|
||||
@unlink('/tmp/stats'.$call->getCallID()['id'].'.txt');
|
||||
@unlink('/tmp/logs' . $call->getCallID()['id'] . '.log');
|
||||
@unlink('/tmp/stats' . $call->getCallID()['id'] . '.txt');
|
||||
unset($this->calls[$key]);
|
||||
} elseif (isset($this->times[$call->getOtherID()]) && $this->times[$call->getOtherID()][0] < time()) {
|
||||
$this->times[$call->getOtherID()][0] += 30 + count($this->calls);
|
||||
|
||||
try {
|
||||
$this->messages->editMessage(['id' => $this->times[$call->getOtherID()][1], 'peer' => $call->getOtherID(), 'message' => 'Total running calls: '.count($this->calls).PHP_EOL.PHP_EOL.$call->getDebugString()]);
|
||||
$this->messages->editMessage(['id' => $this->times[$call->getOtherID()][1], 'peer' => $call->getOtherID(), 'message' => 'Total running calls: ' . count($this->calls) . PHP_EOL . PHP_EOL . $call->getDebugString()]);
|
||||
} catch (\danog\MadelineProto\RPCErrorException $e) {
|
||||
echo $e;
|
||||
}
|
||||
@ -289,7 +281,16 @@ Propic art by @magnaluna on [deviantart](https://magnaluna.deviantart.com).", 'p
|
||||
}
|
||||
}
|
||||
}
|
||||
$MadelineProto = new \danog\MadelineProto\API('session.madeline', ['secret_chats' => ['accept_chats' => false], 'logger' => ['logger' => 3, 'logger_param' => getcwd().'/MadelineProto.log']]);
|
||||
|
||||
\danog\MadelineProto\VoIPServerConfig::update(
|
||||
[
|
||||
'audio_init_bitrate' => 100 * 1000,
|
||||
'audio_max_bitrate' => 100 * 1000,
|
||||
'audio_min_bitrate' => 10 * 1000,
|
||||
'audio_congestion_window' => 4 * 1024,
|
||||
]
|
||||
);
|
||||
$MadelineProto = new \danog\MadelineProto\API('session.madeline', ['secret_chats' => ['accept_chats' => false], 'logger' => ['logger' => 3, 'logger_param' => getcwd() . '/MadelineProto.log']]);
|
||||
$MadelineProto->start();
|
||||
|
||||
if (!isset($MadelineProto->programmed_call)) {
|
||||
|
@ -212,12 +212,6 @@ image: https://docs.madelineproto.xyz/favicons/android-chrome-256x256.png
|
||||
$'.$constructor.$layer.' = '.$params.';
|
||||
```
|
||||
|
||||
[PWRTelegram](https://pwrtelegram.xyz) json-encoded version:
|
||||
|
||||
```
|
||||
'.$pwr_params.'
|
||||
```
|
||||
|
||||
|
||||
Or, if you\'re into Lua:
|
||||
|
||||
|
@ -230,28 +230,6 @@ $MadelineProto->start();
|
||||
$'.$type.' = $MadelineProto->'.$php_method.'(['.$params.']);
|
||||
```
|
||||
|
||||
### [PWRTelegram HTTP API](https://pwrtelegram.xyz) example (NOT FOR MadelineProto):
|
||||
|
||||
'.($bot ? '### As a bot:
|
||||
|
||||
POST/GET to `https://api.pwrtelegram.xyz/botTOKEN/madeline`
|
||||
|
||||
Parameters:
|
||||
|
||||
* method - '.$data['method'].'
|
||||
* params - `{'.$json_params.'}`
|
||||
|
||||
' : '').'
|
||||
|
||||
### As a user:
|
||||
|
||||
POST/GET to `https://api.pwrtelegram.xyz/userTOKEN/'.$data['method'].'`
|
||||
|
||||
Parameters:
|
||||
|
||||
'.$pwr_params.'
|
||||
|
||||
|
||||
Or, if you\'re into Lua:
|
||||
|
||||
```lua
|
||||
|
File diff suppressed because one or more lines are too long
@ -463,11 +463,15 @@ trait AuthKeyHandler
|
||||
}
|
||||
|
||||
public function get_dh_config()
|
||||
{
|
||||
return $this->wait($this->get_dh_config_async());
|
||||
}
|
||||
public function get_dh_config_async()
|
||||
{
|
||||
$this->updates_state['sync_loading'] = true;
|
||||
|
||||
try {
|
||||
$dh_config = $this->method_call('messages.getDhConfig', ['version' => $this->dh_config['version'], 'random_length' => 0], ['datacenter' => $this->datacenter->curdc]);
|
||||
$dh_config = yield $this->method_call_async_read('messages.getDhConfig', ['version' => $this->dh_config['version'], 'random_length' => 0], ['datacenter' => $this->datacenter->curdc]);
|
||||
} finally {
|
||||
$this->updates_state['sync_loading'] = false;
|
||||
}
|
||||
|
@ -30,191 +30,6 @@ use function Amp\Promise\all;
|
||||
*/
|
||||
trait CallHandler
|
||||
{
|
||||
/*
|
||||
public function select()
|
||||
{
|
||||
$result = [];
|
||||
|
||||
try {
|
||||
/*
|
||||
if ($this->is_http($this->settings['connection_settings']['default_dc']) || $this->altervista) {
|
||||
$this->logger->logger("Initial HTTP short poll");
|
||||
$waiting = $this->datacenter->select(0.1);
|
||||
$result = $this->handle_select($waiting, $result);
|
||||
}/
|
||||
$tries = 10; // TODO add setting
|
||||
$this->logger->logger('Long poll');
|
||||
$t = microtime(true);
|
||||
$waiting = $this->datacenter->select();
|
||||
$t = microtime(true) - $t;
|
||||
$this->logger->logger("Long poll took $t");
|
||||
|
||||
$result = $this->handle_select($waiting, $result);
|
||||
|
||||
do {
|
||||
$this->logger->logger('Short poll');
|
||||
$waiting = $this->datacenter->select($this->is_http($this->settings['connection_settings']['default_dc']) || $this->altervista ? $this->settings['connection_settings']['all']['timeout'] / 10 : true);
|
||||
$result = $this->handle_select($waiting, $result);
|
||||
} while ($tries-- && $waiting);
|
||||
} catch (\danog\MadelineProto\NothingInTheSocketException $e) {
|
||||
$this->logger->logger('Nothing in the socket while selecting', \danog\MadelineProto\Logger::ULTRA_VERBOSE);
|
||||
foreach ($this->datacenter->sockets as $dc => $socket) {
|
||||
$this->close_and_reopen($dc);
|
||||
$this->send_messages($dc);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public $did = [];
|
||||
|
||||
public function handle_select($waiting, $result)
|
||||
{
|
||||
foreach ($waiting as $dc) {
|
||||
$error = $this->recv_message($dc);
|
||||
if ($error !== true) {
|
||||
$this->close_and_reopen($dc);
|
||||
if ($error === -404) {
|
||||
if ($this->datacenter->sockets[$dc]->temp_auth_key !== null) {
|
||||
$this->logger->logger('WARNING: Resetting auth key...', \danog\MadelineProto\Logger::WARNING);
|
||||
$this->datacenter->sockets[$dc]->temp_auth_key = null;
|
||||
$this->init_authorization();
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \danog\MadelineProto\RPCErrorException($error, $error);
|
||||
}
|
||||
$result[$dc] = $this->handle_messages($dc) && (isset($result[$dc]) ? $result[$dc] : true);
|
||||
if (($this->is_http($dc) || $this->altervista) && $this->datacenter->sockets[$dc]->new_outgoing) {
|
||||
$this->send_messages($dc);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
public function iorun($updates)
|
||||
{
|
||||
do {
|
||||
if ($updates && time() - $this->last_getdifference > $this->settings['updates']['getdifference_interval']) {
|
||||
$this->get_updates_difference();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($canunset = !$this->updates_state['sync_loading']) {
|
||||
$this->updates_state['sync_loading'] = true;
|
||||
}
|
||||
if ($canunsetpostponeupdates = !$this->postpone_updates) {
|
||||
$this->postpone_updates = true;
|
||||
}
|
||||
if ($canunsetpostponepwrchat = !$this->postpone_pwrchat) {
|
||||
$this->postpone_pwrchat = true;
|
||||
}
|
||||
|
||||
if (($this->is_http($this->settings['connection_settings']['default_dc']) || $this->altervista) && $updates) {
|
||||
$this->send_messages($this->settings['connection_settings']['default_dc']);
|
||||
}
|
||||
foreach ($this->datacenter->sockets as $id => $datacenter) {
|
||||
if ($datacenter->pending_outgoing) {
|
||||
$this->send_messages($id);
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->logger('Polling for ' . ($updates ? 'updates' : 'replies') . ': selecting', \danog\MadelineProto\Logger::ULTRA_VERBOSE);
|
||||
$t = microtime(true);
|
||||
$only_updates = $this->select();
|
||||
$t = microtime(true) - $t;
|
||||
$this->logger->logger('Polling for ' . ($updates ? 'updates' : 'replies') . ': selecting took ' . $t, \danog\MadelineProto\Logger::ULTRA_VERBOSE);
|
||||
$response_result = $this->has_pending_calls();
|
||||
|
||||
$repeat = 0;
|
||||
foreach ($this->datacenter->sockets as $id => $datacenter) {
|
||||
if ($updates) {
|
||||
if (isset($only_updates[$id])) {
|
||||
if ($only_updates[$id]) {
|
||||
$this->logger->logger("Polling for updates: got only updates for DC $id", \danog\MadelineProto\Logger::VERBOSE);
|
||||
} else {
|
||||
$this->logger->logger("Polling for updates: got also RPC replies for DC $id", \danog\MadelineProto\Logger::NOTICE);
|
||||
}
|
||||
if ($response_result[$id]) {
|
||||
$this->logger->logger("Polling for updates: still pending requests, resending for DC $id", \danog\MadelineProto\Logger::WARNING);
|
||||
$this->send_messages($id);
|
||||
}
|
||||
} else {
|
||||
if ($response_result[$id] || $id === $this->settings['connection_settings']['default_dc']) {
|
||||
$this->logger->logger("Polling for updates: got nothing for DC $id", \danog\MadelineProto\Logger::ERROR);
|
||||
|
||||
if ($this->is_http($id) || $this->altervista) {
|
||||
$this->logger->logger("Polling for updates: closing and reopening DC $id since we're on HTTP, and we polled");
|
||||
$this->close_and_reopen($id);
|
||||
$datacenter->last_http_wait = 0;
|
||||
$repeat |= 1;
|
||||
$this->logger->logger("Polling for updates: and now repolling for DC $id");
|
||||
$this->send_messages($id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isset($only_updates[$id])) {
|
||||
if ($only_updates[$id]) {
|
||||
$this->logger->logger("Polling for replies: got only updates for DC $id", \danog\MadelineProto\Logger::WARNING);
|
||||
|
||||
if ($response_result[$id]) {
|
||||
$this->logger->logger("Polling for replies: still pending requests, repolling for DC $id", \danog\MadelineProto\Logger::WARNING);
|
||||
$this->send_messages($id);
|
||||
$repeat |= 1;
|
||||
} else {
|
||||
$this->logger->logger("Polling for replies: got all RPC replies for DC $id", \danog\MadelineProto\Logger::ULTRA_VERBOSE);
|
||||
}
|
||||
} else {
|
||||
if ($response_result[$id]) {
|
||||
$this->logger->logger("Polling for replies: still pending requests, repolling for DC $id", \danog\MadelineProto\Logger::WARNING);
|
||||
$this->send_messages($id);
|
||||
} else {
|
||||
$this->logger->logger("Polling for replies: got all RPC replies for DC $id", \danog\MadelineProto\Logger::NOTICE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($response_result[$id]) {
|
||||
$this->logger->logger("Polling for replies: got nothing for DC $id", \danog\MadelineProto\Logger::ERROR);
|
||||
$this->logger->logger("Polling for replies: closing and reopening DC $id", \danog\MadelineProto\Logger::ERROR);
|
||||
$this->close_and_reopen($id);
|
||||
$datacenter->last_http_wait = 0;
|
||||
$repeat |= 1;
|
||||
$this->logger->logger("Polling for replies: resending for DC $id", \danog\MadelineProto\Logger::WARNING);
|
||||
$this->send_messages($id);
|
||||
} else {
|
||||
$this->logger->logger("Polling for replies: got all RPC replies for DC $id", \danog\MadelineProto\Logger::ULTRA_VERBOSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->logger->logger('Running guzzle promise queue');
|
||||
\GuzzleHttp\Promise\queue()->run();
|
||||
|
||||
if ($repeat) {
|
||||
$this->logger->logger('Repeat iowait');
|
||||
}
|
||||
} while ($repeat);
|
||||
|
||||
if ($canunset) {
|
||||
$this->updates_state['sync_loading'] = false;
|
||||
}
|
||||
if ($canunsetpostponepwrchat) {
|
||||
$this->postpone_pwrchat = false;
|
||||
$this->handle_pending_pwrchat();
|
||||
}
|
||||
if ($canunsetpostponeupdates) {
|
||||
$this->postpone_updates = false;
|
||||
$this->handle_pending_updates();
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
public function has_pending_calls()
|
||||
{
|
||||
$result = [];
|
||||
|
@ -337,9 +337,12 @@ trait ResponseHandler
|
||||
case 500:
|
||||
if ($response['error_message'] === 'MSG_WAIT_FAILED') {
|
||||
$this->datacenter->sockets[$datacenter]->call_queue[$request['queue']] = [];
|
||||
Loop::defer([$this, 'method_recall'], ['message_id' => $request_id, 'datacenter' => $datacenter]);
|
||||
return;
|
||||
}
|
||||
$this->got_response_for_outgoing_message_id($request_id, $datacenter);
|
||||
|
||||
Loop::defer([$this, 'method_recall'], ['message_id' => $request_id, 'datacenter' => $datacenter]);
|
||||
$this->handle_reject($datacenter, $request, new \danog\MadelineProto\RPCErrorException($response['error_message'], $response['error_code']));
|
||||
|
||||
return;
|
||||
case 303:
|
||||
|
@ -55,7 +55,6 @@ trait UpdateHandler
|
||||
return;
|
||||
}
|
||||
$this->updates[$this->updates_key++] = $update;
|
||||
$this->logger->logger('Stored ');
|
||||
}
|
||||
|
||||
public function get_updates_async($params = [])
|
||||
@ -543,7 +542,8 @@ trait UpdateHandler
|
||||
if (isset($this->calls[$update['phone_call']['id']])) {
|
||||
return;
|
||||
}
|
||||
$controller = new \danog\MadelineProto\VoIP(false, $update['phone_call']['admin_id'], ['_' => 'inputPhoneCall', 'id' => $update['phone_call']['id'], 'access_hash' => $update['phone_call']['access_hash']], $this, \danog\MadelineProto\VoIP::CALL_STATE_INCOMING, $update['phone_call']['protocol']);
|
||||
$controller = new \danog\MadelineProto\VoIP(false, $update['phone_call']['admin_id'], $this, \danog\MadelineProto\VoIP::CALL_STATE_INCOMING);
|
||||
$controller->setCall($update['phone_call']);
|
||||
$controller->storage = ['g_a_hash' => $update['phone_call']['g_a_hash']];
|
||||
$update['phone_call'] = $this->calls[$update['phone_call']['id']] = $controller;
|
||||
break;
|
||||
|
@ -51,9 +51,11 @@ trait AuthKeyHandler
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['generating_g_a'], \danog\MadelineProto\Logger::VERBOSE);
|
||||
$g_a = $dh_config['g']->powMod($a, $dh_config['p']);
|
||||
$this->check_G($g_a, $dh_config['p']);
|
||||
$res = $this->method_call('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' => 65]], ['datacenter' => $this->datacenter->curdc]);
|
||||
$this->calls[$res['phone_call']['id']] = $controller = new \danog\MadelineProto\VoIP(true, $user['user_id'], ['_' => 'inputPhoneCall', 'id' => $res['phone_call']['id'], 'access_hash' => $res['phone_call']['access_hash']], $this, \danog\MadelineProto\VoIP::CALL_STATE_REQUESTED, $res['phone_call']['protocol']);
|
||||
$controller = new \danog\MadelineProto\VoIP(true, $user['user_id'], $this, \danog\MadelineProto\VoIP::CALL_STATE_REQUESTED);
|
||||
$controller->storage = ['a' => $a, 'g_a' => str_pad($g_a->toBytes(), 256, chr(0), \STR_PAD_LEFT)];
|
||||
$res = $this->method_call('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' => \danog\MadelineProto\VoIP::getConnectionMaxLayer()]], ['datacenter' => $this->datacenter->curdc]);
|
||||
$controller->setCall($res['phone_call']);
|
||||
$this->calls[$res['phone_call']['id']] = $controller;
|
||||
$this->handle_pending_updates();
|
||||
$this->get_updates_difference();
|
||||
|
||||
@ -83,7 +85,7 @@ trait AuthKeyHandler
|
||||
$this->check_G($g_b, $dh_config['p']);
|
||||
|
||||
try {
|
||||
$res = $this->method_call('phone.acceptCall', ['peer' => $call, 'g_b' => $g_b->toBytes(), 'protocol' => ['_' => 'phoneCallProtocol', 'udp_reflector' => true, 'udp_p2p' => true, 'min_layer' => 65, 'max_layer' => 65]], ['datacenter' => $this->datacenter->curdc]);
|
||||
$res = $this->method_call('phone.acceptCall', ['peer' => $call, 'g_b' => $g_b->toBytes(), 'protocol' => ['_' => 'phoneCallProtocol', 'udp_reflector' => true, 'udp_p2p' => true, 'min_layer' => 65, 'max_layer' => \danog\MadelineProto\VoIP::getConnectionMaxLayer()]], ['datacenter' => $this->datacenter->curdc]);
|
||||
} catch (\danog\MadelineProto\RPCErrorException $e) {
|
||||
if ($e->rpc === 'CALL_ALREADY_ACCEPTED') {
|
||||
$this->logger->logger(sprintf(\danog\MadelineProto\Lang::$current_lang['call_already_accepted'], $call['id']));
|
||||
@ -126,7 +128,7 @@ trait AuthKeyHandler
|
||||
$params['g_b'] = new \phpseclib\Math\BigInteger($params['g_b'], 256);
|
||||
$this->check_G($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);
|
||||
$res = $this->method_call('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' => 65]], ['datacenter' => $this->datacenter->curdc])['phone_call'];
|
||||
$res = $this->method_call('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' => \danog\MadelineProto\VoIP::getConnectionMaxLayer()]], ['datacenter' => $this->datacenter->curdc])['phone_call'];
|
||||
$visualization = [];
|
||||
$length = new \phpseclib\Math\BigInteger(count(\danog\MadelineProto\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) {
|
||||
@ -134,7 +136,7 @@ trait AuthKeyHandler
|
||||
$visualization[] = \danog\MadelineProto\Magic::$emojis[(int) (new \phpseclib\Math\BigInteger($number, 256))->divide($length)[1]->toString()];
|
||||
}
|
||||
$this->calls[$params['id']]->setVisualization($visualization);
|
||||
$this->calls[$params['id']]->configuration['shared_config'] = array_merge($this->method_call('phone.getCallConfig', [], ['datacenter' => $this->datacenter->curdc]), $this->calls[$params['id']]->configuration['shared_config']);
|
||||
|
||||
$this->calls[$params['id']]->configuration['endpoints'] = array_merge([$res['connection']], $res['alternative_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' => \danog\MadelineProto\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' => \danog\MadelineProto\VoIP::NET_TYPE_ETHERNET], $this->calls[$params['id']]->configuration);
|
||||
$this->calls[$params['id']]->parseConfig();
|
||||
@ -177,7 +179,6 @@ trait AuthKeyHandler
|
||||
$visualization[] = \danog\MadelineProto\Magic::$emojis[(int) (new \phpseclib\Math\BigInteger($number, 256))->divide($length)[1]->toString()];
|
||||
}
|
||||
$this->calls[$params['id']]->setVisualization($visualization);
|
||||
$this->calls[$params['id']]->configuration['shared_config'] = array_merge($this->method_call('phone.getCallConfig', [], ['datacenter' => $this->datacenter->curdc]), $this->calls[$params['id']]->configuration['shared_config']);
|
||||
$this->calls[$params['id']]->configuration['endpoints'] = array_merge([$params['connection']], $params['alternative_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' => \danog\MadelineProto\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' => \danog\MadelineProto\VoIP::NET_TYPE_ETHERNET], $this->calls[$params['id']]->configuration);
|
||||
$this->calls[$params['id']]->parseConfig();
|
||||
|
@ -20,6 +20,8 @@
|
||||
namespace danog\MadelineProto\Wrappers;
|
||||
|
||||
use danog\MadelineProto\MTProtoTools\PasswordCalculator;
|
||||
use danog\MadelineProto\VoIPServerConfig;
|
||||
use function Amp\Promise\wait;
|
||||
|
||||
/**
|
||||
* Manages logging in and out.
|
||||
@ -27,6 +29,10 @@ use danog\MadelineProto\MTProtoTools\PasswordCalculator;
|
||||
trait Login
|
||||
{
|
||||
public function logout()
|
||||
{
|
||||
return $this->wait($this->logout_async());
|
||||
}
|
||||
public function logout_async()
|
||||
{
|
||||
foreach ($this->datacenter->sockets as $socket) {
|
||||
$socket->authorized = false;
|
||||
@ -40,41 +46,39 @@ trait Login
|
||||
$this->users = [];
|
||||
$this->state = [];
|
||||
$this->tos = ['expires' => 0, 'accepted' => true];
|
||||
if (!$this->method_call('auth.logOut', [], ['datacenter' => $this->datacenter->curdc])) {
|
||||
throw new \danog\MadelineProto\Exception(\danog\MadelineProto\Lang::$current_lang['logout_error']);
|
||||
}
|
||||
yield $this->method_call_async_read('auth.logOut', [], ['datacenter' => $this->datacenter->curdc]);
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['logout_ok'], \danog\MadelineProto\Logger::NOTICE);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function bot_login($token)
|
||||
public function bot_login_async($token)
|
||||
{
|
||||
if ($this->authorized === self::LOGGED_IN) {
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['already_logged_in'], \danog\MadelineProto\Logger::NOTICE);
|
||||
$this->logout();
|
||||
yield $this->logout_async();
|
||||
}
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['login_bot'], \danog\MadelineProto\Logger::NOTICE);
|
||||
$this->authorization = $this->method_call('auth.importBotAuthorization', ['bot_auth_token' => $token, 'api_id' => $this->settings['app_info']['api_id'], 'api_hash' => $this->settings['app_info']['api_hash']], ['datacenter' => $this->datacenter->curdc]);
|
||||
$this->authorization = yield $this->method_call_async_read('auth.importBotAuthorization', ['bot_auth_token' => $token, 'api_id' => $this->settings['app_info']['api_id'], 'api_hash' => $this->settings['app_info']['api_hash']], ['datacenter' => $this->datacenter->curdc]);
|
||||
$this->authorized = self::LOGGED_IN;
|
||||
$this->authorized_dc = $this->datacenter->curdc;
|
||||
$this->datacenter->sockets[$this->datacenter->curdc]->authorized = true;
|
||||
$this->updates = [];
|
||||
$this->updates_key = 0;
|
||||
$this->init_authorization();
|
||||
yield $this->init_authorization_async();
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['login_ok'], \danog\MadelineProto\Logger::NOTICE);
|
||||
|
||||
return $this->authorization;
|
||||
}
|
||||
|
||||
public function phone_login($number, $sms_type = 5)
|
||||
public function phone_login_async($number, $sms_type = 5)
|
||||
{
|
||||
if ($this->authorized === self::LOGGED_IN) {
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['already_logged_in'], \danog\MadelineProto\Logger::NOTICE);
|
||||
$this->logout();
|
||||
yield $this->logout_async();
|
||||
}
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['login_code_sending'], \danog\MadelineProto\Logger::NOTICE);
|
||||
$this->authorization = $this->method_call('auth.sendCode', ['settings' => ['_' => 'codeSettings'], 'phone_number' => $number, 'sms_type' => $sms_type, 'api_id' => $this->settings['app_info']['api_id'], 'api_hash' => $this->settings['app_info']['api_hash'], 'lang_code' => $this->settings['app_info']['lang_code']], ['datacenter' => $this->datacenter->curdc]);
|
||||
$this->authorization = yield $this->method_call_async_read('auth.sendCode', ['settings' => ['_' => 'codeSettings'], 'phone_number' => $number, 'sms_type' => $sms_type, 'api_id' => $this->settings['app_info']['api_id'], 'api_hash' => $this->settings['app_info']['api_hash'], 'lang_code' => $this->settings['app_info']['lang_code']], ['datacenter' => $this->datacenter->curdc]);
|
||||
$this->authorized_dc = $this->datacenter->curdc;
|
||||
$this->authorization['phone_number'] = $number;
|
||||
//$this->authorization['_'] .= 'MP';
|
||||
@ -86,7 +90,7 @@ trait Login
|
||||
return $this->authorization;
|
||||
}
|
||||
|
||||
public function complete_phone_login($code)
|
||||
public function complete_phone_login_async($code)
|
||||
{
|
||||
if ($this->authorized !== self::WAITING_CODE) {
|
||||
throw new \danog\MadelineProto\Exception(\danog\MadelineProto\Lang::$current_lang['login_code_uncalled']);
|
||||
@ -95,11 +99,11 @@ trait Login
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['login_user'], \danog\MadelineProto\Logger::NOTICE);
|
||||
|
||||
try {
|
||||
$authorization = $this->method_call('auth.signIn', ['phone_number' => $this->authorization['phone_number'], 'phone_code_hash' => $this->authorization['phone_code_hash'], 'phone_code' => (string) $code], ['datacenter' => $this->datacenter->curdc]);
|
||||
$authorization = yield $this->method_call_async_read('auth.signIn', ['phone_number' => $this->authorization['phone_number'], 'phone_code_hash' => $this->authorization['phone_code_hash'], 'phone_code' => (string) $code], ['datacenter' => $this->datacenter->curdc]);
|
||||
} catch (\danog\MadelineProto\RPCErrorException $e) {
|
||||
if ($e->rpc === 'SESSION_PASSWORD_NEEDED') {
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['login_2fa_enabled'], \danog\MadelineProto\Logger::NOTICE);
|
||||
$this->authorization = $this->method_call('account.getPassword', [], ['datacenter' => $this->datacenter->curdc]);
|
||||
$this->authorization = yield $this->method_call_async_read('account.getPassword', [], ['datacenter' => $this->datacenter->curdc]);
|
||||
if (!isset($this->authorization['hint'])) $this->authorization['hint'] = '';
|
||||
$this->authorized = self::WAITING_PASSWORD;
|
||||
|
||||
@ -118,17 +122,19 @@ trait Login
|
||||
$this->authorized = self::LOGGED_IN;
|
||||
$this->authorization = $authorization;
|
||||
$this->datacenter->sockets[$this->datacenter->curdc]->authorized = true;
|
||||
$this->init_authorization();
|
||||
yield $this->init_authorization_async();
|
||||
VoIPServerConfig::updateDefault(yield $this->method_call_async_read('phone.getCallConfig', [], ['datacenter' => $this->datacenter->curdc]));
|
||||
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['login_ok'], \danog\MadelineProto\Logger::NOTICE);
|
||||
|
||||
return $this->authorization;
|
||||
}
|
||||
|
||||
public function import_authorization($authorization)
|
||||
public function import_authorization_async($authorization)
|
||||
{
|
||||
if ($this->authorized === self::LOGGED_IN) {
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['already_logged_in'], \danog\MadelineProto\Logger::NOTICE);
|
||||
$this->logout();
|
||||
yield $this->logout_async();
|
||||
}
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['login_auth_key'], \danog\MadelineProto\Logger::NOTICE);
|
||||
list($dc_id, $auth_key) = $authorization;
|
||||
@ -147,39 +153,42 @@ trait Login
|
||||
$this->datacenter->sockets[$dc_id]->new_incoming = [];
|
||||
$this->datacenter->sockets[$dc_id]->authorized = true;
|
||||
$this->authorized = self::LOGGED_IN;
|
||||
$this->init_authorization();
|
||||
yield $this->init_authorization_async();
|
||||
VoIPServerConfig::updateDefault(yield $this->method_call_async_read('phone.getCallConfig', [], ['datacenter' => $this->datacenter->curdc]));
|
||||
|
||||
return $this->get_self();
|
||||
return yield $this->get_self_async();
|
||||
}
|
||||
|
||||
public function export_authorization()
|
||||
public function export_authorization_async()
|
||||
{
|
||||
if ($this->authorized !== self::LOGGED_IN) {
|
||||
throw new \danog\MadelineProto\Exception(\danog\MadelineProto\Lang::$current_lang['not_logged_in']);
|
||||
}
|
||||
$this->get_self();
|
||||
yield $this->get_self_async();
|
||||
$this->authorized_dc = $this->datacenter->curdc;
|
||||
|
||||
return [$this->datacenter->curdc, $this->datacenter->sockets[$this->datacenter->curdc]->auth_key['auth_key']];
|
||||
}
|
||||
|
||||
public function complete_signup($first_name, $last_name)
|
||||
public function complete_signup_async($first_name, $last_name)
|
||||
{
|
||||
if ($this->authorized !== self::WAITING_SIGNUP) {
|
||||
throw new \danog\MadelineProto\Exception(\danog\MadelineProto\Lang::$current_lang['signup_uncalled']);
|
||||
}
|
||||
$this->authorized = self::NOT_LOGGED_IN;
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['signing_up'], \danog\MadelineProto\Logger::NOTICE);
|
||||
$this->authorization = $this->method_call('auth.signUp', ['phone_number' => $this->authorization['phone_number'], 'phone_code_hash' => $this->authorization['phone_code_hash'], 'phone_code' => $this->authorization['phone_code'], 'first_name' => $first_name, 'last_name' => $last_name], ['datacenter' => $this->datacenter->curdc]);
|
||||
$this->authorization = yield $this->method_call_async_read('auth.signUp', ['phone_number' => $this->authorization['phone_number'], 'phone_code_hash' => $this->authorization['phone_code_hash'], 'phone_code' => $this->authorization['phone_code'], 'first_name' => $first_name, 'last_name' => $last_name], ['datacenter' => $this->datacenter->curdc]);
|
||||
$this->authorized = self::LOGGED_IN;
|
||||
$this->datacenter->sockets[$this->datacenter->curdc]->authorized = true;
|
||||
$this->init_authorization();
|
||||
yield $this->init_authorization_async();
|
||||
VoIPServerConfig::updateDefault(yield $this->method_call_async_read('phone.getCallConfig', [], ['datacenter' => $this->datacenter->curdc]));
|
||||
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['signup_ok'], \danog\MadelineProto\Logger::NOTICE);
|
||||
|
||||
return $this->authorization;
|
||||
}
|
||||
|
||||
public function complete_2fa_login($password)
|
||||
public function complete_2fa_login_async($password)
|
||||
{
|
||||
if ($this->authorized !== self::WAITING_PASSWORD) {
|
||||
throw new \danog\MadelineProto\Exception(\danog\MadelineProto\Lang::$current_lang['2fa_uncalled']);
|
||||
@ -188,20 +197,21 @@ trait Login
|
||||
$hasher = new PasswordCalculator($this->logger);
|
||||
$hasher->addInfo($this->authorization);
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['login_user'], \danog\MadelineProto\Logger::NOTICE);
|
||||
$this->authorization = $this->method_call('auth.checkPassword', ['password' => $hasher->getCheckPassword($password)], ['datacenter' => $this->datacenter->curdc]);
|
||||
$this->authorization = yield $this->method_call_async_read('auth.checkPassword', ['password' => $hasher->getCheckPassword($password)], ['datacenter' => $this->datacenter->curdc]);
|
||||
$this->authorized = self::LOGGED_IN;
|
||||
$this->datacenter->sockets[$this->datacenter->curdc]->authorized = true;
|
||||
$this->init_authorization();
|
||||
yield $this->init_authorization_async();
|
||||
$this->logger->logger(\danog\MadelineProto\Lang::$current_lang['login_ok'], \danog\MadelineProto\Logger::NOTICE);
|
||||
VoIPServerConfig::updateDefault(yield $this->method_call_async_read('phone.getCallConfig', [], ['datacenter' => $this->datacenter->curdc]));
|
||||
|
||||
return $this->authorization;
|
||||
}
|
||||
|
||||
public function update_2fa(array $params): bool
|
||||
public function update_2fa_async(array $params)
|
||||
{
|
||||
$hasher = new PasswordCalculator($this->logger);
|
||||
$hasher->addInfo($this->method_call('account.getPassword', [], ['datacenter' => $this->datacenter->curdc]));
|
||||
$hasher->addInfo(yield $this->method_call_async_read('account.getPassword', [], ['datacenter' => $this->datacenter->curdc]));
|
||||
|
||||
return $this->method_call('account.updatePasswordSettings', $hasher->getPassword($params), ['datacenter' => $this->datacenter->curdc]);
|
||||
return yield $this->method_call_async_read('account.updatePasswordSettings', $hasher->getPassword($params), ['datacenter' => $this->datacenter->curdc]);
|
||||
}
|
||||
}
|
||||
|
@ -102,7 +102,6 @@ trait Loop
|
||||
$this->datacenter->sockets[$this->settings['connection_settings']['default_dc']]->updater->start();
|
||||
|
||||
$this->logger->logger('Started update loop', \danog\MadelineProto\Logger::NOTICE);
|
||||
$offset = 0;
|
||||
|
||||
while (true) {
|
||||
foreach ($this->updates as $update) {
|
||||
|
Loading…
Reference in New Issue
Block a user