]> git.mxchange.org Git - quix0rs-gnu-social.git/blobdiff - classes/Notice.php
Fix retarded spelling mistake
[quix0rs-gnu-social.git] / classes / Notice.php
index 7d2b898d262de8e200a9b2fde3d6150f09ecfdc4..924931e42b59989f0a266ad8ecba39028a64e6eb 100644 (file)
@@ -63,7 +63,7 @@ class Notice extends Memcached_DataObject
     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)
-    public $is_local;                        // tinyint(1)
+    public $is_local;                        // int(4)
     public $source;                          // varchar(32)
     public $conversation;                    // int(4)
     public $lat;                             // decimal(10,7)
@@ -94,10 +94,6 @@ class Notice extends Memcached_DataObject
 
     function delete()
     {
-        $this->blowCaches(true);
-        $this->blowFavesCache(true);
-        $this->blowSubsCache(true);
-
         // For auditing purposes, save a record that the notice
         // was deleted.
 
@@ -109,32 +105,20 @@ class Notice extends Memcached_DataObject
         $deleted->created    = $this->created;
         $deleted->deleted    = common_sql_now();
 
-        $this->query('BEGIN');
-
         $deleted->insert();
 
-        //Null any notices that are replies to this notice
-        $this->query(sprintf("UPDATE notice set reply_to = null WHERE reply_to = %d", $this->id));
-
-        //Null any notices that are repeats of this notice
-        //XXX: probably need to uncache these, too
+        // Clear related records
 
-        $this->query(sprintf("UPDATE notice set repeat_of = null WHERE repeat_of = %d", $this->id));
+        $this->clearReplies();
+        $this->clearRepeats();
+        $this->clearFaves();
+        $this->clearTags();
+        $this->clearGroupInboxes();
 
-        $related = array('Reply',
-                         'Fave',
-                         'Notice_tag',
-                         'Group_inbox',
-                         'Queue_item',
-                         'Notice_inbox');
+        // NOTE: we don't clear inboxes
+        // NOTE: we don't clear queue items
 
-        foreach ($related as $cls) {
-            $inst = new $cls();
-            $inst->notice_id = $this->id;
-            $inst->delete();
-        }
         $result = parent::delete();
-        $this->query('COMMIT');
     }
 
     function saveTags()
@@ -156,6 +140,7 @@ class Notice extends Memcached_DataObject
         foreach(array_unique($hashtags) as $hashtag) {
             /* elide characters we don't want in the tag */
             $this->saveTag($hashtag);
+            self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, $hashtag);
         }
         return true;
     }
@@ -173,18 +158,51 @@ class Notice extends Memcached_DataObject
                                               $last_error->message));
             return;
         }
+
+        // if it's saved, blow its cache
+        $tag->blowCache(false);
     }
 
+    /**
+     * Save a new notice and push it out to subscribers' inboxes.
+     * Poster's permissions are checked before sending.
+     *
+     * @param int $profile_id Profile ID of the poster
+     * @param string $content source message text; links may be shortened
+     *                        per current user's preference
+     * @param string $source source key ('web', 'api', etc)
+     * @param array $options Associative array of optional properties:
+     *              string 'created' timestamp of notice; defaults to now
+     *              int 'is_local' source/gateway ID, one of:
+     *                  Notice::LOCAL_PUBLIC    - Local, ok to appear in public timeline
+     *                  Notice::REMOTE_OMB      - Sent from a remote OMB service;
+     *                                            hide from public timeline but show in
+     *                                            local "and friends" timelines
+     *                  Notice::LOCAL_NONPUBLIC - Local, but hide from public timeline
+     *                  Notice::GATEWAY         - From another non-OMB service;
+     *                                            will not appear in public views
+     *              float 'lat' decimal latitude for geolocation
+     *              float 'lon' decimal longitude for geolocation
+     *              int 'location_id' geoname identifier
+     *              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' permalink to notice; defaults to local notice URL
+     *
+     * @return Notice
+     * @throws ClientException
+     */
     static function saveNew($profile_id, $content, $source, $options=null) {
+        $defaults = array('uri' => null,
+                          'reply_to' => null,
+                          'repeat_of' => null);
 
         if (!empty($options)) {
+            $options = $options + $defaults;
             extract($options);
-            if (!isset($reply_to)) {
-                $reply_to = NULL;
-            }
         }
 
-        if (empty($is_local)) {
+        if (!isset($is_local)) {
             $is_local = Notice::LOCAL_PUBLIC;
         }
 
@@ -246,7 +264,6 @@ class Notice extends Memcached_DataObject
 
         if (isset($repeat_of)) {
             $notice->repeat_of = $repeat_of;
-            $notice->reply_to = $repeat_of;
         } else {
             $notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final);
         }
@@ -259,29 +276,17 @@ class Notice extends Memcached_DataObject
         if (!empty($lat) && !empty($lon)) {
             $notice->lat = $lat;
             $notice->lon = $lon;
+        }
+
+        if (!empty($location_ns) && !empty($location_id)) {
             $notice->location_id = $location_id;
             $notice->location_ns = $location_ns;
-        } else if (!empty($location_ns) && !empty($location_id)) {
-            $location = Location::fromId($location_id, $location_ns);
-            if (!empty($location)) {
-                $notice->lat = $location->lat;
-                $notice->lon = $location->lon;
-                $notice->location_id = $location_id;
-                $notice->location_ns = $location_ns;
-            }
-        } else {
-            $notice->lat         = $profile->lat;
-            $notice->lon         = $profile->lon;
-            $notice->location_id = $profile->location_id;
-            $notice->location_ns = $profile->location_ns;
         }
 
         if (Event::handle('StartNoticeSave', array(&$notice))) {
 
             // XXX: some of these functions write to the DB
 
-            $notice->query('BEGIN');
-
             $id = $notice->insert();
 
             if (!$id) {
@@ -315,25 +320,41 @@ class Notice extends Memcached_DataObject
                 }
             }
 
-            // XXX: do we need to change this for remote users?
+        }
 
-            $notice->saveTags();
+        # Clear the cache for subscribed users, so they'll update at next request
+        # XXX: someone clever could prepend instead of clearing the cache
+        $notice->blowOnInsert();
 
-            $notice->addToInboxes();
+        $notice->distribute();
 
-            $notice->saveUrls();
+        return $notice;
+    }
 
-            $notice->query('COMMIT');
+    function blowOnInsert()
+    {
+        self::blow('profile:notice_ids:%d', $this->profile_id);
+        self::blow('public');
 
-            Event::handle('EndNoticeSave', array($notice));
+        if ($this->conversation != $this->id) {
+            self::blow('notice:conversation_ids:%d', $this->conversation);
         }
 
-        # Clear the cache for subscribed users, so they'll update at next request
-        # XXX: someone clever could prepend instead of clearing the cache
+        if (!empty($this->repeat_of)) {
+            self::blow('notice:repeats:%d', $this->repeat_of);
+        }
 
-        $notice->blowCaches();
+        $original = Notice::staticGet('id', $this->repeat_of);
 
-        return $notice;
+        if (!empty($original)) {
+            $originalUser = User::staticGet('id', $original->profile_id);
+            if (!empty($originalUser)) {
+                self::blow('user:repeats_of_me:%d', $originalUser->id);
+            }
+        }
+
+        $profile = Profile::staticGet($this->profile_id);
+        $profile->blowNoticeCount();
     }
 
     /** save all urls in the notice to the db
@@ -438,410 +459,6 @@ class Notice extends Memcached_DataObject
         return $att;
     }
 
-    function blowCaches($blowLast=false)
-    {
-        $this->blowSubsCache($blowLast);
-        $this->blowNoticeCache($blowLast);
-        $this->blowRepliesCache($blowLast);
-        $this->blowPublicCache($blowLast);
-        $this->blowTagCache($blowLast);
-        $this->blowGroupCache($blowLast);
-        $this->blowConversationCache($blowLast);
-        $this->blowRepeatCache();
-        $profile = Profile::staticGet($this->profile_id);
-        $profile->blowNoticeCount();
-    }
-
-    function blowRepeatCache()
-    {
-        if (!empty($this->repeat_of)) {
-            $cache = common_memcache();
-            if (!empty($cache)) {
-                // XXX: only blow if <100 in cache
-                $ck = common_cache_key('notice:repeats:'.$this->repeat_of);
-                $result = $cache->delete($ck);
-
-                $user = User::staticGet('id', $this->profile_id);
-
-                if (!empty($user)) {
-                    $uk = common_cache_key('user:repeated_by_me:'.$user->id);
-                    $cache->delete($uk);
-                    $user->free();
-                    unset($user);
-                }
-
-                $original = Notice::staticGet('id', $this->repeat_of);
-
-                if (!empty($original)) {
-                    $originalUser = User::staticGet('id', $original->profile_id);
-                    if (!empty($originalUser)) {
-                        $ouk = common_cache_key('user:repeats_of_me:'.$originalUser->id);
-                        $cache->delete($ouk);
-                        $originalUser->free();
-                        unset($originalUser);
-                    }
-                    $original->free();
-                    unset($original);
-                }
-
-                $ni = new Notice_inbox();
-
-                $ni->notice_id = $this->id;
-
-                if ($ni->find()) {
-                    while ($ni->fetch()) {
-                        $tmk = common_cache_key('user:repeated_to_me:'.$ni->user_id);
-                        $cache->delete($tmk);
-                    }
-                }
-
-                $ni->free();
-                unset($ni);
-            }
-        }
-    }
-
-    function blowConversationCache($blowLast=false)
-    {
-        $cache = common_memcache();
-        if ($cache) {
-            $ck = common_cache_key('notice:conversation_ids:'.$this->conversation);
-            $cache->delete($ck);
-            if ($blowLast) {
-                $cache->delete($ck.';last');
-            }
-        }
-    }
-
-    function blowGroupCache($blowLast=false)
-    {
-        $cache = common_memcache();
-        if ($cache) {
-            $group_inbox = new Group_inbox();
-            $group_inbox->notice_id = $this->id;
-            if ($group_inbox->find()) {
-                while ($group_inbox->fetch()) {
-                    $cache->delete(common_cache_key('user_group:notice_ids:' . $group_inbox->group_id));
-                    if ($blowLast) {
-                        $cache->delete(common_cache_key('user_group:notice_ids:' . $group_inbox->group_id.';last'));
-                    }
-                    $member = new Group_member();
-                    $member->group_id = $group_inbox->group_id;
-                    if ($member->find()) {
-                        while ($member->fetch()) {
-                            $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id));
-                            if ($blowLast) {
-                                $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id . ';last'));
-                            }
-                        }
-                    }
-                }
-            }
-            $group_inbox->free();
-            unset($group_inbox);
-        }
-    }
-
-    function blowTagCache($blowLast=false)
-    {
-        $cache = common_memcache();
-        if ($cache) {
-            $tag = new Notice_tag();
-            $tag->notice_id = $this->id;
-            if ($tag->find()) {
-                while ($tag->fetch()) {
-                    $tag->blowCache($blowLast);
-                    $ck = 'profile:notice_ids_tagged:' . $this->profile_id . ':' . $tag->tag;
-
-                    $cache->delete($ck);
-                    if ($blowLast) {
-                        $cache->delete($ck . ';last');
-                    }
-                }
-            }
-            $tag->free();
-            unset($tag);
-        }
-    }
-
-    function blowSubsCache($blowLast=false)
-    {
-        $cache = common_memcache();
-        if ($cache) {
-            $user = new User();
-
-            $UT = common_config('db','type')=='pgsql'?'"user"':'user';
-            $user->query('SELECT id ' .
-
-                         "FROM $UT JOIN subscription ON $UT.id = subscription.subscriber " .
-                         'WHERE subscription.subscribed = ' . $this->profile_id);
-
-            while ($user->fetch()) {
-                $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id));
-                $cache->delete(common_cache_key('notice_inbox:by_user_own:'.$user->id));
-                if ($blowLast) {
-                    $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id.';last'));
-                    $cache->delete(common_cache_key('notice_inbox:by_user_own:'.$user->id.';last'));
-                }
-            }
-            $user->free();
-            unset($user);
-        }
-    }
-
-    function blowNoticeCache($blowLast=false)
-    {
-        if ($this->is_local) {
-            $cache = common_memcache();
-            if (!empty($cache)) {
-                $cache->delete(common_cache_key('profile:notice_ids:'.$this->profile_id));
-                if ($blowLast) {
-                    $cache->delete(common_cache_key('profile:notice_ids:'.$this->profile_id.';last'));
-                }
-            }
-        }
-    }
-
-    function blowRepliesCache($blowLast=false)
-    {
-        $cache = common_memcache();
-        if ($cache) {
-            $reply = new Reply();
-            $reply->notice_id = $this->id;
-            if ($reply->find()) {
-                while ($reply->fetch()) {
-                    $cache->delete(common_cache_key('reply:stream:'.$reply->profile_id));
-                    if ($blowLast) {
-                        $cache->delete(common_cache_key('reply:stream:'.$reply->profile_id.';last'));
-                    }
-                }
-            }
-            $reply->free();
-            unset($reply);
-        }
-    }
-
-    function blowPublicCache($blowLast=false)
-    {
-        if ($this->is_local == Notice::LOCAL_PUBLIC) {
-            $cache = common_memcache();
-            if ($cache) {
-                $cache->delete(common_cache_key('public'));
-                if ($blowLast) {
-                    $cache->delete(common_cache_key('public').';last');
-                }
-            }
-        }
-    }
-
-    function blowFavesCache($blowLast=false)
-    {
-        $cache = common_memcache();
-        if ($cache) {
-            $fave = new Fave();
-            $fave->notice_id = $this->id;
-            if ($fave->find()) {
-                while ($fave->fetch()) {
-                    $cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id));
-                    $cache->delete(common_cache_key('fave:by_user_own:'.$fave->user_id));
-                    if ($blowLast) {
-                        $cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id.';last'));
-                        $cache->delete(common_cache_key('fave:by_user_own:'.$fave->user_id.';last'));
-                    }
-                }
-            }
-            $fave->free();
-            unset($fave);
-        }
-    }
-
-    # XXX: too many args; we need to move to named params or even a separate
-    # class for notice streams
-
-    static function getStream($qry, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0, $order=null, $since=null) {
-
-        if (common_config('memcached', 'enabled')) {
-
-            # Skip the cache if this is a since, since_id or max_id qry
-            if ($since_id > 0 || $max_id > 0 || $since) {
-                return Notice::getStreamDirect($qry, $offset, $limit, $since_id, $max_id, $order, $since);
-            } else {
-                return Notice::getCachedStream($qry, $cachekey, $offset, $limit, $order);
-            }
-        }
-
-        return Notice::getStreamDirect($qry, $offset, $limit, $since_id, $max_id, $order, $since);
-    }
-
-    static function getStreamDirect($qry, $offset, $limit, $since_id, $max_id, $order, $since) {
-
-        $needAnd = false;
-        $needWhere = true;
-
-        if (preg_match('/\bWHERE\b/i', $qry)) {
-            $needWhere = false;
-            $needAnd = true;
-        }
-
-        if ($since_id > 0) {
-
-            if ($needWhere) {
-                $qry .= ' WHERE ';
-                $needWhere = false;
-            } else {
-                $qry .= ' AND ';
-            }
-
-            $qry .= ' notice.id > ' . $since_id;
-        }
-
-        if ($max_id > 0) {
-
-            if ($needWhere) {
-                $qry .= ' WHERE ';
-                $needWhere = false;
-            } else {
-                $qry .= ' AND ';
-            }
-
-            $qry .= ' notice.id <= ' . $max_id;
-        }
-
-        if ($since) {
-
-            if ($needWhere) {
-                $qry .= ' WHERE ';
-                $needWhere = false;
-            } else {
-                $qry .= ' AND ';
-            }
-
-            $qry .= ' notice.created > \'' . date('Y-m-d H:i:s', $since) . '\'';
-        }
-
-        # Allow ORDER override
-
-        if ($order) {
-            $qry .= $order;
-        } else {
-            $qry .= ' ORDER BY notice.created DESC, notice.id DESC ';
-        }
-
-        if (common_config('db','type') == 'pgsql') {
-            $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
-        } else {
-            $qry .= ' LIMIT ' . $offset . ', ' . $limit;
-        }
-
-        $notice = new Notice();
-
-        $notice->query($qry);
-
-        return $notice;
-    }
-
-    # XXX: this is pretty long and should probably be broken up into
-    # some helper functions
-
-    static function getCachedStream($qry, $cachekey, $offset, $limit, $order) {
-
-        # If outside our cache window, just go to the DB
-
-        if ($offset + $limit > NOTICE_CACHE_WINDOW) {
-            return Notice::getStreamDirect($qry, $offset, $limit, null, null, $order, null);
-        }
-
-        # Get the cache; if we can't, just go to the DB
-
-        $cache = common_memcache();
-
-        if (empty($cache)) {
-            return Notice::getStreamDirect($qry, $offset, $limit, null, null, $order, null);
-        }
-
-        # Get the notices out of the cache
-
-        $notices = $cache->get(common_cache_key($cachekey));
-
-        # On a cache hit, return a DB-object-like wrapper
-
-        if ($notices !== false) {
-            $wrapper = new ArrayWrapper(array_slice($notices, $offset, $limit));
-            return $wrapper;
-        }
-
-        # If the cache was invalidated because of new data being
-        # added, we can try and just get the new stuff. We keep an additional
-        # copy of the data at the key + ';last'
-
-        # No cache hit. Try to get the *last* cached version
-
-        $last_notices = $cache->get(common_cache_key($cachekey) . ';last');
-
-        if ($last_notices) {
-
-            # Reverse-chron order, so last ID is last.
-
-            $last_id = $last_notices[0]->id;
-
-            # XXX: this assumes monotonically increasing IDs; a fair
-            # bet with our DB.
-
-            $new_notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW,
-                                                  $last_id, null, $order, null);
-
-            if ($new_notice) {
-                $new_notices = array();
-                while ($new_notice->fetch()) {
-                    $new_notices[] = clone($new_notice);
-                }
-                $new_notice->free();
-                $notices = array_slice(array_merge($new_notices, $last_notices),
-                                       0, NOTICE_CACHE_WINDOW);
-
-                # Store the array in the cache for next time
-
-                $result = $cache->set(common_cache_key($cachekey), $notices);
-                $result = $cache->set(common_cache_key($cachekey) . ';last', $notices);
-
-                # return a wrapper of the array for use now
-
-                return new ArrayWrapper(array_slice($notices, $offset, $limit));
-            }
-        }
-
-        # Otherwise, get the full cache window out of the DB
-
-        $notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW, null, null, $order, null);
-
-        # If there are no hits, just return the value
-
-        if (empty($notice)) {
-            return $notice;
-        }
-
-        # Pack results into an array
-
-        $notices = array();
-
-        while ($notice->fetch()) {
-            $notices[] = clone($notice);
-        }
-
-        $notice->free();
-
-        # Store the array in the cache for next time
-
-        $result = $cache->set(common_cache_key($cachekey), $notices);
-        $result = $cache->set(common_cache_key($cachekey) . ';last', $notices);
-
-        # return a wrapper of the array for use now
-
-        $wrapper = new ArrayWrapper(array_slice($notices, $offset, $limit));
-
-        return $wrapper;
-    }
-
     function getStreamByIds($ids)
     {
         $cache = common_memcache();
@@ -991,11 +608,28 @@ class Notice extends Memcached_DataObject
         return $ids;
     }
 
-    function addToInboxes()
+    /**
+     * @param $groups array of Group *objects*
+     * @param $recipients array of profile *ids*
+     */
+    function whoGets($groups=null, $recipients=null)
     {
-        // XXX: loads constants
+        $c = self::memcache();
+
+        if (!empty($c)) {
+            $ni = $c->get(common_cache_key('notice:who_gets:'.$this->id));
+            if ($ni !== false) {
+                return $ni;
+            }
+        }
+
+        if (is_null($groups)) {
+            $groups = $this->getGroups();
+        }
 
-        $inbox = new Notice_inbox();
+        if (is_null($recipients)) {
+            $recipients = $this->getReplies();
+        }
 
         $users = $this->getSubscribedUsers();
 
@@ -1009,7 +643,6 @@ class Notice extends Memcached_DataObject
             $ni[$id] = NOTICE_INBOX_SOURCE_SUB;
         }
 
-        $groups = $this->saveGroups();
         $profile = $this->getProfile();
 
         foreach ($groups as $group) {
@@ -1024,8 +657,6 @@ class Notice extends Memcached_DataObject
             }
         }
 
-        $recipients = $this->saveReplies();
-
         foreach ($recipients as $recipient) {
 
             if (!array_key_exists($recipient, $ni)) {
@@ -1036,7 +667,19 @@ class Notice extends Memcached_DataObject
             }
         }
 
-        Notice_inbox::bulkInsert($this->id, $this->created, $ni);
+        if (!empty($c)) {
+            // XXX: pack this data better
+            $c->set(common_cache_key('notice:who_gets:'.$this->id), $ni);
+        }
+
+        return $ni;
+    }
+
+    function addToInboxes($groups, $recipients)
+    {
+        $ni = $this->whoGets($groups, $recipients);
+
+        Inbox::bulkInsert($this->id, array_keys($ni));
 
         return;
     }
@@ -1068,8 +711,17 @@ class Notice extends Memcached_DataObject
         return $ids;
     }
 
+    /**
+     * @return array of Group objects
+     */
     function saveGroups()
     {
+        // Don't save groups for repeats
+
+        if (!empty($this->repeat_of)) {
+            return array();
+        }
+
         $groups = array();
 
         /* extract all !group */
@@ -1129,14 +781,30 @@ class Notice extends Memcached_DataObject
             $gi->notice_id = $this->id;
             $gi->created   = $this->created;
 
-            return $gi->insert();
+            $result = $gi->insert();
+
+            if (!$result) {
+                common_log_db_error($gi, 'INSERT', __FILE__);
+                throw new ServerException(_('Problem saving group inbox.'));
+            }
+
+            self::blow('user_group:notice_ids:%d', $gi->group_id);
         }
 
         return true;
     }
 
+    /**
+     * @return array of integer profile IDs
+     */
     function saveReplies()
     {
+        // Don't save reply data for repeats
+
+        if (!empty($this->repeat_of)) {
+            return array();
+        }
+
         // Alternative reply format
         $tname = false;
         if (preg_match('/^T ([A-Z0-9]{1,64}) /', $this->content, $match)) {
@@ -1213,9 +881,10 @@ class Notice extends Memcached_DataObject
 
         $recipientIds = array_keys($replied);
 
-        foreach ($recipientIds as $recipient) {
-            $user = User::staticGet('id', $recipient);
-            if ($user) {
+        foreach ($recipientIds as $recipientId) {
+            $user = User::staticGet('id', $recipientId);
+            if (!empty($user)) {
+                self::blow('reply:stream:%d', $reply->profile_id);
                 mail_notify_attn($user, $this);
             }
         }
@@ -1223,6 +892,63 @@ class Notice extends Memcached_DataObject
         return $recipientIds;
     }
 
+    function getReplies()
+    {
+        // XXX: cache me
+
+        $ids = array();
+
+        $reply = new Reply();
+        $reply->selectAdd();
+        $reply->selectAdd('profile_id');
+        $reply->notice_id = $this->id;
+
+        if ($reply->find()) {
+            while($reply->fetch()) {
+                $ids[] = $reply->profile_id;
+            }
+        }
+
+        $reply->free();
+
+        return $ids;
+    }
+
+    /**
+     * Same calculation as saveGroups but without the saving
+     * @fixme merge the functions
+     * @return array of Group_inbox objects
+     */
+    function getGroups()
+    {
+        // Don't save groups for repeats
+
+        if (!empty($this->repeat_of)) {
+            return array();
+        }
+
+        // XXX: cache me
+
+        $groups = array();
+
+        $gi = new Group_inbox();
+
+        $gi->selectAdd();
+        $gi->selectAdd('group_id');
+
+        $gi->notice_id = $this->id;
+
+        if ($gi->find()) {
+            while ($gi->fetch()) {
+                $groups[] = clone($gi);
+            }
+        }
+
+        $gi->free();
+
+        return $groups;
+    }
+
     function asAtomEntry($namespace=false, $source=false)
     {
         $profile = $this->getProfile();
@@ -1231,7 +957,10 @@ class Notice extends Memcached_DataObject
 
         if ($namespace) {
             $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
-                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0');
+                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0',
+                           'xmlns:georss' => 'http://www.georss.org/georss',
+                           'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
+                           'xmlns:ostatus' => 'http://ostatus.org/schema/1.0');
         } else {
             $attrs = array();
         }
@@ -1257,11 +986,6 @@ class Notice extends Memcached_DataObject
             $xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE));
         }
 
-        $xs->elementStart('author');
-        $xs->element('name', null, $profile->nickname);
-        $xs->element('uri', null, $profile->profileurl);
-        $xs->elementEnd('author');
-
         if ($source) {
             $xs->elementEnd('source');
         }
@@ -1269,13 +993,16 @@ class Notice extends Memcached_DataObject
         $xs->element('title', null, $this->content);
         $xs->element('summary', null, $this->content);
 
+        $xs->raw($profile->asAtomAuthor());
+        $xs->raw($profile->asActivityActor());
+
         $xs->element('link', array('rel' => 'alternate',
                                    'href' => $this->bestUrl()));
 
         $xs->element('id', null, $this->uri);
 
         $xs->element('published', null, common_date_w3dtf($this->created));
-        $xs->element('updated', null, common_date_w3dtf($this->modified));
+        $xs->element('updated', null, common_date_w3dtf($this->created));
 
         if ($this->reply_to) {
             $reply_notice = Notice::staticGet('id', $this->reply_to);
@@ -1288,6 +1015,43 @@ class Notice extends Memcached_DataObject
             }
         }
 
+        if (!empty($this->conversation)
+            && $this->conversation != $this->notice->id) {
+            $xs->element(
+                'link', array(
+                    'rel' => 'ostatus:conversation',
+                    'href' => common_local_url(
+                        'conversation',
+                        array('id' => $this->conversation)
+                        )
+                    )
+                );
+        }
+
+        $reply_ids = $this->getReplies();
+
+        foreach ($reply_ids as $id) {
+            $profile = Profile::staticGet('id', $id);
+            if (!empty($profile)) {
+                $xs->element(
+                    'link', array(
+                        'rel' => 'ostatus:attention',
+                        'href' => $profile->getAcctUri()
+                    )
+                );
+            }
+        }
+
+        if (!empty($this->repeat_of)) {
+            $repeat = Notice::staticGet('id', $this->repeat_of);
+            if (!empty($repeat)) {
+                $xs->element(
+                    'ostatus:forward',
+                     array('ref' => $repeat->uri, 'href' => $repeat->bestUrl())
+                );
+            }
+        }
+
         $xs->element('content', array('type' => 'html'), $this->rendered);
 
         $tag = new Notice_tag();
@@ -1315,9 +1079,7 @@ class Notice extends Memcached_DataObject
         }
 
         if (!empty($this->lat) && !empty($this->lon)) {
-            $xs->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
             $xs->element('georss:point', null, $this->lat . ' ' . $this->lon);
-            $xs->elementEnd('geo');
         }
 
         $xs->elementEnd('entry');
@@ -1353,7 +1115,7 @@ class Notice extends Memcached_DataObject
 
         $idstr = $cache->get($idkey);
 
-        if (!empty($idstr)) {
+        if ($idstr !== false) {
             // Cache hit! Woohoo!
             $window = explode(',', $idstr);
             $ids = array_slice($window, $offset, $limit);
@@ -1362,7 +1124,7 @@ class Notice extends Memcached_DataObject
 
         $laststr = $cache->get($idkey.';last');
 
-        if (!empty($laststr)) {
+        if ($laststr !== false) {
             $window = explode(',', $laststr);
             $last_id = $window[0];
             $new_ids = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW,
@@ -1450,6 +1212,10 @@ class Notice extends Memcached_DataObject
         // 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)) {
@@ -1502,12 +1268,21 @@ class Notice extends Memcached_DataObject
     {
         $author = Profile::staticGet('id', $this->profile_id);
 
-        // FIXME: truncate on long repeats...?
-
         $content = sprintf(_('RT @%1$s %2$s'),
                            $author->nickname,
                            $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) . ' ...';
+        }
+
         return self::saveNew($repeater_id, $content, $source,
                              array('repeat_of' => $this->id));
     }
@@ -1522,7 +1297,7 @@ class Notice extends Memcached_DataObject
             $ids = $this->_repeatStreamDirect($limit);
         } else {
             $idstr = $cache->get(common_cache_key('notice:repeats:'.$this->id));
-            if (!empty($idstr)) {
+            if ($idstr !== false) {
                 $ids = explode(',', $idstr);
             } else {
                 $ids = $this->_repeatStreamDirect(100);
@@ -1565,4 +1340,193 @@ class Notice extends Memcached_DataObject
 
         return $ids;
     }
+
+    function locationOptions($lat, $lon, $location_id, $location_ns, $profile = null)
+    {
+        $options = array();
+
+        if (!empty($location_id) && !empty($location_ns)) {
+
+            $options['location_id'] = $location_id;
+            $options['location_ns'] = $location_ns;
+
+            $location = Location::fromId($location_id, $location_ns);
+
+            if (!empty($location)) {
+                $options['lat'] = $location->lat;
+                $options['lon'] = $location->lon;
+            }
+
+        } else if (!empty($lat) && !empty($lon)) {
+
+            $options['lat'] = $lat;
+            $options['lon'] = $lon;
+
+            $location = Location::fromLatLon($lat, $lon);
+
+            if (!empty($location)) {
+                $options['location_id'] = $location->location_id;
+                $options['location_ns'] = $location->location_ns;
+            }
+        } else if (!empty($profile)) {
+
+            if (isset($profile->lat) && isset($profile->lon)) {
+                $options['lat'] = $profile->lat;
+                $options['lon'] = $profile->lon;
+            }
+
+            if (isset($profile->location_id) && isset($profile->location_ns)) {
+                $options['location_id'] = $profile->location_id;
+                $options['location_ns'] = $profile->location_ns;
+            }
+        }
+
+        return $options;
+    }
+
+    function clearReplies()
+    {
+        $replyNotice = new Notice();
+        $replyNotice->reply_to = $this->id;
+
+        //Null any notices that are replies to this notice
+
+        if ($replyNotice->find()) {
+            while ($replyNotice->fetch()) {
+                $orig = clone($replyNotice);
+                $replyNotice->reply_to = null;
+                $replyNotice->update($orig);
+            }
+        }
+
+        // Reply records
+
+        $reply = new Reply();
+        $reply->notice_id = $this->id;
+
+        if ($reply->find()) {
+            while($reply->fetch()) {
+                self::blow('reply:stream:%d', $reply->profile_id);
+                $reply->delete();
+            }
+        }
+
+        $reply->free();
+    }
+
+    function clearRepeats()
+    {
+        $repeatNotice = new Notice();
+        $repeatNotice->repeat_of = $this->id;
+
+        //Null any notices that are repeats of this notice
+
+        if ($repeatNotice->find()) {
+            while ($repeatNotice->fetch()) {
+                $orig = clone($repeatNotice);
+                $repeatNotice->repeat_of = null;
+                $repeatNotice->update($orig);
+            }
+        }
+    }
+
+    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();
+        $tag->notice_id = $this->id;
+
+        if ($tag->find()) {
+            while ($tag->fetch()) {
+                self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, common_keyize($tag->tag));
+                self::blow('profile:notice_ids_tagged:%d:%s;last', $this->profile_id, common_keyize($tag->tag));
+                self::blow('notice_tag:notice_ids:%s', common_keyize($tag->tag));
+                self::blow('notice_tag:notice_ids:%s;last', common_keyize($tag->tag));
+                $tag->delete();
+            }
+        }
+
+        $tag->free();
+    }
+
+    function clearGroupInboxes()
+    {
+        $gi = new Group_inbox();
+
+        $gi->notice_id = $this->id;
+
+        if ($gi->find()) {
+            while ($gi->fetch()) {
+                self::blow('user_group:notice_ids:%d', $gi->group_id);
+                $gi->delete();
+            }
+        }
+
+        $gi->free();
+    }
+
+    function distribute()
+    {
+        if (common_config('queue', 'inboxes')) {
+            // 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 {
+                    $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;
+            }
+        } else {
+            $handler = new DistribQueueHandler();
+            $handler->handle($this);
+        }
+    }
+
+    function insert()
+    {
+        $result = parent::insert();
+
+        if ($result) {
+            // 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);
+                }
+            }
+        }
+
+        return $result;
+    }
 }