/** * Insert notifications for replies, mentions and repeats * * @return boolean hook flag */ function onStartNoticeDistribute($notice) { assert($notice->id > 0); // since we removed tests below // repeats if ($notice->isRepeat()) { $repeated_notice = Notice::getKV('id', $notice->repeat_of); if ($repeated_notice instanceof Notice) { $this->insertNotification($repeated_notice->profile_id, $notice->profile_id, 'repeat', $repeated_notice->id); // mark reply/mention-notifications as read if we're repeating to a notice we're notified about self::markNotificationAsSeen($repeated_notice->id, $notice->profile_id, 'mention'); self::markNotificationAsSeen($repeated_notice->id, $notice->profile_id, 'reply'); // (no other notifications repeats) return true; } } // don't add notifications for activity/non-post-verb notices if ($notice->source == 'activity' || !ActivityUtils::compareVerbs($notice->verb, array(ActivityVerb::POST))) { return true; } // mark reply/mention-notifications as read if we're replying to a notice we're notified about if ($notice->reply_to) { self::markNotificationAsSeen($notice->reply_to, $notice->profile_id, 'mention'); self::markNotificationAsSeen($notice->reply_to, $notice->profile_id, 'reply'); } // replies and mentions $reply_notification_to = false; // check for reply to insert in notifications if ($notice->reply_to) { try { $replyauthor = $notice->getParent()->getProfile(); $reply_notification_to = $replyauthor->id; $this->insertNotification($replyauthor->id, $notice->profile_id, 'reply', $notice->id); //} catch (NoParentNoticeException $e) { // TODO: catch this when everyone runs latest GNU social! // This is not a reply to something (has no parent) } catch (NoResultException $e) { // Parent author's profile not found! Complain louder? common_log(LOG_ERR, "Parent notice's author not found: " . $e->getMessage()); } } // check for mentions to insert in notifications $mentions = $notice->getReplies(); $sender = Profile::getKV($notice->profile_id); $all_mentioned_user_ids = array(); foreach ($mentions as $mentioned) { // no duplicate mentions if (in_array($mentioned, $all_mentioned_user_ids)) { continue; } $all_mentioned_user_ids[] = $mentioned; // only notify if mentioned user is not already notified for reply if ($reply_notification_to != $mentioned) { $this->insertNotification($mentioned, $notice->profile_id, 'mention', $notice->id); } } return true; }
protected function doActionPost(ManagedAction $action, $verb, Notice $target, Profile $scoped) { switch (true) { case ActivityUtils::compareVerbs($verb, array(ActivityVerb::FAVORITE, ActivityVerb::LIKE)): Fave::addNew($scoped, $target); break; case ActivityUtils::compareVerbs($verb, array(ActivityVerb::UNFAVORITE, ActivityVerb::UNLIKE)): Fave::removeEntry($scoped, $target); break; default: throw new ServerException('ActivityVerb POST not handled by plugin that was supposed to do it.'); } 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; }
/** * This is run before ->insert, so our task in this function is just to * delete if it is the delete verb. */ public function onStartNoticeSave(Notice $stored) { // DELETE is a bit special, we have to remove the existing entry and then // add a new one with the same URI in order to trigger the distribution. // (that's why we don't use $this->isMyNotice(...)) if (!ActivityUtils::compareVerbs($stored->verb, array(ActivityVerb::DELETE))) { return true; } try { $target = Notice::getByUri($stored->uri); } catch (NoResultException $e) { throw new AlreadyFulfilledException('Notice URI not found, so we have nothing to delete.'); } $actor = $stored->getProfile(); $owner = $target->getProfile(); if ($owner->hasRole(Profile_role::DELETED)) { // Don't bother with replacing notices if its author is being deleted. // The later "StoreActivityObject" will pick this up and execute // the deletion then. // (the "delete verb notice" is too new to ever pass through Notice::saveNew // which otherwise wouldn't execute the StoreActivityObject event) return true; } // Since the user deleting may not be the same as the notice's owner, // double-check this and also set the "re-stored" notice profile_id. if (!$actor->sameAs($owner) && !$actor->hasRight(Right::DELETEOTHERSNOTICE)) { throw new AuthorizationException(_('You are not allowed to delete another user\'s notice.')); } // We copy the identifying fields and replace the sensitive ones. //$stored->id = $target->id; // We can't copy this since DB_DataObject won't inject it anyway $props = array('uri', 'profile_id', 'conversation', 'reply_to', 'created', 'repeat_of', 'object_type', 'is_local', 'scope'); foreach ($props as $prop) { $stored->{$prop} = $target->{$prop}; } // Let's see if this has been deleted already. try { $deleted = Deleted_notice::getByKeys(['uri' => $stored->getUri()]); return $deleted; } catch (NoResultException $e) { $deleted = new Deleted_notice(); $deleted->id = $target->getID(); $deleted->profile_id = $actor->getID(); $deleted->uri = $stored->getUri(); $deleted->act_created = $stored->created; $deleted->created = common_sql_now(); // throws exception on error $result = $deleted->insert(); } // Now we delete the original notice, leaving the id and uri free. $target->delete(); return true; }
function _fromAtomEntry($entry, $feed) { $pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM); if (!empty($pubEl)) { $this->time = strtotime($pubEl->textContent); } else { // XXX technically an error; being liberal. Good idea...? $updateEl = $this->_child($entry, self::UPDATED, self::ATOM); if (!empty($updateEl)) { $this->time = strtotime($updateEl->textContent); } else { $this->time = null; } } $this->link = ActivityUtils::getPermalink($entry); $verbEl = $this->_child($entry, self::VERB); if (!empty($verbEl)) { $this->verb = trim($verbEl->textContent); } else { $this->verb = ActivityVerb::POST; // XXX: do other implied stuff here } // get immediate object children $objectEls = ActivityUtils::children($entry, self::OBJECT, self::SPEC); if (count($objectEls) > 0) { foreach ($objectEls as $objectEl) { // Special case for embedded activities $objectType = ActivityUtils::childContent($objectEl, self::OBJECTTYPE, self::SPEC); if (!empty($objectType) && $objectType == ActivityObject::ACTIVITY) { $this->objects[] = new Activity($objectEl); } else { $this->objects[] = new ActivityObject($objectEl); } } } else { // XXX: really? $this->objects[] = new ActivityObject($entry); } $actorEl = $this->_child($entry, self::ACTOR); if (!empty($actorEl)) { // Standalone <activity:actor> elements are a holdover from older // versions of ActivityStreams. Newer feeds should have this data // integrated straight into <atom:author>. $this->actor = new ActivityObject($actorEl); // Cliqset has bad actor IDs (just nickname of user). We // work around it by getting the author data and using its // id instead if (!preg_match('/^\\w+:/', $this->actor->id)) { $authorEl = ActivityUtils::child($entry, 'author'); if (!empty($authorEl)) { $authorObj = new ActivityObject($authorEl); $this->actor->id = $authorObj->id; } } } else { if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) { // An <atom:author> in the entry overrides any author info on // the surrounding feed. $this->actor = new ActivityObject($authorEl); } else { if (!empty($feed) && ($subjectEl = $this->_child($feed, self::SUBJECT))) { // Feed subject is used for things like groups. // Should actually possibly not be interpreted as an actor...? $this->actor = new ActivityObject($subjectEl); } else { if (!empty($feed) && ($authorEl = $this->_child($feed, self::AUTHOR, self::ATOM))) { // If there's no <atom:author> on the entry, it's safe to assume // the containing feed's authorship info applies. $this->actor = new ActivityObject($authorEl); } } } } $contextEl = $this->_child($entry, self::CONTEXT); if (!empty($contextEl)) { $this->context = new ActivityContext($contextEl); } else { $this->context = new ActivityContext($entry); } $targetEl = $this->_child($entry, self::TARGET); if (!empty($targetEl)) { $this->target = new ActivityObject($targetEl); } elseif (ActivityUtils::compareVerbs($this->verb, array(ActivityVerb::FAVORITE))) { // StatusNet didn't send a 'target' for their Favorite atom entries $this->target = clone $this->objects[0]; } $this->summary = ActivityUtils::childContent($entry, 'summary'); $this->id = ActivityUtils::childContent($entry, 'id'); $this->content = ActivityUtils::getContent($entry); $catEls = $entry->getElementsByTagNameNS(self::ATOM, 'category'); if ($catEls) { for ($i = 0; $i < $catEls->length; $i++) { $catEl = $catEls->item($i); $this->categories[] = new AtomCategory($catEl); } } foreach (ActivityUtils::getLinks($entry, 'enclosure') as $link) { $this->enclosures[] = $link->getAttribute('href'); } // From APP. Might be useful. $this->selfLink = ActivityUtils::getLink($entry, 'self', 'application/atom+xml'); $this->editLink = ActivityUtils::getLink($entry, 'edit', 'application/atom+xml'); }
/** * 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, get_called_class() . " 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::compareVerbs($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::compareVerbs($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 static function extendActivity(Notice $stored, Activity $act, Profile $scoped = null) { $target = self::getTargetFromStored($stored); // The following logic was copied from StatusNet's Activity plugin if (ActivityUtils::compareVerbs($target->verb, array(ActivityVerb::POST))) { // "I like the thing you posted" $act->objects = $target->asActivity()->objects; } else { // "I like that you did whatever you did" $act->target = $target->asActivityObject(); $act->objects = array(clone $act->target); } $act->context->replyToID = $target->getUri(); $act->context->replyToUrl = $target->getUrl(); $act->title = ActivityUtils::verbToTitle($act->verb); }