static function noticeCount($id) { $keypart = sprintf('conversation:notice_count:%d', $id); $cnt = self::cacheGet($keypart); if ($cnt !== false) { return $cnt; } $notice = new Notice(); $notice->conversation = $id; $notice->whereAddIn('verb', array(ActivityVerb::POST, ActivityUtils::resolveUri(ActivityVerb::POST, true)), $notice->columnType('verb')); $cnt = $notice->count(); self::cacheSet($keypart, $cnt); return $cnt; }
protected function saveObjectFromActivity(Activity $act, Notice $stored, array $options = array()) { assert($this->isMyActivity($act)); $stored->object_type = ActivityUtils::resolveUri($act->objects[0]->type); if (common_valid_http_url($act->objects[0]->link)) { $stored->url = $act->objects[0]->link; } // We don't have to do just about anything for a new, remote notice since the fields // are handled in the main Notice::saveActivity function. Such as content, attachments, // parent/conversation etc. // By returning true here instead of something that evaluates // to false, we show that we have processed everything properly. return true; }
/** * Action of the form * * @return string URL of the action */ function action() { return common_local_url('activityverb', array('id' => $this->notice->getID(), 'verb' => ActivityUtils::resolveUri(ActivityVerb::UNFAVORITE, true))); }
protected function doActionPreparation(ManagedAction $action, $verb, Notice $target, Profile $scoped) { if ($action->isPost()) { // The below tests are only for presenting to the user. POSTs which inflict // duplicate favorite entries are handled with AlreadyFulfilledException. return false; } $exists = Fave::existsForProfile($target, $scoped); $expected_verb = $exists ? ActivityVerb::UNFAVORITE : ActivityVerb::FAVORITE; switch (true) { case $exists && ActivityUtils::compareVerbs($verb, array(ActivityVerb::FAVORITE, ActivityVerb::LIKE)): case !$exists && ActivityUtils::compareVerbs($verb, array(ActivityVerb::UNFAVORITE, ActivityVerb::UNLIKE)): common_redirect(common_local_url('activityverb', array('id' => $target->getID(), 'verb' => ActivityUtils::resolveUri($expected_verb, true)))); break; default: // No need to redirect as we are on the correct action already. } return false; }
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 = null; if (!empty($act->id)) { // implied object $options['uri'] = $act->id; $options['url'] = $act->link; } else { $actobj = count($act->objects) == 1 ? $act->objects[0] : null; if (!is_null($actobj) && !empty($actobj->id)) { $options['uri'] = $actobj->id; if ($actobj->link) { $options['url'] = $actobj->link; } elseif (preg_match('!^https?://!', $actobj->id)) { $options['url'] = $actobj->id; } } } $defaults = array('groups' => array(), 'is_local' => $actor->isLocal() ? self::LOCAL_PUBLIC : self::REMOTE, '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); // dupe check $stored = new Notice(); if (!empty($uri) && !ActivityUtils::compareVerbs($act->verb, array(ActivityVerb::DELETE))) { $stored->uri = $uri; if ($stored->find()) { common_debug('cannot create duplicate Notice URI: ' . $stored->uri); // I _assume_ saving a Notice with a colliding URI means we're really trying to // save the same notice again... throw new AlreadyFulfilledException('Notice URI already exists'); } } $autosource = common_config('public', 'autosource'); // Sandboxed are non-false, but not 1, either if (!$actor->hasRight(Right::PUBLICNOTICE) || $source && $autosource && in_array($source, $autosource)) { // FIXME: ...what about remote nonpublic? Hmmm. That is, if we sandbox remote profiles... $stored->is_local = Notice::LOCAL_NONPUBLIC; } else { $stored->is_local = intval($is_local); } if (!$stored->isLocal()) { // Only do these checks for non-local notices. Local notices will generate these values later. if (!common_valid_http_url($url)) { common_debug('Bad notice URL: [' . $url . '], URI: [' . $uri . ']. Cannot link back to original! This is normal for shared notices etc.'); } if (empty($uri)) { throw new ServerException('No URI for remote notice. Cannot accept that.'); } } $stored->profile_id = $actor->id; $stored->source = $source; $stored->uri = $uri; $stored->url = $url; $stored->verb = $act->verb; // Notice content. We trust local users to provide HTML we like, but of course not remote users. // FIXME: What about local users importing feeds? Mirror functions must filter out bad HTML first... $content = $act->content ?: $act->summary; if (is_null($content) && !is_null($actobj)) { $content = $actobj->content ?: $actobj->summary; } $stored->rendered = $actor->isLocal() ? $content : common_purify($content); // yeah, just don't use getRendered() here since it's not inserted yet ;) $stored->content = common_strip_html($stored->rendered); // 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) { $replyGroups = $reply->getGroups(); foreach ($replyGroups as $group) { if ($actor->isMember($group)) { $groups[] = $group->id; } } } if (is_null($scope)) { $scope = $reply->scope; } } else { // If we don't know the reply, we might know the conversation! // This will happen if a known remote user replies to an // unknown remote user - within a known conversation. if (empty($stored->conversation) and !empty($act->context->conversation)) { $conv = Conversation::getKV('uri', $act->context->conversation); if ($conv instanceof Conversation) { common_debug('Conversation stitched together from (probably) a reply activity to unknown remote user. Activity creation time (' . $stored->created . ') should maybe be compared to conversation creation time (' . $conv->created . ').'); } else { // Conversation entry with specified URI was not found, so we must create it. common_debug('Conversation URI not found, so we will create it with the URI given in the context of the activity: ' . $act->context->conversation); // The insert in Conversation::create throws exception on failure $conv = Conversation::create($act->context->conversation, $stored->created); } $stored->conversation = $conv->getID(); unset($conv); } } // If it's not part of a conversation, it's the beginning of a new conversation. if (empty($stored->conversation)) { $conv = Conversation::create(); $stored->conversation = $conv->getID(); unset($conv); } $notloc = null; if ($act->context instanceof ActivityContext) { if ($act->context->location instanceof Location) { $notloc = Notice_location::fromLocation($act->context->location); } } 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 (ActivityUtils::compareVerbs($stored->verb, array(ActivityVerb::POST))) { if (empty($act->objects[0]->type)) { // Default type for the post verb is 'note', but we know it's // a 'comment' if it is in reply to something. $stored->object_type = empty($stored->reply_to) ? ActivityObject::NOTE : ActivityObject::COMMENT; } else { //TODO: Is it safe to always return a relative URI? The // JSON version of ActivityStreams always use it, so we // should definitely be able to handle it... $stored->object_type = ActivityUtils::resolveUri($act->objects[0]->type, true); } } if (Event::handle('StartNoticeSave', array(&$stored))) { // XXX: some of these functions write to the DB try { $result = $stored->insert(); // throws exception on error if ($notloc instanceof Notice_location) { $notloc->notice_id = $stored->getID(); $notloc->insert(); } $orig = clone $stored; // for updating later in this try clause $object = null; Event::handle('StoreActivityObject', array($act, $stored, $options, &$object)); if (empty($object)) { throw new ServerException('Unsuccessful call to StoreActivityObject ' . $stored->getUri() . ': ' . $act->asString()); } // If something changed in the Notice during StoreActivityObject $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; } } if (!$stored instanceof Notice) { throw new ServerException('StartNoticeSave did not give back a Notice'); } // Only save 'attention' and metadata stuff (URLs, tags...) stuff if // the activityverb is a POST (since stuff like repeat, favorite etc. // reasonably handle notifications themselves. if (ActivityUtils::compareVerbs($stored->verb, array(ActivityVerb::POST))) { 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. $stored->saveAttentions($act->context->attention); if (!empty($urls)) { $stored->saveKnownUrls($urls); } else { $stored->saveUrls(); } } if ($distribute) { // Prepare inbox delivery, may be queued to background. $stored->distribute(); } return $stored; }
static function canonicalType($type) { return ActivityUtils::resolveUri($type, true); }
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; }
public function __construct() { $this->selectVerbs = array(ActivityVerb::POST, ActivityUtils::resolveUri(ActivityVerb::POST, true)); $this->unselectVerbs = array(ActivityVerb::DELETE); }
static function newUri(Profile $actor, Managed_DataObject $object, $created = null) { if (is_null($created)) { $created = common_sql_now(); } return TagURI::mint(strtolower(get_called_class()) . ':%d:%s:%d:%s', $actor->getID(), ActivityUtils::resolveUri($object->getObjectType(), true), $object->getID(), common_date_iso8601($created)); }
public function asActivityObject(Profile $scoped = null) { $actobj = new ActivityObject(); $actobj->id = $this->getUri(); $actobj->type = ActivityUtils::resolveUri(self::getObjectType()); $actobj->actor = $this->getActorObject(); $actobj->target = $this->getTargetObject(); $actobj->objects = array(clone $actobj->target); $actobj->verb = ActivityVerb::FAVORITE; $actobj->title = ActivityUtils::verbToTitle($actobj->verb); $actobj->content = $this->getTarget()->getRendered(); return $actobj; }