summaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
authorZach Copley <zach@status.net>2010-02-15 21:14:32 +0000
committerZach Copley <zach@status.net>2010-02-15 21:14:32 +0000
commit5db40c440dcfa3f9d19f047c003bbcfaeb69dbc9 (patch)
treea6b86448b61ddeecfc0abdc5a13f1eca5723a3e1 /plugins
parent5cc1f8b001057e9c4301b173391a7f0a5415f153 (diff)
parentf5c69dfbf946438fbe1f472de3adb4514db8d090 (diff)
Merge branch 'testing' of git@gitorious.org:statusnet/mainline into testing
Diffstat (limited to 'plugins')
-rw-r--r--plugins/OStatus/OStatusPlugin.php106
-rw-r--r--plugins/OStatus/actions/feedsubsettings.php33
-rw-r--r--plugins/OStatus/actions/ostatusinit.php28
-rw-r--r--plugins/OStatus/actions/ostatussub.php20
-rw-r--r--plugins/OStatus/actions/pushcallback.php40
-rw-r--r--plugins/OStatus/actions/salmon.php52
-rw-r--r--plugins/OStatus/classes/Feedinfo.php390
-rw-r--r--plugins/OStatus/classes/HubSub.php2
-rw-r--r--plugins/OStatus/classes/Ostatus_profile.php644
-rw-r--r--plugins/OStatus/js/ostatus.js60
-rw-r--r--plugins/OStatus/lib/activity.php393
-rw-r--r--plugins/OStatus/lib/feedmunger.php47
-rw-r--r--plugins/OStatus/lib/hubdistribqueuehandler.php70
-rw-r--r--plugins/OStatus/lib/huboutqueuehandler.php2
-rw-r--r--plugins/OStatus/lib/salmon.php (renamed from plugins/OStatus/lib/Salmon.php)0
-rw-r--r--plugins/OStatus/lib/webfinger.php (renamed from plugins/OStatus/lib/Webfinger.php)0
-rw-r--r--plugins/OStatus/lib/xrd.php (renamed from plugins/OStatus/lib/XRD.php)0
-rw-r--r--plugins/OStatus/tests/ActivityParseTests.php147
-rw-r--r--plugins/OStatus/theme/base/css/ostatus.css30
-rw-r--r--plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php1
20 files changed, 1560 insertions, 505 deletions
diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php
index 62ecaf631..276ca1b3d 100644
--- a/plugins/OStatus/OStatusPlugin.php
+++ b/plugins/OStatus/OStatusPlugin.php
@@ -63,9 +63,9 @@ class OStatusPlugin extends Plugin
$m->connect('main/ostatus?nickname=:nickname',
array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+'));
$m->connect('main/ostatussub',
- array('action' => 'ostatussub'));
+ array('action' => 'ostatussub'));
$m->connect('main/ostatussub',
- array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+'));
+ array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+'));
// PuSH actions
$m->connect('main/push/hub', array('action' => 'pushhub'));
@@ -80,6 +80,9 @@ class OStatusPlugin extends Plugin
$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;
}
@@ -109,24 +112,34 @@ class OStatusPlugin extends Plugin
* Set up a PuSH hub link to our internal link for canonical timeline
* Atom feeds for users and groups.
*/
- function onStartApiAtom(Action $action)
+ function onStartApiAtom(AtomNoticeFeed $feed)
{
- if ($action instanceof ApiTimelineUserAction || $action instanceof ApiTimelineGroupAction) {
- $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')));
-
- // Also, we'll add in the salmon link
- $action->element('link', array('rel' => 'salmon',
- 'href' => common_local_url('salmon')));
+ $id = null;
+
+ if ($feed instanceof AtomUserNoticeFeed) {
+ $salmonAction = 'salmon';
+ $id = $feed->getUser()->id;
+ } else if ($feed instanceof AtomGroupNoticeFeed) {
+ $salmonAction = 'salmongroup';
+ $id = $feed->getGroup()->id;
+ } else {
+ return;
+ }
+
+ if (!empty($id)) {
+ $hub = common_config('ostatus', 'hub');
+ if (empty($hub)) {
+ // Updates will be handled through our internal PuSH hub.
+ $hub = common_local_url('pushhub');
}
+ $feed->addLink($hub, array('rel' => 'hub'));
+
+ // Also, we'll add in the salmon link
+ $salmon = common_local_url($salmonAction, array('id' => $id));
+ $feed->addLink($salmon, array('rel' => 'salmon'));
}
- return true;
}
-
+
/**
* Add the feed settings page to the Connect Settings menu
*
@@ -175,7 +188,7 @@ class OStatusPlugin extends Plugin
/**
* Add in an OStatus subscribe button
*/
- function onStartProfilePageActionsElements($output, $profile)
+ function onStartProfileRemoteSubscribe($output, $profile)
{
$cur = common_current_user();
@@ -186,14 +199,19 @@ class OStatusPlugin extends Plugin
array('nickname' => $profile->nickname));
$output->element('a', array('href' => $url,
'class' => 'entity_remote_subscribe'),
- _('OStatus'));
-
+ _m('Subscribe'));
+
$output->elementEnd('li');
}
+
+ return false;
}
/**
- * Check if we've got some Salmon stuff to send
+ * 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)
{
@@ -204,38 +222,66 @@ class OStatusPlugin extends Plugin
$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;
- }
+ }
+
+ function onEndShowStatusNetStyles($action) {
+ $action->cssLink(common_path('plugins/OStatus/theme/base/css/ostatus.css'));
+ return true;
+ }
+
+ function onEndShowStatusNetScripts($action) {
+ $action->script(common_path('plugins/OStatus/js/ostatus.js'));
+ return true;
+ }
}
diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php
index 6f592bf5b..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,13 +196,16 @@ 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.'));
@@ -212,23 +215,21 @@ class FeedSubSettingsAction extends ConnectSettingsAction
// 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 ($this->feedinfo->isGroup()) {
- if ($user->isMember($profile)) {
+ if ($this->profile->isGroup()) {
+ $group = $this->profile->localGroup();
+ if ($user->isMember($group)) {
$this->showForm(_m('Already a member!'));
- } elseif (Group_member::join($this->feedinfo->group_id, $user->id)) {
+ } 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 {
- if ($user->isSubscribed($profile)) {
+ $local = $this->profile->localProfile();
+ if ($user->isSubscribed($local)) {
$this->showForm(_m('Already subscribed!'));
- } elseif ($user->subscribeTo($profile)) {
+ } elseif ($user->subscribeTo($local)) {
$this->showForm(_m('Feed subscribed!'));
} else {
$this->showForm(_m('Feed subscription failed!'));
@@ -247,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/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php
index bac2c4d43..d21774420 100644
--- a/plugins/OStatus/actions/ostatusinit.php
+++ b/plugins/OStatus/actions/ostatusinit.php
@@ -67,9 +67,21 @@ class OStatusInitAction extends Action
function showForm($err = null)
{
- $this->err = $err;
- $this->showPage();
-
+ $this->err = $err;
+ if ($this->boolean('ajax')) {
+ header('Content-Type: text/xml;charset=utf-8');
+ $this->xw->startDocument('1.0', 'UTF-8');
+ $this->elementStart('html');
+ $this->elementStart('head');
+ $this->element('title', null, _('Subscribe to user'));
+ $this->elementEnd('head');
+ $this->elementStart('body');
+ $this->showContent();
+ $this->elementEnd('body');
+ $this->elementEnd('html');
+ } else {
+ $this->showPage();
+ }
}
function showContent()
@@ -79,15 +91,15 @@ class OStatusInitAction extends Action
'class' => 'form_settings',
'action' => common_local_url('ostatusinit')));
$this->elementStart('fieldset');
- $this->element('legend', _('Subscribe to a remote user'));
+ $this->element('legend', null, sprintf(_('Subscribe to %s'), $this->nickname));
$this->hidden('token', common_session_token());
$this->elementStart('ul', 'form_data');
- $this->elementStart('li');
+ $this->elementStart('li', array('id' => 'ostatus_nickname'));
$this->input('nickname', _('User nickname'), $this->nickname,
_('Nickname of the user you want to follow'));
$this->elementEnd('li');
- $this->elementStart('li');
+ $this->elementStart('li', array('id' => 'ostatus_profile'));
$this->input('acct', _('Profile Account'), $this->acct,
_('Your account id (i.e. user@identi.ca)'));
$this->elementEnd('li');
@@ -95,7 +107,7 @@ class OStatusInitAction extends Action
$this->submit('submit', _('Subscribe'));
$this->elementEnd('fieldset');
$this->elementEnd('form');
- }
+ }
function ostatusConnect()
{
@@ -125,4 +137,4 @@ class OStatusInitAction extends Action
return _('OStatus Connect');
}
-} \ No newline at end of file
+}
diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php
index ffc4ae8df..239122501 100644
--- a/plugins/OStatus/actions/ostatussub.php
+++ b/plugins/OStatus/actions/ostatussub.php
@@ -76,7 +76,7 @@ class OStatusSubAction extends Action
$this->elementStart('fieldset', array('id' => 'settings_feeds'));
$this->elementStart('ul', 'form_data');
- $this->elementStart('li', array('id' => 'settings_twitter_login_button'));
+ $this->elementStart('li');
$this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed'));
$this->elementEnd('li');
$this->elementEnd('ul');
@@ -164,9 +164,9 @@ class OStatusSubAction extends Action
}
$this->munger = $discover->feedMunger();
- $this->feedinfo = $this->munger->feedInfo();
+ $this->profile = $this->munger->ostatusProfile();
- if ($this->feedinfo->huburi == '') {
+ if ($this->profile->huburi == '') {
$this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
return false;
}
@@ -178,13 +178,13 @@ class OStatusSubAction extends Action
{
if ($this->validateFeed()) {
$this->preview = true;
- $this->feedinfo = Feedinfo::ensureProfile($this->munger);
+ $this->profile = Ostatus_profile::ensureProfile($this->munger);
// 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.'));
@@ -194,7 +194,7 @@ class OStatusSubAction extends Action
// And subscribe the current user to the local profile
$user = common_current_user();
- $profile = $this->feedinfo->getProfile();
+ $profile = $this->profile->getProfile();
if ($user->isSubscribed($profile)) {
$this->showForm(_m('Already subscribed!'));
@@ -209,7 +209,7 @@ class OStatusSubAction extends Action
function previewFeed()
{
- $feedinfo = $this->munger->feedinfo();
+ $profile = $this->munger->ostatusProfile();
$notice = $this->munger->notice(0, true); // preview
if ($notice) {
@@ -223,4 +223,4 @@ class OStatusSubAction extends Action
}
-} \ 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
index 012869cf7..c79d09c95 100644
--- a/plugins/OStatus/actions/salmon.php
+++ b/plugins/OStatus/actions/salmon.php
@@ -22,28 +22,60 @@
* @author James Walker <james@status.net>
*/
-if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+if (!defined('STATUSNET')) {
+ exit(1);
+}
class SalmonAction extends Action
{
+ var $user = null;
+ var $xml = null;
+ var $activity = null;
- function handle()
+ function prepare($args)
{
- parent::handle();
- if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- $this->handlePost();
+ 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'));
+ }
- function handlePost()
- {
- $user_id = $this->arg('id');
- common_log(LOG_DEBUG, 'Salmon: incoming post for user: '. $user_id);
+ $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 = new Activity($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/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php
deleted file mode 100644
index 792ea6034..000000000
--- a/plugins/OStatus/classes/Feedinfo.php
+++ /dev/null
@@ -1,390 +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,
- 'group_id' => DB_DATAOBJECT_INT,
- '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, true),
- new ColumnDef('group_id', 'integer',
- null, true),
- 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
- * @param boolean $isGroup is this a group record?
- * @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;
- if ($feedinfo->isGroup()) {
- $group = new User_group();
- $group->nickname = $profile->nickname . '@remote'; // @fixme
- $group->fullname = $profile->fullname;
- $group->homepage = $profile->homepage;
- $group->location = $profile->location;
- $group->created = $profile->created;
- $group->insert();
-
- if ($avatar) {
- $group->setOriginal($filename);
- }
-
- $feedinfo->group_id = $group->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;
- }
-
- /**
- * 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 /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)) {
- // @fixme we might have to do individual and group delivery separately!
- 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));
- }
- common_log(LOG_INFO, __METHOD__ . ": saved notice {$notice->id} for entry $index of update to \"{$this->feeduri}\"");
-
- 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/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/js/ostatus.js b/plugins/OStatus/js/ostatus.js
new file mode 100644
index 000000000..671795558
--- /dev/null
+++ b/plugins/OStatus/js/ostatus.js
@@ -0,0 +1,60 @@
+SN.U.DialogBox = {
+ Subscribe: function(a) {
+ var f = a.parent().find('#form_ostatus_connect');
+ if (f.length > 0) {
+ f.show();
+ }
+ else {
+ $.ajax({
+ type: 'GET',
+ dataType: 'xml',
+ url: a[0].href+'&ajax=1',
+ beforeSend: function(formData) {
+ a.addClass('processing');
+ },
+ error: function (xhr, textStatus, errorThrown) {
+ alert(errorThrown || textStatus);
+ },
+ success: function(data, textStatus, xhr) {
+ if (typeof($('form', data)[0]) != 'undefined') {
+ a.after(document._importNode($('form', data)[0], true));
+
+ var form = a.parent().find('#form_ostatus_connect');
+
+ form
+ .addClass('dialogbox')
+ .append('<button class="close">&#215;</button>');
+
+ form
+ .find('.submit')
+ .addClass('submit_dialogbox')
+ .removeClass('submit')
+ .bind('click', function() {
+ form.addClass('processing');
+ });
+
+ form.find('button.close').click(function(){
+ form.hide();
+
+ return false;
+ });
+
+ form.find('#acct').focus();
+ }
+
+ a.removeClass('processing');
+ }
+ });
+ }
+ }
+};
+
+SN.Init.Subscribe = function() {
+ $('.entity_subscribe a').live('click', function() { SN.U.DialogBox.Subscribe($(this)); return false; });
+};
+
+$(document).ready(function() {
+ if ($('.entity_subscribe .entity_remote_subscribe').length > 0) {
+ SN.Init.Subscribe();
+ }
+});
diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php
new file mode 100644
index 000000000..048efda2c
--- /dev/null
+++ b/plugins/OStatus/lib/activity.php
@@ -0,0 +1,393 @@
+<?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);
+}
+
+/**
+ * Utilities for turning DOMish things into Activityish things
+ *
+ * Some common functions that I didn't have the bandwidth to try to factor
+ * into some kind of reasonable superclass, so just dumped here. Might
+ * be useful to have an ActivityObject parent class or something.
+ *
+ * @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/
+ */
+
+class ActivityUtils
+{
+ const ATOM = 'http://www.w3.org/2005/Atom';
+
+ const LINK = 'link';
+ const REL = 'rel';
+ const TYPE = 'type';
+ const HREF = 'href';
+
+ /**
+ * Get the permalink for an Activity object
+ *
+ * @param DOMElement $element A DOM element
+ *
+ * @return string related link, if any
+ */
+
+ static function getLink($element)
+ {
+ $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK);
+
+ foreach ($links as $link) {
+
+ $rel = $link->getAttribute(self::REL);
+ $type = $link->getAttribute(self::TYPE);
+
+ if ($rel == 'alternate' && $type == 'text/html') {
+ return $link->getAttribute(self::HREF);
+ }
+ }
+
+ return null;
+ }
+}
+
+/**
+ * A noun-ish thing in the activity universe
+ *
+ * The activity streams spec talks about activity objects, while also having
+ * a tag activity:object, which is in fact an activity object. Aaaaaah!
+ *
+ * This is just a thing in the activity universe. Can be the subject, object,
+ * or indirect object (target!) of an activity verb. Rotten name, and I'm
+ * propagating it. *sigh*
+ *
+ * @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/
+ */
+
+class ActivityObject
+{
+ 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!
+
+ // Atom elements we snarf
+
+ const TITLE = 'title';
+ const SUMMARY = 'summary';
+ const CONTENT = 'content';
+ const ID = 'id';
+ const SOURCE = 'source';
+
+ const NAME = 'name';
+ const URI = 'uri';
+ const EMAIL = 'email';
+
+ public $type;
+ public $id;
+ public $title;
+ public $summary;
+ public $content;
+ public $link;
+ public $source;
+
+ /**
+ * Constructor
+ *
+ * This probably needs to be refactored
+ * to generate a local class (ActivityPerson, ActivityFile, ...)
+ * based on the object type.
+ *
+ * @param DOMElement $element DOM thing to turn into an Activity thing
+ */
+
+ function __construct($element)
+ {
+ $this->source = $element;
+
+ if ($element->tagName == 'author') {
+
+ $this->type = self::PERSON; // XXX: is this fair?
+ $this->title = $this->_childContent($element, self::NAME);
+ $this->id = $this->_childContent($element, self::URI);
+
+ if (empty($this->id)) {
+ $email = $this->_childContent($element, self::EMAIL);
+ if (!empty($email)) {
+ // XXX: acct: ?
+ $this->id = 'mailto:'.$email;
+ }
+ }
+
+ } else {
+
+ $this->type = $this->_childContent($element, Activity::OBJECTTYPE,
+ Activity::SPEC);
+
+ if (empty($this->type)) {
+ $this->type = ActivityObject::NOTE;
+ }
+
+ $this->id = $this->_childContent($element, self::ID);
+ $this->title = $this->_childContent($element, self::TITLE);
+ $this->summary = $this->_childContent($element, self::SUMMARY);
+ $this->content = $this->_childContent($element, self::CONTENT);
+ $this->source = $this->_childContent($element, self::SOURCE);
+
+ $this->link = ActivityUtils::getLink($element);
+
+ // XXX: grab PoCo stuff
+ }
+ }
+
+ /**
+ * Grab the text content of a DOM element child of the current element
+ *
+ * @param DOMElement $element Element whose children we examine
+ * @param string $tag Tag to look up
+ * @param string $namespace Namespace to use, defaults to Atom
+ *
+ * @return string content of the child
+ */
+
+ private function _childContent($element, $tag, $namespace=Activity::ATOM)
+ {
+ $els = $element->getElementsByTagnameNS($namespace, $tag);
+
+ if (empty($els) || $els->length == 0) {
+ return null;
+ } else {
+ $el = $els->item(0);
+ return $el->textContent;
+ }
+ }
+}
+
+/**
+ * Utility class to hold a bunch of constant defining default verb types
+ *
+ * @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/
+ */
+
+class ActivityVerb
+{
+ 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';
+}
+
+/**
+ * An activity in the ActivityStrea.ms world
+ *
+ * An activity is kind of like a sentence: someone did something
+ * to something else.
+ *
+ * 'someone' is the 'actor'; 'did something' is the verb;
+ * 'something else' is the object.
+ *
+ * @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/
+ */
+
+class Activity
+{
+ const SPEC = 'http://activitystrea.ms/spec/1.0/';
+ const SCHEMA = 'http://activitystrea.ms/schema/1.0/';
+
+ const VERB = 'verb';
+ const OBJECT = 'object';
+ const ACTOR = 'actor';
+ const SUBJECT = 'subject';
+ const OBJECTTYPE = 'object-type';
+ const CONTEXT = 'context';
+ const TARGET = 'target';
+
+ const ATOM = 'http://www.w3.org/2005/Atom';
+
+ const AUTHOR = 'author';
+ const PUBLISHED = 'published';
+ const UPDATED = 'updated';
+
+ public $actor; // an ActivityObject
+ public $verb; // a string (the URL)
+ public $object; // an ActivityObject
+ public $target; // an ActivityObject
+ public $context; // an ActivityObject
+ public $time; // Time of the activity
+ public $link; // an ActivityObject
+ public $entry; // the source entry
+ public $feed; // the source feed
+
+ /**
+ * Turns a regular old Atom <entry> into a magical activity
+ *
+ * @param DOMElement $entry Atom entry to poke at
+ * @param DOMElement $feed Atom feed, for context
+ */
+
+ function __construct($entry, $feed = null)
+ {
+ $this->entry = $entry;
+ $this->feed = $feed;
+
+ $pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM);
+
+ if (!empty($pubEl)) {
+ $this->time = strtotime($pubEl->textContent);
+ } else {
+ // XXX technically an error; being liberal. Good idea...?
+ $updateEl = $this->_child($entry, self::UPDATED, self::ATOM);
+ if (!empty($updateEl)) {
+ $this->time = strtotime($updateEl->textContent);
+ } else {
+ $this->time = null;
+ }
+ }
+
+ $this->link = ActivityUtils::getLink($entry);
+
+ $verbEl = $this->_child($entry, self::VERB);
+
+ if (!empty($verbEl)) {
+ $this->verb = trim($verbEl->textContent);
+ } else {
+ $this->verb = ActivityVerb::POST;
+ // XXX: do other implied stuff here
+ }
+
+ $objectEl = $this->_child($entry, self::OBJECT);
+
+ if (!empty($objectEl)) {
+ $this->object = new ActivityObject($objectEl);
+ } else {
+ $this->object = new ActivityObject($entry);
+ }
+
+ $actorEl = $this->_child($entry, self::ACTOR);
+
+ if (!empty($actorEl)) {
+
+ $this->actor = new ActivityObject($actorEl);
+
+ } else if (!empty($feed) &&
+ $subjectEl = $this->_child($feed, self::SUBJECT)) {
+
+ $this->actor = new ActivityObject($subjectEl);
+
+ } else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) {
+
+ $this->actor = new ActivityObject($authorEl);
+
+ } else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR,
+ self::ATOM)) {
+
+ $this->actor = new ActivityObject($authorEl);
+ }
+
+ $contextEl = $this->_child($entry, self::CONTEXT);
+
+ if (!empty($contextEl)) {
+ $this->context = new ActivityObject($contextEl);
+ }
+
+ $targetEl = $this->_child($entry, self::TARGET);
+
+ if (!empty($targetEl)) {
+ $this->target = new ActivityObject($targetEl);
+ }
+ }
+
+ /**
+ * Returns an Atom <entry> based on this activity
+ *
+ * @return DOMElement Atom entry
+ */
+
+ function toAtomEntry()
+ {
+ return null;
+ }
+
+ /**
+ * Gets the first child element with the given tag
+ *
+ * @param DOMElement $element element to pick at
+ * @param string $tag tag to look for
+ * @param string $namespace Namespace to look under
+ *
+ * @return DOMElement found element or null
+ */
+
+ private function _child($element, $tag, $namespace=self::SPEC)
+ {
+ $els = $element->getElementsByTagnameNS($namespace, $tag);
+
+ if (empty($els) || $els->length == 0) {
+ return null;
+ } else {
+ return $els->item(0);
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php
index 5dce95342..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
@@ -209,6 +223,7 @@ class FeedMunger
$notice->id = -1;
} else {
$notice = new Notice();
+ $notice->profile_id = $this->profileIdForEntry($index);
}
$link = $this->getAltLink($entry);
@@ -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 189ccbedf..245a57f72 100644
--- a/plugins/OStatus/lib/hubdistribqueuehandler.php
+++ b/plugins/OStatus/lib/hubdistribqueuehandler.php
@@ -38,6 +38,7 @@ class HubDistribQueueHandler extends QueueHandler
foreach ($notice->getGroups() as $group) {
$this->pushGroup($notice, $group->group_id);
}
+ return true;
}
function pushUser($notice)
@@ -48,14 +49,7 @@ class HubDistribQueueHandler extends QueueHandler
$feed = common_local_url('ApiTimelineUser',
array('id' => $notice->profile_id,
'format' => 'atom'));
- $sub = new HubSub();
- $sub->topic = $feed;
- if ($sub->find()) {
- $atom = $this->userFeedForNotice($notice);
- $this->pushFeeds($atom, $sub);
- } else {
- common_log(LOG_INFO, "No PuSH subscribers for $feed");
- }
+ $this->pushFeed($feed, array($this, 'userFeedForNotice'), $notice);
}
function pushGroup($notice, $group_id)
@@ -63,19 +57,69 @@ class HubDistribQueueHandler extends QueueHandler
$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, "Building PuSH feed for $feed");
- $atom = $this->groupFeedForNotice($group_id, $notice);
- $this->pushFeeds($atom, $sub);
+ $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;
}
-
- function pushFeeds($atom, $sub)
+ /**
+ * 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();
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
index 8c77222a6..8c77222a6 100644
--- a/plugins/OStatus/lib/Salmon.php
+++ b/plugins/OStatus/lib/salmon.php
diff --git a/plugins/OStatus/lib/Webfinger.php b/plugins/OStatus/lib/webfinger.php
index 417d54904..417d54904 100644
--- a/plugins/OStatus/lib/Webfinger.php
+++ b/plugins/OStatus/lib/webfinger.php
diff --git a/plugins/OStatus/lib/XRD.php b/plugins/OStatus/lib/xrd.php
index 16d27f8eb..16d27f8eb 100644
--- a/plugins/OStatus/lib/XRD.php
+++ b/plugins/OStatus/lib/xrd.php
diff --git a/plugins/OStatus/tests/ActivityParseTests.php b/plugins/OStatus/tests/ActivityParseTests.php
new file mode 100644
index 000000000..fa8bcdda2
--- /dev/null
+++ b/plugins/OStatus/tests/ActivityParseTests.php
@@ -0,0 +1,147 @@
+<?php
+
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+ print "This script must be run from the command line\n";
+ exit();
+}
+
+// XXX: we should probably have some common source for this stuff
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+define('STATUSNET', true);
+
+require_once INSTALLDIR . '/lib/common.php';
+require_once INSTALLDIR . '/plugins/OStatus/lib/activity.php';
+
+class ActivityParseTests extends PHPUnit_Framework_TestCase
+{
+ public function testExample1()
+ {
+ global $_example1;
+ $dom = DOMDocument::loadXML($_example1);
+ $act = new Activity($dom->documentElement);
+
+ $this->assertFalse(empty($act));
+ $this->assertEquals($act->time, 1243860840);
+ $this->assertEquals($act->verb, ActivityVerb::POST);
+ }
+
+ public function testExample3()
+ {
+ global $_example3;
+ $dom = DOMDocument::loadXML($_example3);
+
+ $feed = $dom->documentElement;
+
+ $entries = $feed->getElementsByTagName('entry');
+
+ $entry = $entries->item(0);
+
+ $act = new Activity($entry, $feed);
+
+ $this->assertFalse(empty($act));
+ $this->assertEquals($act->time, 1071340202);
+ $this->assertEquals($act->link, 'http://example.org/2003/12/13/atom03.html');
+
+ $this->assertEquals($act->verb, ActivityVerb::POST);
+
+ $this->assertFalse(empty($act->actor));
+ $this->assertEquals($act->actor->type, ActivityObject::PERSON);
+ $this->assertEquals($act->actor->title, 'John Doe');
+ $this->assertEquals($act->actor->id, 'mailto:johndoe@example.com');
+
+ $this->assertFalse(empty($act->object));
+ $this->assertEquals($act->object->type, ActivityObject::NOTE);
+ $this->assertEquals($act->object->id, 'urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a');
+ $this->assertEquals($act->object->title, 'Atom-Powered Robots Run Amok');
+ $this->assertEquals($act->object->summary, 'Some text.');
+ $this->assertEquals($act->object->link, 'http://example.org/2003/12/13/atom03.html');
+
+ $this->assertTrue(empty($act->context));
+ $this->assertTrue(empty($act->target));
+
+ $this->assertEquals($act->entry, $entry);
+ $this->assertEquals($act->feed, $feed);
+ }
+}
+
+$_example1 = <<<EXAMPLE1
+<?xml version='1.0' encoding='UTF-8'?>
+<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'>
+ <id>tag:versioncentral.example.org,2009:/commit/1643245</id>
+ <published>2009-06-01T12:54:00Z</published>
+ <title>Geraldine committed a change to yate</title>
+ <content type="xhtml">Geraldine just committed a change to yate on VersionCentral</content>
+ <link rel="alternate" type="text/html"
+ href="http://versioncentral.example.org/geraldine/yate/commit/1643245" />
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <activity:verb>http://versioncentral.example.org/activity/commit</activity:verb>
+ <activity:object>
+ <activity:object-type>http://versioncentral.example.org/activity/changeset</activity:object-type>
+ <id>tag:versioncentral.example.org,2009:/change/1643245</id>
+ <title>Punctuation Changeset</title>
+ <summary>Fixing punctuation because it makes it more readable.</summary>
+ <link rel="alternate" type="text/html" href="..." />
+ </activity:object>
+</entry>
+EXAMPLE1;
+
+$_example2 = <<<EXAMPLE2
+<?xml version='1.0' encoding='UTF-8'?>
+<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'>
+ <id>tag:photopanic.example.com,2008:activity01</id>
+ <title>Geraldine posted a Photo on PhotoPanic</title>
+ <published>2008-11-02T15:29:00Z</published>
+ <link rel="alternate" type="text/html" href="/geraldine/activities/1" />
+ <activity:verb>
+ http://activitystrea.ms/schema/1.0/post
+ </activity:verb>
+ <activity:object>
+ <id>tag:photopanic.example.com,2008:photo01</id>
+ <title>My Cat</title>
+ <published>2008-11-02T15:29:00Z</published>
+ <link rel="alternate" type="text/html" href="/geraldine/photos/1" />
+ <activity:object-type>
+ tag:atomactivity.example.com,2008:photo
+ </activity:object-type>
+ <source>
+ <title>Geraldine's Photos</title>
+ <link rel="self" type="application/atom+xml" href="/geraldine/photofeed.xml" />
+ <link rel="alternate" type="text/html" href="/geraldine/" />
+ </source>
+ </activity:object>
+ <content type="html">
+ &lt;p&gt;Geraldine posted a Photo on PhotoPanic&lt;/p&gt;
+ &lt;img src="/geraldine/photo1.jpg"&gt;
+ </content>
+</entry>
+EXAMPLE2;
+
+$_example3 = <<<EXAMPLE3
+<?xml version="1.0" encoding="utf-8"?>
+
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <title>Example Feed</title>
+ <subtitle>A subtitle.</subtitle>
+ <link href="http://example.org/feed/" rel="self" />
+ <link href="http://example.org/" />
+ <id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <author>
+ <name>John Doe</name>
+ <email>johndoe@example.com</email>
+ </author>
+
+ <entry>
+ <title>Atom-Powered Robots Run Amok</title>
+ <link href="http://example.org/2003/12/13/atom03" />
+ <link rel="alternate" type="text/html" href="http://example.org/2003/12/13/atom03.html"/>
+ <link rel="edit" href="http://example.org/2003/12/13/atom03/edit"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <summary>Some text.</summary>
+ </entry>
+
+</feed>
+EXAMPLE3;
diff --git a/plugins/OStatus/theme/base/css/ostatus.css b/plugins/OStatus/theme/base/css/ostatus.css
new file mode 100644
index 000000000..9bc90a731
--- /dev/null
+++ b/plugins/OStatus/theme/base/css/ostatus.css
@@ -0,0 +1,30 @@
+/** theme: base for OStatus
+ *
+ * @package StatusNet
+ * @author Sarven Capadisli <csarven@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+#form_ostatus_connect.dialogbox {
+width:70%;
+background-image:none;
+}
+#form_ostatus_connect.dialogbox .form_data label {
+width:34%;
+}
+#form_ostatus_connect.dialogbox .form_data input {
+width:57%;
+}
+#form_ostatus_connect.dialogbox .form_data .form_guide {
+margin-left:36%;
+}
+
+#form_ostatus_connect.dialogbox #ostatus_nickname {
+display:none;
+}
+
+#form_ostatus_connect.dialogbox .submit_dialogbox {
+min-width:96px;
+}
diff --git a/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php b/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php
index 14d1608d3..fb4eff738 100644
--- a/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php
+++ b/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php
@@ -45,6 +45,7 @@ class PoweredByStatusNetPlugin extends Plugin
{
function onEndAddressData($action)
{
+ $action->text(' ');
$action->elementStart('span', 'poweredby');
$action->raw(sprintf(_m('powered by %s'),
sprintf('<a href="http://status.net/">%s</a>',