]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch 'testing' of gitorious.org:statusnet/mainline into 0.9.x
authorBrion Vibber <brion@pobox.com>
Fri, 12 Feb 2010 19:18:35 +0000 (11:18 -0800)
committerBrion Vibber <brion@pobox.com>
Fri, 12 Feb 2010 19:18:35 +0000 (11:18 -0800)
45 files changed:
actions/apitimelinefavorites.php
actions/apitimelinefriends.php
actions/apitimelinegroup.php
actions/apitimelinehome.php
actions/apitimelinementions.php
actions/apitimelinepublic.php
actions/apitimelineretweetsofme.php
actions/apitimelinetag.php
actions/apitimelineuser.php
actions/showgroup.php
classes/Nonce.php
classes/Notice.php
classes/Profile.php
classes/User_group.php
classes/statusnet.links.ini
lib/api.php
lib/atom10entry.php [new file with mode: 0644]
lib/atom10feed.php [new file with mode: 0644]
lib/atomnoticefeed.php [new file with mode: 0644]
lib/default.php
lib/queuemanager.php
lib/spawningdaemon.php
lib/stompqueuemanager.php
lib/util.php
plugins/OStatus/OStatusPlugin.php
plugins/OStatus/actions/feedsubsettings.php
plugins/OStatus/actions/hostmeta.php [new file with mode: 0644]
plugins/OStatus/actions/ostatusinit.php [new file with mode: 0644]
plugins/OStatus/actions/ostatussub.php [new file with mode: 0644]
plugins/OStatus/actions/pushcallback.php
plugins/OStatus/actions/salmon.php [new file with mode: 0644]
plugins/OStatus/actions/webfinger.php [new file with mode: 0644]
plugins/OStatus/classes/Feedinfo.php [deleted file]
plugins/OStatus/classes/HubSub.php
plugins/OStatus/classes/Ostatus_profile.php [new file with mode: 0644]
plugins/OStatus/lib/activity.php [new file with mode: 0644]
plugins/OStatus/lib/feedmunger.php
plugins/OStatus/lib/hubdistribqueuehandler.php
plugins/OStatus/lib/huboutqueuehandler.php
plugins/OStatus/lib/salmon.php [new file with mode: 0644]
plugins/OStatus/lib/webfinger.php [new file with mode: 0644]
plugins/OStatus/lib/xrd.php [new file with mode: 0644]
theme/base/css/display.css
theme/default/css/display.css
theme/identica/css/display.css

index 1027d97d440047f77c3401b9b446b070a974137f..f7f900ddfb5d6382f70c4a1752de799f2473038d 100644 (file)
@@ -100,11 +100,11 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction
 
     function showTimeline()
     {
-        $profile = $this->user->getProfile();
-        $avatar     = $profile->getAvatar(AVATAR_PROFILE_SIZE);
+        $profile  = $this->user->getProfile();
+        $avatar   = $profile->getAvatar(AVATAR_PROFILE_SIZE);
 
-        $sitename   = common_config('site', 'name');
-        $title      = sprintf(
+        $sitename = common_config('site', 'name');
+        $title    = sprintf(
             _('%1$s / Favorites from %2$s'),
             $sitename,
             $this->user->nickname
@@ -112,32 +112,69 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction
 
         $taguribase = common_config('integration', 'taguri');
         $id         = "tag:$taguribase:Favorites:" . $this->user->id;
-        $link       = common_local_url(
-            'favorites',
-            array('nickname' => $this->user->nickname)
-        );
-        $subtitle   = sprintf(
+
+        $subtitle = sprintf(
             _('%1$s updates favorited by %2$s / %2$s.'),
             $sitename,
             $profile->getBestName(),
             $this->user->nickname
         );
-        $logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
+        $logo = !empty($avatar)
+            ? $avatar->displayUrl()
+            : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
 
         switch($this->format) {
         case 'xml':
             $this->showXmlTimeline($this->notices);
             break;
         case 'rss':
-            $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
+            $link = common_local_url(
+                'showfavorites',
+                array('nickname' => $this->user->nickname)
+            );
+            $this->showRssTimeline(
+                $this->notices,
+                $title,
+                $link,
+                $subtitle,
+                null,
+                $logo
+            );
             break;
         case 'atom':
-            $selfuri = common_root_url() .
-                ltrim($_SERVER['QUERY_STRING'], 'p=');
-            $this->showAtomTimeline(
-                $this->notices, $title, $id, $link, $subtitle,
-                null, $selfuri, $logo
+
+            header('Content-Type: application/atom+xml; charset=utf-8');
+
+            $atom = new AtomNoticeFeed();
+
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setLogo($logo);
+            $atom->setUpdated('now');
+
+            $atom->addLink(
+                common_local_url(
+                    'showfavorites',
+                    array('nickname' => $this->user->nickname)
+                )
+            );
+
+            $id = $this->arg('id');
+            $aargs = array('format' => 'atom');
+            if (!empty($id)) {
+                $aargs['id'] = $id;
+            }
+
+            $atom->addLink(
+                $this->getSelfUri('ApiTimelineFavorites', $aargs),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
             );
+
+            $atom->addEntryFromNotices($this->notices);
+
+            $this->raw($atom->getString());
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
index 4e3827baea61a5bf9f1c947e8f2cad55b86d7f08..0af04fe4fb2cef71c07b1ce90528e4016806a349 100644 (file)
@@ -114,39 +114,71 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction
         $title      = sprintf(_("%s and friends"), $this->user->nickname);
         $taguribase = common_config('integration', 'taguri');
         $id         = "tag:$taguribase:FriendsTimeline:" . $this->user->id;
-        $link       = common_local_url(
-                                       'all', array('nickname' => $this->user->nickname)
-                                       );
-        $subtitle   = sprintf(
-                              _('Updates from %1$s and friends on %2$s!'),
-                              $this->user->nickname, $sitename
-                              );
-        $logo       = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
+
+        $subtitle = sprintf(
+            _('Updates from %1$s and friends on %2$s!'),
+            $this->user->nickname, $sitename
+        );
+
+        $logo = (!empty($avatar))
+            ? $avatar->displayUrl()
+            : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
 
         switch($this->format) {
         case 'xml':
             $this->showXmlTimeline($this->notices);
             break;
         case 'rss':
-            $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
+
+            $link = common_local_url(
+                'all', array(
+                    'nickname' => $this->user->nickname
+                )
+            );
+
+            $this->showRssTimeline(
+                $this->notices,
+                $title,
+                $link,
+                $subtitle,
+                null,
+                $logo
+            );
             break;
         case 'atom':
 
-            $target_id = $this->arg('id');
+            header('Content-Type: application/atom+xml; charset=utf-8');
+
+            $atom = new AtomNoticeFeed();
+
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setLogo($logo);
+            $atom->setUpdated('now');
 
-            if (isset($target_id)) {
-                $selfuri = common_root_url() .
-                  'api/statuses/friends_timeline/' .
-                  $target_id . '.atom';
-            } else {
-                $selfuri = common_root_url() .
-                  'api/statuses/friends_timeline.atom';
+            $atom->addLink(
+                common_local_url(
+                    'all',
+                    array('nickname' => $this->user->nickname)
+                )
+            );
+
+            $id = $this->arg('id');
+            $aargs = array('format' => 'atom');
+            if (!empty($id)) {
+                $aargs['id'] = $id;
             }
 
-            $this->showAtomTimeline(
-                                    $this->notices, $title, $id, $link,
-                                    $subtitle, null, $selfuri, $logo
-                                    );
+            $atom->addLink(
+                $this->getSelfUri('ApiTimelineFriends', $aargs),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
+            );
+
+            $atom->addEntryFromNotices($this->notices);
+
+            $this->raw($atom->getString());
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
index af414c680403e557361d15ef3c9e1b554353bee4..fd2ed9ff93f228f2cfcff616278b36ef9f8a6927 100644 (file)
@@ -130,7 +130,7 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction
         case 'atom':
             $selfuri = common_root_url() .
                 'api/statusnet/groups/timeline/' .
-                    $this->group->nickname . '.atom';
+                    $this->group->id . '.atom';
             $this->showAtomTimeline(
                 $this->notices,
                 $title,
index 828eae6cf6f912382f350afe2fe17ba716041bc9..ae41680702fd6484349eba4ff178edd589abdc1d 100644 (file)
@@ -115,39 +115,67 @@ class ApiTimelineHomeAction extends ApiBareAuthAction
         $title      = sprintf(_("%s and friends"), $this->user->nickname);
         $taguribase = common_config('integration', 'taguri');
         $id         = "tag:$taguribase:HomeTimeline:" . $this->user->id;
-        $link       = common_local_url(
-            'all', array('nickname' => $this->user->nickname)
-        );
+
         $subtitle   = sprintf(
             _('Updates from %1$s and friends on %2$s!'),
             $this->user->nickname, $sitename
         );
-        $logo       = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
+
+        $logo = (!empty($avatar)) 
+            ? $avatar->displayUrl() 
+            : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
 
         switch($this->format) {
         case 'xml':
             $this->showXmlTimeline($this->notices);
             break;
         case 'rss':
-            $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
+            $link = common_local_url(
+                'all',
+                array('nickname' => $this->user->nickname)
+            );
+            $this->showRssTimeline(
+                $this->notices,
+                $title,
+                $link,
+                $subtitle,
+                null,
+                $logo
+            );
             break;
         case 'atom':
 
-            $target_id = $this->arg('id');
+            header('Content-Type: application/atom+xml; charset=utf-8');
+
+            $atom = new AtomNoticeFeed();
 
-            if (isset($target_id)) {
-                $selfuri = common_root_url() .
-                    'api/statuses/home_timeline/' .
-                    $target_id . '.atom';
-            } else {
-                $selfuri = common_root_url() .
-                    'api/statuses/home_timeline.atom';
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setLogo($logo);
+            $atom->setUpdated('now');
+
+            $atom->addLink(
+                common_local_url(
+                    'all',
+                    array('nickname' => $this->user->nickname)
+                )
+            );
+
+            $id = $this->arg('id');
+            $aargs = array('format' => 'atom');
+            if (!empty($id)) {
+                $aargs['id'] = $id;
             }
 
-            $this->showAtomTimeline(
-                $this->notices, $title, $id, $link,
-                $subtitle, null, $selfuri, $logo
+            $atom->addLink(
+                $this->getSelfUri('ApiTimelineHome', $aargs),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
             );
+
+            $atom->addEntryFromNotices($this->notices);
+            $this->raw($atom->getString());
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
index 9dc2162cc45fa738e8c368c0b8841f19ef3f677a..d2e31d0bdd095fe40e592b98647dd14ee50a5689 100644 (file)
@@ -137,12 +137,36 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction
             $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
             break;
         case 'atom':
-            $selfuri = common_root_url() .
-                ltrim($_SERVER['QUERY_STRING'], 'p=');
-            $this->showAtomTimeline(
-                $this->notices, $title, $id, $link, $subtitle,
-                null, $selfuri, $logo
+
+            $atom = new AtomNoticeFeed();
+
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setLogo($logo);
+            $atom->setUpdated('now');
+
+            $atom->addLink(
+                common_local_url(
+                    'replies',
+                    array('nickname' => $this->user->nickname)
+                )
+            );
+
+            $id = $this->arg('id');
+            $aargs = array('format' => 'atom');
+            if (!empty($id)) {
+                $aargs['id'] = $id;
+            }
+
+            $atom->addLink(
+                $this->getSelfUri('ApiTimelineMentions', $aargs),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
             );
+
+            $atom->addEntryFromNotices($this->notices);
+            $this->raw($atom->getString());
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
index 3f4a46c0fa16e2bc6b55483d32304cce6e0807da..c1fa72a3ee372f72e944931f86c6480bff17aabc 100644 (file)
@@ -75,6 +75,10 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
 
         $this->notices = $this->getNotices();
 
+        if ($this->since) {
+            throw new ServerException("since parameter is disabled for performance; use since_id", 403);
+        }
+
         return true;
     }
 
@@ -118,11 +122,28 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
             $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo);
             break;
         case 'atom':
-            $selfuri = common_root_url() . 'api/statuses/public_timeline.atom';
-            $this->showAtomTimeline(
-                $this->notices, $title, $id, $link,
-                $subtitle, null, $selfuri, $sitelogo
+
+            $atom = new AtomNoticeFeed();
+
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setLogo($sitelogo);
+            $atom->setUpdated('now');
+
+            $atom->addLink(common_local_url('public'));
+
+            $atom->addLink(
+                $this->getSelfUri(
+                    'ApiTimelinePublic', array('format' => 'atom')
+                ),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
             );
+
+            $atom->addEntryFromNotices($this->notices);
+
+            $this->raw($atom->getString());
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
@@ -145,7 +166,7 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
 
         $notice = Notice::publicStream(
             ($this->page - 1) * $this->count, $this->count, $this->since_id,
-            $this->max_id, $this->since
+            $this->max_id
         );
 
         while ($notice->fetch()) {
index e4b09e9bdaf334b860022482d5d1e0bac6b41bd9..26706a75e7650c1c65717bd79237676e635d36b5 100644 (file)
@@ -99,6 +99,8 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction
 
         $strm = $this->auth_user->repeatsOfMe($offset, $limit, $this->since_id, $this->max_id);
 
+        common_debug(var_export($strm, true));
+
         switch ($this->format) {
         case 'xml':
             $this->showXmlTimeline($strm);
@@ -112,10 +114,38 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction
             $title      = sprintf(_("Repeats of %s"), $this->auth_user->nickname);
             $taguribase = common_config('integration', 'taguri');
             $id         = "tag:$taguribase:RepeatsOfMe:" . $this->auth_user->id;
-            $link       = common_local_url('showstream',
-                                           array('nickname' => $this->auth_user->nickname));
 
-            $this->showAtomTimeline($strm, $title, $id, $link);
+            header('Content-Type: application/atom+xml; charset=utf-8');
+
+            $atom = new AtomNoticeFeed();
+
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setUpdated('now');
+
+            $atom->addLink(
+                common_local_url(
+                    'showstream',
+                    array('nickname' => $this->auth_user->nickname)
+                )
+            );
+
+            $id = $this->arg('id');
+            $aargs = array('format' => 'atom');
+            if (!empty($id)) {
+                $aargs['id'] = $id;
+            }
+
+            $atom->addLink(
+                $this->getSelfUri('ApiTimelineRetweetsOfMe', $aargs),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
+            );
+
+            $atom->addEntryFromNotices($strm);
+
+            $this->raw($atom->getString());
+
             break;
 
         default:
index 1427d23b6a45d70e0f93f26292f11d25b8c4f1d1..5b6ded4c048d6b08fe77838b2ced98b674484e51 100644 (file)
@@ -100,10 +100,6 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction
         $sitename   = common_config('site', 'name');
         $sitelogo   = (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png');
         $title      = sprintf(_("Notices tagged with %s"), $this->tag);
-        $link       = common_local_url(
-            'tag',
-            array('tag' => $this->tag)
-        );
         $subtitle   = sprintf(
             _('Updates tagged with %1$s on %2$s!'),
             $this->tag,
@@ -117,22 +113,51 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction
             $this->showXmlTimeline($this->notices);
             break;
         case 'rss':
-            $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo);
-            break;
-        case 'atom':
-            $selfuri = common_root_url() .
-                'api/statusnet/tags/timeline/' .
-                    $this->tag . '.atom';
-            $this->showAtomTimeline(
+            $link = common_local_url(
+                'tag',
+                array('tag' => $this->tag)
+            );
+            $this->showRssTimeline(
                 $this->notices,
                 $title,
-                $id,
                 $link,
                 $subtitle,
                 null,
-                $selfuri,
                 $sitelogo
             );
+            break;
+        case 'atom':
+
+            header('Content-Type: application/atom+xml; charset=utf-8');
+
+            $atom = new AtomNoticeFeed();
+
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setLogo($logo);
+            $atom->setUpdated('now');
+
+            $atom->addLink(
+                common_local_url(
+                    'tag',
+                    array('tag' => $this->tag)
+                )
+            );
+
+            $aargs = array('format' => 'atom');
+            if (!empty($this->tag)) {
+                $aargs['tag'] = $this->tag;
+            }
+
+            $atom->addLink(
+                $this->getSelfUri('ApiTimelineTag', $aargs),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
+            );
+
+            $atom->addEntryFromNotices($this->notices);
+            $this->raw($atom->getString());
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
index ed9104905de1f316c1066df4a9fdbf3332e5836f..d20bb0d2023db66ea908de0a4681a1a1891f7379 100644 (file)
@@ -145,19 +145,47 @@ class ApiTimelineUserAction extends ApiBareAuthAction
             );
             break;
         case 'atom':
+
+            header('Content-Type: application/atom+xml; charset=utf-8');
+
+            $atom = new AtomNoticeFeed();
+
+            $atom->setId($id);
+            $atom->setTitle($title);
+            $atom->setSubtitle($subtitle);
+            $atom->setLogo($logo);
+            $atom->setUpdated('now');
+
+            $atom->addLink(
+                common_local_url(
+                    'showstream',
+                    array('nickname' => $this->user->nickname)
+                )
+            );
+
             $id = $this->arg('id');
-            if ($id) {
-                $selfuri = common_root_url() .
-                    'api/statuses/user_timeline/' .
-                    rawurlencode($id) . '.atom';
-            } else {
-                $selfuri = common_root_url() .
-                    'api/statuses/user_timeline.atom';
+            $aargs = array('format' => 'atom');
+            if (!empty($id)) {
+                $aargs['id'] = $id;
             }
-            $this->showAtomTimeline(
-                $this->notices, $title, $id, $link,
-                $subtitle, $suplink, $selfuri, $logo
+
+            $atom->addLink(
+                $this->getSelfUri('ApiTimelineUser', $aargs),
+                array('rel' => 'self', 'type' => 'application/atom+xml')
+            );
+
+            $atom->addLink(
+                $suplink,
+                array(
+                    'rel' => 'http://api.friendfeed.com/2008/03#sup',
+                    'type' => 'application/json'
+                )
             );
+
+            $atom->addEntryFromNotices($this->notices);
+
+            $this->raw($atom->getString());
+
             break;
         case 'json':
             $this->showJsonTimeline($this->notices);
index 8042a4951339a26f7aebaf4ad56f2e05de3a386b..eb12389029a096dd0373ae7ed523ddc517092d62 100644 (file)
@@ -330,13 +330,13 @@ class ShowgroupAction extends GroupDesignAction
                      new Feed(Feed::RSS2,
                               common_local_url('ApiTimelineGroup',
                                                array('format' => 'rss',
-                                                     'id' => $this->group->nickname)),
+                                                     'id' => $this->group->id)),
                               sprintf(_('Notice feed for %s group (RSS 2.0)'),
                                       $this->group->nickname)),
                      new Feed(Feed::ATOM,
                               common_local_url('ApiTimelineGroup',
                                                array('format' => 'atom',
-                                                     'id' => $this->group->nickname)),
+                                                     'id' => $this->group->id)),
                               sprintf(_('Notice feed for %s group (Atom)'),
                                       $this->group->nickname)),
                      new Feed(Feed::FOAF,
index 486a65a3c7e2aa6d78bc8478f579b27e8d80ab93..2f8ab00b5dc097886687a8e2413bd59df689c59d 100644 (file)
@@ -22,4 +22,19 @@ class Nonce extends Memcached_DataObject
 
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
+
+    /**
+     * Compatibility hack for PHP 5.3
+     *
+     * The statusnet.links.ini entry cannot be read because "," is no longer
+     * allowed in key names when read by parse_ini_file().
+     *
+     * @return   array
+     * @access   public
+     */
+    function links()
+    {
+        return array('consumer_key,token' => 'token:consumer_key,token');
+    }
+
 }
index fca1c599ce39830993f8304397a11cefb8470799..924931e42b59989f0a266ad8ecba39028a64e6eb 100644 (file)
@@ -783,7 +783,7 @@ class Notice extends Memcached_DataObject
 
             $result = $gi->insert();
 
-            if (!result) {
+            if (!$result) {
                 common_log_db_error($gi, 'INSERT', __FILE__);
                 throw new ServerException(_('Problem saving group inbox.'));
             }
@@ -917,7 +917,7 @@ class Notice extends Memcached_DataObject
     /**
      * Same calculation as saveGroups but without the saving
      * @fixme merge the functions
-     * @return array of Group objects
+     * @return array of Group_inbox objects
      */
     function getGroups()
     {
@@ -957,7 +957,10 @@ class Notice extends Memcached_DataObject
 
         if ($namespace) {
             $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
-                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0');
+                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0',
+                           'xmlns:georss' => 'http://www.georss.org/georss',
+                           'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
+                           'xmlns:ostatus' => 'http://ostatus.org/schema/1.0');
         } else {
             $attrs = array();
         }
@@ -983,11 +986,6 @@ class Notice extends Memcached_DataObject
             $xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE));
         }
 
-        $xs->elementStart('author');
-        $xs->element('name', null, $profile->nickname);
-        $xs->element('uri', null, $profile->profileurl);
-        $xs->elementEnd('author');
-
         if ($source) {
             $xs->elementEnd('source');
         }
@@ -995,6 +993,9 @@ class Notice extends Memcached_DataObject
         $xs->element('title', null, $this->content);
         $xs->element('summary', null, $this->content);
 
+        $xs->raw($profile->asAtomAuthor());
+        $xs->raw($profile->asActivityActor());
+
         $xs->element('link', array('rel' => 'alternate',
                                    'href' => $this->bestUrl()));
 
@@ -1014,6 +1015,43 @@ class Notice extends Memcached_DataObject
             }
         }
 
+        if (!empty($this->conversation)
+            && $this->conversation != $this->notice->id) {
+            $xs->element(
+                'link', array(
+                    'rel' => 'ostatus:conversation',
+                    'href' => common_local_url(
+                        'conversation',
+                        array('id' => $this->conversation)
+                        )
+                    )
+                );
+        }
+
+        $reply_ids = $this->getReplies();
+
+        foreach ($reply_ids as $id) {
+            $profile = Profile::staticGet('id', $id);
+            if (!empty($profile)) {
+                $xs->element(
+                    'link', array(
+                        'rel' => 'ostatus:attention',
+                        'href' => $profile->getAcctUri()
+                    )
+                );
+            }
+        }
+
+        if (!empty($this->repeat_of)) {
+            $repeat = Notice::staticGet('id', $this->repeat_of);
+            if (!empty($repeat)) {
+                $xs->element(
+                    'ostatus:forward',
+                     array('ref' => $repeat->uri, 'href' => $repeat->bestUrl())
+                );
+            }
+        }
+
         $xs->element('content', array('type' => 'html'), $this->rendered);
 
         $tag = new Notice_tag();
@@ -1041,9 +1079,7 @@ class Notice extends Memcached_DataObject
         }
 
         if (!empty($this->lat) && !empty($this->lon)) {
-            $xs->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
             $xs->element('georss:point', null, $this->lat . ' ' . $this->lon);
-            $xs->elementEnd('geo');
         }
 
         $xs->elementEnd('entry');
index feabc250870c2effcff82b065b3b65eb021c8764..ab05bb8546bfe83d22ce66ce6164e509d32b6bc4 100644 (file)
@@ -754,4 +754,89 @@ class Profile extends Memcached_DataObject
 
         return !empty($notice);
     }
+
+    /**
+     * Returns an XML string fragment with limited profile information
+     * as an Atom <author> element.
+     *
+     * Assumes that Atom has been previously set up as the base namespace.
+     *
+     * @return string
+     */
+    function asAtomAuthor()
+    {
+        $xs = new XMLStringer(true);
+
+        $xs->elementStart('author');
+        $xs->element('name', null, $this->nickname);
+        $xs->element('uri', null, $this->profileurl);
+        $xs->elementEnd('author');
+
+        return $xs->getString();
+    }
+
+    /**
+     * Returns an XML string fragment with profile information as an
+     * Activity Streams <activity:actor> element.
+     *
+     * Assumes that 'activity' namespace has been previously defined.
+     *
+     * @return string
+     */
+    function asActivityActor()
+    {
+        return $this->asActivityNoun('actor');
+    }
+
+    /**
+     * Returns an XML string fragment with profile information as an
+     * Activity Streams noun object with the given element type.
+     *
+     * Assumes that 'activity' namespace has been previously defined.
+     *
+     * @param string $element one of 'actor', 'subject', 'object', 'target'
+     * @return string
+     */
+    function asActivityNoun($element)
+    {
+        $xs = new XMLStringer(true);
+
+        $xs->elementStart('activity:' . $element);
+        $xs->element(
+            'activity:object-type',
+            null,
+            'http://activitystrea.ms/schema/1.0/person'
+        );
+        $xs->element(
+            'id',
+            null,
+            common_local_url(
+                'userbyid',
+                array('id' => $this->id)
+                )
+            );
+        $xs->element('title', null, $this->getBestName());
+
+        $avatar = $this->getAvatar(AVATAR_PROFILE_SIZE);
+
+        $xs->element(
+            'link', array(
+                'type' => empty($avatar) ? 'image/png' : $avatar->mediatype,
+                'href' => empty($avatar)
+                ? Avatar::defaultImage(AVATAR_PROFILE_SIZE)
+                : $avatar->displayUrl()
+            ),
+            ''
+        );
+
+        $xs->elementEnd('activity:' . $element);
+
+        return $xs->getString();
+    }
+
+    function getAcctUri()
+    {
+        return $this->nickname . '@' . common_config('site', 'server');
+    }
+
 }
index c86eadf8fa7fff1e21ca9cb2f496d4aa4ce9acb4..1fbb50a6eb2b59cbecf7cc02b3768c2ad57f9b31 100644 (file)
@@ -49,12 +49,12 @@ class User_group extends Memcached_DataObject
                                 array('id' => $this->id));
     }
 
-    function getNotices($offset, $limit)
+    function getNotices($offset, $limit, $since_id=null, $max_id=null)
     {
         $ids = Notice::stream(array($this, '_streamDirect'),
                               array(),
                               'user_group:notice_ids:' . $this->id,
-                              $offset, $limit);
+                              $offset, $limit, $since_id, $max_id);
 
         return Notice::getStreamByIds($ids);
     }
index 7f233e6760be02689ad98fc5dcf62905c32689a9..b9dd5af0c9b6ab284653422d3ef5c5d3c3f29cb3 100644 (file)
@@ -19,8 +19,11 @@ profile_id = profile:id
 [token]
 consumer_key = consumer:consumer_key
 
-[nonce]
-consumer_key,token = token:consumer_key,token
+; Compatibility hack for PHP 5.3
+; This entry has been moved to the class definition, as commas are no longer
+; considered valid in keys, causing parse_ini_file() to reject the whole file.
+;[nonce]
+;consumer_key,token = token:consumer_key,token
 
 [confirm_address]
 user_id = user:id
index b987badc06a78bfd74053a5f96b032715c42b768..5758cc87455ae71d597f766b74875a00554df616 100644 (file)
@@ -1322,4 +1322,22 @@ class ApiAction extends Action
         }
     }
 
+    function getSelfUri($action, $aargs)
+    {
+        parse_str($_SERVER['QUERY_STRING'], $params);
+        $pstring = '';
+        if (!empty($params)) {
+            unset($params['p']);
+            $pstring = http_build_query($params);
+        }
+
+        $uri = common_local_url($action, $aargs);
+
+        if (!empty($pstring)) {
+            $uri .= '?' . $pstring;
+        }
+
+        return $uri;
+    }
+
 }
diff --git a/lib/atom10entry.php b/lib/atom10entry.php
new file mode 100644 (file)
index 0000000..5710c80
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building / manipulating an Atom entry in memory
+ *
+ * PHP version 5
+ *
+ * LICENCE: 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/>.
+ *
+ * @category  Feed
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')
+{
+    exit(1);
+}
+
+class Atom10EntryException extends Exception
+{
+}
+
+/**
+ * Class for manipulating an Atom entry in memory. Get the entry as an XML
+ * string with Atom10Entry::getString().
+ *
+ * @category Feed
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://status.net/
+ */
+class Atom10Entry extends XMLStringer
+{
+    private $namespaces;
+    private $categories;
+    private $content;
+    private $contributors;
+    private $id;
+    private $links;
+    private $published;
+    private $rights;
+    private $source;
+    private $summary;
+    private $title;
+
+    function __construct($indent = true) {
+        parent::__construct($indent);
+        $this->namespaces = array();
+    }
+
+    function addNamespace($namespace, $uri)
+    {
+        $ns = array($namespace => $uri);
+        $this->namespaces = array_merge($this->namespaces, $ns);
+    }
+
+    function initEntry()
+    {
+
+    }
+
+    function endEntry()
+    {
+
+    }
+
+    /**
+     * Check that all required elements have been set, etc.
+     * Throws an Atom10EntryException if something's missing.
+     *
+     * @return void
+     */
+    function validate
+    {
+
+    }
+
+    function getString()
+    {
+        $this->validate();
+
+        $this->initEntry();
+        $this->renderEntries();
+        $this->endEntry();
+
+        return $this->xw->outputMemory();
+    }
+
+}
\ No newline at end of file
diff --git a/lib/atom10feed.php b/lib/atom10feed.php
new file mode 100644 (file)
index 0000000..ccca76a
--- /dev/null
@@ -0,0 +1,227 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building an Atom feed in memory
+ *
+ * PHP version 5
+ *
+ * LICENCE: 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/>.
+ *
+ * @category  Feed
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET'))
+{
+    exit(1);
+}
+
+class Atom10FeedException extends Exception
+{
+}
+
+/**
+ * Class for building an Atom feed in memory.  Get the finished doc
+ * as a string with Atom10Feed::getString().
+ *
+ * @category Feed
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://status.net/
+ */
+class Atom10Feed extends XMLStringer
+{
+    public  $xw;
+    private $namespaces;
+    private $authors;
+    private $categories;
+    private $contributors;
+    private $generator;
+    private $icon;
+    private $links;
+    private $logo;
+    private $rights;
+    private $subtitle;
+    private $title;
+    private $published;
+    private $updated;
+    private $entries;
+
+    /**
+     * Constructor
+     *
+     * @param boolean $indent  flag to turn indenting on or off
+     *
+     * @return void
+     */
+    function __construct($indent = true) {
+        parent::__construct($indent);
+        $this->namespaces = array();
+        $this->links      = array();
+        $this->entries    = array();
+        $this->addNamespace('xmlns', 'http://www.w3.org/2005/Atom');
+    }
+
+    /**
+     * Add another namespace to the feed
+     *
+     * @param string $namespace the namespace
+     * @param string $uri       namspace uri
+     *
+     * @return void
+     */
+    function addNamespace($namespace, $uri)
+    {
+        $ns = array($namespace => $uri);
+        $this->namespaces = array_merge($this->namespaces, $ns);
+    }
+
+    function getNamespaces()
+    {
+        return $this->namespaces;
+    }
+
+    function initFeed()
+    {
+        $this->xw->startDocument('1.0', 'UTF-8');
+        $commonAttrs = array('xml:lang' => 'en-US');
+        $commonAttrs = array_merge($commonAttrs, $this->namespaces);
+        $this->elementStart('feed', $commonAttrs);
+
+        $this->element('id', null, $this->id);
+        $this->element('title', null, $this->title);
+        $this->element('subtitle', null, $this->subtitle);
+
+        if (!empty($this->logo)) {
+            $this->element('logo', null, $this->logo);
+        }
+
+        $this->element('updated', null, $this->updated);
+
+        $this->renderLinks();
+    }
+
+    /**
+     * Check that all required elements have been set, etc.
+     * Throws an Atom10FeedException if something's missing.
+     *
+     * @return void
+     */
+    function validate()
+    {
+    }
+
+    function renderLinks()
+    {
+        foreach ($this->links as $attrs)
+        {
+            $this->element('link', $attrs, null);
+        }
+    }
+
+    function addEntryRaw($entry)
+    {
+        array_push($this->entries, $entry);
+    }
+
+    function addEntry($entry)
+    {
+        array_push($this->entries, $entry->getString());
+    }
+
+    function renderEntries()
+    {
+        foreach ($this->entries as $entry) {
+            $this->raw($entry);
+        }
+    }
+
+    function endFeed()
+    {
+        $this->elementEnd('feed');
+        $this->xw->endDocument();
+    }
+
+    function getString()
+    {
+        $this->validate();
+
+        $this->initFeed();
+        $this->renderEntries();
+        $this->endFeed();
+
+        return $this->xw->outputMemory();
+    }
+
+    function setId($id)
+    {
+        $this->id = $id;
+    }
+
+    function setTitle($title)
+    {
+        $this->title = $title;
+    }
+
+    function setSubtitle($subtitle)
+    {
+        $this->subtitle = $subtitle;
+    }
+
+    function setLogo($logo)
+    {
+        $this->logo = $logo;
+    }
+
+    function setUpdated($dt)
+    {
+        $this->updated = common_date_iso8601($dt);
+    }
+
+    function setPublished($dt)
+    {
+        $this->published = common_date_iso8601($dt);
+    }
+
+    /**
+     * Adds a link element into the Atom document
+     *
+     * Assumes you want rel="alternate" and type="text/html" unless
+     * you send in $otherAttrs.
+     *
+     * @param string $uri            the uri the href needs to point to
+     * @param array  $otherAttrs     other attributes to stick in
+     *
+     * @return void
+     */
+    function addLink($uri, $otherAttrs = null) {
+        $attrs = array('href' => $uri);
+
+        if (is_null($otherAttrs)) {
+            $attrs['rel']  = 'alternate';
+            $attrs['type'] = 'text/html';
+        } else {
+            $attrs = array_merge($attrs, $otherAttrs);
+        }
+
+        array_push($this->links, $attrs);
+    }
+
+}
diff --git a/lib/atomnoticefeed.php b/lib/atomnoticefeed.php
new file mode 100644 (file)
index 0000000..34ed44b
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building and Atom feed from a collection of notices
+ *
+ * PHP version 5
+ *
+ * LICENCE: 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/>.
+ *
+ * @category  Feed
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET'))
+{
+    exit(1);
+}
+
+/**
+ * Class for creating a feed that represents a collection of notices. Builds the
+ * feed in memory. Get the feed as a string with AtomNoticeFeed::getString().
+ *
+ * @category Feed
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://status.net/
+ */
+class AtomNoticeFeed extends Atom10Feed
+{
+    function __construct($indent = true) {
+        parent::__construct($indent);
+
+        // Feeds containing notice info use these namespaces
+
+        $this->addNamespace(
+            'xmlns:thr',
+            'http://purl.org/syndication/thread/1.0'
+        );
+
+        $this->addNamespace(
+            'xmlns:georss',
+            'http://www.georss.org/georss'
+        );
+
+        $this->addNamespace(
+            'xmlns:activity',
+            'http://activitystrea.ms/spec/1.0/'
+        );
+
+        // XXX: What should the uri be?
+        $this->addNamespace(
+            'xmlns:ostatus',
+            'http://ostatus.org/schema/1.0'
+        );
+    }
+
+    /**
+     * Add more than one Notice to the feed
+     *
+     * @param mixed $notices an array of Notice objects or handle
+     *
+     */
+    function addEntryFromNotices($notices)
+    {
+        if (is_array($notices)) {
+            foreach ($notices as $notice) {
+                $this->addEntryFromNotice($notice);
+            }
+        } else {
+            while ($notices->fetch()) {
+                $this->addEntryFromNotice($notices);
+            }
+        }
+    }
+
+    /**
+     * Add a single Notice to the feed
+     *
+     * @param Notice $notice a Notice to add
+     */
+    function addEntryFromNotice($notice)
+    {
+        $this->addEntryRaw($notice->asAtomEntry());
+    }
+
+}
index 16d1330f06b0a6f4b3686d35249c106896809843..cc6863488feb3454a2a35ba21e8e8e3a451b7a46 100644 (file)
@@ -88,6 +88,7 @@ $default =
               'stomp_manual_failover' => true, // if multiple servers are listed, treat them as separate (enqueue on one randomly, listen on all)
               'monitor' => null, // URL to monitor ping endpoint (work in progress)
               'softlimit' => '90%', // total size or % of memory_limit at which to restart queue threads gracefully
+              'spawndelay' => 1, // Wait at least N seconds between (re)spawns of child processes to avoid slamming the queue server with subscription startup
               'debug_memory' => false, // true to spit memory usage to log
               'inboxes' => true, // true to do inbox distribution & output queueing from in background via 'distrib' queue
               ),
index 274e1c2f695604853e87a4cd94d89569991db956..64bb52e106ee7289bf03934ebb32b145f33315d5 100644 (file)
@@ -155,26 +155,26 @@ abstract class QueueManager extends IoManager
     }
 
     /**
-     * Encode an object for queued storage.
-     * Next gen may use serialization.
+     * Encode an object or variable for queued storage.
+     * Notice objects are currently stored as an id reference;
+     * other items are serialized.
      *
-     * @param mixed $object
+     * @param mixed $item
      * @return string
      */
-    protected function encode($object)
+    protected function encode($item)
     {
-        if ($object instanceof Notice) {
-            return $object->id;
-        } else if (is_string($object)) {
-            return $object;
+        if ($item instanceof Notice) {
+            // Backwards compat
+            return $item->id;
         } else {
-            throw new ServerException("Can't queue this type", 500);
+            return serialize($item);
         }
     }
 
     /**
      * Decode an object from queued storage.
-     * Accepts back-compat notice reference entries and strings for now.
+     * Accepts notice reference entries and serialized items.
      *
      * @param string
      * @return mixed
@@ -182,9 +182,23 @@ abstract class QueueManager extends IoManager
     protected function decode($frame)
     {
         if (is_numeric($frame)) {
+            // Back-compat for notices...
             return Notice::staticGet(intval($frame));
-        } else {
+        } elseif (substr($frame, 0, 1) == '<') {
+            // Back-compat for XML source
             return $frame;
+        } else {
+            // Deserialize!
+            #$old = error_reporting();
+            #error_reporting($old & ~E_NOTICE);
+            $out = unserialize($frame);
+            #error_reporting($old);
+
+            if ($out === false && $frame !== 'b:0;') {
+                common_log(LOG_ERR, "Couldn't unserialize queued frame: $frame");
+                return false;
+            }
+            return $out;
         }
     }
 
index b1961d68801c308fc25429a4cee80b028439f72d..862cbb4fa342b88b735a68c60481cfff26a620f5 100644 (file)
@@ -83,6 +83,7 @@ abstract class SpawningDaemon extends Daemon
                 $this->log(LOG_INFO, "Spawned thread $i as pid $pid");
                 $children[$i] = $pid;
             }
+            sleep(common_config('queue', 'spawndelay'));
         }
         
         $this->log(LOG_INFO, "Waiting for children to complete.");
@@ -111,6 +112,7 @@ abstract class SpawningDaemon extends Daemon
                         $this->log(LOG_INFO, "Respawned thread $i as pid $pid");
                         $children[$i] = $pid;
                     }
+                    sleep(common_config('queue', 'spawndelay'));
                 } else {
                     $this->log(LOG_INFO, "Thread $i pid $pid exited with status $exitCode; closing out thread.");
                 }
index 6730cd213d789dcd1e4de8ed273a27f8653a4ebf..cd62c25bd828c9a6bc37b158e2b9dc3ae5bb5180 100644 (file)
@@ -107,9 +107,10 @@ class StompQueueManager extends QueueManager
             $message .= ':' . $param;
         }
         $this->_connect();
-        $result = $this->_send($this->control,
-                               $message,
-                               array ('created' => common_sql_now()));
+        $con = $this->cons[$this->defaultIdx];
+        $result = $con->send($this->control,
+                             $message,
+                             array ('created' => common_sql_now()));
         if ($result) {
             $this->_log(LOG_INFO, "Sent control ping to queue daemons: $message");
             return true;
@@ -368,17 +369,10 @@ class StompQueueManager extends QueueManager
         foreach ($this->cons as $i => $con) {
             if ($con) {
                 $this->rollback($i);
-                $con->unsubscribe($this->control);
+                $con->disconnect();
+                $this->cons[$i] = null;
             }
         }
-        if ($this->sites) {
-            foreach ($this->sites as $server) {
-                StatusNet::init($server);
-                $this->doUnsubscribe();
-            }
-        } else {
-            $this->doUnsubscribe();
-        }
         return true;
     }
 
@@ -555,26 +549,14 @@ class StompQueueManager extends QueueManager
         }
 
         $host = $this->cons[$idx]->getServer();
-        if (is_numeric($frame->body)) {
-            $id = intval($frame->body);
-            $info = "notice $id posted at {$frame->headers['created']} in queue $queue from $host";
-
-            $notice = Notice::staticGet('id', $id);
-            if (empty($notice)) {
-                $this->_log(LOG_WARNING, "Skipping missing $info");
-                $this->ack($idx, $frame);
-                $this->commit($idx);
-                $this->begin($idx);
-                $this->stats('badnotice', $queue);
-                return false;
-            }
-
-            $item = $notice;
-        } else {
-            // @fixme should we serialize, or json, or what here?
-            $info = "string posted at {$frame->headers['created']} in queue $queue from $host";
-            $item = $frame->body;
+        $item = $this->decode($frame->body);
+        if (empty($item)) {
+            $this->_log(LOG_ERR, "Skipping empty or deleted item in queue $queue from $host");
+            return true;
         }
+        $info = $this->logrep($item) . " posted at " .
+                $frame->headers['created'] . " in queue $queue from $host";
+        $this->_log(LOG_DEBUG, "Dequeued $info");
 
         $handler = $this->getHandler($queue);
         if (!$handler) {
index 879834a3d5ef52647a0ec14a150b68deba4b7f12..e255c5fe0899f5616fb2d3ecbbd6e026f0875dcf 100644 (file)
@@ -690,7 +690,7 @@ function common_group_link($sender_id, $nickname)
 {
     $sender = Profile::staticGet($sender_id);
     $group = User_group::getForNickname($nickname);
-    if ($group && $sender->isMember($group)) {
+    if ($sender && $group && $sender->isMember($group)) {
         $attrs = array('href' => $group->permalink(),
                        'class' => 'url');
         if (!empty($group->fullname)) {
index 4e8b892c6b1ed04022a9b11f9378d051f8b652dd..8444c3d73d2f2a35aaab5e4cdb5325fd2c9c24b2 100644 (file)
@@ -53,6 +53,21 @@ class OStatusPlugin extends Plugin
      */
     function onRouterInitialized($m)
     {
+        // Discovery actions
+        $m->connect('.well-known/host-meta',
+                    array('action' => 'hostmeta'));
+        $m->connect('main/webfinger',
+                    array('action' => 'webfinger'));
+        $m->connect('main/ostatus',
+                    array('action' => 'ostatusinit'));
+        $m->connect('main/ostatus?nickname=:nickname',
+                  array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+'));
+        $m->connect('main/ostatussub',
+                    array('action' => 'ostatussub'));          
+        $m->connect('main/ostatussub',
+                    array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+'));          
+
+        // PuSH actions
         $m->connect('main/push/hub', array('action' => 'pushhub'));
 
         $m->connect('main/push/callback/:feed',
@@ -60,6 +75,14 @@ class OStatusPlugin extends Plugin
                     array('feed' => '[0-9]+'));
         $m->connect('settings/feedsub',
                     array('action' => 'feedsubsettings'));
+
+        // Salmon endpoint
+        $m->connect('main/salmon/user/:id',
+                    array('action' => 'salmon'),
+                    array('id' => '[0-9]+'));
+        $m->connect('main/salmon/group/:id',
+                    array('action' => 'salmongroup'),
+                    array('id' => '[0-9]+'));
         return true;
     }
 
@@ -87,22 +110,37 @@ class OStatusPlugin extends Plugin
 
     /**
      * Set up a PuSH hub link to our internal link for canonical timeline
-     * Atom feeds for users.
+     * Atom feeds for users and groups.
      */
     function onStartApiAtom(Action $action)
     {
         if ($action instanceof ApiTimelineUserAction) {
-            $id = $action->arg('id');
-            if (strval(intval($id)) === strval($id)) {
-                // Canonical form of id in URL?
-                // Updates will be handled for our internal PuSH hub.
-                $action->element('link', array('rel' => 'hub',
-                                               'href' => common_local_url('pushhub')));
+            $salmonAction = 'salmon';
+        } else if ($action instanceof ApiTimelineGroupAction) {
+            $salmonAction = 'salmongroup';
+        } else {
+            return;
+        }
+
+        $id = $action->arg('id');
+        if (strval(intval($id)) === strval($id)) {
+            // Canonical form of id in URL? These are used for OStatus syndication.
+
+            $hub = common_config('ostatus', 'hub');
+            if (empty($hub)) {
+                // Updates will be handled through our internal PuSH hub.
+                $hub = common_local_url('pushhub');
             }
+            $action->element('link', array('rel' => 'hub',
+                                           'href' => $hub));
+
+            // Also, we'll add in the salmon link
+            $salmon = common_local_url($salmonAction, array('id' => $id));
+            $action->element('link', array('rel' => 'salmon',
+                                           'href' => $salmon));
         }
-        return true;
     }
-
+    
     /**
      * Add the feed settings page to the Connect Settings menu
      *
@@ -148,11 +186,90 @@ class OStatusPlugin extends Plugin
         return true;
     }
 
+    /**
+     * Add in an OStatus subscribe button
+     */
+    function onStartProfilePageActionsElements($output, $profile)
+    {
+        $cur = common_current_user();
+
+        if (empty($cur)) {
+            // Add an OStatus subscribe
+            $output->elementStart('li', 'entity_subscribe');
+            $url = common_local_url('ostatusinit',
+                                    array('nickname' => $profile->nickname));
+            $output->element('a', array('href' => $url,
+                                        'class' => 'entity_remote_subscribe'),
+                                _m('OStatus'));
+            
+            $output->elementEnd('li');
+        }
+    }
+
+    /**
+     * Check if we've got remote replies to send via Salmon.
+     *
+     * @fixme push webfinger lookup & sending to a background queue
+     * @fixme also detect short-form name for remote subscribees where not ambiguous
+     */
+    function onEndNoticeSave($notice)
+    {
+        $count = preg_match_all('/(\w+\.)*\w+@(\w+\.)*\w+(\w+\-\w+)*\.\w+/', $notice->content, $matches);
+        if ($count) {
+            foreach ($matches[0] as $webfinger) {
+                // Check to see if we've got an actual webfinger
+                $w = new Webfinger;
+
+                $endpoint_uri = '';
+                
+                $result = $w->lookup($webfinger);
+                if (empty($result)) {
+                    continue;
+                }
+                
+                foreach ($result->links as $link) {
+                    if ($link['rel'] == 'salmon') {
+                        $endpoint_uri = $link['href'];
+                    }
+                }
+                
+                if (empty($endpoint_uri)) {
+                    continue;
+                }
+
+                $xml = '<?xml version="1.0" encoding="UTF-8" ?>';
+                $xml .= $notice->asAtomEntry();
+               
+                $salmon = new Salmon();
+                $salmon->post($endpoint_uri, $xml);
+            }
+        }
+    }
+
+    /**
+     * Garbage collect unused feeds on unsubscribe
+     */
+    function onEndUnsubscribe($user, $other)
+    {
+        $profile = Ostatus_profile::staticGet('profile_id', $other->id);
+        if ($feed) {
+            $sub = new Subscription();
+            $sub->subscribed = $other->id;
+            $sub->limit(1);
+            if (!$sub->find(true)) {
+                common_log(LOG_INFO, "Unsubscribing from now-unused feed $feed->feeduri on hub $feed->huburi");
+                $profile->unsubscribe();
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Make sure necessary tables are filled out.
+     */
     function onCheckSchema() {
-        // warning: the autoincrement doesn't seem to set.
-        // alter table feedinfo change column id id int(11) not null  auto_increment;
         $schema = Schema::get();
-        $schema->ensureTable('feedinfo', Feedinfo::schemaDef());
+        $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef());
         $schema->ensureTable('hubsub', HubSub::schemaDef());
         return true;
     }
index 4d5b7b60f4f551205d01b483d881a80068dd2eeb..6933c9bf21a8ab5f6e3c41362f0d0f61125db5e0 100644 (file)
@@ -182,9 +182,9 @@ class FeedSubSettingsAction extends ConnectSettingsAction
         }
         
         $this->munger = $discover->feedMunger();
-        $this->feedinfo = $this->munger->feedInfo();
+        $this->profile = $this->munger->ostatusProfile();
 
-        if ($this->feedinfo->huburi == '' && !common_config('feedsub', 'nohub')) {
+        if ($this->profile->huburi == '' && !common_config('feedsub', 'nohub')) {
             $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
             return false;
         }
@@ -196,33 +196,44 @@ class FeedSubSettingsAction extends ConnectSettingsAction
     {
         if ($this->validateFeed()) {
             $this->preview = true;
-            $this->feedinfo = Feedinfo::ensureProfile($this->munger);
+            $this->profile = Ostatus_profile::ensureProfile($this->munger);
+            if (!$this->profile) {
+                throw new ServerException("Feed profile was not saved properly.");
+            }
 
             // If not already in use, subscribe to updates via the hub
-            if ($this->feedinfo->sub_start) {
-                common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}");
+            if ($this->profile->sub_start) {
+                common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}");
             } else {
-                $ok = $this->feedinfo->subscribe();
+                $ok = $this->profile->subscribe();
                 common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
                 if (!$ok) {
                     $this->showForm(_m('Feed subscription failed! Bad response from hub.'));
                     return;
                 }
             }
-            
+
             // And subscribe the current user to the local profile
             $user = common_current_user();
-            $profile = $this->feedinfo->getProfile();
-            if (!$profile) {
-                throw new ServerException("Feed profile was not saved properly.");
-            }
 
-            if ($user->isSubscribed($profile)) {
-                $this->showForm(_m('Already subscribed!'));
-            } elseif ($user->subscribeTo($profile)) {
-                $this->showForm(_m('Feed subscribed!'));
+            if ($this->profile->isGroup()) {
+                $group = $this->profile->localGroup();
+                if ($user->isMember($group)) {
+                    $this->showForm(_m('Already a member!'));
+                } elseif (Group_member::join($this->profile->group_id, $user->id)) {
+                    $this->showForm(_m('Joined remote group!'));
+                } else {
+                    $this->showForm(_m('Remote group join failed!'));
+                }
             } else {
-                $this->showForm(_m('Feed subscription failed!'));
+                $local = $this->profile->localProfile();
+                if ($user->isSubscribed($local)) {
+                    $this->showForm(_m('Already subscribed!'));
+                } elseif ($user->subscribeTo($local)) {
+                    $this->showForm(_m('Feed subscribed!'));
+                } else {
+                    $this->showForm(_m('Feed subscription failed!'));
+                }
             }
         }
     }
@@ -237,7 +248,7 @@ class FeedSubSettingsAction extends ConnectSettingsAction
 
     function previewFeed()
     {
-        $feedinfo = $this->munger->feedinfo();
+        $profile = $this->munger->ostatusProfile();
         $notice = $this->munger->notice(0, true); // preview
 
         if ($notice) {
diff --git a/plugins/OStatus/actions/hostmeta.php b/plugins/OStatus/actions/hostmeta.php
new file mode 100644 (file)
index 0000000..850b8a0
--- /dev/null
@@ -0,0 +1,42 @@
+<?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/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class HostMetaAction extends Action
+{
+
+    function handle()
+    {
+        parent::handle();
+
+        $w = new Webfinger();
+
+
+        $domain = common_config('site', 'server');
+        $url = common_local_url('webfinger');
+        $url.= '?uri={uri}';
+        print $w->getHostMeta($domain, $url);
+    }
+}
diff --git a/plugins/OStatus/actions/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php
new file mode 100644 (file)
index 0000000..bac2c4d
--- /dev/null
@@ -0,0 +1,128 @@
+<?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/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+
+class OStatusInitAction extends Action
+{
+
+    var $nickname;
+    var $acct;
+    var $err;
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        if (common_logged_in()) {
+            $this->clientError(_('You can use the local subscription!'));
+            return false;
+        }
+
+        $this->nickname    = $this->trimmed('nickname');
+        $this->acct = $this->trimmed('acct');
+
+        return true;
+    }
+    
+    function handle($args)
+    {
+        parent::handle($args);
+
+        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+            /* Use a session token for CSRF protection. */
+            $token = $this->trimmed('token');
+            if (!$token || $token != common_session_token()) {
+                $this->showForm(_('There was a problem with your session token. '.
+                                  'Try again, please.'));
+                return;
+            }
+            $this->ostatusConnect();
+        } else {
+            $this->showForm();
+        }
+    }
+    
+    function showForm($err = null)
+    {
+      $this->err = $err;
+      $this->showPage();
+
+    }
+
+    function showContent()
+    {
+        $this->elementStart('form', array('id' => 'form_ostatus_connect',
+                                          'method' => 'post',
+                                          'class' => 'form_settings',
+                                          'action' => common_local_url('ostatusinit')));
+        $this->elementStart('fieldset');
+        $this->element('legend', _('Subscribe to a remote user'));
+        $this->hidden('token', common_session_token());
+
+        $this->elementStart('ul', 'form_data');
+        $this->elementStart('li');
+        $this->input('nickname', _('User nickname'), $this->nickname,
+                     _('Nickname of the user you want to follow'));
+        $this->elementEnd('li');
+        $this->elementStart('li');
+        $this->input('acct', _('Profile Account'), $this->acct,
+                     _('Your account id (i.e. user@identi.ca)'));
+        $this->elementEnd('li');
+        $this->elementEnd('ul');
+        $this->submit('submit', _('Subscribe'));
+        $this->elementEnd('fieldset');
+        $this->elementEnd('form');
+    }        
+
+    function ostatusConnect()
+    {
+      $w = new Webfinger;
+
+      $result = $w->lookup($this->acct);
+      foreach ($result->links as $link) {
+          if ($link['rel'] == 'http://ostatus.org/schema/1.0/subscribe') {
+              // We found a URL - let's redirect!
+
+              $user = User::staticGet('nickname', $this->nickname);
+
+              $feed_url = common_local_url('ApiTimelineUser',
+                                           array('id' => $user->id,
+                                                 'format' => 'atom'));
+              $url = $w->applyTemplate($link['template'], $feed_url);
+
+              common_redirect($url, 303);
+          }
+
+      }
+      
+    }
+    
+    function title()
+    {
+      return _('OStatus Connect');  
+    }
+  
+}
\ No newline at end of file
diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php
new file mode 100644 (file)
index 0000000..9774286
--- /dev/null
@@ -0,0 +1,226 @@
+<?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/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class OStatusSubAction extends Action
+{
+
+    protected $feedurl;
+    
+    function title()
+    {
+        return _m("OStatus Subscribe");
+    }
+
+    function handle($args)
+    {
+        if ($this->validateFeed()) {
+            $this->showForm();
+        }
+
+        return true;
+
+    }
+
+    function showForm($err = null)
+    {
+        $this->err = $err;
+        $this->showPage();
+    }
+
+
+    function showContent()
+    {
+        $user = common_current_user();
+
+        $profile = $user->getProfile();
+
+        $fuser = null;
+
+        $flink = Foreign_link::getByUserID($user->id, FEEDSUB_SERVICE);
+
+        if (!empty($flink)) {
+            $fuser = $flink->getForeignUser();
+        }
+
+        $this->elementStart('form', array('method' => 'post',
+                                          'id' => 'form_settings_feedsub',
+                                          'class' => 'form_settings',
+                                          'action' =>
+                                          common_local_url('feedsubsettings')));
+
+        $this->hidden('token', common_session_token());
+
+        $this->elementStart('fieldset', array('id' => 'settings_feeds'));
+
+        $this->elementStart('ul', 'form_data');
+        $this->elementStart('li', array('id' => 'settings_twitter_login_button'));
+        $this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed'));
+        $this->elementEnd('li');
+        $this->elementEnd('ul');
+
+        $this->submit('subscribe', _m('Subscribe'));
+
+        $this->elementEnd('fieldset');
+
+        $this->elementEnd('form');
+
+        $this->previewFeed();
+    }
+
+    /**
+     * Handle posts to this form
+     *
+     * Based on the button that was pressed, muxes out to other functions
+     * to do the actual task requested.
+     *
+     * All sub-functions reload the form with a message -- success or failure.
+     *
+     * @return void
+     */
+
+    function handlePost()
+    {
+        // CSRF protection
+        $token = $this->trimmed('token');
+        if (!$token || $token != common_session_token()) {
+            $this->showForm(_('There was a problem with your session token. '.
+                              'Try again, please.'));
+            return;
+        }
+
+        if ($this->arg('subscribe')) {
+            $this->saveFeed();
+        } else {
+            $this->showForm(_('Unexpected form submission.'));
+        }
+    }
+
+    
+    /**
+     * Set up and add a feed
+     *
+     * @return boolean true if feed successfully read
+     * Sends you back to input form if not.
+     */
+    function validateFeed()
+    {
+        $feedurl = $this->trimmed('feed');
+        
+        if ($feedurl == '') {
+            $this->showForm(_m('Empty feed URL!'));
+            return;
+        }
+        $this->feedurl = $feedurl;
+        
+        // Get the canonical feed URI and check it
+        try {
+            $discover = new FeedDiscovery();
+            $uri = $discover->discoverFromURL($feedurl);
+        } catch (FeedSubBadURLException $e) {
+            $this->showForm(_m('Invalid URL or could not reach server.'));
+            return false;
+        } catch (FeedSubBadResponseException $e) {
+            $this->showForm(_m('Cannot read feed; server returned error.'));
+            return false;
+        } catch (FeedSubEmptyException $e) {
+            $this->showForm(_m('Cannot read feed; server returned an empty page.'));
+            return false;
+        } catch (FeedSubBadHTMLException $e) {
+            $this->showForm(_m('Bad HTML, could not find feed link.'));
+            return false;
+        } catch (FeedSubNoFeedException $e) {
+            $this->showForm(_m('Could not find a feed linked from this URL.'));
+            return false;
+        } catch (FeedSubUnrecognizedTypeException $e) {
+            $this->showForm(_m('Not a recognized feed type.'));
+            return false;
+        } catch (FeedSubException $e) {
+            // Any new ones we forgot about
+            $this->showForm(_m('Bad feed URL.'));
+            return false;
+        }
+        
+        $this->munger = $discover->feedMunger();
+        $this->profile = $this->munger->ostatusProfile();
+
+        if ($this->profile->huburi == '') {
+            $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
+            return false;
+        }
+        
+        return true;
+    }
+
+    function saveFeed()
+    {
+        if ($this->validateFeed()) {
+            $this->preview = true;
+            $this->profile = Ostatus_profile::ensureProfile($this->munger);
+
+            // If not already in use, subscribe to updates via the hub
+            if ($this->profile->sub_start) {
+                common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}");
+            } else {
+                $ok = $this->profile->subscribe();
+                common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
+                if (!$ok) {
+                    $this->showForm(_m('Feed subscription failed! Bad response from hub.'));
+                    return;
+                }
+            }
+            
+            // And subscribe the current user to the local profile
+            $user = common_current_user();
+            $profile = $this->profile->getProfile();
+            
+            if ($user->isSubscribed($profile)) {
+                $this->showForm(_m('Already subscribed!'));
+            } elseif ($user->subscribeTo($profile)) {
+                $this->showForm(_m('Feed subscribed!'));
+            } else {
+                $this->showForm(_m('Feed subscription failed!'));
+            }
+        }
+    }
+
+    
+    function previewFeed()
+    {
+        $profile = $this->munger->ostatusProfile();
+        $notice = $this->munger->notice(0, true); // preview
+
+        if ($notice) {
+            $this->element('b', null, 'Preview of latest post from this feed:');
+
+            $item = new NoticeList($notice, $this);
+            $item->show();
+        } else {
+            $this->element('b', null, 'No posts in this feed yet.');
+        }
+    }
+
+
+}
\ No newline at end of file
index a5e02e08f1442f9603174fc2fcace621b84b1048..2601a377a0bdf2d5b32246a05d3e43cc9dfb6686 100644 (file)
@@ -48,9 +48,9 @@ class PushCallbackAction extends Action
             throw new ServerException('Empty or invalid feed id', 400);
         }
 
-        $feedinfo = Feedinfo::staticGet('id', $feedid);
-        if (!$feedinfo) {
-            throw new ServerException('Unknown feed id ' . $feedid, 400);
+        $profile = Ostatus_profile::staticGet('id', $feedid);
+        if (!$profile) {
+            throw new ServerException('Unknown OStatus/PuSH feed id ' . $feedid, 400);
         }
 
         $hmac = '';
@@ -59,7 +59,7 @@ class PushCallbackAction extends Action
         }
 
         $post = file_get_contents('php://input');
-        $feedinfo->postUpdates($post, $hmac);
+        $profile->postUpdates($post, $hmac);
     }
     
     /**
@@ -78,28 +78,30 @@ class PushCallbackAction extends Action
             throw new ServerException("Bogus hub callback: bad mode", 404);
         }
         
-        $feedinfo = Feedinfo::staticGet('feeduri', $topic);
-        if (!$feedinfo) {
+        $profile = Ostatus_profile::staticGet('feeduri', $topic);
+        if (!$profile) {
             common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic");
             throw new ServerException("Bogus hub callback: unknown feed", 404);
         }
 
-        # Can't currently set the token in our sub api
-        #if ($feedinfo->verify_token !== $verify_token) {
-        #    common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic");
-        #    throw new ServerError("Bogus hub callback: bad token", 404);
-        #}
-        
+        if ($profile->verify_token !== $verify_token) {
+            common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic");
+            throw new ServerError("Bogus hub callback: bad token", 404);
+        }
+
+        if ($mode != $profile->sub_state) {
+            common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad mode \"$mode\" for feed $topic in state \"{$profile->sub_state}\"");
+            throw new ServerException("Bogus hub callback: mode doesn't match subscription state.", 404);
+        }
+
         // OK!
-        common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
-        $feedinfo->sub_start = common_sql_date(time());
-        if ($lease_seconds > 0) {
-            $feedinfo->sub_end = common_sql_date(time() + $lease_seconds);
+        if ($mode == 'subscribe') {
+            common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
+            $profile->confirmSubscribe($lease_seconds);
         } else {
-            $feedinfo->sub_end = null;
+            common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic");
+            $profile->confirmUnsubscribe();
         }
-        $feedinfo->update();
-        
         print $challenge;
     }
 }
diff --git a/plugins/OStatus/actions/salmon.php b/plugins/OStatus/actions/salmon.php
new file mode 100644 (file)
index 0000000..b616027
--- /dev/null
@@ -0,0 +1,81 @@
+<?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/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @author James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+class SalmonAction extends Action
+{
+    var $user     = null;
+    var $xml      = null;
+    var $activity = null;
+
+    function prepare($args)
+    {
+        if ($_SERVER['REQUEST_METHOD'] != 'POST') {
+            $this->clientError(_('This method requires a POST.'));
+        }
+
+        if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') {
+            $this->clientError(_('Salmon requires application/atom+xml'));
+        }
+
+        $id = $this->trimmed('id');
+
+        if (!$id) {
+            $this->clientError(_('No ID.'));
+        }
+
+        $this->user = User::staticGet($id);
+
+        if (empty($this->user)) {
+            $this->clientError(_('No such user.'));
+        }
+
+        $xml = file_get_contents('php://input');
+
+        $dom = DOMDocument::loadXML($xml);
+
+        // XXX: check that document element is Atom entry
+        // XXX: check the signature
+
+        $this->act = Activity::fromAtomEntry($dom->documentElement);
+    }
+
+    function handle($args)
+    {
+        common_log(LOG_DEBUG, 'Salmon: incoming post for user: '. $user_id);
+
+        // TODO : Insert new $xml -> notice code
+
+        switch ($this->act->verb)
+        {
+        case Activity::POST:
+        case Activity::SHARE:
+        case Activity::FAVORITE:
+        case Activity::FOLLOW:
+        }
+    }
+}
diff --git a/plugins/OStatus/actions/webfinger.php b/plugins/OStatus/actions/webfinger.php
new file mode 100644 (file)
index 0000000..75ba166
--- /dev/null
@@ -0,0 +1,77 @@
+<?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/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class WebfingerAction extends Action
+{
+
+    public $uri;
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        $this->uri = $this->trimmed('uri');
+
+        return true;
+    }
+        
+    function handle()
+    {
+        $acct = Webfinger::normalize($this->uri);
+
+        $xrd = new XRD();
+
+        list($nick, $domain) = explode('@', urldecode($acct));
+        $nick = common_canonical_nickname($nick);
+
+        $this->user = User::staticGet('nickname', $nick);
+        if (!$this->user) {
+            $this->clientError(_('No such user.'), 404);
+            return false;
+        }
+
+        $xrd->subject = $this->uri;
+        $xrd->alias[] = common_profile_url($nick);
+        $xrd->links[] = array('rel' => 'http://webfinger.net/rel/profile-page',
+                              'type' => 'text/html',
+                              'href' => common_profile_url($nick));
+
+        $salmon_url = common_local_url('salmon',
+                                       array('id' => $this->user->id));
+
+        $xrd->links[] = array('rel' => 'salmon',
+                              'href' => $salmon_url);
+        
+        // TODO - finalize where the redirect should go on the publisher
+        $url = common_local_url('ostatussub') . '?feed={uri}';
+        $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe',
+                              'template' => $url );
+
+        header('Content-type: text/xml');
+        print $xrd->toXML();
+    }
+
+}
diff --git a/plugins/OStatus/classes/Feedinfo.php b/plugins/OStatus/classes/Feedinfo.php
deleted file mode 100644 (file)
index 107faf0..0000000
+++ /dev/null
@@ -1,345 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2009-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/>.
- */
-
-/**
- * @package FeedSubPlugin
- * @maintainer Brion Vibber <brion@status.net>
- */
-
-/*
-PuSH subscription flow:
-
-    $feedinfo->subscribe()
-        generate random verification token
-            save to verify_token
-        sends a sub request to the hub...
-    
-    feedsub/callback
-        hub sends confirmation back to us via GET
-        We verify the request, then echo back the challenge.
-        On our end, we save the time we subscribed and the lease expiration
-    
-    feedsub/callback
-        hub sends us updates via POST
-    
-*/
-
-class FeedDBException extends FeedSubException
-{
-    public $obj;
-
-    function __construct($obj)
-    {
-        parent::__construct('Database insert failure');
-        $this->obj = $obj;
-    }
-}
-
-class Feedinfo extends Memcached_DataObject
-{
-    public $__table = 'feedinfo';
-
-    public $id;
-    public $profile_id;
-
-    public $feeduri;
-    public $homeuri;
-    public $huburi;
-
-    // PuSH subscription data
-    public $secret;
-    public $verify_token;
-    public $sub_start;
-    public $sub_end;
-
-    public $created;
-    public $lastupdate;
-
-
-    public /*static*/ function staticGet($k, $v=null)
-    {
-        return parent::staticGet(__CLASS__, $k, $v);
-    }
-
-    /**
-     * return table definition for DB_DataObject
-     *
-     * DB_DataObject needs to know something about the table to manipulate
-     * instances. This method provides all the DB_DataObject needs to know.
-     *
-     * @return array array of column definitions
-     */
-
-    function table()
-    {
-        return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
-                     'profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
-                     'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
-                     'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
-                     'huburi' =>  DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
-                     'secret' => DB_DATAOBJECT_STR,
-                     'verify_token' => DB_DATAOBJECT_STR,
-                     'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
-                     'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
-                     'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
-                     'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
-    }
-    
-    static function schemaDef()
-    {
-        return array(new ColumnDef('id', 'integer',
-                                   /*size*/ null,
-                                   /*nullable*/ false,
-                                   /*key*/ 'PRI',
-                                   /*default*/ '0',
-                                   /*extra*/ null,
-                                   /*auto_increment*/ true),
-                     new ColumnDef('profile_id', 'integer',
-                                   null, false),
-                     new ColumnDef('feeduri', 'varchar',
-                                   255, false, 'UNI'),
-                     new ColumnDef('homeuri', 'varchar',
-                                   255, false),
-                     new ColumnDef('huburi', 'varchar',
-                                   255, false),
-                     new ColumnDef('verify_token', 'varchar',
-                                   32, true),
-                     new ColumnDef('secret', 'varchar',
-                                   64, true),
-                     new ColumnDef('sub_start', 'datetime',
-                                   null, true),
-                     new ColumnDef('sub_end', 'datetime',
-                                   null, true),
-                     new ColumnDef('created', 'datetime',
-                                   null, false),
-                     new ColumnDef('lastupdate', 'datetime',
-                                   null, false));
-    }
-
-    /**
-     * return key definitions for DB_DataObject
-     *
-     * DB_DataObject needs to know about keys that the table has; this function
-     * defines them.
-     *
-     * @return array key definitions
-     */
-
-    function keys()
-    {
-        return array_keys($this->keyTypes());
-    }
-
-    /**
-     * return key definitions for Memcached_DataObject
-     *
-     * Our caching system uses the same key definitions, but uses a different
-     * method to get them.
-     *
-     * @return array key definitions
-     */
-
-    function keyTypes()
-    {
-        return array('id' => 'K'); // @fixme we'll need a profile_id key at least
-    }
-
-    function sequenceKey()
-    {
-        return array('id', true, false);
-    }
-
-    /**
-     * Fetch the StatusNet-side profile for this feed
-     * @return Profile
-     */
-    public function getProfile()
-    {
-        return Profile::staticGet('id', $this->profile_id);
-    }
-
-    /**
-     * @param FeedMunger $munger
-     * @return Feedinfo
-     */
-    public static function ensureProfile($munger)
-    {
-        $feedinfo = $munger->feedinfo();
-
-        $current = self::staticGet('feeduri', $feedinfo->feeduri);
-        if ($current) {
-            // @fixme we should probably update info as necessary
-            return $current;
-        }
-
-        $feedinfo->query('BEGIN');
-
-        // Awful hack! Awful hack!
-        $feedinfo->verify = common_good_rand(16);
-        $feedinfo->secret = common_good_rand(32);
-
-        try {
-            $profile = $munger->profile();
-            $result = $profile->insert();
-            if (empty($result)) {
-                throw new FeedDBException($profile);
-            }
-
-            $avatar = $munger->getAvatar();
-            if ($avatar) {
-                // @fixme this should be better encapsulated
-                // ripped from oauthstore.php (for old OMB client)
-                $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
-                copy($avatar, $temp_filename);
-                $imagefile = new ImageFile($profile->id, $temp_filename);
-                $filename = Avatar::filename($profile->id,
-                                             image_type_to_extension($imagefile->type),
-                                             null,
-                                             common_timestamp());
-                rename($temp_filename, Avatar::path($filename));
-                $profile->setOriginal($filename);
-            }
-
-            $feedinfo->profile_id = $profile->id;
-            $result = $feedinfo->insert();
-            if (empty($result)) {
-                throw new FeedDBException($feedinfo);
-            }
-
-            $feedinfo->query('COMMIT');
-        } catch (FeedDBException $e) {
-            common_log_db_error($e->obj, 'INSERT', __FILE__);
-            $feedinfo->query('ROLLBACK');
-            return false;
-        }
-        return $feedinfo;
-    }
-
-    /**
-     * Send a subscription request to the hub for this feed.
-     * The hub will later send us a confirmation POST to /feedsub/callback.
-     *
-     * @return bool true on success, false on failure
-     */
-    public function subscribe()
-    {
-        if (common_config('feedsub', 'nohub')) {
-            // Fake it! We're just testing remote feeds w/o hubs.
-            return true;
-        }
-        // @fixme use the verification token
-        #$token = md5(mt_rand() . ':' . $this->feeduri);
-        #$this->verify_token = $token;
-        #$this->update(); // @fixme
-        try {
-            $callback = common_local_url('pushcallback', array('feed' => $this->id));
-            $headers = array('Content-Type: application/x-www-form-urlencoded');
-            $post = array('hub.mode' => 'subscribe',
-                          'hub.callback' => $callback,
-                          'hub.verify' => 'async',
-                          'hub.verify_token' => $this->verify_token,
-                          'hub.secret' => $this->secret,
-                          //'hub.lease_seconds' => 0,
-                          'hub.topic' => $this->feeduri);
-            $client = new HTTPClient();
-            $response = $client->post($this->huburi, $headers, $post);
-            $status = $response->getStatus();
-            if ($status == 202) {
-                common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
-                return true;
-            } else if ($status == 204) {
-                common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified');
-                return true;
-            } else if ($status >= 200 && $status < 300) {
-                common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
-                return false;
-            } else {
-                common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody());
-                return false;
-            }
-        } catch (Exception $e) {
-            // wtf!
-            common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri");
-            return false;
-        }
-    }
-
-    /**
-     * Read and post notices for updates from the feed.
-     * Currently assumes that all items in the feed are new,
-     * coming from a PuSH hub.
-     *
-     * @param string $xml source of Atom or RSS feed
-     * @param string $hmac X-Hub-Signature header, if present
-     */
-    public function postUpdates($xml, $hmac)
-    {
-        common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml");
-
-        if ($this->secret) {
-            if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) {
-                $their_hmac = strtolower($matches[1]);
-                $our_hmac = sha1($xml . $this->secret);
-                if ($their_hmac !== $our_hmac) {
-                    common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac");
-                    return;
-                }
-            } else {
-                common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
-                return;
-            }
-        } else if ($hmac) {
-            common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'");
-            return;
-        }
-
-        require_once "XML/Feed/Parser.php";
-        $feed = new XML_Feed_Parser($xml, false, false, true);
-        $munger = new FeedMunger($feed);
-        
-        $hits = 0;
-        foreach ($feed as $index => $entry) {
-            // @fixme this might sort in wrong order if we get multiple updates
-            
-            $notice = $munger->notice($index);
-            $notice->profile_id = $this->profile_id;
-            
-            // Double-check for oldies
-            // @fixme this could explode horribly for multiple feeds on a blog. sigh
-            $dupe = new Notice();
-            $dupe->uri = $notice->uri;
-            if ($dupe->find(true)) {
-                common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}");
-                continue;
-            }
-            
-            if (Event::handle('StartNoticeSave', array(&$notice))) {
-                $id = $notice->insert();
-                Event::handle('EndNoticeSave', array($notice));
-            }
-            $notice->addToInboxes();
-
-            common_log(LOG_INFO, __METHOD__ . ": saved notice {$notice->id} for entry $index of update to \"{$this->feeduri}\"");
-            $hits++;
-        }
-        if ($hits == 0) {
-            common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml");
-        }
-    }
-}
index 1769f6c9417f0c213c00fc9ebe8180d2676b343f..7071ee5b4f82e9933973b7a3afc705ef58934233 100644 (file)
@@ -242,7 +242,7 @@ class HubSub extends Memcached_DataObject
     {
         $headers = array('Content-Type: application/atom+xml');
         if ($this->secret) {
-            $hmac = sha1($atom . $this->secret);
+            $hmac = hash_hmac('sha1', $atom, $this->secret);
             $headers[] = "X-Hub-Signature: sha1=$hmac";
         } else {
             $hmac = '(none)';
diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php
new file mode 100644 (file)
index 0000000..733d884
--- /dev/null
@@ -0,0 +1,644 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009-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/>.
+ */
+
+/**
+ * @package FeedSubPlugin
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+/*
+PuSH subscription flow:
+
+    $profile->subscribe()
+        generate random verification token
+            save to verify_token
+        sends a sub request to the hub...
+    
+    main/push/callback
+        hub sends confirmation back to us via GET
+        We verify the request, then echo back the challenge.
+        On our end, we save the time we subscribed and the lease expiration
+    
+    main/push/callback
+        hub sends us updates via POST
+    
+*/
+
+class FeedDBException extends FeedSubException
+{
+    public $obj;
+
+    function __construct($obj)
+    {
+        parent::__construct('Database insert failure');
+        $this->obj = $obj;
+    }
+}
+
+class Ostatus_profile extends Memcached_DataObject
+{
+    public $__table = 'ostatus_profile';
+
+    public $id;
+    public $profile_id;
+    public $group_id;
+
+    public $feeduri;
+    public $homeuri;
+
+    // PuSH subscription data
+    public $huburi;
+    public $secret;
+    public $verify_token;
+    public $sub_state; // subscribe, active, unsubscribe
+    public $sub_start;
+    public $sub_end;
+
+    public $salmonuri;
+
+    public $created;
+    public $lastupdate;
+
+
+    public /*static*/ function staticGet($k, $v=null)
+    {
+        return parent::staticGet(__CLASS__, $k, $v);
+    }
+
+    /**
+     * return table definition for DB_DataObject
+     *
+     * DB_DataObject needs to know something about the table to manipulate
+     * instances. This method provides all the DB_DataObject needs to know.
+     *
+     * @return array array of column definitions
+     */
+
+    function table()
+    {
+        return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+                     'profile_id' => DB_DATAOBJECT_INT,
+                     'group_id' => DB_DATAOBJECT_INT,
+                     'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+                     'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+                     'huburi' =>  DB_DATAOBJECT_STR,
+                     'secret' => DB_DATAOBJECT_STR,
+                     'verify_token' => DB_DATAOBJECT_STR,
+                     'sub_state' => DB_DATAOBJECT_STR,
+                     'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
+                     'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
+                     'salmonuri' =>  DB_DATAOBJECT_STR,
+                     'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
+                     'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
+    }
+    
+    static function schemaDef()
+    {
+        return array(new ColumnDef('id', 'integer',
+                                   /*size*/ null,
+                                   /*nullable*/ false,
+                                   /*key*/ 'PRI',
+                                   /*default*/ '0',
+                                   /*extra*/ null,
+                                   /*auto_increment*/ true),
+                     new ColumnDef('profile_id', 'integer',
+                                   null, true, 'UNI'),
+                     new ColumnDef('group_id', 'integer',
+                                   null, true, 'UNI'),
+                     new ColumnDef('feeduri', 'varchar',
+                                   255, false, 'UNI'),
+                     new ColumnDef('homeuri', 'varchar',
+                                   255, false),
+                     new ColumnDef('huburi', 'text',
+                                   null, true),
+                     new ColumnDef('verify_token', 'varchar',
+                                   32, true),
+                     new ColumnDef('secret', 'varchar',
+                                   64, true),
+                     new ColumnDef('sub_state', "enum('subscribe','active','unsubscribe')",
+                                   null, true),
+                     new ColumnDef('sub_start', 'datetime',
+                                   null, true),
+                     new ColumnDef('sub_end', 'datetime',
+                                   null, true),
+                     new ColumnDef('salmonuri', 'text',
+                                   null, true),
+                     new ColumnDef('created', 'datetime',
+                                   null, false),
+                     new ColumnDef('lastupdate', 'datetime',
+                                   null, false));
+    }
+
+    /**
+     * return key definitions for DB_DataObject
+     *
+     * DB_DataObject needs to know about keys that the table has; this function
+     * defines them.
+     *
+     * @return array key definitions
+     */
+
+    function keys()
+    {
+        return array_keys($this->keyTypes());
+    }
+
+    /**
+     * return key definitions for Memcached_DataObject
+     *
+     * Our caching system uses the same key definitions, but uses a different
+     * method to get them.
+     *
+     * @return array key definitions
+     */
+
+    function keyTypes()
+    {
+        return array('id' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U');
+    }
+
+    function sequenceKey()
+    {
+        return array('id', true, false);
+    }
+
+    /**
+     * Fetch the StatusNet-side profile for this feed
+     * @return Profile
+     */
+    public function localProfile()
+    {
+        if ($this->profile_id) {
+            return Profile::staticGet('id', $this->profile_id);
+        }
+        return null;
+    }
+
+    /**
+     * Fetch the StatusNet-side profile for this feed
+     * @return Profile
+     */
+    public function localGroup()
+    {
+        if ($this->group_id) {
+            return User_group::staticGet('id', $this->group_id);
+        }
+        return null;
+    }
+
+    /**
+     * @param FeedMunger $munger
+     * @param boolean $isGroup is this a group record?
+     * @return Ostatus_profile
+     */
+    public static function ensureProfile($munger)
+    {
+        $profile = $munger->ostatusProfile();
+
+        $current = self::staticGet('feeduri', $profile->feeduri);
+        if ($current) {
+            // @fixme we should probably update info as necessary
+            return $current;
+        }
+
+        $profile->query('BEGIN');
+
+        // Awful hack! Awful hack!
+        $profile->verify = common_good_rand(16);
+        $profile->secret = common_good_rand(32);
+
+        try {
+            $local = $munger->profile();
+
+            if ($entity->isGroup()) {
+                $group = new User_group();
+                $group->nickname = $local->nickname . '@remote'; // @fixme
+                $group->fullname = $local->fullname;
+                $group->homepage = $local->homepage;
+                $group->location = $local->location;
+                $group->created = $local->created;
+                $group->insert();
+                if (empty($result)) {
+                    throw new FeedDBException($group);
+                }
+                $profile->group_id = $group->id;
+            } else {
+                $result = $local->insert();
+                if (empty($result)) {
+                    throw new FeedDBException($local);
+                }
+                $profile->profile_id = $local->id;
+            }
+
+            $profile->created = sql_common_date();
+            $profile->lastupdate = sql_common_date();
+            $result = $profile->insert();
+            if (empty($result)) {
+                throw new FeedDBException($profile);
+            }
+
+            $entity->query('COMMIT');
+        } catch (FeedDBException $e) {
+            common_log_db_error($e->obj, 'INSERT', __FILE__);
+            $entity->query('ROLLBACK');
+            return false;
+        }
+
+        $avatar = $munger->getAvatar();
+        if ($avatar) {
+            try {
+                $this->updateAvatar($avatar);
+            } catch (Exception $e) {
+                common_log(LOG_ERR, "Exception setting OStatus avatar: " .
+                                    $e->getMessage());
+            }
+        }
+
+        return $entity;
+    }
+
+    /**
+     * Download and update given avatar image
+     * @param string $url
+     * @throws Exception in various failure cases
+     */
+    public function updateAvatar($url)
+    {
+        // @fixme this should be better encapsulated
+        // ripped from oauthstore.php (for old OMB client)
+        $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
+        copy($url, $temp_filename);
+        $imagefile = new ImageFile($profile->id, $temp_filename);
+        $filename = Avatar::filename($profile->id,
+                                     image_type_to_extension($imagefile->type),
+                                     null,
+                                     common_timestamp());
+        rename($temp_filename, Avatar::path($filename));
+        if ($this->isGroup()) {
+            $group = $this->localGroup();
+            $group->setOriginal($filename);
+        } else {
+            $profile = $this->localProfile();
+            $profile->setOriginal($filename);
+        }
+    }
+
+    /**
+     * Returns an XML string fragment with profile information as an
+     * Activity Streams noun object with the given element type.
+     *
+     * Assumes that 'activity' namespace has been previously defined.
+     *
+     * @param string $element one of 'actor', 'subject', 'object', 'target'
+     * @return string
+     */
+    function asActivityNoun($element)
+    {
+        $xs = new XMLStringer(true);
+
+        $avatarHref = Avatar::defaultImage(AVATAR_PROFILE_SIZE);
+        $avatarType = 'image/png';
+        if ($this->isGroup()) {
+            $type = 'http://activitystrea.ms/schema/1.0/group';
+            $self = $this->localGroup();
+
+            // @fixme put a standard getAvatar() interface on groups too
+            if ($self->homepage_logo) {
+                $avatarHref = $self->homepage_logo;
+                $map = array('png' => 'image/png',
+                             'jpg' => 'image/jpeg',
+                             'jpeg' => 'image/jpeg',
+                             'gif' => 'image/gif');
+                $extension = pathinfo(parse_url($avatarHref, PHP_URL_PATH), PATHINFO_EXTENSION);
+                if (isset($map[$extension])) {
+                    $avatarType = $map[$extension];
+                }
+            }
+        } else {
+            $type = 'http://activitystrea.ms/schema/1.0/person';
+            $self = $this->localProfile();
+            $avatar = $self->getAvatar(AVATAR_PROFILE_SIZE);
+            if ($avatar) {
+                $avatarHref = $avatar->
+                $avatarType = $avatar->mediatype;
+            }
+        }
+        $xs->elementStart('activity:' . $element);
+        $xs->element(
+            'activity:object-type',
+            null,
+            $type
+        );
+        $xs->element(
+            'id',
+            null,
+            $this->homeuri); // ?
+        $xs->element('title', null, $self->getBestName());
+
+        $xs->element(
+            'link', array(
+                'type' => $avatarType,
+                'href' => $avatarHref
+            ),
+            ''
+        );
+
+        $xs->elementEnd('activity:' . $element);
+
+        return $xs->getString();
+    }
+
+    /**
+     * Damn dirty hack!
+     */
+    function isGroup()
+    {
+        return (strpos($this->feeduri, '/groups/') !== false);
+    }
+
+    /**
+     * Send a subscription request to the hub for this feed.
+     * The hub will later send us a confirmation POST to /main/push/callback.
+     *
+     * @return bool true on success, false on failure
+     */
+    public function subscribe($mode='subscribe')
+    {
+        if (common_config('feedsub', 'nohub')) {
+            // Fake it! We're just testing remote feeds w/o hubs.
+            return true;
+        }
+        // @fixme use the verification token
+        #$token = md5(mt_rand() . ':' . $this->feeduri);
+        #$this->verify_token = $token;
+        #$this->update(); // @fixme
+        try {
+            $callback = common_local_url('pushcallback', array('feed' => $this->id));
+            $headers = array('Content-Type: application/x-www-form-urlencoded');
+            $post = array('hub.mode' => $mode,
+                          'hub.callback' => $callback,
+                          'hub.verify' => 'async',
+                          'hub.verify_token' => $this->verify_token,
+                          'hub.secret' => $this->secret,
+                          //'hub.lease_seconds' => 0,
+                          'hub.topic' => $this->feeduri);
+            $client = new HTTPClient();
+            $response = $client->post($this->huburi, $headers, $post);
+            $status = $response->getStatus();
+            if ($status == 202) {
+                common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
+                return true;
+            } else if ($status == 204) {
+                common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified');
+                return true;
+            } else if ($status >= 200 && $status < 300) {
+                common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
+                return false;
+            } else {
+                common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody());
+                return false;
+            }
+        } catch (Exception $e) {
+            // wtf!
+            common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri");
+            return false;
+        }
+    }
+
+    /**
+     * Save PuSH subscription confirmation.
+     * Sets approximate lease start and end times and finalizes state.
+     *
+     * @param int $lease_seconds provided hub.lease_seconds parameter, if given
+     */
+    public function confirmSubscribe($lease_seconds=0)
+    {
+        $original = clone($this);
+
+        $this->sub_state = 'active';
+        $this->sub_start = common_sql_date(time());
+        if ($lease_seconds > 0) {
+            $this->sub_end = common_sql_date(time() + $lease_seconds);
+        } else {
+            $this->sub_end = null;
+        }
+        $this->lastupdate = common_sql_date();
+
+        return $this->update($original);
+    }
+
+    /**
+     * Save PuSH unsubscription confirmation.
+     * Wipes active PuSH sub info and resets state.
+     */
+    public function confirmUnsubscribe()
+    {
+        $original = clone($this);
+
+        $this->verify_token = null;
+        $this->secret = null;
+        $this->sub_state = null;
+        $this->sub_start = null;
+        $this->sub_end = null;
+        $this->lastupdate = common_sql_date();
+
+        return $this->update($original);
+    }
+
+    /**
+     * Send a PuSH unsubscription request to the hub for this feed.
+     * The hub will later send us a confirmation POST to /main/push/callback.
+     *
+     * @return bool true on success, false on failure
+     */
+    public function unsubscribe() {
+        return $this->subscribe('unsubscribe');
+    }
+
+    /**
+     * Send an Activity Streams notification to the remote Salmon endpoint,
+     * if so configured.
+     *
+     * @param Profile $actor
+     * @param $verb eg Activity::SUBSCRIBE or Activity::JOIN
+     * @param $object object of the action; if null, the remote entity itself is assumed
+     */
+    public function notify(Profile $actor, $verb, $object=null)
+    {
+        if ($object == null) {
+            $object = $this;
+        }
+        if ($this->salmonuri) {
+            $text = 'update'; // @fixme
+            $id = 'tag:' . common_config('site', 'server') . 
+                ':' . $verb .
+                ':' . $actor->id .
+                ':' . time(); // @fixme
+
+            $entry = new Atom10Entry();
+            $entry->elementStart('entry');
+            $entry->element('id', null, $id);
+            $entry->element('title', null, $text);
+            $entry->element('summary', null, $text);
+            $entry->element('published', null, common_date_w3dtf());
+
+            $entry->element('activity:verb', null, $verb);
+            $entry->raw($profile->asAtomAuthor());
+            $entry->raw($profile->asActivityActor());
+            $entry->raw($object->asActivityNoun('object'));
+            $entry->elmentEnd('entry');
+
+            $feed = $this->atomFeed($actor);
+            $feed->initFeed();
+            $feed->addEntry($entry);
+            $feed->renderEntries();
+            $feed->endFeed();
+
+            $xml = $feed->getString();
+            common_log(LOG_INFO, "Posting to Salmon endpoint $salmon: $xml");
+
+            $salmon = new Salmon(); // ?
+            $salmon->post($this->salmonuri, $xml);
+        }
+    }
+
+    function getBestName()
+    {
+        if ($this->isGroup()) {
+            return $this->localGroup()->getBestName();
+        } else {
+            return $this->localProfile()->getBestName();
+        }
+    }
+
+    function atomFeed($actor)
+    {
+        $feed = new Atom10Feed();
+        // @fixme should these be set up somewhere else?
+        $feed->addNamespace('activity', 'http://activitystrea.ms/spec/1.0/');
+        $feed->addNamesapce('thr', 'http://purl.org/syndication/thread/1.0');
+        $feed->addNamespace('georss', 'http://www.georss.org/georss');
+        $feed->addNamespace('ostatus', 'http://ostatus.org/schema/1.0');
+
+        $taguribase = common_config('integration', 'taguri');
+        $feed->setId("tag:{$taguribase}:UserTimeline:{$actor->id}"); // ???
+
+        $feed->setTitle($actor->getBestName() . ' timeline'); // @fixme
+        $feed->setUpdated(time());
+        $feed->setPublished(time());
+
+        $feed->addLink(common_url('ApiTimelineUser',
+                                  array('id' => $actor->id,
+                                        'type' => 'atom')),
+                       array('rel' => 'self',
+                             'type' => 'application/atom+xml'));
+
+        $feed->addLink(common_url('userbyid',
+                                  array('id' => $actor->id)),
+                       array('rel' => 'alternate',
+                             'type' => 'text/html'));
+
+        return $feed;
+    }
+
+    /**
+     * Read and post notices for updates from the feed.
+     * Currently assumes that all items in the feed are new,
+     * coming from a PuSH hub.
+     *
+     * @param string $xml source of Atom or RSS feed
+     * @param string $hmac X-Hub-Signature header, if present
+     */
+    public function postUpdates($xml, $hmac)
+    {
+        common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml");
+
+        if ($this->secret) {
+            if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) {
+                $their_hmac = strtolower($matches[1]);
+                $our_hmac = hash_hmac('sha1', $xml, $this->secret);
+                if ($their_hmac !== $our_hmac) {
+                    common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac");
+                    return;
+                }
+            } else {
+                common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
+                return;
+            }
+        } else if ($hmac) {
+            common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'");
+            return;
+        }
+
+        require_once "XML/Feed/Parser.php";
+        $feed = new XML_Feed_Parser($xml, false, false, true);
+        $munger = new FeedMunger($feed);
+        
+        $hits = 0;
+        foreach ($feed as $index => $entry) {
+            // @fixme this might sort in wrong order if we get multiple updates
+
+            $notice = $munger->notice($index);
+
+            // Double-check for oldies
+            // @fixme this could explode horribly for multiple feeds on a blog. sigh
+            $dupe = new Notice();
+            $dupe->uri = $notice->uri;
+            if ($dupe->find(true)) {
+                common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}");
+                continue;
+            }
+
+            // @fixme need to ensure that groups get handled correctly
+            $saved = Notice::saveNew($notice->profile_id,
+                                     $notice->content,
+                                     'ostatus',
+                                     array('is_local' => Notice::REMOTE_OMB,
+                                           'uri' => $notice->uri,
+                                           'lat' => $notice->lat,
+                                           'lon' => $notice->lon,
+                                           'location_ns' => $notice->location_ns,
+                                           'location_id' => $notice->location_id));
+
+            /*
+            common_log(LOG_DEBUG, "going to check group delivery...");
+            if ($this->group_id) {
+                $group = User_group::staticGet($this->group_id);
+                if ($group) {
+                    common_log(LOG_INFO, __METHOD__ . ": saving to local shadow group $group->id $group->nickname");
+                    $groups = array($group);
+                } else {
+                    common_log(LOG_INFO, __METHOD__ . ": lost the local shadow group?");
+                }
+            } else {
+                common_log(LOG_INFO, __METHOD__ . ": no local shadow groups");
+                $groups = array();
+            }
+            common_log(LOG_DEBUG, "going to add to inboxes...");
+            $notice->addToInboxes($groups, array());
+            common_log(LOG_DEBUG, "added to inboxes.");
+            */
+
+            $hits++;
+        }
+        if ($hits == 0) {
+            common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml");
+        }
+    }
+}
diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php
new file mode 100644 (file)
index 0000000..36e2279
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * PHP version 5
+ *
+ * LICENCE: 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/>.
+ *
+ * @category  OStatus
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @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()
+    {
+    }
+}
index cbaec677505a249a35d35b29cf9776d8f7330f28..c895b6ce24afde9a48ae991530415cc457437045 100644 (file)
@@ -83,13 +83,17 @@ class FeedMunger
         $this->url = $url;
     }
     
-    function feedinfo()
+    function ostatusProfile()
     {
-        $feedinfo = new Feedinfo();
-        $feedinfo->feeduri = $this->url;
-        $feedinfo->homeuri = $this->feed->link;
-        $feedinfo->huburi = $this->getHubLink();
-        return $feedinfo;
+        $profile = new Ostatus_profile();
+        $profile->feeduri = $this->url;
+        $profile->homeuri = $this->feed->link;
+        $profile->huburi = $this->getHubLink();
+        $salmon = $this->getSalmonLink();
+        if ($salmon) {
+            $profile->salmonuri = $salmon;
+        }
+        return $profile;
     }
 
     function getAtomLink($item, $attribs=array())
@@ -155,6 +159,16 @@ class FeedMunger
         return $this->getAtomLink($this->feed, array('rel' => 'hub'));
     }
 
+    function getSalmonLink()
+    {
+        return $this->getAtomLink($this->feed, array('rel' => 'salmon'));
+    }
+
+    function getSelfLink()
+    {
+        return $this->getAtomLink($this->feed, array('rel' => 'self'));
+    }
+
     /**
      * Get an appropriate avatar image source URL, if available.
      * @return mixed string or false
@@ -203,12 +217,13 @@ class FeedMunger
         if (!$entry) {
             return null;
         }
-        
+
         if ($preview) {
             $notice = new FeedSubPreviewNotice($this->profile(true));
             $notice->id = -1;
         } else {
             $notice = new Notice();
+            $notice->profile_id = $this->profileIdForEntry($index);
         }
 
         $link = $this->getAltLink($entry);
@@ -221,7 +236,7 @@ class FeedMunger
         $notice->uri = $link;
         $notice->url = $link;
         $notice->content = $this->noticeFromEntry($entry);
-        $notice->rendered = common_render_content($notice->content, $notice);
+        $notice->rendered = common_render_content($notice->content, $notice); // @fixme this is failing on group posts
         $notice->created = common_sql_date($entry->updated); // @fixme
         $notice->is_local = Notice::GATEWAY;
         $notice->source = 'feed';
@@ -239,7 +254,22 @@ class FeedMunger
         return $notice;
     }
 
+    function profileIdForEntry($index=1)
+    {
+        // hack hack hack
+        // should get profile for this entry's author...
+        $remote = Ostatus_profile::staticGet('feeduri', $this->getSelfLink());
+        if ($feed) {
+            return $feed->profile_id;
+        } else {
+            throw new Exception("Can't find feed profile");
+        }
+    }
+
     /**
+     * Parse location given as a GeoRSS-simple point, if provided.
+     * http://www.georss.org/simple
+     *
      * @param feed item $entry
      * @return mixed Location or false
      */
@@ -249,7 +279,10 @@ class FeedMunger
         $points = $dom->getElementsByTagNameNS('http://www.georss.org/georss', 'point');
         
         for ($i = 0; $i < $points->length; $i++) {
-            $point = trim($points->item(0)->textContent);
+            $point = $points->item(0)->textContent;
+            $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace"
+            $point = preg_replace('/\s+/', ' ', $point);
+            $point = trim($point);
             $coords = explode(' ', $point);
             if (count($coords) == 2) {
                 list($lat, $lon) = $coords;
index 126f1355f903feca35d09098ee3dcf2bbef37e92..245a57f7200dac1e321d3aefe98661d9edaec724 100644 (file)
@@ -34,27 +34,101 @@ class HubDistribQueueHandler extends QueueHandler
     {
         assert($notice instanceof Notice);
 
+        $this->pushUser($notice);
+        foreach ($notice->getGroups() as $group) {
+            $this->pushGroup($notice, $group->group_id);
+        }
+        return true;
+    }
+    
+    function pushUser($notice)
+    {
         // See if there's any PuSH subscriptions, including OStatus clients.
         // @fixme handle group subscriptions as well
         // http://identi.ca/api/statuses/user_timeline/1.atom
         $feed = common_local_url('ApiTimelineUser',
                                  array('id' => $notice->profile_id,
                                        'format' => 'atom'));
+        $this->pushFeed($feed, array($this, 'userFeedForNotice'), $notice);
+    }
+
+    function pushGroup($notice, $group_id)
+    {
+        $feed = common_local_url('ApiTimelineGroup',
+                                 array('id' => $group_id,
+                                       'format' => 'atom'));
+        $this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group_id, $notice);
+    }
+
+    /**
+     * @param string $feed URI to the feed
+     * @param callable $callback function to generate Atom feed update if needed
+     *        any additional params are passed to the callback.
+     */
+    function pushFeed($feed, $callback)
+    {
+        $hub = common_config('ostatus', 'hub');
+        if ($hub) {
+            $this->pushFeedExternal($feed, $hub);
+        }
+
         $sub = new HubSub();
         $sub->topic = $feed;
         if ($sub->find()) {
-            common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $feed");
-            $qm = QueueManager::get();
-            $atom = $this->userFeedForNotice($notice);
-            while ($sub->fetch()) {
-                common_log(LOG_INFO, "Prepping PuSH distribution to $sub->callback for $feed");
-                $data = array('sub' => clone($sub),
-                              'atom' => $atom);
-                $qm->enqueue($data, 'hubout');
-            }
+            $args = array_slice(func_get_args(), 2);
+            $atom = call_user_func_array($callback, $args);
+            $this->pushFeedInternal($atom, $sub);
         } else {
             common_log(LOG_INFO, "No PuSH subscribers for $feed");
         }
+        return true;
+    }
+
+    /**
+     * Ping external hub about this update.
+     * The hub will pull the feed and check for new items later.
+     * Not guaranteed safe in an environment with database replication.
+     *
+     * @param string $feed feed topic URI
+     * @param string $hub PuSH hub URI
+     * @fixme can consolidate pings for user & group posts
+     */
+    function pushFeedExternal($feed, $hub)
+    {
+        $client = new HTTPClient();
+        try {
+            $data = array('hub.mode' => 'publish',
+                          'hub.url' => $feed);
+            $response = $client->post($hub, array(), $data);
+            if ($response->getStatus() == 204) {
+                common_log(LOG_INFO, "PuSH ping to hub $hub for $feed ok");
+                return true;
+            } else {
+                common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed with HTTP " .
+                                    $response->getStatus() . ': ' .
+                                    $response->getBody());
+            }
+        } catch (Exception $e) {
+            common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed: " . $e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Queue up direct feed update pushes to subscribers on our internal hub.
+     * @param string $atom update feed, containing only new/changed items
+     * @param HubSub $sub open query of subscribers
+     */
+    function pushFeedInternal($atom, $sub)
+    {
+        common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $sub->topic");
+        $qm = QueueManager::get();
+        while ($sub->fetch()) {
+            common_log(LOG_INFO, "Prepping PuSH distribution to $sub->callback for $sub->topic");
+            $data = array('sub' => clone($sub),
+                          'atom' => $atom);
+            $qm->enqueue($data, 'hubout');
+        }
     }
 
     /**
@@ -83,5 +157,29 @@ class HubDistribQueueHandler extends QueueHandler
         common_log(LOG_DEBUG, $feed);
         return $feed;
     }
+
+    function groupFeedForNotice($group_id, $notice)
+    {
+        // @fixme this feels VERY hacky...
+        // should probably be a cleaner way to do it
+
+        ob_start();
+        $api = new ApiTimelineGroupAction();
+        $args = array('id' => $group_id,
+                      'format' => 'atom',
+                      'max_id' => $notice->id,
+                      'since_id' => $notice->id - 1);
+        $api->prepare($args);
+        $api->handle($args);
+        $feed = ob_get_clean();
+        
+        // ...and override the content-type back to something normal... eww!
+        // hope there's no other headers that got set while we weren't looking.
+        header('Content-Type: text/html; charset=utf-8');
+
+        common_log(LOG_DEBUG, $feed);
+        return $feed;
+    }
+
 }
 
index cb44ad2c4edc55d298b2ae09060740d42eed5e52..0791c7e5db16c4fd186ba5158bddc93546172770 100644 (file)
@@ -43,7 +43,7 @@ class HubOutQueueHandler extends QueueHandler
             common_log(LOG_ERR, "Failed PuSH to $sub->callback for $sub->topic: " .
                                 $e->getMessage());
             // @fixme Reschedule a later delivery?
-            // Currently we have no way to do this other than 'send NOW'
+            return true;
         }
 
         return true;
diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php
new file mode 100644 (file)
index 0000000..8c77222
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * PHP version 5
+ *
+ * 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/>.
+ *
+ * @package   StatusNet
+ * @author    James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+class Salmon
+{
+    public function post($endpoint_uri, $xml)
+    {
+        if (empty($endpoint_uri)) {
+            return FALSE;
+        }
+
+        $headers = array('Content-type: application/atom+xml');
+
+        try {
+            $client = new HTTPClient();
+            $client->setBody($xml);
+            $response = $client->post($endpoint_uri, $headers);
+        } catch (HTTP_Request2_Exception $e) {
+            return false;
+        }
+        if ($response->getStatus() != 200) {
+            return false;
+        }
+
+    }
+
+    public function createMagicEnv($text, $userid)
+    {
+
+
+    }
+
+
+    public function verifyMagicEnv($env)
+    {
+
+
+    }
+}
diff --git a/plugins/OStatus/lib/webfinger.php b/plugins/OStatus/lib/webfinger.php
new file mode 100644 (file)
index 0000000..417d549
--- /dev/null
@@ -0,0 +1,143 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * PHP version 5
+ *
+ * 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/>.
+ *
+ * @package   StatusNet
+ * @author    James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+define('WEBFINGER_SERVICE_REL_VALUE', 'lrdd');
+
+/**
+ * Implement the webfinger protocol.
+ */
+class Webfinger
+{
+    /**
+     * Perform a webfinger lookup given an account.
+     */ 
+    public function lookup($id)
+    {
+        $id = $this->normalize($id);
+        list($name, $domain) = explode('@', $id);
+
+        $links = $this->getServiceLinks($domain);
+        if (!$links) {
+            return false;
+        }
+        
+        $services = array();
+        foreach ($links as $link) {
+            if ($link['template']) {
+                return $this->getServiceDescription($link['template'], $id);
+            }
+            if ($link['href']) {
+                return $this->getServiceDescription($link['href'], $id);
+            }
+        }
+    }
+
+    /**
+     * Normalize an account ID
+     */
+    function normalize($id)
+    {
+        if (substr($id, 0, 7) == 'acct://') {
+            return substr($id, 7); 
+        } else if (substr($id, 0, 5) == 'acct:') {
+            return substr($id, 5);
+        }
+
+        return $id;
+    }
+
+    function getServiceLinks($domain)
+    {
+        $url = 'http://'. $domain .'/.well-known/host-meta';
+        $content = $this->fetchURL($url);
+        if (empty($content)) {
+            common_log(LOG_DEBUG, 'Error fetching host-meta');
+            return false;
+        }
+        $result = XRD::parse($content);
+
+        // Ensure that the host == domain (spec may include signing later)
+        if ($result->host != $domain) {
+            return false;
+        }
+        
+        $links = array();
+        foreach ($result->links as $link) {
+            if ($link['rel'] == WEBFINGER_SERVICE_REL_VALUE) {
+                $links[] = $link;
+            }
+
+        }
+        return $links;
+    }
+
+    function getServiceDescription($template, $id)
+    {
+        $url = $this->applyTemplate($template, 'acct:' . $id);
+
+        $content = $this->fetchURL($url);
+
+        return XRD::parse($content);
+    }
+
+    function fetchURL($url)
+    {
+        try {
+            $client = new HTTPClient();
+            $response = $client->get($url);
+        } catch (HTTP_Request2_Exception $e) {
+            return false;
+        }
+
+        if ($response->getStatus() != 200) {
+            return false;
+        }
+
+        return $response->getBody();
+    }
+
+    function applyTemplate($template, $id)
+    {
+        $template = str_replace('{uri}', urlencode($id), $template);
+
+        return $template;
+    }
+
+    function getHostMeta($domain, $template) {
+        $xrd = new XRD();
+        $xrd->host = $domain;
+        $xrd->links[] = array('rel' => 'lrdd',
+                              'template' => $template,
+                              'title' => array('Resource Descriptor'));
+
+        return $xrd->toXML();
+    }
+}
+
+
diff --git a/plugins/OStatus/lib/xrd.php b/plugins/OStatus/lib/xrd.php
new file mode 100644 (file)
index 0000000..16d27f8
--- /dev/null
@@ -0,0 +1,183 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * PHP version 5
+ *
+ * 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/>.
+ *
+ * @package   StatusNet
+ * @author    James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+
+class XRD
+{
+    const XML_NS = 'http://www.w3.org/2000/xmlns/';
+    
+    const XRD_NS = 'http://docs.oasis-open.org/ns/xri/xrd-1.0';
+
+    const HOST_META_NS = 'http://host-meta.net/xrd/1.0';
+    
+    public $expires;
+
+    public $subject;
+
+    public $host;
+
+    public $alias = array();
+    
+    public $types = array();
+    
+    public $links = array();
+    
+    public static function parse($xml)
+    {
+        $xrd = new XRD();
+
+        $dom = new DOMDocument();
+        $dom->loadXML($xml);
+        $xrd_element = $dom->getElementsByTagName('XRD')->item(0);
+
+        // Check for host-meta host
+        $host = $xrd_element->getElementsByTagName('Host')->item(0)->nodeValue;
+        if ($host) {
+            $xrd->host = $host;
+        }
+
+        // Loop through other elements
+        foreach ($xrd_element->childNodes as $node) {
+            switch ($node->tagName) {
+            case 'Expires':
+                $xrd->expires = $node->nodeValue;
+                break;
+            case 'Subject':
+                $xrd->subject = $node->nodeValue;
+                break;
+                
+            case 'Alias':
+                $xrd->alias[] = $node->nodeValue;
+                break;
+
+            case 'Link':
+                $xrd->links[] = $xrd->parseLink($node);
+                break;
+
+            case 'Type':
+                $xrd->types[] = $xrd->parseType($node);
+                break;
+
+            }
+        }
+        return $xrd;
+    }
+
+    public function toXML()
+    {
+        $dom = new DOMDocument('1.0', 'UTF-8');
+        $dom->formatOutput = true;
+        
+        $xrd_dom = $dom->createElementNS(XRD::XRD_NS, 'XRD');
+        $dom->appendChild($xrd_dom);
+
+        if ($this->host) {
+            $host_dom = $dom->createElement('hm:Host', $this->host);
+            $xrd_dom->setAttributeNS(XRD::XML_NS, 'xmlns:hm', XRD::HOST_META_NS);
+            $xrd_dom->appendChild($host_dom);
+        }
+        
+               if ($this->expires) {
+                       $expires_dom = $dom->createElement('Expires', $this->expires);
+                       $xrd_dom->appendChild($expires_dom);
+               }
+
+               if ($this->subject) {
+                       $subject_dom = $dom->createElement('Subject', $this->subject);
+                       $xrd_dom->appendChild($subject_dom);
+               }
+
+               foreach ($this->alias as $alias) {
+                       $alias_dom = $dom->createElement('Alias', $alias);
+                       $xrd_dom->appendChild($alias_dom);
+               }
+
+               foreach ($this->types as $type) {
+                       $type_dom = $dom->createElement('Type', $type);
+                       $xrd_dom->appendChild($type_dom);
+               }
+
+               foreach ($this->links as $link) {
+                       $link_dom = $this->saveLink($dom, $link);
+                       $xrd_dom->appendChild($link_dom);
+               }
+
+        return $dom->saveXML();
+    }
+
+    function parseType($element)
+    {
+        return array();
+    }
+    
+    function parseLink($element)
+    {
+        $link = array();
+        $link['rel'] = $element->getAttribute('rel');
+        $link['type'] = $element->getAttribute('type');
+        $link['href'] = $element->getAttribute('href');
+        $link['template'] = $element->getAttribute('template');
+        foreach ($element->childNodes as $node) {
+            switch($node->tagName) {
+            case 'Title':
+                $link['title'][] = $node->nodeValue;
+            }
+        }
+
+        return $link;
+    }
+
+    function saveLink($doc, $link)
+    {
+        $link_element = $doc->createElement('Link');
+        if ($link['rel']) {
+            $link_element->setAttribute('rel', $link['rel']);
+        }
+        if ($link['type']) {
+            $link_element->setAttribute('type', $link['type']);
+        }
+        if ($link['href']) {
+            $link_element->setAttribute('href', $link['href']);
+        }
+        if ($link['template']) {
+            $link_element->setAttribute('template', $link['template']);
+        }
+
+        if (is_array($link['title'])) {
+            foreach($link['title'] as $title) {
+                $title = $doc->createElement('Title', $title);
+                $link_element->appendChild($title);
+            }
+        }
+
+        
+        return $link_element;
+    }
+}
+
index 70ddc411f834a2bb4d8070490b158ac3737257a8..3218276a689eba869f69e1f5b577ae73d40ba772 100644 (file)
@@ -1104,10 +1104,9 @@ left:0;
 
 .dialogbox {
 position:absolute;
-top:-4px;
-right:29px;
+top:-1px;
+right:-1px;
 z-index:9;
-min-width:199px;
 float:none;
 padding:11px;
 border-radius:7px;
@@ -1120,6 +1119,7 @@ border-width:1px;
 .dialogbox legend {
 display:block !important;
 margin-right:18px;
+margin-bottom:18px;
 }
 
 .dialogbox button.close {
@@ -1128,11 +1128,22 @@ right:3px;
 top:3px;
 }
 
+.dialogbox .form_guide {
+font-weight:normal;
+padding:0;
+}
+
 .dialogbox .submit_dialogbox {
 font-weight:bold;
 text-indent:0;
 min-width:46px;
 }
+.dialogbox input {
+padding-left:4px;
+}
+.dialogbox fieldset {
+margin-bottom:0;
+}
 
 #wrap form.processing input.submit,
 .entity_actions a.processing,
@@ -1142,6 +1153,12 @@ outline:none;
 text-indent:-9999px;
 }
 
+.form_repeat.dialogbox {
+top:-4px;
+right:29px;
+min-width:199px;
+}
+
 .notice-options {
 position:relative;
 font-size:0.95em;
index 02e1645f47aecd7771a0025f26a5ad95e32822fd..a2f10134283210534a36c9f08a13cc25f9fcf31e 100644 (file)
@@ -30,7 +30,9 @@ border-radius:4px;
 input, textarea, select, option {
 font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
 }
-input, textarea, select {
+input, textarea, select,
+.entity_actions .dialogbox input,
+.mark-top {
 border-color:#AAAAAA;
 }
 
@@ -79,7 +81,8 @@ background-color:transparent;
 input:focus, textarea:focus, select:focus,
 .form_notice.warning #notice_data-text,
 .form_notice.warning #notice_text-count,
-.form_settings .form_note {
+.form_settings .form_note,
+.entity_actions .dialogbox .form_data input:focus {
 border-color:#9BB43E;
 }
 input.submit {
@@ -134,9 +137,6 @@ color:#002FA7;
 #content tbody tr {
 border-top-color:#C8D1D5;
 }
-.mark-top {
-border-color:#AAAAAA;
-}
 
 #aside_primary {
 background-color:#C8D1D5;
@@ -145,7 +145,9 @@ background-color:#C8D1D5;
 #notice_text-count {
 color:#333333;
 }
-.form_notice.warning #notice_text-count {
+.form_notice.warning #notice_text-count,
+.dialogbox,
+.entity_actions .dialogbox input {
 color:#000000;
 }
 .form_notice label[for=notice_data-attach] {
index 6dc7d21df05d62c52c8f099e690517fd228cf3c7..e214047451efbd8c79a0f33c98a4f61bd3e86de7 100644 (file)
@@ -30,7 +30,9 @@ border-radius:4px;
 input, textarea, select, option {
 font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
 }
-input, textarea, select {
+input, textarea, select,
+.entity_actions .dialogbox input,
+.mark-top {
 border-color:#AAAAAA;
 }
 
@@ -135,9 +137,6 @@ color:#002FA7;
 #content tbody tr {
 border-top-color:#CEE1E9;
 }
-.mark-top {
-border-color:#AAAAAA;
-}
 
 #aside_primary {
 background-color:#CEE1E9;
@@ -146,7 +145,9 @@ background-color:#CEE1E9;
 #notice_text-count {
 color:#333333;
 }
-.form_notice.warning #notice_text-count {
+.form_notice.warning #notice_text-count,
+.dialogbox,
+.entity_actions .dialogbox input {
 color:#000000;
 }
 .form_notice label[for=notice_data-attach] {