Андрей (andy) wrote in changelog,
Андрей
andy
changelog

[livejournal] r16116: LJSV-878: overhaul of ESN

Committer: ailyin
LJSV-878: overhaul of ESN

Featured in this patch:

User-facing:
* old-style comment notifications are gone and are sent with ESN
* "someone replies to my comment" and "someone replies to my community entry" are now ESN events, so these can be delivered to Inbox, Jabber, etc.
* Subscriptions are not anymore mandatorily delivered to Inbox, with "I receive a new PM" being a notable exception
* It is possible for a maintainer to subscribe when a new comment is posted in their community; this requires for the community to be Paid.
* Provided a modern JavaScript-enabled browser, subscriptions can be removed with an AJAXy interface without page reload
* Only "tracking" subscriptions are counted towards the quota

Backend:
* LJ::subscribe_interface from weblib.pl is gone and has been replaced by LJ::Widget::SubscribeInterface
* LJ::Subscription::Group and LJ::Subscription::GroupSet are the new APIs for handling notification settings
* LJ::Event and LJ::Event::* classes can now return information about whether they should render at all, or render disabled, and which methods should
they should allow for

TODO:
* LJ::Widget::SubscribeInterface should be further adjusted by the Frontend team (hey there, nely_snork)
* Documentation
* Ensure that the logic here matches one in the spec
* Testing, testing, testing.

U   trunk/bin/upgrading/en.dat
U   trunk/cgi-bin/LJ/ESN.pm
U   trunk/cgi-bin/LJ/Event/Befriended.pm
U   trunk/cgi-bin/LJ/Event/Birthday.pm
A   trunk/cgi-bin/LJ/Event/CommentReply.pm
A   trunk/cgi-bin/LJ/Event/CommunityEntryReply.pm
U   trunk/cgi-bin/LJ/Event/CommunityInvite.pm
U   trunk/cgi-bin/LJ/Event/CommunityJoinApprove.pm
U   trunk/cgi-bin/LJ/Event/CommunityJoinReject.pm
U   trunk/cgi-bin/LJ/Event/CommunityJoinRequest.pm
U   trunk/cgi-bin/LJ/Event/Defriended.pm
U   trunk/cgi-bin/LJ/Event/InvitedFriendJoins.pm
U   trunk/cgi-bin/LJ/Event/JournalNewComment.pm
U   trunk/cgi-bin/LJ/Event/JournalNewEntry.pm
U   trunk/cgi-bin/LJ/Event/NewUserpic.pm
U   trunk/cgi-bin/LJ/Event/OfficialPost.pm
U   trunk/cgi-bin/LJ/Event/PollVote.pm
U   trunk/cgi-bin/LJ/Event/SecurityAttributeChanged.pm
U   trunk/cgi-bin/LJ/Event/UserMessageRecvd.pm
U   trunk/cgi-bin/LJ/Event/UserMessageSent.pm
U   trunk/cgi-bin/LJ/Event.pm
A   trunk/cgi-bin/LJ/Subscription/Group.pm
A   trunk/cgi-bin/LJ/Subscription/GroupSet.pm
A   trunk/cgi-bin/LJ/Subscription/QuotaError.pm
U   trunk/cgi-bin/LJ/Subscription.pm
A   trunk/cgi-bin/LJ/Widget/SubscribeInterface.pm
U   trunk/cgi-bin/LJ/Widget.pm
U   trunk/cgi-bin/redirect.dat
U   trunk/cgi-bin/talklib.pl
U   trunk/htdocs/js/esn.js
U   trunk/htdocs/manage/settings/index.bml
U   trunk/htdocs/manage/subscriptions/comments.bml
U   trunk/htdocs/manage/subscriptions/entry.bml
U   trunk/htdocs/manage/subscriptions/index.bml
U   trunk/htdocs/manage/subscriptions/user.bml
U   trunk/htdocs/stc/esn.css
Modified: trunk/bin/upgrading/en.dat
===================================================================
--- trunk/bin/upgrading/en.dat	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/bin/upgrading/en.dat	2010-01-21 12:38:13 UTC (rev 16116)
@@ -1554,6 +1554,8 @@
 [[event]]
 .
 
+esn.error.quota=You have exceeded your quota of [[quota]] subscriptions. Please <a[[aopts]]>delete some of your existing subscriptions</a> in order to add new ones.
+
 entryform.adultcontent=Adult Content:
 
 entryform.adultcontent.concepts=Adult Concepts
@@ -1914,6 +1916,10 @@
 
 event.journal_new_comment.user_journal.untitled_entry.untitled_thread.anonymous=Someone comments under <a href='[[threadurl]]'>the thread</a> by (Anonymous) in <a href='[[entryurl]]'>an entry</a> in [[user]]
 
+event.comment_reply=Someone replies to my comment
+
+event.community_entry_reply=Someone replies to my entry in a community
+
 event.journal_new_entry.tag.community=Someone posts an entry tagged [[tags]] to [[user]]
 
 event.journal_new_entry.tag.user=[[user]] posts a new entry tagged [[tags]]
@@ -2931,6 +2937,8 @@
 
 subscribe_interface.category.subscription-tracking=Subscription Tracking
 
+subscribe_interface.category.this-journal=Events in [[journal]]
+
 subscribe_interface.inbox=Inbox
 
 subscribe_interface.manage=Manage settings
@@ -2949,6 +2957,8 @@
 
 subscribe_interface.special_subs.note=These notification options are only available by email and will not show in your LJ Inbox.
 
+subscribe_interface.getselfemail=Email me copies of my comments
+
 taglib.error.toomany=This would make you exceed your maximum of [[max]] tags.  Please remove some and try again.
 
 talk.anonwrote=Someone wrote,

Modified: trunk/cgi-bin/LJ/ESN.pm
===================================================================
--- trunk/cgi-bin/LJ/ESN.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/ESN.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -52,7 +52,7 @@
                                              funcname => 'LJ::Worker::ProcessSub',
                                              arg      => [
                                                           $s->userid + 0,
-                                                          $s->id     + 0,
+                                                          $s->dump,
                                                           $params           # arrayref of event params
                                                           ],
                                              );
@@ -209,12 +209,12 @@
             last BUILD_SET if $finish_set;
         }
 
-        # $sublist is [ [userid, subid]+ ]. also, pass clusterid through
+        # $sublist is [ [userid, subdump]+ ]. also, pass clusterid through
         # to filtersubs so we can check that we got a subscription for that
         # user from the right cluster. (to avoid user moves with old data
         # on old clusters from causing duplicates). easier to do it there
         # than here, to avoid a load_userids call.
-        my $sublist = [ map { [ $_->userid + 0, $_->id + 0 ] } @set ];
+        my $sublist = [ map { [ $_->userid + 0, $_->dump + 0 ] } @set ];
         push @subjobs, TheSchwartz::Job->new(
                                              funcname => 'LJ::Worker::FilterSubs',
                                              arg      => [ $e_params, $sublist, $cid ],
@@ -238,7 +238,7 @@
 
     my @subs;
     foreach my $sp (@$sublist) {
-        my ($userid, $subid) = @$sp;
+        my ($userid, $subdump) = @$sp;
         my $u = LJ::load_userid($userid)
             or die "Failed to load userid: $userid\n";
 
@@ -248,7 +248,7 @@
 
         # TODO: discern difference between cluster not up and subscription
         #       having been deleted
-        my $subsc = LJ::Subscription->new_by_id($u, $subid)
+        my $subsc = LJ::Subscription->new_from_dump($u, $subdump)
             or next;
 
         push @subs, $subsc;
@@ -266,10 +266,10 @@
 sub work {
     my ($class, $job) = @_;
     my $a = $job->arg;
-    my ($userid, $subid, $eparams) = @$a;
+    my ($userid, $subdump, $eparams) = @$a;
     my $u     = LJ::load_userid($userid);
     my $evt   = LJ::Event->new_from_raw_params(@$eparams);
-    my $subsc = $evt->get_subscriptions($u, $subid);
+    my $subsc = $evt->get_subscriptions($u, $subdump);
 
     # if the subscription doesn't exist anymore, we're done here
     # (race: if they delete the subscription between when we start processing
@@ -286,7 +286,7 @@
         # in the subscription object so the email notifier can access them
         my $debug_headers = {
             'X-ESN_Debug-sch_jobid' => $job->jobid,
-            'X-ESN_Debug-subid'     => $subid,
+            'X-ESN_Debug-subid'     => $subdump,
             'X-ESN_Debug-eparams'   => join(', ', @$eparams),
         };
 
@@ -297,7 +297,7 @@
 
     # NEXT: do sub's ntypeid, unless it's inbox, then we're done.
     $subsc->process($evt)
-        or die "Failed to process notification method for userid=$userid/subid=$subid, evt=[@$eparams]\n";
+        or die "Failed to process notification method for userid=$userid/subid=$subdump, evt=[@$eparams]\n";
     $job->completed;
 }
 

Modified: trunk/cgi-bin/LJ/Event/Befriended.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/Befriended.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/Befriended.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -159,4 +159,6 @@
     return $self->as_html_actions;
 }
 
+sub is_tracking { 0 }
+
 1;

Modified: trunk/cgi-bin/LJ/Event/Birthday.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/Birthday.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/Birthday.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -166,4 +166,10 @@
     return $self->as_html_actions;
 }
 
+sub is_tracking {
+    my ($self) = @_;
+
+    return $self->{'userid'} ? 1 : 0;
+}
+
 1;

Added: trunk/cgi-bin/LJ/Event/CommentReply.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/CommentReply.pm	                        (rev 0)
+++ trunk/cgi-bin/LJ/Event/CommentReply.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -0,0 +1,15 @@
+package LJ::Event::CommentReply;
+use strict;
+use base 'LJ::Event::JournalNewComment';
+
+sub subscription_as_html {
+    my ($class, $subscr) = @_;
+    
+    return BML::ml('event.comment_reply');
+}
+
+sub available_for_user  { 1 }
+
+sub is_tracking { 0 }
+
+1;

Added: trunk/cgi-bin/LJ/Event/CommunityEntryReply.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/CommunityEntryReply.pm	                        (rev 0)
+++ trunk/cgi-bin/LJ/Event/CommunityEntryReply.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -0,0 +1,15 @@
+package LJ::Event::CommunityEntryReply;
+use strict;
+use base 'LJ::Event::JournalNewComment';
+
+sub subscription_as_html {
+    my ($class, $subscr) = @_;
+    
+    return BML::ml('event.community_entry_reply');
+}
+
+sub available_for_user  { 1 }
+
+sub is_tracking { 0 }
+
+1;

Modified: trunk/cgi-bin/LJ/Event/CommunityInvite.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/CommunityInvite.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/CommunityInvite.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -160,6 +160,8 @@
     return BML::ml('event.comm_invite'); # "I receive an invitation to join a community";
 }
 
+sub is_tracking { 0 }
+
 package LJ::Error::Event::CommunityInvite;
 sub fields { 'u' }
 sub as_string {

Modified: trunk/cgi-bin/LJ/Event/CommunityJoinApprove.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/CommunityJoinApprove.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/CommunityJoinApprove.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -98,4 +98,6 @@
     return LJ::load_userid($self->arg1);
 }
 
+sub is_tracking { 0 }
+
 1;

Modified: trunk/cgi-bin/LJ/Event/CommunityJoinReject.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/CommunityJoinReject.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/CommunityJoinReject.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -89,4 +89,6 @@
     return LJ::load_userid($self->arg1);
 }
 
+sub is_tracking { 0 }
+
 1;

Modified: trunk/cgi-bin/LJ/Event/CommunityJoinRequest.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/CommunityJoinRequest.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/CommunityJoinRequest.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -158,6 +158,7 @@
     return BML::ml('event.community_join_requst'); # Someone requests membership in a community I maintain';
 }
 
+sub is_tracking { 0 }
 package LJ::Error::Event::CommunityJoinRequest;
 sub fields { 'u' }
 sub as_string {

Modified: trunk/cgi-bin/LJ/Event/Defriended.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/Defriended.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/Defriended.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -133,7 +133,7 @@
 
 # only users with the track_defriended cap can use this
 sub available_for_user  {
-    my ($class, $u, $subscr) = @_;
+    my ($self, $u) = @_;
     return $u->get_cap("track_defriended") ? 1 : 0;
 }
 
@@ -143,4 +143,6 @@
     return $self->as_html_actions;
 }
 
+sub is_tracking { 0 }
+
 1;

Modified: trunk/cgi-bin/LJ/Event/InvitedFriendJoins.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/InvitedFriendJoins.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/InvitedFriendJoins.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -135,4 +135,6 @@
     return $self->as_html_actions;
 }
 
+sub is_tracking { 0 }
+
 1;

Modified: trunk/cgi-bin/LJ/Event/JournalNewComment.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/JournalNewComment.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/JournalNewComment.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -490,11 +490,18 @@
     my ($sarg1, $sarg2) = ($subscr->arg1, $subscr->arg2);
 
     my $comment = $self->comment;
+    my $parent_comment = $comment->parent;
+    my $parent_comment_author = $parent_comment ?
+        $parent_comment->poster : undef;
+
     my $entry   = $comment->entry;
 
     my $watcher = $subscr->owner;
-    return 0 unless $comment->visible_to($watcher);
 
+    return 0 unless 
+        $comment->visible_to($watcher) ||
+        LJ::u_equals($parent_comment_author, $watcher);
+
     # not a match if this user posted the comment and they don't
     # want to be notified of their own posts
     if (LJ::u_equals($comment->poster, $watcher)) {
@@ -552,15 +559,36 @@
 }
 
 sub available_for_user  {
-    my ($class, $u, $subscr) = @_;
+    my ($self, $u) = @_;
 
     # not allowed to track replies to comments
     return 0 if ! $u->get_cap('track_thread') &&
-        $subscr->arg2;
+        $self->arg2;
 
+    return 1 if $self->arg1;
+
+    my $journal = $self->event_journal;
+
+    return 1 if LJ::u_equals($u, $journal); # one can always subscribe to self
+
+    return 0 unless LJ::can_manage($u, $journal); # not a maintainer
+    return 0 unless $journal->get_cap('maintainer_track_commments');
+
     return 1;
 }
 
+sub get_disabled_pic {
+    my ($self, $u) = @_;
+
+    my $journal = $self->event_journal;
+
+    return LJ::run_hook('esn_community_comments_track_upgrade', $u, $journal) || ''
+        unless ref $self ne 'LJ::Event::JournalNewComment' ||
+            $self->arg1 || $self->arg2 || LJ::u_equals($u, $journal);
+
+    return $self->SUPER::get_disabled_pic($u);
+}
+
 # return detailed data for XMLRPC::getinbox
 sub raw_info {
     my ($self, $target, $flags) = @_;
@@ -600,4 +628,110 @@
     }
 }
 
+sub subscriptions {
+    my ($self, %args) = @_;
+    my $cid   = delete $args{'cluster'};  # optional
+    my $limit = int delete $args{'limit'};    # optional
+    my $original_limit = int $limit;
+
+    my $acquire_sub_slot = sub {
+        my ($how_much) = @_;
+        $how_much ||= 1;
+
+        return $how_much unless $original_limit;
+
+        $how_much = $limit if $limit < $how_much;
+
+        $limit -= $how_much;
+        return $how_much;
+    };
+
+    croak("Unknown options: " . join(', ', keys %args)) if %args;
+    croak("Can't call in web context") if LJ::is_web_context();
+
+    my $comment = $self->comment;
+    my $parent_comment = $comment->parent;
+    my $entry = $comment->entry;
+
+    my $comment_author = $comment->poster;
+    my $parent_comment_author = $parent_comment ?
+        $parent_comment->poster :
+        undef;
+    my $entry_author = $entry->poster;
+    my $entry_journal = $entry->journal;
+
+    my @subs;
+
+    my $email_ntypeid =  LJ::NotificationMethod::Email->ntypeid;
+
+    # own comments are deliberately sent to email only
+    if ($comment_author->prop('opt_getselfemail') && $acquire_sub_slot->()) {
+        push @subs, LJ::Subscription->new_from_row({
+            'userid'  => $comment_author->id,
+            'ntypeid' => $email_ntypeid,
+        });
+    }
+
+    if (
+        $parent_comment &&
+        !LJ::u_equals($comment_author, $parent_comment_author)
+    ) {
+        my @subs2 = LJ::Subscription->find($parent_comment_author,
+            'event' => 'CommentReply',
+            'require_active' => 1,
+        );
+
+        warn "sending an email to parent comment author: ". $parent_comment_author->display_name
+            if $parent_comment_author->{'opt_gettalkemail'};
+
+        push @subs2, LJ::Subscription->new_from_row({
+            'userid'  => $parent_comment_author->id,
+            'ntypeid' => $email_ntypeid,
+        }) if $parent_comment_author->{'opt_gettalkemail'};
+
+        if (my $count = $acquire_sub_slot->(scalar(@subs2))) {
+            $#subs2 = $count - 1;
+            push @subs, @subs2;
+        }
+    }
+
+    if (
+        !LJ::u_equals($comment_author, $entry_author)
+    ) {
+        my @subs2 = LJ::Subscription->find($entry_author,
+            'event' => 'CommunityEntryReply',
+            'require_active' => 1,
+        );
+
+        warn "sending an email to entry author: ". $entry_author->display_name
+            if $entry_author->{'opt_gettalkemail'};
+
+        push @subs2, LJ::Subscription->new_from_row({
+            'userid'  => $entry_author->id,
+            'ntypeid' => $email_ntypeid,
+        }) if $entry_author->{'opt_gettalkemail'};
+
+        if (my $count = $acquire_sub_slot->(scalar(@subs2))) {
+            $#subs2 = $count - 1;
+            push @subs, @subs2;
+        }
+    }
+
+    push @subs, eval { $self->SUPER::subscriptions(
+        cluster => $cid,
+        limit   => $limit
+    ) };
+
+    return @subs;
+}
+
+sub is_tracking {
+    my ($self, $ownerid) = @_;
+
+    return 1 if $self->arg1 || $self->arg2;
+    return 1 unless $self->{'userid'} == $ownerid;
+
+    return 0;
+}
+
 1;

Modified: trunk/cgi-bin/LJ/Event/JournalNewEntry.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/JournalNewEntry.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/JournalNewEntry.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -324,7 +324,7 @@
 }
 
 sub subscription_as_html {
-    my ($class, $subscr) = @_;
+    my ($class, $subscr, $field_num) = @_;
 
     my $journal = $subscr->journal;
 
@@ -334,16 +334,29 @@
 
     if ($arg1 eq '?') {
         my @unsub_tags = $class->unsubscribed_tags($subscr);
-        my @tagdropdown;
+        my %tagdropdown;
 
         foreach my $unsub_tag (@unsub_tags) {
             while (my ($tagid, $name) = each %$unsub_tag) {
-                push @tagdropdown, ($tagid, $name);
+                my $group = bless({
+                    'userid' => $subscr->userid,
+                    'journalid' => $subscr->journalid,
+                    'etypeid' => $subscr->etypeid,
+                    'arg1' => $tagid,
+                    'arg2' => 0,
+                }, 'LJ::Subscription::Group');
+
+                $tagdropdown{$group->freeze} = $name;
             }
         }
 
+        my @tagdropdown =
+            map { $_ => $tagdropdown{$_} }
+            sort { $tagdropdown{$a} cmp $tagdropdown{$b} }
+            keys %tagdropdown;
+
         $usertags = LJ::html_select({
-            name => $subscr->freeze('arg1'),
+            name => 'event-'.$field_num,
         }, @tagdropdown);
 
     } elsif ($arg1) {
@@ -366,6 +379,36 @@
             });
 }
 
+sub event_as_html {
+    my ($class, $group, $field_num) = @_;
+
+    my $ret = $class->subscription_as_html($group, $field_num);
+    $ret .= LJ::html_hidden('event-'.$field_num => $group->freeze)
+        unless $group->arg1 eq '?';
+
+    return $ret;
+}
+
+sub is_subscription_visible_to {
+    my ($self, $u) = @_;
+
+    if ($self->arg1 eq '?') {
+        my $journal = $self->u;
+        my $usertags = LJ::Tags::get_usertags($journal, { 'remote' => $u });
+        my @tagids = grep {
+            !$u->has_subscription(
+                etypeid => $self->etypeid,
+                arg1    => $_,
+                journal => $journal,
+            );
+        } (keys %$usertags);
+
+        return 0 unless scalar(@tagids);
+    }
+
+    return $self->SUPER::is_subscription_visible_to($u);
+}
+
 # when was this entry made?
 sub eventtime_unix {
     my $self = shift;

Modified: trunk/cgi-bin/LJ/Event/NewUserpic.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/NewUserpic.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/NewUserpic.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -142,12 +142,18 @@
 
 # only users with the track_user_newuserpic cap can use this
 sub available_for_user  {
-    my ($class, $u, $subscr) = @_;
+    my ($self, $u) = @_;
 
     return 0 if ! $u->get_cap('track_user_newuserpic') &&
-        $subscr->journalid;
+        $self->{'userid'};
 
     return 1;
 }
 
+sub is_tracking {
+    my ($self) = @_;
+
+    return $self->{'userid'} ? 1 : 0;
+}
+
 1;

Modified: trunk/cgi-bin/LJ/Event/OfficialPost.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/OfficialPost.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/OfficialPost.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -160,4 +160,6 @@
 
 sub schwartz_role { 'mass' }
 
+sub is_tracking { 0 }
+
 1;

Modified: trunk/cgi-bin/LJ/Event/PollVote.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/PollVote.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/PollVote.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -168,8 +168,14 @@
 
 # only users with the track_pollvotes cap can use this
 sub available_for_user  {
-    my ($class, $u, $subscr) = @_;
+    my ($self, $u) = @_;
     return $u->get_cap("track_pollvotes") ? 1 : 0;
 }
 
+sub is_tracking {
+    my ($self) = @_;
+
+    return $self->{'arg1'} ? 1 : 0;
+}
+
 1;

Modified: trunk/cgi-bin/LJ/Event/SecurityAttributeChanged.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/SecurityAttributeChanged.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/SecurityAttributeChanged.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -111,7 +111,7 @@
 
 sub is_significant { 1 }
 
-# override parent class sbuscriptions method to always return
+# override parent class subscriptions method to always return
 # a subscription object for the user
 sub subscriptions {
     my ($self, %args) = @_;
@@ -122,13 +122,15 @@
 
     my @subs;
     my $u = $self->u;
-    return unless ( $cid == $u->clusterid );
 
-    my $row = { userid  => $self->u->{userid},
-                ntypeid => LJ::NotificationMethod::Email->ntypeid, # Email
-              };
+    if ($cid == $u->clusterid) {
+        my $row = { userid  => $self->u->{userid},
+                    ntypeid => LJ::NotificationMethod::Email->ntypeid, # Email
+                  };
 
-    push @subs, LJ::Subscription->new_from_row($row);
+        push @subs, LJ::Subscription->new_from_row($row);
+        $limit--;
+    }
 
     push @subs, eval { $self->SUPER::subscriptions(cluster => $cid,
                                                    limit   => $limit) };
@@ -136,20 +138,6 @@
     return @subs;
 }
 
-sub get_subscriptions {
-    my ($self, $u, $subid) = @_;
-
-    unless ($subid) {
-        my $row = { userid  => $u->{userid},
-                    ntypeid => LJ::NotificationMethod::Email->ntypeid, # Email
-                  };
-
-        return LJ::Subscription->new_from_row($row);
-    }
-
-    return $self->SUPER::get_subscriptions($u, $subid);
-}
-
 sub _arg1_to_mlkey {
     my $action = shift;
     my @ml_actions = (

Modified: trunk/cgi-bin/LJ/Event/UserMessageRecvd.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/UserMessageRecvd.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/UserMessageRecvd.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -198,20 +198,6 @@
     return @subs;
 }
 
-sub get_subscriptions {
-    my ($self, $u, $subid) = @_;
-
-    unless ($subid) {
-        my $row = { userid  => $u->{userid},
-                    ntypeid => LJ::NotificationMethod::Inbox->ntypeid, # Inbox
-                  };
-
-        return LJ::Subscription->new_from_row($row);
-    }
-
-    return $self->SUPER::get_subscriptions($u, $subid);
-}
-
 sub display_pic {
     my ($msg, $u) = @_;
 
@@ -258,4 +244,24 @@
     return $res;
 }
 
+sub is_tracking {
+    my ($self, $ownerid) = @_;
+
+    return $self->{'userid'} == $ownerid ? 0 : 1;
+}
+
+sub is_subscription_ntype_disabled_for {
+    my ($self, $ntypeid, $u) = @_;
+
+    return 1 if $ntypeid == LJ::NotificationMethod::Inbox->ntypeid;
+    return $self->SUPER::is_subscription_ntype_disabled_for($ntypeid, $u);
+}
+
+sub get_subscription_ntype_force {
+    my ($self, $ntypeid, $u) = @_;
+
+    return 1 if $ntypeid == LJ::NotificationMethod::Inbox->ntypeid;
+    return $self->SUPER::get_subscription_ntype_force($ntypeid, $u);
+}
+
 1;

Modified: trunk/cgi-bin/LJ/Event/UserMessageSent.pm
===================================================================
--- trunk/cgi-bin/LJ/Event/UserMessageSent.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event/UserMessageSent.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -87,19 +87,6 @@
     return @subs;
 }
 
-sub get_subscriptions {
-    my ($self, $u, $subid) = @_;
-
-    unless ($subid) {
-        my $row = { userid  => $u->{userid},
-                    ntypeid => LJ::NotificationMethod::Inbox->ntypeid, # Inbox
-                  };
-
-        return LJ::Subscription->new_from_row($row);
-    }
-
-}
-
 # Have notifications for this event show up as read
 sub mark_read {
     my $self = shift;

Modified: trunk/cgi-bin/LJ/Event.pm
===================================================================
--- trunk/cgi-bin/LJ/Event.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Event.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -48,11 +48,9 @@
 sub new {
     my ($class, $u, @args) = @_;
     croak("too many args")        if @args > 2;
-    croak("args must be numeric") if grep { /\D/ } @args;
-    croak("u isn't a user")       unless LJ::isu($u);
 
     return bless {
-        userid => $u->id,
+        userid => $u ? $u->id : 0,
         args => \@args,
     }, $class;
 }
@@ -62,7 +60,7 @@
     my (undef, $etypeid, $journalid, $arg1, $arg2) = @_;
 
     my $class   = LJ::Event->class($etypeid) or die "Classname cannot be undefined/false";
-    my $journal = LJ::load_userid($journalid) or die "Invalid journalid $journalid";
+    my $journal = LJ::load_userid($journalid);
     my $evt     = LJ::Event->new($journal, $arg1, $arg2);
 
     # bless into correct class
@@ -206,7 +204,7 @@
 
 # can $u subscribe to this event?
 sub available_for_user  {
-    my ($class, $u, $subscr) = @_;
+    my ($self, $u) = @_;
 
     return 1;
 }
@@ -385,9 +383,9 @@
 }
 
 sub get_subscriptions {
-    my ($self, $u, $subid) = @_;
+    my ($self, $u, $subdump) = @_;
 
-    return LJ::Subscription->new_by_id($u, $subid);
+    return LJ::Subscription->new_from_dump($u, $subdump);
 }
 
 
@@ -530,4 +528,62 @@
     return $options;
 }
 
+sub is_tracking { 1 }
+
+sub is_subscription_visible_to {
+    my ($self, $u) = @_;
+
+    return 0 if ref $self eq 'LJ::Event';
+
+    return 0 if $self->is_subscription_disabled_for($u) && $u->is_identity;
+    return 1;
+}
+
+sub is_subscription_disabled_for {
+    my ($self, $u) = @_;
+
+    return !$self->available_for_user($u);
+}
+
+sub is_subscription_ntype_visible_to { 1 }
+
+sub is_subscription_ntype_disabled_for {
+    my ($self, $ntypeid, $u) = @_;
+
+    return 1 if $self->is_subscription_disabled_for($u);
+
+    my $nclass = LJ::NotificationMethod->class($ntypeid);
+    return 1 unless $nclass->configured_for_user($u);
+
+    return 0;
+}
+
+sub get_subscription_ntype_force { 0 }
+
+sub get_disabled_pic {
+    my ($self, $u) = @_;
+
+    return LJ::run_hook("disabled_esn_sub", $u) || '';
+}
+
+sub get_interface_status {
+    my ($self, $u) = @_;
+
+    return {
+        'visible' => $self->is_subscription_visible_to($u),
+        'disabled' => $self->is_subscription_disabled_for($u),
+        'disabled_pic' => $self->get_disabled_pic($u),
+    };
+}
+
+sub get_ntype_interface_status {
+    my ($self, $ntypeid, $u) = @_;
+
+    return {
+        'visible' => $self->is_subscription_ntype_visible_to($ntypeid, $u),
+        'disabled' => $self->is_subscription_ntype_disabled_for($ntypeid, $u),
+        'force' => $self->get_subscription_ntype_force($ntypeid, $u),
+    };
+}
+
 1;

Added: trunk/cgi-bin/LJ/Subscription/Group.pm
===================================================================
--- trunk/cgi-bin/LJ/Subscription/Group.pm	                        (rev 0)
+++ trunk/cgi-bin/LJ/Subscription/Group.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -0,0 +1,324 @@
+package LJ::Subscription::Group;
+
+use strict;
+
+use Carp qw(cluck confess);
+
+use constant GROUP_COLS => qw(userid journalid etypeid arg1 arg2);
+use constant OTHER_COLS => qw(is_dirty ntypeid subid createtime expiretime flags);
+
+my @group_cols = (GROUP_COLS);
+my @other_cols = (OTHER_COLS);
+
+sub freeze {
+    my ($self) = @_;
+    
+    my %self = %$self;
+    return join('-', map { $_ || 0 } @self{@group_cols});
+}
+
+sub thaw {
+    my ($class, $frozen) = @_;
+
+    my %self;
+    @self{@group_cols} = split /-/, $frozen;
+
+    return bless({
+        %self,
+        'subs' => {},
+    }, $class);
+}
+
+sub group_from_sub {
+    my ($class, $sub) = @_;
+
+    my %sub = map { $_ => int $sub->{$_} } @group_cols;
+
+    return bless({
+        %sub,
+        'subs' => {},
+    }, $class);
+}
+
+sub insert_sub {
+    my ($self, $sub) = @_;
+
+    my $key = $sub->ntypeid;
+
+    $self->{'subs'}->{$key} = $sub;
+}
+
+sub create_sub {
+    my ($self, $ntypeid) = @_;
+
+    my %sub = map { $_ => $self->{$_} } @group_cols;
+    my $sub = bless({
+        %sub,
+        'ntypeid' => $ntypeid,
+        'is_dirty' => 0,
+        'createtime' => time,
+        'expiretime' => 0,
+        'flags' => 0,
+    }, 'LJ::Subscription');
+
+    return $self->insert_sub($sub);
+}
+
+sub ensure_inbox_created {
+    my ($self) = @_;
+
+    my $inbox_ntypeid = LJ::NotificationMethod::Inbox->ntypeid;
+    return if $self->{'subs'}->{$inbox_ntypeid};
+
+    my %sub = map { $_ => $self->{$_} } @group_cols;
+    my $sub = bless({
+        %sub,
+        'ntypeid' => $inbox_ntypeid,
+        'is_dirty' => 0,
+        'createtime' => time,
+        'expiretime' => 0,
+        'flags' => LJ::Subscription::INACTIVE(),
+    }, 'LJ::Subscription');
+
+    $self->{'subs'}->{$inbox_ntypeid} = $sub;
+}
+
+sub find_sub {
+    my ($self, $sub) = @_;
+
+    my $key = $sub->ntypeid;
+
+    return undef unless $self->{'subs'}->{$key};
+    return bless($self->{'subs'}->{$key}, 'LJ::Subscription');
+}
+
+sub find_or_insert_sub {
+    my ($self, $sub) = @_;
+
+    my $key = $sub->ntypeid;
+
+    $self->{'subs'}->{$key} = $sub unless $self->{'subs'}->{$key};
+    return bless($self->{'subs'}->{$key}, 'LJ::Subscription');
+}
+
+sub find_or_insert_ntype {
+    my ($self, $ntypeid) = @_;
+
+    my %subprops = map { $_ => $self->{$_} } @group_cols;
+
+    my $sub = bless({
+        %subprops,
+        'ntypeid' => $ntypeid,
+        'createtime' => time,
+        'expiretime' => 0,
+        'flags' => LJ::Subscription::INACTIVE(),
+    }, 'LJ::Subscription');
+
+    return $self->find_or_insert_sub($sub);
+}
+
+sub custom_user_groups {
+    my ($class, $u) = @_;
+
+    
+}
+
+sub user_groups {
+    my ($class, $u) = @_;
+
+    my $group_cols = join(',', @group_cols);
+    my $other_cols = join(',', @other_cols);
+
+    $u = LJ::want_user($u);
+
+    confess "cannot get a user" unless $u;
+
+    my $dbr = $u->get_cluster_reader();
+
+    confess "cannot get a database handle" unless $dbr;
+
+    my $res = $dbr->selectall_hashref(qq{
+        SELECT
+            $group_cols, $other_cols
+        FROM subs
+        WHERE userid=?
+        ORDER BY
+            $group_cols
+    }, Slice => {}, $u->id);
+
+    my @ret;
+
+    my $lastrow = undef;
+    my $lastobj = undef;
+
+    foreach my $row (@$res) {
+        my $neednew = !defined $lastrow;
+
+        unless ($neednew) {
+            foreach my $col (@group_cols) {
+                next if $lastrow->{$col} eq $row->{$col};
+
+                $neednew = 1;
+                last;
+            }
+        }
+
+        if ($neednew) {
+            my %newprops = map { $_ => $row->{$_} } @group_cols;
+
+            $lastobj = $class->new(\%newprops);
+            push @ret, $lastobj;
+        }
+
+        my %otherprops = map { $_ => $row->{$_} } @other_cols;
+        $lastobj->push_ntype(\%otherprops);
+
+        $lastrow = $row;
+    }
+
+    return \@ret;
+}
+
+sub new {
+    my ($class, $props) = @_;
+
+    confess "need a hashref here" unless ref $props eq 'HASH';
+
+    my %newprops = map { delete $props->{$_} } @group_cols;
+    $newprops{'ntypes'} = [];
+
+    confess "unknown properties: " . join(', ', keys %$props)
+        if scalar(%$props);
+
+    return bless(\%newprops, $class);
+}
+
+sub push_ntype {
+    my ($self, $props) = @_;
+
+    confess "need a hashref here" unless ref $props eq 'HASH';
+
+    my %newprops = map { delete $props->{$_} } @other_cols;
+
+    confess "unknown properties: " . join(', ', keys %$props)
+        if scalar(%$props);
+
+    push @{$self->{'ntypes'}}, \%newprops;
+}
+
+# getters
+
+sub arg1 {
+    my ($self) = @_;
+    return $self->{'arg1'};
+}
+
+sub arg2 {
+    my ($self) = @_;
+    return $self->{'arg2'};
+}
+
+sub journalid {
+    my ($self) = @_;
+    return $self->{'journalid'};
+}
+
+sub userid {
+    my ($self) = @_;
+    return $self->{'userid'};
+}
+
+sub etypeid {
+    my ($self) = @_;
+    return $self->{'etypeid'};
+}
+
+sub journal {
+    my ($self) = @_;
+    return LJ::want_user($self->journalid);
+}
+
+sub owner {
+    my ($self) = @_;
+    return LJ::want_user($self->userid);
+}
+
+sub event_class {
+    my ($self) = @_;
+
+    my $evtclass = LJ::Event->class($self->etypeid);
+    return $evtclass || undef;
+}
+
+sub as_html {
+    my ($self) = @_;
+
+    my $evtclass = $self->event_class || return undef;
+
+    return $evtclass->subscription_as_html($self);
+}
+
+sub event_as_html {
+    my ($self, $field_num) = @_;
+
+    my $ret = '';
+
+    my $evtclass = $self->event_class || return undef;
+    if ($evtclass->can('event_as_html')) {
+        return $evtclass->event_as_html($self, $field_num);
+    }
+
+    $ret .= LJ::html_hidden('event-'.$field_num => $self->freeze);
+    $ret .= $self->as_html;
+}
+
+sub event {
+    my ($self) = @_;
+
+    return LJ::Event->new_from_raw_params(
+        $self->etypeid,
+        $self->journalid,
+        $self->arg1,
+        $self->arg2,
+    );
+
+}
+
+sub is_tracking {
+    my ($self) = @_;
+
+    my $evt = $self->event;
+
+    return 0 unless $evt;
+    return $evt->is_tracking($self->userid);
+}
+
+sub get_interface_status {
+    my ($self, $u) = @_;
+
+    my $evt = $self->event;
+
+    return $evt->get_interface_status($u);
+}
+
+sub get_ntype_interface_status {
+    my ($self, $ntypeid, $u) = @_;
+
+    my $evt = $self->event;
+
+    return $evt->get_ntype_interface_status($ntypeid, $u);
+}
+
+sub group {
+    my ($self) = @_;
+    return $self;
+}
+
+sub createtime {
+    my ($self) = @_;
+
+    my $inbox_ntypeid = LJ::NotificationMethod::Inbox->ntypeid;
+    return $self->{'subs'}->{$inbox_ntypeid}->{'createtime'};
+}
+
+1;

Added: trunk/cgi-bin/LJ/Subscription/GroupSet.pm
===================================================================
--- trunk/cgi-bin/LJ/Subscription/GroupSet.pm	                        (rev 0)
+++ trunk/cgi-bin/LJ/Subscription/GroupSet.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -0,0 +1,449 @@
+package LJ::Subscription::GroupSet;
+
+use strict;
+
+use LJ::Subscription::Group;
+use LJ::Subscription::QuotaError;
+
+my @group_cols = (LJ::Subscription::Group::GROUP_COLS);
+my @other_cols = (LJ::Subscription::Group::OTHER_COLS);
+
+use Carp qw(confess cluck);
+
+sub new {
+    my ($class, $u) = @_;
+
+    return bless({
+        'user' => LJ::want_user($u),
+        'groups' => {},
+        'inbox_count' => 0,
+    }, $class);
+}
+
+sub clone {
+    my ($self) = @_;
+
+    my $class = ref $self;
+
+    my $ret = $class->new($self->user);
+
+    foreach my $group (values %{$self->{'groups'}}) {
+        foreach my $sub (values %{$group->{'subs'}}) {
+            $ret->insert_sub($sub);
+        }
+    }
+
+    return $ret;
+}
+
+sub _dbh {
+    my ($self) = @_;
+
+    unless ($self->{'dbh'}) {
+        $self->{'dbh'} = LJ::get_cluster_master($self->user);
+        $self->{'dbh'}->{'RaiseError'} = 1;
+    }
+
+    return $self->{'dbh'};
+}
+
+sub fetch_for_user {
+    my ($class, $u, $filter) = @_;
+
+    $filter ||= sub { 1 };
+
+    my $self = $class->new($u);
+
+    $u = LJ::want_user($u);
+
+    confess "cannot get a user" unless $u;
+
+    my $dbr = LJ::get_cluster_reader($u);
+    $dbr->{'RaiseError'} = 1;
+
+    confess "cannot get a database handle" unless $dbr;
+
+    my $group_cols = join(',', @group_cols);
+    my $other_cols = join(',', @other_cols);
+
+    my $res = $dbr->selectall_arrayref(qq{
+        SELECT
+            $group_cols, $other_cols
+        FROM subs
+        WHERE userid=?
+    }, { Slice => {} }, $u->id);
+
+    my $lastrow = undef;
+    my $lastobj = undef;
+
+    foreach my $row (@$res) {
+        my $sub = bless($row, 'LJ::Subscription');
+
+        $self->{'inbox_count'}++ if
+            $row->{'ntypeid'} == LJ::NotificationMethod::Inbox->ntypeid &&
+            $sub->group->is_tracking;
+
+        next unless $filter->($row);
+
+        $self->insert_sub($sub);
+    }
+
+    if ($u->{'opt_gettalkemail'} eq 'Y') {
+        my @virtual_subs = (
+            {
+                'event' => 'JournalNewComment',
+                'journalid' => $u->id,
+            },
+            {
+                'event' => 'CommunityEntryReply',
+            },
+            {
+                'event' => 'CommentReply',
+            },
+        );
+
+        foreach my $subhash (@virtual_subs) {
+            $subhash->{'etypeid'} =
+                LJ::Event->event_to_etypeid(delete $subhash->{'event'});
+            $subhash->{'journalid'} ||= 0;
+
+            $subhash = {
+                %$subhash,
+                'userid' => $u->id,
+                'is_dirty' => 0,
+                'arg1' => 0,
+                'arg2' => 0,
+                'ntypeid' => LJ::NotificationMethod::Email->ntypeid,
+                'createtime' => $u->timecreate,
+                'expiretime' => 0,
+                'flags' => 0,
+            };
+
+            next unless $filter->($subhash);
+
+            $self->find_or_insert_sub(bless($subhash, 'LJ::Subscription'));
+        }
+    }
+
+    return $self;
+}
+
+sub insert_group {
+    my ($self, $subgroup) = @_;
+
+    my $key = $subgroup->freeze;
+
+    $self->{'groups'}->{$key} = $subgroup;
+}
+
+sub find_group {
+    my ($self, $subgroup) = @_;
+
+    my $key = $subgroup->freeze;
+
+    return undef unless $self->{'groups'}->{$key};
+    return $self->{'groups'}->{$key};
+}
+
+sub find_or_insert_group {
+    my ($self, $subgroup) = @_;
+
+    my $key = $subgroup->freeze;
+
+    $self->{'groups'}->{$key} = $subgroup unless $self->{'groups'}->{$key};
+    return $self->{'groups'}->{$key};
+}
+
+sub insert_sub {
+    my ($self, $sub) = @_;
+
+    my $groupobj = $self->find_or_insert_group($sub->group);
+    $groupobj->insert_sub($sub);
+}
+
+sub find_sub {
+    my ($self, $sub) = @_;
+
+    my $groupobj = $self->find_group($sub->group) or return undef;
+    return $groupobj->find_sub($sub);
+}
+
+sub find_or_insert_sub {
+    my ($self, $sub) = @_;
+
+    my $groupobj = $self->find_or_insert_group($sub->group);
+    return $groupobj->find_or_insert_sub($sub);
+}
+
+sub groups {
+    my ($self) = @_;
+
+    return values %{$self->{'groups'}};
+}
+
+sub user {
+    my ($self) = @_;
+
+    return $self->{'user'};
+}
+
+sub userid {
+    my ($self) = @_;
+
+    return $self->user->id;
+}
+
+sub extract_groups {
+    my ($self, $groups) = @_;
+
+    my @ret;
+
+    foreach my $group (@$groups) {
+        $group = { 'event' => $group } if ref $group eq '';
+
+        if ($group->{'event'}) {
+            my $event = delete $group->{'event'};
+
+            if ($event =~ /\-u$/) {
+                $event =~ s/\-u$//;
+                $group->{'journalid'} ||= $self->userid;
+            }
+
+            $group->{'etypeid'} ||=
+                LJ::Event->event_to_etypeid($event);
+        }
+
+        $group->{'userid'} ||= $self->userid;
+        $group->{'arg1'} ||= 0;
+        $group->{'arg2'} ||= 0;
+
+        $group = bless($group, 'LJ::Subscription::Group') if ref $group eq 'HASH';
+
+        push @ret, $self->find_or_insert_group($group);
+    }
+
+    return \@ret;
+}
+
+sub convert_old_subs {
+    my ($self) = @_;
+
+    my $u = $self->user;
+
+    if ($u->{'opt_gettalkemail'} eq 'Y') {
+        my @virtual_subs = (
+            {
+                'event' => 'JournalNewComment',
+                'journalid' => $u->id,
+            },
+            {
+                'event' => 'CommunityEntryReply',
+            },
+            {
+                'event' => 'CommentReply',
+            },
+        );
+
+        foreach my $subhash (@virtual_subs) {
+            $subhash->{'etypeid'} =
+                LJ::Event->event_to_etypeid(delete $subhash->{'event'});
+            $subhash->{'journalid'} ||= 0;
+
+            $subhash = {
+                %$subhash,
+                'userid' => $u->id,
+                'is_dirty' => 0,
+                'arg1' => 0,
+                'arg2' => 0,
+                'ntypeid' => LJ::NotificationMethod::Email->ntypeid,
+                'createtime' => $u->timecreate,
+                'expiretime' => 0,
+                'flags' => 0,
+            };
+
+            my $sub = $self->find_sub(bless($subhash, 'LJ::Subscription'));
+
+            unless ($sub->{'subid'}) {
+                $self->_db_insert_sub($sub);
+            }
+        }
+
+        LJ::update_user($u, {'opt_gettalkemail' => 'N'});
+    }
+
+
+}
+
+sub update {
+    my ($self, $newset) = @_;
+
+    my $u = $self->user;
+
+    my $array2hash = sub {
+        return map { $_ => 1 } @_;
+    };
+
+    my @self_groups = keys %{$self->{'groups'}};
+    my @new_groups = keys %{$newset->{'groups'}};
+
+    my %self_groups = $array2hash->(@self_groups);
+    my %new_groups = $array2hash->(@new_groups);
+
+    my @added_groups = grep { !$self_groups{$_} } @new_groups;
+    my @changed_groups = grep { $self_groups{$_} } @new_groups;
+    my @cleaned_groups = grep { !$new_groups{$_} } @self_groups;
+
+    foreach my $gkey (@added_groups) {
+        my $group_new = $newset->{'groups'}->{$gkey};
+
+        # easy check: if they didn't specify any notification methods,
+        # we shall not create it
+        next unless keys %{$group_new->{'subs'}};
+
+        # ensure that the inbox method is in there
+        $group_new->ensure_inbox_created;
+
+        foreach my $ntypeid (keys %{$group_new->{'subs'}}) {
+            my $sub = $group_new->{'subs'}->{$ntypeid};
+            $self->_db_insert_sub($sub);
+        }
+    }
+
+    # cleaned groups are simply changed to have no subscriptions. we're not
+    # deleting them from the DB here -- at least, "inbox" ntype indicator sub
+    # must stay.
+    foreach my $gkey (@cleaned_groups) {
+        push @changed_groups, $gkey;
+        $newset->{'groups'}->{$gkey} = bless({
+            'subs' => {},
+        }, 'LJ::Subscription::Group');
+    }
+
+    foreach my $gkey (@changed_groups) {
+        my $group_old = $self->{'groups'}->{$gkey};
+
+        # if they can't access it, don't touch it at all.
+        next unless $group_old->event->available_for_user($self->user);
+
+        my $group_new = $newset->{'groups'}->{$gkey};
+
+        $group_new->ensure_inbox_created;
+
+        my %ntypeids = map { $_ => 1 } (
+            keys %{$group_old->{'subs'}},
+            keys %{$group_new->{'subs'}},
+        );
+
+        my $inbox_ntypeid = LJ::NotificationMethod::Inbox->ntypeid;
+        my @ntypeids;
+
+        # inbox goes first, for quota counting
+        push @ntypeids, $inbox_ntypeid if delete $ntypeids{$inbox_ntypeid};
+        push @ntypeids, keys %ntypeids;
+
+        foreach my $ntypeid (@ntypeids) {
+            my $sub_old = $group_old->{'subs'}->{$ntypeid};
+            my $sub_new = $group_new->{'subs'}->{$ntypeid};
+            if ($sub_old && $sub_new) {
+                # "update" case
+
+                # did they really change it? the only change at this point
+                # can be enabling/disabling, so:
+                next if $sub_old->{'flags'} == $sub_new->{'flags'};
+
+                # fine, they could tweak old-style subs, let's handle it
+                $self->convert_old_subs unless $sub_old->{'subid'};
+
+                $self->_db_update_sub($sub_old->{'subid'}, $sub_new);
+            } elsif ($sub_old) {
+                # "delete" case
+
+                $self->convert_old_subs unless $sub_old->{'subid'};
+                $self->_db_drop_sub($sub_old);
+            } elsif ($sub_new) {
+                # "insert" case
+
+                $self->_db_insert_sub($sub_new);
+            }
+        }
+    }
+}
+
+sub drop_group {
+    my ($self, $group) = @_;
+
+    return unless $self->find_group($group);
+
+    my (@sets, @binds);
+
+    foreach my $prop (@group_cols) {
+        push @sets, "$prop=?";
+        push @binds, $group->{$prop};
+    }
+
+    my $sets = join(' AND ', @sets);
+
+    $self->_dbh->do("DELETE FROM subs WHERE $sets", undef, @binds);
+}
+
+sub _db_collect_sets_binds {
+    my ($self, $sub, $cols) = @_;
+
+    $cols ||= [@group_cols, @other_cols];
+
+    my (@sets, @binds);
+    foreach my $key (@$cols) {
+        next if $key eq 'subid';
+
+        push @sets, "$key=?";
+        push @binds, int $sub->{$key};
+    }
+    my $sets = join(',', @sets);
+
+    return ($sets, @binds);
+}
+
+sub _db_insert_sub {
+    my ($self, $sub) = @_;
+
+    if ($sub->{'ntypeid'} == LJ::NotificationMethod::Inbox->ntypeid) {
+        die LJ::Subscription::QuotaError->new if
+            $self->{'inbox_count'} >= LJ::get_cap($self->user, 'subscriptions');
+
+        $self->{'inbox_count'}++ if
+            $sub->group->is_tracking;
+    }
+
+    $sub->{'userid'} ||= $self->user->id;
+    my ($sets, @binds) = $self->_db_collect_sets_binds($sub);
+    my $subid = LJ::alloc_user_counter($self->user, 'E');
+
+    $self->_dbh->do("INSERT INTO subs SET $sets, subid=?", undef, @binds, $subid);
+}
+
+sub _db_update_sub {
+    my ($self, $subid, $sub) = @_;
+
+    my ($sets, @binds) = $self->_db_collect_sets_binds($sub, ['flags']);
+
+    $self->_dbh->do("UPDATE subs SET $sets WHERE userid=? AND subid=?", undef, @binds, $self->user->id, $subid);
+}
+
+sub _db_drop_sub {
+    my ($self, $sub) = @_;
+
+    $self->{'inbox_count'}-- if
+        $sub->{'ntypeid'} == LJ::NotificationMethod::Inbox->ntypeid &&
+        $sub->group->is_tracking;
+
+    my (@sets, @binds);
+    foreach my $prop (@group_cols, 'ntypeid') {
+        push @sets, "$prop=?";
+        push @binds, $sub->{$prop};
+    }
+    my $sets = join(' AND ', @sets);
+
+    $self->_dbh->do("DELETE FROM subs WHERE $sets", undef, @binds);
+}
+
+1;

Added: trunk/cgi-bin/LJ/Subscription/QuotaError.pm
===================================================================
--- trunk/cgi-bin/LJ/Subscription/QuotaError.pm	                        (rev 0)
+++ trunk/cgi-bin/LJ/Subscription/QuotaError.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -0,0 +1,10 @@
+package LJ::Subscription::QuotaError;
+
+use strict;
+
+sub new {
+    my ($class) = @_;
+    return bless({}, $class);
+}
+
+1;

Modified: trunk/cgi-bin/LJ/Subscription.pm
===================================================================
--- trunk/cgi-bin/LJ/Subscription.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Subscription.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -1,11 +1,12 @@
 package LJ::Subscription;
 use strict;
-use Carp qw(croak confess);
+use Carp qw(croak confess cluck);
 use Class::Autouse qw(
                       LJ::NotificationMethod
                       LJ::Typemap
                       LJ::Event
                       LJ::Subscription::Pending
+                      LJ::Subscription::Group
                       );
 
 use constant {
@@ -35,6 +36,22 @@
     return $class->new_from_row($row);
 }
 
+sub dump {
+    my ($self) = @_;
+
+    return $self->id if $self->id && $self->id != 0;
+
+    my %props = map { $_ => $self->{$_} } @subs_fields;
+    return \%props;
+}
+
+sub new_from_dump {
+    my ($class, $u, $dump) = @_;
+
+    return $class->new_by_id($u, $dump) if ref $dump eq '';
+    return bless($dump, $class);
+}
+
 sub freeze {
     my $self = shift;
     return "subid-" . $self->owner->{userid} . '-' . $self->id;
@@ -65,25 +82,53 @@
 sub pending { 0 }
 sub default_selected { $_[0]->active && $_[0]->enabled }
 
-sub subscriptions_of_user {
+sub has_cached_subscriptions {
     my ($class, $u) = @_;
+    return defined $u->{'_subscriptions'};
+}
+
+sub query_user_subscriptions {
+    my ($class, $u, %filters) = @_;
     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 $dbh = LJ::get_cluster_reader($u) or die "cannot get a DB handle";
 
-    my @subs;
-    while (my $row = $sth->fetchrow_hashref) {
-        push @subs, LJ::Subscription->new_from_row($row);
+    my (@conditions, @binds);
+
+    push @conditions, 1;
+
+    foreach my $prop (qw(journalid ntypeid etypeid flags arg1 arg2)) {
+        next unless defined $filters{$prop};
+        push @conditions, "$prop=?";
+        push @binds, $filters{$prop};
     }
 
+    my $conditions = join(' AND ', @conditions);
+    return $dbh->selectall_arrayref(
+        qq{
+            SELECT
+                userid, subid, is_dirty, journalid, etypeid,
+                arg1, arg2, ntypeid, createtime, expiretime, flags
+            FROM subs
+            WHERE userid=? AND $conditions
+        }, { Slice => {} }, $u->id, @binds
+    );
+}
+
+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 $class->has_cached_subscriptions($u);
+
+    my @subs = map { $class->new_from_row($_) }
+        @{ $class->query_user_subscriptions($u) };
+
     $u->{_subscriptions} = \@subs;
 
     return @subs;
@@ -124,19 +169,36 @@
     return () if defined $arg1 && $arg1 =~ /\D/;
     return () if defined $arg2 && $arg2 =~ /\D/;
 
-    my @subs = $u->subscriptions;
+    my @subs;
+    if ($class->has_cached_subscriptions($u)) {
+        @subs = $u->subscriptions;
 
-    @subs = grep { $_->active && $_->enabled } @subs if $require_active;
+        @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;
-    @subs = grep { $_->flags     == $flags }             @subs if defined $flags;
+        # 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;
+        @subs = grep { $_->flags     == $flags }     @subs if defined $flags;
 
-    @subs = grep { $_->arg1 == $arg1 }                   @subs if defined $arg1;
-    @subs = grep { $_->arg2 == $arg2 }                   @subs if defined $arg2;
+        @subs = grep { $_->arg1 == $arg1 }           @subs if defined $arg1;
+        @subs = grep { $_->arg2 == $arg2 }           @subs if defined $arg2;
+    } else {
+        my %filters;
 
+        $filters{'journalid'} = $journalid           if defined $journalid;
+        $filters{'ntypeid'}   = $ntypeid             if $ntypeid;
+        $filters{'etypeid'}   = $etypeid             if $etypeid;
+        $filters{'flags'}     = $flags               if defined $flags;
+        $filters{'arg1'}      = $arg1                if defined $arg1;
+        $filters{'arg2'}      = $arg2                if defined $arg2;
+
+        @subs = map { $class->new_from_row($_) }
+            @{ $class->query_user_subscriptions($u, %filters) };
+
+        @subs = grep { $_->active && $_->enabled } @subs if $require_active;
+    }
+
     return @subs;
 }
 
@@ -585,9 +647,14 @@
 
     $u ||= $self->owner;
 
-    return $self->event_class->available_for_user($u, $self);
+    return $self->group->event->available_for_user($u);
 }
 
+sub group {
+    my ($self) = @_;
+    return LJ::Subscription::Group->group_from_sub($self);
+}
+
 package LJ::Error::Subscription::TooMany;
 sub fields { qw(subscr u); }
 

Added: trunk/cgi-bin/LJ/Widget/SubscribeInterface.pm
===================================================================
--- trunk/cgi-bin/LJ/Widget/SubscribeInterface.pm	                        (rev 0)
+++ trunk/cgi-bin/LJ/Widget/SubscribeInterface.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -0,0 +1,157 @@
+package LJ::Widget::SubscribeInterface;
+
+use strict;
+use base qw(LJ::Widget);
+
+sub need_res {
+    return qw(
+        stc/esn.css
+        js/core.js
+        js/dom.js
+        js/checkallbutton.js
+        js/esn.js
+    );
+};
+
+sub render_body {
+    my ($self, $opts) = @_;
+
+    my @groups = @{$opts->{'groups'}};
+    my $u = $opts->{'u'} || LJ::get_remote();
+
+    my @ntypes = @LJ::NOTIFY_TYPES;
+    my $colnum = scalar(@ntypes) + 1;
+
+    my %ntypeids = map { $_ => $_->ntypeid } @ntypes;
+
+    my $ret = '';
+
+    $ret .= '<table class="Subscribe" cellspacing="0" cellpadding="0" ' .
+        'style="clear:none">' unless $self->{'no_table'};
+
+    $self->{'catnum'} ||= 0;
+    $self->{'field_num'} ||= 0;
+
+    my $curcatnum = $self->{'catnum'}++;
+    $ret .= '<tbody class="CategoryRow-' . $curcatnum . ' ' .
+        'CategoryRow-'.$opts->{'css_class'}.'">';
+    
+    $ret .= '<tr class="CategoryRow CategoryRowFirst">';
+    $ret .= '<td>';
+    $ret .= '<span class="CategoryHeading">'.$opts->{'title'}.'</span>';
+
+    unless ($self->{'printed_ntypeids_hidden'}) {
+        $self->{'printed_ntypeids_hidden'} = 1;
+        $ret .= LJ::html_hidden({'id' => 'ntypeids', 'value' => join(',', values %ntypeids)});
+    }
+
+    $ret .= '</td>';
+    foreach my $ntype (@ntypes) {
+        $ret .= "<td>";
+
+        my $class = $ntype;
+        my $title = $class->title;
+        my $enabled = $class->configured_for_user($u);
+
+        if ($class->disabled_url && !$enabled) {
+            $title = "<a href='" . $class->disabled_url . "'>$title</a>";
+        } elsif ($class->url) {
+            $title = "<a href='" . $class->url . "'>$title</a>";
+        }
+
+        $title .= " " . LJ::help_icon($class->help_url) if $class->help_url;
+
+        $ret .= LJ::html_check({
+            'name' => '',
+            'class' => 'CheckAll',
+            'id' => 'CheckAll-'.$curcatnum.'-'.$ntypeids{$ntype},
+            'label' => $title,
+            'disabled' => !$enabled,
+            'noescape' => 1,
+        });
+        $ret .= "</td>";
+    }
+    $ret .= '</tr>';
+
+    my $altrow = 0;
+    my $visible_groups = 0;
+
+    foreach my $group (@groups) {
+        my $interface = $group->get_interface_status($u);
+        next unless $interface->{'visible'};
+
+        $visible_groups++;
+
+        my @classes;
+        push @classes, "altrow" if $altrow; $altrow = !$altrow;
+        push @classes, "disabled" if $interface->{'disabled'};
+        my $field_num = $self->{'field_num'}++;
+
+        $ret .= '<tr class="'.join(' ', @classes).'">';
+
+        my $event_html = $group->event_as_html($field_num);
+
+        if ($self->{'allow_delete'} && $group->is_tracking) {
+            my $link;
+            my $frozen = $group->freeze;
+
+            $link = $self->{'page'};
+            $link .= ($link =~ /\?/ ? '&' : '?');
+            $link .= 'delete_group='.$group->freeze . '&';
+            $link .= 'auth_token='.LJ::eurl(LJ::Auth->ajax_auth_token(
+                $u, $self->{'page'},
+                'delete_group' => $group->freeze,
+            ));
+
+            $event_html = qq{
+                <a href="$link" class="delete-group-$frozen">
+                    <img src="$LJ::SITEROOT/img/portal/btn_del.gif">
+                </a>
+                $event_html
+            };
+        }
+
+        if ($interface->{'disabled'}) {
+            $event_html .= ' ' . $interface->{'disabled_pic'};
+        }
+
+        $ret .= '<td>' . $event_html . '</td>';
+        foreach my $ntype (@ntypes) {
+            my $ntypeid = $ntypeids{$ntype};
+            my $sub = $group->find_or_insert_ntype($ntypeid);
+
+            my $ntype_interface = $group->get_ntype_interface_status($ntypeid, $u);
+            my $value = $ntype_interface->{'disabled'} ?
+                $ntype_interface->{'force'} :
+                $sub->active;
+
+            $ret .= '<td>' . LJ::html_check({
+                    'name' => 'sub-' . $field_num . '-' . $ntypeid,
+                    'id' => 'sub-' . $field_num . '-' . $ntypeid,
+                    'selected' => $value,
+                    'disabled' => $ntype_interface->{'disabled'},
+                    'class' => 'SubscribeCheckbox-'.$curcatnum.'-'.$ntypeids{$ntype},
+                }) . '</td>';
+        }
+        $ret .= '</td>';
+    }    
+
+    unless ($visible_groups) {
+        my $blurb = "<?p <strong>" . LJ::Lang::ml('subscribe_interface.nosubs.title') . "</strong><br />";
+        $blurb .= LJ::Lang::ml('subscribe_interface.nosubs.text', { img => "<img src='$LJ::SITEROOT/img/btn_track.gif' width='22' height='20' align='absmiddle' />" }) . " p?>";
+
+        $ret .= "<tr>";
+        $ret .= "<td colspan='$colnum'>$blurb</td>";
+        $ret .= "</tr>";
+    }
+
+    $ret .= '</tbody>';
+
+    $ret .= '</table>' unless $self->{'no_table'};
+    $ret .= LJ::html_hidden({'id' => 'catids', 'value' => $curcatnum})
+        unless $self->{'no_table'};
+
+    return $ret;
+}
+
+1;

Modified: trunk/cgi-bin/LJ/Widget.pm
===================================================================
--- trunk/cgi-bin/LJ/Widget.pm	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/LJ/Widget.pm	2010-01-21 12:38:13 UTC (rev 16116)
@@ -93,7 +93,9 @@
 
     return "" unless $class->should_render;
 
-    my $ret = "<div class='appwidget appwidget-$css_subclass' id='$widget_ele_id'>\n";
+    my $ret = '';
+    $ret .= "<div class='appwidget appwidget-$css_subclass' id='$widget_ele_id'>\n"
+        unless ref $class && $class->{'no_container_div'};
 
     my $rv = eval {
         my $widget = ref $class ? $class : "LJ::Widget::$subclass";
@@ -127,7 +129,8 @@
     } or $class->handle_error($@);
 
     $ret .= $rv;
-    $ret .= "</div><!-- end .appwidget-$css_subclass -->\n";
+    $ret .= "</div><!-- end .appwidget-$css_subclass -->\n"
+        unless ref $class && $class->{'no_container_div'};
 
     return $ret;
 }

Modified: trunk/cgi-bin/redirect.dat
===================================================================
--- trunk/cgi-bin/redirect.dat	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/redirect.dat	2010-01-21 12:38:13 UTC (rev 16116)
@@ -42,4 +42,7 @@
 /customize/style.bml                /customize/
 /customize/themes.bml               /customize/
 /manage/links.bml                   /customize/options.bml?group=linkslist
+/manage/subscriptions            /manage/settings/?cat=notifications
+/manage/subscriptions/           /manage/settings/?cat=notifications
+/manage/subscriptions/index.bml  /manage/settings/?cat=notifications
 /userinfo_2008.bml                  /userinfo.bml

Modified: trunk/cgi-bin/talklib.pl
===================================================================
--- trunk/cgi-bin/talklib.pl	2010-01-21 10:27:46 UTC (rev 16115)
+++ trunk/cgi-bin/talklib.pl	2010-01-21 12:38:13 UTC (rev 16116)
@@ -298,7 +298,7 @@
     # note $form no longer used
 
     my $err = sub {
-        $$errref = "<h1><?_ml Error _ml?></h1><p>$_[0]</p>";
+        $$errref = "<?h1 <?_ml Error _ml?> h1?><?p $_[0] p?>";
         return 0;
     };
 
@@ -1309,28 +1309,6 @@
             if LJ::Talk::Post::over_maxcomments($journalu, $jitemid);
     }
 
-    # can a comment even be made?
-    
-    my $entry = LJ::Entry->new($journalu->id, jitemid => $opts->{ditemid});
-    if ($entry->prop('opt_nocomments')) {
-        $ret .= "<h1>$BML::ML{'Sorry'}</h1><p>$BML::ML{'.error.nocommentspost'}</p>";
-        $opts->{'err'} = 1;
-        return $ret;
-    }
-    if ($journalu->{'opt_showtalklinks'} eq "N") {
-        $ret .= "<h1>$BML::ML{'Sorry'}</h1><p>$BML::ML{'.error.nocommentsjournal'}</p>";
-        $opts->{'err'} = 1;
-        return $ret;
-    }
-    unless (LJ::get_cap($journalu, "get_comments") ||
-            ($remote && LJ::get_cap($remote, "leave_comments"))) {
-        $ret .= "<h1>$BML::ML{'Sorry'}</h1><p>";
-        $ret .= $LJ::MSG_NO_COMMENT || "Sorry, you cannot leave comments at this time.";
-        $ret .= "</p>";
-        $opts->{'err'} = 1;
-        return $ret;
-    }
-
     if (!$editid && $parpost->{'state'} eq "S") {
         $ret .= "<div class='ljwarnscreened'>$BML::ML{'.warnscreened'}</div>";
     }
@@ -2284,373 +2262,6 @@
     return "<$type-$jid-$did\@$LJ::DOMAIN>";
 }
 
-my @_ml_strings_en =...
 (truncated)
Tags: andy, bml, css, dat, js, livejournal, pl, pm
Subscribe

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 4 comments