diff options
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/Comet/CometPlugin.php | 166 | ||||
-rw-r--r-- | plugins/Comet/cometupdate.js | 30 | ||||
-rw-r--r-- | plugins/Meteor/MeteorPlugin.php | 120 | ||||
-rw-r--r-- | plugins/Meteor/README | 27 | ||||
-rw-r--r-- | plugins/Meteor/meteorupdater.js | 21 | ||||
-rw-r--r-- | plugins/Realtime/RealtimePlugin.php | 229 | ||||
-rw-r--r-- | plugins/Realtime/json2.js (renamed from plugins/Comet/json2.js) | 0 | ||||
-rw-r--r-- | plugins/Realtime/realtimeupdate.js (renamed from plugins/Comet/updatetimeline.js) | 87 |
8 files changed, 495 insertions, 185 deletions
diff --git a/plugins/Comet/CometPlugin.php b/plugins/Comet/CometPlugin.php index 45251c66f..1735d2b15 100644 --- a/plugins/Comet/CometPlugin.php +++ b/plugins/Comet/CometPlugin.php @@ -31,6 +31,8 @@ if (!defined('LACONICA')) { exit(1); } +require_once INSTALLDIR.'/plugins/Realtime/RealtimePlugin.php'; + /** * Plugin to do realtime updates using Comet * @@ -41,165 +43,65 @@ if (!defined('LACONICA')) { * @link http://laconi.ca/ */ -class CometPlugin extends Plugin +class CometPlugin extends RealtimePlugin { - var $server = null; + public $server = null; + public $username = null; + public $password = null; + public $prefix = null; + protected $bay = null; - function __construct($server=null, $username=null, $password=null) + function __construct($server=null, $username=null, $password=null, $prefix=null) { $this->server = $server; $this->username = $username; $this->password = $password; + $this->prefix = $prefix; parent::__construct(); } - function onEndShowScripts($action) + function _getScripts() { - $timeline = null; - - $this->log(LOG_DEBUG, 'got action ' . $action->trimmed('action')); - - switch ($action->trimmed('action')) { - case 'public': - $timeline = '/timelines/public'; - break; - case 'tag': - $tag = $action->trimmed('tag'); - if (!empty($tag)) { - $timeline = '/timelines/tag/'.$tag; - } else { - return true; - } - break; - default: - return true; - } - - $scripts = array('jquery.comet.js', 'json2.js', 'updatetimeline.js'); + $scripts = parent::_getScripts(); - foreach ($scripts as $script) { - $action->element('script', array('type' => 'text/javascript', - 'src' => common_path('plugins/Comet/'.$script)), - ' '); - } - - $user = common_current_user(); + $ours = array('jquery.comet.js', 'cometupdate.js'); - if (!empty($user->id)) { - $user_id = $user->id; - } else { - $user_id = 0; + foreach ($ours as $script) { + $scripts[] = common_path('plugins/Comet/'.$script); } - $replyurl = common_local_url('newnotice'); - $favorurl = common_local_url('favor'); - // FIXME: need to find a better way to pass this pattern in - $deleteurl = common_local_url('deletenotice', - array('notice' => '0000000000')); - - $action->elementStart('script', array('type' => 'text/javascript')); - $action->raw("$(document).ready(function() { updater.init(\"$this->server\", \"$timeline\", $user_id, \"$replyurl\", \"$favorurl\", \"$deleteurl\"); });"); - $action->elementEnd('script'); - - return true; + return $scripts; } - function onEndNoticeSave($notice) + function _updateInitialize($timeline, $user_id) { - $this->log(LOG_INFO, "Called for save notice."); - - $timelines = array(); - - // XXX: Add other timelines; this is just for the public one - - if ($notice->is_local || - ($notice->is_local == 0 && !common_config('public', 'localonly'))) { - $timelines[] = '/timelines/public'; - } - - $tags = $this->getNoticeTags($notice); - - if (!empty($tags)) { - foreach ($tags as $tag) { - $timelines[] = '/timelines/tag/' . $tag; - } - } - - if (count($timelines) > 0) { - // Require this, since we need it - require_once(INSTALLDIR.'/plugins/Comet/bayeux.class.inc.php'); - - $json = $this->noticeAsJson($notice); - - // Bayeux? Comet? Huh? These terms confuse me - $bay = new Bayeux($this->server, $this->user, $this->password); - - foreach ($timelines as $timeline) { - $this->log(LOG_INFO, "Posting notice $notice->id to '$timeline'."); - $bay->publish($timeline, $json); - } - - $bay = NULL; - } - - return true; + $script = parent::_updateInitialize($timeline, $user_id); + return $script." CometUpdate.init(\"$this->server\", \"$timeline\", $user_id, \"$this->replyurl\", \"$this->favorurl\", \"$this->deleteurl\");"; } - function noticeAsJson($notice) + function _connect() { - // FIXME: this code should be abstracted to a neutral third - // party, like Notice::asJson(). I'm not sure of the ethics - // of refactoring from within a plugin, so I'm just abusing - // the TwitterApiAction method. Don't do this unless you're me! - - require_once(INSTALLDIR.'/lib/twitterapi.php'); - - $act = new TwitterApiAction('/dev/null'); - - $arr = $act->twitter_status_array($notice, true); - $arr['url'] = $notice->bestUrl(); - $arr['html'] = htmlspecialchars($notice->rendered); - $arr['source'] = htmlspecialchars($arr['source']); - - if (!empty($notice->reply_to)) { - $reply_to = Notice::staticGet('id', $notice->reply_to); - if (!empty($reply_to)) { - $arr['in_reply_to_status_url'] = $reply_to->bestUrl(); - } - $reply_to = null; - } - - $profile = $notice->getProfile(); - $arr['user']['profile_url'] = $profile->profileurl; - - return $arr; + require_once INSTALLDIR.'/plugins/Comet/bayeux.class.inc.php'; + // Bayeux? Comet? Huh? These terms confuse me + $this->bay = new Bayeux($this->server, $this->user, $this->password); } - function getNoticeTags($notice) + function _publish($timeline, $json) { - $tags = null; - - $nt = new Notice_tag(); - $nt->notice_id = $notice->id; - - if ($nt->find()) { - $tags = array(); - while ($nt->fetch()) { - $tags[] = $nt->tag; - } - } - - $nt->free(); - $nt = null; - - return $tags; + $this->bay->publish($timeline, $json); } - // Push this up to Plugin + function _disconnect() + { + unset($this->bay); + } - function log($level, $msg) + function _pathToChannel($path) { - common_log($level, get_class($this) . ': '.$msg); + if (!empty($this->prefix)) { + array_unshift($path, $this->prefix); + } + return '/' . implode('/', $path); } } diff --git a/plugins/Comet/cometupdate.js b/plugins/Comet/cometupdate.js new file mode 100644 index 000000000..72cca004b --- /dev/null +++ b/plugins/Comet/cometupdate.js @@ -0,0 +1,30 @@ +// update the local timeline from a Comet server +// + +var CometUpdate = function() +{ + var _server; + var _timeline; + var _userid; + var _replyurl; + var _favorurl; + var _deleteurl; + var _cometd; + + return { + init: function(server, timeline, userid, replyurl, favorurl, deleteurl) + { + _cometd = $.cometd; // Uses the default Comet object + _cometd.init(server); + _server = server; + _timeline = timeline; + _userid = userid; + _favorurl = favorurl; + _replyurl = replyurl; + _deleteurl = deleteurl; + _cometd.subscribe(timeline, function(message) { RealtimeUpdate.receive(message.data) }); + $(window).unload(function() { _cometd.disconnect(); } ); + } + } +}(); + diff --git a/plugins/Meteor/MeteorPlugin.php b/plugins/Meteor/MeteorPlugin.php new file mode 100644 index 000000000..d54d565bd --- /dev/null +++ b/plugins/Meteor/MeteorPlugin.php @@ -0,0 +1,120 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Plugin to do "real time" updates using Comet/Bayeux + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Plugin + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/plugins/Realtime/RealtimePlugin.php'; + +/** + * Plugin to do realtime updates using Meteor + * + * @category Plugin + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class MeteorPlugin extends RealtimePlugin +{ + public $webserver = null; + public $webport = null; + public $controlport = null; + public $controlserver = null; + public $channelbase = null; + protected $_socket = null; + + function __construct($webserver=null, $webport=4670, $controlport=4671, $controlserver=null, $channelbase='') + { + global $config; + + $this->webserver = (empty($webserver)) ? $config['site']['server'] : $webserver; + $this->webport = $webport; + $this->controlport = $controlport; + $this->controlserver = (empty($controlserver)) ? $webserver : $controlserver; + $this->channelbase = $channelbase; + + parent::__construct(); + } + + function _getScripts() + { + $scripts = parent::_getScripts(); + $scripts[] = 'http://'.$this->webserver.(($this->webport == 80) ? '':':'.$this->webport).'/meteor.js'; + $scripts[] = common_path('plugins/Meteor/meteorupdater.js'); + return $scripts; + } + + function _updateInitialize($timeline, $user_id) + { + $script = parent::_updateInitialize($timeline, $user_id); + return $script." MeteorUpdater.init(\"$this->webserver\", $this->webport, \"{$timeline}\");"; + } + + function _connect() + { + $controlserver = (empty($this->controlserver)) ? $this->webserver : $this->controlserver; + // May throw an exception. + $this->_socket = stream_socket_client("tcp://{$controlserver}:{$this->controlport}"); + if (!$this->_socket) { + throw new Exception("Couldn't connect to {$controlserver} on {$this->controlport}"); + } + } + + function _publish($channel, $message) + { + $message = json_encode($message); + $message = addslashes($message); + $cmd = "ADDMESSAGE $channel $message\n"; + $cnt = fwrite($this->_socket, $cmd); + $result = fgets($this->_socket); + if (preg_match('/^ERR (.*)$/', $result, $matches)) { + throw new Exception('Error adding meteor message "'.$matches[1].'"'); + } + // TODO: parse and deal with result + } + + function _disconnect() + { + $cnt = fwrite($this->_socket, "QUIT\n"); + @fclose($this->_socket); + } + + // Meteord flips out with default '/' separator + + function _pathToChannel($path) + { + if (!empty($this->channelbase)) { + array_unshift($path, $this->channelbase); + } + return implode('-', $path); + } +} diff --git a/plugins/Meteor/README b/plugins/Meteor/README new file mode 100644 index 000000000..22f548462 --- /dev/null +++ b/plugins/Meteor/README @@ -0,0 +1,27 @@ +This is a plugin to automatically load notices in the browser no +matter who creates them -- the kind of thing we see with +search.twitter.com, rejaw.com, or FriendFeed's "real time" news. + +It requires a meteor server. + + http://meteorserver.org/ + +Note that the controller interface needs to be accessible by the Web +server, and the subscriber interface needs to be accessible by your +Web users. You MUST firewall the controller interface from users; +otherwise anyone will be able to push any message to your subscribers. +Not good! + +You can enable the plugin with this line in config.php: + +addPlugin('Meteor', array('webserver' => 'meteor server address')); + +Available parameters: + +* webserver: Web server address. Defaults to site server. +* webport: port to connect to for Web access. Defaults to 4670. +* controlserver: Control server address. Defaults to webserver. +* controlport: port to connect to for control. Defaults to 4671. +* channelbase: a base string to use for channels. Good if you have + multiple sites using the same meteor server. + diff --git a/plugins/Meteor/meteorupdater.js b/plugins/Meteor/meteorupdater.js new file mode 100644 index 000000000..2e688336f --- /dev/null +++ b/plugins/Meteor/meteorupdater.js @@ -0,0 +1,21 @@ +// update the local timeline from a Meteor server +// + +var MeteorUpdater = function() +{ + return { + + init: function(server, port, timeline) + { + Meteor.callbacks["process"] = function(data) { + RealtimeUpdate.receive(JSON.parse(data)); + }; + + Meteor.host = server; + Meteor.port = port; + Meteor.joinChannel(timeline, 0); + Meteor.connect(); + } + } +}(); + diff --git a/plugins/Realtime/RealtimePlugin.php b/plugins/Realtime/RealtimePlugin.php new file mode 100644 index 000000000..507f0194d --- /dev/null +++ b/plugins/Realtime/RealtimePlugin.php @@ -0,0 +1,229 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Superclass for plugins that do "real time" updates of timelines using Ajax + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Plugin + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * Superclass for plugin to do realtime updates + * + * Based on experience with the Comet and Meteor plugins, + * this superclass extracts out some of the common functionality + * + * @category Plugin + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class RealtimePlugin extends Plugin +{ + protected $replyurl = null; + protected $favorurl = null; + protected $deleteurl = null; + + function onInitializePlugin() + { + $this->replyurl = common_local_url('newnotice'); + $this->favorurl = common_local_url('favor'); + // FIXME: need to find a better way to pass this pattern in + $this->deleteurl = common_local_url('deletenotice', + array('notice' => '0000000000')); + } + + function onEndShowScripts($action) + { + $path = null; + + switch ($action->trimmed('action')) { + case 'public': + $path = array('public'); + break; + case 'tag': + $tag = $action->trimmed('tag'); + if (!empty($tag)) { + $path = array('tag', $tag); + } else { + return true; + } + break; + default: + return true; + } + + $timeline = $this->_pathToChannel($path); + + $scripts = $this->_getScripts(); + + foreach ($scripts as $script) { + $action->element('script', array('type' => 'text/javascript', + 'src' => $script), + ' '); + } + + $user = common_current_user(); + + if (!empty($user->id)) { + $user_id = $user->id; + } else { + $user_id = 0; + } + + $action->elementStart('script', array('type' => 'text/javascript')); + $action->raw("$(document).ready(function() { "); + $action->raw($this->_updateInitialize($timeline, $user_id)); + $action->raw(" });"); + $action->elementEnd('script'); + + return true; + } + + function onEndNoticeSave($notice) + { + $paths = array(); + + // XXX: Add other timelines; this is just for the public one + + if ($notice->is_local || + ($notice->is_local == 0 && !common_config('public', 'localonly'))) { + $paths[] = array('public'); + } + + $tags = $this->getNoticeTags($notice); + + if (!empty($tags)) { + foreach ($tags as $tag) { + $paths[] = array('tag', $tag); + } + } + + if (count($paths) > 0) { + + $json = $this->noticeAsJson($notice); + + $this->_connect(); + + foreach ($paths as $path) { + $timeline = $this->_pathToChannel($path); + $this->_publish($timeline, $json); + } + + $this->_disconnect(); + } + + return true; + } + + function noticeAsJson($notice) + { + // FIXME: this code should be abstracted to a neutral third + // party, like Notice::asJson(). I'm not sure of the ethics + // of refactoring from within a plugin, so I'm just abusing + // the TwitterApiAction method. Don't do this unless you're me! + + require_once(INSTALLDIR.'/lib/twitterapi.php'); + + $act = new TwitterApiAction('/dev/null'); + + $arr = $act->twitter_status_array($notice, true); + $arr['url'] = $notice->bestUrl(); + $arr['html'] = htmlspecialchars($notice->rendered); + $arr['source'] = htmlspecialchars($arr['source']); + + if (!empty($notice->reply_to)) { + $reply_to = Notice::staticGet('id', $notice->reply_to); + if (!empty($reply_to)) { + $arr['in_reply_to_status_url'] = $reply_to->bestUrl(); + } + $reply_to = null; + } + + $profile = $notice->getProfile(); + $arr['user']['profile_url'] = $profile->profileurl; + + return $arr; + } + + function getNoticeTags($notice) + { + $tags = null; + + $nt = new Notice_tag(); + $nt->notice_id = $notice->id; + + if ($nt->find()) { + $tags = array(); + while ($nt->fetch()) { + $tags[] = $nt->tag; + } + } + + $nt->free(); + $nt = null; + + return $tags; + } + + // Push this up to Plugin + + function log($level, $msg) + { + common_log($level, get_class($this) . ': '.$msg); + } + + function _getScripts() + { + return array(common_path('plugins/Realtime/realtimeupdate.js'), + common_path('plugins/Realtime/json2.js')); + } + + function _updateInitialize($timeline, $user_id) + { + return "RealtimeUpdate.init($user_id, \"$this->replyurl\", \"$this->favorurl\", \"$this->deleteurl\"); "; + } + + function _connect() + { + } + + function _publish($timeline, $json) + { + } + + function _disconnect() + { + } + + function _pathToChannel($path) + { + return ''; + } +} diff --git a/plugins/Comet/json2.js b/plugins/Realtime/json2.js index 7e27df518..7e27df518 100644 --- a/plugins/Comet/json2.js +++ b/plugins/Realtime/json2.js diff --git a/plugins/Comet/updatetimeline.js b/plugins/Realtime/realtimeupdate.js index 170949e9b..d55db5859 100644 --- a/plugins/Comet/updatetimeline.js +++ b/plugins/Realtime/realtimeupdate.js @@ -1,41 +1,24 @@ -// update the local timeline from a Comet server +// add a notice encoded as JSON into the current timeline // -var updater = function() -{ - var _server; - var _timeline; - var _userid; - var _replyurl; - var _favorurl; - var _deleteurl; - var _cometd; - - return { - init: function(server, timeline, userid, replyurl, favorurl, deleteurl) - { - _cometd = $.cometd; // Uses the default Comet object - _cometd.setLogLevel('debug'); - _cometd.init(server); - _server = server; - _timeline = timeline; - _userid = userid; - _favorurl = favorurl; - _replyurl = replyurl; - _deleteurl = deleteurl; - _cometd.subscribe(timeline, receive); - $(window).unload(leave); - } - } +RealtimeUpdate = { + + _userid: 0, + _replyurl: '', + _favorurl: '', + _deleteurl: '', - function leave() + init: function(userid, replyurl, favorurl, deleteurl) { - _cometd.disconnect(); - } + RealtimeUpdate._userid = userid; + RealtimeUpdate._replyurl = replyurl; + RealtimeUpdate._favorurl = favorurl; + RealtimeUpdate._deleteurl = deleteurl; + }, - function receive(message) + receive: function(data) { - id = message.data.id; + id = data.id; // Don't add it if it already exists @@ -43,15 +26,14 @@ var updater = function() return; } - var noticeItem = makeNoticeItem(message.data); + var noticeItem = RealtimeUpdate.makeNoticeItem(data); $("#notices_primary .notices").prepend(noticeItem, true); $("#notices_primary .notice:first").css({display:"none"}); $("#notices_primary .notice:first").fadeIn(1000); - NoticeHover(); NoticeReply(); - } + }, - function makeNoticeItem(data) + makeNoticeItem: function(data) { user = data['user']; html = data['html'].replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); @@ -93,26 +75,26 @@ var updater = function() ni = ni+"</div>"+ "<div class=\"notice-options\">"; - if (_userid != 0) { + if (RealtimeUpdate._userid != 0) { var input = $("form#form_notice fieldset input#token"); var session_key = input.val(); - ni = ni+makeFavoriteForm(data['id'], session_key); - ni = ni+makeReplyLink(data['id'], data['user']['screen_name']); - if (_userid == data['user']['id']) { - ni = ni+makeDeleteLink(data['id']); + ni = ni+RealtimeUpdate.makeFavoriteForm(data['id'], session_key); + ni = ni+RealtimeUpdate.makeReplyLink(data['id'], data['user']['screen_name']); + if (RealtimeUpdate._userid == data['user']['id']) { + ni = ni+RealtimeUpdate.makeDeleteLink(data['id']); } } ni = ni+"</div>"+ "</li>"; return ni; - } + }, - function makeFavoriteForm(id, session_key) + makeFavoriteForm: function(id, session_key) { var ff; - ff = "<form id=\"favor-"+id+"\" class=\"form_favor\" method=\"post\" action=\""+_favorurl+"\">"+ + ff = "<form id=\"favor-"+id+"\" class=\"form_favor\" method=\"post\" action=\""+RealtimeUpdate._favorurl+"\">"+ "<fieldset>"+ "<legend>Favor this notice</legend>"+ // XXX: i18n "<input name=\"token-"+id+"\" type=\"hidden\" id=\"token-"+id+"\" value=\""+session_key+"\"/>"+ @@ -121,25 +103,25 @@ var updater = function() "</fieldset>"+ "</form>"; return ff; - } + }, - function makeReplyLink(id, nickname) + makeReplyLink: function(id, nickname) { var rl; rl = "<dl class=\"notice_reply\">"+ "<dt>Reply to this notice</dt>"+ "<dd>"+ - "<a href=\""+_replyurl+"?replyto="+nickname+"\" title=\"Reply to this notice\">Reply <span class=\"notice_id\">"+id+"</span>"+ + "<a href=\""+RealtimeUpdate._replyurl+"?replyto="+nickname+"\" title=\"Reply to this notice\">Reply <span class=\"notice_id\">"+id+"</span>"+ "</a>"+ "</dd>"+ "</dl>"; return rl; - } + }, - function makeDeleteLink(id) + makeDeleteLink: function(id) { var dl, delurl; - delurl = _deleteurl.replace("0000000000", id); + delurl = RealtimeUpdate._deleteurl.replace("0000000000", id); dl = "<dl class=\"notice_delete\">"+ "<dt>Delete this notice</dt>"+ @@ -149,6 +131,5 @@ var updater = function() "</dl>"; return dl; - } -}(); - + }, +} |