.
namespace Tests\Unit;
if (!defined('INSTALLDIR')) {
define('INSTALLDIR', dirname(dirname(__DIR__)));
}
if (!defined('PUBLICDIR')) {
define('PUBLICDIR', INSTALLDIR . DIRECTORY_SEPARATOR . 'public');
}
if (!defined('GNUSOCIAL')) {
define('GNUSOCIAL', true);
}
if (!defined('STATUSNET')) { // Compatibility
define('STATUSNET', true);
}
use Activity;
use ActivityObject;
use ActivityUtils;
use ActivityVerb;
use DOMDocument;
use PHPUnit\Framework\TestCase;
require_once INSTALLDIR . '/lib/common.php';
final class ActivityParseTests extends TestCase
{
public function testMastodonRetweet()
{
global $_mastodon_retweet;
$dom = new DOMDocument();
$dom->loadXML($_mastodon_retweet);
$feed = $dom->documentElement;
$entries = $feed->getElementsByTagName('entry');
$entry = $entries->item(0);
$act = new Activity($entry, $feed);
$this->assertFalse(empty($act));
$this->assertFalse(empty($act->objects[0]));
$object = $act->objects[0];
$this->assertEquals($object->verb, ActivityVerb::POST);
}
public function testGSReweet()
{
global $_gs_retweet;
$dom = new DOMDocument();
$dom->loadXML($_gs_retweet);
$feed = $dom->documentElement;
$entries = $feed->getElementsByTagName('entry');
$entry = $entries->item(0);
$act = new Activity($entry, $feed);
$this->assertFalse(empty($act));
$this->assertFalse(empty($act->objects[0]));
$object = $act->objects[0];
$this->assertEquals($object->verb, ActivityVerb::POST);
}
public function testExample1()
{
global $_example1;
$dom = new DOMDocument();
$dom->loadXML($_example1);
$act = new Activity($dom->documentElement);
$this->assertFalse(empty($act));
$this->assertEquals(1243860840, $act->time);
$this->assertEquals(ActivityVerb::POST, $act->verb);
$this->assertFalse(empty($act->objects[0]));
$this->assertEquals('Punctuation Changeset', $act->objects[0]->title);
$this->assertEquals('http://versioncentral.example.org/activity/changeset', $act->objects[0]->type);
$this->assertEquals('Fixing punctuation because it makes it more readable.', $act->objects[0]->summary);
$this->assertEquals('tag:versioncentral.example.org,2009:/change/1643245', $act->objects[0]->id);
}
public function testExample2()
{
global $_example2;
$dom = new DOMDocument();
$dom->loadXML($_example2);
$act = new Activity($dom->documentElement);
$this->assertFalse(empty($act));
// Did we handle correctly with a typical payload?
$this->assertEquals("
Geraldine posted a Photo on PhotoPanic
\n " .
"", trim($act->content));
}
public function testExample3()
{
global $_example3;
$dom = new DOMDocument();
$dom->loadXML($_example3);
$feed = $dom->documentElement;
$entries = $feed->getElementsByTagName('entry');
$entry = $entries->item(0);
$act = new Activity($entry, $feed);
$this->assertFalse(empty($act));
$this->assertEquals(1071340202, $act->time);
$this->assertEquals('http://example.org/2003/12/13/atom03.html', $act->link);
$this->assertEquals($act->verb, ActivityVerb::POST);
$this->assertFalse(empty($act->actor));
$this->assertEquals(ActivityObject::PERSON, $act->actor->type);
$this->assertEquals('John Doe', $act->actor->title);
$this->assertEquals('mailto:johndoe@example.com', $act->actor->id);
$this->assertFalse(empty($act->objects[0]));
$this->assertEquals(ActivityObject::NOTE, $act->objects[0]->type);
$this->assertEquals('urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a', $act->objects[0]->id);
$this->assertEquals('Atom-Powered Robots Run Amok', $act->objects[0]->title);
$this->assertEquals('Some text.', $act->objects[0]->summary);
$this->assertEquals('http://example.org/2003/12/13/atom03.html', $act->objects[0]->link);
$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 = new DOMDocument();
$dom->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'), array_keys($act->context->attention));
$this->assertFalse(empty($act->objects[0]));
$this->assertEquals($act->objects[0]->content,
'@evan now is the time for all good men to come to the aid of their country. #thetime');
$this->assertFalse(empty($act->actor));
}
public function testExample5()
{
global $_example5;
$dom = new DOMDocument();
$dom->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');
$avatars = $act->actor->avatarLinks;
$this->assertEquals(
$avatars[0]->url,
'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');
$this->assertEquals($act->actor->geopoint, '37.7749295 -122.4194155');
}
public function testExample6()
{
global $_example6;
$dom = new DOMDocument();
$dom->loadXML($_example6);
$rss = $dom->documentElement;
$channels = $dom->getElementsByTagName('channel');
$channel = $channels->item(0);
$items = $channel->getElementsByTagName('item');
$item = $items->item(0);
$act = new Activity($item, $channel);
$this->assertEquals($act->verb, ActivityVerb::POST);
$this->assertEquals($act->id, 'http://en.blog.wordpress.com/?p=3857');
$this->assertEquals($act->link, 'http://en.blog.wordpress.com/2010/03/03/rub-a-dub-dub-in-the-pubsubhubbub/');
$this->assertEquals($act->title, 'Rub-a-Dub-Dub in the PubSubHubbub');
$this->assertEquals($act->time, 1267634892);
$actor = $act->actor;
$this->assertFalse(empty($actor));
$this->assertEquals($actor->title, "Joseph Scott");
}
public function testExample7()
{
global $_example7;
$dom = new DOMDocument();
$dom->loadXML($_example7);
$rss = $dom->documentElement;
$channels = $dom->getElementsByTagName('channel');
$channel = $channels->item(0);
$items = $channel->getElementsByTagName('item');
$item = $items->item(0);
$act = new Activity($item, $channel);
$this->assertEquals(ActivityVerb::POST, $act->verb);
$this->assertEquals('http://evanpro.posterous.com/checking-out-captain-bones', $act->link);
$this->assertEquals('http://evanpro.posterous.com/checking-out-captain-bones', $act->id);
$this->assertEquals('Checking out captain bones', $act->title);
$this->assertEquals(1269095551, $act->time);
$actor = $act->actor;
$this->assertEquals(ActivityObject::PERSON, $actor->type);
$this->assertEquals('http://posterous.com/people/3sDslhaepotz', $actor->id);
$this->assertEquals('Evan Prodromou', $actor->title);
$this->assertNull($actor->summary);
$this->assertNull($actor->content);
$this->assertEquals('http://posterous.com/people/3sDslhaepotz', $actor->link);
$this->assertNull($actor->source);
$this->assertTrue(is_array($actor->avatarLinks));
$this->assertEquals(1, count($actor->avatarLinks));
$this->assertEquals('http://files.posterous.com/user_profile_pics/480326/2009-08-05-142447.jpg',
$actor->avatarLinks[0]->url);
$this->assertNotNull($actor->poco);
$this->assertEquals('evanpro', $actor->poco->preferredUsername);
$this->assertEquals('Evan Prodromou', $actor->poco->displayName);
$this->assertNull($actor->poco->note);
$this->assertNull($actor->poco->address);
$this->assertEquals(0, count($actor->poco->urls));
}
// Media test - cliqset
public function testExample8()
{
global $_example8;
$dom = new DOMDocument();
$dom->loadXML($_example8);
$feed = $dom->documentElement;
$entries = $feed->getElementsByTagName('entry');
$entry = $entries->item(0);
$act = new Activity($entry, $feed);
$this->assertFalse(empty($act));
$this->assertEquals($act->time, 1269221753);
$this->assertEquals($act->verb, ActivityVerb::POST);
$this->assertEquals($act->summary, 'zcopley posted 5 photos on Flickr');
$this->assertFalse(empty($act->objects));
$this->assertEquals(sizeof($act->objects), 5);
$this->assertEquals($act->objects[0]->type, ActivityObject::PHOTO);
$this->assertEquals($act->objects[0]->title, 'IMG_1368');
$this->assertNull($act->objects[0]->description);
$this->assertEquals(
$act->objects[0]->thumbnail,
'http://media.cliqset.com/6f6fbee9d7dfbffc73b6ef626275eb5f_thumb.jpg'
);
$this->assertEquals(
$act->objects[0]->link,
'http://www.flickr.com/photos/zcopley/4452933806/'
);
$this->assertEquals($act->objects[1]->type, ActivityObject::PHOTO);
$this->assertEquals($act->objects[1]->title, 'IMG_1365');
$this->assertNull($act->objects[1]->description);
$this->assertEquals(
$act->objects[1]->thumbnail,
'http://media.cliqset.com/b8f3932cd0bba1b27f7c8b3ef986915e_thumb.jpg'
);
$this->assertEquals(
$act->objects[1]->link,
'http://www.flickr.com/photos/zcopley/4442630390/'
);
$this->assertEquals($act->objects[2]->type, ActivityObject::PHOTO);
$this->assertEquals($act->objects[2]->title, 'Classic');
$this->assertEquals(
$act->objects[2]->description,
'-Powered by pikchur.com/n0u'
);
$this->assertEquals(
$act->objects[2]->thumbnail,
'http://media.cliqset.com/fc54c15f850b7a9a8efa644087a48c91_thumb.jpg'
);
$this->assertEquals(
$act->objects[2]->link,
'http://www.flickr.com/photos/zcopley/4430754103/'
);
$this->assertEquals($act->objects[3]->type, ActivityObject::PHOTO);
$this->assertEquals($act->objects[3]->title, 'IMG_1363');
$this->assertNull($act->objects[3]->description);
$this->assertEquals(
$act->objects[3]->thumbnail,
'http://media.cliqset.com/4b1d307c9217e2114391a8b229d612cb_thumb.jpg'
);
$this->assertEquals(
$act->objects[3]->link,
'http://www.flickr.com/photos/zcopley/4416969717/'
);
$this->assertEquals($act->objects[4]->type, ActivityObject::PHOTO);
$this->assertEquals($act->objects[4]->title, 'IMG_1361');
$this->assertNull($act->objects[4]->description);
$this->assertEquals(
$act->objects[4]->thumbnail,
'http://media.cliqset.com/23d9b4b96b286e0347d36052f22f6e60_thumb.jpg'
);
$this->assertEquals(
$act->objects[4]->link,
'http://www.flickr.com/photos/zcopley/4417734232/'
);
}
public function testAtomContent()
{
$tests = array(array("Some regular plain text.",
"Some regular plain text."),
array("<b>this is not HTML</b>",
"<b>this is not HTML</b>"),
array("Some regular plain HTML.",
"Some regular plain HTML."),
array("<b>this is too HTML</b>",
"this is too HTML"),
array("<b>but this is not HTML!</b>",
"<b>but this is not HTML!</b>"),
array("
Some regular plain XHTML.
",
"Some regular plain XHTML."),
array("
This is some XHTML!
",
"This is some XHTML!"),
array("
<b>This is not some XHTML!</b>
",
"<b>This is not some XHTML!</b>"),
array("
<b>This is not some XHTML either!</b>
",
"<b>This is not some XHTML either!</b>"));
foreach ($tests as $data) {
list($source, $output) = $data;
$xml = "" .
"http://example.com/fakeid" .
"Test" .
"Atom content tests" .
$source .
"";
$dom = new DOMDocument();
$dom->loadXML($xml);
$act = new Activity($dom->documentElement);
$this->assertFalse(empty($act));
$this->assertEquals($output, trim($act->content));
}
}
public function testRssContent()
{
$tests = array(array("Some regular plain HTML.",
"Some regular plain HTML."),
array("Some <b>exciting bold HTML</b>",
"Some exciting bold HTML"),
array("Some <b>escaped non-HTML.</b>",
"Some <b>escaped non-HTML.</b>"),
array("Some plain text.",
"Some plain text."),
array("Some <b>non-HTML text</b>",
"Some <b>non-HTML text</b>"),
array("Some <b>double-escaped text</b>",
"Some <b>double-escaped text</b>"));
foreach ($tests as $data) {
list($source, $output) = $data;
$xml = "" .
"http://example.com/fakeid" .
"RSS content tests" .
$source .
"";
$dom = new DOMDocument();
$dom->loadXML($xml);
$act = new Activity($dom->documentElement);
$this->assertFalse(empty($act));
$this->assertEquals($output, trim($act->content));
}
}
public function testExample10()
{
global $_example10;
$dom = new DOMDocument();
$dom->loadXML($_example10);
// example 10 is a PuSH item of a post on a group feed, as generated
// by 0.9.7 code after migration away from to
$feed = $dom->documentElement;
$entry = $dom->getElementsByTagName('entry')->item(0);
$expected = 'http://lazarus.local/mublog/user/557';
// Reading just the entry alone should pick up its own
// as the actor.
$act = new Activity($entry);
$this->assertEquals($act->actor->id, $expected);
// Reading the entry in feed context used to be buggy, picking up
// the feed's which referred to the group.
// It should now be returning the expected author entry...
$act = new Activity($entry, $feed);
$this->assertEquals($act->actor->id, $expected);
}
public function testBookmarkRelated()
{
global $_example11;
$dom = new DOMDocument();
$dom->loadXML($_example11);
$feed = $dom->documentElement;
$entry = $dom->getElementsByTagName('entry')->item(0);
$expected = 'http://blog.teambox.com/open-source-companies';
$links = ActivityUtils::getLinks($entry, 'related');
$this->assertFalse(empty($links));
$this->assertTrue(is_array($links));
$this->assertEquals(count($links), 1);
$url = $links[0]->getAttribute('href');
$this->assertEquals($url, $expected);
}
}
$_example1 = <<tag:versioncentral.example.org,2009:/commit/16432452009-06-01T12:54:00ZGeraldine committed a change to yateGeraldine just committed a change to yate on VersionCentralhttp://activitystrea.ms/schema/1.0/posthttp://versioncentral.example.org/activity/commithttp://versioncentral.example.org/activity/changesettag:versioncentral.example.org,2009:/change/1643245Punctuation ChangesetFixing punctuation because it makes it more readable.
EXAMPLE1;
$_example2 = <<tag:photopanic.example.com,2008:activity01Geraldine posted a Photo on PhotoPanic2008-11-02T15:29:00Z
http://activitystrea.ms/schema/1.0/post
tag:photopanic.example.com,2008:photo01My Cat2008-11-02T15:29:00Z
tag:atomactivity.example.com,2008:photo
<p>Geraldine posted a Photo on PhotoPanic</p>
<img src="/geraldine/photo1.jpg">
EXAMPLE2;
$_example3 = <<Example FeedA subtitle.urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af62003-12-13T18:30:02ZJohn Doejohndoe@example.comAtom-Powered Robots Run Amokurn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a2003-12-13T18:30:02ZSome text.
EXAMPLE3;
$_example4 = <<@evan now is the time for all good men to come to the aid of their country. #thetime@evan now is the time for all good men to come to the aid of their country. #thetimespockhttp://example.net/user/2http://activitystrea.ms/schema/1.0/personhttp://example.net/user/2spockhttp://example.net/notice/142010-02-19T02:52:38+00:002010-02-19T02:52:38+00:00@<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>
EXAMPLE4;
$_example5 = <<3testuser timelineUpdates from testuser on Zach Dev!http://example.net/mysite/avatar/3-96-20100224004207.jpeg2010-02-24T06:38:49+00:00testuserhttp://example.net/mysite/user/3http://activitystrea.ms/schema/1.0/personhttp://example.net/mysite/user/3Test User37.7749295 -122.4194155testuserTest UserJust another test user.San Francisco, CAhomepagehttp://example.com/blog.htmltrueHey man, is that Freedom Code?! #freedom #hippyHey man, is that Freedom Code?! #freedom #hippytestuserhttp://example.net/mysite/user/3http://activitystrea.ms/schema/1.0/personhttp://example.net/mysite/user/3Test User37.7749295 -122.4194155testuserTest UserJust another test user.San Francisco, CAhomepagehttp://example.com/blog.htmltruehttp://example.net/mysite/notice/72010-02-24T00:53:06+00:002010-02-24T00:53:06+00:00Hey man, is that Freedom Code?! #<span class="tag"><a href="http://example.net/mysite/tag/freedom" rel="tag">freedom</a></span> #<span class="tag"><a href="http://example.net/mysite/tag/hippy" rel="tag">hippy</a></span>37.8313160 -122.2852473
EXAMPLE5;
$_example6 = <<WordPress.com News
http://en.blog.wordpress.com
The latest news on WordPress.com and the WordPress community.Thu, 18 Mar 2010 23:25:35 +0000http://wordpress.com/enhourly1http://www.gravatar.com/blavatar/e6392390e3bcfadff3671c5a5653d95b?s=96&d=http://s2.wp.com/i/buttonw-com.pngWordPress.com News
http://en.blog.wordpress.com
Rub-a-Dub-Dub in the PubSubHubbub
http://en.blog.wordpress.com/2010/03/03/rub-a-dub-dub-in-the-pubsubhubbub/
http://en.blog.wordpress.com/2010/03/03/rub-a-dub-dub-in-the-pubsubhubbub/#commentsWed, 03 Mar 2010 16:48:12 +0000Joseph Scotthttp://en.blog.wordpress.com/?p=3857]]>From the tongue twisting name department we welcome PubSubHubbub, or as some people have shortened it to: PuSH. Like rssCloud, PuSH is a way for services that subscribe to updates from your blog to get updates even faster. In a nutshell, instead of having to periodically ask your blog if there are any updates they can now register to automatically receive updates each time you publish new content. In most cases these updates are sent out within a second or two of when you hit the publish button.
Today we’ve turned on PuSH support for the more than 10.5 million blogs on WordPress.com. There’s nothing to configure, it’s working right now behind the scenes to help others keep up to date with your posts.
For those using the WordPress.org software we are releasing a new PuSH plugin: PuSHPress. This plugin differs from the current PuSH related plugins by including a built-in hub.
]]>
http://en.blog.wordpress.com/2010/03/03/rub-a-dub-dub-in-the-pubsubhubbub/feed/96josephscott
EXAMPLE6;
$_example7 = <<evanpro's posterous
http://evanpro.posterous.com
Most recent posts at evanpro's posterousposterous.comSat, 20 Mar 2010 07:32:31 -0700Checking out captain bones
http://evanpro.posterous.com/checking-out-captain-bones
http://evanpro.posterous.com/checking-out-captain-bones
]]>
http://files.posterous.com/user_profile_pics/480326/2009-08-05-142447.jpghttp://posterous.com/people/3sDslhaepotzEvanProdromouevanproEvan Prodromou
EXAMPLE7;
$_example8 = <<Activity Stream for: zcopleyhttp://cliqset.com/feed/atom?uid=zcopley0http://activitystrea.ms/schema/1.0/post2010-03-22T01:35:53.000Zflickrhttp://flickr.comhttp://cliqset-services.s3.amazonaws.com/flickr.pnghttp://activitystrea.ms/schema/1.0/photoIMG_1368http://activitystrea.ms/schema/1.0/photoIMG_1365http://activitystrea.ms/schema/1.0/photoClassic-Powered by pikchur.com/n0uhttp://activitystrea.ms/schema/1.0/photoIMG_1363http://activitystrea.ms/schema/1.0/photoIMG_1361zcopley posted some photos on Flickrzcopley posted 5 photos on Flickr2010-03-22T20:46:42.778Ztag:cliqset.com,2010-03-22:/user/zcopley/SVgAZubGhtAnSAeezcopleyhttp://cliqset.com/user/zcopleyhttp://activitystrea.ms/schema/1.0/personzcopleyZachCopley
EXAMPLE8;
$_example9 = <<Test2010-03-22T01:55:53.596Ztest53725745374testBuzz by Zach Copley from FlickrIMG_13662010-03-18T04:29:23.000Z2010-03-18T05:14:03.325Ztest53725745374entryZach Copleyhttps://mywebsite.net/profiles/zcopley<div>IMG_1366</div>IMG_1366IMG_1365http://activitystrea.ms/schema/1.0/posthttp://activitystrea.ms/schema/1.0/phototest53725745374entryBuzz by Zach Copley from Flickr<div>IMG_1366</div>0
EXAMPLE9;
// Sample PuSH entry from a group feed in 0.9.7
// Old has been removed from entries in this version.
// A bug in the order of input processing meant that we were incorrectly
// reading the feed's instead of the entry's ,
// causing the entry to get rejected as malformed (groups can't post on
// their own; we want to see the actual author's info here).
$_example10 = <<StatusNethttp://lazarus.local/mublog/api/statusnet/groups/timeline/22.atomgrouptest316173 timelineUpdates from grouptest316173 on Blaguette!http://lazarus.local/mublog/theme/default/default-avatar-profile.png2011-01-06T22:44:18+00:00http://activitystrea.ms/schema/1.0/grouphttp://lazarus.local/mublog/group/22/idgrouptest316173grouptest316173grouptest316173http://activitystrea.ms/schema/1.0/grouphttp://lazarus.local/mublog/group/22/idgrouptest316173grouptest316173grouptest316173http://activitystrea.ms/schema/1.0/notehttp://lazarus.local/mublog/notice/1243Group post from local to !grouptest316173, should go out over push.Group post from local to !<span class="vcard"><a href="http://lazarus.local/mublog/group/22/id" class="url"><span class="fn nickname">grouptest316173</span></a></span>, should go out over push.http://activitystrea.ms/schema/1.0/post2011-01-06T22:44:18+00:002011-01-06T22:44:18+00:00http://activitystrea.ms/schema/1.0/personhttp://lazarus.local/mublog/user/557Pubtest316173 Smithpubtest316173Pubtest316173 SmithStub account for OStatus tests.homepagehttp://example.org/pubtest316173true
EXAMPLE10;
$_example11 = <<StatusNethttp://freelish.us/api/statuses/user_timeline/1.atomdemon timelineUpdates from demon on freelish.us!http://avatar.status.net/f/freelishus/1-96-20110331163048.jpeg2011-05-30T09:36:03-04:00http://activitystrea.ms/schema/1.0/personhttp://freelishus.status.net/user/1demon45.50884 -73.58781demonEvan ProdromouMontreal hacker and entrepreneur.Montreal, Quebechomepagehttp://evan.status.net/truehttp://activitystrea.ms/schema/1.0/personhttp://freelishus.status.net/user/1Evan Prodromou45.50884 -73.58781demonEvan ProdromouMontreal hacker and entrepreneur.Montreal, Quebechomepagehttp://evan.status.net/truehttp://activitystrea.ms/schema/1.0/bookmarkhttp://freelish.us/bookmark/9e930c3e-7ed9-47de-aba5-df6c60cec542Why you should build an open-source startup | Teambox Bloghttp://activitystrea.ms/schema/1.0/post2011-05-26T20:36:25+00:002011-05-26T20:36:25+00:00
EXAMPLE11;