summaryrefslogtreecommitdiff
path: root/plugins/OStatus
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/OStatus')
-rw-r--r--plugins/OStatus/OStatusPlugin.php56
-rw-r--r--plugins/OStatus/actions/groupsalmon.php9
-rw-r--r--plugins/OStatus/actions/ostatussub.php11
-rw-r--r--plugins/OStatus/actions/webfinger.php15
-rw-r--r--plugins/OStatus/classes/Ostatus_profile.php373
-rw-r--r--plugins/OStatus/js/ostatus.js34
-rw-r--r--plugins/OStatus/lib/ostatusqueuehandler.php (renamed from plugins/OStatus/lib/hubdistribqueuehandler.php)95
-rw-r--r--plugins/OStatus/lib/salmonaction.php50
-rw-r--r--plugins/OStatus/lib/salmonoutqueuehandler.php44
-rw-r--r--plugins/OStatus/tests/ActivityParseTests.php209
10 files changed, 423 insertions, 473 deletions
diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php
index 934c858ac..8c8d909a8 100644
--- a/plugins/OStatus/OStatusPlugin.php
+++ b/plugins/OStatus/OStatusPlugin.php
@@ -78,11 +78,16 @@ class OStatusPlugin extends Plugin
*/
function onEndInitializeQueueManager(QueueManager $qm)
{
+ // Prepare outgoing distributions after notice save.
+ $qm->connect('ostatus', 'OStatusQueueHandler');
+
// Outgoing from our internal PuSH hub
$qm->connect('hubverify', 'HubVerifyQueueHandler');
- $qm->connect('hubdistrib', 'HubDistribQueueHandler');
$qm->connect('hubout', 'HubOutQueueHandler');
+ // Outgoing Salmon replies (when we don't need a return value)
+ $qm->connect('salmonout', 'SalmonOutQueueHandler');
+
// Incoming from a foreign PuSH hub
$qm->connect('pushinput', 'PushInputQueueHandler');
return true;
@@ -93,7 +98,7 @@ class OStatusPlugin extends Plugin
*/
function onStartEnqueueNotice($notice, &$transports)
{
- $transports[] = 'hubdistrib';
+ $transports[] = 'ostatus';
return true;
}
@@ -199,25 +204,6 @@ class OStatusPlugin extends Plugin
function onEndNoticeSave($notice)
{
- $mentioned = $notice->getReplies();
-
- foreach ($mentioned as $profile_id) {
-
- $oprofile = Ostatus_profile::staticGet('profile_id', $profile_id);
-
- if (!empty($oprofile) && !empty($oprofile->salmonuri)) {
-
- common_log(LOG_INFO, "Sending notice '{$notice->uri}' to remote profile '{$oprofile->uri}'.");
-
- // FIXME: this needs to go out in a queue handler
-
- $xml = '<?xml version="1.0" encoding="UTF-8" ?' . '>';
- $xml .= $notice->asAtomEntry(true, true);
-
- $salmon = new Salmon();
- $salmon->post($oprofile->salmonuri, $xml);
- }
- }
}
/**
@@ -295,13 +281,19 @@ class OStatusPlugin extends Plugin
function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
{
if ($notice->source == 'ostatus') {
- $bits = parse_url($notice->uri);
- $domain = $bits['host'];
-
- $name = $domain;
- $url = $notice->uri;
- $title = sprintf(_m("Sent from %s via OStatus"), $domain);
- return false;
+ if ($notice->url) {
+ $bits = parse_url($notice->url);
+ $domain = $bits['host'];
+ if (substr($domain, 0, 4) == 'www.') {
+ $name = substr($domain, 4);
+ } else {
+ $name = $domain;
+ }
+
+ $url = $notice->url;
+ $title = sprintf(_m("Sent from %s via OStatus"), $domain);
+ return false;
+ }
}
}
@@ -316,7 +308,7 @@ class OStatusPlugin extends Plugin
{
$oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri);
if ($oprofile) {
- $oprofile->processFeed($feed);
+ $oprofile->processFeed($feed, 'push');
} else {
common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri");
}
@@ -517,12 +509,8 @@ class OStatusPlugin extends Plugin
$oprofile = Ostatus_profile::staticGet('group_id', $group->id);
if ($oprofile) {
// Drop the PuSH subscription if there are no other subscribers.
+ $oprofile->garbageCollect();
- $members = $group->getMembers(0, 1);
- if ($members->N == 0) {
- common_log(LOG_INFO, "Unsubscribing from now-unused group feed $oprofile->feeduri");
- $oprofile->unsubscribe();
- }
$member = Profile::staticGet($user->id);
diff --git a/plugins/OStatus/actions/groupsalmon.php b/plugins/OStatus/actions/groupsalmon.php
index 2e4fe9443..29377b5fa 100644
--- a/plugins/OStatus/actions/groupsalmon.php
+++ b/plugins/OStatus/actions/groupsalmon.php
@@ -46,6 +46,11 @@ class GroupsalmonAction extends SalmonAction
$this->clientError(_('No such group.'));
}
+ $oprofile = Ostatus_profile::staticGet('group_id', $id);
+ if ($oprofile) {
+ $this->clientError(_m("Can't accept remote posts for a remote group."));
+ }
+
return true;
}
@@ -74,13 +79,13 @@ class GroupsalmonAction extends SalmonAction
throw new ClientException("Not to the attention of anyone.");
} else {
$uri = common_local_url('groupbyid', array('id' => $this->group->id));
- if (!in_array($context->attention, $uri)) {
+ if (!in_array($uri, $context->attention)) {
throw new ClientException("Not to the attention of this group.");
}
}
$profile = $this->ensureProfile();
- // @fixme save the post
+ $this->saveNotice();
}
/**
diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php
index b3569e695..12832cdcf 100644
--- a/plugins/OStatus/actions/ostatussub.php
+++ b/plugins/OStatus/actions/ostatussub.php
@@ -288,10 +288,15 @@ class OStatusSubAction extends Action
}
$this->profile_uri = $profile_uri;
- // @fixme validate, normalize bla bla
try {
- $oprofile = Ostatus_profile::ensureProfile($this->profile_uri);
- $this->oprofile = $oprofile;
+ if (Validate::email($this->profile_uri)) {
+ $this->oprofile = Ostatus_profile::ensureWebfinger($this->profile_uri);
+ } else if (Validate::uri($this->profile_uri)) {
+ $this->oprofile = Ostatus_profile::ensureProfile($this->profile_uri);
+ } else {
+ $this->error = _m("Invalid address format.");
+ return false;
+ }
return true;
} catch (FeedSubBadURLException $e) {
$this->error = _m('Invalid URL or could not reach server.');
diff --git a/plugins/OStatus/actions/webfinger.php b/plugins/OStatus/actions/webfinger.php
index fbbd8d039..34336a903 100644
--- a/plugins/OStatus/actions/webfinger.php
+++ b/plugins/OStatus/actions/webfinger.php
@@ -65,6 +65,21 @@ class WebfingerAction extends Action
'format' => 'atom')),
'type' => 'application/atom+xml');
+ // hCard
+ $xrd->links[] = array('rel' => 'http://microformats.org/profile/hcard',
+ 'type' => 'text/html',
+ 'href' => common_profile_url($nick));
+
+ // XFN
+ $xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11',
+ 'type' => 'text/html',
+ 'href' => common_profile_url($nick));
+ // FOAF
+ $xrd->links[] = array('rel' => 'describedby',
+ 'type' => 'application/rdf+xml',
+ 'href' => common_local_url('foaf',
+ array('nickname' => $nick)));
+
$salmon_url = common_local_url('salmon',
array('id' => $this->user->id));
diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php
index 91b957fa2..9f9efb96e 100644
--- a/plugins/OStatus/classes/Ostatus_profile.php
+++ b/plugins/OStatus/classes/Ostatus_profile.php
@@ -33,6 +33,7 @@ class Ostatus_profile extends Memcached_DataObject
public $feeduri;
public $salmonuri;
+ public $avatar; // remote URL of the last avatar we saved
public $created;
public $modified;
@@ -58,6 +59,7 @@ class Ostatus_profile extends Memcached_DataObject
'group_id' => DB_DATAOBJECT_INT,
'feeduri' => DB_DATAOBJECT_STR,
'salmonuri' => DB_DATAOBJECT_STR,
+ 'avatar' => DB_DATAOBJECT_STR,
'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
}
@@ -74,6 +76,8 @@ class Ostatus_profile extends Memcached_DataObject
255, true, 'UNI'),
new ColumnDef('salmonuri', 'text',
null, true),
+ new ColumnDef('avatar', 'text',
+ null, true),
new ColumnDef('created', 'datetime',
null, false),
new ColumnDef('modified', 'datetime',
@@ -488,7 +492,7 @@ class Ostatus_profile extends Memcached_DataObject
*
* @param DOMDocument $feed
*/
- public function processFeed($feed)
+ public function processFeed($feed, $source)
{
$entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
if ($entries->length == 0) {
@@ -498,7 +502,7 @@ class Ostatus_profile extends Memcached_DataObject
for ($i = 0; $i < $entries->length; $i++) {
$entry = $entries->item($i);
- $this->processEntry($entry, $feed);
+ $this->processEntry($entry, $feed, $source);
}
}
@@ -508,15 +512,12 @@ class Ostatus_profile extends Memcached_DataObject
* @param DOMElement $entry
* @param DOMElement $feed for context
*/
- protected function processEntry($entry, $feed)
+ public function processEntry($entry, $feed, $source)
{
$activity = new Activity($entry, $feed);
- $debug = var_export($activity, true);
- common_log(LOG_DEBUG, $debug);
-
if ($activity->verb == ActivityVerb::POST) {
- $this->processPost($activity);
+ $this->processPost($activity, $source);
} else {
common_log(LOG_INFO, "Ignoring activity with unrecognized verb $activity->verb");
}
@@ -525,130 +526,190 @@ class Ostatus_profile extends Memcached_DataObject
/**
* Process an incoming post activity from this remote feed.
* @param Activity $activity
+ * @param string $method 'push' or 'salmon'
+ * @return mixed saved Notice or false
* @fixme break up this function, it's getting nasty long
*/
- protected function processPost($activity)
+ public function processPost($activity, $method)
{
if ($this->isGroup()) {
+ // A group feed will contain posts from multiple authors.
// @fixme validate these profiles in some way!
$oprofile = self::ensureActorProfile($activity);
+ if ($oprofile->isGroup()) {
+ // Groups can't post notices in StatusNet.
+ common_log(LOG_WARNING, "OStatus: skipping post with group listed as author: $oprofile->uri in feed from $this->uri");
+ return false;
+ }
} else {
+ // Individual user feeds may contain only posts from themselves.
+ // Authorship is validated against the profile URI on upper layers,
+ // through PuSH setup or Salmon signature checks.
$actorUri = self::getActorProfileURI($activity);
if ($actorUri == $this->uri) {
- // @fixme check if profile info has changed and update it
+ // Check if profile info has changed and update it
+ $this->updateFromActivityObject($activity->actor);
} else {
- // @fixme drop or reject the messages once we've got the canonical profile URI recorded sanely
- common_log(LOG_INFO, "OStatus: Warning: non-group post with unexpected author: $actorUri expected $this->uri");
- //return;
+ common_log(LOG_WARNING, "OStatus: skipping post with bad author: got $actorUri expected $this->uri");
+ return false;
}
$oprofile = $this;
}
- $sourceUri = $activity->object->id;
+ // The id URI will be used as a unique identifier for for the notice,
+ // protecting against duplicate saves. It isn't required to be a URL;
+ // tag: URIs for instance are found in Google Buzz feeds.
+ $sourceUri = $activity->object->id;
$dupe = Notice::staticGet('uri', $sourceUri);
-
if ($dupe) {
common_log(LOG_INFO, "OStatus: ignoring duplicate post: $sourceUri");
- return;
+ return false;
}
+ // We'll also want to save a web link to the original notice, if provided.
$sourceUrl = null;
-
if ($activity->object->link) {
$sourceUrl = $activity->object->link;
+ } else if ($activity->link) {
+ $sourceUrl = $activity->link;
} else if (preg_match('!^https?://!', $activity->object->id)) {
$sourceUrl = $activity->object->id;
}
- // @fixme sanitize and save HTML content if available
+ // Get (safe!) HTML and text versions of the content
+ $rendered = $this->purify($activity->object->content);
+ $content = html_entity_decode(strip_tags($rendered));
- $content = $activity->object->title;
-
- $params = array('is_local' => Notice::REMOTE_OMB,
+ $options = array('is_local' => Notice::REMOTE_OMB,
'url' => $sourceUrl,
- 'uri' => $sourceUri);
+ 'uri' => $sourceUri,
+ 'rendered' => $rendered,
+ 'replies' => array(),
+ 'groups' => array());
- $location = $activity->context->location;
+ // Check for optional attributes...
- if ($location) {
- $params['lat'] = $location->lat;
- $params['lon'] = $location->lon;
- if ($location->location_id) {
- $params['location_ns'] = $location->location_ns;
- $params['location_id'] = $location->location_id;
- }
+ if (!empty($activity->time)) {
+ $options['created'] = common_sql_date($activity->time);
}
- $profile = $oprofile->localProfile();
- $params['groups'] = array();
- $params['replies'] = array();
if ($activity->context) {
- foreach ($activity->context->attention as $recipient) {
- $roprofile = Ostatus_profile::staticGet('uri', $recipient);
- if ($roprofile) {
- if ($roprofile->isGroup()) {
- // Deliver to local recipients of this remote group.
- // @fixme sender verification?
- $params['groups'][] = $roprofile->group_id;
- continue;
- } else {
- // Delivery to remote users is the source service's job.
- continue;
- }
- }
-
- $user = User::staticGet('uri', $recipient);
- if ($user) {
- // An @-reply directed to a local user.
- // @fixme sender verification, spam etc?
- $params['replies'][] = $recipient;
- continue;
+ // Any individual or group attn: targets?
+ $replies = $activity->context->attention;
+ $options['groups'] = $this->filterReplies($oprofile, $replies);
+ $options['replies'] = $replies;
+
+ // Maintain direct reply associations
+ // @fixme what about conversation ID?
+ if (!empty($activity->context->replyToID)) {
+ $orig = Notice::staticGet('uri',
+ $activity->context->replyToID);
+ if (!empty($orig)) {
+ $options['reply_to'] = $orig->id;
}
-
- // @fixme we need a uri on user_group
- // $group = User_group::staticGet('uri', $recipient);
- $template = common_local_url('groupbyid', array('id' => '31337'));
- $template = preg_quote($template, '/');
- $template = str_replace('31337', '(\d+)', $template);
- common_log(LOG_DEBUG, $template);
- if (preg_match("/$template/", $recipient, $matches)) {
- $id = $matches[1];
- $group = User_group::staticGet('id', $id);
- if ($group) {
- // Deliver to all members of this local group.
- // @fixme sender verification?
- if ($profile->isMember($group)) {
- common_log(LOG_DEBUG, "delivering to group $id $group->nickname");
- $params['groups'][] = $group->id;
- } else {
- common_log(LOG_DEBUG, "not delivering to group $id $group->nickname because sender $profile->nickname is not a member");
- }
- continue;
- } else {
- common_log(LOG_DEBUG, "not delivering to missing group $id");
- }
- } else {
- common_log(LOG_DEBUG, "not delivering to groups for $recipient");
+ }
+
+ $location = $activity->context->location;
+ if ($location) {
+ $options['lat'] = $location->lat;
+ $options['lon'] = $location->lon;
+ if ($location->location_id) {
+ $options['location_ns'] = $location->location_ns;
+ $options['location_id'] = $location->location_id;
}
}
}
try {
- $saved = Notice::saveNew($profile->id,
+ $saved = Notice::saveNew($oprofile->profile_id,
$content,
'ostatus',
- $params);
+ $options);
+ if ($saved) {
+ Ostatus_source::saveNew($saved, $this, $method);
+ }
} catch (Exception $e) {
- common_log(LOG_ERR, "Failed saving notice entry for $sourceUri: " . $e->getMessage());
- return;
+ common_log(LOG_ERR, "OStatus save of remote message $sourceUri failed: " . $e->getMessage());
+ throw $e;
}
+ common_log(LOG_INFO, "OStatus saved remote message $sourceUri as notice id $saved->id");
+ return $saved;
+ }
+
+ /**
+ * Clean up HTML
+ */
+ protected function purify($html)
+ {
+ // @fixme disable caching or set a sane temp dir
+ require_once(INSTALLDIR.'/extlib/HTMLPurifier/HTMLPurifier.auto.php');
+ $purifier = new HTMLPurifier();
+ return $purifier->purify($html);
+ }
+
+ /**
+ * Filters a list of recipient ID URIs to just those for local delivery.
+ * @param Ostatus_profile local profile of sender
+ * @param array in/out &$attention_uris set of URIs, will be pruned on output
+ * @return array of group IDs
+ */
+ protected function filterReplies($sender, &$attention_uris)
+ {
+ common_log(LOG_DEBUG, "Original reply recipients: " . implode(', ', $attention_uris));
+ $groups = array();
+ $replies = array();
+ foreach ($attention_uris as $recipient) {
+ // Is the recipient a local user?
+ $user = User::staticGet('uri', $recipient);
+ if ($user) {
+ // @fixme sender verification, spam etc?
+ $replies[] = $recipient;
+ continue;
+ }
+
+ // Is the recipient a remote group?
+ $oprofile = Ostatus_profile::staticGet('uri', $recipient);
+ if ($oprofile) {
+ if ($oprofile->isGroup()) {
+ // Deliver to local members of this remote group.
+ // @fixme sender verification?
+ $groups[] = $oprofile->group_id;
+ } else {
+ common_log(LOG_DEBUG, "Skipping reply to remote profile $recipient");
+ }
+ continue;
+ }
+
+ // Is the recipient a local group?
+ // @fixme we need a uri on user_group
+ // $group = User_group::staticGet('uri', $recipient);
+ $template = common_local_url('groupbyid', array('id' => '31337'));
+ $template = preg_quote($template, '/');
+ $template = str_replace('31337', '(\d+)', $template);
+ if (preg_match("/$template/", $recipient, $matches)) {
+ $id = $matches[1];
+ $group = User_group::staticGet('id', $id);
+ if ($group) {
+ // Deliver to all members of this local group if allowed.
+ $profile = $sender->localProfile();
+ if ($profile->isMember($group)) {
+ $groups[] = $group->id;
+ } else {
+ common_log(LOG_DEBUG, "Skipping reply to local group $group->nickname as sender $profile->id is not a member");
+ }
+ continue;
+ } else {
+ common_log(LOG_DEBUG, "Skipping reply to bogus group $recipient");
+ }
+ }
+
+ common_log(LOG_DEBUG, "Skipping reply to unrecognized profile $recipient");
- // Record which feed this came through...
- try {
- Ostatus_source::saveNew($saved, $this, 'push');
- } catch (Exception $e) {
- common_log(LOG_ERR, "Failed saving ostatus_source entry for $saved->notice_id: " . $e->getMessage());
}
+ $attention_uris = $replies;
+ common_log(LOG_DEBUG, "Local reply recipients: " . implode(', ', $replies));
+ common_log(LOG_DEBUG, "Local group recipients: " . implode(', ', $groups));
+ return $groups;
}
/**
@@ -729,6 +790,11 @@ class Ostatus_profile extends Memcached_DataObject
*/
protected function updateAvatar($url)
{
+ if ($url == $this->avatar) {
+ // We've already got this one.
+ return;
+ }
+
if ($this->isGroup()) {
$self = $this->localGroup();
} else {
@@ -760,12 +826,28 @@ class Ostatus_profile extends Memcached_DataObject
common_timestamp());
rename($temp_filename, Avatar::path($filename));
$self->setOriginal($filename);
+
+ $orig = clone($this);
+ $this->avatar = $url;
+ $this->update($orig);
}
- protected static function getActivityObjectAvatar($object)
+ /**
+ * Pull avatar URL from ActivityObject or profile hints
+ *
+ * @param ActivityObject $object
+ * @param array $hints
+ * @return mixed URL string or false
+ */
+
+ protected static function getActivityObjectAvatar($object, $hints=array())
{
- // XXX: go poke around in the feed
- return $object->avatar;
+ if ($object->avatar) {
+ return $object->avatar;
+ } else if (array_key_exists('avatar', $hints)) {
+ return $hints['avatar'];
+ }
+ return false;
}
/**
@@ -832,7 +914,9 @@ class Ostatus_profile extends Memcached_DataObject
public static function ensureActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array())
{
$profile = self::getActivityObjectProfile($object);
- if (!$profile) {
+ if ($profile) {
+ $profile->updateFromActivityObject($object, $hints);
+ } else {
$profile = self::createActivityObjectProfile($object, $feeduri, $salmonuri, $hints);
}
return $profile;
@@ -901,8 +985,6 @@ class Ostatus_profile extends Memcached_DataObject
protected static function createActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array())
{
$homeuri = $object->id;
- $nickname = self::getActivityObjectNickname($object, $hints);
- $avatar = self::getActivityObjectAvatar($object);
if (!$homeuri) {
common_log(LOG_DEBUG, __METHOD__ . " empty actor profile URI: " . var_export($activity, true));
@@ -946,43 +1028,19 @@ class Ostatus_profile extends Memcached_DataObject
if ($object->type == ActivityObject::PERSON) {
$profile = new Profile();
- $profile->nickname = $nickname;
- $profile->fullname = $object->title;
- if (!empty($object->link)) {
- $profile->profileurl = $object->link;
- } else if (array_key_exists('profileurl', $hints)) {
- $profile->profileurl = $hints['profileurl'];
- }
- $profile->created = common_sql_now();
-
- // @fixme bio
- // @fixme tags/categories
- // @fixme location?
- // @todo tags from categories
- // @todo lat/lon/location?
-
+ self::updateProfile($profile, $object, $hints);
+ $profile->created = common_sql_now();
+
$oprofile->profile_id = $profile->insert();
-
if (!$oprofile->profile_id) {
throw new ServerException("Can't save local profile");
}
} else {
$group = new User_group();
- $group->nickname = $nickname;
- $group->fullname = $object->title;
- // @fixme no canonical profileurl; using homepage instead for now
- $group->homepage = $homeuri;
$group->created = common_sql_now();
-
- // @fixme homepage
- // @fixme bio
- // @fixme tags/categories
- // @fixme location?
- // @todo tags from categories
- // @todo lat/lon/location?
+ self::updateGroup($group, $object, $hints);
$oprofile->group_id = $group->insert();
-
if (!$oprofile->group_id) {
throw new ServerException("Can't save local profile");
}
@@ -991,6 +1049,7 @@ class Ostatus_profile extends Memcached_DataObject
$ok = $oprofile->insert();
if ($ok) {
+ $avatar = self::getActivityObjectAvatar($object, $hints);
if ($avatar) {
$oprofile->updateAvatar($avatar);
}
@@ -1000,8 +1059,82 @@ class Ostatus_profile extends Memcached_DataObject
}
}
+ /**
+ * Save any updated profile information to our local copy.
+ * @param ActivityObject $object
+ * @param array $hints
+ */
+ protected function updateFromActivityObject($object, $hints=array())
+ {
+ if ($this->isGroup()) {
+ $group = $this->localGroup();
+ self::updateGroup($group, $object, $hints);
+ } else {
+ $profile = $this->localProfile();
+ self::updateProfile($profile, $object, $hints);
+ }
+ $avatar = self::getActivityObjectAvatar($object, $hints);
+ if ($avatar) {
+ $this->updateAvatar($avatar);
+ }
+ }
+
+ protected static function updateProfile($profile, $object, $hints=array())
+ {
+ $orig = clone($profile);
+
+ $profile->nickname = self::getActivityObjectNickname($object, $hints);
+ $profile->fullname = $object->title;
+ if (!empty($object->link)) {
+ $profile->profileurl = $object->link;
+ } else if (array_key_exists('profileurl', $hints)) {
+ $profile->profileurl = $hints['profileurl'];
+ }
+
+ // @fixme bio
+ // @fixme tags/categories
+ // @fixme location?
+ // @todo tags from categories
+ // @todo lat/lon/location?
+
+ if ($profile->id) {
+ common_log(LOG_DEBUG, "Updating OStatus profile $profile->id from remote info $object->id: " . var_export($object, true) . var_export($hints, true));
+ $profile->update($orig);
+ }
+ }
+
+ protected static function updateGroup($group, $object, $hints=array())
+ {
+ $orig = clone($group);
+
+ // @fixme need to make nick unique etc *hack hack*
+ $group->nickname = self::getActivityObjectNickname($object, $hints);
+ $group->fullname = $object->title;
+
+ // @fixme no canonical profileurl; using homepage instead for now
+ $group->homepage = $object->id;
+
+ // @fixme homepage
+ // @fixme bio
+ // @fixme tags/categories
+ // @fixme location?
+ // @todo tags from categories
+ // @todo lat/lon/location?
+
+ if ($group->id) {
+ common_log(LOG_DEBUG, "Updating OStatus group $group->id from remote info $object->id: " . var_export($object, true) . var_export($hints, true));
+ $group->update($orig);
+ }
+ }
+
+
protected static function getActivityObjectNickname($object, $hints=array())
{
+ if ($object->poco) {
+ if (!empty($object->poco->preferredUsername)) {
+ return common_nicknamize($object->poco->preferredUsername);
+ }
+ }
if (!empty($object->nickname)) {
return common_nicknamize($object->nickname);
}
diff --git a/plugins/OStatus/js/ostatus.js b/plugins/OStatus/js/ostatus.js
index 1fc44b21b..bd29b5c0c 100644
--- a/plugins/OStatus/js/ostatus.js
+++ b/plugins/OStatus/js/ostatus.js
@@ -24,35 +24,9 @@
* @note Everything in here should eventually migrate over to /js/util.js's SN.
*/
-SN.C.S.StatusNetInstance = 'StatusNetInstance';
-
-SN.U.StatusNetInstance = {
- Set: function(value) {
- $.cookie(
- SN.C.S.StatusNetInstance,
- JSON.stringify(value),
- {
- path: '/',
- expires: SN.U.GetFullYear(2029, 0, 1)
- });
- },
-
- Get: function() {
- var cookieValue = $.cookie(SN.C.S.StatusNetInstance);
- if (cookieValue !== null) {
- return JSON.parse(cookieValue);
- }
- return null;
- },
-
- Delete: function() {
- $.cookie(SN.C.S.StatusNetInstance, null);
- }
-};
-
SN.Init.OStatusCookie = function() {
if (SN.U.StatusNetInstance.Get() === null) {
- SN.U.StatusNetInstance.Set({profile: null});
+ SN.U.StatusNetInstance.Set({RemoteProfile: null});
}
};
@@ -101,10 +75,10 @@ SN.U.DialogBox = {
if (form.attr('id') == 'form_ostatus_connect') {
SN.Init.OStatusCookie();
- form.find('#profile').val(SN.U.StatusNetInstance.Get().profile);
+ form.find('#profile').val(SN.U.StatusNetInstance.Get().RemoteProfile);
form.find("[type=submit]").bind('click', function() {
- SN.U.StatusNetInstance.Set({profile: form.find('#profile').val()});
+ SN.U.StatusNetInstance.Set({RemoteProfile: form.find('#profile').val()});
return true;
});
}
@@ -123,4 +97,6 @@ SN.Init.Subscribe = function() {
$(document).ready(function() {
SN.Init.Subscribe();
+
+ $('.form_remote_authorize').bind('submit', function() { $(this).addClass(SN.C.S.Processing); return true; });
});
diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/ostatusqueuehandler.php
index c2bd630f9..c1e50bffa 100644
--- a/plugins/OStatus/lib/hubdistribqueuehandler.php
+++ b/plugins/OStatus/lib/ostatusqueuehandler.php
@@ -18,46 +18,89 @@
*/
/**
- * Send a PuSH subscription verification from our internal hub.
- * Queue up final distribution for
- * @package Hub
+ * Prepare PuSH and Salmon distributions for an outgoing message.
+ *
+ * @package OStatusPlugin
* @author Brion Vibber <brion@status.net>
*/
-class HubDistribQueueHandler extends QueueHandler
+class OStatusQueueHandler extends QueueHandler
{
function transport()
{
- return 'hubdistrib';
+ return 'ostatus';
}
function handle($notice)
{
assert($notice instanceof Notice);
- $this->pushUser($notice);
+ $this->notice = $notice;
+ $this->user = User::staticGet($notice->profile_id);
+
+ $this->pushUser();
+
foreach ($notice->getGroups() as $group) {
- $this->pushGroup($notice, $group->id);
+ $oprofile = Ostatus_profile::staticGet('group_id', $group->id);
+ if ($oprofile) {
+ $this->pingReply($oprofile);
+ } else {
+ $this->pushGroup($group->id);
+ }
}
+
+ foreach ($notice->getReplies() as $profile_id) {
+ $oprofile = Ostatus_profile::staticGet('profile_id', $profile_id);
+ if ($oprofile) {
+ $this->pingReply($oprofile);
+ }
+ }
+
return true;
}
-
- function pushUser($notice)
+
+ function pushUser()
{
- // See if there's any PuSH subscriptions, including OStatus clients.
- // @fixme handle group subscriptions as well
- // http://identi.ca/api/statuses/user_timeline/1.atom
- $feed = common_local_url('ApiTimelineUser',
- array('id' => $notice->profile_id,
- 'format' => 'atom'));
- $this->pushFeed($feed, array($this, 'userFeedForNotice'), $notice);
+ if ($this->user) {
+ // For local posts, ping the PuSH hub to update their feed.
+ // http://identi.ca/api/statuses/user_timeline/1.atom
+ $feed = common_local_url('ApiTimelineUser',
+ array('id' => $this->user->id,
+ 'format' => 'atom'));
+ $this->pushFeed($feed, array($this, 'userFeedForNotice'));
+ }
}
- function pushGroup($notice, $group_id)
+ function pushGroup($group_id)
{
+ // For a local group, ping the PuSH hub to update its feed.
+ // Updates may come from either a local or a remote user.
$feed = common_local_url('ApiTimelineGroup',
array('id' => $group_id,
'format' => 'atom'));
- $this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group_id, $notice);
+ $this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group_id);
+ }
+
+ function pingReply($oprofile)
+ {
+ if ($this->user) {
+ if (!empty($oprofile->salmonuri)) {
+ // For local posts, send a Salmon ping to the mentioned
+ // remote user or group.
+ // @fixme as an optimization we can skip this if the
+ // remote profile is subscribed to the author.
+
+ common_log(LOG_INFO, "Prepping to send notice '{$this->notice->uri}' to remote profile '{$oprofile->uri}'.");
+
+ $xml = '<?xml version="1.0" encoding="UTF-8" ?' . '>';
+ $xml .= $this->notice->asAtomEntry(true, true);
+
+ $data = array('salmonuri' => $oprofile->salmonuri,
+ 'entry' => $xml);
+
+ $qm = QueueManager::get();
+ $qm->enqueue($data, 'salmonout');
+ }
+ }
}
/**
@@ -122,7 +165,6 @@ class HubDistribQueueHandler extends QueueHandler
function pushFeedInternal($atom, $sub)
{
common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $sub->topic");
- $qm = QueueManager::get();
while ($sub->fetch()) {
$sub->distribute($atom);
}
@@ -130,20 +172,19 @@ class HubDistribQueueHandler extends QueueHandler
/**
* Build a single-item version of the sending user's Atom feed.
- * @param Notice $notice
* @return string
*/
- function userFeedForNotice($notice)
+ function userFeedForNotice()
{
// @fixme this feels VERY hacky...
// should probably be a cleaner way to do it
ob_start();
$api = new ApiTimelineUserAction();
- $api->prepare(array('id' => $notice->profile_id,
+ $api->prepare(array('id' => $this->notice->profile_id,
'format' => 'atom',
- 'max_id' => $notice->id,
- 'since_id' => $notice->id - 1));
+ 'max_id' => $this->notice->id,
+ 'since_id' => $this->notice->id - 1));
$api->showTimeline();
$feed = ob_get_clean();
@@ -155,7 +196,7 @@ class HubDistribQueueHandler extends QueueHandler
return $feed;
}
- function groupFeedForNotice($group_id, $notice)
+ function groupFeedForNotice($group_id)
{
// @fixme this feels VERY hacky...
// should probably be a cleaner way to do it
@@ -164,8 +205,8 @@ class HubDistribQueueHandler extends QueueHandler
$api = new ApiTimelineGroupAction();
$args = array('id' => $group_id,
'format' => 'atom',
- 'max_id' => $notice->id,
- 'since_id' => $notice->id - 1);
+ 'max_id' => $this->notice->id,
+ 'since_id' => $this->notice->id - 1);
$api->prepare($args);
$api->handle($args);
$feed = ob_get_clean();
diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php
index 83cf0b8f8..9aac2ed52 100644
--- a/plugins/OStatus/lib/salmonaction.php
+++ b/plugins/OStatus/lib/salmonaction.php
@@ -185,54 +185,6 @@ class SalmonAction extends Action
function saveNotice()
{
$oprofile = $this->ensureProfile();
-
- // Get (safe!) HTML and text versions of the content
-
- require_once(INSTALLDIR.'/extlib/HTMLPurifier/HTMLPurifier.auto.php');
-
- $html = $this->act->object->content;
-
- $purifier = new HTMLPurifier();
-
- $rendered = $purifier->purify($html);
-
- $content = html_entity_decode(strip_tags($rendered));
-
- $options = array('is_local' => Notice::REMOTE_OMB,
- 'uri' => $this->act->object->id,
- 'url' => $this->act->object->link,
- 'rendered' => $rendered,
- 'replies' => $this->act->context->attention);
-
- if (!empty($this->act->context->location)) {
- $options['lat'] = $location->lat;
- $options['lon'] = $location->lon;
- if ($location->location_id) {
- $options['location_ns'] = $location->location_ns;
- $options['location_id'] = $location->location_id;
- }
- }
-
- if (!empty($this->act->context->replyToID)) {
- $orig = Notice::staticGet('uri',
- $this->act->context->replyToID);
- if (!empty($orig)) {
- $options['reply_to'] = $orig->id;
- }
- }
-
- if (!empty($this->act->time)) {
- $options['created'] = common_sql_date($this->act->time);
- }
-
- $saved = Notice::saveNew($oprofile->profile_id,
- $content,
- 'ostatus+salmon',
- $options);
-
- // Record that this was saved through a validated Salmon source
- // @fixme actually do the signature validation!
- Ostatus_source::saveNew($saved, $oprofile, 'salmon');
- return $saved;
+ return $oprofile->processPost($this->act, 'salmon');
}
}
diff --git a/plugins/OStatus/lib/salmonoutqueuehandler.php b/plugins/OStatus/lib/salmonoutqueuehandler.php
new file mode 100644
index 000000000..536ff94af
--- /dev/null
+++ b/plugins/OStatus/lib/salmonoutqueuehandler.php
@@ -0,0 +1,44 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Send a Salmon notification in the background.
+ * @package OStatusPlugin
+ * @author Brion Vibber <brion@status.net>
+ */
+class SalmonOutQueueHandler extends QueueHandler
+{
+ function transport()
+ {
+ return 'salmonout';
+ }
+
+ function handle($data)
+ {
+ assert(is_array($data));
+ assert(is_string($data['salmonuri']));
+ assert(is_string($data['entry']));
+
+ $salmon = new Salmon();
+ $salmon->post($data['salmonuri'], $data['entry']);
+
+ // @fixme detect failure and attempt to resend
+ return true;
+ }
+}
diff --git a/plugins/OStatus/tests/ActivityParseTests.php b/plugins/OStatus/tests/ActivityParseTests.php
deleted file mode 100644
index d7305dede..000000000
--- a/plugins/OStatus/tests/ActivityParseTests.php
+++ /dev/null
@@ -1,209 +0,0 @@
-<?php
-
-if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
- print "This script must be run from the command line\n";
- exit();
-}
-
-// XXX: we should probably have some common source for this stuff
-
-define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
-define('STATUSNET', true);
-
-require_once INSTALLDIR . '/lib/common.php';
-require_once INSTALLDIR . '/plugins/OStatus/lib/activity.php';
-
-class ActivityParseTests extends PHPUnit_Framework_TestCase
-{
- public function testExample1()
- {
- global $_example1;
- $dom = DOMDocument::loadXML($_example1);
- $act = new Activity($dom->documentElement);
-
- $this->assertFalse(empty($act));
-
- $this->assertEquals($act->time, 1243860840);
- $this->assertEquals($act->verb, ActivityVerb::POST);
-
- $this->assertFalse(empty($act->object));
- $this->assertEquals($act->object->title, 'Punctuation Changeset');
- $this->assertEquals($act->object->type, 'http://versioncentral.example.org/activity/changeset');
- $this->assertEquals($act->object->summary, 'Fixing punctuation because it makes it more readable.');
- $this->assertEquals($act->object->id, 'tag:versioncentral.example.org,2009:/change/1643245');
- }
-
- public function testExample3()
- {
- global $_example3;
- $dom = DOMDocument::loadXML($_example3);
-
- $feed = $dom->documentElement;
-
- $entries = $feed->getElementsByTagName('entry');
-
- $entry = $entries->item(0);
-
- $act = new Activity($entry, $feed);
-
- $this->assertFalse(empty($act));
- $this->assertEquals($act->time, 1071340202);
- $this->assertEquals($act->link, 'http://example.org/2003/12/13/atom03.html');
-
- $this->assertEquals($act->verb, ActivityVerb::POST);
-
- $this->assertFalse(empty($act->actor));
- $this->assertEquals($act->actor->type, ActivityObject::PERSON);
- $this->assertEquals($act->actor->title, 'John Doe');
- $this->assertEquals($act->actor->id, 'mailto:johndoe@example.com');
-
- $this->assertFalse(empty($act->object));
- $this->assertEquals($act->object->type, ActivityObject::NOTE);
- $this->assertEquals($act->object->id, 'urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a');
- $this->assertEquals($act->object->title, 'Atom-Powered Robots Run Amok');
- $this->assertEquals($act->object->summary, 'Some text.');
- $this->assertEquals($act->object->link, 'http://example.org/2003/12/13/atom03.html');
-
- $this->assertFalse(empty($act->context));
-
- $this->assertTrue(empty($act->target));
-
- $this->assertEquals($act->entry, $entry);
- $this->assertEquals($act->feed, $feed);
- }
-
- public function testExample4()
- {
- global $_example4;
- $dom = DOMDocument::loadXML($_example4);
-
- $entry = $dom->documentElement;
-
- $act = new Activity($entry);
-
- $this->assertFalse(empty($act));
- $this->assertEquals(1266547958, $act->time);
- $this->assertEquals('http://example.net/notice/14', $act->link);
-
- $this->assertFalse(empty($act->context));
- $this->assertEquals('http://example.net/notice/12', $act->context->replyToID);
- $this->assertEquals('http://example.net/notice/12', $act->context->replyToUrl);
- $this->assertEquals('http://example.net/conversation/11', $act->context->conversation);
- $this->assertEquals(array('http://example.net/user/1'), $act->context->attention);
-
- $this->assertFalse(empty($act->object));
- $this->assertEquals($act->object->content,
- '@<span class="vcard"><a href="http://example.net/user/1" class="url"><span class="fn nickname">evan</span></a></span> now is the time for all good men to come to the aid of their country. #<span class="tag"><a href="http://example.net/tag/thetime" rel="tag">thetime</a></span>');
-
- $this->assertFalse(empty($act->actor));
- }
-}
-
-$_example1 = <<<EXAMPLE1
-<?xml version='1.0' encoding='UTF-8'?>
-<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'>
- <id>tag:versioncentral.example.org,2009:/commit/1643245</id>
- <published>2009-06-01T12:54:00Z</published>
- <title>Geraldine committed a change to yate</title>
- <content type="xhtml">Geraldine just committed a change to yate on VersionCentral</content>
- <link rel="alternate" type="text/html"
- href="http://versioncentral.example.org/geraldine/yate/commit/1643245" />
- <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
- <activity:verb>http://versioncentral.example.org/activity/commit</activity:verb>
- <activity:object>
- <activity:object-type>http://versioncentral.example.org/activity/changeset</activity:object-type>
- <id>tag:versioncentral.example.org,2009:/change/1643245</id>
- <title>Punctuation Changeset</title>
- <summary>Fixing punctuation because it makes it more readable.</summary>
- <link rel="alternate" type="text/html" href="..." />
- </activity:object>
-</entry>
-EXAMPLE1;
-
-$_example2 = <<<EXAMPLE2
-<?xml version='1.0' encoding='UTF-8'?>
-<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'>
- <id>tag:photopanic.example.com,2008:activity01</id>
- <title>Geraldine posted a Photo on PhotoPanic</title>
- <published>2008-11-02T15:29:00Z</published>
- <link rel="alternate" type="text/html" href="/geraldine/activities/1" />
- <activity:verb>
- http://activitystrea.ms/schema/1.0/post
- </activity:verb>
- <activity:object>
- <id>tag:photopanic.example.com,2008:photo01</id>
- <title>My Cat</title>
- <published>2008-11-02T15:29:00Z</published>
- <link rel="alternate" type="text/html" href="/geraldine/photos/1" />
- <activity:object-type>
- tag:atomactivity.example.com,2008:photo
- </activity:object-type>
- <source>
- <title>Geraldine's Photos</title>
- <link rel="self" type="application/atom+xml" href="/geraldine/photofeed.xml" />
- <link rel="alternate" type="text/html" href="/geraldine/" />
- </source>
- </activity:object>
- <content type="html">
- &lt;p&gt;Geraldine posted a Photo on PhotoPanic&lt;/p&gt;
- &lt;img src="/geraldine/photo1.jpg"&gt;
- </content>
-</entry>
-EXAMPLE2;
-
-$_example3 = <<<EXAMPLE3
-<?xml version="1.0" encoding="utf-8"?>
-
-<feed xmlns="http://www.w3.org/2005/Atom">
-
- <title>Example Feed</title>
- <subtitle>A subtitle.</subtitle>
- <link href="http://example.org/feed/" rel="self" />
- <link href="http://example.org/" />
- <id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
- <updated>2003-12-13T18:30:02Z</updated>
- <author>
- <name>John Doe</name>
- <email>johndoe@example.com</email>
- </author>
-
- <entry>
- <title>Atom-Powered Robots Run Amok</title>
- <link href="http://example.org/2003/12/13/atom03" />
- <link rel="alternate" type="text/html" href="http://example.org/2003/12/13/atom03.html"/>
- <link rel="edit" href="http://example.org/2003/12/13/atom03/edit"/>
- <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
- <updated>2003-12-13T18:30:02Z</updated>
- <summary>Some text.</summary>
- </entry>
-
-</feed>
-EXAMPLE3;
-
-$_example4 = <<<EXAMPLE4
-<?xml version='1.0' encoding='UTF-8'?>
-<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:ostatus="http://ostatus.org/schema/1.0">
- <title>@evan now is the time for all good men to come to the aid of their country. #thetime</title>
- <summary>@evan now is the time for all good men to come to the aid of their country. #thetime</summary>
-<author>
- <name>spock</name>
- <uri>http://example.net/user/2</uri>
-</author>
-<activity:actor>
- <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
- <id>http://example.net/user/2</id>
- <title>spock</title>
- <link type="image/png" rel="avatar" href="http://example.net/theme/identica/default-avatar-profile.png"></link>
-</activity:actor>
- <link rel="alternate" type="text/html" href="http://example.net/notice/14"/>
- <id>http://example.net/notice/14</id>
- <published>2010-02-19T02:52:38+00:00</published>
- <updated>2010-02-19T02:52:38+00:00</updated>
- <link rel="related" href="http://example.net/notice/12"/>
- <thr:in-reply-to ref="http://example.net/notice/12" href="http://example.net/notice/12"></thr:in-reply-to>
- <link rel="ostatus:conversation" href="http://example.net/conversation/11"/>
- <link rel="ostatus:attention" href="http://example.net/user/1"/>
- <content type="html">@&lt;span class=&quot;vcard&quot;&gt;&lt;a href=&quot;http://example.net/user/1&quot; class=&quot;url&quot;&gt;&lt;span class=&quot;fn nickname&quot;&gt;evan&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; now is the time for all good men to come to the aid of their country. #&lt;span class=&quot;tag&quot;&gt;&lt;a href=&quot;http://example.net/tag/thetime&quot; rel=&quot;tag&quot;&gt;thetime&lt;/a&gt;&lt;/span&gt;</content>
- <category term="thetime"></category>
-</entry>
-EXAMPLE4;