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