ljr/livejournal/cgi-bin/directorylib.pl

650 lines
18 KiB
Perl
Executable File

#!/usr/bin/perl
#
# Directory search code.
#
############################################################
#
# Misc Notes...
#
# directory handle can only touch:
# community
# friends
# payments
# userinterests
# userprop
# userusage
#
use strict;
package LJ::Dir;
use Digest::MD5 qw(md5_hex);
my $MAX_RETURN_RESULT = 1000;
my %filters = (
'int' => { 'searcher' => \&search_int,
'validate' => \&validate_int, },
'fr' => { 'searcher' => \&search_fr,
'validate' => \&validate_fr, },
'fro' => { 'searcher' => \&search_fro,
'validate' => \&validate_fro, },
'loc' => { 'validate' => \&validate_loc,
'searcher' => \&search_loc, },
#'gen' => { 'validate' => \&validate_gen,
# 'searcher' => \&search_gen, },
'age' => { 'validate' => \&validate_age,
'searcher' => \&search_age, },
'ut' => { 'validate' => \&validate_ut,
'searcher' => \&search_ut, },
'com' => { 'searcher' => \&search_com,
'validate' => \&validate_com, },
);
# validate all filter options
#
sub validate
{
my ($req, $errors) = @_;
my @filters;
foreach my $f (sort keys %filters) {
next unless $filters{$f}->{'validate'};
if ($filters{$f}->{'validate'}->($req, $errors)) {
push @filters, $f;
}
}
return sort @filters;
}
# entry point to do a search: give it
# a db-read handle
# directory master (must be able to write to dirsearchres2)
# hashref of the request,
# a listref of where to put the user hashrefs returned,
# hashref of where to return results of the query
sub do_search
{
my ($dbr, $dbdir, $req, $users, $info) = @_;
my $sth;
# clear return buffers
@{$users} = ();
%{$info} = ();
my @crits;
foreach my $f (sort keys %filters)
{
next unless $filters{$f}->{'validate'}->($req, []);
my @criteria = $filters{$f}->{'searcher'}->($dbr, $req, $info);
if (@criteria) {
push @crits, @criteria;
} else {
# filters return nothing to signal an error, and should have set $info->{'errmsg'}
$info->{'errmsg'} = "[Filter $f failed] $info->{'errmsg'}";
return 0;
}
}
unless (scalar(@crits)) {
$info->{'errmsg'} = "You did not enter any search criteria.";
return 0;
}
########## time to build us some huge SQL statement. yee haw.
my $orderby;
my %only_one_copy = qw(community c user u userusage uu);
## keep track of what table aliases we've used
my %alias_used;
$alias_used{'u'} = "?"; # only used by dbr, not dbdir
$alias_used{'c'} = "?"; # might be used later, if opt_format eq "com"
$alias_used{'uu'} = "?"; # might be used later, if opt_sort is by time
my %conds; # where condition -> 1
my %useridcol; # all keys here equal each other (up.userid == uu.userid == ..)
## foreach each critera, build up the query
foreach my $crit (@crits)
{
### each search criteria has its own table aliases. make those unique.
my %map_alias = (); # keep track of local -> global table alias mapping
foreach my $localalias (keys %{$crit->{'tables'}})
{
my $table = $crit->{'tables'}->{$localalias};
my $newalias;
# some tables might be used multiple times but they're
# setup such that opening them multiple times is useless.
if ($only_one_copy{$table}) {
$newalias = $only_one_copy{$table};
$alias_used{$newalias} = $table;
} else {
my $ct = 1;
$newalias = $localalias;
while ($alias_used{$newalias}) {
$ct++;
$newalias = "$localalias$ct";
}
$alias_used{$newalias} = $table;
}
$map_alias{$localalias} = $newalias;
}
## add each condition to the where clause, after fixing up aliases
foreach my $cond (@{$crit->{'conds'}}) {
$cond =~ s/\{(\w+?)\}/$map_alias{$1}/g;
$conds{$cond} = 1;
}
## add join to u.userid table
my $cond = $crit->{'userid'};
if ($cond) {
$cond =~ s/\{(\w+?)\}/$map_alias{$1}/g;
$useridcol{$cond} = 1;
}
}
my $pagesize = $req->{'opt_pagesize'}+0 || 100;
if ($pagesize > 200) { $pagesize = 200; }
if ($pagesize < 5) { $pagesize = 5; }
$req->{'opt_format'} ||= "pics";
if ($req->{'opt_format'} eq "com") {
$alias_used{'c'} = "community";
$useridcol{"c.userid"} = 1;
}
$req->{'opt_sort'} ||= "ut";
if ($req->{'opt_sort'} eq "ut") {
$alias_used{'uu'} = 'userusage';
$useridcol{"uu.userid"} = 1;
$orderby = "ORDER BY uu.timeupdate DESC";
} elsif ($req->{'opt_sort'} eq "user") {
$alias_used{'u'} = 'user';
$useridcol{"u.userid"} = 1;
$orderby = "ORDER BY u.user";
} elsif ($req->{'opt_sort'} eq "name") {
$alias_used{'u'} = 'user';
$useridcol{"u.userid"} = 1;
$orderby = "ORDER BY u.name";
}
# delete reserved table aliases the didn't end up being used
foreach (keys %alias_used) {
delete $alias_used{$_} if $alias_used{$_} eq "?";
}
# add clauses to make all userid cols equal each other
my $useridcol; # any one
foreach my $ca (keys %useridcol) {
foreach my $cb (keys %useridcol) {
next if $ca eq $cb;
$conds{"$ca=$cb"} = 1;
}
$useridcol = $ca;
}
my $fromwhat = join(", ", map { "$alias_used{$_} $_" } keys %alias_used);
my $conds = join(" AND ", keys %conds);
my $sql = "SELECT $useridcol FROM $fromwhat WHERE $conds $orderby LIMIT $MAX_RETURN_RESULT";
if ($req->{'sql'}) {
$info->{'errmsg'} = "SQL: $sql";
return 0;
}
my $qdig = $dbr->quote(md5_hex($sql));
my $hit_cache = 0;
my $count = 0;
my @ids;
# delete any stale results
$dbdir->do("DELETE FROM dirsearchres2 WHERE qdigest=$qdig AND ".
"dateins < DATE_SUB(NOW(), INTERVAL 15 MINUTE)");
# mark query as in progress.
$dbdir->do("INSERT INTO dirsearchres2 (qdigest, dateins, userids) ".
"VALUES ($qdig, NOW(), '[searching]')");
if ($dbdir->err)
{
# if there's an error inserting that, we know something's there.
# let's see what!
my $ids = $dbdir->selectrow_array("SELECT userids FROM dirsearchres2 ".
"WHERE qdigest=$qdig");
if (defined $ids) {
if ($ids eq "[searching]") {
# somebody else (or same user before) is still searching
$info->{'searching'} = 1;
return 1;
}
@ids = split(/,/, $ids);
$count = scalar(@ids);
$hit_cache = 1;
}
}
## guess we'll have to query it.
if (! $hit_cache)
{
BML::do_later(sub {
$sth = $dbdir->prepare($sql);
$sth->execute;
while (my ($id) = $sth->fetchrow_array) {
push @ids, $id;
}
my $ids = $dbdir->quote(join(",", @ids));
$dbdir->do("REPLACE INTO dirsearchres2 (qdigest, dateins, userids) ".
"VALUES ($qdig, NOW(), $ids)");
});
$info->{'searching'} = 1;
return 1;
}
my $page = $req->{'page'} || 1;
my $skip = ($page-1)*$pagesize;
my $pages = int($count / $pagesize) + (($count % $pagesize) ? 1 : 0);
$pages ||= 1;
if ($page > $pages) { $page = $pages; }
$info->{'pages'} = $pages;
$info->{'page'} = $page;
$info->{'first'} = ($page-1)*$pagesize+1;
$info->{'last'} = $page * $pagesize;
$info->{'count'} = $count;
if ($count == $MAX_RETURN_RESULT) {
$info->{'overflow'} = 1;
}
if ($page == $pages) { $info->{'last'} = $count; }
## now, get info on the ones we want.
@ids = grep{ $_+0 } @ids[($info->{'first'}-1)..($info->{'last'}-1)];
return 1 unless @ids;
my %u;
LJ::load_userids_multiple([ map { $_ => \$u{$_} } @ids ]);
my $tu = LJ::get_timeupdate_multi(@ids);
my $now = time();
# need to get community info
if ($req->{'opt_format'} eq "com") {
my $in = join(',', @ids);
my $sth = $dbr->prepare("SELECT userid, membership, postlevel ".
"FROM community ".
"WHERE userid IN ($in)");
$sth->execute;
while (my ($uid, $mem, $postlev) = $sth->fetchrow_array) {
next unless $u{$uid};
$u{$uid}->{'membership'} = $mem;
$u{$uid}->{'postlevel'} = $postlev;
}
foreach (@ids) {
delete $u{$_} unless $u{$_}->{'membership'};
}
}
foreach my $id (@ids) {
next unless $u{$id} && $u{$id}->{'statusvis'} eq "V";
$u{$id}->{'secondsold'} = $tu->{$id} ? $now - $tu->{$id} : undef;
push @$users, $u{$id} if $u{$id};
}
return 1;
}
sub ago_text
{
my $secondsold = shift;
return "Never." unless ($secondsold);
my $num;
my $unit;
if ($secondsold > 60*60*24*7) {
$num = int($secondsold / (60*60*24*7));
$unit = "week";
} elsif ($secondsold > 60*60*24) {
$num = int($secondsold / (60*60*24));
$unit = "day";
} elsif ($secondsold > 60*60) {
$num = int($secondsold / (60*60));
$unit = "hour";
} elsif ($secondsold > 60) {
$num = int($secondsold / (60));
$unit = "minute";
} else {
$num = $secondsold;
$unit = "second";
}
return "$num $unit" . ($num==1?"":"s") . " ago";
}
########## INTEREST ############
sub validate_int
{
my ($req, $errors) = @_;
my $int = lc($req->{'int_like'});
$int =~ s/^\s+//;
$int =~ s/\s+$//;
return 0 unless $int;
$req->{'int_like'} = $int;
return 1;
}
sub search_int
{
my ($dbr, $req, $info) = @_;
my $arg = $req->{'int_like'};
push @{$info->{'english'}}, "are interested in \"$arg\"";
## find interest id, if one doth exist.
my $qint = $dbr->quote($req->{'int_like'});
my $intid = $dbr->selectrow_array("SELECT intid FROM interests ".
"WHERE interest=$qint");
unless ($intid) {
$info->{'errmsg'} = "The interest you have entered is not valid.";
return;
}
my $UI_TABLE = $req->{'com_do'} ? "comminterests" : "userinterests";
return {
'tables' => {
'ui' => $UI_TABLE,
},
'conds' => [ "{ui}.intid=$intid" ],
'userid' => "{ui}.userid",
};
}
######## HAS FRIEND ##############
sub validate_fr
{
my ($req, $errors) = @_;
return 0 unless $req->{'fr_user'} =~ /\S/;
return 1;
}
sub search_fr
{
my ($dbr, $req, $info) = @_;
my $user = lc($req->{'fr_user'});
my $arg = $user;
push @{$info->{'english'}}, "consider \"$arg\" a friend";
my $friendid = LJ::get_userid($user);
return {
'tables' => {
'f' => 'friends',
},
'conds' => [ "{f}.friendid=$friendid" ],
'userid' => "{f}.userid",
};
}
######## FRIEND OF ##############
sub validate_fro
{
my ($req, $errors) = @_;
return 0 unless $req->{'fro_user'} =~ /\S/;
return 1;
}
sub search_fro
{
my ($dbr, $req, $info) = @_;
my $user = lc($req->{'fro_user'});
my $arg = $user;
push @{$info->{'english'}}, "are considered a friend by \"$arg\"";
my $userid = LJ::get_userid($user);
return {
'tables' => {
'f' => 'friends',
},
'conds' => [ "{f}.userid=$userid" ],
'userid' => "{f}.friendid",
};
}
########### LOCATION ###############
sub validate_loc
{
my ($req, $errors) = @_;
return 0 unless $req->{'loc_cn'};
unless ($req->{'loc_cn'} =~ /^[A-Z]{2}$/ || # ISO code
$req->{'loc_cn'} =~ /^LJ/) # site-local country/region code
{
push @$errors, "Invalid country for location search.";
return 0;
}
return 1;
}
sub search_loc
{
my ($dbr, $req, $info) = @_;
my ($sth);
my ($longcountry, $longstate, $longcity);
my $qcode = $dbr->quote(uc($req->{'loc_cn'}));
$sth = $dbr->prepare("SELECT item FROM codes WHERE type='country' AND code=$qcode");
$sth->execute;
($longcountry) = $sth->fetchrow_array;
$longstate = lc($req->{'loc_st'});
$longstate =~ s/(\w+)/\u$1/g;
$longcity = lc($req->{'loc_ci'});
$longcity =~ s/(\w+)/\u$1/g;
$req->{'loc_st'} = lc($req->{'loc_st'});
$req->{'loc_ci'} = lc($req->{'loc_ci'});
if ($req->{'loc_cn'} eq "US") {
my $qstate = $dbr->quote($req->{'loc_st'});
if (length($req->{'loc_st'}) > 2) {
## convert long state name into state code
$sth = $dbr->prepare("SELECT code FROM codes WHERE type='state' AND item=$qstate");
$sth->execute;
my ($code) = $sth->fetchrow_array;
if ($code) {
$req->{'loc_st'} = lc($code);
}
} else {
$sth = $dbr->prepare("SELECT item FROM codes WHERE type='state' AND code=$qstate");
$sth->execute;
($longstate) = $sth->fetchrow_array;
}
}
push @{$info->{'english'}}, "live in " . join(", ", grep { $_; } ($longcity, $longstate, $longcountry));
my $p = LJ::get_prop("user", "sidx_loc");
unless ($p) {
$info->{'errmsg'} = "Userprop sidx_loc doesn't exist. Run update-db.pl?";
return;
}
my $prefix = join("-", $req->{'loc_cn'}, $req->{'loc_st'}, $req->{'loc_ci'});
$prefix =~ s/\-+$//; # remove trailing hyphens
$prefix =~ s![\_\%\"\']!\\$&!g;
#### do the sub requests.
return {
'tables' => {
'up' => 'userprop',
},
'conds' => [ "{up}.upropid=$p->{'id'}",
"{up}.value LIKE '$prefix%'",
],
'userid' => "{up}.userid",
};
}
########### GENDER ###################
sub validate_gen
{
my ($req, $errors) = @_;
return 0 unless $req->{'gen_sel'};
unless ($req->{'gen_sel'} eq "M" ||
$req->{'gen_sel'} eq "F")
{
push @$errors, "You must select either Male or Female when searching by gender.\n";
return 0;
}
return 1;
}
sub search_gen
{
my ($dbr, $req, $info) = @_;
my $args = $req->{'gen_sel'};
push @{$info->{'english'}}, "are " . ($args eq "M" ? "male" : "female");
my $qgen = $dbr->quote($args);
my $p = LJ::get_prop("user", "gender");
unless ($p) {
$info->{'errmsg'} = "Userprop gender doesn't exist. Run update-db.pl?";
return;
}
return {
'tables' => {
'up' => 'userprop',
},
'conds' => [ "{up}.upropid=$p->{'id'}",
"{up}.value=$qgen",
],
'userid' => "{up}.userid",
};
}
########### AGE ###################
sub validate_age
{
my ($req, $errors) = @_;
return 0 if $req->{'age_min'} eq "" && $req->{'age_max'} eq "";
for (qw(age_min age_max)) {
unless ($req->{$_} =~ /^\d+$/) {
push @$errors, "Both min and max age must be specified for an age query.";
return 0;
}
}
if ($req->{'age_min'} > $req->{'age_max'}) {
push @$errors, "Minimum age must be less than maximum age.";
return 0;
}
if ($req->{'age_min'} < 14) {
push @$errors, "You cannot search for users under 14 years of age.";
return 0;
}
return 1;
}
sub search_age
{
my ($dbr, $req, $info) = @_;
my $qagemin = $dbr->quote($req->{'age_min'});
my $qagemax = $dbr->quote($req->{'age_max'});
my $args = "$req->{'age_min'}-$req->{'age_max'}";
if ($req->{'age_min'} == $req->{'age_max'}) {
push @{$info->{'english'}}, "are $req->{'age_min'} years old";
} else {
push @{$info->{'english'}}, "are between $req->{'age_min'} and $req->{'age_max'} years old";
}
my $p = LJ::get_prop("user", "sidx_bdate");
unless ($p) {
$info->{'errmsg'} = "Userprop sidx_bdate doesn't exist. Run update-db.pl?";
return;
}
return {
'tables' => {
'up' => 'userprop',
},
'conds' => [ "{up}.upropid=$p->{'id'}",
"{up}.value BETWEEN DATE_SUB(NOW(), INTERVAL $qagemax YEAR) AND DATE_SUB(NOW(), INTERVAL $qagemin YEAR)",
],
'userid' => "{up}.userid",
};
}
########### UPDATE TIME ###################
sub validate_ut
{
my ($req, $errors) = @_;
return 0 unless $req->{'ut_days'};
unless ($req->{'ut_days'} =~ /^\d+$/) {
push @$errors, "Days since last updated must be a postive, whole number.";
return;
}
return 1;
}
sub search_ut
{
my ($dbr, $req, $info) = @_;
my $qdays = $req->{'ut_days'}+0;
if ($qdays == 1) {
push @{$info->{'english'}}, "have updated their journal in the past day";
} else {
push @{$info->{'english'}}, "have updated their journal in the past $qdays days";
}
return {
'tables' => {
'uu' => 'userusage',
},
'conds' => [ "{uu}.timeupdate > DATE_SUB(NOW(), INTERVAL $qdays DAY)", ],
'userid' => "{uu}.userid",
};
}
######### community
sub validate_com
{
my ($req, $errors) = @_;
return 0 unless $req->{'com_do'};
return 1;
}
sub search_com
{
my ($dbr, $req, $info) = @_;
$info->{'allwhat'} = "communities";
return {
'tables' => {
'c' => 'community',
},
'userid' => "{c}.userid",
};
}
1;