1 package LJ
::NotificationInbox
;
9 LJ
::Event
::JournalNewComment
10 LJ
::Event
::InboxUserMessageRecvd
11 LJ
::Event LJ
::NotificationArchive
15 use LJ
::MemCacheProxy
;
16 use LJ
::NotificationItem
;
18 my ($comment_typeid, $rmessage_typeid, $smessage_typeid);
20 # constructor takes a $u
22 my ($class, $user) = @_;
24 croak
'Invalid args to construct LJ::NotificationInbox' unless $class and $user;
25 croak
'Invalid user' unless LJ
::isu
($user);
27 # return singleton from $u if it already exists
28 return $LJ::REQ_CACHE_INBOX
{'inbox'} if $LJ::REQ_CACHE_INBOX
{'inbox'};
35 $comment_typeid ||= LJ
::Event
::JournalNewComment
->etypeid;
36 $rmessage_typeid ||= LJ
::Event
::UserMessageRecvd
->etypeid;
37 $smessage_typeid ||= LJ
::Event
::UserMessageSent
->etypeid;
39 return $LJ::REQ_CACHE_INBOX
{'inbox'} = bless $self;
42 # returns the user object associated with this queue
44 sub owner
{ $_[0]->{'user'} }
46 # Returns a list of LJ::NotificationItems in this queue.
48 return @
{ $_[0]->{'items'} }
53 $_[0]->{'items'} = [map {
54 LJ
::NotificationItem
->new($user, $_);
58 # now items are defined ... if any are comment
59 # objects we'll instantiate those ahead of time
60 # so that if one has its data loaded they will
61 # all benefit from a coalesced load
62 &instantiate_singletons
;
64 return @
{ $_[0]->{'items'} };
67 # returns a list of all notification items except for sent user messages
69 grep { $_->{state} ne 'S' } grep { $_->event->class ne "LJ::Event::UserMessageSent" } grep {$_->event} $_[0]->items;
72 # returns a list of friend-related notificationitems
74 my @friend_events = friend_event_list
();
76 my %friend_events = map { "LJ::Event::" . $_ => 1 } @friend_events;
78 grep { $friend_events{$_->event->class} } grep {$_->event} $_[0]->items;
81 # returns a list of friend-related notificationitems
82 sub friendplus_items
{
83 my @friend_events = friendplus_event_list
();
85 my %friend_events = map { "LJ::Event::" . $_ => 1 } @friend_events;
87 grep { $friend_events{$_->event->class} } grep {$_->event} $_[0]->items;
90 # returns a list of non user-messaging notificationitems
91 sub non_usermsg_items
{
92 my @usermsg_events = qw(
97 @usermsg_events = (@usermsg_events, (LJ
::run_hook
('usermsg_notification_types') || ()));
99 my %usermsg_events = map { "LJ::Event::" . $_ => 1 } @usermsg_events;
101 grep { !$usermsg_events{$_->event->class} } grep {$_->event} $_[0]->items;
104 # returns a list of non user-message recvd notificationitems
105 sub usermsg_recvd_items
{
106 my @events = ( 'UserMessageRecvd' );
107 my @items = $_[0]->subset_items(@events);
109 @items = grep { $_->{state} ne 'S' } @items if LJ
::is_enabled
('spam_inbox');
114 # returns a list of non user-message recvd notificationitems
115 sub usermsg_sent_items
{
116 $_[0]->subset_items('UserMessageSent');
119 # returns a list of spam notificationitems
121 grep { $_->{'state'} eq 'S' } $_[0]->subset_items('UserMessageRecvd');
125 $_[0]->subset_items('Birthday');
128 sub befriended_items
{
129 $_[0]->subset_items('Befriended');
132 sub entrycomment_items
{
133 $_[0]->subset_items(entrycomment_event_list
());
136 # return a subset of notificationitems
138 my ($self, @subset) = @_;
140 my %subset_events = map { "LJ::Event::" . $_ => 1 } @subset;
141 return grep { $subset_events{$_->event->class} } $self->items;
144 # return flagged notifications
146 grep { $_[0]->is_bookmark($_->qid) } $_[0]->items;
149 # return archived notifications
151 &owner
->notification_archive->items;
155 my $count = LJ
::MemCacheProxy
::get
(&_count_memkey
);
162 $count = @
{ $LJ::REQ_CACHE_INBOX
{'events'} };
164 LJ
::MemCacheProxy
::set
(&_count_memkey
, $count, 86400);
169 # returns number of unread items in inbox
173 my $unread = LJ
::MemCacheProxy
::get
(&_unread_memkey
);
181 'N' eq uc $_->{'state'}
182 } @
{ $LJ::REQ_CACHE_INBOX
{'events'} };
184 LJ
::MemCacheProxy
::set
(&_unread_memkey
, $unread, 86400);
189 # unread message count
190 sub unread_message_count
{
192 my $unread = LJ
::MemCacheProxy
::get
(&_unread_msg_memkey
);
200 'N' eq uc $_->{'state'}
202 $_->{'etypeid'} == $rmessage_typeid
203 } @
{ $LJ::REQ_CACHE_INBOX
{'events'} };
205 LJ
::MemCacheProxy
::set
(&_unread_msg_memkey
, $unread, 86400);
208 } # unread_message_count
210 # unread message count
211 sub unread_event_count
{
215 'N' eq uc $_->{'state'}
217 $_->{'etypeid'} != $rmessage_typeid and
218 $_->{'etypeid'} != $smessage_typeid
219 } @
{ $LJ::REQ_CACHE_INBOX
{'events'} };
222 sub spam_event_count
{
227 } @
{ $LJ::REQ_CACHE_INBOX
{'events'} };
230 # load the items in this queue
231 # returns internal items hashref
233 &LJ
::NotificationItem
::_load
;
236 unless defined wantarray;
238 return map $_->{'qid'}, @
{ $LJ::REQ_CACHE_INBOX
{'events'} };
241 sub instantiate_singletons
{
242 return 1 unless $LJ::DISABLED
{'inbox_controller'};
245 my $event = $_->event() or next;
246 my $etypeid = $event->etypeid();
248 # instantiate all the comment singletons so that they will all be
249 # loaded efficiently later as soon as preload_rows is called on
250 # the first comment object
252 $event->event_journal,
253 jtalkid
=> $event->{'args'}[0],
254 ) if $etypeid == $comment_typeid;
256 # instantiate all the message singletons so that they will all be
257 # loaded efficiently later as soon as preload_rows is called on
258 # the first message object
260 msgid
=> $event->{'args'}[0],
261 otherid
=> $event->{'args'}[1],
262 journalid
=> $event->{'userid'},
263 }) if $etypeid == $rmessage_typeid or $etypeid == $smessage_typeid;
270 my $userid = $_[0]->{'userid'};
271 return [$userid, "inbox:$userid"];
275 my $userid = $_[0]->{'userid'};
276 return [$userid, "inbox:cnt:$userid"];
280 my $userid = $_[0]->{'userid'};
281 return [$userid, "inbox:newct:$userid"];
284 sub _unread_msg_memkey
{
285 my $userid = $_[0]->{'userid'};
286 return [$userid, "inbox:newct:msg:$userid"];
289 sub _bookmark_memkey
{
290 my $userid = $_[0]->{'userid'};
291 return [$userid, "inbox:bookmarks:$userid"];
294 *_events_memkey
= \
&LJ
::NotificationItem
::_events_memkey
;
296 # deletes an Event that is queued for this user
297 # args: Queue ID to remove from queue
298 sub delete_from_queue
{
299 my ($self, $item) = @_;
301 my $qid = $item->qid();
303 croak
'no queueid for queue item passed to delete_from_queue'
307 or die 'No user object';
309 # Try to remove bookmark first
310 $self->remove_bookmark($qid);
312 $user->do('DELETE FROM notifyqueue WHERE userid=? AND qid=?', undef, $self->{'userid'}, $qid);
326 delete $self->{'items'};
328 $self->{'_loaded'} = 0;
330 delete $LJ::REQ_CACHE_INBOX
{'events'};
332 LJ
::MemCacheProxy
::delete(&_memkey
);
333 LJ
::MemCacheProxy
::delete(&_count_memkey
);
334 LJ
::MemCacheProxy
::delete(&_unread_memkey
);
335 LJ
::MemCacheProxy
::delete(&_unread_msg_memkey
);
336 LJ
::MemCacheProxy
::delete(&_events_memkey
);
337 LJ
::MemCacheProxy
::delete(&_bookmark_memkey
);
340 # This will enqueue an event object
341 # Returns the enqueued item
343 my ($self, %opts) = @_;
344 my $evt = delete $opts{event
};
345 my $archive = delete $opts{archive
} || 1;
347 croak
"No event" unless $evt;
348 croak
"Extra args passed to enqueue" if %opts;
350 my $u = &owner
or die "No user";
352 # if over the max, delete the oldest notification
353 my $max = $u->get_cap('inbox_max');
354 my $skip = $max - 1; # number to skip to get to max
356 if ($max && $self->count >= $max) {
358 # Get list of bookmarks and ignore them when checking inbox limits
359 my $bmarks = join ',', $self->get_bookmarks_ids;
360 my $bookmark_sql = ($bmarks) ?
"AND qid NOT IN ($bmarks) " : '';
362 my $too_old_qid = $u->selectrow_array
363 ("SELECT qid FROM notifyqueue ".
364 "WHERE userid=? $bookmark_sql".
365 "ORDER BY qid DESC LIMIT $skip,1",
369 $u->do("DELETE FROM notifyqueue WHERE userid=? AND qid <= ? $bookmark_sql",
370 undef, $u->id, $too_old_qid);
376 my $qid = LJ
::alloc_user_counter
($u, 'Q');
380 die "Could not alloc new queue ID";
384 if ($evt->etypeid == LJ
::Event
::UserMessageRecvd
->etypeid) {
385 if (my $msg = $evt->load_message()) {
392 my %item = (qid
=> $qid,
393 userid
=> $u->{userid
},
394 journalid
=> $evt->userid,
395 etypeid
=> $evt->etypeid,
398 state => $spam ?
'S' : $evt->mark_read ?
'R' : 'N',
399 createtime
=> $evt->eventtime_unix || time());
401 # insert this event into the notifyqueue table
402 $u->do("INSERT INTO notifyqueue (" . join(",", keys %item) . ") VALUES (" .
403 join(",", map { '?' } values %item) . ")", undef, values %item)
407 # insert into the notifyarchive table with State defaulted to space
409 $u->do("INSERT INTO notifyarchive (" . join(",", keys %item) . ") VALUES (" .
410 join(",", map { '?' } values %item) . ")", undef, values %item)
415 if ( LJ
::Event
::UserMessageRecvd
->etypeid == $evt->etypeid ) {
416 $self->__send_notify({
418 'journal_u' => LJ
::want_user
($evt->arg2),
419 'msgid' => $evt->arg1,
420 'etypeid' => $evt->etypeid,
424 # invalidate memcache
427 return LJ
::NotificationItem
->new($u, $qid);
431 my ($self, $data) = @_;
432 my $msgid = $data->{'msgid'};
433 my $u = $data->{'u'};
434 my $journal_u = $data->{'journal_u'};
436 LJ
::Event
::InboxUserMessageRecvd
->new($u, $msgid, $journal_u)->fire;
439 # return true if item is bookmarked
441 my ($self, $qid) = @_;
443 # load bookmarks if they don't already exist
445 unless $self->{'bookmarks'};
447 return $self->{'bookmarks'}{$qid}?
1 : 0;
451 my ($class, $u, $target) = @_;
452 die 'Expected parameter $u in is_contact' unless $u;
453 die 'Expected parameter $target in is_contact' unless $target;
456 my $tid = $target->id;
461 my $key = [$uid, "inbox:contacts:$uid"];
462 my $val = LJ
::MemCacheProxy
::get
($key);
465 my %uids = unpack '(VC)*', $val;
467 if (exists $uids{$uid}) {
472 my $dbh = LJ
::get_cluster_master
($u);
476 my $res = $dbh->selectcol_arrayref(qq[
506 $val .= pack 'VC', $uid, $res;
509 LJ
::MemCacheProxy
::set
($key, $val, 86400);
515 # populate the bookmark hash
520 return scalar keys %{ $self->{'bookmarks'} }
521 if $self->{'bookmarks'};
524 my $row = LJ
::MemCacheProxy
::get
(&_bookmark_memkey
);
527 $self->{'bookmarks'}{$_} = 1
528 foreach unpack $format, $row;
532 my %items = map { $_->{'qid'} => 1 } @
{ $LJ::REQ_CACHE_INBOX
{'events'} };
534 my @obsolete = grep {
535 not exists $items{$_}
536 } keys %{ $self->{'bookmarks'} };
538 return scalar keys %{ $self->{'bookmarks'} }
541 $self->{'bookmarks'} = {};
543 $u->do(join(join(',', map '?', @obsolete), 'DELETE FROM notifybookmarks WHERE userid=? AND qid IN (', ')'), undef, $self->{'userid'}, @obsolete);
546 my $qids = $u->selectcol_arrayref('SELECT qid FROM notifybookmarks WHERE userid=?', undef, $self->{'userid'});
548 die sprintf "Failed to load bookmarks: %s\n", $u->errstr
551 $self->{'bookmarks'}{$_} = 1
554 $row = pack $format, @
$qids;
556 LJ
::MemCacheProxy
::set
(&_bookmark_memkey
, $row, 86400);
558 return scalar keys %{ $self->{'bookmarks'} };
561 ## returns array of qid of 'bookmarked' messages
562 sub get_bookmarks_ids
{
566 unless $self->{'bookmarks'};
568 return keys %{ $self->{'bookmarks'} };
573 my ($self, $qid) = @_;
578 unless &can_add_bookmark
;
583 $user->do('INSERT IGNORE INTO notifybookmarks (userid, qid) VALUES (?, ?)', undef, $self->{'userid'}, $qid);
588 # try to load message by qid
589 $rmessage_typeid ||= LJ
::Event
::UserMessageRecvd
->etypeid;
590 $smessage_typeid ||= LJ
::Event
::UserMessageSent
->etypeid;
592 if (my ($msgid) = $user->selectrow_array("
595 WHERE userid=? AND qid=? AND etypeid IN (?, ?)",
596 undef, $self->{'userid'}, $qid, $rmessage_typeid, $smessage_typeid)) {
598 $user->do('INSERT IGNORE INTO usermsgbookmarks (journalid, msgid) VALUES (?, ?)', undef, $self->{'userid'}, $msgid);
604 # Make sure notice is in inbox
605 $self->ensure_queued($qid);
607 $self->{'bookmarks'}{$qid} = 1;
609 LJ
::MemCacheProxy
::delete(&_bookmark_memkey
);
615 sub remove_bookmark
{
616 my ($self, $qid) = @_;
623 $user->do('DELETE FROM notifybookmarks WHERE userid=? AND qid=?', undef, $self->{'userid'}, $qid);
628 # try to load message by qid
629 $rmessage_typeid ||= LJ
::Event
::UserMessageRecvd
->etypeid;
630 $smessage_typeid ||= LJ
::Event
::UserMessageSent
->etypeid;
632 if (my ($msgid) = $user->selectrow_array("
635 WHERE userid=? AND qid=? AND etypeid IN (?, ?)",
636 undef, $self->{'userid'}, $qid, $rmessage_typeid, $smessage_typeid)) {
638 $user->do('DELETE FROM usermsgbookmarks WHERE journalid=? AND msgid=?', undef, $self->{'userid'}, $msgid);
644 delete $self->{'bookmarks'}{$qid};
646 LJ
::MemCacheProxy
::delete(&_bookmark_memkey
);
651 # add or remove bookmark based on whether it is already bookmarked
652 sub toggle_bookmark
{ &is_bookmark?
&remove_bookmark
: &add_bookmark
}
654 # return true if can add a bookmark
655 sub can_add_bookmark
{
656 my $max = &owner
->get_cap('inbox_max');
658 return 0 if $max <= &load_bookmarks
+ 1;
663 my ( $self, $view, %opts ) = @_;
666 # Unless in folder 'Bookmarks', don't fetch any bookmarks
667 if ( $view eq 'all' ) {
668 @items = $self->all_items;
669 push @items, $self->usermsg_sent_items;
670 } elsif ( $view eq 'usermsg_recvd' ) {
671 @items = $self->usermsg_recvd_items;
672 } elsif ( $view eq 'friendplus' ) {
673 @items = $self->friendplus_items;
674 push @items, $self->birthday_items;
675 push @items, $self->befriended_items;
676 } elsif ( $view eq 'birthday' ) {
677 @items = $self->birthday_items;
678 } elsif ( $view eq 'befriended' ) {
679 @items = $self->befriended_items;
680 } elsif ( $view eq 'entrycomment' ) {
681 @items = $self->entrycomment_items;
682 } elsif ( $view eq 'bookmark' ) {
683 @items = $self->bookmark_items;
684 } elsif ( $view eq 'usermsg_sent' ) {
685 @items = $self->usermsg_sent_items;
686 } elsif ( $view eq 'spam' ) {
687 @items = $self->spam_items;
690 @items = grep { !$self->is_bookmark($_->qid) } @items
691 unless $view eq 'bookmark';
694 foreach my $item (@items) {
695 push @ret, {qid
=> $item->qid};
699 my $interface = $opts{'interface'};
701 LJ
::User
::UserlogRecord
::InboxMassDelete
->create( $u,
703 'items' => scalar @items,
704 'method' => 'delete_all',
710 foreach my $item (@items) {
712 my $msg = $item->event->load_message();
713 $msg->mark_as_spam();
721 sub delete_all_from_sender
{
722 my ( $self, $senderid ) = @_;
725 @items = grep { $_->event->class ne "LJ::Event::UserMessageSent" } grep {$_->event} $self->items;
727 @items = grep { !$self->is_bookmark($_->qid) } @items;
731 foreach my $item (@items) {
732 next unless $item->event->arg2 == $senderid;
733 push @ret, {qid
=> $item->qid};
741 my ( $self, $view ) = @_;
744 # Only get items in currently viewed folder and subfolders
745 if ( $view eq 'all' ) {
746 @items = $self->all_items;
747 push @items, $self->usermsg_sent_items;
748 } elsif ( $view eq 'usermsg_recvd' ) {
749 @items = $self->usermsg_recvd_items;
750 } elsif ( $view eq 'friendplus' ) {
751 @items = $self->friendplus_items;
752 push @items, $self->birthday_items;
753 push @items, $self->befriended_items;
754 } elsif ( $view eq 'birthday' ) {
755 @items = $self->birthday_items;
756 } elsif ( $view eq 'befriended' ) {
757 @items = $self->befriended_items;
758 } elsif ( $view eq 'entrycomment' ) {
759 @items = $self->entrycomment_items;
760 } elsif ( $view eq 'bookmark' ) {
761 @items = $self->bookmark_items;
762 } elsif ( $view eq 'usermsg_sent' ) {
763 @items = $self->usermsg_sent_items;
767 $_->mark_read foreach @items;
771 # Copy archive notice to inbox
772 # Needed when bookmarking a notice that only lives in archive
774 my ($self, $qid) = @_;
777 or die "No user object";
779 my $sth = $u->prepare
780 ("SELECT userid, qid, journalid, etypeid, arg1, arg2, state, createtime " .
781 "FROM notifyarchive WHERE userid=? AND qid=?");
782 $sth->execute($u->{userid
}, $qid);
783 die $sth->errstr if $sth->err;
785 my $row = $sth->fetchrow_hashref;
787 my %item = (qid
=> $row->{qid
},
788 userid
=> $row->{userid
},
789 journalid
=> $row->{journalid
},
790 etypeid
=> $row->{etypeid
},
791 arg1
=> $row->{arg1
},
792 arg2
=> $row->{arg2
},
794 createtime
=> $row->{createtime
});
796 # insert this event into the notifyqueue table
797 $u->do("INSERT IGNORE INTO notifyqueue (" . join(",", keys %item) . ") VALUES (" .
798 join(",", map { '?' } values %item) . ")", undef, values %item)
801 # invalidate memcache
808 # return a count of a subset of notificationitems
809 sub subset_unread_count
{
810 my ($self, @subset) = @_;
812 my %subset_events = map { "LJ::Event::" . $_ => 1 } @subset;
813 my @events = grep { $subset_events{$_->event->class} && $_->unread } grep {$_->event} $self->items;
814 return scalar @events;
817 sub all_event_count
{
818 scalar grep { $_->event->class ne 'LJ::Event::UserMessageSent' && $_->unread } grep { $_->event } $_[0]->items;
821 sub friend_event_count
{
822 $_[0]->subset_unread_count(friend_event_list
());
825 sub friendplus_event_count
{
826 $_[0]->subset_unread_count(friendplus_event_list
());
829 sub entrycomment_event_count
{
830 $_[0]->subset_unread_count(entrycomment_event_list
());
833 sub usermsg_recvd_event_count
{
834 $_[0]->subset_unread_count('UserMessageRecvd');
837 sub usermsg_sent_event_count
{
838 $_[0]->subset_unread_count('UserMessageSent');
842 # Methods that return Arrays of Event categories
843 sub friend_event_list
{
850 @events = (@events, (LJ
::run_hook
('friend_notification_types') || ()));
854 sub friendplus_event_list
{
863 @events = (@events, (LJ
::run_hook
('friend_notification_types') || ()));
867 sub entrycomment_event_list
{