/** * @return Ostatus_profile */ function ensureProfile() { $actor = $this->act->actor; if (empty($actor->id)) { common_log(LOG_ERR, "broken actor: " . var_export($actor, true)); common_log(LOG_ERR, "activity with no actor: " . var_export($this->act, true)); throw new Exception("Received a salmon slap from unidentified actor."); } return Ostatus_profile::ensureActivityObjectProfile($actor); }
function joinGroup($user, $activity) { // XXX: check that actor == subject $uri = $activity->objects[0]->id; $group = User_group::getKV('uri', $uri); if (!$group instanceof User_group) { $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]); if (!$oprofile->isGroup()) { // TRANS: Client exception thrown when trying to join a remote group that is not a group. throw new ClientException(_('Remote profile is not a group!')); } $group = $oprofile->localGroup(); } assert(!empty($group)); if ($user->isMember($group)) { // TRANS: Client exception thrown when trying to join a group the importing user is already a member of. throw new ClientException(_("User is already a member of this group.")); } $user->joinGroup($group); }
/** * Handle a posted object from Salmon * * @param Activity $activity activity to handle * @param mixed $target user or group targeted * * @return boolean hook value */ function onStartHandleSalmonTarget(Activity $activity, $target) { if (!$this->isMyActivity($activity)) { return true; } $this->log(LOG_INFO, "Checking {$activity->id} as a valid Salmon slap."); if ($target instanceof User_group || $target->isGroup()) { $uri = $target->getUri(); if (!array_key_exists($uri, $activity->context->attention)) { // @todo FIXME: please document (i18n). // TRANS: Client exception thrown when ... throw new ClientException(_('Object not posted to this group.')); } } elseif ($target instanceof Profile && $target->isLocal()) { $original = null; // FIXME: Shouldn't favorites show up with a 'target' activityobject? if (!ActivityUtils::compareTypes($activity->verb, array(ActivityVerb::POST)) && isset($activity->objects[0])) { // If this is not a post, it's a verb targeted at something (such as a Favorite attached to a note) if (!empty($activity->objects[0]->id)) { $activity->context->replyToID = $activity->objects[0]->id; } } if (!empty($activity->context->replyToID)) { $original = Notice::getKV('uri', $activity->context->replyToID); } if ((!$original instanceof Notice || $original->profile_id != $target->id) && !array_key_exists($target->getUri(), $activity->context->attention)) { // @todo FIXME: Please document (i18n). // TRANS: Client exception when ... throw new ClientException(_('Object not posted to this user.')); } } else { // TRANS: Server exception thrown when a micro app plugin uses a target that cannot be handled. throw new ServerException(_('Do not know how to handle this kind of target.')); } $oactor = Ostatus_profile::ensureActivityObjectProfile($activity->actor); $actor = $oactor->localProfile(); // FIXME: will this work in all cases? I made it work for Favorite... if (ActivityUtils::compareTypes($activity->verb, array(ActivityVerb::POST))) { $object = $activity->objects[0]; } else { $object = $activity; } $options = array('uri' => $object->id, 'url' => $object->link, 'is_local' => Notice::REMOTE, 'source' => 'ostatus'); if (!isset($this->oldSaveNew)) { $notice = Notice::saveActivity($activity, $actor, $options); } else { $notice = $this->saveNoticeFromActivity($activity, $actor, $options); } return false; }
public function processShare($activity, $method) { $notice = null; $oprofile = $this->checkAuthorship($activity); if (empty($oprofile)) { common_log(LOG_INFO, "No author matched share activity"); return null; } if (count($activity->objects) != 1) { // TRANS: Client exception thrown when trying to share multiple activities at once. throw new ClientException(_m('Can only handle share activities with exactly one object.')); } $shared = $activity->objects[0]; if (!$shared instanceof Activity) { // TRANS: Client exception thrown when trying to share a non-activity object. throw new ClientException(_m('Can only handle shared activities.')); } $other = Ostatus_profile::ensureActivityObjectProfile($shared->actor); // Save the item (or check for a dupe) $sharedNotice = $other->processActivity($shared, $method); if (empty($sharedNotice)) { $sharedId = $shared->id ? $shared->id : $shared->objects[0]->id; // TRANS: Client exception thrown when saving an activity share fails. // TRANS: %s is a share ID. throw new ClientException(sprintf(_m('Failed to save activity %s.'), $sharedId)); } // The id URI will be used as a unique identifier for for the notice, // protecting against duplicate saves. It isn't required to be a URL; // tag: URIs for instance are found in Google Buzz feeds. $sourceUri = $activity->id; $dupe = Notice::staticGet('uri', $sourceUri); if ($dupe) { common_log(LOG_INFO, "OStatus: ignoring duplicate post: {$sourceUri}"); return $dupe; } // We'll also want to save a web link to the original notice, if provided. $sourceUrl = null; if ($activity->link) { $sourceUrl = $activity->link; } else { if ($activity->link) { $sourceUrl = $activity->link; } else { if (preg_match('!^https?://!', $activity->id)) { $sourceUrl = $activity->id; } } } // Use summary as fallback for content if (!empty($activity->content)) { $sourceContent = $activity->content; } else { if (!empty($activity->summary)) { $sourceContent = $activity->summary; } else { if (!empty($activity->title)) { $sourceContent = $activity->title; } else { // @todo FIXME: Fetch from $sourceUrl? // TRANS: Client exception. %s is a source URI. throw new ClientException(sprintf(_m('No content for notice %s.'), $sourceUri)); } } } // Get (safe!) HTML and text versions of the content $rendered = $this->purify($sourceContent); $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8'); $shortened = common_shorten_links($content); // If it's too long, try using the summary, and make the // HTML an attachment. $attachment = null; if (Notice::contentTooLong($shortened)) { $attachment = $this->saveHTMLFile($activity->title, $rendered); $summary = html_entity_decode(strip_tags($activity->summary), ENT_QUOTES, 'UTF-8'); if (empty($summary)) { $summary = $content; } $shortSummary = common_shorten_links($summary); if (Notice::contentTooLong($shortSummary)) { $url = common_shorten_url($sourceUrl); $shortSummary = substr($shortSummary, 0, Notice::maxContent() - (mb_strlen($url) + 2)); $content = $shortSummary . ' ' . $url; // We mark up the attachment link specially for the HTML output // so we can fold-out the full version inline. // @todo FIXME i18n: This tooltip will be saved with the site's default language // TRANS: Shown when a notice is longer than supported and/or when attachments are present. At runtime // TRANS: this will usually be replaced with localised text from StatusNet core messages. $showMoreText = _m('Show more'); $attachUrl = common_local_url('attachment', array('attachment' => $attachment->id)); $rendered = common_render_text($shortSummary) . '<a href="' . htmlspecialchars($attachUrl) . '"' . ' class="attachment more"' . ' title="' . htmlspecialchars($showMoreText) . '">' . '…' . '</a>'; } } $options = array('is_local' => Notice::REMOTE, 'url' => $sourceUrl, 'uri' => $sourceUri, 'rendered' => $rendered, 'replies' => array(), 'groups' => array(), 'peopletags' => array(), 'tags' => array(), 'urls' => array(), 'repeat_of' => $sharedNotice->id, 'scope' => $sharedNotice->scope); // Check for optional attributes... if (!empty($activity->time)) { $options['created'] = common_sql_date($activity->time); } if ($activity->context) { // Any individual or group attn: targets? $replies = $activity->context->attention; $options['groups'] = $this->filterReplies($oprofile, $replies); $options['replies'] = $replies; // Maintain direct reply associations // @todo FIXME: What about conversation ID? if (!empty($activity->context->replyToID)) { $orig = Notice::staticGet('uri', $activity->context->replyToID); if (!empty($orig)) { $options['reply_to'] = $orig->id; } } $location = $activity->context->location; if ($location) { $options['lat'] = $location->lat; $options['lon'] = $location->lon; if ($location->location_id) { $options['location_ns'] = $location->location_ns; $options['location_id'] = $location->location_id; } } } if ($this->isPeopletag()) { $options['peopletags'][] = $this->localPeopletag(); } // Atom categories <-> hashtags foreach ($activity->categories as $cat) { if ($cat->term) { $term = common_canonical_tag($cat->term); if ($term) { $options['tags'][] = $term; } } } // Atom enclosures -> attachment URLs foreach ($activity->enclosures as $href) { // @todo FIXME: Save these locally or....? $options['urls'][] = $href; } $notice = Notice::saveNew($oprofile->profile_id, $content, 'ostatus', $options); return $notice; }
protected function saveObjectFromActivity(Activity $act, Notice $stored, array $options = array()) { assert($this->isMyActivity($act)); // The below algorithm is mainly copied from the previous Ostatus_profile->processShare() if (count($act->objects) !== 1) { // TRANS: Client exception thrown when trying to share multiple activities at once. throw new ClientException(_m('Can only handle share activities with exactly one object.')); } $shared = $act->objects[0]; if (!$shared instanceof Activity) { // TRANS: Client exception thrown when trying to share a non-activity object. throw new ClientException(_m('Can only handle shared activities.')); } $sharedUri = $shared->id; if (!empty($shared->objects[0]->id)) { // Because StatusNet since commit 8cc4660 sets $shared->id to a TagURI which // f***s up federation, because the URI is no longer recognised by the origin. // So we set it to the object ID if it exists, otherwise we trust $shared->id $sharedUri = $shared->objects[0]->id; } if (empty($sharedUri)) { throw new ClientException(_m('Shared activity does not have an id')); } try { // First check if we have the shared activity. This has to be done first, because // we can't use these functions to "ensureActivityObjectProfile" of a local user, // who might be the creator of the shared activity in question. $sharedNotice = Notice::getByUri($sharedUri); } catch (NoResultException $e) { // If no locally stored notice is found, process it! // TODO: Remember to check Deleted_notice! // TODO: If a post is shared that we can't retrieve - what to do? $other = Ostatus_profile::ensureActivityObjectProfile($shared->actor); $sharedNotice = $other->processActivity($shared, 'push'); // FIXME: push/salmon/what? if (!$sharedNotice instanceof Notice) { // And if we apparently can't get the shared notice, we'll abort the whole thing. // TRANS: Client exception thrown when saving an activity share fails. // TRANS: %s is a share ID. throw new ClientException(sprintf(_m('Failed to save activity %s.'), $sharedUri)); } } catch (FeedSubException $e) { // Remote feed could not be found or verified, should we // transform this into an "RT @user Blah, blah, blah..."? common_log(LOG_INFO, __METHOD__ . ' got a ' . get_class($e) . ': ' . $e->getMessage()); return false; } // Setting this here because when the algorithm gets back to // Notice::saveActivity it will update the Notice object. $stored->repeat_of = $sharedNotice->getID(); $stored->conversation = $sharedNotice->conversation; $stored->object_type = ActivityUtils::resolveUri(ActivityObject::ACTIVITY, true); // We don't have to save a repeat in a separate table, we can // find repeats by just looking at the notice.repeat_of field. // By returning true here instead of something that evaluates // to false, we show that we have processed everything properly. return true; }
function joinGroup($user, $activity) { // XXX: check that actor == subject $uri = $activity->objects[0]->id; $group = User_group::staticGet('uri', $uri); if (empty($group)) { $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]); if (!$oprofile->isGroup()) { // TRANS: Client exception thrown when trying to join a remote group that is not a group. throw new ClientException(_("Remote profile is not a group!")); } $group = $oprofile->localGroup(); } assert(!empty($group)); if ($user->isMember($group)) { // TRANS: Client exception thrown when trying to join a group the importing user is already a member of. throw new ClientException(_("User is already a member of this group.")); } if (Event::handle('StartJoinGroup', array($group, $user))) { Group_member::join($group->id, $user->id); Event::handle('EndJoinGroup', array($group, $user)); } }
function ensureProfiles() { try { $this->oprofile = Ostatus_profile::getActorProfile($this->activity); if (!$this->oprofile instanceof Ostatus_profile) { throw new UnknownUriException($this->activity->actor->id); } } catch (UnknownUriException $e) { // Apparently we didn't find the Profile object based on our URI, // so OStatus doesn't have it with this URI in ostatus_profile. // Try to look it up again, remote side may have changed from http to https // or maybe publish an acct: URI now instead of an http: URL. // // Steps: // 1. Check the newly received URI. Who does it say it is? // 2. Compare these alleged identities to our local database. // 3. If we found any locally stored identities, ask it about its aliases. // 4. Do any of the aliases from our known identity match the recently introduced one? // // Example: We have stored http://example.com/user/1 but this URI says https://example.com/user/1 common_debug('No local Profile object found for a magicsigned activity author URI: ' . $e->object_uri); $disco = new Discovery(); $xrd = $disco->lookup($e->object_uri); // Step 1: We got a bunch of discovery data for https://example.com/user/1 which includes // aliases https://example.com/user and hopefully our original http://example.com/user/1 too $all_ids = array_merge(array($xrd->subject), $xrd->aliases); if (!in_array($e->object_uri, $all_ids)) { common_debug('The activity author URI we got was not listed itself when doing discovery on it.'); throw $e; } // Go through each reported alias from lookup to see if we know this already foreach ($all_ids as $aliased_uri) { $oprofile = Ostatus_profile::getKV('uri', $aliased_uri); if (!$oprofile instanceof Ostatus_profile) { continue; // unknown locally, check the next alias } // Step 2: We found the alleged http://example.com/user/1 URI in our local database, // but this can't be trusted yet because anyone can publish any alias. common_debug('Found a local Ostatus_profile for "' . $e->object_uri . '" with this URI: ' . $aliased_uri); // We found an existing OStatus profile, but is it really the same? Do a callback to the URI's origin // Step 3: lookup our previously known http://example.com/user/1 webfinger etc. $xrd = $disco->lookup($oprofile->getUri()); // getUri returns ->uri, which we filtered on earlier $doublecheck_aliases = array_merge(array($xrd->subject), $xrd->aliases); common_debug('Trying to match known "' . $aliased_uri . '" against its returned aliases: ' . implode(' ', $doublecheck_aliases)); // if we find our original URI here, it is a legitimate alias // Step 4: Is the newly introduced https://example.com/user/1 URI in the list of aliases // presented by http://example.com/user/1 (i.e. do they both say they are the same identity?) if (in_array($e->object_uri, $doublecheck_aliases)) { $oprofile->updateUriKeys($e->object_uri, DiscoveryHints::fromXRD($xrd)); $this->oprofile = $oprofile; break; // don't iterate through aliases anymore } } // We might end up here after $all_ids is iterated through without a $this->oprofile value, if (!$this->oprofile instanceof Ostatus_profile) { common_debug("We do not have a local profile to connect to this activity's author. Let's create one."); // ensureActivityObjectProfile throws exception on failure $this->oprofile = Ostatus_profile::ensureActivityObjectProfile($this->activity->actor); } } assert($this->oprofile instanceof Ostatus_profile); $this->actor = $this->oprofile->localProfile(); }
/** * Handle a posted object from Salmon * * @param Activity $activity activity to handle * @param mixed $target user or group targeted * * @return boolean hook value */ function onStartHandleSalmonTarget($activity, $target) { if ($this->isMyActivity($activity)) { $this->log(LOG_INFO, "Checking {$activity->id} as a valid Salmon slap."); if ($target instanceof User_group) { $uri = $target->getUri(); if (!in_array($uri, $activity->context->attention)) { // @todo FIXME: please document (i18n). // TRANS: Client exception thrown when ... throw new ClientException(_('Bookmark not posted to this group.')); } } else { if ($target instanceof User) { $uri = $target->uri; $original = null; if (!empty($activity->context->replyToID)) { $original = Notice::staticGet('uri', $activity->context->replyToID); } if (!in_array($uri, $activity->context->attention) && (empty($original) || $original->profile_id != $target->id)) { // @todo FIXME: Please document (i18n). // TRANS: Client exception when ... throw new ClientException(_('Object not posted to this user.')); } } else { // TRANS: Server exception thrown when a micro app plugin uses a target that cannot be handled. throw new ServerException(_('Do not know how to handle this kind of target.')); } } $actor = Ostatus_profile::ensureActivityObjectProfile($activity->actor); $object = $activity->objects[0]; $options = array('uri' => $object->id, 'url' => $object->link, 'is_local' => Notice::REMOTE, 'source' => 'ostatus'); // $actor is an ostatus_profile $this->saveNoticeFromActivity($activity, $actor->localProfile(), $options); return false; } return true; }
function joinGroup($user, $activity) { // XXX: check that actor == subject $uri = $activity->objects[0]->id; $group = User_group::staticGet('uri', $uri); if (empty($group)) { $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]); if (!$oprofile->isGroup()) { throw new Exception("Remote profile is not a group!"); } $group = $oprofile->localGroup(); } assert(!empty($group)); if (Event::handle('StartJoinGroup', array($group, $user))) { Group_member::join($group->id, $user->id); Event::handle('EndJoinGroup', array($group, $user)); } }
function handleUntag() { if ($this->activity->target->type == ActivityObject::_LIST) { if ($this->activity->objects[0]->type != ActivityObject::PERSON) { // TRANS: Client exception. throw new ClientException(_m('Not a person object.')); return false; } // this is a peopletag $tagged = User::staticGet('uri', $this->activity->objects[0]->id); if (empty($tagged)) { // TRANS: Client exception. throw new ClientException(_m('Unidentified profile being untagged.')); } if ($tagged->id !== $this->user->id) { // TRANS: Client exception. throw new ClientException(_m('This user is not the one being untagged.')); } // save the list $tagger = $this->ensureProfile(); $list = Ostatus_profile::ensureActivityObjectProfile($this->activity->target); $ptag = $list->localPeopletag(); $result = Profile_tag::unTag($ptag->tagger, $tagged->id, $ptag->tag); if (!$result) { // TRANS: Client exception. throw new ClientException(_m('The tag could not be deleted.')); } } }
/** * Handle a posted bookmark from Salmon * * @param Activity $activity activity to handle * @param mixed $target user or group targeted * * @return boolean hook value */ function onStartHandleSalmonTarget($activity, $target) { if (self::_isPostBookmark($activity)) { $this->log(LOG_INFO, "Checking {$activity->id} as a valid Salmon slap."); if ($target instanceof User_group) { $uri = $target->getUri(); if (!in_array($uri, $activity->context->attention)) { throw new ClientException(_("Bookmark not posted " . "to this group.")); } } else { if ($target instanceof User) { $uri = $target->uri; $original = null; if (!empty($activity->context->replyToID)) { $original = Notice::staticGet('uri', $activity->context->replyToID); } if (!in_array($uri, $activity->context->attention) && (empty($original) || $original->profile_id != $target->id)) { throw new ClientException(_("Bookmark not posted " . "to this user.")); } } else { throw new ServerException(_("Don't know how to handle " . "this kind of target.")); } } $author = Ostatus_profile::ensureActivityObjectProfile($activity->actor); self::_postRemoteBookmark($author, $activity); return false; } return true; }