1 package LJ
::Subscription
;
3 use Carp
qw(croak confess cluck);
8 LJ::Subscription::Pending
9 LJ::Subscription::Group
13 INACTIVE
=> 1 << 0, # user has deactivated
16 my @subs_fields = qw(userid subid is_dirty journalid etypeid arg1 arg2
17 ntypeid createtime expiretime flags);
20 my ($class, $u, $subid) = @_;
21 croak
"new_by_id requires a valid 'u' object"
23 return if $u->is_expunged;
25 croak
"invalid subscription id passed"
26 unless defined $subid && int($subid) > 0;
28 my $row = $u->selectrow_hashref
29 ("SELECT userid, subid, is_dirty, journalid, etypeid, " .
30 "arg1, arg2, ntypeid, createtime, expiretime, flags " .
31 "FROM subs WHERE userid=? AND subid=?", undef, $u->{userid
}, $subid);
32 die $u->errstr if $u->err;
34 return $class->new_from_row($row);
40 return $self->id if $self->id && $self->id != 0;
42 my %props = map { $_ => $self->{$_} } @subs_fields;
47 my ($class, $u, $dump) = @_;
49 return $class->new_by_id($u, $dump) if ref $dump eq '';
50 return bless($dump, $class);
55 return "subid-" . $self->owner->{userid
} . '-' . $self->id;
58 # can return either a LJ::Subscription or LJ::Subscription::Pending object
60 my ($class, $data, $u, $POST) = @_;
63 return undef unless ($data =~ /^(pending|subid) - $u->{userid} .+ ?(-old)?$/x);
65 my ($type, $userid, $subid) = split("-", $data);
67 return LJ
::Subscription
::Pending
->thaw($data, $u, $POST) if $type eq 'pending';
68 die "Invalid subscription data type: $type" unless $type eq 'subid';
71 my $subuser = LJ
::load_userid
($userid);
72 die "no user" unless $subuser;
73 $u = LJ
::get_authas_user
($subuser);
74 die "Invalid user $subuser->{user}" unless $u;
77 return $class->new_by_id($u, $subid);
81 sub default_selected
{ $_[0]->active }
83 sub query_user_subscriptions
{
84 my ($class, $u, %filters) = @_;
85 croak
"subscriptions_of_user requires a valid 'u' object"
88 return if $u->is_expunged;
90 my $dbh = LJ
::get_cluster_reader
($u) or die "cannot get a DB handle";
92 my (@conditions, @binds);
96 foreach my $prop (qw(journalid ntypeid etypeid flags arg1 arg2)) {
97 next unless defined $filters{$prop};
98 push @conditions, "$prop=?";
99 push @binds, $filters{$prop};
102 my $conditions = join(' AND ', @conditions);
103 return $dbh->selectall_arrayref(
106 userid
, subid
, is_dirty
, journalid
, etypeid
,
107 arg1
, arg2
, ntypeid
, createtime
, expiretime
, flags
109 WHERE userid
=? AND
$conditions
110 }, { Slice
=> {} }, $u->id, @binds
114 sub subscriptions_of_user
{
115 my ($class, $u) = @_;
117 croak
"subscriptions_of_user requires a valid 'u' object"
120 return if $u->is_expunged;
122 return @
{$u->{_subscriptions
}} if $u->{_subscriptions
};
126 my $val = LJ
::MemCache
::get
('subscriptions:' . $u->id);
128 my @ints = unpack("N*", $val);
129 for (my $i = 0; $i < scalar(@ints); $i += 11) {
131 @row{@subs_fields} = @ints[$i..$i+10];
132 push @subs, $class->new_from_row(\
%row);
135 @subs = map { $class->new_from_row($_) }
136 @
{ $class->query_user_subscriptions($u) };
139 foreach my $sub (@subs) {
141 push @ints, @row{@subs_fields};
143 LJ
::MemCache
::set
('subscriptions:' . $u->id, pack("N*", @ints));
146 @
{$u->{_subscriptions
}} = @subs;
151 # Look for a subscription matching the parameters: journalu/journalid,
152 # ntypeid/method, event/etypeid, arg1, arg2
153 # Returns a list of subscriptions for this user matching the parameters
155 # For pages with tons of comments search is divided in two stages:
157 # II postprocess => [$a, $b, $c]
158 # First stage is done once for request and is needed to fetch and filter out
159 # active user subscriptions in current journal
160 # Second stage is done for every comment on page and contains all additional filtering conditions
162 my ($class, $u, %params) = @_;
164 my ($etypeid, $ntypeid, $arg1, $arg2, $flags, $prefetch, $postprocess);
166 $prefetch = delete $params{'prefetch'};
167 $postprocess = delete $params{'postprocess'};
169 if (my $evt = delete $params{event
}) {
170 $etypeid = LJ
::Event
->event_to_etypeid($evt);
173 if (my $nmeth = delete $params{method
}) {
174 $ntypeid = LJ
::NotificationMethod
->method_to_ntypeid($nmeth);
177 $etypeid ||= delete $params{etypeid
};
178 $ntypeid ||= delete $params{ntypeid
};
180 $flags = delete $params{flags
};
182 my $journalid = delete $params{journalid
};
183 $journalid ||= LJ
::want_userid
(delete $params{journal
}) if defined $params{journal
};
185 $arg1 = delete $params{arg1
};
186 $arg2 = delete $params{arg2
};
188 my $require_active = delete $params{require_active
} ?
1 : 0;
190 croak
"Invalid parameters passed to ${class}->find" if keys %params;
192 return () if defined $arg1 && $arg1 =~ /\D/;
193 return () if defined $arg2 && $arg2 =~ /\D/;
196 if ( $postprocess ) {
197 @subs = @
$postprocess;
199 @subs = $class->subscriptions_of_user($u);
202 unless ( $postprocess ) {
203 # This filter should be at first place because we need not to check
204 # subscriptions in other journals and this can save us from tons of work later
205 # Matters on pages with comments, when both journalid and require_active
206 # conditions are active
207 @subs = grep { $_->journalid == $journalid } @subs if defined $journalid;
209 @subs = grep { $_->active } @subs if $require_active;
212 return @subs if $prefetch;
214 # filter subs on each parameter
215 @subs = grep { $_->ntypeid == $ntypeid } @subs if $ntypeid;
216 @subs = grep { $_->{etypeid
} == $etypeid } @subs if $etypeid;
217 @subs = grep { $_->flags == $flags } @subs if defined $flags;
219 @subs = grep { $_->{arg1
} == $arg1 } @subs if defined $arg1;
220 @subs = grep { $_->{arg2
} == $arg2 } @subs if defined $arg2;
226 # Deactivates a subscription. If this is not a "tracking" subscription,
227 # it will delete it instead.
232 my $force = delete $opts{force
}; # force-delete
234 croak
"Invalid args" if scalar keys %opts;
236 my $subid = $self->id
237 or croak
"Invalid subsciption";
239 my $u = $self->owner;
241 # if it's the inbox method, deactivate/delete the other notification methods too
244 my @subs = $self->corresponding_subs;
246 foreach my $subscr (@subs) {
247 # Don't deactivate if the Inbox is always subscribed to
248 my $always_checked = $subscr->event_class->always_checked ?
1 : 0;
250 # delete non-inbox methods if we're deactivating
251 if ($subscr->method eq 'LJ::NotificationMethod::Inbox' && !$always_checked) {
252 $subscr->_deactivate;
262 # deletes a subscription
265 my $u = $self->owner;
267 my @subs = $self->corresponding_subs;
268 foreach my $subscr (@subs) {
269 $u->do("DELETE FROM subs WHERE subid=? AND userid=?", undef, $subscr->id, $u->id);
272 # delete from cache in user
273 undef $u->{_subscriptions
};
275 $self->invalidate_cache($u);
280 # class method, nukes all subs for a user
281 sub delete_all_subs
{
282 my ($class, $u) = @_;
284 return if $u->is_expunged;
285 $u->do("DELETE FROM subs WHERE userid = ?", undef, $u->id);
286 undef $u->{_subscriptions
};
288 $class->invalidate_cache($u);
293 # class method, nukes all inactive subs for a user
294 sub delete_all_inactive_subs
{
295 my ($class, $u, $dryrun) = @_;
297 return if $u->is_expunged;
299 my $set = LJ
::Subscription
::GroupSet
->fetch_for_user($u);
300 my @inactive_groups = grep { !$_->active } $set->groups;
303 $set->drop_group($_) foreach (@inactive_groups);
306 return scalar(@inactive_groups);
309 # find matching subscriptions with different notification methods
310 sub corresponding_subs
{
315 if ($self->method eq 'LJ::NotificationMethod::Inbox') {
316 push @subs, $self->owner->find_subscriptions(
317 journalid
=> $self->journalid,
318 etypeid
=> $self->etypeid,
329 my ($class, $row) = @_;
331 return undef unless $row;
332 my $self = bless {%$row}, $class;
333 # TODO validate keys of row.
338 my ($class, $u, %args) = @_;
340 # easier way for eveenttype
341 if (my $evt = delete $args{'event'}) {
342 $args{etypeid
} = LJ
::Event
->event_to_etypeid($evt);
345 # easier way to specify ntypeid
346 if (my $ntype = delete $args{'method'}) {
347 $args{ntypeid
} = LJ
::NotificationMethod
->method_to_ntypeid($ntype);
350 # easier way to specify journal
351 if (my $ju = delete $args{'journal'}) {
352 $args{journalid
} = $ju->{userid
}
353 if !$args{journalid
} && $ju;
359 $args{journalid
} ||= 0;
361 foreach (qw(ntypeid etypeid)) {
362 croak
"Required field '$_' not found in call to $class->create" unless defined $args{$_};
364 foreach (qw(userid subid createtime)) {
365 croak
"Can't specify field '$_'" if defined $args{$_};
368 my ($existing) = grep {
369 $args{etypeid
} == $_->{etypeid
} &&
370 $args{ntypeid
} == $_->{ntypeid
} &&
371 $args{journalid
} == $_->{journalid
} &&
372 $args{arg1
} == $_->{arg1
} &&
373 $args{arg2
} == $_->{arg2
} &&
374 $args{flags
} == $_->{flags
}
375 } $class->subscriptions_of_user($u);
378 if defined $existing;
380 my $subid = LJ
::alloc_user_counter
($u, 'E')
381 or die "Could not alloc subid for user $u->{user}";
383 $args{subid
} = $subid;
384 $args{userid
} = $u->{userid
};
385 $args{createtime
} = time();
387 my $self = $class->new_from_row( \
%args );
390 foreach (@subs_fields) {
391 if (exists( $args{$_} )) {
392 push @fields, { name
=> $_, value
=> delete $args{$_} };
396 croak
( "Extra args defined, (" . join( ', ', keys( %args ) ) . ")" ) if keys %args;
398 # DELETE FROM subs records with all selected field values
399 # without 'subid', 'flags' and 'createtime'.
400 my $sql_filter = sub { $_->{'name'} !~ /subid|flags|createtime/ };
402 my $sth = $u->prepare( 'DELETE FROM subs WHERE ' .
403 join( ' AND ', map { $_->{'name'} . '=?' } grep { $sql_filter->($_) } @fields )
406 $sth->execute( map { $_->{'value'} } grep { $sql_filter->($_) } @fields );
409 'INSERT INTO subs (' . join( ',', map { $_->{'name'} } @fields ) . ')' .
410 'VALUES (' . join( ',', map {'?'} @fields ) . ')' );
412 $sth->execute( map { $_->{'value'} } @fields );
413 LJ
::errobj
($u)->throw if $u->err;
415 push @
{$u->{_subscriptions
}}, $self;
417 $self->invalidate_cache($u);
422 # returns a hash of arguments representing this subscription (useful for passing to
423 # other functions, such as find)
427 journalid
=> $self->journalid,
428 etypeid
=> $self->etypeid,
429 ntypeid
=> $self->ntypeid,
432 flags
=> $self->flags,
436 # returns a nice HTML description of this current subscription
440 my $evtclass = LJ
::Event
->class($self->etypeid);
441 return undef unless $evtclass;
442 return $evtclass->subscription_as_html($self);
447 $self->clear_flag(INACTIVE
);
452 $self->set_flag(INACTIVE
);
456 my ($self, $flag) = @_;
458 my $flags = $self->flags;
460 # don't bother if flag already set
461 return if $flags & $flag;
465 if ($self->owner && ! $self->pending) {
466 $self->owner->do("UPDATE subs SET flags = flags | ? WHERE userid=? AND subid=?", undef,
467 $flag, $self->owner->userid, $self->id);
468 die $self->owner->errstr if $self->owner->errstr;
470 $self->{flags
} = $flags;
471 delete $self->owner->{_subscriptions
};
476 my ($self, $flag) = @_;
478 my $flags = $self->flags;
480 # don't bother if flag already cleared
481 return unless $flags & $flag;
487 if ($self->owner && ! $self->pending) {
488 $self->owner->do("UPDATE subs SET flags = flags & ~? WHERE userid=? AND subid=?", undef,
489 $flag, $self->owner->userid, $self->id);
490 die $self->owner->errstr if $self->owner->errstr;
492 $self->{flags
} = $flags;
493 delete $self->owner->{_subscriptions
};
500 return $self->{subid
};
505 return $self->{createtime
};
510 return $self->{flags
} || 0;
515 return ! ($self->flags & INACTIVE
);
520 return $self->{expiretime
};
525 return $self->{journalid
};
530 return LJ
::load_userid
($self->{journalid
});
535 return $self->{arg1
};
540 return $self->{arg2
};
545 return $self->{ntypeid
};
550 return LJ
::NotificationMethod
->class($self->ntypeid);
555 return LJ
::NotificationMethod
->class($self->{ntypeid
});
560 return $self->{etypeid
};
565 return LJ
::Event
->class($self->{etypeid
});
568 # returns the owner (userid) of the subscription
571 return $self->{userid
};
576 return LJ
::load_userid
($self->{userid
});
581 return $self->{is_dirty
};
586 my $class = LJ
::NotificationMethod
->class($subscr->{ntypeid
});
589 if ($LJ::DEBUG
{'official_post_esn'} && $subscr->etypeid == LJ
::Event
::OfficialPost
->etypeid) {
590 # we had (are having) some problems with subscriptions to millions of people, so
591 # this exists for now for debugging that, without actually emailing/inboxing
592 # those people while we debug
593 $note = LJ
::NotificationMethod
::DebugLog
->new_from_subscription($subscr, $class);
595 $note = $class->new_from_subscription($subscr);
602 my ($self, $opts, @events) = @_;
603 my $note = $self->notification or return;
605 return 1 if $self->etypeid == LJ
::Event
::OfficialPost
->etypeid && $LJ::DISABLED
{"officialpost_esn"};
607 # process events for unauthorised users;
608 return 1 if LJ
::Event
->class($self->etypeid) eq 'LJ::Event::SupportRequest' &&
611 # significant events (such as SecurityAttributeChanged) must be processed even for inactive users.
613 unless $self->notify_class->configured_for_user($self->owner)
614 || LJ
::Event
->class($self->etypeid)->is_significant;
616 return $note->notify($opts, @events);
622 my $note = $self->notification or return undef;
623 return $note->unique . ':' . $self->owner->{user
};
626 # returns true if two subscriptions are equivilant
628 my ($self, $other) = @_;
630 return 1 if $self->id == $other->id;
632 my $match = $self->ntypeid == $other->ntypeid &&
633 $self->etypeid == $other->etypeid && $self->flags == $other->flags;
635 $match &&= $other->arg1 && ($self->arg1 == $other->arg1) if $self->arg1;
636 $match &&= $other->arg2 && ($self->arg2 == $other->arg2) if $self->arg2;
638 $match &&= $self->journalid == $other->journalid;
643 sub available_for_user
{
648 return $self->group->event->available_for_user($u);
653 return LJ
::Subscription
::Group
->group_from_sub($self);
658 return $self->group->event;
664 my $ret = $self->group->enabled;
668 sub invalidate_cache
{
669 my ($class, $u) = @_;
670 LJ
::MemCache
::delete('subscriptions:'.$u->id);
671 LJ
::MemCache
::delete('subscriptions_count:'.$u->id);
674 package LJ
::Error
::Subscription
::TooMany
;
675 sub fields
{ qw(subscr u); }
677 sub as_html
{ $_[0]->as_string }
680 my $max = $self->field('u')->get_cap('subscriptions');
681 return 'The subscription "' . $self->field('subscr')->as_html . '" was not saved because you have' .
682 " reached your limit of $max active subscriptions. Subscriptions need to be deactivated before more can be added.";
685 # Too many subscriptions exist, not necessarily active
686 package LJ
::Error
::Subscription
::TooManySystemMax
;
687 sub fields
{ qw(subscr u max); }
689 sub as_html
{ $_[0]->as_string }
692 my $max = $self->field('max');
693 return 'The subscription "' . $self->field('subscr')->as_html . '" was not saved because you have' .
694 " more than $max existing subscriptions. Subscriptions need to be completely removed before more can be added.";