We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

Notice.php 58.2 KB
Newer Older
Evan Prodromou's avatar
Evan Prodromou committed
1
<?php
Brenda Wallace's avatar
Brenda Wallace committed
2
/**
3
 * StatusNet - the distributed open-source microblogging tool
4
 * Copyright (C) 2008, 2009, StatusNet, Inc.
Evan Prodromou's avatar
Evan Prodromou committed
5
 *
Evan Prodromou's avatar
Evan Prodromou committed
6 7 8 9
 * 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
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
Evan Prodromou's avatar
Evan Prodromou committed
10
 *
Evan Prodromou's avatar
Evan Prodromou committed
11 12
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.     See the
Evan Prodromou's avatar
Evan Prodromou committed
14
 * GNU Affero General Public License for more details.
Evan Prodromou's avatar
Evan Prodromou committed
15
 *
Evan Prodromou's avatar
Evan Prodromou committed
16
 * You should have received a copy of the GNU Affero General Public License
17
 * along with this program.     If not, see <http://www.gnu.org/licenses/>.
Evan Prodromou's avatar
Evan Prodromou committed
18
 *
Brenda Wallace's avatar
Brenda Wallace committed
19 20 21 22 23 24 25 26 27 28 29 30 31
 * @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>
32
 * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
Brenda Wallace's avatar
Brenda Wallace committed
33
 * @license  GNU Affero General Public License http://www.gnu.org/licenses/
Evan Prodromou's avatar
Evan Prodromou committed
34
 */
Evan Prodromou's avatar
Evan Prodromou committed
35

Evan Prodromou's avatar
Evan Prodromou committed
36 37
if (!defined('STATUSNET') && !defined('LACONICA')) {
    exit(1);
Brenda Wallace's avatar
Brenda Wallace committed
38
}
Evan Prodromou's avatar
Evan Prodromou committed
39

Evan Prodromou's avatar
Evan Prodromou committed
40
/**
Evan Prodromou's avatar
Evan Prodromou committed
41 42
 * Table Definition for notice
 */
43
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
Evan Prodromou's avatar
Evan Prodromou committed
44

45
/* We keep 200 notices, the max number of notices available per API request,
46 47
 * in the memcached cache. */

48
define('NOTICE_CACHE_WINDOW', 200);
49

Evan Prodromou's avatar
Evan Prodromou committed
50 51
define('MAX_BOXCARS', 128);

52
class Notice extends Memcached_DataObject
Evan Prodromou's avatar
Evan Prodromou committed
53
{
54 55
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */
Evan Prodromou's avatar
Evan Prodromou committed
56

57 58
    public $__table = 'notice';                          // table name
    public $id;                              // int(4)  primary_key not_null
59
    public $profile_id;                      // int(4)  multiple_key not_null
60
    public $uri;                             // varchar(255)  unique_key
61 62
    public $content;                         // text
    public $rendered;                        // text
63
    public $url;                             // varchar(255)
64 65
    public $created;                         // datetime  multiple_key not_null default_0000-00-00%2000%3A00%3A00
    public $modified;                        // timestamp   not_null default_CURRENT_TIMESTAMP
66
    public $reply_to;                        // int(4)
67
    public $is_local;                        // int(4)
68
    public $source;                          // varchar(32)
69
    public $conversation;                    // int(4)
70 71 72 73
    public $lat;                             // decimal(10,7)
    public $lon;                             // decimal(10,7)
    public $location_id;                     // int(4)
    public $location_ns;                     // int(4)
74
    public $repeat_of;                       // int(4)
Evan Prodromou's avatar
Evan Prodromou committed
75

76
    /* Static get */
77 78
    function staticGet($k,$v=NULL)
    {
79 80
        return Memcached_DataObject::staticGet('Notice',$k,$v);
    }
Evan Prodromou's avatar
Evan Prodromou committed
81

82 83
    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE
Evan Prodromou's avatar
Evan Prodromou committed
84

85
    /* Notice types */
86 87 88 89
    const LOCAL_PUBLIC    =  1;
    const REMOTE_OMB      =  0;
    const LOCAL_NONPUBLIC = -1;
    const GATEWAY         = -2;
Evan Prodromou's avatar
Evan Prodromou committed
90

91 92
    function getProfile()
    {
93 94 95 96 97 98 99
        $profile = Profile::staticGet('id', $this->profile_id);

        if (empty($profile)) {
            throw new ServerException(sprintf(_('No such profile (%d) for notice (%d)'), $this->profile_id, $this->id));
        }

        return $profile;
100
    }
Evan Prodromou's avatar
Evan Prodromou committed
101

102 103
    function delete()
    {
104 105
        // For auditing purposes, save a record that the notice
        // was deleted.
Evan Prodromou's avatar
Evan Prodromou committed
106

107 108 109 110 111
        // @fixme we have some cases where things get re-run and so the
        // insert fails.
        $deleted = Deleted_notice::staticGet('id', $this->id);
        if (!$deleted) {
            $deleted = new Deleted_notice();
Evan Prodromou's avatar
Evan Prodromou committed
112

113 114 115 116 117
            $deleted->id         = $this->id;
            $deleted->profile_id = $this->profile_id;
            $deleted->uri        = $this->uri;
            $deleted->created    = $this->created;
            $deleted->deleted    = common_sql_now();
Evan Prodromou's avatar
Evan Prodromou committed
118

119 120
            $deleted->insert();
        }
Evan Prodromou's avatar
Evan Prodromou committed
121

122
        // Clear related records
123

124 125 126 127 128
        $this->clearReplies();
        $this->clearRepeats();
        $this->clearFaves();
        $this->clearTags();
        $this->clearGroupInboxes();
129

130 131
        // NOTE: we don't clear inboxes
        // NOTE: we don't clear queue items
Evan Prodromou's avatar
Evan Prodromou committed
132

Evan Prodromou's avatar
Evan Prodromou committed
133
        $result = parent::delete();
134 135 136

        $this->blowOnDelete();
        return $result;
137
    }
Evan Prodromou's avatar
Evan Prodromou committed
138

139 140 141
    /**
     * Extract #hashtags from this notice's content and save them to the database.
     */
142 143
    function saveTags()
    {
144
        /* extract all #hastags */
145
        $count = preg_match_all('/(?:^|\s)#([\pL\pN_\-\.]{1,64})/', strtolower($this->content), $match);
146 147 148
        if (!$count) {
            return true;
        }
Evan Prodromou's avatar
Evan Prodromou committed
149

150 151 152 153 154 155 156 157 158 159
        /* Add them to the database */
        return $this->saveKnownTags($match[1]);
    }

    /**
     * Record the given set of hash tags in the db for this notice.
     * Given tag strings will be normalized and checked for dupes.
     */
    function saveKnownTags($hashtags)
    {
160 161
        //turn each into their canonical tag
        //this is needed to remove dupes before saving e.g. #hash.tag = #hashtag
162
        for($i=0; $i<count($hashtags); $i++) {
Brion Vibber's avatar
Brion Vibber committed
163
            /* elide characters we don't want in the tag */
164
            $hashtags[$i] = common_canonical_tag($hashtags[$i]);
165
        }
Evan Prodromou's avatar
Evan Prodromou committed
166

167
        foreach(array_unique($hashtags) as $hashtag) {
168
            $this->saveTag($hashtag);
169
            self::blow('profile:notice_ids_tagged:%d:%s', $this->profile_id, $hashtag);
170 171 172
        }
        return true;
    }
Evan Prodromou's avatar
Evan Prodromou committed
173

174 175 176 177
    /**
     * Record a single hash tag as associated with this notice.
     * Tag format and uniqueness must be validated by caller.
     */
178 179 180 181 182 183 184
    function saveTag($hashtag)
    {
        $tag = new Notice_tag();
        $tag->notice_id = $this->id;
        $tag->tag = $hashtag;
        $tag->created = $this->created;
        $id = $tag->insert();
Evan Prodromou's avatar
Evan Prodromou committed
185

186
        if (!$id) {
187 188
            // TRANS: Server exception. %s are the error details.
            throw new ServerException(sprintf(_('Database error inserting hashtag: %s'),
189 190 191
                                              $last_error->message));
            return;
        }
192 193 194

        // if it's saved, blow its cache
        $tag->blowCache(false);
195
    }
Evan Prodromou's avatar
Evan Prodromou committed
196

197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
    /**
     * 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
221 222 223 224 225 226 227
     *              string 'uri' unique ID for notice; defaults to local notice URL
     *              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
     *                              place of extracting @-replies from content.
     *              array 'groups' list of group IDs to deliver to, in place of
     *                              extracting ! tags from content
228 229
     *              array 'tags' list of hashtag strings to save with the notice
     *                           in place of extracting # tags from content
230 231
     *              array 'urls' list of attached/referred URLs to save with the
     *                           notice in place of extracting links from content
232
     * @fixme tag override
233 234 235 236
     *
     * @return Notice
     * @throws ClientException
     */
237
    static function saveNew($profile_id, $content, $source, $options=null) {
238
        $defaults = array('uri' => null,
239
                          'url' => null,
240 241
                          'reply_to' => null,
                          'repeat_of' => null);
242 243

        if (!empty($options)) {
244
            $options = $options + $defaults;
245 246 247
            extract($options);
        }

248
        if (!isset($is_local)) {
249 250
            $is_local = Notice::LOCAL_PUBLIC;
        }
Evan Prodromou's avatar
Evan Prodromou committed
251 252 253 254 255 256 257 258 259

        $profile = Profile::staticGet($profile_id);

        $final = common_shorten_links($content);

        if (Notice::contentTooLong($final)) {
            throw new ClientException(_('Problem saving notice. Too long.'));
        }

260
        if (empty($profile)) {
Evan Prodromou's avatar
Evan Prodromou committed
261 262 263 264 265 266
            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.');
            throw new ClientException(_('Too many notices too fast; take a breather '.
267
                                        'and post again in a few minutes.'));
Evan Prodromou's avatar
Evan Prodromou committed
268 269 270 271 272
        }

        if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) {
            common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.');
            throw new ClientException(_('Too many duplicate messages too quickly;'.
273
                                        ' take a breather and post again in a few minutes.'));
Evan Prodromou's avatar
Evan Prodromou committed
274 275
        }

276 277
        if (!$profile->hasRight(Right::NEWNOTICE)) {
            common_log(LOG_WARNING, "Attempted post from user disallowed to post: " . $profile->nickname);
278
            throw new ClientException(_('You are banned from posting notices on this site.'), 403);
Evan Prodromou's avatar
Evan Prodromou committed
279 280 281 282 283 284 285
        }

        $notice = new Notice();
        $notice->profile_id = $profile_id;

        $autosource = common_config('public', 'autosource');

286
        # Sandboxed are non-false, but not 1, either
Evan Prodromou's avatar
Evan Prodromou committed
287

288
        if (!$profile->hasRight(Right::PUBLICNOTICE) ||
Evan Prodromou's avatar
Evan Prodromou committed
289 290 291 292 293 294 295 296 297 298 299 300 301
            ($source && $autosource && in_array($source, $autosource))) {
            $notice->is_local = Notice::LOCAL_NONPUBLIC;
        } else {
            $notice->is_local = $is_local;
        }

        if (!empty($created)) {
            $notice->created = $created;
        } else {
            $notice->created = common_sql_now();
        }

        $notice->content = $final;
302

Evan Prodromou's avatar
Evan Prodromou committed
303 304
        $notice->source = $source;
        $notice->uri = $uri;
305
        $notice->url = $url;
Evan Prodromou's avatar
Evan Prodromou committed
306

Evan Prodromou's avatar
Evan Prodromou committed
307 308 309 310 311 312 313
        // Handle repeat case

        if (isset($repeat_of)) {
            $notice->repeat_of = $repeat_of;
        } else {
            $notice->reply_to = self::getReplyTo($reply_to, $profile_id, $source, $final);
        }
Evan Prodromou's avatar
Evan Prodromou committed
314 315 316 317 318 319

        if (!empty($notice->reply_to)) {
            $reply = Notice::staticGet('id', $notice->reply_to);
            $notice->conversation = $reply->conversation;
        }

320 321 322
        if (!empty($lat) && !empty($lon)) {
            $notice->lat = $lat;
            $notice->lon = $lon;
323 324 325
        }

        if (!empty($location_ns) && !empty($location_id)) {
326 327 328 329
            $notice->location_id = $location_id;
            $notice->location_ns = $location_ns;
        }

330 331 332 333 334 335
        if (!empty($rendered)) {
            $notice->rendered = $rendered;
        } else {
            $notice->rendered = common_render_content($final, $notice);
        }

Evan Prodromou's avatar
Evan Prodromou committed
336 337
        if (Event::handle('StartNoticeSave', array(&$notice))) {

338
            // XXX: some of these functions write to the DB
Evan Prodromou's avatar
Evan Prodromou committed
339 340 341 342 343 344 345 346

            $id = $notice->insert();

            if (!$id) {
                common_log_db_error($notice, 'INSERT', __FILE__);
                throw new ServerException(_('Problem saving notice.'));
            }

347
            // Update ID-dependent columns: URI, conversation
Evan Prodromou's avatar
Evan Prodromou committed
348 349 350 351 352 353 354 355 356 357

            $orig = clone($notice);

            $changed = false;

            if (empty($uri)) {
                $notice->uri = common_notice_uri($notice);
                $changed = true;
            }

358 359
            // If it's not part of a conversation, it's
            // the beginning of a new conversation.
Evan Prodromou's avatar
Evan Prodromou committed
360 361

            if (empty($notice->conversation)) {
362 363
                $conv = Conversation::create();
                $notice->conversation = $conv->id;
Evan Prodromou's avatar
Evan Prodromou committed
364 365 366 367 368 369 370 371 372 373
                $changed = true;
            }

            if ($changed) {
                if (!$notice->update($orig)) {
                    common_log_db_error($notice, 'UPDATE', __FILE__);
                    throw new ServerException(_('Problem saving notice.'));
                }
            }

374 375 376 377
        }

        # Clear the cache for subscribed users, so they'll update at next request
        # XXX: someone clever could prepend instead of clearing the cache
378

379
        $notice->blowOnInsert();
Evan Prodromou's avatar
Evan Prodromou committed
380

381 382
        // Save per-notice metadata...

383 384 385 386 387 388
        if (isset($replies)) {
            $notice->saveKnownReplies($replies);
        } else {
            $notice->saveReplies();
        }

389 390 391 392 393 394
        if (isset($tags)) {
            $notice->saveKnownTags($tags);
        } else {
            $notice->saveTags();
        }

395 396 397 398 399 400 401 402
        // Note: groups may save tags, so must be run after tags are saved
        // to avoid errors on duplicates.
        if (isset($groups)) {
            $notice->saveKnownGroups($groups);
        } else {
            $notice->saveGroups();
        }

403 404 405 406 407
        if (isset($urls)) {
            $notice->saveKnownUrls($urls);
        } else {
            $notice->saveUrls();
        }
408 409

        // Prepare inbox delivery, may be queued to background.
410
        $notice->distribute();
411

412 413
        return $notice;
    }
414

415
    function blowOnInsert($conversation = false)
416 417 418
    {
        self::blow('profile:notice_ids:%d', $this->profile_id);
        self::blow('public');
Evan Prodromou's avatar
Evan Prodromou committed
419

420 421 422 423
        // XXX: Before we were blowing the casche only if the notice id
        // was not the root of the conversation.  What to do now?

        self::blow('notice:conversation_ids:%d', $this->conversation);
Evan Prodromou's avatar
Evan Prodromou committed
424

425 426
        if (!empty($this->repeat_of)) {
            self::blow('notice:repeats:%d', $this->repeat_of);
Evan Prodromou's avatar
Evan Prodromou committed
427 428
        }

429
        $original = Notice::staticGet('id', $this->repeat_of);
Evan Prodromou's avatar
Evan Prodromou committed
430

431 432 433 434 435 436
        if (!empty($original)) {
            $originalUser = User::staticGet('id', $original->profile_id);
            if (!empty($originalUser)) {
                self::blow('user:repeats_of_me:%d', $originalUser->id);
            }
        }
Evan Prodromou's avatar
Evan Prodromou committed
437

438
        $profile = Profile::staticGet($this->profile_id);
439 440 441
        if (!empty($profile)) {
            $profile->blowNoticeCount();
        }
Evan Prodromou's avatar
Evan Prodromou committed
442 443
    }

444 445 446 447 448 449 450 451 452 453 454 455
    /**
     * Clear cache entries related to this notice at delete time.
     * Necessary to avoid breaking paging on public, profile timelines.
     */
    function blowOnDelete()
    {
        $this->blowOnInsert();

        self::blow('profile:notice_ids:%d;last', $this->profile_id);
        self::blow('public;last');
    }

456
    /** save all urls in the notice to the db
Evan Prodromou's avatar
Evan Prodromou committed
457 458 459 460 461 462 463 464 465 466
     *
     * 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);
    }

467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485
    /**
     * Save the given URLs as related links/attachments to the db
     *
     * follow redirects and save all available file information
     * (mimetype, date, size, oembed, etc.)
     *
     * @return void
     */
    function saveKnownUrls($urls)
    {
        // @fixme validation?
        foreach ($urls as $url) {
            File::processNew($url, $this->id);
        }
    }

    /**
     * @private callback
     */
486 487 488 489
    function saveUrl($data) {
        list($url, $notice_id) = $data;
        File::processNew($url, $notice_id);
    }
Evan Prodromou's avatar
Evan Prodromou committed
490

491 492
    static function checkDupes($profile_id, $content) {
        $profile = Profile::staticGet($profile_id);
493
        if (empty($profile)) {
494 495 496
            return false;
        }
        $notice = $profile->getNotices(0, NOTICE_CACHE_WINDOW);
497
        if (!empty($notice)) {
498 499 500 501 502 503 504 505 506 507 508 509 510 511
            $last = 0;
            while ($notice->fetch()) {
                if (time() - strtotime($notice->created) >= common_config('site', 'dupelimit')) {
                    return true;
                } else if ($notice->content == $content) {
                    return false;
                }
            }
        }
        # If we get here, oldest item in cache window is not
        # old enough for dupe limit; do direct check against DB
        $notice = new Notice();
        $notice->profile_id = $profile_id;
        $notice->content = $content;
512
        if (common_config('db','type') == 'pgsql')
Evan Prodromou's avatar
Evan Prodromou committed
513
          $notice->whereAdd('extract(epoch from now() - created) < ' . common_config('site', 'dupelimit'));
514
        else
Evan Prodromou's avatar
Evan Prodromou committed
515 516
          $notice->whereAdd('now() - created < ' . common_config('site', 'dupelimit'));

517
        $cnt = $notice->count();
518
        return ($cnt == 0);
519
    }
Evan Prodromou's avatar
Evan Prodromou committed
520

Evan Prodromou's avatar
Evan Prodromou committed
521 522
    static function checkEditThrottle($profile_id) {
        $profile = Profile::staticGet($profile_id);
523
        if (empty($profile)) {
Evan Prodromou's avatar
Evan Prodromou committed
524 525 526 527 528 529 530 531 532 533 534 535 536 537
            return false;
        }
        # Get the Nth notice
        $notice = $profile->getNotices(common_config('throttle', 'count') - 1, 1);
        if ($notice && $notice->fetch()) {
            # If the Nth notice was posted less than timespan seconds ago
            if (time() - strtotime($notice->created) <= common_config('throttle', 'timespan')) {
                # Then we throttle
                return false;
            }
        }
        # Either not N notices in the stream, OR the Nth was not posted within timespan seconds
        return true;
    }
Evan Prodromou's avatar
Evan Prodromou committed
538

539 540
    function getUploadedAttachment() {
        $post = clone $this;
541
        $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"';
542 543
        $post->query($query);
        $post->fetch();
544 545 546 547 548
        if (empty($post->up) || empty($post->i)) {
            $ret = false;
        } else {
            $ret = array($post->up, $post->i);
        }
549 550 551
        $post->free();
        return $ret;
    }
Evan Prodromou's avatar
Evan Prodromou committed
552

553
    function hasAttachments() {
554 555
        $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);
556 557 558 559 560 561
        $post->query($query);
        $post->fetch();
        $n_attachments = intval($post->n_attachments);
        $post->free();
        return $n_attachments;
    }
Evan Prodromou's avatar
Evan Prodromou committed
562

Evan Prodromou's avatar
Evan Prodromou committed
563 564 565 566 567 568 569 570 571 572 573 574 575
    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;
    }
Evan Prodromou's avatar
Evan Prodromou committed
576

577 578 579
    function getStreamByIds($ids)
    {
        $cache = common_memcache();
Evan Prodromou's avatar
Evan Prodromou committed
580

581 582 583
        if (!empty($cache)) {
            $notices = array();
            foreach ($ids as $id) {
584 585 586 587
                $n = Notice::staticGet('id', $id);
                if (!empty($n)) {
                    $notices[] = $n;
                }
588 589 590 591
            }
            return new ArrayWrapper($notices);
        } else {
            $notice = new Notice();
592 593 594 595
            if (empty($ids)) {
                //if no IDs requested, just return the notice object
                return $notice;
            }
596
            $notice->whereAdd('id in (' . implode(', ', $ids) . ')');
Evan Prodromou's avatar
Evan Prodromou committed
597

598
            $notice->find();
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614

            $temp = array();

            while ($notice->fetch()) {
                $temp[$notice->id] = clone($notice);
            }

            $wrapped = array();

            foreach ($ids as $id) {
                if (array_key_exists($id, $temp)) {
                    $wrapped[] = $temp[$id];
                }
            }

            return new ArrayWrapper($wrapped);
615 616
        }
    }
Evan Prodromou's avatar
Evan Prodromou committed
617

618
    function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0)
619
    {
Evan Prodromou's avatar
Evan Prodromou committed
620 621 622
        $ids = Notice::stream(array('Notice', '_publicStreamDirect'),
                              array(),
                              'public',
623
                              $offset, $limit, $since_id, $max_id);
Evan Prodromou's avatar
Evan Prodromou committed
624 625
        return Notice::getStreamByIds($ids);
    }
Evan Prodromou's avatar
Evan Prodromou committed
626

627
    function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $max_id=0)
Evan Prodromou's avatar
Evan Prodromou committed
628 629
    {
        $notice = new Notice();
Evan Prodromou's avatar
Evan Prodromou committed
630

Evan Prodromou's avatar
Evan Prodromou committed
631 632
        $notice->selectAdd(); // clears it
        $notice->selectAdd('id');
Evan Prodromou's avatar
Evan Prodromou committed
633

Evan Prodromou's avatar
Evan Prodromou committed
634
        $notice->orderBy('id DESC');
Evan Prodromou's avatar
Evan Prodromou committed
635

Evan Prodromou's avatar
Evan Prodromou committed
636 637 638
        if (!is_null($offset)) {
            $notice->limit($offset, $limit);
        }
Evan Prodromou's avatar
Evan Prodromou committed
639

640
        if (common_config('public', 'localonly')) {
641
            $notice->whereAdd('is_local = ' . Notice::LOCAL_PUBLIC);
642
        } else {
643 644 645
            # -1 == blacklisted, -2 == gateway (i.e. Twitter)
            $notice->whereAdd('is_local !='. Notice::LOCAL_NONPUBLIC);
            $notice->whereAdd('is_local !='. Notice::GATEWAY);
646
        }
Evan Prodromou's avatar
Evan Prodromou committed
647

648 649 650
        if ($since_id != 0) {
            $notice->whereAdd('id > ' . $since_id);
        }
Evan Prodromou's avatar
Evan Prodromou committed
651

652 653 654
        if ($max_id != 0) {
            $notice->whereAdd('id <= ' . $max_id);
        }
Evan Prodromou's avatar
Evan Prodromou committed
655

656
        $ids = array();
Evan Prodromou's avatar
Evan Prodromou committed
657

658 659 660 661 662
        if ($notice->find()) {
            while ($notice->fetch()) {
                $ids[] = $notice->id;
            }
        }
Evan Prodromou's avatar
Evan Prodromou committed
663

664 665
        $notice->free();
        $notice = NULL;
Evan Prodromou's avatar
Evan Prodromou committed
666

667 668
        return $ids;
    }
Evan Prodromou's avatar
Evan Prodromou committed
669

670
    function conversationStream($id, $offset=0, $limit=20, $since_id=0, $max_id=0)
671 672 673 674
    {
        $ids = Notice::stream(array('Notice', '_conversationStreamDirect'),
                              array($id),
                              'notice:conversation_ids:'.$id,
675
                              $offset, $limit, $since_id, $max_id);
Evan Prodromou's avatar
Evan Prodromou committed
676

677 678
        return Notice::getStreamByIds($ids);
    }
Evan Prodromou's avatar
Evan Prodromou committed
679

680
    function _conversationStreamDirect($id, $offset=0, $limit=20, $since_id=0, $max_id=0)
681 682
    {
        $notice = new Notice();
Evan Prodromou's avatar
Evan Prodromou committed
683

684 685
        $notice->selectAdd(); // clears it
        $notice->selectAdd('id');
Evan Prodromou's avatar
Evan Prodromou committed
686

Evan Prodromou's avatar
Evan Prodromou committed
687
        $notice->conversation = $id;
Evan Prodromou's avatar
Evan Prodromou committed
688

689
        $notice->orderBy('id DESC');
Evan Prodromou's avatar
Evan Prodromou committed
690

691 692
        if (!is_null($offset)) {
            $notice->limit($offset, $limit);
693
        }
Evan Prodromou's avatar
Evan Prodromou committed
694

Evan Prodromou's avatar
Evan Prodromou committed
695 696
        if ($since_id != 0) {
            $notice->whereAdd('id > ' . $since_id);
697
        }
Evan Prodromou's avatar
Evan Prodromou committed
698

699 700
        if ($max_id != 0) {
            $notice->whereAdd('id <= ' . $max_id);
Evan Prodromou's avatar
Evan Prodromou committed
701
        }
Evan Prodromou's avatar
Evan Prodromou committed
702

Evan Prodromou's avatar
Evan Prodromou committed
703
        $ids = array();
Evan Prodromou's avatar
Evan Prodromou committed
704

Evan Prodromou's avatar
Evan Prodromou committed
705 706 707 708
        if ($notice->find()) {
            while ($notice->fetch()) {
                $ids[] = $notice->id;
            }
709
        }
Evan Prodromou's avatar
Evan Prodromou committed
710

Evan Prodromou's avatar
Evan Prodromou committed
711 712
        $notice->free();
        $notice = NULL;
Evan Prodromou's avatar
Evan Prodromou committed
713

Evan Prodromou's avatar
Evan Prodromou committed
714
        return $ids;
715
    }
Evan Prodromou's avatar
Evan Prodromou committed
716

717 718
    /**
     * Is this notice part of an active conversation?
719
     *
720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737
     * @return boolean true if other messages exist in the same
     *                 conversation, false if this is the only one
     */
    function hasConversation()
    {
        if (!empty($this->conversation)) {
            $conversation = Notice::conversationStream(
                $this->conversation,
                1,
                1
            );
            if ($conversation->N > 0) {
                return true;
            }
        }
        return false;
    }

738 739 740 741
    /**
     * @param $groups array of Group *objects*
     * @param $recipients array of profile *ids*
     */
742
    function whoGets($groups=null, $recipients=null)
743
    {
Evan Prodromou's avatar
Evan Prodromou committed
744 745 746 747 748 749 750 751 752
        $c = self::memcache();

        if (!empty($c)) {
            $ni = $c->get(common_cache_key('notice:who_gets:'.$this->id));
            if ($ni !== false) {
                return $ni;
            }
        }

753 754 755 756 757 758 759 760
        if (is_null($groups)) {
            $groups = $this->getGroups();
        }

        if (is_null($recipients)) {
            $recipients = $this->getReplies();
        }

Evan Prodromou's avatar
Evan Prodromou committed
761
        $users = $this->getSubscribedUsers();
Evan Prodromou's avatar
Evan Prodromou committed
762

Evan Prodromou's avatar
Evan Prodromou committed
763 764 765
        // FIXME: kind of ignoring 'transitional'...
        // we'll probably stop supporting inboxless mode
        // in 0.9.x
Evan Prodromou's avatar
Evan Prodromou committed
766

Evan Prodromou's avatar
Evan Prodromou committed
767
        $ni = array();
Evan Prodromou's avatar
Evan Prodromou committed
768

Evan Prodromou's avatar
Evan Prodromou committed
769 770 771
        foreach ($users as $id) {
            $ni[$id] = NOTICE_INBOX_SOURCE_SUB;
        }
Evan Prodromou's avatar
Evan Prodromou committed
772

773
        $profile = $this->getProfile();
Evan Prodromou's avatar
Evan Prodromou committed
774

Evan Prodromou's avatar
Evan Prodromou committed
775 776
        foreach ($groups as $group) {
            $users = $group->getUserMembers();
777
            foreach ($users as $id) {
Evan Prodromou's avatar
Evan Prodromou committed
778
                if (!array_key_exists($id, $ni)) {
779
                    $user = User::staticGet('id', $id);
780
                    if (!$user->hasBlocked($profile)) {
781 782
                        $ni[$id] = NOTICE_INBOX_SOURCE_GROUP;
                    }
Evan Prodromou's avatar
Evan Prodromou committed
783 784
                }
            }
Evan Prodromou's avatar
Evan Prodromou committed
785
        }
Evan Prodromou's avatar
Evan Prodromou committed
786

Evan Prodromou's avatar
Evan Prodromou committed
787 788 789 790 791 792 793 794 795 796
        foreach ($recipients as $recipient) {

            if (!array_key_exists($recipient, $ni)) {
                $recipientUser = User::staticGet('id', $recipient);
                if (!empty($recipientUser)) {
                    $ni[$recipient] = NOTICE_INBOX_SOURCE_REPLY;
                }
            }
        }

Evan Prodromou's avatar
Evan Prodromou committed
797 798 799 800 801
        if (!empty($c)) {
            // XXX: pack this data better
            $c->set(common_cache_key('notice:who_gets:'.$this->id), $ni);
        }

Evan Prodromou's avatar
Evan Prodromou committed
802 803 804
        return $ni;
    }

805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820
    /**
     * Adds this notice to the inboxes of each local user who should receive
     * it, based on author subscriptions, group memberships, and @-replies.
     *
     * Warning: running a second time currently will make items appear
     * multiple times in users' inboxes.
     *
     * @fixme make more robust against errors
     * @fixme break up massive deliveries to smaller background tasks
     *
     * @param array $groups optional list of Group objects;
     *              if left empty, will be loaded from group_inbox records
     * @param array $recipient optional list of reply profile ids
     *              if left empty, will be loaded from reply records
     */
    function addToInboxes($groups=null, $recipients=null)
Evan Prodromou's avatar
Evan Prodromou committed
821
    {
822
        $ni = $this->whoGets($groups, $recipients);
Evan Prodromou's avatar
Evan Prodromou committed
823

824 825 826 827 828 829 830 831 832 833 834 835 836 837
        $ids = array_keys($ni);

        // We remove the author (if they're a local user),
        // since we'll have already done this in distribute()

        $i = array_search($this->profile_id, $ids);

        if ($i !== false) {
            unset($ids[$i]);
        }

        // Bulk insert

        Inbox::bulkInsert($this->id, $ids);
Evan Prodromou's avatar
Evan Prodromou committed
838

839 840
        return;
    }
Evan Prodromou's avatar
Evan Prodromou committed
841

842 843 844
    function getSubscribedUsers()
    {
        $user = new User();
Evan Prodromou's avatar
Evan Prodromou committed
845

846
        if(common_config('db','quote_identifiers'))
Evan Prodromou's avatar
Evan Prodromou committed
847
          $user_table = '"user"';
848
        else $user_table = 'user';
Evan Prodromou's avatar
Evan Prodromou committed
849

850
        $qry =
Evan Prodromou's avatar
Evan Prodromou committed
851 852 853 854 855
          'SELECT id ' .
          'FROM '. $user_table .' JOIN subscription '.
          'ON '. $user_table .'.id = subscription.subscriber ' .
          'WHERE subscription.subscribed = %d ';

856
        $user->query(sprintf($qry, $this->profile_id));
Evan Prodromou's avatar
Evan Prodromou committed
857

858
        $ids = array();
Evan Prodromou's avatar
Evan Prodromou committed
859

860 861 862
        while ($user->fetch()) {
            $ids[] = $user->id;
        }
Evan Prodromou's avatar
Evan Prodromou committed
863

864
        $user->free();
Evan Prodromou's avatar
Evan Prodromou committed
865

866 867
        return $ids;
    }
Evan Prodromou's avatar
Evan Prodromou committed
868

869
    /**
870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905
     * Record this notice to the given group inboxes for delivery.
     * Overrides the regular parsing of !group markup.
     *
     * @param string $group_ids
     * @fixme might prefer URIs as identifiers, as for replies?
     *        best with generalizations on user_group to support
     *        remote groups better.
     */
    function saveKnownGroups($group_ids)
    {
        if (!is_array($group_ids)) {
            throw new ServerException("Bad type provided to saveKnownGroups");
        }

        $groups = array();
        foreach ($group_ids as $id) {
            $group = User_group::staticGet('id', $id);
            if ($group) {
                common_log(LOG_ERR, "Local delivery to group id $id, $group->nickname");
                $result = $this->addToGroupInbox($group);
                if (!$result) {
                    common_log_db_error($gi, 'INSERT', __FILE__);
                }

                // @fixme should we save the tags here or not?
                $groups[] = clone($group);
            } else {
                common_log(LOG_ERR, "Local delivery to group id $id skipped, doesn't exist");
            }
        }

        return $groups;
    }

    /**
     * Parse !group delivery and record targets into group_inbox.
906 907
     * @return array of Group objects
     */
908 909
    function saveGroups()
    {
910 911 912 913 914 915
        // Don't save groups for repeats

        if (!empty($this->repeat_of)) {
            return array();
        }

Evan Prodromou's avatar
Evan Prodromou committed
916
        $groups = array();
Evan Prodromou's avatar
Evan Prodromou committed
917

918 919 920 921 922
        /* extract all !group */
        $count = preg_match_all('/(?:^|\s)!([A-Za-z0-9]{1,64})/',
                                strtolower($this->content),
                                $match);
        if (!$count) {
Evan Prodromou's avatar
Evan Prodromou committed
923
            return $groups;
924
        }
Evan Prodromou's avatar
Evan Prodromou committed
925

926
        $profile = $this->getProfile();
Evan Prodromou's avatar
Evan Prodromou committed
927

928
        /* Add them to the database */
Evan Prodromou's avatar
Evan Prodromou committed
929

930 931
        foreach (array_unique($match[1]) as $nickname) {
            /* XXX: remote groups. */
932
            $group = User_group::getForNickname($nickname, $profile);
Evan Prodromou's avatar
Evan Prodromou committed
933

934
            if (empty($group)) {
935 936
                continue;
            }
Evan Prodromou's avatar
Evan Prodromou committed
937

938
            // we automatically add a tag for every group name, too
Evan Prodromou's avatar
Evan Prodromou committed
939

940
            $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($nickname),
941
                                             'notice_id' => $this->id));
Evan Prodromou's avatar
Evan Prodromou committed
942