Skip to content

Commit

Permalink
Merge pull request #27 from clue-labs/upcast-users-channels
Browse files Browse the repository at this point in the history
Upcast legacy Network sync model to newer datastream protocol variant
  • Loading branch information
clue authored Sep 29, 2017
2 parents 43810df + 5eec7de commit 60351fa
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 0 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,30 @@ $client->on('close', function () {
});
```

The `data` event will be forwarded with the PHP representation of whatever the
remote Quassel IRC core sent to this client.
From a consumer perspective this looks very similar to a parsed JSON structure,
but this actually uses a binary wire format under the hood.
This library exposes this parsed structure as-is and does usually not change
anything about it.

There are only few noticable exceptions to this rule:

* Incoming chat messages use a plain Unix timestamp integers, while all other
`data` events usually use `DateTime` objects.
This library always converts this to `DateTime` for consistency reasons.
* The legacy protocol uses plain times for heartbeat messages while the newer
datastream protocol uses `DateTime` objects.
This library always converts this to `DateTime` for consistency reasons.
* The legacy protocol uses excessive map structures for initial "Network"
synchronization, while the newer datastream protocol users optimized list
structures to avoid repeatedly sending the same keys.
This library always exposes the legacy protocol format in the same way as
the newer datastream protocol for consistency reasons.

This combined basically means that you should always get consistent `data`
events for both the legacy protocol and the newer datastream protocol.

#### close()

The `close()` method can be used to force-close the Quassel connection immediately.
Expand Down
108 changes: 108 additions & 0 deletions examples/channels.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

use Clue\React\Quassel\Factory;
use Clue\React\Quassel\Client;
use Clue\React\Quassel\Io\Protocol;

require __DIR__ . '/../vendor/autoload.php';
$host = '127.0.0.1';
$user = array();
if (isset($argv[1])) { $host = $argv[1]; }

echo 'Server: ' . $host . PHP_EOL;

echo 'User name: ';
$user['name'] = trim(fgets(STDIN));

echo 'Password: ';
$user['password'] = trim(fgets(STDIN));

$loop = \React\EventLoop\Factory::create();
$factory = new Factory($loop);

$factory->createClient($host)->then(function (Client $client) use ($loop, $user) {
var_dump('CONNECTED');

$await = array();

$client->on('data', function ($message) use ($client, $user, &$await) {
$type = null;
if (is_array($message) && isset($message['MsgType'])) {
$type = $message['MsgType'];
}

if ($type === 'ClientInitAck') {
if (!$message['Configured']) {
var_dump('core not configured yet');
$client->close();
return;
}
var_dump('core ready, now logging in');
$client->writeClientLogin($user['name'], $user['password']);

return;
}
if ($type === 'ClientLoginReject') {
var_dump('Unable to login', $message['Error']);

var_dump('Now closing connection');
$client->close();

return;
}
if ($type === 'ClientLoginAck') {
var_dump('successfully logged in, now waiting for a SessionInit message');

return;
}
if ($type === 'SessionInit') {
var_dump('session initialized');

foreach ($message['SessionState']['NetworkIds'] as $nid) {
var_dump('requesting Network for ' . $nid . ', this may take a few seconds');
$client->writeInitRequest("Network", $nid);

$await[$nid] = true;
}

if ($await) {
var_dump('initialization completed, now waiting for network information');
} else {
var_dump('no networks found');
$client->close();
}

return;
}

// network information received
if (isset($message[0]) && $message[0] === Protocol::REQUEST_INITDATA && $message[1] === 'Network') {
// print network information except for huge users/channels list
$info = $message[3];
unset($info['IrcUsersAndChannels']);
echo json_encode($info, JSON_PRETTY_PRINT) . PHP_EOL;

// print names of all known channels on this network
foreach ($message[3]['IrcUsersAndChannels']['Channels']['name'] as $name) {
echo $name . PHP_EOL;
}

// close connection after showing all networks
$id = $message[2];
unset($await[$id]);
if (!$await) {
var_dump('network information received');
$client->close();
}
return;
}

echo 'received unhandled: ' . json_encode($message, JSON_PRETTY_PRINT) . PHP_EOL;
});

$client->writeClientInit();
})->then(null, function ($e) {
echo $e;
});

$loop->run();
24 changes: 24 additions & 0 deletions src/Io/LegacyProtocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,30 @@ public function parseVariantPacket($packet)
$data[1]->modify($data[1]->getOffset() . ' seconds');
}

// upcast legacy InitData for "Network" to newer datagram variant
// https://github.com/quassel/quassel/commit/208ccb6d91ebb3c26a67c35c11411ba3ab27708a#diff-c3c5a4e63a0b757912ba28686747b040
if (isset($data[0]) && $data[0] === self::REQUEST_INITDATA && $data[1] === 'Network' && isset($data[3]['IrcUsersAndChannels'])) {
$new = array();
// $type would be "users" and "channels"
foreach ($data[3]['IrcUsersAndChannels'] as $type => $all) {
$map = array();

// iterate over all users/channels
foreach ($all as $one) {
// iterate over all keys/values for this user/channel
foreach ($one as $key => $value) {
$map[$key][] = $value;
}
}

// store new map with uppercase Users/Channels
$new[ucfirst($type)] = $map;
}

// make sure new structure comes first
$data[3] = array('IrcUsersAndChannels' => $new) + $data[3];
}

return $data;
}
}
68 changes: 68 additions & 0 deletions tests/Io/LegacyProtocolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,74 @@ public function testIsLegacy()
$this->assertTrue($this->protocol->isLegacy());
}

public function testInitDataNetworkUnchangedWithEmptyUsersAndChannels()
{
$writer = new Writer();
$writer->writeQVariant(array(
Protocol::REQUEST_INITDATA,
'Network',
'1',
$data = array(
'a' => 1,
'IrcUsersAndChannels' => array(),
'b' => 2,
)
));

$packet = (string)$writer;

$values = $this->protocol->parseVariantPacket($packet);

$this->assertCount(4, $values);
$this->assertCount(3, $values[3]);
$this->assertEquals($data, $values[3]);
}

public function testInitDataNetworkUpcastToNewDatastreamFormat()
{
$writer = new Writer();
$writer->writeQVariant(array(
Protocol::REQUEST_INITDATA,
'Network',
'1',
array(
'a' => 1,
'IrcUsersAndChannels' => array(
'users' => array(
'anything1' => array(
'nick' => 'a',
'here' => true
),
'anything2' => array(
'nick' => 'b',
'here' => false
)
)
),
'b' => 2,
)
));

$packet = (string)$writer;

$values = $this->protocol->parseVariantPacket($packet);

$this->assertCount(4, $values);
$this->assertCount(3, $values[3]);

$expected = array('Users' => array(
'nick' => array(
'a',
'b'
),
'here' => array(
true,
false
)
));
$this->assertEquals($expected, $values[3]['IrcUsersAndChannels']);
}

/**
* The legacy protocol uses QTime which only transports time of the day and
* not the actual day information. This means that reading in a QTime will
Expand Down

0 comments on commit 60351fa

Please sign in to comment.