summaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
authorBrion Vibber <brion@pobox.com>2010-02-12 11:18:35 -0800
committerBrion Vibber <brion@pobox.com>2010-02-12 11:18:35 -0800
commit122c8677b7004bae4cfe7e2bd49fc1bc3187c72c (patch)
tree552bf77b587fbb674329a95745067f358c0a25d3 /plugins
parent3c79448cd817d01b4421262fefc29eb558cede20 (diff)
parentfd3c9334bfcfe627446feb86ac3054b24ed05449 (diff)
Merge branch 'testing' of gitorious.org:statusnet/mainline into 0.9.x
Diffstat (limited to 'plugins')
-rw-r--r--plugins/OStatus/OStatusPlugin.php141
-rw-r--r--plugins/OStatus/actions/feedsubsettings.php45
-rw-r--r--plugins/OStatus/actions/hostmeta.php42
-rw-r--r--plugins/OStatus/actions/ostatusinit.php128
-rw-r--r--plugins/OStatus/actions/ostatussub.php226
-rw-r--r--plugins/OStatus/actions/pushcallback.php40
-rw-r--r--plugins/OStatus/actions/salmon.php81
-rw-r--r--plugins/OStatus/actions/webfinger.php77
-rw-r--r--plugins/OStatus/classes/Feedinfo.php345
-rw-r--r--plugins/OStatus/classes/HubSub.php2
-rw-r--r--plugins/OStatus/classes/Ostatus_profile.php644
-rw-r--r--plugins/OStatus/lib/activity.php85
-rw-r--r--plugins/OStatus/lib/feedmunger.php51
-rw-r--r--plugins/OStatus/lib/hubdistribqueuehandler.php116
-rw-r--r--plugins/OStatus/lib/huboutqueuehandler.php2
-rw-r--r--plugins/OStatus/lib/salmon.php64
-rw-r--r--plugins/OStatus/lib/webfinger.php143
-rw-r--r--plugins/OStatus/lib/xrd.php183
18 files changed, 2002 insertions, 413 deletions
diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php
index 4e8b892c6..8444c3d73 100644
--- a/plugins/OStatus/OStatusPlugin.php
+++ b/plugins/OStatus/OStatusPlugin.php
@@ -53,6 +53,21 @@ class OStatusPlugin extends Plugin
*/
function onRouterInitialized($m)
{
+ // Discovery actions
+ $m->connect('.well-known/host-meta',
+ array('action' => 'hostmeta'));
+ $m->connect('main/webfinger',
+ array('action' => 'webfinger'));
+ $m->connect('main/ostatus',
+ array('action' => 'ostatusinit'));
+ $m->connect('main/ostatus?nickname=:nickname',
+ array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+'));
+ $m->connect('main/ostatussub',
+ array('action' => 'ostatussub'));
+ $m->connect('main/ostatussub',
+ array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+'));
+
+ // PuSH actions
$m->connect('main/push/hub', array('action' => 'pushhub'));
$m->connect('main/push/callback/:feed',
@@ -60,6 +75,14 @@ class OStatusPlugin extends Plugin
array('feed' => '[0-9]+'));
$m->connect('settings/feedsub',
array('action' => 'feedsubsettings'));
+
+ // Salmon endpoint
+ $m->connect('main/salmon/user/:id',
+ array('action' => 'salmon'),
+ array('id' => '[0-9]+'));
+ $m->connect('main/salmon/group/:id',
+ array('action' => 'salmongroup'),
+ array('id' => '[0-9]+'));
return true;
}
@@ -87,22 +110,37 @@ class OStatusPlugin extends Plugin
/**
* Set up a PuSH hub link to our internal link for canonical timeline
- * Atom feeds for users.
+ * Atom feeds for users and groups.
*/
function onStartApiAtom(Action $action)
{
if ($action instanceof ApiTimelineUserAction) {
- $id = $action->arg('id');
- if (strval(intval($id)) === strval($id)) {
- // Canonical form of id in URL?
- // Updates will be handled for our internal PuSH hub.
- $action->element('link', array('rel' => 'hub',
- 'href' => common_local_url('pushhub')));
+ $salmonAction = 'salmon';
+ } else if ($action instanceof ApiTimelineGroupAction) {
+ $salmonAction = 'salmongroup';
+ } else {
+ return;
+ }
+
+ $id = $action->arg('id');
+ if (strval(intval($id)) === strval($id)) {
+ // Canonical form of id in URL? These are used for OStatus syndication.
+
+ $hub = common_config('ostatus', 'hub');
+ if (empty($hub)) {
+ // Updates will be handled through our internal PuSH hub.
+ $hub = common_local_url('pushhub');
}
+ $action->element('link', array('rel' => 'hub',
+ 'href' => $hub));
+
+ // Also, we'll add in the salmon link
+ $salmon = common_local_url($salmonAction, array('id' => $id));
+ $action->element('link', array('rel' => 'salmon',
+ 'href' => $salmon));
}
- return true;
}
-
+
/**
* Add the feed settings page to the Connect Settings menu
*
@@ -148,11 +186,90 @@ class OStatusPlugin extends Plugin
return true;
}
+ /**
+ * Add in an OStatus subscribe button
+ */
+ function onStartProfilePageActionsElements($output, $profile)
+ {
+ $cur = common_current_user();
+
+ if (empty($cur)) {
+ // Add an OStatus subscribe
+ $output->elementStart('li', 'entity_subscribe');
+ $url = common_local_url('ostatusinit',
+ array('nickname' => $profile->nickname));
+ $output->element('a', array('href' => $url,
+ 'class' => 'entity_remote_subscribe'),
+ _m('OStatus'));
+
+ $output->elementEnd('li');
+ }
+ }
+
+ /**
+ * Check if we've got remote replies to send via Salmon.
+ *
+ * @fixme push webfinger lookup & sending to a background queue
+ * @fixme also detect short-form name for remote subscribees where not ambiguous
+ */
+ function onEndNoticeSave($notice)
+ {
+ $count = preg_match_all('/(\w+\.)*\w+@(\w+\.)*\w+(\w+\-\w+)*\.\w+/', $notice->content, $matches);
+ if ($count) {
+ foreach ($matches[0] as $webfinger) {
+ // Check to see if we've got an actual webfinger
+ $w = new Webfinger;
+
+ $endpoint_uri = '';
+
+ $result = $w->lookup($webfinger);
+ if (empty($result)) {
+ continue;
+ }
+
+ foreach ($result->links as $link) {
+ if ($link['rel'] == 'salmon') {
+ $endpoint_uri = $link['href'];
+ }
+ }
+
+ if (empty($endpoint_uri)) {
+ continue;
+ }
+
+ $xml = '<?xml version="1.0" encoding="UTF-8" ?>';
+ $xml .= $notice->asAtomEntry();
+
+ $salmon = new Salmon();
+ $salmon->post($endpoint_uri, $xml);
+ }
+ }
+ }
+
+ /**
+ * Garbage collect unused feeds on unsubscribe
+ */
+ function onEndUnsubscribe($user, $other)
+ {
+ $profile = Ostatus_profile::staticGet('profile_id', $other->id);
+ if ($feed) {
+ $sub = new Subscription();
+ $sub->subscribed = $other->id;
+ $sub->limit(1);
+ if (!$sub->find(true)) {
+ common_log(LOG_INFO, "Unsubscribing from now-unused feed $feed->feeduri on hub $feed->huburi");
+ $profile->unsubscribe();
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Make sure necessary tables are filled out.
+ */
function onCheckSchema() {
- // warning: the autoincrement doesn't seem to set.
- // alter table feedinfo change column id id int(11) not null auto_increment;
$schema = Schema::get();
- $schema->ensureTable('feedinfo', Feedinfo::schemaDef());
+ $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef());
$schema->ensureTable('hubsub', HubSub::schemaDef());
return true;
}
diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php
index 4d5b7b60f..6933c9bf2 100644
--- a/plugins/OStatus/actions/feedsubsettings.php
+++ b/plugins/OStatus/actions/feedsubsettings.php
@@ -182,9 +182,9 @@ class FeedSubSettingsAction extends ConnectSettingsAction
}
$this->munger = $discover->feedMunger();
- $this->feedinfo = $this->munger->feedInfo();
+ $this->profile = $this->munger->ostatusProfile();
- if ($this->feedinfo->huburi == '' && !common_config('feedsub', 'nohub')) {
+ if ($this->profile->huburi == '' && !common_config('feedsub', 'nohub')) {
$this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
return false;
}
@@ -196,33 +196,44 @@ class FeedSubSettingsAction extends ConnectSettingsAction
{
if ($this->validateFeed()) {
$this->preview = true;
- $this->feedinfo = Feedinfo::ensureProfile($this->munger);
+ $this->profile = Ostatus_profile::ensureProfile($this->munger);
+ if (!$this->profile) {
+ throw new ServerException("Feed profile was not saved properly.");
+ }
// If not already in use, subscribe to updates via the hub
- if ($this->feedinfo->sub_start) {
- common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}");
+ if ($this->profile->sub_start) {
+ common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}");
} else {
- $ok = $this->feedinfo->subscribe();
+ $ok = $this->profile->subscribe();
common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
if (!$ok) {
$this->showForm(_m('Feed subscription failed! Bad response from hub.'));
return;
}
}
-
+
// And subscribe the current user to the local profile
$user = common_current_user();
- $profile = $this->feedinfo->getProfile();
- if (!$profile) {
- throw new ServerException("Feed profile was not saved properly.");
- }
- if ($user->isSubscribed($profile)) {
- $this->showForm(_m('Already subscribed!'));
- } elseif ($user->subscribeTo($profile)) {
- $this->showForm(_m('Feed subscribed!'));
+ if ($this->profile->isGroup()) {
+ $group = $this->profile->localGroup();
+ if ($user->isMember($group)) {
+ $this->showForm(_m('Already a member!'));
+ } elseif (Group_member::join($this->profile->group_id, $user->id)) {
+ $this->showForm(_m('Joined remote group!'));
+ } else {
+ $this->showForm(_m('Remote group join failed!'));
+ }
} else {
- $this->showForm(_m('Feed subscription failed!'));
+ $local = $this->profile->localProfile();
+ if ($user->isSubscribed($local)) {
+ $this->showForm(_m('Already subscribed!'));
+ } elseif ($user->subscribeTo($local)) {
+ $this->showForm(_m('Feed subscribed!'));
+ } else {
+ $this->showForm(_m('Feed subscription failed!'));
+ }
}
}
}
@@ -237,7 +248,7 @@ class FeedSubSettingsAction extends ConnectSettingsAction
function previewFeed()
{
- $feedinfo = $this->munger->feedinfo();
+ $profile = $this->munger->ostatusProfile();
$notice = $this->munger->notice(0, true); // preview
if ($notice) {
diff --git a/plugins/OStatus/actions/hostmeta.php b/plugins/OStatus/actions/hostmeta.php
new file mode 100644
index 000000000..850b8a0fe
--- /dev/null
+++ b/plugins/OStatus/actions/hostmeta.php
@@ -0,0 +1,42 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * 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/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class HostMetaAction extends Action
+{
+
+ function handle()
+ {
+ parent::handle();
+
+ $w = new Webfinger();
+
+
+ $domain = common_config('site', 'server');
+ $url = common_local_url('webfinger');
+ $url.= '?uri={uri}';
+ print $w->getHostMeta($domain, $url);
+ }
+}
diff --git a/plugins/OStatus/actions/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php
new file mode 100644
index 000000000..bac2c4d43
--- /dev/null
+++ b/plugins/OStatus/actions/ostatusinit.php
@@ -0,0 +1,128 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * 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/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+
+class OStatusInitAction extends Action
+{
+
+ var $nickname;
+ var $acct;
+ var $err;
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ if (common_logged_in()) {
+ $this->clientError(_('You can use the local subscription!'));
+ return false;
+ }
+
+ $this->nickname = $this->trimmed('nickname');
+ $this->acct = $this->trimmed('acct');
+
+ return true;
+ }
+
+ function handle($args)
+ {
+ parent::handle($args);
+
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ /* Use a session token for CSRF protection. */
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. '.
+ 'Try again, please.'));
+ return;
+ }
+ $this->ostatusConnect();
+ } else {
+ $this->showForm();
+ }
+ }
+
+ function showForm($err = null)
+ {
+ $this->err = $err;
+ $this->showPage();
+
+ }
+
+ function showContent()
+ {
+ $this->elementStart('form', array('id' => 'form_ostatus_connect',
+ 'method' => 'post',
+ 'class' => 'form_settings',
+ 'action' => common_local_url('ostatusinit')));
+ $this->elementStart('fieldset');
+ $this->element('legend', _('Subscribe to a remote user'));
+ $this->hidden('token', common_session_token());
+
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->input('nickname', _('User nickname'), $this->nickname,
+ _('Nickname of the user you want to follow'));
+ $this->elementEnd('li');
+ $this->elementStart('li');
+ $this->input('acct', _('Profile Account'), $this->acct,
+ _('Your account id (i.e. user@identi.ca)'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+ $this->submit('submit', _('Subscribe'));
+ $this->elementEnd('fieldset');
+ $this->elementEnd('form');
+ }
+
+ function ostatusConnect()
+ {
+ $w = new Webfinger;
+
+ $result = $w->lookup($this->acct);
+ foreach ($result->links as $link) {
+ if ($link['rel'] == 'http://ostatus.org/schema/1.0/subscribe') {
+ // We found a URL - let's redirect!
+
+ $user = User::staticGet('nickname', $this->nickname);
+
+ $feed_url = common_local_url('ApiTimelineUser',
+ array('id' => $user->id,
+ 'format' => 'atom'));
+ $url = $w->applyTemplate($link['template'], $feed_url);
+
+ common_redirect($url, 303);
+ }
+
+ }
+
+ }
+
+ function title()
+ {
+ return _('OStatus Connect');
+ }
+
+} \ No newline at end of file
diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php
new file mode 100644
index 000000000..9774286fd
--- /dev/null
+++ b/plugins/OStatus/actions/ostatussub.php
@@ -0,0 +1,226 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * 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/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class OStatusSubAction extends Action
+{
+
+ protected $feedurl;
+
+ function title()
+ {
+ return _m("OStatus Subscribe");
+ }
+
+ function handle($args)
+ {
+ if ($this->validateFeed()) {
+ $this->showForm();
+ }
+
+ return true;
+
+ }
+
+ function showForm($err = null)
+ {
+ $this->err = $err;
+ $this->showPage();
+ }
+
+
+ function showContent()
+ {
+ $user = common_current_user();
+
+ $profile = $user->getProfile();
+
+ $fuser = null;
+
+ $flink = Foreign_link::getByUserID($user->id, FEEDSUB_SERVICE);
+
+ if (!empty($flink)) {
+ $fuser = $flink->getForeignUser();
+ }
+
+ $this->elementStart('form', array('method' => 'post',
+ 'id' => 'form_settings_feedsub',
+ 'class' => 'form_settings',
+ 'action' =>
+ common_local_url('feedsubsettings')));
+
+ $this->hidden('token', common_session_token());
+
+ $this->elementStart('fieldset', array('id' => 'settings_feeds'));
+
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li', array('id' => 'settings_twitter_login_button'));
+ $this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+
+ $this->submit('subscribe', _m('Subscribe'));
+
+ $this->elementEnd('fieldset');
+
+ $this->elementEnd('form');
+
+ $this->previewFeed();
+ }
+
+ /**
+ * Handle posts to this form
+ *
+ * Based on the button that was pressed, muxes out to other functions
+ * to do the actual task requested.
+ *
+ * All sub-functions reload the form with a message -- success or failure.
+ *
+ * @return void
+ */
+
+ function handlePost()
+ {
+ // CSRF protection
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. '.
+ 'Try again, please.'));
+ return;
+ }
+
+ if ($this->arg('subscribe')) {
+ $this->saveFeed();
+ } else {
+ $this->showForm(_('Unexpected form submission.'));
+ }
+ }
+
+
+ /**
+ * Set up and add a feed
+ *
+ * @return boolean true if feed successfully read
+ * Sends you back to input form if not.
+ */
+ function validateFeed()
+ {
+ $feedurl = $this->trimmed('feed');
+
+ if ($feedurl == '') {
+ $this->showForm(_m('Empty feed URL!'));
+ return;
+ }
+ $this->feedurl = $feedurl;
+
+ // Get the canonical feed URI and check it
+ try {
+ $discover = new FeedDiscovery();
+ $uri = $discover->discoverFromURL($feedurl);
+ } catch (FeedSubBadURLException $e) {
+ $this->showForm(_m('Invalid URL or could not reach server.'));
+ return false;
+ } catch (FeedSubBadResponseException $e) {
+ $this->showForm(_m('Cannot read feed; server returned error.'));
+ return false;
+ } catch (FeedSubEmptyException $e) {
+ $this->showForm(_m('Cannot read feed; server returned an empty page.'));
+ return false;
+ } catch (FeedSubBadHTMLException $e) {
+ $this->showForm(_m('Bad HTML, could not find feed link.'));
+ return false;
+ } catch (FeedSubNoFeedException $e) {
+ $this->showForm(_m('Could not find a feed linked from this URL.'));
+ return false;
+ } catch (FeedSubUnrecognizedTypeException $e) {
+ $this->showForm(_m('Not a recognized feed type.'));
+ return false;
+ } catch (FeedSubException $e) {
+ // Any new ones we forgot about
+ $this->showForm(_m('Bad feed URL.'));
+ return false;
+ }
+
+ $this->munger = $discover->feedMunger();
+ $this->profile = $this->munger->ostatusProfile();
+
+ if ($this->profile->huburi == '') {
+ $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
+ return false;
+ }
+
+ return true;
+ }
+
+ function saveFeed()
+ {
+ if ($this->validateFeed()) {
+ $this->preview = true;
+ $this->profile = Ostatus_profile::ensureProfile($this->munger);
+
+ // If not already in use, subscribe to updates via the hub
+ if ($this->profile->sub_start) {
+ common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}");
+ } else {
+ $ok = $this->profile->subscribe();
+ common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
+ if (!$ok) {
+ $this->showForm(_m('Feed subscription failed! Bad response from hub.'));
+ return;
+ }
+ }
+
+ // And subscribe the current user to the local profile
+ $user = common_current_user();
+ $profile = $this->profile->getProfile();
+
+ if ($user->isSubscribed($profile)) {
+ $this->showForm(_m('Already subscribed!'));
+ } elseif ($user->subscribeTo($profile)) {
+ $this->showForm(_m('Feed subscribed!'));
+ } else {
+ $this->showForm(_m('Feed subscription failed!'));
+ }
+ }
+ }
+
+
+ function previewFeed()
+ {
+ $profile = $this->munger->ostatusProfile();
+ $notice = $this->munger->notice(0, true); // preview
+
+ if ($notice) {
+ $this->element('b', null, 'Preview of latest post from this feed:');
+
+ $item = new NoticeList($notice, $this);
+ $item->show();
+ } else {
+ $this->element('b', null, 'No posts in this feed yet.');
+ }
+ }
+
+
+} \ No newline at end of file
diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php
index a5e02e08f..2601a377a 100644
--- a/plugins/OStatus/actions/pushcallback.php
+++ b/plugins/OStatus/actions/pushcallback.php
@@ -48,9 +48,9 @@ class PushCallbackAction extends Action
throw new ServerException('Empty or invalid feed id', 400);
}
- $feedinfo = Feedinfo::staticGet('id', $feedid);
- if (!$feedinfo) {
- throw new ServerException('Unknown feed id ' . $feedid, 400);
+ $profile = Ostatus_profile::staticGet('id', $feedid);
+ if (!$profile) {
+ throw new ServerException('Unknown OStatus/PuSH feed id ' . $feedid, 400);
}
$hmac = '';
@@ -59,7 +59,7 @@ class PushCallbackAction extends Action
}
$post = file_get_contents('php://input');
- $feedinfo->postUpdates($post, $hmac);
+ $profile->postUpdates($post, $hmac);
}
/**
@@ -78,28 +78,30 @@ class PushCallbackAction extends Action
throw new ServerException("Bogus hub callback: bad mode", 404);
}
- $feedinfo = Feedinfo::staticGet('feeduri', $topic);
- if (!$feedinfo) {
+ $profile = Ostatus_profile::staticGet('feeduri', $topic);
+ if (!$profile) {
common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic");
throw new ServerException("Bogus hub callback: unknown feed", 404);
}
- # Can't currently set the token in our sub api
- #if ($feedinfo->verify_token !== $verify_token) {
- # common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic");
- # throw new ServerError("Bogus hub callback: bad token", 404);
- #}
-
+ if ($profile->verify_token !== $verify_token) {
+ common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic");
+ throw new ServerError("Bogus hub callback: bad token", 404);
+ }
+
+ if ($mode != $profile->sub_state) {
+ common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad mode \"$mode\" for feed $topic in state \"{$profile->sub_state}\"");
+ throw new ServerException("Bogus hub callback: mode doesn't match subscription state.", 404);
+ }
+
// OK!
- common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
- $feedinfo->sub_start = common_sql_date(time());
- if ($lease_seconds > 0) {
- $feedinfo->sub_end = common_sql_date(time() + $lease_seconds);
+ if ($mode == 'subscribe') {
+ common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
+ $profile->confirmSubscribe($lease_seconds);
} else {
- $feedinfo->sub_end = null;
+ common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic");
+ $profile->confirmUnsubscribe();
}
- $feedinfo->update();
-
print $challenge;
}
}
diff --git a/plugins/OStatus/actions/salmon.php b/plugins/OStatus/actions/salmon.php
new file mode 100644
index 000000000..b616027a9
--- /dev/null
+++ b/plugins/OStatus/actions/salmon.php
@@ -0,0 +1,81 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * 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/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @author James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+class SalmonAction extends Action
+{
+ var $user = null;
+ var $xml = null;
+ var $activity = null;
+
+ function prepare($args)
+ {
+ if ($_SERVER['REQUEST_METHOD'] != 'POST') {
+ $this->clientError(_('This method requires a POST.'));
+ }
+
+ if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') {
+ $this->clientError(_('Salmon requires application/atom+xml'));
+ }
+
+ $id = $this->trimmed('id');
+
+ if (!$id) {
+ $this->clientError(_('No ID.'));
+ }
+
+ $this->user = User::staticGet($id);
+
+ if (empty($this->user)) {
+ $this->clientError(_('No such user.'));
+ }
+
+ $xml = file_get_contents('php://input');
+
+ $dom = DOMDocument::loadXML($xml);
+
+ // XXX: check that document element is Atom entry
+ // XXX: check the signature
+
+ $this->act = Activity::fromAtomEntry($dom->documentElement);
+ }
+
+ function handle($args)
+ {
+ common_log(LOG_DEBUG, 'Salmon: incoming post for user: '. $user_id);
+
+ // TODO : Insert new $xml -> notice code
+
+ switch ($this->act->verb)
+ {
+ case Activity::POST:
+ case Activity::SHARE:
+ case Activity::FAVORITE:
+ case Activity::FOLLOW:
+ }
+ }
+}
diff --git a/plugins/OStatus/actions/webfinger.php b/plugins/OStatus/actions/webfinger.php
new file mode 100644
index 000000000..75ba16638
--- /dev/null
+++ b/plugins/OStatus/actions/webfinger.php
@@ -0,0 +1,77 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * 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/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class WebfingerAction extends Action
+{
+
+ public $uri;
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ $this->uri = $this->trimmed('uri');
+
+ return true;
+ }
+
+ function handle()
+ {
+ $acct = Webfinger::normalize($this->uri);
+
+ $xrd = new XRD();
+
+ list($nick, $domain) = explode('@', urldecode($acct));
+ $nick = common_canonical_nickname($nick);
+
+ $this->user = User::staticGet('nickname', $nick);
+ if (!$this->user) {
+ $this->clientError(_('No such user.'), 404);
+ return false;
+ }
+
+ $xrd->subject = $this->uri;
+ $xrd->alias[] = common_profile_url($nick);
+ $xrd->links[] = array('rel' => 'http://webfinger.net/rel/profile-page',
+ 'type' => 'text/html',
+ 'href' => common_profile_url($nick));
+
+ $salmon_url = common_local_url('salmon',
+ array('id' => $this->user->id));
+
+ $xrd->links[] = array('rel' => 'salmon',
+ 'href' => $salmon_url);
+
+ // TODO - finalize where the redirect should go on the publisher
+ $url = common_local_url('ostatussub') . '?feed={uri}';
+ $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe',
+ 'template' => $url );
+
+ header('Content-type: text/xml');
+ print $xrd->toXML();
+ }
+
+}
diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php
deleted file mode 100644
index 107faf012..000000000
--- a/plugins/OStatus/classes/Feedinfo.php
+++ /dev/null
@@ -1,345 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2009-2010, StatusNet, Inc.
- *
- * 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/>.
- */
-
-/**
- * @package FeedSubPlugin
- * @maintainer Brion Vibber <brion@status.net>
- */
-
-/*
-PuSH subscription flow:
-
- $feedinfo->subscribe()
- generate random verification token
- save to verify_token
- sends a sub request to the hub...
-
- feedsub/callback
- hub sends confirmation back to us via GET
- We verify the request, then echo back the challenge.
- On our end, we save the time we subscribed and the lease expiration
-
- feedsub/callback
- hub sends us updates via POST
-
-*/
-
-class FeedDBException extends FeedSubException
-{
- public $obj;
-
- function __construct($obj)
- {
- parent::__construct('Database insert failure');
- $this->obj = $obj;
- }
-}
-
-class Feedinfo extends Memcached_DataObject
-{
- public $__table = 'feedinfo';
-
- public $id;
- public $profile_id;
-
- public $feeduri;
- public $homeuri;
- public $huburi;
-
- // PuSH subscription data
- public $secret;
- public $verify_token;
- public $sub_start;
- public $sub_end;
-
- public $created;
- public $lastupdate;
-
-
- public /*static*/ function staticGet($k, $v=null)
- {
- return parent::staticGet(__CLASS__, $k, $v);
- }
-
- /**
- * return table definition for DB_DataObject
- *
- * DB_DataObject needs to know something about the table to manipulate
- * instances. This method provides all the DB_DataObject needs to know.
- *
- * @return array array of column definitions
- */
-
- function table()
- {
- return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
- 'profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
- 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
- 'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
- 'huburi' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
- 'secret' => DB_DATAOBJECT_STR,
- 'verify_token' => DB_DATAOBJECT_STR,
- 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
- 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
- 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
- 'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
- }
-
- static function schemaDef()
- {
- return array(new ColumnDef('id', 'integer',
- /*size*/ null,
- /*nullable*/ false,
- /*key*/ 'PRI',
- /*default*/ '0',
- /*extra*/ null,
- /*auto_increment*/ true),
- new ColumnDef('profile_id', 'integer',
- null, false),
- new ColumnDef('feeduri', 'varchar',
- 255, false, 'UNI'),
- new ColumnDef('homeuri', 'varchar',
- 255, false),
- new ColumnDef('huburi', 'varchar',
- 255, false),
- new ColumnDef('verify_token', 'varchar',
- 32, true),
- new ColumnDef('secret', 'varchar',
- 64, true),
- new ColumnDef('sub_start', 'datetime',
- null, true),
- new ColumnDef('sub_end', 'datetime',
- null, true),
- new ColumnDef('created', 'datetime',
- null, false),
- new ColumnDef('lastupdate', 'datetime',
- null, false));
- }
-
- /**
- * return key definitions for DB_DataObject
- *
- * DB_DataObject needs to know about keys that the table has; this function
- * defines them.
- *
- * @return array key definitions
- */
-
- function keys()
- {
- return array_keys($this->keyTypes());
- }
-
- /**
- * return key definitions for Memcached_DataObject
- *
- * Our caching system uses the same key definitions, but uses a different
- * method to get them.
- *
- * @return array key definitions
- */
-
- function keyTypes()
- {
- return array('id' => 'K'); // @fixme we'll need a profile_id key at least
- }
-
- function sequenceKey()
- {
- return array('id', true, false);
- }
-
- /**
- * Fetch the StatusNet-side profile for this feed
- * @return Profile
- */
- public function getProfile()
- {
- return Profile::staticGet('id', $this->profile_id);
- }
-
- /**
- * @param FeedMunger $munger
- * @return Feedinfo
- */
- public static function ensureProfile($munger)
- {
- $feedinfo = $munger->feedinfo();
-
- $current = self::staticGet('feeduri', $feedinfo->feeduri);
- if ($current) {
- // @fixme we should probably update info as necessary
- return $current;
- }
-
- $feedinfo->query('BEGIN');
-
- // Awful hack! Awful hack!
- $feedinfo->verify = common_good_rand(16);
- $feedinfo->secret = common_good_rand(32);
-
- try {
- $profile = $munger->profile();
- $result = $profile->insert();
- if (empty($result)) {
- throw new FeedDBException($profile);
- }
-
- $avatar = $munger->getAvatar();
- if ($avatar) {
- // @fixme this should be better encapsulated
- // ripped from oauthstore.php (for old OMB client)
- $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
- copy($avatar, $temp_filename);
- $imagefile = new ImageFile($profile->id, $temp_filename);
- $filename = Avatar::filename($profile->id,
- image_type_to_extension($imagefile->type),
- null,
- common_timestamp());
- rename($temp_filename, Avatar::path($filename));
- $profile->setOriginal($filename);
- }
-
- $feedinfo->profile_id = $profile->id;
- $result = $feedinfo->insert();
- if (empty($result)) {
- throw new FeedDBException($feedinfo);
- }
-
- $feedinfo->query('COMMIT');
- } catch (FeedDBException $e) {
- common_log_db_error($e->obj, 'INSERT', __FILE__);
- $feedinfo->query('ROLLBACK');
- return false;
- }
- return $feedinfo;
- }
-
- /**
- * Send a subscription request to the hub for this feed.
- * The hub will later send us a confirmation POST to /feedsub/callback.
- *
- * @return bool true on success, false on failure
- */
- public function subscribe()
- {
- if (common_config('feedsub', 'nohub')) {
- // Fake it! We're just testing remote feeds w/o hubs.
- return true;
- }
- // @fixme use the verification token
- #$token = md5(mt_rand() . ':' . $this->feeduri);
- #$this->verify_token = $token;
- #$this->update(); // @fixme
- try {
- $callback = common_local_url('pushcallback', array('feed' => $this->id));
- $headers = array('Content-Type: application/x-www-form-urlencoded');
- $post = array('hub.mode' => 'subscribe',
- 'hub.callback' => $callback,
- 'hub.verify' => 'async',
- 'hub.verify_token' => $this->verify_token,
- 'hub.secret' => $this->secret,
- //'hub.lease_seconds' => 0,
- 'hub.topic' => $this->feeduri);
- $client = new HTTPClient();
- $response = $client->post($this->huburi, $headers, $post);
- $status = $response->getStatus();
- if ($status == 202) {
- common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
- return true;
- } else if ($status == 204) {
- common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified');
- return true;
- } else if ($status >= 200 && $status < 300) {
- common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
- return false;
- } else {
- common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody());
- return false;
- }
- } catch (Exception $e) {
- // wtf!
- common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri");
- return false;
- }
- }
-
- /**
- * Read and post notices for updates from the feed.
- * Currently assumes that all items in the feed are new,
- * coming from a PuSH hub.
- *
- * @param string $xml source of Atom or RSS feed
- * @param string $hmac X-Hub-Signature header, if present
- */
- public function postUpdates($xml, $hmac)
- {
- common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml");
-
- if ($this->secret) {
- if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) {
- $their_hmac = strtolower($matches[1]);
- $our_hmac = sha1($xml . $this->secret);
- if ($their_hmac !== $our_hmac) {
- common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac");
- return;
- }
- } else {
- common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
- return;
- }
- } else if ($hmac) {
- common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'");
- return;
- }
-
- require_once "XML/Feed/Parser.php";
- $feed = new XML_Feed_Parser($xml, false, false, true);
- $munger = new FeedMunger($feed);
-
- $hits = 0;
- foreach ($feed as $index => $entry) {
- // @fixme this might sort in wrong order if we get multiple updates
-
- $notice = $munger->notice($index);
- $notice->profile_id = $this->profile_id;
-
- // Double-check for oldies
- // @fixme this could explode horribly for multiple feeds on a blog. sigh
- $dupe = new Notice();
- $dupe->uri = $notice->uri;
- if ($dupe->find(true)) {
- common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}");
- continue;
- }
-
- if (Event::handle('StartNoticeSave', array(&$notice))) {
- $id = $notice->insert();
- Event::handle('EndNoticeSave', array($notice));
- }
- $notice->addToInboxes();
-
- common_log(LOG_INFO, __METHOD__ . ": saved notice {$notice->id} for entry $index of update to \"{$this->feeduri}\"");
- $hits++;
- }
- if ($hits == 0) {
- common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml");
- }
- }
-}
diff --git a/plugins/OStatus/classes/HubSub.php b/plugins/OStatus/classes/HubSub.php
index 1769f6c94..7071ee5b4 100644
--- a/plugins/OStatus/classes/HubSub.php
+++ b/plugins/OStatus/classes/HubSub.php
@@ -242,7 +242,7 @@ class HubSub extends Memcached_DataObject
{
$headers = array('Content-Type: application/atom+xml');
if ($this->secret) {
- $hmac = sha1($atom . $this->secret);
+ $hmac = hash_hmac('sha1', $atom, $this->secret);
$headers[] = "X-Hub-Signature: sha1=$hmac";
} else {
$hmac = '(none)';
diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php
new file mode 100644
index 000000000..733d8843b
--- /dev/null
+++ b/plugins/OStatus/classes/Ostatus_profile.php
@@ -0,0 +1,644 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009-2010, StatusNet, Inc.
+ *
+ * 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/>.
+ */
+
+/**
+ * @package FeedSubPlugin
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+/*
+PuSH subscription flow:
+
+ $profile->subscribe()
+ generate random verification token
+ save to verify_token
+ sends a sub request to the hub...
+
+ main/push/callback
+ hub sends confirmation back to us via GET
+ We verify the request, then echo back the challenge.
+ On our end, we save the time we subscribed and the lease expiration
+
+ main/push/callback
+ hub sends us updates via POST
+
+*/
+
+class FeedDBException extends FeedSubException
+{
+ public $obj;
+
+ function __construct($obj)
+ {
+ parent::__construct('Database insert failure');
+ $this->obj = $obj;
+ }
+}
+
+class Ostatus_profile extends Memcached_DataObject
+{
+ public $__table = 'ostatus_profile';
+
+ public $id;
+ public $profile_id;
+ public $group_id;
+
+ public $feeduri;
+ public $homeuri;
+
+ // PuSH subscription data
+ public $huburi;
+ public $secret;
+ public $verify_token;
+ public $sub_state; // subscribe, active, unsubscribe
+ public $sub_start;
+ public $sub_end;
+
+ public $salmonuri;
+
+ public $created;
+ public $lastupdate;
+
+
+ public /*static*/ function staticGet($k, $v=null)
+ {
+ return parent::staticGet(__CLASS__, $k, $v);
+ }
+
+ /**
+ * return table definition for DB_DataObject
+ *
+ * DB_DataObject needs to know something about the table to manipulate
+ * instances. This method provides all the DB_DataObject needs to know.
+ *
+ * @return array array of column definitions
+ */
+
+ function table()
+ {
+ return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+ 'profile_id' => DB_DATAOBJECT_INT,
+ 'group_id' => DB_DATAOBJECT_INT,
+ 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'huburi' => DB_DATAOBJECT_STR,
+ 'secret' => DB_DATAOBJECT_STR,
+ 'verify_token' => DB_DATAOBJECT_STR,
+ 'sub_state' => DB_DATAOBJECT_STR,
+ 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
+ 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
+ 'salmonuri' => DB_DATAOBJECT_STR,
+ 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
+ 'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
+ }
+
+ static function schemaDef()
+ {
+ return array(new ColumnDef('id', 'integer',
+ /*size*/ null,
+ /*nullable*/ false,
+ /*key*/ 'PRI',
+ /*default*/ '0',
+ /*extra*/ null,
+ /*auto_increment*/ true),
+ new ColumnDef('profile_id', 'integer',
+ null, true, 'UNI'),
+ new ColumnDef('group_id', 'integer',
+ null, true, 'UNI'),
+ new ColumnDef('feeduri', 'varchar',
+ 255, false, 'UNI'),
+ new ColumnDef('homeuri', 'varchar',
+ 255, false),
+ new ColumnDef('huburi', 'text',
+ null, true),
+ new ColumnDef('verify_token', 'varchar',
+ 32, true),
+ new ColumnDef('secret', 'varchar',
+ 64, true),
+ new ColumnDef('sub_state', "enum('subscribe','active','unsubscribe')",
+ null, true),
+ new ColumnDef('sub_start', 'datetime',
+ null, true),
+ new ColumnDef('sub_end', 'datetime',
+ null, true),
+ new ColumnDef('salmonuri', 'text',
+ null, true),
+ new ColumnDef('created', 'datetime',
+ null, false),
+ new ColumnDef('lastupdate', 'datetime',
+ null, false));
+ }
+
+ /**
+ * return key definitions for DB_DataObject
+ *
+ * DB_DataObject needs to know about keys that the table has; this function
+ * defines them.
+ *
+ * @return array key definitions
+ */
+
+ function keys()
+ {
+ return array_keys($this->keyTypes());
+ }
+
+ /**
+ * return key definitions for Memcached_DataObject
+ *
+ * Our caching system uses the same key definitions, but uses a different
+ * method to get them.
+ *
+ * @return array key definitions
+ */
+
+ function keyTypes()
+ {
+ return array('id' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U');
+ }
+
+ function sequenceKey()
+ {
+ return array('id', true, false);
+ }
+
+ /**
+ * Fetch the StatusNet-side profile for this feed
+ * @return Profile
+ */
+ public function localProfile()
+ {
+ if ($this->profile_id) {
+ return Profile::staticGet('id', $this->profile_id);
+ }
+ return null;
+ }
+
+ /**
+ * Fetch the StatusNet-side profile for this feed
+ * @return Profile
+ */
+ public function localGroup()
+ {
+ if ($this->group_id) {
+ return User_group::staticGet('id', $this->group_id);
+ }
+ return null;
+ }
+
+ /**
+ * @param FeedMunger $munger
+ * @param boolean $isGroup is this a group record?
+ * @return Ostatus_profile
+ */
+ public static function ensureProfile($munger)
+ {
+ $profile = $munger->ostatusProfile();
+
+ $current = self::staticGet('feeduri', $profile->feeduri);
+ if ($current) {
+ // @fixme we should probably update info as necessary
+ return $current;
+ }
+
+ $profile->query('BEGIN');
+
+ // Awful hack! Awful hack!
+ $profile->verify = common_good_rand(16);
+ $profile->secret = common_good_rand(32);
+
+ try {
+ $local = $munger->profile();
+
+ if ($entity->isGroup()) {
+ $group = new User_group();
+ $group->nickname = $local->nickname . '@remote'; // @fixme
+ $group->fullname = $local->fullname;
+ $group->homepage = $local->homepage;
+ $group->location = $local->location;
+ $group->created = $local->created;
+ $group->insert();
+ if (empty($result)) {
+ throw new FeedDBException($group);
+ }
+ $profile->group_id = $group->id;
+ } else {
+ $result = $local->insert();
+ if (empty($result)) {
+ throw new FeedDBException($local);
+ }
+ $profile->profile_id = $local->id;
+ }
+
+ $profile->created = sql_common_date();
+ $profile->lastupdate = sql_common_date();
+ $result = $profile->insert();
+ if (empty($result)) {
+ throw new FeedDBException($profile);
+ }
+
+ $entity->query('COMMIT');
+ } catch (FeedDBException $e) {
+ common_log_db_error($e->obj, 'INSERT', __FILE__);
+ $entity->query('ROLLBACK');
+ return false;
+ }
+
+ $avatar = $munger->getAvatar();
+ if ($avatar) {
+ try {
+ $this->updateAvatar($avatar);
+ } catch (Exception $e) {
+ common_log(LOG_ERR, "Exception setting OStatus avatar: " .
+ $e->getMessage());
+ }
+ }
+
+ return $entity;
+ }
+
+ /**
+ * Download and update given avatar image
+ * @param string $url
+ * @throws Exception in various failure cases
+ */
+ public function updateAvatar($url)
+ {
+ // @fixme this should be better encapsulated
+ // ripped from oauthstore.php (for old OMB client)
+ $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
+ copy($url, $temp_filename);
+ $imagefile = new ImageFile($profile->id, $temp_filename);
+ $filename = Avatar::filename($profile->id,
+ image_type_to_extension($imagefile->type),
+ null,
+ common_timestamp());
+ rename($temp_filename, Avatar::path($filename));
+ if ($this->isGroup()) {
+ $group = $this->localGroup();
+ $group->setOriginal($filename);
+ } else {
+ $profile = $this->localProfile();
+ $profile->setOriginal($filename);
+ }
+ }
+
+ /**
+ * Returns an XML string fragment with profile information as an
+ * Activity Streams noun object with the given element type.
+ *
+ * Assumes that 'activity' namespace has been previously defined.
+ *
+ * @param string $element one of 'actor', 'subject', 'object', 'target'
+ * @return string
+ */
+ function asActivityNoun($element)
+ {
+ $xs = new XMLStringer(true);
+
+ $avatarHref = Avatar::defaultImage(AVATAR_PROFILE_SIZE);
+ $avatarType = 'image/png';
+ if ($this->isGroup()) {
+ $type = 'http://activitystrea.ms/schema/1.0/group';
+ $self = $this->localGroup();
+
+ // @fixme put a standard getAvatar() interface on groups too
+ if ($self->homepage_logo) {
+ $avatarHref = $self->homepage_logo;
+ $map = array('png' => 'image/png',
+ 'jpg' => 'image/jpeg',
+ 'jpeg' => 'image/jpeg',
+ 'gif' => 'image/gif');
+ $extension = pathinfo(parse_url($avatarHref, PHP_URL_PATH), PATHINFO_EXTENSION);
+ if (isset($map[$extension])) {
+ $avatarType = $map[$extension];
+ }
+ }
+ } else {
+ $type = 'http://activitystrea.ms/schema/1.0/person';
+ $self = $this->localProfile();
+ $avatar = $self->getAvatar(AVATAR_PROFILE_SIZE);
+ if ($avatar) {
+ $avatarHref = $avatar->
+ $avatarType = $avatar->mediatype;
+ }
+ }
+ $xs->elementStart('activity:' . $element);
+ $xs->element(
+ 'activity:object-type',
+ null,
+ $type
+ );
+ $xs->element(
+ 'id',
+ null,
+ $this->homeuri); // ?
+ $xs->element('title', null, $self->getBestName());
+
+ $xs->element(
+ 'link', array(
+ 'type' => $avatarType,
+ 'href' => $avatarHref
+ ),
+ ''
+ );
+
+ $xs->elementEnd('activity:' . $element);
+
+ return $xs->getString();
+ }
+
+ /**
+ * Damn dirty hack!
+ */
+ function isGroup()
+ {
+ return (strpos($this->feeduri, '/groups/') !== false);
+ }
+
+ /**
+ * Send a subscription request to the hub for this feed.
+ * The hub will later send us a confirmation POST to /main/push/callback.
+ *
+ * @return bool true on success, false on failure
+ */
+ public function subscribe($mode='subscribe')
+ {
+ if (common_config('feedsub', 'nohub')) {
+ // Fake it! We're just testing remote feeds w/o hubs.
+ return true;
+ }
+ // @fixme use the verification token
+ #$token = md5(mt_rand() . ':' . $this->feeduri);
+ #$this->verify_token = $token;
+ #$this->update(); // @fixme
+ try {
+ $callback = common_local_url('pushcallback', array('feed' => $this->id));
+ $headers = array('Content-Type: application/x-www-form-urlencoded');
+ $post = array('hub.mode' => $mode,
+ 'hub.callback' => $callback,
+ 'hub.verify' => 'async',
+ 'hub.verify_token' => $this->verify_token,
+ 'hub.secret' => $this->secret,
+ //'hub.lease_seconds' => 0,
+ 'hub.topic' => $this->feeduri);
+ $client = new HTTPClient();
+ $response = $client->post($this->huburi, $headers, $post);
+ $status = $response->getStatus();
+ if ($status == 202) {
+ common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
+ return true;
+ } else if ($status == 204) {
+ common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified');
+ return true;
+ } else if ($status >= 200 && $status < 300) {
+ common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
+ return false;
+ } else {
+ common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody());
+ return false;
+ }
+ } catch (Exception $e) {
+ // wtf!
+ common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri");
+ return false;
+ }
+ }
+
+ /**
+ * Save PuSH subscription confirmation.
+ * Sets approximate lease start and end times and finalizes state.
+ *
+ * @param int $lease_seconds provided hub.lease_seconds parameter, if given
+ */
+ public function confirmSubscribe($lease_seconds=0)
+ {
+ $original = clone($this);
+
+ $this->sub_state = 'active';
+ $this->sub_start = common_sql_date(time());
+ if ($lease_seconds > 0) {
+ $this->sub_end = common_sql_date(time() + $lease_seconds);
+ } else {
+ $this->sub_end = null;
+ }
+ $this->lastupdate = common_sql_date();
+
+ return $this->update($original);
+ }
+
+ /**
+ * Save PuSH unsubscription confirmation.
+ * Wipes active PuSH sub info and resets state.
+ */
+ public function confirmUnsubscribe()
+ {
+ $original = clone($this);
+
+ $this->verify_token = null;
+ $this->secret = null;
+ $this->sub_state = null;
+ $this->sub_start = null;
+ $this->sub_end = null;
+ $this->lastupdate = common_sql_date();
+
+ return $this->update($original);
+ }
+
+ /**
+ * Send a PuSH unsubscription request to the hub for this feed.
+ * The hub will later send us a confirmation POST to /main/push/callback.
+ *
+ * @return bool true on success, false on failure
+ */
+ public function unsubscribe() {
+ return $this->subscribe('unsubscribe');
+ }
+
+ /**
+ * Send an Activity Streams notification to the remote Salmon endpoint,
+ * if so configured.
+ *
+ * @param Profile $actor
+ * @param $verb eg Activity::SUBSCRIBE or Activity::JOIN
+ * @param $object object of the action; if null, the remote entity itself is assumed
+ */
+ public function notify(Profile $actor, $verb, $object=null)
+ {
+ if ($object == null) {
+ $object = $this;
+ }
+ if ($this->salmonuri) {
+ $text = 'update'; // @fixme
+ $id = 'tag:' . common_config('site', 'server') .
+ ':' . $verb .
+ ':' . $actor->id .
+ ':' . time(); // @fixme
+
+ $entry = new Atom10Entry();
+ $entry->elementStart('entry');
+ $entry->element('id', null, $id);
+ $entry->element('title', null, $text);
+ $entry->element('summary', null, $text);
+ $entry->element('published', null, common_date_w3dtf());
+
+ $entry->element('activity:verb', null, $verb);
+ $entry->raw($profile->asAtomAuthor());
+ $entry->raw($profile->asActivityActor());
+ $entry->raw($object->asActivityNoun('object'));
+ $entry->elmentEnd('entry');
+
+ $feed = $this->atomFeed($actor);
+ $feed->initFeed();
+ $feed->addEntry($entry);
+ $feed->renderEntries();
+ $feed->endFeed();
+
+ $xml = $feed->getString();
+ common_log(LOG_INFO, "Posting to Salmon endpoint $salmon: $xml");
+
+ $salmon = new Salmon(); // ?
+ $salmon->post($this->salmonuri, $xml);
+ }
+ }
+
+ function getBestName()
+ {
+ if ($this->isGroup()) {
+ return $this->localGroup()->getBestName();
+ } else {
+ return $this->localProfile()->getBestName();
+ }
+ }
+
+ function atomFeed($actor)
+ {
+ $feed = new Atom10Feed();
+ // @fixme should these be set up somewhere else?
+ $feed->addNamespace('activity', 'http://activitystrea.ms/spec/1.0/');
+ $feed->addNamesapce('thr', 'http://purl.org/syndication/thread/1.0');
+ $feed->addNamespace('georss', 'http://www.georss.org/georss');
+ $feed->addNamespace('ostatus', 'http://ostatus.org/schema/1.0');
+
+ $taguribase = common_config('integration', 'taguri');
+ $feed->setId("tag:{$taguribase}:UserTimeline:{$actor->id}"); // ???
+
+ $feed->setTitle($actor->getBestName() . ' timeline'); // @fixme
+ $feed->setUpdated(time());
+ $feed->setPublished(time());
+
+ $feed->addLink(common_url('ApiTimelineUser',
+ array('id' => $actor->id,
+ 'type' => 'atom')),
+ array('rel' => 'self',
+ 'type' => 'application/atom+xml'));
+
+ $feed->addLink(common_url('userbyid',
+ array('id' => $actor->id)),
+ array('rel' => 'alternate',
+ 'type' => 'text/html'));
+
+ return $feed;
+ }
+
+ /**
+ * Read and post notices for updates from the feed.
+ * Currently assumes that all items in the feed are new,
+ * coming from a PuSH hub.
+ *
+ * @param string $xml source of Atom or RSS feed
+ * @param string $hmac X-Hub-Signature header, if present
+ */
+ public function postUpdates($xml, $hmac)
+ {
+ common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml");
+
+ if ($this->secret) {
+ if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) {
+ $their_hmac = strtolower($matches[1]);
+ $our_hmac = hash_hmac('sha1', $xml, $this->secret);
+ if ($their_hmac !== $our_hmac) {
+ common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac");
+ return;
+ }
+ } else {
+ common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
+ return;
+ }
+ } else if ($hmac) {
+ common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'");
+ return;
+ }
+
+ require_once "XML/Feed/Parser.php";
+ $feed = new XML_Feed_Parser($xml, false, false, true);
+ $munger = new FeedMunger($feed);
+
+ $hits = 0;
+ foreach ($feed as $index => $entry) {
+ // @fixme this might sort in wrong order if we get multiple updates
+
+ $notice = $munger->notice($index);
+
+ // Double-check for oldies
+ // @fixme this could explode horribly for multiple feeds on a blog. sigh
+ $dupe = new Notice();
+ $dupe->uri = $notice->uri;
+ if ($dupe->find(true)) {
+ common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}");
+ continue;
+ }
+
+ // @fixme need to ensure that groups get handled correctly
+ $saved = Notice::saveNew($notice->profile_id,
+ $notice->content,
+ 'ostatus',
+ array('is_local' => Notice::REMOTE_OMB,
+ 'uri' => $notice->uri,
+ 'lat' => $notice->lat,
+ 'lon' => $notice->lon,
+ 'location_ns' => $notice->location_ns,
+ 'location_id' => $notice->location_id));
+
+ /*
+ common_log(LOG_DEBUG, "going to check group delivery...");
+ if ($this->group_id) {
+ $group = User_group::staticGet($this->group_id);
+ if ($group) {
+ common_log(LOG_INFO, __METHOD__ . ": saving to local shadow group $group->id $group->nickname");
+ $groups = array($group);
+ } else {
+ common_log(LOG_INFO, __METHOD__ . ": lost the local shadow group?");
+ }
+ } else {
+ common_log(LOG_INFO, __METHOD__ . ": no local shadow groups");
+ $groups = array();
+ }
+ common_log(LOG_DEBUG, "going to add to inboxes...");
+ $notice->addToInboxes($groups, array());
+ common_log(LOG_DEBUG, "added to inboxes.");
+ */
+
+ $hits++;
+ }
+ if ($hits == 0) {
+ common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml");
+ }
+ }
+}
diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php
new file mode 100644
index 000000000..36e227913
--- /dev/null
+++ b/plugins/OStatus/lib/activity.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * 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 OStatus
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+class ActivityNoun
+{
+ const ARTICLE = 'http://activitystrea.ms/schema/1.0/article';
+ const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry';
+ const NOTE = 'http://activitystrea.ms/schema/1.0/note';
+ const STATUS = 'http://activitystrea.ms/schema/1.0/status';
+ const FILE = 'http://activitystrea.ms/schema/1.0/file';
+ const PHOTO = 'http://activitystrea.ms/schema/1.0/photo';
+ const ALBUM = 'http://activitystrea.ms/schema/1.0/photo-album';
+ const PLAYLIST = 'http://activitystrea.ms/schema/1.0/playlist';
+ const VIDEO = 'http://activitystrea.ms/schema/1.0/video';
+ const AUDIO = 'http://activitystrea.ms/schema/1.0/audio';
+ const BOOKMARK = 'http://activitystrea.ms/schema/1.0/bookmark';
+ const PERSON = 'http://activitystrea.ms/schema/1.0/person';
+ const GROUP = 'http://activitystrea.ms/schema/1.0/group';
+ const PLACE = 'http://activitystrea.ms/schema/1.0/place';
+ const COMMENT = 'http://activitystrea.ms/schema/1.0/comment'; // tea
+
+ public $type;
+ public $id;
+ public $title;
+ public $summary;
+ public $content;
+}
+
+class Activity
+{
+ const NAMESPACE = 'http://activitystrea.ms/schema/1.0/';
+
+ const POST = 'http://activitystrea.ms/schema/1.0/post';
+ const SHARE = 'http://activitystrea.ms/schema/1.0/share';
+ const SAVE = 'http://activitystrea.ms/schema/1.0/save';
+ const FAVORITE = 'http://activitystrea.ms/schema/1.0/favorite';
+ const PLAY = 'http://activitystrea.ms/schema/1.0/play';
+ const FOLLOW = 'http://activitystrea.ms/schema/1.0/follow';
+ const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend';
+ const JOIN = 'http://activitystrea.ms/schema/1.0/join';
+ const TAG = 'http://activitystrea.ms/schema/1.0/tag';
+
+ public $actor; // an ActivityNoun
+ public $verb; // a string (the URL)
+ public $object; // an ActivityNoun
+ public $target; // an ActivityNoun
+
+ static function fromAtomEntry($domEntry)
+ {
+ }
+
+ function toAtomEntry()
+ {
+ }
+}
diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php
index cbaec6775..c895b6ce2 100644
--- a/plugins/OStatus/lib/feedmunger.php
+++ b/plugins/OStatus/lib/feedmunger.php
@@ -83,13 +83,17 @@ class FeedMunger
$this->url = $url;
}
- function feedinfo()
+ function ostatusProfile()
{
- $feedinfo = new Feedinfo();
- $feedinfo->feeduri = $this->url;
- $feedinfo->homeuri = $this->feed->link;
- $feedinfo->huburi = $this->getHubLink();
- return $feedinfo;
+ $profile = new Ostatus_profile();
+ $profile->feeduri = $this->url;
+ $profile->homeuri = $this->feed->link;
+ $profile->huburi = $this->getHubLink();
+ $salmon = $this->getSalmonLink();
+ if ($salmon) {
+ $profile->salmonuri = $salmon;
+ }
+ return $profile;
}
function getAtomLink($item, $attribs=array())
@@ -155,6 +159,16 @@ class FeedMunger
return $this->getAtomLink($this->feed, array('rel' => 'hub'));
}
+ function getSalmonLink()
+ {
+ return $this->getAtomLink($this->feed, array('rel' => 'salmon'));
+ }
+
+ function getSelfLink()
+ {
+ return $this->getAtomLink($this->feed, array('rel' => 'self'));
+ }
+
/**
* Get an appropriate avatar image source URL, if available.
* @return mixed string or false
@@ -203,12 +217,13 @@ class FeedMunger
if (!$entry) {
return null;
}
-
+
if ($preview) {
$notice = new FeedSubPreviewNotice($this->profile(true));
$notice->id = -1;
} else {
$notice = new Notice();
+ $notice->profile_id = $this->profileIdForEntry($index);
}
$link = $this->getAltLink($entry);
@@ -221,7 +236,7 @@ class FeedMunger
$notice->uri = $link;
$notice->url = $link;
$notice->content = $this->noticeFromEntry($entry);
- $notice->rendered = common_render_content($notice->content, $notice);
+ $notice->rendered = common_render_content($notice->content, $notice); // @fixme this is failing on group posts
$notice->created = common_sql_date($entry->updated); // @fixme
$notice->is_local = Notice::GATEWAY;
$notice->source = 'feed';
@@ -239,7 +254,22 @@ class FeedMunger
return $notice;
}
+ function profileIdForEntry($index=1)
+ {
+ // hack hack hack
+ // should get profile for this entry's author...
+ $remote = Ostatus_profile::staticGet('feeduri', $this->getSelfLink());
+ if ($feed) {
+ return $feed->profile_id;
+ } else {
+ throw new Exception("Can't find feed profile");
+ }
+ }
+
/**
+ * Parse location given as a GeoRSS-simple point, if provided.
+ * http://www.georss.org/simple
+ *
* @param feed item $entry
* @return mixed Location or false
*/
@@ -249,7 +279,10 @@ class FeedMunger
$points = $dom->getElementsByTagNameNS('http://www.georss.org/georss', 'point');
for ($i = 0; $i < $points->length; $i++) {
- $point = trim($points->item(0)->textContent);
+ $point = $points->item(0)->textContent;
+ $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace"
+ $point = preg_replace('/\s+/', ' ', $point);
+ $point = trim($point);
$coords = explode(' ', $point);
if (count($coords) == 2) {
list($lat, $lon) = $coords;
diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/hubdistribqueuehandler.php
index 126f1355f..245a57f72 100644
--- a/plugins/OStatus/lib/hubdistribqueuehandler.php
+++ b/plugins/OStatus/lib/hubdistribqueuehandler.php
@@ -34,27 +34,101 @@ class HubDistribQueueHandler extends QueueHandler
{
assert($notice instanceof Notice);
+ $this->pushUser($notice);
+ foreach ($notice->getGroups() as $group) {
+ $this->pushGroup($notice, $group->group_id);
+ }
+ return true;
+ }
+
+ function pushUser($notice)
+ {
// See if there's any PuSH subscriptions, including OStatus clients.
// @fixme handle group subscriptions as well
// http://identi.ca/api/statuses/user_timeline/1.atom
$feed = common_local_url('ApiTimelineUser',
array('id' => $notice->profile_id,
'format' => 'atom'));
+ $this->pushFeed($feed, array($this, 'userFeedForNotice'), $notice);
+ }
+
+ function pushGroup($notice, $group_id)
+ {
+ $feed = common_local_url('ApiTimelineGroup',
+ array('id' => $group_id,
+ 'format' => 'atom'));
+ $this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group_id, $notice);
+ }
+
+ /**
+ * @param string $feed URI to the feed
+ * @param callable $callback function to generate Atom feed update if needed
+ * any additional params are passed to the callback.
+ */
+ function pushFeed($feed, $callback)
+ {
+ $hub = common_config('ostatus', 'hub');
+ if ($hub) {
+ $this->pushFeedExternal($feed, $hub);
+ }
+
$sub = new HubSub();
$sub->topic = $feed;
if ($sub->find()) {
- common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $feed");
- $qm = QueueManager::get();
- $atom = $this->userFeedForNotice($notice);
- while ($sub->fetch()) {
- common_log(LOG_INFO, "Prepping PuSH distribution to $sub->callback for $feed");
- $data = array('sub' => clone($sub),
- 'atom' => $atom);
- $qm->enqueue($data, 'hubout');
- }
+ $args = array_slice(func_get_args(), 2);
+ $atom = call_user_func_array($callback, $args);
+ $this->pushFeedInternal($atom, $sub);
} else {
common_log(LOG_INFO, "No PuSH subscribers for $feed");
}
+ return true;
+ }
+
+ /**
+ * Ping external hub about this update.
+ * The hub will pull the feed and check for new items later.
+ * Not guaranteed safe in an environment with database replication.
+ *
+ * @param string $feed feed topic URI
+ * @param string $hub PuSH hub URI
+ * @fixme can consolidate pings for user & group posts
+ */
+ function pushFeedExternal($feed, $hub)
+ {
+ $client = new HTTPClient();
+ try {
+ $data = array('hub.mode' => 'publish',
+ 'hub.url' => $feed);
+ $response = $client->post($hub, array(), $data);
+ if ($response->getStatus() == 204) {
+ common_log(LOG_INFO, "PuSH ping to hub $hub for $feed ok");
+ return true;
+ } else {
+ common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed with HTTP " .
+ $response->getStatus() . ': ' .
+ $response->getBody());
+ }
+ } catch (Exception $e) {
+ common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed: " . $e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Queue up direct feed update pushes to subscribers on our internal hub.
+ * @param string $atom update feed, containing only new/changed items
+ * @param HubSub $sub open query of subscribers
+ */
+ function pushFeedInternal($atom, $sub)
+ {
+ common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $sub->topic");
+ $qm = QueueManager::get();
+ while ($sub->fetch()) {
+ common_log(LOG_INFO, "Prepping PuSH distribution to $sub->callback for $sub->topic");
+ $data = array('sub' => clone($sub),
+ 'atom' => $atom);
+ $qm->enqueue($data, 'hubout');
+ }
}
/**
@@ -83,5 +157,29 @@ class HubDistribQueueHandler extends QueueHandler
common_log(LOG_DEBUG, $feed);
return $feed;
}
+
+ function groupFeedForNotice($group_id, $notice)
+ {
+ // @fixme this feels VERY hacky...
+ // should probably be a cleaner way to do it
+
+ ob_start();
+ $api = new ApiTimelineGroupAction();
+ $args = array('id' => $group_id,
+ 'format' => 'atom',
+ 'max_id' => $notice->id,
+ 'since_id' => $notice->id - 1);
+ $api->prepare($args);
+ $api->handle($args);
+ $feed = ob_get_clean();
+
+ // ...and override the content-type back to something normal... eww!
+ // hope there's no other headers that got set while we weren't looking.
+ header('Content-Type: text/html; charset=utf-8');
+
+ common_log(LOG_DEBUG, $feed);
+ return $feed;
+ }
+
}
diff --git a/plugins/OStatus/lib/huboutqueuehandler.php b/plugins/OStatus/lib/huboutqueuehandler.php
index cb44ad2c4..0791c7e5d 100644
--- a/plugins/OStatus/lib/huboutqueuehandler.php
+++ b/plugins/OStatus/lib/huboutqueuehandler.php
@@ -43,7 +43,7 @@ class HubOutQueueHandler extends QueueHandler
common_log(LOG_ERR, "Failed PuSH to $sub->callback for $sub->topic: " .
$e->getMessage());
// @fixme Reschedule a later delivery?
- // Currently we have no way to do this other than 'send NOW'
+ return true;
}
return true;
diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php
new file mode 100644
index 000000000..8c77222a6
--- /dev/null
+++ b/plugins/OStatus/lib/salmon.php
@@ -0,0 +1,64 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * PHP version 5
+ *
+ * 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/>.
+ *
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+class Salmon
+{
+ public function post($endpoint_uri, $xml)
+ {
+ if (empty($endpoint_uri)) {
+ return FALSE;
+ }
+
+ $headers = array('Content-type: application/atom+xml');
+
+ try {
+ $client = new HTTPClient();
+ $client->setBody($xml);
+ $response = $client->post($endpoint_uri, $headers);
+ } catch (HTTP_Request2_Exception $e) {
+ return false;
+ }
+ if ($response->getStatus() != 200) {
+ return false;
+ }
+
+ }
+
+ public function createMagicEnv($text, $userid)
+ {
+
+
+ }
+
+
+ public function verifyMagicEnv($env)
+ {
+
+
+ }
+}
diff --git a/plugins/OStatus/lib/webfinger.php b/plugins/OStatus/lib/webfinger.php
new file mode 100644
index 000000000..417d54904
--- /dev/null
+++ b/plugins/OStatus/lib/webfinger.php
@@ -0,0 +1,143 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * PHP version 5
+ *
+ * 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/>.
+ *
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+define('WEBFINGER_SERVICE_REL_VALUE', 'lrdd');
+
+/**
+ * Implement the webfinger protocol.
+ */
+class Webfinger
+{
+ /**
+ * Perform a webfinger lookup given an account.
+ */
+ public function lookup($id)
+ {
+ $id = $this->normalize($id);
+ list($name, $domain) = explode('@', $id);
+
+ $links = $this->getServiceLinks($domain);
+ if (!$links) {
+ return false;
+ }
+
+ $services = array();
+ foreach ($links as $link) {
+ if ($link['template']) {
+ return $this->getServiceDescription($link['template'], $id);
+ }
+ if ($link['href']) {
+ return $this->getServiceDescription($link['href'], $id);
+ }
+ }
+ }
+
+ /**
+ * Normalize an account ID
+ */
+ function normalize($id)
+ {
+ if (substr($id, 0, 7) == 'acct://') {
+ return substr($id, 7);
+ } else if (substr($id, 0, 5) == 'acct:') {
+ return substr($id, 5);
+ }
+
+ return $id;
+ }
+
+ function getServiceLinks($domain)
+ {
+ $url = 'http://'. $domain .'/.well-known/host-meta';
+ $content = $this->fetchURL($url);
+ if (empty($content)) {
+ common_log(LOG_DEBUG, 'Error fetching host-meta');
+ return false;
+ }
+ $result = XRD::parse($content);
+
+ // Ensure that the host == domain (spec may include signing later)
+ if ($result->host != $domain) {
+ return false;
+ }
+
+ $links = array();
+ foreach ($result->links as $link) {
+ if ($link['rel'] == WEBFINGER_SERVICE_REL_VALUE) {
+ $links[] = $link;
+ }
+
+ }
+ return $links;
+ }
+
+ function getServiceDescription($template, $id)
+ {
+ $url = $this->applyTemplate($template, 'acct:' . $id);
+
+ $content = $this->fetchURL($url);
+
+ return XRD::parse($content);
+ }
+
+ function fetchURL($url)
+ {
+ try {
+ $client = new HTTPClient();
+ $response = $client->get($url);
+ } catch (HTTP_Request2_Exception $e) {
+ return false;
+ }
+
+ if ($response->getStatus() != 200) {
+ return false;
+ }
+
+ return $response->getBody();
+ }
+
+ function applyTemplate($template, $id)
+ {
+ $template = str_replace('{uri}', urlencode($id), $template);
+
+ return $template;
+ }
+
+ function getHostMeta($domain, $template) {
+ $xrd = new XRD();
+ $xrd->host = $domain;
+ $xrd->links[] = array('rel' => 'lrdd',
+ 'template' => $template,
+ 'title' => array('Resource Descriptor'));
+
+ return $xrd->toXML();
+ }
+}
+
+
diff --git a/plugins/OStatus/lib/xrd.php b/plugins/OStatus/lib/xrd.php
new file mode 100644
index 000000000..16d27f8eb
--- /dev/null
+++ b/plugins/OStatus/lib/xrd.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * PHP version 5
+ *
+ * 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/>.
+ *
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+
+class XRD
+{
+ const XML_NS = 'http://www.w3.org/2000/xmlns/';
+
+ const XRD_NS = 'http://docs.oasis-open.org/ns/xri/xrd-1.0';
+
+ const HOST_META_NS = 'http://host-meta.net/xrd/1.0';
+
+ public $expires;
+
+ public $subject;
+
+ public $host;
+
+ public $alias = array();
+
+ public $types = array();
+
+ public $links = array();
+
+ public static function parse($xml)
+ {
+ $xrd = new XRD();
+
+ $dom = new DOMDocument();
+ $dom->loadXML($xml);
+ $xrd_element = $dom->getElementsByTagName('XRD')->item(0);
+
+ // Check for host-meta host
+ $host = $xrd_element->getElementsByTagName('Host')->item(0)->nodeValue;
+ if ($host) {
+ $xrd->host = $host;
+ }
+
+ // Loop through other elements
+ foreach ($xrd_element->childNodes as $node) {
+ switch ($node->tagName) {
+ case 'Expires':
+ $xrd->expires = $node->nodeValue;
+ break;
+ case 'Subject':
+ $xrd->subject = $node->nodeValue;
+ break;
+
+ case 'Alias':
+ $xrd->alias[] = $node->nodeValue;
+ break;
+
+ case 'Link':
+ $xrd->links[] = $xrd->parseLink($node);
+ break;
+
+ case 'Type':
+ $xrd->types[] = $xrd->parseType($node);
+ break;
+
+ }
+ }
+ return $xrd;
+ }
+
+ public function toXML()
+ {
+ $dom = new DOMDocument('1.0', 'UTF-8');
+ $dom->formatOutput = true;
+
+ $xrd_dom = $dom->createElementNS(XRD::XRD_NS, 'XRD');
+ $dom->appendChild($xrd_dom);
+
+ if ($this->host) {
+ $host_dom = $dom->createElement('hm:Host', $this->host);
+ $xrd_dom->setAttributeNS(XRD::XML_NS, 'xmlns:hm', XRD::HOST_META_NS);
+ $xrd_dom->appendChild($host_dom);
+ }
+
+ if ($this->expires) {
+ $expires_dom = $dom->createElement('Expires', $this->expires);
+ $xrd_dom->appendChild($expires_dom);
+ }
+
+ if ($this->subject) {
+ $subject_dom = $dom->createElement('Subject', $this->subject);
+ $xrd_dom->appendChild($subject_dom);
+ }
+
+ foreach ($this->alias as $alias) {
+ $alias_dom = $dom->createElement('Alias', $alias);
+ $xrd_dom->appendChild($alias_dom);
+ }
+
+ foreach ($this->types as $type) {
+ $type_dom = $dom->createElement('Type', $type);
+ $xrd_dom->appendChild($type_dom);
+ }
+
+ foreach ($this->links as $link) {
+ $link_dom = $this->saveLink($dom, $link);
+ $xrd_dom->appendChild($link_dom);
+ }
+
+ return $dom->saveXML();
+ }
+
+ function parseType($element)
+ {
+ return array();
+ }
+
+ function parseLink($element)
+ {
+ $link = array();
+ $link['rel'] = $element->getAttribute('rel');
+ $link['type'] = $element->getAttribute('type');
+ $link['href'] = $element->getAttribute('href');
+ $link['template'] = $element->getAttribute('template');
+ foreach ($element->childNodes as $node) {
+ switch($node->tagName) {
+ case 'Title':
+ $link['title'][] = $node->nodeValue;
+ }
+ }
+
+ return $link;
+ }
+
+ function saveLink($doc, $link)
+ {
+ $link_element = $doc->createElement('Link');
+ if ($link['rel']) {
+ $link_element->setAttribute('rel', $link['rel']);
+ }
+ if ($link['type']) {
+ $link_element->setAttribute('type', $link['type']);
+ }
+ if ($link['href']) {
+ $link_element->setAttribute('href', $link['href']);
+ }
+ if ($link['template']) {
+ $link_element->setAttribute('template', $link['template']);
+ }
+
+ if (is_array($link['title'])) {
+ foreach($link['title'] as $title) {
+ $title = $doc->createElement('Title', $title);
+ $link_element->appendChild($title);
+ }
+ }
+
+
+ return $link_element;
+ }
+}
+