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

This commit is contained in:
Evan Prodromou 2010-02-24 15:00:52 -05:00
commit daea988786
15 changed files with 772 additions and 321 deletions

View File

@ -501,7 +501,11 @@ class Memcached_DataObject extends Safe_DataObject
function raiseError($message, $type = null, $behaviour = null) function raiseError($message, $type = null, $behaviour = null)
{ {
throw new ServerException("DB_DataObject error [$type]: $message"); $id = get_class($this);
if ($this->id) {
$id .= ':' . $this->id;
}
throw new ServerException("[$id] DB_DataObject error [$type]: $message");
} }
static function cacheGet($keyPart) static function cacheGet($keyPart)

View File

@ -54,7 +54,8 @@ var SN = { // StatusNet
NoticeGeoName: 'notice_data-geo_name', NoticeGeoName: 'notice_data-geo_name',
NoticeDataGeo: 'notice_data-geo', NoticeDataGeo: 'notice_data-geo',
NoticeDataGeoCookie: 'notice_data-geo_cookie', NoticeDataGeoCookie: 'notice_data-geo_cookie',
NoticeDataGeoSelected: 'notice_data-geo_selected' NoticeDataGeoSelected: 'notice_data-geo_selected',
StatusNetInstance:'StatusNetInstance'
} }
}, },
@ -670,6 +671,35 @@ var SN = { // StatusNet
date.setFullYear(year, month, day); date.setFullYear(year, month, day);
return date; return date;
},
StatusNetInstance: {
Set: function(value) {
var SNI = SN.U.StatusNetInstance.Get();
if (SNI !== null) {
value = $.extend(SNI, 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);
}
} }
}, },
@ -707,6 +737,20 @@ var SN = { // StatusNet
SN.U.NewDirectMessage(); SN.U.NewDirectMessage();
} }
},
Login: function() {
if (SN.U.StatusNetInstance.Get() !== null) {
var nickname = SN.U.StatusNetInstance.Get().Nickname;
if (nickname !== null) {
$('#form_login #nickname').val(nickname);
}
}
$('#form_login').bind('submit', function() {
SN.U.StatusNetInstance.Set({Nickname: $('#form_login #nickname').val()});
return true;
});
} }
} }
}; };
@ -721,5 +765,8 @@ $(document).ready(function(){
if ($('#content .entity_actions').length > 0) { if ($('#content .entity_actions').length > 0) {
SN.Init.EntityActions(); SN.Init.EntityActions();
} }
if ($('#form_login').length > 0) {
SN.Init.Login();
}
}); });

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,21 +71,19 @@ 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()
{ {
$xs = new XMLStringer(true); if (!empty($this->formatted)) {
$xs->elementStart('poco:address'); $xs = new XMLStringer(true);
$xs->element('poco:formatted', null, $this->formatted); $xs->elementStart('poco:address');
$xs->elementEnd('poco:address'); $xs->element('poco:formatted', null, $this->formatted);
return $xs->getString(); $xs->elementEnd('poco:address');
return $xs->getString();
}
return null;
} }
} }
@ -92,25 +91,117 @@ 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 NOTE = 'note'; const DISPLAYNAME = 'displayName';
const URLS = 'urls'; const NOTE = 'note';
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)) {
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,
@ -118,6 +209,8 @@ class PoCo
) )
); );
} }
return $poco;
} }
function asString() function asString()
@ -129,6 +222,12 @@ class PoCo
$this->preferredUsername $this->preferredUsername
); );
$xs->element(
'poco:displayName',
null,
$this->displayName
);
if (!empty($this->note)) { if (!empty($this->note)) {
$xs->element('poco:note', null, $this->note); $xs->element('poco:note', null, $this->note);
} }
@ -374,6 +473,8 @@ class ActivityObject
public $source; public $source;
public $avatar; public $avatar;
public $geopoint; public $geopoint;
public $poco;
public $displayName;
/** /**
* Constructor * Constructor
@ -426,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...
@ -434,8 +534,9 @@ class ActivityObject
$this->displayName = $this->title; $this->displayName = $this->title;
// @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);
} }
} }
@ -490,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;
} }
@ -519,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,
@ -532,7 +641,7 @@ class ActivityObject
? Avatar::defaultImage(AVATAR_PROFILE_SIZE) ? Avatar::defaultImage(AVATAR_PROFILE_SIZE)
: $this->avatar->displayUrl() : $this->avatar->displayUrl()
), ),
'' null
); );
} }
@ -848,11 +957,24 @@ class Activity
} }
// XXX: add context // XXX: add context
// XXX: add target
$xs->elementStart('author');
$xs->element('uri', array(), $this->actor->id);
if ($this->actor->title) {
$xs->element('name', array(), $this->actor->title);
}
$xs->elementEnd('author');
$xs->raw($this->actor->asString('activity:actor')); $xs->raw($this->actor->asString('activity:actor'));
$xs->element('activity:verb', null, $this->verb); $xs->element('activity:verb', null, $this->verb);
$xs->raw($this->object->asString());
if ($this->object) {
$xs->raw($this->object->asString());
}
if ($this->target) {
$xs->raw($this->target->asString('activity:target'));
}
$xs->elementEnd('entry'); $xs->elementEnd('entry');

View File

@ -438,14 +438,15 @@ class NoticeListItem extends Widget
$this->out->text(_('at')); $this->out->text(_('at'));
$this->out->text(' '); $this->out->text(' ');
if (empty($url)) { if (empty($url)) {
$this->out->element('span', array('class' => 'geo', $this->out->element('abbr', array('class' => 'geo',
'title' => $latlon), 'title' => $latlon),
$name); $name);
} else { } else {
$this->out->element('a', array('class' => 'geo', $this->out->elementStart('a', array('href' => $url));
'title' => $latlon, $this->out->element('abbr', array('class' => 'geo',
'href' => $url), 'title' => $latlon),
$name); $name);
$this->out->elementEnd('a');
} }
$this->out->elementEnd('span'); $this->out->elementEnd('span');
} }

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);
}
}
} }
/** /**
@ -295,13 +281,19 @@ class OStatusPlugin extends Plugin
function onStartNoticeSourceLink($notice, &$name, &$url, &$title) function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
{ {
if ($notice->source == 'ostatus') { if ($notice->source == 'ostatus') {
$bits = parse_url($notice->uri); if ($notice->url) {
$domain = $bits['host']; $bits = parse_url($notice->url);
$domain = $bits['host'];
if (substr($domain, 0, 4) == 'www.') {
$name = substr($domain, 4);
} else {
$name = $domain;
}
$name = $domain; $url = $notice->url;
$url = $notice->uri; $title = sprintf(_m("Sent from %s via OStatus"), $domain);
$title = sprintf(_m("Sent from %s via OStatus"), $domain); return false;
return false; }
} }
} }
@ -316,7 +308,7 @@ class OStatusPlugin extends Plugin
{ {
$oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri); $oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri);
if ($oprofile) { if ($oprofile) {
$oprofile->processFeed($feed); $oprofile->processFeed($feed, 'push');
} else { } else {
common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri"); 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); $oprofile = Ostatus_profile::staticGet('group_id', $group->id);
if ($oprofile) { if ($oprofile) {
// Drop the PuSH subscription if there are no other subscribers. // 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); $member = Profile::staticGet($user->id);

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

@ -288,10 +288,15 @@ class OStatusSubAction extends Action
} }
$this->profile_uri = $profile_uri; $this->profile_uri = $profile_uri;
// @fixme validate, normalize bla bla
try { try {
$oprofile = Ostatus_profile::ensureProfile($this->profile_uri); if (Validate::email($this->profile_uri)) {
$this->oprofile = $oprofile; $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; return true;
} catch (FeedSubBadURLException $e) { } catch (FeedSubBadURLException $e) {
$this->error = _m('Invalid URL or could not reach server.'); $this->error = _m('Invalid URL or could not reach server.');

View File

@ -65,6 +65,21 @@ class WebfingerAction extends Action
'format' => 'atom')), 'format' => 'atom')),
'type' => 'application/atom+xml'); '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', $salmon_url = common_local_url('salmon',
array('id' => $this->user->id)); array('id' => $this->user->id));

View File

@ -33,6 +33,7 @@ class Ostatus_profile extends Memcached_DataObject
public $feeduri; public $feeduri;
public $salmonuri; public $salmonuri;
public $avatar; // remote URL of the last avatar we saved
public $created; public $created;
public $modified; public $modified;
@ -58,6 +59,7 @@ class Ostatus_profile extends Memcached_DataObject
'group_id' => DB_DATAOBJECT_INT, 'group_id' => DB_DATAOBJECT_INT,
'feeduri' => DB_DATAOBJECT_STR, 'feeduri' => DB_DATAOBJECT_STR,
'salmonuri' => DB_DATAOBJECT_STR, 'salmonuri' => DB_DATAOBJECT_STR,
'avatar' => DB_DATAOBJECT_STR,
'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, '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); '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'), 255, true, 'UNI'),
new ColumnDef('salmonuri', 'text', new ColumnDef('salmonuri', 'text',
null, true), null, true),
new ColumnDef('avatar', 'text',
null, true),
new ColumnDef('created', 'datetime', new ColumnDef('created', 'datetime',
null, false), null, false),
new ColumnDef('modified', 'datetime', new ColumnDef('modified', 'datetime',
@ -488,7 +492,7 @@ class Ostatus_profile extends Memcached_DataObject
* *
* @param DOMDocument $feed * @param DOMDocument $feed
*/ */
public function processFeed($feed) public function processFeed($feed, $source)
{ {
$entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
if ($entries->length == 0) { if ($entries->length == 0) {
@ -498,7 +502,7 @@ class Ostatus_profile extends Memcached_DataObject
for ($i = 0; $i < $entries->length; $i++) { for ($i = 0; $i < $entries->length; $i++) {
$entry = $entries->item($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 $entry
* @param DOMElement $feed for context * @param DOMElement $feed for context
*/ */
protected function processEntry($entry, $feed) public function processEntry($entry, $feed, $source)
{ {
$activity = new Activity($entry, $feed); $activity = new Activity($entry, $feed);
$debug = var_export($activity, true);
common_log(LOG_DEBUG, $debug);
if ($activity->verb == ActivityVerb::POST) { if ($activity->verb == ActivityVerb::POST) {
$this->processPost($activity); $this->processPost($activity, $source);
} else { } else {
common_log(LOG_INFO, "Ignoring activity with unrecognized verb $activity->verb"); 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. * Process an incoming post activity from this remote feed.
* @param Activity $activity * @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 * @fixme break up this function, it's getting nasty long
*/ */
protected function processPost($activity) public function processPost($activity, $method)
{ {
if ($this->isGroup()) { if ($this->isGroup()) {
// A group feed will contain posts from multiple authors.
// @fixme validate these profiles in some way! // @fixme validate these profiles in some way!
$oprofile = self::ensureActorProfile($activity); $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 { } 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); $actorUri = self::getActorProfileURI($activity);
if ($actorUri == $this->uri) { 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 { } else {
// @fixme drop or reject the messages once we've got the canonical profile URI recorded sanely common_log(LOG_WARNING, "OStatus: skipping post with bad author: got $actorUri expected $this->uri");
common_log(LOG_INFO, "OStatus: Warning: non-group post with unexpected author: $actorUri expected $this->uri"); return false;
//return;
} }
$oprofile = $this; $oprofile = $this;
} }
// 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; $sourceUri = $activity->object->id;
$dupe = Notice::staticGet('uri', $sourceUri); $dupe = Notice::staticGet('uri', $sourceUri);
if ($dupe) { if ($dupe) {
common_log(LOG_INFO, "OStatus: ignoring duplicate post: $sourceUri"); 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; $sourceUrl = null;
if ($activity->object->link) { if ($activity->object->link) {
$sourceUrl = $activity->object->link; $sourceUrl = $activity->object->link;
} else if ($activity->link) {
$sourceUrl = $activity->link;
} else if (preg_match('!^https?://!', $activity->object->id)) { } else if (preg_match('!^https?://!', $activity->object->id)) {
$sourceUrl = $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; $options = array('is_local' => Notice::REMOTE_OMB,
$params = array('is_local' => Notice::REMOTE_OMB,
'url' => $sourceUrl, 'url' => $sourceUrl,
'uri' => $sourceUri); 'uri' => $sourceUri,
'rendered' => $rendered,
'replies' => array(),
'groups' => array());
$location = $activity->context->location; // Check for optional attributes...
if ($location) { if (!empty($activity->time)) {
$params['lat'] = $location->lat; $options['created'] = common_sql_date($activity->time);
$params['lon'] = $location->lon;
if ($location->location_id) {
$params['location_ns'] = $location->location_ns;
$params['location_id'] = $location->location_id;
}
} }
$profile = $oprofile->localProfile();
$params['groups'] = array();
$params['replies'] = array();
if ($activity->context) { if ($activity->context) {
foreach ($activity->context->attention as $recipient) { // Any individual or group attn: targets?
$roprofile = Ostatus_profile::staticGet('uri', $recipient); $replies = $activity->context->attention;
if ($roprofile) { $options['groups'] = $this->filterReplies($oprofile, $replies);
if ($roprofile->isGroup()) { $options['replies'] = $replies;
// Deliver to local recipients of this remote group.
// @fixme sender verification? // Maintain direct reply associations
$params['groups'][] = $roprofile->group_id; // @fixme what about conversation ID?
continue; if (!empty($activity->context->replyToID)) {
} else { $orig = Notice::staticGet('uri',
// Delivery to remote users is the source service's job. $activity->context->replyToID);
continue; if (!empty($orig)) {
} $options['reply_to'] = $orig->id;
} }
}
$user = User::staticGet('uri', $recipient);
if ($user) { $location = $activity->context->location;
// An @-reply directed to a local user. if ($location) {
// @fixme sender verification, spam etc? $options['lat'] = $location->lat;
$params['replies'][] = $recipient; $options['lon'] = $location->lon;
continue; if ($location->location_id) {
} $options['location_ns'] = $location->location_ns;
$options['location_id'] = $location->location_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");
} }
} }
} }
try { try {
$saved = Notice::saveNew($profile->id, $saved = Notice::saveNew($oprofile->profile_id,
$content, $content,
'ostatus', 'ostatus',
$params); $options);
if ($saved) {
Ostatus_source::saveNew($saved, $this, $method);
}
} catch (Exception $e) { } catch (Exception $e) {
common_log(LOG_ERR, "Failed saving notice entry for $sourceUri: " . $e->getMessage()); common_log(LOG_ERR, "OStatus save of remote message $sourceUri failed: " . $e->getMessage());
return; 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) protected function updateAvatar($url)
{ {
if ($url == $this->avatar) {
// We've already got this one.
return;
}
if ($this->isGroup()) { if ($this->isGroup()) {
$self = $this->localGroup(); $self = $this->localGroup();
} else { } else {
@ -760,12 +826,28 @@ class Ostatus_profile extends Memcached_DataObject
common_timestamp()); common_timestamp());
rename($temp_filename, Avatar::path($filename)); rename($temp_filename, Avatar::path($filename));
$self->setOriginal($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 if ($object->avatar) {
return $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()) public static function ensureActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array())
{ {
$profile = self::getActivityObjectProfile($object); $profile = self::getActivityObjectProfile($object);
if (!$profile) { if ($profile) {
$profile->updateFromActivityObject($object, $hints);
} else {
$profile = self::createActivityObjectProfile($object, $feeduri, $salmonuri, $hints); $profile = self::createActivityObjectProfile($object, $feeduri, $salmonuri, $hints);
} }
return $profile; return $profile;
@ -901,8 +985,6 @@ class Ostatus_profile extends Memcached_DataObject
protected static function createActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array()) protected static function createActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array())
{ {
$homeuri = $object->id; $homeuri = $object->id;
$nickname = self::getActivityObjectNickname($object, $hints);
$avatar = self::getActivityObjectAvatar($object);
if (!$homeuri) { if (!$homeuri) {
common_log(LOG_DEBUG, __METHOD__ . " empty actor profile URI: " . var_export($activity, true)); 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) { if ($object->type == ActivityObject::PERSON) {
$profile = new Profile(); $profile = new Profile();
$profile->nickname = $nickname; self::updateProfile($profile, $object, $hints);
$profile->fullname = $object->title; $profile->created = common_sql_now();
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?
$oprofile->profile_id = $profile->insert(); $oprofile->profile_id = $profile->insert();
if (!$oprofile->profile_id) { if (!$oprofile->profile_id) {
throw new ServerException("Can't save local profile"); throw new ServerException("Can't save local profile");
} }
} else { } else {
$group = new User_group(); $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(); $group->created = common_sql_now();
self::updateGroup($group, $object, $hints);
// @fixme homepage
// @fixme bio
// @fixme tags/categories
// @fixme location?
// @todo tags from categories
// @todo lat/lon/location?
$oprofile->group_id = $group->insert(); $oprofile->group_id = $group->insert();
if (!$oprofile->group_id) { if (!$oprofile->group_id) {
throw new ServerException("Can't save local profile"); throw new ServerException("Can't save local profile");
} }
@ -991,6 +1049,7 @@ class Ostatus_profile extends Memcached_DataObject
$ok = $oprofile->insert(); $ok = $oprofile->insert();
if ($ok) { if ($ok) {
$avatar = self::getActivityObjectAvatar($object, $hints);
if ($avatar) { if ($avatar) {
$oprofile->updateAvatar($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()) 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)) { if (!empty($object->nickname)) {
return common_nicknamize($object->nickname); return common_nicknamize($object->nickname);
} }

View File

@ -24,35 +24,9 @@
* @note Everything in here should eventually migrate over to /js/util.js's SN. * @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() { SN.Init.OStatusCookie = function() {
if (SN.U.StatusNetInstance.Get() === null) { 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') { if (form.attr('id') == 'form_ostatus_connect') {
SN.Init.OStatusCookie(); 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() { 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; return true;
}); });
} }
@ -123,4 +97,6 @@ SN.Init.Subscribe = function() {
$(document).ready(function() { $(document).ready(function() {
SN.Init.Subscribe(); SN.Init.Subscribe();
$('.form_remote_authorize').bind('submit', function() { $(this).addClass(SN.C.S.Processing); return true; });
}); });

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

@ -185,54 +185,6 @@ class SalmonAction extends Action
function saveNotice() function saveNotice()
{ {
$oprofile = $this->ensureProfile(); $oprofile = $this->ensureProfile();
return $oprofile->processPost($this->act, 'salmon');
// 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;
} }
} }

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

View File

@ -7,11 +7,10 @@ if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
// XXX: we should probably have some common source for this stuff // XXX: we should probably have some common source for this stuff
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..')); define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
define('STATUSNET', true); define('STATUSNET', true);
require_once INSTALLDIR . '/lib/common.php'; require_once INSTALLDIR . '/lib/common.php';
require_once INSTALLDIR . '/plugins/OStatus/lib/activity.php';
class ActivityParseTests extends PHPUnit_Framework_TestCase class ActivityParseTests extends PHPUnit_Framework_TestCase
{ {
@ -97,6 +96,45 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase
$this->assertFalse(empty($act->actor)); $this->assertFalse(empty($act->actor));
} }
public function testExample5()
{
global $_example5;
$dom = DOMDocument::loadXML($_example5);
$feed = $dom->documentElement;
// @todo Test feed elements
$entries = $feed->getElementsByTagName('entry');
$entry = $entries->item(0);
$act = new Activity($entry, $feed);
// Post
$this->assertEquals($act->verb, ActivityVerb::POST);
$this->assertFalse(empty($act->context));
// Actor w/Portable Contacts stuff
$this->assertFalse(empty($act->actor));
$this->assertEquals($act->actor->type, ActivityObject::PERSON);
$this->assertEquals($act->actor->title, 'Test User');
$this->assertEquals($act->actor->id, 'http://example.net/mysite/user/3');
$this->assertEquals($act->actor->link, 'http://example.net/mysite/testuser');
$this->assertEquals(
$act->actor->avatar,
'http://example.net/mysite/avatar/3-96-20100224004207.jpeg'
);
$this->assertEquals($act->actor->displayName, 'Test User');
$poco = $act->actor->poco;
$this->assertEquals($poco->preferredUsername, 'testuser');
$this->assertEquals($poco->address->formatted, 'San Francisco, CA');
$this->assertEquals($poco->urls[0]->type, 'homepage');
$this->assertEquals($poco->urls[0]->value, 'http://example.com/blog.html');
$this->assertEquals($poco->urls[0]->primary, 'true');
}
} }
$_example1 = <<<EXAMPLE1 $_example1 = <<<EXAMPLE1
@ -156,26 +194,26 @@ $_example3 = <<<EXAMPLE3
<feed xmlns="http://www.w3.org/2005/Atom"> <feed xmlns="http://www.w3.org/2005/Atom">
<title>Example Feed</title> <title>Example Feed</title>
<subtitle>A subtitle.</subtitle> <subtitle>A subtitle.</subtitle>
<link href="http://example.org/feed/" rel="self" /> <link href="http://example.org/feed/" rel="self" />
<link href="http://example.org/" /> <link href="http://example.org/" />
<id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id> <id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
<updated>2003-12-13T18:30:02Z</updated> <updated>2003-12-13T18:30:02Z</updated>
<author> <author>
<name>John Doe</name> <name>John Doe</name>
<email>johndoe@example.com</email> <email>johndoe@example.com</email>
</author> </author>
<entry> <entry>
<title>Atom-Powered Robots Run Amok</title> <title>Atom-Powered Robots Run Amok</title>
<link href="http://example.org/2003/12/13/atom03" /> <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="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"/> <link rel="edit" href="http://example.org/2003/12/13/atom03/edit"/>
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id> <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
<updated>2003-12-13T18:30:02Z</updated> <updated>2003-12-13T18:30:02Z</updated>
<summary>Some text.</summary> <summary>Some text.</summary>
</entry> </entry>
</feed> </feed>
EXAMPLE3; EXAMPLE3;
@ -207,3 +245,82 @@ $_example4 = <<<EXAMPLE4
<category term="thetime"></category> <category term="thetime"></category>
</entry> </entry>
EXAMPLE4; EXAMPLE4;
$_example5 = <<<EXAMPLE5
<?xml version="1.0" encoding="UTF-8"?>
<feed xml:lang="en-US" 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:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0">
<id>3</id>
<title>testuser timeline</title>
<subtitle>Updates from testuser on Zach Dev!</subtitle>
<logo>http://example.net/mysite/avatar/3-96-20100224004207.jpeg</logo>
<updated>2010-02-24T06:38:49+00:00</updated>
<author>
<name>testuser</name>
<uri>http://example.net/mysite/user/3</uri>
</author>
<link href="http://example.net/mysite/testuser" rel="alternate" type="text/html"/>
<link href="http://example.net/mysite/api/statuses/user_timeline/3.atom" rel="self" type="application/atom+xml"/>
<link href="http://example.net/mysite/main/sup#3" rel="http://api.friendfeed.com/2008/03#sup" type="application/json"/>
<link href="http://example.net/mysite/main/push/hub" rel="hub"/>
<link href="http://example.net/mysite/main/salmon/user/3" rel="salmon"/>
<activity:subject>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<id>http://example.net/mysite/user/3</id>
<title>Test User</title>
<link rel="alternate" type="text/html" href="http://example.net/mysite/testuser"/>
<link type="image/jpeg" rel="avatar" href="http://example.net/mysite/avatar/3-96-20100224004207.jpeg"/>
<georss:point>37.7749295 -122.4194155</georss:point>
<poco:preferredUsername>testuser</poco:preferredUsername>
<poco:displayName>Test User</poco:displayName>
<poco:note>Just another test user.</poco:note>
<poco:address>
<poco:formatted>San Francisco, CA</poco:formatted>
</poco:address>
<poco:urls>
<poco:type>homepage</poco:type>
<poco:value>http://example.com/blog.html</poco:value>
<poco:primary>true</poco:primary>
</poco:urls>
</activity:subject>
<entry>
<title>Hey man, is that Freedom Code?! #freedom #hippy</title>
<summary>Hey man, is that Freedom Code?! #freedom #hippy</summary>
<author>
<name>testuser</name>
<uri>http://example.net/mysite/user/3</uri>
</author>
<activity:actor>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<id>http://example.net/mysite/user/3</id>
<title>Test User</title>
<link rel="alternate" type="text/html" href="http://example.net/mysite/testuser"/>
<link type="image/jpeg" rel="avatar" href="http://example.net/mysite/avatar/3-96-20100224004207.jpeg"/>
<georss:point>37.7749295 -122.4194155</georss:point>
<poco:preferredUsername>testuser</poco:preferredUsername>
<poco:displayName>Test User</poco:displayName>
<poco:note>Just another test user.</poco:note>
<poco:address>
<poco:formatted>San Francisco, CA</poco:formatted>
</poco:address>
<poco:urls>
<poco:type>homepage</poco:type>
<poco:value>http://example.com/blog.html</poco:value>
<poco:primary>true</poco:primary>
</poco:urls>
</activity:actor>
<link rel="alternate" type="text/html" href="http://example.net/mysite/notice/7"/>
<id>http://example.net/mysite/notice/7</id>
<published>2010-02-24T00:53:06+00:00</published>
<updated>2010-02-24T00:53:06+00:00</updated>
<link rel="ostatus:conversation" href="http://example.net/mysite/conversation/7"/>
<content type="html">Hey man, is that Freedom Code?! #&lt;span class=&quot;tag&quot;&gt;&lt;a href=&quot;http://example.net/mysite/tag/freedom&quot; rel=&quot;tag&quot;&gt;freedom&lt;/a&gt;&lt;/span&gt; #&lt;span class=&quot;tag&quot;&gt;&lt;a href=&quot;http://example.net/mysite/tag/hippy&quot; rel=&quot;tag&quot;&gt;hippy&lt;/a&gt;&lt;/span&gt;</content>
<georss:point>37.8313160 -122.2852473</georss:point>
</entry>
</feed>
EXAMPLE5;

View File

@ -1490,6 +1490,7 @@ text-align:center;
} }
.aside .tag-cloud { .aside .tag-cloud {
font-size:0.8em; font-size:0.8em;
word-wrap:break-word;
} }
.tag-cloud li { .tag-cloud li {
display:inline; display:inline;