X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=classes%2FNotice.php;h=c631c1fcc6768dd690bb449c8115096c1806f644;hb=1a1b9928584ec9496f1c7c85d8b8283b0d7e8cfc;hp=d3c56bac542f5c591814454564c53b490f41f1da;hpb=f99c4b7f079ab8c6580d4ab4cfd8a777c4a23cf8;p=quix0rs-gnu-social.git diff --git a/classes/Notice.php b/classes/Notice.php index d3c56bac54..c631c1fcc6 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -34,14 +34,11 @@ * @license GNU Affero General Public License http://www.gnu.org/licenses/ */ -if (!defined('STATUSNET') && !defined('LACONICA')) { - exit(1); -} +if (!defined('GNUSOCIAL')) { exit(1); } /** * Table Definition for notice */ -require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; /* We keep 200 notices, the max number of notices available per API request, * in the memcached cache. */ @@ -58,10 +55,10 @@ class Notice extends Managed_DataObject public $__table = 'notice'; // table name public $id; // int(4) primary_key not_null public $profile_id; // int(4) multiple_key not_null - public $uri; // varchar(255) unique_key + public $uri; // varchar(191) unique_key not 255 because utf8mb4 takes more space public $content; // text public $rendered; // text - public $url; // varchar(255) + public $url; // varchar(191) not 255 because utf8mb4 takes more space public $created; // datetime multiple_key not_null default_0000-00-00%2000%3A00%3A00 public $modified; // timestamp not_null default_CURRENT_TIMESTAMP public $reply_to; // int(4) @@ -73,8 +70,8 @@ class Notice extends Managed_DataObject public $location_id; // int(4) public $location_ns; // int(4) public $repeat_of; // int(4) - public $verb; // varchar(255) - public $object_type; // varchar(255) + public $verb; // varchar(191) not 255 because utf8mb4 takes more space + public $object_type; // varchar(191) not 255 because utf8mb4 takes more space public $scope; // int(4) /* the code above is auto generated do not remove the tag below */ @@ -86,10 +83,10 @@ class Notice extends Managed_DataObject 'fields' => array( 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'), 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'who made the update'), - 'uri' => array('type' => 'varchar', 'length' => 255, 'description' => 'universally unique identifier, usually a tag URI'), + 'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier, usually a tag URI'), 'content' => array('type' => 'text', 'description' => 'update content', 'collate' => 'utf8_general_ci'), 'rendered' => array('type' => 'text', 'description' => 'HTML version of the content'), - 'url' => array('type' => 'varchar', 'length' => 255, 'description' => 'URL of any attachment (image, video, bookmark, whatever)'), + 'url' => array('type' => 'varchar', 'length' => 191, 'description' => 'URL of any attachment (image, video, bookmark, whatever)'), 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), 'reply_to' => array('type' => 'int', 'description' => 'notice replied to (usually a guess)'), @@ -101,8 +98,8 @@ class Notice extends Managed_DataObject 'location_id' => array('type' => 'int', 'description' => 'location id if possible'), 'location_ns' => array('type' => 'int', 'description' => 'namespace for location'), 'repeat_of' => array('type' => 'int', 'description' => 'notice this is a repeat of'), - 'object_type' => array('type' => 'varchar', 'length' => 255, 'description' => 'URI representing activity streams object type', 'default' => 'http://activitystrea.ms/schema/1.0/note'), - 'verb' => array('type' => 'varchar', 'length' => 255, 'description' => 'URI representing activity streams verb', 'default' => 'http://activitystrea.ms/schema/1.0/post'), + 'object_type' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI representing activity streams object type', 'default' => 'http://activitystrea.ms/schema/1.0/note'), + 'verb' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI representing activity streams verb', 'default' => 'http://activitystrea.ms/schema/1.0/post'), 'scope' => array('type' => 'int', 'description' => 'bit map for distribution scope; 0 = everywhere; 1 = this server only; 2 = addressees; 4 = followers; null = default'), ), @@ -131,7 +128,7 @@ class Notice extends Managed_DataObject return $def; } - + /* Notice types */ const LOCAL_PUBLIC = 1; const REMOTE = 0; @@ -144,25 +141,32 @@ class Notice extends Managed_DataObject const GROUP_SCOPE = 4; const FOLLOWER_SCOPE = 8; - protected $_profile = -1; + protected $_profile = array(); + /** + * Will always return a profile, if anything fails it will + * (through _setProfile) throw a NoProfileException. + */ public function getProfile() { - if ($this->_profile === -1) { - $this->_setProfile(Profile::getKV('id', $this->profile_id)); + if (!isset($this->_profile[$this->profile_id])) { + // We could've sent getKV directly to _setProfile, but occasionally we get + // a "false" (instead of null), likely because it indicates a cache miss. + $profile = Profile::getKV('id', $this->profile_id); + $this->_setProfile($profile instanceof Profile ? $profile : null); } - if (!$this->_profile instanceof Profile) { - throw new NoProfileException($this->profile_id); - } - return $this->_profile; + return $this->_profile[$this->profile_id]; } - - function _setProfile(Profile $profile) + + public function _setProfile(Profile $profile=null) { - $this->_profile = $profile; + if (!$profile instanceof Profile) { + throw new NoProfileException($this->profile_id); + } + $this->_profile[$this->profile_id] = $profile; } - function delete() + function delete($useWhere=false) { // For auditing purposes, save a record that the notice // was deleted. @@ -171,11 +175,11 @@ class Notice extends Managed_DataObject // insert fails. $deleted = Deleted_notice::getKV('id', $this->id); - if (!$deleted) { + if (!$deleted instanceof Deleted_notice) { $deleted = Deleted_notice::getKV('uri', $this->uri); } - if (!$deleted) { + if (!$deleted instanceof Deleted_notice) { $deleted = new Deleted_notice(); $deleted->id = $this->id; @@ -193,16 +197,15 @@ class Notice extends Managed_DataObject $this->clearReplies(); $this->clearRepeats(); - $this->clearFaves(); $this->clearTags(); $this->clearGroupInboxes(); $this->clearFiles(); + $this->clearAttentions(); - // NOTE: we don't clear inboxes // NOTE: we don't clear queue items } - $result = parent::delete(); + $result = parent::delete($useWhere); $this->blowOnDelete(); return $result; @@ -213,10 +216,101 @@ class Notice extends Managed_DataObject return $this->uri; } - public function getUrl() + /* + * Get a Notice object by URI. Will call external plugins for help + * using the event StartGetNoticeFromURI. + * + * @param string $uri A unique identifier for a resource (notice in this case) + */ + static function fromUri($uri) + { + $notice = null; + + if (Event::handle('StartGetNoticeFromUri', array($uri, &$notice))) { + $notice = Notice::getKV('uri', $uri); + Event::handle('EndGetNoticeFromUri', array($uri, $notice)); + } + + if (!$notice instanceof Notice) { + throw new UnknownUriException($uri); + } + + return $notice; + } + + /* + * @param $root boolean If true, link to just the conversation root. + * + * @return URL to conversation + */ + public function getConversationUrl($anchor=true) + { + return Conversation::getUrlFromNotice($this, $anchor); + } + + /* + * Get the local representation URL of this notice. + */ + public function getLocalUrl() + { + return common_local_url('shownotice', array('notice' => $this->id), null, null, false); + } + + public function getTitle() + { + $title = null; + if (Event::handle('GetNoticeTitle', array($this, &$title))) { + // TRANS: Title of a notice posted without a title value. + // TRANS: %1$s is a user name, %2$s is the notice creation date/time. + $title = sprintf(_('%1$s\'s status on %2$s'), + $this->getProfile()->getFancyName(), + common_exact_date($this->created)); + } + return $title; + } + + public function getContent() + { + return $this->content; + } + + /* + * Get the original representation URL of this notice. + * + * @param boolean $fallback Whether to fall back to generate a local URL or throw InvalidUrlException + */ + public function getUrl($fallback=false) { // The risk is we start having empty urls and non-http uris... - return $this->url ?: $this->uri; + // and we can't really handle any other protocol right now. + switch (true) { + case common_valid_http_url($this->url): // should we allow non-http/https URLs? + return $this->url; + case !$this->isLocal() && common_valid_http_url($this->uri): // Sometimes we only have the URI for remote posts. + return $this->uri; + case $this->isLocal() || $fallback: + // let's generate a valid link to our locally available notice on demand + return common_local_url('shownotice', array('notice' => $this->id), null, null, false); + default: + common_debug('No URL available for notice: id='.$this->id); + throw new InvalidUrlException($this->url); + } + } + + public function get_object_type($canonical=false) { + return $canonical + ? ActivityObject::canonicalType($this->object_type) + : $this->object_type; + } + + public static function getByUri($uri) + { + $notice = new Notice(); + $notice->uri = $uri; + if (!$notice->find(true)) { + throw new NoResultException($notice); + } + return $notice; } /** @@ -301,7 +395,7 @@ class Notice extends Managed_DataObject * int 'location_ns' geoname namespace to interpret location_id * int 'reply_to'; notice ID this is a reply to * int 'repeat_of'; notice ID this is a repeat of - * string 'uri' unique ID for notice; defaults to local notice URL + * string 'uri' unique ID for notice; a unique tag uri (can be url or anything too) * string 'url' permalink to notice; defaults to local notice URL * string 'rendered' rendered HTML version of content * array 'replies' list of profile URIs for reply delivery in @@ -325,8 +419,9 @@ class Notice extends Managed_DataObject static function saveNew($profile_id, $content, $source, array $options=null) { $defaults = array('uri' => null, 'url' => null, - 'reply_to' => null, - 'repeat_of' => null, + 'conversation' => null, // URI of conversation + 'reply_to' => null, // This will override convo URI if the parent is known + 'repeat_of' => null, // This will override convo URI if the repeated notice is known 'scope' => null, 'distribute' => true, 'object_type' => null, @@ -403,6 +498,16 @@ class Notice extends Managed_DataObject $notice->created = common_sql_now(); } + if (!$notice->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.'); + } + } + $notice->content = $final; $notice->source = $source; @@ -410,9 +515,8 @@ class Notice extends Managed_DataObject $notice->url = $url; // Get the groups here so we can figure out replies and such - if (!isset($groups)) { - $groups = self::groupsFromText($notice->content, $profile); + $groups = User_group::idsFromText($notice->content, $profile); } $reply = null; @@ -452,12 +556,22 @@ class Notice extends Managed_DataObject throw new ClientException(_('You already repeated that notice.')); } - $notice->repeat_of = $repeat_of; + $notice->repeat_of = $repeat->id; + $notice->conversation = $repeat->conversation; } else { - $reply = self::getReplyTo($reply_to, $profile_id, $source, $final); + $reply = null; - if (!empty($reply)) { + // If $reply_to is specified, we check that it exists, and then + // return it if it does + if (!empty($reply_to)) { + $reply = Notice::getKV('id', $reply_to); + } elseif (in_array($source, array('xmpp', 'mail', 'sms'))) { + // If the source lacks capability of sending the "reply_to" + // metadata, let's try to find an inline replyto-reference. + $reply = self::getInlineReplyTo($profile, $final); + } + if ($reply instanceof Notice) { if (!$reply->inScope($profile)) { // 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). @@ -465,11 +579,17 @@ class Notice extends Managed_DataObject $profile->nickname, $reply->id), 403); } - $notice->reply_to = $reply->id; + // If it's a repeat, the reply_to should be to the original + if ($reply->isRepeat()) { + $notice->reply_to = $reply->repeat_of; + } else { + $notice->reply_to = $reply->id; + } + // But the conversation ought to be the same :) $notice->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 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(); @@ -483,6 +603,26 @@ class Notice extends Managed_DataObject // Scope set below } + + // 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($notice->conversation) and !empty($options['conversation'])) { + $conv = Conversation::getKV('uri', $options['conversation']); + if ($conv instanceof Conversation) { + common_debug('Conversation stitched together from (probably) reply to unknown remote user. Activity creation time ('.$notice->created.') should maybe be compared to conversation creation time ('.$conv->created.').'); + $notice->conversation = $conv->id; + } else { + // Conversation URI was not found, so we must create it. But we can't create it + // until we have a Notice ID because of the database layout... + $notice->tmp_conv_uri = $options['conversation']; + } + } else { + // If we're not using the attached conversation URI let's remove it + // so we don't mistake ourselves later, when creating our own Conversation. + // This implies that the notice knows which conversation it belongs to. + $options['conversation'] = null; + } } if (!empty($lat) && !empty($lon)) { @@ -502,7 +642,7 @@ class Notice extends Managed_DataObject } if (empty($verb)) { - if (!empty($notice->repeat_of)) { + if ($notice->isRepeat()) { $notice->verb = ActivityVerb::SHARE; $notice->object_type = ActivityObject::ACTIVITY; } else { @@ -518,121 +658,337 @@ 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 + $notice->scope = self::figureOutScope($profile, $groups, $notice->scope); - try { - $user = $profile->getUser(); + if (Event::handle('StartNoticeSave', array(&$notice))) { + + // XXX: some of these functions write to the DB - if ($user->private_stream && - ($notice->scope == Notice::PUBLIC_SCOPE || - $notice->scope == Notice::SITE_SCOPE)) { - $notice->scope |= Notice::FOLLOWER_SCOPE; + try { + $notice->insert(); // throws exception on failure + // If it's not part of a conversation, it's + // the beginning of a new conversation. + if (empty($notice->conversation)) { + $orig = clone($notice); + // $act->context->conversation will be null if it was not provided + $conv = Conversation::create($notice, $options['conversation']); + $notice->conversation = $conv->id; + $notice->update($orig); + } + } catch (Exception $e) { + // Let's test if we managed initial insert, which would imply + // failing on some update-part (check 'insert()'). Delete if + // something had been stored to the database. + if (!empty($notice->id)) { + $notice->delete(); + } + throw $e; } - } catch (NoSuchUserException $e) { - // Cannot handle private streams for remote profiles } - // Force the scope for private groups + // Clear the cache for subscribed users, so they'll update at next request + // XXX: someone clever could prepend instead of clearing the cache - 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; + // Save per-notice metadata... + + if (isset($replies)) { + $notice->saveKnownReplies($replies); + } else { + $notice->saveReplies(); + } + + if (isset($tags)) { + $notice->saveKnownTags($tags); + } else { + $notice->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. + + $notice->saveKnownGroups($groups); + + if (isset($urls)) { + $notice->saveKnownUrls($urls); + } else { + $notice->saveUrls(); + } + + if ($distribute) { + // Prepare inbox delivery, may be queued to background. + $notice->distribute(); + } + + 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 + 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; } } } - if (Event::handle('StartNoticeSave', array(&$notice))) { - - // XXX: some of these functions write to the DB + $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); - $id = $notice->insert(); + // 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); - if (!$id) { - common_log_db_error($notice, 'INSERT', __FILE__); - // TRANS: Server exception thrown when a notice cannot be saved. - throw new ServerException(_('Problem saving notice.')); + $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'); } + } - // Update ID-dependent columns: URI, conversation + $stored->profile_id = $actor->id; + $stored->source = $source; + $stored->uri = $uri; + $stored->url = $url; + $stored->verb = $act->verb; - $orig = clone($notice); + // Use the local user's shortening preferences, if applicable. + $stored->rendered = $actor->isLocal() + ? $actor->shortenLinks($act->content) + : $act->content; + $stored->content = common_strip_html($stored->rendered); - $changed = false; + $autosource = common_config('public', 'autosource'); - if (empty($uri)) { - $notice->uri = common_notice_uri($notice); - $changed = true; + // 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 it's not part of a conversation, it's - // the beginning of a new conversation. + if (is_null($scope)) { + $scope = $reply->scope; + } + } - if (empty($notice->conversation)) { - $conv = Conversation::create(); - $notice->conversation = $conv->id; - $changed = true; + 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); - if ($changed) { - if (!$notice->update($orig)) { - common_log_db_error($notice, 'UPDATE', __FILE__); - // TRANS: Server exception thrown when a notice cannot be updated. - throw new ServerException(_('Problem saving notice.')); + 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; } - // Clear the cache for subscribed users, so they'll update at next request - // XXX: someone clever could prepend instead of clearing the cache + if (Event::handle('StartNoticeSave', array(&$stored))) { + // XXX: some of these functions write to the DB - $notice->blowOnInsert(); + try { + $stored->insert(); // throws exception on error + $orig = clone($stored); // for updating later in this try clause + + // If it's not part of a conversation, it's + // the beginning of a new conversation. + if (empty($stored->conversation)) { + // $act->context->conversation will be null if it was not provided + $conv = Conversation::create($stored, $act->context->conversation); + $stored->conversation = $conv->id; + } + + $object = null; + Event::handle('StoreActivityObject', array($act, $stored, $options, &$object)); + if (empty($object)) { + throw new ServerException('No object from StoreActivityObject '.$stored->uri . ': '.$act->asString()); + } + $stored->object_type = ActivityUtils::resolveUri($object->getObjectType(), 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; + } + } + if (!$stored instanceof Notice) { + throw new ServerException('StartNoticeSave did not give back a Notice'); + } // Save per-notice metadata... + $mentions = array(); + $groups = array(); - if (isset($replies)) { - $notice->saveKnownReplies($replies); + // 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 { - $notice->saveReplies(); + $stored->saveReplies(); } - if (isset($tags)) { - $notice->saveKnownTags($tags); + if (!empty($tags)) { + $stored->saveKnownTags($tags); } else { - $notice->saveTags(); + $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. - $notice->saveKnownGroups($groups); + $stored->saveKnownGroups($groups); - if (isset($urls)) { - $notice->saveKnownUrls($urls); + if (!empty($urls)) { + $stored->saveKnownUrls($urls); } else { - $notice->saveUrls(); + $stored->saveUrls(); } if ($distribute) { // Prepare inbox delivery, may be queued to background. - $notice->distribute(); + $stored->distribute(); } - return $notice; + 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) @@ -641,12 +997,13 @@ class Notice extends Managed_DataObject if ($this->isPublic()) { $this->blowStream('public'); + $this->blowStream('networkpublic'); } self::blow('notice:list-ids:conversation:%s', $this->conversation); self::blow('conversation:notice_count:%d', $this->conversation); - if (!empty($this->repeat_of)) { + if ($this->isRepeat()) { // XXX: we should probably only use one of these $this->blowStream('notice:repeats:%d', $this->repeat_of); self::blow('notice:list-ids:repeat_of:%d', $this->repeat_of); @@ -685,6 +1042,7 @@ class Notice extends Managed_DataObject if ($this->isPublic()) { self::blow('public;last'); + self::blow('networkpublic;last'); } self::blow('fave:by_notice', $this->id); @@ -709,13 +1067,9 @@ class Notice extends Managed_DataObject } $args = func_get_args(); - $format = array_shift($args); - $keyPart = vsprintf($format, $args); - $cacheKey = Cache::key($keyPart); - $c->delete($cacheKey); // delete the "last" stream, too, if this notice is @@ -762,7 +1116,11 @@ class Notice extends Managed_DataObject if (common_config('attachments', 'process_links')) { // @fixme validation? foreach (array_unique($urls) as $url) { - File::processNew($url, $this->id); + try { + File::processNew($url, $this->id); + } catch (ServerException $e) { + // Could not save URL. Log it? + } } } } @@ -771,7 +1129,11 @@ class Notice extends Managed_DataObject * @private callback */ function saveUrl($url, $notice_id) { - File::processNew($url, $notice_id); + try { + File::processNew($url, $notice_id); + } catch (ServerException $e) { + // Could not save URL. Log it? + } } static function checkDupes($profile_id, $content) { @@ -820,32 +1182,27 @@ class Notice extends Managed_DataObject return true; } - protected $_attachments = -1; - - function attachments() { + protected $_attachments = array(); - if ($this->_attachments != -1) { - return $this->_attachments; + function attachments() { + if (isset($this->_attachments[$this->id])) { + return $this->_attachments[$this->id]; } - + $f2ps = File_to_post::listGet('post_id', array($this->id)); - $ids = array(); - foreach ($f2ps[$this->id] as $f2p) { - $ids[] = $f2p->file_id; + $ids[] = $f2p->file_id; } - - $files = File::multiGet('id', $ids); - $this->_attachments = $files->fetchAll(); - - return $this->_attachments; + $files = File::multiGet('id', $ids); + $this->_attachments[$this->id] = $files->fetchAll(); + return $this->_attachments[$this->id]; } function _setAttachments($attachments) { - $this->_attachments = $attachments; + $this->_attachments[$this->id] = $attachments; } function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0) @@ -910,12 +1267,18 @@ class Notice extends Managed_DataObject } // If this isn't a reply to anything, then it's its own - // root. + // root if it's the earliest notice in the conversation: if (empty($this->reply_to)) { - return $this; + $root = new Notice; + $root->conversation = $this->conversation; + $root->orderBy('notice.created ASC'); + $root->find(); + $root->fetch(); + $root->free(); + return $root; } - + if (is_null($profile)) { $keypart = sprintf('notice:conversation_root:%d:null', $this->id); } else { @@ -923,7 +1286,7 @@ class Notice extends Managed_DataObject $this->id, $profile->id); } - + $root = self::cacheGet($keypart); if ($root !== false && $root->inScope($profile)) { @@ -973,30 +1336,23 @@ class Notice extends Managed_DataObject } } - if (is_null($groups)) { - $groups = $this->getGroups(); - } - if (is_null($recipients)) { $recipients = $this->getReplies(); } - $users = $this->getSubscribedUsers(); - $ptags = $this->getProfileTags(); - - // FIXME: kind of ignoring 'transitional'... - // we'll probably stop supporting inboxless mode - // in 0.9.x - $ni = array(); // Give plugins a chance to add folks in at start... if (Event::handle('StartNoticeWhoGets', array($this, &$ni))) { + $users = $this->getSubscribedUsers(); foreach ($users as $id) { $ni[$id] = NOTICE_INBOX_SOURCE_SUB; } + if (is_null($groups)) { + $groups = $this->getGroups(); + } foreach ($groups as $group) { $users = $group->getUserMembers(); foreach ($users as $id) { @@ -1006,12 +1362,10 @@ class Notice extends Managed_DataObject } } - foreach ($ptags as $ptag) { - $users = $ptag->getUserSubscribers(); - foreach ($users as $id) { - if (!array_key_exists($id, $ni)) { - $ni[$id] = NOTICE_INBOX_SOURCE_PROFILE_TAG; - } + $ptAtts = $this->getAttentionsFromProfileTags(); + foreach ($ptAtts as $key=>$val) { + if (!array_key_exists($key, $ni)) { + $ni[$key] = $val; } } @@ -1024,7 +1378,7 @@ class Notice extends Managed_DataObject // Exclude any deleted, non-local, or blocking recipients. $profile = $this->getProfile(); $originalProfile = null; - if ($this->repeat_of) { + if ($this->isRepeat()) { // Check blocks against the original notice's poster as well. $original = Notice::getKV('id', $this->repeat_of); if ($original instanceof Notice) { @@ -1058,43 +1412,6 @@ class Notice extends Managed_DataObject return $ni; } - /** - * Adds this notice to the inboxes of each local user who should receive - * it, based on author subscriptions, group memberships, and @-replies. - * - * Warning: running a second time currently will make items appear - * multiple times in users' inboxes. - * - * @fixme make more robust against errors - * @fixme break up massive deliveries to smaller background tasks - * - * @param array $groups optional list of Group objects; - * if left empty, will be loaded from group_inbox records - * @param array $recipient optional list of reply profile ids - * if left empty, will be loaded from reply records - */ - function addToInboxes(array $groups=null, array $recipients=null) - { - $ni = $this->whoGets($groups, $recipients); - - $ids = array_keys($ni); - - // We remove the author (if they're a local user), - // since we'll have already done this in distribute() - - $i = array_search($this->profile_id, $ids); - - if ($i !== false) { - unset($ids[$i]); - } - - // Bulk insert - - Inbox::bulkInsert($this->id, $ids); - - return; - } - function getSubscribedUsers() { $user = new User(); @@ -1135,6 +1452,19 @@ class Notice extends Managed_DataObject return $ptags; } + public function getAttentionsFromProfileTags() + { + $ni = array(); + $ptags = $this->getProfileTags(); + foreach ($ptags as $ptag) { + $users = $ptag->getUserSubscribers(); + foreach ($users as $id) { + $ni[$id] = NOTICE_INBOX_SOURCE_PROFILE_TAG; + } + } + return $ni; + } + /** * Record this notice to the given group inboxes for delivery. * Overrides the regular parsing of !group markup. @@ -1154,8 +1484,8 @@ class Notice extends Managed_DataObject $groups = array(); foreach (array_unique($group_ids) as $id) { $group = User_group::getKV('id', $id); - if ($group) { - common_log(LOG_ERR, "Local delivery to group id $id, $group->nickname"); + if ($group instanceof User_group) { + common_log(LOG_DEBUG, "Local delivery to group id $id, $group->nickname"); $result = $this->addToGroupInbox($group); if (!$result) { common_log_db_error($gi, 'INSERT', __FILE__); @@ -1181,48 +1511,7 @@ class Notice extends Managed_DataObject return $groups; } - /** - * Parse !group delivery and record targets into group_inbox. - * @return array of Group objects - */ - function saveGroups() - { - // Don't save groups for repeats - - if (!empty($this->repeat_of)) { - return array(); - } - - $profile = $this->getProfile(); - - $groups = self::groupsFromText($this->content, $profile); - - /* Add them to the database */ - - foreach ($groups as $group) { - /* XXX: remote groups. */ - - if (empty($group)) { - continue; - } - - - if ($profile->isMember($group)) { - - $result = $this->addToGroupInbox($group); - - if (!$result) { - common_log_db_error($gi, 'INSERT', __FILE__); - } - - $groups[] = clone($group); - } - } - - return $groups; - } - - function addToGroupInbox($group) + function addToGroupInbox(User_group $group) { $gi = Group_inbox::pkeyGet(array('group_id' => $group->id, 'notice_id' => $this->id)); @@ -1270,10 +1559,9 @@ class Notice extends Managed_DataObject $sender = Profile::getKV($this->profile_id); foreach (array_unique($uris) as $uri) { - - $profile = Profile::fromURI($uri); - - if (!$profile instanceof Profile) { + try { + $profile = Profile::fromUri($uri); + } catch (UnknownUriException $e) { common_log(LOG_WARNING, "Unable to determine profile for URI '$uri'"); continue; } @@ -1304,23 +1592,21 @@ class Notice extends Managed_DataObject { // Don't save reply data for repeats - if (!empty($this->repeat_of)) { + if ($this->isRepeat()) { return array(); } - $sender = Profile::getKV($this->profile_id); + $sender = $this->getProfile(); $replied = array(); // If it's a reply, save for the replied-to author try { $parent = $this->getParent(); - $author = $parent->getProfile(); - if ($author instanceof Profile) { - $this->saveReply($author->id); - $replied[$author->id] = 1; - self::blow('reply:stream:%d', $author->id); - } + $parentauthor = $parent->getProfile(); + $this->saveReply($parentauthor->id); + $replied[$parentauthor->id] = 1; + self::blow('reply:stream:%d', $parentauthor->id); } catch (Exception $e) { // Not a reply, since it has no parent! } @@ -1374,7 +1660,7 @@ class Notice extends Managed_DataObject return $reply; } - protected $_replies = -1; + protected $_replies = array(); /** * Pull the complete list of @-reply targets for this notice. @@ -1383,8 +1669,8 @@ class Notice extends Managed_DataObject */ function getReplies() { - if ($this->_replies != -1) { - return $this->_replies; + if (isset($this->_replies[$this->id])) { + return $this->_replies[$this->id]; } $replyMap = Reply::listGet('notice_id', array($this->id)); @@ -1395,14 +1681,14 @@ class Notice extends Managed_DataObject $ids[] = $reply->profile_id; } - $this->_replies = $ids; + $this->_replies[$this->id] = $ids; return $ids; } function _setReplies($replies) { - $this->_replies = $replies; + $this->_replies[$this->id] = $replies; } /** @@ -1413,9 +1699,9 @@ class Notice extends Managed_DataObject function getReplyProfiles() { $ids = $this->getReplies(); - + $profiles = Profile::multiGet('id', $ids); - + return $profiles->fetchAll(); } @@ -1429,29 +1715,33 @@ class Notice extends Managed_DataObject { // Don't send reply notifications for repeats - if (!empty($this->repeat_of)) { + if ($this->isRepeat()) { return array(); } $recipientIds = $this->getReplies(); + if (Event::handle('StartNotifyMentioned', array($this, &$recipientIds))) { + require_once INSTALLDIR.'/lib/mail.php'; - foreach ($recipientIds as $recipientId) { - $user = User::getKV('id', $recipientId); - if ($user instanceof User) { - mail_notify_attn($user, $this); + foreach ($recipientIds as $recipientId) { + $user = User::getKV('id', $recipientId); + if ($user instanceof User) { + mail_notify_attn($user, $this); + } } + Event::handle('EndNotifyMentioned', array($this, $recipientIds)); } } /** * Pull list of groups this notice needs to be delivered to, - * as previously recorded by saveGroups() or saveKnownGroups(). + * as previously recorded by saveKnownGroups(). * * @return array of Group objects */ - - protected $_groups = -1; - + + protected $_groups = array(); + function getGroups() { // Don't save groups for repeats @@ -1459,42 +1749,38 @@ class Notice extends Managed_DataObject if (!empty($this->repeat_of)) { return array(); } - - if ($this->_groups != -1) - { - return $this->_groups; + + if (isset($this->_groups[$this->id])) { + return $this->_groups[$this->id]; } - + $gis = Group_inbox::listGet('notice_id', array($this->id)); $ids = array(); - foreach ($gis[$this->id] as $gi) - { + foreach ($gis[$this->id] as $gi) { $ids[] = $gi->group_id; } - + $groups = User_group::multiGet('id', $ids); - - $this->_groups = $groups->fetchAll(); - - return $this->_groups; + $this->_groups[$this->id] = $groups->fetchAll(); + return $this->_groups[$this->id]; } - + function _setGroups($groups) { - $this->_groups = $groups; + $this->_groups[$this->id] = $groups; } /** * Convert a notice into an activity for export. * - * @param User $cur Current user + * @param Profile $scoped The currently logged in/scoped profile * * @return Activity activity object representing this Notice. */ - function asActivity($cur=null) + function asActivity(Profile $scoped=null) { $act = self::cacheGet(Cache::codeKey('notice:as-activity:'.$this->id)); @@ -1503,27 +1789,37 @@ class Notice extends Managed_DataObject } $act = new Activity(); - if (Event::handle('StartNoticeAsActivity', array($this, &$act))) { + if (Event::handle('StartNoticeAsActivity', array($this, $act, $scoped))) { $act->id = $this->uri; $act->time = strtotime($this->created); - $act->link = $this->bestUrl(); + try { + $act->link = $this->getUrl(); + } catch (InvalidUrlException $e) { + // The notice is probably a share or similar, which don't + // have a representational URL of their own. + } $act->content = common_xml_safe_str($this->rendered); $profile = $this->getProfile(); - $act->actor = ActivityObject::fromProfile($profile); - $act->actor->extra[] = $profile->profileInfo($cur); + $act->actor = $profile->asActivityObject(); + $act->actor->extra[] = $profile->profileInfo($scoped); $act->verb = $this->verb; if ($this->repeat_of) { $repeated = Notice::getKV('id', $this->repeat_of); if ($repeated instanceof Notice) { - $act->objects[] = $repeated->asActivity($cur); + // TRANS: A repeat activity's title. %1$s is repeater's nickname + // and %2$s is the repeated user's nickname. + $act->title = sprintf(_('%1$s repeated a notice by %2$s'), + $this->getProfile()->getNickname(), + $repeated->getProfile()->getNickname()); + $act->objects[] = $repeated->asActivity($scoped); } } else { - $act->objects[] = ActivityObject::fromNotice($this); + $act->objects[] = $this->asActivityObject(); } // XXX: should this be handled by default processing for object entry? @@ -1555,8 +1851,8 @@ class Notice extends Managed_DataObject try { $reply = $this->getParent(); - $ctx->replyToID = $reply->uri; - $ctx->replyToUrl = $reply->bestUrl(); + $ctx->replyToID = $reply->getUri(); + $ctx->replyToUrl = $reply->getUrl(); } catch (Exception $e) { // This is not a reply to something } @@ -1577,27 +1873,23 @@ class Notice extends Managed_DataObject foreach ($reply_ids as $id) { $rprofile = Profile::getKV('id', $id); if ($rprofile instanceof Profile) { - $ctx->attention[] = $rprofile->getUri(); - $ctx->attentionType[$rprofile->getUri()] = ActivityObject::PERSON; + $ctx->attention[$rprofile->getUri()] = ActivityObject::PERSON; } } $groups = $this->getGroups(); foreach ($groups as $group) { - $ctx->attention[] = $group->getUri(); - $ctx->attentionType[$group->getUri()] = ActivityObject::GROUP; + $ctx->attention[$group->getUri()] = ActivityObject::GROUP; } switch ($this->scope) { case Notice::PUBLIC_SCOPE: - $ctx->attention[] = "http://activityschema.org/collection/public"; - $ctx->attentionType["http://activityschema.org/collection/public"] = ActivityObject::COLLECTION; + $ctx->attention[ActivityContext::ATTN_PUBLIC] = ActivityObject::COLLECTION; break; case Notice::FOLLOWER_SCOPE: $surl = common_local_url("subscribers", array('nickname' => $profile->nickname)); - $ctx->attention[] = $surl; - $ctx->attentionType[$surl] = ActivityObject::COLLECTION; + $ctx->attention[$surl] = ActivityObject::COLLECTION; break; } @@ -1649,7 +1941,7 @@ class Notice extends Managed_DataObject $act->editLink = $act->selfLink; } - Event::handle('EndNoticeAsActivity', array($this, &$act)); + Event::handle('EndNoticeAsActivity', array($this, $act, $scoped)); } self::cacheSet(Cache::codeKey('notice:as-activity:'.$this->id), $act); @@ -1663,10 +1955,10 @@ class Notice extends Managed_DataObject function asAtomEntry($namespace=false, $source=false, $author=true, - $cur=null) + Profile $scoped=null) { - $act = $this->asActivity($cur); - $act->extra[] = $this->noticeInfo($cur); + $act = $this->asActivity($scoped); + $act->extra[] = $this->noticeInfo($scoped); return $act->asString($namespace, $author, $source); } @@ -1676,12 +1968,12 @@ class Notice extends Managed_DataObject * Clients use some extra notice info in the atom stream. * This gives it to them. * - * @param User $cur Current user + * @param Profile $scoped The currently logged in/scoped profile * * @return array representation of element */ - function noticeInfo($cur) + function noticeInfo(Profile $scoped=null) { // local notice ID (useful to clients for ordering) @@ -1707,16 +1999,16 @@ class Notice extends Managed_DataObject // favorite and repeated - if (!empty($cur)) { - $cp = $cur->getProfile(); - $noticeInfoAttr['favorite'] = ($cp->hasFave($this)) ? "true" : "false"; - $noticeInfoAttr['repeated'] = ($cp->hasRepeated($this)) ? "true" : "false"; + if ($scoped instanceof Profile) { + $noticeInfoAttr['repeated'] = ($scoped->hasRepeated($this)) ? "true" : "false"; } if (!empty($this->repeat_of)) { $noticeInfoAttr['repeat_of'] = $this->repeat_of; } + Event::handle('StatusNetApiNoticeInfo', array($this, &$noticeInfoAttr, $scoped)); + return array('statusnet:notice_info', $noticeInfoAttr, null); } @@ -1732,96 +2024,64 @@ class Notice extends Managed_DataObject function asActivityNoun($element) { - $noun = ActivityObject::fromNotice($this); + $noun = $this->asActivityObject(); return $noun->asString('activity:' . $element); } - function bestUrl() + public function asActivityObject() { - if (!empty($this->url)) { - return $this->url; - } else if (!empty($this->uri) && preg_match('/^https?:/', $this->uri)) { - return $this->uri; - } else { - return common_local_url('shownotice', - array('notice' => $this->id)); + $object = new ActivityObject(); + + if (Event::handle('StartActivityObjectFromNotice', array($this, &$object))) { + $object->type = $this->object_type ?: ActivityObject::NOTE; + $object->id = $this->getUri(); + $object->title = sprintf('New %1$s by %2$s', ActivityObject::canonicalType($object->type), $this->getProfile()->getNickname()); + $object->content = $this->rendered; + $object->link = $this->getUrl(); + + $object->extra[] = array('status_net', array('notice_id' => $this->id)); + + Event::handle('EndActivityObjectFromNotice', array($this, &$object)); } - } + return $object; + } /** * Determine which notice, if any, a new notice is in reply to. * * For conversation tracking, we try to see where this notice fits - * in the tree. Rough algorithm is: - * - * if (reply_to is set and valid) { - * return reply_to; - * } else if ((source not API or Web) and (content starts with "T NAME" or "@name ")) { - * return ID of last notice by initial @name in content; - * } + * in the tree. Beware that this may very well give false positives + * and add replies to wrong threads (if there have been newer posts + * by the same user as we're replying to). * - * Note that all @nickname instances will still be used to save "reply" records, - * so the notice shows up in the mentioned users' "replies" tab. - * - * @param integer $reply_to ID passed in by Web or API - * @param integer $profile_id ID of author - * @param string $source Source tag, like 'web' or 'gwibber' + * @param Profile $sender Author profile * @param string $content Final notice content * * @return integer ID of replied-to notice, or null for not a reply. */ - static function getReplyTo($reply_to, $profile_id, $source, $content) + static function getInlineReplyTo(Profile $sender, $content) { - static $lb = array('xmpp', 'mail', 'sms', 'omb'); - - // If $reply_to is specified, we check that it exists, and then - // return it if it does - - if (!empty($reply_to)) { - $reply_notice = Notice::getKV('id', $reply_to); - if ($reply_notice instanceof Notice) { - return $reply_notice; - } - } - - // If it's not a "low bandwidth" source (one where you can't set - // a reply_to argument), we return. This is mostly web and API - // clients. - - if (!in_array($source, $lb)) { - return null; - } - // Is there an initial @ or T? - - if (preg_match('/^T ([A-Z0-9]{1,64}) /', $content, $match) || - preg_match('/^@([a-z0-9]{1,64})\s+/', $content, $match)) { + if (preg_match('/^T ([A-Z0-9]{1,64}) /', $content, $match) + || preg_match('/^@([a-z0-9]{1,64})\s+/', $content, $match)) { $nickname = common_canonical_nickname($match[1]); } else { return null; } // Figure out who that is. - - $sender = Profile::getKV('id', $profile_id); - if (!$sender instanceof Profile) { - return null; - } - $recipient = common_relative_profile($sender, $nickname, common_sql_now()); - if (!$recipient instanceof Profile) { - return null; - } - - // Get their last notice - - $last = $recipient->getCurrentNotice(); - - if ($last instanceof Notice) { - return $last; + if ($recipient instanceof Profile) { + // Get their last notice + $last = $recipient->getCurrentNotice(); + if ($last instanceof Notice) { + return $last; + } + // Maybe in the future we want to handle something else below + // so don't return getCurrentNotice() immediately. } return null; @@ -1863,23 +2123,23 @@ class Notice extends Managed_DataObject /** * Convenience function for posting a repeat of an existing message. * - * @param int $repeater_id: profile ID of user doing the repeat + * @param Profile $repeater Profile which is doing the repeat * @param string $source: posting source key, eg 'web', 'api', etc * @return Notice * * @throws Exception on failure or permission problems */ - function repeat($repeater_id, $source) + function repeat(Profile $repeater, $source) { - $author = Profile::getKV('id', $this->profile_id); + $author = $this->getProfile(); // TRANS: Message used to repeat a notice. RT is the abbreviation of 'retweet'. // TRANS: %1$s is the repeated user's name, %2$s is the repeated notice. $content = sprintf(_('RT @%1$s %2$s'), - $author->nickname, + $author->getNickname(), $this->content); - $maxlen = common_config('site', 'textlimit'); + $maxlen = self::maxContent(); if ($maxlen > 0 && mb_strlen($content) > $maxlen) { // Web interface and current Twitter API clients will // pull the original notice's text, but some older @@ -1890,9 +2150,9 @@ class Notice extends Managed_DataObject $content = mb_substr($content, 0, $maxlen - 4) . ' ...'; } - // Scope is same as this one's - return self::saveNew($repeater_id, + // Scope is same as this one's + return self::saveNew($repeater->id, $content, $source, array('repeat_of' => $this->id, @@ -1986,6 +2246,20 @@ class Notice extends Managed_DataObject return $options; } + function clearAttentions() + { + $att = new Attention(); + $att->notice_id = $this->getID(); + + if ($att->find()) { + while ($att->fetch()) { + // Can't do delete() on the object directly since it won't remove all of it + $other = clone($att); + $other->delete(); + } + } + } + function clearReplies() { $replyNotice = new Notice(); @@ -2047,24 +2321,6 @@ class Notice extends Managed_DataObject } } - function clearFaves() - { - $fave = new Fave(); - $fave->notice_id = $this->id; - - if ($fave->find()) { - while ($fave->fetch()) { - self::blow('fave:ids_by_user_own:%d', $fave->user_id); - self::blow('fave:ids_by_user_own:%d;last', $fave->user_id); - self::blow('fave:ids_by_user:%d', $fave->user_id); - self::blow('fave:ids_by_user:%d;last', $fave->user_id); - $fave->delete(); - } - } - - $fave->free(); - } - function clearTags() { $tag = new Notice_tag(); @@ -2105,33 +2361,23 @@ class Notice extends Managed_DataObject // have to wait Event::handle('StartNoticeDistribute', array($this)); - $user = User::getKV('id', $this->profile_id); - if ($user instanceof User) { - Inbox::insertNotice($user->id, $this->id); - } - - if (common_config('queue', 'inboxes')) { - // If there's a failure, we want to _force_ - // distribution at this point. + // If there's a failure, we want to _force_ + // distribution at this point. + try { + $qm = QueueManager::get(); + $qm->enqueue($this, 'distrib'); + } catch (Exception $e) { + // If the exception isn't transient, this + // may throw more exceptions as DQH does + // its own enqueueing. So, we ignore them! try { - $qm = QueueManager::get(); - $qm->enqueue($this, 'distrib'); + $handler = new DistribQueueHandler(); + $handler->handle($this); } catch (Exception $e) { - // If the exception isn't transient, this - // may throw more exceptions as DQH does - // its own enqueueing. So, we ignore them! - try { - $handler = new DistribQueueHandler(); - $handler->handle($this); - } catch (Exception $e) { - common_log(LOG_ERR, "emergency redistribution resulted in " . $e->getMessage()); - } - // Re-throw so somebody smarter can handle it. - throw $e; + common_log(LOG_ERR, "emergency redistribution resulted in " . $e->getMessage()); } - } else { - $handler = new DistribQueueHandler(); - $handler->handle($this); + // Re-throw so somebody smarter can handle it. + throw $e; } } @@ -2139,20 +2385,47 @@ class Notice extends Managed_DataObject { $result = parent::insert(); - if ($result !== false) { - // Profile::hasRepeated() abuses pkeyGet(), so we - // have to clear manually - if (!empty($this->repeat_of)) { - $c = self::memcache(); - if (!empty($c)) { - $ck = self::multicacheKey('Notice', - array('profile_id' => $this->profile_id, - 'repeat_of' => $this->repeat_of)); - $c->delete($ck); - } + if ($result === false) { + common_log_db_error($this, 'INSERT', __FILE__); + // TRANS: Server exception thrown when a stored object entry cannot be saved. + throw new ServerException('Could not save Notice'); + } + + // Profile::hasRepeated() abuses pkeyGet(), so we + // have to clear manually + if (!empty($this->repeat_of)) { + $c = self::memcache(); + if (!empty($c)) { + $ck = self::multicacheKey('Notice', + array('profile_id' => $this->profile_id, + 'repeat_of' => $this->repeat_of)); + $c->delete($ck); } } + // Update possibly ID-dependent columns: URI, conversation + // (now that INSERT has added the notice's local id) + $orig = clone($this); + $changed = false; + + // We can only get here if it's a local notice, since remote notices + // should've bailed out earlier due to lacking a URI. + if (empty($this->uri)) { + $this->uri = sprintf('%s%s=%d:%s=%s', + TagURI::mint(), + 'noticeId', $this->id, + 'objectType', $this->get_object_type(true)); + $changed = true; + } + + if ($changed && $this->update($orig) === false) { + common_log_db_error($notice, 'UPDATE', __FILE__); + // TRANS: Server exception thrown when a notice cannot be updated. + throw new ServerException(_('Problem saving notice.')); + } + + $this->blowOnInsert(); + return $result; } @@ -2164,31 +2437,34 @@ class Notice extends Managed_DataObject */ function getSource() { + if (empty($this->source)) { + return false; + } + $ns = new Notice_source(); - if (!empty($this->source)) { - switch ($this->source) { - case 'web': - case 'xmpp': - case 'mail': - case 'omb': - case 'system': - case 'api': + switch ($this->source) { + case 'web': + case 'xmpp': + case 'mail': + case 'omb': + case 'system': + case 'api': + $ns->code = $this->source; + break; + default: + $ns = Notice_source::getKV($this->source); + if (!$ns) { + $ns = new Notice_source(); $ns->code = $this->source; - break; - default: - $ns = Notice_source::getKV($this->source); - if (!$ns) { - $ns = new Notice_source(); - $ns->code = $this->source; - $app = Oauth_application::getKV('name', $this->source); - if ($app) { - $ns->name = $app->name; - $ns->url = $app->source_url; - } + $app = Oauth_application::getKV('name', $this->source); + if ($app) { + $ns->name = $app->name; + $ns->url = $app->source_url; } - break; } + break; } + return $ns; } @@ -2204,6 +2480,11 @@ class Notice extends Managed_DataObject $this->is_local == Notice::LOCAL_NONPUBLIC); } + public function isRepeat() + { + return !empty($this->repeat_of); + } + /** * Get the list of hash tags saved with this notice. * @@ -2346,12 +2627,8 @@ class Notice extends Managed_DataObject function isPublic() { - if (common_config('public', 'localonly')) { - return ($this->is_local == Notice::LOCAL_PUBLIC); - } else { - return (($this->is_local != Notice::LOCAL_NONPUBLIC) && - ($this->is_local != Notice::GATEWAY)); - } + return (($this->is_local != Notice::LOCAL_NONPUBLIC) && + ($this->is_local != Notice::GATEWAY)); } /** @@ -2399,89 +2676,69 @@ class Notice extends Managed_DataObject $scope = self::defaultScope(); } - // If there's no scope, anyone (even anon) is in scope. - - if ($scope == 0) { // Not private - + if ($scope == 0 && !$this->getProfile()->isPrivateStream()) { // Not scoping, so it is public. return !$this->isHiddenSpam($profile); + } - } else { // Private, somehow - - // If there's scope, anon cannot be in scope + // If there's scope, anon cannot be in scope + if (empty($profile)) { + return false; + } - if (empty($profile)) { - return false; - } + // Author is always in scope + if ($this->profile_id == $profile->id) { + return true; + } - // Author is always in scope + // Only for users on this site + if (($scope & Notice::SITE_SCOPE) && !$profile->isLocal()) { + return false; + } - if ($this->profile_id == $profile->id) { - return true; - } + // Only for users mentioned in the notice + if ($scope & Notice::ADDRESSEE_SCOPE) { - // Only for users on this site + $reply = Reply::pkeyGet(array('notice_id' => $this->id, + 'profile_id' => $profile->id)); - if (($scope & Notice::SITE_SCOPE) && !$profile->isLocal()) { + if (!$reply instanceof Reply) { return false; } + } - // Only for users mentioned in the notice - - if ($scope & Notice::ADDRESSEE_SCOPE) { - - $reply = Reply::pkeyGet(array('notice_id' => $this->id, - 'profile_id' => $profile->id)); - - if (!$reply instanceof Reply) { - return false; - } - } - - // Only for members of the given group - - if ($scope & Notice::GROUP_SCOPE) { - - // XXX: just query for the single membership + // Only for members of the given group + if ($scope & Notice::GROUP_SCOPE) { - $groups = $this->getGroups(); + // XXX: just query for the single membership - $foundOne = false; + $groups = $this->getGroups(); - foreach ($groups as $group) { - if ($profile->isMember($group)) { - $foundOne = true; - break; - } - } + $foundOne = false; - if (!$foundOne) { - return false; + foreach ($groups as $group) { + if ($profile->isMember($group)) { + $foundOne = true; + break; } } - // Only for followers of the author - - $author = null; + if (!$foundOne) { + return false; + } + } - if ($scope & Notice::FOLLOWER_SCOPE) { + if ($scope & Notice::FOLLOWER_SCOPE || $this->getProfile()->isPrivateStream()) { - try { - $author = $this->getProfile(); - } catch (Exception $e) { - return false; - } - - if (!Subscription::exists($profile, $author)) { - return false; - } + if (!Subscription::exists($profile, $this->getProfile())) { + return false; } - - return !$this->isHiddenSpam($profile); } + + return !$this->isHiddenSpam($profile); } function isHiddenSpam($profile) { - + // Hide posts by silenced users from everyone but moderators. if (common_config('notice', 'hidespam')) { @@ -2504,29 +2761,6 @@ class Notice extends Managed_DataObject return false; } - static function groupsFromText($text, $profile) - { - $groups = array(); - - /* extract all !group */ - $count = preg_match_all('/(?:^|\s)!(' . Nickname::DISPLAY_FMT . ')/', - strtolower($text), - $match); - - if (!$count) { - return $groups; - } - - foreach (array_unique($match[1]) as $nickname) { - $group = User_group::getForNickname($nickname, $profile); - if ($group instanceof User_group && $profile->isMember($group)) { - $groups[] = $group->id; - } - } - - return $groups; - } - public function getParent() { $parent = Notice::getKV('id', $this->reply_to); @@ -2554,7 +2788,7 @@ class Notice extends Managed_DataObject $skip = array('_profile', '_groups', '_attachments', '_faves', '_replies', '_repeats'); return array_diff($vars, $skip); } - + static function defaultScope() { $scope = common_config('notice', 'defaultscope'); @@ -2571,48 +2805,45 @@ class Notice extends Managed_DataObject static function fillProfiles($notices) { $map = self::getProfiles($notices); - - foreach ($notices as $notice) { - if (array_key_exists($notice->profile_id, $map)) { - $notice->_setProfile($map[$notice->profile_id]); - } + foreach ($notices as $entry=>$notice) { + try { + if (array_key_exists($notice->profile_id, $map)) { + $notice->_setProfile($map[$notice->profile_id]); + } + } catch (NoProfileException $e) { + common_log(LOG_WARNING, "Failed to fill profile in Notice with non-existing entry for profile_id: {$e->profile_id}"); + unset($notices[$entry]); + } } - + return array_values($map); } - + static function getProfiles(&$notices) { $ids = array(); foreach ($notices as $notice) { $ids[] = $notice->profile_id; } - $ids = array_unique($ids); - - return Profile::pivotGet('id', $ids); + return Profile::pivotGet('id', $ids); } - + static function fillGroups(&$notices) { $ids = self::_idsOf($notices); - $gis = Group_inbox::listGet('notice_id', $ids); - $gids = array(); - foreach ($gis as $id => $gi) - { + foreach ($gis as $id => $gi) { foreach ($gi as $g) { $gids[] = $g->group_id; } } - + $gids = array_unique($gids); - $group = User_group::pivotGet('id', $gids); - foreach ($notices as $notice) { $grps = array(); @@ -2624,34 +2855,28 @@ class Notice extends Managed_DataObject } } - static function _idsOf(&$notices) + static function _idsOf(array &$notices) { $ids = array(); foreach ($notices as $notice) { - $ids[] = $notice->id; + $ids[$notice->id] = true; } - $ids = array_unique($ids); - return $ids; + return array_keys($ids); } static function fillAttachments(&$notices) { $ids = self::_idsOf($notices); - $f2pMap = File_to_post::listGet('post_id', $ids); - $fileIds = array(); - foreach ($f2pMap as $noticeId => $f2ps) { foreach ($f2ps as $f2p) { - $fileIds[] = $f2p->file_id; + $fileIds[] = $f2p->file_id; } } $fileIds = array_unique($fileIds); - $fileMap = File::pivotGet('id', $fileIds); - foreach ($notices as $notice) { $files = array(); @@ -2663,47 +2888,6 @@ class Notice extends Managed_DataObject } } - protected $_faves; - - /** - * All faves of this notice - * - * @return array Array of Fave objects - */ - - function getFaves() - { - if (isset($this->_faves) && is_array($this->_faves)) { - return $this->_faves; - } - $faveMap = Fave::listGet('notice_id', array($this->id)); - $this->_faves = $faveMap[$this->id]; - return $this->_faves; - } - - function _setFaves($faves) - { - $this->_faves = $faves; - } - - static function fillFaves(&$notices) - { - $ids = self::_idsOf($notices); - $faveMap = Fave::listGet('notice_id', $ids); - $cnt = 0; - $faved = array(); - foreach ($faveMap as $id => $faves) { - $cnt += count($faves); - if (count($faves) > 0) { - $faved[] = $id; - } - } - foreach ($notices as $notice) { - $faves = $faveMap[$notice->id]; - $notice->_setFaves($faves); - } - } - static function fillReplies(&$notices) { $ids = self::_idsOf($notices); @@ -2718,21 +2902,21 @@ class Notice extends Managed_DataObject } } - protected $_repeats; + protected $_repeats = array(); function getRepeats() { - if (isset($this->_repeats) && is_array($this->_repeats)) { - return $this->_repeats; + if (isset($this->_repeats[$this->id])) { + return $this->_repeats[$this->id]; } $repeatMap = Notice::listGet('repeat_of', array($this->id)); - $this->_repeats = $repeatMap[$this->id]; - return $this->_repeats; + $this->_repeats[$this->id] = $repeatMap[$this->id]; + return $this->_repeats[$this->id]; } function _setRepeats($repeats) { - $this->_repeats = $repeats; + $this->_repeats[$this->id] = $repeats; } static function fillRepeats(&$notices)