]> git.mxchange.org Git - quix0rs-gnu-social.git/blobdiff - classes/Notice.php
Merge branch 'admin-sections/4' into 0.9.x
[quix0rs-gnu-social.git] / classes / Notice.php
index bca4b22c4c76c73e45f1e84fa659c2fb3f98389a..ebb5022b9910f6e1c89d528a620d461ae3bdc3cc 100644 (file)
@@ -1,7 +1,7 @@
 <?php
-/*
- * Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
  *
  * You should have received a copy of the GNU Affero General Public License
  * along with this program.     If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Notices
+ * @package  StatusNet
+ * @author   Brenda Wallace <shiny@cpan.org>
+ * @author   Christopher Vollick <psycotica0@gmail.com>
+ * @author   CiaranG <ciaran@ciarang.com>
+ * @author   Craig Andrews <candrews@integralblue.com>
+ * @author   Evan Prodromou <evan@controlezvous.ca>
+ * @author   Gina Haeussge <osd@foosel.net>
+ * @author   Jeffery To <jeffery.to@gmail.com>
+ * @author   Mike Cochrane <mikec@mikenz.geek.nz>
+ * @author   Robin Millette <millette@controlyourself.ca>
+ * @author   Sarven Capadisli <csarven@controlyourself.ca>
+ * @author   Tom Adams <tom@holizz.com>
+ * @license  GNU Affero General Public License http://www.gnu.org/licenses/
  */
 
-if (!defined('LACONICA')) { exit(1); }
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+    exit(1);
+}
 
 /**
  * Table Definition for notice
@@ -29,6 +46,8 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
 
 define('NOTICE_CACHE_WINDOW', 61);
 
+define('MAX_BOXCARS', 128);
+
 class Notice extends Memcached_DataObject
 {
     ###START_AUTOCODE
@@ -38,7 +57,7 @@ class Notice extends Memcached_DataObject
     public $id;                              // int(4)  primary_key not_null
     public $profile_id;                      // int(4)   not_null
     public $uri;                             // varchar(255)  unique_key
-    public $content;                         // varchar(140)
+    public $content;                         // text()
     public $rendered;                        // text()
     public $url;                             // varchar(255)
     public $created;                         // datetime()   not_null
@@ -46,6 +65,11 @@ class Notice extends Memcached_DataObject
     public $reply_to;                        // int(4)
     public $is_local;                        // tinyint(1)
     public $source;                          // varchar(32)
+    public $conversation;                    // int(4)
+    public $lat;                             // decimal(10,7)
+    public $lon;                             // decimal(10,7)
+    public $location_id;                     // int(4)
+    public $location_ns;                     // int(4)
 
     /* Static get */
     function staticGet($k,$v=NULL) {
@@ -55,6 +79,12 @@ class Notice extends Memcached_DataObject
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
 
+    /* Notice types */
+    const LOCAL_PUBLIC    =  1;
+    const REMOTE_OMB      =  0;
+    const LOCAL_NONPUBLIC = -1;
+    const GATEWAY         = -2;
+
     function getProfile()
     {
         return Profile::staticGet('id', $this->profile_id);
@@ -66,17 +96,30 @@ class Notice extends Memcached_DataObject
         $this->blowFavesCache(true);
         $this->blowSubsCache(true);
 
+        // For auditing purposes, save a record that the notice
+        // was deleted.
+
+        $deleted = new Deleted_notice();
+
+        $deleted->id         = $this->id;
+        $deleted->profile_id = $this->profile_id;
+        $deleted->uri        = $this->uri;
+        $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));
         $related = array('Reply',
                          'Fave',
                          'Notice_tag',
                          'Group_inbox',
-                         'Queue_item');
-        if (common_config('inboxes', 'enabled')) {
-            $related[] = 'Notice_inbox';
-        }
+                         'Queue_item',
+                         'Notice_inbox');
+
         foreach ($related as $cls) {
             $inst = new $cls();
             $inst->notice_id = $this->id;
@@ -89,13 +132,20 @@ class Notice extends Memcached_DataObject
     function saveTags()
     {
         /* extract all #hastags */
-        $count = preg_match_all('/(?:^|\s)#([A-Za-z0-9_\-\.]{1,64})/', strtolower($this->content), $match);
+        $count = preg_match_all('/(?:^|\s)#([\pL\pN_\-\.]{1,64})/', strtolower($this->content), $match);
         if (!$count) {
             return true;
         }
 
+        //turn each into their canonical tag
+        //this is needed to remove dupes before saving e.g. #hash.tag = #hashtag
+        $hashtags = array();
+        for($i=0; $i<count($match[1]); $i++) {
+            $hashtags[] = common_canonical_tag($match[1][$i]);
+        }
+
         /* Add them to the database */
-        foreach(array_unique($match[1]) as $hashtag) {
+        foreach(array_unique($hashtags) as $hashtag) {
             /* elide characters we don't want in the tag */
             $this->saveTag($hashtag);
         }
@@ -104,8 +154,6 @@ class Notice extends Memcached_DataObject
 
     function saveTag($hashtag)
     {
-        $hashtag = common_canonical_tag($hashtag);
-
         $tag = new Notice_tag();
         $tag->notice_id = $this->id;
         $tag->tag = $hashtag;
@@ -119,90 +167,137 @@ class Notice extends Memcached_DataObject
         }
     }
 
-    static function saveNew($profile_id, $content, $source=null, $is_local=1, $reply_to=null, $uri=null) {
+    static function saveNew($profile_id, $content, $source=null,
+                            $is_local=Notice::LOCAL_PUBLIC, $reply_to=null, $uri=null, $created=null,
+                            $lat=null, $lon=null, $location_id=null, $location_ns=null) {
 
         $profile = Profile::staticGet($profile_id);
 
         $final = common_shorten_links($content);
 
-        if (mb_strlen($final) > 140) {
-            common_log(LOG_INFO, 'Rejecting notice that is too long.');
-            return _('Problem saving notice. Too long.');
+        if (Notice::contentTooLong($final)) {
+            throw new ClientException(_('Problem saving notice. Too long.'));
         }
 
-        if (!$profile) {
-            common_log(LOG_ERR, 'Problem saving notice. Unknown user.');
-            return _('Problem saving notice. Unknown user.');
+        if (empty($profile)) {
+            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.');
-            return _('Too many notices too fast; take a breather and post again in a few minutes.');
+            throw new ClientException(_('Too many notices too fast; take a breather '.
+                                        'and post again in a few minutes.'));
         }
 
         if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) {
             common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.');
-                       return _('Too many duplicate messages too quickly; take a breather and post again in a few minutes.');
+            throw new ClientException(_('Too many duplicate messages too quickly;'.
+                                        ' take a breather and post again in a few minutes.'));
         }
 
-               $banned = common_config('profile', 'banned');
-
-        if ( in_array($profile_id, $banned) || in_array($profile->nickname, $banned)) {
-            common_log(LOG_WARNING, "Attempted post from banned user: $profile->nickname (user id = $profile_id).");
-            return _('You are banned from posting notices on this site.');
+        if (!$profile->hasRight(Right::NEWNOTICE)) {
+            common_log(LOG_WARNING, "Attempted post from user disallowed to post: " . $profile->nickname);
+            throw new ClientException(_('You are banned from posting notices on this site.'));
         }
 
         $notice = new Notice();
         $notice->profile_id = $profile_id;
 
-        $blacklist = common_config('public', 'blacklist');
         $autosource = common_config('public', 'autosource');
 
-        # Blacklisted are non-false, but not 1, either
+        # Sandboxed are non-false, but not 1, either
 
-        if (($blacklist && in_array($profile_id, $blacklist)) ||
+        if (!$profile->hasRight(Right::PUBLICNOTICE) ||
             ($source && $autosource && in_array($source, $autosource))) {
-            $notice->is_local = -1;
+            $notice->is_local = Notice::LOCAL_NONPUBLIC;
         } else {
             $notice->is_local = $is_local;
         }
 
-               $notice->query('BEGIN');
+        if (!empty($created)) {
+            $notice->created = $created;
+        } else {
+            $notice->created = common_sql_now();
+        }
 
-               $notice->reply_to = $reply_to;
-               $notice->created = common_sql_now();
-               $notice->content = $final;
-               $notice->rendered = common_render_content($final, $notice);
-               $notice->source = $source;
-               $notice->uri = $uri;
+        $notice->content = $final;
+        $notice->rendered = common_render_content($final, $notice);
+        $notice->source = $source;
+        $notice->uri = $uri;
+
+        $notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final);
+
+        if (!empty($notice->reply_to)) {
+            $reply = Notice::staticGet('id', $notice->reply_to);
+            $notice->conversation = $reply->conversation;
+        }
+
+        if (!empty($lat) && !empty($lon)) {
+            $notice->lat = $lat;
+            $notice->lon = $lon;
+            $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) {
                 common_log_db_error($notice, 'INSERT', __FILE__);
-                return _('Problem saving notice.');
+                throw new ServerException(_('Problem saving notice.'));
             }
 
-            # Update the URI after the notice is in the database
-            if (!$uri) {
-                $orig = clone($notice);
+            // Update ID-dependent columns: URI, conversation
+
+            $orig = clone($notice);
+
+            $changed = false;
+
+            if (empty($uri)) {
                 $notice->uri = common_notice_uri($notice);
+                $changed = true;
+            }
+
+            // If it's not part of a conversation, it's
+            // the beginning of a new conversation.
+
+            if (empty($notice->conversation)) {
+                $notice->conversation = $notice->id;
+                $changed = true;
+            }
 
+            if ($changed) {
                 if (!$notice->update($orig)) {
                     common_log_db_error($notice, 'UPDATE', __FILE__);
-                    return _('Problem saving notice.');
+                    throw new ServerException(_('Problem saving notice.'));
                 }
             }
 
-            # XXX: do we need to change this for remote users?
+            // XXX: do we need to change this for remote users?
 
-            $notice->saveReplies();
             $notice->saveTags();
 
             $notice->addToInboxes();
-            $notice->saveGroups();
+
+            $notice->saveUrls();
 
             $notice->query('COMMIT');
 
@@ -217,13 +312,29 @@ class Notice extends Memcached_DataObject
         return $notice;
     }
 
+    /** save all urls in the notice to the db
+     *
+     * follow redirects and save all available file information
+     * (mimetype, date, size, oembed, etc.)
+     *
+     * @return void
+     */
+    function saveUrls() {
+        common_replace_urls_callback($this->content, array($this, 'saveUrl'), $this->id);
+    }
+
+    function saveUrl($data) {
+        list($url, $notice_id) = $data;
+        File::processNew($url, $notice_id);
+    }
+
     static function checkDupes($profile_id, $content) {
         $profile = Profile::staticGet($profile_id);
-        if (!$profile) {
+        if (empty($profile)) {
             return false;
         }
         $notice = $profile->getNotices(0, NOTICE_CACHE_WINDOW);
-        if ($notice) {
+        if (!empty($notice)) {
             $last = 0;
             while ($notice->fetch()) {
                 if (time() - strtotime($notice->created) >= common_config('site', 'dupelimit')) {
@@ -239,9 +350,9 @@ class Notice extends Memcached_DataObject
         $notice->profile_id = $profile_id;
         $notice->content = $content;
         if (common_config('db','type') == 'pgsql')
-            $notice->whereAdd('extract(epoch from now() - created) < ' . common_config('site', 'dupelimit'));
+          $notice->whereAdd('extract(epoch from now() - created) < ' . common_config('site', 'dupelimit'));
         else
-            $notice->whereAdd('now() - created < ' . common_config('site', 'dupelimit'));
+          $notice->whereAdd('now() - created < ' . common_config('site', 'dupelimit'));
 
         $cnt = $notice->count();
         return ($cnt == 0);
@@ -249,7 +360,7 @@ class Notice extends Memcached_DataObject
 
     static function checkEditThrottle($profile_id) {
         $profile = Profile::staticGet($profile_id);
-        if (!$profile) {
+        if (empty($profile)) {
             return false;
         }
         # Get the Nth notice
@@ -265,6 +376,44 @@ class Notice extends Memcached_DataObject
         return true;
     }
 
+    function getUploadedAttachment() {
+        $post = clone $this;
+        $query = 'select file.url as up, file.id as i from file join file_to_post on file.id = file_id where post_id=' . $post->escape($post->id) . ' and url like "%/notice/%/file"';
+        $post->query($query);
+        $post->fetch();
+        if (empty($post->up) || empty($post->i)) {
+            $ret = false;
+        } else {
+            $ret = array($post->up, $post->i);
+        }
+        $post->free();
+        return $ret;
+    }
+
+    function hasAttachments() {
+        $post = clone $this;
+        $query = "select count(file_id) as n_attachments from file join file_to_post on (file_id = file.id) join notice on (post_id = notice.id) where post_id = " . $post->escape($post->id);
+        $post->query($query);
+        $post->fetch();
+        $n_attachments = intval($post->n_attachments);
+        $post->free();
+        return $n_attachments;
+    }
+
+    function attachments() {
+        // XXX: cache this
+        $att = array();
+        $f2p = new File_to_post;
+        $f2p->post_id = $this->id;
+        if ($f2p->find()) {
+            while ($f2p->fetch()) {
+                $f = File::staticGet($f2p->file_id);
+                $att[] = clone($f);
+            }
+        }
+        return $att;
+    }
+
     function blowCaches($blowLast=false)
     {
         $this->blowSubsCache($blowLast);
@@ -273,6 +422,21 @@ class Notice extends Memcached_DataObject
         $this->blowPublicCache($blowLast);
         $this->blowTagCache($blowLast);
         $this->blowGroupCache($blowLast);
+        $this->blowConversationCache($blowLast);
+        $profile = Profile::staticGet($this->profile_id);
+        $profile->blowNoticeCount();
+    }
+
+    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)
@@ -313,6 +477,12 @@ class Notice extends Memcached_DataObject
             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();
@@ -334,8 +504,10 @@ class Notice extends Memcached_DataObject
 
             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();
@@ -377,7 +549,7 @@ class Notice extends Memcached_DataObject
 
     function blowPublicCache($blowLast=false)
     {
-        if ($this->is_local == 1) {
+        if ($this->is_local == Notice::LOCAL_PUBLIC) {
             $cache = common_memcache();
             if ($cache) {
                 $cache->delete(common_cache_key('public'));
@@ -397,8 +569,10 @@ class Notice extends Memcached_DataObject
             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'));
                     }
                 }
             }
@@ -507,7 +681,7 @@ class Notice extends Memcached_DataObject
 
         $cache = common_memcache();
 
-        if (!$cache) {
+        if (empty($cache)) {
             return Notice::getStreamDirect($qry, $offset, $limit, null, null, $order, null);
         }
 
@@ -568,7 +742,7 @@ class Notice extends Memcached_DataObject
 
         # If there are no hits, just return the value
 
-        if (!$notice) {
+        if (empty($notice)) {
             return $notice;
         }
 
@@ -601,11 +775,18 @@ class Notice extends Memcached_DataObject
         if (!empty($cache)) {
             $notices = array();
             foreach ($ids as $id) {
-                $notices[] = Notice::staticGet('id', $id);
+                $n = Notice::staticGet('id', $id);
+                if (!empty($n)) {
+                    $notices[] = $n;
+                }
             }
             return new ArrayWrapper($notices);
         } else {
             $notice = new Notice();
+            if (empty($ids)) {
+                //if no IDs requested, just return the notice object
+                return $notice;
+            }
             $notice->whereAdd('id in (' . implode(', ', $ids) . ')');
             $notice->orderBy('id DESC');
 
@@ -638,10 +819,62 @@ class Notice extends Memcached_DataObject
         }
 
         if (common_config('public', 'localonly')) {
-            $notice->whereAdd('is_local = 1');
+            $notice->whereAdd('is_local = ' . Notice::LOCAL_PUBLIC);
         } else {
-            # -1 == blacklisted
-            $notice->whereAdd('is_local != -1');
+            # -1 == blacklisted, -2 == gateway (i.e. Twitter)
+            $notice->whereAdd('is_local !='. Notice::LOCAL_NONPUBLIC);
+            $notice->whereAdd('is_local !='. Notice::GATEWAY);
+        }
+
+        if ($since_id != 0) {
+            $notice->whereAdd('id > ' . $since_id);
+        }
+
+        if ($max_id != 0) {
+            $notice->whereAdd('id <= ' . $max_id);
+        }
+
+        if (!is_null($since)) {
+            $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
+        }
+
+        $ids = array();
+
+        if ($notice->find()) {
+            while ($notice->fetch()) {
+                $ids[] = $notice->id;
+            }
+        }
+
+        $notice->free();
+        $notice = NULL;
+
+        return $ids;
+    }
+
+    function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null)
+    {
+        $ids = Notice::stream(array('Notice', '_conversationStreamDirect'),
+                              array($id),
+                              'notice:conversation_ids:'.$id,
+                              $offset, $limit, $since_id, $max_id, $since);
+
+        return Notice::getStreamByIds($ids);
+    }
+
+    function _conversationStreamDirect($id, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null)
+    {
+        $notice = new Notice();
+
+        $notice->selectAdd(); // clears it
+        $notice->selectAdd('id');
+
+        $notice->conversation = $id;
+
+        $notice->orderBy('id DESC');
+
+        if (!is_null($offset)) {
+            $notice->limit($offset, $limit);
         }
 
         if ($since_id != 0) {
@@ -672,40 +905,116 @@ class Notice extends Memcached_DataObject
 
     function addToInboxes()
     {
-        $enabled = common_config('inboxes', 'enabled');
+        // XXX: loads constants
 
-        if ($enabled === true || $enabled === 'transitional') {
-            $inbox = new Notice_inbox();
-            $UT = common_config('db','type')=='pgsql'?'"user"':'user';
-            $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created) ' .
-              "SELECT $UT.id, " . $this->id . ", '" . $this->created . "' " .
-              "FROM $UT JOIN subscription ON $UT.id = subscription.subscriber " .
-              'WHERE subscription.subscribed = ' . $this->profile_id . ' ' .
-              'AND NOT EXISTS (SELECT user_id, notice_id ' .
-              'FROM notice_inbox ' .
-              "WHERE user_id = $UT.id " .
-              'AND notice_id = ' . $this->id . ' )';
-            if ($enabled === 'transitional') {
-                $qry .= " AND $UT.inboxed = 1";
+        $inbox = new Notice_inbox();
+
+        $users = $this->getSubscribedUsers();
+
+        // FIXME: kind of ignoring 'transitional'...
+        // we'll probably stop supporting inboxless mode
+        // in 0.9.x
+
+        $ni = array();
+
+        foreach ($users as $id) {
+            $ni[$id] = NOTICE_INBOX_SOURCE_SUB;
+        }
+
+        $groups = $this->saveGroups();
+
+        foreach ($groups as $group) {
+            $users = $group->getUserMembers();
+            foreach ($users as $id) {
+                if (!array_key_exists($id, $ni)) {
+                    $user = User::staticGet('id', $id);
+                    if (!$user->hasBlocked($notice->profile_id)) {
+                        $ni[$id] = NOTICE_INBOX_SOURCE_GROUP;
+                    }
+                }
             }
+        }
+
+        $recipients = $this->saveReplies();
+
+        foreach ($recipients as $recipient) {
+
+            if (!array_key_exists($recipient, $ni)) {
+                $recipientUser = User::staticGet('id', $recipient);
+                if (!empty($recipientUser)) {
+                    $ni[$recipient] = NOTICE_INBOX_SOURCE_REPLY;
+                }
+            }
+        }
+
+        $cnt = 0;
+
+        $qryhdr = 'INSERT INTO notice_inbox (user_id, notice_id, source, created) VALUES ';
+        $qry = $qryhdr;
+
+        foreach ($ni as $id => $source) {
+            if ($cnt > 0) {
+                $qry .= ', ';
+            }
+            $qry .= '('.$id.', '.$this->id.', '.$source.", '".$this->created. "') ";
+            $cnt++;
+            if (rand() % NOTICE_INBOX_SOFT_LIMIT == 0) {
+                // FIXME: Causes lag in replicated servers
+                // Notice_inbox::gc($id);
+            }
+            if ($cnt >= MAX_BOXCARS) {
+                $inbox = new Notice_inbox();
+                $inbox->query($qry);
+                $qry = $qryhdr;
+                $cnt = 0;
+            }
+        }
+
+        if ($cnt > 0) {
+            $inbox = new Notice_inbox();
             $inbox->query($qry);
         }
+
         return;
     }
 
-    function saveGroups()
+    function getSubscribedUsers()
     {
-        $enabled = common_config('inboxes', 'enabled');
-        if ($enabled !== true && $enabled !== 'transitional') {
-            return;
+        $user = new User();
+
+        if(common_config('db','quote_identifiers'))
+          $user_table = '"user"';
+        else $user_table = 'user';
+
+        $qry =
+          'SELECT id ' .
+          'FROM '. $user_table .' JOIN subscription '.
+          'ON '. $user_table .'.id = subscription.subscriber ' .
+          'WHERE subscription.subscribed = %d ';
+
+        $user->query(sprintf($qry, $this->profile_id));
+
+        $ids = array();
+
+        while ($user->fetch()) {
+            $ids[] = $user->id;
         }
 
+        $user->free();
+
+        return $ids;
+    }
+
+    function saveGroups()
+    {
+        $groups = array();
+
         /* extract all !group */
         $count = preg_match_all('/(?:^|\s)!([A-Za-z0-9]{1,64})/',
                                 strtolower($this->content),
                                 $match);
         if (!$count) {
-            return true;
+            return $groups;
         }
 
         $profile = $this->getProfile();
@@ -714,16 +1023,16 @@ class Notice extends Memcached_DataObject
 
         foreach (array_unique($match[1]) as $nickname) {
             /* XXX: remote groups. */
-            $group = User_group::staticGet('nickname', $nickname);
+            $group = User_group::getForNickname($nickname);
 
-            if (!$group) {
+            if (empty($group)) {
                 continue;
             }
 
             // we automatically add a tag for every group name, too
 
             $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($nickname),
-                                           'notice_id' => $this->id));
+                                             'notice_id' => $this->id));
 
             if (is_null($tag)) {
                 $this->saveTag($nickname);
@@ -731,41 +1040,36 @@ class Notice extends Memcached_DataObject
 
             if ($profile->isMember($group)) {
 
-                $gi = new Group_inbox();
-
-                $gi->group_id  = $group->id;
-                $gi->notice_id = $this->id;
-                $gi->created   = common_sql_now();
-
-                $result = $gi->insert();
+                $result = $this->addToGroupInbox($group);
 
                 if (!$result) {
                     common_log_db_error($gi, 'INSERT', __FILE__);
                 }
 
-                // FIXME: do this in an offline daemon
-
-                $this->addToGroupInboxes($group);
+                $groups[] = clone($group);
             }
         }
+
+        return $groups;
     }
 
-    function addToGroupInboxes($group)
+    function addToGroupInbox($group)
     {
-        $inbox = new Notice_inbox();
-        $UT = common_config('db','type')=='pgsql'?'"user"':'user';
-        $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created, source) ' .
-          "SELECT $UT.id, " . $this->id . ", '" . $this->created . "', 2 " .
-          "FROM $UT JOIN group_member ON $UT.id = group_member.profile_id " .
-          'WHERE group_member.group_id = ' . $group->id . ' ' .
-          'AND NOT EXISTS (SELECT user_id, notice_id ' .
-          'FROM notice_inbox ' .
-          "WHERE user_id = $UT.id " .
-          'AND notice_id = ' . $this->id . ' )';
-        if ($enabled === 'transitional') {
-            $qry .= " AND $UT.inboxed = 1";
+        $gi = Group_inbox::pkeyGet(array('group_id' => $group->id,
+                                         'notice_id' => $this->id));
+
+        if (empty($gi)) {
+
+            $gi = new Group_inbox();
+
+            $gi->group_id  = $group->id;
+            $gi->notice_id = $this->id;
+            $gi->created   = $this->created;
+
+            return $gi->insert();
         }
-        $result = $inbox->query($qry);
+
+        return true;
     }
 
     function saveReplies()
@@ -795,21 +1099,12 @@ class Notice extends Memcached_DataObject
         for ($i=0; $i<count($names); $i++) {
             $nickname = $names[$i];
             $recipient = common_relative_profile($sender, $nickname, $this->created);
-            if (!$recipient) {
+            if (empty($recipient)) {
                 continue;
             }
-            if ($i == 0 && ($recipient->id != $sender->id) && !$this->reply_to) { // Don't save reply to self
-                $reply_for = $recipient;
-                $recipient_notice = $reply_for->getCurrentNotice();
-                if ($recipient_notice) {
-                    $orig = clone($this);
-                    $this->reply_to = $recipient_notice->id;
-                    $this->update($orig);
-                }
-            }
             // Don't save replies from blocked profile to local user
             $recipient_user = User::staticGet('id', $recipient->id);
-            if ($recipient_user && $recipient_user->hasBlocked($sender)) {
+            if (!empty($recipient_user) && $recipient_user->hasBlocked($sender)) {
                 continue;
             }
             $reply = new Reply();
@@ -820,7 +1115,7 @@ class Notice extends Memcached_DataObject
                 $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
                 common_log(LOG_ERR, 'DB error inserting reply: ' . $last_error->message);
                 common_server_error(sprintf(_('DB error inserting reply: %s'), $last_error->message));
-                return;
+                return array();
             } else {
                 $replied[$recipient->id] = 1;
             }
@@ -844,7 +1139,7 @@ class Notice extends Memcached_DataObject
                         $id = $reply->insert();
                         if (!$id) {
                             common_log_db_error($reply, 'INSERT', __FILE__);
-                            return;
+                            return array();
                         } else {
                             $replied[$recipient->id] = 1;
                         }
@@ -853,12 +1148,16 @@ class Notice extends Memcached_DataObject
             }
         }
 
-        foreach (array_keys($replied) as $recipient) {
+        $recipientIds = array_keys($replied);
+
+        foreach ($recipientIds as $recipient) {
             $user = User::staticGet('id', $recipient);
             if ($user) {
                 mail_notify_attn($user, $this);
             }
         }
+
+        return $recipientIds;
     }
 
     function asAtomEntry($namespace=false, $source=false)
@@ -882,10 +1181,9 @@ class Notice extends Memcached_DataObject
             $xs->element('link', array('href' => $profile->profileurl));
             $user = User::staticGet('id', $profile->id);
             if (!empty($user)) {
-                $atom_feed = common_local_url('api',
-                                              array('apiaction' => 'statuses',
-                                                    'method' => 'user_timeline',
-                                                    'argument' => $profile->nickname.'.atom'));
+                $atom_feed = common_local_url('ApiTimelineUser',
+                                              array('format' => 'atom',
+                                                    'id' => $profile->nickname));
                 $xs->element('link', array('rel' => 'self',
                                            'type' => 'application/atom+xml',
                                            'href' => $profile->profileurl));
@@ -938,6 +1236,27 @@ class Notice extends Memcached_DataObject
         }
         $tag->free();
 
+        # Enclosures
+        $attachments = $this->attachments();
+        if($attachments){
+            foreach($attachments as $attachment){
+                $enclosure=$attachment->getEnclosure();
+                if ($enclosure) {
+                    $attributes = array('rel'=>'enclosure','href'=>$enclosure->url,'type'=>$enclosure->mimetype,'length'=>$enclosure->size);
+                    if($enclosure->title){
+                        $attributes['title']=$enclosure->title;
+                    }
+                    $xs->element('link', $attributes, null);
+                }
+            }
+        }
+
+        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');
 
         return $xs->getString();
@@ -961,6 +1280,7 @@ class Notice extends Memcached_DataObject
 
         if (empty($cache) ||
             $since_id != 0 || $max_id != 0 || (!is_null($since) && $since > 0) ||
+            is_null($limit) ||
             ($offset + $limit) > NOTICE_CACHE_WINDOW) {
             return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id,
                                                                       $max_id, $since)));
@@ -1009,4 +1329,109 @@ class Notice extends Memcached_DataObject
 
         return $ids;
     }
+
+    /**
+     * 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.
+     *
+     * @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 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 $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_to;
+            }
+        }
+
+        // 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)) {
+            $nickname = common_canonical_nickname($match[1]);
+        } else {
+            return null;
+        }
+
+        // Figure out who that is.
+
+        $sender = Profile::staticGet('id', $profile_id);
+        $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->id;
+        }
+    }
+
+    static function maxContent()
+    {
+        $contentlimit = common_config('notice', 'contentlimit');
+        // null => use global limit (distinct from 0!)
+        if (is_null($contentlimit)) {
+            $contentlimit = common_config('site', 'textlimit');
+        }
+        return $contentlimit;
+    }
+
+    static function contentTooLong($content)
+    {
+        $contentlimit = self::maxContent();
+        return ($contentlimit > 0 && !empty($content) && (mb_strlen($content) > $contentlimit));
+    }
+
+    function getLocation()
+    {
+        $location = null;
+
+        if (!empty($this->location_id) && !empty($this->location_ns)) {
+            $location = Location::fromId($this->location_id, $this->location_ns);
+        }
+
+        if (is_null($location)) { // no ID, or Location::fromId() failed
+            if (!empty($this->lat) && !empty($this->lon)) {
+                $location = Location::fromLatLon($this->lat, $this->lon);
+            }
+        }
+
+        return $location;
+    }
 }