247 lines
7.8 KiB
Perl
247 lines
7.8 KiB
Perl
|
|
#!/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 <pauamma@dreamwidth.org>
|
||
|
|
#
|
||
|
|
# 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;
|