]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - classes/Notice.php
Updated Cloudy theme default avatars and minor CSS
[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             if (common_config('queue', 'enabled')) {
210                 $notice->addToAuthorInbox();
211             } else {
212                 $notice->addToInboxes();
213             }
214
215             $notice->query('COMMIT');
216
217             Event::handle('EndNoticeSave', array($notice));
218         }
219
220         # Clear the cache for subscribed users, so they'll update at next request
221         # XXX: someone clever could prepend instead of clearing the cache
222
223         if (common_config('memcached', 'enabled')) {
224             if (common_config('queue', 'enabled')) {
225                 $notice->blowAuthorCaches();
226             } else {
227                 $notice->blowCaches();
228             }
229         }
230
231         return $notice;
232     }
233
234     static function checkDupes($profile_id, $content) {
235         $profile = Profile::staticGet($profile_id);
236         if (!$profile) {
237             return false;
238         }
239         $notice = $profile->getNotices(0, NOTICE_CACHE_WINDOW);
240         if ($notice) {
241             $last = 0;
242             while ($notice->fetch()) {
243                 if (time() - strtotime($notice->created) >= common_config('site', 'dupelimit')) {
244                     return true;
245                 } else if ($notice->content == $content) {
246                     return false;
247                 }
248             }
249         }
250         # If we get here, oldest item in cache window is not
251         # old enough for dupe limit; do direct check against DB
252         $notice = new Notice();
253         $notice->profile_id = $profile_id;
254         $notice->content = $content;
255         if (common_config('db','type') == 'pgsql')
256             $notice->whereAdd('extract(epoch from now() - created) < ' . common_config('site', 'dupelimit'));
257         else
258             $notice->whereAdd('now() - created < ' . common_config('site', 'dupelimit'));
259
260         $cnt = $notice->count();
261         return ($cnt == 0);
262     }
263
264     static function checkEditThrottle($profile_id) {
265         $profile = Profile::staticGet($profile_id);
266         if (!$profile) {
267             return false;
268         }
269         # Get the Nth notice
270         $notice = $profile->getNotices(common_config('throttle', 'count') - 1, 1);
271         if ($notice && $notice->fetch()) {
272             # If the Nth notice was posted less than timespan seconds ago
273             if (time() - strtotime($notice->created) <= common_config('throttle', 'timespan')) {
274                 # Then we throttle
275                 return false;
276             }
277         }
278         # Either not N notices in the stream, OR the Nth was not posted within timespan seconds
279         return true;
280     }
281
282     function blowCaches($blowLast=false)
283     {
284         $this->blowSubsCache($blowLast);
285         $this->blowNoticeCache($blowLast);
286         $this->blowRepliesCache($blowLast);
287         $this->blowPublicCache($blowLast);
288         $this->blowTagCache($blowLast);
289         $this->blowGroupCache($blowLast);
290     }
291
292     function blowAuthorCaches($blowLast=false)
293     {
294         // Clear the user's cache
295         $cache = common_memcache();
296         if (!empty($cache)) {
297             $cache->delete(common_cache_key('notice_inbox:by_user:'.$this->profile_id));
298         }
299         $this->blowNoticeCache($blowLast);
300         $this->blowPublicCache($blowLast);
301     }
302
303     function blowGroupCache($blowLast=false)
304     {
305         $cache = common_memcache();
306         if ($cache) {
307             $group_inbox = new Group_inbox();
308             $group_inbox->notice_id = $this->id;
309             if ($group_inbox->find()) {
310                 while ($group_inbox->fetch()) {
311                     $cache->delete(common_cache_key('user_group:notice_ids:' . $group_inbox->group_id));
312                     if ($blowLast) {
313                         $cache->delete(common_cache_key('user_group:notice_ids:' . $group_inbox->group_id.';last'));
314                     }
315                     $member = new Group_member();
316                     $member->group_id = $group_inbox->group_id;
317                     if ($member->find()) {
318                         while ($member->fetch()) {
319                             $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id));
320                             if ($blowLast) {
321                                 $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id . ';last'));
322                             }
323                         }
324                     }
325                 }
326             }
327             $group_inbox->free();
328             unset($group_inbox);
329         }
330     }
331
332     function blowTagCache($blowLast=false)
333     {
334         $cache = common_memcache();
335         if ($cache) {
336             $tag = new Notice_tag();
337             $tag->notice_id = $this->id;
338             if ($tag->find()) {
339                 while ($tag->fetch()) {
340                     $tag->blowCache($blowLast);
341                 }
342             }
343             $tag->free();
344             unset($tag);
345         }
346     }
347
348     function blowSubsCache($blowLast=false)
349     {
350         $cache = common_memcache();
351         if ($cache) {
352             $user = new User();
353
354             $UT = common_config('db','type')=='pgsql'?'"user"':'user';
355             $user->query('SELECT id ' .
356
357                          "FROM $UT JOIN subscription ON $UT.id = subscription.subscriber " .
358                          'WHERE subscription.subscribed = ' . $this->profile_id);
359
360             while ($user->fetch()) {
361                 $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id));
362                 if ($blowLast) {
363                     $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id.';last'));
364                 }
365             }
366             $user->free();
367             unset($user);
368         }
369     }
370
371     function blowNoticeCache($blowLast=false)
372     {
373         if ($this->is_local) {
374             $cache = common_memcache();
375             if (!empty($cache)) {
376                 $cache->delete(common_cache_key('profile:notice_ids:'.$this->profile_id));
377                 if ($blowLast) {
378                     $cache->delete(common_cache_key('profile:notice_ids:'.$this->profile_id.';last'));
379                 }
380             }
381         }
382     }
383
384     function blowRepliesCache($blowLast=false)
385     {
386         $cache = common_memcache();
387         if ($cache) {
388             $reply = new Reply();
389             $reply->notice_id = $this->id;
390             if ($reply->find()) {
391                 while ($reply->fetch()) {
392                     $cache->delete(common_cache_key('reply:stream:'.$reply->profile_id));
393                     if ($blowLast) {
394                         $cache->delete(common_cache_key('reply:stream:'.$reply->profile_id.';last'));
395                     }
396                 }
397             }
398             $reply->free();
399             unset($reply);
400         }
401     }
402
403     function blowPublicCache($blowLast=false)
404     {
405         if ($this->is_local == 1) {
406             $cache = common_memcache();
407             if ($cache) {
408                 $cache->delete(common_cache_key('public'));
409                 if ($blowLast) {
410                     $cache->delete(common_cache_key('public').';last');
411                 }
412             }
413         }
414     }
415
416     function blowFavesCache($blowLast=false)
417     {
418         $cache = common_memcache();
419         if ($cache) {
420             $fave = new Fave();
421             $fave->notice_id = $this->id;
422             if ($fave->find()) {
423                 while ($fave->fetch()) {
424                     $cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id));
425                     if ($blowLast) {
426                         $cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id.';last'));
427                     }
428                 }
429             }
430             $fave->free();
431             unset($fave);
432         }
433     }
434
435     # XXX: too many args; we need to move to named params or even a separate
436     # class for notice streams
437
438     static function getStream($qry, $cachekey, $offset=0, $limit=20, $since_id=0, $before_id=0, $order=null, $since=null) {
439
440         if (common_config('memcached', 'enabled')) {
441
442             # Skip the cache if this is a since, since_id or before_id qry
443             if ($since_id > 0 || $before_id > 0 || $since) {
444                 return Notice::getStreamDirect($qry, $offset, $limit, $since_id, $before_id, $order, $since);
445             } else {
446                 return Notice::getCachedStream($qry, $cachekey, $offset, $limit, $order);
447             }
448         }
449
450         return Notice::getStreamDirect($qry, $offset, $limit, $since_id, $before_id, $order, $since);
451     }
452
453     static function getStreamDirect($qry, $offset, $limit, $since_id, $before_id, $order, $since) {
454
455         $needAnd = false;
456         $needWhere = true;
457
458         if (preg_match('/\bWHERE\b/i', $qry)) {
459             $needWhere = false;
460             $needAnd = true;
461         }
462
463         if ($since_id > 0) {
464
465             if ($needWhere) {
466                 $qry .= ' WHERE ';
467                 $needWhere = false;
468             } else {
469                 $qry .= ' AND ';
470             }
471
472             $qry .= ' notice.id > ' . $since_id;
473         }
474
475         if ($before_id > 0) {
476
477             if ($needWhere) {
478                 $qry .= ' WHERE ';
479                 $needWhere = false;
480             } else {
481                 $qry .= ' AND ';
482             }
483
484             $qry .= ' notice.id < ' . $before_id;
485         }
486
487         if ($since) {
488
489             if ($needWhere) {
490                 $qry .= ' WHERE ';
491                 $needWhere = false;
492             } else {
493                 $qry .= ' AND ';
494             }
495
496             $qry .= ' notice.created > \'' . date('Y-m-d H:i:s', $since) . '\'';
497         }
498
499         # Allow ORDER override
500
501         if ($order) {
502             $qry .= $order;
503         } else {
504             $qry .= ' ORDER BY notice.created DESC, notice.id DESC ';
505         }
506
507         if (common_config('db','type') == 'pgsql') {
508             $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
509         } else {
510             $qry .= ' LIMIT ' . $offset . ', ' . $limit;
511         }
512
513         $notice = new Notice();
514
515         $notice->query($qry);
516
517         return $notice;
518     }
519
520     # XXX: this is pretty long and should probably be broken up into
521     # some helper functions
522
523     static function getCachedStream($qry, $cachekey, $offset, $limit, $order) {
524
525         # If outside our cache window, just go to the DB
526
527         if ($offset + $limit > NOTICE_CACHE_WINDOW) {
528             return Notice::getStreamDirect($qry, $offset, $limit, null, null, $order, null);
529         }
530
531         # Get the cache; if we can't, just go to the DB
532
533         $cache = common_memcache();
534
535         if (!$cache) {
536             return Notice::getStreamDirect($qry, $offset, $limit, null, null, $order, null);
537         }
538
539         # Get the notices out of the cache
540
541         $notices = $cache->get(common_cache_key($cachekey));
542
543         # On a cache hit, return a DB-object-like wrapper
544
545         if ($notices !== false) {
546             $wrapper = new ArrayWrapper(array_slice($notices, $offset, $limit));
547             return $wrapper;
548         }
549
550         # If the cache was invalidated because of new data being
551         # added, we can try and just get the new stuff. We keep an additional
552         # copy of the data at the key + ';last'
553
554         # No cache hit. Try to get the *last* cached version
555
556         $last_notices = $cache->get(common_cache_key($cachekey) . ';last');
557
558         if ($last_notices) {
559
560             # Reverse-chron order, so last ID is last.
561
562             $last_id = $last_notices[0]->id;
563
564             # XXX: this assumes monotonically increasing IDs; a fair
565             # bet with our DB.
566
567             $new_notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW,
568                                                   $last_id, null, $order, null);
569
570             if ($new_notice) {
571                 $new_notices = array();
572                 while ($new_notice->fetch()) {
573                     $new_notices[] = clone($new_notice);
574                 }
575                 $new_notice->free();
576                 $notices = array_slice(array_merge($new_notices, $last_notices),
577                                        0, NOTICE_CACHE_WINDOW);
578
579                 # Store the array in the cache for next time
580
581                 $result = $cache->set(common_cache_key($cachekey), $notices);
582                 $result = $cache->set(common_cache_key($cachekey) . ';last', $notices);
583
584                 # return a wrapper of the array for use now
585
586                 return new ArrayWrapper(array_slice($notices, $offset, $limit));
587             }
588         }
589
590         # Otherwise, get the full cache window out of the DB
591
592         $notice = Notice::getStreamDirect($qry, 0, NOTICE_CACHE_WINDOW, null, null, $order, null);
593
594         # If there are no hits, just return the value
595
596         if (!$notice) {
597             return $notice;
598         }
599
600         # Pack results into an array
601
602         $notices = array();
603
604         while ($notice->fetch()) {
605             $notices[] = clone($notice);
606         }
607
608         $notice->free();
609
610         # Store the array in the cache for next time
611
612         $result = $cache->set(common_cache_key($cachekey), $notices);
613         $result = $cache->set(common_cache_key($cachekey) . ';last', $notices);
614
615         # return a wrapper of the array for use now
616
617         $wrapper = new ArrayWrapper(array_slice($notices, $offset, $limit));
618
619         return $wrapper;
620     }
621
622     function getStreamByIds($ids)
623     {
624         $cache = common_memcache();
625
626         if (!empty($cache)) {
627             $notices = array();
628             foreach ($ids as $id) {
629                 $notices[] = Notice::staticGet('id', $id);
630             }
631             return new ArrayWrapper($notices);
632         } else {
633             $notice = new Notice();
634             $notice->whereAdd('id in (' . implode(', ', $ids) . ')');
635             $notice->orderBy('id DESC');
636
637             $notice->find();
638             return $notice;
639         }
640     }
641
642     function publicStream($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null)
643     {
644         $ids = Notice::stream(array('Notice', '_publicStreamDirect'),
645                               array(),
646                               'public',
647                               $offset, $limit, $since_id, $before_id, $since);
648
649         return Notice::getStreamByIds($ids);
650     }
651
652     function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null)
653     {
654         $notice = new Notice();
655
656         $notice->selectAdd(); // clears it
657         $notice->selectAdd('id');
658
659         $notice->orderBy('id DESC');
660
661         if (!is_null($offset)) {
662             $notice->limit($offset, $limit);
663         }
664
665         if (common_config('public', 'localonly')) {
666             $notice->whereAdd('is_local = 1');
667         } else {
668             # -1 == blacklisted
669             $notice->whereAdd('is_local != -1');
670         }
671
672         if ($since_id != 0) {
673             $notice->whereAdd('id > ' . $since_id);
674         }
675
676         if ($before_id != 0) {
677             $notice->whereAdd('id < ' . $before_id);
678         }
679
680         if (!is_null($since)) {
681             $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
682         }
683
684         $ids = array();
685
686         if ($notice->find()) {
687             while ($notice->fetch()) {
688                 $ids[] = $notice->id;
689             }
690         }
691
692         $notice->free();
693         $notice = NULL;
694
695         return $ids;
696     }
697
698     function addToInboxes()
699     {
700         $enabled = common_config('inboxes', 'enabled');
701
702         if ($enabled === true || $enabled === 'transitional') {
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) ' .
706               "SELECT $UT.id, " . $this->id . ", '" . $this->created . "' " .
707               "FROM $UT JOIN subscription ON $UT.id = subscription.subscriber " .
708               'WHERE subscription.subscribed = ' . $this->profile_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             $inbox->query($qry);
717         }
718         return;
719     }
720
721     function addToAuthorInbox()
722     {
723         $enabled = common_config('inboxes', 'enabled');
724
725         if ($enabled === true || $enabled === 'transitional') {
726             $user = User::staticGet('id', $this->profile_id);
727             if (empty($user)) {
728                 return;
729             }
730             $inbox = new Notice_inbox();
731             $UT = common_config('db','type')=='pgsql'?'"user"':'user';
732             $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created) ' .
733               "SELECT $UT.id, " . $this->id . ", '" . $this->created . "' " .
734               "FROM $UT " .
735               "WHERE $UT.id = " . $this->profile_id . ' ' .
736               'AND NOT EXISTS (SELECT user_id, notice_id ' .
737               'FROM notice_inbox ' .
738               "WHERE user_id = " . $this->profile_id . ' '.
739               'AND notice_id = ' . $this->id . ' )';
740             if ($enabled === 'transitional') {
741                 $qry .= " AND $UT.inboxed = 1";
742             }
743             $inbox->query($qry);
744         }
745         return;
746     }
747
748     function saveGroups()
749     {
750         $enabled = common_config('inboxes', 'enabled');
751         if ($enabled !== true && $enabled !== 'transitional') {
752             return;
753         }
754
755         /* extract all !group */
756         $count = preg_match_all('/(?:^|\s)!([A-Za-z0-9]{1,64})/',
757                                 strtolower($this->content),
758                                 $match);
759         if (!$count) {
760             return true;
761         }
762
763         $profile = $this->getProfile();
764
765         /* Add them to the database */
766
767         foreach (array_unique($match[1]) as $nickname) {
768             /* XXX: remote groups. */
769             $group = User_group::staticGet('nickname', $nickname);
770
771             if (!$group) {
772                 continue;
773             }
774
775             // we automatically add a tag for every group name, too
776
777             $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($nickname),
778                                            'notice_id' => $this->id));
779
780             if (is_null($tag)) {
781                 $this->saveTag($nickname);
782             }
783
784             if ($profile->isMember($group)) {
785
786                 $gi = new Group_inbox();
787
788                 $gi->group_id  = $group->id;
789                 $gi->notice_id = $this->id;
790                 $gi->created   = common_sql_now();
791
792                 $result = $gi->insert();
793
794                 if (!$result) {
795                     common_log_db_error($gi, 'INSERT', __FILE__);
796                 }
797
798                 // FIXME: do this in an offline daemon
799
800                 $this->addToGroupInboxes($group);
801             }
802         }
803     }
804
805     function addToGroupInboxes($group)
806     {
807         $inbox = new Notice_inbox();
808         $UT = common_config('db','type')=='pgsql'?'"user"':'user';
809         $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created, source) ' .
810           "SELECT $UT.id, " . $this->id . ", '" . $this->created . "', 2 " .
811           "FROM $UT JOIN group_member ON $UT.id = group_member.profile_id " .
812           'WHERE group_member.group_id = ' . $group->id . ' ' .
813           'AND NOT EXISTS (SELECT user_id, notice_id ' .
814           'FROM notice_inbox ' .
815           "WHERE user_id = $UT.id " .
816           'AND notice_id = ' . $this->id . ' )';
817         if ($enabled === 'transitional') {
818             $qry .= " AND $UT.inboxed = 1";
819         }
820         $result = $inbox->query($qry);
821     }
822
823     function saveReplies()
824     {
825         // Alternative reply format
826         $tname = false;
827         if (preg_match('/^T ([A-Z0-9]{1,64}) /', $this->content, $match)) {
828             $tname = $match[1];
829         }
830         // extract all @messages
831         $cnt = preg_match_all('/(?:^|\s)@([a-z0-9]{1,64})/', $this->content, $match);
832
833         $names = array();
834
835         if ($cnt || $tname) {
836             // XXX: is there another way to make an array copy?
837             $names = ($tname) ? array_unique(array_merge(array(strtolower($tname)), $match[1])) : array_unique($match[1]);
838         }
839
840         $sender = Profile::staticGet($this->profile_id);
841
842         $replied = array();
843
844         // store replied only for first @ (what user/notice what the reply directed,
845         // we assume first @ is it)
846
847         for ($i=0; $i<count($names); $i++) {
848             $nickname = $names[$i];
849             $recipient = common_relative_profile($sender, $nickname, $this->created);
850             if (!$recipient) {
851                 continue;
852             }
853             if ($i == 0 && ($recipient->id != $sender->id) && !$this->reply_to) { // Don't save reply to self
854                 $reply_for = $recipient;
855                 $recipient_notice = $reply_for->getCurrentNotice();
856                 if ($recipient_notice) {
857                     $orig = clone($this);
858                     $this->reply_to = $recipient_notice->id;
859                     $this->conversation = $recipient_notice->conversation;
860                     $this->update($orig);
861                 }
862             }
863             // Don't save replies from blocked profile to local user
864             $recipient_user = User::staticGet('id', $recipient->id);
865             if ($recipient_user && $recipient_user->hasBlocked($sender)) {
866                 continue;
867             }
868             $reply = new Reply();
869             $reply->notice_id = $this->id;
870             $reply->profile_id = $recipient->id;
871             $id = $reply->insert();
872             if (!$id) {
873                 $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
874                 common_log(LOG_ERR, 'DB error inserting reply: ' . $last_error->message);
875                 common_server_error(sprintf(_('DB error inserting reply: %s'), $last_error->message));
876                 return;
877             } else {
878                 $replied[$recipient->id] = 1;
879             }
880         }
881
882         // Hash format replies, too
883         $cnt = preg_match_all('/(?:^|\s)@#([a-z0-9]{1,64})/', $this->content, $match);
884         if ($cnt) {
885             foreach ($match[1] as $tag) {
886                 $tagged = Profile_tag::getTagged($sender->id, $tag);
887                 foreach ($tagged as $t) {
888                     if (!$replied[$t->id]) {
889                         // Don't save replies from blocked profile to local user
890                         $t_user = User::staticGet('id', $t->id);
891                         if ($t_user && $t_user->hasBlocked($sender)) {
892                             continue;
893                         }
894                         $reply = new Reply();
895                         $reply->notice_id = $this->id;
896                         $reply->profile_id = $t->id;
897                         $id = $reply->insert();
898                         if (!$id) {
899                             common_log_db_error($reply, 'INSERT', __FILE__);
900                             return;
901                         } else {
902                             $replied[$recipient->id] = 1;
903                         }
904                     }
905                 }
906             }
907         }
908
909         // If it's not a reply, make it the root of a new conversation
910
911         if (empty($this->conversation)) {
912             $orig = clone($this);
913             $this->conversation = $this->id;
914             $this->update($orig);
915         }
916
917         foreach (array_keys($replied) as $recipient) {
918             $user = User::staticGet('id', $recipient);
919             if ($user) {
920                 mail_notify_attn($user, $this);
921             }
922         }
923     }
924
925     function asAtomEntry($namespace=false, $source=false)
926     {
927         $profile = $this->getProfile();
928
929         $xs = new XMLStringer(true);
930
931         if ($namespace) {
932             $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
933                            'xmlns:thr' => 'http://purl.org/syndication/thread/1.0');
934         } else {
935             $attrs = array();
936         }
937
938         $xs->elementStart('entry', $attrs);
939
940         if ($source) {
941             $xs->elementStart('source');
942             $xs->element('title', null, $profile->nickname . " - " . common_config('site', 'name'));
943             $xs->element('link', array('href' => $profile->profileurl));
944             $user = User::staticGet('id', $profile->id);
945             if (!empty($user)) {
946                 $atom_feed = common_local_url('api',
947                                               array('apiaction' => 'statuses',
948                                                     'method' => 'user_timeline',
949                                                     'argument' => $profile->nickname.'.atom'));
950                 $xs->element('link', array('rel' => 'self',
951                                            'type' => 'application/atom+xml',
952                                            'href' => $profile->profileurl));
953                 $xs->element('link', array('rel' => 'license',
954                                            'href' => common_config('license', 'url')));
955             }
956
957             $xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE));
958         }
959
960         $xs->elementStart('author');
961         $xs->element('name', null, $profile->nickname);
962         $xs->element('uri', null, $profile->profileurl);
963         $xs->elementEnd('author');
964
965         if ($source) {
966             $xs->elementEnd('source');
967         }
968
969         $xs->element('title', null, $this->content);
970         $xs->element('summary', null, $this->content);
971
972         $xs->element('link', array('rel' => 'alternate',
973                                    'href' => $this->bestUrl()));
974
975         $xs->element('id', null, $this->uri);
976
977         $xs->element('published', null, common_date_w3dtf($this->created));
978         $xs->element('updated', null, common_date_w3dtf($this->modified));
979
980         if ($this->reply_to) {
981             $reply_notice = Notice::staticGet('id', $this->reply_to);
982             if (!empty($reply_notice)) {
983                 $xs->element('link', array('rel' => 'related',
984                                            'href' => $reply_notice->bestUrl()));
985                 $xs->element('thr:in-reply-to',
986                              array('ref' => $reply_notice->uri,
987                                    'href' => $reply_notice->bestUrl()));
988             }
989         }
990
991         $xs->element('content', array('type' => 'html'), $this->rendered);
992
993         $tag = new Notice_tag();
994         $tag->notice_id = $this->id;
995         if ($tag->find()) {
996             while ($tag->fetch()) {
997                 $xs->element('category', array('term' => $tag->tag));
998             }
999         }
1000         $tag->free();
1001
1002         $xs->elementEnd('entry');
1003
1004         return $xs->getString();
1005     }
1006
1007     function bestUrl()
1008     {
1009         if (!empty($this->url)) {
1010             return $this->url;
1011         } else if (!empty($this->uri) && preg_match('/^https?:/', $this->uri)) {
1012             return $this->uri;
1013         } else {
1014             return common_local_url('shownotice',
1015                                     array('notice' => $this->id));
1016         }
1017     }
1018
1019     function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $before_id=0, $since=null)
1020     {
1021         $cache = common_memcache();
1022
1023         if (empty($cache) ||
1024             $since_id != 0 || $before_id != 0 || !is_null($since) ||
1025             ($offset + $limit) > NOTICE_CACHE_WINDOW) {
1026             return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id,
1027                                                                       $before_id, $since)));
1028         }
1029
1030         $idkey = common_cache_key($cachekey);
1031
1032         $idstr = $cache->get($idkey);
1033
1034         if (!empty($idstr)) {
1035             // Cache hit! Woohoo!
1036             $window = explode(',', $idstr);
1037             $ids = array_slice($window, $offset, $limit);
1038             return $ids;
1039         }
1040
1041         $laststr = $cache->get($idkey.';last');
1042
1043         if (!empty($laststr)) {
1044             $window = explode(',', $laststr);
1045             $last_id = $window[0];
1046             $new_ids = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW,
1047                                                                           $last_id, 0, null)));
1048
1049             $new_window = array_merge($new_ids, $window);
1050
1051             $new_windowstr = implode(',', $new_window);
1052
1053             $result = $cache->set($idkey, $new_windowstr);
1054             $result = $cache->set($idkey . ';last', $new_windowstr);
1055
1056             $ids = array_slice($new_window, $offset, $limit);
1057
1058             return $ids;
1059         }
1060
1061         $window = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW,
1062                                                                      0, 0, null)));
1063
1064         $windowstr = implode(',', $window);
1065
1066         $result = $cache->set($idkey, $windowstr);
1067         $result = $cache->set($idkey . ';last', $windowstr);
1068
1069         $ids = array_slice($window, $offset, $limit);
1070
1071         return $ids;
1072     }
1073 }