OStatus sub/unsub updates:

- fix for PuSH unsub verification
- send Salmon notification on unsub
This commit is contained in:
Brion Vibber 2010-02-18 18:20:48 +00:00
parent c2ba764535
commit 22ff358ba8
11 changed files with 108 additions and 48 deletions

View File

@ -27,8 +27,7 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('STATUSNET') if (!defined('STATUSNET')) {
{
exit(1); exit(1);
} }
@ -87,7 +86,7 @@ class Atom10Entry extends XMLStringer
* *
* @return void * @return void
*/ */
function validate function validate()
{ {
} }

View File

@ -78,7 +78,7 @@ class Atom10Feed extends XMLStringer
$this->authors = array(); $this->authors = array();
$this->links = array(); $this->links = array();
$this->entries = array(); $this->entries = array();
$this->addNamespace('xmlns', 'http://www.w3.org/2005/Atom'); $this->addNamespace('', 'http://www.w3.org/2005/Atom');
} }
/** /**
@ -162,7 +162,14 @@ class Atom10Feed extends XMLStringer
{ {
$this->xw->startDocument('1.0', 'UTF-8'); $this->xw->startDocument('1.0', 'UTF-8');
$commonAttrs = array('xml:lang' => 'en-US'); $commonAttrs = array('xml:lang' => 'en-US');
$commonAttrs = array_merge($commonAttrs, $this->namespaces); foreach ($this->namespaces as $prefix => $uri) {
if ($prefix == '') {
$attr = 'xmlns';
} else {
$attr = 'xmlns:' . $prefix;
}
$commonAttrs[$attr] = $uri;
}
$this->elementStart('feed', $commonAttrs); $this->elementStart('feed', $commonAttrs);
$this->element('id', null, $this->id); $this->element('id', null, $this->id);

View File

@ -50,23 +50,23 @@ class AtomNoticeFeed extends Atom10Feed
// Feeds containing notice info use these namespaces // Feeds containing notice info use these namespaces
$this->addNamespace( $this->addNamespace(
'xmlns:thr', 'thr',
'http://purl.org/syndication/thread/1.0' 'http://purl.org/syndication/thread/1.0'
); );
$this->addNamespace( $this->addNamespace(
'xmlns:georss', 'georss',
'http://www.georss.org/georss' 'http://www.georss.org/georss'
); );
$this->addNamespace( $this->addNamespace(
'xmlns:activity', 'activity',
'http://activitystrea.ms/spec/1.0/' 'http://activitystrea.ms/spec/1.0/'
); );
// XXX: What should the uri be? // XXX: What should the uri be?
$this->addNamespace( $this->addNamespace(
'xmlns:ostatus', 'ostatus',
'http://ostatus.org/schema/1.0' 'http://ostatus.org/schema/1.0'
); );
} }

View File

@ -112,7 +112,7 @@ class OStatusPlugin extends Plugin
* Set up a PuSH hub link to our internal link for canonical timeline * Set up a PuSH hub link to our internal link for canonical timeline
* Atom feeds for users and groups. * Atom feeds for users and groups.
*/ */
function onStartApiAtom(AtomNoticeFeed $feed) function onStartApiAtom($feed)
{ {
$id = null; $id = null;
@ -171,6 +171,12 @@ class OStatusPlugin extends Plugin
{ {
$base = dirname(__FILE__); $base = dirname(__FILE__);
$lower = strtolower($cls); $lower = strtolower($cls);
$map = array('activityverb' => 'activity',
'activityobject' => 'activity',
'activityutils' => 'activity');
if (isset($map[$lower])) {
$lower = $map[$lower];
}
$files = array("$base/classes/$cls.php", $files = array("$base/classes/$cls.php",
"$base/lib/$lower.php"); "$base/lib/$lower.php");
if (substr($lower, -6) == 'action') { if (substr($lower, -6) == 'action') {
@ -253,18 +259,45 @@ class OStatusPlugin extends Plugin
} }
/** /**
* Garbage collect unused feeds on unsubscribe * Notify remote server when one of our users subscribes.
* @fixme Check and restart the PuSH subscription if needed
*
* @param User $user
* @param Profile $other
* @return hook return value
*/
function onEndSubscribe($user, $other)
{
$oprofile = Ostatus_profile::staticGet('profile_id', $other->id);
if ($oprofile) {
// Notify the remote server of the unsub, if supported.
$oprofile->notify($user->getProfile(), ActivityVerb::FOLLOW, $oprofile);
}
return true;
}
/**
* Notify remote server and garbage collect unused feeds on unsubscribe.
* @fixme send these operations to background queues
*
* @param User $user
* @param Profile $other
* @return hook return value
*/ */
function onEndUnsubscribe($user, $other) function onEndUnsubscribe($user, $other)
{ {
$profile = Ostatus_profile::staticGet('profile_id', $other->id); $oprofile = Ostatus_profile::staticGet('profile_id', $other->id);
if ($feed) { if ($oprofile) {
// Notify the remote server of the unsub, if supported.
$oprofile->notify($user->getProfile(), ActivityVerb::UNFOLLOW, $oprofile);
// Drop the PuSH subscription if there are no other subscribers.
$sub = new Subscription(); $sub = new Subscription();
$sub->subscribed = $other->id; $sub->subscribed = $other->id;
$sub->limit(1); $sub->limit(1);
if (!$sub->find(true)) { if (!$sub->find(true)) {
common_log(LOG_INFO, "Unsubscribing from now-unused feed $feed->feeduri on hub $feed->huburi"); common_log(LOG_INFO, "Unsubscribing from now-unused feed $oprofile->feeduri on hub $oprofile->huburi");
$profile->unsubscribe(); $oprofile->unsubscribe();
} }
} }
return true; return true;
@ -290,6 +323,16 @@ class OStatusPlugin extends Plugin
return true; return true;
} }
/**
* Override the "from ostatus" bit in notice lists to link to the
* original post and show the domain it came from.
*
* @param Notice in $notice
* @param string out &$name
* @param string out &$url
* @param string out &$title
* @return mixed hook return code
*/
function onStartNoticeSourceLink($notice, &$name, &$url, &$title) function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
{ {
if ($notice->source == 'ostatus') { if ($notice->source == 'ostatus') {

View File

@ -89,7 +89,7 @@ class PushCallbackAction extends Action
if ($profile->verify_token !== $verify_token) { if ($profile->verify_token !== $verify_token) {
common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); 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); throw new ServerException("Bogus hub callback: bad token", 404);
} }
if ($mode != $profile->sub_state) { if ($mode != $profile->sub_state) {

View File

@ -83,6 +83,7 @@ class PushHubAction extends Action
{ {
$feed = $this->argUrl('hub.topic'); $feed = $this->argUrl('hub.topic');
$callback = $this->argUrl('hub.callback'); $callback = $this->argUrl('hub.callback');
$token = $this->arg('hub.verify_token', null);
common_log(LOG_DEBUG, __METHOD__ . ": checking sub'd to $feed $callback"); common_log(LOG_DEBUG, __METHOD__ . ": checking sub'd to $feed $callback");
if ($this->getSub($feed, $callback)) { if ($this->getSub($feed, $callback)) {
@ -96,7 +97,6 @@ class PushHubAction extends Action
$sub = new HubSub(); $sub = new HubSub();
$sub->topic = $feed; $sub->topic = $feed;
$sub->callback = $callback; $sub->callback = $callback;
$sub->verify_token = $this->arg('hub.verify_token', null);
$sub->secret = $this->arg('hub.secret', null); $sub->secret = $this->arg('hub.secret', null);
if (strlen($sub->secret) > 200) { if (strlen($sub->secret) > 200) {
throw new ClientException("hub.secret must be no longer than 200 chars", 400); throw new ClientException("hub.secret must be no longer than 200 chars", 400);
@ -115,7 +115,7 @@ class PushHubAction extends Action
// @fixme check errors ;) // @fixme check errors ;)
$data = array('sub' => $sub, 'mode' => 'subscribe'); $data = array('sub' => $sub, 'mode' => 'subscribe', 'token' => $token);
$qm = QueueManager::get(); $qm = QueueManager::get();
$qm->enqueue($data, 'hubverify'); $qm->enqueue($data, 'hubverify');
@ -130,6 +130,8 @@ class PushHubAction extends Action
* 202 Accepted - request saved and awaiting verification * 202 Accepted - request saved and awaiting verification
* 204 No Content - already subscribed * 204 No Content - already subscribed
* 400 Bad Request - invalid params or rejected feed * 400 Bad Request - invalid params or rejected feed
*
* @fixme background this
*/ */
function unsubscribe() function unsubscribe()
{ {
@ -138,7 +140,8 @@ class PushHubAction extends Action
$sub = $this->getSub($feed, $callback); $sub = $this->getSub($feed, $callback);
if ($sub) { if ($sub) {
if ($sub->verify('unsubscribe')) { $token = $this->arg('hub.verify_token', null);
if ($sub->verify('unsubscribe', $token)) {
$sub->delete(); $sub->delete();
common_log(LOG_INFO, "PuSH unsubscribed $feed for $callback"); common_log(LOG_INFO, "PuSH unsubscribed $feed for $callback");
} else { } else {

View File

@ -34,6 +34,8 @@ class SalmonAction extends Action
function prepare($args) function prepare($args)
{ {
parent::prepare($args);
if ($_SERVER['REQUEST_METHOD'] != 'POST') { if ($_SERVER['REQUEST_METHOD'] != 'POST') {
$this->clientError(_('This method requires a POST.')); $this->clientError(_('This method requires a POST.'));
} }

View File

@ -30,7 +30,6 @@ class HubSub extends Memcached_DataObject
public $topic; public $topic;
public $callback; public $callback;
public $secret; public $secret;
public $verify_token;
public $challenge; public $challenge;
public $lease; public $lease;
public $sub_start; public $sub_start;
@ -62,7 +61,6 @@ class HubSub extends Memcached_DataObject
'topic' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, 'topic' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
'callback' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, 'callback' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
'secret' => DB_DATAOBJECT_STR, 'secret' => DB_DATAOBJECT_STR,
'verify_token' => DB_DATAOBJECT_STR,
'challenge' => DB_DATAOBJECT_STR, 'challenge' => DB_DATAOBJECT_STR,
'lease' => DB_DATAOBJECT_INT, 'lease' => DB_DATAOBJECT_INT,
'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
@ -84,8 +82,6 @@ class HubSub extends Memcached_DataObject
255, false), 255, false),
new ColumnDef('secret', 'text', new ColumnDef('secret', 'text',
null, true), null, true),
new ColumnDef('verify_token', 'text',
null, true),
new ColumnDef('challenge', 'varchar', new ColumnDef('challenge', 'varchar',
32, true), 32, true),
new ColumnDef('lease', 'int', new ColumnDef('lease', 'int',
@ -154,8 +150,9 @@ class HubSub extends Memcached_DataObject
/** /**
* Send a verification ping to subscriber * Send a verification ping to subscriber
* @param string $mode 'subscribe' or 'unsubscribe' * @param string $mode 'subscribe' or 'unsubscribe'
* @param string $token hub.verify_token value, if provided by client
*/ */
function verify($mode) function verify($mode, $token=null)
{ {
assert($mode == 'subscribe' || $mode == 'unsubscribe'); assert($mode == 'subscribe' || $mode == 'unsubscribe');
@ -172,8 +169,8 @@ class HubSub extends Memcached_DataObject
if ($mode == 'subscribe') { if ($mode == 'subscribe') {
$params['hub.lease_seconds'] = $this->lease; $params['hub.lease_seconds'] = $this->lease;
} }
if ($this->verify_token) { if ($token !== null) {
$params['hub.verify_token'] = $this->verify_token; $params['hub.verify_token'] = $token;
} }
$url = $this->callback . '?' . http_build_query($params, '', '&'); // @fixme ugly urls $url = $this->callback . '?' . http_build_query($params, '', '&'); // @fixme ugly urls

View File

@ -484,7 +484,7 @@ class Ostatus_profile extends Memcached_DataObject
} else { } else {
$this->sub_end = null; $this->sub_end = null;
} }
$this->lastupdate = common_sql_date(); $this->lastupdate = common_sql_now();
return $this->update($original); return $this->update($original);
} }
@ -497,12 +497,13 @@ class Ostatus_profile extends Memcached_DataObject
{ {
$original = clone($this); $original = clone($this);
$this->verify_token = null; // @fixme these should all be null, but DB_DataObject doesn't save null values...?????
$this->secret = null; $this->verify_token = '';
$this->sub_state = null; $this->secret = '';
$this->sub_start = null; $this->sub_state = '';
$this->sub_end = null; $this->sub_start = '';
$this->lastupdate = common_sql_date(); $this->sub_end = '';
$this->lastupdate = common_sql_now();
return $this->update($original); return $this->update($original);
} }
@ -527,24 +528,25 @@ class Ostatus_profile extends Memcached_DataObject
':' . $actor->id . ':' . $actor->id .
':' . time(); // @fixme ':' . time(); // @fixme
$entry = new Atom10Entry(); //$entry = new Atom10Entry();
$entry = new XMLStringer();
$entry->elementStart('entry'); $entry->elementStart('entry');
$entry->element('id', null, $id); $entry->element('id', null, $id);
$entry->element('title', null, $text); $entry->element('title', null, $text);
$entry->element('summary', null, $text); $entry->element('summary', null, $text);
$entry->element('published', null, common_date_w3dtf()); $entry->element('published', null, common_date_w3dtf(time()));
$entry->element('activity:verb', null, $verb); $entry->element('activity:verb', null, $verb);
$entry->raw($profile->asAtomAuthor()); $entry->raw($actor->asAtomAuthor());
$entry->raw($profile->asActivityActor()); $entry->raw($actor->asActivityActor());
$entry->raw($object->asActivityNoun('object')); $entry->raw($object->asActivityNoun('object'));
$entry->elmentEnd('entry'); $entry->elementEnd('entry');
$feed = $this->atomFeed($actor); $feed = $this->atomFeed($actor);
$feed->initFeed(); #$feed->initFeed();
$feed->addEntry($entry); $feed->addEntry($entry);
$feed->renderEntries(); #$feed->renderEntries();
$feed->endFeed(); #$feed->endFeed();
$xml = $feed->getString(); $xml = $feed->getString();
common_log(LOG_INFO, "Posting to Salmon endpoint $salmon: $xml"); common_log(LOG_INFO, "Posting to Salmon endpoint $salmon: $xml");
@ -568,7 +570,7 @@ class Ostatus_profile extends Memcached_DataObject
$feed = new Atom10Feed(); $feed = new Atom10Feed();
// @fixme should these be set up somewhere else? // @fixme should these be set up somewhere else?
$feed->addNamespace('activity', 'http://activitystrea.ms/spec/1.0/'); $feed->addNamespace('activity', 'http://activitystrea.ms/spec/1.0/');
$feed->addNamesapce('thr', 'http://purl.org/syndication/thread/1.0'); $feed->addNamespace('thr', 'http://purl.org/syndication/thread/1.0');
$feed->addNamespace('georss', 'http://www.georss.org/georss'); $feed->addNamespace('georss', 'http://www.georss.org/georss');
$feed->addNamespace('ostatus', 'http://ostatus.org/schema/1.0'); $feed->addNamespace('ostatus', 'http://ostatus.org/schema/1.0');
@ -579,14 +581,14 @@ class Ostatus_profile extends Memcached_DataObject
$feed->setUpdated(time()); $feed->setUpdated(time());
$feed->setPublished(time()); $feed->setPublished(time());
$feed->addLink(common_url('ApiTimelineUser', $feed->addLink(common_local_url('ApiTimelineUser',
array('id' => $actor->id, array('id' => $actor->id,
'type' => 'atom')), 'type' => 'atom')),
array('rel' => 'self', array('rel' => 'self',
'type' => 'application/atom+xml')); 'type' => 'application/atom+xml'));
$feed->addLink(common_url('userbyid', $feed->addLink(common_local_url('userbyid',
array('id' => $actor->id)), array('id' => $actor->id)),
array('rel' => 'alternate', array('rel' => 'alternate',
'type' => 'text/html')); 'type' => 'text/html'));

View File

@ -303,6 +303,12 @@ class ActivityVerb
const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend'; const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend';
const JOIN = 'http://activitystrea.ms/schema/1.0/join'; const JOIN = 'http://activitystrea.ms/schema/1.0/join';
const TAG = 'http://activitystrea.ms/schema/1.0/tag'; const TAG = 'http://activitystrea.ms/schema/1.0/tag';
// Custom OStatus verbs for the flipside until they're standardized
const DELETE = 'http://ostatus.org/schema/1.0/unfollow';
const UNFAVORITE = 'http://ostatus.org/schema/1.0/unfavorite';
const UNFOLLOW = 'http://ostatus.org/schema/1.0/unfollow';
const LEAVE = 'http://ostatus.org/schema/1.0/leave';
} }
/** /**

View File

@ -33,13 +33,14 @@ class HubVerifyQueueHandler extends QueueHandler
{ {
$sub = $data['sub']; $sub = $data['sub'];
$mode = $data['mode']; $mode = $data['mode'];
$token = $data['token'];
assert($sub instanceof HubSub); assert($sub instanceof HubSub);
assert($mode === 'subscribe' || $mode === 'unsubscribe'); assert($mode === 'subscribe' || $mode === 'unsubscribe');
common_log(LOG_INFO, __METHOD__ . ": $mode $sub->callback $sub->topic"); common_log(LOG_INFO, __METHOD__ . ": $mode $sub->callback $sub->topic");
try { try {
$sub->verify($mode); $sub->verify($mode, $token);
} catch (Exception $e) { } catch (Exception $e) {
common_log(LOG_ERR, "Failed PuSH $mode verify to $sub->callback for $sub->topic: " . common_log(LOG_ERR, "Failed PuSH $mode verify to $sub->callback for $sub->topic: " .
$e->getMessage()); $e->getMessage());