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

655 lines
16 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.
package LJ::Subscription;
use strict;
use v5.10;
use Log::Log4perl;
my $log = Log::Log4perl->get_logger(__PACKAGE__);
use Carp qw/ croak confess /;
use LJ::Event;
use LJ::NotificationMethod;
use LJ::Subscription::Pending;
use LJ::Typemap;
use constant {
INACTIVE => 1 << 0, # user has deactivated
DISABLED => 1 << 1, # system has disabled
TRACKING => 1 << 2, # subs in the "notices" category
};
my @subs_fields = qw(userid subid is_dirty journalid etypeid arg1 arg2
ntypeid createtime expiretime flags);
sub new_by_id {
my ( $class, $u, $subid ) = @_;
croak "new_by_id requires a valid 'u' object"
unless LJ::isu($u);
return if $u->is_expunged;
croak "invalid subscription id passed"
unless defined $subid && int($subid) > 0;
my $row = $u->selectrow_hashref(
"SELECT userid, subid, is_dirty, journalid, etypeid, "
. "arg1, arg2, ntypeid, createtime, expiretime, flags "
. "FROM subs WHERE userid=? AND subid=?",
undef, $u->{userid}, $subid
);
die $u->errstr if $u->err;
return $class->new_from_row($row);
}
sub freeze {
my $self = shift;
return "subid-" . $self->owner->{userid} . '-' . $self->id;
}
# can return either a LJ::Subscription or LJ::Subscription::Pending object
sub thaw {
my ( $class, $data, $u, $POST ) = @_;
# valid format?
return undef unless ( $data =~ /^(pending|subid) - $u->{userid} .+ ?(-old)?$/x );
my ( $type, $userid, $subid ) = split( "-", $data );
return LJ::Subscription::Pending->thaw( $data, $u, $POST ) if $type eq 'pending';
die "Invalid subscription data type: $type" unless $type eq 'subid';
unless ($u) {
my $subuser = LJ::load_userid($userid);
die "no user" unless $subuser;
$u = LJ::get_authas_user($subuser);
die "Invalid user $subuser->{user}" unless $u;
}
return $class->new_by_id( $u, $subid );
}
sub pending { 0 }
sub default_selected { $_[0]->active && $_[0]->enabled }
sub subscriptions_of_user {
my ( $class, $u ) = @_;
croak "subscriptions_of_user requires a valid 'u' object"
unless LJ::isu($u);
return if $u->is_expunged;
return @{ $u->{_subscriptions} } if defined $u->{_subscriptions};
my $sth =
$u->prepare( "SELECT userid, subid, is_dirty, journalid, etypeid, "
. "arg1, arg2, ntypeid, createtime, expiretime, flags "
. "FROM subs WHERE userid=?" );
$sth->execute( $u->{userid} );
die $u->errstr if $u->err;
my @subs;
while ( my $row = $sth->fetchrow_hashref ) {
push @subs, LJ::Subscription->new_from_row($row);
}
$u->{_subscriptions} = \@subs;
return @subs;
}
# Class method
# Look for a subscription matching the parameters: journalu/journalid,
# ntypeid/method, event/etypeid, arg1, arg2
# Returns a list of subscriptions for this user matching the parameters
sub find {
my ( $class, $u, %params ) = @_;
my ( $etypeid, $ntypeid, $arg1, $arg2, $flags );
if ( my $evt = delete $params{event} ) {
$etypeid = LJ::Event->event_to_etypeid($evt);
}
if ( my $nmeth = delete $params{method} ) {
$ntypeid = LJ::NotificationMethod->method_to_ntypeid($nmeth);
}
$etypeid ||= delete $params{etypeid};
$ntypeid ||= delete $params{ntypeid};
$flags = delete $params{flags};
my $journalid = delete $params{journalid};
$journalid ||= LJ::want_userid( delete $params{journal} ) if defined $params{journal};
$arg1 = delete $params{arg1};
$arg2 = delete $params{arg2};
my $require_active = delete $params{require_active} ? 1 : 0;
croak "Invalid parameters passed to ${class}->find" if keys %params;
return () if defined $arg1 && $arg1 =~ /\D/;
return () if defined $arg2 && $arg2 =~ /\D/;
my @subs = $u->subscriptions;
@subs = grep { $_->active && $_->enabled } @subs if $require_active;
# filter subs on each parameter
@subs = grep { $_->journalid == $journalid } @subs if defined $journalid;
@subs = grep { $_->ntypeid == $ntypeid } @subs if $ntypeid;
@subs = grep { $_->etypeid == $etypeid } @subs if $etypeid;
if ( defined $flags ) {
# check DISABLED and TRACKING flags, but not INACTIVE flag.
@subs = grep { ( $flags & DISABLED ) == $_->disabled } @subs;
@subs = grep { ( $flags & TRACKING ) == $_->is_tracking_category } @subs;
}
@subs = grep { $_->arg1 == $arg1 } @subs if defined $arg1;
@subs = grep { $_->arg2 == $arg2 } @subs if defined $arg2;
return @subs;
}
# Instance method
# Deactivates a subscription. If this is not a "tracking" subscription,
# it will delete it instead. Does nothing to disabled subscriptions.
sub deactivate {
my $self = shift;
my %opts = @_;
my $force = delete $opts{force}; # force-delete
croak "Invalid args" if scalar keys %opts;
my $subid = $self->id
or croak "Invalid subsciption";
my $u = $self->owner;
# don't care about disabled subscriptions
return if $self->disabled;
# if it's the inbox method, deactivate/delete the other notification methods too
my @to_remove = ();
my @subs = $self->corresponding_subs;
foreach my $subscr (@subs) {
# Don't deactivate if the Inbox is always subscribed to
my $always_checked = $subscr->event_class->always_checked ? 1 : 0;
if ( $subscr->is_tracking_category && !$force ) {
# delete non-inbox methods if we're deactivating
if ( $subscr->method eq 'LJ::NotificationMethod::Inbox' && !$always_checked ) {
$subscr->_deactivate;
}
else {
$subscr->delete;
}
}
else {
$subscr->delete;
}
}
}
# deletes a subscription
sub delete {
my $self = shift;
my $u = $self->owner;
my @subs = $self->corresponding_subs;
foreach my $subscr (@subs) {
$u->do( "DELETE FROM subs WHERE subid=? AND userid=?", undef, $subscr->id, $u->id );
}
# delete from cache in user
undef $u->{_subscriptions};
return 1;
}
# class method, nukes all subs for a user
sub delete_all_subs {
my ( $class, $u ) = @_;
return if $u->is_expunged;
$u->do( "DELETE FROM subs WHERE userid = ?", undef, $u->id );
undef $u->{_subscriptions};
return 1;
}
# class method, nukes all inactive subs for a user
sub delete_all_inactive_subs {
my ( $class, $u, $dryrun ) = @_;
return if $u->is_expunged;
my @subs = $class->find($u);
@subs = grep { !( $_->active && $_->enabled ) } @subs;
my $count = scalar @subs;
if ( $count > 0 && !$dryrun ) {
$_->delete foreach (@subs);
undef $u->{_subscriptions};
}
return $count;
}
# find matching subscriptions with different notification methods
sub corresponding_subs {
my $self = shift;
my @subs = ($self);
if ( $self->method eq 'LJ::NotificationMethod::Inbox' ) {
push @subs,
$self->owner->find_subscriptions(
journalid => $self->journalid,
etypeid => $self->etypeid,
arg1 => $self->arg1,
arg2 => $self->arg2,
);
}
return @subs;
}
# Class method
sub new_from_row {
my ( $class, $row ) = @_;
return undef unless $row;
my $self = bless {%$row}, $class;
# TODO validate keys of row.
return $self;
}
sub create {
my ( $class, $u, %args ) = @_;
# easier way for eveenttype
if ( my $evt = delete $args{'event'} ) {
$args{etypeid} = LJ::Event->event_to_etypeid($evt);
}
# easier way to specify ntypeid
if ( my $ntype = delete $args{'method'} ) {
$args{ntypeid} = LJ::NotificationMethod->method_to_ntypeid($ntype);
}
# easier way to specify journal
if ( my $ju = delete $args{'journal'} ) {
$args{journalid} = $ju->{userid} if $ju;
}
$args{arg1} ||= 0;
$args{arg2} ||= 0;
$args{journalid} ||= 0;
foreach (qw(ntypeid etypeid)) {
croak "Required field '$_' not found in call to $class->create" unless defined $args{$_};
}
foreach (qw(userid subid createtime)) {
croak "Can't specify field '$_'" if defined $args{$_};
}
# load current subscription, check if subscription already exists
$class->subscriptions_of_user($u) unless $u->{_subscriptions};
my ($existing) = grep {
$args{etypeid} == $_->{etypeid}
&& $args{ntypeid} == $_->{ntypeid}
&& $args{journalid} == $_->{journalid}
&& $args{arg1} == $_->{arg1}
&& $args{arg2} == $_->{arg2}
&& ( $args{flags} & DISABLED ) == $_->disabled
&& ( $args{flags} & TRACKING ) == $_->is_tracking_category
} @{ $u->{_subscriptions} };
# allow matches if the activation state is unequal
if ( defined $existing ) {
$existing->activate;
return $existing;
}
my $subid = LJ::alloc_user_counter( $u, 'E' )
or die "Could not alloc subid for user $u->{user}";
$args{subid} = $subid;
$args{userid} = $u->{userid};
$args{createtime} = time();
my $self = $class->new_from_row( \%args );
my @columns;
my @values;
foreach (@subs_fields) {
if ( exists( $args{$_} ) ) {
push @columns, $_;
push @values, delete $args{$_};
}
}
croak( "Extra args defined, (" . join( ', ', keys(%args) ) . ")" ) if keys %args;
my $sth =
$u->prepare( 'INSERT INTO subs ('
. join( ',', @columns ) . ')'
. 'VALUES ('
. join( ',', map { '?' } @values )
. ')' );
LJ::errobj($u)->throw if $u->err;
$sth->execute(@values);
die $sth->errstr if $sth->err;
$self->subscriptions_of_user($u) unless $u->{_subscriptions};
push @{ $u->{_subscriptions} }, $self;
return $self;
}
# returns a hash of arguments representing this subscription (useful for passing to
# other functions, such as find)
sub sub_info {
my $self = shift;
return (
journalid => $self->journalid,
etypeid => $self->etypeid,
ntypeid => $self->ntypeid,
arg1 => $self->arg1,
arg2 => $self->arg2,
flags => $self->flags,
);
}
# returns a nice HTML description of this current subscription
sub as_html {
my $self = shift;
my $evtclass = LJ::Event->class( $self->etypeid );
return undef unless $evtclass;
return $evtclass->subscription_as_html($self);
}
sub set_tracking {
my $self = shift;
$self->set_flag(TRACKING);
}
sub activate {
my $self = shift;
$self->clear_flag(INACTIVE);
}
sub _deactivate {
my $self = shift;
$self->set_flag(INACTIVE);
}
sub enable {
my $self = shift;
$_->clear_flag(DISABLED) foreach $self->corresponding_subs;
}
sub disable {
my $self = shift;
$_->set_flag(DISABLED) foreach $self->corresponding_subs;
}
sub set_flag {
my ( $self, $flag ) = @_;
my $flags = $self->flags;
# don't bother if flag already set
return if $flags & $flag;
$flags |= $flag;
if ( $self->owner && !$self->pending ) {
$self->owner->do( "UPDATE subs SET flags = flags | ? WHERE userid=? AND subid=?",
undef, $flag, $self->owner->userid, $self->id );
die $self->owner->errstr if $self->owner->errstr;
$self->{flags} = $flags;
delete $self->owner->{_subscriptions};
}
}
sub clear_flag {
my ( $self, $flag ) = @_;
my $flags = $self->flags;
# don't bother if flag already cleared
return unless $flags & $flag;
# clear the flag
$flags &= ~$flag;
if ( $self->owner && !$self->pending ) {
$self->owner->do( "UPDATE subs SET flags = flags & ~? WHERE userid=? AND subid=?",
undef, $flag, $self->owner->userid, $self->id );
die $self->owner->errstr if $self->owner->errstr;
$self->{flags} = $flags;
delete $self->owner->{_subscriptions};
}
}
sub id {
my $self = shift;
return $self->{subid};
}
sub createtime {
my $self = shift;
return $self->{createtime};
}
sub flags {
my $self = shift;
return $self->{flags} || 0;
}
sub active {
my $self = shift;
return !( $self->flags & INACTIVE );
}
sub enabled {
my $self = shift;
return !( $self->flags & DISABLED );
}
sub disabled {
my $self = shift;
return !$self->enabled;
}
sub is_tracking_category {
my $self = shift;
return $self->flags & TRACKING;
}
sub expiretime {
my $self = shift;
return $self->{expiretime};
}
sub journalid {
my $self = shift;
return $self->{journalid};
}
sub journal {
my $self = shift;
return LJ::load_userid( $self->{journalid} );
}
sub arg1 {
my $self = shift;
return $self->{arg1};
}
sub arg2 {
my $self = shift;
return $self->{arg2};
}
sub ntypeid {
my $self = shift;
return $self->{ntypeid};
}
sub method {
my $self = shift;
return LJ::NotificationMethod->class( $self->ntypeid );
}
sub notify_class {
my $self = shift;
return LJ::NotificationMethod->class( $self->{ntypeid} );
}
sub etypeid {
my $self = shift;
return $self->{etypeid};
}
sub event_class {
my $self = shift;
return LJ::Event->class( $self->{etypeid} );
}
# returns the owner (userid) of the subscription
sub userid {
my $self = shift;
return $self->{userid};
}
sub owner {
my $self = shift;
return LJ::load_userid( $self->{userid} );
}
sub dirty {
my $self = shift;
return $self->{is_dirty};
}
sub notification {
my $subscr = shift;
my $class = LJ::NotificationMethod->class( $subscr->{ntypeid} );
my $note = $class->new_from_subscription($subscr);
return $note;
}
sub process {
my ( $self, @events ) = @_;
my $note = $self->notification or return;
return 1
if $self->etypeid == LJ::Event::OfficialPost->etypeid
&& !LJ::is_enabled('officialpost_esn');
# significant events (such as SecurityAttributeChanged) must be processed even for inactive users.
return 1
unless $self->notify_class->configured_for_user( $self->owner )
|| LJ::Event->class( $self->etypeid )->is_significant;
return $note->notify(@events);
}
sub unique {
my $self = shift;
my $note = $self->notification or return undef;
return $note->unique . ':' . $self->owner->{user};
}
# returns true if two subscriptions are equivalent
sub equals {
my ( $self, $other ) = @_;
return 1 if defined $other->id && $self->id == $other->id;
my $match =
$self->ntypeid == $other->ntypeid
&& $self->etypeid == $other->etypeid
&& $self->flags == $other->flags;
$match &&= $other->arg1 && ( $self->arg1 == $other->arg1 ) if $self->arg1;
$match &&= $other->arg2 && ( $self->arg2 == $other->arg2 ) if $self->arg2;
$match &&= $self->journalid == $other->journalid;
return $match;
}
sub available_for_user {
my ( $self, $u ) = @_;
$u ||= $self->owner;
return $self->event_class->available_for_user( $u, $self );
}
package LJ::Error::Subscription::TooMany;
sub fields { qw(subscr u); }
sub as_html { $_[0]->as_string }
sub as_string {
my $self = shift;
my $max = $self->field('u')->count_max_subscriptions;
return
'The notification tracking "'
. $self->field('subscr')->as_html
. '" was not saved because you have'
. " reached your limit of $max active notifications. Notifications need to be deactivated before more can be added.";
}
# Too many subscriptions exist, not necessarily active
package LJ::Error::Subscription::TooManySystemMax;
sub fields { qw(subscr u max); }
sub as_html { $_[0]->as_string }
sub as_string {
my $self = shift;
my $max = $self->field('max');
return
'The notification tracking "'
. $self->field('subscr')->as_html
. '" was not saved because you have'
. " more than $max existing notifications. Notifications need to be completely removed before more can be added.";
}
1;