From 5f94efc45463378f246f9db82e9f2e0e8a109f7d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 12 Feb 2010 00:42:42 -0500 Subject: stub for activities --- plugins/OStatus/lib/activity.php | 85 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 plugins/OStatus/lib/activity.php (limited to 'plugins/OStatus/lib/activity.php') diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php new file mode 100644 index 000000000..36e227913 --- /dev/null +++ b/plugins/OStatus/lib/activity.php @@ -0,0 +1,85 @@ +. + * + * @category OStatus + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class ActivityNoun +{ + const ARTICLE = 'http://activitystrea.ms/schema/1.0/article'; + const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry'; + const NOTE = 'http://activitystrea.ms/schema/1.0/note'; + const STATUS = 'http://activitystrea.ms/schema/1.0/status'; + const FILE = 'http://activitystrea.ms/schema/1.0/file'; + const PHOTO = 'http://activitystrea.ms/schema/1.0/photo'; + const ALBUM = 'http://activitystrea.ms/schema/1.0/photo-album'; + const PLAYLIST = 'http://activitystrea.ms/schema/1.0/playlist'; + const VIDEO = 'http://activitystrea.ms/schema/1.0/video'; + const AUDIO = 'http://activitystrea.ms/schema/1.0/audio'; + const BOOKMARK = 'http://activitystrea.ms/schema/1.0/bookmark'; + const PERSON = 'http://activitystrea.ms/schema/1.0/person'; + const GROUP = 'http://activitystrea.ms/schema/1.0/group'; + const PLACE = 'http://activitystrea.ms/schema/1.0/place'; + const COMMENT = 'http://activitystrea.ms/schema/1.0/comment'; // tea + + public $type; + public $id; + public $title; + public $summary; + public $content; +} + +class Activity +{ + const NAMESPACE = 'http://activitystrea.ms/schema/1.0/'; + + const POST = 'http://activitystrea.ms/schema/1.0/post'; + const SHARE = 'http://activitystrea.ms/schema/1.0/share'; + const SAVE = 'http://activitystrea.ms/schema/1.0/save'; + const FAVORITE = 'http://activitystrea.ms/schema/1.0/favorite'; + const PLAY = 'http://activitystrea.ms/schema/1.0/play'; + const FOLLOW = 'http://activitystrea.ms/schema/1.0/follow'; + const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend'; + const JOIN = 'http://activitystrea.ms/schema/1.0/join'; + const TAG = 'http://activitystrea.ms/schema/1.0/tag'; + + public $actor; // an ActivityNoun + public $verb; // a string (the URL) + public $object; // an ActivityNoun + public $target; // an ActivityNoun + + static function fromAtomEntry($domEntry) + { + } + + function toAtomEntry() + { + } +} -- cgit v1.2.3-54-g00ecf From f78cf31303f9973447b85ecfe7557d452e97a21c Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 14 Feb 2010 12:12:47 -0500 Subject: update activity and salmon for previous commit --- plugins/OStatus/actions/salmon.php | 2 +- plugins/OStatus/lib/activity.php | 303 +++++++++++++++++++++++++++++++++++-- 2 files changed, 293 insertions(+), 12 deletions(-) (limited to 'plugins/OStatus/lib/activity.php') diff --git a/plugins/OStatus/actions/salmon.php b/plugins/OStatus/actions/salmon.php index b616027a9..c79d09c95 100644 --- a/plugins/OStatus/actions/salmon.php +++ b/plugins/OStatus/actions/salmon.php @@ -61,7 +61,7 @@ class SalmonAction extends Action // XXX: check that document element is Atom entry // XXX: check the signature - $this->act = Activity::fromAtomEntry($dom->documentElement); + $this->act = new Activity($dom->documentElement); } function handle($args) diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php index 36e227913..11aab2848 100644 --- a/plugins/OStatus/lib/activity.php +++ b/plugins/OStatus/lib/activity.php @@ -31,7 +31,72 @@ if (!defined('STATUSNET')) { exit(1); } -class ActivityNoun +/** + * Utilities for turning DOMish things into Activityish things + * + * Some common functions that I didn't have the bandwidth to try to factor + * into some kind of reasonable superclass, so just dumped here. Might + * be useful to have an ActivityObject parent class or something. + * + * @category OStatus + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class ActivityUtils +{ + const ATOM = 'http://www.w3.org/2005/Atom'; + + const LINK = 'link'; + const REL = 'rel'; + const TYPE = 'type'; + const HREF = 'href'; + + /** + * Get the permalink for an Activity object + * + * @param DOMElement $element A DOM element + * + * @return string related link, if any + */ + + static function getLink($element) + { + $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK); + + foreach ($links as $link) { + if ($link->getAttributeNS(self::ATOM, self::REL) == 'alternate' && + $link->getAttributeNS(self::ATOM, self::TYPE) == 'text/html') { + return $link->getAttributeNS(self::ATOM, self::HREF); + } + } + + return null; + } +} + +/** + * A noun-ish thing in the activity universe + * + * The activity streams spec talks about activity objects, while also having + * a tag activity:object, which is in fact an activity object. Aaaaaah! + * + * This is just a thing in the activity universe. Can be the subject, object, + * or indirect object (target!) of an activity verb. Rotten name, and I'm + * propagating it. *sigh* + * + * @category OStatus + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class ActivityObject { const ARTICLE = 'http://activitystrea.ms/schema/1.0/article'; const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry'; @@ -47,19 +112,101 @@ class ActivityNoun const PERSON = 'http://activitystrea.ms/schema/1.0/person'; const GROUP = 'http://activitystrea.ms/schema/1.0/group'; const PLACE = 'http://activitystrea.ms/schema/1.0/place'; - const COMMENT = 'http://activitystrea.ms/schema/1.0/comment'; // tea + const COMMENT = 'http://activitystrea.ms/schema/1.0/comment'; + // ^^^^^^^^^^ tea! + + // Atom elements we snarf + + const TITLE = 'title'; + const SUMMARY = 'summary'; + const CONTENT = 'content'; + const ID = 'id'; + const SOURCE = 'source'; + + const NAME = 'name'; + const URI = 'uri'; public $type; public $id; public $title; public $summary; public $content; + public $link; + public $source; + + /** + * Constructor + * + * This probably needs to be refactored + * to generate a local class (ActivityPerson, ActivityFile, ...) + * based on the object type. + * + * @param DOMElement $element DOM thing to turn into an Activity thing + */ + + function __construct($element) + { + $this->source = $element; + + if ($element->tagName == 'author') { + + $this->type = self::PERSON; // XXX: is this fair? + $this->title = $this->_childContent($element, self::NAME); + $this->id = $this->_childContent($element, self::URI); + + } else { + + $this->type = $this->_childContent($element, Activity::OBJECTTYPE, + Activity::SPEC); + + $this->id = $this->_childContent($element, self::ID); + $this->title = $this->_childContent($element, self::TITLE); + $this->summary = $this->_childContent($element, self::SUMMARY); + $this->content = $this->_childContent($element, self::CONTENT); + $this->source = $this->_childContent($element, self::SOURCE); + + $this->link = ActivityUtils::getLink($element); + + // XXX: grab PoCo stuff + } + } + + /** + * Grab the text content of a DOM element child of the current element + * + * @param DOMElement $element Element whose children we examine + * @param string $tag Tag to look up + * @param string $namespace Namespace to use, defaults to Atom + * + * @return string content of the child + */ + + private function _childContent($element, $tag, $namespace=Activity::ATOM) + { + $els = $element->getElementsByTagnameNS($namespace, $tag); + + if (empty($els) || $els->length == 0) { + return null; + } else { + $el = $els->item(0); + return $el->textContent; + } + } } -class Activity -{ - const NAMESPACE = 'http://activitystrea.ms/schema/1.0/'; +/** + * Utility class to hold a bunch of constant defining default verb types + * + * @category OStatus + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ +class ActivityVerb +{ const POST = 'http://activitystrea.ms/schema/1.0/post'; const SHARE = 'http://activitystrea.ms/schema/1.0/share'; const SAVE = 'http://activitystrea.ms/schema/1.0/save'; @@ -69,17 +216,151 @@ class Activity const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend'; const JOIN = 'http://activitystrea.ms/schema/1.0/join'; const TAG = 'http://activitystrea.ms/schema/1.0/tag'; +} + +/** + * An activity in the ActivityStrea.ms world + * + * An activity is kind of like a sentence: someone did something + * to something else. + * + * 'someone' is the 'actor'; 'did something' is the verb; + * 'something else' is the object. + * + * @category OStatus + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class Activity +{ + const SPEC = 'http://activitystrea.ms/spec/1.0/'; + const SCHEMA = 'http://activitystrea.ms/schema/1.0/'; + + const VERB = 'verb'; + const OBJECT = 'object'; + const ACTOR = 'actor'; + const SUBJECT = 'subject'; + const OBJECTTYPE = 'object-type'; + const CONTEXT = 'context'; + const TARGET = 'target'; + + const ATOM = 'http://www.w3.org/2005/Atom'; - public $actor; // an ActivityNoun - public $verb; // a string (the URL) - public $object; // an ActivityNoun - public $target; // an ActivityNoun + const AUTHOR = 'author'; + const PUBLISHED = 'published'; - static function fromAtomEntry($domEntry) + public $actor; // an ActivityObject + public $verb; // a string (the URL) + public $object; // an ActivityObject + public $target; // an ActivityObject + public $context; // an ActivityObject + public $time; // Time of the activity + public $link; // an ActivityObject + public $entry; // the source entry + public $feed; // the source feed + + /** + * Turns a regular old Atom into a magical activity + * + * @param DOMElement $entry Atom entry to poke at + * @param DOMElement $feed Atom feed, for context + */ + + function __construct($entry, $feed = null) { + $this->entry = $entry; + $this->feed = $feed; + + $pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM); + + if (!empty($pubEl)) { + $this->time = strtotime($pubEl->textContent); + } else { + // XXX technically an error; being liberal. Good idea...? + $this->time = null; + } + + $this->link = ActivityUtils::getLink($entry); + + $verbEl = $this->_child($entry, self::VERB); + + if (!empty($verbEl)) { + $this->verb = trim($verbEl->textContent); + } else { + $this->verb = ActivityVerb::POST; + // XXX: do other implied stuff here + } + + $objectEl = $this->_child($entry, self::OBJECT); + + if (!empty($objectEl)) { + $this->object = new ActivityObject($objectEl); + } else { + $this->object = new ActivityObject($entry); + } + + $actorEl = $this->_child($entry, self::ACTOR); + + if (!empty($actorEl)) { + + $this->actor = new ActivityObject($actorEl); + + } else if (!empty($feed) && + $subjectEl = $this->_child($feed, self::SUBJECT)) { + + $this->actor = new ActivityObject($subjectEl); + + } else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) { + + $this->actor = new ActivityObject($authorEl); + } + + $contextEl = $this->_child($entry, self::CONTEXT); + + if (!empty($contextEl)) { + $this->context = new ActivityObject($contextEl); + } + + $targetEl = $this->_child($entry, self::TARGET); + + if (!empty($targetEl)) { + $this->target = new ActivityObject($targetEl); + } } + /** + * Returns an Atom based on this activity + * + * @return DOMElement Atom entry + */ + function toAtomEntry() { + return null; } -} + + /** + * Gets the first child element with the given tag + * + * @param DOMElement $element element to pick at + * @param string $tag tag to look for + * @param string $namespace Namespace to look under + * + * @return DOMElement found element or null + */ + + private function _child($element, $tag, $namespace=self::SPEC) + { + $els = $element->getElementsByTagnameNS($namespace, $tag); + + if (empty($els) || $els->length == 0) { + return null; + } else { + return $els->item(0); + } + } +} \ No newline at end of file -- cgit v1.2.3-54-g00ecf From 73e2264c6aa7c1fa3a6e4b63f41b210af3c50597 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 14 Feb 2010 13:19:32 -0500 Subject: test parsing a default atom feed for activities --- plugins/OStatus/lib/activity.php | 39 +++++++++++++--- plugins/OStatus/tests/ActivityParseTests.php | 67 ++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 6 deletions(-) (limited to 'plugins/OStatus/lib/activity.php') diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php index 11aab2848..048efda2c 100644 --- a/plugins/OStatus/lib/activity.php +++ b/plugins/OStatus/lib/activity.php @@ -68,9 +68,12 @@ class ActivityUtils $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK); foreach ($links as $link) { - if ($link->getAttributeNS(self::ATOM, self::REL) == 'alternate' && - $link->getAttributeNS(self::ATOM, self::TYPE) == 'text/html') { - return $link->getAttributeNS(self::ATOM, self::HREF); + + $rel = $link->getAttribute(self::REL); + $type = $link->getAttribute(self::TYPE); + + if ($rel == 'alternate' && $type == 'text/html') { + return $link->getAttribute(self::HREF); } } @@ -123,8 +126,9 @@ class ActivityObject const ID = 'id'; const SOURCE = 'source'; - const NAME = 'name'; - const URI = 'uri'; + const NAME = 'name'; + const URI = 'uri'; + const EMAIL = 'email'; public $type; public $id; @@ -154,11 +158,23 @@ class ActivityObject $this->title = $this->_childContent($element, self::NAME); $this->id = $this->_childContent($element, self::URI); + if (empty($this->id)) { + $email = $this->_childContent($element, self::EMAIL); + if (!empty($email)) { + // XXX: acct: ? + $this->id = 'mailto:'.$email; + } + } + } else { $this->type = $this->_childContent($element, Activity::OBJECTTYPE, Activity::SPEC); + if (empty($this->type)) { + $this->type = ActivityObject::NOTE; + } + $this->id = $this->_childContent($element, self::ID); $this->title = $this->_childContent($element, self::TITLE); $this->summary = $this->_childContent($element, self::SUMMARY); @@ -252,6 +268,7 @@ class Activity const AUTHOR = 'author'; const PUBLISHED = 'published'; + const UPDATED = 'updated'; public $actor; // an ActivityObject public $verb; // a string (the URL) @@ -281,7 +298,12 @@ class Activity $this->time = strtotime($pubEl->textContent); } else { // XXX technically an error; being liberal. Good idea...? - $this->time = null; + $updateEl = $this->_child($entry, self::UPDATED, self::ATOM); + if (!empty($updateEl)) { + $this->time = strtotime($updateEl->textContent); + } else { + $this->time = null; + } } $this->link = ActivityUtils::getLink($entry); @@ -317,6 +339,11 @@ class Activity } else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) { $this->actor = new ActivityObject($authorEl); + + } else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR, + self::ATOM)) { + + $this->actor = new ActivityObject($authorEl); } $contextEl = $this->_child($entry, self::CONTEXT); diff --git a/plugins/OStatus/tests/ActivityParseTests.php b/plugins/OStatus/tests/ActivityParseTests.php index 889fa892f..fa8bcdda2 100644 --- a/plugins/OStatus/tests/ActivityParseTests.php +++ b/plugins/OStatus/tests/ActivityParseTests.php @@ -25,6 +25,44 @@ class ActivityParseTests extends PHPUnit_Framework_TestCase $this->assertEquals($act->time, 1243860840); $this->assertEquals($act->verb, ActivityVerb::POST); } + + public function testExample3() + { + global $_example3; + $dom = DOMDocument::loadXML($_example3); + + $feed = $dom->documentElement; + + $entries = $feed->getElementsByTagName('entry'); + + $entry = $entries->item(0); + + $act = new Activity($entry, $feed); + + $this->assertFalse(empty($act)); + $this->assertEquals($act->time, 1071340202); + $this->assertEquals($act->link, 'http://example.org/2003/12/13/atom03.html'); + + $this->assertEquals($act->verb, ActivityVerb::POST); + + $this->assertFalse(empty($act->actor)); + $this->assertEquals($act->actor->type, ActivityObject::PERSON); + $this->assertEquals($act->actor->title, 'John Doe'); + $this->assertEquals($act->actor->id, 'mailto:johndoe@example.com'); + + $this->assertFalse(empty($act->object)); + $this->assertEquals($act->object->type, ActivityObject::NOTE); + $this->assertEquals($act->object->id, 'urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a'); + $this->assertEquals($act->object->title, 'Atom-Powered Robots Run Amok'); + $this->assertEquals($act->object->summary, 'Some text.'); + $this->assertEquals($act->object->link, 'http://example.org/2003/12/13/atom03.html'); + + $this->assertTrue(empty($act->context)); + $this->assertTrue(empty($act->target)); + + $this->assertEquals($act->entry, $entry); + $this->assertEquals($act->feed, $feed); + } } $_example1 = << EXAMPLE2; + +$_example3 = << + + + + Example Feed + A subtitle. + + + urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6 + 2003-12-13T18:30:02Z + + John Doe + johndoe@example.com + + + + Atom-Powered Robots Run Amok + + + + urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a + 2003-12-13T18:30:02Z + Some text. + + + +EXAMPLE3; -- cgit v1.2.3-54-g00ecf