1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-30 17:30:59 +01:00

Very basic IRC Bot

Summary:
This is sort of a silly/fun project but I think there's some utility. For
example, mroch added some handlers to an eggdrop or something similar to look
for "D12345" and print out the title/link, which was actually pretty useful.

We could also add logging here and subsume the more-or-less unowned Facebook
tool that does the same thing, especially since we can get a bunch of good stuff
it doesn't support (like search) more or less for free.

This is also an easy way to provide some example code for writing Conduit system
agents.

This is a minimal implementation which creates a bot that connects to a
hard-coded server and sits there indefinitely. Next steps:

  - Add conduit/sysagent support
  - Write differential/maniphest/diffusion handlers
  - Move configuration to the web interface (?) and integrate with phd
  - Write a logging handler?

Test Plan:
Ran bot with "exec_daemon.php", it connected to the hard-coded server and sat
there indefinitely.

Reviewed By: aran
Reviewers: codeblock, mroch, tuomaspelkonen, aran, jungejason
CC: aran, epriestley
Differential Revision: 283
This commit is contained in:
epriestley 2011-05-15 09:29:48 -07:00
parent 20892b0bc2
commit 3fdc115b54
11 changed files with 376 additions and 1 deletions

View file

@ -0,0 +1,11 @@
{
"server" : "irc.freenode.net",
"port" : 6667,
"nick" : "phabot",
"join" : [
"#phabot-test"
],
"handlers" : [
"PhabricatorIRCProtocolHandler"
]
}

View file

@ -324,6 +324,10 @@ phutil_register_library_map(array(
'PhabricatorFileViewController' => 'applications/files/controller/view', 'PhabricatorFileViewController' => 'applications/files/controller/view',
'PhabricatorGoodForNothingWorker' => 'infrastructure/daemon/workers/worker/goodfornothing', 'PhabricatorGoodForNothingWorker' => 'infrastructure/daemon/workers/worker/goodfornothing',
'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/selector', 'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/selector',
'PhabricatorIRCBot' => 'infrastructure/daemon/irc/bot',
'PhabricatorIRCHandler' => 'infrastructure/daemon/irc/handler/base',
'PhabricatorIRCMessage' => 'infrastructure/daemon/irc/message',
'PhabricatorIRCProtocolHandler' => 'infrastructure/daemon/irc/handler/protocol',
'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/javelin', 'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/javelin',
'PhabricatorLintEngine' => 'infrastructure/lint/engine', 'PhabricatorLintEngine' => 'infrastructure/lint/engine',
'PhabricatorLiskDAO' => 'applications/base/storage/lisk', 'PhabricatorLiskDAO' => 'applications/base/storage/lisk',
@ -748,6 +752,8 @@ phutil_register_library_map(array(
'PhabricatorFileUploadController' => 'PhabricatorFileController', 'PhabricatorFileUploadController' => 'PhabricatorFileController',
'PhabricatorFileViewController' => 'PhabricatorFileController', 'PhabricatorFileViewController' => 'PhabricatorFileController',
'PhabricatorGoodForNothingWorker' => 'PhabricatorWorker', 'PhabricatorGoodForNothingWorker' => 'PhabricatorWorker',
'PhabricatorIRCBot' => 'PhabricatorDaemon',
'PhabricatorIRCProtocolHandler' => 'PhabricatorIRCHandler',
'PhabricatorJavelinLinter' => 'ArcanistLinter', 'PhabricatorJavelinLinter' => 'ArcanistLinter',
'PhabricatorLintEngine' => 'PhutilLintEngine', 'PhabricatorLintEngine' => 'PhutilLintEngine',
'PhabricatorLiskDAO' => 'LiskDAO', 'PhabricatorLiskDAO' => 'LiskDAO',

View file

@ -127,7 +127,7 @@ final class PhabricatorDaemonControl {
**COMMAND REFERENCE** **COMMAND REFERENCE**
**launch** [__n__] __daemon__ **launch** [__n__] __daemon__ [argv ...]
Start a daemon (or n copies of a daemon). Start a daemon (or n copies of a daemon).
**list** **list**

View file

@ -0,0 +1,193 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Simple IRC bot which runs as a Phabricator daemon. Although this bot is
* somewhat useful, it is also intended to serve as a demo of how to write
* "system agents" which communicate with Phabricator over Conduit, so you can
* script system interactions and integrate with other systems.
*
* NOTE: This is super janky and experimental right now.
*
* @group irc
*/
final class PhabricatorIRCBot extends PhabricatorDaemon {
private $socket;
private $handlers;
private $writeBuffer;
private $readBuffer;
public function run() {
$argv = $this->getArgv();
if (count($argv) !== 1) {
throw new Exception("usage: PhabricatorIRCBot <json_config_file>");
}
$json_raw = Filesystem::readFile($argv[0]);
$config = json_decode($json_raw, true);
if (!is_array($config)) {
throw new Exception("File '{$argv[0]}' is not valid JSON!");
}
$server = idx($config, 'server');
$port = idx($config, 'port', 6667);
$join = idx($config, 'join', array());
$handlers = idx($config, 'handlers', array());
$nick = idx($config, 'nick', 'phabot');
if (!preg_match('/^[A-Za-z0-9_]+$/', $nick)) {
throw new Exception(
"Nickname '{$nick}' is invalid, must be alphanumeric!");
}
if (!$join) {
throw new Exception("No channels to 'join' in config!");
}
foreach ($handlers as $handler) {
$obj = newv($handler, array($this));
$this->handlers[] = $obj;
}
$errno = null;
$error = null;
$socket = fsockopen($server, $port, $errno, $error);
if (!$socket) {
throw new Exception("Failed to connect, #{$errno}: {$error}");
}
$ok = stream_set_blocking($socket, false);
if (!$ok) {
throw new Exception("Failed to set stream nonblocking.");
}
$this->socket = $socket;
$this->writeCommand('USER', "{$nick} 0 * :{$nick}");
$this->writeCommand('NICK', "{$nick}");
foreach ($join as $channel) {
$this->writeCommand('JOIN', "{$channel}");
}
$this->runSelectLoop();
}
private function runSelectLoop() {
do {
$read = array($this->socket);
if (strlen($this->writeBuffer)) {
$write = array($this->socket);
} else {
$write = array();
}
$except = array();
$ok = @stream_select($read, $write, $except, $timeout_sec = 1);
if ($ok === false) {
throw new Exception(
"socket_select() failed: ".socket_strerror(socket_last_error()));
}
if ($read) {
do {
$data = fread($this->socket, 4096);
if ($data === false) {
throw new Exception("fread() failed!");
} else {
$this->debugLog(true, $data);
$this->readBuffer .= $data;
}
} while (strlen($data));
}
if ($write) {
do {
$len = fwrite($this->socket, $this->writeBuffer);
if ($len === false) {
throw new Exception("fwrite() failed!");
} else {
$this->debugLog(false, substr($this->writeBuffer, 0, $len));
$this->writeBuffer = substr($this->writeBuffer, $len);
}
} while (strlen($this->writeBuffer));
}
$this->processReadBuffer();
} while (true);
}
private function write($message) {
$this->writeBuffer .= $message;
return $this;
}
public function writeCommand($command, $message) {
return $this->write($command.' '.$message."\r\n");
}
private function processReadBuffer() {
$until = strpos($this->readBuffer, "\r\n");
if ($until === false) {
return;
}
$message = substr($this->readBuffer, 0, $until);
$this->readBuffer = substr($this->readBuffer, $until + 2);
$pattern =
'/^'.
'(?:(?P<sender>:(\S+)) )?'. // This may not be present.
'(?P<command>[A-Z0-9]+) '.
'(?P<data>.*)'.
'$/';
$matches = null;
if (!preg_match($pattern, $message, $matches)) {
throw new Exception("Unexpected message from server: {$message}");
}
$irc_message = new PhabricatorIRCMessage(
idx($matches, 'sender'),
$matches['command'],
$matches['data']);
$this->routeMessage($irc_message);
}
private function routeMessage(PhabricatorIRCMessage $message) {
foreach ($this->handlers as $handler) {
$handler->receiveMessage($message);
}
}
public function __destroy() {
$this->write("QUIT Goodbye.\r\n");
fclose($this->socket);
}
private function debugLog($is_read, $message) {
echo $is_read ? '<<< ' : '>>> ';
echo addcslashes($message, "\0..\37\177..\377");
echo "\n";
}
}

View file

@ -0,0 +1,16 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'infrastructure/daemon/base');
phutil_require_module('phabricator', 'infrastructure/daemon/irc/message');
phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorIRCBot.php');

View file

@ -0,0 +1,40 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Responds to IRC messages. You plug a bunch of these into a
* @{class:PhabricatorIRCBot} to give it special behavior.
*
* @group irc
*/
abstract class PhabricatorIRCHandler {
private $bot;
final public function __construct(PhabricatorIRCBot $irc_bot) {
$this->bot = $irc_bot;
}
final protected function write($command, $message) {
$this->bot->writeCommand($command, $message);
return $this;
}
abstract public function receiveMessage(PhabricatorIRCMessage $message);
}

View file

@ -0,0 +1,10 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_source('PhabricatorIRCHandler.php');

View file

@ -0,0 +1,34 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Implements the base IRC protocol so servers don't kick you off.
*
* @group irc
*/
class PhabricatorIRCProtocolHandler extends PhabricatorIRCHandler {
public function receiveMessage(PhabricatorIRCMessage $message) {
switch ($message->getCommand()) {
case 'PING':
$this->write('PONG', $message->getRawData());
break;
}
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'infrastructure/daemon/irc/handler/base');
phutil_require_source('PhabricatorIRCProtocolHandler.php');

View file

@ -0,0 +1,43 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorIRCMessage {
private $sender;
private $command;
private $data;
public function __construct($sender, $command, $data) {
$this->sender = $sender;
$this->command = $command;
$this->data = $data;
}
public function getRawSender() {
return $this->sender;
}
public function getRawData() {
return $this->data;
}
public function getCommand() {
return $this->command;
}
}

View file

@ -0,0 +1,10 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_source('PhabricatorIRCMessage.php');