mourningdove/cgi-bin/LJ/NotificationInbox.pm
2026-05-24 01:03:05 +00:00

897 lines
23 KiB
Perl

# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License.
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.
# This package is for managing a queue of notifications
# for a user.
# Mischa Spiegelmock, 4/28/06
package LJ::NotificationInbox;
use strict;
use Carp qw(croak);
use LJ::NotificationItem;
use LJ::Event;
use LJ::NotificationArchive;
# constructor takes a $u
sub new {
my ( $class, $u ) = @_;
croak "Invalid args to construct LJ::NotificationInbox" unless $class && $u;
croak "Invalid user" unless LJ::isu($u);
# return singleton from $u if it already exists
return $u->{_notification_inbox} if $u->{_notification_inbox};
my $self = {
uid => $u->userid,
count => undef, # defined once ->count is loaded/cached
items => undef, # defined to arrayref once items loaded
bookmarks => undef, # defined to arrayref
};
return $u->{_notification_inbox} = bless $self, $class;
}
# returns the user object associated with this queue
*owner = \&u;
sub u {
my $self = shift;
return LJ::load_userid( $self->{uid} );
}
# Returns a list of LJ::NotificationItems in this queue.
sub items {
my $self = shift;
croak "items is an object method"
unless ( ref $self ) eq __PACKAGE__;
return @{ $self->{items} } if defined $self->{items};
my @qids = $self->_load;
my @items = ();
foreach my $qid (@qids) {
push @items, LJ::NotificationItem->new( $self->owner, $qid );
}
$self->{items} = \@items;
# optimization:
# now items are defined ... if any are comment
# objects we'll instantiate those ahead of time
# so that if one has its data loaded they will
# all benefit from a coalesced load
$self->instantiate_comment_singletons;
$self->instantiate_message_singletons;
return @items;
}
# returns a list of all notification items except for sent user messages
sub all_items {
my $self = shift;
return grep { $_->event && $_->event->class ne "LJ::Event::UserMessageSent" } $self->items;
}
# returns a list of friend-related notificationitems
sub friend_items {
my $self = shift;
my @friend_events = friend_event_list();
my %friend_events = map { "LJ::Event::" . $_ => 1 } @friend_events;
return grep { $friend_events{ $_->event->class } } $self->items;
}
# returns a list of friend-related notificationitems
sub circle_items {
my $self = shift;
my @friend_events = circle_event_list();
my %friend_events = map { "LJ::Event::" . $_ => 1 } @friend_events;
return grep { $friend_events{ $_->event->class } } $self->items;
}
# returns a list of non user-messaging notificationitems
sub non_usermsg_items {
my $self = shift;
my @usermsg_events = qw(
UserMessageRecvd
UserMessageSent
);
@usermsg_events =
( @usermsg_events, ( LJ::Hooks::run_hook('usermsg_notification_types') || () ) );
my %usermsg_events = map { "LJ::Event::" . $_ => 1 } @usermsg_events;
return grep { !$usermsg_events{ $_->event->class } } $self->items;
}
# returns a list of non user-message recvd notificationitems
sub usermsg_recvd_items {
my $self = shift;
my @events = ('UserMessageRecvd');
return $self->subset_items(@events);
}
# returns a list of non user-message recvd notificationitems
sub usermsg_sent_items {
my $self = shift;
my @events = ('UserMessageSent');
return $self->subset_items(@events);
}
sub usermsg_sent_last_items {
my $self = shift;
my @events = ('UserMessageSent');
return $self->subset_items(@events);
}
sub birthday_items {
my $self = shift;
my @events = ('Birthday');
return $self->subset_items(@events);
}
sub encircled_items {
my $self = shift;
my @events = ('AddedToCircle');
return $self->subset_items(@events);
}
sub entrycomment_items {
my $self = shift;
my @events = entrycomment_event_list();
return $self->subset_items(@events);
}
sub pollvote_items {
return $_[0]->subset_items('PollVote');
}
sub communitymembership_items {
my $self = shift;
my @community_events = communitymembership_event_list();
my %community_events = map { "LJ::Event::" . $_ => 1 } @community_events;
return grep { $community_events{ $_->event->class } } $self->items;
}
sub sitenotices_items {
my $self = shift;
my @site_events = sitenotices_event_list();
my %site_events = map { "LJ::Event::" . $_ => 1 } @site_events;
return grep { $site_events{ $_->event->class } } $self->items;
}
# return a subset of notificationitems
sub subset_items {
my ( $self, @subset ) = @_;
my %subset_events = map { "LJ::Event::" . $_ => 1 } @subset;
return grep { $subset_events{ $_->event->class } } $self->items;
}
sub singleentry_items {
my ( $self, $itemid ) = @_;
my %related_events = map { $_ => 1 } LJ::Event::JournalNewComment->related_events;
return grep {
$related_events{ $_->event->etypeid }
&& $_->event->comment
&& $_->event->comment
->entry # may have been deleted, which breaks all filter to entry comments
&& $_->event->comment->entry->ditemid == $itemid
} $self->items;
}
# return flagged notifications
sub bookmark_items {
my $self = shift;
return grep { $self->is_bookmark( $_->qid ) } $self->items;
}
# return archived notifications
sub archived_items {
my $self = shift;
my $u = $self->u;
my $archive = $u->notification_archive;
return $archive->items;
}
# return unread notifications
sub unread_items {
my $self = shift;
return grep { $_->unread } $self->items;
}
sub count {
my $self = shift;
return $self->{count} if defined $self->{count};
if ( defined $self->{items} ) {
return $self->{count} = scalar @{ $self->{items} };
}
my $u = $self->owner;
return $self->{count} =
$u->selectrow_array( "SELECT COUNT(*) FROM notifyqueue WHERE userid=?", undef, $u->id );
}
# returns number of unread items in inbox
# returns a maximum of 1000, if you get 1000 it's safe to
# assume "more than 1000"
sub unread_count {
my $self = shift;
# cached unread count
my $unread = LJ::MemCache::get( $self->_unread_memkey );
return $unread if defined $unread;
# not cached, load from DB
my $u = $self->u or die "No user";
my $sth =
$u->prepare("SELECT COUNT(*) FROM notifyqueue WHERE userid=? AND state='N' LIMIT 1000");
$sth->execute( $u->id );
die $sth->errstr if $sth->err;
($unread) = $sth->fetchrow_array;
# cache it
LJ::MemCache::set( $self->_unread_memkey, $unread, 30 * 60 );
return $unread;
}
# load the items in this queue
# returns internal items hashref
sub _load {
my $self = shift;
my $u = $self->u
or die "No user object";
# is it memcached?
my $qids;
$qids = LJ::MemCache::get( $self->_memkey ) and return @$qids;
# not cached, load
my $sth = $u->prepare( "SELECT userid, qid, journalid, etypeid, arg1, arg2, state, createtime "
. "FROM notifyqueue WHERE userid=?" );
$sth->execute( $u->{userid} );
die $sth->errstr if $sth->err;
my @items = ();
while ( my $row = $sth->fetchrow_hashref ) {
my $qid = $row->{qid};
# load this item into process cache so it's ready to go
my $qitem = LJ::NotificationItem->new( $u, $qid );
$qitem->absorb_row($row);
push @items, $qitem;
}
# sort based on create time
@items = sort { $a->when_unixtime <=> $b->when_unixtime } @items;
# get sorted list of ids
my @item_ids = map { $_->qid } @items;
# cache
LJ::MemCache::set( $self->_memkey, \@item_ids, 86400 );
return @item_ids;
}
sub instantiate_comment_singletons {
my $self = shift;
# instantiate all the comment singletons so that they will all be
# loaded efficiently later as soon as preload_rows is called on
# the first comment object
my @comment_items = grep {
$_->event
&& ( $_->event->class eq 'LJ::Event::JournalNewComment'
|| $_->event->class eq 'LJ::Event::JournalNewComment::TopLevel'
|| $_->event->class eq 'LJ::Event::JournalNewComment::Edited' )
} $self->items;
my @comment_events = map { $_->event } @comment_items;
# instantiate singletons
LJ::Comment->new( $_->event_journal, jtalkid => $_->jtalkid ) foreach @comment_events;
return 1;
}
sub instantiate_message_singletons {
my $self = shift;
# instantiate all the message singletons so that they will all be
# loaded efficiently later as soon as preload_rows is called on
# the first message object
my @message_items =
grep { $_->event && $_->event->class eq 'LJ::Event::UserMessageRecvd' } $self->items;
my @message_events = map { $_->event } @message_items;
# instantiate singletons
LJ::Message->load( { msgid => $_->arg1, journalid => $_->u->{userid} } )
foreach @message_events;
return 1;
}
sub _memkey {
my $self = shift;
my $userid = $self->u->id;
return [ $userid, "inbox:$userid" ];
}
sub _unread_memkey {
my $self = shift;
my $userid = $self->u->id;
return [ $userid, "inbox:newct:${userid}" ];
}
sub _bookmark_memkey {
my $self = shift;
my $userid = $self->u->id;
return [ $userid, "inbox:bookmarks:${userid}" ];
}
# deletes an Event that is queued for this user
# args: Queue ID to remove from queue
sub delete_from_queue {
my ( $self, $qitem ) = @_;
croak "delete_from_queue is an object method"
unless ( ref $self ) eq __PACKAGE__;
my $qid = $qitem->qid;
croak "no queueid for queue item passed to delete_from_queue" unless int($qid);
my $u = $self->u
or die "No user object";
$u->do( "DELETE FROM notifyqueue WHERE userid=? AND qid=?", undef, $u->id, $qid );
die $u->errstr if $u->err;
# invalidate caches
$self->expire_cache;
return 1;
}
sub expire_cache {
my $self = shift;
$self->{count} = undef;
$self->{items} = undef;
LJ::MemCache::delete( $self->_memkey );
LJ::MemCache::delete( $self->_unread_memkey );
}
# FIXME: make this faster
sub oldest_item {
my $self = shift;
my @items = $self->items;
my $oldest;
foreach my $item (@items) {
$oldest = $item if !$oldest || $item->when_unixtime < $oldest->when_unixtime;
}
return $oldest;
}
# This will enqueue an event object
# Returns the enqueued item
sub enqueue {
my ( $self, %opts ) = @_;
my $evt = delete $opts{event};
my $archive = delete $opts{archive} || LJ::is_enabled('esn_archive');
croak "No event" unless $evt;
croak "Extra args passed to enqueue" if %opts;
my $u = $self->u or die "No user";
# if over the max, delete the oldest notification
my $max = $u->count_inbox_max;
my $skip = $max - 1; # number to skip to get to max
if ( $max && $self->count >= $max ) {
# Get list of bookmarks and ignore them when checking inbox limits
my $bmarks = join ',', map { $self->is_bookmark( $_->qid ) ? $_->qid : () } $self->items;
my $bookmark_sql = '';
$bookmark_sql = "AND qid NOT IN ($bmarks) " if ($bmarks);
my $too_old_qid = $u->selectrow_array(
"SELECT qid FROM notifyqueue "
. "WHERE userid=? $bookmark_sql"
. "ORDER BY qid DESC LIMIT $skip,1",
undef, $u->id
);
if ($too_old_qid) {
$u->do( "DELETE FROM notifyqueue WHERE userid=? AND qid <= ? $bookmark_sql",
undef, $u->id, $too_old_qid );
$self->expire_cache;
}
}
# get a qid
my $qid = LJ::alloc_user_counter( $u, 'Q' )
or die "Could not alloc new queue ID";
my %item = (
qid => $qid,
userid => $u->{userid},
journalid => $evt->u->{userid},
etypeid => $evt->etypeid,
arg1 => $evt->arg1,
arg2 => $evt->arg2,
state => $evt->mark_read ? 'R' : 'N',
createtime => $evt->eventtime_unix || time()
);
# insert this event into the notifyqueue table
$u->do(
"INSERT INTO notifyqueue ("
. join( ",", keys %item )
. ") VALUES ("
. join( ",", map { '?' } values %item ) . ")",
undef,
values %item
) or die $u->errstr;
if ($archive) {
# insert into the notifyarchive table with State defaulted to space
$item{state} = ' ';
$u->do(
"INSERT INTO notifyarchive ("
. join( ",", keys %item )
. ") VALUES ("
. join( ",", map { '?' } values %item ) . ")",
undef,
values %item
) or die $u->errstr;
}
# invalidate memcache
$self->expire_cache;
return LJ::NotificationItem->new( $u, $qid );
}
# return true if item is bookmarked
sub is_bookmark {
my ( $self, $qid ) = @_;
# load bookmarks if they don't already exist
$self->load_bookmarks unless defined $self->{bookmarks};
return $self->{bookmarks}{$qid} ? 1 : 0;
}
# populate the bookmark hash
sub load_bookmarks {
my ($self) = @_;
my $u = $self->u;
my $uid = $self->u->id;
my $row = LJ::MemCache::get( $self->_bookmark_memkey );
$self->{bookmarks} = ();
if ($row) {
my @qids = unpack( "N*", $row );
foreach my $qid (@qids) {
$self->{bookmarks}{$qid} = 1;
}
return;
}
my $sql = "SELECT qid FROM notifybookmarks WHERE userid=?";
my $qids = $u->selectcol_arrayref( $sql, undef, $uid );
die "Failed to load bookmarks: " . $u->errstr . "\n" if $u->err;
foreach my $qid (@$qids) {
$self->{bookmarks}{$qid} = 1;
}
$row = pack( "N*", @$qids );
LJ::MemCache::set( $self->_bookmark_memkey, $row, 3600 );
return;
}
# add a bookmark
sub add_bookmark {
my ( $self, $qid ) = @_;
my $u = $self->u;
my $uid = $self->u->id;
return 0 unless $self->can_add_bookmark;
my $sql = "INSERT IGNORE INTO notifybookmarks (userid, qid) VALUES (?, ?)";
$u->do( $sql, undef, $uid, $qid );
die "Failed to add bookmark: " . $u->errstr . "\n" if $u->err;
# Make sure notice is in inbox
$self->ensure_queued($qid);
$self->{bookmarks}{$qid} = 1 if defined $self->{bookmarks};
LJ::MemCache::delete( $self->_bookmark_memkey );
return 1;
}
# remove bookmark
sub remove_bookmark {
my ( $self, $qid ) = @_;
my $u = $self->u;
my $uid = $self->u->id;
my $sql = "DELETE FROM notifybookmarks WHERE userid=? AND qid=?";
$u->do( $sql, undef, $uid, $qid );
die "Failed to remove bookmark: " . $u->errstr . "\n" if $u->err;
delete $self->{bookmarks}->{$qid} if defined $self->{bookmarks};
LJ::MemCache::delete( $self->_bookmark_memkey );
return 1;
}
# add or remove bookmark based on whether it is already bookmarked
sub toggle_bookmark {
my ( $self, $qid ) = @_;
my $ret =
$self->is_bookmark($qid)
? $self->remove_bookmark($qid)
: $self->add_bookmark($qid);
return $ret;
}
# return true if can add a bookmark
sub can_add_bookmark {
my ( $self, $count ) = @_;
my $max = $self->u->count_bookmark_max;
$count = $count || 1;
my $bookmark_count = scalar $self->bookmark_items;
return 0 if ( ( $bookmark_count + $count ) > $max );
return 1;
}
# return count of unread bookmarks
sub bookmark_count {
my ( $self, $count ) = @_;
my $unread_bookmark_count = scalar grep { $_->unread } $self->bookmark_items;
return $unread_bookmark_count;
}
sub delete_all {
my ( $self, $view, %args ) = @_;
my @items;
# Unless in folder 'Bookmarks', don't fetch any bookmarks
if ( $view eq 'all' ) {
@items = $self->all_items;
push @items, $self->usermsg_sent_items;
}
elsif ( $view eq 'usermsg_recvd' ) {
@items = $self->usermsg_recvd_items;
}
elsif ( $view eq 'circle' ) {
@items = $self->circle_items;
push @items, $self->birthday_items;
push @items, $self->encircled_items;
}
elsif ( $view eq 'birthday' ) {
@items = $self->birthday_items;
}
elsif ( $view eq 'encircled' ) {
@items = $self->encircled_items;
}
elsif ( $view eq 'entrycomment' ) {
@items = $self->entrycomment_items;
}
elsif ( $view eq 'bookmark' ) {
@items = $self->bookmark_items;
}
elsif ( $view eq 'usermsg_sent' ) {
@items = $self->usermsg_sent_items;
}
elsif ( $view eq 'usermsg_sent_last' ) {
@items = $self->usermsg_sent_last_items;
}
elsif ( $view eq 'singleentry' && $args{itemid} ) {
my $itemid = $args{itemid} + 0;
return unless $itemid;
@items = $self->singleentry_items($itemid);
}
elsif ( $view eq 'pollvote' ) {
@items = $self->pollvote_items;
}
elsif ( $view eq 'communitymembership' ) {
@items = $self->communitymembership_items;
}
elsif ( $view eq 'sitenotices' ) {
@items = $self->sitenotices_items;
}
elsif ( $view eq 'unread' ) {
@items = $self->unread_items;
}
@items = grep { !$self->is_bookmark( $_->qid ) } @items
unless $view eq 'bookmark';
my @ret;
foreach my $item (@items) {
push @ret, { qid => $item->qid };
}
# Delete items
foreach my $item (@items) {
$item->delete;
}
return @ret;
}
sub mark_all_read {
my ( $self, $view, %args ) = @_;
my @items;
# Only get items in currently viewed folder and subfolders
if ( $view eq 'all' ) {
@items = $self->all_items;
push @items, $self->usermsg_sent_items;
}
elsif ( $view eq 'usermsg_recvd' ) {
@items = $self->usermsg_recvd_items;
}
elsif ( $view eq 'circle' ) {
@items = $self->circle_items;
push @items, $self->birthday_items;
push @items, $self->encircled_items;
}
elsif ( $view eq 'birthday' ) {
@items = $self->birthday_items;
}
elsif ( $view eq 'encircled' ) {
@items = $self->encircled_items;
}
elsif ( $view eq 'entrycomment' ) {
@items = $self->entrycomment_items;
}
elsif ( $view eq 'bookmark' ) {
@items = $self->bookmark_items;
}
elsif ( $view eq 'usermsg_sent' ) {
@items = $self->usermsg_sent_items;
}
elsif ( $view eq 'usermsg_sent_last' ) {
@items = $self->usermsg_sent_last_items;
}
elsif ( $view eq 'singleentry' && $args{itemid} ) {
my $itemid = $args{itemid} + 0;
return unless $itemid;
@items = $self->singleentry_items($itemid);
}
elsif ( $view eq 'pollvote' ) {
@items = $self->pollvote_items;
}
elsif ( $view eq 'communitymembership' ) {
@items = $self->communitymembership_items;
}
elsif ( $view eq 'sitenotices' ) {
@items = $self->sitenotices_items;
}
elsif ( $view eq 'unread' ) {
@items = $self->unread_items;
}
# Mark read
$_->mark_read foreach @items;
return @items;
}
# Copy archive notice to inbox
# Needed when bookmarking a notice that only lives in archive
sub ensure_queued {
my ( $self, $qid ) = @_;
my $u = $self->u
or die "No user object";
my $sth = $u->prepare( "SELECT userid, qid, journalid, etypeid, arg1, arg2, state, createtime "
. "FROM notifyarchive WHERE userid=? AND qid=?" );
$sth->execute( $u->{userid}, $qid );
die $sth->errstr if $sth->err;
my $row = $sth->fetchrow_hashref;
if ($row) {
my %item = (
qid => $row->{qid},
userid => $row->{userid},
journalid => $row->{journalid},
etypeid => $row->{etypeid},
arg1 => $row->{arg1},
arg2 => $row->{arg2},
state => 'R',
createtime => $row->{createtime}
);
# insert this event into the notifyqueue table
$u->do(
"INSERT IGNORE INTO notifyqueue ("
. join( ",", keys %item )
. ") VALUES ("
. join( ",", map { '?' } values %item ) . ")",
undef,
values %item
) or die $u->errstr;
# invalidate memcache
$self->expire_cache;
}
return;
}
# return a count of a subset of notificationitems
sub subset_unread_count {
my ( $self, @subset ) = @_;
my %subset_events = map { "LJ::Event::" . $_ => 1 } @subset;
my @events =
grep { $_->event && $subset_events{ $_->event->class } && $_->unread } $self->items;
return scalar @events;
}
sub all_event_count {
my $self = shift;
my @events =
grep { $_->event && $_->event->class ne 'LJ::Event::UserMessageSent' && $_->unread }
$self->items;
return scalar @events;
}
sub friend_event_count {
my $self = shift;
return $self->subset_unread_count( friend_event_list() );
}
sub circle_event_count {
my $self = shift;
return $self->subset_unread_count( circle_event_list() );
}
sub entrycomment_event_count {
my $self = shift;
return $self->subset_unread_count( entrycomment_event_list() );
}
sub pollvote_event_count {
my $self = shift;
return $self->subset_unread_count('PollVote');
}
sub usermsg_recvd_event_count {
my $self = shift;
my @events = ('UserMessageRecvd');
return $self->subset_unread_count(@events);
}
sub usermsg_sent_event_count {
my $self = shift;
my @events = ('UserMessageSent');
return $self->subset_unread_count(@events);
}
sub communitymembership_event_count {
my $self = shift;
return $self->subset_unread_count( communitymembership_event_list() );
}
sub sitenotices_event_count {
my $self = shift;
return $self->subset_unread_count( sitenotices_event_list() );
}
# Methods that return Arrays of Event categories
sub friend_event_list {
my @events = qw(
AddedToCircle
RemovedFromCircle
InvitedFriendJoins
CommunityInvite
NewUserpic
);
@events = ( @events, ( LJ::Hooks::run_hook('friend_notification_types') || () ) );
return @events;
}
sub circle_event_list {
my @events = qw(
AddedToCircle
RemovedFromCircle
InvitedFriendJoins
CommunityInvite
NewUserpic
Birthday
);
@events = ( @events, ( LJ::Hooks::run_hook('friend_notification_types') || () ) );
return @events;
}
sub entrycomment_event_list {
my @events = (
'JournalNewEntry', 'JournalNewComment',
'JournalNewComment::TopLevel', 'JournalNewComment::Edited'
);
return @events;
}
sub communitymembership_event_list {
my @events = ( 'CommunityJoinApprove', 'CommunityJoinReject', 'CommunityJoinRequest' );
return @events;
}
sub sitenotices_event_list {
my @events = (
'ImportStatus', 'OfficialPost',
'SecurityAttributeChanged', 'UserExpunged',
'VgiftApproved', 'VgiftDelivered',
'XPostFailure', 'XPostSuccess'
);
return @events;
}
1;