Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change IrcUsersAndChannels structure to its logic represenation #49

Merged
merged 1 commit into from
Mar 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,11 @@ There are only few noticable exceptions to this rule:
* 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.
* The initial `Network` synchronization uses different structures for the
`IrcUsersAndChannels` format structures depending on which wire-protocol is
used. This library always exposes this structure in its simpler "logic" form
for consistency reasons. This means it always contains the keys `Users` and
`Channels` which both contain a list of objects decribing each element.

This combined basically means that you should always get consistent `data`
events for both the legacy protocol and the newer datastream protocol.
Expand Down
10 changes: 5 additions & 5 deletions examples/01-channels.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,16 @@
if (isset($message[0]) && $message[0] === Protocol::REQUEST_INITDATA && $message[1] === 'Network') {
// print network information except for huge users/channels list
$info = clone $message[3];
unset($info->IrcUsersAndChannels);
//unset($info->IrcUsersAndChannels);
echo json_encode($info, JSON_PRETTY_PRINT) . PHP_EOL;

// print names of all known channels on this network (if connected)
if (isset($message[3]->IrcUsersAndChannels->Channels)) {
foreach ($message[3]->IrcUsersAndChannels->Channels->name as $name) {
echo $name . PHP_EOL;
if ($message[3]->IrcUsersAndChannels->Channels) {
foreach ($message[3]->IrcUsersAndChannels->Channels as $channel) {
echo $channel->name . PHP_EOL;
}
} else {
echo 'No channels in ' . $message[3]->networkName . ' (disconnected)' . PHP_EOL;
echo 'No channels in ' . $message[3]->networkName . PHP_EOL;
}

// close connection after showing all networks
Expand Down
42 changes: 36 additions & 6 deletions src/Io/DatastreamProtocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,54 @@ public function parseVariantPacket($packet)
$reader = new Reader($packet, $this->userTypeReader);

// datastream protocol always uses list contents (even for maps)
$value = $reader->readQVariantList();
$data = $reader->readQVariantList();

// if the first element is a string, then this is actually a map transported as a list
// actual lists will always start with an integer request type
if (is_string($value[0])) {
if (is_string($data[0])) {
// datastream protocol uses lists with UTF-8 keys
// https://github.com/quassel/quassel/blob/master/src/common/protocols/datastream/datastreampeer.cpp#L109
return $this->listToMap($value);
return $this->listToMap($data);
}

if ($value[0] === self::REQUEST_INITDATA) {
if ($data[0] === self::REQUEST_INITDATA) {
// make sure InitData is in line with legacy protocol wire format
// first 3 elements are unchanged, everything else should be a map
// https://github.com/quassel/quassel/blob/master/src/common/protocols/datastream/datastreampeer.cpp#L383
return array_slice($value, 0, 3) + array(3 => $this->listToMap(array_slice($value, 3)));
$data = array_slice($data, 0, 3) + array(3 => $this->listToMap(array_slice($data, 3)));
}

return $value;
// Don't downcast newer datagram InitData for "Network" to older legacy variant.
// The datastream protocol uses a much more network-efficient wire-protocol
// which avoids repeating the same keys over and over again, but this
// format is very hard to work with from a consumer's perspective.
// Instead, we use a "logic" representation of the data inspired by the legacy protocol:
// The "IrcUsersAndChannels" structure always contains the keys "Users" and "Channels"
// both keys always consist of a list of objects with additional details.
// https://github.com/quassel/quassel/commit/208ccb6d91ebb3c26a67c35c11411ba3ab27708a#diff-c3c5a4e63a0b757912ba28686747b040
if (is_array($data) && isset($data[0]) && $data[0] === self::REQUEST_INITDATA && $data[1] === 'Network' && isset($data[3]->IrcUsersAndChannels)) {
$new = (object)array(
'Users' => array(),
'Channels' => array()
);
foreach ($data[3]->IrcUsersAndChannels as $type => $all) {
// each type is logically represented by a list of objects
// initialize with empty list even if no records are found at all
$list = array();
foreach ($all as $key => $values) {
foreach ($values as $i => $value) {
if (!isset($list[$i])) {
$list[$i] = new \stdClass();
}
$list[$i]->$key = $value;
}
}
$new->$type = $list;
}
$data[3]->IrcUsersAndChannels = $new;
}

return $data;
}

/**
Expand Down
30 changes: 10 additions & 20 deletions src/Io/LegacyProtocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,28 +43,18 @@ public function parseVariantPacket($packet)
$data[1]->modify($data[1]->getOffset() . ' seconds');
}

// upcast legacy InitData for "Network" to newer datagram variant
// Don't upcast legacy InitData for "Network" to newer datagram variant.
// The legacy protocol uses a rather inefficient format which repeats the
// same keys for each and every object, but it's "logic" and easy to work with.
// We use a similar "logic" representation on the data:
// The "IrcUsersAndChannels" structure always contains the keys "Users" and "Channels"
// both keys always consist of a list of objects with additional details.
// https://github.com/quassel/quassel/commit/208ccb6d91ebb3c26a67c35c11411ba3ab27708a#diff-c3c5a4e63a0b757912ba28686747b040
if (is_array($data) && 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)] = (object)$map;
}

// make sure new structure comes first
$data[3] = (object)(array('IrcUsersAndChannels' => (object)$new) + (array)$data[3]);
$data[3]->IrcUsersAndChannels = (object)array(
'Users' => array_values((array)$data[3]->IrcUsersAndChannels->users),
'Channels' => array_values((array)$data[3]->IrcUsersAndChannels->channels)
);
}

return $data;
Expand Down
72 changes: 72 additions & 0 deletions tests/Io/DatastreamProtocolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,76 @@ public function testReceiveHeartBeatRequestWithCorrectTimeZone()
$this->assertEquals(Protocol::REQUEST_HEARTBEAT, $values[0]);
$this->assertEquals(new \DateTime('2016-09-24 16:20:00.123', new \DateTimeZone('Europe/Berlin')), $values[1]);
}

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

$packet = (string)$writer;

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

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

$expected = (object)array(
'a' => 1,
'b' => 2
);
$this->assertEquals($expected, $values[3]);
}

public function testInitDataNetworkUpcastedToLogicFormat()
{
$writer = new Writer();
$writer->writeQVariantList(array(
Protocol::REQUEST_INITDATA,
'Network',
'1',
'IrcUsersAndChannels',
array(
'Users' => array(
'nick' => array(
'a',
'b'
),
'here' => array(
true,
false
)
)
)
));

$packet = (string)$writer;

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

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

$expected = (object)array(
'Users' => array(
(object)array(
'nick' => 'a',
'here' => true
),
(object)array(
'nick' => 'b',
'here' => false
)
),
'Channels' => array()
);
$this->assertEquals($expected, $values[3]->IrcUsersAndChannels);
}
}
78 changes: 54 additions & 24 deletions tests/Io/LegacyProtocolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public function testIsLegacy()
$this->assertTrue($this->protocol->isLegacy());
}

public function testInitDataNetworkUnchangedWithEmptyUsersAndChannels()
public function testInitDataNetworkUnchangedWithoutUsersAndChannels()
{
$writer = new Writer();
$writer->writeQVariant(array(
Expand All @@ -26,7 +26,6 @@ public function testInitDataNetworkUnchangedWithEmptyUsersAndChannels()
'1',
$data = array(
'a' => 1,
'IrcUsersAndChannels' => (object)array(),
'b' => 2,
)
));
Expand All @@ -36,11 +35,11 @@ public function testInitDataNetworkUnchangedWithEmptyUsersAndChannels()
$values = $this->protocol->parseVariantPacket($packet);

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

public function testInitDataNetworkUpcastToNewDatastreamFormat()
public function testInitDataNetworkUpcastedToLogicFormatFromEmptyUsersAndChannels()
{
$writer = new Writer();
$writer->writeQVariant(array(
Expand All @@ -49,17 +48,9 @@ public function testInitDataNetworkUpcastToNewDatastreamFormat()
'1',
array(
'a' => 1,
'IrcUsersAndChannels' => array(
'users' => array(
'anything1' => array(
'nick' => 'a',
'here' => true
),
'anything2' => array(
'nick' => 'b',
'here' => false
)
)
'IrcUsersAndChannels' => (object)array(
'users' => (object)array(),
'channels' => (object)array()
),
'b' => 2,
)
Expand All @@ -69,20 +60,59 @@ public function testInitDataNetworkUpcastToNewDatastreamFormat()

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

$data = array(
'Users' => array(),
'Channels' => array()
);

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

$expected = (object)array('Users' => (object)array(
'nick' => array(
'a',
'b'
),
'here' => array(
true,
false
public function testInitDataNetworkUpcastedToLogicFormatWithoutKeysFromUsersMap()
{
$writer = new Writer();
$writer->writeQVariant(array(
Protocol::REQUEST_INITDATA,
'Network',
'1',
array(
'a' => 1,
'IrcUsersAndChannels' => (object)array(
'users' => (object)array(
'a' => (object)array(
'nick' => 'a'
),
'b' => (object)array(
'nick' => 'b'
)
),
'channels' => (object)array()
),
'b' => 2,
)
));
$this->assertEquals($expected, $values[3]->IrcUsersAndChannels);

$packet = (string)$writer;

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

$data = array(
'Users' => array(
(object)array(
'nick' => 'a'
),
(object)array(
'nick' => 'b'
)
),
'Channels' => array()
);

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

/**
Expand Down