From acb07ef52f2adfd1a87a044c173d7363a1748d63 Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Tue, 1 Jul 2014 11:42:08 +0200 Subject: [PATCH] Added saveActivity method to Notice class saveActivity will accept an Activity which gets parsed and saved through plugins. So when an ActivityHandlerPlugin (such as Favorite will be soon) gets a feed to save, this will be the function called instead of saveNew. --- classes/Notice.php | 280 +++++++++++++++++--- plugins/OStatus/OStatusPlugin.php | 5 + plugins/OStatus/classes/Ostatus_profile.php | 23 +- 3 files changed, 263 insertions(+), 45 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index 3f138e1cfe..f11c670603 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -627,41 +627,13 @@ class Notice extends Managed_DataObject $notice->object_type = $object_type; } - if (is_null($scope)) { // 0 is a valid value - if (!empty($reply)) { - $notice->scope = $reply->scope; - } else { - $notice->scope = self::defaultScope(); - } + if (is_null($scope) && $reply instanceof Notice) { + $notice->scope = $reply->scope; } else { $notice->scope = $scope; } - // For private streams - - try { - $user = $profile->getUser(); - - if ($user->private_stream && - ($notice->scope == Notice::PUBLIC_SCOPE || - $notice->scope == Notice::SITE_SCOPE)) { - $notice->scope |= Notice::FOLLOWER_SCOPE; - } - } catch (NoSuchUserException $e) { - // Cannot handle private streams for remote profiles - } - - // Force the scope for private groups - - foreach ($groups as $groupId) { - $group = User_group::getKV('id', $groupId); - if ($group instanceof User_group) { - if ($group->force_scope) { - $notice->scope |= Notice::GROUP_SCOPE; - break; - } - } - } + $notice->scope = self::figureOutScope($profile, $groups, $notice->scope); if (Event::handle('StartNoticeSave', array(&$notice))) { @@ -749,6 +721,252 @@ class Notice extends Managed_DataObject return $notice; } + static function saveActivity(Activity $act, Profile $actor, array $options=array()) { + + // First check if we're going to let this Activity through from the specific actor + if (!$actor->hasRight(Right::NEWNOTICE)) { + common_log(LOG_WARNING, "Attempted post from user disallowed to post: " . $actor->getNickname()); + + // TRANS: Client exception thrown when a user tries to post while being banned. + throw new ClientException(_m('You are banned from posting notices on this site.'), 403); + } + if (common_config('throttle', 'enabled') && !self::checkEditThrottle($actor->id)) { + common_log(LOG_WARNING, 'Excessive posting by profile #' . $actor->id . '; throttled.'); + // TRANS: Client exception thrown when a user tries to post too many notices in a given time frame. + throw new ClientException(_m('Too many notices too fast; take a breather '. + 'and post again in a few minutes.')); + } + + // Get ActivityObject properties + $actobj = count($act->objects)==1 ? $act->objects[0] : null; + if (!is_null($actobj) && $actobj->id) { + $options['uri'] = $actobj->id; + if ($actobj->link) { + $options['url'] = $actobj->link; + } elseif ($act->link) { + $options['url'] = $act->link; + } elseif (preg_match('!^https?://!', $actobj->id)) { + $options['url'] = $actobj->id; + } + } else { + // implied object + $options['uri'] = $act->id; + $options['url'] = $act->link; + } + + $defaults = array( + 'groups' => array(), + 'is_local' => self::LOCAL_PUBLIC, + 'mentions' => array(), + 'reply_to' => null, + 'repeat_of' => null, + 'scope' => null, + 'source' => 'unknown', + 'tags' => array(), + 'uri' => null, + 'url' => null, + 'urls' => array(), + 'distribute' => true); + + // options will have default values when nothing has been supplied + $options = array_merge($defaults, $options); + foreach (array_keys($defaults) as $key) { + // Only convert the keynames we specify ourselves from 'defaults' array into variables + $$key = $options[$key]; + } + extract($options, EXTR_SKIP); + + $stored = new Notice(); + if (!empty($uri)) { + $stored->uri = $uri; + if ($stored->find()) { + common_debug('cannot create duplicate Notice URI: '.$stored->uri); + throw new Exception('Notice URI already exists'); + } + } + + $stored->profile_id = $actor->id; + $stored->source = $source; + $stored->uri = $uri; + $stored->url = $url; + $stored->verb = $act->verb; + + $autosource = common_config('public', 'autosource'); + + // Sandboxed are non-false, but not 1, either + if (!$actor->hasRight(Right::PUBLICNOTICE) || + ($source && $autosource && in_array($source, $autosource))) { + $stored->is_local = Notice::LOCAL_NONPUBLIC; + } + + // Maybe a missing act-time should be fatal if the actor is not local? + if (!empty($act->time)) { + $stored->created = common_sql_date($act->time); + } else { + $stored->created = common_sql_now(); + } + + $reply = null; + if ($act->context instanceof ActivityContext && !empty($act->context->replyToID)) { + $reply = self::getKV('uri', $act->context->replyToID); + } + if (!$reply instanceof Notice && $act->target instanceof ActivityObject) { + $reply = self::getKV('uri', $act->target->id); + } + + if ($reply instanceof Notice) { + if (!$reply->inScope($actor)) { + // TRANS: Client error displayed when trying to reply to a notice a the target has no access to. + // TRANS: %1$s is a user nickname, %2$d is a notice ID (number). + throw new ClientException(sprintf(_m('%1$s has no right to reply to notice %2$d.'), $actor->getNickname(), $reply->id), 403); + } + + $stored->reply_to = $reply->id; + $stored->conversation = $reply->conversation; + + // If the original is private to a group, and notice has no group specified, + // make it to the same group(s) + if (empty($groups) && ($reply->scope & Notice::GROUP_SCOPE)) { + $groups = array(); + $replyGroups = $reply->getGroups(); + foreach ($replyGroups as $group) { + if ($actor->isMember($group)) { + $groups[] = $group->id; + } + } + } + + if (is_null($scope)) { + $scope = $reply->scope; + } + } + + if ($act->context instanceof ActivityContext) { + $location = $act->context->location; + if ($location) { + $stored->lat = $location->lat; + $stored->lon = $location->lon; + if ($location->location_id) { + $stored->location_ns = $location->location_ns; + $stored->location_id = $location->location_id; + } + } + } else { + $act->context = new ActivityContext(); + } + + $stored->scope = self::figureOutScope($actor, $groups, $scope); + + foreach ($act->categories as $cat) { + if ($cat->term) { + $term = common_canonical_tag($cat->term); + if (!empty($term)) { + $tags[] = $term; + } + } + } + + foreach ($act->enclosures as $href) { + // @todo FIXME: Save these locally or....? + $urls[] = $href; + } + + if (Event::handle('StartNoticeSave', array(&$stored))) { + // XXX: some of these functions write to the DB + + try { + $stored->insert(); // throws exception on error + + $object = null; + Event::handle('StoreActivityObject', array($act, $stored, $options, &$object)); + if (empty($object)) { + throw new ServerException('No object from StoreActivityObject '.$stored->uri . ': '.$act->asString()); + } + $orig = clone($stored); + $stored->object_type = ActivityUtils::resolveUri($object->type, true); + $stored->update($orig); + } catch (Exception $e) { + if (empty($stored->id)) { + common_debug('Failed to save stored object entry in database ('.$e->getMessage().')'); + } else { + common_debug('Failed to store activity object in database ('.$e->getMessage().'), deleting notice id '.$stored->id); + $stored->delete(); + } + throw $e; + } + } + + + // Save per-notice metadata... + $mentions = array(); + $groups = array(); + + // This event lets plugins filter out non-local recipients (attentions we don't care about) + // Used primarily for OStatus (and if we don't federate, all attentions would be local anyway) + Event::handle('GetLocalAttentions', array($actor, $act->context->attention, &$mentions, &$groups)); + + if (!empty($mentions)) { + $stored->saveKnownReplies($mentions); + } else { + $stored->saveReplies(); + } + + if (!empty($tags)) { + $stored->saveKnownTags($tags); + } else { + $stored->saveTags(); + } + + // Note: groups may save tags, so must be run after tags are saved + // to avoid errors on duplicates. + // Note: groups should always be set. + + $stored->saveKnownGroups($groups); + + if (!empty($urls)) { + $stored->saveKnownUrls($urls); + } else { + $stored->saveUrls(); + } + + if ($distribute) { + // Prepare inbox delivery, may be queued to background. + $stored->distribute(); + } + + return $stored; + } + + static public function figureOutScope(Profile $actor, array $groups, $scope=null) { + if (is_null($scope)) { + $scope = self::defaultScope(); + } + + // For private streams + try { + $user = $actor->getUser(); + // FIXME: We can't do bit comparison with == (Legacy StatusNet thing. Let's keep it for now.) + if ($user->private_stream && ($scope == Notice::PUBLIC_SCOPE || $scope == Notice::SITE_SCOPE)) { + $scope |= Notice::FOLLOWER_SCOPE; + } + } catch (NoSuchUserException $e) { + // TODO: Not a local user, so we don't know about scope preferences... yet! + } + + // Force the scope for private groups + foreach ($groups as $group_id) { + $group = User_group::staticGet('id', $group_id); + if ($group instanceof User_group) { + if ($group->force_scope) { + $scope |= Notice::GROUP_SCOPE; + break; + } + } + } + + return $scope; + } + function blowOnInsert($conversation = false) { $this->blowStream('profile:notice_ids:%d', $this->profile_id); diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index 42ee9a43ef..f799b4033d 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -1360,4 +1360,9 @@ class OStatusPlugin extends Plugin return true; } + + public function onGetLocalAttentions(Profile $actor, array $attention_uris, array &$mentions, array &$groups) + { + list($mentions, $groups) = Ostatus_profile::filterAttention($actor, $attention_uris); + } } diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 2bcb9b4071..3d550872d6 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -670,7 +670,7 @@ class Ostatus_profile extends Managed_DataObject if ($activity->context) { // TODO: context->attention list($options['groups'], $options['replies']) - = $this->filterAttention($oprofile, $activity->context->attention); + = self::filterAttention($oprofile->localProfile(), $activity->context->attention); // Maintain direct reply associations // @todo FIXME: What about conversation ID? @@ -839,7 +839,7 @@ class Ostatus_profile extends Managed_DataObject if ($activity->context) { // TODO: context->attention list($options['groups'], $options['replies']) - = $this->filterAttention($oprofile, $activity->context->attention); + = self::filterAttention($oprofile->localProfile(), $activity->context->attention); // Maintain direct reply associations // @todo FIXME: What about conversation ID? @@ -913,11 +913,11 @@ class Ostatus_profile extends Managed_DataObject /** * Filters a list of recipient ID URIs to just those for local delivery. - * @param Ostatus_profile local profile of sender + * @param Profile local profile of sender * @param array in/out &$attention_uris set of URIs, will be pruned on output * @return array of group IDs */ - protected function filterAttention($sender, array $attention) + static public function filterAttention(Profile $sender, array $attention) { common_log(LOG_DEBUG, "Original reply recipients: " . implode(', ', array_keys($attention))); $groups = array(); @@ -937,16 +937,11 @@ class Ostatus_profile extends Managed_DataObject if ($id) { $group = User_group::getKV('id', $id); if ($group instanceof User_group) { - try { - // Deliver to all members of this local group if allowed. - $profile = $sender->localProfile(); - if ($profile->isMember($group)) { - $groups[] = $group->id; - } else { - common_log(LOG_DEBUG, "Skipping reply to local group $group->nickname as sender $profile->id is not a member"); - } - } catch (NoProfileException $e) { - // Sender has no profile! Do some garbage collection, please. + // Deliver to all members of this local group if allowed. + if ($sender->isMember($group)) { + $groups[] = $group->id; + } else { + common_log(LOG_DEBUG, sprintf('Skipping reply to local group %s as sender %d is not a member', $group->getNickname(), $sender->id)); } continue; } else {