From dc09453a77f33c4dfdff306321ce93cf5fbd2d57 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 8 Feb 2010 11:06:03 -0800 Subject: First steps on converting FeedSub into the pub/sub basis for OStatus communications: * renamed FeedSub plugin to OStatus * now setting avatar on subscriptions * general fixes for subscription * integrated PuSH hub to handle only user timelines on canonical ID url; sends updates directly * set $config['feedsub']['nohub'] = true to test w/ foreign feeds that don't have hubs (won't actually receive updates though) * a few bits of code documentation * HMAC support for verified distributions (safest if sub setup is on HTTPS) And a couple core changes: * minimizing HTML output for exceptions in API requests to aid in debugging * fix for rel=self link in apitimelineuser when id given This does not not yet include any of the individual subscription management (Salmon notifications for sub/unsub, etc) nor a nice UI for user subscriptions. Needs some further cleanup to treat posts as status updates instead of link references. --- plugins/OStatus/actions/feedsubsettings.php | 258 ++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 plugins/OStatus/actions/feedsubsettings.php (limited to 'plugins/OStatus/actions/feedsubsettings.php') diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php new file mode 100644 index 000000000..4d5b7b60f --- /dev/null +++ b/plugins/OStatus/actions/feedsubsettings.php @@ -0,0 +1,258 @@ +. + */ + +/** + * @package FeedSubPlugin + * @maintainer Brion Vibber + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +class FeedSubSettingsAction extends ConnectSettingsAction +{ + protected $feedurl; + 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(); + + $fuser = null; + + $flink = Foreign_link::getByUserID($user->id, FEEDSUB_SERVICE); + + if (!empty($flink)) { + $fuser = $flink->getForeignUser(); + } + + $this->elementStart('form', array('method' => 'post', + 'id' => 'form_settings_feedsub', + 'class' => 'form_settings', + 'action' => + common_local_url('feedsubsettings'))); + + $this->hidden('token', common_session_token()); + + $this->elementStart('fieldset', array('id' => 'settings_feeds')); + + $this->elementStart('ul', 'form_data'); + $this->elementStart('li', array('id' => 'settings_twitter_login_button')); + $this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed')); + $this->elementEnd('li'); + $this->elementEnd('ul'); + + 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() + { + $feedurl = trim($this->arg('feedurl')); + + if ($feedurl == '') { + $this->showForm(_m('Empty feed URL!')); + return; + } + $this->feedurl = $feedurl; + + // Get the canonical feed URI and check it + try { + $discover = new FeedDiscovery(); + $uri = $discover->discoverFromURL($feedurl); + } catch (FeedSubBadURLException $e) { + $this->showForm(_m('Invalid URL or could not reach server.')); + return false; + } catch (FeedSubBadResponseException $e) { + $this->showForm(_m('Cannot read feed; server returned error.')); + return false; + } catch (FeedSubEmptyException $e) { + $this->showForm(_m('Cannot read feed; server returned an empty page.')); + return false; + } catch (FeedSubBadHTMLException $e) { + $this->showForm(_m('Bad HTML, could not find feed link.')); + return false; + } catch (FeedSubNoFeedException $e) { + $this->showForm(_m('Could not find a feed linked from this URL.')); + return false; + } catch (FeedSubUnrecognizedTypeException $e) { + $this->showForm(_m('Not a recognized feed type.')); + return false; + } catch (FeedSubException $e) { + // Any new ones we forgot about + $this->showForm(_m('Bad feed URL.')); + return false; + } + + $this->munger = $discover->feedMunger(); + $this->feedinfo = $this->munger->feedInfo(); + + if ($this->feedinfo->huburi == '' && !common_config('feedsub', 'nohub')) { + $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.')); + return false; + } + + return true; + } + + function saveFeed() + { + if ($this->validateFeed()) { + $this->preview = true; + $this->feedinfo = Feedinfo::ensureProfile($this->munger); + + // If not already in use, subscribe to updates via the hub + if ($this->feedinfo->sub_start) { + common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}"); + } else { + $ok = $this->feedinfo->subscribe(); + common_log(LOG_INFO, __METHOD__ . ": sub was $ok"); + if (!$ok) { + $this->showForm(_m('Feed subscription failed! Bad response from hub.')); + return; + } + } + + // And subscribe the current user to the local profile + $user = common_current_user(); + $profile = $this->feedinfo->getProfile(); + if (!$profile) { + throw new ServerException("Feed profile was not saved properly."); + } + + if ($user->isSubscribed($profile)) { + $this->showForm(_m('Already subscribed!')); + } elseif ($user->subscribeTo($profile)) { + $this->showForm(_m('Feed subscribed!')); + } else { + $this->showForm(_m('Feed subscription failed!')); + } + } + } + + function validateAndPreview() + { + if ($this->validateFeed()) { + $this->preview = true; + $this->showForm(_m('Previewing feed:')); + } + } + + function previewFeed() + { + $feedinfo = $this->munger->feedinfo(); + $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.'); + } + } + + function showScripts() + { + parent::showScripts(); + $this->autofocus('feedurl'); + } +} -- cgit v1.2.3-54-g00ecf From 8449256817f5a2bd7a7cac6bc04e4cb477d7dc49 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 9 Feb 2010 18:32:52 -0800 Subject: OStatus partial support for group subscriptions: * detection of group feeds is currently a nasty hack based on presence of '/groups/' in URL -- should use some property on the feed? * listing for the remote group is kinda cruddy; needs to be named more cleanly * still need to establish per-author profiles (easier once we have the updated Atom code in) * group delivery probably not right yet * saving of group messages still triggering some weird behavior Added support for since_id and max_id on group timeline feeds as a free extra. Enjoy! --- actions/apitimelinegroup.php | 2 +- actions/showgroup.php | 4 +- classes/Notice.php | 4 +- classes/User_group.php | 4 +- lib/util.php | 2 +- plugins/OStatus/OStatusPlugin.php | 4 +- plugins/OStatus/actions/feedsubsettings.php | 22 +++++--- plugins/OStatus/classes/Feedinfo.php | 55 ++++++++++++++++++-- plugins/OStatus/lib/feedmunger.php | 4 +- plugins/OStatus/lib/hubdistribqueuehandler.php | 70 +++++++++++++++++++++++--- 10 files changed, 140 insertions(+), 31 deletions(-) (limited to 'plugins/OStatus/actions/feedsubsettings.php') diff --git a/actions/apitimelinegroup.php b/actions/apitimelinegroup.php index af414c680..fd2ed9ff9 100644 --- a/actions/apitimelinegroup.php +++ b/actions/apitimelinegroup.php @@ -130,7 +130,7 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction case 'atom': $selfuri = common_root_url() . 'api/statusnet/groups/timeline/' . - $this->group->nickname . '.atom'; + $this->group->id . '.atom'; $this->showAtomTimeline( $this->notices, $title, diff --git a/actions/showgroup.php b/actions/showgroup.php index 8042a4951..eb1238902 100644 --- a/actions/showgroup.php +++ b/actions/showgroup.php @@ -330,13 +330,13 @@ class ShowgroupAction extends GroupDesignAction new Feed(Feed::RSS2, common_local_url('ApiTimelineGroup', array('format' => 'rss', - 'id' => $this->group->nickname)), + 'id' => $this->group->id)), sprintf(_('Notice feed for %s group (RSS 2.0)'), $this->group->nickname)), new Feed(Feed::ATOM, common_local_url('ApiTimelineGroup', array('format' => 'atom', - 'id' => $this->group->nickname)), + 'id' => $this->group->id)), sprintf(_('Notice feed for %s group (Atom)'), $this->group->nickname)), new Feed(Feed::FOAF, diff --git a/classes/Notice.php b/classes/Notice.php index fca1c599c..247440f29 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -783,7 +783,7 @@ class Notice extends Memcached_DataObject $result = $gi->insert(); - if (!result) { + if (!$result) { common_log_db_error($gi, 'INSERT', __FILE__); throw new ServerException(_('Problem saving group inbox.')); } @@ -917,7 +917,7 @@ class Notice extends Memcached_DataObject /** * Same calculation as saveGroups but without the saving * @fixme merge the functions - * @return array of Group objects + * @return array of Group_inbox objects */ function getGroups() { diff --git a/classes/User_group.php b/classes/User_group.php index c86eadf8f..1fbb50a6e 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -49,12 +49,12 @@ class User_group extends Memcached_DataObject array('id' => $this->id)); } - function getNotices($offset, $limit) + function getNotices($offset, $limit, $since_id=null, $max_id=null) { $ids = Notice::stream(array($this, '_streamDirect'), array(), 'user_group:notice_ids:' . $this->id, - $offset, $limit); + $offset, $limit, $since_id, $max_id); return Notice::getStreamByIds($ids); } diff --git a/lib/util.php b/lib/util.php index 00c21aeb2..a07fe49e3 100644 --- a/lib/util.php +++ b/lib/util.php @@ -697,7 +697,7 @@ function common_group_link($sender_id, $nickname) { $sender = Profile::staticGet($sender_id); $group = User_group::getForNickname($nickname); - if ($group && $sender->isMember($group)) { + if ($sender && $group && $sender->isMember($group)) { $attrs = array('href' => $group->permalink(), 'class' => 'url'); if (!empty($group->fullname)) { diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 60a4e3827..89b5c4caa 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -107,11 +107,11 @@ class OStatusPlugin extends Plugin /** * Set up a PuSH hub link to our internal link for canonical timeline - * Atom feeds for users. + * Atom feeds for users and groups. */ function onStartApiAtom(Action $action) { - if ($action instanceof ApiTimelineUserAction) { + if ($action instanceof ApiTimelineUserAction || $action instanceof ApiTimelineGroupAction) { $id = $action->arg('id'); if (strval(intval($id)) === strval($id)) { // Canonical form of id in URL? diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php index 4d5b7b60f..6f592bf5b 100644 --- a/plugins/OStatus/actions/feedsubsettings.php +++ b/plugins/OStatus/actions/feedsubsettings.php @@ -209,7 +209,7 @@ class FeedSubSettingsAction extends ConnectSettingsAction return; } } - + // And subscribe the current user to the local profile $user = common_current_user(); $profile = $this->feedinfo->getProfile(); @@ -217,12 +217,22 @@ class FeedSubSettingsAction extends ConnectSettingsAction throw new ServerException("Feed profile was not saved properly."); } - if ($user->isSubscribed($profile)) { - $this->showForm(_m('Already subscribed!')); - } elseif ($user->subscribeTo($profile)) { - $this->showForm(_m('Feed subscribed!')); + if ($this->feedinfo->isGroup()) { + if ($user->isMember($profile)) { + $this->showForm(_m('Already a member!')); + } elseif (Group_member::join($this->feedinfo->group_id, $user->id)) { + $this->showForm(_m('Joined remote group!')); + } else { + $this->showForm(_m('Remote group join failed!')); + } } else { - $this->showForm(_m('Feed subscription failed!')); + if ($user->isSubscribed($profile)) { + $this->showForm(_m('Already subscribed!')); + } elseif ($user->subscribeTo($profile)) { + $this->showForm(_m('Feed subscribed!')); + } else { + $this->showForm(_m('Feed subscription failed!')); + } } } } diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php index 107faf012..792ea6034 100644 --- a/plugins/OStatus/classes/Feedinfo.php +++ b/plugins/OStatus/classes/Feedinfo.php @@ -89,7 +89,8 @@ class Feedinfo extends Memcached_DataObject function table() { return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, - 'profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'profile_id' => DB_DATAOBJECT_INT, + 'group_id' => DB_DATAOBJECT_INT, 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, 'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, 'huburi' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, @@ -111,7 +112,9 @@ class Feedinfo extends Memcached_DataObject /*extra*/ null, /*auto_increment*/ true), new ColumnDef('profile_id', 'integer', - null, false), + null, true), + new ColumnDef('group_id', 'integer', + null, true), new ColumnDef('feeduri', 'varchar', 255, false, 'UNI'), new ColumnDef('homeuri', 'varchar', @@ -176,6 +179,7 @@ class Feedinfo extends Memcached_DataObject /** * @param FeedMunger $munger + * @param boolean $isGroup is this a group record? * @return Feedinfo */ public static function ensureProfile($munger) @@ -217,6 +221,22 @@ class Feedinfo extends Memcached_DataObject } $feedinfo->profile_id = $profile->id; + if ($feedinfo->isGroup()) { + $group = new User_group(); + $group->nickname = $profile->nickname . '@remote'; // @fixme + $group->fullname = $profile->fullname; + $group->homepage = $profile->homepage; + $group->location = $profile->location; + $group->created = $profile->created; + $group->insert(); + + if ($avatar) { + $group->setOriginal($filename); + } + + $feedinfo->group_id = $group->id; + } + $result = $feedinfo->insert(); if (empty($result)) { throw new FeedDBException($feedinfo); @@ -231,6 +251,14 @@ class Feedinfo extends Memcached_DataObject return $feedinfo; } + /** + * Damn dirty hack! + */ + function isGroup() + { + return (strpos($this->feeduri, '/groups/') !== false); + } + /** * Send a subscription request to the hub for this feed. * The hub will later send us a confirmation POST to /feedsub/callback. @@ -325,17 +353,34 @@ class Feedinfo extends Memcached_DataObject $dupe = new Notice(); $dupe->uri = $notice->uri; if ($dupe->find(true)) { + // @fixme we might have to do individual and group delivery separately! common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}"); continue; } - + if (Event::handle('StartNoticeSave', array(&$notice))) { $id = $notice->insert(); Event::handle('EndNoticeSave', array($notice)); } - $notice->addToInboxes(); - common_log(LOG_INFO, __METHOD__ . ": saved notice {$notice->id} for entry $index of update to \"{$this->feeduri}\""); + + common_log(LOG_DEBUG, "going to check group delivery..."); + if ($this->group_id) { + $group = User_group::staticGet($this->group_id); + if ($group) { + common_log(LOG_INFO, __METHOD__ . ": saving to local shadow group $group->id $group->nickname"); + $groups = array($group); + } else { + common_log(LOG_INFO, __METHOD__ . ": lost the local shadow group?"); + } + } else { + common_log(LOG_INFO, __METHOD__ . ": no local shadow groups"); + $groups = array(); + } + common_log(LOG_DEBUG, "going to add to inboxes..."); + $notice->addToInboxes($groups, array()); + common_log(LOG_DEBUG, "added to inboxes."); + $hits++; } if ($hits == 0) { diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php index cbaec6775..5dce95342 100644 --- a/plugins/OStatus/lib/feedmunger.php +++ b/plugins/OStatus/lib/feedmunger.php @@ -203,7 +203,7 @@ class FeedMunger if (!$entry) { return null; } - + if ($preview) { $notice = new FeedSubPreviewNotice($this->profile(true)); $notice->id = -1; @@ -221,7 +221,7 @@ class FeedMunger $notice->uri = $link; $notice->url = $link; $notice->content = $this->noticeFromEntry($entry); - $notice->rendered = common_render_content($notice->content, $notice); + $notice->rendered = common_render_content($notice->content, $notice); // @fixme this is failing on group posts $notice->created = common_sql_date($entry->updated); // @fixme $notice->is_local = Notice::GATEWAY; $notice->source = 'feed'; diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/hubdistribqueuehandler.php index 126f1355f..189ccbedf 100644 --- a/plugins/OStatus/lib/hubdistribqueuehandler.php +++ b/plugins/OStatus/lib/hubdistribqueuehandler.php @@ -34,6 +34,14 @@ class HubDistribQueueHandler extends QueueHandler { assert($notice instanceof Notice); + $this->pushUser($notice); + foreach ($notice->getGroups() as $group) { + $this->pushGroup($notice, $group->group_id); + } + } + + 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 @@ -43,20 +51,42 @@ class HubDistribQueueHandler extends QueueHandler $sub = new HubSub(); $sub->topic = $feed; if ($sub->find()) { - common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $feed"); - $qm = QueueManager::get(); $atom = $this->userFeedForNotice($notice); - while ($sub->fetch()) { - common_log(LOG_INFO, "Prepping PuSH distribution to $sub->callback for $feed"); - $data = array('sub' => clone($sub), - 'atom' => $atom); - $qm->enqueue($data, 'hubout'); - } + $this->pushFeeds($atom, $sub); } else { common_log(LOG_INFO, "No PuSH subscribers for $feed"); } } + function pushGroup($notice, $group_id) + { + $feed = common_local_url('ApiTimelineGroup', + array('id' => $group_id, + 'format' => 'atom')); + $sub = new HubSub(); + $sub->topic = $feed; + if ($sub->find()) { + common_log(LOG_INFO, "Building PuSH feed for $feed"); + $atom = $this->groupFeedForNotice($group_id, $notice); + $this->pushFeeds($atom, $sub); + } else { + common_log(LOG_INFO, "No PuSH subscribers for $feed"); + } + } + + + function pushFeeds($atom, $sub) + { + common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $sub->topic"); + $qm = QueueManager::get(); + while ($sub->fetch()) { + common_log(LOG_INFO, "Prepping PuSH distribution to $sub->callback for $sub->topic"); + $data = array('sub' => clone($sub), + 'atom' => $atom); + $qm->enqueue($data, 'hubout'); + } + } + /** * Build a single-item version of the sending user's Atom feed. * @param Notice $notice @@ -83,5 +113,29 @@ class HubDistribQueueHandler extends QueueHandler common_log(LOG_DEBUG, $feed); return $feed; } + + function groupFeedForNotice($group_id, $notice) + { + // @fixme this feels VERY hacky... + // should probably be a cleaner way to do it + + ob_start(); + $api = new ApiTimelineGroupAction(); + $args = array('id' => $group_id, + 'format' => 'atom', + 'max_id' => $notice->id, + 'since_id' => $notice->id - 1); + $api->prepare($args); + $api->handle($args); + $feed = ob_get_clean(); + + // ...and override the content-type back to something normal... eww! + // hope there's no other headers that got set while we weren't looking. + header('Content-Type: text/html; charset=utf-8'); + + common_log(LOG_DEBUG, $feed); + return $feed; + } + } -- cgit v1.2.3-54-g00ecf From 8e6b52e8994ce9a3180554f999bdc89b414fc892 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Feb 2010 00:22:16 +0000 Subject: OStatus: renamed feedinfo table to ostatus_profile -- will cover remote ostatus people and groups whether a subscription's active or not (maintains identity over unsub/resub, and between subscribers and subscribees) --- plugins/OStatus/OStatusPlugin.php | 6 +- plugins/OStatus/actions/feedsubsettings.php | 20 +- plugins/OStatus/actions/ostatussub.php | 16 +- plugins/OStatus/actions/pushcallback.php | 22 +- plugins/OStatus/classes/Feedinfo.php | 408 --------------------------- plugins/OStatus/classes/Ostatus_profile.php | 415 ++++++++++++++++++++++++++++ plugins/OStatus/lib/feedmunger.php | 18 +- 7 files changed, 455 insertions(+), 450 deletions(-) delete mode 100644 plugins/OStatus/classes/Feedinfo.php create mode 100644 plugins/OStatus/classes/Ostatus_profile.php (limited to 'plugins/OStatus/actions/feedsubsettings.php') diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index c0f9dadc4..8444c3d73 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -251,14 +251,14 @@ class OStatusPlugin extends Plugin */ function onEndUnsubscribe($user, $other) { - $feed = Feedinfo::staticGet('profile_id', $other->id); + $profile = Ostatus_profile::staticGet('profile_id', $other->id); if ($feed) { $sub = new Subscription(); $sub->subscribed = $other->id; $sub->limit(1); if (!$sub->find(true)) { common_log(LOG_INFO, "Unsubscribing from now-unused feed $feed->feeduri on hub $feed->huburi"); - $feed->unsubscribe(); + $profile->unsubscribe(); } } return true; @@ -269,7 +269,7 @@ class OStatusPlugin extends Plugin */ function onCheckSchema() { $schema = Schema::get(); - $schema->ensureTable('feedinfo', Feedinfo::schemaDef()); + $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef()); $schema->ensureTable('hubsub', HubSub::schemaDef()); return true; } diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php index 6f592bf5b..af8bf4d25 100644 --- a/plugins/OStatus/actions/feedsubsettings.php +++ b/plugins/OStatus/actions/feedsubsettings.php @@ -182,9 +182,9 @@ class FeedSubSettingsAction extends ConnectSettingsAction } $this->munger = $discover->feedMunger(); - $this->feedinfo = $this->munger->feedInfo(); + $this->profile = $this->munger->ostatusProfile(); - if ($this->feedinfo->huburi == '' && !common_config('feedsub', 'nohub')) { + if ($this->profile->huburi == '' && !common_config('feedsub', 'nohub')) { $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.')); return false; } @@ -196,13 +196,13 @@ class FeedSubSettingsAction extends ConnectSettingsAction { if ($this->validateFeed()) { $this->preview = true; - $this->feedinfo = Feedinfo::ensureProfile($this->munger); + $this->profile = Ostatus_profile::ensureProfile($this->munger); // If not already in use, subscribe to updates via the hub - if ($this->feedinfo->sub_start) { - common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}"); + if ($this->profile->sub_start) { + common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}"); } else { - $ok = $this->feedinfo->subscribe(); + $ok = $this->profile->subscribe(); common_log(LOG_INFO, __METHOD__ . ": sub was $ok"); if (!$ok) { $this->showForm(_m('Feed subscription failed! Bad response from hub.')); @@ -212,15 +212,15 @@ class FeedSubSettingsAction extends ConnectSettingsAction // And subscribe the current user to the local profile $user = common_current_user(); - $profile = $this->feedinfo->getProfile(); + $profile = $this->profile->getLocalProfile(); if (!$profile) { throw new ServerException("Feed profile was not saved properly."); } - if ($this->feedinfo->isGroup()) { + if ($this->profile->isGroup()) { if ($user->isMember($profile)) { $this->showForm(_m('Already a member!')); - } elseif (Group_member::join($this->feedinfo->group_id, $user->id)) { + } elseif (Group_member::join($this->profile->group_id, $user->id)) { $this->showForm(_m('Joined remote group!')); } else { $this->showForm(_m('Remote group join failed!')); @@ -247,7 +247,7 @@ class FeedSubSettingsAction extends ConnectSettingsAction function previewFeed() { - $feedinfo = $this->munger->feedinfo(); + $profile = $this->munger->ostatusProfile(); $notice = $this->munger->notice(0, true); // preview if ($notice) { diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php index ffc4ae8df..9774286fd 100644 --- a/plugins/OStatus/actions/ostatussub.php +++ b/plugins/OStatus/actions/ostatussub.php @@ -164,9 +164,9 @@ class OStatusSubAction extends Action } $this->munger = $discover->feedMunger(); - $this->feedinfo = $this->munger->feedInfo(); + $this->profile = $this->munger->ostatusProfile(); - if ($this->feedinfo->huburi == '') { + if ($this->profile->huburi == '') { $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.')); return false; } @@ -178,13 +178,13 @@ class OStatusSubAction extends Action { if ($this->validateFeed()) { $this->preview = true; - $this->feedinfo = Feedinfo::ensureProfile($this->munger); + $this->profile = Ostatus_profile::ensureProfile($this->munger); // If not already in use, subscribe to updates via the hub - if ($this->feedinfo->sub_start) { - common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}"); + if ($this->profile->sub_start) { + common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}"); } else { - $ok = $this->feedinfo->subscribe(); + $ok = $this->profile->subscribe(); common_log(LOG_INFO, __METHOD__ . ": sub was $ok"); if (!$ok) { $this->showForm(_m('Feed subscription failed! Bad response from hub.')); @@ -194,7 +194,7 @@ class OStatusSubAction extends Action // And subscribe the current user to the local profile $user = common_current_user(); - $profile = $this->feedinfo->getProfile(); + $profile = $this->profile->getProfile(); if ($user->isSubscribed($profile)) { $this->showForm(_m('Already subscribed!')); @@ -209,7 +209,7 @@ class OStatusSubAction extends Action function previewFeed() { - $feedinfo = $this->munger->feedinfo(); + $profile = $this->munger->ostatusProfile(); $notice = $this->munger->notice(0, true); // preview if ($notice) { diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php index 471d079ab..a446593ff 100644 --- a/plugins/OStatus/actions/pushcallback.php +++ b/plugins/OStatus/actions/pushcallback.php @@ -48,9 +48,9 @@ class PushCallbackAction extends Action throw new ServerException('Empty or invalid feed id', 400); } - $feedinfo = Feedinfo::staticGet('id', $feedid); - if (!$feedinfo) { - throw new ServerException('Unknown feed id ' . $feedid, 400); + $profile = Ostatus_profile::staticGet('id', $feedid); + if (!$profile) { + throw new ServerException('Unknown OStatus/PuSH feed id ' . $feedid, 400); } $hmac = ''; @@ -59,7 +59,7 @@ class PushCallbackAction extends Action } $post = file_get_contents('php://input'); - $feedinfo->postUpdates($post, $hmac); + $profile->postUpdates($post, $hmac); } /** @@ -78,8 +78,8 @@ class PushCallbackAction extends Action throw new ServerException("Bogus hub callback: bad mode", 404); } - $feedinfo = Feedinfo::staticGet('feeduri', $topic); - if (!$feedinfo) { + $profile = Ostatus_profile::staticGet('feeduri', $topic); + if (!$profile) { common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic"); throw new ServerException("Bogus hub callback: unknown feed", 404); } @@ -93,16 +93,16 @@ class PushCallbackAction extends Action // OK! if ($mode == 'subscribe') { common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); - $feedinfo->sub_start = common_sql_date(time()); + $profile->sub_start = common_sql_date(time()); if ($lease_seconds > 0) { - $feedinfo->sub_end = common_sql_date(time() + $lease_seconds); + $profile->sub_end = common_sql_date(time() + $lease_seconds); } else { - $feedinfo->sub_end = null; + $profile->sub_end = null; } - $feedinfo->update(); + $profile->update(); } else { common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic"); - $feedinfo->delete(); + $profile->delete(); } print $challenge; diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php deleted file mode 100644 index 5b8a9039a..000000000 --- a/plugins/OStatus/classes/Feedinfo.php +++ /dev/null @@ -1,408 +0,0 @@ -. - */ - -/** - * @package FeedSubPlugin - * @maintainer Brion Vibber - */ - -/* -PuSH subscription flow: - - $feedinfo->subscribe() - generate random verification token - save to verify_token - sends a sub request to the hub... - - feedsub/callback - hub sends confirmation back to us via GET - We verify the request, then echo back the challenge. - On our end, we save the time we subscribed and the lease expiration - - feedsub/callback - hub sends us updates via POST - -*/ - -class FeedDBException extends FeedSubException -{ - public $obj; - - function __construct($obj) - { - parent::__construct('Database insert failure'); - $this->obj = $obj; - } -} - -class Feedinfo extends Memcached_DataObject -{ - public $__table = 'feedinfo'; - - public $id; - public $profile_id; - - public $feeduri; - public $homeuri; - public $huburi; - - // PuSH subscription data - public $secret; - public $verify_token; - public $sub_start; - public $sub_end; - - public $created; - public $lastupdate; - - - public /*static*/ function staticGet($k, $v=null) - { - return parent::staticGet(__CLASS__, $k, $v); - } - - /** - * return table definition for DB_DataObject - * - * DB_DataObject needs to know something about the table to manipulate - * instances. This method provides all the DB_DataObject needs to know. - * - * @return array array of column definitions - */ - - function table() - { - return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, - 'profile_id' => DB_DATAOBJECT_INT, - 'group_id' => DB_DATAOBJECT_INT, - 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, - 'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, - 'huburi' => DB_DATAOBJECT_STR, - 'secret' => DB_DATAOBJECT_STR, - 'verify_token' => DB_DATAOBJECT_STR, - 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, - 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, - 'salmonuri' => DB_DATAOBJECT_STR, - 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, - 'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); - } - - static function schemaDef() - { - return array(new ColumnDef('id', 'integer', - /*size*/ null, - /*nullable*/ false, - /*key*/ 'PRI', - /*default*/ '0', - /*extra*/ null, - /*auto_increment*/ true), - new ColumnDef('profile_id', 'integer', - null, true, 'UNI'), - new ColumnDef('group_id', 'integer', - null, true, 'UNI'), - new ColumnDef('feeduri', 'varchar', - 255, false, 'UNI'), - new ColumnDef('homeuri', 'varchar', - 255, false), - new ColumnDef('huburi', 'text', - null, true), - new ColumnDef('verify_token', 'varchar', - 32, true), - new ColumnDef('secret', 'varchar', - 64, true), - new ColumnDef('sub_start', 'datetime', - null, true), - new ColumnDef('sub_end', 'datetime', - null, true), - new ColumnDef('salmonuri', 'text', - null, true), - new ColumnDef('created', 'datetime', - null, false), - new ColumnDef('lastupdate', 'datetime', - null, false)); - } - - /** - * return key definitions for DB_DataObject - * - * DB_DataObject needs to know about keys that the table has; this function - * defines them. - * - * @return array key definitions - */ - - function keys() - { - return array_keys($this->keyTypes()); - } - - /** - * return key definitions for Memcached_DataObject - * - * Our caching system uses the same key definitions, but uses a different - * method to get them. - * - * @return array key definitions - */ - - function keyTypes() - { - return array('id' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U'); - } - - function sequenceKey() - { - return array('id', true, false); - } - - /** - * Fetch the StatusNet-side profile for this feed - * @return Profile - */ - public function getProfile() - { - return Profile::staticGet('id', $this->profile_id); - } - - /** - * @param FeedMunger $munger - * @param boolean $isGroup is this a group record? - * @return Feedinfo - */ - public static function ensureProfile($munger) - { - $feedinfo = $munger->feedinfo(); - - $current = self::staticGet('feeduri', $feedinfo->feeduri); - if ($current) { - // @fixme we should probably update info as necessary - return $current; - } - - $feedinfo->query('BEGIN'); - - // Awful hack! Awful hack! - $feedinfo->verify = common_good_rand(16); - $feedinfo->secret = common_good_rand(32); - - try { - $profile = $munger->profile(); - $result = $profile->insert(); - if (empty($result)) { - throw new FeedDBException($profile); - } - - $avatar = $munger->getAvatar(); - if ($avatar) { - // @fixme this should be better encapsulated - // ripped from oauthstore.php (for old OMB client) - $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); - copy($avatar, $temp_filename); - $imagefile = new ImageFile($profile->id, $temp_filename); - $filename = Avatar::filename($profile->id, - image_type_to_extension($imagefile->type), - null, - common_timestamp()); - rename($temp_filename, Avatar::path($filename)); - $profile->setOriginal($filename); - } - - $feedinfo->profile_id = $profile->id; - if ($feedinfo->isGroup()) { - $group = new User_group(); - $group->nickname = $profile->nickname . '@remote'; // @fixme - $group->fullname = $profile->fullname; - $group->homepage = $profile->homepage; - $group->location = $profile->location; - $group->created = $profile->created; - $group->insert(); - - if ($avatar) { - $group->setOriginal($filename); - } - - $feedinfo->group_id = $group->id; - } - - $result = $feedinfo->insert(); - if (empty($result)) { - throw new FeedDBException($feedinfo); - } - - $feedinfo->query('COMMIT'); - } catch (FeedDBException $e) { - common_log_db_error($e->obj, 'INSERT', __FILE__); - $feedinfo->query('ROLLBACK'); - return false; - } - return $feedinfo; - } - - /** - * Damn dirty hack! - */ - function isGroup() - { - return (strpos($this->feeduri, '/groups/') !== false); - } - - /** - * Send a subscription request to the hub for this feed. - * The hub will later send us a confirmation POST to /main/push/callback. - * - * @return bool true on success, false on failure - */ - public function subscribe($mode='subscribe') - { - if (common_config('feedsub', 'nohub')) { - // Fake it! We're just testing remote feeds w/o hubs. - return true; - } - // @fixme use the verification token - #$token = md5(mt_rand() . ':' . $this->feeduri); - #$this->verify_token = $token; - #$this->update(); // @fixme - try { - $callback = common_local_url('pushcallback', array('feed' => $this->id)); - $headers = array('Content-Type: application/x-www-form-urlencoded'); - $post = array('hub.mode' => $mode, - 'hub.callback' => $callback, - 'hub.verify' => 'async', - 'hub.verify_token' => $this->verify_token, - 'hub.secret' => $this->secret, - //'hub.lease_seconds' => 0, - 'hub.topic' => $this->feeduri); - $client = new HTTPClient(); - $response = $client->post($this->huburi, $headers, $post); - $status = $response->getStatus(); - if ($status == 202) { - common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback'); - return true; - } else if ($status == 204) { - common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified'); - return true; - } else if ($status >= 200 && $status < 300) { - common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody()); - return false; - } else { - common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody()); - return false; - } - } catch (Exception $e) { - // wtf! - common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri"); - return false; - } - } - - /** - * Send an unsubscription request to the hub for this feed. - * The hub will later send us a confirmation POST to /main/push/callback. - * - * @return bool true on success, false on failure - */ - public function unsubscribe() { - return $this->subscribe('unsubscribe'); - } - - /** - * Read and post notices for updates from the feed. - * Currently assumes that all items in the feed are new, - * coming from a PuSH hub. - * - * @param string $xml source of Atom or RSS feed - * @param string $hmac X-Hub-Signature header, if present - */ - public function postUpdates($xml, $hmac) - { - common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml"); - - if ($this->secret) { - if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) { - $their_hmac = strtolower($matches[1]); - $our_hmac = hash_hmac('sha1', $xml, $this->secret); - if ($their_hmac !== $our_hmac) { - common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac"); - return; - } - } else { - common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'"); - return; - } - } else if ($hmac) { - common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'"); - return; - } - - require_once "XML/Feed/Parser.php"; - $feed = new XML_Feed_Parser($xml, false, false, true); - $munger = new FeedMunger($feed); - - $hits = 0; - foreach ($feed as $index => $entry) { - // @fixme this might sort in wrong order if we get multiple updates - - $notice = $munger->notice($index); - - // Double-check for oldies - // @fixme this could explode horribly for multiple feeds on a blog. sigh - $dupe = new Notice(); - $dupe->uri = $notice->uri; - if ($dupe->find(true)) { - common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}"); - continue; - } - - // @fixme need to ensure that groups get handled correctly - $saved = Notice::saveNew($notice->profile_id, - $notice->content, - 'ostatus', - array('is_local' => Notice::REMOTE_OMB, - 'uri' => $notice->uri, - 'lat' => $notice->lat, - 'lon' => $notice->lon, - 'location_ns' => $notice->location_ns, - 'location_id' => $notice->location_id)); - - /* - common_log(LOG_DEBUG, "going to check group delivery..."); - if ($this->group_id) { - $group = User_group::staticGet($this->group_id); - if ($group) { - common_log(LOG_INFO, __METHOD__ . ": saving to local shadow group $group->id $group->nickname"); - $groups = array($group); - } else { - common_log(LOG_INFO, __METHOD__ . ": lost the local shadow group?"); - } - } else { - common_log(LOG_INFO, __METHOD__ . ": no local shadow groups"); - $groups = array(); - } - common_log(LOG_DEBUG, "going to add to inboxes..."); - $notice->addToInboxes($groups, array()); - common_log(LOG_DEBUG, "added to inboxes."); - */ - - $hits++; - } - if ($hits == 0) { - common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml"); - } - } -} diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php new file mode 100644 index 000000000..748ecce18 --- /dev/null +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -0,0 +1,415 @@ +. + */ + +/** + * @package FeedSubPlugin + * @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 $profile_id; + public $group_id; + + public $feeduri; + public $homeuri; + + // PuSH subscription data + public $huburi; + public $secret; + public $verify_token; + public $sub_state; // subscribe, active, unsubscribe + public $sub_start; + public $sub_end; + + public $salmonuri; + + public $created; + public $lastupdate; + + + public /*static*/ function staticGet($k, $v=null) + { + return parent::staticGet(__CLASS__, $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'profile_id' => DB_DATAOBJECT_INT, + 'group_id' => DB_DATAOBJECT_INT, + 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'huburi' => DB_DATAOBJECT_STR, + 'secret' => DB_DATAOBJECT_STR, + 'verify_token' => DB_DATAOBJECT_STR, + 'sub_state' => DB_DATAOBJECT_STR, + 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME, + 'salmonuri' => DB_DATAOBJECT_STR, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, + 'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); + } + + static function schemaDef() + { + return array(new ColumnDef('id', 'integer', + /*size*/ null, + /*nullable*/ false, + /*key*/ 'PRI', + /*default*/ '0', + /*extra*/ null, + /*auto_increment*/ true), + new ColumnDef('profile_id', 'integer', + null, true, 'UNI'), + new ColumnDef('group_id', 'integer', + null, true, 'UNI'), + new ColumnDef('feeduri', 'varchar', + 255, false, 'UNI'), + new ColumnDef('homeuri', 'varchar', + 255, false), + new ColumnDef('huburi', 'text', + null, true), + new ColumnDef('verify_token', 'varchar', + 32, true), + new ColumnDef('secret', 'varchar', + 64, true), + new ColumnDef('sub_state', "enum('subscribe','active','unsubscribe')", + null, true), + new ColumnDef('sub_start', 'datetime', + null, true), + new ColumnDef('sub_end', 'datetime', + null, true), + new ColumnDef('salmonuri', 'text', + null, true), + new ColumnDef('created', 'datetime', + null, false), + new ColumnDef('lastupdate', 'datetime', + null, false)); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has; this function + * defines them. + * + * @return array key definitions + */ + + function keys() + { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. + * + * @return array key definitions + */ + + function keyTypes() + { + return array('id' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U'); + } + + function sequenceKey() + { + return array('id', true, false); + } + + /** + * Fetch the StatusNet-side profile for this feed + * @return Profile + */ + public function getLocalProfile() + { + return Profile::staticGet('id', $this->profile_id); + } + + /** + * @param FeedMunger $munger + * @param boolean $isGroup is this a group record? + * @return Ostatus_profile + */ + public static function ensureProfile($munger) + { + $entity = $munger->ostatusProfile(); + + $current = self::staticGet('feeduri', $entity->feeduri); + if ($current) { + // @fixme we should probably update info as necessary + return $current; + } + + $entity->query('BEGIN'); + + // Awful hack! Awful hack! + $entity->verify = common_good_rand(16); + $entity->secret = common_good_rand(32); + + try { + $profile = $munger->profile(); + $result = $profile->insert(); + if (empty($result)) { + throw new FeedDBException($profile); + } + + $avatar = $munger->getAvatar(); + if ($avatar) { + // @fixme this should be better encapsulated + // ripped from oauthstore.php (for old OMB client) + $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); + copy($avatar, $temp_filename); + $imagefile = new ImageFile($profile->id, $temp_filename); + $filename = Avatar::filename($profile->id, + image_type_to_extension($imagefile->type), + null, + common_timestamp()); + rename($temp_filename, Avatar::path($filename)); + $profile->setOriginal($filename); + } + + $entity->profile_id = $profile->id; + if ($entity->isGroup()) { + $group = new User_group(); + $group->nickname = $profile->nickname . '@remote'; // @fixme + $group->fullname = $profile->fullname; + $group->homepage = $profile->homepage; + $group->location = $profile->location; + $group->created = $profile->created; + $group->insert(); + + if ($avatar) { + $group->setOriginal($filename); + } + + $entity->group_id = $group->id; + } + + $result = $entity->insert(); + if (empty($result)) { + throw new FeedDBException($entity); + } + + $entity->query('COMMIT'); + } catch (FeedDBException $e) { + common_log_db_error($e->obj, 'INSERT', __FILE__); + $entity->query('ROLLBACK'); + return false; + } + return $entity; + } + + /** + * Damn dirty hack! + */ + function isGroup() + { + return (strpos($this->feeduri, '/groups/') !== false); + } + + /** + * Send a subscription request to the hub for this feed. + * The hub will later send us a confirmation POST to /main/push/callback. + * + * @return bool true on success, false on failure + */ + public function subscribe($mode='subscribe') + { + if (common_config('feedsub', 'nohub')) { + // Fake it! We're just testing remote feeds w/o hubs. + return true; + } + // @fixme use the verification token + #$token = md5(mt_rand() . ':' . $this->feeduri); + #$this->verify_token = $token; + #$this->update(); // @fixme + try { + $callback = common_local_url('pushcallback', array('feed' => $this->id)); + $headers = array('Content-Type: application/x-www-form-urlencoded'); + $post = array('hub.mode' => $mode, + 'hub.callback' => $callback, + 'hub.verify' => 'async', + 'hub.verify_token' => $this->verify_token, + 'hub.secret' => $this->secret, + //'hub.lease_seconds' => 0, + 'hub.topic' => $this->feeduri); + $client = new HTTPClient(); + $response = $client->post($this->huburi, $headers, $post); + $status = $response->getStatus(); + if ($status == 202) { + common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback'); + return true; + } else if ($status == 204) { + common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified'); + return true; + } else if ($status >= 200 && $status < 300) { + common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody()); + return false; + } else { + common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody()); + return false; + } + } catch (Exception $e) { + // wtf! + common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri"); + return false; + } + } + + /** + * Send an unsubscription request to the hub for this feed. + * The hub will later send us a confirmation POST to /main/push/callback. + * + * @return bool true on success, false on failure + */ + public function unsubscribe() { + return $this->subscribe('unsubscribe'); + } + + /** + * Read and post notices for updates from the feed. + * Currently assumes that all items in the feed are new, + * coming from a PuSH hub. + * + * @param string $xml source of Atom or RSS feed + * @param string $hmac X-Hub-Signature header, if present + */ + public function postUpdates($xml, $hmac) + { + common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml"); + + if ($this->secret) { + if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) { + $their_hmac = strtolower($matches[1]); + $our_hmac = hash_hmac('sha1', $xml, $this->secret); + if ($their_hmac !== $our_hmac) { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac"); + return; + } + } else { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'"); + return; + } + } else if ($hmac) { + common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'"); + return; + } + + require_once "XML/Feed/Parser.php"; + $feed = new XML_Feed_Parser($xml, false, false, true); + $munger = new FeedMunger($feed); + + $hits = 0; + foreach ($feed as $index => $entry) { + // @fixme this might sort in wrong order if we get multiple updates + + $notice = $munger->notice($index); + + // Double-check for oldies + // @fixme this could explode horribly for multiple feeds on a blog. sigh + $dupe = new Notice(); + $dupe->uri = $notice->uri; + if ($dupe->find(true)) { + common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}"); + continue; + } + + // @fixme need to ensure that groups get handled correctly + $saved = Notice::saveNew($notice->profile_id, + $notice->content, + 'ostatus', + array('is_local' => Notice::REMOTE_OMB, + 'uri' => $notice->uri, + 'lat' => $notice->lat, + 'lon' => $notice->lon, + 'location_ns' => $notice->location_ns, + 'location_id' => $notice->location_id)); + + /* + common_log(LOG_DEBUG, "going to check group delivery..."); + if ($this->group_id) { + $group = User_group::staticGet($this->group_id); + if ($group) { + common_log(LOG_INFO, __METHOD__ . ": saving to local shadow group $group->id $group->nickname"); + $groups = array($group); + } else { + common_log(LOG_INFO, __METHOD__ . ": lost the local shadow group?"); + } + } else { + common_log(LOG_INFO, __METHOD__ . ": no local shadow groups"); + $groups = array(); + } + common_log(LOG_DEBUG, "going to add to inboxes..."); + $notice->addToInboxes($groups, array()); + common_log(LOG_DEBUG, "added to inboxes."); + */ + + $hits++; + } + if ($hits == 0) { + common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml"); + } + } +} diff --git a/plugins/OStatus/lib/feedmunger.php b/plugins/OStatus/lib/feedmunger.php index 927a2fe7a..c895b6ce2 100644 --- a/plugins/OStatus/lib/feedmunger.php +++ b/plugins/OStatus/lib/feedmunger.php @@ -83,17 +83,17 @@ class FeedMunger $this->url = $url; } - function feedinfo() + function ostatusProfile() { - $feedinfo = new Feedinfo(); - $feedinfo->feeduri = $this->url; - $feedinfo->homeuri = $this->feed->link; - $feedinfo->huburi = $this->getHubLink(); + $profile = new Ostatus_profile(); + $profile->feeduri = $this->url; + $profile->homeuri = $this->feed->link; + $profile->huburi = $this->getHubLink(); $salmon = $this->getSalmonLink(); if ($salmon) { - $feedinfo->salmonuri = $salmon; + $profile->salmonuri = $salmon; } - return $feedinfo; + return $profile; } function getAtomLink($item, $attribs=array()) @@ -258,9 +258,7 @@ class FeedMunger { // hack hack hack // should get profile for this entry's author... - $feed = new Feedinfo(); - $feed->feeduri = $self; - $feed = Feedinfo::staticGet('feeduri', $this->getSelfLink()); + $remote = Ostatus_profile::staticGet('feeduri', $this->getSelfLink()); if ($feed) { return $feed->profile_id; } else { -- cgit v1.2.3-54-g00ecf From bc46621af2bea8d2f9f132f275c70c5964f880b4 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Feb 2010 01:11:46 +0000 Subject: OStatus sub setup code cleanup and partial group fixes (needs more work after the Atom updates are done) --- plugins/OStatus/actions/feedsubsettings.php | 15 +-- plugins/OStatus/actions/pushcallback.php | 27 +++-- plugins/OStatus/classes/Ostatus_profile.php | 158 +++++++++++++++++++++------- 3 files changed, 138 insertions(+), 62 deletions(-) (limited to 'plugins/OStatus/actions/feedsubsettings.php') diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php index af8bf4d25..6933c9bf2 100644 --- a/plugins/OStatus/actions/feedsubsettings.php +++ b/plugins/OStatus/actions/feedsubsettings.php @@ -197,6 +197,9 @@ class FeedSubSettingsAction extends ConnectSettingsAction 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) { @@ -212,13 +215,10 @@ class FeedSubSettingsAction extends ConnectSettingsAction // And subscribe the current user to the local profile $user = common_current_user(); - $profile = $this->profile->getLocalProfile(); - if (!$profile) { - throw new ServerException("Feed profile was not saved properly."); - } if ($this->profile->isGroup()) { - if ($user->isMember($profile)) { + $group = $this->profile->localGroup(); + if ($user->isMember($group)) { $this->showForm(_m('Already a member!')); } elseif (Group_member::join($this->profile->group_id, $user->id)) { $this->showForm(_m('Joined remote group!')); @@ -226,9 +226,10 @@ class FeedSubSettingsAction extends ConnectSettingsAction $this->showForm(_m('Remote group join failed!')); } } else { - if ($user->isSubscribed($profile)) { + $local = $this->profile->localProfile(); + if ($user->isSubscribed($local)) { $this->showForm(_m('Already subscribed!')); - } elseif ($user->subscribeTo($profile)) { + } elseif ($user->subscribeTo($local)) { $this->showForm(_m('Feed subscribed!')); } else { $this->showForm(_m('Feed subscription failed!')); diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php index a446593ff..2601a377a 100644 --- a/plugins/OStatus/actions/pushcallback.php +++ b/plugins/OStatus/actions/pushcallback.php @@ -84,27 +84,24 @@ class PushCallbackAction extends Action throw new ServerException("Bogus hub callback: unknown feed", 404); } - # Can't currently set the token in our sub api - #if ($feedinfo->verify_token !== $verify_token) { - # common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); - # throw new ServerError("Bogus hub callback: bad token", 404); - #} - + if ($profile->verify_token !== $verify_token) { + common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); + throw new ServerError("Bogus hub callback: bad token", 404); + } + + if ($mode != $profile->sub_state) { + common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad mode \"$mode\" for feed $topic in state \"{$profile->sub_state}\""); + throw new ServerException("Bogus hub callback: mode doesn't match subscription state.", 404); + } + // OK! if ($mode == 'subscribe') { common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); - $profile->sub_start = common_sql_date(time()); - if ($lease_seconds > 0) { - $profile->sub_end = common_sql_date(time() + $lease_seconds); - } else { - $profile->sub_end = null; - } - $profile->update(); + $profile->confirmSubscribe($lease_seconds); } else { common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic"); - $profile->delete(); + $profile->confirmUnsubscribe(); } - print $challenge; } } diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 748ecce18..f7bbcd028 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -182,9 +182,24 @@ class Ostatus_profile extends Memcached_DataObject * Fetch the StatusNet-side profile for this feed * @return Profile */ - public function getLocalProfile() + public function localProfile() { - return Profile::staticGet('id', $this->profile_id); + 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; } /** @@ -194,62 +209,48 @@ class Ostatus_profile extends Memcached_DataObject */ public static function ensureProfile($munger) { - $entity = $munger->ostatusProfile(); + $profile = $munger->ostatusProfile(); - $current = self::staticGet('feeduri', $entity->feeduri); + $current = self::staticGet('feeduri', $profile->feeduri); if ($current) { // @fixme we should probably update info as necessary return $current; } - $entity->query('BEGIN'); + $profile->query('BEGIN'); // Awful hack! Awful hack! - $entity->verify = common_good_rand(16); - $entity->secret = common_good_rand(32); + $profile->verify = common_good_rand(16); + $profile->secret = common_good_rand(32); try { - $profile = $munger->profile(); - $result = $profile->insert(); - if (empty($result)) { - throw new FeedDBException($profile); - } - - $avatar = $munger->getAvatar(); - if ($avatar) { - // @fixme this should be better encapsulated - // ripped from oauthstore.php (for old OMB client) - $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); - copy($avatar, $temp_filename); - $imagefile = new ImageFile($profile->id, $temp_filename); - $filename = Avatar::filename($profile->id, - image_type_to_extension($imagefile->type), - null, - common_timestamp()); - rename($temp_filename, Avatar::path($filename)); - $profile->setOriginal($filename); - } + $local = $munger->profile(); - $entity->profile_id = $profile->id; if ($entity->isGroup()) { $group = new User_group(); - $group->nickname = $profile->nickname . '@remote'; // @fixme - $group->fullname = $profile->fullname; - $group->homepage = $profile->homepage; - $group->location = $profile->location; - $group->created = $profile->created; + $group->nickname = $local->nickname . '@remote'; // @fixme + $group->fullname = $local->fullname; + $group->homepage = $local->homepage; + $group->location = $local->location; + $group->created = $local->created; $group->insert(); - - if ($avatar) { - $group->setOriginal($filename); + if (empty($result)) { + throw new FeedDBException($group); } - - $entity->group_id = $group->id; + $profile->group_id = $group->id; + } else { + $result = $local->insert(); + if (empty($result)) { + throw new FeedDBException($local); + } + $profile->profile_id = $local->id; } - $result = $entity->insert(); + $profile->created = sql_common_date(); + $profile->lastupdate = sql_common_date(); + $result = $profile->insert(); if (empty($result)) { - throw new FeedDBException($entity); + throw new FeedDBException($profile); } $entity->query('COMMIT'); @@ -258,9 +259,46 @@ class Ostatus_profile extends Memcached_DataObject $entity->query('ROLLBACK'); return false; } + + $avatar = $munger->getAvatar(); + if ($avatar) { + try { + $this->updateAvatar($avatar); + } catch (Exception $e) { + common_log(LOG_ERR, "Exception setting OStatus avatar: " . + $e->getMessage()); + } + } + return $entity; } + /** + * Download and update given avatar image + * @param string $url + * @throws Exception in various failure cases + */ + public function updateAvatar($url) + { + // @fixme this should be better encapsulated + // ripped from oauthstore.php (for old OMB client) + $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); + copy($url, $temp_filename); + $imagefile = new ImageFile($profile->id, $temp_filename); + $filename = Avatar::filename($profile->id, + image_type_to_extension($imagefile->type), + null, + common_timestamp()); + rename($temp_filename, Avatar::path($filename)); + if ($this->isGroup()) { + $group = $this->localGroup(); + $group->setOriginal($filename); + } else { + $profile = $this->localProfile(); + $profile->setOriginal($filename); + } + } + /** * Damn dirty hack! */ @@ -318,6 +356,46 @@ class Ostatus_profile extends Memcached_DataObject } } + /** + * Save PuSH subscription confirmation. + * Sets approximate lease start and end times and finalizes state. + * + * @param int $lease_seconds provided hub.lease_seconds parameter, if given + */ + public function confirmSubscribe($lease_seconds=0) + { + $original = clone($this); + + $this->sub_state = 'active'; + $this->sub_start = common_sql_date(time()); + if ($lease_seconds > 0) { + $this->sub_end = common_sql_date(time() + $lease_seconds); + } else { + $this->sub_end = null; + } + $this->lastupdate = common_sql_date(); + + return $this->update($original); + } + + /** + * Save PuSH unsubscription confirmation. + * Wipes active PuSH sub info and resets state. + */ + public function confirmUnsubscribe() + { + $original = clone($this); + + $this->verify_token = null; + $this->secret = null; + $this->sub_state = null; + $this->sub_start = null; + $this->sub_end = null; + $this->lastupdate = common_sql_date(); + + return $this->update($original); + } + /** * Send an unsubscription request to the hub for this feed. * The hub will later send us a confirmation POST to /main/push/callback. -- cgit v1.2.3-54-g00ecf