From 22ff358ba8d1fd0396136e1de570d788dd0727b6 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Thu, 18 Feb 2010 18:20:48 +0000 Subject: OStatus sub/unsub updates: - fix for PuSH unsub verification - send Salmon notification on unsub --- plugins/OStatus/OStatusPlugin.php | 55 ++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 6 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index b6c9fa1d4..e548a151c 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -112,7 +112,7 @@ 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(AtomNoticeFeed $feed) + function onStartApiAtom($feed) { $id = null; @@ -171,6 +171,12 @@ class OStatusPlugin extends Plugin { $base = dirname(__FILE__); $lower = strtolower($cls); + $map = array('activityverb' => 'activity', + 'activityobject' => 'activity', + 'activityutils' => 'activity'); + if (isset($map[$lower])) { + $lower = $map[$lower]; + } $files = array("$base/classes/$cls.php", "$base/lib/$lower.php"); 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) { - $profile = Ostatus_profile::staticGet('profile_id', $other->id); - if ($feed) { + $oprofile = Ostatus_profile::staticGet('profile_id', $other->id); + 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->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(); + common_log(LOG_INFO, "Unsubscribing from now-unused feed $oprofile->feeduri on hub $oprofile->huburi"); + $oprofile->unsubscribe(); } } return true; @@ -290,6 +323,16 @@ class OStatusPlugin extends Plugin 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) { if ($notice->source == 'ostatus') { -- cgit v1.2.3-54-g00ecf From 0dac13d197248bf24ea51cb7911d32286764c0c8 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Thu, 18 Feb 2010 21:22:21 +0000 Subject: OStatus refactoring to clean up profile vs feed and fix up subscription issues. PuSH subscription maintenance broken back out to FeedSub, letting Ostatus_profile deal with the profile level (user or group, with unique id URI) --- plugins/OStatus/OStatusPlugin.php | 53 +-- plugins/OStatus/actions/feedsubsettings.php | 99 ++--- plugins/OStatus/actions/pushcallback.php | 22 +- plugins/OStatus/actions/salmon.php | 32 +- plugins/OStatus/classes/FeedSub.php | 443 ++++++++++++++++++++++ plugins/OStatus/classes/Ostatus_profile.php | 544 +++++++++------------------- plugins/OStatus/lib/feeddiscovery.php | 50 ++- plugins/OStatus/lib/feedmunger.php | 350 ------------------ 8 files changed, 741 insertions(+), 852 deletions(-) create mode 100644 plugins/OStatus/classes/FeedSub.php delete mode 100644 plugins/OStatus/lib/feedmunger.php (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index e548a151c..4ebe4551e 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -1,17 +1,7 @@ -Author URI: http://status.net/ -*/ - /* * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2009, StatusNet, Inc. + * 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 @@ -28,17 +18,12 @@ Author URI: http://status.net/ */ /** - * @package FeedSubPlugin + * @package OStatusPlugin * @maintainer Brion Vibber */ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -define('FEEDSUB_SERVICE', 100); // fixme -- avoid hardcoding these? - -// We bundle the XML_Parse_Feed library... -set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib'); - class FeedSubException extends Exception { } @@ -258,24 +243,6 @@ class OStatusPlugin extends Plugin } } - /** - * 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 @@ -309,6 +276,7 @@ class OStatusPlugin extends Plugin function onCheckSchema() { $schema = Schema::get(); $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef()); + $schema->ensureTable('feedsub', FeedSub::schemaDef()); $schema->ensureTable('hubsub', HubSub::schemaDef()); return true; } @@ -345,4 +313,19 @@ class OStatusPlugin extends Plugin return false; } } + + /** + * Send incoming PuSH feeds for OStatus endpoints in for processing. + * + * @param FeedSub $feedsub + * @param DOMDocument $feed + * @return mixed hook return code + */ + function onStartFeedSubReceive($feedsub, $feed) + { + $oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri); + if ($oprofile) { + $oprofile->processFeed($feed); + } + } } diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php index 6933c9bf2..3e1d0aa82 100644 --- a/plugins/OStatus/actions/feedsubsettings.php +++ b/plugins/OStatus/actions/feedsubsettings.php @@ -26,7 +26,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } class FeedSubSettingsAction extends ConnectSettingsAction { - protected $feedurl; + protected $profile_uri; protected $preview; protected $munger; @@ -88,7 +88,10 @@ class FeedSubSettingsAction extends ConnectSettingsAction $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->input('profile_uri', + _m('Feed URL'), + $this->profile_uri, + _m('Enter the profile URL of a PubSubHubbub-enabled feed')); $this->elementEnd('li'); $this->elementEnd('ul'); @@ -145,79 +148,55 @@ class FeedSubSettingsAction extends ConnectSettingsAction */ function validateFeed() { - $feedurl = trim($this->arg('feedurl')); + $profile_uri = trim($this->arg('profile_uri')); - if ($feedurl == '') { - $this->showForm(_m('Empty feed URL!')); + if ($profile_uri == '') { + $this->showForm(_m('Empty remote profile URL!')); return; } - $this->feedurl = $feedurl; + $this->profile_uri = $profile_uri; - // Get the canonical feed URI and check it + // @fixme validate, normalize bla bla try { - $discover = new FeedDiscovery(); - $uri = $discover->discoverFromURL($feedurl); + $oprofile = Ostatus_profile::ensureProfile($this->profile_uri); + $this->oprofile = $oprofile; + return true; } catch (FeedSubBadURLException $e) { - $this->showForm(_m('Invalid URL or could not reach server.')); - return false; + $err = _m('Invalid URL or could not reach server.'); } catch (FeedSubBadResponseException $e) { - $this->showForm(_m('Cannot read feed; server returned error.')); - return false; + $err = _m('Cannot read feed; server returned error.'); } catch (FeedSubEmptyException $e) { - $this->showForm(_m('Cannot read feed; server returned an empty page.')); - return false; + $err = _m('Cannot read feed; server returned an empty page.'); } catch (FeedSubBadHTMLException $e) { - $this->showForm(_m('Bad HTML, could not find feed link.')); - return false; + $err = _m('Bad HTML, could not find feed link.'); } catch (FeedSubNoFeedException $e) { - $this->showForm(_m('Could not find a feed linked from this URL.')); - return false; + $err = _m('Could not find a feed linked from this URL.'); } catch (FeedSubUnrecognizedTypeException $e) { - $this->showForm(_m('Not a recognized feed type.')); - return false; + $err = _m('Not a recognized feed type.'); } catch (FeedSubException $e) { // Any new ones we forgot about - $this->showForm(_m('Bad feed URL.')); - return false; + $err = sprintf(_m('Bad feed URL: %s %s'), get_class($e), $e->getMessage()); } - - $this->munger = $discover->feedMunger(); - $this->profile = $this->munger->ostatusProfile(); - if ($this->profile->huburi == '' && !common_config('feedsub', 'nohub')) { - $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.')); - return false; - } - - return true; + $this->showForm($err); + return false; } function saveFeed() { if ($this->validateFeed()) { $this->preview = true; - $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->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(); - if ($this->profile->isGroup()) { - $group = $this->profile->localGroup(); + if (!$this->oprofile->subscribe()) { + $this->showForm(_m("Failed to set up server-to-server subscription.")); + return; + } + + if ($this->oprofile->isGroup()) { + $group = $this->oprofile->localGroup(); if ($user->isMember($group)) { $this->showForm(_m('Already a member!')); } elseif (Group_member::join($this->profile->group_id, $user->id)) { @@ -226,13 +205,13 @@ class FeedSubSettingsAction extends ConnectSettingsAction $this->showForm(_m('Remote group join failed!')); } } else { - $local = $this->profile->localProfile(); + $local = $this->oprofile->localProfile(); if ($user->isSubscribed($local)) { $this->showForm(_m('Already subscribed!')); - } elseif ($user->subscribeTo($local)) { - $this->showForm(_m('Feed subscribed!')); + } elseif ($this->oprofile->subscribeLocalToRemote($user)) { + $this->showForm(_m('Remote user subscribed!')); } else { - $this->showForm(_m('Feed subscription failed!')); + $this->showForm(_m('Remote subscription failed!')); } } } @@ -248,17 +227,7 @@ class FeedSubSettingsAction extends ConnectSettingsAction 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.'); - } + $this->text('Profile preview should go here'); } function showScripts() diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php index ed859a32f..7e1227a66 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); } - $profile = Ostatus_profile::staticGet('id', $feedid); - if (!$profile) { - throw new ServerException('Unknown OStatus/PuSH feed id ' . $feedid, 400); + $feedsub = FeedSub::staticGet('id', $feedid); + if (!$feedsub) { + throw new ServerException('Unknown PuSH feed id ' . $feedid, 400); } $hmac = ''; @@ -62,7 +62,7 @@ class PushCallbackAction extends Action // @fixme Queue this to a background process; we should return // as quickly as possible from a distribution POST. - $profile->postUpdates($post, $hmac); + $feedsub->receive($post, $hmac); } /** @@ -81,29 +81,29 @@ class PushCallbackAction extends Action throw new ServerException("Bogus hub callback: bad mode", 404); } - $profile = Ostatus_profile::staticGet('feeduri', $topic); - if (!$profile) { + $feedsub = FeedSub::staticGet('uri', $topic); + if (!$feedsub) { common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic"); throw new ServerException("Bogus hub callback: unknown feed", 404); } - if ($profile->verify_token !== $verify_token) { + if ($feedsub->verify_token !== $verify_token) { common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); throw new ServerException("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}\""); + if ($mode != $feedsub->sub_state) { + common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad mode \"$mode\" for feed $topic in state \"{$feedsub->sub_state}\""); throw new ServerException("Bogus hub callback: mode doesn't match subscription state.", 404); } // OK! if ($mode == 'subscribe') { common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); - $profile->confirmSubscribe($lease_seconds); + $feedsub->confirmSubscribe($lease_seconds); } else { common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic"); - $profile->confirmUnsubscribe(); + $feedsub->confirmUnsubscribe(); } print $challenge; } diff --git a/plugins/OStatus/actions/salmon.php b/plugins/OStatus/actions/salmon.php index 224134cd7..ea5b8e4ea 100644 --- a/plugins/OStatus/actions/salmon.php +++ b/plugins/OStatus/actions/salmon.php @@ -68,6 +68,9 @@ class SalmonAction extends Action return true; } + /** + * @fixme probably call Ostatus_profile::processFeed + */ function handle($args) { common_log(LOG_INFO, 'Salmon: incoming post for user '. $this->user->id); @@ -95,6 +98,9 @@ class SalmonAction extends Action } } + /** + * @fixme probably call Ostatus_profile::processFeed + */ function handlePost() { switch ($this->act->object->type) { @@ -111,14 +117,23 @@ class SalmonAction extends Action $profile = $this->ensureProfile(); } + /** + * @fixme probably call Ostatus_profile::processFeed + */ function handleFollow() { } + /** + * @fixme probably call Ostatus_profile::processFeed + */ function handleFavorite() { } + /** + * @fixme probably call Ostatus_profile::processFeed + */ function handleShare() { } @@ -131,17 +146,13 @@ class SalmonAction extends Action throw new Exception("Received a salmon slap from unidentified actor."); } - $ostatusProfile = Ostatus_profile::staticGet('homeuri', $actor->id); - - if (empty($ostatusProfile)) { - return $this->createProfile(); - } else { - // XXX: can we receive a salmon slap from a group...? - assert(!empty($ostatusProfile->profile_id)); - return Profile::staticGet($ostatusProfile->profile_id); - } + $ostatusProfile = Ostatus_profile::ensureActorProfile($this->act); + return $oprofile->localProfile(); } + /** + * @fixme anything new in here probably should be merged into Ostatus_profile::ensureActorProfile and friends + */ function createProfile() { $actor = $this->act->actor; @@ -186,6 +197,9 @@ class SalmonAction extends Action return $profile; } + /** + * @fixme should be merged into Ostatus_profile + */ function nicknameFromURI($uri) { preg_match('/(\w+):/', $uri, $matches); diff --git a/plugins/OStatus/classes/FeedSub.php b/plugins/OStatus/classes/FeedSub.php new file mode 100644 index 000000000..dc2c0b710 --- /dev/null +++ b/plugins/OStatus/classes/FeedSub.php @@ -0,0 +1,443 @@ +. + */ + +/** + * @package OStatusPlugin + * @maintainer Brion Vibber + */ + +/* +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; + } +} + +/** + * FeedSub handles low-level PubHubSubbub (PuSH) subscriptions. + * Higher-level behavior building OStatus stuff on top is handled + * under Ostatus_profile. + */ +class FeedSub extends Memcached_DataObject +{ + public $__table = 'feedsub'; + + public $id; + public $feeduri; + + // PuSH subscription data + public $huburi; + public $secret; + public $verify_token; + public $sub_state; // subscribe, active, unsubscribe, inactive + public $sub_start; + public $sub_end; + public $last_update; + + public $created; + public $modified; + + 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, + 'uri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'huburi' => DB_DATAOBJECT_STR, + 'secret' => DB_DATAOBJECT_STR, + 'verify_token' => DB_DATAOBJECT_STR, + 'sub_state' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'last_update' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, + 'modified' => 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('uri', 'varchar', + 255, false, 'UNI'), + new ColumnDef('huburi', 'text', + null, true), + new ColumnDef('verify_token', 'text', + null, true), + new ColumnDef('secret', 'text', + null, true), + new ColumnDef('sub_state', "enum('subscribe','active','unsubscribe','inactive')", + null, false), + new ColumnDef('sub_start', 'datetime', + null, true), + new ColumnDef('sub_end', 'datetime', + null, true), + new ColumnDef('last_update', 'datetime', + null, false), + new ColumnDef('created', 'datetime', + null, false), + new ColumnDef('modified', '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', 'uri' => '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 string $feeduri + * @return FeedSub + * @throws FeedSubException if feed is invalid or lacks PuSH setup + */ + public static function ensureFeed($feeduri) + { + $current = self::staticGet('uri', $feeduri); + if ($current) { + return $current; + } + + $discover = new FeedDiscovery(); + $discover->discoverFromFeedURL($feeduri); + + $huburi = $discover->getAtomLink('hub'); + if (!$huburi) { + throw new FeedSubNoHubException(); + } + + $feedsub = new FeedSub(); + $feedsub->uri = $feeduri; + $feedsub->huburi = $huburi; + $feedsub->sub_state = 'inactive'; + + $feedsub->created = common_sql_now(); + $feedsub->modified = common_sql_now(); + + $result = $feedsub->insert(); + if (empty($result)) { + throw new FeedDBException($feedsub); + } + + return $feedsub; + } + + /** + * 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 + * @throws ServerException if feed state is not valid + */ + public function subscribe($mode='subscribe') + { + if ($this->sub_state && $this->sub_state != 'inactive') { + throw new ServerException("Attempting to start PuSH subscription to feed in state $this->sub_state"); + } + if (empty($this->huburi)) { + if (common_config('feedsub', 'nohub')) { + // Fake it! We're just testing remote feeds w/o hubs. + return true; + } else { + throw new ServerException("Attempting to start PuSH subscription for feed with no hub"); + } + } + + return $this->doSubscribe('subscribe'); + } + + /** + * 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 + * @throws ServerException if feed state is not valid + */ + public function unsubscribe() { + if ($this->sub_state != 'active') { + throw new ServerException("Attempting to end PuSH subscription to feed in state $this->sub_state"); + } + if (empty($this->huburi)) { + if (common_config('feedsub', 'nohub')) { + // Fake it! We're just testing remote feeds w/o hubs. + return true; + } else { + throw new ServerException("Attempting to end PuSH subscription for feed with no hub"); + } + } + + return $this->doSubscribe('unsubscribe'); + } + + protected function doSubscribe($mode) + { + $orig = clone($this); + $this->verify_token = common_good_rand(16); + if ($mode == 'subscribe') { + $this->secret = common_good_rand(32); + } + $this->sub_state = $mode; + $this->update($orig); + unset($orig); + + 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->uri); + $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->uri"); + + $orig = clone($this); + $this->verify_token = null; + $this->sub_state = null; + $this->update($orig); + unset($orig); + + 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_now(); + + return $this->update($original); + } + + /** + * Save PuSH unsubscription confirmation. + * Wipes active PuSH sub info and resets state. + */ + public function confirmUnsubscribe() + { + $original = clone($this); + + // @fixme these should all be null, but DB_DataObject doesn't save null values...????? + $this->verify_token = ''; + $this->secret = ''; + $this->sub_state = ''; + $this->sub_start = ''; + $this->sub_end = ''; + $this->lastupdate = common_sql_now(); + + return $this->update($original); + } + + /** + * Accept updates from a PuSH feed. If validated, this object and the + * feed (as a DOMDocument) will be passed to the StartFeedSubHandleFeed + * and EndFeedSubHandleFeed events for processing. + * + * @param string $post source of Atom or RSS feed + * @param string $hmac X-Hub-Signature header, if present + */ + public function receive($post, $hmac) + { + common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->uri\"! $hmac $post"); + + if ($this->sub_state != 'active') { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH for inactive feed $this->uri (in state '$this->sub_state')"); + return; + } + + if ($post === '') { + common_log(LOG_ERR, __METHOD__ . ": ignoring empty post"); + return; + } + + if (!$this->validatePushSig($post, $hmac)) { + // Per spec we silently drop input with a bad sig, + // while reporting receipt to the server. + return; + } + + $feed = new DOMDocument(); + if (!$feed->loadXML($post)) { + // @fixme might help to include the err message + common_log(LOG_ERR, __METHOD__ . ": ignoring invalid XML"); + return; + } + + Event::handle('StartFeedSubReceive', array($this, $feed)); + Event::handle('EndFeedSubReceive', array($this, $feed)); + } + + /** + * Validate the given Atom chunk and HMAC signature against our + * shared secret that was set up at subscription time. + * + * If we don't have a shared secret, there should be no signature. + * If we we do, our the calculated HMAC should match theirs. + * + * @param string $post raw XML source as POSTed to us + * @param string $hmac X-Hub-Signature HTTP header value, or empty + * @return boolean true for a match + */ + protected function validatePushSig($post, $hmac) + { + if ($this->secret) { + if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) { + $their_hmac = strtolower($matches[1]); + $our_hmac = hash_hmac('sha1', $post, $this->secret); + if ($their_hmac === $our_hmac) { + return true; + } + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac"); + } else { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'"); + } + } else { + if (empty($hmac)) { + return true; + } else { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'"); + } + } + return false; + } + +} diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 486417617..1ce8ac491 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -18,62 +18,24 @@ */ /** - * @package FeedSubPlugin + * @package OStatusPlugin * @maintainer Brion Vibber */ -/* -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 $uri; + 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 $modified; public /*static*/ function staticGet($k, $v=null) { @@ -91,56 +53,30 @@ class Ostatus_profile extends Memcached_DataObject function table() { - return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + return array('uri' => DB_DATAOBJECT_STR + 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); + 'modified' => 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), + return array(new ColumnDef('uri', 'varchar', + 255, false, 'PRI'), 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', + new ColumnDef('modified', 'datetime', null, false)); } @@ -169,12 +105,12 @@ class Ostatus_profile extends Memcached_DataObject function keyTypes() { - return array('id' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U'); + return array('uri' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U'); } function sequenceKey() { - return array('id', true, false); + return array(false, false, false); } /** @@ -201,101 +137,6 @@ class Ostatus_profile extends Memcached_DataObject 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'); - - try { - $local = $munger->profile(); - - if ($profile->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 = common_sql_now(); - $profile->lastupdate = common_sql_now(); - $result = $profile->insert(); - if (empty($result)) { - throw new FeedDBException($profile); - } - - $profile->query('COMMIT'); - } catch (FeedDBException $e) { - common_log_db_error($e->obj, 'INSERT', __FILE__); - $profile->query('ROLLBACK'); - return false; - } - - $avatar = $munger->getAvatar(); - if ($avatar) { - try { - $profile->updateAvatar($avatar); - } catch (Exception $e) { - common_log(LOG_ERR, "Exception setting OStatus avatar: " . - $e->getMessage()); - } - } - - return $profile; - } - - /** - * 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); - - // @fixme should we be using different ids? - $imagefile = new ImageFile($this->id, $temp_filename); - $filename = Avatar::filename($this->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. @@ -345,7 +186,7 @@ class Ostatus_profile extends Memcached_DataObject $xs->element( 'id', null, - $this->homeuri); // ? + $this->uri); // ? $xs->element('title', null, $self->getBestName()); $xs->element( @@ -370,142 +211,95 @@ class Ostatus_profile extends Memcached_DataObject } /** - * Send a subscription request to the hub for this feed. - * The hub will later send us a confirmation POST to /main/push/callback. + * Subscribe a local user to this remote user. + * PuSH subscription will be started if necessary, and we'll + * send a Salmon notification to the remote server if available + * notifying them of the sub. * - * @return bool true on success, false on failure - * @throws ServerException if feed state is not valid + * @param User $user + * @return boolean success + * @throws FeedException */ - public function subscribe($mode='subscribe') + public function subscribeLocalToRemote(User $user) { - if ($this->sub_state != '') { - throw new ServerException("Attempting to start PuSH subscription to feed in state $this->sub_state"); + if ($this->isGroup()) { + throw new ServerException("Can't subscribe to a remote group"); } - if (empty($this->huburi)) { - if (common_config('feedsub', 'nohub')) { - // Fake it! We're just testing remote feeds w/o hubs. + + if ($this->subscribe()) { + if ($user->subscribeTo($this->localProfile())) { + $this->notify($user->getProfile(), ActivityVerb::FOLLOW, $this); return true; - } else { - throw new ServerException("Attempting to start PuSH subscription for feed with no hub"); } } - - return $this->doSubscribe('subscribe'); + return false; } /** - * Send a PuSH unsubscription request to the hub for this feed. - * The hub will later send us a confirmation POST to /main/push/callback. + * Mark this remote profile as subscribing to the given local user, + * and send appropriate notifications to the user. * - * @return bool true on success, false on failure - * @throws ServerException if feed state is not valid + * This will generally be in response to a subscription notification + * from a foreign site to our local Salmon response channel. + * + * @param User $user + * @return boolean success */ - public function unsubscribe() { - if ($this->sub_state != 'active') { - throw new ServerException("Attempting to end PuSH subscription to feed in state $this->sub_state"); - } - if (empty($this->huburi)) { - if (common_config('feedsub', 'nohub')) { - // Fake it! We're just testing remote feeds w/o hubs. - return true; - } else { - throw new ServerException("Attempting to end PuSH subscription for feed with no hub"); - } - } - - return $this->doSubscribe('unsubscribe'); - } - - protected function doSubscribe($mode) + public function subscribeRemoteToLocal(User $user) { - $orig = clone($this); - $this->verify_token = common_good_rand(16); - if ($mode == 'subscribe') { - $this->secret = common_good_rand(32); - } - $this->sub_state = $mode; - $this->update($orig); - unset($orig); - - 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"); + if ($this->isGroup()) { + throw new ServerException("Remote groups can't subscribe to local users"); + } - $orig = clone($this); - $this->verify_token = null; - $this->sub_state = null; - $this->update($orig); - unset($orig); + // @fixme use regular channels for subbing, once they accept remote profiles + $sub = new Subscription(); + $sub->subscriber = $this->profile_id; + $sub->subscribed = $user->id; + $sub->created = common_sql_now(); // current time - return false; + if ($sub->insert()) { + // @fixme use subs_notify() if refactored to take profiles? + mail_subscribe_notify_profile($user, $this->localProfile()); + return true; } + return false; } /** - * Save PuSH subscription confirmation. - * Sets approximate lease start and end times and finalizes state. + * Send a subscription request to the hub for this feed. + * The hub will later send us a confirmation POST to /main/push/callback. * - * @param int $lease_seconds provided hub.lease_seconds parameter, if given + * @return bool true on success, false on failure + * @throws ServerException if feed state is not valid */ - public function confirmSubscribe($lease_seconds=0) + public function subscribe($mode='subscribe') { - $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; + $feedsub = FeedSub::ensureFeed($this->feeduri); + if ($feedsub->sub_state == 'active' || $feedsub->sub_state == 'subscribe') { + return true; + } else if ($feedsub->sub_state == '' || $feedsub->sub_state == 'inactive') { + return $feedsub->subscribe(); + } else if ('unsubscribe') { + throw new FeedSubException("Unsub is pending, can't subscribe..."); } - $this->lastupdate = common_sql_now(); - - return $this->update($original); } /** - * Save PuSH unsubscription confirmation. - * Wipes active PuSH sub info and resets state. + * 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 + * @throws ServerException if feed state is not valid */ - public function confirmUnsubscribe() - { - $original = clone($this); - - // @fixme these should all be null, but DB_DataObject doesn't save null values...????? - $this->verify_token = ''; - $this->secret = ''; - $this->sub_state = ''; - $this->sub_start = ''; - $this->sub_end = ''; - $this->lastupdate = common_sql_now(); - - return $this->update($original); + public function unsubscribe() { + $feedsub = FeedSub::staticGet('uri', $this->feeduri); + if ($feedsub->sub_state == 'active') { + return $feedsub->unsubscribe(); + } else if ($feedsub->sub_state == '' || $feedsub->sub_state == 'inactive' || $feedsub->sub_state == 'unsubscribe') { + return true; + } else if ($feedsub->sub_state == 'subscribe') { + throw new FeedSubException("Feed is awaiting subscription, can't unsub..."); + } } /** @@ -543,10 +337,7 @@ class Ostatus_profile extends Memcached_DataObject $entry->elementEnd('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"); @@ -600,36 +391,10 @@ class Ostatus_profile extends Memcached_DataObject * Currently assumes that all items in the feed are new, * coming from a PuSH hub. * - * @param string $post source of Atom or RSS feed - * @param string $hmac X-Hub-Signature header, if present + * @param DOMDocument $feed */ - public function postUpdates($post, $hmac) + public function processFeed($feed) { - common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $post"); - - if ($this->sub_state != 'active') { - common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH for inactive feed $this->feeduri (in state '$this->sub_state')"); - return; - } - - if ($post === '') { - common_log(LOG_ERR, __METHOD__ . ": ignoring empty post"); - return; - } - - if (!$this->validatePushSig($post, $hmac)) { - // Per spec we silently drop input with a bad sig, - // while reporting receipt to the server. - return; - } - - $feed = new DOMDocument(); - if (!$feed->loadXML($post)) { - // @fixme might help to include the err message - common_log(LOG_ERR, __METHOD__ . ": ignoring invalid XML"); - return; - } - $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); if ($entries->length == 0) { common_log(LOG_ERR, __METHOD__ . ": no entries in feed update, ignoring"); @@ -642,40 +407,6 @@ class Ostatus_profile extends Memcached_DataObject } } - /** - * Validate the given Atom chunk and HMAC signature against our - * shared secret that was set up at subscription time. - * - * If we don't have a shared secret, there should be no signature. - * If we we do, our the calculated HMAC should match theirs. - * - * @param string $post raw XML source as POSTed to us - * @param string $hmac X-Hub-Signature HTTP header value, or empty - * @return boolean true for a match - */ - protected function validatePushSig($post, $hmac) - { - if ($this->secret) { - if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) { - $their_hmac = strtolower($matches[1]); - $our_hmac = hash_hmac('sha1', $post, $this->secret); - if ($their_hmac === $our_hmac) { - return true; - } - common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac"); - } else { - common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'"); - } - } else { - if (empty($hmac)) { - return true; - } else { - common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'"); - } - } - return false; - } - /** * Process a posted entry from this feed source. * @@ -704,14 +435,14 @@ class Ostatus_profile extends Memcached_DataObject { if ($this->isGroup()) { // @fixme validate these profiles in some way! - $oprofile = $this->ensureActorProfile($activity); + $oprofile = self::ensureActorProfile($activity); } else { - $actorUri = $this->getActorProfileURI($activity); - if ($actorUri == $this->homeuri) { + $actorUri = self::getActorProfileURI($activity); + if ($actorUri == $this->uri) { // @fixme check if profile info has changed and update it } else { // @fixme drop or reject the messages once we've got the canonical profile URI recorded sanely - common_log(LOG_INFO, "OStatus: Warning: non-group post with unexpected author: $actorUri expected $this->homeuri"); + common_log(LOG_INFO, "OStatus: Warning: non-group post with unexpected author: $actorUri expected $this->uri"); //return; } $oprofile = $this; @@ -787,6 +518,65 @@ class Ostatus_profile extends Memcached_DataObject return false; } + /** + * @param string $profile_url + * @return Ostatus_profile + * @throws FeedSubException + */ + public static function ensureProfile($profile_uri) + { + // Get the canonical feed URI and check it + $discover = new FeedDiscovery(); + $feeduri = $discover->discoverFromURL($profile_uri); + + $feedsub = FeedSub::ensureFeed($feeduri, $discover->feed); + $huburi = $discover->getAtomLink('hub'); + $salmonuri = $discover->getAtomLink('salmon'); + + if (!$huburi) { + // We can only deal with folks with a PuSH hub + throw new FeedSubNoHubException(); + } + + // Ok this is going to be a terrible hack! + // Won't be suitable for groups, empty feeds, or getting + // info that's only available on the profile page. + $entries = $discover->feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); + if (!$entries || $entries->length == 0) { + throw new FeedSubException('empty feed'); + } + $first = new Activity($entries->item(0), $discover->feed); + return self::ensureActorProfile($first, $feeduri); + } + + /** + * Download and update given avatar image + * @param string $url + * @throws Exception in various failure cases + */ + protected 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); + + // @fixme should we be using different ids? + $imagefile = new ImageFile($this->id, $temp_filename); + $filename = Avatar::filename($this->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); + } + } + /** * Get an appropriate avatar image source URL, if available. * @@ -794,7 +584,7 @@ class Ostatus_profile extends Memcached_DataObject * @param DOMElement $feed * @return string */ - function getAvatar($actor, $feed) + protected static function getAvatar($actor, $feed) { $url = ''; $icon = ''; @@ -833,13 +623,18 @@ class Ostatus_profile extends Memcached_DataObject } /** - * @fixme move off of ostatus_profile or static? + * Fetch, or build if necessary, an Ostatus_profile for the actor + * in a given Activity Streams activity. + * + * @param Activity $activity + * @param string $feeduri if we already know the canonical feed URI! + * @return Ostatus_profile */ - function ensureActorProfile($activity) + public static function ensureActorProfile($activity, $feeduri=null) { - $profile = $this->getActorProfile($activity); + $profile = self::getActorProfile($activity); if (!$profile) { - $profile = $this->createActorProfile($activity); + $profile = self::createActorProfile($activity, $feeduri); } return $profile; } @@ -848,10 +643,10 @@ class Ostatus_profile extends Memcached_DataObject * @param Activity $activity * @return mixed matching Ostatus_profile or false if none known */ - function getActorProfile($activity) + protected static function getActorProfile($activity) { - $homeuri = $this->getActorProfileURI($activity); - return Ostatus_profile::staticGet('homeuri', $homeuri); + $homeuri = self::getActorProfileURI($activity); + return self::staticGet('uri', $homeuri); } /** @@ -859,7 +654,7 @@ class Ostatus_profile extends Memcached_DataObject * @return string * @throws ServerException */ - function getActorProfileURI($activity) + protected static function getActorProfileURI($activity) { $opts = array('allowed_schemes' => array('http', 'https')); $actor = $activity->actor; @@ -873,14 +668,19 @@ class Ostatus_profile extends Memcached_DataObject } /** - * + * @fixme validate stuff somewhere */ - function createActorProfile($activity) + protected static function createActorProfile($activity, $feeduri=null) { - $actor = $activity->actor(); - $homeuri = $this->getActivityProfileURI($activity); - $nickname = $this->getAuthorNick($activity); - $avatar = $this->getAvatar($actor, $feed); + $actor = $activity->actor; + $homeuri = self::getActorProfileURI($activity); + $nickname = self::getAuthorNick($activity); + $avatar = self::getAvatar($actor, $feed); + + if (!$homeuri) { + common_log(LOG_DEBUG, __METHOD__ . " empty actor profile URI: " . var_export($activity, true)); + throw new ServerException("No profile URI"); + } $profile = new Profile(); $profile->nickname = $nickname; @@ -894,9 +694,7 @@ class Ostatus_profile extends Memcached_DataObject // @todo lat/lon/location? $ok = $profile->insert(); - if ($ok) { - $this->updateAvatar($profile, $avatar); - } else { + if (!$ok) { throw new ServerException("Can't save local profile"); } @@ -904,11 +702,15 @@ class Ostatus_profile extends Memcached_DataObject // or need to split out some of the feed stuff // so we can leave it empty until later. $oprofile = new Ostatus_profile(); - $oprofile->homeuri = $homeuri; + $oprofile->uri = $homeuri; + if ($feeduri) { + $oprofile->feeduri = $feeduri; + } $oprofile->profile_id = $profile->id; $ok = $oprofile->insert(); if ($ok) { + $oprofile->updateAvatar($avatar); return $oprofile; } else { throw new ServerException("Can't save OStatus profile"); @@ -920,13 +722,13 @@ class Ostatus_profile extends Memcached_DataObject * @param Activity $activity * @return string */ - function getAuthorNick($activity) + protected static function getAuthorNick($activity) { // @fixme not technically part of the actor? foreach (array($activity->entry, $activity->feed) as $source) { - $author = ActivityUtil::child($source, 'author', Activity::ATOM); + $author = ActivityUtils::child($source, 'author', Activity::ATOM); if ($author) { - $name = ActivityUtil::child($author, 'name', Activity::ATOM); + $name = ActivityUtils::child($author, 'name', Activity::ATOM); if ($name) { return trim($name->textContent); } diff --git a/plugins/OStatus/lib/feeddiscovery.php b/plugins/OStatus/lib/feeddiscovery.php index 39985fc90..7afb71bdc 100644 --- a/plugins/OStatus/lib/feeddiscovery.php +++ b/plugins/OStatus/lib/feeddiscovery.php @@ -48,6 +48,14 @@ class FeedSubNoFeedException extends FeedSubException { } +class FeedSubBadXmlException extends FeedSubException +{ +} + +class FeedSubNoHubException extends FeedSubException +{ +} + /** * Given a web page or feed URL, discover the final location of the feed * and return its current contents. @@ -57,21 +65,25 @@ class FeedSubNoFeedException extends FeedSubException * if ($feed->discoverFromURL($url)) { * print $feed->uri; * print $feed->type; - * processFeed($feed->body); + * processFeed($feed->feed); // DOMDocument * } */ class FeedDiscovery { public $uri; public $type; - public $body; + public $feed; + /** Post-initialize query helper... */ + public function getLink($rel, $type=null) + { + // @fixme check for non-Atom links in RSS2 feeds as well + return self::getAtomLink($rel, $type); + } - public function feedMunger() + public function getAtomLink($rel, $type=null) { - require_once 'XML/Feed/Parser.php'; - $feed = new XML_Feed_Parser($this->body, false, false, true); // @fixme - return new FeedMunger($feed, $this->uri); + return ActivityUtils::getLink($this->feed->documentElement, $rel, $type); } /** @@ -90,6 +102,7 @@ class FeedDiscovery $client = new HTTPClient(); $response = $client->get($url); } catch (HTTP_Request2_Exception $e) { + common_log(LOG_ERR, __METHOD__ . " Failure for $url - " . $e->getMessage()); throw new FeedSubBadURLException($e); } @@ -107,7 +120,12 @@ class FeedDiscovery return $this->initFromResponse($response); } - + + function discoverFromFeedURL($url) + { + return $this->discoverFromURL($url, false); + } + function initFromResponse($response) { if (!$response->isOk()) { @@ -122,16 +140,26 @@ class FeedDiscovery $type = $response->getHeader('Content-Type'); if (preg_match('!^(text/xml|application/xml|application/(rss|atom)\+xml)!i', $type)) { - $this->uri = $sourceurl; - $this->type = $type; - $this->body = $body; - return true; + return $this->init($sourceurl, $type, $body); } else { common_log(LOG_WARNING, "Unrecognized feed type $type for $sourceurl"); throw new FeedSubUnrecognizedTypeException($type); } } + function init($sourceurl, $type, $body) + { + $feed = new DOMDocument(); + if ($feed->loadXML($body)) { + $this->uri = $sourceurl; + $this->type = $type; + $this->feed = $feed; + return $this->uri; + } else { + throw new FeedSubBadXmlException($url); + } + } + /** * @param string $url source URL, used to resolve relative links * @param string $body HTML body text diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php deleted file mode 100644 index e8c46de90..000000000 --- a/plugins/OStatus/lib/feedmunger.php +++ /dev/null @@ -1,350 +0,0 @@ -. - */ - -/** - * @package FeedSubPlugin - * @maintainer Brion Vibber - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - -class FeedSubPreviewNotice extends Notice -{ - protected $fetched = true; - - function __construct($profile) - { - $this->profile = $profile; - $this->profile_id = 0; - } - - function getProfile() - { - return $this->profile; - } - - function find() - { - return true; - } - - function fetch() - { - $got = $this->fetched; - $this->fetched = false; - return $got; - } -} - -class FeedSubPreviewProfile extends Profile -{ - function getAvatar($width, $height=null) - { - return new FeedSubPreviewAvatar($width, $height, $this->avatar); - } -} - -class FeedSubPreviewAvatar extends Avatar -{ - function __construct($width, $height, $remote) - { - $this->remoteImage = $remote; - } - - function displayUrl() { - return $this->remoteImage; - } -} - -class FeedMunger -{ - /** - * @param XML_Feed_Parser $feed - */ - function __construct($feed, $url=null) - { - $this->feed = $feed; - $this->url = $url; - } - - function ostatusProfile() - { - $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()) - { - // XML_Feed_Parser gets confused by multiple elements. - $dom = $item->model; - - // Note that RSS feeds would embed an so this should work for both. - /// http://code.google.com/p/pubsubhubbub/wiki/RssFeeds - // - $links = $dom->getElementsByTagNameNS('http://www.w3.org/2005/Atom', 'link'); - for ($i = 0; $i < $links->length; $i++) { - $node = $links->item($i); - if ($node->hasAttributes()) { - $href = $node->attributes->getNamedItem('href'); - if ($href) { - $matches = 0; - foreach ($attribs as $name => $val) { - $attrib = $node->attributes->getNamedItem($name); - if ($attrib && $attrib->value == $val) { - $matches++; - } - } - if ($matches == count($attribs)) { - return $href->value; - } - } - } - } - return false; - } - - function getRssLink($item) - { - // XML_Feed_Parser gets confused by multiple elements. - $dom = $item->model; - - // Note that RSS feeds would embed an so this should work for both. - /// http://code.google.com/p/pubsubhubbub/wiki/RssFeeds - // - $links = $dom->getElementsByTagName('link'); - for ($i = 0; $i < $links->length; $i++) { - $node = $links->item($i); - if (!$node->hasAttributes()) { - return $node->textContent; - } - } - return false; - } - - function getAltLink($item) - { - // Check for an atom link... - $link = $this->getAtomLink($item, array('rel' => 'alternate', 'type' => 'text/html')); - if (!$link) { - $link = $this->getRssLink($item); - } - return $link; - } - - function getHubLink() - { - 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 - */ - function getAvatar() - { - $logo = $this->feed->logo; - if ($logo) { - return $logo; - } - $icon = $this->feed->icon; - if ($icon) { - return $icon; - } - return common_path('plugins/OStatus/images/48px-Feed-icon.svg.png'); - } - - function profile($preview=false) - { - if ($preview) { - $profile = new FeedSubPreviewProfile(); - } else { - $profile = new Profile(); - } - - // @todo validate/normalize nick? - $profile->nickname = $this->feed->title; - $profile->fullname = $this->feed->title; - $profile->homepage = $this->getAltLink($this->feed); - $profile->bio = $this->feed->description; - $profile->profileurl = $this->getAltLink($this->feed); - - if ($preview) { - $profile->avatar = $this->getAvatar(); - } - - // @todo tags from categories - // @todo lat/lon/location? - - return $profile; - } - - function notice($index=1, $preview=false) - { - $entry = $this->feed->getEntryByOffset($index); - 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); - if (empty($link)) { - if (preg_match('!^https?://!', $entry->id)) { - $link = $entry->id; - common_log(LOG_DEBUG, "No link on entry, using URL from id: $link"); - } - } - $notice->uri = $link; - $notice->url = $link; - $notice->content = $this->noticeFromEntry($entry); - $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'; - - $location = $this->getLocation($entry); - if ($location) { - if ($location->location_id) { - $notice->location_ns = $location->location_ns; - $notice->location_id = $location->location_id; - } - $notice->lat = $location->lat; - $notice->lon = $location->lon; - } - - return $notice; - } - - function profileIdForEntry($index=1) - { - // hack hack hack - // should get profile for this entry's author... - $feeduri = $this->getSelfLink(); - $remote = Ostatus_profile::staticGet('feeduri', $feeduri); - if ($remote) { - return $remote->profile_id; - } else { - throw new Exception("Can't find feed profile for $feeduri"); - } - } - - /** - * Parse location given as a GeoRSS-simple point, if provided. - * http://www.georss.org/simple - * - * @param feed item $entry - * @return mixed Location or false - */ - function getLocation($entry) - { - $dom = $entry->model; - $points = $dom->getElementsByTagNameNS('http://www.georss.org/georss', 'point'); - - for ($i = 0; $i < $points->length; $i++) { - $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; - if (is_numeric($lat) && is_numeric($lon)) { - common_log(LOG_INFO, "Looking up location for $lat $lon from georss"); - return Location::fromLatLon($lat, $lon); - } - } - common_log(LOG_ERR, "Ignoring bogus georss:point value $point"); - } - - return false; - } - - /** - * @param XML_Feed_Type $entry - * @return string notice text, within post size limit - */ - function noticeFromEntry($entry) - { - $max = Notice::maxContent(); - $ellipsis = "\xe2\x80\xa6"; // U+2026 HORIZONTAL ELLIPSIS - $title = $entry->title; - $link = $entry->link; - - // @todo We can get entries like this: - // $cats = $entry->getCategory('category', array(0, true)); - // but it feels like an awful hack. If it's accessible cleanly, - // try adding #hashtags from the categories/tags on a post. - - $title = $entry->title; - $link = $this->getAltLink($entry); - if ($link) { - // Blog post or such... - // @todo Should we force a language here? - $format = _m('New post: "%1$s" %2$s'); - $out = sprintf($format, $title, $link); - - // Trim link if needed... - if (mb_strlen($out) > $max) { - $link = common_shorten_url($link); - $out = sprintf($format, $title, $link); - } - - // Trim title if needed... - if (mb_strlen($out) > $max) { - $used = mb_strlen($out) - mb_strlen($title); - $available = $max - $used - mb_strlen($ellipsis); - $title = mb_substr($title, 0, $available) . $ellipsis; - $out = sprintf($format, $title, $link); - } - } else { - // No link? Consider a bare status update. - if (mb_strlen($title) > $max) { - $available = $max - mb_strlen($ellipsis); - $out = mb_substr($title, 0, $available) . $ellipsis; - } else { - $out = $title; - } - } - - return $out; - } -} -- cgit v1.2.3-54-g00ecf From a1a3ab1c58cf8636c4e9ac6b9d44bdc18946a547 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 19 Feb 2010 12:08:07 -0800 Subject: OStatus: hooked up follow/unfollow events on Salmon endpoint to create/destroy remote subscriber relationships --- plugins/OStatus/OStatusPlugin.php | 9 +++- plugins/OStatus/actions/feedsubsettings.php | 8 ---- plugins/OStatus/actions/salmon.php | 66 +++++++++++++++++++++++++---- plugins/OStatus/classes/Ostatus_profile.php | 22 +++++++--- 4 files changed, 81 insertions(+), 24 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 4ebe4551e..4c43d8a30 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -253,17 +253,22 @@ class OStatusPlugin extends Plugin */ function onEndUnsubscribe($user, $other) { + if ($user instanceof Profile) { + $profile = $user; + } else if ($user instanceof Profile) { + $profile = $user->getProfile(); + } $oprofile = Ostatus_profile::staticGet('profile_id', $other->id); if ($oprofile) { // Notify the remote server of the unsub, if supported. - $oprofile->notify($user->getProfile(), ActivityVerb::UNFOLLOW, $oprofile); + $oprofile->notify($profile, ActivityVerb::UNFOLLOW, $oprofile); // Drop the PuSH subscription if there are no other subscribers. $sub = new Subscription(); $sub->subscribed = $other->id; $sub->limit(1); if (!$sub->find(true)) { - common_log(LOG_INFO, "Unsubscribing from now-unused feed $oprofile->feeduri on hub $oprofile->huburi"); + common_log(LOG_INFO, "Unsubscribing from now-unused feed $oprofile->feeduri"); $oprofile->unsubscribe(); } } diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php index 3e1d0aa82..aee4cee9a 100644 --- a/plugins/OStatus/actions/feedsubsettings.php +++ b/plugins/OStatus/actions/feedsubsettings.php @@ -68,14 +68,6 @@ class FeedSubSettingsAction extends ConnectSettingsAction $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', diff --git a/plugins/OStatus/actions/salmon.php b/plugins/OStatus/actions/salmon.php index 2da9db9eb..1ed322ed2 100644 --- a/plugins/OStatus/actions/salmon.php +++ b/plugins/OStatus/actions/salmon.php @@ -63,13 +63,17 @@ class SalmonAction extends Action // XXX: check that document element is Atom entry // XXX: check the signature - $this->act = new Activity($dom->documentElement); + $entries = $dom->getElementsByTagNameNS(Activity::ATOM, 'entry'); + if ($entries && $entries->length) { + // @fixme is it legit to have multiple entries? + $this->act = new Activity($entries->item(0), $dom->documentElement); + } return true; } /** - * @fixme probably call Ostatus_profile::processFeed + * Check the posted activity type and break out to appropriate processing. */ function handle($args) @@ -94,13 +98,19 @@ class SalmonAction extends Action case ActivityVerb::FRIEND: $this->handleFollow(); break; + case ActivityVerb::UNFOLLOW: + $this->handleUnfollow(); + break; } Event::handle('EndHandleSalmon', array($this->user, $this->activity)); } } /** - * @fixme probably call Ostatus_profile::processFeed + * We've gotten a post event on the Salmon backchannel, probably a reply. + * + * @todo validate if we need to handle this post, then call into + * ostatus_profile's general incoming-post handling. */ function handlePost() { @@ -116,43 +126,81 @@ class SalmonAction extends Action } $profile = $this->ensureProfile(); + // @fixme do something with the post } /** - * @fixme probably call Ostatus_profile::processFeed + * We've gotten a follow/subscribe notification from a remote user. + * Save a subscription relationship for them. */ function handleFollow() { + $oprofile = $this->ensureProfile(); + if ($oprofile) { + common_log(LOG_INFO, "Setting up subscription from remote {$oprofile->uri} to local {$this->user->nickname}"); + $oprofile->subscribeRemoteToLocal($this->user); + } else { + common_log(LOG_INFO, "Can't set up subscription from remote; missing profile."); + } + } + + /** + * We've gotten an unfollow/unsubscribe notification from a remote user. + * Check if we have a subscription relationship for them and kill it. + * + * @fixme probably catch exceptions on fail? + */ + function handleUnfollow() + { + $oprofile = $this->ensureProfile(); + if ($oprofile) { + common_log(LOG_INFO, "Canceling subscription from remote {$oprofile->uri} to local {$this->user->nickname}"); + Subscription::cancel($oprofile->localProfile(), $this->user->getProfile()); + } else { + common_log(LOG_ERR, "Can't cancel subscription from remote, didn't find the profile"); + } } /** - * @fixme probably call Ostatus_profile::processFeed + * Remote user likes one of our posts. + * Confirm the post is ours, and save a local favorite event. */ function handleFavorite() { } /** - * @fixme probably call Ostatus_profile::processFeed + * Remote user doesn't like one of our posts after all! + * Confirm the post is ours, and save a local favorite event. + */ + function handleUnfavorite() + { + } + + /** + * Hmmmm */ function handleShare() { } + /** + * @return Ostatus_profile + */ function ensureProfile() { $actor = $this->act->actor; common_log(LOG_DEBUG, "Received salmon bit: " . var_export($this->act, true)); if (empty($actor->id)) { + common_log(LOG_ERR, "broken actor: " . var_export($actor, true)); throw new Exception("Received a salmon slap from unidentified actor."); } - $ostatusProfile = Ostatus_profile::ensureActorProfile($this->act); - return $oprofile->localProfile(); + return Ostatus_profile::ensureActorProfile($this->act); } /** - * @fixme anything new in here probably should be merged into Ostatus_profile::ensureActorProfile and friends + * @fixme merge into Ostatus_profile::ensureActorProfile and friends */ function createProfile() { diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 319b76a66..97d8eec10 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -310,8 +310,15 @@ class Ostatus_profile extends Memcached_DataObject * @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) + public function notify($actor, $verb, $object=null) { + if (!($actor instanceof Profile)) { + $type = gettype($actor); + if ($type == 'object') { + $type = get_class($actor); + } + throw new ServerException("Invalid actor passed to " . __METHOD__ . ": " . $type); + } if ($object == null) { $object = $this; } @@ -340,7 +347,7 @@ class Ostatus_profile extends Memcached_DataObject $feed->addEntry($entry); $xml = $feed->getString(); - common_log(LOG_INFO, "Posting to Salmon endpoint $salmon: $xml"); + common_log(LOG_INFO, "Posting to Salmon endpoint $this->salmonuri: $xml"); $salmon = new Salmon(); // ? $salmon->post($this->salmonuri, $xml); @@ -531,9 +538,14 @@ class Ostatus_profile extends Memcached_DataObject $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); copy($url, $temp_filename); + if ($this->isGroup()) { + $id = $this->group_id; + } else { + $id = $this->profile_id; + } // @fixme should we be using different ids? - $imagefile = new ImageFile($this->id, $temp_filename); - $filename = Avatar::filename($this->id, + $imagefile = new ImageFile($id, $temp_filename); + $filename = Avatar::filename($id, image_type_to_extension($imagefile->type), null, common_timestamp()); @@ -646,7 +658,7 @@ class Ostatus_profile extends Memcached_DataObject $actor = $activity->actor; $homeuri = self::getActorProfileURI($activity); $nickname = self::getAuthorNick($activity); - $avatar = self::getAvatar($actor, $feed); + $avatar = self::getAvatar($actor, $activity->feed); if (!$homeuri) { common_log(LOG_DEBUG, __METHOD__ . " empty actor profile URI: " . var_export($activity, true)); -- cgit v1.2.3-54-g00ecf From 557df3d3f78dbfce656c4c8e3ddf82ee0e34da0a Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 19 Feb 2010 16:21:17 -0800 Subject: OStatus: sub/unsub notifications working again. Fixed up autodetection of feed info at profile setup time --- plugins/OStatus/OStatusPlugin.php | 2 ++ plugins/OStatus/actions/salmon.php | 13 +++++++++--- plugins/OStatus/classes/Ostatus_profile.php | 31 +++++++++++++++++++---------- plugins/OStatus/lib/salmon.php | 3 +++ 4 files changed, 35 insertions(+), 14 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 4c43d8a30..1b58d3c35 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -331,6 +331,8 @@ class OStatusPlugin extends Plugin $oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri); if ($oprofile) { $oprofile->processFeed($feed); + } else { + common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri"); } } } diff --git a/plugins/OStatus/actions/salmon.php b/plugins/OStatus/actions/salmon.php index 7a4474ff6..43d79cf4a 100644 --- a/plugins/OStatus/actions/salmon.php +++ b/plugins/OStatus/actions/salmon.php @@ -34,6 +34,8 @@ class SalmonAction extends Action function prepare($args) { + StatusNet::setApi(true); // Send smaller error pages + parent::prepare($args); if ($_SERVER['REQUEST_METHOD'] != 'POST') { @@ -63,8 +65,12 @@ class SalmonAction extends Action // XXX: check that document element is Atom entry // XXX: check the signature - $this->act = new Activity($dom->documentElement); - + // We need to run an entry into Activity, so get the first one + $entries = $dom->getElementsByTagNameNS(Activity::ATOM, 'entry'); + if ($entries && $entries->length) { + // @fixme is it legit to have multiple entries? + $this->act = new Activity($entries->item(0), $dom->documentElement); + } return true; } @@ -74,7 +80,9 @@ class SalmonAction extends Action function handle($args) { + StatusNet::setApi(true); // Send smaller error pages common_log(LOG_INFO, 'Salmon: incoming post for user '. $this->user->id); + common_log(LOG_DEBUG, "Received salmon bit: " . var_export($this->act, true)); // TODO : Insert new $xml -> notice code @@ -254,7 +262,6 @@ class SalmonAction extends Action function ensureProfile() { $actor = $this->act->actor; - common_log(LOG_DEBUG, "Received salmon bit: " . var_export($this->act, true)); if (empty($actor->id)) { common_log(LOG_ERR, "broken actor: " . var_export($actor, true)); throw new Exception("Received a salmon slap from unidentified actor."); diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 97d8eec10..5fe135f96 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -56,7 +56,7 @@ class Ostatus_profile extends Memcached_DataObject return array('uri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, 'profile_id' => DB_DATAOBJECT_INT, 'group_id' => DB_DATAOBJECT_INT, - 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'feeduri' => DB_DATAOBJECT_STR, 'salmonuri' => DB_DATAOBJECT_STR, 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, 'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); @@ -71,7 +71,7 @@ class Ostatus_profile extends Memcached_DataObject new ColumnDef('group_id', 'integer', null, true, 'UNI'), new ColumnDef('feeduri', 'varchar', - 255, false, 'UNI'), + 255, true, 'UNI'), new ColumnDef('salmonuri', 'text', null, true), new ColumnDef('created', 'datetime', @@ -272,7 +272,7 @@ class Ostatus_profile extends Memcached_DataObject * @return bool true on success, false on failure * @throws ServerException if feed state is not valid */ - public function subscribe($mode='subscribe') + public function subscribe() { $feedsub = FeedSub::ensureFeed($this->feeduri); if ($feedsub->sub_state == 'active' || $feedsub->sub_state == 'subscribe') { @@ -506,7 +506,7 @@ class Ostatus_profile extends Memcached_DataObject $discover = new FeedDiscovery(); $feeduri = $discover->discoverFromURL($profile_uri); - $feedsub = FeedSub::ensureFeed($feeduri, $discover->feed); + //$feedsub = FeedSub::ensureFeed($feeduri, $discover->feed); $huburi = $discover->getAtomLink('hub'); $salmonuri = $discover->getAtomLink('salmon'); @@ -665,6 +665,20 @@ class Ostatus_profile extends Memcached_DataObject throw new ServerException("No profile URI"); } + if (!$feeduri || !$salmonuri) { + // Get the canonical feed URI and check it + $discover = new FeedDiscovery(); + $feeduri = $discover->discoverFromURL($homeuri); + + $huburi = $discover->getAtomLink('hub'); + $salmonuri = $discover->getAtomLink('salmon'); + + if (!$huburi) { + // We can only deal with folks with a PuSH hub + throw new FeedSubNoHubException(); + } + } + $profile = new Profile(); $profile->nickname = $nickname; $profile->fullname = $actor->displayName; @@ -686,13 +700,8 @@ class Ostatus_profile extends Memcached_DataObject // so we can leave it empty until later. $oprofile = new Ostatus_profile(); $oprofile->uri = $homeuri; - if ($feeduri) { - // If we don't have these, we can look them up later. - $oprofile->feeduri = $feeduri; - if ($salmonuri) { - $oprofile->salmonuri = $salmonuri; - } - } + $oprofile->feeduri = $feeduri; + $oprofile->salmonuri = $salmonuri; $oprofile->profile_id = $profile->id; $oprofile->created = common_sql_now(); diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php index 8c77222a6..df17a7006 100644 --- a/plugins/OStatus/lib/salmon.php +++ b/plugins/OStatus/lib/salmon.php @@ -41,9 +41,12 @@ class Salmon $client->setBody($xml); $response = $client->post($endpoint_uri, $headers); } catch (HTTP_Request2_Exception $e) { + common_log(LOG_ERR, "Salmon post to $endpoint_uri failed: " . $e->getMessage()); return false; } if ($response->getStatus() != 200) { + common_log(LOG_ERR, "Salmon at $endpoint_uri returned status " . + $response->getStatus() . ': ' . $response->getBody()); return false; } -- cgit v1.2.3-54-g00ecf From 2df3bbc80b6c9dd44134bcf3b5b4a09ea1a803d1 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sat, 20 Feb 2010 11:12:43 -0500 Subject: Move some salmon processing to a superclass Moved some salmon processing to a superclass so we could handle group salmon posts, too. --- plugins/OStatus/OStatusPlugin.php | 4 +- plugins/OStatus/actions/groupsalmon.php | 108 ++++++++++ plugins/OStatus/actions/salmon.php | 341 -------------------------------- plugins/OStatus/actions/usersalmon.php | 0 plugins/OStatus/lib/salmonaction.php | 231 ++++++++++++++++++++++ 5 files changed, 341 insertions(+), 343 deletions(-) create mode 100644 plugins/OStatus/actions/groupsalmon.php delete mode 100644 plugins/OStatus/actions/salmon.php create mode 100644 plugins/OStatus/actions/usersalmon.php create mode 100644 plugins/OStatus/lib/salmonaction.php (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 1b58d3c35..e1f3fd9d3 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -63,10 +63,10 @@ class OStatusPlugin extends Plugin // Salmon endpoint $m->connect('main/salmon/user/:id', - array('action' => 'salmon'), + array('action' => 'usersalmon'), array('id' => '[0-9]+')); $m->connect('main/salmon/group/:id', - array('action' => 'salmongroup'), + array('action' => 'groupsalmon'), array('id' => '[0-9]+')); return true; } diff --git a/plugins/OStatus/actions/groupsalmon.php b/plugins/OStatus/actions/groupsalmon.php new file mode 100644 index 000000000..64ae9f3cc --- /dev/null +++ b/plugins/OStatus/actions/groupsalmon.php @@ -0,0 +1,108 @@ +. + */ + +/** + * @package OStatusPlugin + * @author James Walker + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class GroupsalmonAction extends SalmonAction +{ + var $group = null; + + function prepare($args) + { + parent::prepare($args); + + $id = $this->trimmed('id'); + + if (!$id) { + $this->clientError(_('No ID.')); + } + + $this->group = User_group::staticGet('id', $id); + + if (empty($this->group)) { + $this->clientError(_('No such group.')); + } + + return true; + } + + /** + * We've gotten a post event on the Salmon backchannel, probably a reply. + */ + + function handlePost() + { + switch ($this->act->object->type) { + case ActivityObject::ARTICLE: + case ActivityObject::BLOGENTRY: + case ActivityObject::NOTE: + case ActivityObject::STATUS: + case ActivityObject::COMMENT: + break; + default: + throw new ClientException("Can't handle that kind of post."); + } + + // Notice must be to the attention of this group + + $context = $this->act->context; + + if (empty($context->attention)) { + throw new ClientException("Not to the attention of anyone."); + } else { + $uri = common_local_url('groupbyid', array('id' => $this->group->id)); + if (!in_array($context->attention, $uri)) { + throw new ClientException("Not to the attention of this group."); + } + } + + $profile = $this->ensureProfile(); + // @fixme save the post + } + + /** + * We've gotten a follow/subscribe notification from a remote user. + * Save a subscription relationship for them. + */ + + function handleFollow() + { + $this->handleJoin(); // ??? + } + + function handleUnfollow() + { + } + + /** + * A remote user joined our group. + */ + + function handleJoin() + { + } + +} diff --git a/plugins/OStatus/actions/salmon.php b/plugins/OStatus/actions/salmon.php deleted file mode 100644 index f7c86dc0c..000000000 --- a/plugins/OStatus/actions/salmon.php +++ /dev/null @@ -1,341 +0,0 @@ -. - */ - -/** - * @package OStatusPlugin - * @author James Walker - */ - -if (!defined('STATUSNET')) { - exit(1); -} - -class SalmonAction extends Action -{ - var $user = null; - var $xml = null; - var $activity = null; - - function prepare($args) - { - StatusNet::setApi(true); // Send smaller error pages - - parent::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); - - if ($dom->documentElement->namespaceURI != Activity::ATOM || - $dom->documentElement->localName != 'entry') { - $this->clientError(_m('Salmon post must be an Atom entry.')); - } - // XXX: check the signature - - $this->act = new Activity($dom->documentElement); - return true; - } - - /** - * Check the posted activity type and break out to appropriate processing. - */ - - function handle($args) - { - StatusNet::setApi(true); // Send smaller error pages - common_log(LOG_INFO, 'Salmon: incoming post for user '. $this->user->id); - common_log(LOG_DEBUG, "Received salmon bit: " . var_export($this->act, true)); - - // TODO : Insert new $xml -> notice code - - if (Event::handle('StartHandleSalmon', array($this->user, $this->activity))) { - switch ($this->act->verb) - { - case ActivityVerb::POST: - $this->handlePost(); - break; - case ActivityVerb::SHARE: - $this->handleShare(); - break; - case ActivityVerb::FAVORITE: - $this->handleFavorite(); - break; - case ActivityVerb::FOLLOW: - case ActivityVerb::FRIEND: - $this->handleFollow(); - break; - case ActivityVerb::UNFOLLOW: - $this->handleUnfollow(); - break; - } - Event::handle('EndHandleSalmon', array($this->user, $this->activity)); - } - } - - /** - * We've gotten a post event on the Salmon backchannel, probably a reply. - * - * @todo validate if we need to handle this post, then call into - * ostatus_profile's general incoming-post handling. - */ - function handlePost() - { - switch ($this->act->object->type) { - case ActivityObject::ARTICLE: - case ActivityObject::BLOGENTRY: - case ActivityObject::NOTE: - case ActivityObject::STATUS: - case ActivityObject::COMMENT: - break; - default: - throw new ClientException("Can't handle that kind of post."); - } - - // Notice must either be a) in reply to a notice by this user - // or b) to the attention of this user - - $context = $this->act->context; - - if (!empty($context->replyToID)) { - $notice = Notice::staticGet('uri', $context->replyToID); - if (empty($notice)) { - throw new ClientException("In reply to unknown notice"); - } - if ($notice->profile_id != $this->user->id) { - throw new ClientException("In reply to a notice not by this user"); - } - } else if (!empty($context->attention)) { - if (!in_array($context->attention, $this->user->uri)) { - throw new ClientException("To the attention of user(s) not including this one!"); - } - } else { - throw new ClientException("Not to anyone in reply to anything!"); - } - - $profile = $this->ensureProfile(); - // @fixme do something with the post - } - - /** - * We've gotten a follow/subscribe notification from a remote user. - * Save a subscription relationship for them. - */ - - function handleFollow() - { - $oprofile = $this->ensureProfile(); - if ($oprofile) { - common_log(LOG_INFO, "Setting up subscription from remote {$oprofile->uri} to local {$this->user->nickname}"); - $oprofile->subscribeRemoteToLocal($this->user); - } else { - common_log(LOG_INFO, "Can't set up subscription from remote; missing profile."); - } - } - - /** - * We've gotten an unfollow/unsubscribe notification from a remote user. - * Check if we have a subscription relationship for them and kill it. - * - * @fixme probably catch exceptions on fail? - */ - function handleUnfollow() - { - $oprofile = $this->ensureProfile(); - if ($oprofile) { - common_log(LOG_INFO, "Canceling subscription from remote {$oprofile->uri} to local {$this->user->nickname}"); - Subscription::cancel($oprofile->localProfile(), $this->user->getProfile()); - } else { - common_log(LOG_ERR, "Can't cancel subscription from remote, didn't find the profile"); - } - } - - /** - * Remote user likes one of our posts. - * Confirm the post is ours, and save a local favorite event. - */ - - function handleFavorite() - { - // WORST VARIABLE NAME EVER - $object = $this->act->object; - - switch ($this->act->object->type) { - case ActivityObject::ARTICLE: - case ActivityObject::BLOGENTRY: - case ActivityObject::NOTE: - case ActivityObject::STATUS: - case ActivityObject::COMMENT: - break; - default: - throw new ClientException("Can't handle that kind of object for liking/faving."); - } - - $notice = Notice::staticGet('uri', $object->id); - - if (empty($notice)) { - throw new ClientException("Notice with ID $object->id unknown."); - } - - if ($notice->profile_id != $this->user->id) { - throw new ClientException("Notice with ID $object->id not posted by $this->user->id."); - } - - $profile = $this->ensureProfile(); - - $old = Fave::pkeyGet(array('user_id' => $profile->id, - 'notice_id' => $notice->id)); - - if (!empty($old)) { - throw new ClientException("We already know that's a fave!"); - } - - $fave = new Fave(); - - // @fixme need to change this attribute name, maybe references - $fave->user_id = $profile->id; - $fave->notice_id = $notice->id; - - $result = $fave->insert(); - - if (!$result) { - common_log_db_error($fave, 'INSERT', __FILE__); - throw new ServerException('Could not save new favorite.'); - } - } - - /** - * Remote user doesn't like one of our posts after all! - * Confirm the post is ours, and save a local favorite event. - */ - function handleUnfavorite() - { - } - - /** - * Hmmmm - */ - function handleShare() - { - } - - /** - * @return Ostatus_profile - */ - function ensureProfile() - { - $actor = $this->act->actor; - if (empty($actor->id)) { - common_log(LOG_ERR, "broken actor: " . var_export($actor, true)); - throw new Exception("Received a salmon slap from unidentified actor."); - } - - return Ostatus_profile::ensureActorProfile($this->act); - } - - /** - * @fixme merge into Ostatus_profile::ensureActorProfile and friends - */ - function createProfile() - { - $actor = $this->act->actor; - - $profile = new Profile(); - - $profile->nickname = $this->nicknameFromURI($actor->id); - - if (empty($profile->nickname)) { - $profile->nickname = common_nicknamize($actor->title); - } - - $profile->fullname = $actor->title; - $profile->bio = $actor->summary; // XXX: is that right? - $profile->profileurl = $actor->link; // XXX: is that right? - $profile->created = common_sql_now(); - - $id = $profile->insert(); - - if (empty($id)) { - common_log_db_error($profile, 'INSERT', __FILE__); - throw new Exception("Couldn't save new profile for $actor->id\n"); - } - - // XXX: add avatars - - $op = new Ostatus_profile(); - - $op->profile_id = $id; - $op->homeuri = $actor->id; - $op->created = $profile->created; - - // XXX: determine feed URI from source or Webfinger or whatever - - $id = $op->insert(); - - if (empty($id)) { - common_log_db_error($op, 'INSERT', __FILE__); - throw new Exception("Couldn't save new ostatus profile for $actor->id\n"); - } - - return $profile; - } - - /** - * @fixme should be merged into Ostatus_profile - */ - function nicknameFromURI($uri) - { - preg_match('/(\w+):/', $uri, $matches); - - $protocol = $matches[1]; - - switch ($protocol) { - case 'acct': - case 'mailto': - if (preg_match("/^$protocol:(.*)?@.*\$/", $uri, $matches)) { - return common_canonical_nickname($matches[1]); - } - return null; - case 'http': - return common_url_to_nickname($uri); - break; - default: - return null; - } - } -} diff --git a/plugins/OStatus/actions/usersalmon.php b/plugins/OStatus/actions/usersalmon.php new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php new file mode 100644 index 000000000..7085e4583 --- /dev/null +++ b/plugins/OStatus/lib/salmonaction.php @@ -0,0 +1,231 @@ +. + */ + +/** + * @package OStatusPlugin + * @author James Walker + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class SalmonAction extends Action +{ + var $xml = null; + var $activity = null; + + function prepare($args) + { + StatusNet::setApi(true); // Send smaller error pages + + parent::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')); + } + + $xml = file_get_contents('php://input'); + + $dom = DOMDocument::loadXML($xml); + + if ($dom->documentElement->namespaceURI != Activity::ATOM || + $dom->documentElement->localName != 'entry') { + $this->clientError(_m('Salmon post must be an Atom entry.')); + } + // XXX: check the signature + + $this->act = new Activity($dom->documentElement); + return true; + } + + /** + * Check the posted activity type and break out to appropriate processing. + */ + + function handle($args) + { + StatusNet::setApi(true); // Send smaller error pages + + // TODO : Insert new $xml -> notice code + + if (Event::handle('StartHandleSalmon', array($this->activity))) { + switch ($this->act->verb) + { + case ActivityVerb::POST: + $this->handlePost(); + break; + case ActivityVerb::SHARE: + $this->handleShare(); + break; + case ActivityVerb::FAVORITE: + $this->handleFavorite(); + break; + case ActivityVerb::FOLLOW: + case ActivityVerb::FRIEND: + $this->handleFollow(); + break; + case ActivityVerb::UNFOLLOW: + $this->handleUnfollow(); + break; + case ActivityVerb::JOIN: + $this->handleJoin(); + break; + default: + throw new ClientException(_("Unimplemented.")); + } + Event::handle('EndHandleSalmon', array($this->activity)); + } + } + + function handlePost() + { + throw new ClientException(_("Unimplemented!")); + } + + function handleFollow() + { + throw new ClientException(_("Unimplemented!")); + } + + function handleUnfollow() + { + throw new ClientException(_("Unimplemented!")); + } + + function handleFavorite() + { + throw new ClientException(_("Unimplemented!")); + } + + /** + * Remote user doesn't like one of our posts after all! + * Confirm the post is ours, and delete a local favorite event. + */ + + function handleUnfavorite() + { + throw new ClientException(_("Unimplemented!")); + } + + /** + * Hmmmm + */ + function handleShare() + { + throw new ClientException(_("Unimplemented!")); + } + + /** + * Hmmmm + */ + function handleJoin() + { + throw new ClientException(_("Unimplemented!")); + } + + /** + * @return Ostatus_profile + */ + function ensureProfile() + { + $actor = $this->act->actor; + if (empty($actor->id)) { + common_log(LOG_ERR, "broken actor: " . var_export($actor, true)); + throw new Exception("Received a salmon slap from unidentified actor."); + } + + return Ostatus_profile::ensureActorProfile($this->act); + } + + /** + * @fixme merge into Ostatus_profile::ensureActorProfile and friends + */ + function createProfile() + { + $actor = $this->act->actor; + + $profile = new Profile(); + + $profile->nickname = $this->nicknameFromURI($actor->id); + + if (empty($profile->nickname)) { + $profile->nickname = common_nicknamize($actor->title); + } + + $profile->fullname = $actor->title; + $profile->bio = $actor->summary; // XXX: is that right? + $profile->profileurl = $actor->link; // XXX: is that right? + $profile->created = common_sql_now(); + + $id = $profile->insert(); + + if (empty($id)) { + common_log_db_error($profile, 'INSERT', __FILE__); + throw new Exception("Couldn't save new profile for $actor->id\n"); + } + + // XXX: add avatars + + $op = new Ostatus_profile(); + + $op->profile_id = $id; + $op->homeuri = $actor->id; + $op->created = $profile->created; + + // XXX: determine feed URI from source or Webfinger or whatever + + $id = $op->insert(); + + if (empty($id)) { + common_log_db_error($op, 'INSERT', __FILE__); + throw new Exception("Couldn't save new ostatus profile for $actor->id\n"); + } + + return $profile; + } + + /** + * @fixme should be merged into Ostatus_profile + */ + function nicknameFromURI($uri) + { + preg_match('/(\w+):/', $uri, $matches); + + $protocol = $matches[1]; + + switch ($protocol) { + case 'acct': + case 'mailto': + if (preg_match("/^$protocol:(.*)?@.*\$/", $uri, $matches)) { + return common_canonical_nickname($matches[1]); + } + return null; + case 'http': + return common_url_to_nickname($uri); + break; + default: + return null; + } + } +} -- cgit v1.2.3-54-g00ecf From 866b6470629b570b9f817553f85f6d8e801f0d43 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sat, 20 Feb 2010 11:48:42 -0500 Subject: add hooks for OStatus notification on subscribe/unsubscribe --- plugins/OStatus/OStatusPlugin.php | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index e1f3fd9d3..9e6d03177 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -335,4 +335,48 @@ class OStatusPlugin extends Plugin common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri"); } } + + function onEndSubscribe($subscriber, $other) + { + $user = User::staticGet('id', $subscriber->id); + + if (empty($user)) { + return true; + } + + $oprofile = Ostatus_profile::staticGet('profile_id', $other->id); + + if (empty($oprofile)) { + return true; + } + + // We have a local user subscribing to a remote profile; make the + // magic happen! + + $oprofile->notify($subscriber, ActivityVerb::FOLLOW); + + return true; + } + + function onEndUnsubscribe($subscriber, $other) + { + $user = User::staticGet('id', $subscriber->id); + + if (empty($user)) { + return true; + } + + $oprofile = Ostatus_profile::staticGet('profile_id', $other->id); + + if (empty($oprofile)) { + return true; + } + + // We have a local user subscribing to a remote profile; make the + // magic happen! + + $oprofile->notify($subscriber, ActivityVerb::UNFOLLOW); + + return true; + } } -- cgit v1.2.3-54-g00ecf From f891b135fbbf79bee6f9753e6a9cded65f219ab7 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Sat, 20 Feb 2010 10:20:42 -0800 Subject: OStatus: fix regressions in plugin & usersalmon action. Sub/unsub notifications are working again. --- plugins/OStatus/OStatusPlugin.php | 22 ---- plugins/OStatus/actions/usersalmon.php | 189 +++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 22 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 9e6d03177..e78e658a6 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -357,26 +357,4 @@ class OStatusPlugin extends Plugin return true; } - - function onEndUnsubscribe($subscriber, $other) - { - $user = User::staticGet('id', $subscriber->id); - - if (empty($user)) { - return true; - } - - $oprofile = Ostatus_profile::staticGet('profile_id', $other->id); - - if (empty($oprofile)) { - return true; - } - - // We have a local user subscribing to a remote profile; make the - // magic happen! - - $oprofile->notify($subscriber, ActivityVerb::UNFOLLOW); - - return true; - } } diff --git a/plugins/OStatus/actions/usersalmon.php b/plugins/OStatus/actions/usersalmon.php index e69de29bb..4363488dd 100644 --- a/plugins/OStatus/actions/usersalmon.php +++ b/plugins/OStatus/actions/usersalmon.php @@ -0,0 +1,189 @@ +. + */ + +/** + * @package OStatusPlugin + * @author James Walker + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class UsersalmonAction extends SalmonAction +{ + function prepare($args) + { + parent::prepare($args); + + $id = $this->trimmed('id'); + + if (!$id) { + $this->clientError(_('No ID.')); + } + + $this->user = User::staticGet('id', $id); + + if (empty($this->user)) { + $this->clientError(_('No such user.')); + } + + return true; + } + + /** + * We've gotten a post event on the Salmon backchannel, probably a reply. + * + * @todo validate if we need to handle this post, then call into + * ostatus_profile's general incoming-post handling. + */ + function handlePost() + { + switch ($this->act->object->type) { + case ActivityObject::ARTICLE: + case ActivityObject::BLOGENTRY: + case ActivityObject::NOTE: + case ActivityObject::STATUS: + case ActivityObject::COMMENT: + break; + default: + throw new ClientException("Can't handle that kind of post."); + } + + // Notice must either be a) in reply to a notice by this user + // or b) to the attention of this user + + $context = $this->act->context; + + if (!empty($context->replyToID)) { + $notice = Notice::staticGet('uri', $context->replyToID); + if (empty($notice)) { + throw new ClientException("In reply to unknown notice"); + } + if ($notice->profile_id != $this->user->id) { + throw new ClientException("In reply to a notice not by this user"); + } + } else if (!empty($context->attention)) { + if (!in_array($context->attention, $this->user->uri)) { + throw new ClientException("To the attention of user(s) not including this one!"); + } + } else { + throw new ClientException("Not to anyone in reply to anything!"); + } + + $profile = $this->ensureProfile(); + // @fixme do something with the post + } + + /** + * We've gotten a follow/subscribe notification from a remote user. + * Save a subscription relationship for them. + */ + + function handleFollow() + { + $oprofile = $this->ensureProfile(); + if ($oprofile) { + common_log(LOG_INFO, "Setting up subscription from remote {$oprofile->uri} to local {$this->user->nickname}"); + $oprofile->subscribeRemoteToLocal($this->user); + } else { + common_log(LOG_INFO, "Can't set up subscription from remote; missing profile."); + } + } + + /** + * We've gotten an unfollow/unsubscribe notification from a remote user. + * Check if we have a subscription relationship for them and kill it. + * + * @fixme probably catch exceptions on fail? + */ + function handleUnfollow() + { + $oprofile = $this->ensureProfile(); + if ($oprofile) { + common_log(LOG_INFO, "Canceling subscription from remote {$oprofile->uri} to local {$this->user->nickname}"); + Subscription::cancel($oprofile->localProfile(), $this->user->getProfile()); + } else { + common_log(LOG_ERR, "Can't cancel subscription from remote, didn't find the profile"); + } + } + + /** + * Remote user likes one of our posts. + * Confirm the post is ours, and save a local favorite event. + */ + + function handleFavorite() + { + // WORST VARIABLE NAME EVER + $object = $this->act->object; + + switch ($this->act->object->type) { + case ActivityObject::ARTICLE: + case ActivityObject::BLOGENTRY: + case ActivityObject::NOTE: + case ActivityObject::STATUS: + case ActivityObject::COMMENT: + break; + default: + throw new ClientException("Can't handle that kind of object for liking/faving."); + } + + $notice = Notice::staticGet('uri', $object->id); + + if (empty($notice)) { + throw new ClientException("Notice with ID $object->id unknown."); + } + + if ($notice->profile_id != $this->user->id) { + throw new ClientException("Notice with ID $object->id not posted by $this->user->id."); + } + + $profile = $this->ensureProfile(); + + $old = Fave::pkeyGet(array('user_id' => $profile->id, + 'notice_id' => $notice->id)); + + if (!empty($old)) { + throw new ClientException("We already know that's a fave!"); + } + + $fave = new Fave(); + + // @fixme need to change this attribute name, maybe references + $fave->user_id = $profile->id; + $fave->notice_id = $notice->id; + + $result = $fave->insert(); + + if (!$result) { + common_log_db_error($fave, 'INSERT', __FILE__); + throw new ServerException('Could not save new favorite.'); + } + } + + /** + * Remote user doesn't like one of our posts after all! + * Confirm the post is ours, and save a local favorite event. + */ + function handleUnfavorite() + { + } + +} -- cgit v1.2.3-54-g00ecf From 9c2fe8492f7dae183e0369f8d43f124fd80e4433 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Sat, 20 Feb 2010 15:56:36 -0800 Subject: OStatus: send favorite/unfavorite notifications to remote authors --- classes/Notice.php | 32 ++++++++++++++++++++++++++++++++ plugins/OStatus/OStatusPlugin.php | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/classes/Notice.php b/classes/Notice.php index a52cfed70..8b8f90474 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1104,6 +1104,38 @@ class Notice extends Memcached_DataObject return $xs->getString(); } + /** + * Returns an XML string fragment with a reference to a notice as an + * Activity Streams noun object with the given element type. + * + * Assumes that 'activity' namespace has been previously defined. + * + * @param string $element one of 'subject', 'object', 'target' + * @return string + */ + function asActivityNoun($element) + { + $xs = new XMLStringer(true); + + $xs->elementStart('activity:' . $element); + $xs->element('activity:object-type', + null, + 'http://activitystrea.ms/schema/1.0/note'); + $xs->element('id', + null, + $this->uri); + $xs->element('content', + array('type' => 'text/html'), + $this->rendered); + $xs->element('link', + array('type' => 'text/html', + 'rel' => 'permalink', + 'href' => $this->bestUrl())); + $xs->elementEnd('activity:' . $element); + + return $xs->getString(); + } + function bestUrl() { if (!empty($this->url)) { diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index e78e658a6..4cbf78e63 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -357,4 +357,39 @@ class OStatusPlugin extends Plugin return true; } + + /** + * Notify remote users when their notices get favorited. + * + * @param Profile or User $profile of local user doing the faving + * @param Notice $notice being favored + * @return hook return value + */ + function onEndFavorNotice($profile, Notice $notice) + { + if ($profile instanceof User) { + // @fixme upstream function should clarify its parameters + $profile = $profile->getProfile(); + } + $oprofile = Ostatus_profile::staticGet('profile_id', $notice->profile_id); + if ($oprofile) { + $oprofile->notify($profile, ActivityVerb::FAVORITE, $notice); + } + } + + /** + * Notify remote users when their notices get de-favorited. + * + * @param Profile or User $profile of local user doing the de-faving + * @param Notice $notice being favored + * @return hook return value + */ + function onEndDisfavorNotice(Profile $profile, Notice $notice) + { + $oprofile = Ostatus_profile::staticGet('profile_id', $notice->profile_id); + if ($oprofile) { + $oprofile->notify($profile, ActivityVerb::UNFAVORITE, $notice); + } + } + } -- cgit v1.2.3-54-g00ecf From 96c6019638f6407b38fbc5a45485a3797af47adb Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sat, 20 Feb 2010 19:58:20 -0500 Subject: Add support for favor and disfavor notification Added support for favoring and disfavoring in OStatusPlugin. Needed to represent the Notice as an activity:object, so added some code for that in lib/activity.php. Also, made some small changes to OStatusPlugin so it handled having a non-default argument $object correctly. --- plugins/OStatus/OStatusPlugin.php | 50 +++++++++++++++++++++----- plugins/OStatus/classes/Ostatus_profile.php | 6 ++-- plugins/OStatus/lib/activity.php | 55 +++++++++++++++++++++++++++-- 3 files changed, 98 insertions(+), 13 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 9e6d03177..98e048c14 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -255,7 +255,7 @@ class OStatusPlugin extends Plugin { if ($user instanceof Profile) { $profile = $user; - } else if ($user instanceof Profile) { + } else if ($user instanceof User) { $profile = $user->getProfile(); } $oprofile = Ostatus_profile::staticGet('profile_id', $other->id); @@ -353,30 +353,64 @@ class OStatusPlugin extends Plugin // We have a local user subscribing to a remote profile; make the // magic happen! - $oprofile->notify($subscriber, ActivityVerb::FOLLOW); + $oprofile->notify($subscriber, ActivityVerb::FOLLOW, $oprofile); return true; } - function onEndUnsubscribe($subscriber, $other) + function onEndFavor($profile, $notice) { - $user = User::staticGet('id', $subscriber->id); + // is the favorer a local user? + + $user = User::staticGet('id', $profile->id); if (empty($user)) { return true; } - $oprofile = Ostatus_profile::staticGet('profile_id', $other->id); + // is the author an OStatus remote user? + + $oprofile = Ostatus_profile::staticGet('profile_id', $notice->profile_id); if (empty($oprofile)) { return true; } - // We have a local user subscribing to a remote profile; make the - // magic happen! + // Local user faved an Ostatus profile's notice; notify them! + + $obj = ActivityObject::fromNotice($notice); + + $oprofile->notify($profile, + ActivityVerb::FAVORITE, + $obj->asString()); + return true; + } + + function onEndDisfavor($profile, $notice) + { + // is the favorer a local user? + + $user = User::staticGet('id', $profile->id); + + if (empty($user)) { + return true; + } + + // is the author an OStatus remote user? + + $oprofile = Ostatus_profile::staticGet('profile_id', $notice->profile_id); + + if (empty($oprofile)) { + return true; + } + + // Local user faved an Ostatus profile's notice; notify them! - $oprofile->notify($subscriber, ActivityVerb::UNFOLLOW); + $obj = ActivityObject::fromNotice($notice); + $oprofile->notify($profile, + ActivityVerb::UNFAVORITE, + $obj->asString()); return true; } } diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 4dd565288..5bd899bc4 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -307,7 +307,7 @@ class Ostatus_profile extends Memcached_DataObject * * @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 + * @param string $object object of the action; if null, the remote entity itself is assumed */ public function notify($actor, $verb, $object=null) { @@ -319,7 +319,7 @@ class Ostatus_profile extends Memcached_DataObject throw new ServerException("Invalid actor passed to " . __METHOD__ . ": " . $type); } if ($object == null) { - $object = $this; + $object = $this->asActivityNoun('object'); } if ($this->salmonuri) { $text = 'update'; // @fixme @@ -345,7 +345,7 @@ class Ostatus_profile extends Memcached_DataObject $entry->element('activity:verb', null, $verb); $entry->raw($actor->asAtomAuthor()); $entry->raw($actor->asActivityActor()); - $entry->raw($object->asActivityNoun('object')); + $entry->raw($object); $entry->elementEnd('entry'); $xml = $entry->getString(); diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php index 5bc8f78e5..6d15f85b0 100644 --- a/plugins/OStatus/lib/activity.php +++ b/plugins/OStatus/lib/activity.php @@ -199,7 +199,7 @@ class ActivityObject public $link; public $source; - /** + /** * Constructor * * This probably needs to be refactored @@ -209,8 +209,12 @@ class ActivityObject * @param DOMElement $element DOM thing to turn into an Activity thing */ - function __construct($element) + function __construct($element = null) { + if (empty($element)) { + return; + } + $this->element = $element; if ($element->tagName == 'author') { @@ -279,6 +283,53 @@ class ActivityObject } } } + + static fromNotice($notice) + { + $object = new ActivityObject(); + + $object->type = ActivityObject::NOTE; + + $object->id = $notice->uri; + $object->title = $notice->content; + $object->content = $notice->rendered; + $object->link = $notice->bestUrl(); + + return $object; + } + + function asString($tag='activity:object') + { + $xs = new XMLStringer(true); + + $xs->elementStart($tag); + + $xs->element('activity:object-type', null, $this->type); + + $xs->element(self::ID, null, $this->id); + + if (!empty($this->title)) { + $xs->element(self::TITLE, null, $this->title); + } + + if (!empty($this->summary)) { + $xs->element(self::SUMMARY, null, $this->summary); + } + + if (!empty($this->content)) { + // XXX: assuming HTML content here + $xs->element(self::CONTENT, array('type' => 'html'), $this->content); + } + + if (!empty($this->link)) { + $xs->element('link', array('rel' => 'alternate', 'type' => 'text/html'), + $this->content); + } + + $xs->elementEnd($tag); + + return $xs->getString(); + } } /** -- cgit v1.2.3-54-g00ecf From 0c62c686756de0752cdd44b63c10cf6e4047860f Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sat, 20 Feb 2010 20:34:29 -0500 Subject: do some double-checks on favor and disfavor handlers in OStatusPlugin --- plugins/OStatus/OStatusPlugin.php | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 5081c4d98..29799b0dc 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -365,16 +365,21 @@ class OStatusPlugin extends Plugin * @param Notice $notice being favored * @return hook return value */ - function onEndFavorNotice($profile, Notice $notice) + function onEndFavorNotice(Profile $profile, Notice $notice) { - if ($profile instanceof User) { - // @fixme upstream function should clarify its parameters - $profile = $profile->getProfile(); + $user = User::staticGet('id', $profile->id); + + if (empty($user)) { + return true; } + $oprofile = Ostatus_profile::staticGet('profile_id', $notice->profile_id); + if ($oprofile) { $oprofile->notify($profile, ActivityVerb::FAVORITE, $notice); } + + return true; } /** @@ -386,10 +391,18 @@ class OStatusPlugin extends Plugin */ function onEndDisfavorNotice(Profile $profile, Notice $notice) { + $user = User::staticGet('id', $profile->id); + + if (empty($user)) { + return true; + } + $oprofile = Ostatus_profile::staticGet('profile_id', $notice->profile_id); + if ($oprofile) { $oprofile->notify($profile, ActivityVerb::UNFAVORITE, $notice); } - } + return true; + } } -- cgit v1.2.3-54-g00ecf From df7c6b37c8b8f548a56193181acd5b2d8ee9bd9e Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 10:53:32 -0500 Subject: use notifyActivity() for notifications in OStatusPlugin --- plugins/OStatus/OStatusPlugin.php | 122 ++++++++++++++++++++++++++++++-------- 1 file changed, 98 insertions(+), 24 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 29799b0dc..3aaa769db 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -251,27 +251,47 @@ class OStatusPlugin extends Plugin * @param Profile $other * @return hook return value */ - function onEndUnsubscribe($user, $other) + function onEndUnsubscribe($profile, $other) { - if ($user instanceof Profile) { - $profile = $user; - } else if ($user instanceof User) { - $profile = $user->getProfile(); + $user = User::staticGet('id', $profile->id); + + if (empty($user)) { + return true; } + $oprofile = Ostatus_profile::staticGet('profile_id', $other->id); - if ($oprofile) { - // Notify the remote server of the unsub, if supported. - $oprofile->notify($profile, ActivityVerb::UNFOLLOW, $oprofile); - - // Drop the PuSH subscription if there are no other subscribers. - $sub = new Subscription(); - $sub->subscribed = $other->id; - $sub->limit(1); - if (!$sub->find(true)) { - common_log(LOG_INFO, "Unsubscribing from now-unused feed $oprofile->feeduri"); - $oprofile->unsubscribe(); - } + + if (empty($oprofile)) { + return true; } + + // Drop the PuSH subscription if there are no other subscribers. + + if ($other->subscriberCount() == 0) { + common_log(LOG_INFO, "Unsubscribing from now-unused feed $oprofile->feeduri"); + $oprofile->unsubscribe(); + } + + $act = new Activity(); + + $act->verb = ActivityVerb::UNFOLLOW; + + $act->id = TagURI::mint('unfollow:%d:%d:%s', + $profile->id, + $other->id, + common_date_iso8601(time())); + + $act->time = time(); + $act->title = _("Unfollow"); + $act->content = sprintf(_("%s stopped following %s."), + $profile->getBestName(), + $other->getBestName()); + + $act->actor = ActivityObject::fromProfile($subscriber); + $act->object = ActivityObject::fromProfile($other); + + $oprofile->notifyActivity($act); + return true; } @@ -350,10 +370,25 @@ class OStatusPlugin extends Plugin return true; } - // We have a local user subscribing to a remote profile; make the - // magic happen! + $act = new Activity(); + + $act->verb = ActivityVerb::FOLLOW; - $oprofile->notify($subscriber, ActivityVerb::FOLLOW, $oprofile); + $act->id = TagURI::mint('follow:%d:%d:%s', + $subscriber->id, + $other->id, + common_date_iso8601(time())); + + $act->time = time(); + $act->title = _("Follow"); + $act->content = sprintf(_("%s is now following %s."), + $subscriber->getBestName(), + $other->getBestName()); + + $act->actor = ActivityObject::fromProfile($subscriber); + $act->object = ActivityObject::fromProfile($other); + + $oprofile->notifyActivity($act); return true; } @@ -365,6 +400,7 @@ class OStatusPlugin extends Plugin * @param Notice $notice being favored * @return hook return value */ + function onEndFavorNotice(Profile $profile, Notice $notice) { $user = User::staticGet('id', $profile->id); @@ -375,10 +411,29 @@ class OStatusPlugin extends Plugin $oprofile = Ostatus_profile::staticGet('profile_id', $notice->profile_id); - if ($oprofile) { - $oprofile->notify($profile, ActivityVerb::FAVORITE, $notice); + if (empty($oprofile)) { + return true; } + $act = new Activity(); + + $act->verb = ActivityVerb::FAVORITE; + $act->id = TagURI::mint('favor:%d:%d:%s', + $profile->id, + $notice->id, + common_date_iso8601(time())); + + $act->time = time(); + $act->title = _("Favor"); + $act->content = sprintf(_("%s marked notice %s as a favorite."), + $profile->getBestName(), + $notice->uri); + + $act->actor = ActivityObject::fromProfile($profile); + $act->object = ActivityObject::fromNotice($notice); + + $oprofile->notifyActivity($act); + return true; } @@ -389,6 +444,7 @@ class OStatusPlugin extends Plugin * @param Notice $notice being favored * @return hook return value */ + function onEndDisfavorNotice(Profile $profile, Notice $notice) { $user = User::staticGet('id', $profile->id); @@ -399,10 +455,28 @@ class OStatusPlugin extends Plugin $oprofile = Ostatus_profile::staticGet('profile_id', $notice->profile_id); - if ($oprofile) { - $oprofile->notify($profile, ActivityVerb::UNFAVORITE, $notice); + if (empty($oprofile)) { + return true; } + $act = new Activity(); + + $act->verb = ActivityVerb::UNFAVORITE; + $act->id = TagURI::mint('disfavor:%d:%d:%s', + $profile->id, + $notice->id, + common_date_iso8601(time())); + $act->time = time(); + $act->title = _("Disfavor"); + $act->content = sprintf(_("%s marked notice %s as no longer a favorite."), + $profile->getBestName(), + $notice->uri); + + $act->actor = ActivityObject::fromProfile($profile); + $act->object = ActivityObject::fromNotice($notice); + + $oprofile->notifyActivity($act); + return true; } } -- cgit v1.2.3-54-g00ecf From 5aeed9e04110c34bca12e601836797afd5acadba Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 13:11:00 -0500 Subject: add activity:subject to atom feeds --- plugins/OStatus/OStatusPlugin.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 3aaa769db..b5cfb5cae 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -102,16 +102,21 @@ class OStatusPlugin extends Plugin $id = null; if ($feed instanceof AtomUserNoticeFeed) { - $salmonAction = 'salmon'; - $id = $feed->getUser()->id; + $salmonAction = 'usersalmon'; + $user = $feed->getUser(); + $id = $user->id; + $profile = $user->getProfile(); + $feed->setActivitySubject($profile->asActivityNoun('subject')); } else if ($feed instanceof AtomGroupNoticeFeed) { - $salmonAction = 'salmongroup'; - $id = $feed->getGroup()->id; + $salmonAction = 'groupsalmon'; + $group = $feed->getGroup(); + $id = $group->id; + $feed->setActivitySubject($group->asActivitySubject()); } else { - return; + return true; } - if (!empty($id)) { + if (!empty($id)) { $hub = common_config('ostatus', 'hub'); if (empty($hub)) { // Updates will be handled through our internal PuSH hub. @@ -123,6 +128,8 @@ class OStatusPlugin extends Plugin $salmon = common_local_url($salmonAction, array('id' => $id)); $feed->addLink($salmon, array('rel' => 'salmon')); } + + return true; } /** -- cgit v1.2.3-54-g00ecf From de093537f6c7f3b83817146247fbd9edbed16935 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 13:32:24 -0500 Subject: correct actor for OStatusPlugin::onEndUnsubscribe() --- plugins/OStatus/OStatusPlugin.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index b5cfb5cae..b966661db 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -294,7 +294,7 @@ class OStatusPlugin extends Plugin $profile->getBestName(), $other->getBestName()); - $act->actor = ActivityObject::fromProfile($subscriber); + $act->actor = ActivityObject::fromProfile($profile); $act->object = ActivityObject::fromProfile($other); $oprofile->notifyActivity($act); @@ -447,8 +447,9 @@ class OStatusPlugin extends Plugin /** * Notify remote users when their notices get de-favorited. * - * @param Profile or User $profile of local user doing the de-faving - * @param Notice $notice being favored + * @param Profile $profile Profile person doing the de-faving + * @param Notice $notice Notice being favored + * * @return hook return value */ -- cgit v1.2.3-54-g00ecf From 5349aa420ed45c2f5bf773d10c7709826ae6babd Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Sun, 21 Feb 2010 13:40:59 -0800 Subject: OStatus feedsub fixlets: - actually udpate feedsub.last_update when we get a new PuSH update in - move incoming PuSH processing to a queue handler to minimize time spent before POST return, as recommended by PuSH spec. When queues are disabled this'll still be handled immediately. --- plugins/OStatus/OStatusPlugin.php | 4 +++ plugins/OStatus/actions/pushcallback.php | 9 +++-- plugins/OStatus/classes/FeedSub.php | 10 ++++++ plugins/OStatus/lib/pushinputqueuehandler.php | 49 +++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 plugins/OStatus/lib/pushinputqueuehandler.php (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index b966661db..c5a2db3d8 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -78,9 +78,13 @@ class OStatusPlugin extends Plugin */ function onEndInitializeQueueManager(QueueManager $qm) { + // Outgoing from our internal PuSH hub $qm->connect('hubverify', 'HubVerifyQueueHandler'); $qm->connect('hubdistrib', 'HubDistribQueueHandler'); $qm->connect('hubout', 'HubOutQueueHandler'); + + // Incoming from a foreign PuSH hub + $qm->connect('pushinput', 'PushInputQueueHandler'); return true; } diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php index 7e1227a66..9e976a80d 100644 --- a/plugins/OStatus/actions/pushcallback.php +++ b/plugins/OStatus/actions/pushcallback.php @@ -60,9 +60,14 @@ class PushCallbackAction extends Action $post = file_get_contents('php://input'); - // @fixme Queue this to a background process; we should return + // Queue this to a background process; we should return // as quickly as possible from a distribution POST. - $feedsub->receive($post, $hmac); + // If queues are disabled this'll process immediately. + $data = array('feedsub_id' => $feedsub->id, + 'post' => $post, + 'hmac' => $hmac); + $qm = QueueManager::get(); + $qm->enqueue($data, 'pushinput'); } /** diff --git a/plugins/OStatus/classes/FeedSub.php b/plugins/OStatus/classes/FeedSub.php index bf9d063fa..31241d3de 100644 --- a/plugins/OStatus/classes/FeedSub.php +++ b/plugins/OStatus/classes/FeedSub.php @@ -372,6 +372,12 @@ class FeedSub extends Memcached_DataObject * feed (as a DOMDocument) will be passed to the StartFeedSubHandleFeed * and EndFeedSubHandleFeed events for processing. * + * Not guaranteed to be running in an immediate POST context; may be run + * from a queue handler. + * + * Side effects: the feedsub record's lastupdate field will be updated + * to the current time (not published time) if we got a legit update. + * * @param string $post source of Atom or RSS feed * @param string $hmac X-Hub-Signature header, if present */ @@ -402,6 +408,10 @@ class FeedSub extends Memcached_DataObject return; } + $orig = clone($this); + $this->last_update = common_sql_now(); + $this->update($orig); + Event::handle('StartFeedSubReceive', array($this, $feed)); Event::handle('EndFeedSubReceive', array($this, $feed)); } diff --git a/plugins/OStatus/lib/pushinputqueuehandler.php b/plugins/OStatus/lib/pushinputqueuehandler.php new file mode 100644 index 000000000..cbd9139b5 --- /dev/null +++ b/plugins/OStatus/lib/pushinputqueuehandler.php @@ -0,0 +1,49 @@ +. + */ + +/** + * Process a feed distribution POST from a PuSH hub. + * @package FeedSub + * @author Brion Vibber + */ + +class PushInputQueueHandler extends QueueHandler +{ + function transport() + { + return 'pushinput'; + } + + function handle($data) + { + assert(is_array($data)); + + $feedsub_id = $data['feedsub_id']; + $post = $data['post']; + $hmac = $data['hmac']; + + $feedsub = FeedSub::staticGet('id', $feedsub_id); + if ($feedsub) { + $feedsub->receive($post, $hmac); + } else { + common_log(LOG_ERR, "Discarding POST to unknown feed subscription id $feedsub_id"); + } + return true; + } +} -- cgit v1.2.3-54-g00ecf From bd74f05a665df73078134c37f059cc1797d1a0ea Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 21:38:16 -0500 Subject: Do mention lookup for Webfinger accounts in OStatusPlugin --- plugins/OStatus/OStatusPlugin.php | 79 ++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 42 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index c5a2db3d8..60bb144a8 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -136,25 +136,6 @@ class OStatusPlugin extends Plugin return true; } - /** - * Add the feed settings page to the Connect Settings menu - * - * @param Action &$action The calling page - * - * @return boolean hook return - */ - function onEndConnectSettingsNav(&$action) - { - $action_name = $action->trimmed('action'); - - $action->menuItem(common_local_url('feedsubsettings'), - _m('Feeds'), - _m('Feed subscription options'), - $action_name === 'feedsubsettings'); - - return true; - } - /** * Automatically load the actions and libraries used by the plugin * @@ -215,33 +196,16 @@ class OStatusPlugin extends Plugin * @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) { + $mentioned = $notice->getReplies(); - // FIXME: look up locally first + foreach ($mentioned as $profile) { - // Check to see if we've got an actual webfinger - $w = new Webfinger; + $oprofile = Ostatus_profile::staticGet('profile_id', $profile->id); - $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; - } + if (!empty($oprofile) && !empty($oprofile->salmonuri)) { // FIXME: this needs to go out in a queue handler @@ -249,9 +213,40 @@ class OStatusPlugin extends Plugin $xml .= $notice->asAtomEntry(); $salmon = new Salmon(); - $salmon->post($endpoint_uri, $xml); + $salmon->post($oprofile->salmonuri, $xml); + } + } + } + + /** + * + */ + + function onEndFindMentions($sender, $text, &$mentions) + { + preg_match_all('/(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)/', + $text, + $wmatches, + PREG_OFFSET_CAPTURE); + + foreach ($wmatches[1] as $wmatch) { + + $webfinger = $wmatch[0]; + + $oprofile = Ostatus_profile::ensureWebfinger($webfinger); + + if (!empty($oprofile)) { + + $profile = $oprofile->localProfile(); + + $mentions[] = array('mentioned' => array($profile), + 'text' => $wmatch[0], + 'position' => $wmatch[1], + 'url' => $profile->profileurl); } } + + return true; } /** -- cgit v1.2.3-54-g00ecf From ad10e6e8da5d1bc238fb90d7d9ef168c2525ceb4 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Sun, 21 Feb 2010 19:01:32 -0800 Subject: OStatus: drop the remnants of feedsubsettings, replaced by ostatussub and no longer linked in UI --- plugins/OStatus/OStatusPlugin.php | 2 - plugins/OStatus/actions/feedsubsettings.php | 230 ---------------------------- 2 files changed, 232 deletions(-) delete mode 100644 plugins/OStatus/actions/feedsubsettings.php (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 60bb144a8..304cf14ae 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -58,8 +58,6 @@ class OStatusPlugin extends Plugin $m->connect('main/push/callback/:feed', array('action' => 'pushcallback'), array('feed' => '[0-9]+')); - $m->connect('settings/feedsub', - array('action' => 'feedsubsettings')); // Salmon endpoint $m->connect('main/salmon/user/:id', diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php deleted file mode 100644 index aee4cee9a..000000000 --- a/plugins/OStatus/actions/feedsubsettings.php +++ /dev/null @@ -1,230 +0,0 @@ -. - */ - -/** - * @package FeedSubPlugin - * @maintainer Brion Vibber - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - -class FeedSubSettingsAction extends ConnectSettingsAction -{ - protected $profile_uri; - protected $preview; - protected $munger; - - /** - * Title of the page - * - * @return string Title of the page - */ - - function title() - { - return _m('Feed subscriptions'); - } - - /** - * Instructions for use - * - * @return instructions for use - */ - - function getInstructions() - { - return _m('You can subscribe to feeds from other sites; ' . - 'updates will appear in your personal timeline.'); - } - - /** - * Content area of the page - * - * Shows a form for associating a Twitter account with this - * StatusNet account. Also lets the user set preferences. - * - * @return void - */ - - function showContent() - { - $user = common_current_user(); - - $profile = $user->getProfile(); - - $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('profile_uri', - _m('Feed URL'), - $this->profile_uri, - _m('Enter the profile URL of a PubSubHubbub-enabled feed')); - $this->elementEnd('li'); - $this->elementEnd('ul'); - - if ($this->preview) { - $this->submit('subscribe', _m('Subscribe')); - } else { - $this->submit('validate', _m('Continue')); - } - - $this->elementEnd('fieldset'); - - $this->elementEnd('form'); - - if ($this->preview) { - $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('validate')) { - $this->validateAndPreview(); - } else 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() - { - $profile_uri = trim($this->arg('profile_uri')); - - if ($profile_uri == '') { - $this->showForm(_m('Empty remote profile URL!')); - return; - } - $this->profile_uri = $profile_uri; - - // @fixme validate, normalize bla bla - try { - $oprofile = Ostatus_profile::ensureProfile($this->profile_uri); - $this->oprofile = $oprofile; - return true; - } catch (FeedSubBadURLException $e) { - $err = _m('Invalid URL or could not reach server.'); - } catch (FeedSubBadResponseException $e) { - $err = _m('Cannot read feed; server returned error.'); - } catch (FeedSubEmptyException $e) { - $err = _m('Cannot read feed; server returned an empty page.'); - } catch (FeedSubBadHTMLException $e) { - $err = _m('Bad HTML, could not find feed link.'); - } catch (FeedSubNoFeedException $e) { - $err = _m('Could not find a feed linked from this URL.'); - } catch (FeedSubUnrecognizedTypeException $e) { - $err = _m('Not a recognized feed type.'); - } catch (FeedSubException $e) { - // Any new ones we forgot about - $err = sprintf(_m('Bad feed URL: %s %s'), get_class($e), $e->getMessage()); - } - - $this->showForm($err); - return false; - } - - function saveFeed() - { - if ($this->validateFeed()) { - $this->preview = true; - - // And subscribe the current user to the local profile - $user = common_current_user(); - - if (!$this->oprofile->subscribe()) { - $this->showForm(_m("Failed to set up server-to-server subscription.")); - return; - } - - if ($this->oprofile->isGroup()) { - $group = $this->oprofile->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 { - $local = $this->oprofile->localProfile(); - if ($user->isSubscribed($local)) { - $this->showForm(_m('Already subscribed!')); - } elseif ($this->oprofile->subscribeLocalToRemote($user)) { - $this->showForm(_m('Remote user subscribed!')); - } else { - $this->showForm(_m('Remote subscription failed!')); - } - } - } - } - - function validateAndPreview() - { - if ($this->validateFeed()) { - $this->preview = true; - $this->showForm(_m('Previewing feed:')); - } - } - - function previewFeed() - { - $this->text('Profile preview should go here'); - } - - function showScripts() - { - parent::showScripts(); - $this->autofocus('feedurl'); - } -} -- cgit v1.2.3-54-g00ecf From bd21f48ceed83699ca4c4613805b1edc1bf49f8c Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 22:34:40 -0500 Subject: Notice::getReplies() returns array of profile IDs --- plugins/OStatus/OStatusPlugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 60bb144a8..fdc07b813 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -201,9 +201,9 @@ class OStatusPlugin extends Plugin { $mentioned = $notice->getReplies(); - foreach ($mentioned as $profile) { + foreach ($mentioned as $profile_id) { - $oprofile = Ostatus_profile::staticGet('profile_id', $profile->id); + $oprofile = Ostatus_profile::staticGet('profile_id', $profile_id); if (!empty($oprofile) && !empty($oprofile->salmonuri)) { -- cgit v1.2.3-54-g00ecf From de522d7978e0a843e338561b35a98b3e47a1e93c Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 22:38:18 -0500 Subject: Wasn't putting in namespaces for reply salmons --- plugins/OStatus/OStatusPlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index ec6f3f3b0..641765dae 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -208,7 +208,7 @@ class OStatusPlugin extends Plugin // FIXME: this needs to go out in a queue handler $xml = ''; - $xml .= $notice->asAtomEntry(); + $xml .= $notice->asAtomEntry(true, true); $salmon = new Salmon(); $salmon->post($oprofile->salmonuri, $xml); -- cgit v1.2.3-54-g00ecf From cc18f757a8e6ef854cae89d8947efac505b1fd7c Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 22:52:52 -0500 Subject: hook in OStatusPlugin to return Ostatus_profile URIs where applicable --- plugins/OStatus/OStatusPlugin.php | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 641765dae..a8c292301 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -484,4 +484,14 @@ class OStatusPlugin extends Plugin return true; } + + function onStartGetProfileUri($profile, &$uri) + { + $oprofile = Ostatus_profile::staticGet('profile_id', $profile->id); + if (!empty($oprofile)) { + $uri = $oprofile->uri; + return false; + } + return true; + } } -- cgit v1.2.3-54-g00ecf From 5207783765328a3d6f0101f143edb8807247bcfe Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Sun, 21 Feb 2010 19:51:11 -0800 Subject: OStatus: record source profile & saving method in ostatus_source table; this allows us to distinguish posts that have come through an unverified group feed --- plugins/OStatus/OStatusPlugin.php | 1 + plugins/OStatus/classes/Ostatus_profile.php | 4 +- plugins/OStatus/classes/Ostatus_source.php | 114 ++++++++++++++++++++++++++++ plugins/OStatus/lib/salmonaction.php | 13 +++- 4 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 plugins/OStatus/classes/Ostatus_source.php (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 641765dae..5b9e3be2b 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -305,6 +305,7 @@ class OStatusPlugin extends Plugin function onCheckSchema() { $schema = Schema::get(); $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef()); + $schema->ensureTable('ostatus_source', Ostatus_source::schemaDef()); $schema->ensureTable('feedsub', FeedSub::schemaDef()); $schema->ensureTable('hubsub', HubSub::schemaDef()); return true; diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 3bed1c2aa..71885bcdc 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -508,13 +508,15 @@ class Ostatus_profile extends Memcached_DataObject } } - // @fixme save detailed ostatus source info // @fixme ensure that groups get handled correctly $saved = Notice::saveNew($oprofile->localProfile()->id, $content, 'ostatus', $params); + + // Record which feed this came through... + Ostatus_source::saveNew($saved, $this, 'push'); } /** diff --git a/plugins/OStatus/classes/Ostatus_source.php b/plugins/OStatus/classes/Ostatus_source.php new file mode 100644 index 000000000..e6ce7d442 --- /dev/null +++ b/plugins/OStatus/classes/Ostatus_source.php @@ -0,0 +1,114 @@ +. + */ + +/** + * @package OStatusPlugin + * @maintainer Brion Vibber + */ + +class Ostatus_source extends Memcached_DataObject +{ + public $__table = 'ostatus_source'; + + public $notice_id; // notice we're referring to + public $profile_uri; // uri of the ostatus_profile this came through -- may be a group feed + public $method; // push or salmon + + 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('notice_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'profile_uri' => DB_DATAOBJECT_STR, + 'method' => DB_DATAOBJECT_STR); + } + + static function schemaDef() + { + return array(new ColumnDef('notice_id', 'integer', + null, false, 'PRI'), + new ColumnDef('profile_uri', 'varchar', + 255, false), + new ColumnDef('method', "ENUM('push','salmon')", + 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('notice_id' => 'K'); + } + + function sequenceKey() + { + return array(false, false, false); + } + + /** + * Save a remote notice source record; this helps indicate how trusted we are. + * @param string $method + */ + public static function saveNew(Notice $notice, Ostatus_profile $oprofile, $method) + { + $osource = new Ostatus_source(); + $osource->notice_id = $notice->id; + $osource->profile_uri = $oprofile->uri; + $osource->method = $method; + if ($osource->insert()) { + return true; + } else { + common_log_db_error($osource, 'INSERT', __FILE__); + return false; + } + } +} diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 11c411c7d..d93cc9aa4 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -202,9 +202,14 @@ class SalmonAction extends Action $options['created'] = common_sql_time($this->act->time); } - return Notice::saveNew($oprofile->profile_id, - $content, - 'ostatus+salmon', - $options); + $saved = Notice::saveNew($oprofile->profile_id, + $content, + 'ostatus+salmon', + $options); + + // Record that this was saved through a validated Salmon source + // @fixme actually do the signature validation! + Ostatus_source::saveNew($saved, $oprofile, 'salmon'); + return $saved; } } -- cgit v1.2.3-54-g00ecf From a9599d53c5e33eff2927f02393b9ae6984ba3a6e Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 21 Feb 2010 23:39:52 -0500 Subject: some info code for usersalmon.php --- plugins/OStatus/OStatusPlugin.php | 2 ++ plugins/OStatus/actions/usersalmon.php | 3 +++ 2 files changed, 5 insertions(+) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 548c87711..3ac2bb87d 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -205,6 +205,8 @@ class OStatusPlugin extends Plugin if (!empty($oprofile) && !empty($oprofile->salmonuri)) { + common_log(LOG_INFO, "Sending notice '{$notice->uri}' to remote profile '{$oprofile->uri}'."); + // FIXME: this needs to go out in a queue handler $xml = ''; diff --git a/plugins/OStatus/actions/usersalmon.php b/plugins/OStatus/actions/usersalmon.php index 8368eeccf..c8a16e06f 100644 --- a/plugins/OStatus/actions/usersalmon.php +++ b/plugins/OStatus/actions/usersalmon.php @@ -55,6 +55,8 @@ class UsersalmonAction extends SalmonAction */ function handlePost() { + common_log(LOG_INFO, "Received post of '{$this->act->object->id}' from '{$this->act->actor->id}'"); + switch ($this->act->object->type) { case ActivityObject::ARTICLE: case ActivityObject::BLOGENTRY: @@ -92,6 +94,7 @@ class UsersalmonAction extends SalmonAction if (!empty($existing)) { common_log(LOG_ERR, "Not saving notice '{$existing->uri}'; already exists."); + return; } $this->saveNotice(); -- cgit v1.2.3-54-g00ecf From effa4f5d1efffcb52d4a95562e0b4831f7bd3435 Mon Sep 17 00:00:00 2001 From: James Walker Date: Mon, 22 Feb 2010 01:39:00 -0500 Subject: adding extlib for Crypt_RSA --- plugins/OStatus/OStatusPlugin.php | 2 ++ 1 file changed, 2 insertions(+) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 60bb144a8..24ed23a00 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -24,6 +24,8 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/'); + class FeedSubException extends Exception { } -- cgit v1.2.3-54-g00ecf From 3569493ba7e77a1a9f19bdbbf3f2d5f262ea8484 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Mon, 22 Feb 2010 17:07:48 +0100 Subject: Added link to Subscriptions page to XHR get the ostatus sub form --- plugins/OStatus/OStatusPlugin.php | 17 +++++++++++++++++ plugins/OStatus/actions/ostatussub.php | 19 ++++++++++++++++--- plugins/OStatus/theme/base/css/ostatus.css | 30 ++++++++++++++++++++++++------ 3 files changed, 57 insertions(+), 9 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 3ac2bb87d..0b0317316 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -497,4 +497,21 @@ class OStatusPlugin extends Plugin } return true; } + + function onStartShowSubscriptionsContent($action) + { + $user = common_current_user(); + if ($user && ($user->id == $action->profile->id)) { + $action->elementStart('div', 'entity_actions'); + $action->elementStart('p', array('id' => 'entity_remote_subscribe', + 'class' => 'entity_subscribe')); + $action->element('a', array('href' => common_local_url('ostatussub'), + 'class' => 'entity_remote_subscribe') + , _m('Subscribe to remote user')); + $action->elementEnd('p'); + $action->elementEnd('div'); + } + + return true; + } } diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php index 8cb8e2ae7..95dec19af 100644 --- a/plugins/OStatus/actions/ostatussub.php +++ b/plugins/OStatus/actions/ostatussub.php @@ -55,7 +55,20 @@ class OStatusSubAction extends Action function showForm($error=null) { $this->error = $error; - $this->showPage(); + 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, _m('Subscribe to user')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->showContent(); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + $this->showPage(); + } } function showPageNotice() @@ -81,7 +94,7 @@ class OStatusSubAction extends Action $profile = $user->getProfile(); $this->elementStart('form', array('method' => 'post', - 'id' => 'ostatus_sub', + 'id' => 'form_ostatus_sub', 'class' => 'form_settings', 'action' => common_local_url('ostatussub'))); @@ -141,7 +154,7 @@ class OStatusSubAction extends Action if ($this->profile_uri) { $this->validateAndPreview(); } else { - $this->showPage(); + $this->showForm(); } } } diff --git a/plugins/OStatus/theme/base/css/ostatus.css b/plugins/OStatus/theme/base/css/ostatus.css index 9bc90a731..feeeb47d3 100644 --- a/plugins/OStatus/theme/base/css/ostatus.css +++ b/plugins/OStatus/theme/base/css/ostatus.css @@ -7,24 +7,42 @@ * @link http://status.net/ */ -#form_ostatus_connect.dialogbox { +#form_ostatus_connect.dialogbox, +#form_ostatus_sub.dialogbox { width:70%; background-image:none; } -#form_ostatus_connect.dialogbox .form_data label { +#form_ostatus_sub.dialogbox { +width:65%; +} +#form_ostatus_connect.dialogbox .form_data label, +#form_ostatus_sub.dialogbox .form_data label { width:34%; } -#form_ostatus_connect.dialogbox .form_data input { +#form_ostatus_connect.dialogbox .form_data input, +#form_ostatus_sub.dialogbox .form_data input { width:57%; } -#form_ostatus_connect.dialogbox .form_data .form_guide { +#form_ostatus_connect.dialogbox .form_data .form_guide, +#form_ostatus_sub.dialogbox .form_data .form_guide { margin-left:36%; } -#form_ostatus_connect.dialogbox #ostatus_nickname { +#form_ostatus_connect.dialogbox #ostatus_nickname, +#form_ostatus_sub.dialogbox #ostatus_nickname { display:none; } -#form_ostatus_connect.dialogbox .submit_dialogbox { +#form_ostatus_connect.dialogbox .submit_dialogbox, +#form_ostatus_sub.dialogbox .submit_dialogbox { min-width:96px; } + +#subscriptions #entity_remote_subscribe { +padding:0; +float:right; +} + +#subscriptions .entity_remote_subscribe { +float:right; +} -- cgit v1.2.3-54-g00ecf From 06f155c02df91ae81eb4401c738815ee46b802a6 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 22 Feb 2010 09:43:27 -0800 Subject: OStatus: initial hookup of remote group membership (notice delivery not yet working quite right) - added a temp config var to disable salmon magic signatures until they're working consistently --- plugins/OStatus/OStatusPlugin.php | 93 ++++++++++++++- plugins/OStatus/actions/groupsalmon.php | 77 ++++++++++++- plugins/OStatus/actions/ostatussub.php | 2 +- plugins/OStatus/classes/Ostatus_profile.php | 169 +++++++++++++++++++++------- plugins/OStatus/lib/activity.php | 6 + plugins/OStatus/lib/salmon.php | 21 +++- plugins/OStatus/lib/salmonaction.php | 23 +++- 7 files changed, 336 insertions(+), 55 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 7c6c0c69f..061ed4bd1 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -211,7 +211,7 @@ class OStatusPlugin extends Plugin // FIXME: this needs to go out in a queue handler - $xml = ''; + $xml = ''; $xml .= $notice->asAtomEntry(true, true); $salmon = new Salmon(); @@ -402,6 +402,97 @@ class OStatusPlugin extends Plugin return true; } + /** + * When one of our local users tries to join a remote group, + * notify the remote server. If the notification is rejected, + * deny the join. + * + * @param User_group $group + * @param User $user + * + * @return mixed hook return value + */ + + function onStartJoinGroup($group, $user) + { + $oprofile = Ostatus_profile::staticGet('group_id', $group->id); + if ($oprofile) { + $member = Profile::staticGet($user->id); + + $act = new Activity(); + $act->id = TagURI::mint('join:%d:%d:%s', + $member->id, + $group->id, + common_date_iso8601(time())); + + $act->actor = ActivityObject::fromProfile($member); + $act->verb = ActivityVerb::JOIN; + $act->object = $oprofile->asActivityObject(); + + $act->time = time(); + $act->title = _m("Join"); + $act->content = sprintf(_m("%s has joined group %s."), + $member->getBestName(), + $oprofile->getBestName()); + + if ($oprofile->notifyActivity($act)) { + return true; + } else { + throw new ServerException(_m("Failed joining remote group.")); + } + } + } + + /** + * When one of our local users leaves a remote group, notify the remote + * server. + * + * @fixme Might be good to schedule a resend of the leave notification + * if it failed due to a transitory error. We've canceled the local + * membership already anyway, but if the remote server comes back up + * it'll be left with a stray membership record. + * + * @param User_group $group + * @param User $user + * + * @return mixed hook return value + */ + + function onEndLeaveGroup($group, $user) + { + $oprofile = Ostatus_profile::staticGet('group_id', $group->id); + if ($oprofile) { + // Drop the PuSH subscription if there are no other subscribers. + + $members = $group->getMembers(0, 1); + if ($members->N == 0) { + common_log(LOG_INFO, "Unsubscribing from now-unused group feed $oprofile->feeduri"); + $oprofile->unsubscribe(); + } + + + $member = Profile::staticGet($user->id); + + $act = new Activity(); + $act->id = TagURI::mint('leave:%d:%d:%s', + $member->id, + $group->id, + common_date_iso8601(time())); + + $act->actor = ActivityObject::fromProfile($member); + $act->verb = ActivityVerb::LEAVE; + $act->object = $oprofile->asActivityObject(); + + $act->time = time(); + $act->title = _m("Leave"); + $act->content = sprintf(_m("%s has left group %s."), + $member->getBestName(), + $oprofile->getBestName()); + + $oprofile->notifyActivity($act); + } + } + /** * Notify remote users when their notices get favorited. * diff --git a/plugins/OStatus/actions/groupsalmon.php b/plugins/OStatus/actions/groupsalmon.php index 64ae9f3cc..2e4fe9443 100644 --- a/plugins/OStatus/actions/groupsalmon.php +++ b/plugins/OStatus/actions/groupsalmon.php @@ -88,21 +88,96 @@ class GroupsalmonAction extends SalmonAction * Save a subscription relationship for them. */ + /** + * Postel's law: consider a "follow" notification as a "join". + */ function handleFollow() { - $this->handleJoin(); // ??? + $this->handleJoin(); } + /** + * Postel's law: consider an "unfollow" notification as a "leave". + */ function handleUnfollow() { + $this->handleLeave(); } /** * A remote user joined our group. + * @fixme move permission checks and event call into common code, + * currently we're doing the main logic in joingroup action + * and so have to repeat it here. */ function handleJoin() { + $oprofile = $this->ensureProfile(); + if (!$oprofile) { + $this->clientError(_m("Can't read profile to set up group membership.")); + } + if ($oprofile->isGroup()) { + $this->clientError(_m("Groups can't join groups.")); + } + + common_log(LOG_INFO, "Remote profile {$oprofile->uri} joining local group {$this->group->nickname}"); + $profile = $oprofile->localProfile(); + + if ($profile->isMember($this->group)) { + // Already a member; we'll take it silently to aid in resolving + // inconsistencies on the other side. + return true; + } + + if (Group_block::isBlocked($this->group, $profile)) { + $this->clientError(_('You have been blocked from that group by the admin.'), 403); + return false; + } + + try { + // @fixme that event currently passes a user from main UI + // Event should probably move into Group_member::join + // and take a Profile object. + // + //if (Event::handle('StartJoinGroup', array($this->group, $profile))) { + Group_member::join($this->group->id, $profile->id); + //Event::handle('EndJoinGroup', array($this->group, $profile)); + //} + } catch (Exception $e) { + $this->serverError(sprintf(_m('Could not join remote user %1$s to group %2$s.'), + $oprofile->uri, $this->group->nickname)); + } + } + + /** + * A remote user left our group. + */ + + function handleLeave() + { + $oprofile = $this->ensureProfile(); + if (!$oprofile) { + $this->clientError(_m("Can't read profile to cancel group membership.")); + } + if ($oprofile->isGroup()) { + $this->clientError(_m("Groups can't join groups.")); + } + + common_log(LOG_INFO, "Remote profile {$oprofile->uri} leaving local group {$this->group->nickname}"); + $profile = $oprofile->localProfile(); + + try { + // @fixme event needs to be refactored as above + //if (Event::handle('StartLeaveGroup', array($this->group, $profile))) { + Group_member::leave($this->group->id, $profile->id); + //Event::handle('EndLeaveGroup', array($this->group, $profile)); + //} + } catch (Exception $e) { + $this->serverError(sprintf(_m('Could not remove remote user %1$s from group %2$s.'), + $oprofile->uri, $this->group->nickname)); + return; + } } } diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php index 95dec19af..592ae387e 100644 --- a/plugins/OStatus/actions/ostatussub.php +++ b/plugins/OStatus/actions/ostatussub.php @@ -248,7 +248,7 @@ class OStatusSubAction extends Action $group = $this->oprofile->localGroup(); if ($user->isMember($group)) { $this->showForm(_m('Already a member!')); - } elseif (Group_member::join($this->profile->group_id, $user->id)) { + } elseif (Group_member::join($this->oprofile->group_id, $user->id)) { $this->showForm(_m('Joined remote group!')); } else { $this->showForm(_m('Remote group join failed!')); diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 0e12f8fc6..c0e39add8 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -137,12 +137,49 @@ class Ostatus_profile extends Memcached_DataObject return null; } + /** + * Returns an ActivityObject describing this remote user or group profile. + * Can then be used to generate Atom chunks. + * + * @return ActivityObject + */ + function asActivityObject() + { + if ($this->isGroup()) { + $object = new ActivityObject(); + $object->type = 'http://activitystrea.ms/schema/1.0/group'; + $object->id = $this->uri; + $self = $this->localGroup(); + + // @fixme put a standard getAvatar() interface on groups too + if ($self->homepage_logo) { + $object->avatar = $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])) { + // @fixme this ain't used/saved yet + $object->avatarType = $map[$extension]; + } + } + + $object->link = $this->uri; // @fixme accurate? + return $object; + } else { + return ActivityObject::fromProfile($this->localProfile()); + } + } + /** * 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. * + * @fixme replace with wrappers on asActivityObject when it's got everything. + * * @param string $element one of 'actor', 'subject', 'object', 'target' * @return string */ @@ -202,11 +239,19 @@ class Ostatus_profile extends Memcached_DataObject } /** - * Damn dirty hack! + * @return boolean true if this is a remote group */ function isGroup() { - return (strpos($this->feeduri, '/groups/') !== false); + if ($this->profile_id && !$this->group_id) { + return false; + } else if ($this->group_id && !$this->profile_id) { + return true; + } else if ($this->group_id && $this->profile_id) { + throw new ServerException("Invalid ostatus_profile state: both group and profile IDs set for $this->uri"); + } else { + throw new ServerException("Invalid ostatus_profile state: both group and profile IDs empty for $this->uri"); + } } /** @@ -353,22 +398,24 @@ class Ostatus_profile extends Memcached_DataObject common_log(LOG_INFO, "Posting to Salmon endpoint $this->salmonuri: $xml"); $salmon = new Salmon(); // ? - $salmon->post($this->salmonuri, $xml); + return $salmon->post($this->salmonuri, $xml); } + return false; } public function notifyActivity($activity) { if ($this->salmonuri) { - $xml = $activity->asString(true); + $xml = '' . + $activity->asString(true); $salmon = new Salmon(); // ? - $salmon->post($this->salmonuri, $xml); + return $salmon->post($this->salmonuri, $xml); } - return; + return false; } function getBestName() @@ -597,10 +644,23 @@ class Ostatus_profile extends Memcached_DataObject */ protected function updateAvatar($url) { + if ($this->isGroup()) { + $self = $this->localGroup(); + } else { + $self = $this->localProfile(); + } + if (!$self) { + throw new ServerException(sprintf( + _m("Tried to update avatar for unsaved remote profile %s"), + $this->uri)); + } + // @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); + if (!copy($url, $temp_filename)) { + throw new ServerException(sprintf(_m("Unable to fetch avatar from %s"), $url)); + } if ($this->isGroup()) { $id = $this->group_id; @@ -614,13 +674,7 @@ class Ostatus_profile extends Memcached_DataObject 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); - } + $self->setOriginal($filename); } protected static function getActivityObjectAvatar($object) @@ -747,6 +801,18 @@ class Ostatus_profile extends Memcached_DataObject self::createActivityObjectProfile($actor, $feeduri, $salmonuri); } + /** + * Create local ostatus_profile and profile/user_group entries for + * the provided remote user or group. + * + * @param ActivityObject $object + * @param string $feeduri + * @param string $salmonuri + * @param array $hints + * + * @fixme fold $feeduri/$salmonuri into $hints + * @return Ostatus_profile + */ protected static function createActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array()) { $homeuri = $object->id; @@ -784,46 +850,65 @@ class Ostatus_profile extends Memcached_DataObject } } - $profile = new Profile(); - $profile->nickname = $nickname; - $profile->fullname = $object->title; - if (!empty($object->link)) { - $profile->profileurl = $object->link; - } else if (array_key_exists('profileurl', $hints)) { - $profile->profileurl = $hints['profileurl']; - } - $profile->created = common_sql_now(); - - // @fixme bio - // @fixme tags/categories - // @fixme location? - // @todo tags from categories - // @todo lat/lon/location? - - $profile_id = $profile->insert(); - - if (!$profile_id) { - throw new ServerException("Can't save local profile"); - } - - // @fixme either need to do feed discovery here - // or need to split out some of the feed stuff - // so we can leave it empty until later. - $oprofile = new Ostatus_profile(); $oprofile->uri = $homeuri; $oprofile->feeduri = $feeduri; $oprofile->salmonuri = $salmonuri; - $oprofile->profile_id = $profile_id; $oprofile->created = common_sql_now(); $oprofile->modified = common_sql_now(); + if ($object->type == ActivityObject::PERSON) { + $profile = new Profile(); + $profile->nickname = $nickname; + $profile->fullname = $object->title; + if (!empty($object->link)) { + $profile->profileurl = $object->link; + } else if (array_key_exists('profileurl', $hints)) { + $profile->profileurl = $hints['profileurl']; + } + $profile->created = common_sql_now(); + + // @fixme bio + // @fixme tags/categories + // @fixme location? + // @todo tags from categories + // @todo lat/lon/location? + + $oprofile->profile_id = $profile->insert(); + + if (!$oprofile->profile_id) { + throw new ServerException("Can't save local profile"); + } + } else { + $group = new User_group(); + $group->nickname = $nickname; + $group->fullname = $object->title; + // @fixme no canonical profileurl; using homepage instead for now + $group->homepage = $homeuri; + $group->created = common_sql_now(); + + // @fixme homepage + // @fixme bio + // @fixme tags/categories + // @fixme location? + // @todo tags from categories + // @todo lat/lon/location? + + $oprofile->group_id = $group->insert(); + + if (!$oprofile->group_id) { + throw new ServerException("Can't save local profile"); + } + } + $ok = $oprofile->insert(); if ($ok) { - $oprofile->updateAvatar($avatar); + if ($avatar) { + $oprofile->updateAvatar($avatar); + } return $oprofile; } else { throw new ServerException("Can't save OStatus profile"); diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php index a26248f19..6cb9881bf 100644 --- a/plugins/OStatus/lib/activity.php +++ b/plugins/OStatus/lib/activity.php @@ -367,6 +367,9 @@ class ActivityObject return $object; } + /** + * @fixme missing avatar, bio info, etc + */ static function fromProfile($profile) { $object = new ActivityObject(); @@ -379,6 +382,9 @@ class ActivityObject return $object; } + /** + * @fixme missing avatar, bio info, etc + */ function asString($tag='activity:object') { $xs = new XMLStringer(true); diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php index 53925dc3f..b5f178cc6 100644 --- a/plugins/OStatus/lib/salmon.php +++ b/plugins/OStatus/lib/salmon.php @@ -28,15 +28,26 @@ */ class Salmon { + /** + * Sign and post the given Atom entry as a Salmon message. + * + * @fixme pass through the actor for signing? + * + * @param string $endpoint_uri + * @param string $xml + * @return boolean success + */ public function post($endpoint_uri, $xml) { if (empty($endpoint_uri)) { - return FALSE; + return false; } - $xml = $this->createMagicEnv($xml); - - $headers = array('Content-type: application/atom+xml'); + if (!common_config('ostatus', 'skip_signatures')) { + $xml = $this->createMagicEnv($xml); + } + + $headers = array('Content-Type: application/atom+xml'); try { $client = new HTTPClient(); @@ -51,7 +62,7 @@ class Salmon $response->getStatus() . ': ' . $response->getBody()); return false; } - + return true; } public function createMagicEnv($text) diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 09a042975..83cf0b8f8 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -41,7 +41,7 @@ class SalmonAction extends Action $this->clientError(_('This method requires a POST.')); } - if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') { + if (empty($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/atom+xml') { $this->clientError(_('Salmon requires application/atom+xml')); } @@ -57,11 +57,13 @@ class SalmonAction extends Action // Check the signature $salmon = new Salmon; - if (!$salmon->verifyMagicEnv($dom)) { - common_log(LOG_DEBUG, "Salmon signature verification failed."); - $this->clientError(_m('Salmon signature verification failed.')); + if (!common_config('ostatus', 'skip_signatures')) { + if (!$salmon->verifyMagicEnv($dom)) { + common_log(LOG_DEBUG, "Salmon signature verification failed."); + $this->clientError(_m('Salmon signature verification failed.')); + } } - + $this->act = new Activity($dom->documentElement); return true; } @@ -101,6 +103,9 @@ class SalmonAction extends Action case ActivityVerb::JOIN: $this->handleJoin(); break; + case ActivityVerb::LEAVE: + $this->handleLeave(); + break; default: throw new ClientException(_("Unimplemented.")); } @@ -154,6 +159,14 @@ class SalmonAction extends Action throw new ClientException(_("Unimplemented!")); } + /** + * Hmmmm + */ + function handleLeave() + { + throw new ClientException(_("Unimplemented!")); + } + /** * @return Ostatus_profile */ -- cgit v1.2.3-54-g00ecf From d410df040684f443d14bd921c450ca464d52c9d4 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 23 Feb 2010 00:44:45 +0000 Subject: OStatus group delivery initial implementation. - added rel="ostatus:attention" links for group delivery - added events for plugins to override group profile/permalink pages - pulled Notice::saveGroups up to save-time so we can override; it's relatively cheap and gives us a clean list of target groups for distrib time even with customized delivery. - fixed notice::getGroups to return group objects as expected - added some doc on new parameters to Notice::saveNew - 'groups' list of group IDs to push to in place of parsing - messages that come in via PuSH and contain local group targets are delivered to local group members - messages that come in via PuSH and contain remote group targets are delivered to local members of the remote group Todo: - handle group posts that only come through Salmon - handle conflicts in case something comes in both through Salmon and PuSH - better source verification - need a cleaner interface to look up groups by URI - need a way to handle remote groups with conflicting names --- actions/apitimelinegroup.php | 3 +- classes/Notice.php | 111 +++++++++++++++++++++++-- classes/User_group.php | 18 +++- lib/distribqueuehandler.php | 14 +--- plugins/OStatus/OStatusPlugin.php | 16 ++++ plugins/OStatus/classes/Ostatus_profile.php | 75 +++++++++++++++-- plugins/OStatus/lib/hubdistribqueuehandler.php | 2 +- 7 files changed, 207 insertions(+), 32 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/actions/apitimelinegroup.php b/actions/apitimelinegroup.php index 1d0c4afdd..0bb4860ea 100644 --- a/actions/apitimelinegroup.php +++ b/actions/apitimelinegroup.php @@ -176,7 +176,8 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction $atom->addEntryFromNotices($this->notices); - $this->raw($atom->getString()); + //$this->raw($atom->getString()); + print $atom->getString(); // temp hack until PuSH feeds are redone cleanly } catch (Atom10FeedException $e) { $this->serverError( diff --git a/classes/Notice.php b/classes/Notice.php index a12839d72..754c126ed 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -187,7 +187,14 @@ class Notice extends Memcached_DataObject * int 'location_ns' geoname namespace to interpret location_id * int 'reply_to'; notice ID this is a reply to * int 'repeat_of'; notice ID this is a repeat of - * string 'uri' permalink to notice; defaults to local notice URL + * string 'uri' unique ID for notice; defaults to local notice URL + * string 'url' permalink to notice; defaults to local notice URL + * string 'rendered' rendered HTML version of content + * array 'replies' list of profile URIs for reply delivery in + * place of extracting @-replies from content. + * array 'groups' list of group IDs to deliver to, in place of + * extracting ! tags from content + * @fixme tag override * * @return Notice * @throws ClientException @@ -342,6 +349,12 @@ class Notice extends Memcached_DataObject $notice->saveReplies(); } + if (isset($groups)) { + $notice->saveKnownGroups($groups); + } else { + $notice->saveGroups(); + } + $notice->distribute(); return $notice; @@ -692,7 +705,22 @@ class Notice extends Memcached_DataObject return $ni; } - function addToInboxes($groups, $recipients) + /** + * Adds this notice to the inboxes of each local user who should receive + * it, based on author subscriptions, group memberships, and @-replies. + * + * Warning: running a second time currently will make items appear + * multiple times in users' inboxes. + * + * @fixme make more robust against errors + * @fixme break up massive deliveries to smaller background tasks + * + * @param array $groups optional list of Group objects; + * if left empty, will be loaded from group_inbox records + * @param array $recipient optional list of reply profile ids + * if left empty, will be loaded from reply records + */ + function addToInboxes($groups=null, $recipients=null) { $ni = $this->whoGets($groups, $recipients); @@ -742,6 +770,42 @@ class Notice extends Memcached_DataObject } /** + * Record this notice to the given group inboxes for delivery. + * Overrides the regular parsing of !group markup. + * + * @param string $group_ids + * @fixme might prefer URIs as identifiers, as for replies? + * best with generalizations on user_group to support + * remote groups better. + */ + function saveKnownGroups($group_ids) + { + if (!is_array($group_ids)) { + throw new ServerException("Bad type provided to saveKnownGroups"); + } + + $groups = array(); + foreach ($group_ids as $id) { + $group = User_group::staticGet('id', $id); + if ($group) { + common_log(LOG_ERR, "Local delivery to group id $id, $group->nickname"); + $result = $this->addToGroupInbox($group); + if (!$result) { + common_log_db_error($gi, 'INSERT', __FILE__); + } + + // @fixme should we save the tags here or not? + $groups[] = clone($group); + } else { + common_log(LOG_ERR, "Local delivery to group id $id skipped, doesn't exist"); + } + } + + return $groups; + } + + /** + * Parse !group delivery and record targets into group_inbox. * @return array of Group objects */ function saveGroups() @@ -824,6 +888,19 @@ class Notice extends Memcached_DataObject return true; } + /** + * Save reply records indicating that this notice needs to be + * delivered to the local users with the given URIs. + * + * Since this is expected to be used when saving foreign-sourced + * messages, we won't deliver to any remote targets as that's the + * source service's responsibility. + * + * @fixme Unlike saveReplies() there's no mail notification here. + * Move that to distrib queue handler? + * + * @param array of unique identifier URIs for recipients + */ function saveKnownReplies($uris) { foreach ($uris as $uri) { @@ -845,6 +922,13 @@ class Notice extends Memcached_DataObject } /** + * Pull @-replies from this message's content in StatusNet markup format + * and save reply records indicating that this message needs to be + * delivered to those users. + * + * Side effect: local recipients get e-mail notifications here. + * @fixme move mail notifications to distrib? + * * @return array of integer profile IDs */ @@ -934,9 +1018,10 @@ class Notice extends Memcached_DataObject } /** - * Same calculation as saveGroups but without the saving - * @fixme merge the functions - * @return array of Group_inbox objects + * Pull list of groups this notice needs to be delivered to, + * as previously recorded by saveGroups() or saveKnownGroups(). + * + * @return array of Group objects */ function getGroups() { @@ -959,7 +1044,10 @@ class Notice extends Memcached_DataObject if ($gi->find()) { while ($gi->fetch()) { - $groups[] = clone($gi); + $group = User_group::staticGet('id', $gi->group_id); + if ($group) { + $groups[] = $group; + } } } @@ -1063,6 +1151,17 @@ class Notice extends Memcached_DataObject } } + $groups = $this->getGroups(); + + foreach ($groups as $group) { + $xs->element( + 'link', array( + 'rel' => 'ostatus:attention', + 'href' => $group->permalink() + ) + ); + } + if (!empty($this->repeat_of)) { $repeat = Notice::staticGet('id', $this->repeat_of); if (!empty($repeat)) { diff --git a/classes/User_group.php b/classes/User_group.php index 379e6b721..1382aa407 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -39,14 +39,24 @@ class User_group extends Memcached_DataObject function homeUrl() { - return common_local_url('showgroup', - array('nickname' => $this->nickname)); + $url = null; + if (Event::handle('StartUserGroupHomeUrl', array($this, &$url))) { + $url = common_local_url('showgroup', + array('nickname' => $this->nickname)); + } + Event::handle('EndUserGroupHomeUrl', array($this, &$url)); + return $url; } function permalink() { - return common_local_url('groupbyid', - array('id' => $this->id)); + $url = null; + if (Event::handle('StartUserGroupPermalink', array($this, &$url))) { + $url = common_local_url('groupbyid', + array('id' => $this->id)); + } + Event::handle('EndUserGroupPermalink', array($this, &$url)); + return $url; } function getNotices($offset, $limit, $since_id=null, $max_id=null) diff --git a/lib/distribqueuehandler.php b/lib/distribqueuehandler.php index c31b675c1..dc183fb36 100644 --- a/lib/distribqueuehandler.php +++ b/lib/distribqueuehandler.php @@ -69,19 +69,7 @@ class DistribQueueHandler } try { - $groups = $notice->saveGroups(); - } catch (Exception $e) { - $this->logit($notice, $e); - } - - try { - $recipients = $notice->getReplies(); - } catch (Exception $e) { - $this->logit($notice, $e); - } - - try { - $notice->addToInboxes($groups, $recipients); + $notice->addToInboxes(); } catch (Exception $e) { $this->logit($notice, $e); } diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 061ed4bd1..472008419 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -591,6 +591,22 @@ class OStatusPlugin extends Plugin return true; } + function onStartUserGroupHomeUrl($group, &$url) + { + return $this->onStartUserGroupPermalink($group, &$url); + } + + function onStartUserGroupPermalink($group, &$url) + { + $oprofile = Ostatus_profile::staticGet('group_id', $group->id); + if ($oprofile) { + // @fixme this should probably be in the user_group table + // @fixme this uri not guaranteed to be a profile page + $url = $oprofile->uri; + return false; + } + } + function onStartShowSubscriptionsContent($action) { $user = common_current_user(); diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index c0e39add8..e8cc13c6c 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -501,6 +501,7 @@ class Ostatus_profile extends Memcached_DataObject /** * Process an incoming post activity from this remote feed. * @param Activity $activity + * @fixme break up this function, it's getting nasty long */ protected function processPost($activity) { @@ -518,7 +519,6 @@ class Ostatus_profile extends Memcached_DataObject } $oprofile = $this; } - $sourceUri = $activity->object->id; $dupe = Notice::staticGet('uri', $sourceUri); @@ -555,15 +555,76 @@ class Ostatus_profile extends Memcached_DataObject } } - // @fixme ensure that groups get handled correctly + $profile = $oprofile->localProfile(); + $params['groups'] = array(); + $params['replies'] = array(); + if ($activity->context) { + foreach ($activity->context->attention as $recipient) { + $roprofile = Ostatus_profile::staticGet('uri', $recipient); + if ($roprofile) { + if ($roprofile->isGroup()) { + // Deliver to local recipients of this remote group. + // @fixme sender verification? + $params['groups'][] = $roprofile->group_id; + continue; + } else { + // Delivery to remote users is the source service's job. + continue; + } + } + + $user = User::staticGet('uri', $recipient); + if ($user) { + // An @-reply directed to a local user. + // @fixme sender verification, spam etc? + $params['replies'][] = $recipient; + continue; + } + + // @fixme we need a uri on user_group + // $group = User_group::staticGet('uri', $recipient); + $template = common_local_url('groupbyid', array('id' => '31337')); + $template = preg_quote($template, '/'); + $template = str_replace('31337', '(\d+)', $template); + common_log(LOG_DEBUG, $template); + if (preg_match("/$template/", $recipient, $matches)) { + $id = $matches[1]; + $group = User_group::staticGet('id', $id); + if ($group) { + // Deliver to all members of this local group. + // @fixme sender verification? + if ($profile->isMember($group)) { + common_log(LOG_DEBUG, "delivering to group $id $group->nickname"); + $params['groups'][] = $group->id; + } else { + common_log(LOG_DEBUG, "not delivering to group $id $group->nickname because sender $profile->nickname is not a member"); + } + continue; + } else { + common_log(LOG_DEBUG, "not delivering to missing group $id"); + } + } else { + common_log(LOG_DEBUG, "not delivering to groups for $recipient"); + } + } + } - $saved = Notice::saveNew($oprofile->localProfile()->id, - $content, - 'ostatus', - $params); + try { + $saved = Notice::saveNew($profile->id, + $content, + 'ostatus', + $params); + } catch (Exception $e) { + common_log(LOG_ERR, "Failed saving notice entry for $sourceUri: " . $e->getMessage()); + return; + } // Record which feed this came through... - Ostatus_source::saveNew($saved, $this, 'push'); + try { + Ostatus_source::saveNew($saved, $this, 'push'); + } catch (Exception $e) { + common_log(LOG_ERR, "Failed saving ostatus_source entry for $saved->notice_id: " . $e->getMessage()); + } } /** diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/hubdistribqueuehandler.php index 30a427e3f..c2bd630f9 100644 --- a/plugins/OStatus/lib/hubdistribqueuehandler.php +++ b/plugins/OStatus/lib/hubdistribqueuehandler.php @@ -36,7 +36,7 @@ class HubDistribQueueHandler extends QueueHandler $this->pushUser($notice); foreach ($notice->getGroups() as $group) { - $this->pushGroup($notice, $group->group_id); + $this->pushGroup($notice, $group->id); } return true; } -- cgit v1.2.3-54-g00ecf From f4b34d67c54022b70185e83fe628c17e3656d91f Mon Sep 17 00:00:00 2001 From: James Walker Date: Mon, 22 Feb 2010 23:11:40 -0500 Subject: generate keypairs for users, and put them in the XRD for discovery --- plugins/OStatus/OStatusPlugin.php | 1 + plugins/OStatus/actions/webfinger.php | 11 +++++++ plugins/OStatus/classes/Magicsig.php | 55 +++++++++++++++++++++++++++-------- 3 files changed, 55 insertions(+), 12 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 472008419..db4a0af35 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -312,6 +312,7 @@ class OStatusPlugin extends Plugin $schema->ensureTable('ostatus_source', Ostatus_source::schemaDef()); $schema->ensureTable('feedsub', FeedSub::schemaDef()); $schema->ensureTable('hubsub', HubSub::schemaDef()); + $schema->ensureTable('magicsig', Magicsig::schemaDef()); return true; } diff --git a/plugins/OStatus/actions/webfinger.php b/plugins/OStatus/actions/webfinger.php index cf60b8069..fbbd8d039 100644 --- a/plugins/OStatus/actions/webfinger.php +++ b/plugins/OStatus/actions/webfinger.php @@ -71,6 +71,17 @@ class WebfingerAction extends Action $xrd->links[] = array('rel' => 'salmon', 'href' => $salmon_url); + // Get this user's keypair + $magickey = Magicsig::staticGet('user_id', $this->user->id); + if (!$magickey) { + // No keypair yet, let's generate one. + $magickey = new Magicsig(); + $magickey->generate(); + } + + $xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL, + 'href' => 'data:application/magic-public-key;'. $magickey->keypair); + // TODO - finalize where the redirect should go on the publisher $url = common_local_url('ostatussub') . '?profile={uri}'; $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe', diff --git a/plugins/OStatus/classes/Magicsig.php b/plugins/OStatus/classes/Magicsig.php index 6d09c54ec..85664bbf9 100644 --- a/plugins/OStatus/classes/Magicsig.php +++ b/plugins/OStatus/classes/Magicsig.php @@ -32,6 +32,8 @@ require_once 'Crypt/RSA.php'; class Magicsig extends Memcached_DataObject { + const PUBLICKEYREL = 'magic-public-key'; + public $__table = 'magicsig'; public $user_id; @@ -40,6 +42,11 @@ class Magicsig extends Memcached_DataObject private $_rsa; + public function __construct($alg = 'RSA-SHA256') + { + $this->alg = $alg; + } + public /*static*/ function staticGet($k, $v=null) { return parent::staticGet(__CLASS__, $k, $v); @@ -75,23 +82,33 @@ class Magicsig extends Memcached_DataObject { return array('user_id' => 'K'); } + + function insert() + { + $this->keypair = $this->toString(); + + return parent::insert(); + } public function generate($key_length = 512) { + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $keypair = new Crypt_RSA_KeyPair($key_length); $params['public_key'] = $keypair->getPublicKey(); $params['private_key'] = $keypair->getPrivateKey(); - PEAR::pushErrorHandling(PEAR_ERROR_RETURN); - $this->keypair = new Crypt_RSA($params); + $this->_rsa = new Crypt_RSA($params); PEAR::popErrorHandling(); + + $this->insert(); } public function toString($full_pair = true) { - $public_key = $this->keypair->_public_key; - $private_key = $this->keypair->_private_key; + $public_key = $this->_rsa->_public_key; + $private_key = $this->_rsa->_private_key; $mod = base64_url_encode($public_key->getModulus()); $exp = base64_url_encode($public_key->getExponent()); @@ -103,10 +120,12 @@ class Magicsig extends Memcached_DataObject return 'RSA.' . $mod . '.' . $exp . $private_exp; } - public function fromString($text) + public static function fromString($text) { PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $magic_sig = new Magicsig(); + // remove whitespace $text = preg_replace('/\s+/', '', $text); @@ -136,20 +155,32 @@ class Magicsig extends Memcached_DataObject } } - $this->keypair = new Crypt_RSA($params); + $magic_sig->_rsa = new Crypt_RSA($params); PEAR::popErrorHandling(); + + return $magic_sig; } public function getName() { - return 'RSA-SHA256'; + $this->alg; } + public function getHash() + { + switch ($this->alg) { + + case 'RSA-SHA256': + return 'sha256'; + } + + } + public function sign($bytes) { - $sig = $this->keypair->createSign($bytes, null, 'sha256'); - if ($this->keypair->isError()) { - $error = $this->keypair->getLastError(); + $sig = $this->_rsa->createSign($bytes, null, 'sha256'); + if ($this->_rsa->isError()) { + $error = $this->_rsa->getLastError(); common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage()); } @@ -158,8 +189,8 @@ class Magicsig extends Memcached_DataObject public function verify($signed_bytes, $signature) { - $result = $this->keypair->validateSign($signed_bytes, $signature, null, 'sha256'); - if ($this->keypair->isError()) { + $result = $this->_rsa->validateSign($signed_bytes, $signature, null, 'sha256'); + if ($this->_rsa->isError()) { $error = $this->keypair->getLastError(); //common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage()); print $error->getMessage(); -- cgit v1.2.3-54-g00ecf From 90d34b26c66ceb5d37a9c2782356b46361a523cc Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 23 Feb 2010 20:44:27 +0000 Subject: OStatus: do PuSH subscription setup from subscribe/join event hooks, so resubscribing directly from a profile/group list works correctly if there aren't active subscriptions at the moment. --- lib/activity.php | 4 +- plugins/OStatus/OStatusPlugin.php | 158 +++++++++++++++++----------- plugins/OStatus/actions/ostatussub.php | 5 - plugins/OStatus/classes/Ostatus_profile.php | 26 ++++- 4 files changed, 127 insertions(+), 66 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/lib/activity.php b/lib/activity.php index 3689dac38..04c57c561 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -823,7 +823,9 @@ class Activity if ($namespace) { $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom', 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', - 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0'); + 'xmlns:georss' => 'http://www.georss.org/georss', + 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0', + 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0'); } else { $attrs = array(); } diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index db4a0af35..629645767 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -251,58 +251,6 @@ class OStatusPlugin extends Plugin 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($profile, $other) - { - $user = User::staticGet('id', $profile->id); - - if (empty($user)) { - return true; - } - - $oprofile = Ostatus_profile::staticGet('profile_id', $other->id); - - if (empty($oprofile)) { - return true; - } - - // Drop the PuSH subscription if there are no other subscribers. - - if ($other->subscriberCount() == 0) { - common_log(LOG_INFO, "Unsubscribing from now-unused feed $oprofile->feeduri"); - $oprofile->unsubscribe(); - } - - $act = new Activity(); - - $act->verb = ActivityVerb::UNFOLLOW; - - $act->id = TagURI::mint('unfollow:%d:%d:%s', - $profile->id, - $other->id, - common_date_iso8601(time())); - - $act->time = time(); - $act->title = _("Unfollow"); - $act->content = sprintf(_("%s stopped following %s."), - $profile->getBestName(), - $other->getBestName()); - - $act->actor = ActivityObject::fromProfile($profile); - $act->object = ActivityObject::fromProfile($other); - - $oprofile->notifyActivity($act); - - return true; - } - /** * Make sure necessary tables are filled out. */ @@ -366,6 +314,50 @@ class OStatusPlugin extends Plugin } } + /** + * When about to subscribe to a remote user, start a server-to-server + * PuSH subscription if needed. If we can't establish that, abort. + * + * @fixme If something else aborts later, we could end up with a stray + * PuSH subscription. This is relatively harmless, though. + * + * @param Profile $subscriber + * @param Profile $other + * + * @return hook return code + * + * @throws Exception + */ + function onStartSubscribe($subscriber, $other) + { + $user = User::staticGet('id', $subscriber->id); + + if (empty($user)) { + return true; + } + + $oprofile = Ostatus_profile::staticGet('profile_id', $other->id); + + if (empty($oprofile)) { + return true; + } + + if (!$oprofile->subscribe()) { + throw new Exception(_m('Could not set up remote subscription.')); + } + } + + /** + * Having established a remote subscription, send a notification to the + * remote OStatus profile's endpoint. + * + * @param Profile $subscriber + * @param Profile $other + * + * @return hook return code + * + * @throws Exception + */ function onEndSubscribe($subscriber, $other) { $user = User::staticGet('id', $subscriber->id); @@ -403,6 +395,54 @@ class OStatusPlugin extends Plugin 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($profile, $other) + { + $user = User::staticGet('id', $profile->id); + + if (empty($user)) { + return true; + } + + $oprofile = Ostatus_profile::staticGet('profile_id', $other->id); + + if (empty($oprofile)) { + return true; + } + + // Drop the PuSH subscription if there are no other subscribers. + $oprofile->garbageCollect(); + + $act = new Activity(); + + $act->verb = ActivityVerb::UNFOLLOW; + + $act->id = TagURI::mint('unfollow:%d:%d:%s', + $profile->id, + $other->id, + common_date_iso8601(time())); + + $act->time = time(); + $act->title = _("Unfollow"); + $act->content = sprintf(_("%s stopped following %s."), + $profile->getBestName(), + $other->getBestName()); + + $act->actor = ActivityObject::fromProfile($profile); + $act->object = ActivityObject::fromProfile($other); + + $oprofile->notifyActivity($act); + + return true; + } + /** * When one of our local users tries to join a remote group, * notify the remote server. If the notification is rejected, @@ -418,6 +458,10 @@ class OStatusPlugin extends Plugin { $oprofile = Ostatus_profile::staticGet('group_id', $group->id); if ($oprofile) { + if (!$oprofile->subscribe()) { + throw new Exception(_m('Could not set up remote group membership.')); + } + $member = Profile::staticGet($user->id); $act = new Activity(); @@ -439,7 +483,8 @@ class OStatusPlugin extends Plugin if ($oprofile->notifyActivity($act)) { return true; } else { - throw new ServerException(_m("Failed joining remote group.")); + $oprofile->garbageCollect(); + throw new Exception(_m("Failed joining remote group.")); } } } @@ -464,12 +509,7 @@ class OStatusPlugin extends Plugin $oprofile = Ostatus_profile::staticGet('group_id', $group->id); if ($oprofile) { // Drop the PuSH subscription if there are no other subscribers. - - $members = $group->getMembers(0, 1); - if ($members->N == 0) { - common_log(LOG_INFO, "Unsubscribing from now-unused group feed $oprofile->feeduri"); - $oprofile->unsubscribe(); - } + $oprofile->garbageCollect(); $member = Profile::staticGet($user->id); diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php index df9aa80b0..b3569e695 100644 --- a/plugins/OStatus/actions/ostatussub.php +++ b/plugins/OStatus/actions/ostatussub.php @@ -324,11 +324,6 @@ class OStatusSubAction extends Action // And subscribe the current user to the local profile $user = common_current_user(); - if (!$this->oprofile->subscribe()) { - $this->showForm(_m("Failed to set up server-to-server subscription.")); - return; - } - if ($this->oprofile->isGroup()) { $group = $this->oprofile->localGroup(); if ($user->isMember($group)) { diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index e8cc13c6c..91b957fa2 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -346,6 +346,29 @@ class Ostatus_profile extends Memcached_DataObject } } + /** + * Check if this remote profile has any active local subscriptions, and + * if not drop the PuSH subscription feed. + * + * @return boolean + */ + public function garbageCollect() + { + if ($this->isGroup()) { + $members = $this->localGroup()->getMembers(0, 1); + $count = $members->N; + } else { + $count = $this->localProfile()->subscriberCount(); + } + if ($count == 0) { + common_log(LOG_INFO, "Unsubscribing from now-unused remote feed $oprofile->feeduri"); + $this->unsubscribe(); + return true; + } else { + return false; + } + } + /** * Send an Activity Streams notification to the remote Salmon endpoint, * if so configured. @@ -379,7 +402,8 @@ class Ostatus_profile extends Memcached_DataObject 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/', 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0', 'xmlns:georss' => 'http://www.georss.org/georss', - 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0'); + 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0', + 'xmlns:poco' => 'http://portablecontacts.net/spec/1.0'); $entry = new XMLStringer(); $entry->elementStart('entry', $attributes); -- cgit v1.2.3-54-g00ecf From a0c255e231a84cf77554cc3c0a095eae7b0ffd9c Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 23 Feb 2010 15:59:10 -0500 Subject: move mention detection before default in OStatusPlugin --- plugins/OStatus/OStatusPlugin.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index db4a0af35..679857f89 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -224,7 +224,7 @@ class OStatusPlugin extends Plugin * */ - function onEndFindMentions($sender, $text, &$mentions) + function onStartFindMentions($sender, $text, &$mentions) { preg_match_all('/(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)/', $text, @@ -464,14 +464,13 @@ class OStatusPlugin extends Plugin $oprofile = Ostatus_profile::staticGet('group_id', $group->id); if ($oprofile) { // Drop the PuSH subscription if there are no other subscribers. - + $members = $group->getMembers(0, 1); if ($members->N == 0) { common_log(LOG_INFO, "Unsubscribing from now-unused group feed $oprofile->feeduri"); $oprofile->unsubscribe(); } - $member = Profile::staticGet($user->id); $act = new Activity(); -- cgit v1.2.3-54-g00ecf From f5ec7c27070dac4ac28ba860f4cc9a808b5f7c30 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 23 Feb 2010 16:13:24 -0500 Subject: some logging for OStatusPlugin::onStartFindMentions() --- plugins/OStatus/OStatusPlugin.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 3a9e77c2a..934c858ac 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -235,9 +235,17 @@ class OStatusPlugin extends Plugin $webfinger = $wmatch[0]; + $this->log(LOG_INFO, "Checking Webfinger for address '$webfinger'"); + $oprofile = Ostatus_profile::ensureWebfinger($webfinger); - if (!empty($oprofile)) { + if (empty($oprofile)) { + + $this->log(LOG_INFO, "No Ostatus_profile found for address '$webfinger'"); + + } else { + + $this->log(LOG_INFO, "Ostatus_profile found for address '$webfinger'"); $profile = $oprofile->localProfile(); -- cgit v1.2.3-54-g00ecf From d6ad7332475f1cc4ab45d55fc04ef491d5f3999d Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 23 Feb 2010 21:47:14 +0000 Subject: OStatus: fixes for link/id and text extraction gets import of Buzz feeds working. --- plugins/OStatus/OStatusPlugin.php | 26 ++++++++++++++------------ plugins/OStatus/classes/Ostatus_profile.php | 17 ++++++++++++++--- 2 files changed, 28 insertions(+), 15 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 3a9e77c2a..724634924 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -287,13 +287,19 @@ class OStatusPlugin extends Plugin function onStartNoticeSourceLink($notice, &$name, &$url, &$title) { if ($notice->source == 'ostatus') { - $bits = parse_url($notice->uri); - $domain = $bits['host']; - - $name = $domain; - $url = $notice->uri; - $title = sprintf(_m("Sent from %s via OStatus"), $domain); - return false; + if ($notice->url) { + $bits = parse_url($notice->url); + $domain = $bits['host']; + if (substr($domain, 0, 4) == 'www.') { + $name = substr($domain, 4); + } else { + $name = $domain; + } + + $url = $notice->url; + $title = sprintf(_m("Sent from %s via OStatus"), $domain); + return false; + } } } @@ -509,12 +515,8 @@ class OStatusPlugin extends Plugin $oprofile = Ostatus_profile::staticGet('group_id', $group->id); if ($oprofile) { // Drop the PuSH subscription if there are no other subscribers. + $oprofile->garbageCollect(); - $members = $group->getMembers(0, 1); - if ($members->N == 0) { - common_log(LOG_INFO, "Unsubscribing from now-unused group feed $oprofile->feeduri"); - $oprofile->unsubscribe(); - } $member = Profile::staticGet($user->id); diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 91b957fa2..4998809bc 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -556,17 +556,28 @@ class Ostatus_profile extends Memcached_DataObject if ($activity->object->link) { $sourceUrl = $activity->object->link; + } else if ($activity->link) { + $sourceUrl = $activity->link; } else if (preg_match('!^https?://!', $activity->object->id)) { $sourceUrl = $activity->object->id; } - // @fixme sanitize and save HTML content if available + // Get (safe!) HTML and text versions of the content - $content = $activity->object->title; + require_once(INSTALLDIR.'/extlib/HTMLPurifier/HTMLPurifier.auto.php'); + + $html = $activity->object->content; + + $purifier = new HTMLPurifier(); + + $rendered = $purifier->purify($html); + + $content = html_entity_decode(strip_tags($rendered)); $params = array('is_local' => Notice::REMOTE_OMB, 'url' => $sourceUrl, - 'uri' => $sourceUri); + 'uri' => $sourceUri, + 'rendered' => $rendered); $location = $activity->context->location; -- cgit v1.2.3-54-g00ecf From 584b87cfe57bd2d683101194040e3563f0706536 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 24 Feb 2010 01:09:52 +0000 Subject: OStatus: consolidate the low-level notice save code between Salmon and PuSH input paths. Validation etc remains at higher levels. --- plugins/OStatus/OStatusPlugin.php | 2 +- plugins/OStatus/classes/Ostatus_profile.php | 214 ++++++++++++++++------------ plugins/OStatus/lib/salmonaction.php | 50 +------ 3 files changed, 125 insertions(+), 141 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 724634924..35f952935 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -314,7 +314,7 @@ class OStatusPlugin extends Plugin { $oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri); if ($oprofile) { - $oprofile->processFeed($feed); + $oprofile->processFeed($feed, 'push'); } else { common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri"); } diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 4998809bc..6beaf0f5d 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -488,7 +488,7 @@ class Ostatus_profile extends Memcached_DataObject * * @param DOMDocument $feed */ - public function processFeed($feed) + public function processFeed($feed, $source) { $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); if ($entries->length == 0) { @@ -498,7 +498,7 @@ class Ostatus_profile extends Memcached_DataObject for ($i = 0; $i < $entries->length; $i++) { $entry = $entries->item($i); - $this->processEntry($entry, $feed); + $this->processEntry($entry, $feed, $source); } } @@ -508,15 +508,12 @@ class Ostatus_profile extends Memcached_DataObject * @param DOMElement $entry * @param DOMElement $feed for context */ - protected function processEntry($entry, $feed) + public function processEntry($entry, $feed, $source) { $activity = new Activity($entry, $feed); - $debug = var_export($activity, true); - common_log(LOG_DEBUG, $debug); - if ($activity->verb == ActivityVerb::POST) { - $this->processPost($activity); + $this->processPost($activity, $source); } else { common_log(LOG_INFO, "Ignoring activity with unrecognized verb $activity->verb"); } @@ -525,35 +522,47 @@ class Ostatus_profile extends Memcached_DataObject /** * Process an incoming post activity from this remote feed. * @param Activity $activity + * @param string $method 'push' or 'salmon' + * @return mixed saved Notice or false * @fixme break up this function, it's getting nasty long */ - protected function processPost($activity) + public function processPost($activity, $method) { if ($this->isGroup()) { + // A group feed will contain posts from multiple authors. // @fixme validate these profiles in some way! $oprofile = self::ensureActorProfile($activity); + if ($oprofile->isGroup()) { + // Groups can't post notices in StatusNet. + common_log(LOG_WARNING, "OStatus: skipping post with group listed as author: $oprofile->uri in feed from $this->uri"); + return false; + } } else { + // Individual user feeds may contain only posts from themselves. + // Authorship is validated against the profile URI on upper layers, + // through PuSH setup or Salmon signature checks. $actorUri = self::getActorProfileURI($activity); if ($actorUri == $this->uri) { // @fixme check if profile info has changed and update it } else { - // @fixme drop or reject the messages once we've got the canonical profile URI recorded sanely - common_log(LOG_INFO, "OStatus: Warning: non-group post with unexpected author: $actorUri expected $this->uri"); - //return; + common_log(LOG_WARNING, "OStatus: skipping post with bad author: got $actorUri expected $this->uri"); + return false; } $oprofile = $this; } - $sourceUri = $activity->object->id; + // The id URI will be used as a unique identifier for for the notice, + // protecting against duplicate saves. It isn't required to be a URL; + // tag: URIs for instance are found in Google Buzz feeds. + $sourceUri = $activity->object->id; $dupe = Notice::staticGet('uri', $sourceUri); - if ($dupe) { common_log(LOG_INFO, "OStatus: ignoring duplicate post: $sourceUri"); - return; + return false; } + // We'll also want to save a web link to the original notice, if provided. $sourceUrl = null; - if ($activity->object->link) { $sourceUrl = $activity->object->link; } else if ($activity->link) { @@ -563,103 +572,126 @@ class Ostatus_profile extends Memcached_DataObject } // Get (safe!) HTML and text versions of the content - - require_once(INSTALLDIR.'/extlib/HTMLPurifier/HTMLPurifier.auto.php'); - - $html = $activity->object->content; - - $purifier = new HTMLPurifier(); - - $rendered = $purifier->purify($html); - + $rendered = $this->purify($activity->object->content); $content = html_entity_decode(strip_tags($rendered)); - $params = array('is_local' => Notice::REMOTE_OMB, + $options = array('is_local' => Notice::REMOTE_OMB, 'url' => $sourceUrl, 'uri' => $sourceUri, - 'rendered' => $rendered); + 'rendered' => $rendered, + 'replies' => array(), + 'groups' => array()); - $location = $activity->context->location; + // Check for optional attributes... - if ($location) { - $params['lat'] = $location->lat; - $params['lon'] = $location->lon; - if ($location->location_id) { - $params['location_ns'] = $location->location_ns; - $params['location_id'] = $location->location_id; - } + if (!empty($activity->time)) { + $options['created'] = common_sql_date($activity->time); } - $profile = $oprofile->localProfile(); - $params['groups'] = array(); - $params['replies'] = array(); if ($activity->context) { - foreach ($activity->context->attention as $recipient) { - $roprofile = Ostatus_profile::staticGet('uri', $recipient); - if ($roprofile) { - if ($roprofile->isGroup()) { - // Deliver to local recipients of this remote group. - // @fixme sender verification? - $params['groups'][] = $roprofile->group_id; - continue; - } else { - // Delivery to remote users is the source service's job. - continue; - } - } - - $user = User::staticGet('uri', $recipient); - if ($user) { - // An @-reply directed to a local user. - // @fixme sender verification, spam etc? - $params['replies'][] = $recipient; - continue; + // Any individual or group attn: targets? + $replies = $activity->context->attention; + $options['groups'] = $this->filterReplies($oprofile, $replies); + $options['replies'] = $replies; + + // Maintain direct reply associations + // @fixme what about conversation ID? + if (!empty($activity->context->replyToID)) { + $orig = Notice::staticGet('uri', + $activity->context->replyToID); + if (!empty($orig)) { + $options['reply_to'] = $orig->id; } - - // @fixme we need a uri on user_group - // $group = User_group::staticGet('uri', $recipient); - $template = common_local_url('groupbyid', array('id' => '31337')); - $template = preg_quote($template, '/'); - $template = str_replace('31337', '(\d+)', $template); - common_log(LOG_DEBUG, $template); - if (preg_match("/$template/", $recipient, $matches)) { - $id = $matches[1]; - $group = User_group::staticGet('id', $id); - if ($group) { - // Deliver to all members of this local group. - // @fixme sender verification? - if ($profile->isMember($group)) { - common_log(LOG_DEBUG, "delivering to group $id $group->nickname"); - $params['groups'][] = $group->id; - } else { - common_log(LOG_DEBUG, "not delivering to group $id $group->nickname because sender $profile->nickname is not a member"); - } - continue; - } else { - common_log(LOG_DEBUG, "not delivering to missing group $id"); - } - } else { - common_log(LOG_DEBUG, "not delivering to groups for $recipient"); + } + + $location = $activity->context->location; + if ($location) { + $options['lat'] = $location->lat; + $options['lon'] = $location->lon; + if ($location->location_id) { + $options['location_ns'] = $location->location_ns; + $options['location_id'] = $location->location_id; } } } try { - $saved = Notice::saveNew($profile->id, + $saved = Notice::saveNew($oprofile->profile_id, $content, 'ostatus', - $params); + $options); + if ($saved) { + Ostatus_source::saveNew($saved, $this, $method); + } } catch (Exception $e) { - common_log(LOG_ERR, "Failed saving notice entry for $sourceUri: " . $e->getMessage()); - return; + common_log(LOG_ERR, "OStatus save of remote message $sourceUri failed: " . $e->getMessage()); + throw $e; } + common_log(LOG_INFO, "OStatus saved remote message $sourceUri as notice id $saved->id"); + return $saved; + } - // Record which feed this came through... - try { - Ostatus_source::saveNew($saved, $this, 'push'); - } catch (Exception $e) { - common_log(LOG_ERR, "Failed saving ostatus_source entry for $saved->notice_id: " . $e->getMessage()); + /** + * Clean up HTML + */ + protected function purify($html) + { + // @fixme disable caching or set a sane temp dir + require_once(INSTALLDIR.'/extlib/HTMLPurifier/HTMLPurifier.auto.php'); + $purifier = new HTMLPurifier(); + return $purifier->purify($html); + } + + /** + * Filters a list of recipient ID URIs to just those for local delivery. + * @param Ostatus_profile local profile of sender + * @param array in/out &$attention_uris set of URIs, will be pruned on output + * @return array of group IDs + */ + protected function filterReplies($sender, &$attention_uris) + { + $groups = array(); + $replies = array(); + foreach ($attention_uris as $recipient) { + // Is the recipient a local user? + $user = User::staticGet('uri', $recipient); + if ($user) { + // @fixme sender verification, spam etc? + $replies[] = $recipient; + continue; + } + + // Is the recipient a remote group? + $oprofile = Ostatus_profile::staticGet('uri', $recipient); + if ($oprofile) { + if ($oprofile->isGroup()) { + // Deliver to local members of this remote group. + // @fixme sender verification? + $groups[] = $oprofile->group_id; + } + continue; + } + + // Is the recipient a local group? + // @fixme we need a uri on user_group + // $group = User_group::staticGet('uri', $recipient); + $template = common_local_url('groupbyid', array('id' => '31337')); + $template = preg_quote($template, '/'); + $template = str_replace('31337', '(\d+)', $template); + if (preg_match("/$template/", $recipient, $matches)) { + $id = $matches[1]; + $group = User_group::staticGet('id', $id); + if ($group) { + // Deliver to all members of this local group if allowed. + if ($sender->localProfile()->isMember($group)) { + $groups[] = $group->id; + } + continue; + } + } } + $attention_uris = $replies; + return $groups; } /** diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 83cf0b8f8..9aac2ed52 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -185,54 +185,6 @@ class SalmonAction extends Action function saveNotice() { $oprofile = $this->ensureProfile(); - - // Get (safe!) HTML and text versions of the content - - require_once(INSTALLDIR.'/extlib/HTMLPurifier/HTMLPurifier.auto.php'); - - $html = $this->act->object->content; - - $purifier = new HTMLPurifier(); - - $rendered = $purifier->purify($html); - - $content = html_entity_decode(strip_tags($rendered)); - - $options = array('is_local' => Notice::REMOTE_OMB, - 'uri' => $this->act->object->id, - 'url' => $this->act->object->link, - 'rendered' => $rendered, - 'replies' => $this->act->context->attention); - - if (!empty($this->act->context->location)) { - $options['lat'] = $location->lat; - $options['lon'] = $location->lon; - if ($location->location_id) { - $options['location_ns'] = $location->location_ns; - $options['location_id'] = $location->location_id; - } - } - - if (!empty($this->act->context->replyToID)) { - $orig = Notice::staticGet('uri', - $this->act->context->replyToID); - if (!empty($orig)) { - $options['reply_to'] = $orig->id; - } - } - - if (!empty($this->act->time)) { - $options['created'] = common_sql_date($this->act->time); - } - - $saved = Notice::saveNew($oprofile->profile_id, - $content, - 'ostatus+salmon', - $options); - - // Record that this was saved through a validated Salmon source - // @fixme actually do the signature validation! - Ostatus_source::saveNew($saved, $oprofile, 'salmon'); - return $saved; + return $oprofile->processPost($this->act, 'salmon'); } } -- cgit v1.2.3-54-g00ecf From 2e58802cc9959763f28e2f43c8e0cd0dbe7bcd8e Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 24 Feb 2010 02:19:13 +0000 Subject: OStatus: fix group delivery, send reply/group Salmon pings from background. --- plugins/OStatus/OStatusPlugin.php | 28 +--- plugins/OStatus/actions/groupsalmon.php | 9 +- plugins/OStatus/classes/Ostatus_profile.php | 15 +- plugins/OStatus/lib/hubdistribqueuehandler.php | 182 -------------------- plugins/OStatus/lib/ostatusqueuehandler.php | 223 +++++++++++++++++++++++++ plugins/OStatus/lib/salmonoutqueuehandler.php | 44 +++++ 6 files changed, 295 insertions(+), 206 deletions(-) delete mode 100644 plugins/OStatus/lib/hubdistribqueuehandler.php create mode 100644 plugins/OStatus/lib/ostatusqueuehandler.php create mode 100644 plugins/OStatus/lib/salmonoutqueuehandler.php (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 35f952935..9376c048d 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -78,11 +78,16 @@ class OStatusPlugin extends Plugin */ function onEndInitializeQueueManager(QueueManager $qm) { + // Prepare outgoing distributions after notice save. + $qm->connect('ostatus', 'OStatusQueueHandler'); + // Outgoing from our internal PuSH hub $qm->connect('hubverify', 'HubVerifyQueueHandler'); - $qm->connect('hubdistrib', 'HubDistribQueueHandler'); $qm->connect('hubout', 'HubOutQueueHandler'); + // Outgoing Salmon replies (when we don't need a return value) + $qm->connect('salmonout', 'SalmonOutQueueHandler'); + // Incoming from a foreign PuSH hub $qm->connect('pushinput', 'PushInputQueueHandler'); return true; @@ -93,7 +98,7 @@ class OStatusPlugin extends Plugin */ function onStartEnqueueNotice($notice, &$transports) { - $transports[] = 'hubdistrib'; + $transports[] = 'ostatus'; return true; } @@ -199,25 +204,6 @@ class OStatusPlugin extends Plugin function onEndNoticeSave($notice) { - $mentioned = $notice->getReplies(); - - foreach ($mentioned as $profile_id) { - - $oprofile = Ostatus_profile::staticGet('profile_id', $profile_id); - - if (!empty($oprofile) && !empty($oprofile->salmonuri)) { - - common_log(LOG_INFO, "Sending notice '{$notice->uri}' to remote profile '{$oprofile->uri}'."); - - // FIXME: this needs to go out in a queue handler - - $xml = ''; - $xml .= $notice->asAtomEntry(true, true); - - $salmon = new Salmon(); - $salmon->post($oprofile->salmonuri, $xml); - } - } } /** diff --git a/plugins/OStatus/actions/groupsalmon.php b/plugins/OStatus/actions/groupsalmon.php index 2e4fe9443..29377b5fa 100644 --- a/plugins/OStatus/actions/groupsalmon.php +++ b/plugins/OStatus/actions/groupsalmon.php @@ -46,6 +46,11 @@ class GroupsalmonAction extends SalmonAction $this->clientError(_('No such group.')); } + $oprofile = Ostatus_profile::staticGet('group_id', $id); + if ($oprofile) { + $this->clientError(_m("Can't accept remote posts for a remote group.")); + } + return true; } @@ -74,13 +79,13 @@ class GroupsalmonAction extends SalmonAction throw new ClientException("Not to the attention of anyone."); } else { $uri = common_local_url('groupbyid', array('id' => $this->group->id)); - if (!in_array($context->attention, $uri)) { + if (!in_array($uri, $context->attention)) { throw new ClientException("Not to the attention of this group."); } } $profile = $this->ensureProfile(); - // @fixme save the post + $this->saveNotice(); } /** diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 6beaf0f5d..82dbf773d 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -650,6 +650,7 @@ class Ostatus_profile extends Memcached_DataObject */ protected function filterReplies($sender, &$attention_uris) { + common_log(LOG_DEBUG, "Original reply recipients: " . implode(', ', $attention_uris)); $groups = array(); $replies = array(); foreach ($attention_uris as $recipient) { @@ -668,6 +669,8 @@ class Ostatus_profile extends Memcached_DataObject // Deliver to local members of this remote group. // @fixme sender verification? $groups[] = $oprofile->group_id; + } else { + common_log(LOG_DEBUG, "Skipping reply to remote profile $recipient"); } continue; } @@ -683,14 +686,24 @@ class Ostatus_profile extends Memcached_DataObject $group = User_group::staticGet('id', $id); if ($group) { // Deliver to all members of this local group if allowed. - if ($sender->localProfile()->isMember($group)) { + $profile = $sender->localProfile(); + if ($profile->isMember($group)) { $groups[] = $group->id; + } else { + common_log(LOG_DEBUG, "Skipping reply to local group $group->nickname as sender $profile->id is not a member"); } continue; + } else { + common_log(LOG_DEBUG, "Skipping reply to bogus group $recipient"); } } + + common_log(LOG_DEBUG, "Skipping reply to unrecognized profile $recipient"); + } $attention_uris = $replies; + common_log(LOG_DEBUG, "Local reply recipients: " . implode(', ', $replies)); + common_log(LOG_DEBUG, "Local group recipients: " . implode(', ', $groups)); return $groups; } diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/hubdistribqueuehandler.php deleted file mode 100644 index c2bd630f9..000000000 --- a/plugins/OStatus/lib/hubdistribqueuehandler.php +++ /dev/null @@ -1,182 +0,0 @@ -. - */ - -/** - * Send a PuSH subscription verification from our internal hub. - * Queue up final distribution for - * @package Hub - * @author Brion Vibber - */ -class HubDistribQueueHandler extends QueueHandler -{ - function transport() - { - return 'hubdistrib'; - } - - function handle($notice) - { - assert($notice instanceof Notice); - - $this->pushUser($notice); - foreach ($notice->getGroups() as $group) { - $this->pushGroup($notice, $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()) { - $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()) { - $sub->distribute($atom); - } - } - - /** - * Build a single-item version of the sending user's Atom feed. - * @param Notice $notice - * @return string - */ - function userFeedForNotice($notice) - { - // @fixme this feels VERY hacky... - // should probably be a cleaner way to do it - - ob_start(); - $api = new ApiTimelineUserAction(); - $api->prepare(array('id' => $notice->profile_id, - 'format' => 'atom', - 'max_id' => $notice->id, - 'since_id' => $notice->id - 1)); - $api->showTimeline(); - $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; - } - - 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/ostatusqueuehandler.php b/plugins/OStatus/lib/ostatusqueuehandler.php new file mode 100644 index 000000000..c1e50bffa --- /dev/null +++ b/plugins/OStatus/lib/ostatusqueuehandler.php @@ -0,0 +1,223 @@ +. + */ + +/** + * Prepare PuSH and Salmon distributions for an outgoing message. + * + * @package OStatusPlugin + * @author Brion Vibber + */ +class OStatusQueueHandler extends QueueHandler +{ + function transport() + { + return 'ostatus'; + } + + function handle($notice) + { + assert($notice instanceof Notice); + + $this->notice = $notice; + $this->user = User::staticGet($notice->profile_id); + + $this->pushUser(); + + foreach ($notice->getGroups() as $group) { + $oprofile = Ostatus_profile::staticGet('group_id', $group->id); + if ($oprofile) { + $this->pingReply($oprofile); + } else { + $this->pushGroup($group->id); + } + } + + foreach ($notice->getReplies() as $profile_id) { + $oprofile = Ostatus_profile::staticGet('profile_id', $profile_id); + if ($oprofile) { + $this->pingReply($oprofile); + } + } + + return true; + } + + function pushUser() + { + if ($this->user) { + // For local posts, ping the PuSH hub to update their feed. + // http://identi.ca/api/statuses/user_timeline/1.atom + $feed = common_local_url('ApiTimelineUser', + array('id' => $this->user->id, + 'format' => 'atom')); + $this->pushFeed($feed, array($this, 'userFeedForNotice')); + } + } + + function pushGroup($group_id) + { + // For a local group, ping the PuSH hub to update its feed. + // Updates may come from either a local or a remote user. + $feed = common_local_url('ApiTimelineGroup', + array('id' => $group_id, + 'format' => 'atom')); + $this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group_id); + } + + function pingReply($oprofile) + { + if ($this->user) { + if (!empty($oprofile->salmonuri)) { + // For local posts, send a Salmon ping to the mentioned + // remote user or group. + // @fixme as an optimization we can skip this if the + // remote profile is subscribed to the author. + + common_log(LOG_INFO, "Prepping to send notice '{$this->notice->uri}' to remote profile '{$oprofile->uri}'."); + + $xml = ''; + $xml .= $this->notice->asAtomEntry(true, true); + + $data = array('salmonuri' => $oprofile->salmonuri, + 'entry' => $xml); + + $qm = QueueManager::get(); + $qm->enqueue($data, 'salmonout'); + } + } + } + + /** + * @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()) { + $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"); + while ($sub->fetch()) { + $sub->distribute($atom); + } + } + + /** + * Build a single-item version of the sending user's Atom feed. + * @return string + */ + function userFeedForNotice() + { + // @fixme this feels VERY hacky... + // should probably be a cleaner way to do it + + ob_start(); + $api = new ApiTimelineUserAction(); + $api->prepare(array('id' => $this->notice->profile_id, + 'format' => 'atom', + 'max_id' => $this->notice->id, + 'since_id' => $this->notice->id - 1)); + $api->showTimeline(); + $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; + } + + function groupFeedForNotice($group_id) + { + // @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' => $this->notice->id, + 'since_id' => $this->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/salmonoutqueuehandler.php b/plugins/OStatus/lib/salmonoutqueuehandler.php new file mode 100644 index 000000000..536ff94af --- /dev/null +++ b/plugins/OStatus/lib/salmonoutqueuehandler.php @@ -0,0 +1,44 @@ +. + */ + +/** + * Send a Salmon notification in the background. + * @package OStatusPlugin + * @author Brion Vibber + */ +class SalmonOutQueueHandler extends QueueHandler +{ + function transport() + { + return 'salmonout'; + } + + function handle($data) + { + assert(is_array($data)); + assert(is_string($data['salmonuri'])); + assert(is_string($data['entry'])); + + $salmon = new Salmon(); + $salmon->post($data['salmonuri'], $data['entry']); + + // @fixme detect failure and attempt to resend + return true; + } +} -- cgit v1.2.3-54-g00ecf From c36bdc1ba535dc3e2dc9098dbe40735b1955d96d Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 24 Feb 2010 20:36:36 +0000 Subject: - break OMB profile update pings to a background queue - add event hooks to profile update pings - send Salmon pings with custom update-profile event to OStatus subscribees and groups (subscribers will see it on your next post) - fix OStatus queues with overlong transport names, should work on DB queues now - Ostatus_profile::notifyActivity() and ::notifyDeferred() now can take XML, Notice, or Activity for convenience --- lib/activity.php | 3 ++ lib/profilequeuehandler.php | 48 ++++++++++++++++++++++++ lib/queuemanager.php | 3 ++ lib/util.php | 14 ++++--- plugins/OStatus/OStatusPlugin.php | 53 ++++++++++++++++++++++++-- plugins/OStatus/actions/pushcallback.php | 2 +- plugins/OStatus/classes/HubSub.php | 2 +- plugins/OStatus/classes/Ostatus_profile.php | 46 ++++++++++++++++++++--- plugins/OStatus/lib/hubconfqueuehandler.php | 54 +++++++++++++++++++++++++++ plugins/OStatus/lib/hubverifyqueuehandler.php | 54 --------------------------- plugins/OStatus/lib/ostatusqueuehandler.php | 22 +++-------- plugins/OStatus/lib/pushinputqueuehandler.php | 49 ------------------------ plugins/OStatus/lib/pushinqueuehandler.php | 49 ++++++++++++++++++++++++ plugins/OStatus/lib/salmonoutqueuehandler.php | 44 ---------------------- plugins/OStatus/lib/salmonqueuehandler.php | 44 ++++++++++++++++++++++ 15 files changed, 308 insertions(+), 179 deletions(-) create mode 100644 lib/profilequeuehandler.php create mode 100644 plugins/OStatus/lib/hubconfqueuehandler.php delete mode 100644 plugins/OStatus/lib/hubverifyqueuehandler.php delete mode 100644 plugins/OStatus/lib/pushinputqueuehandler.php create mode 100644 plugins/OStatus/lib/pushinqueuehandler.php delete mode 100644 plugins/OStatus/lib/salmonoutqueuehandler.php create mode 100644 plugins/OStatus/lib/salmonqueuehandler.php (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/lib/activity.php b/lib/activity.php index fa4ae0274..33932ad0e 100644 --- a/lib/activity.php +++ b/lib/activity.php @@ -691,6 +691,9 @@ class ActivityVerb 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'; + + // For simple profile-update pings; no content to share. + const UPDATE_PROFILE = 'http://ostatus.org/schema/1.0/update-profile'; } class ActivityContext diff --git a/lib/profilequeuehandler.php b/lib/profilequeuehandler.php new file mode 100644 index 000000000..e8a00aef3 --- /dev/null +++ b/lib/profilequeuehandler.php @@ -0,0 +1,48 @@ +. + */ + +/** + * @package QueueHandler + * @maintainer Brion Vibber + */ + +class ProfileQueueHandler extends QueueHandler +{ + + function transport() + { + return 'profile'; + } + + function handle($profile) + { + if (!($profile instanceof Profile)) { + common_log(LOG_ERR, "Got a bogus profile, not broadcasting"); + return true; + } + + if (Event::handle('StartBroadcastProfile', array($profile))) { + require_once(INSTALLDIR.'/lib/omb.php'); + omb_broadcast_profile($profile); + } + Event::handle('EndBroadcastProfile', array($profile)); + return true; + } + +} diff --git a/lib/queuemanager.php b/lib/queuemanager.php index 8f8c8f133..9fdc80110 100644 --- a/lib/queuemanager.php +++ b/lib/queuemanager.php @@ -262,6 +262,9 @@ abstract class QueueManager extends IoManager $this->connect('sms', 'SmsQueueHandler'); } + // Broadcasting profile updates to OMB remote subscribers + $this->connect('profile', 'ProfileQueueHandler'); + // XMPP output handlers... if (common_config('xmpp', 'enabled')) { // Delivery prep, read by queuedaemon.php: diff --git a/lib/util.php b/lib/util.php index 7fb2c6c4b..9354431f2 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1119,12 +1119,16 @@ function common_enqueue_notice($notice) return true; } -function common_broadcast_profile($profile) +/** + * Broadcast profile updates to OMB and other remote subscribers. + * + * Since this may be slow with a lot of subscribers or bad remote sites, + * this is run through the background queues if possible. + */ +function common_broadcast_profile(Profile $profile) { - // XXX: optionally use a queue system like http://code.google.com/p/microapps/wiki/NQDQ - require_once(INSTALLDIR.'/lib/omb.php'); - omb_broadcast_profile($profile); - // XXX: Other broadcasts...? + $qm = QueueManager::get(); + $qm->enqueue($profile, "profile"); return true; } diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 9376c048d..90abe034d 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -82,14 +82,14 @@ class OStatusPlugin extends Plugin $qm->connect('ostatus', 'OStatusQueueHandler'); // Outgoing from our internal PuSH hub - $qm->connect('hubverify', 'HubVerifyQueueHandler'); + $qm->connect('hubconf', 'HubConfQueueHandler'); $qm->connect('hubout', 'HubOutQueueHandler'); // Outgoing Salmon replies (when we don't need a return value) - $qm->connect('salmonout', 'SalmonOutQueueHandler'); + $qm->connect('salmon', 'SalmonQueueHandler'); // Incoming from a foreign PuSH hub - $qm->connect('pushinput', 'PushInputQueueHandler'); + $qm->connect('pushin', 'PushInQueueHandler'); return true; } @@ -656,4 +656,51 @@ class OStatusPlugin extends Plugin return true; } + + /** + * Ping remote profiles with updates to this profile. + * Salmon pings are queued for background processing. + */ + function onEndBroadcastProfile(Profile $profile) + { + $user = User::staticGet('id', $profile->id); + + // Find foreign accounts I'm subscribed to that support Salmon pings. + // + // @fixme we could run updates through the PuSH feed too, + // in which case we can skip Salmon pings to folks who + // are also subscribed to me. + $sql = "SELECT * FROM ostatus_profile " . + "WHERE profile_id IN " . + "(SELECT subscribed FROM subscription WHERE subscriber=%d) " . + "OR group_id IN " . + "(SELECT group_id FROM group_member WHERE profile_id=%d)"; + $oprofile = new Ostatus_profile(); + $oprofile->query(sprintf($sql, $profile->id, $profile->id)); + + if ($oprofile->N == 0) { + common_log(LOG_DEBUG, "No OStatus remote subscribees for $profile->nickname"); + return true; + } + + $act = new Activity(); + + $act->verb = ActivityVerb::UPDATE_PROFILE; + $act->id = TagURI::mint('update-profile:%d:%s', + $profile->id, + common_date_iso8601(time())); + $act->time = time(); + $act->title = _m("Profile update"); + $act->content = sprintf(_m("%s has updated their profile page."), + $profile->getBestName()); + + $act->actor = ActivityObject::fromProfile($profile); + $act->object = $act->actor; + + while ($oprofile->fetch()) { + $oprofile->notifyDeferred($act); + } + + return true; + } } diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php index 4184f0e0c..9a2067b8c 100644 --- a/plugins/OStatus/actions/pushcallback.php +++ b/plugins/OStatus/actions/pushcallback.php @@ -68,7 +68,7 @@ class PushCallbackAction extends Action 'post' => $post, 'hmac' => $hmac); $qm = QueueManager::get(); - $qm->enqueue($data, 'pushinput'); + $qm->enqueue($data, 'pushin'); } /** diff --git a/plugins/OStatus/classes/HubSub.php b/plugins/OStatus/classes/HubSub.php index eae2928c3..1ac181fee 100644 --- a/plugins/OStatus/classes/HubSub.php +++ b/plugins/OStatus/classes/HubSub.php @@ -164,7 +164,7 @@ class HubSub extends Memcached_DataObject 'token' => $token, 'retries' => $retries); $qm = QueueManager::get(); - $qm->enqueue($data, 'hubverify'); + $qm->enqueue($data, 'hubconf'); } /** diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 9f9efb96e..61505206e 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -431,21 +431,57 @@ class Ostatus_profile extends Memcached_DataObject return false; } - public function notifyActivity($activity) + /** + * Send a Salmon notification ping immediately, and confirm that we got + * an acceptable response from the remote site. + * + * @param mixed $entry XML string, Notice, or Activity + * @return boolean success + */ + public function notifyActivity($entry) { if ($this->salmonuri) { + $salmon = new Salmon(); + return $salmon->post($this->salmonuri, $this->notifyPrepXml($entry)); + } - $xml = '' . - $activity->asString(true); + return false; + } - $salmon = new Salmon(); // ? + /** + * Queue a Salmon notification for later. If queues are disabled we'll + * send immediately but won't get the return value. + * + * @param mixed $entry XML string, Notice, or Activity + * @return boolean success + */ + public function notifyDeferred($entry) + { + if ($this->salmonuri) { + $data = array('salmonuri' => $this->salmonuri, + 'entry' => $this->notifyPrepXml($entry)); - return $salmon->post($this->salmonuri, $xml); + $qm = QueueManager::get(); + return $qm->enqueue($data, 'salmon'); } return false; } + protected function notifyPrepXml($entry) + { + $preamble = ''; + if (is_string($entry)) { + return $entry; + } else if ($entry instanceof Activity) { + return $preamble . $entry->asString(true); + } else if ($entry instanceof Notice) { + return $preamble . $entry->asAtomEntry(true, true); + } else { + throw new ServerException("Invalid type passed to Ostatus_profile::notify; must be XML string or Activity entry"); + } + } + function getBestName() { if ($this->isGroup()) { diff --git a/plugins/OStatus/lib/hubconfqueuehandler.php b/plugins/OStatus/lib/hubconfqueuehandler.php new file mode 100644 index 000000000..c8e0b72fe --- /dev/null +++ b/plugins/OStatus/lib/hubconfqueuehandler.php @@ -0,0 +1,54 @@ +. + */ + +/** + * Send a PuSH subscription verification from our internal hub. + * @package Hub + * @author Brion Vibber + */ +class HubConfQueueHandler extends QueueHandler +{ + function transport() + { + return 'hubconf'; + } + + function handle($data) + { + $sub = $data['sub']; + $mode = $data['mode']; + $token = $data['token']; + + assert($sub instanceof HubSub); + assert($mode === 'subscribe' || $mode === 'unsubscribe'); + + common_log(LOG_INFO, __METHOD__ . ": $mode $sub->callback $sub->topic"); + try { + $sub->verify($mode, $token); + } catch (Exception $e) { + common_log(LOG_ERR, "Failed PuSH $mode verify to $sub->callback for $sub->topic: " . + $e->getMessage()); + // @fixme schedule retry? + // @fixme just kill it? + } + + return true; + } +} + diff --git a/plugins/OStatus/lib/hubverifyqueuehandler.php b/plugins/OStatus/lib/hubverifyqueuehandler.php deleted file mode 100644 index 7ce9e1431..000000000 --- a/plugins/OStatus/lib/hubverifyqueuehandler.php +++ /dev/null @@ -1,54 +0,0 @@ -. - */ - -/** - * Send a PuSH subscription verification from our internal hub. - * @package Hub - * @author Brion Vibber - */ -class HubVerifyQueueHandler extends QueueHandler -{ - function transport() - { - return 'hubverify'; - } - - function handle($data) - { - $sub = $data['sub']; - $mode = $data['mode']; - $token = $data['token']; - - assert($sub instanceof HubSub); - assert($mode === 'subscribe' || $mode === 'unsubscribe'); - - common_log(LOG_INFO, __METHOD__ . ": $mode $sub->callback $sub->topic"); - try { - $sub->verify($mode, $token); - } catch (Exception $e) { - common_log(LOG_ERR, "Failed PuSH $mode verify to $sub->callback for $sub->topic: " . - $e->getMessage()); - // @fixme schedule retry? - // @fixme just kill it? - } - - return true; - } -} - diff --git a/plugins/OStatus/lib/ostatusqueuehandler.php b/plugins/OStatus/lib/ostatusqueuehandler.php index c1e50bffa..0da85600f 100644 --- a/plugins/OStatus/lib/ostatusqueuehandler.php +++ b/plugins/OStatus/lib/ostatusqueuehandler.php @@ -83,23 +83,11 @@ class OStatusQueueHandler extends QueueHandler function pingReply($oprofile) { if ($this->user) { - if (!empty($oprofile->salmonuri)) { - // For local posts, send a Salmon ping to the mentioned - // remote user or group. - // @fixme as an optimization we can skip this if the - // remote profile is subscribed to the author. - - common_log(LOG_INFO, "Prepping to send notice '{$this->notice->uri}' to remote profile '{$oprofile->uri}'."); - - $xml = ''; - $xml .= $this->notice->asAtomEntry(true, true); - - $data = array('salmonuri' => $oprofile->salmonuri, - 'entry' => $xml); - - $qm = QueueManager::get(); - $qm->enqueue($data, 'salmonout'); - } + // For local posts, send a Salmon ping to the mentioned + // remote user or group. + // @fixme as an optimization we can skip this if the + // remote profile is subscribed to the author. + $oprofile->notifyDeferred($this->notice); } } diff --git a/plugins/OStatus/lib/pushinputqueuehandler.php b/plugins/OStatus/lib/pushinputqueuehandler.php deleted file mode 100644 index cbd9139b5..000000000 --- a/plugins/OStatus/lib/pushinputqueuehandler.php +++ /dev/null @@ -1,49 +0,0 @@ -. - */ - -/** - * Process a feed distribution POST from a PuSH hub. - * @package FeedSub - * @author Brion Vibber - */ - -class PushInputQueueHandler extends QueueHandler -{ - function transport() - { - return 'pushinput'; - } - - function handle($data) - { - assert(is_array($data)); - - $feedsub_id = $data['feedsub_id']; - $post = $data['post']; - $hmac = $data['hmac']; - - $feedsub = FeedSub::staticGet('id', $feedsub_id); - if ($feedsub) { - $feedsub->receive($post, $hmac); - } else { - common_log(LOG_ERR, "Discarding POST to unknown feed subscription id $feedsub_id"); - } - return true; - } -} diff --git a/plugins/OStatus/lib/pushinqueuehandler.php b/plugins/OStatus/lib/pushinqueuehandler.php new file mode 100644 index 000000000..a90f52df2 --- /dev/null +++ b/plugins/OStatus/lib/pushinqueuehandler.php @@ -0,0 +1,49 @@ +. + */ + +/** + * Process a feed distribution POST from a PuSH hub. + * @package FeedSub + * @author Brion Vibber + */ + +class PushInQueueHandler extends QueueHandler +{ + function transport() + { + return 'pushin'; + } + + function handle($data) + { + assert(is_array($data)); + + $feedsub_id = $data['feedsub_id']; + $post = $data['post']; + $hmac = $data['hmac']; + + $feedsub = FeedSub::staticGet('id', $feedsub_id); + if ($feedsub) { + $feedsub->receive($post, $hmac); + } else { + common_log(LOG_ERR, "Discarding POST to unknown feed subscription id $feedsub_id"); + } + return true; + } +} diff --git a/plugins/OStatus/lib/salmonoutqueuehandler.php b/plugins/OStatus/lib/salmonoutqueuehandler.php deleted file mode 100644 index 536ff94af..000000000 --- a/plugins/OStatus/lib/salmonoutqueuehandler.php +++ /dev/null @@ -1,44 +0,0 @@ -. - */ - -/** - * Send a Salmon notification in the background. - * @package OStatusPlugin - * @author Brion Vibber - */ -class SalmonOutQueueHandler extends QueueHandler -{ - function transport() - { - return 'salmonout'; - } - - function handle($data) - { - assert(is_array($data)); - assert(is_string($data['salmonuri'])); - assert(is_string($data['entry'])); - - $salmon = new Salmon(); - $salmon->post($data['salmonuri'], $data['entry']); - - // @fixme detect failure and attempt to resend - return true; - } -} diff --git a/plugins/OStatus/lib/salmonqueuehandler.php b/plugins/OStatus/lib/salmonqueuehandler.php new file mode 100644 index 000000000..aa97018dc --- /dev/null +++ b/plugins/OStatus/lib/salmonqueuehandler.php @@ -0,0 +1,44 @@ +. + */ + +/** + * Send a Salmon notification in the background. + * @package OStatusPlugin + * @author Brion Vibber + */ +class SalmonQueueHandler extends QueueHandler +{ + function transport() + { + return 'salmon'; + } + + function handle($data) + { + assert(is_array($data)); + assert(is_string($data['salmonuri'])); + assert(is_string($data['entry'])); + + $salmon = new Salmon(); + $salmon->post($data['salmonuri'], $data['entry']); + + // @fixme detect failure and attempt to resend + return true; + } +} -- cgit v1.2.3-54-g00ecf From 58e232a10ad45808caeb1bbd72641aea535e00e8 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Thu, 25 Feb 2010 02:43:55 +0000 Subject: OStatus: when finding webfinger @-replies, override a local profile match if found at the same location (eg @someguy vs @someguy@example.org) Fixes inconsistent application of webfinger @-mentions in OStatus; once a local profile is set up the local name would often match first and ended up overriding in output. --- plugins/OStatus/OStatusPlugin.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 5feb53b26..7f75b7b2b 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -210,7 +210,7 @@ class OStatusPlugin extends Plugin * */ - function onStartFindMentions($sender, $text, &$mentions) + function onEndFindMentions($sender, $text, &$mentions) { preg_match_all('/(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)/', $text, @@ -233,11 +233,21 @@ class OStatusPlugin extends Plugin $this->log(LOG_INFO, "Ostatus_profile found for address '$webfinger'"); + if ($oprofile->isGroup()) { + continue; + } $profile = $oprofile->localProfile(); + $pos = $wmatch[1]; + foreach ($mentions as $i => $other) { + // If we share a common prefix with a local user, override it! + if ($other['position'] == $pos) { + unset($mentions[$i]); + } + } $mentions[] = array('mentioned' => array($profile), 'text' => $wmatch[0], - 'position' => $wmatch[1], + 'position' => $pos, 'url' => $profile->profileurl); } } -- cgit v1.2.3-54-g00ecf From 5cb6e54bedd6aa7dda14bb43fd29d8c119fe6d4c Mon Sep 17 00:00:00 2001 From: James Walker Date: Thu, 25 Feb 2010 14:59:15 -0500 Subject: call-time pass by reference --- plugins/OStatus/OStatusPlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 7f75b7b2b..9a543df3b 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -644,7 +644,7 @@ class OStatusPlugin extends Plugin function onStartUserGroupHomeUrl($group, &$url) { - return $this->onStartUserGroupPermalink($group, &$url); + return $this->onStartUserGroupPermalink($group, $url); } function onStartUserGroupPermalink($group, &$url) -- cgit v1.2.3-54-g00ecf From 93f4f07c122613c0afd2257bd9e898629124178a Mon Sep 17 00:00:00 2001 From: James Walker Date: Thu, 25 Feb 2010 17:52:18 -0500 Subject: moving webfinger action to xrdaction --- plugins/OStatus/OStatusPlugin.php | 4 +- plugins/OStatus/actions/hostmeta.php | 2 +- plugins/OStatus/actions/webfinger.php | 109 ---------------------------------- plugins/OStatus/actions/xrd.php | 109 ++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 112 deletions(-) delete mode 100644 plugins/OStatus/actions/webfinger.php create mode 100644 plugins/OStatus/actions/xrd.php (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 9a543df3b..91d055498 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -43,8 +43,8 @@ class OStatusPlugin extends Plugin // Discovery actions $m->connect('.well-known/host-meta', array('action' => 'hostmeta')); - $m->connect('main/webfinger', - array('action' => 'webfinger')); + $m->connect('main/xrd', + array('action' => 'xrd')); $m->connect('main/ostatus', array('action' => 'ostatusinit')); $m->connect('main/ostatus?nickname=:nickname', diff --git a/plugins/OStatus/actions/hostmeta.php b/plugins/OStatus/actions/hostmeta.php index 85715ecf4..3d00b98ae 100644 --- a/plugins/OStatus/actions/hostmeta.php +++ b/plugins/OStatus/actions/hostmeta.php @@ -32,7 +32,7 @@ class HostMetaAction extends Action parent::handle(); $domain = common_config('site', 'server'); - $url = common_local_url('webfinger'); + $url = common_local_url('xrd'); $url.= '?uri={uri}'; $xrd = new XRD(); diff --git a/plugins/OStatus/actions/webfinger.php b/plugins/OStatus/actions/webfinger.php deleted file mode 100644 index fa41b2444..000000000 --- a/plugins/OStatus/actions/webfinger.php +++ /dev/null @@ -1,109 +0,0 @@ -. - */ - -/** - * @package OStatusPlugin - * @maintainer James Walker - */ - -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 = Discovery::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' => Discovery::PROFILEPAGE, - 'type' => 'text/html', - 'href' => common_profile_url($nick)); - - $xrd->links[] = array('rel' => Discovery::UPDATESFROM, - 'href' => common_local_url('ApiTimelineUser', - array('id' => $this->user->id, - 'format' => 'atom')), - 'type' => 'application/atom+xml'); - - // hCard - $xrd->links[] = array('rel' => 'http://microformats.org/profile/hcard', - 'type' => 'text/html', - 'href' => common_profile_url($nick)); - - // XFN - $xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11', - 'type' => 'text/html', - 'href' => common_profile_url($nick)); - // FOAF - $xrd->links[] = array('rel' => 'describedby', - 'type' => 'application/rdf+xml', - 'href' => common_local_url('foaf', - array('nickname' => $nick))); - - $salmon_url = common_local_url('salmon', - array('id' => $this->user->id)); - - $xrd->links[] = array('rel' => 'salmon', - 'href' => $salmon_url); - - // Get this user's keypair - $magickey = Magicsig::staticGet('user_id', $this->user->id); - if (!$magickey) { - // No keypair yet, let's generate one. - $magickey = new Magicsig(); - $magickey->generate(); - } - - $xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL, - 'href' => 'data:application/magic-public-key;'. $magickey->keypair); - - // TODO - finalize where the redirect should go on the publisher - $url = common_local_url('ostatussub') . '?profile={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/actions/xrd.php b/plugins/OStatus/actions/xrd.php new file mode 100644 index 000000000..cc5c70b08 --- /dev/null +++ b/plugins/OStatus/actions/xrd.php @@ -0,0 +1,109 @@ +. + */ + +/** + * @package OStatusPlugin + * @maintainer James Walker + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +class XrdAction extends Action +{ + + public $uri; + + function prepare($args) + { + parent::prepare($args); + + $this->uri = $this->trimmed('uri'); + + return true; + } + + function handle() + { + $acct = Discovery::normalize($this->uri); + + $xrd = new XRD(); + + list($nick, $domain) = explode('@', substr(urldecode($acct), 5)); + $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' => Discovery::PROFILEPAGE, + 'type' => 'text/html', + 'href' => common_profile_url($nick)); + + $xrd->links[] = array('rel' => Discovery::UPDATESFROM, + 'href' => common_local_url('ApiTimelineUser', + array('id' => $this->user->id, + 'format' => 'atom')), + 'type' => 'application/atom+xml'); + + // hCard + $xrd->links[] = array('rel' => 'http://microformats.org/profile/hcard', + 'type' => 'text/html', + 'href' => common_profile_url($nick)); + + // XFN + $xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11', + 'type' => 'text/html', + 'href' => common_profile_url($nick)); + // FOAF + $xrd->links[] = array('rel' => 'describedby', + 'type' => 'application/rdf+xml', + 'href' => common_local_url('foaf', + array('nickname' => $nick))); + + $salmon_url = common_local_url('salmon', + array('id' => $this->user->id)); + + $xrd->links[] = array('rel' => 'salmon', + 'href' => $salmon_url); + + // Get this user's keypair + $magickey = Magicsig::staticGet('user_id', $this->user->id); + if (!$magickey) { + // No keypair yet, let's generate one. + $magickey = new Magicsig(); + $magickey->generate($this->user->id); + } + + $xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL, + 'href' => 'data:application/magic-public-key;'. $magickey->keypair); + + // TODO - finalize where the redirect should go on the publisher + $url = common_local_url('ostatussub') . '?profile={uri}'; + $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe', + 'template' => $url ); + + header('Content-type: text/xml'); + print $xrd->toXML(); + } + +} -- cgit v1.2.3-54-g00ecf From 84d0c865c4c2dd597e249c76fa1429175f5461a1 Mon Sep 17 00:00:00 2001 From: James Walker Date: Fri, 26 Feb 2010 03:25:51 -0500 Subject: salmon actually fetching remote keypairs --- plugins/OStatus/OStatusPlugin.php | 14 ++++++++++++++ plugins/OStatus/actions/ostatusinit.php | 2 +- plugins/OStatus/lib/discovery.php | 34 +++++++++++++++++---------------- plugins/OStatus/lib/magicenvelope.php | 5 ++--- 4 files changed, 35 insertions(+), 20 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 91d055498..46f986682 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -102,6 +102,20 @@ class OStatusPlugin extends Plugin return true; } + /** + * Add a link header for LRDD Discovery + */ + function onStartShowHTML($action) + { + if ($action instanceof ShowstreamAction) { + $acct = 'acct:'. $action->profile->nickname .'@'. common_config('site', 'server'); + $url = common_local_url('xrd'); + $url.= '?uri='. $acct; + + header('Link: <'.$url.'>; rel="'. Discovery::LRDD_REL.'"; type="application/xrd+xml"'); + } + } + /** * Set up a PuSH hub link to our internal link for canonical timeline * Atom feeds for users and groups. diff --git a/plugins/OStatus/actions/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php index 5c8575595..8ba8dcdcc 100644 --- a/plugins/OStatus/actions/ostatusinit.php +++ b/plugins/OStatus/actions/ostatusinit.php @@ -144,7 +144,7 @@ class OStatusInitAction extends Action $user = User::staticGet('nickname', $this->nickname); $target_profile = common_local_url('userbyid', array('id' => $user->id)); - $url = $w->applyTemplate($link['template'], $target_profile); + $url = Discovery::applyTemplate($link['template'], $target_profile); common_log(LOG_INFO, "Sending remote subscriber $acct to $url"); common_redirect($url, 303); } diff --git a/plugins/OStatus/lib/discovery.php b/plugins/OStatus/lib/discovery.php index 8aba31328..c268ad05c 100644 --- a/plugins/OStatus/lib/discovery.php +++ b/plugins/OStatus/lib/discovery.php @@ -91,7 +91,6 @@ class Discovery foreach ($this->methods as $class) { $links = call_user_func(array($class, 'discover'), $uri); - if ($link = Discovery::getService($links, Discovery::LRDD_REL)) { // Load the LRDD XRD if ($link['template']) { @@ -141,7 +140,7 @@ class Discovery } return XRD::parse($response->getBody()); - } + } } interface Discovery_LRDD @@ -153,13 +152,12 @@ class Discovery_LRDD_Host_Meta implements Discovery_LRDD { public function discover($uri) { - if (Discovery::isWebfinger($uri)) { - // We have a webfinger acct: - start with host-meta - list($name, $domain) = explode('@', $id); - } else { - $domain = @parse_url($uri, PHP_URL_HOST); + if (!Discovery::isWebfinger($uri)) { + return false; } + // We have a webfinger acct: - start with host-meta + list($name, $domain) = explode('@', $uri); $url = 'http://'. $domain .'/.well-known/host-meta'; $xrd = Discovery::fetchXrd($url); @@ -180,27 +178,29 @@ class Discovery_LRDD_Link_Header implements Discovery_LRDD { try { $client = new HTTPClient(); - $response = $client->get($url); + $response = $client->get($uri); } catch (HTTP_Request2_Exception $e) { return false; } - + if ($response->getStatus() != 200) { return false; } $link_header = $response->getHeader('Link'); if (!$link_header) { - return false; + // return false; } - return Discovery_LRDD_Link_Header::parseHeader($header); + return Discovery_LRDD_Link_Header::parseHeader($link_header); } protected static function parseHeader($header) { preg_match('/^<[^>]+>/', $header, $uri_reference); - if (empty($uri_reference)) return; + //if (empty($uri_reference)) return; + + $links = array(); $link_uri = trim($uri_reference[0], '<>'); $link_rel = array(); @@ -210,7 +210,7 @@ class Discovery_LRDD_Link_Header implements Discovery_LRDD $header = substr($header, strlen($uri_reference[0])); // parse link-params - $params = explode($header, ';'); + $params = explode(';', $header); foreach ($params as $param) { if (empty($param)) continue; @@ -229,11 +229,13 @@ class Discovery_LRDD_Link_Header implements Discovery_LRDD $link_type = trim($param_value); } } - - return array( + + $links[] = array( 'href' => $link_uri, 'rel' => $link_rel, 'type' => $link_type); + + return $links; } } @@ -243,7 +245,7 @@ class Discovery_LRDD_Link_HTML implements Discovery_LRDD { try { $client = new HTTPClient(); - $response = $client->get($url); + $response = $client->get($uri); } catch (HTTP_Request2_Exception $e) { return false; } diff --git a/plugins/OStatus/lib/magicenvelope.php b/plugins/OStatus/lib/magicenvelope.php index 4f8f88155..c642af548 100644 --- a/plugins/OStatus/lib/magicenvelope.php +++ b/plugins/OStatus/lib/magicenvelope.php @@ -52,14 +52,13 @@ class MagicEnvelope { $disco = new Discovery(); - $links = $disco->lookup($signer_uri); - if ($link = Discovery::getService($links, 'magic-public-key')) { + $xrd = $disco->lookup($signer_uri); + if ($link = Discovery::getService($xrd->links, Magicsig::PUBLICKEYREL)) { list($type, $keypair) = explode(';', $link['href']); return $keypair; } throw new Exception('Unable to locate signer public key'); - //return 'RSA.79_L2gq-TD72Nsb5yGS0r9stLLpJZF5AHXyxzWmQmlqKl276LEJEs8CppcerLcR90MbYQUwt-SX9slx40Yq3vA==.AQAB.AR-jo5KMfSISmDAT2iMs2_vNFgWRjl5rbJVvA0SpGIEWyPdCGxlPtCbTexp8-0ZEIe8a4SyjatBECH5hxgMTpw=='; } -- cgit v1.2.3-54-g00ecf From bbb0a7d5bc2c8815a2057eb0a6588d8d60ff4416 Mon Sep 17 00:00:00 2001 From: James Walker Date: Fri, 26 Feb 2010 13:17:24 -0500 Subject: updating to use latest salmon NS definitions --- plugins/OStatus/OStatusPlugin.php | 3 ++- plugins/OStatus/actions/xrd.php | 8 ++++++-- plugins/OStatus/classes/Ostatus_profile.php | 6 +++--- plugins/OStatus/lib/salmon.php | 5 +++++ 4 files changed, 16 insertions(+), 6 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 46f986682..89fa45f91 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -149,7 +149,8 @@ class OStatusPlugin extends Plugin // Also, we'll add in the salmon link $salmon = common_local_url($salmonAction, array('id' => $id)); - $feed->addLink($salmon, array('rel' => 'salmon')); + $feed->addLink($salmon, array('rel' => Salmon::NS_REPLIES)); + $feed->addLink($salmon, array('rel' => Salmon::NS_MENTIONS)); } return true; diff --git a/plugins/OStatus/actions/xrd.php b/plugins/OStatus/actions/xrd.php index e6b694d61..4350ad025 100644 --- a/plugins/OStatus/actions/xrd.php +++ b/plugins/OStatus/actions/xrd.php @@ -80,10 +80,14 @@ class XrdAction extends Action 'href' => common_local_url('foaf', array('nickname' => $nick))); - $salmon_url = common_local_url('salmon', + // Salmon + $salmon_url = common_local_url('usersalmon', array('id' => $this->user->id)); - $xrd->links[] = array('rel' => 'salmon', + $xrd->links[] = array('rel' => Salmon::NS_REPLIES, + 'href' => $salmon_url); + + $xrd->links[] = array('rel' => Salmon::NS_MENTIONS, 'href' => $salmon_url); // Get this user's keypair diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 091056c54..c6f7378c4 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -707,7 +707,7 @@ class Ostatus_profile extends Memcached_DataObject $huburi = $discover->getAtomLink('hub'); $hints['hub'] = $huburi; - $salmonuri = $discover->getAtomLink('salmon'); + $salmonuri = $discover->getAtomLink(Salmon::NS_REPLIES); $hints['salmon'] = $salmonuri; if (!$huburi) { @@ -991,7 +991,7 @@ class Ostatus_profile extends Memcached_DataObject $discover = new FeedDiscovery(); $discover->discoverFromFeedURL($hints['feedurl']); } - $salmonuri = $discover->getAtomLink('salmon'); + $salmonuri = $discover->getAtomLink(Salmon::NS_REPLIES); } if (array_key_exists('hub', $hints)) { @@ -1299,7 +1299,7 @@ class Ostatus_profile extends Memcached_DataObject case Discovery::PROFILEPAGE: $profileUrl = $link['href']; break; - case 'salmon': + case Salmon::NS_REPLIES: $salmonEndpoint = $link['href']; break; case Discovery::UPDATESFROM: diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php index 9d4359f74..c59de2a14 100644 --- a/plugins/OStatus/lib/salmon.php +++ b/plugins/OStatus/lib/salmon.php @@ -28,6 +28,11 @@ */ class Salmon { + + const NS_REPLIES = "http://salmon-protocol.org/ns/salmon-replies"; + + const NS_MENTIONS = "http://salmon-protocol.org/ns/salmon-mention"; + /** * Sign and post the given Atom entry as a Salmon message. * -- cgit v1.2.3-54-g00ecf From 223ebc765c454e030a49df7e2e1b9cdc2b005fe6 Mon Sep 17 00:00:00 2001 From: James Walker Date: Fri, 26 Feb 2010 14:21:21 -0500 Subject: move signing to take a local actor profile and use local keys --- plugins/OStatus/OStatusPlugin.php | 14 +++++++------- plugins/OStatus/classes/Magicsig.php | 3 ++- plugins/OStatus/classes/Ostatus_profile.php | 11 ++++++----- plugins/OStatus/lib/magicenvelope.php | 12 +----------- plugins/OStatus/lib/ostatusqueuehandler.php | 2 +- plugins/OStatus/lib/salmon.php | 24 ++++++++++++++++++------ plugins/OStatus/lib/salmonqueuehandler.php | 4 +++- 7 files changed, 38 insertions(+), 32 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 89fa45f91..720dedd0a 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -415,7 +415,7 @@ class OStatusPlugin extends Plugin $act->actor = ActivityObject::fromProfile($subscriber); $act->object = ActivityObject::fromProfile($other); - $oprofile->notifyActivity($act); + $oprofile->notifyActivity($act, $subscriber); return true; } @@ -463,7 +463,7 @@ class OStatusPlugin extends Plugin $act->actor = ActivityObject::fromProfile($profile); $act->object = ActivityObject::fromProfile($other); - $oprofile->notifyActivity($act); + $oprofile->notifyActivity($act, $profile); return true; } @@ -505,7 +505,7 @@ class OStatusPlugin extends Plugin $member->getBestName(), $oprofile->getBestName()); - if ($oprofile->notifyActivity($act)) { + if ($oprofile->notifyActivity($act, $member)) { return true; } else { $oprofile->garbageCollect(); @@ -555,7 +555,7 @@ class OStatusPlugin extends Plugin $member->getBestName(), $oprofile->getBestName()); - $oprofile->notifyActivity($act); + $oprofile->notifyActivity($act, $member); } } @@ -598,7 +598,7 @@ class OStatusPlugin extends Plugin $act->actor = ActivityObject::fromProfile($profile); $act->object = ActivityObject::fromNotice($notice); - $oprofile->notifyActivity($act); + $oprofile->notifyActivity($act, $profile); return true; } @@ -642,7 +642,7 @@ class OStatusPlugin extends Plugin $act->actor = ActivityObject::fromProfile($profile); $act->object = ActivityObject::fromNotice($notice); - $oprofile->notifyActivity($act); + $oprofile->notifyActivity($act, $profile); return true; } @@ -731,7 +731,7 @@ class OStatusPlugin extends Plugin $act->object = $act->actor; while ($oprofile->fetch()) { - $oprofile->notifyDeferred($act); + $oprofile->notifyDeferred($act, $profile); } return true; diff --git a/plugins/OStatus/classes/Magicsig.php b/plugins/OStatus/classes/Magicsig.php index 02882d19b..751527c81 100644 --- a/plugins/OStatus/classes/Magicsig.php +++ b/plugins/OStatus/classes/Magicsig.php @@ -49,7 +49,8 @@ class Magicsig extends Memcached_DataObject public /*static*/ function staticGet($k, $v=null) { - return parent::staticGet(__CLASS__, $k, $v); + $obj = parent::staticGet(__CLASS__, $k, $v); + return Magicsig::fromString($obj->keypair); } diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index c6f7378c4..35539bff7 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -357,7 +357,7 @@ class Ostatus_profile extends Memcached_DataObject common_log(LOG_INFO, "Posting to Salmon endpoint $this->salmonuri: $xml"); $salmon = new Salmon(); // ? - return $salmon->post($this->salmonuri, $xml); + return $salmon->post($this->salmonuri, $xml, $actor); } return false; } @@ -369,11 +369,11 @@ class Ostatus_profile extends Memcached_DataObject * @param mixed $entry XML string, Notice, or Activity * @return boolean success */ - public function notifyActivity($entry) + public function notifyActivity($entry, $actor) { if ($this->salmonuri) { $salmon = new Salmon(); - return $salmon->post($this->salmonuri, $this->notifyPrepXml($entry)); + return $salmon->post($this->salmonuri, $this->notifyPrepXml($entry), $actor); } return false; @@ -386,11 +386,12 @@ class Ostatus_profile extends Memcached_DataObject * @param mixed $entry XML string, Notice, or Activity * @return boolean success */ - public function notifyDeferred($entry) + public function notifyDeferred($entry, $actor) { if ($this->salmonuri) { $data = array('salmonuri' => $this->salmonuri, - 'entry' => $this->notifyPrepXml($entry)); + 'entry' => $this->notifyPrepXml($entry), + 'actor' => $actor->id); $qm = QueueManager::get(); return $qm->enqueue($data, 'salmon'); diff --git a/plugins/OStatus/lib/magicenvelope.php b/plugins/OStatus/lib/magicenvelope.php index 457c0fba2..f33119b8f 100644 --- a/plugins/OStatus/lib/magicenvelope.php +++ b/plugins/OStatus/lib/magicenvelope.php @@ -67,18 +67,8 @@ class MagicEnvelope } - public function signMessage($text, $mimetype, $signer_uri) + public function signMessage($text, $mimetype, $keypair) { - $signer_uri = $this->normalizeUser($signer_uri); - - if (!$this->checkAuthor($text, $signer_uri)) { - throw new Exception("Unable to determine entry author."); - } - - $keypair = $this->getKeyPair($signer_uri); - if (!$keypair) { - throw new Exception("Unable to retrive keypair for ". $signer_uri); - } $signature_alg = Magicsig::fromString($keypair); $armored_text = base64_encode($text); diff --git a/plugins/OStatus/lib/ostatusqueuehandler.php b/plugins/OStatus/lib/ostatusqueuehandler.php index 0da85600f..6ca31c485 100644 --- a/plugins/OStatus/lib/ostatusqueuehandler.php +++ b/plugins/OStatus/lib/ostatusqueuehandler.php @@ -87,7 +87,7 @@ class OStatusQueueHandler extends QueueHandler // remote user or group. // @fixme as an optimization we can skip this if the // remote profile is subscribed to the author. - $oprofile->notifyDeferred($this->notice); + $oprofile->notifyDeferred($this->notice, $this->user); } } diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php index c59de2a14..6e2459544 100644 --- a/plugins/OStatus/lib/salmon.php +++ b/plugins/OStatus/lib/salmon.php @@ -42,14 +42,14 @@ class Salmon * @param string $xml * @return boolean success */ - public function post($endpoint_uri, $xml) + public function post($endpoint_uri, $xml, $actor) { if (empty($endpoint_uri)) { return false; } if (!common_config('ostatus', 'skip_signatures')) { - $xml = $this->createMagicEnv($xml); + $xml = $this->createMagicEnv($xml, $actor); } $headers = array('Content-Type: application/atom+xml'); @@ -70,15 +70,27 @@ class Salmon return true; } - public function createMagicEnv($text) + public function createMagicEnv($text, $actor) { + common_log(LOG_DEBUG, "Got actor as : ". print_r($actor, true)); $magic_env = new MagicEnvelope(); - // TODO: Should probably be getting the signer uri as an argument? - $signer_uri = $magic_env->getAuthor($text); + $user = User::staticGet('id', $actor->id); + if ($user->id) { + // Use local key + $magickey = Magicsig::staticGet('user_id', $user->id); + if (!$magickey) { + // No keypair yet, let's generate one. + $magickey = new Magicsig(); + $magickey->generate($user->id); + } + common_log(LOG_DEBUG, "Salmon: Loaded key for ". $user->id); + } else { + throw new Exception("Salmon invalid actor for signing"); + } try { - $env = $magic_env->signMessage($text, 'application/atom+xml', $signer_uri); + $env = $magic_env->signMessage($text, 'application/atom+xml', $magickey->toString()); } catch (Exception $e) { common_log(LOG_ERR, "Salmon signing failed: ". $e->getMessage()); return $text; diff --git a/plugins/OStatus/lib/salmonqueuehandler.php b/plugins/OStatus/lib/salmonqueuehandler.php index aa97018dc..7eeb5f8e9 100644 --- a/plugins/OStatus/lib/salmonqueuehandler.php +++ b/plugins/OStatus/lib/salmonqueuehandler.php @@ -35,8 +35,10 @@ class SalmonQueueHandler extends QueueHandler assert(is_string($data['salmonuri'])); assert(is_string($data['entry'])); + $actor = Profile::staticGet($data['actor']); + $salmon = new Salmon(); - $salmon->post($data['salmonuri'], $data['entry']); + $salmon->post($data['salmonuri'], $data['entry'], $actor); // @fixme detect failure and attempt to resend return true; -- cgit v1.2.3-54-g00ecf From 493b5479c15f0e135054fd6b961e50e690077eda Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 1 Mar 2010 16:36:33 -0800 Subject: OStatus: support @example.com/path/to/profile mentions as well as @profile@example.com (latter requires webfinger, former doesn't) Plus misc warnings/notices cleanup in the submission path. --- actions/newnotice.php | 3 ++ plugins/OStatus/OStatusPlugin.php | 57 ++++++++++++++++++++++------- plugins/OStatus/classes/Ostatus_profile.php | 2 +- plugins/OStatus/lib/discovery.php | 2 +- plugins/OStatus/lib/xrd.php | 10 ++--- 5 files changed, 54 insertions(+), 20 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/actions/newnotice.php b/actions/newnotice.php index 78480abab..ed0fa1b2b 100644 --- a/actions/newnotice.php +++ b/actions/newnotice.php @@ -294,6 +294,9 @@ class NewnoticeAction extends Action if ($profile) { $content = '@' . $profile->nickname . ' '; } + } else { + // @fixme most of these bits above aren't being passed on above + $inreplyto = null; } $notice_form = new NoticeForm($this, '', $content, null, $inreplyto); diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 720dedd0a..4ffbba45b 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -222,31 +222,62 @@ class OStatusPlugin extends Plugin } /** - * + * Find any explicit remote mentions. Accepted forms: + * Webfinger: @user@example.com + * Profile link: @example.com/mublog/user + * @param Profile $sender (os user?) + * @param string $text input markup text + * @param array &$mention in/out param: set of found mentions + * @return boolean hook return value */ function onEndFindMentions($sender, $text, &$mentions) { - preg_match_all('/(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)/', + preg_match_all('!(?:^|\s+) + @( # Webfinger: + (?:\w+\.)*\w+ # user + @ # @ + (?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+ # domain + | # Profile: + (?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+ # domain + (?:/\w+)+ # /path1(/path2...) + )!x', $text, $wmatches, PREG_OFFSET_CAPTURE); foreach ($wmatches[1] as $wmatch) { - - $webfinger = $wmatch[0]; - - $this->log(LOG_INFO, "Checking Webfinger for address '$webfinger'"); - - $oprofile = Ostatus_profile::ensureWebfinger($webfinger); + $target = $wmatch[0]; + $oprofile = null; + + if (strpos($target, '/') === false) { + $this->log(LOG_INFO, "Checking Webfinger for address '$target'"); + try { + $oprofile = Ostatus_profile::ensureWebfinger($target); + } catch (Exception $e) { + $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage()); + } + } else { + $schemes = array('https', 'http'); + foreach ($schemes as $scheme) { + $url = "$scheme://$target"; + $this->log(LOG_INFO, "Checking profile address '$url'"); + try { + $oprofile = Ostatus_profile::ensureProfile($url); + if ($oprofile) { + continue; + } + } catch (Exception $e) { + $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage()); + } + } + } if (empty($oprofile)) { - - $this->log(LOG_INFO, "No Ostatus_profile found for address '$webfinger'"); - + $this->log(LOG_INFO, "No Ostatus_profile found for address '$target'"); } else { - $this->log(LOG_INFO, "Ostatus_profile found for address '$webfinger'"); + $this->log(LOG_INFO, "Ostatus_profile found for address '$target'"); if ($oprofile->isGroup()) { continue; @@ -261,7 +292,7 @@ class OStatusPlugin extends Plugin } } $mentions[] = array('mentioned' => array($profile), - 'text' => $wmatch[0], + 'text' => $target, 'position' => $pos, 'url' => $profile->profileurl); } diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 7b1aec76b..668a31df4 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -698,7 +698,7 @@ class Ostatus_profile extends Memcached_DataObject { // Get the canonical feed URI and check it $discover = new FeedDiscovery(); - if ($hints['feedurl']) { + if (isset($hints['feedurl'])) { $feeduri = $hints['feedurl']; $feeduri = $discover->discoverFromFeedURL($feeduri); } else { diff --git a/plugins/OStatus/lib/discovery.php b/plugins/OStatus/lib/discovery.php index 388df0a28..f8449b309 100644 --- a/plugins/OStatus/lib/discovery.php +++ b/plugins/OStatus/lib/discovery.php @@ -94,7 +94,7 @@ class Discovery $links = call_user_func(array($class, 'discover'), $uri); if ($link = Discovery::getService($links, Discovery::LRDD_REL)) { // Load the LRDD XRD - if ($link['template']) { + if (!empty($link['template'])) { $xrd_uri = Discovery::applyTemplate($link['template'], $uri); } else { $xrd_uri = $link['href']; diff --git a/plugins/OStatus/lib/xrd.php b/plugins/OStatus/lib/xrd.php index 16d27f8eb..48c1c88e1 100644 --- a/plugins/OStatus/lib/xrd.php +++ b/plugins/OStatus/lib/xrd.php @@ -156,20 +156,20 @@ class XRD function saveLink($doc, $link) { $link_element = $doc->createElement('Link'); - if ($link['rel']) { + if (!empty($link['rel'])) { $link_element->setAttribute('rel', $link['rel']); } - if ($link['type']) { + if (!empty($link['type'])) { $link_element->setAttribute('type', $link['type']); } - if ($link['href']) { + if (!empty($link['href'])) { $link_element->setAttribute('href', $link['href']); } - if ($link['template']) { + if (!empty($link['template'])) { $link_element->setAttribute('template', $link['template']); } - if (is_array($link['title'])) { + if (!empty($link['title']) && is_array($link['title'])) { foreach($link['title'] as $title) { $title = $doc->createElement('Title', $title); $link_element->appendChild($title); -- cgit v1.2.3-54-g00ecf From 88c33bbb57433b3f10407f590b49d11ec4d5d530 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 2 Mar 2010 12:35:27 -0500 Subject: Showing remote subscription button on the personal timeline --- plugins/OStatus/OStatusPlugin.php | 18 ++++++++++++++++++ plugins/OStatus/theme/base/css/ostatus.css | 4 ++++ 2 files changed, 22 insertions(+) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 4ffbba45b..64504547f 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -721,6 +721,24 @@ class OStatusPlugin extends Plugin return true; } + function onStartShowAllContent($action) + { + $user = common_current_user(); + if ($user && ($user->id == $action->profile->id)) { + $action->elementStart('div', 'entity_actions'); + $action->elementStart('p', array('id' => 'entity_remote_subscribe', + 'class' => 'entity_subscribe')); + $action->element('a', array('href' => common_local_url('ostatussub'), + 'class' => 'entity_remote_subscribe') + , _m('Subscribe to remote user')); + $action->elementEnd('p'); + $action->elementEnd('div'); + } + + return true; + } + + /** * Ping remote profiles with updates to this profile. * Salmon pings are queued for background processing. diff --git a/plugins/OStatus/theme/base/css/ostatus.css b/plugins/OStatus/theme/base/css/ostatus.css index e370810af..68470f4e4 100644 --- a/plugins/OStatus/theme/base/css/ostatus.css +++ b/plugins/OStatus/theme/base/css/ostatus.css @@ -46,3 +46,7 @@ float:right; .entity_remote_subscribe { float:right; } + +#all #entity_remote_subscribe { +margin-top:-52px; +} -- cgit v1.2.3-54-g00ecf From e3c2b8e0b50d750ad35cb313726fd2706684c09b Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 2 Mar 2010 12:41:18 -0500 Subject: Refactored remote subscribe action in OStatus --- plugins/OStatus/OStatusPlugin.php | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 64504547f..7b6d22acf 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -706,22 +706,19 @@ class OStatusPlugin extends Plugin function onStartShowSubscriptionsContent($action) { - $user = common_current_user(); - if ($user && ($user->id == $action->profile->id)) { - $action->elementStart('div', 'entity_actions'); - $action->elementStart('p', array('id' => 'entity_remote_subscribe', - 'class' => 'entity_subscribe')); - $action->element('a', array('href' => common_local_url('ostatussub'), - 'class' => 'entity_remote_subscribe') - , _m('Subscribe to remote user')); - $action->elementEnd('p'); - $action->elementEnd('div'); - } + $this->showEntityRemoteSubscribe($action); return true; } function onStartShowAllContent($action) + { + $this->showEntityRemoteSubscribe($action); + + return true; + } + + function showEntityRemoteSubscribe($action) { $user = common_current_user(); if ($user && ($user->id == $action->profile->id)) { @@ -734,11 +731,8 @@ class OStatusPlugin extends Plugin $action->elementEnd('p'); $action->elementEnd('div'); } - - return true; } - /** * Ping remote profiles with updates to this profile. * Salmon pings are queued for background processing. -- cgit v1.2.3-54-g00ecf From 1a4652b1ade6ea4de842835a7d2a011845a1cd62 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 3 Mar 2010 10:32:54 -0500 Subject: Changed label text for remote subscription to something similar. Given that this button will be used within context of subscriptions, 'New' works along with the '+' icon. --- plugins/OStatus/OStatusPlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 7b6d22acf..b5e6867c8 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -727,7 +727,7 @@ class OStatusPlugin extends Plugin 'class' => 'entity_subscribe')); $action->element('a', array('href' => common_local_url('ostatussub'), 'class' => 'entity_remote_subscribe') - , _m('Subscribe to remote user')); + , _m('New')); $action->elementEnd('p'); $action->elementEnd('div'); } -- cgit v1.2.3-54-g00ecf From ea10805e3f68f5781321e7c93144242478081a1f Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 3 Mar 2010 11:22:21 -0500 Subject: Moved the remote subscription button to subscription mini list --- plugins/OStatus/OStatusPlugin.php | 2 +- plugins/OStatus/theme/base/css/ostatus.css | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index b5e6867c8..eb777c6b2 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -711,7 +711,7 @@ class OStatusPlugin extends Plugin return true; } - function onStartShowAllContent($action) + function onEndShowSubscriptionsMiniList($action) { $this->showEntityRemoteSubscribe($action); diff --git a/plugins/OStatus/theme/base/css/ostatus.css b/plugins/OStatus/theme/base/css/ostatus.css index 13e30ef5d..40cdfcef1 100644 --- a/plugins/OStatus/theme/base/css/ostatus.css +++ b/plugins/OStatus/theme/base/css/ostatus.css @@ -41,8 +41,22 @@ min-width:96px; #entity_remote_subscribe { padding:0; float:right; +position:relative; } -#all #entity_remote_subscribe { -margin-top:-52px; +.section .entity_actions { +margin-bottom:0; +} + +.section #entity_remote_subscribe .entity_remote_subscribe { +border-color:#AAAAAA; +} + +.section #entity_remote_subscribe .dialogbox { +width:405px; +} + + +.aside #entity_subscriptions .more { +float:left; } -- cgit v1.2.3-54-g00ecf From 11750e832f994013b2fbce860bd24d24f49a14db Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 3 Mar 2010 12:02:10 -0500 Subject: Added remote join action for group profile --- plugins/OStatus/OStatusPlugin.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index eb777c6b2..014fb0b38 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -210,6 +210,26 @@ class OStatusPlugin extends Plugin return false; } + function onStartGroupSubscribe($output, $group) + { + $cur = common_current_user(); + + if (empty($cur)) { + // Add an OStatus subscribe + $output->elementStart('li', 'entity_subscribe'); + $url = common_local_url('ostatusinit', + array('nickname' => $group->nickname)); + $output->element('a', array('href' => $url, + 'class' => 'entity_remote_subscribe'), + _m('Join')); + + $output->elementEnd('li'); + } + + return false; + } + + /** * Check if we've got remote replies to send via Salmon. * -- cgit v1.2.3-54-g00ecf From 1e63fda6695c8ab06f2cd8181de26f03f7f7a5d4 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 3 Mar 2010 09:32:25 -0800 Subject: Clean up OStatus mentions finding; separate regexes keeps the code paths a bit clearer. Also switched to hitting HTTP profile first; as the common case it'll be faster. --- plugins/OStatus/OStatusPlugin.php | 86 +++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 43 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 7b6d22acf..f435d6283 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -233,70 +233,70 @@ class OStatusPlugin extends Plugin function onEndFindMentions($sender, $text, &$mentions) { - preg_match_all('!(?:^|\s+) - @( # Webfinger: - (?:\w+\.)*\w+ # user - @ # @ - (?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+ # domain - | # Profile: - (?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+ # domain - (?:/\w+)+ # /path1(/path2...) - )!x', + $matches = array(); + + // Webfinger matches: @user@example.com + if (preg_match_all('!(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)!', $text, $wmatches, - PREG_OFFSET_CAPTURE); - - foreach ($wmatches[1] as $wmatch) { - $target = $wmatch[0]; - $oprofile = null; - - if (strpos($target, '/') === false) { - $this->log(LOG_INFO, "Checking Webfinger for address '$target'"); + PREG_OFFSET_CAPTURE)) { + foreach ($wmatches[1] as $wmatch) { + list($target, $pos) = $wmatch; + $this->log(LOG_INFO, "Checking webfinger '$target'"); try { $oprofile = Ostatus_profile::ensureWebfinger($target); + if ($oprofile && !$oprofile->isGroup()) { + $profile = $oprofile->localProfile(); + $matches[$pos] = array('mentioned' => array($profile), + 'text' => $target, + 'position' => $pos, + 'url' => $profile->profileurl); + } } catch (Exception $e) { $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage()); } - } else { - $schemes = array('https', 'http'); + } + } + + // Profile matches: @example.com/mublog/user + if (preg_match_all('!(?:^|\s+)@((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)!', + $text, + $wmatches, + PREG_OFFSET_CAPTURE)) { + foreach ($wmatches[1] as $wmatch) { + list($target, $pos) = $wmatch; + $schemes = array('http', 'https'); foreach ($schemes as $scheme) { $url = "$scheme://$target"; $this->log(LOG_INFO, "Checking profile address '$url'"); try { $oprofile = Ostatus_profile::ensureProfile($url); - if ($oprofile) { - continue; + if ($oprofile && !$oprofile->isGroup()) { + $profile = $oprofile->localProfile(); + $matches[$pos] = array('mentioned' => array($profile), + 'text' => $target, + 'position' => $pos, + 'url' => $profile->profileurl); + break; } } catch (Exception $e) { $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage()); } } } + } - if (empty($oprofile)) { - $this->log(LOG_INFO, "No Ostatus_profile found for address '$target'"); - } else { - - $this->log(LOG_INFO, "Ostatus_profile found for address '$target'"); - - if ($oprofile->isGroup()) { - continue; - } - $profile = $oprofile->localProfile(); - - $pos = $wmatch[1]; - foreach ($mentions as $i => $other) { - // If we share a common prefix with a local user, override it! - if ($other['position'] == $pos) { - unset($mentions[$i]); - } - } - $mentions[] = array('mentioned' => array($profile), - 'text' => $target, - 'position' => $pos, - 'url' => $profile->profileurl); + foreach ($mentions as $i => $other) { + // If we share a common prefix with a local user, override it! + $pos = $other['position']; + if (isset($matches[$pos])) { + $mentions[$i] = $matches[$pos]; + unset($matches[$pos]); } } + foreach ($matches as $mention) { + $mentions[] = $mention; + } return true; } -- cgit v1.2.3-54-g00ecf From c8bdf3cacbff1e5ce92b4553480e44d9f0053158 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 3 Mar 2010 12:56:19 -0500 Subject: Added group subscription button to groups mini list --- plugins/OStatus/OStatusPlugin.php | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 014fb0b38..38f5e8d41 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -738,6 +738,13 @@ class OStatusPlugin extends Plugin return true; } + function onEndShowGroupsMiniList($action) + { + $this->showEntityRemoteSubscribe($action); + + return true; + } + function showEntityRemoteSubscribe($action) { $user = common_current_user(); -- cgit v1.2.3-54-g00ecf From 023b9bb00f096a532218b6689c1f035051653c8e Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 3 Mar 2010 13:30:14 -0500 Subject: Renamed subscribe button from New to Remote since it only does remote subscriptions at the moment. --- plugins/OStatus/OStatusPlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 9d5613b20..e371f72f9 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -754,7 +754,7 @@ class OStatusPlugin extends Plugin 'class' => 'entity_subscribe')); $action->element('a', array('href' => common_local_url('ostatussub'), 'class' => 'entity_remote_subscribe') - , _m('New')); + , _m('Remote')); $action->elementEnd('p'); $action->elementEnd('div'); } -- cgit v1.2.3-54-g00ecf From ccd0db1e0a928fe914c894966ecf2260964a68f0 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 3 Mar 2010 14:32:03 -0500 Subject: add remote subscribe button for not-logged-in users looking a profile list with local users in it --- plugins/OStatus/OStatusPlugin.php | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index e371f72f9..da7ca2fe2 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -111,11 +111,11 @@ class OStatusPlugin extends Plugin $acct = 'acct:'. $action->profile->nickname .'@'. common_config('site', 'server'); $url = common_local_url('xrd'); $url.= '?uri='. $acct; - + header('Link: <'.$url.'>; rel="'. Discovery::LRDD_REL.'"; type="application/xrd+xml"'); } } - + /** * Set up a PuSH hub link to our internal link for canonical timeline * Atom feeds for users and groups. @@ -229,7 +229,6 @@ class OStatusPlugin extends Plugin return false; } - /** * Check if we've got remote replies to send via Salmon. * @@ -587,7 +586,6 @@ class OStatusPlugin extends Plugin // Drop the PuSH subscription if there are no other subscribers. $oprofile->garbageCollect(); - $member = Profile::staticGet($user->id); $act = new Activity(); @@ -806,4 +804,28 @@ class OStatusPlugin extends Plugin return true; } + + function onStartProfileListItemActionElements($item) + { + if (!common_logged_in()) { + + $profileUser = User::staticGet('id', $item->profile->id); + + if (!empty($profileUser)) { + + $output = $item->out; + + // Add an OStatus subscribe + $output->elementStart('li', 'entity_subscribe'); + $url = common_local_url('ostatusinit', + array('nickname' => $profileUser->nickname)); + $output->element('a', array('href' => $url, + 'class' => 'entity_remote_subscribe'), + _m('Subscribe')); + $output->elementEnd('li'); + } + } + + return true; + } } -- cgit v1.2.3-54-g00ecf From 3c55edde39086918eea8c1adba5d01a026e230a3 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 3 Mar 2010 15:30:43 -0500 Subject: Showing the remote subscribe button on the user groups page --- plugins/OStatus/OStatusPlugin.php | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index da7ca2fe2..dc502e52c 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -729,6 +729,13 @@ class OStatusPlugin extends Plugin return true; } + function onStartShowUserGroupsContent($action) + { + $this->showEntityRemoteSubscribe($action); + + return true; + } + function onEndShowSubscriptionsMiniList($action) { $this->showEntityRemoteSubscribe($action); -- cgit v1.2.3-54-g00ecf From 219e15ac64b0ceefcd714af27b9bbb53123dbe71 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 3 Mar 2010 16:26:02 -0500 Subject: Returning true instead for group remote subscription. If not logged in, it gives the chance to use the logged in join/leave instead. --- plugins/OStatus/OStatusPlugin.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index dc502e52c..d28260a4a 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -216,17 +216,14 @@ class OStatusPlugin extends Plugin if (empty($cur)) { // Add an OStatus subscribe - $output->elementStart('li', 'entity_subscribe'); $url = common_local_url('ostatusinit', array('nickname' => $group->nickname)); $output->element('a', array('href' => $url, 'class' => 'entity_remote_subscribe'), _m('Join')); - - $output->elementEnd('li'); } - return false; + return true; } /** -- cgit v1.2.3-54-g00ecf From a42d1116db06539f2da4eb5332bbdbef5beda3fe Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 3 Mar 2010 13:40:26 -0800 Subject: Separate the UI paths for ostatussub and ostatusgroup. They'll redirect to each other transparently if they find you've put a remote entity of the other type. --- plugins/OStatus/OStatusPlugin.php | 12 +- plugins/OStatus/actions/ostatusgroup.php | 181 +++++++++++++++++++++++++++++++ plugins/OStatus/actions/ostatussub.php | 134 +++++++---------------- 3 files changed, 224 insertions(+), 103 deletions(-) create mode 100644 plugins/OStatus/actions/ostatusgroup.php (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index dc502e52c..b4b446a7f 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -51,8 +51,8 @@ class OStatusPlugin extends Plugin 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\.\/\:]+')); + $m->connect('main/ostatusgroup', + array('action' => 'ostatusgroup')); // PuSH actions $m->connect('main/push/hub', array('action' => 'pushhub')); @@ -731,7 +731,7 @@ class OStatusPlugin extends Plugin function onStartShowUserGroupsContent($action) { - $this->showEntityRemoteSubscribe($action); + $this->showEntityRemoteSubscribe($action, 'ostatusgroup'); return true; } @@ -745,19 +745,19 @@ class OStatusPlugin extends Plugin function onEndShowGroupsMiniList($action) { - $this->showEntityRemoteSubscribe($action); + $this->showEntityRemoteSubscribe($action, 'ostatusgroup'); return true; } - function showEntityRemoteSubscribe($action) + function showEntityRemoteSubscribe($action, $target='ostatussub') { $user = common_current_user(); if ($user && ($user->id == $action->profile->id)) { $action->elementStart('div', 'entity_actions'); $action->elementStart('p', array('id' => 'entity_remote_subscribe', 'class' => 'entity_subscribe')); - $action->element('a', array('href' => common_local_url('ostatussub'), + $action->element('a', array('href' => common_local_url($target), 'class' => 'entity_remote_subscribe') , _m('Remote')); $action->elementEnd('p'); diff --git a/plugins/OStatus/actions/ostatusgroup.php b/plugins/OStatus/actions/ostatusgroup.php new file mode 100644 index 000000000..4fcd0eb39 --- /dev/null +++ b/plugins/OStatus/actions/ostatusgroup.php @@ -0,0 +1,181 @@ +. + */ + +/** + * @package OStatusPlugin + * @maintainer Brion Vibber + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +/** + * Key UI methods: + * + * showInputForm() - form asking for a remote profile account or URL + * We end up back here on errors + * + * showPreviewForm() - surrounding form for preview-and-confirm + * preview() - display profile for a remote group + * + * success() - redirects to groups page on join + */ +class OStatusGroupAction extends OStatusSubAction +{ + protected $profile_uri; // provided acct: or URI of remote entity + protected $oprofile; // Ostatus_profile of remote entity, if valid + + + function validateRemoteProfile() + { + if (!$this->oprofile->isGroup()) { + // Send us to the user subscription form for conf + $target = common_local_url('ostatussub', array(), array('profile' => $this->profile_uri)); + common_redirect($target, 303); + } + } + + /** + * Show the initial form, when we haven't yet been given a valid + * remote profile. + */ + function showInputForm() + { + $user = common_current_user(); + + $profile = $user->getProfile(); + + $this->elementStart('form', array('method' => 'post', + 'id' => 'form_ostatus_sub', + 'class' => 'form_settings', + 'action' => $this->selfLink())); + + $this->hidden('token', common_session_token()); + + $this->elementStart('fieldset', array('id' => 'settings_feeds')); + + $this->elementStart('ul', 'form_data'); + $this->elementStart('li'); + $this->input('profile', + _m('Group profile URL'), + $this->profile_uri, + _m('Enter the profile URL of a group on another StatusNet site')); + $this->elementEnd('li'); + $this->elementEnd('ul'); + + $this->submit('validate', _m('Continue')); + + $this->elementEnd('fieldset'); + + $this->elementEnd('form'); + } + + /** + * Show a preview for a remote group's profile + * @return boolean true if we're ok to try joining + */ + function preview() + { + $oprofile = $this->oprofile; + $group = $oprofile->localGroup(); + + $cur = common_current_user(); + if ($cur->isMember($group)) { + $this->element('div', array('class' => 'error'), + _m("You are already a member of this group.")); + $ok = false; + } else { + $ok = true; + } + + $this->showEntity($group, + $group->getProfileUrl(), + $group->homepage_logo, + $group->description); + return $ok; + } + + /** + * Redirect on successful remote group join + */ + function success() + { + $cur = common_current_user(); + $url = common_local_url('usergroups', array('nickname' => $cur->nickname)); + common_redirect($url, 303); + } + + /** + * Attempt to finalize subscription. + * validateFeed must have been run first. + * + * Calls showForm on failure or success on success. + */ + function saveFeed() + { + $user = common_current_user(); + $group = $this->oprofile->localGroup(); + if ($user->isMember($group)) { + // TRANS: OStatus remote group subscription dialog error. + $this->showForm(_m('Already a member!')); + return; + } + + if (Event::handle('StartJoinGroup', array($group, $user))) { + $ok = Group_member::join($this->oprofile->group_id, $user->id); + if ($ok) { + Event::handle('EndJoinGroup', array($group, $user)); + $this->success(); + } else { + // TRANS: OStatus remote group subscription dialog error. + $this->showForm(_m('Remote group join failed!')); + } + } else { + // TRANS: OStatus remote group subscription dialog error. + $this->showForm(_m('Remote group join aborted!')); + } + } + + /** + * Title of the page + * + * @return string Title of the page + */ + + function title() + { + // TRANS: Page title for OStatus remote group join form + return _m('Confirm joining remote group'); + } + + /** + * Instructions for use + * + * @return instructions for use + */ + + function getInstructions() + { + return _m('You can subscribe to groups from other supported sites. Paste the group\'s profile URI below:'); + } + + function selfLink() + { + return common_local_url('ostatusgroup'); + } +} diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php index e318701a2..542f7e20c 100644 --- a/plugins/OStatus/actions/ostatussub.php +++ b/plugins/OStatus/actions/ostatussub.php @@ -1,7 +1,7 @@ elementStart('form', array('method' => 'post', 'id' => 'form_ostatus_sub', 'class' => 'form_settings', - 'action' => - common_local_url('ostatussub'))); + 'action' => $this->selfLink())); $this->hidden('token', common_session_token()); @@ -87,11 +84,7 @@ class OStatusSubAction extends Action */ function showPreviewForm() { - if ($this->oprofile->isGroup()) { - $ok = $this->previewGroup(); - } else { - $ok = $this->previewUser(); - } + $ok = $this->preview(); if (!$ok) { // @fixme maybe provide a cancel button or link back? return; @@ -104,7 +97,7 @@ class OStatusSubAction extends Action 'id' => 'form_ostatus_sub', 'class' => 'form_remote_authorize', 'action' => - common_local_url('ostatussub'))); + $this->selfLink())); $this->elementStart('fieldset'); $this->hidden('token', common_session_token()); $this->hidden('profile', $this->profile_uri); @@ -126,7 +119,7 @@ class OStatusSubAction extends Action * Show a preview for a remote user's profile * @return boolean true if we're ok to try subscribing */ - function previewUser() + function preview() { $oprofile = $this->oprofile; $profile = $oprofile->localProfile(); @@ -150,32 +143,6 @@ class OStatusSubAction extends Action return $ok; } - /** - * Show a preview for a remote group's profile - * @return boolean true if we're ok to try joining - */ - function previewGroup() - { - $oprofile = $this->oprofile; - $group = $oprofile->localGroup(); - - $cur = common_current_user(); - if ($cur->isMember($group)) { - $this->element('div', array('class' => 'error'), - _m("You are already a member of this group.")); - $ok = false; - } else { - $ok = true; - } - - $this->showEntity($group, - $group->getProfileUrl(), - $group->homepage_logo, - $group->description); - return $ok; - } - - function showEntity($entity, $profile, $avatar, $note) { $nickname = $entity->nickname; @@ -254,23 +221,13 @@ class OStatusSubAction extends Action /** * Redirect on successful remote user subscription */ - function successUser() + function success() { $cur = common_current_user(); $url = common_local_url('subscriptions', array('nickname' => $cur->nickname)); common_redirect($url, 303); } - /** - * Redirect on successful remote group join - */ - function successGroup() - { - $cur = common_current_user(); - $url = common_local_url('usergroups', array('nickname' => $cur->nickname)); - common_redirect($url, 303); - } - /** * Pull data for a remote profile and check if it's valid. * Fills out error UI string in $this->error @@ -278,16 +235,9 @@ class OStatusSubAction extends Action * * @return boolean */ - function validateFeed() + function pullRemoteProfile() { - $profile_uri = trim($this->arg('profile')); - - if ($profile_uri == '') { - $this->showForm(_m('Empty remote profile URL!')); - return; - } - $this->profile_uri = $profile_uri; - + $this->profile_uri = $this->trimmed('profile'); try { if (Validate::email($this->profile_uri)) { $this->oprofile = Ostatus_profile::ensureWebfinger($this->profile_uri); @@ -318,48 +268,34 @@ class OStatusSubAction extends Action return false; } + function validateRemoteProfile() + { + if ($this->oprofile->isGroup()) { + // Send us to the group subscription form for conf + $target = common_local_url('ostatusgroup', array(), array('profile' => $this->profile_uri)); + common_redirect($target, 303); + } + } + /** * Attempt to finalize subscription. * validateFeed must have been run first. * - * Calls showForm on failure or successUser/successGroup on success. + * Calls showForm on failure or success on success. */ function saveFeed() { // And subscribe the current user to the local profile $user = common_current_user(); - - if ($this->oprofile->isGroup()) { - $group = $this->oprofile->localGroup(); - if ($user->isMember($group)) { - // TRANS: OStatus remote group subscription dialog error. - $this->showForm(_m('Already a member!')); - return; - } - if (Event::handle('StartJoinGroup', array($group, $user))) { - $ok = Group_member::join($this->oprofile->group_id, $user->id); - if ($ok) { - Event::handle('EndJoinGroup', array($group, $user)); - $this->successGroup(); - } else { - // TRANS: OStatus remote group subscription dialog error. - $this->showForm(_m('Remote group join failed!')); - } - } else { - // TRANS: OStatus remote group subscription dialog error. - $this->showForm(_m('Remote group join aborted!')); - } + $local = $this->oprofile->localProfile(); + if ($user->isSubscribed($local)) { + // TRANS: OStatus remote subscription dialog error. + $this->showForm(_m('Already subscribed!')); + } elseif ($this->oprofile->subscribeLocalToRemote($user)) { + $this->success(); } else { - $local = $this->oprofile->localProfile(); - if ($user->isSubscribed($local)) { - // TRANS: OStatus remote subscription dialog error. - $this->showForm(_m('Already subscribed!')); - } elseif ($this->oprofile->subscribeLocalToRemote($user)) { - $this->successUser(); - } else { - // TRANS: OStatus remote subscription dialog error. - $this->showForm(_m('Remote subscription failed!')); - } + // TRANS: OStatus remote subscription dialog error. + $this->showForm(_m('Remote subscription failed!')); } } @@ -376,7 +312,9 @@ class OStatusSubAction extends Action return false; } - $this->profile_uri = $this->arg('profile'); + if ($this->pullRemoteProfile()) { + $this->validateRemoteProfile(); + } return true; } @@ -390,9 +328,6 @@ class OStatusSubAction extends Action if ($_SERVER['REQUEST_METHOD'] == 'POST') { $this->handlePost(); } else { - if ($this->arg('profile')) { - $this->validateFeed(); - } $this->showForm(); } } @@ -414,7 +349,7 @@ class OStatusSubAction extends Action return; } - if ($this->validateFeed()) { + if ($this->oprofile) { if ($this->arg('submit')) { $this->saveFeed(); return; @@ -500,4 +435,9 @@ class OStatusSubAction extends Action parent::showScripts(); $this->autofocus('feedurl'); } + + function selfLink() + { + return common_local_url('ostatussub'); + } } -- cgit v1.2.3-54-g00ecf From 628338265c1607000eaffc42418c63092b019720 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 3 Mar 2010 14:06:05 -0800 Subject: OStatus: fix up remote join button on group profiles --- plugins/OStatus/OStatusPlugin.php | 4 ++- plugins/OStatus/actions/ostatusinit.php | 51 ++++++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 11 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index ca6786a26..cc7e75976 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -49,6 +49,8 @@ class OStatusPlugin extends Plugin array('action' => 'ostatusinit')); $m->connect('main/ostatus?nickname=:nickname', array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+')); + $m->connect('main/ostatus?group=:group', + array('action' => 'ostatusinit'), array('group' => '[A-Za-z0-9_-]+')); $m->connect('main/ostatussub', array('action' => 'ostatussub')); $m->connect('main/ostatusgroup', @@ -217,7 +219,7 @@ class OStatusPlugin extends Plugin if (empty($cur)) { // Add an OStatus subscribe $url = common_local_url('ostatusinit', - array('nickname' => $group->nickname)); + array('group' => $group->nickname)); $output->element('a', array('href' => $url, 'class' => 'entity_remote_subscribe'), _m('Join')); diff --git a/plugins/OStatus/actions/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php index 8ba8dcdcc..1e45025b0 100644 --- a/plugins/OStatus/actions/ostatusinit.php +++ b/plugins/OStatus/actions/ostatusinit.php @@ -29,6 +29,7 @@ class OStatusInitAction extends Action { var $nickname; + var $group; var $profile; var $err; @@ -41,8 +42,9 @@ class OStatusInitAction extends Action return false; } - // Local user the remote wants to subscribe to + // Local user or group the remote wants to subscribe to $this->nickname = $this->trimmed('nickname'); + $this->group = $this->trimmed('group'); // Webfinger or profile URL of the remote user $this->profile = $this->trimmed('profile'); @@ -89,25 +91,33 @@ class OStatusInitAction extends Action function showContent() { + if ($this->group) { + $header = sprintf(_m('Join group %s'), $this->group); + $submit = _m('Join'); + } else { + $header = sprintf(_m('Subscribe to %s'), $this->nickname); + $submit = _m('Subscribe'); + } $this->elementStart('form', array('id' => 'form_ostatus_connect', 'method' => 'post', 'class' => 'form_settings', 'action' => common_local_url('ostatusinit'))); $this->elementStart('fieldset'); - $this->element('legend', null, sprintf(_m('Subscribe to %s'), $this->nickname)); + $this->element('legend', null, $header); $this->hidden('token', common_session_token()); $this->elementStart('ul', 'form_data'); $this->elementStart('li', array('id' => 'ostatus_nickname')); $this->input('nickname', _m('User nickname'), $this->nickname, _m('Nickname of the user you want to follow')); + $this->hidden('group', $this->group); // pass-through for magic links $this->elementEnd('li'); $this->elementStart('li', array('id' => 'ostatus_profile')); $this->input('profile', _m('Profile Account'), $this->profile, _m('Your account id (i.e. user@identi.ca)')); $this->elementEnd('li'); $this->elementEnd('ul'); - $this->submit('submit', _m('Subscribe')); + $this->submit('submit', $submit); $this->elementEnd('fieldset'); $this->elementEnd('form'); } @@ -131,19 +141,17 @@ class OStatusInitAction extends Action function connectWebfinger($acct) { - $disco = new Discovery; + $target_profile = $this->targetProfile(); + $disco = new Discovery; $result = $disco->lookup($acct); if (!$result) { $this->clientError(_m("Couldn't look up OStatus account profile.")); } + 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); - $target_profile = common_local_url('userbyid', array('id' => $user->id)); - $url = Discovery::applyTemplate($link['template'], $target_profile); common_log(LOG_INFO, "Sending remote subscriber $acct to $url"); common_redirect($url, 303); @@ -155,8 +163,7 @@ class OStatusInitAction extends Action function connectProfile($subscriber_profile) { - $user = User::staticGet('nickname', $this->nickname); - $target_profile = common_local_url('userbyid', array('id' => $user->id)); + $target_profile = $this->targetProfile(); // @fixme hack hack! We should look up the remote sub URL from XRDS $suburl = preg_replace('!^(.*)/(.*?)$!', '$1/main/ostatussub', $subscriber_profile); @@ -166,6 +173,30 @@ class OStatusInitAction extends Action common_redirect($suburl, 303); } + /** + * Build the canonical profile URI+URL of the requested user or group + */ + function targetProfile() + { + if ($this->nickname) { + $user = User::staticGet('nickname', $this->nickname); + if ($user) { + return common_local_url('userbyid', array('id' => $user->id)); + } else { + $this->clientError("No such user."); + } + } else if ($this->group) { + $group = Local_group::staticGet('id', $this->group); + if ($group) { + return common_local_url('groupbyid', array('id' => $group->group_id)); + } else { + $this->clientError("No such group."); + } + } else { + $this->clientError("No local user or group nickname provided."); + } + } + function title() { return _m('OStatus Connect'); -- cgit v1.2.3-54-g00ecf From 1c8399fde123fa2bc7b4ebf21fb323d215a9e7b4 Mon Sep 17 00:00:00 2001 From: James Walker Date: Wed, 3 Mar 2010 23:20:04 -0500 Subject: refactor xrd to allow for ownerxrd - xrd document for the site owner. introduced $config['webfinger']['owner'] for a custom xrd subject --- plugins/OStatus/OStatusPlugin.php | 4 +- plugins/OStatus/actions/ownerxrd.php | 56 +++++++++++++++++ plugins/OStatus/actions/userxrd.php | 48 +++++++++++++++ plugins/OStatus/actions/xrd.php | 113 ----------------------------------- plugins/OStatus/lib/xrdaction.php | 105 ++++++++++++++++++++++++++++++++ 5 files changed, 212 insertions(+), 114 deletions(-) create mode 100644 plugins/OStatus/actions/ownerxrd.php create mode 100644 plugins/OStatus/actions/userxrd.php delete mode 100644 plugins/OStatus/actions/xrd.php create mode 100644 plugins/OStatus/lib/xrdaction.php (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index cc7e75976..8baa857d8 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -44,7 +44,9 @@ class OStatusPlugin extends Plugin $m->connect('.well-known/host-meta', array('action' => 'hostmeta')); $m->connect('main/xrd', - array('action' => 'xrd')); + array('action' => 'userxrd')); + $m->connect('main/ownerxrd', + array('action' => 'ownerxrd')); $m->connect('main/ostatus', array('action' => 'ostatusinit')); $m->connect('main/ostatus?nickname=:nickname', diff --git a/plugins/OStatus/actions/ownerxrd.php b/plugins/OStatus/actions/ownerxrd.php new file mode 100644 index 000000000..9c141d8c7 --- /dev/null +++ b/plugins/OStatus/actions/ownerxrd.php @@ -0,0 +1,56 @@ +. + */ + +/** + * @package OStatusPlugin + * @maintainer James Walker + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +class OwnerxrdAction extends XrdAction +{ + + public $uri; + + function prepare($args) + { + $this->user = User::siteOwner(); + + if (!$this->user) { + $this->clientError(_('No such user.'), 404); + return false; + } + + $nick = common_canonical_nickname($this->user->nickname); + $acct = 'acct:' . $nick . '@' . common_config('site', 'server'); + + $this->xrd = new XRD(); + + // Check to see if a $config['webfinger']['owner'] has been set + if ($owner = common_config('webfinger', 'owner')) { + $this->xrd->subject = Discovery::normalize($owner); + $this->xrd->alias[] = $acct; + } else { + $this->xrd->subject = $acct; + } + + return true; + } +} diff --git a/plugins/OStatus/actions/userxrd.php b/plugins/OStatus/actions/userxrd.php new file mode 100644 index 000000000..414de9364 --- /dev/null +++ b/plugins/OStatus/actions/userxrd.php @@ -0,0 +1,48 @@ +. + */ + +/** + * @package OStatusPlugin + * @maintainer James Walker + */ + +if (!defined('STATUSNET')) { exit(1); } + +class UserxrdAction extends XrdAction +{ + + function prepare($args) + { + parent::prepare($args); + + $this->uri = $this->trimmed('uri'); + $acct = Discovery::normalize($this->uri); + + list($nick, $domain) = explode('@', substr(urldecode($acct), 5)); + $nick = common_canonical_nickname($nick); + + $this->user = User::staticGet('nickname', $nick); + if (!$this->user) { + $this->clientError(_('No such user.'), 404); + return false; + } + + return true; + } +} diff --git a/plugins/OStatus/actions/xrd.php b/plugins/OStatus/actions/xrd.php deleted file mode 100644 index f574b60ee..000000000 --- a/plugins/OStatus/actions/xrd.php +++ /dev/null @@ -1,113 +0,0 @@ -. - */ - -/** - * @package OStatusPlugin - * @maintainer James Walker - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - -class XrdAction extends Action -{ - - public $uri; - - function prepare($args) - { - parent::prepare($args); - - $this->uri = $this->trimmed('uri'); - - return true; - } - - function handle() - { - $acct = Discovery::normalize($this->uri); - - $xrd = new XRD(); - - list($nick, $domain) = explode('@', substr(urldecode($acct), 5)); - $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' => Discovery::PROFILEPAGE, - 'type' => 'text/html', - 'href' => common_profile_url($nick)); - - $xrd->links[] = array('rel' => Discovery::UPDATESFROM, - 'href' => common_local_url('ApiTimelineUser', - array('id' => $this->user->id, - 'format' => 'atom')), - 'type' => 'application/atom+xml'); - - // hCard - $xrd->links[] = array('rel' => Discovery::HCARD, - 'type' => 'text/html', - 'href' => common_local_url('hcard', array('nickname' => $nick))); - - // XFN - $xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11', - 'type' => 'text/html', - 'href' => common_profile_url($nick)); - // FOAF - $xrd->links[] = array('rel' => 'describedby', - 'type' => 'application/rdf+xml', - 'href' => common_local_url('foaf', - array('nickname' => $nick))); - - // Salmon - $salmon_url = common_local_url('usersalmon', - array('id' => $this->user->id)); - - $xrd->links[] = array('rel' => Salmon::NS_REPLIES, - 'href' => $salmon_url); - - $xrd->links[] = array('rel' => Salmon::NS_MENTIONS, - 'href' => $salmon_url); - - // Get this user's keypair - $magickey = Magicsig::staticGet('user_id', $this->user->id); - if (!$magickey) { - // No keypair yet, let's generate one. - $magickey = new Magicsig(); - $magickey->generate($this->user->id); - } - - $xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL, - 'href' => 'data:application/magic-public-key;'. $magickey->toString(false)); - - // TODO - finalize where the redirect should go on the publisher - $url = common_local_url('ostatussub') . '?profile={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/lib/xrdaction.php b/plugins/OStatus/lib/xrdaction.php new file mode 100644 index 000000000..6881292ad --- /dev/null +++ b/plugins/OStatus/lib/xrdaction.php @@ -0,0 +1,105 @@ +. + */ + +/** + * @package OStatusPlugin + * @maintainer James Walker + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +class XrdAction extends Action +{ + + public $uri; + + public $user; + + public $xrd; + + function handle() + { + $nick = $this->user->nickname; + + if (empty($this->xrd)) { + $xrd = new XRD(); + } else { + $xrd = $this->xrd; + } + + if (empty($xrd->subject)) { + $xrd->subject = Discovery::normalize($this->uri); + } + $xrd->alias[] = common_profile_url($nick); + $xrd->links[] = array('rel' => Discovery::PROFILEPAGE, + 'type' => 'text/html', + 'href' => common_profile_url($nick)); + + $xrd->links[] = array('rel' => Discovery::UPDATESFROM, + 'href' => common_local_url('ApiTimelineUser', + array('id' => $this->user->id, + 'format' => 'atom')), + 'type' => 'application/atom+xml'); + + // hCard + $xrd->links[] = array('rel' => Discovery::HCARD, + 'type' => 'text/html', + 'href' => common_local_url('hcard', array('nickname' => $nick))); + + // XFN + $xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11', + 'type' => 'text/html', + 'href' => common_profile_url($nick)); + // FOAF + $xrd->links[] = array('rel' => 'describedby', + 'type' => 'application/rdf+xml', + 'href' => common_local_url('foaf', + array('nickname' => $nick))); + + // Salmon + $salmon_url = common_local_url('usersalmon', + array('id' => $this->user->id)); + + $xrd->links[] = array('rel' => Salmon::NS_REPLIES, + 'href' => $salmon_url); + + $xrd->links[] = array('rel' => Salmon::NS_MENTIONS, + 'href' => $salmon_url); + + // Get this user's keypair + $magickey = Magicsig::staticGet('user_id', $this->user->id); + if (!$magickey) { + // No keypair yet, let's generate one. + $magickey = new Magicsig(); + $magickey->generate($this->user->id); + } + + $xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL, + 'href' => 'data:application/magic-public-key;'. $magickey->toString(false)); + + // TODO - finalize where the redirect should go on the publisher + $url = common_local_url('ostatussub') . '?profile={uri}'; + $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe', + 'template' => $url ); + + header('Content-type: text/xml'); + print $xrd->toXML(); + } + +} -- cgit v1.2.3-54-g00ecf From 849f5783c445934da370769310b3b0382bc5d303 Mon Sep 17 00:00:00 2001 From: James Walker Date: Thu, 4 Mar 2010 01:30:15 -0500 Subject: update xrd -> userxrd --- plugins/OStatus/OStatusPlugin.php | 2 +- plugins/OStatus/actions/hostmeta.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 8baa857d8..ad4f61389 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -113,7 +113,7 @@ class OStatusPlugin extends Plugin { if ($action instanceof ShowstreamAction) { $acct = 'acct:'. $action->profile->nickname .'@'. common_config('site', 'server'); - $url = common_local_url('xrd'); + $url = common_local_url('userxrd'); $url.= '?uri='. $acct; header('Link: <'.$url.'>; rel="'. Discovery::LRDD_REL.'"; type="application/xrd+xml"'); diff --git a/plugins/OStatus/actions/hostmeta.php b/plugins/OStatus/actions/hostmeta.php index 3d00b98ae..6d35ada6c 100644 --- a/plugins/OStatus/actions/hostmeta.php +++ b/plugins/OStatus/actions/hostmeta.php @@ -32,7 +32,7 @@ class HostMetaAction extends Action parent::handle(); $domain = common_config('site', 'server'); - $url = common_local_url('xrd'); + $url = common_local_url('userxrd'); $url.= '?uri={uri}'; $xrd = new XRD(); -- cgit v1.2.3-54-g00ecf From 45f11d9637c90c0bc7348c6bc5707516ea457d6c Mon Sep 17 00:00:00 2001 From: James Walker Date: Thu, 4 Mar 2010 12:02:01 -0500 Subject: adding plugin version to OStatus --- plugins/OStatus/OStatusPlugin.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'plugins/OStatus/OStatusPlugin.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index ad4f61389..bdcaae366 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -836,4 +836,17 @@ class OStatusPlugin extends Plugin return true; } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'OStatus', + 'version' => STATUSNET_VERSION, + 'author' => 'Evan Prodromou, James Walker, Brion Vibber, Zach Copley', + 'homepage' => 'http://status.net/wiki/Plugin:OStatus', + 'rawdescription' => + _m('Follow people across social networks that implement '. + 'OStatus.')); + + return true; + } } -- cgit v1.2.3-54-g00ecf