]> git.mxchange.org Git - quix0rs-gnu-social.git/blobdiff - classes/Notice.php
More Favorite pluginification (favecount, cache, menus(favecount, cache, menus))
[quix0rs-gnu-social.git] / classes / Notice.php
index 2cebc475747e4c426cecc3d1bfe37560cb862b39..4c831c7cc75645ccfdead4b2f1fc6cd5d8324e6d 100644 (file)
@@ -141,22 +141,26 @@ 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) {
+        if (!isset($this->_profile[$this->profile_id])) {
             $this->_setProfile(Profile::getKV('id', $this->profile_id));
         }
-        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($useWhere=false)
@@ -190,12 +194,11 @@ 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
         }
 
@@ -210,10 +213,62 @@ class Notice extends Managed_DataObject
         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...
-        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():
+            // 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)
@@ -308,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
@@ -410,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;
@@ -458,12 +523,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).
@@ -471,11 +546,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 (!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)) {
                     $groups = array();
@@ -578,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;
             }
 
@@ -768,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?
+                }
             }
         }
     }
@@ -777,7 +867,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) {
@@ -826,12 +920,11 @@ 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 = File_to_post::listGet('post_id', array($this->id));
@@ -844,14 +937,14 @@ class Notice extends Managed_DataObject
                
                $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)
@@ -979,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) {
@@ -1012,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;
                 }
             }
 
@@ -1064,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(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, $ids);
-
-        return;
-    }
-
     function getSubscribedUsers()
     {
         $user = new User();
@@ -1141,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.
@@ -1235,10 +1295,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;
             }
@@ -1273,19 +1332,17 @@ class Notice extends Managed_DataObject
             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!
         }
@@ -1339,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.
@@ -1348,8 +1405,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));
@@ -1360,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;
     }
 
     /**
@@ -1415,7 +1472,7 @@ class Notice extends Managed_DataObject
      * @return array of Group objects
      */
     
-    protected $_groups = -1;
+    protected $_groups = array();
     
     function getGroups()
     {
@@ -1425,9 +1482,8 @@ 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 = Group_inbox::listGet('notice_id', array($this->id));
@@ -1441,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;
     }
 
     /**
@@ -1472,7 +1528,12 @@ 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);
 
             $profile = $this->getProfile();
@@ -1520,8 +1581,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
             }
@@ -1668,16 +1729,18 @@ class Notice extends Managed_DataObject
 
         // favorite and repeated
 
+        $scoped = null;
         if (!empty($cur)) {
-            $cp = $cur->getProfile();
-            $noticeInfoAttr['favorite'] = ($cp->hasFave($this)) ? "true" : "false";
-            $noticeInfoAttr['repeated'] = ($cp->hasRepeated($this)) ? "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);
     }
 
@@ -1697,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;
-     * }
+     * 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;
@@ -1824,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::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');
-        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,
@@ -1947,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();
@@ -2008,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();
@@ -2066,33 +2062,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($this, $user->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;
         }
     }
 
@@ -2510,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);
@@ -2562,14 +2553,13 @@ 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)
@@ -2601,47 +2591,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);
@@ -2656,21 +2605,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)