Merge branch 'testing' of git@gitorious.org:statusnet/mainline into testing

This commit is contained in:
Zach Copley 2010-02-24 06:37:23 +00:00
commit d4b47eb699
6 changed files with 272 additions and 81 deletions

View File

@ -34,6 +34,7 @@ if (!defined('STATUSNET')) {
class PoCoURL class PoCoURL
{ {
const URLS = 'urls';
const TYPE = 'type'; const TYPE = 'type';
const VALUE = 'value'; const VALUE = 'value';
const PRIMARY = 'primary'; const PRIMARY = 'primary';
@ -55,7 +56,7 @@ class PoCoURL
$xs->elementStart('poco:urls'); $xs->elementStart('poco:urls');
$xs->element('poco:type', null, $this->type); $xs->element('poco:type', null, $this->type);
$xs->element('poco:value', null, $this->value); $xs->element('poco:value', null, $this->value);
if ($this->primary) { if (!empty($this->primary)) {
$xs->element('poco:primary', null, 'true'); $xs->element('poco:primary', null, 'true');
} }
$xs->elementEnd('poco:urls'); $xs->elementEnd('poco:urls');
@ -70,22 +71,20 @@ class PoCoAddress
public $formatted; public $formatted;
function __construct($formatted) // @todo Other address fields
{
if (empty($formatted)) {
return null;
}
$this->formatted = $formatted;
}
function asString() function asString()
{ {
if (!empty($this->formatted)) {
$xs = new XMLStringer(true); $xs = new XMLStringer(true);
$xs->elementStart('poco:address'); $xs->elementStart('poco:address');
$xs->element('poco:formatted', null, $this->formatted); $xs->element('poco:formatted', null, $this->formatted);
$xs->elementEnd('poco:address'); $xs->elementEnd('poco:address');
return $xs->getString(); return $xs->getString();
} }
return null;
}
} }
class PoCo class PoCo
@ -93,25 +92,116 @@ class PoCo
const NS = 'http://portablecontacts.net/spec/1.0'; const NS = 'http://portablecontacts.net/spec/1.0';
const USERNAME = 'preferredUsername'; const USERNAME = 'preferredUsername';
const DISPLAYNAME = 'displayName';
const NOTE = 'note'; const NOTE = 'note';
const URLS = 'urls';
public $preferredUsername; public $preferredUsername;
public $displayName;
public $note; public $note;
public $address; public $address;
public $urls = array(); public $urls = array();
function __construct($profile) function __construct($element = null)
{ {
$this->preferredUsername = $profile->nickname; if (empty($element)) {
$this->displayName = $profile->getBestName(); return;
}
$this->note = $profile->bio; $this->preferredUsername = ActivityUtils::childContent(
$this->address = new PoCoAddress($profile->location); $element,
self::USERNAME,
self::NS
);
$this->displayName = ActivityUtils::childContent(
$element,
self::DISPLAYNAME,
self::NS
);
$this->note = ActivityUtils::childContent(
$element,
self::NOTE,
self::NS
);
$this->address = $this->_getAddress($element);
$this->urls = $this->_getURLs($element);
}
private function _getURLs($element)
{
$urlEls = $element->getElementsByTagnameNS(self::NS, PoCoURL::URLS);
$urls = array();
foreach ($urlEls as $urlEl) {
$type = ActivityUtils::childContent(
$urlEl,
PoCoURL::TYPE,
PoCo::NS
);
$value = ActivityUtils::childContent(
$urlEl,
PoCoURL::VALUE,
PoCo::NS
);
$primary = ActivityUtils::childContent(
$urlEl,
PoCoURL::PRIMARY,
PoCo::NS
);
array_push($urls, new PoCoURL($type, $value, $primary));
}
return $urls;
}
private function _getAddress($element)
{
$addressEl = ActivityUtils::child(
$element,
PoCoAddress::ADDRESS,
PoCo::NS
);
$formatted = ActivityUtils::childContent(
$addressEl,
PoCoAddress::FORMATTED,
self::NS
);
if (!empty($formatted)) {
$address = new PoCoAddress();
$address->formatted = $formatted;
return $address;
}
return null;
}
function fromProfile($profile)
{
if (empty($profile)) {
return null;
}
$poco = new PoCo();
$poco->preferredUsername = $profile->nickname;
$poco->displayName = $profile->getBestName();
$poco->note = $profile->bio;
$paddy = new PoCoAddress();
$paddy->formatted = $profile->location;
$poco->address = $paddy;
if (!empty($profile->homepage)) { if (!empty($profile->homepage)) {
array_push( array_push(
$this->urls, $poco->urls,
new PoCoURL( new PoCoURL(
'homepage', 'homepage',
$profile->homepage, $profile->homepage,
@ -119,6 +209,8 @@ class PoCo
) )
); );
} }
return $poco;
} }
function asString() function asString()
@ -381,6 +473,8 @@ class ActivityObject
public $source; public $source;
public $avatar; public $avatar;
public $geopoint; public $geopoint;
public $poco;
public $displayName;
/** /**
* Constructor * Constructor
@ -433,7 +527,6 @@ class ActivityObject
$this->link = ActivityUtils::getPermalink($element); $this->link = ActivityUtils::getPermalink($element);
// XXX: grab PoCo stuff
} }
// Some per-type attributes... // Some per-type attributes...
@ -442,7 +535,8 @@ class ActivityObject
// @fixme we may have multiple avatars with different resolutions specified // @fixme we may have multiple avatars with different resolutions specified
$this->avatar = ActivityUtils::getLink($element, 'avatar'); $this->avatar = ActivityUtils::getLink($element, 'avatar');
$this->nickname = ActivityUtils::childContent($element, PoCo::USERNAME, PoCo::NS);
$this->poco = new PoCo($element);
} }
} }
@ -497,7 +591,7 @@ class ActivityObject
$object->geopoint = (float)$profile->lat . ' ' . (float)$profile->lon; $object->geopoint = (float)$profile->lat . ' ' . (float)$profile->lon;
} }
$object->poco = new PoCo($profile); $object->poco = PoCo::fromProfile($profile);
return $object; return $object;
} }
@ -526,11 +620,19 @@ class ActivityObject
} }
if (!empty($this->link)) { if (!empty($this->link)) {
$xs->element('link', array('rel' => 'alternate', 'type' => 'text/html'), $xs->element(
$this->link); 'link',
array(
'rel' => 'alternate',
'type' => 'text/html',
'href' => $this->link
),
null
);
} }
if ($this->type == ActivityObject::PERSON) { if ($this->type == ActivityObject::PERSON
|| $this->type == ActivityObject::GROUP) {
$xs->element( $xs->element(
'link', array( 'link', array(
'type' => empty($this->avatar) ? 'image/png' : $this->avatar->mediatype, 'type' => empty($this->avatar) ? 'image/png' : $this->avatar->mediatype,
@ -539,7 +641,7 @@ class ActivityObject
? Avatar::defaultImage(AVATAR_PROFILE_SIZE) ? Avatar::defaultImage(AVATAR_PROFILE_SIZE)
: $this->avatar->displayUrl() : $this->avatar->displayUrl()
), ),
'' null
); );
} }

View File

@ -78,11 +78,16 @@ class OStatusPlugin extends Plugin
*/ */
function onEndInitializeQueueManager(QueueManager $qm) function onEndInitializeQueueManager(QueueManager $qm)
{ {
// Prepare outgoing distributions after notice save.
$qm->connect('ostatus', 'OStatusQueueHandler');
// Outgoing from our internal PuSH hub // Outgoing from our internal PuSH hub
$qm->connect('hubverify', 'HubVerifyQueueHandler'); $qm->connect('hubverify', 'HubVerifyQueueHandler');
$qm->connect('hubdistrib', 'HubDistribQueueHandler');
$qm->connect('hubout', 'HubOutQueueHandler'); $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 // Incoming from a foreign PuSH hub
$qm->connect('pushinput', 'PushInputQueueHandler'); $qm->connect('pushinput', 'PushInputQueueHandler');
return true; return true;
@ -93,7 +98,7 @@ class OStatusPlugin extends Plugin
*/ */
function onStartEnqueueNotice($notice, &$transports) function onStartEnqueueNotice($notice, &$transports)
{ {
$transports[] = 'hubdistrib'; $transports[] = 'ostatus';
return true; return true;
} }
@ -199,25 +204,6 @@ class OStatusPlugin extends Plugin
function onEndNoticeSave($notice) 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);
}
}
} }
/** /**

View File

@ -46,6 +46,11 @@ class GroupsalmonAction extends SalmonAction
$this->clientError(_('No such group.')); $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; return true;
} }
@ -74,13 +79,13 @@ class GroupsalmonAction extends SalmonAction
throw new ClientException("Not to the attention of anyone."); throw new ClientException("Not to the attention of anyone.");
} else { } else {
$uri = common_local_url('groupbyid', array('id' => $this->group->id)); $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."); throw new ClientException("Not to the attention of this group.");
} }
} }
$profile = $this->ensureProfile(); $profile = $this->ensureProfile();
// @fixme save the post $this->saveNotice();
} }
/** /**

View File

@ -650,6 +650,7 @@ class Ostatus_profile extends Memcached_DataObject
*/ */
protected function filterReplies($sender, &$attention_uris) protected function filterReplies($sender, &$attention_uris)
{ {
common_log(LOG_DEBUG, "Original reply recipients: " . implode(', ', $attention_uris));
$groups = array(); $groups = array();
$replies = array(); $replies = array();
foreach ($attention_uris as $recipient) { foreach ($attention_uris as $recipient) {
@ -668,6 +669,8 @@ class Ostatus_profile extends Memcached_DataObject
// Deliver to local members of this remote group. // Deliver to local members of this remote group.
// @fixme sender verification? // @fixme sender verification?
$groups[] = $oprofile->group_id; $groups[] = $oprofile->group_id;
} else {
common_log(LOG_DEBUG, "Skipping reply to remote profile $recipient");
} }
continue; continue;
} }
@ -683,14 +686,24 @@ class Ostatus_profile extends Memcached_DataObject
$group = User_group::staticGet('id', $id); $group = User_group::staticGet('id', $id);
if ($group) { if ($group) {
// Deliver to all members of this local group if allowed. // Deliver to all members of this local group if allowed.
if ($sender->localProfile()->isMember($group)) { $profile = $sender->localProfile();
if ($profile->isMember($group)) {
$groups[] = $group->id; $groups[] = $group->id;
} else {
common_log(LOG_DEBUG, "Skipping reply to local group $group->nickname as sender $profile->id is not a member");
} }
continue; continue;
} else {
common_log(LOG_DEBUG, "Skipping reply to bogus group $recipient");
} }
} }
common_log(LOG_DEBUG, "Skipping reply to unrecognized profile $recipient");
} }
$attention_uris = $replies; $attention_uris = $replies;
common_log(LOG_DEBUG, "Local reply recipients: " . implode(', ', $replies));
common_log(LOG_DEBUG, "Local group recipients: " . implode(', ', $groups));
return $groups; return $groups;
} }

View File

@ -18,46 +18,89 @@
*/ */
/** /**
* Send a PuSH subscription verification from our internal hub. * Prepare PuSH and Salmon distributions for an outgoing message.
* Queue up final distribution for *
* @package Hub * @package OStatusPlugin
* @author Brion Vibber <brion@status.net> * @author Brion Vibber <brion@status.net>
*/ */
class HubDistribQueueHandler extends QueueHandler class OStatusQueueHandler extends QueueHandler
{ {
function transport() function transport()
{ {
return 'hubdistrib'; return 'ostatus';
} }
function handle($notice) function handle($notice)
{ {
assert($notice instanceof 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) { 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; return true;
} }
function pushUser($notice) function pushUser()
{ {
// See if there's any PuSH subscriptions, including OStatus clients. if ($this->user) {
// @fixme handle group subscriptions as well // For local posts, ping the PuSH hub to update their feed.
// http://identi.ca/api/statuses/user_timeline/1.atom // http://identi.ca/api/statuses/user_timeline/1.atom
$feed = common_local_url('ApiTimelineUser', $feed = common_local_url('ApiTimelineUser',
array('id' => $notice->profile_id, array('id' => $this->user->id,
'format' => 'atom')); 'format' => 'atom'));
$this->pushFeed($feed, array($this, 'userFeedForNotice'), $notice); $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', $feed = common_local_url('ApiTimelineGroup',
array('id' => $group_id, array('id' => $group_id,
'format' => 'atom')); '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) function pushFeedInternal($atom, $sub)
{ {
common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $sub->topic"); common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $sub->topic");
$qm = QueueManager::get();
while ($sub->fetch()) { while ($sub->fetch()) {
$sub->distribute($atom); $sub->distribute($atom);
} }
@ -130,20 +172,19 @@ class HubDistribQueueHandler extends QueueHandler
/** /**
* Build a single-item version of the sending user's Atom feed. * Build a single-item version of the sending user's Atom feed.
* @param Notice $notice
* @return string * @return string
*/ */
function userFeedForNotice($notice) function userFeedForNotice()
{ {
// @fixme this feels VERY hacky... // @fixme this feels VERY hacky...
// should probably be a cleaner way to do it // should probably be a cleaner way to do it
ob_start(); ob_start();
$api = new ApiTimelineUserAction(); $api = new ApiTimelineUserAction();
$api->prepare(array('id' => $notice->profile_id, $api->prepare(array('id' => $this->notice->profile_id,
'format' => 'atom', 'format' => 'atom',
'max_id' => $notice->id, 'max_id' => $this->notice->id,
'since_id' => $notice->id - 1)); 'since_id' => $this->notice->id - 1));
$api->showTimeline(); $api->showTimeline();
$feed = ob_get_clean(); $feed = ob_get_clean();
@ -155,7 +196,7 @@ class HubDistribQueueHandler extends QueueHandler
return $feed; return $feed;
} }
function groupFeedForNotice($group_id, $notice) function groupFeedForNotice($group_id)
{ {
// @fixme this feels VERY hacky... // @fixme this feels VERY hacky...
// should probably be a cleaner way to do it // should probably be a cleaner way to do it
@ -164,8 +205,8 @@ class HubDistribQueueHandler extends QueueHandler
$api = new ApiTimelineGroupAction(); $api = new ApiTimelineGroupAction();
$args = array('id' => $group_id, $args = array('id' => $group_id,
'format' => 'atom', 'format' => 'atom',
'max_id' => $notice->id, 'max_id' => $this->notice->id,
'since_id' => $notice->id - 1); 'since_id' => $this->notice->id - 1);
$api->prepare($args); $api->prepare($args);
$api->handle($args); $api->handle($args);
$feed = ob_get_clean(); $feed = ob_get_clean();

View File

@ -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;
}
}