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