X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=classes%2FNotice.php;h=4c831c7cc75645ccfdead4b2f1fc6cd5d8324e6d;hb=7e597ea7cc85c3b7c7226d443b21fb3252c8fd7e;hp=2c404e2550544a307c40c037f30514a00167842b;hpb=0b53b6768e03932f4beec6b6655763e6ecedc36d;p=quix0rs-gnu-social.git diff --git a/classes/Notice.php b/classes/Notice.php index 2c404e2550..4c831c7cc7 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -29,18 +29,16 @@ * @author Robin Millette * @author Sarven Capadisli * @author Tom Adams + * @author Mikael Nordfeldth * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @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. */ @@ -76,12 +74,6 @@ class Notice extends Managed_DataObject public $object_type; // varchar(255) public $scope; // int(4) - /* Static get */ - function staticGet($k,$v=NULL) - { - return Memcached_DataObject::staticGet('Notice',$k,$v); - } - /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE @@ -136,11 +128,6 @@ class Notice extends Managed_DataObject return $def; } - - function multiGet($kc, $kvs, $skipNulls=true) - { - return Memcached_DataObject::multiGet('Notice', $kc, $kvs, $skipNulls); - } /* Notice types */ const LOCAL_PUBLIC = 1; @@ -154,42 +141,42 @@ class Notice extends Managed_DataObject const GROUP_SCOPE = 4; const FOLLOWER_SCOPE = 8; - protected $_profile = -1; - - function getProfile() + protected $_profile = array(); + + /** + * Will always return a profile, if anything fails it will + * (through _setProfile) throw a NoProfileException. + */ + public function getProfile() { - if (is_int($this->_profile) && $this->_profile == -1) { - $this->_setProfile(Profile::staticGet('id', $this->profile_id)); - - if (empty($this->_profile)) { - // TRANS: Server exception thrown when a user profile for a notice cannot be found. - // TRANS: %1$d is a profile ID (number), %2$d is a notice ID (number). - throw new ServerException(sprintf(_('No such profile (%1$d) for notice (%2$d).'), $this->profile_id, $this->id)); - } + if (!isset($this->_profile[$this->profile_id])) { + $this->_setProfile(Profile::getKV('id', $this->profile_id)); } - - return $this->_profile; + return $this->_profile[$this->profile_id]; } - function _setProfile($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. // @fixme we have some cases where things get re-run and so the // insert fails. - $deleted = Deleted_notice::staticGet('id', $this->id); + $deleted = Deleted_notice::getKV('id', $this->id); - if (!$deleted) { - $deleted = Deleted_notice::staticGet('uri', $this->uri); + 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; @@ -207,21 +194,93 @@ 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; } + public function getUri() + { + return $this->uri; + } + + /* + * @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; + } + + /* + * Get the original representation URL of this notice. + */ + public function getUrl() + { + // The risk is we start having empty urls and non-http uris... + // 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(): + // 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); + case common_valid_http_url($this->uri): + return $this->uri; + 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; + } + /** * Extract #hashtags from this notice's content and save them to the database. */ @@ -304,7 +363,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,7 +384,7 @@ class Notice extends Managed_DataObject * @return Notice * @throws ClientException */ - static function saveNew($profile_id, $content, $source, $options=null) { + static function saveNew($profile_id, $content, $source, array $options=null) { $defaults = array('uri' => null, 'url' => null, 'reply_to' => null, @@ -346,9 +405,14 @@ class Notice extends Managed_DataObject $is_local = Notice::LOCAL_PUBLIC; } - $profile = Profile::staticGet('id', $profile_id); - $user = User::staticGet('id', $profile_id); - if ($user) { + $profile = Profile::getKV('id', $profile_id); + if (!$profile instanceof Profile) { + // TRANS: Client exception thrown when trying to save a notice for an unknown user. + throw new ClientException(_('Problem saving notice. Unknown user.')); + } + + $user = User::getKV('id', $profile_id); + if ($user instanceof User) { // Use the local user's shortening preferences, if applicable. $final = $user->shortenLinks($content); } else { @@ -360,11 +424,6 @@ class Notice extends Managed_DataObject throw new ClientException(_('Problem saving notice. Too long.')); } - if (empty($profile)) { - // TRANS: Client exception thrown when trying to save a notice for an unknown user. - throw new ClientException(_('Problem saving notice. Unknown user.')); - } - if (common_config('throttle', 'enabled') && !Notice::checkEditThrottle($profile_id)) { common_log(LOG_WARNING, 'Excessive posting by profile #' . $profile_id . '; throttled.'); // TRANS: Client exception thrown when a user tries to post too many notices in a given time frame. @@ -406,6 +465,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; @@ -413,9 +482,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; @@ -426,9 +494,9 @@ class Notice extends Managed_DataObject // Check for a private one - $repeat = Notice::staticGet('id', $repeat_of); + $repeat = Notice::getKV('id', $repeat_of); - if (empty($repeat)) { + if (!($repeat instanceof Notice)) { // TRANS: Client exception thrown in notice when trying to repeat a missing or deleted notice. throw new ClientException(_('Cannot repeat; original notice is missing or deleted.')); } @@ -450,17 +518,27 @@ class Notice extends Managed_DataObject throw new ClientException(_('Cannot repeat a notice you cannot read.'), 403); } - if ($profile->hasRepeated($repeat->id)) { + if ($profile->hasRepeated($repeat)) { // TRANS: Client error displayed when trying to repeat an already repeated notice. 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). @@ -468,13 +546,19 @@ 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 (!empty($reply->repeat_of)) { + $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)) { + if (empty($groups) && ($reply->scope & Notice::GROUP_SCOPE)) { $groups = array(); $replyGroups = $reply->getGroups(); foreach ($replyGroups as $group) { @@ -533,21 +617,23 @@ class Notice extends Managed_DataObject // For private streams - $user = $profile->getUser(); + try { + $user = $profile->getUser(); - if (!empty($user)) { if ($user->private_stream && ($notice->scope == Notice::PUBLIC_SCOPE || $notice->scope == Notice::SITE_SCOPE)) { $notice->scope |= Notice::FOLLOWER_SCOPE; } + } catch (NoSuchUserException $e) { + // Cannot handle private streams for remote profiles } // Force the scope for private groups foreach ($groups as $groupId) { - $group = User_group::staticGet('id', $groupId); - if (!empty($group)) { + $group = User_group::getKV('id', $groupId); + if ($group instanceof User_group) { if ($group->force_scope) { $notice->scope |= Notice::GROUP_SCOPE; break; @@ -573,8 +659,13 @@ class Notice extends Managed_DataObject $changed = false; - if (empty($uri)) { - $notice->uri = common_notice_uri($notice); + // 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($notice->uri)) { + $notice->uri = sprintf('%s%s=%d:%s=%s', + TagURI::mint(), + 'noticeId', $notice->id, + 'objectType', $notice->get_object_type(true)); $changed = true; } @@ -582,13 +673,13 @@ class Notice extends Managed_DataObject // the beginning of a new conversation. if (empty($notice->conversation)) { - $conv = Conversation::create(); + $conv = Conversation::create($notice); $notice->conversation = $conv->id; $changed = true; } if ($changed) { - if (!$notice->update($orig)) { + if ($notice->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.')); @@ -653,18 +744,18 @@ class Notice extends Managed_DataObject self::blow('notice:list-ids:repeat_of:%d', $this->repeat_of); } - $original = Notice::staticGet('id', $this->repeat_of); + $original = Notice::getKV('id', $this->repeat_of); - if (!empty($original)) { - $originalUser = User::staticGet('id', $original->profile_id); - if (!empty($originalUser)) { + if ($original instanceof Notice) { + $originalUser = User::getKV('id', $original->profile_id); + if ($originalUser instanceof User) { $this->blowStream('user:repeats_of_me:%d', $originalUser->id); } } - $profile = Profile::staticGet($this->profile_id); + $profile = Profile::getKV($this->profile_id); - if (!empty($profile)) { + if ($profile instanceof Profile) { $profile->blowNoticeCount(); } @@ -729,8 +820,8 @@ class Notice extends Managed_DataObject if ($lastStr !== false) { $window = explode(',', $lastStr); $lastID = $window[0]; - $lastNotice = Notice::staticGet('id', $lastID); - if (empty($lastNotice) // just weird + $lastNotice = Notice::getKV('id', $lastID); + if (!$lastNotice instanceof Notice // just weird || strtotime($lastNotice->created) >= strtotime($this->created)) { $c->delete($lastKey); } @@ -763,7 +854,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? + } } } } @@ -772,12 +867,16 @@ 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) { - $profile = Profile::staticGet($profile_id); - if (empty($profile)) { + $profile = Profile::getKV($profile_id); + if (!$profile instanceof Profile) { return false; } $notice = $profile->getNotices(0, CachingNoticeStream::CACHE_WINDOW); @@ -804,8 +903,8 @@ class Notice extends Managed_DataObject } static function checkEditThrottle($profile_id) { - $profile = Profile::staticGet($profile_id); - if (empty($profile)) { + $profile = Profile::getKV($profile_id); + if (!$profile instanceof Profile) { return false; } // Get the Nth notice @@ -821,15 +920,14 @@ class Notice extends Managed_DataObject return true; } - protected $_attachments = -1; + protected $_attachments = array(); function attachments() { - - if ($this->_attachments != -1) { - return $this->_attachments; + if (isset($this->_attachments[$this->id])) { + return $this->_attachments[$this->id]; } - $f2ps = Memcached_DataObject::listGet('File_to_post', 'post_id', array($this->id)); + $f2ps = File_to_post::listGet('post_id', array($this->id)); $ids = array(); @@ -837,16 +935,16 @@ class Notice extends Managed_DataObject $ids[] = $f2p->file_id; } - $files = Memcached_DataObject::multiGet('File', 'id', $ids); + $files = File::multiGet('id', $ids); - $this->_attachments = $files->fetchAll(); + $this->_attachments[$this->id] = $files->fetchAll(); - return $this->_attachments; + 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) @@ -929,23 +1027,26 @@ class Notice extends Managed_DataObject if ($root !== false && $root->inScope($profile)) { return $root; - } else { - $last = $this; + } - do { - $parent = $last->getOriginal(); - if (!empty($parent) && $parent->inScope($profile)) { + $last = $this; + while (true) { + try { + $parent = $last->getParent(); + if ($parent->inScope($profile)) { $last = $parent; continue; - } else { - $root = $last; - break; } - } while (!empty($parent)); - - self::cacheSet($keypart, $root); + } catch (Exception $e) { + // Latest notice has no parent + } + // No parent, or parent out of scope + $root = $last; + break; } + self::cacheSet($keypart, $root); + return $root; } @@ -960,7 +1061,7 @@ class Notice extends Managed_DataObject * if left empty, will be loaded from reply records * @return array associating recipient user IDs with an inbox source constant */ - function whoGets($groups=null, $recipients=null) + function whoGets(array $groups=null, array $recipients=null) { $c = self::memcache(); @@ -971,30 +1072,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) { @@ -1004,12 +1098,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,16 +1116,22 @@ class Notice extends Managed_DataObject $originalProfile = null; if ($this->repeat_of) { // Check blocks against the original notice's poster as well. - $original = Notice::staticGet('id', $this->repeat_of); - if ($original) { + $original = Notice::getKV('id', $this->repeat_of); + if ($original instanceof Notice) { $originalProfile = $original->getProfile(); } } foreach ($ni as $id => $source) { - $user = User::staticGet('id', $id); - if (empty($user) || $user->hasBlocked($profile) || - ($originalProfile && $user->hasBlocked($originalProfile))) { + try { + $user = User::getKV('id', $id); + if (!$user instanceof User || + $user->hasBlocked($profile) || + ($originalProfile && $user->hasBlocked($originalProfile))) { + unset($ni[$id]); + } + } catch (UserNoProfileException $e) { + // User doesn't have a profile; invalid; skip them. unset($ni[$id]); } } @@ -1050,43 +1148,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($groups=null, $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(); @@ -1127,6 +1188,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. @@ -1145,8 +1219,8 @@ class Notice extends Managed_DataObject $groups = array(); foreach (array_unique($group_ids) as $id) { - $group = User_group::staticGet('id', $id); - if ($group) { + $group = User_group::getKV('id', $id); + if ($group instanceof User_group) { common_log(LOG_ERR, "Local delivery to group id $id, $group->nickname"); $result = $this->addToGroupInbox($group); if (!$result) { @@ -1173,53 +1247,12 @@ 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)); - if (empty($gi)) { + if (!$gi instanceof Group_inbox) { $gi = new Group_inbox(); @@ -1251,21 +1284,20 @@ class Notice extends Managed_DataObject * * Mail notifications etc will be handled later. * - * @param array of unique identifier URIs for recipients + * @param array $uris Array of unique identifier URIs for recipients */ - function saveKnownReplies($uris) + function saveKnownReplies(array $uris) { if (empty($uris)) { return; } - $sender = Profile::staticGet($this->profile_id); + $sender = Profile::getKV($this->profile_id); foreach (array_unique($uris) as $uri) { - - $profile = Profile::fromURI($uri); - - if (empty($profile)) { + try { + $profile = Profile::fromUri($uri); + } catch (UnknownUriException $e) { common_log(LOG_WARNING, "Unable to determine profile for URI '$uri'"); continue; } @@ -1300,22 +1332,19 @@ class Notice extends Managed_DataObject return array(); } - $sender = Profile::staticGet($this->profile_id); + $sender = $this->getProfile(); $replied = array(); // If it's a reply, save for the replied-to author - - if (!empty($this->reply_to)) { - $original = $this->getOriginal(); - if (!empty($original)) { // that'd be weird - $author = $original->getProfile(); - if (!empty($author)) { - $this->saveReply($author->id); - $replied[$author->id] = 1; - self::blow('reply:stream:%d', $author->id); - } - } + try { + $parent = $this->getParent(); + $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! } // @todo ideally this parser information would only @@ -1338,8 +1367,8 @@ class Notice extends Managed_DataObject // Don't save replies from blocked profile to local user - $mentioned_user = User::staticGet('id', $mentioned->id); - if (!empty($mentioned_user) && $mentioned_user->hasBlocked($sender)) { + $mentioned_user = User::getKV('id', $mentioned->id); + if ($mentioned_user instanceof User && $mentioned_user->hasBlocked($sender)) { continue; } @@ -1367,7 +1396,7 @@ class Notice extends Managed_DataObject return $reply; } - protected $_replies = -1; + protected $_replies = array(); /** * Pull the complete list of @-reply targets for this notice. @@ -1376,11 +1405,11 @@ 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 = Memcached_DataObject::listGet('Reply', 'notice_id', array($this->id)); + $replyMap = Reply::listGet('notice_id', array($this->id)); $ids = array(); @@ -1388,14 +1417,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; } /** @@ -1429,8 +1458,8 @@ class Notice extends Managed_DataObject $recipientIds = $this->getReplies(); foreach ($recipientIds as $recipientId) { - $user = User::staticGet('id', $recipientId); - if (!empty($user)) { + $user = User::getKV('id', $recipientId); + if ($user instanceof User) { mail_notify_attn($user, $this); } } @@ -1438,12 +1467,12 @@ class Notice extends Managed_DataObject /** * 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() { @@ -1453,12 +1482,11 @@ class Notice extends Managed_DataObject return array(); } - if ($this->_groups != -1) - { - return $this->_groups; + if (isset($this->_groups[$this->id])) { + return $this->_groups[$this->id]; } - $gis = Memcached_DataObject::listGet('Group_inbox', 'notice_id', array($this->id)); + $gis = Group_inbox::listGet('notice_id', array($this->id)); $ids = array(); @@ -1469,14 +1497,14 @@ class Notice extends Managed_DataObject $groups = User_group::multiGet('id', $ids); - $this->_groups = $groups->fetchAll(); + $this->_groups[$this->id] = $groups->fetchAll(); - return $this->_groups; + return $this->_groups[$this->id]; } function _setGroups($groups) { - $this->_groups = $groups; + $this->_groups[$this->id] = $groups; } /** @@ -1487,11 +1515,11 @@ class Notice extends Managed_DataObject * @return Activity activity object representing this Notice. */ - function asActivity($cur) + function asActivity($cur=null) { $act = self::cacheGet(Cache::codeKey('notice:as-activity:'.$this->id)); - if (!empty($act)) { + if ($act instanceof Activity) { return $act; } $act = new Activity(); @@ -1500,9 +1528,13 @@ class Notice extends Managed_DataObject $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); - $act->title = common_xml_safe_str($this->content); $profile = $this->getProfile(); @@ -1512,8 +1544,8 @@ class Notice extends Managed_DataObject $act->verb = $this->verb; if ($this->repeat_of) { - $repeated = Notice::staticGet('id', $this->repeat_of); - if (!empty($repeated)) { + $repeated = Notice::getKV('id', $this->repeat_of); + if ($repeated instanceof Notice) { $act->objects[] = $repeated->asActivity($cur); } } else { @@ -1539,20 +1571,20 @@ class Notice extends Managed_DataObject $attachments = $this->attachments(); foreach ($attachments as $attachment) { - $enclosure = $attachment->getEnclosure(); - if ($enclosure) { - $act->enclosures[] = $enclosure; + // Save local attachments + if (!empty($attachment->filename)) { + $act->attachments[] = ActivityObject::fromFile($attachment); } } $ctx = new ActivityContext(); - if (!empty($this->reply_to)) { - $reply = Notice::staticGet('id', $this->reply_to); - if (!empty($reply)) { - $ctx->replyToID = $reply->uri; - $ctx->replyToUrl = $reply->bestUrl(); - } + try { + $reply = $this->getParent(); + $ctx->replyToID = $reply->getUri(); + $ctx->replyToUrl = $reply->getUrl(); + } catch (Exception $e) { + // This is not a reply to something } $ctx->location = $this->getLocation(); @@ -1560,8 +1592,8 @@ class Notice extends Managed_DataObject $conv = null; if (!empty($this->conversation)) { - $conv = Conversation::staticGet('id', $this->conversation); - if (!empty($conv)) { + $conv = Conversation::getKV('id', $this->conversation); + if ($conv instanceof Conversation) { $ctx->conversation = $conv->uri; } } @@ -1569,32 +1601,36 @@ class Notice extends Managed_DataObject $reply_ids = $this->getReplies(); foreach ($reply_ids as $id) { - $rprofile = Profile::staticGet('id', $id); - if (!empty($rprofile)) { - $ctx->attention[] = $rprofile->getUri(); + $rprofile = Profile::getKV('id', $id); + if ($rprofile instanceof Profile) { + $ctx->attention[$rprofile->getUri()] = ActivityObject::PERSON; } } $groups = $this->getGroups(); foreach ($groups as $group) { - $ctx->attention[] = $group->getUri(); + $ctx->attention[$group->getUri()] = ActivityObject::GROUP; } - // XXX: deprecated; use ActivityVerb::SHARE instead - - $repeat = null; - - if (!empty($this->repeat_of)) { - $repeat = Notice::staticGet('id', $this->repeat_of); - if (!empty($repeat)) { - $ctx->forwardID = $repeat->uri; - $ctx->forwardUrl = $repeat->bestUrl(); - } + switch ($this->scope) { + case Notice::PUBLIC_SCOPE: + $ctx->attention[ActivityContext::ATTN_PUBLIC] = ActivityObject::COLLECTION; + break; + case Notice::FOLLOWER_SCOPE: + $surl = common_local_url("subscribers", array('nickname' => $profile->nickname)); + $ctx->attention[$surl] = ActivityObject::COLLECTION; + break; } $act->context = $ctx; + $source = $this->getSource(); + + if ($source instanceof Notice_source) { + $act->generator = ActivityObject::fromNoticeSource($source); + } + // Source $atom_feed = $profile->getAtomFeed(); @@ -1618,13 +1654,13 @@ class Notice extends Managed_DataObject $notice = $profile->getCurrentNotice(); - if (!empty($notice)) { + if ($notice instanceof Notice) { $act->source->updated = self::utcDate($notice->created); } - $user = User::staticGet('id', $profile->id); + $user = User::getKV('id', $profile->id); - if (!empty($user)) { + if ($user instanceof User) { $act->source->links['license'] = common_config('license', 'url'); } } @@ -1677,7 +1713,7 @@ class Notice extends Managed_DataObject $ns = $this->getSource(); - if (!empty($ns)) { + if ($ns instanceof Notice_source) { $noticeInfoAttr['source'] = $ns->code; if (!empty($ns->url)) { $noticeInfoAttr['source_link'] = $ns->url; @@ -1693,16 +1729,18 @@ class Notice extends Managed_DataObject // favorite and repeated + $scoped = null; if (!empty($cur)) { - $noticeInfoAttr['favorite'] = ($cur->hasFave($this)) ? "true" : "false"; - $cp = $cur->getProfile(); - $noticeInfoAttr['repeated'] = ($cp->hasRepeated($this->id)) ? "true" : "false"; + $scoped = $cur->getProfile(); + $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); } @@ -1722,92 +1760,41 @@ class Notice extends Managed_DataObject return $noun->asString('activity:' . $element); } - function bestUrl() - { - 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)); - } - } - - /** * 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; - * } - * - * Note that all @nickname instances will still be used to save "reply" records, - * so the notice shows up in the mentioned users' "replies" tab. + * 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). * - * @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::staticGet('id', $reply_to); - if (!empty($reply_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::staticGet('id', $profile_id); - if (empty($sender)) { - return null; - } - $recipient = common_relative_profile($sender, $nickname, common_sql_now()); - if (empty($recipient)) { - return null; - } - - // Get their last notice - - $last = $recipient->getCurrentNotice(); - - if (!empty($last)) { - 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; @@ -1849,36 +1836,24 @@ 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::staticGet('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'); - if ($maxlen > 0 && mb_strlen($content) > $maxlen) { - // Web interface and current Twitter API clients will - // pull the original notice's text, but some older - // clients and RSS/Atom feeds will see this trimmed text. - // - // Unfortunately this is likely to lose tags or URLs - // at the end of long notices. - $content = mb_substr($content, 0, $maxlen - 4) . ' ...'; - } - // Scope is same as this one's - - return self::saveNew($repeater_id, + return self::saveNew($repeater->id, $content, $source, array('repeat_of' => $this->id, @@ -1942,7 +1917,7 @@ class Notice extends Managed_DataObject $location = Location::fromId($location_id, $location_ns); - if (!empty($location)) { + if ($location instanceof Location) { $options['lat'] = $location->lat; $options['lon'] = $location->lon; } @@ -1953,7 +1928,7 @@ class Notice extends Managed_DataObject $location = Location::fromLatLon($lat, $lon); - if (!empty($location)) { + if ($location instanceof Location) { $options['location_id'] = $location->location_id; $options['location_ns'] = $location->location_ns; } @@ -1972,6 +1947,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(); @@ -2033,24 +2022,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(); @@ -2091,33 +2062,23 @@ class Notice extends Managed_DataObject // have to wait Event::handle('StartNoticeDistribute', array($this)); - $user = User::staticGet('id', $this->profile_id); - if (!empty($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; } } @@ -2125,7 +2086,7 @@ class Notice extends Managed_DataObject { $result = parent::insert(); - if ($result) { + if ($result !== false) { // Profile::hasRepeated() abuses pkeyGet(), so we // have to clear manually if (!empty($this->repeat_of)) { @@ -2162,11 +2123,11 @@ class Notice extends Managed_DataObject $ns->code = $this->source; break; default: - $ns = Notice_source::staticGet($this->source); + $ns = Notice_source::getKV($this->source); if (!$ns) { $ns = new Notice_source(); $ns->code = $this->source; - $app = Oauth_application::staticGet('name', $this->source); + $app = Oauth_application::getKV('name', $this->source); if ($app) { $ns->name = $app->name; $ns->url = $app->source_url; @@ -2239,12 +2200,12 @@ class Notice extends Managed_DataObject return false; } - $notice = Notice::staticGet('id', $id); + $notice = Notice::getKV('id', $id); if ($notice) { return $notice->created; } - $deleted = Deleted_notice::staticGet('id', $id); + $deleted = Deleted_notice::getKV('id', $id); if ($deleted) { return $deleted->created; } @@ -2407,21 +2368,18 @@ class Notice extends Managed_DataObject // Only for users on this site - if ($scope & Notice::SITE_SCOPE) { - $user = $profile->getUser(); - if (empty($user)) { - return false; - } + if (($scope & Notice::SITE_SCOPE) && !$profile->isLocal()) { + return false; } // Only for users mentioned in the notice if ($scope & Notice::ADDRESSEE_SCOPE) { - $repl = Reply::pkeyGet(array('notice_id' => $this->id, + $reply = Reply::pkeyGet(array('notice_id' => $this->id, 'profile_id' => $profile->id)); - if (empty($repl)) { + if (!$reply instanceof Reply) { return false; } } @@ -2484,7 +2442,7 @@ class Notice extends Managed_DataObject } if ($author->hasRole(Profile_role::SILENCED)) { - if (empty($profile) || (($profile->id !== $author->id) && (!$profile->hasRight(Right::REVIEWSPAM)))) { + if (!$profile instanceof Profile || (($profile->id !== $author->id) && (!$profile->hasRight(Right::REVIEWSPAM)))) { return true; } } @@ -2493,41 +2451,15 @@ class Notice extends Managed_DataObject return false; } - static function groupsFromText($text, $profile) + public function getParent() { - $groups = array(); - - /* extract all !group */ - $count = preg_match_all('/(?:^|\s)!(' . Nickname::DISPLAY_FMT . ')/', - strtolower($text), - $match); + $parent = Notice::getKV('id', $this->reply_to); - if (!$count) { - return $groups; - } - - foreach (array_unique($match[1]) as $nickname) { - $group = User_group::getForNickname($nickname, $profile); - if (!empty($group) && $profile->isMember($group)) { - $groups[] = $group->id; - } + if (!$parent instanceof Notice) { + throw new ServerException('Notice has no parent'); } - return $groups; - } - - protected $_original = -1; - - function getOriginal() - { - if (is_int($this->_original) && $this->_original == -1) { - if (empty($this->reply_to)) { - $this->_original = null; - } else { - $this->_original = Notice::staticGet('id', $this->reply_to); - } - } - return $this->_original; + return $parent; } /** @@ -2543,7 +2475,7 @@ class Notice extends Managed_DataObject function __sleep() { $vars = parent::__sleep(); - $skip = array('_original', '_profile', '_groups', '_attachments', '_faves', '_replies', '_repeats'); + $skip = array('_profile', '_groups', '_attachments', '_faves', '_replies', '_repeats'); return array_diff($vars, $skip); } @@ -2564,10 +2496,15 @@ class Notice extends Managed_DataObject { $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); @@ -2582,14 +2519,14 @@ class Notice extends Managed_DataObject $ids = array_unique($ids); - return Memcached_DataObject::pivotGet('Profile', 'id', $ids); + return Profile::pivotGet('id', $ids); } static function fillGroups(&$notices) { $ids = self::_idsOf($notices); - $gis = Memcached_DataObject::listGet('Group_inbox', 'notice_id', $ids); + $gis = Group_inbox::listGet('notice_id', $ids); $gids = array(); @@ -2603,7 +2540,7 @@ class Notice extends Managed_DataObject $gids = array_unique($gids); - $group = Memcached_DataObject::pivotGet('User_group', 'id', $gids); + $group = User_group::pivotGet('id', $gids); foreach ($notices as $notice) { @@ -2616,21 +2553,20 @@ 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 = Memcached_DataObject::listGet('File_to_post', 'post_id', $ids); + $f2pMap = File_to_post::listGet('post_id', $ids); $fileIds = array(); @@ -2642,7 +2578,7 @@ class Notice extends Managed_DataObject $fileIds = array_unique($fileIds); - $fileMap = Memcached_DataObject::pivotGet('File', 'id', $fileIds); + $fileMap = File::pivotGet('id', $fileIds); foreach ($notices as $notice) { @@ -2655,51 +2591,10 @@ 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 = Memcached_DataObject::listGet('Fave', '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 = Memcached_DataObject::listGet('Fave', '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); - $replyMap = Memcached_DataObject::listGet('Reply', 'notice_id', $ids); + $replyMap = Reply::listGet('notice_id', $ids); foreach ($notices as $notice) { $replies = $replyMap[$notice->id]; $ids = array(); @@ -2710,27 +2605,27 @@ 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 = Memcached_DataObject::listGet('Notice', 'repeat_of', array($this->id)); - $this->_repeats = $repeatMap[$this->id]; - return $this->_repeats; + $repeatMap = Notice::listGet('repeat_of', array($this->id)); + $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) { $ids = self::_idsOf($notices); - $repeatMap = Memcached_DataObject::listGet('Notice', 'repeat_of', $ids); + $repeatMap = Notice::listGet('repeat_of', $ids); foreach ($notices as $notice) { $repeats = $repeatMap[$notice->id]; $notice->_setRepeats($repeats);