* @maintainer Brion Vibber <brion@status.net>
*/
-if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+if (!defined('STATUSNET')) {
+ exit(1);
+}
set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/');
class FeedSubException extends Exception
{
+ function __construct($msg=null)
+ {
+ $type = get_class($this);
+ if ($msg) {
+ parent::__construct("$type: $msg");
+ } else {
+ parent::__construct($type);
+ }
+ }
}
class OStatusPlugin extends Plugin
function onRouterInitialized($m)
{
// Discovery actions
- $m->connect('.well-known/host-meta',
- array('action' => 'hostmeta'));
- $m->connect('main/xrd',
- array('action' => 'xrd'));
+ $m->connect('main/ownerxrd',
+ array('action' => 'ownerxrd'));
$m->connect('main/ostatus',
array('action' => 'ostatusinit'));
$m->connect('main/ostatus?nickname=:nickname',
array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+'));
+ $m->connect('main/ostatus?group=:group',
+ array('action' => 'ostatusinit'), array('group' => '[A-Za-z0-9_-]+'));
$m->connect('main/ostatussub',
array('action' => 'ostatussub'));
- $m->connect('main/ostatussub',
- array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+'));
+ $m->connect('main/ostatusgroup',
+ array('action' => 'ostatusgroup'));
// PuSH actions
$m->connect('main/push/hub', array('action' => 'pushhub'));
// Outgoing from our internal PuSH hub
$qm->connect('hubconf', 'HubConfQueueHandler');
+ $qm->connect('hubprep', 'HubPrepQueueHandler');
+
$qm->connect('hubout', 'HubOutQueueHandler');
// Outgoing Salmon replies (when we don't need a return value)
*/
function onStartEnqueueNotice($notice, &$transports)
{
- $transports[] = 'ostatus';
+ if ($notice->isLocal()) {
+ // put our transport first, in case there's any conflict (like OMB)
+ array_unshift($transports, 'ostatus');
+ }
return true;
}
{
if ($action instanceof ShowstreamAction) {
$acct = 'acct:'. $action->profile->nickname .'@'. common_config('site', 'server');
- $url = common_local_url('xrd');
+ $url = common_local_url('userxrd');
$url.= '?uri='. $acct;
-
+
header('Link: <'.$url.'>; rel="'. Discovery::LRDD_REL.'"; type="application/xrd+xml"');
}
}
-
+
/**
* Set up a PuSH hub link to our internal link for canonical timeline
* Atom feeds for users and groups.
$user = $feed->getUser();
$id = $user->id;
$profile = $user->getProfile();
- $feed->setActivitySubject($profile->asActivityNoun('subject'));
} else if ($feed instanceof AtomGroupNoticeFeed) {
$salmonAction = 'groupsalmon';
$group = $feed->getGroup();
$id = $group->id;
- $feed->setActivitySubject($group->asActivitySubject());
} else {
return true;
}
// Also, we'll add in the salmon link
$salmon = common_local_url($salmonAction, array('id' => $id));
+ $feed->addLink($salmon, array('rel' => Salmon::REL_SALMON));
+
+ // XXX: these are deprecated
$feed->addLink($salmon, array('rel' => Salmon::NS_REPLIES));
$feed->addLink($salmon, array('rel' => Salmon::NS_MENTIONS));
}
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');
return false;
}
- /**
- * Check if we've got remote replies to send via Salmon.
- *
- * @fixme push webfinger lookup & sending to a background queue
- * @fixme also detect short-form name for remote subscribees where not ambiguous
- */
-
- function onEndNoticeSave($notice)
+ function onStartGroupSubscribe($output, $group)
{
+ $cur = common_current_user();
+
+ if (empty($cur)) {
+ // Add an OStatus subscribe
+ $url = common_local_url('ostatusinit',
+ 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;
}
/**
- *
+ * Find any explicit remote mentions. Accepted forms:
+ * Webfinger: @user@example.com
+ * Profile link: @example.com/mublog/user
+ * @param Profile $sender (os user?)
+ * @param string $text input markup text
+ * @param array &$mention in/out param: set of found mentions
+ * @return boolean hook return value
*/
function onEndFindMentions($sender, $text, &$mentions)
{
- preg_match_all('/(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)/',
+ $matches = array();
+
+ // Webfinger matches: @user@example.com
+ if (preg_match_all('!(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\-?\w+\.)*\w+(?:\w+\-\w+)*\.\w+)!',
$text,
$wmatches,
- PREG_OFFSET_CAPTURE);
-
- foreach ($wmatches[1] as $wmatch) {
-
- $webfinger = $wmatch[0];
-
- $this->log(LOG_INFO, "Checking Webfinger for address '$webfinger'");
+ PREG_OFFSET_CAPTURE)) {
+ foreach ($wmatches[1] as $wmatch) {
+ list($target, $pos) = $wmatch;
+ $this->log(LOG_INFO, "Checking webfinger '$target'");
+ try {
+ $oprofile = Ostatus_profile::ensureWebfinger($target);
+ if ($oprofile && !$oprofile->isGroup()) {
+ $profile = $oprofile->localProfile();
+ $matches[$pos] = array('mentioned' => array($profile),
+ 'text' => $target,
+ 'position' => $pos,
+ 'url' => $profile->profileurl);
+ }
+ } catch (Exception $e) {
+ $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage());
+ }
+ }
+ }
- $oprofile = Ostatus_profile::ensureWebfinger($webfinger);
+ // Profile matches: @example.com/mublog/user
+ if (preg_match_all('!(?:^|\s+)@((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)!',
+ $text,
+ $wmatches,
+ PREG_OFFSET_CAPTURE)) {
+ foreach ($wmatches[1] as $wmatch) {
+ list($target, $pos) = $wmatch;
+ $schemes = array('http', 'https');
+ foreach ($schemes as $scheme) {
+ $url = "$scheme://$target";
+ $this->log(LOG_INFO, "Checking profile address '$url'");
+ try {
+ $oprofile = Ostatus_profile::ensureProfileURL($url);
+ if ($oprofile && !$oprofile->isGroup()) {
+ $profile = $oprofile->localProfile();
+ $matches[$pos] = array('mentioned' => array($profile),
+ 'text' => $target,
+ 'position' => $pos,
+ 'url' => $profile->profileurl);
+ break;
+ }
+ } catch (Exception $e) {
+ $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage());
+ }
+ }
+ }
+ }
- if (empty($oprofile)) {
+ foreach ($mentions as $i => $other) {
+ // If we share a common prefix with a local user, override it!
+ $pos = $other['position'];
+ if (isset($matches[$pos])) {
+ $mentions[$i] = $matches[$pos];
+ unset($matches[$pos]);
+ }
+ }
+ foreach ($matches as $mention) {
+ $mentions[] = $mention;
+ }
- $this->log(LOG_INFO, "No Ostatus_profile found for address '$webfinger'");
+ return true;
+ }
- } else {
+ /**
+ * Allow remote profile references to be used in commands:
+ * sub update@status.net
+ * whois evan@identi.ca
+ * reply http://identi.ca/evan hey what's up
+ *
+ * @param Command $command
+ * @param string $arg
+ * @param Profile &$profile
+ * @return hook return code
+ */
+ function onStartCommandGetProfile($command, $arg, &$profile)
+ {
+ $oprofile = $this->pullRemoteProfile($arg);
+ if ($oprofile && !$oprofile->isGroup()) {
+ $profile = $oprofile->localProfile();
+ return false;
+ } else {
+ return true;
+ }
+ }
- $this->log(LOG_INFO, "Ostatus_profile found for address '$webfinger'");
+ /**
+ * Allow remote group references to be used in commands:
+ * join group+statusnet@identi.ca
+ * join http://identi.ca/group/statusnet
+ * drop identi.ca/group/statusnet
+ *
+ * @param Command $command
+ * @param string $arg
+ * @param User_group &$group
+ * @return hook return code
+ */
+ function onStartCommandGetGroup($command, $arg, &$group)
+ {
+ $oprofile = $this->pullRemoteProfile($arg);
+ if ($oprofile && $oprofile->isGroup()) {
+ $group = $oprofile->localGroup();
+ return false;
+ } else {
+ return true;
+ }
+ }
- if ($oprofile->isGroup()) {
- continue;
- }
- $profile = $oprofile->localProfile();
+ protected function pullRemoteProfile($arg)
+ {
+ $oprofile = null;
+ if (preg_match('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $arg)) {
+ // webfinger lookup
+ try {
+ return Ostatus_profile::ensureWebfinger($arg);
+ } catch (Exception $e) {
+ common_log(LOG_ERR, 'Webfinger lookup failed for ' .
+ $arg . ': ' . $e->getMessage());
+ }
+ }
- $pos = $wmatch[1];
- foreach ($mentions as $i => $other) {
- // If we share a common prefix with a local user, override it!
- if ($other['position'] == $pos) {
- unset($mentions[$i]);
- }
- }
- $mentions[] = array('mentioned' => array($profile),
- 'text' => $wmatch[0],
- 'position' => $pos,
- 'url' => $profile->profileurl);
+ // Look for profile URLs, with or without scheme:
+ $urls = array();
+ if (preg_match('!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
+ $urls[] = $arg;
+ }
+ if (preg_match('!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
+ $schemes = array('http', 'https');
+ foreach ($schemes as $scheme) {
+ $urls[] = "$scheme://$arg";
}
}
- return true;
+ foreach ($urls as $url) {
+ try {
+ return Ostatus_profile::ensureProfileURL($url);
+ } catch (Exception $e) {
+ common_log(LOG_ERR, 'Profile lookup failed for ' .
+ $arg . ': ' . $e->getMessage());
+ }
+ }
+ return null;
}
/**
}
function onEndShowStatusNetStyles($action) {
- $action->cssLink(common_path('plugins/OStatus/theme/base/css/ostatus.css'));
+ $action->cssLink('plugins/OStatus/theme/base/css/ostatus.css');
return true;
}
function onEndShowStatusNetScripts($action) {
- $action->script(common_path('plugins/OStatus/js/ostatus.js'));
+ $action->script('plugins/OStatus/js/ostatus.js');
return true;
}
}
$url = $notice->url;
+ // TRANSLATE: %s is a domain.
$title = sprintf(_m("Sent from %s via OStatus"), $domain);
return false;
}
}
+ return true;
}
/**
}
}
+ /**
+ * Tell the FeedSub infrastructure whether we have any active OStatus
+ * usage for the feed; if not it'll be able to garbage-collect the
+ * feed subscription.
+ *
+ * @param FeedSub $feedsub
+ * @param integer $count in/out
+ * @return mixed hook return code
+ */
+ function onFeedSubSubscriberCount($feedsub, &$count)
+ {
+ $oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri);
+ if ($oprofile) {
+ $count += $oprofile->subscriberCount();
+ }
+ return true;
+ }
+
/**
* When about to subscribe to a remote user, start a server-to-server
* PuSH subscription if needed. If we can't establish that, abort.
}
if (!$oprofile->subscribe()) {
+ // TRANS: Exception.
throw new Exception(_m('Could not set up remote subscription.'));
}
}
return true;
}
- $act = new Activity();
-
- $act->verb = ActivityVerb::FOLLOW;
-
- $act->id = TagURI::mint('follow:%d:%d:%s',
- $subscriber->id,
- $other->id,
- common_date_iso8601(time()));
-
- $act->time = time();
- $act->title = _("Follow");
- $act->content = sprintf(_("%s is now following %s."),
- $subscriber->getBestName(),
- $other->getBestName());
+ $sub = Subscription::pkeyGet(array('subscriber' => $subscriber->id,
+ 'subscribed' => $other->id));
- $act->actor = ActivityObject::fromProfile($subscriber);
- $act->object = ActivityObject::fromProfile($other);
+ $act = $sub->asActivity();
$oprofile->notifyActivity($act, $subscriber);
common_date_iso8601(time()));
$act->time = time();
- $act->title = _("Unfollow");
- $act->content = sprintf(_("%s stopped following %s."),
+ $act->title = _m('Unfollow');
+ // TRANS: Success message for unsubscribe from user attempt through OStatus.
+ // TRANS: %1$s is the unsubscriber's name, %2$s is the unsubscribed user's name.
+ $act->content = sprintf(_m('%1$s stopped following %2$s.'),
$profile->getBestName(),
$other->getBestName());
throw new Exception(_m('Could not set up remote group membership.'));
}
+ // NOTE: we don't use Group_member::asActivity() since that record
+ // has not yet been created.
+
$member = Profile::staticGet($user->id);
$act = new Activity();
$act->time = time();
$act->title = _m("Join");
- $act->content = sprintf(_m("%s has joined group %s."),
+ // TRANS: Success message for subscribe to group attempt through OStatus.
+ // TRANS: %1$s is the member name, %2$s is the subscribed group's name.
+ $act->content = sprintf(_m('%1$s has joined group %2$s.'),
$member->getBestName(),
$oprofile->getBestName());
return true;
} else {
$oprofile->garbageCollect();
+ // TRANS: Exception.
throw new Exception(_m("Failed joining remote group."));
}
}
// Drop the PuSH subscription if there are no other subscribers.
$oprofile->garbageCollect();
-
$member = Profile::staticGet($user->id);
$act = new Activity();
$act->time = time();
$act->title = _m("Leave");
- $act->content = sprintf(_m("%s has left group %s."),
+ // TRANS: Success message for unsubscribe from group attempt through OStatus.
+ // TRANS: %1$s is the member name, %2$s is the unsubscribed group's name.
+ $act->content = sprintf(_m('%1$s has left group %2$s.'),
$member->getBestName(),
$oprofile->getBestName());
* @param Notice $notice being favored
* @return hook return value
*/
-
function onEndFavorNotice(Profile $profile, Notice $notice)
{
$user = User::staticGet('id', $profile->id);
return true;
}
- $act = new Activity();
+ $fav = Fave::pkeyGet(array('user_id' => $user->id,
+ 'notice_id' => $notice->id));
- $act->verb = ActivityVerb::FAVORITE;
- $act->id = TagURI::mint('favor:%d:%d:%s',
- $profile->id,
- $notice->id,
- common_date_iso8601(time()));
+ if (empty($fav)) {
+ // That's weird.
+ return true;
+ }
- $act->time = time();
- $act->title = _("Favor");
- $act->content = sprintf(_("%s marked notice %s as a favorite."),
- $profile->getBestName(),
- $notice->uri);
-
- $act->actor = ActivityObject::fromProfile($profile);
- $act->object = ActivityObject::fromNotice($notice);
+ $act = $fav->asActivity();
$oprofile->notifyActivity($act, $profile);
$notice->id,
common_date_iso8601(time()));
$act->time = time();
- $act->title = _("Disfavor");
- $act->content = sprintf(_("%s marked notice %s as no longer a favorite."),
+ $act->title = _m('Disfavor');
+ // TRANS: Success message for remove a favorite notice through OStatus.
+ // TRANS: %1$s is the unfavoring user's name, %2$s is URI to the no longer favored notice.
+ $act->content = sprintf(_m('%1$s marked notice %2$s as no longer a favorite.'),
$profile->getBestName(),
$notice->uri);
}
function onStartShowSubscriptionsContent($action)
+ {
+ $this->showEntityRemoteSubscribe($action);
+
+ return true;
+ }
+
+ function onStartShowUserGroupsContent($action)
+ {
+ $this->showEntityRemoteSubscribe($action, 'ostatusgroup');
+
+ return true;
+ }
+
+ function onEndShowSubscriptionsMiniList($action)
+ {
+ $this->showEntityRemoteSubscribe($action);
+
+ return true;
+ }
+
+ function onEndShowGroupsMiniList($action)
+ {
+ $this->showEntityRemoteSubscribe($action, 'ostatusgroup');
+
+ return true;
+ }
+
+ function showEntityRemoteSubscribe($action, $target='ostatussub')
{
$user = common_current_user();
if ($user && ($user->id == $action->profile->id)) {
$action->elementStart('div', 'entity_actions');
$action->elementStart('p', array('id' => 'entity_remote_subscribe',
'class' => 'entity_subscribe'));
- $action->element('a', array('href' => common_local_url('ostatussub'),
- 'class' => 'entity_remote_subscribe')
- , _m('Subscribe to remote user'));
+ $action->element('a', array('href' => common_local_url($target),
+ 'class' => 'entity_remote_subscribe'),
+ // TRANS: Link text for link to remote subscribe.
+ _m('Remote'));
$action->elementEnd('p');
$action->elementEnd('div');
}
-
- return true;
}
/**
$profile->id,
common_date_iso8601(time()));
$act->time = time();
+ // TRANS: Title for activity.
$act->title = _m("Profile update");
+ // TRANS: Ping text for remote profile update through OStatus.
+ // TRANS: %s is user that updated their profile.
$act->content = sprintf(_m("%s has updated their profile page."),
$profile->getBestName());
return true;
}
+
+ function onStartProfileListItemActionElements($item)
+ {
+ if (!common_logged_in()) {
+
+ $profileUser = User::staticGet('id', $item->profile->id);
+
+ if (!empty($profileUser)) {
+
+ $output = $item->out;
+
+ // Add an OStatus subscribe
+ $output->elementStart('li', 'entity_subscribe');
+ $url = common_local_url('ostatusinit',
+ array('nickname' => $profileUser->nickname));
+ $output->element('a', array('href' => $url,
+ 'class' => 'entity_remote_subscribe'),
+ // TRANS: Link text for a user to subscribe to an OStatus user.
+ _m('Subscribe'));
+ $output->elementEnd('li');
+ }
+ }
+
+ return true;
+ }
+
+ function onPluginVersion(&$versions)
+ {
+ $versions[] = array('name' => 'OStatus',
+ 'version' => STATUSNET_VERSION,
+ 'author' => 'Evan Prodromou, James Walker, Brion Vibber, Zach Copley',
+ 'homepage' => 'http://status.net/wiki/Plugin:OStatus',
+ // TRANS: Plugin description.
+ 'rawdescription' => _m('Follow people across social networks that implement '.
+ '<a href="http://ostatus.org/">OStatus</a>.'));
+
+ return true;
+ }
+
+ /**
+ * Utility function to check if the given URI is a canonical group profile
+ * page, and if so return the ID number.
+ *
+ * @param string $url
+ * @return mixed int or false
+ */
+ public static function localGroupFromUrl($url)
+ {
+ $group = User_group::staticGet('uri', $url);
+ if ($group) {
+ $local = Local_group::staticGet('group_id', $group->id);
+ if ($local) {
+ return $group->id;
+ }
+ } else {
+ // To find local groups which haven't had their uri fields filled out...
+ // If the domain has changed since a subscriber got the URI, it'll
+ // be broken.
+ $template = common_local_url('groupbyid', array('id' => '31337'));
+ $template = preg_quote($template, '/');
+ $template = str_replace('31337', '(\d+)', $template);
+ if (preg_match("/$template/", $url, $matches)) {
+ return intval($matches[1]);
+ }
+ }
+ return false;
+ }
+
+ public function onStartProfileGetAtomFeed($profile, &$feed)
+ {
+ $oprofile = Ostatus_profile::staticGet('profile_id', $profile->id);
+
+ if (empty($oprofile)) {
+ return true;
+ }
+
+ $feed = $oprofile->feeduri;
+ return false;
+ }
+
+ function onStartGetProfileFromURI($uri, &$profile)
+ {
+ // Don't want to do Web-based discovery on our own server,
+ // so we check locally first.
+
+ $user = User::staticGet('uri', $uri);
+
+ if (!empty($user)) {
+ $profile = $user->getProfile();
+ return false;
+ }
+
+ // Now, check remotely
+
+ $oprofile = Ostatus_profile::ensureProfileURI($uri);
+
+ if (!empty($oprofile)) {
+ $profile = $oprofile->localProfile();
+ return false;
+ }
+
+ // Still not a hit, so give up.
+
+ return true;
+ }
+
+ function onEndXrdActionLinks(&$xrd, $user)
+ {
+ $xrd->links[] = array('rel' => Discovery::UPDATESFROM,
+ 'href' => common_local_url('ApiTimelineUser',
+ array('id' => $user->id,
+ 'format' => 'atom')),
+ 'type' => 'application/atom+xml');
+
+ // Salmon
+ $salmon_url = common_local_url('usersalmon',
+ array('id' => $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', $user->id);
+ if (!$magickey) {
+ // No keypair yet, let's generate one.
+ $magickey = new Magicsig();
+ $magickey->generate($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 );
+
+ return true;
+ }
}