#!/usr/bin/perl # # DW::BusinessRules::InviteCodes::DWS # # This module implements business rules for invite code distribution that are # specific to Dreamwidth Studios, LLC # # Authors: # Pau Amma # # Copyright (c) 2009-2016 by Dreamwidth Studios, LLC. # # This program is free software; you may redistribute it and/or modify it under # the same terms as Perl itself. For a copy of the license, please reference # 'perldoc perlartistic' or 'perldoc perlgpl'. package DW::BusinessRules::InviteCodes::DWS; use strict; use warnings; use Carp (); use base 'DW::BusinessRules::InviteCodes'; use DW::InviteCodes; use LJ::User; use DW::Pay; =head1 NAME DW::BusinessRules::InviteCodes::DWS - DWS-specific invite code business rules =head1 DESCRIPTION This module implements business rules for invite code distribution that are specific to Dreamwidth Studios, LLC. Refer to DW::BusinessRules::InviteCodes for more information and for the external API (user_classes, max_users, search_class, and adj_invites). =cut # key => { search => \&search_fun [, search_arg => 'second argument' ] } # key is also used for long cat name (invitecodes.userclass.*), and first # argument is always max users to return. my %user_classes = ( basic_paid => { search => \&_search_paystatus, search_arg => [ typeid => 3 ] }, premium_paid => { search => \&_search_paystatus, search_arg => [ typeid => 4 ] }, permanent_paid => { search => \&_search_paystatus, search_arg => [ permanent => 1 ] }, # Users active in the last 30 days active30d => { search => \&_search_ctrk, search_arg => 30 }, # Users with no invites left noinvleft => { search => \&_search_noinvleft }, # No search arg # Users with no invites left and 1 invitee paid/perm/active in last 30 days noinvleft_apinv => { search => \&_search_noinvleft_apinvitee, search_arg => 30 }, ); sub user_classes { my ($lang) = @_; $lang ||= LJ::Lang::get_effective_lang(); my %ucname; $ucname{$_} = LJ::Lang::get_text( $lang, "invitecodes.userclass.$_" ) foreach keys %user_classes; return \%ucname; } # If there are fewer invites than qualifying users, invites get up to 1 per # user, but only if invites are at least 3/4 of users. Hence, user limit is # 4/3 of invites + 1. sub max_users { my ($ninv) = @_; return int( $ninv + $ninv / 3 + 1 ); } sub search_class { my ( $uckey, $max_nusers ) = @_; Carp::croak("$uckey not a known user class") unless exists $user_classes{$uckey}; my $uclass = $user_classes{$uckey}; return $uclass->{search}->( $uckey, $max_nusers, $uclass->{search_arg} ); } # Search pay status sub _search_paystatus { my ( $uckey, $max_nusers, $search_arg ) = @_; my $uids = DW::Pay::get_current_paid_userids( limit => $max_nusers, @$search_arg ); # TODO: Allow nonvalidated email addresses? We need to deal with users who # shouldn't be send email for some reason anyway (eg because they opted out # of mass mailings) by putting the notice in their inbox instead (or in # addition) or discarding it altogether, so might as well handle # nonvalidated addresses the same way. (Note that this applies to all # search functions, not just this one.) # Don't filter if too many, otherwise we lose that information return ( $max_nusers <= scalar @$uids ) ? $uids : _filter_pav($uids); } # Search in "clustertrack2" (clustered) for recent activity sub _search_ctrk { my ( $uckey, $max_nusers, $days ) = @_; my @uids; LJ::foreach_cluster( sub { return if $max_nusers <= @uids; my ( $cid, $dbh ) = @_; # Can't do a join here to weed out comms/unvalidated/not visible, since # the table with that info is elsecluster. So do separate filtering # pass using _filter_pav. my $sth = $dbh->prepare( "SELECT userid FROM clustertrack2 " . "WHERE timeactive >= UNIX_TIMESTAMP() - ? " . "LIMIT ?" ) or die $dbh->errstr; my $cuids = $dbh->selectcol_arrayref( $sth, {}, $days * 86400, $max_nusers - @uids ) or die $dbh->errstr; push @uids, @$cuids; } ); # Don't filter if too many, otherwise we lose that information return ( $max_nusers <= scalar @uids ) ? \@uids : _filter_pav( \@uids ); } # TODO: refactor into DW::InviteCodes # Search "acctcode" (unclustered) for users with no invite left sub _search_noinvleft { my ( $uckey, $max_nusers ) = @_; my $dbslow = LJ::get_dbh('slow') or die "Can't get slow role"; # return all personal, active, visible journals... no need to use _filter_pav # later, when we can do it all on the user table to begin with. this returns # all users that either have no invites OR my $uids = $dbslow->selectcol_arrayref( q{SELECT DISTINCT u.userid FROM user u LEFT JOIN acctcode a ON a.userid = u.userid AND a.rcptid = 0 WHERE (u.journaltype = 'P' AND u.status = 'A' AND u.statusvis = 'V') AND a.userid IS NULL } ); return $uids; } # TODO: refactor into DW::InviteCodes # Search "acctcode" (unclustered) for users with no invite left, then restrict # to those having at least one active or paid invitee sub _search_noinvleft_apinvitee { my ( $uckey, $max_nusers, $days ) = @_; my $dbslow = LJ::get_dbh('slow') or die "Can't get slow role"; # Second column will be all 0 here (and is unneeded anyway), but putting it # in HAVING and not SELECT is non-standard SQL. my $sth = $dbslow->prepare( "SELECT userid, min(rcptid) FROM acctcode " . "GROUP BY userid HAVING min(rcptid) > 0 LIMIT ?" ) or die $dbslow->errstr; # Keep only userid my $uids = $dbslow->selectcol_arrayref( $sth, { Columns => [1] }, $max_nusers ) or die $dbslow->errstr; # Don't filter if too many, otherwise we lose that information return $uids if $max_nusers <= scalar @$uids; $uids = _filter_pav($uids); my @filtered_uids; OWNER: foreach my $ouid (@$uids) { my @ics = DW::InviteCodes->by_owner( userid => $ouid ); my @inv_uids; foreach my $code (@ics) { push @inv_uids, $code->recipient if $code->recipient; } my $inv_uhash = LJ::load_userids(@inv_uids); foreach my $iuser ( values %$inv_uhash ) { if ( defined( DW::Pay::get_current_account_status($iuser) ) || $iuser->get_timeactive >= time() - $days * 86400 ) { push @filtered_uids, $ouid; next OWNER; } } } return \@filtered_uids; } # From a list of userids, returns those for personal, visible journals with # validated email addresses. sub _filter_pav { my ($in_uids) = @_; my @out_uids; # TODO: make magic number configurable. # TODO: use splice() # perldoc -f splice for ( my $start = 0 ; $start < @$in_uids ; $start += 1000 ) { my $end = ( $start + 999 <= $#$in_uids ) ? $start + 999 : $#$in_uids; my $uhash = LJ::load_userids( @{$in_uids}[ $start .. $end ] ); while ( my ( $uid, $user ) = each %$uhash ) { push @out_uids, $uid if $user->is_person && $user->is_visible && $user->is_validated; } } return \@out_uids; } # Returns $ninv adjusted to next higher multiple of $nusers if remainder is at # least 75% of $nusers, to next lower multiple instead. sub adj_invites { my ( $ninv, $nusers ) = @_; return 0 if $ninv <= 0 || $nusers <= 0; my $remainder = $ninv % $nusers; return ( $remainder < 0.75 * $nusers ) ? $ninv - $remainder : $ninv + $nusers - $remainder; } 1;