From c335db4bbc1521d45308c757ef31da9625e38ee5 Mon Sep 17 00:00:00 2001 From: Shashi Gowda Date: Mon, 7 Mar 2011 00:45:34 +0530 Subject: [PATCH] OStatus support for people tags --- plugins/OStatus/OStatusPlugin.php | 333 ++++++++++++++++++- plugins/OStatus/actions/ostatusinit.php | 26 +- plugins/OStatus/actions/ostatuspeopletag.php | 179 ++++++++++ plugins/OStatus/actions/ostatussub.php | 5 +- plugins/OStatus/actions/ostatustag.php | 117 +++++++ plugins/OStatus/actions/peopletagsalmon.php | 141 ++++++++ plugins/OStatus/actions/pushhub.php | 17 +- plugins/OStatus/actions/usersalmon.php | 61 ++++ plugins/OStatus/classes/Ostatus_profile.php | 134 +++++++- plugins/OStatus/js/ostatus.js | 3 +- plugins/OStatus/lib/ostatusqueuehandler.php | 35 +- plugins/OStatus/lib/salmonaction.php | 16 + plugins/OStatus/lib/xrdaction.php | 131 ++++++++ 13 files changed, 1151 insertions(+), 47 deletions(-) create mode 100644 plugins/OStatus/actions/ostatuspeopletag.php create mode 100644 plugins/OStatus/actions/ostatustag.php create mode 100644 plugins/OStatus/actions/peopletagsalmon.php create mode 100644 plugins/OStatus/lib/xrdaction.php diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index ef9a39a377..666683affc 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -56,14 +56,25 @@ class OStatusPlugin extends Plugin array('action' => 'ownerxrd')); $m->connect('main/ostatus', array('action' => 'ostatusinit')); + $m->connect('main/ostatustag', + array('action' => 'ostatustag')); + $m->connect('main/ostatustag?nickname=:nickname', + array('action' => 'ostatustag'), array('nickname' => '[A-Za-z0-9_-]+')); $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/ostatus?peopletag=:peopletag&tagger=:tagger', + array('action' => 'ostatusinit'), array('tagger' => '[A-Za-z0-9_-]+', + 'peopletag' => '[A-Za-z0-9_-]+')); + + // Remote subscription actions $m->connect('main/ostatussub', array('action' => 'ostatussub')); $m->connect('main/ostatusgroup', array('action' => 'ostatusgroup')); + $m->connect('main/ostatuspeopletag', + array('action' => 'ostatuspeopletag')); // PuSH actions $m->connect('main/push/hub', array('action' => 'pushhub')); @@ -79,6 +90,9 @@ class OStatusPlugin extends Plugin $m->connect('main/salmon/group/:id', array('action' => 'groupsalmon'), array('id' => '[0-9]+')); + $m->connect('main/salmon/peopletag/:id', + array('action' => 'peopletagsalmon'), + array('id' => '[0-9]+')); return true; } @@ -149,6 +163,10 @@ class OStatusPlugin extends Plugin $salmonAction = 'groupsalmon'; $group = $feed->getGroup(); $id = $group->id; + } else if ($feed instanceof AtomListNoticeFeed) { + $salmonAction = 'peopletagsalmon'; + $peopletag = $feed->getList(); + $id = $peopletag->id; } else { return true; } @@ -210,21 +228,7 @@ class OStatusPlugin extends Plugin */ function onStartProfileRemoteSubscribe($output, $profile) { - $cur = common_current_user(); - - if (empty($cur)) { - // Add an OStatus subscribe - $output->elementStart('li', 'entity_subscribe'); - $url = common_local_url('ostatusinit', - array('nickname' => $profile->nickname)); - $output->element('a', array('href' => $url, - 'class' => 'entity_remote_subscribe'), - // TRANS: Link description for link to subscribe to a remote user. - _m('Subscribe')); - - $output->elementEnd('li'); - } - + $this->onStartProfileListItemActionElements($output, $profile); return false; } @@ -238,13 +242,119 @@ class OStatusPlugin extends Plugin array('group' => $group->nickname)); $output->element('a', array('href' => $url, 'class' => 'entity_remote_subscribe'), - // TRANS: Link description for link to join a remote group. _m('Join')); + } return true; } + function onStartSubscribePeopletagForm($output, $peopletag) + { + $cur = common_current_user(); + + if (empty($cur)) { + $output->elementStart('li', 'entity_subscribe'); + $profile = $peopletag->getTagger(); + $url = common_local_url('ostatusinit', + array('tagger' => $profile->nickname, 'peopletag' => $peopletag->tag)); + $output->element('a', array('href' => $url, + 'class' => 'entity_remote_subscribe'), + _m('Subscribe')); + + $output->elementEnd('li'); + return false; + } + + return true; + } + + function onStartShowTagProfileForm($action, $profile) + { + $action->elementStart('form', array('method' => 'post', + 'id' => 'form_tag_user', + 'class' => 'form_settings', + 'name' => 'tagprofile', + 'action' => common_local_url('tagprofile', array('id' => @$profile->id)))); + + $action->elementStart('fieldset'); + $action->element('legend', null, _('Tag remote profile')); + $action->hidden('token', common_session_token()); + + $user = common_current_user(); + + $action->elementStart('ul', 'form_data'); + $action->elementStart('li'); + + $action->input('uri', _('Remote profile'), $action->trimmed('uri'), + _('OStatus user\'s address, like nickname@example.com or http://example.net/nickname')); + $action->elementEnd('li'); + $action->elementEnd('ul'); + $action->submit('fetch', _('Fetch')); + $action->elementEnd('fieldset'); + $action->elementEnd('form'); + } + + function onStartTagProfileAction($action, $profile) + { + $err = null; + $uri = $action->trimmed('uri'); + + if (!$profile && $uri) { + try { + if (Validate::email($uri)) { + $oprofile = Ostatus_profile::ensureWebfinger($uri); + } else if (Validate::uri($uri)) { + $oprofile = Ostatus_profile::ensureProfileURL($uri); + } else { + throw new Exception('Invalid URI'); + } + + // redirect to the new profile. + common_redirect(common_local_url('tagprofile', array('id' => $oprofile->profile_id)), 303); + return false; + + } catch (Exception $e) { + $err = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname"); + } + + $action->showForm($err); + return false; + } + return true; + } + + /* + * If the field being looked for is URI look for the profile + */ + function onStartProfileCompletionSearch($action, $profile, $search_engine) { + if ($action->field == 'uri') { + $user = new User(); + $profile->joinAdd($user); + $profile->whereAdd('uri LIKE "%' . $profile->escape($q) . '%"'); + $profile->query(); + + if ($profile->N == 0) { + try { + if (Validate::email($q)) { + $oprofile = Ostatus_profile::ensureWebfinger($q); + } else if (Validate::uri($q)) { + $oprofile = Ostatus_profile::ensureProfileURL($q); + } else { + throw new Exception('Invalid URI'); + } + return $this->filter(array($oprofile->localProfile())); + + } catch (Exception $e) { + $this->msg = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname"); + return array(); + } + } + return false; + } + return true; + } + /** * Find any explicit remote mentions. Accepted forms: * Webfinger: @user@example.com @@ -711,6 +821,95 @@ class OStatusPlugin extends Plugin } } + /** + * When one of our local users tries to subscribe to a remote peopletag, + * notify the remote server. If the notification is rejected, + * deny the subscription. + * + * @param Profile_list $peopletag + * @param User $user + * + * @return mixed hook return value + */ + + function onStartSubscribePeopletag($peopletag, $user) + { + $oprofile = Ostatus_profile::staticGet('peopletag_id', $peopletag->id); + if ($oprofile) { + if (!$oprofile->subscribe()) { + throw new Exception(_m('Could not set up remote peopletag subscription.')); + } + + $sub = $user->getProfile(); + $tagger = Profile::staticGet($peopletag->tagger); + + $act = new Activity(); + $act->id = TagURI::mint('subscribe_peopletag:%d:%d:%s', + $sub->id, + $peopletag->id, + common_date_iso8601(time())); + + $act->actor = ActivityObject::fromProfile($sub); + $act->verb = ActivityVerb::FOLLOW; + $act->object = $oprofile->asActivityObject(); + + $act->time = time(); + $act->title = _m("Follow list"); + $act->content = sprintf(_m("%s is now following people tagged %s by %s."), + $sub->getBestName(), + $oprofile->getBestName(), + $tagger->getBestName()); + + if ($oprofile->notifyActivity($act, $sub)) { + return true; + } else { + $oprofile->garbageCollect(); + throw new Exception(_m("Failed subscribing to remote peopletag.")); + } + } + } + + /** + * When one of our local users unsubscribes to a remote peopletag, notify the remote + * server. + * + * @param Profile_list $peopletag + * @param User $user + * + * @return mixed hook return value + */ + + function onEndUnsubscribePeopletag($peopletag, $user) + { + $oprofile = Ostatus_profile::staticGet('peopletag_id', $peopletag->id); + if ($oprofile) { + // Drop the PuSH subscription if there are no other subscribers. + $oprofile->garbageCollect(); + + $sub = Profile::staticGet($user->id); + $tagger = Profile::staticGet($peopletag->tagger); + + $act = new Activity(); + $act->id = TagURI::mint('unsubscribe_peopletag:%d:%d:%s', + $sub->id, + $peopletag->id, + common_date_iso8601(time())); + + $act->actor = ActivityObject::fromProfile($member); + $act->verb = ActivityVerb::UNFOLLOW; + $act->object = $oprofile->asActivityObject(); + + $act->time = time(); + $act->title = _m("Unfollow peopletag"); + $act->content = sprintf(_m("%s stopped following the list %s by %s."), + $sub->getBestName(), + $oprofile->getBestName(), + $tagger->getBestName()); + + $oprofile->notifyActivity($act, $user); + } + } + /** * Notify remote users when their notices get favorited. * @@ -747,6 +946,91 @@ class OStatusPlugin extends Plugin return true; } + function onEndTagProfile($ptag) + { + $oprofile = Ostatus_profile::staticGet('profile_id', $ptag->tagged); + + if (empty($oprofile)) { + return true; + } + + $plist = $ptag->getMeta(); + if ($plist->private) { + return true; + } + + $act = new Activity(); + + $tagger = $plist->getTagger(); + $tagged = Profile::staticGet('id', $ptag->tagged); + + $act->verb = ActivityVerb::TAG; + $act->id = TagURI::mint('tag_profile:%d:%d:%s', + $plist->tagger, $plist->id, + common_date_iso8601(time())); + $act->time = time(); + $act->title = _("Tag"); + $act->content = sprintf(_("%s tagged %s in the list %s"), + $tagger->getBestName(), + $tagged->getBestName(), + $plist->getBestName()); + + $act->actor = ActivityObject::fromProfile($tagger); + $act->objects = array(ActivityObject::fromProfile($tagged)); + $act->target = ActivityObject::fromPeopletag($plist); + + $oprofile->notifyActivity($act, $tagger); + + // initiate a PuSH subscription for the person being tagged + if (!$oprofile->subscribe()) { + throw new Exception(sprintf(_('Could not complete subscription to remote '. + 'profile\'s feed. Tag %s could not be saved.'), $ptag->tag)); + return false; + } + return true; + } + + function onEndUntagProfile($ptag) + { + $oprofile = Ostatus_profile::staticGet('profile_id', $ptag->tagged); + + if (empty($oprofile)) { + return true; + } + + $plist = $ptag->getMeta(); + if ($plist->private) { + return true; + } + + $act = new Activity(); + + $tagger = $plist->getTagger(); + $tagged = Profile::staticGet('id', $ptag->tagged); + + $act->verb = ActivityVerb::UNTAG; + $act->id = TagURI::mint('untag_profile:%d:%d:%s', + $plist->tagger, $plist->id, + common_date_iso8601(time())); + $act->time = time(); + $act->title = _("Untag"); + $act->content = sprintf(_("%s untagged %s from the list %s"), + $tagger->getBestName(), + $tagged->getBestName(), + $plist->getBestName()); + + $act->actor = ActivityObject::fromProfile($tagger); + $act->objects = array(ActivityObject::fromProfile($tagged)); + $act->target = ActivityObject::fromPeopletag($plist); + + $oprofile->notifyActivity($act, $tagger); + + // unsubscribe to PuSH feed if no more required + $oprofile->garbageCollect(); + + return true; + } + /** * Notify remote users when their notices get de-favorited. * @@ -913,7 +1197,7 @@ class OStatusPlugin extends Plugin return true; } - function onStartProfileListItemActionElements($item) + function onStartProfileListItemActionElements($item, $profile=null) { if (!common_logged_in()) { @@ -921,7 +1205,12 @@ class OStatusPlugin extends Plugin if (!empty($profileUser)) { - $output = $item->out; + if ($item instanceof Action) { + $output = $item; + $profile = $item->profile; + } else { + $output = $item->out; + } // Add an OStatus subscribe $output->elementStart('li', 'entity_subscribe'); @@ -932,6 +1221,14 @@ class OStatusPlugin extends Plugin // TRANS: Link text for a user to subscribe to an OStatus user. _m('Subscribe')); $output->elementEnd('li'); + + $output->elementStart('li', 'entity_tag'); + $url = common_local_url('ostatustag', + array('nickname' => $profileUser->nickname)); + $output->element('a', array('href' => $url, + 'class' => 'entity_remote_tag'), + _m('Tag')); + $output->elementEnd('li'); } } diff --git a/plugins/OStatus/actions/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php index 9832f33c05..bcef3eef29 100644 --- a/plugins/OStatus/actions/ostatusinit.php +++ b/plugins/OStatus/actions/ostatusinit.php @@ -29,6 +29,8 @@ if (!defined('STATUSNET')) { class OStatusInitAction extends Action { var $nickname; + var $tagger; + var $peopletag; var $group; var $profile; var $err; @@ -45,6 +47,8 @@ class OStatusInitAction extends Action // Local user or group the remote wants to subscribe to $this->nickname = $this->trimmed('nickname'); + $this->tagger = $this->trimmed('tagger'); + $this->peopletag = $this->trimmed('peopletag'); $this->group = $this->trimmed('group'); // Webfinger or profile URL of the remote user @@ -96,8 +100,12 @@ class OStatusInitAction extends Action if ($this->group) { // TRANS: Form legend. $header = sprintf(_m('Join group %s'), $this->group); - // TRANS: Button text. $submit = _m('BUTTON','Join'); + } else if ($this->peopletag && $this->tagger) { + $header = sprintf(_m('Subscribe to people tagged %s by %s'), $this->peopletag, $this->tagger); + $submit = _m('Subscribe'); + $submit = _m('BUTTON','Subscribe'); + // TRANS: Button text. } else { // TRANS: Form legend. $header = sprintf(_m('Subscribe to %s'), $this->nickname); @@ -114,6 +122,7 @@ class OStatusInitAction extends Action $this->elementStart('ul', 'form_data'); $this->elementStart('li', array('id' => 'ostatus_nickname')); + if ($this->group) { // TRANS: Field label. $this->input('group', _m('Group nickname'), $this->group, @@ -122,7 +131,10 @@ class OStatusInitAction extends Action // TRANS: Field label. $this->input('nickname', _m('User nickname'), $this->nickname, _m('Nickname of the user you want to follow.')); + $this->hidden('tagger', $this->tagger); + $this->hidden('peopletag', $this->peopletag); } + $this->elementEnd('li'); $this->elementStart('li', array('id' => 'ostatus_profile')); // TRANS: Field label. @@ -211,6 +223,18 @@ class OStatusInitAction extends Action // TRANS: Client error. $this->clientError("No such group."); } + } else if ($this->peopletag && $this->tagger) { + $user = User::staticGet('nickname', $this->tagger); + if (empty($user)) { + $this->clientError("No such user."); + } + + $peopletag = Profile_list::getByTaggerAndTag($user->id, $this->peopletag); + if ($peopletag) { + return common_local_url('profiletagbyid', + array('tagger_id' => $user->id, 'id' => $peopletag->id)); + } + $this->clientError("No such people tag."); } else { // TRANS: Client error. $this->clientError("No local user or group nickname provided."); diff --git a/plugins/OStatus/actions/ostatuspeopletag.php b/plugins/OStatus/actions/ostatuspeopletag.php new file mode 100644 index 0000000000..737a7c50f3 --- /dev/null +++ b/plugins/OStatus/actions/ostatuspeopletag.php @@ -0,0 +1,179 @@ +. + */ + +/** + * @package OStatusPlugin + * @maintainer Brion Vibber + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +require_once INSTALLDIR . '/lib/peopletaglist.php'; + +/** + * 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 OStatusPeopletagAction 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->isPeopletag()) { + // 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() + { + $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('Subscribe to people tag'), + $this->profile_uri, + _m("Address of the OStatus people tag, like http://example.net/user/all/tag")); + $this->elementEnd('li'); + $this->elementEnd('ul'); + + $this->submit('validate', _m('Continue')); + + $this->elementEnd('fieldset'); + + $this->elementEnd('form'); + } + + /** + * Show a preview for a remote peopletag's profile + * @return boolean true if we're ok to try joining + */ + function preview() + { + $oprofile = $this->oprofile; + $ptag = $oprofile->localPeopletag(); + + $cur = common_current_user(); + if ($ptag->hasSubscriber($cur->id)) { + $this->element('div', array('class' => 'error'), + _m("You are already subscribed to this peopletag.")); + $ok = false; + } else { + $ok = true; + } + + $this->showEntity($ptag); + return $ok; + } + + function showEntity($ptag) + { + $this->elementStart('div', 'peopletag'); + $widget = new PeopletagListItem($ptag, common_current_user(), $this); + $widget->showCreator(); + $widget->showTag(); + $widget->showDescription(); + $this->elementEnd('div'); + } + + /** + * Redirect on successful remote people tag subscription + */ + function success() + { + $cur = common_current_user(); + $url = common_local_url('peopletagsubscriptions', 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(); + $ptag = $this->oprofile->localPeopletag(); + if ($ptag->hasSubscriber($user->id)) { + // TRANS: OStatus remote group subscription dialog error. + $this->showForm(_m('Already subscribed!')); + return; + } + + try { + Profile_tag_subscription::add($ptag, $user); + $this->success(); + } catch (Exception $e) { + $this->showForm($e->getMessage()); + } + } + + /** + * Title of the page + * + * @return string Title of the page + */ + + function title() + { + // TRANS: Page title for OStatus remote people tag subscription form + return _m('Confirm subscription to remote people tag'); + } + + /** + * Instructions for use + * + * @return instructions for use + */ + + function getInstructions() + { + return _m('You can subscribe to people tags from other supported sites. Paste the tag\'s profile URI below:'); + } + + function selfLink() + { + return common_local_url('ostatuspeopletag'); + } +} diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php index 5ca7ce7674..95c6e18c18 100644 --- a/plugins/OStatus/actions/ostatussub.php +++ b/plugins/OStatus/actions/ostatussub.php @@ -270,10 +270,13 @@ class OStatusSubAction extends Action function validateRemoteProfile() { + // Send us to the respective subscription form for conf 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); + } else if ($this->oprofile->isPeopletag()) { + $target = common_local_url('ostatuspeopletag', array(), array('profile' => $this->profile_uri)); + common_redirect($target, 303); } } diff --git a/plugins/OStatus/actions/ostatustag.php b/plugins/OStatus/actions/ostatustag.php new file mode 100644 index 0000000000..85ead81ca2 --- /dev/null +++ b/plugins/OStatus/actions/ostatustag.php @@ -0,0 +1,117 @@ +. + */ + +/** + * @package OStatusPlugin + * @maintainer James Walker + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + + +class OStatusTagAction extends OStatusInitAction +{ + + var $nickname; + var $profile; + var $err; + + function prepare($args) + { + parent::prepare($args); + + if (common_logged_in()) { + $this->clientError(_m('You can use the local tagging!')); + return false; + } + + $this->nickname = $this->trimmed('nickname'); + + // Webfinger or profile URL of the remote user + $this->profile = $this->trimmed('profile'); + + return true; + } + + function showContent() + { + $header = sprintf(_m('Tag %s'), $this->nickname); + $submit = _m('Go'); + $this->elementStart('form', array('id' => 'form_ostatus_connect', + 'method' => 'post', + 'class' => 'form_settings', + 'action' => common_local_url('ostatustag'))); + $this->elementStart('fieldset'); + $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 tag')); + $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', $submit); + $this->elementEnd('fieldset'); + $this->elementEnd('form'); + } + + function connectWebfinger($acct) + { + $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/tag') { + // We found a URL - let's redirect! + $url = Discovery::applyTemplate($link['template'], $target_profile); + common_log(LOG_INFO, "Sending remote subscriber $acct to $url"); + common_redirect($url, 303); + } + + } + $this->clientError(_m("Couldn't confirm remote profile address.")); + } + + function connectProfile($subscriber_profile) + { + $target_profile = $this->targetProfile(); + + // @fixme hack hack! We should look up the remote sub URL from XRDS + $suburl = preg_replace('!^(.*)/(.*?)$!', '$1/main/tagprofile', $subscriber_profile); + $suburl .= '?uri=' . urlencode($target_profile); + + common_log(LOG_INFO, "Sending remote subscriber $subscriber_profile to $suburl"); + common_redirect($suburl, 303); + } + + function title() + { + return _m('OStatus people tag'); + } +} diff --git a/plugins/OStatus/actions/peopletagsalmon.php b/plugins/OStatus/actions/peopletagsalmon.php new file mode 100644 index 0000000000..c5e972e233 --- /dev/null +++ b/plugins/OStatus/actions/peopletagsalmon.php @@ -0,0 +1,141 @@ +. + */ + +/** + * @package OStatusPlugin + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class PeopletagsalmonAction extends SalmonAction +{ + var $peopletag = null; + + function prepare($args) + { + parent::prepare($args); + + $id = $this->trimmed('id'); + + if (!$id) { + $this->clientError(_('No ID.')); + } + + $this->peopletag = Profile_list::staticGet('id', $id); + + if (empty($this->peopletag)) { + $this->clientError(_('No such peopletag.')); + } + + $oprofile = Ostatus_profile::staticGet('peopletag_id', $id); + + if (!empty($oprofile)) { + $this->clientError(_m("Can't accept remote posts for a remote peopletag.")); + } + + return true; + } + + /** + * We've gotten a follow/subscribe notification from a remote user. + * Save a subscription relationship for them. + */ + + /** + * Postel's law: consider a "follow" notification as a "join". + */ + function handleFollow() + { + $this->handleSubscribe(); + } + + /** + * Postel's law: consider an "unfollow" notification as a "unsubscribe". + */ + function handleUnfollow() + { + $this->handleUnsubscribe(); + } + + /** + * A remote user subscribed. + * @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 handleSubscribe() + { + $oprofile = $this->ensureProfile(); + if (!$oprofile) { + $this->clientError(_m("Can't read profile to set up profiletag subscription.")); + } + if ($oprofile->isGroup()) { + $this->clientError(_m("Groups can't subscribe to peopletags.")); + } + + common_log(LOG_INFO, "Remote profile {$oprofile->uri} subscribing to local peopletag ".$this->peopletag->getBestName()); + $profile = $oprofile->localProfile(); + + if ($this->peopletag->hasSubscriber($profile)) { + // Already a member; we'll take it silently to aid in resolving + // inconsistencies on the other side. + return true; + } + + // should we block those whom the tagger has blocked from listening to + // his own updates? + + try { + Profile_tag_subscription::add($this->peopletag, $profile); + } catch (Exception $e) { + $this->serverError(sprintf(_m('Could not subscribe remote user %1$s to peopletag %2$s.'), + $oprofile->uri, $this->peopletag->getBestName())); + } + } + + /** + * A remote user unsubscribed from our peopletag. + */ + + function handleUnsubscribe() + { + $oprofile = $this->ensureProfile(); + if (!$oprofile) { + $this->clientError(_m("Can't read profile to cancel peopletag membership.")); + } + if ($oprofile->isGroup()) { + $this->clientError(_m("Groups can't subscribe to peopletags.")); + } + + common_log(LOG_INFO, "Remote profile {$oprofile->uri} unsubscribing from local peopletag ".$this->peopletag->getBestName()); + $profile = $oprofile->localProfile(); + + try { + Profile_tag_subscription::remove($this->peopletag->tagger, $this->peopletag->tag, $profile->id); + + } catch (Exception $e) { + $this->serverError(sprintf(_m('Could not remove remote user %1$s from peopletag %2$s.'), + $oprofile->uri, $this->peopletag->getBestName())); + return; + } + } +} diff --git a/plugins/OStatus/actions/pushhub.php b/plugins/OStatus/actions/pushhub.php index bfd51ec02f..e7716c8cd0 100644 --- a/plugins/OStatus/actions/pushhub.php +++ b/plugins/OStatus/actions/pushhub.php @@ -176,7 +176,22 @@ class PushHubAction extends Action return true; } } - common_log(LOG_DEBUG, "Not a user or group feed? $feed $userFeed $groupFeed"); + } else if (preg_match('!/(\d+)/lists/(\d+)/statuses\.atom$!', $feed, $matches)) { + $user = $matches[1]; + $id = $matches[2]; + $params = array('user' => $user, 'id' => $id, 'format' => 'atom'); + $listFeed = common_local_url('ApiTimelineList', $params); + + if ($feed == $listFeed) { + $list = Profile_list::staticGet('id', $id); + $user = User::staticGet('id', $user); + if (!$list || !$user || $list->tagger != $user->id) { + throw new ClientException("Invalid hub.topic $feed; people tag doesn't exist."); + } else { + return true; + } + } + common_log(LOG_DEBUG, "Not a user, group or people tag feed? $feed $userFeed $groupFeed $listFeed"); } common_log(LOG_DEBUG, "LOST $feed"); return false; diff --git a/plugins/OStatus/actions/usersalmon.php b/plugins/OStatus/actions/usersalmon.php index 5355aeba03..76125553cf 100644 --- a/plugins/OStatus/actions/usersalmon.php +++ b/plugins/OStatus/actions/usersalmon.php @@ -185,6 +185,67 @@ class UsersalmonAction extends SalmonAction $fave->delete(); } + function handleTag() + { + if ($this->activity->target->type == ActivityObject::_LIST) { + if ($this->activity->objects[0]->type != ActivityObject::PERSON) { + throw new ClientException("Not a person object"); + return false; + } + // this is a peopletag + $tagged = User::staticGet('uri', $this->activity->objects[0]->id); + + if (empty($tagged)) { + throw new ClientException("Unidentified profile being tagged"); + } + + if ($tagged->id !== $this->user->id) { + throw new ClientException("This user is not the one being tagged"); + } + + // save the list + $tagger = $this->ensureProfile(); + $list = Ostatus_profile::ensureActivityObjectProfile($this->activity->target); + + $ptag = $list->localPeopletag(); + $result = Profile_tag::setTag($ptag->tagger, $tagged->id, $ptag->tag); + if (!$result) { + throw new ClientException("The tag could not be saved."); + } + } + } + + function handleUntag() + { + if ($this->activity->target->type == ActivityObject::_LIST) { + if ($this->activity->objects[0]->type != ActivityObject::PERSON) { + throw new ClientException("Not a person object"); + return false; + } + // this is a peopletag + $tagged = User::staticGet('uri', $this->activity->objects[0]->id); + + if (empty($tagged)) { + throw new ClientException("Unidentified profile being untagged"); + } + + if ($tagged->id !== $this->user->id) { + throw new ClientException("This user is not the one being untagged"); + } + + // save the list + $tagger = $this->ensureProfile(); + $list = Ostatus_profile::ensureActivityObjectProfile($this->activity->target); + + $ptag = $list->localPeopletag(); + $result = Profile_tag::unTag($ptag->tagger, $tagged->id, $ptag->tag); + + if (!$result) { + throw new ClientException("The tag could not be deleted."); + } + } + } + /** * @param ActivityObject $object * @return Notice diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index e71e5c9131..821ebef3d5 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -34,6 +34,7 @@ class Ostatus_profile extends Managed_DataObject public $profile_id; public $group_id; + public $peopletag_id; public $feeduri; public $salmonuri; @@ -60,6 +61,7 @@ class Ostatus_profile extends Managed_DataObject 'uri' => array('type' => 'varchar', 'length' => 255, 'not null' => true), 'profile_id' => array('type' => 'integer'), 'group_id' => array('type' => 'integer'), + 'peopletag_id' => array('type' => 'integer'), 'feeduri' => array('type' => 'varchar', 'length' => 255), 'salmonuri' => array('type' => 'varchar', 'length' => 255), 'avatar' => array('type' => 'text'), @@ -70,11 +72,13 @@ class Ostatus_profile extends Managed_DataObject 'unique keys' => array( 'ostatus_profile_profile_id_idx' => array('profile_id'), 'ostatus_profile_group_id_idx' => array('group_id'), + 'ostatus_profile_peopletag_id_idx' => array('peopletag_id'), 'ostatus_profile_feeduri_idx' => array('feeduri'), ), 'foreign keys' => array( 'ostatus_profile_profile_id_fkey' => array('profile', array('profile_id' => 'id')), 'ostatus_profile_group_id_fkey' => array('user_group', array('group_id' => 'id')), + 'ostatus_profile_peopletag_id_fkey' => array('profile_list', array('peopletag_id' => 'id')), ), ); } @@ -103,6 +107,18 @@ class Ostatus_profile extends Managed_DataObject return null; } + /** + * Fetch the StatusNet-side peopletag for this feed + * @return Profile + */ + public function localPeopletag() + { + if ($this->peopletag_id) { + return Profile_list::staticGet('id', $this->peopletag_id); + } + return null; + } + /** * Returns an ActivityObject describing this remote user or group profile. * Can then be used to generate Atom chunks. @@ -113,6 +129,8 @@ class Ostatus_profile extends Managed_DataObject { if ($this->isGroup()) { return ActivityObject::fromGroup($this->localGroup()); + } else if ($this->isPeopletag()) { + return ActivityObject::fromPeopletag($this->localPeopletag()); } else { return ActivityObject::fromProfile($this->localProfile()); } @@ -134,6 +152,9 @@ class Ostatus_profile extends Managed_DataObject if ($this->isGroup()) { $noun = ActivityObject::fromGroup($this->localGroup()); return $noun->asString('activity:' . $element); + } else if ($this->isPeopletag()) { + $noun = ActivityObject::fromPeopletag($this->localPeopletag()); + return $noun->asString('activity:' . $element); } else { $noun = ActivityObject::fromProfile($this->localProfile()); return $noun->asString('activity:' . $element); @@ -145,16 +166,34 @@ class Ostatus_profile extends Managed_DataObject */ function isGroup() { - if ($this->profile_id && !$this->group_id) { + if ($this->profile_id || $this->peopletag_id && !$this->group_id) { return false; - } else if ($this->group_id && !$this->profile_id) { + } else if ($this->group_id && !$this->profile_id && !$this->peopletag_id) { return true; - } else if ($this->group_id && $this->profile_id) { - // TRANS: Server exception. %s is a URI. - throw new ServerException(sprintf(_m('Invalid ostatus_profile state: both group and profile IDs set for %s.'),$this->uri)); + } else if ($this->group_id && ($this->profile_id || $this->peopletag_id)) { + // TRANS: Server exception. %s is a URI + throw new ServerException(_m("Invalid ostatus_profile state: two or more IDs set for %s", $this->uri)); } else { - // TRANS: Server exception. %s is a URI. - throw new ServerException(sprintf(_m('Invalid ostatus_profile state: both group and profile IDs empty for %s.'),$this->uri)); + // TRANS: Server exception. %s is a URI + throw new ServerException(_m("Invalid ostatus_profile state: all IDs empty for %s", $this->uri)); + } + } + + /** + * @return boolean true if this is a remote peopletag + */ + function isPeopletag() + { + if ($this->profile_id || $this->group_id && !$this->peopletag_id) { + return false; + } else if ($this->peopletag_id && !$this->profile_id && !$this->group_id) { + return true; + } else if ($this->peopletag_id && ($this->profile_id || $this->group_id)) { + // TRANS: Server exception. %s is a URI + throw new ServerException(_m("Invalid ostatus_profile state: two or more IDs set for %s", $this->uri)); + } else { + // TRANS: Server exception. %s is a URI + throw new ServerException(_m("Invalid ostatus_profile state: all IDs empty for %s", $this->uri)); } } @@ -214,8 +253,15 @@ class Ostatus_profile extends Managed_DataObject if ($this->isGroup()) { $members = $this->localGroup()->getMembers(0, 1); $count = $members->N; + } else if ($this->isPeopletag()) { + $subscribers = $this->localPeopletag()->getSubscribers(0, 1); + $count = $subscribers->N; } else { - $count = $this->localProfile()->subscriberCount(); + $profile = $this->localProfile(); + $count = $profile->subscriberCount(); + if ($profile->hasLocalTags()) { + $count = 1; + } } common_log(LOG_INFO, __METHOD__ . " SUB COUNT BEFORE: $count"); @@ -235,7 +281,7 @@ class Ostatus_profile extends Managed_DataObject * @param string $verb Activity::SUBSCRIBE or Activity::JOIN * @param Object $object object of the action; must define asActivityNoun($tag) */ - public function notify($actor, $verb, $object=null) + public function notify($actor, $verb, $object=null, $target=null) { if (!($actor instanceof Profile)) { $type = gettype($actor); @@ -277,6 +323,9 @@ class Ostatus_profile extends Managed_DataObject $entry->raw($actor->asAtomAuthor()); $entry->raw($actor->asActivityActor()); $entry->raw($object->asActivityNoun('object')); + if ($target != null) { + $entry->raw($target->asActivityNoun('target')); + } $entry->elementEnd('entry'); $xml = $entry->getString(); @@ -346,6 +395,8 @@ class Ostatus_profile extends Managed_DataObject { if ($this->isGroup()) { return $this->localGroup()->getBestName(); + } else if ($this->isPeopletag()) { + return $this->localPeopletag()->getBestName(); } else { return $this->localProfile()->getBestName(); } @@ -550,6 +601,7 @@ class Ostatus_profile extends Managed_DataObject 'rendered' => $rendered, 'replies' => array(), 'groups' => array(), + 'peopletags' => array(), 'tags' => array(), 'urls' => array()); @@ -586,6 +638,10 @@ class Ostatus_profile extends Managed_DataObject } } + if ($this->isPeopletag()) { + $options['peopletags'][] = $this->localPeopletag(); + } + // Atom categories <-> hashtags foreach ($activity->categories as $cat) { if ($cat->term) { @@ -1202,6 +1258,14 @@ class Ostatus_profile extends Managed_DataObject throw new Exception(_m('Local group can\'t be referenced as remote.')); } + $ptag = Profile_list::staticGet('uri', $homeuri); + if ($ptag) { + $local_user = User::staticGet('id', $ptag->tagger); + if (!empty($local_user)) { + throw new Exception("Local peopletag can't be referenced as remote."); + } + } + if (array_key_exists('feedurl', $hints)) { $feeduri = $hints['feedurl']; } else { @@ -1253,7 +1317,7 @@ class Ostatus_profile extends Managed_DataObject // TRANS: Server exception. throw new ServerException(_m('Can\'t save local profile.')); } - } else { + } else if ($object->type == ActivityObject::GROUP) { $group = new User_group(); $group->uri = $homeuri; $group->created = common_sql_now(); @@ -1264,6 +1328,16 @@ class Ostatus_profile extends Managed_DataObject // TRANS: Server exception. throw new ServerException(_m('Can\'t save local profile.')); } + } else if ($object->type == ActivityObject::_LIST) { + $ptag = new Profile_list(); + $ptag->uri = $homeuri; + $ptag->created = common_sql_now(); + self::updatePeopletag($ptag, $object, $hints); + + $oprofile->peopletag_id = $ptag->insert(); + if (!$oprofile->peopletag_id) { + throw new ServerException("Can't save local peopletag"); + } } $ok = $oprofile->insert(); @@ -1298,12 +1372,16 @@ class Ostatus_profile extends Managed_DataObject if ($this->isGroup()) { $group = $this->localGroup(); self::updateGroup($group, $object, $hints); + } else if ($this->isPeopletag()) { + $ptag = $this->localPeopletag(); + self::updatePeopletag($ptag, $object, $hints); } else { $profile = $this->localProfile(); self::updateProfile($profile, $object, $hints); } + $avatar = self::getActivityObjectAvatar($object, $hints); - if ($avatar) { + if ($avatar && !isset($ptag)) { try { $this->updateAvatar($avatar); } catch (Exception $ex) { @@ -1401,6 +1479,27 @@ class Ostatus_profile extends Managed_DataObject } } + protected static function updatePeopletag($tag, $object, $hints=array()) { + $orig = clone($tag); + + $tag->tag = $object->title; + + if (!empty($object->link)) { + $tag->mainpage = $object->link; + } else if (array_key_exists('profileurl', $hints)) { + $tag->mainpage = $hints['profileurl']; + } + + $tag->description = $object->summary; + $tagger = self::ensureActivityObjectProfile($object->owner); + $tag->tagger = $tagger->profile_id; + + if ($tag->id) { + common_log(LOG_DEBUG, "Updating OStatus peopletag $tag->id from remote info $object->id: " . var_export($object, true) . var_export($hints, true)); + $tag->update($orig); + } + } + protected static function getActivityObjectHomepage($object, $hints=array()) { $homepage = null; @@ -1781,15 +1880,14 @@ class Ostatus_profile extends Managed_DataObject function checkAuthorship($activity) { - if ($this->isGroup()) { - // A group feed will contain posts from multiple authors. - // @fixme validate these profiles in some way! + if ($this->isGroup() || $this->isPeopletag()) { + // A group or propletag feed will contain posts from multiple authors. $oprofile = self::ensureActorProfile($activity); - if ($oprofile->isGroup()) { + if ($oprofile->isGroup() || $oprofile->isPeopletag()) { // 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"); + common_log(LOG_WARNING, + "OStatus: skipping post with group listed ". + "as author: $oprofile->uri in feed from $this->uri"); return false; } } else { diff --git a/plugins/OStatus/js/ostatus.js b/plugins/OStatus/js/ostatus.js index bd29b5c0cf..59d9f23f05 100644 --- a/plugins/OStatus/js/ostatus.js +++ b/plugins/OStatus/js/ostatus.js @@ -92,7 +92,8 @@ SN.U.DialogBox = { }; SN.Init.Subscribe = function() { - $('.entity_subscribe .entity_remote_subscribe').live('click', function() { SN.U.DialogBox.Subscribe($(this)); return false; }); + $('.entity_subscribe .entity_remote_subscribe, .entity_tag .entity_remote_tag') + .live('click', function() { SN.U.DialogBox.Subscribe($(this)); return false; }); }; $(document).ready(function() { diff --git a/plugins/OStatus/lib/ostatusqueuehandler.php b/plugins/OStatus/lib/ostatusqueuehandler.php index d5ee0c5041..d7dc921ad6 100644 --- a/plugins/OStatus/lib/ostatusqueuehandler.php +++ b/plugins/OStatus/lib/ostatusqueuehandler.php @@ -64,13 +64,6 @@ class OStatusQueueHandler extends QueueHandler } } - foreach ($notice->getReplies() as $profile_id) { - $oprofile = Ostatus_profile::staticGet('profile_id', $profile_id); - if ($oprofile) { - $this->pingReply($oprofile); - } - } - if (!empty($this->notice->reply_to)) { $replyTo = Notice::staticGet('id', $this->notice->reply_to); if (!empty($replyTo)) { @@ -82,6 +75,14 @@ class OStatusQueueHandler extends QueueHandler } } } + + foreach ($notice->getProfileTags() as $ptag) { + $oprofile = Ostatus_profile::staticGet('peopletag_id', $ptag->id); + if (!$oprofile) { + $this->pushPeopletag($ptag); + } + } + return true; } @@ -107,6 +108,17 @@ class OStatusQueueHandler extends QueueHandler $this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group_id); } + function pushPeopletag($ptag) + { + // For a local people tag, ping the PuSH hub to update its feed. + // Updates may come from either a local or a remote user. + $feed = common_local_url('ApiTimelineList', + array('id' => $ptag->id, + 'user' => $ptag->tagger, + 'format' => 'atom')); + $this->pushFeed($feed, array($this, 'peopletagFeedForNotice'), $ptag); + } + function pingReply($oprofile) { if ($this->user) { @@ -225,4 +237,13 @@ class OStatusQueueHandler extends QueueHandler return $feed; } + + function peopletagFeedForNotice($ptag) + { + $atom = new AtomListNoticeFeed($ptag); + $atom->addEntryFromNotice($this->notice); + $feed = $atom->getString(); + + return $feed; + } } diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 8bfd7c8261..774709d6f5 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -112,6 +112,12 @@ class SalmonAction extends Action case ActivityVerb::LEAVE: $this->handleLeave(); break; + case ActivityVerb::TAG: + $this->handleTag(); + break; + case ActivityVerb::UNTAG: + $this->handleUntag(); + break; case ActivityVerb::UPDATE_PROFILE: $this->handleUpdateProfile(); break; @@ -172,6 +178,16 @@ class SalmonAction extends Action throw new ClientException(_m("This target doesn't understand leave events.")); } + function handleTag() + { + throw new ClientException(_m("This target doesn't understand tag events.")); + } + + function handleUntag() + { + throw new ClientException(_m("This target doesn't understand untag events.")); + } + /** * Remote user sent us an update to their profile. * If we already know them, accept the updates. diff --git a/plugins/OStatus/lib/xrdaction.php b/plugins/OStatus/lib/xrdaction.php new file mode 100644 index 0000000000..1ac4d40a50 --- /dev/null +++ b/plugins/OStatus/lib/xrdaction.php @@ -0,0 +1,131 @@ +. + */ + +/** + * @package OStatusPlugin + * @maintainer James Walker + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class XrdAction extends Action +{ + public $uri; + + public $user; + + public $xrd; + + function handle() + { + $nick = $this->user->nickname; + $profile = $this->user->getProfile(); + + if (empty($this->xrd)) { + $xrd = new XRD(); + } else { + $xrd = $this->xrd; + } + + if (empty($xrd->subject)) { + $xrd->subject = Discovery::normalize($this->uri); + } + + // Possible aliases for the user + + $uris = array($this->user->uri, $profile->profileurl); + + // FIXME: Webfinger generation code should live somewhere on its own + + $path = common_config('site', 'path'); + + if (empty($path)) { + $uris[] = sprintf('acct:%s@%s', $nick, common_config('site', 'server')); + } + + foreach ($uris as $uri) { + if ($uri != $xrd->subject) { + $xrd->alias[] = $uri; + } + } + + $xrd->links[] = array('rel' => Discovery::PROFILEPAGE, + 'type' => 'text/html', + 'href' => $profile->profileurl); + + $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' => $profile->profileurl); + // 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::REL_SALMON, + 'href' => $salmon_url); + // XXX : Deprecated - to be removed. + $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 ); + + $url = common_local_url('tagprofile') . '?uri={uri}'; + $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/tag', + 'template' => $url ); + + header('Content-type: application/xrd+xml'); + print $xrd->toXML(); + } +}