diff --git a/resources/ircbot/example_config.json b/resources/ircbot/example_config.json index e514848daa..6ce5d3ac3c 100644 --- a/resources/ircbot/example_config.json +++ b/resources/ircbot/example_config.json @@ -8,6 +8,7 @@ "handlers" : [ "PhabricatorIRCProtocolHandler", "PhabricatorIRCObjectNameHandler", + "PhabricatorIRCLogHandler", "PhabricatorIRCDifferentialNotificationHandler" ], diff --git a/resources/sql/patches/106.chatlog.sql b/resources/sql/patches/106.chatlog.sql new file mode 100644 index 0000000000..ebc0e4b1b0 --- /dev/null +++ b/resources/sql/patches/106.chatlog.sql @@ -0,0 +1,11 @@ +CREATE DATABASE IF NOT EXISTS phabricator_chatlog; +CREATE TABLE phabricator_chatlog.chatlog_event ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + channel VARCHAR(64) BINARY NOT NULL, + epoch INT UNSIGNED NOT NULL, + author VARCHAR(64) BINARY NOT NULL, + type VARCHAR(4) NOT NULL, + message LONGBLOB NOT NULL, + loggedByPHID VARCHAR(64) BINARY NOT NULL, + KEY (channel, epoch) +); \ No newline at end of file diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index de98ac2516..f692d638f2 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -1330,6 +1330,15 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/herald/PathTypeahead.js', ), + 'phabricator-chatlog-css' => + array( + 'uri' => '/res/a5aa9eef/rsrc/css/application/chatlog/chatlog.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/chatlog/chatlog.css', + ), 'phabricator-content-source-view-css' => array( 'uri' => '/res/8c738a93/rsrc/css/application/contentsource/content-source-view.css', @@ -1508,6 +1517,17 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/core/remarkup.css', ), + 0 => + array( + 'uri' => '/res/b6096fdd/rsrc/js/javelin/lib/__tests__/URI.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-uri', + 1 => 'javelin-php-serializer', + ), + 'disk' => '/rsrc/js/javelin/lib/__tests__/URI.js', + ), 'phabricator-search-results-css' => array( 'uri' => '/res/f8a86e27/rsrc/css/application/search/search-results.css', @@ -1529,17 +1549,6 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/core/ShapedRequest.js', ), - 0 => - array( - 'uri' => '/res/b6096fdd/rsrc/js/javelin/lib/__tests__/URI.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-uri', - 1 => 'javelin-php-serializer', - ), - 'disk' => '/rsrc/js/javelin/lib/__tests__/URI.js', - ), 'phabricator-slowvote-css' => array( 'uri' => '/res/94d20443/rsrc/css/application/slowvote/slowvote.css', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2d2ec5c4ec..0c4945f6dd 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -104,6 +104,9 @@ phutil_register_library_map(array( 'ConduitAPIResponse' => 'applications/conduit/protocol/response', 'ConduitAPI_arcanist_Method' => 'applications/conduit/method/arcanist/base', 'ConduitAPI_arcanist_projectinfo_Method' => 'applications/conduit/method/arcanist/projectinfo', + 'ConduitAPI_chatlog_Method' => 'applications/conduit/method/chatlog/base', + 'ConduitAPI_chatlog_query_Method' => 'applications/conduit/method/chatlog/query', + 'ConduitAPI_chatlog_record_Method' => 'applications/conduit/method/chatlog/record', 'ConduitAPI_conduit_connect_Method' => 'applications/conduit/method/conduit/connect', 'ConduitAPI_conduit_getcertificate_Method' => 'applications/conduit/method/conduit/getcertificate', 'ConduitAPI_conduit_ping_Method' => 'applications/conduit/method/conduit/ping', @@ -437,6 +440,14 @@ phutil_register_library_map(array( 'PhabricatorAuthController' => 'applications/auth/controller/base', 'PhabricatorCalendarBrowseController' => 'applications/calendar/controller/browse', 'PhabricatorCalendarController' => 'applications/calendar/controller/base', + 'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/channellist', + 'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/channellog', + 'PhabricatorChatLogConstants' => 'applications/chatlog/constants/base', + 'PhabricatorChatLogController' => 'applications/chatlog/controller/base', + 'PhabricatorChatLogDAO' => 'applications/chatlog/storage/base', + 'PhabricatorChatLogEvent' => 'applications/chatlog/storage/event', + 'PhabricatorChatLogEventType' => 'applications/chatlog/constants/eventtype', + 'PhabricatorChatLogQuery' => 'applications/chatlog/query', 'PhabricatorConduitAPIController' => 'applications/conduit/controller/api', 'PhabricatorConduitCertificateToken' => 'applications/conduit/storage/token', 'PhabricatorConduitConnectionLog' => 'applications/conduit/storage/connectionlog', @@ -544,6 +555,7 @@ phutil_register_library_map(array( 'PhabricatorIRCBot' => 'infrastructure/daemon/irc/bot', 'PhabricatorIRCDifferentialNotificationHandler' => 'infrastructure/daemon/irc/handler/differentialnotification', 'PhabricatorIRCHandler' => 'infrastructure/daemon/irc/handler/base', + 'PhabricatorIRCLogHandler' => 'infrastructure/daemon/irc/handler/log', 'PhabricatorIRCMessage' => 'infrastructure/daemon/irc/message', 'PhabricatorIRCObjectNameHandler' => 'infrastructure/daemon/irc/handler/objectname', 'PhabricatorIRCProtocolHandler' => 'infrastructure/daemon/irc/handler/protocol', @@ -919,6 +931,9 @@ phutil_register_library_map(array( 'CelerityResourceGraph' => 'AbstractDirectedGraph', 'ConduitAPI_arcanist_Method' => 'ConduitAPIMethod', 'ConduitAPI_arcanist_projectinfo_Method' => 'ConduitAPI_arcanist_Method', + 'ConduitAPI_chatlog_Method' => 'ConduitAPIMethod', + 'ConduitAPI_chatlog_query_Method' => 'ConduitAPI_chatlog_Method', + 'ConduitAPI_chatlog_record_Method' => 'ConduitAPI_chatlog_Method', 'ConduitAPI_conduit_connect_Method' => 'ConduitAPIMethod', 'ConduitAPI_conduit_getcertificate_Method' => 'ConduitAPIMethod', 'ConduitAPI_conduit_ping_Method' => 'ConduitAPIMethod', @@ -1177,6 +1192,12 @@ phutil_register_library_map(array( 'PhabricatorAuthController' => 'PhabricatorController', 'PhabricatorCalendarBrowseController' => 'PhabricatorCalendarController', 'PhabricatorCalendarController' => 'PhabricatorController', + 'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController', + 'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController', + 'PhabricatorChatLogController' => 'PhabricatorController', + 'PhabricatorChatLogDAO' => 'PhabricatorLiskDAO', + 'PhabricatorChatLogEvent' => 'PhabricatorChatLogDAO', + 'PhabricatorChatLogEventType' => 'PhabricatorChatLogConstants', 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', 'PhabricatorConduitCertificateToken' => 'PhabricatorConduitDAO', 'PhabricatorConduitConnectionLog' => 'PhabricatorConduitDAO', @@ -1267,6 +1288,7 @@ phutil_register_library_map(array( 'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController', 'PhabricatorIRCBot' => 'PhabricatorDaemon', 'PhabricatorIRCDifferentialNotificationHandler' => 'PhabricatorIRCHandler', + 'PhabricatorIRCLogHandler' => 'PhabricatorIRCHandler', 'PhabricatorIRCObjectNameHandler' => 'PhabricatorIRCHandler', 'PhabricatorIRCProtocolHandler' => 'PhabricatorIRCHandler', 'PhabricatorInfrastructureTestCase' => 'PhabricatorTestCase', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index d3cca8559c..d9815e4091 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -401,6 +401,13 @@ class AphrontDefaultApplicationConfiguration ), 'lease/$' => 'DrydockLeaseListController', ), + + '/chatlog/' => array( + '$' => + 'PhabricatorChatLogChannelListController', + 'channel/(?P[^/]+)/$' => + 'PhabricatorChatLogChannelLogController', + ), ); } diff --git a/src/applications/chatlog/constants/base/PhabricatorChatLogConstants.php b/src/applications/chatlog/constants/base/PhabricatorChatLogConstants.php new file mode 100644 index 0000000000..7e90ecd10c --- /dev/null +++ b/src/applications/chatlog/constants/base/PhabricatorChatLogConstants.php @@ -0,0 +1,21 @@ +buildStandardPageView(); + + $page->setApplicationName('Chat Log'); + $page->setBaseURI('/chatlog/'); + $page->setTitle(idx($data, 'title')); + $page->setGlyph('#'); + $page->appendChild($view); + + $response = new AphrontWebpageResponse(); + return $response->setContent($page->render()); + } + +} diff --git a/src/applications/chatlog/controller/base/__init__.php b/src/applications/chatlog/controller/base/__init__.php new file mode 100644 index 0000000000..354990f2a5 --- /dev/null +++ b/src/applications/chatlog/controller/base/__init__.php @@ -0,0 +1,15 @@ +establishConnection('r'), + 'SELECT DISTINCT channel FROM %T', + $table->getTableName()); + + $rows = array(); + foreach ($channels as $channel) { + $name = $channel['channel']; + $rows[] = array( + phutil_render_tag( + 'a', + array( + 'href' => '/chatlog/channel/'.phutil_escape_uri($name).'/', + ), + phutil_escape_html($name))); + } + + $table = new AphrontTableView($rows); + $table->setHeaders( + array( + 'Channel', + )); + $table->setColumnClasses( + array( + 'pri wide', + )); + + $panel = new AphrontPanelView(); + $panel->appendChild($table); + + return $this->buildStandardPageResponse( + $panel, + array( + 'title' => 'Channel List', + )); + } +} diff --git a/src/applications/chatlog/controller/channellist/__init__.php b/src/applications/chatlog/controller/channellist/__init__.php new file mode 100644 index 0000000000..18f2a2b166 --- /dev/null +++ b/src/applications/chatlog/controller/channellist/__init__.php @@ -0,0 +1,18 @@ +channel = $data['channel']; + } + + public function processRequest() { + + $request = $this->getRequest(); + $user = $request->getUser(); + + $query = new PhabricatorChatLogQuery(); + $query->withChannels(array($this->channel)); + $query->setLimit(1000); + $logs = $query->execute(); + + require_celerity_resource('phabricator-chatlog-css'); + + $last_author = null; + $last_epoch = null; + + $row_idx = 0; + $row_colors = array( + 'normal', + 'alternate', + ); + + $out = array(); + $out[] = ''; + foreach ($logs as $log) { + $this_author = $log->getAuthor(); + $this_epoch = $log->getEpoch(); + + if (($this_author !== $last_author) || + ($this_epoch - (60 * 5) > $last_epoch)) { + ++$row_idx; + $out[] = ''; + $out[] = ''; + + $author = $log->getAuthor(); + $author = phutil_utf8_shorten($author, 18); + $out[] = ''; + } else { + $out[] = ''; + $out[] = ''; + } + $out[] = ''; + $out[] = ''; + + $last_author = $this_author; + $last_epoch = $this_epoch; + } + $out[] = '
'. + phabricator_datetime($log->getEpoch(), $user).''. + phutil_escape_html($author).'
'. + phutil_escape_html($log->getMessage()).'
'; + + + return $this->buildStandardPageResponse( + implode("\n", $out), + array( + 'title' => 'Channel Log', + )); + } +} diff --git a/src/applications/chatlog/controller/channellog/__init__.php b/src/applications/chatlog/controller/channellog/__init__.php new file mode 100644 index 0000000000..ca4927c293 --- /dev/null +++ b/src/applications/chatlog/controller/channellog/__init__.php @@ -0,0 +1,18 @@ +channels = $channels; + return $this; + } + + public function setLimit($limit) { + $this->limit = $limit; + return $this; + } + + public function execute() { + $table = new PhabricatorChatLogEvent(); + $conn_r = $table->establishConnection('r'); + + $where_clause = $this->buildWhereClause($conn_r); + + $limit_clause = ''; + if ($this->limit) { + $limit_clause = qsprintf( + $conn_r, + 'LIMIT %d', + $this->limit); + } + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T e %Q ORDER BY epoch ASC %Q', + $table->getTableName(), + $where_clause, + $limit_clause); + + $logs = $table->loadAllFromArray($data); + + return $logs; + } + + private function buildWhereClause($conn_r) { + $where = array(); + + if ($this->channels) { + $where[] = qsprintf( + $conn_r, + 'channel IN (%Ls)', + $this->channels); + } + + if ($where) { + $where = 'WHERE ('.implode(') AND (', $where).')'; + } else { + $where = ''; + } + + return $where; + } + +} diff --git a/src/applications/chatlog/query/__init__.php b/src/applications/chatlog/query/__init__.php new file mode 100644 index 0000000000..6af86c96a0 --- /dev/null +++ b/src/applications/chatlog/query/__init__.php @@ -0,0 +1,14 @@ + false, + ) + parent::getConfiguration(); + } + +} diff --git a/src/applications/chatlog/storage/event/__init__.php b/src/applications/chatlog/storage/event/__init__.php new file mode 100644 index 0000000000..65e71129c3 --- /dev/null +++ b/src/applications/chatlog/storage/event/__init__.php @@ -0,0 +1,12 @@ + 'optional list', + 'limit' => 'optional int (default = 100)', + ); + } + + public function defineReturnType() { + return 'nonempty list'; + } + + public function defineErrorTypes() { + return array(); + } + + protected function execute(ConduitAPIRequest $request) { + + $query = new PhabricatorChatLogQuery(); + + $channels = $request->getValue('channels'); + if ($channels) { + $query->withChannels($channels); + } + + $limit = $request->getValue('limit'); + if (!$limit) { + $limit = 100; + } + $query->setLimit($limit); + + $logs = $query->execute(); + + $results = array(); + foreach ($logs as $log) { + $results[] = array( + 'channel' => $log->getChannel(), + 'epoch' => $log->getEpoch(), + 'author' => $log->getAuthor(), + 'type' => $log->getType(), + 'message' => $log->getMessage(), + 'loggedByPHID' => $log->getLoggedByPHID(), + ); + } + + return $results; + } + +} diff --git a/src/applications/conduit/method/chatlog/query/__init__.php b/src/applications/conduit/method/chatlog/query/__init__.php new file mode 100644 index 0000000000..229b0dec1b --- /dev/null +++ b/src/applications/conduit/method/chatlog/query/__init__.php @@ -0,0 +1,13 @@ + 'required list', + ); + } + + public function defineReturnType() { + return 'list'; + } + + public function defineErrorTypes() { + return array( + ); + } + + protected function execute(ConduitAPIRequest $request) { + $logs = $request->getValue('logs'); + if (!is_array($logs)) { + $logs = array(); + } + + $template = new PhabricatorChatLogEvent(); + $template->setLoggedByPHID($request->getUser()->getPHID()); + + $objs = array(); + foreach ($logs as $log) { + $obj = clone $template; + $obj->setChannel(idx($log, 'channel')); + $obj->setType(idx($log, 'type')); + $obj->setAuthor(idx($log, 'author')); + $obj->setEpoch(idx($log, 'epoch')); + $obj->setMessage(idx($log, 'message')); + $obj->save(); + + $objs[] = $obj; + } + + return array_values(mpull($objs, 'getID')); + } + +} diff --git a/src/applications/conduit/method/chatlog/record/__init__.php b/src/applications/conduit/method/chatlog/record/__init__.php new file mode 100644 index 0000000000..3a9b11a626 --- /dev/null +++ b/src/applications/conduit/method/chatlog/record/__init__.php @@ -0,0 +1,15 @@ +bot->getConfig('conduit.uri').$path; + $base_uri = new PhutilURI($this->bot->getConfig('conduit.uri')); + $base_uri->setPath($path); + return (string)$base_uri; } final protected function isChannelName($name) { diff --git a/src/infrastructure/daemon/irc/handler/base/__init__.php b/src/infrastructure/daemon/irc/handler/base/__init__.php index 6dcece1eb8..66f278b6d4 100644 --- a/src/infrastructure/daemon/irc/handler/base/__init__.php +++ b/src/infrastructure/daemon/irc/handler/base/__init__.php @@ -6,5 +6,7 @@ +phutil_require_module('phutil', 'parser/uri'); + phutil_require_source('PhabricatorIRCHandler.php'); diff --git a/src/infrastructure/daemon/irc/handler/log/PhabricatorIRCLogHandler.php b/src/infrastructure/daemon/irc/handler/log/PhabricatorIRCLogHandler.php new file mode 100644 index 0000000000..f51d3c2172 --- /dev/null +++ b/src/infrastructure/daemon/irc/handler/log/PhabricatorIRCLogHandler.php @@ -0,0 +1,94 @@ +getCommand()) { + case 'PRIVMSG': + $reply_to = $message->getReplyTo(); + if (!$reply_to) { + break; + } + if (!$this->isChannelName($reply_to)) { + // Don't log private messages, although maybe we should for debugging? + break; + } + + $logs = array( + array( + 'channel' => $reply_to, + 'type' => 'mesg', + 'epoch' => time(), + 'author' => $message->getSenderNickname(), + 'message' => $message->getMessageText(), + ), + ); + + $this->futures[] = $this->getConduit()->callMethod( + 'chatlog.record', + array( + 'logs' => $logs, + )); + + $prompts = array( + '/where is the (chat)?log\?/i', + '/where am i\?/i', + '/what year is (this|it)\?/i', + ); + + $tell = false; + foreach ($prompts as $prompt) { + if (preg_match($prompt, $message->getMessageText())) { + $tell = true; + break; + } + } + + if ($tell) { + $response = $this->getURI( + '/chatlog/channel/'.phutil_escape_uri($reply_to).'/'); + $this->write('PRIVMSG', "{$reply_to} :{$response}"); + } + + break; + } + } + + public function runBackgroundTasks() { + foreach ($this->futures as $key => $future) { + try { + if ($future->isReady()) { + unset($this->futures[$key]); + } + } catch (Exception $ex) { + unset($this->futures[$key]); + phlog($ex); + } + } + } + +} diff --git a/src/infrastructure/daemon/irc/handler/log/__init__.php b/src/infrastructure/daemon/irc/handler/log/__init__.php new file mode 100644 index 0000000000..8158f96add --- /dev/null +++ b/src/infrastructure/daemon/irc/handler/log/__init__.php @@ -0,0 +1,15 @@ +getRawSender(); + $nick = ltrim($nick, ':'); + $nick = head(explode('!', $nick)); + return $nick; + } + public function getTarget() { switch ($this->getCommand()) { case 'PRIVMSG': diff --git a/src/infrastructure/daemon/irc/message/__init__.php b/src/infrastructure/daemon/irc/message/__init__.php index cb0374cf0a..db66a9e2f9 100644 --- a/src/infrastructure/daemon/irc/message/__init__.php +++ b/src/infrastructure/daemon/irc/message/__init__.php @@ -6,5 +6,7 @@ +phutil_require_module('phutil', 'utils'); + phutil_require_source('PhabricatorIRCMessage.php'); diff --git a/webroot/rsrc/css/application/chatlog/chatlog.css b/webroot/rsrc/css/application/chatlog/chatlog.css new file mode 100644 index 0000000000..5ae98f56d4 --- /dev/null +++ b/webroot/rsrc/css/application/chatlog/chatlog.css @@ -0,0 +1,39 @@ +/** + * @provides phabricator-chatlog-css + */ + +.phabricator-chat-log { + margin: 1em 2em; + font-size: 12px; +} + +.phabricator-chat-log tr.initial { + border-top: 4px solid white; +} + +.phabricator-chat-log tr.normal { + background: #e9e9e9; +} + +.phabricator-chat-log tr.alternate { + background: #f6f6f6; +} + +.phabricator-chat-log td { + padding: 2px 4px; +} + +.phabricator-chat-log td.timestamp { + white-space: nowrap; + color: #666666; +} + +.phabricator-chat-log td.author { + white-space: nowrap; + text-align: right; + font-weight: bold; +} + +.phabricator-chat-log td.message { + width: 100%; +}