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

RFC Plugin - New endpoint: 'Voting' will list all voters for a given RFC id. #162

Merged
merged 12 commits into from Feb 17, 2017
Merged
6 changes: 6 additions & 0 deletions src/Chat/Client/Actions/PinOrUnpinMessageAction.php
Expand Up @@ -14,6 +14,12 @@ public function processResponse($response, int $attempt): int
return self::SUCCESS;
}

if($response === 'Only a room-owner can pin messages') {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// TODO @DaveRandom Suggests checking the response code, or to see if there's any other way Jeeves can identify a failure other than direct string comparison.

$errStr = 'Jeeves cannot pin this message because it is not an owner of this room.';
$this->logger->log(Level::ERROR, $errStr, $response);
$this->fail(new MessageEditFailureException($errStr));
}

$errStr = 'A JSON response that I don\'t understand was received';
$this->logger->log(Level::ERROR, $errStr, $response);
$this->fail(new MessageEditFailureException($errStr));
Expand Down
3 changes: 3 additions & 0 deletions src/Chat/Client/Chars.php
Expand Up @@ -7,4 +7,7 @@ class Chars
const ELLIPSIS = "\xE2\x80\xA6";
const BULLET = "\xE2\x80\xA2";
const RIGHTWARDS_ARROW = "\xE2\x86\x92";
const ZWNJ = "\xE2\x80\x8C";
const EM_SPACE = "\xE2\x80\x83";
const WHITE_BULLET = "\xE2\x97\xA6";
}
209 changes: 167 additions & 42 deletions src/Plugins/RFC.php
Expand Up @@ -5,6 +5,8 @@
use Amp\Artax\Request as HttpRequest;
use Amp\Artax\Response as HttpResponse;
use Amp\Promise;
use Amp\Success;
use Room11\Jeeves\Chat\Client\Chars;
use Room11\Jeeves\Chat\Client\ChatClient;
use Room11\Jeeves\Chat\Entities\PostedMessage;
use Room11\Jeeves\Chat\Message\Command;
Expand All @@ -22,7 +24,6 @@ class RFC extends BasePlugin
private $pluginData;

const BASE_URI = 'https://wiki.php.net/rfc';
const BULLET = "\xE2\x80\xA2";

public function __construct(ChatClient $chatClient, HttpClient $httpClient, KeyValueStore $pluginData)
{
Expand Down Expand Up @@ -82,11 +83,16 @@ public function search(Command $command)
}

if (empty($rfcsInVoting)) {
return all([
$this->clearLastPinId($room),
$this->chatClient->postMessage($command, "Sorry, but we can't have nice things."),
$this->unpinPreviousMessage($room, $pinInfoPromise),
]);
yield $this->chatClient->postMessage($command, "Sorry, but we can't have nice things.");

if (yield $this->chatClient->isBotUserRoomOwner($room)) {
return all([
$this->clearLastPinId($room),
$this->unpinPreviousMessage($room, $pinInfoPromise),
]);
}

return new Success();
}

/** @var PostedMessage $postedMessage */
Expand All @@ -98,10 +104,14 @@ public function search(Command $command)
)
);

return all([
$this->unpinPreviousMessage($room, $pinInfoPromise),
$this->pinCurrentMessage($room, $postedMessage),
]);
if (yield $this->chatClient->isBotUserRoomOwner($room)) {
return all([
$this->unpinPreviousMessage($room, $pinInfoPromise),
$this->pinCurrentMessage($room, $postedMessage),
]);
}

return new Success();
}

private function pinCurrentMessage(ChatRoom $room, PostedMessage $message): Promise
Expand All @@ -114,7 +124,7 @@ private function pinCurrentMessage(ChatRoom $room, PostedMessage $message): Prom

private function getLastPinId(ChatRoom $room): Promise
{
return resolve(function() use($room) {
return resolve(function () use ($room) {
return (yield $this->pluginData->exists('lastpinid', $room))
? yield $this->pluginData->get('lastpinid', $room)
: -1;
Expand All @@ -123,7 +133,7 @@ private function getLastPinId(ChatRoom $room): Promise

private function clearLastPinId(ChatRoom $room): Promise
{
return resolve(function() use($room) {
return resolve(function () use ($room) {
if (yield $this->pluginData->exists('lastpinid', $room)) {
yield $this->pluginData->unset('lastpinid', $room);
}
Expand All @@ -132,7 +142,7 @@ private function clearLastPinId(ChatRoom $room): Promise

private function unpinPreviousMessage(ChatRoom $room, Promise $pinInfoPromise): Promise
{
return resolve(function() use($room, $pinInfoPromise) {
return resolve(function () use ($room, $pinInfoPromise) {
list($pinnedMessages, $lastPinId) = yield $pinInfoPromise;

if (in_array($lastPinId, $pinnedMessages)) {
Expand All @@ -144,9 +154,10 @@ private function unpinPreviousMessage(ChatRoom $room, Promise $pinInfoPromise):
public function getRFC(Command $command)
{
$rfc = $command->getParameter(0);

if ($rfc === null) {
// e.g.: !!rfc pipe-operator
return $this->chatClient->postMessage($command, "RFC id required");
return $this->chatClient->postMessage($command, "Usage: `!!rfc <id>`");
}

$uri = self::BASE_URI . '/' . urlencode($rfc);
Expand All @@ -158,28 +169,10 @@ public function getRFC(Command $command)
return $this->chatClient->postMessage($command, "Nope, we can't have nice things.");
}

$votes = self::parseVotes($response->getBody());
if (empty($votes)) {
return $this->chatClient->postMessage($command, "No votes found");
}

$messages = [];

foreach ($votes as $id => $vote) {
$breakdown = [];
$messages = $this->prepareVotes($response->getBody(), $uri);

$total = array_sum($vote['votes']);
if ($total > 0) {
foreach ($vote['votes'] as $option => $value) {
$breakdown[] = sprintf("%s (%d: %d%%)", $option, $value, 100 * $value / $total);
}
}

$messages[] = [
'name' => $vote['name'],
'href' => $uri . '#' . $id,
'breakdown' => implode(', ', $breakdown),
];
if (count($messages) === 0) {
return $this->chatClient->postMessage($command, "No votes found.");
}

if (count($messages) === 1) {
Expand All @@ -194,10 +187,10 @@ public function getRFC(Command $command)
);
}

$message = implode("\n", array_map(function($message) {
$message = implode("\n", array_map(function ($message) {
return sprintf(
'%s %s - %s (%s)',
self::BULLET,
Chars::BULLET,
$message['name'],
$message['breakdown'],
$message['href']
Expand All @@ -207,7 +200,96 @@ public function getRFC(Command $command)
return $this->chatClient->postMessage($command, $message);
}

private static function parseVotes(string $html) {
public function getRFCVotes(Command $command)
{
$rfc = $command->getParameter(0);

if ($rfc === null) {
return $this->chatClient->postMessage($command, "Usage: `!!voting <id>`");
}

$uri = self::BASE_URI . '/' . urlencode($rfc);

/** @var HttpResponse $response */
$response = yield $this->httpClient->request($uri);

if ($response->getStatus() !== 200) {
return $this->chatClient->postMessage($command, "Nope, we can't have nice things.");
}

$messages = $this->prepareVotes($response->getBody(), $uri);

if (count($messages) === 0) {
return $this->chatClient->postMessage($command, "No votes found.");
}

return $this->chatClient->postMessage($command, implode("\n", array_map(function ($message) {

$formattedVotes = implode("\n", array_map(function ($option) use ($message) {

$voters = implode(
', ',
array_column(
array_filter($message['voters'], function ($voter) use ($option) {
return $voter['voted'] && $voter['choice'] === $option;
}),
'name'
)
);

return sprintf(
'%s %s: %s',
Chars::ZWNJ . Chars::EM_SPACE . Chars::WHITE_BULLET,
$option,
$voters
);
}, $message['options']));

return sprintf(
"%s %s - %s (%s)\n%s",
Chars::BULLET,
$message['name'],
$message['breakdown'],
$message['href'],
$formattedVotes
);
}, $messages)));
}

private function prepareVotes(string $rfcData, string $uri): array
{

$votes = self::parseVotes($rfcData);
if (empty($votes)) {
return [];
}

$messages = [];

foreach ($votes as $id => $vote) {
$breakdown = [];

$total = array_sum($vote['votes']);
if ($total > 0) {
foreach ($vote['votes'] as $option => $value) {
$breakdown[] = sprintf("%s (%d: %d%%)", $option, $value, 100 * $value / $total);
}
}

$messages[] = [
'name' => $vote['name'],
'href' => $uri . '#' . $id,
'breakdown' => implode(', ', $breakdown),
'options' => array_keys($vote['votes']),
'voters' => $vote['voters']
];
}

return $messages;
}

private static function parseVotes(string $html)
{
$dom = domdocument_load_html($html);
$votes = [];

Expand All @@ -221,6 +303,7 @@ private static function parseVotes(string $html) {
$info = [
'name' => $id,
'votes' => [],
'voters' => []
];
$options = [];

Expand Down Expand Up @@ -251,13 +334,39 @@ private static function parseVotes(string $html) {
continue;
}

$voterColumns = $row->getElementsByTagName('td');

if ($voterColumns->length === 0) {
continue;
}

$voter = [
'name' => '',
'voted' => false,
'choice' => null,
'at' => null
];

/** @var \DOMElement $vote */
foreach ($row->getElementsByTagName('td') as $i => $vote) {
foreach ($voterColumns as $i => $vote) {
if ($i === 0) {
$voter['name'] = $vote->getElementsByTagName('a')[0]->nodeValue;
}

// Adjust by one to ignore voter name
if ($vote->getElementsByTagName('img')->length > 0) {
$voteImageCollection = $vote->getElementsByTagName('img');

if ($voteImageCollection->length > 0) {
/** @var \DOMElement $voteImage */
$voteImage = $voteImageCollection[0];
++$info['votes'][$options[$i - 1]];
$voter['voted'] = true;
$voter['choice'] = $options[$i - 1];
$voter['at'] = $voteImage->getAttribute('title') ?: null;
}
}

$info['voters'][] = $voter;
}

$votes[$id] = $info;
Expand All @@ -282,8 +391,24 @@ public function getDescription(): string
public function getCommandEndpoints(): array
{
return [
new PluginCommandEndpoint('Search', [$this, 'search'], 'rfcs', 'List RFCs currently in voting, or get the current vote status of a given RFC'),
new PluginCommandEndpoint('Votes', [$this, 'getRFC'], 'rfc', 'Get the current vote status of a given RFC'),
new PluginCommandEndpoint(
'Search',
[$this, 'search'],
'rfcs',
'List RFCs currently in voting, or get the current vote status of a given RFC'
),
new PluginCommandEndpoint(
'Votes',
[$this, 'getRFC'],
'rfc',
'Get the current vote status of a given RFC'
),
new PluginCommandEndpoint(
'Voting',
[$this, 'getRFCVotes'],
'voting',
'List all voters of a given RFC.'
),
];
}
}
8 changes: 4 additions & 4 deletions tests/src/Chat/BuiltInCommandManagerTest.php
Expand Up @@ -41,8 +41,8 @@ public function testRegisterLogs()
->expects($this->exactly(2))
->method('log')
->withConsecutive(
[Level::DEBUG, 'Registering command name \'foo\' with built in command ' . get_class($command)],
[Level::DEBUG, 'Registering command name \'bar\' with built in command ' . get_class($command)]
[Level::DEBUG, 'Registered command name \'foo\' with built in command ' . get_class($command)],
[Level::DEBUG, 'Registered command name \'bar\' with built in command ' . get_class($command)]
)
;

Expand Down Expand Up @@ -141,7 +141,7 @@ public function testHandleCommandWhenBanned()
->expects($this->exactly(2))
->method('log')
->withConsecutive(
[Level::DEBUG, 'Registering command name \'foo\' with built in command ' . get_class($command)],
[Level::DEBUG, 'Registered command name \'foo\' with built in command ' . get_class($command)],
[Level::DEBUG, 'User #14 is banned, ignoring event #721 for built in commands']
)
;
Expand Down Expand Up @@ -256,7 +256,7 @@ public function testHandleCommandWhenMatches()
->expects($this->exactly(2))
->method('log')
->withConsecutive(
[Level::DEBUG, 'Registering command name \'foo\' with built in command ' . get_class($registeredCommand)],
[Level::DEBUG, 'Registered command name \'foo\' with built in command ' . get_class($registeredCommand)],
[Level::DEBUG, 'Passing event #721 to built in command handler ' . get_class($registeredCommand)]
)
;
Expand Down