]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - classes/Notice.php
Merge branch 'mgrdcm-review' into 0.7.x
[quix0rs-gnu-social.git] / classes / Notice.php
1 <?php
2 /*
3  * Laconica - a distributed open-source microblogging tool
4  * Copyright (C) 2008, Controlez-Vous, Inc.
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU Affero General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.     See the
14  * GNU Affero General Public License for more details.
15  *
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/>.
18  */
19
20 if (!defined('LACONICA')) { exit(1); }
21
22 /**
23  * Table Definition for notice
24  */
25 require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
26
27 /* We keep the first three 20-notice pages, plus one for pagination check,
28  * in the memcached cache. */
29
30 define('NOTICE_CACHE_WINDOW', 61);
31
32 class Notice extends Memcached_DataObject
33 {
34     ###START_AUTOCODE
35     /* the code below is auto generated do not remove the above tag */
36
37     public $__table = 'notice';                          // table name
38     public $id;                              // int(4)  primary_key not_null
39     public $profile_id;                      // int(4)   not_null
40     public $uri;                             // varchar(255)  unique_key
41     public $content;                         // varchar(140)
42     public $rendered;                        // text()
43     public $url;                             // varchar(255)
44     public $created;                         // datetime()   not_null
45     public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP
46     public $reply_to;                        // int(4)
47     public $is_local;                        // tinyint(1)
48     public $source;                          // varchar(32)
49
50     /* Static get */
51     function staticGet($k,$v=NULL) {
52         return Memcached_DataObject::staticGet('Notice',$k,$v);
53     }
54
55     /* the code above is auto generated do not remove the tag below */
56     ###END_AUTOCODE
57
58     function getProfile()
59     {
60         return Profile::staticGet('id', $this->profile_id);
61     }
62
63     function delete()
64     {
65         $this->blowCaches(true);
66         $this->blowFavesCache(true);
67         $this->blowSubsCache(true);
68
69         $this->query('BEGIN');
70         //Null any notices that are replies to this notice
71         $this->query(sprintf("UPDATE notice set reply_to = null WHERE reply_to = %d", $this->id));
72         $related = array('Reply',
73                          'Fave',
74                          'Notice_tag',
75                          'Group_inbox',
76                          'Queue_item');
77         if (common_config('inboxes', 'enabled')) {
78             $related[] = 'Notice_inbox';
79         }
80         foreach ($related as $cls) {
81             $inst = new $cls();
82             $inst->notice_id = $this->id;
83             $inst->delete();
84         }
85         $result = parent::delete();
86         $this->query('COMMIT');
87     }
88
89     function saveTags()
90     {
91         /* extract all #hastags */
92         $count = preg_match_all('/(?:^|\s)#([A-Za-z0-9_\-\.]{1,64})/', strtolower($this->content), $match);
93         if (!$count) {
94             return true;
95         }
96
97         /* Add them to the database */
98         foreach(array_unique($match[1]) as $hashtag) {
99             /* elide characters we don't want in the tag */
100             $this->saveTag($hashtag);
101         }
102         return true;
103     }
104
105     function saveTag($hashtag)
106     {
107         $hashtag = common_canonical_tag($hashtag);
108
109         $tag = new Notice_tag();
110         $tag->notice_id = $this->id;
111         $tag->tag = $hashtag;
112         $tag->created = $this->created;
113         $id = $tag->insert();
114
115         if (!$id) {
116             throw new ServerException(sprintf(_('DB error inserting hashtag: %s'),
117                                               $last_error->message));
118             return;
119         }
120     }
121
122     static function saveNew($profile_id, $content, $source=null, $is_local=1, $reply_to=null, $uri=null) {
123
124         $profile = Profile::staticGet($profile_id);
125
126         $final = common_shorten_links($content);
127
128         if (mb_strlen($final) > 140) {
129             common_log(LOG_INFO, 'Rejecting notice that is too long.');
130             return _('Problem saving notice. Too long.');
131         }
132
133         if (!$profile) {
134             common_log(LOG_ERR, 'Problem saving notice. Unknown user.');
135             return _('Problem saving notice. Unknown user.');
136         }
137
138         if (common_config('throttle', 'enabled') && !Notice::checkEditThrottle($profile_id)) {
139             common_log(LOG_WARNING, 'Excessive posting by profile #' . $profile_id . '; throttled.');
140             return _('Too many notices too fast; take a breather and post again in a few minutes.');
141         }
142
143         if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) {
144             common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.');
145                         return _('Too many duplicate messages too quickly; take a breather and post again in a few minutes.');
146         }
147
148                 $banned = common_config('profile', 'banned');
149
150         if ( in_array($profile_id, $banned) || in_array($profile->nickname, $banned)) {
151             common_log(LOG_WARNING, "Attempted post from banned user: $profile->nickname (user id = $profile_id).");
152             return _('You are banned from posting notices on this site.');
153         }
154
155         $notice = new Notice();
156         $notice->profile_id = $profile_id;
157
158         $blacklist = common_config('public', 'blacklist');
159         $autosource = common_config('public', 'autosource');
160
161         # Blacklisted are non-false, but not 1, either
162
163         if (($blacklist && in_array($profile_id, $blacklist)) ||
164             ($source && $autosource && in_array($source, $autosource))) {
165             $notice->is_local = -1;
166         } else {
167             $notice->is_local = $is_local;
168         }
169
170                 $notice->query('BEGIN');
171
172                 $notice->reply_to = $reply_to;
173                 $notice->created = common_sql_now();
174                 $notice->content = $final;
175                 $notice->rendered = common_render_content($final, $notice);
176                 $notice->source = $source;
177                 $notice->uri = $uri;
178
179         if (Event::handle('StartNoticeSave', array(&$notice))) {
180
181             $id = $notice->insert();
182
183             if (!$id) {
184                 common_log_db_error($notice, 'INSERT', __FILE__);
185                 return _('Problem saving notice.');
186             }
187
188             # Update the URI after the notice is in the database
189             if (!$uri) {
190                 $orig = clone($notice);
191                 $notice->uri = common_notice_uri($notice);
192
193                 if (!$notice->update($orig)) {
194                     common_log_db_error($notice, 'UPDATE', __FILE__);
195                     return _('Problem saving notice.');
196                 }
197             }
198
199             # XXX: do we need to change this for remote users?
200
201             $notice->saveReplies();
202             $notice->saveTags();
203
204             $notice->addToInboxes();
205             $notice->saveGroups();
206
207             $notice->query('COMMIT');
208
209             Event::handle('EndNoticeSave', array($notice));
210         }
211
212         # Clear the cache for subscribed users, so they'll update at next request
213         # XXX: someone clever could prepend instead of clearing the cache
214
215         $notice->blowCaches();
216
217         return $notice;
218     }
219
220     static function checkDupes($profile_id, $content) {
221         $profile = Profile::staticGet($profile_id);
222         if (!$profile) {
223             return false;
224         }
225         $notice = $profile->getNotices(0, NOTICE_CACHE_WINDOW);
226         if ($notice) {
227             $last = 0;
228             while ($notice->fetch()) {
229                 if (time() - strtotime($notice->created) >= common_config('site', 'dupelimit')) {
230                     return true;
231                 } else if ($notice->content == $content) {
232                     return false;
233                 }
234             }
235         }
236         # If we get here, oldest item in cache window is not
237         # old enough for dupe limit; do direct check against DB
238         $notice = new Notice();
239         $notice->profile_id = $profile_id;
240         $notice->content = $content;
241         if (common_config('db','type') == 'pgsql')
242             $notice->whereAdd('extract(epoch from now() - created) < ' . common_config('site', 'dupelimit'));
243         else
244             $notice->whereAdd('now() - created < ' . common_config('site', 'dupelimit'));
245
246         $cnt = $notice->count();
247         return ($cnt == 0);
248     }
249
250     static function checkEditThrottle($profile_id) {
251         $profile = Profile::staticGet($profile_id);
252         if (!$profile) {
253             return false;
254         }
255         # Get the Nth notice
256         $notice = $profile->getNotices(common_config('throttle', 'count') - 1, 1);
257         if ($notice && $notice->fetch()) {
258             # If the Nth notice was posted less than timespan seconds ago
259             if (time() - strtotime($notice->created) <= common_config('throttle', 'timespan')) {
260                 # Then we throttle
261                 return false;
262             }
263         }
264         # Either not N notices in the stream, OR the Nth was not posted within timespan seconds
265         return true;
266     }
267
268     function blowCaches($blowLast=false)
269     {
270         $this->blowSubsCache($blowLast);
271         $this->blowNoticeCache($blowLast);
272         $this->blowRepliesCache($blowLast);
273         $this->blowPublicCache($blowLast);
274         $this->blowTagCache($blowLast);
275         $this->blowGroupCache($blowLast);
276     }
277
278     function blowGroupCache($blowLast=false)
279     {
280         $cache = common_memcache();
281         if ($cache) {
282             $group_inbox = new Group_inbox();
283             $group_inbox->notice_id = $this->id;
284             if ($group_inbox->find()) {
285                 while ($group_inbox->fetch()) {
286                     $cache->delete(common_cache_key('user_group:notice_ids:' . $group_inbox->group_id));
287                     if ($blowLast) {
288                         $cache->delete(common_cache_key('user_group:notice_ids:' . $group_inbox->group_id.';last'));
289                     }
290                     $member = new Group_member();
291                     $member->group_id = $group_inbox->group_id;
292                     if ($member->find()) {
293                         while ($member->fetch()) {
294                             $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id));
295                             if ($blowLast) {
296                                 $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id . ';last'));
297                             }
298                         }
299                     }
300                 }
301             }
302             $group_inbox->free();
303             unset($group_inbox);
304         }
305     }
306
307     function blowTagCache($blowLast=false)
308     {
309         $cache = common_memcache();
310         if ($cache) {
311             $tag = new Notice_tag();
312             $tag->notice_id = $this->id;
313             if ($tag->find()) {
314                 while ($tag->fetch()) {
315                     $tag->blowCache($blowLast);
316                 }
317             }
318             $tag->free();
319             unset($tag);
320         }
321     }
322
323     function blowSubsCache($blowLast=false)
324     {
325         $cache = common_memcache();
326         if ($cache) {
327             $user = new User();
328
329             $UT = common_config('db','type')=='pgsql'?'"user"':'user';
330             $user->query('SELECT id ' .
331
332                          "FROM $UT JOIN subscription ON $UT.id = subscription.subscriber " .
333                          'WHERE subscription.subscribed = ' . $this->profile_id);
334
335             while ($user->fetch()) {
336                 $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id));
337                 if ($blowLast) {
338                     $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id.';last'));
339                 }
340             }
341             $user->free();
342             unset($user);
343         }
344     }
345
346     function blowNoticeCache($blowLast=false)
347     {
348         if ($this->is_local) {
349             $cache = common_memcache();
350             if (!empty($cache)) {
351                 $cache->delete(common_cache_key('profile:notice_ids:'.$this->profile_id));
352                 if ($blowLast) {
353                     $cache->delete(common_cache_key('profile:notice_ids:'.$this->profile_id.';last'));
354                 }
355             }
356         }
357     }
358
359     function blowRepliesCache($blowLast=false)
360     {
361         $cache = common_memcache();
362         if ($cache) {
363             $reply = new Reply();
364             $reply->notice_id = $this->id;
365             if ($reply->find()) {
366                 while ($reply->fetch()) {
367                     $cache->delete(common_cache_key('reply:stream:'.$reply->profile_id));
368                     if ($blowLast) {
369                         $cache->delete(common_cache_key('reply:stream:'.$reply->profile_id.';last'));
370                     }
371                 }
372             }
373             $reply->free();
374             unset($reply);
375         }
376     }
377
378     function blowPublicCache($blowLast=false)
379     {
380         if ($this->is_local == 1) {
381             $cache = common_memcache();
382             if ($cache) {
383                 $cache->delete(common_cache_key('public'));
384                 if ($blowLast) {
385                     $cache->delete(common_cache_key('public').';last');
386                 }
387             }
388         }
389     }
390
391     function blowFavesCache($blowLast=false)
392     {
393         $cache = common_memcache();
394         if ($cache) {
395             $fave = new Fave();
396             $fave->notice_id = $this->id;
397             if ($fave->find()) {
398                 while ($fave->fetch()) {
399                     $cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id));
400                     if ($blowLast) {
401                         $cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id.';last'));
402                     }
403                 }
404             }
405             $fave->free();
406             unset($fave);
407         }
408     }
409
410     # XXX: too many args; we need to move to named params or even a separate
411     # class for notice streams
412
413     static function getStream($qry, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0, $order=null, $since=null) {
414
415         if (common_config('memcached', 'enabled')) {
416
417             # Skip the cache if this is a since, since_id or max_id qry
418             if ($since_id > 0 || $max_id > 0 || $since) {
419                 return Notice::getStreamDirect($qry, $offset, $limit, $since_id, $max_id, $order, $since);
420             } else {
421                 return Notice::getCachedStream($qry, $cachekey, $offset, $limit, $order);
422             }
423         }
424
425         return Notice::getStreamDirect($qry, $offset, $limit, $since_id, $max_id, $order, $since);
426     }
427
428     static function getStreamDirect($qry, $offset, $limit, $since_id, $max_id, $order, $since) {
429
430         $needAnd = false;
431         $needWhere = true;
432
433         if (preg_match('/\bWHERE\b/i', $qry)) {
434             $needWhere = false;
435             $needAnd = true;
436         }
437
438         if ($since_id > 0) {
439
440             if ($needWhere) {
441                 $qry .= ' WHERE ';
442                 $needWhere = false;
443             } else {
444                 $qry .= ' AND ';
445             }
446
447             $qry .= ' notice.id > ' . $since_id;
448         }
449
450         if ($max_id > 0) {
451
452             if ($needWhere) {
453                 $qry .= ' WHERE ';
454                 $needWhere = false;
455             } else {
456                 $qry .= ' AND ';
457             }
458
459             $qry .= ' notice.id <= ' . $max_id;
460         }
461
462         if ($since) {
463
464             if ($needWhere) {
465                 $qry .= ' WHERE ';
466                 $needWhere = false;
467             } else {
468                 $qry .= ' AND ';
469             }
470
471             $qry .= ' notice.created > \'' . date('Y-m-d H:i:s', $since) . '\'';
472         }
473
474         # Allow ORDER override
475
476         if ($order) {
477             $qry .= $order;
478         } else {
479             $qry .= ' ORDER BY notice.created DESC, notice.id DESC ';
480         }
481
482         if (common_config('db','type') == 'pgsql') {
483             $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
484         } else {
485             $qry .= ' LIMIT ' . $offset . ', ' . $limit;
486         }
487
488         $notice = new Notice();
489
490         $notice->query($qry);
491
492         return $notice;
493     }
494
495     # XXX: this is pretty long and should probably be broken up into
496     # some helper functions
497
498     static function getCachedStream($qry, $cachekey, $offset, $limit, $order) {
499
500         # If outside our cache window, just go to the DB
501
502         if ($offset + $limit > NOTICE_CACHE_WINDOW) {
503             return Notice::getStreamDirect($qry, $offset, $limit, null, null, $order, null);
504         }
505
506         # Get the cache; if we can't, just go to the DB
507
508         $cache = common_memcache();
509
510         if (!$cache) {
511             return Notice::getStreamDirect($qry, $offset, $limit, null, null, $order, null);
512         }
513
514         # Get the notices out of the cache
515
516         $notices = $cache->get(common_cache_key($cachekey));
517
518         # On a cache hit, return a DB-object-like wrapper
519
520         if ($notices !== false) {
521             $wrapper = new ArrayWrapper(array_slice($notices, $offset, $limit));
522             return $wrapper;
523         }
524
525         # If the cache was invalidated because of new data being
526         # added, we can try and just get the new stuff. We keep an additional
527         # copy of the data at the key + ';last'
528
529         # No cache hit. Try to get the *last* cached version
530
531         $last_notices = $cache->get(common_cache_key($cachekey) . ';last');
532
533         if ($last_notices) {
534
535             # Reverse-chron order, so last ID is last.
536
537             $last_id = $last_notices[0]->id;
538
539             # XXX: this assumes monotonically increasing IDs; a fair
540             # bet with our DB.
541
542             $new_notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW,
543                                                   $last_id, null, $order, null);
544
545             if ($new_notice) {
546                 $new_notices = array();
547                 while ($new_notice->fetch()) {
548                     $new_notices[] = clone($new_notice);
549                 }
550                 $new_notice->free();
551                 $notices = array_slice(array_merge($new_notices, $last_notices),
552                                        0, NOTICE_CACHE_WINDOW);
553
554                 # Store the array in the cache for next time
555
556                 $result = $cache->set(common_cache_key($cachekey), $notices);
557                 $result = $cache->set(common_cache_key($cachekey) . ';last', $notices);
558
559                 # return a wrapper of the array for use now
560
561                 return new ArrayWrapper(array_slice($notices, $offset, $limit));
562             }
563         }
564
565         # Otherwise, get the full cache window out of the DB
566
567         $notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW, null, null, $order, null);
568
569         # If there are no hits, just return the value
570
571         if (!$notice) {
572             return $notice;
573         }
574
575         # Pack results into an array
576
577         $notices = array();
578
579         while ($notice->fetch()) {
580             $notices[] = clone($notice);
581         }
582
583         $notice->free();
584
585         # Store the array in the cache for next time
586
587         $result = $cache->set(common_cache_key($cachekey), $notices);
588         $result = $cache->set(common_cache_key($cachekey) . ';last', $notices);
589
590         # return a wrapper of the array for use now
591
592         $wrapper = new ArrayWrapper(array_slice($notices, $offset, $limit));
593
594         return $wrapper;
595     }
596
597     function getStreamByIds($ids)
598     {
599         $cache = common_memcache();
600
601         if (!empty($cache)) {
602             $notices = array();
603             foreach ($ids as $id) {
604                 $notices[] = Notice::staticGet('id', $id);
605             }
606             return new ArrayWrapper($notices);
607         } else {
608             $notice = new Notice();
609             $notice->whereAdd('id in (' . implode(', ', $ids) . ')');
610             $notice->orderBy('id DESC');
611
612             $notice->find();
613             return $notice;
614         }
615     }
616
617     function publicStream($offset=0, $limit=20, $since_id=0, $max_id=0, $since=null)
618     {
619         $ids = Notice::stream(array('Notice', '_publicStreamDirect'),
620                               array(),
621                               'public',
622                               $offset, $limit, $since_id, $max_id, $since);
623
624         return Notice::getStreamByIds($ids);
625     }
626
627     function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $max_id=0, $since=null)
628     {
629         $notice = new Notice();
630
631         $notice->selectAdd(); // clears it
632         $notice->selectAdd('id');
633
634         $notice->orderBy('id DESC');
635
636         if (!is_null($offset)) {
637             $notice->limit($offset, $limit);
638         }
639
640         if (common_config('public', 'localonly')) {
641             $notice->whereAdd('is_local = 1');
642         } else {
643             # -1 == blacklisted
644             $notice->whereAdd('is_local != -1');
645         }
646
647         if ($since_id != 0) {
648             $notice->whereAdd('id > ' . $since_id);
649         }
650
651         if ($max_id != 0) {
652             $notice->whereAdd('id <= ' . $max_id);
653         }
654
655         if (!is_null($since)) {
656             $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
657         }
658
659         $ids = array();
660
661         if ($notice->find()) {
662             while ($notice->fetch()) {
663                 $ids[] = $notice->id;
664             }
665         }
666
667         $notice->free();
668         $notice = NULL;
669
670         return $ids;
671     }
672
673     function addToInboxes()
674     {
675         $enabled = common_config('inboxes', 'enabled');
676
677         if ($enabled === true || $enabled === 'transitional') {
678             $inbox = new Notice_inbox();
679             $UT = common_config('db','type')=='pgsql'?'"user"':'user';
680             $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created) ' .
681               "SELECT $UT.id, " . $this->id . ", '" . $this->created . "' " .
682               "FROM $UT JOIN subscription ON $UT.id = subscription.subscriber " .
683               'WHERE subscription.subscribed = ' . $this->profile_id . ' ' .
684               'AND NOT EXISTS (SELECT user_id, notice_id ' .
685               'FROM notice_inbox ' .
686               "WHERE user_id = $UT.id " .
687               'AND notice_id = ' . $this->id . ' )';
688             if ($enabled === 'transitional') {
689                 $qry .= " AND $UT.inboxed = 1";
690             }
691             $inbox->query($qry);
692         }
693         return;
694     }
695
696     function saveGroups()
697     {
698         $enabled = common_config('inboxes', 'enabled');
699         if ($enabled !== true && $enabled !== 'transitional') {
700             return;
701         }
702
703         /* extract all !group */
704         $count = preg_match_all('/(?:^|\s)!([A-Za-z0-9]{1,64})/',
705                                 strtolower($this->content),
706                                 $match);
707         if (!$count) {
708             return true;
709         }
710
711         $profile = $this->getProfile();
712
713         /* Add them to the database */
714
715         foreach (array_unique($match[1]) as $nickname) {
716             /* XXX: remote groups. */
717             $group = User_group::staticGet('nickname', $nickname);
718
719             if (!$group) {
720                 continue;
721             }
722
723             // we automatically add a tag for every group name, too
724
725             $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($nickname),
726                                            'notice_id' => $this->id));
727
728             if (is_null($tag)) {
729                 $this->saveTag($nickname);
730             }
731
732             if ($profile->isMember($group)) {
733
734                 $gi = new Group_inbox();
735
736                 $gi->group_id  = $group->id;
737                 $gi->notice_id = $this->id;
738                 $gi->created   = common_sql_now();
739
740                 $result = $gi->insert();
741
742                 if (!$result) {
743                     common_log_db_error($gi, 'INSERT', __FILE__);
744                 }
745
746                 // FIXME: do this in an offline daemon
747
748                 $this->addToGroupInboxes($group);
749             }
750         }
751     }
752
753     function addToGroupInboxes($group)
754     {
755         $inbox = new Notice_inbox();
756         $UT = common_config('db','type')=='pgsql'?'"user"':'user';
757         $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created, source) ' .
758           "SELECT $UT.id, " . $this->id . ", '" . $this->created . "', 2 " .
759           "FROM $UT JOIN group_member ON $UT.id = group_member.profile_id " .
760           'WHERE group_member.group_id = ' . $group->id . ' ' .
761           'AND NOT EXISTS (SELECT user_id, notice_id ' .
762           'FROM notice_inbox ' .
763           "WHERE user_id = $UT.id " .
764           'AND notice_id = ' . $this->id . ' )';
765         if ($enabled === 'transitional') {
766             $qry .= " AND $UT.inboxed = 1";
767         }
768         $result = $inbox->query($qry);
769     }
770
771     function saveReplies()
772     {
773         // Alternative reply format
774         $tname = false;
775         if (preg_match('/^T ([A-Z0-9]{1,64}) /', $this->content, $match)) {
776             $tname = $match[1];
777         }
778         // extract all @messages
779         $cnt = preg_match_all('/(?:^|\s)@([a-z0-9]{1,64})/', $this->content, $match);
780
781         $names = array();
782
783         if ($cnt || $tname) {
784             // XXX: is there another way to make an array copy?
785             $names = ($tname) ? array_unique(array_merge(array(strtolower($tname)), $match[1])) : array_unique($match[1]);
786         }
787
788         $sender = Profile::staticGet($this->profile_id);
789
790         $replied = array();
791
792         // store replied only for first @ (what user/notice what the reply directed,
793         // we assume first @ is it)
794
795         for ($i=0; $i<count($names); $i++) {
796             $nickname = $names[$i];
797             $recipient = common_relative_profile($sender, $nickname, $this->created);
798             if (!$recipient) {
799                 continue;
800             }
801             if ($i == 0 && ($recipient->id != $sender->id) && !$this->reply_to) { // Don't save reply to self
802                 $reply_for = $recipient;
803                 $recipient_notice = $reply_for->getCurrentNotice();
804                 if ($recipient_notice) {
805                     $orig = clone($this);
806                     $this->reply_to = $recipient_notice->id;
807                     $this->update($orig);
808                 }
809             }
810             // Don't save replies from blocked profile to local user
811             $recipient_user = User::staticGet('id', $recipient->id);
812             if ($recipient_user && $recipient_user->hasBlocked($sender)) {
813                 continue;
814             }
815             $reply = new Reply();
816             $reply->notice_id = $this->id;
817             $reply->profile_id = $recipient->id;
818             $id = $reply->insert();
819             if (!$id) {
820                 $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
821                 common_log(LOG_ERR, 'DB error inserting reply: ' . $last_error->message);
822                 common_server_error(sprintf(_('DB error inserting reply: %s'), $last_error->message));
823                 return;
824             } else {
825                 $replied[$recipient->id] = 1;
826             }
827         }
828
829         // Hash format replies, too
830         $cnt = preg_match_all('/(?:^|\s)@#([a-z0-9]{1,64})/', $this->content, $match);
831         if ($cnt) {
832             foreach ($match[1] as $tag) {
833                 $tagged = Profile_tag::getTagged($sender->id, $tag);
834                 foreach ($tagged as $t) {
835                     if (!$replied[$t->id]) {
836                         // Don't save replies from blocked profile to local user
837                         $t_user = User::staticGet('id', $t->id);
838                         if ($t_user && $t_user->hasBlocked($sender)) {
839                             continue;
840                         }
841                         $reply = new Reply();
842                         $reply->notice_id = $this->id;
843                         $reply->profile_id = $t->id;
844                         $id = $reply->insert();
845                         if (!$id) {
846                             common_log_db_error($reply, 'INSERT', __FILE__);
847                             return;
848                         } else {
849                             $replied[$recipient->id] = 1;
850                         }
851                     }
852                 }
853             }
854         }
855
856         foreach (array_keys($replied) as $recipient) {
857             $user = User::staticGet('id', $recipient);
858             if ($user) {
859                 mail_notify_attn($user, $this);
860             }
861         }
862     }
863
864     function asAtomEntry($namespace=false, $source=false)
865     {
866         $profile = $this->getProfile();
867
868         $xs = new XMLStringer(true);
869
870         if ($namespace) {
871             $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
872                            'xmlns:thr' => 'http://purl.org/syndication/thread/1.0');
873         } else {
874             $attrs = array();
875         }
876
877         $xs->elementStart('entry', $attrs);
878
879         if ($source) {
880             $xs->elementStart('source');
881             $xs->element('title', null, $profile->nickname . " - " . common_config('site', 'name'));
882             $xs->element('link', array('href' => $profile->profileurl));
883             $user = User::staticGet('id', $profile->id);
884             if (!empty($user)) {
885                 $atom_feed = common_local_url('api',
886                                               array('apiaction' => 'statuses',
887                                                     'method' => 'user_timeline',
888                                                     'argument' => $profile->nickname.'.atom'));
889                 $xs->element('link', array('rel' => 'self',
890                                            'type' => 'application/atom+xml',
891                                            'href' => $profile->profileurl));
892                 $xs->element('link', array('rel' => 'license',
893                                            'href' => common_config('license', 'url')));
894             }
895
896             $xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE));
897         }
898
899         $xs->elementStart('author');
900         $xs->element('name', null, $profile->nickname);
901         $xs->element('uri', null, $profile->profileurl);
902         $xs->elementEnd('author');
903
904         if ($source) {
905             $xs->elementEnd('source');
906         }
907
908         $xs->element('title', null, $this->content);
909         $xs->element('summary', null, $this->content);
910
911         $xs->element('link', array('rel' => 'alternate',
912                                    'href' => $this->bestUrl()));
913
914         $xs->element('id', null, $this->uri);
915
916         $xs->element('published', null, common_date_w3dtf($this->created));
917         $xs->element('updated', null, common_date_w3dtf($this->modified));
918
919         if ($this->reply_to) {
920             $reply_notice = Notice::staticGet('id', $this->reply_to);
921             if (!empty($reply_notice)) {
922                 $xs->element('link', array('rel' => 'related',
923                                            'href' => $reply_notice->bestUrl()));
924                 $xs->element('thr:in-reply-to',
925                              array('ref' => $reply_notice->uri,
926                                    'href' => $reply_notice->bestUrl()));
927             }
928         }
929
930         $xs->element('content', array('type' => 'html'), $this->rendered);
931
932         $tag = new Notice_tag();
933         $tag->notice_id = $this->id;
934         if ($tag->find()) {
935             while ($tag->fetch()) {
936                 $xs->element('category', array('term' => $tag->tag));
937             }
938         }
939         $tag->free();
940
941         $xs->elementEnd('entry');
942
943         return $xs->getString();
944     }
945
946     function bestUrl()
947     {
948         if (!empty($this->url)) {
949             return $this->url;
950         } else if (!empty($this->uri) && preg_match('/^https?:/', $this->uri)) {
951             return $this->uri;
952         } else {
953             return common_local_url('shownotice',
954                                     array('notice' => $this->id));
955         }
956     }
957
958     function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $max_id=0, $since=null)
959     {
960         $cache = common_memcache();
961
962         if (empty($cache) ||
963             $since_id != 0 || $max_id != 0 || (!is_null($since) && $since > 0) ||
964             ($offset + $limit) > NOTICE_CACHE_WINDOW) {
965             return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id,
966                                                                       $max_id, $since)));
967         }
968
969         $idkey = common_cache_key($cachekey);
970
971         $idstr = $cache->get($idkey);
972
973         if (!empty($idstr)) {
974             // Cache hit! Woohoo!
975             $window = explode(',', $idstr);
976             $ids = array_slice($window, $offset, $limit);
977             return $ids;
978         }
979
980         $laststr = $cache->get($idkey.';last');
981
982         if (!empty($laststr)) {
983             $window = explode(',', $laststr);
984             $last_id = $window[0];
985             $new_ids = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW,
986                                                                           $last_id, 0, null)));
987
988             $new_window = array_merge($new_ids, $window);
989
990             $new_windowstr = implode(',', $new_window);
991
992             $result = $cache->set($idkey, $new_windowstr);
993             $result = $cache->set($idkey . ';last', $new_windowstr);
994
995             $ids = array_slice($new_window, $offset, $limit);
996
997             return $ids;
998         }
999
1000         $window = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW,
1001                                                                      0, 0, null)));
1002
1003         $windowstr = implode(',', $window);
1004
1005         $result = $cache->set($idkey, $windowstr);
1006         $result = $cache->set($idkey . ';last', $windowstr);
1007
1008         $ids = array_slice($window, $offset, $limit);
1009
1010         return $ids;
1011     }
1012 }