]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
OStatus: initial hookup of remote group membership (notice delivery not yet working...
authorBrion Vibber <brion@pobox.com>
Mon, 22 Feb 2010 17:43:27 +0000 (09:43 -0800)
committerBrion Vibber <brion@pobox.com>
Mon, 22 Feb 2010 20:13:57 +0000 (12:13 -0800)
- added a temp config var to disable salmon magic signatures until they're working consistently

plugins/OStatus/OStatusPlugin.php
plugins/OStatus/actions/groupsalmon.php
plugins/OStatus/actions/ostatussub.php
plugins/OStatus/classes/Ostatus_profile.php
plugins/OStatus/lib/activity.php
plugins/OStatus/lib/salmon.php
plugins/OStatus/lib/salmonaction.php

index 7c6c0c69f300b49b1794136333c9bc63e817dd9a..061ed4bd1b28239f0eb2cb66c743419ac3fe4f76 100644 (file)
@@ -211,7 +211,7 @@ class OStatusPlugin extends Plugin
 
                 // FIXME: this needs to go out in a queue handler
 
-                $xml = '<?xml version="1.0" encoding="UTF-8" ?>';
+                $xml = '<?xml version="1.0" encoding="UTF-8" ?' . '>';
                 $xml .= $notice->asAtomEntry(true, true);
 
                 $salmon = new Salmon();
@@ -402,6 +402,97 @@ class OStatusPlugin extends Plugin
         return true;
     }
 
+    /**
+     * When one of our local users tries to join a remote group,
+     * notify the remote server. If the notification is rejected,
+     * deny the join.
+     *
+     * @param User_group $group
+     * @param User $user
+     *
+     * @return mixed hook return value
+     */
+
+    function onStartJoinGroup($group, $user)
+    {
+        $oprofile = Ostatus_profile::staticGet('group_id', $group->id);
+        if ($oprofile) {
+            $member = Profile::staticGet($user->id);
+
+            $act = new Activity();
+            $act->id = TagURI::mint('join:%d:%d:%s',
+                                    $member->id,
+                                    $group->id,
+                                    common_date_iso8601(time()));
+
+            $act->actor = ActivityObject::fromProfile($member);
+            $act->verb = ActivityVerb::JOIN;
+            $act->object = $oprofile->asActivityObject();
+
+            $act->time = time();
+            $act->title = _m("Join");
+            $act->content = sprintf(_m("%s has joined group %s."),
+                                    $member->getBestName(),
+                                    $oprofile->getBestName());
+
+            if ($oprofile->notifyActivity($act)) {
+                return true;
+            } else {
+                throw new ServerException(_m("Failed joining remote group."));
+            }
+        }
+    }
+
+    /**
+     * When one of our local users leaves a remote group, notify the remote
+     * server.
+     *
+     * @fixme Might be good to schedule a resend of the leave notification
+     * if it failed due to a transitory error. We've canceled the local
+     * membership already anyway, but if the remote server comes back up
+     * it'll be left with a stray membership record.
+     *
+     * @param User_group $group
+     * @param User $user
+     *
+     * @return mixed hook return value
+     */
+
+    function onEndLeaveGroup($group, $user)
+    {
+        $oprofile = Ostatus_profile::staticGet('group_id', $group->id);
+        if ($oprofile) {
+            // Drop the PuSH subscription if there are no other subscribers.
+    
+            $members = $group->getMembers(0, 1);
+            if ($members->N == 0) {
+                common_log(LOG_INFO, "Unsubscribing from now-unused group feed $oprofile->feeduri");
+                $oprofile->unsubscribe();
+            }
+
+
+            $member = Profile::staticGet($user->id);
+
+            $act = new Activity();
+            $act->id = TagURI::mint('leave:%d:%d:%s',
+                                    $member->id,
+                                    $group->id,
+                                    common_date_iso8601(time()));
+
+            $act->actor = ActivityObject::fromProfile($member);
+            $act->verb = ActivityVerb::LEAVE;
+            $act->object = $oprofile->asActivityObject();
+
+            $act->time = time();
+            $act->title = _m("Leave");
+            $act->content = sprintf(_m("%s has left group %s."),
+                                    $member->getBestName(),
+                                    $oprofile->getBestName());
+
+            $oprofile->notifyActivity($act);
+        }
+    }
+
     /**
      * Notify remote users when their notices get favorited.
      *
index 64ae9f3cc0aefa68b42ed3fe7dd5742c4439f781..2e4fe94436aa747ae4f7809cbff33081b2cc0fca 100644 (file)
@@ -88,21 +88,96 @@ class GroupsalmonAction extends SalmonAction
      * Save a subscription relationship for them.
      */
 
+    /**
+     * Postel's law: consider a "follow" notification as a "join".
+     */
     function handleFollow()
     {
-        $this->handleJoin(); // ???
+        $this->handleJoin();
     }
 
+    /**
+     * Postel's law: consider an "unfollow" notification as a "leave".
+     */
     function handleUnfollow()
     {
+        $this->handleLeave();
     }
 
     /**
      * A remote user joined our group.
+     * @fixme move permission checks and event call into common code,
+     *        currently we're doing the main logic in joingroup action
+     *        and so have to repeat it here.
      */
 
     function handleJoin()
     {
+        $oprofile = $this->ensureProfile();
+        if (!$oprofile) {
+            $this->clientError(_m("Can't read profile to set up group membership."));
+        }
+        if ($oprofile->isGroup()) {
+            $this->clientError(_m("Groups can't join groups."));
+        }
+
+        common_log(LOG_INFO, "Remote profile {$oprofile->uri} joining local group {$this->group->nickname}");
+        $profile = $oprofile->localProfile();
+
+        if ($profile->isMember($this->group)) {
+            // Already a member; we'll take it silently to aid in resolving
+            // inconsistencies on the other side.
+            return true;
+        }
+
+        if (Group_block::isBlocked($this->group, $profile)) {
+            $this->clientError(_('You have been blocked from that group by the admin.'), 403);
+            return false;
+        }
+
+        try {
+            // @fixme that event currently passes a user from main UI
+            // Event should probably move into Group_member::join
+            // and take a Profile object.
+            //
+            //if (Event::handle('StartJoinGroup', array($this->group, $profile))) {
+                Group_member::join($this->group->id, $profile->id);
+                //Event::handle('EndJoinGroup', array($this->group, $profile));
+            //}
+        } catch (Exception $e) {
+            $this->serverError(sprintf(_m('Could not join remote user %1$s to group %2$s.'),
+                                       $oprofile->uri, $this->group->nickname));
+        }
+    }
+
+    /**
+     * A remote user left our group.
+     */
+
+    function handleLeave()
+    {
+        $oprofile = $this->ensureProfile();
+        if (!$oprofile) {
+            $this->clientError(_m("Can't read profile to cancel group membership."));
+        }
+        if ($oprofile->isGroup()) {
+            $this->clientError(_m("Groups can't join groups."));
+        }
+
+        common_log(LOG_INFO, "Remote profile {$oprofile->uri} leaving local group {$this->group->nickname}");
+        $profile = $oprofile->localProfile();
+
+        try {
+            // @fixme event needs to be refactored as above
+            //if (Event::handle('StartLeaveGroup', array($this->group, $profile))) {
+                Group_member::leave($this->group->id, $profile->id);
+                //Event::handle('EndLeaveGroup', array($this->group, $profile));
+            //}
+        } catch (Exception $e) {
+            $this->serverError(sprintf(_m('Could not remove remote user %1$s from group %2$s.'),
+                                       $oprofile->uri, $this->group->nickname));
+            return;
+        }
     }
 
 }
index 95dec19afc5e9890a6940e5d950ff3b0e4e602b6..592ae387eaeba8c4f7236b2c7045360bf87ef2f6 100644 (file)
@@ -248,7 +248,7 @@ class OStatusSubAction extends Action
                 $group = $this->oprofile->localGroup();
                 if ($user->isMember($group)) {
                     $this->showForm(_m('Already a member!'));
-                } elseif (Group_member::join($this->profile->group_id, $user->id)) {
+                } elseif (Group_member::join($this->oprofile->group_id, $user->id)) {
                     $this->showForm(_m('Joined remote group!'));
                 } else {
                     $this->showForm(_m('Remote group join failed!'));
index 0e12f8fc6e14005b50648b22311c832f320e91f6..c0e39add8fc2d87c47a430ec767275b61bd05a0c 100644 (file)
@@ -137,12 +137,49 @@ class Ostatus_profile extends Memcached_DataObject
         return null;
     }
 
+    /**
+     * Returns an ActivityObject describing this remote user or group profile.
+     * Can then be used to generate Atom chunks.
+     *
+     * @return ActivityObject
+     */
+    function asActivityObject()
+    {
+        if ($this->isGroup()) {
+            $object = new ActivityObject();
+            $object->type = 'http://activitystrea.ms/schema/1.0/group';
+            $object->id = $this->uri;
+            $self = $this->localGroup();
+
+            // @fixme put a standard getAvatar() interface on groups too
+            if ($self->homepage_logo) {
+                $object->avatar = $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])) {
+                    // @fixme this ain't used/saved yet
+                    $object->avatarType = $map[$extension];
+                }
+            }
+
+            $object->link = $this->uri; // @fixme accurate?
+            return $object;
+        } else {
+            return ActivityObject::fromProfile($this->localProfile());
+        }
+    }
+
     /**
      * 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.
      *
+     * @fixme replace with wrappers on asActivityObject when it's got everything.
+     *
      * @param string $element one of 'actor', 'subject', 'object', 'target'
      * @return string
      */
@@ -202,11 +239,19 @@ class Ostatus_profile extends Memcached_DataObject
     }
 
     /**
-     * Damn dirty hack!
+     * @return boolean true if this is a remote group
      */
     function isGroup()
     {
-        return (strpos($this->feeduri, '/groups/') !== false);
+        if ($this->profile_id && !$this->group_id) {
+            return false;
+        } else if ($this->group_id && !$this->profile_id) {
+            return true;
+        } else if ($this->group_id && $this->profile_id) {
+            throw new ServerException("Invalid ostatus_profile state: both group and profile IDs set for $this->uri");
+        } else {
+            throw new ServerException("Invalid ostatus_profile state: both group and profile IDs empty for $this->uri");
+        }
     }
 
     /**
@@ -353,22 +398,24 @@ class Ostatus_profile extends Memcached_DataObject
             common_log(LOG_INFO, "Posting to Salmon endpoint $this->salmonuri: $xml");
 
             $salmon = new Salmon(); // ?
-            $salmon->post($this->salmonuri, $xml);
+            return $salmon->post($this->salmonuri, $xml);
         }
+        return false;
     }
 
     public function notifyActivity($activity)
     {
         if ($this->salmonuri) {
 
-            $xml = $activity->asString(true);
+            $xml = '<?xml version="1.0" encoding="UTF-8" ?' . '>' .
+                          $activity->asString(true);
 
             $salmon = new Salmon(); // ?
 
-            $salmon->post($this->salmonuri, $xml);
+            return $salmon->post($this->salmonuri, $xml);
         }
 
-        return;
+        return false;
     }
 
     function getBestName()
@@ -597,10 +644,23 @@ class Ostatus_profile extends Memcached_DataObject
      */
     protected function updateAvatar($url)
     {
+        if ($this->isGroup()) {
+            $self = $this->localGroup();
+        } else {
+            $self = $this->localProfile();
+        }
+        if (!$self) {
+            throw new ServerException(sprintf(
+                _m("Tried to update avatar for unsaved remote profile %s"),
+                $this->uri));
+        }
+
         // @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);
+        if (!copy($url, $temp_filename)) {
+            throw new ServerException(sprintf(_m("Unable to fetch avatar from %s"), $url));
+        }
 
         if ($this->isGroup()) {
             $id = $this->group_id;
@@ -614,13 +674,7 @@ class Ostatus_profile extends Memcached_DataObject
                                      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);
-        }
+        $self->setOriginal($filename);
     }
 
     protected static function getActivityObjectAvatar($object)
@@ -747,6 +801,18 @@ class Ostatus_profile extends Memcached_DataObject
         self::createActivityObjectProfile($actor, $feeduri, $salmonuri);
     }
 
+    /**
+     * Create local ostatus_profile and profile/user_group entries for
+     * the provided remote user or group.
+     *
+     * @param ActivityObject $object
+     * @param string $feeduri
+     * @param string $salmonuri
+     * @param array $hints
+     *
+     * @fixme fold $feeduri/$salmonuri into $hints
+     * @return Ostatus_profile
+     */
     protected static function createActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array())
     {
         $homeuri  = $object->id;
@@ -784,46 +850,65 @@ class Ostatus_profile extends Memcached_DataObject
             }
         }
 
-        $profile = new Profile();
-        $profile->nickname   = $nickname;
-        $profile->fullname   = $object->title;
-        if (!empty($object->link)) {
-            $profile->profileurl = $object->link;
-        } else if (array_key_exists('profileurl', $hints)) {
-            $profile->profileurl = $hints['profileurl'];
-        }
-        $profile->created    = common_sql_now();
-
-        // @fixme bio
-        // @fixme tags/categories
-        // @fixme location?
-        // @todo tags from categories
-        // @todo lat/lon/location?
-
-        $profile_id = $profile->insert();
-
-        if (!$profile_id) {
-            throw new ServerException("Can't save local profile");
-        }
-
-        // @fixme either need to do feed discovery here
-        // or need to split out some of the feed stuff
-        // so we can leave it empty until later.
-
         $oprofile = new Ostatus_profile();
 
         $oprofile->uri        = $homeuri;
         $oprofile->feeduri    = $feeduri;
         $oprofile->salmonuri  = $salmonuri;
-        $oprofile->profile_id = $profile_id;
 
         $oprofile->created    = common_sql_now();
         $oprofile->modified   = common_sql_now();
 
+        if ($object->type == ActivityObject::PERSON) {
+            $profile = new Profile();
+            $profile->nickname   = $nickname;
+            $profile->fullname   = $object->title;
+            if (!empty($object->link)) {
+                $profile->profileurl = $object->link;
+            } else if (array_key_exists('profileurl', $hints)) {
+                $profile->profileurl = $hints['profileurl'];
+            }
+            $profile->created    = common_sql_now();
+    
+            // @fixme bio
+            // @fixme tags/categories
+            // @fixme location?
+            // @todo tags from categories
+            // @todo lat/lon/location?
+    
+            $oprofile->profile_id = $profile->insert();
+    
+            if (!$oprofile->profile_id) {
+                throw new ServerException("Can't save local profile");
+            }
+        } else {
+            $group = new User_group();
+            $group->nickname = $nickname;
+            $group->fullname = $object->title;
+            // @fixme no canonical profileurl; using homepage instead for now
+            $group->homepage = $homeuri;
+            $group->created = common_sql_now();
+
+            // @fixme homepage
+            // @fixme bio
+            // @fixme tags/categories
+            // @fixme location?
+            // @todo tags from categories
+            // @todo lat/lon/location?
+
+            $oprofile->group_id = $group->insert();
+
+            if (!$oprofile->group_id) {
+                throw new ServerException("Can't save local profile");
+            }
+        }
+
         $ok = $oprofile->insert();
 
         if ($ok) {
-            $oprofile->updateAvatar($avatar);
+            if ($avatar) {
+                $oprofile->updateAvatar($avatar);
+            }
             return $oprofile;
         } else {
             throw new ServerException("Can't save OStatus profile");
index a26248f199c1a01e552c3d03f227b0d1bd065d3c..6cb9881bf6d9115d690664d75fe6ca17141725fc 100644 (file)
@@ -367,6 +367,9 @@ class ActivityObject
         return $object;
     }
 
+    /**
+     * @fixme missing avatar, bio info, etc
+     */
     static function fromProfile($profile)
     {
         $object = new ActivityObject();
@@ -379,6 +382,9 @@ class ActivityObject
         return $object;
     }
 
+    /**
+     * @fixme missing avatar, bio info, etc
+     */
     function asString($tag='activity:object')
     {
         $xs = new XMLStringer(true);
index 53925dc3f449687aa15cdcfe2462a3294bc98c09..b5f178cc6a2e4cec2f191dad33d96ed39aa9a4ad 100644 (file)
  */
 class Salmon
 {
+    /**
+     * Sign and post the given Atom entry as a Salmon message.
+     *
+     * @fixme pass through the actor for signing?
+     *
+     * @param string $endpoint_uri
+     * @param string $xml
+     * @return boolean success
+     */
     public function post($endpoint_uri, $xml)
     {
         if (empty($endpoint_uri)) {
-            return FALSE;
+            return false;
         }
 
-        $xml = $this->createMagicEnv($xml);
-        
-        $headers = array('Content-type: application/atom+xml');
+        if (!common_config('ostatus', 'skip_signatures')) {
+            $xml = $this->createMagicEnv($xml);
+        }
+
+        $headers = array('Content-Type: application/atom+xml');
 
         try {
             $client = new HTTPClient();
@@ -51,7 +62,7 @@ class Salmon
                 $response->getStatus() . ': ' . $response->getBody());
             return false;
         }
-
+        return true;
     }
 
     public function createMagicEnv($text)
index 09a042975dd9a3af0660df9d65c3f9c18ef6991e..83cf0b8f8a30ebd9e767643a63b221031d1b8ef3 100644 (file)
@@ -41,7 +41,7 @@ class SalmonAction extends Action
             $this->clientError(_('This method requires a POST.'));
         }
 
-        if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') {
+        if (empty($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/atom+xml') {
             $this->clientError(_('Salmon requires application/atom+xml'));
         }
 
@@ -57,11 +57,13 @@ class SalmonAction extends Action
 
         // Check the signature
         $salmon = new Salmon;
-        if (!$salmon->verifyMagicEnv($dom)) {
-            common_log(LOG_DEBUG, "Salmon signature verification failed.");
-            $this->clientError(_m('Salmon signature verification failed.'));
+        if (!common_config('ostatus', 'skip_signatures')) {
+            if (!$salmon->verifyMagicEnv($dom)) {
+                common_log(LOG_DEBUG, "Salmon signature verification failed.");
+                $this->clientError(_m('Salmon signature verification failed.'));
+            }
         }
-            
+
         $this->act = new Activity($dom->documentElement);
         return true;
     }
@@ -101,6 +103,9 @@ class SalmonAction extends Action
             case ActivityVerb::JOIN:
                 $this->handleJoin();
                 break;
+            case ActivityVerb::LEAVE:
+                $this->handleLeave();
+                break;
             default:
                 throw new ClientException(_("Unimplemented."));
             }
@@ -154,6 +159,14 @@ class SalmonAction extends Action
         throw new ClientException(_("Unimplemented!"));
     }
 
+    /**
+     * Hmmmm
+     */
+    function handleLeave()
+    {
+        throw new ClientException(_("Unimplemented!"));
+    }
+
     /**
      * @return Ostatus_profile
      */