- added a temp config var to disable salmon magic signatures until they're working consistently
// 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();
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.
*
* 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;
+ }
}
}
$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!'));
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
*/
}
/**
- * 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");
+ }
}
/**
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()
*/
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;
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)
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;
}
}
- $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");
return $object;
}
+ /**
+ * @fixme missing avatar, bio info, etc
+ */
static function fromProfile($profile)
{
$object = new ActivityObject();
return $object;
}
+ /**
+ * @fixme missing avatar, bio info, etc
+ */
function asString($tag='activity:object')
{
$xs = new XMLStringer(true);
*/
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();
$response->getStatus() . ': ' . $response->getBody());
return false;
}
-
+ return true;
}
public function createMagicEnv($text)
$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'));
}
// 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;
}
case ActivityVerb::JOIN:
$this->handleJoin();
break;
+ case ActivityVerb::LEAVE:
+ $this->handleLeave();
+ break;
default:
throw new ClientException(_("Unimplemented."));
}
throw new ClientException(_("Unimplemented!"));
}
+ /**
+ * Hmmmm
+ */
+ function handleLeave()
+ {
+ throw new ClientException(_("Unimplemented!"));
+ }
+
/**
* @return Ostatus_profile
*/