496 lines
16 KiB
Perl
496 lines
16 KiB
Perl
# This code was forked from the LiveJournal project owned and operated
|
|
# by Live Journal, Inc. The code has been modified and expanded by
|
|
# Dreamwidth Studios, LLC. These files were originally licensed under
|
|
# the terms of the license supplied by Live Journal, Inc, which can
|
|
# currently be found at:
|
|
#
|
|
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
|
|
#
|
|
# In accordance with the original license, this code and all its
|
|
# modifications are provided under the GNU General Public License.
|
|
# A copy of that license can be found in the LICENSE file included as
|
|
# part of this distribution.
|
|
|
|
use strict;
|
|
use LJ::Global::Constants;
|
|
|
|
package LJ;
|
|
|
|
# this function takes an intid and returns the associated keyword/intcount.
|
|
sub get_interest {
|
|
my $intid = $_[0];
|
|
return undef unless $intid && $intid =~ /^\d+$/;
|
|
my ( $int, $intcount );
|
|
|
|
my $memkey = [ $intid, "introw:$intid" ];
|
|
my $cached = LJ::MemCache::get($memkey);
|
|
|
|
# memcache row is of form [$intid, $int, $intcount];
|
|
|
|
if ( $cached && ref $cached eq 'ARRAY' ) {
|
|
( $intid, $int, $intcount ) = @$cached;
|
|
}
|
|
else {
|
|
my $dbr = LJ::get_db_reader();
|
|
($int) =
|
|
$dbr->selectrow_array( "SELECT keyword FROM sitekeywords WHERE kwid=?", undef, $intid );
|
|
die $dbr->errstr if $dbr->err;
|
|
($intcount) =
|
|
$dbr->selectrow_array( "SELECT intcount FROM interests WHERE intid=?", undef, $intid );
|
|
die $dbr->errstr if $dbr->err;
|
|
LJ::MemCache::set( $memkey, [ $intid, $int, $intcount ], 3600 * 12 );
|
|
}
|
|
|
|
return wantarray() ? ( $int, $intcount ) : $int;
|
|
}
|
|
|
|
# name: LJ::get_sitekeyword_id
|
|
# des: Get the id for a global keyword.
|
|
# args: keyword, autovivify?
|
|
# des-keyword: A string keyword to get the id of.
|
|
# returns: Returns a kwid into [dbtable[sitekeywords]].
|
|
# If the keyword doesn't exist, it is automatically created for you.
|
|
# des-autovivify: If present and 1, automatically create keyword.
|
|
# If present and 0, do not automatically create the keyword.
|
|
# If not present, default behavior is the old
|
|
# style -- yes, do automatically create the keyword.
|
|
#
|
|
sub get_sitekeyword_id {
|
|
my ( $kw, $autovivify, %opts ) = @_;
|
|
$autovivify = 1 unless defined $autovivify;
|
|
|
|
# setup the keyword for use
|
|
return 0 unless defined $kw && $kw =~ /\S/;
|
|
$kw = LJ::text_trim( $kw, LJ::BMAX_SITEKEYWORD, LJ::CMAX_SITEKEYWORD );
|
|
$kw = LJ::utf8_lc($kw) unless $opts{allowmixedcase};
|
|
|
|
# get the keyword and insert it if necessary
|
|
my $dbr = LJ::get_db_reader();
|
|
my $kwid = $dbr->selectrow_array( "SELECT kwid FROM sitekeywords WHERE keyword=?", undef, $kw );
|
|
$kwid = defined $kwid ? $kwid + 0 : 0;
|
|
if ( $autovivify && !$kwid ) {
|
|
|
|
# create a new keyword
|
|
$kwid = LJ::alloc_global_counter('K');
|
|
return undef unless $kwid;
|
|
|
|
# attempt to insert the keyword
|
|
my $dbh = LJ::get_db_writer();
|
|
my $rv = $dbh->do( "INSERT IGNORE INTO sitekeywords (kwid, keyword) VALUES (?, ?)",
|
|
undef, $kwid, $kw );
|
|
return undef if $dbh->err;
|
|
|
|
# at this point, if $rv is 0, the keyword is already there so try again
|
|
unless ($rv) {
|
|
$kwid = $dbh->selectrow_array( "SELECT kwid FROM sitekeywords WHERE keyword=?", undef,
|
|
$kw );
|
|
$kwid = defined $kwid ? $kwid + 0 : 0;
|
|
return undef if $dbh->err;
|
|
}
|
|
}
|
|
return $kwid;
|
|
}
|
|
|
|
sub interest_string_to_list {
|
|
my $intstr = $_[0];
|
|
return unless defined $intstr;
|
|
|
|
$intstr =~ s/^\s+//; # strip leading space
|
|
$intstr =~ s/\s+$//; # strip trailing space
|
|
$intstr =~ s/\n/,/g; # newlines become commas
|
|
$intstr =~ s/\s+/ /g; # strip duplicate spaces from the interest
|
|
|
|
# final list is ,-sep
|
|
return grep { length } split( /\s*,\s*/, $intstr );
|
|
}
|
|
|
|
# This function takes a list of intids and returns the list of uids
|
|
# for accounts interested in ALL of the given interests.
|
|
#
|
|
# Args: arrayref of intids, hashref of opts
|
|
# Returns: array of uids
|
|
#
|
|
# Valid opts: nousers => 1, nocomms => 1
|
|
#
|
|
sub users_with_all_ints {
|
|
my ( $ints, $opts ) = @_;
|
|
$opts ||= {};
|
|
return unless defined $ints && ref $ints eq 'ARRAY';
|
|
|
|
my @intids = grep /^\d+$/, @$ints; # numeric only
|
|
return unless @intids;
|
|
|
|
my @tables;
|
|
push @tables, 'userinterests' unless $opts->{nousers};
|
|
push @tables, 'comminterests' unless $opts->{nocomms};
|
|
return unless @tables;
|
|
|
|
# allow restricting to user's circle
|
|
my $cids;
|
|
if ( $opts->{circle} && ( my $u = LJ::get_remote() ) ) {
|
|
my @circle = ( $u->circle_userids, $u->member_of_userids, $u->id );
|
|
$cids = join ',', @circle;
|
|
}
|
|
|
|
my $dbr = LJ::get_db_reader();
|
|
my $qs = join ',', map { '?' } @intids;
|
|
my @uids;
|
|
|
|
foreach (@tables) {
|
|
my $q = "SELECT userid FROM $_ WHERE intid IN ($qs)";
|
|
$q .= " AND userid IN ($cids)" if $cids;
|
|
my $uref = $dbr->selectall_arrayref( $q, undef, @intids );
|
|
die $dbr->errstr if $dbr->err;
|
|
|
|
# Count the number of times the uid appears.
|
|
# If it's the same as the number of interests, it has all of them.
|
|
my %ucount;
|
|
$ucount{ $_->[0] }++ foreach @$uref;
|
|
push @uids, grep { $ucount{$_} == scalar @intids } keys %ucount;
|
|
}
|
|
|
|
return @uids;
|
|
}
|
|
|
|
sub validate_interest_list {
|
|
my $interrors = ref $_[0] eq "ARRAY" ? shift : [];
|
|
my @ints = @_;
|
|
|
|
my @valid_ints = ();
|
|
foreach my $int (@ints) {
|
|
$int = lc($int); # FIXME: use utf8?
|
|
$int =~ s/^i like //; # *sigh*
|
|
next unless $int;
|
|
|
|
# Specific interest failures
|
|
my ( $bytes, $chars ) = LJ::text_length($int);
|
|
|
|
my $error_string = '';
|
|
if ( $int =~ /[\<\>]/ ) {
|
|
$int = LJ::ehtml($int);
|
|
$error_string .= '.invalid';
|
|
}
|
|
else {
|
|
$error_string .= '.bytes' if $bytes > LJ::BMAX_SITEKEYWORD;
|
|
$error_string .= '.chars' if $chars > LJ::CMAX_SITEKEYWORD;
|
|
}
|
|
|
|
if ($error_string) {
|
|
$error_string = "error.interest$error_string";
|
|
push @$interrors,
|
|
[
|
|
$error_string,
|
|
{
|
|
int => $int,
|
|
bytes => $bytes,
|
|
bytes_max => LJ::BMAX_SITEKEYWORD,
|
|
chars => $chars,
|
|
chars_max => LJ::CMAX_SITEKEYWORD
|
|
}
|
|
];
|
|
next;
|
|
}
|
|
push @valid_ints, $int;
|
|
}
|
|
return @valid_ints;
|
|
}
|
|
|
|
# end package LJ functions; begin user object methods.
|
|
|
|
package LJ::User;
|
|
|
|
# $opts is optional, with keys:
|
|
# forceids => 1 : don't use memcache for loading the intids
|
|
# forceints => 1 : don't use memcache for loading the interest rows
|
|
# justids => 1 : return arrayref of intids only, not names/counts
|
|
# returns otherwise an arrayref of interest rows, sorted by interest name
|
|
#
|
|
sub get_interests {
|
|
my ( $u, $opts ) = @_;
|
|
$opts ||= {};
|
|
return undef unless LJ::isu($u);
|
|
|
|
# first check request cache inside $u
|
|
if ( my $ints = $u->{_cache_interests} ) {
|
|
return [ map { $_->[0] } @$ints ] if $opts->{justids};
|
|
return $ints;
|
|
}
|
|
|
|
my $uid = $u->userid;
|
|
my $uitable = $u->is_community ? 'comminterests' : 'userinterests';
|
|
|
|
# load the ids
|
|
my $mk_ids = [ $uid, "intids:$uid" ];
|
|
my $ids;
|
|
$ids = LJ::MemCache::get($mk_ids) unless $opts->{forceids};
|
|
unless ( $ids && ref $ids eq "ARRAY" ) {
|
|
$ids = [];
|
|
my $dbh = LJ::get_db_writer();
|
|
my $sth = $dbh->prepare("SELECT intid FROM $uitable WHERE userid=?");
|
|
$sth->execute($uid);
|
|
push @$ids, $_ while ($_) = $sth->fetchrow_array;
|
|
LJ::MemCache::add( $mk_ids, $ids, 3600 * 12 );
|
|
}
|
|
|
|
# FIXME: set a 'justids' $u cache key in this case, then only return that
|
|
# later if 'justids' is requested? probably not worth it.
|
|
return $ids if $opts->{justids};
|
|
|
|
# load interest rows
|
|
my %need;
|
|
$need{$_} = 1 foreach @$ids;
|
|
my @ret;
|
|
|
|
unless ( $opts->{forceints} ) {
|
|
if ( my $mc = LJ::MemCache::get_multi( map { [ $_, "introw:$_" ] } @$ids ) ) {
|
|
while ( my ( $k, $v ) = each %$mc ) {
|
|
next unless $k =~ /^introw:(\d+)/;
|
|
delete $need{$1};
|
|
push @ret, $v;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (%need) {
|
|
my $ids = join( ",", map { $_ + 0 } keys %need );
|
|
my $dbr = LJ::get_db_reader();
|
|
my $ints = $dbr->selectall_hashref(
|
|
"SELECT kwid, keyword FROM sitekeywords " . "WHERE kwid IN ($ids)", 'kwid' );
|
|
my $counts = $dbr->selectall_hashref(
|
|
"SELECT intid, intcount FROM interests " . "WHERE intid IN ($ids)", 'intid' );
|
|
my $memc_store = 0;
|
|
foreach my $intid ( keys %$ints ) {
|
|
my $int = $ints->{$intid}->{keyword};
|
|
my $count = $counts->{$intid}->{intcount} + 0;
|
|
my $aref = [ $intid, $int, $count ];
|
|
|
|
# minimize latency... only store 25 into memcache at a time
|
|
# (too bad we don't have set_multi.... hmmmm)
|
|
if ( $memc_store++ < 25 ) {
|
|
|
|
# if the count is fairly high, keep item in memcache longer,
|
|
# since count's not so important.
|
|
my $expire = $count < 10 ? 3600 * 12 : 3600 * 48;
|
|
LJ::MemCache::add( [ $intid, "introw:$intid" ], $aref, $expire );
|
|
}
|
|
push @ret, $aref;
|
|
}
|
|
}
|
|
|
|
@ret = sort { $a->[1] cmp $b->[1] } @ret;
|
|
return $u->{_cache_interests} = \@ret;
|
|
}
|
|
|
|
sub interest_count {
|
|
my $u = shift;
|
|
return undef unless LJ::isu($u);
|
|
|
|
# FIXME: fall back to SELECT COUNT(*) if not cached already?
|
|
return scalar @{ $u->get_interests( { justids => 1 } ) };
|
|
}
|
|
|
|
sub interest_list {
|
|
my $u = shift;
|
|
return undef unless LJ::isu($u);
|
|
|
|
return map { $_->[1] } @{ $u->get_interests() };
|
|
}
|
|
|
|
sub interest_update {
|
|
my ( $u, %opts ) = @_;
|
|
return undef unless LJ::isu($u);
|
|
my ( $add, $del ) = ( $opts{add}, $opts{del} );
|
|
return 1 unless $add || $del; # nothing to do
|
|
|
|
my $lock;
|
|
unless ( $opts{has_lock} ) {
|
|
while (1) {
|
|
$lock = LJ::locker()->trylock( 'interests:' . $u->userid );
|
|
last if $lock;
|
|
|
|
# pause for 0.0-0.3 seconds to shuffle things up. generally good behavior
|
|
# when you're contending for locks.
|
|
select undef, undef, undef, rand() * 0.3;
|
|
}
|
|
}
|
|
|
|
my %wanted_add = map { $_ => 1 } @$add;
|
|
my %wanted_del = map { $_ => 1 } @$del;
|
|
|
|
my %cur_ids = map { $_ => 1 } @{ $u->get_interests( { justids => 1, forceids => 1 } ) };
|
|
|
|
# track if we made changes to refresh memcache later.
|
|
my $did_mod = 0;
|
|
|
|
# community interests go in a different table than user interests,
|
|
# though the schemas are the same so we can run the same queries on them
|
|
my $uitable = $u->is_community ? 'comminterests' : 'userinterests';
|
|
my $uid = $u->userid;
|
|
my $dbh = LJ::get_db_writer() or die LJ::Lang::ml("error.nodb");
|
|
|
|
my @filtered_del = grep { delete $cur_ids{$_} && !$wanted_add{$_} } @$del;
|
|
if ( $del && @filtered_del ) {
|
|
$did_mod = 1;
|
|
my $intid_in = join ',', map { $dbh->quote($_) } @filtered_del;
|
|
|
|
$dbh->do("DELETE FROM $uitable WHERE userid=$uid AND intid IN ($intid_in)");
|
|
die $dbh->errstr if $dbh->err;
|
|
$dbh->do("UPDATE interests SET intcount=intcount-1 WHERE intid IN ($intid_in)");
|
|
die $dbh->errstr if $dbh->err;
|
|
}
|
|
|
|
my @filtered_add = grep { !$cur_ids{$_}++ && !$wanted_del{$_} } @$add;
|
|
if ( $add && @filtered_add ) {
|
|
|
|
# assume we've already checked maxinterests
|
|
$did_mod = 1;
|
|
my $intid_in = join ',', map { $dbh->quote($_) } @filtered_add;
|
|
my $sqlp = join ',', map { "(?,?)" } @filtered_add;
|
|
$dbh->do( "REPLACE INTO $uitable (userid, intid) VALUES $sqlp",
|
|
undef, map { ( $uid, $_ ) } @filtered_add );
|
|
die $dbh->errstr if $dbh->err;
|
|
|
|
# set a zero intcount for any new ints
|
|
$dbh->do( "INSERT IGNORE INTO interests (intid, intcount) VALUES $sqlp",
|
|
undef, map { ( $_, 0 ) } @filtered_add );
|
|
die $dbh->errstr if $dbh->err;
|
|
|
|
# now do the increment for all ints
|
|
$dbh->do("UPDATE interests SET intcount=intcount+1 WHERE intid IN ($intid_in)");
|
|
die $dbh->errstr if $dbh->err;
|
|
}
|
|
|
|
# do migrations to clean up userinterests vs comminterests conflicts
|
|
# also clears memcache and object cache for intids if needed
|
|
$u->lazy_interests_cleanup($did_mod);
|
|
|
|
return 1;
|
|
}
|
|
|
|
# return hashref with intname => intid
|
|
sub interests {
|
|
my ( $u, $opts ) = @_;
|
|
return undef unless LJ::isu($u);
|
|
delete $opts->{justids} if $opts && ref $opts;
|
|
my $uints = $u->get_interests($opts);
|
|
my %interests;
|
|
|
|
foreach my $int (@$uints) {
|
|
$interests{ $int->[1] } = $int->[0]; # $interests{name} = intid
|
|
}
|
|
|
|
return \%interests;
|
|
}
|
|
|
|
sub lazy_interests_cleanup {
|
|
my ( $u, $expire ) = @_;
|
|
|
|
my $dbh = LJ::get_db_writer();
|
|
|
|
if ( $u->is_community ) {
|
|
$dbh->do( "INSERT IGNORE INTO comminterests SELECT * FROM userinterests WHERE userid=?",
|
|
undef, $u->id );
|
|
$dbh->do( "DELETE FROM userinterests WHERE userid=?", undef, $u->id );
|
|
}
|
|
else {
|
|
$dbh->do( "INSERT IGNORE INTO userinterests SELECT * FROM comminterests WHERE userid=?",
|
|
undef, $u->id );
|
|
$dbh->do( "DELETE FROM comminterests WHERE userid=?", undef, $u->id );
|
|
}
|
|
|
|
# don't expire memcache unless requested
|
|
return 1 unless $expire;
|
|
|
|
LJ::memcache_kill( $u, "intids" );
|
|
$u->{_cache_interests} = undef;
|
|
|
|
return 1;
|
|
}
|
|
|
|
# des: Change a user's interests.
|
|
# des-new: listref of new interests
|
|
# returns: 1 on success, undef on failure
|
|
sub set_interests {
|
|
my ( $u, $new ) = @_;
|
|
|
|
$u = LJ::want_user($u) or return undef;
|
|
|
|
return undef unless ref $new eq 'ARRAY';
|
|
|
|
my $lock;
|
|
while (1) {
|
|
$lock = LJ::locker()->trylock( 'interests:' . $u->userid );
|
|
last if $lock;
|
|
|
|
# pause for 0.0-0.3 seconds to shuffle things up. generally good behavior
|
|
# when you're contending for locks.
|
|
select undef, undef, undef, rand() * 0.3;
|
|
}
|
|
|
|
my $old = $u->interests( { forceids => 1 } );
|
|
my %int_add = ();
|
|
my %int_del = %$old; # assume deleting everything, unless in @$new
|
|
|
|
my @valid_ints = LJ::validate_interest_list(@$new);
|
|
foreach my $int (@valid_ints) {
|
|
$int_add{$int} = 1 unless $old->{$int};
|
|
delete $int_del{$int};
|
|
}
|
|
|
|
### do we have new interests to add?
|
|
my @new_intids = (); ## existing IDs we'll add for this user
|
|
foreach my $int ( keys %int_add ) {
|
|
my $intid = LJ::get_sitekeyword_id($int);
|
|
push @new_intids, $intid if $intid;
|
|
}
|
|
|
|
# Note this does NOT check against maxinterests, do that in the caller.
|
|
$u->interest_update( add => \@new_intids, del => [ values %int_del ], has_lock => 1 );
|
|
|
|
LJ::Hooks::run_hooks( "set_interests", $u, \%int_del, \@new_intids ); # interest => intid
|
|
|
|
return 1;
|
|
}
|
|
|
|
# arguments: hashref of submitted form and list of user's previous intids
|
|
# returns: hashref with number of ints added (or toomany) and deleted
|
|
sub sync_interests {
|
|
my ( $u, $args, @intids ) = @_;
|
|
warn "sync_interests: invalid arguments"
|
|
and return undef
|
|
unless LJ::isu($u)
|
|
and ref $args eq "HASH";
|
|
@intids = grep /^\d+$/, @intids; # numeric
|
|
|
|
my %uint = reverse %{ $u->interests }; # intid => interest
|
|
my $rv = {};
|
|
my ( @todel, @toadd );
|
|
|
|
foreach my $intid (@intids) {
|
|
next unless $intid > 0; # prevent adding zero or negative intid
|
|
push @todel, $intid if $uint{$intid} && !$args->{"int_$intid"};
|
|
push @toadd, $intid if !$uint{$intid} && $args->{"int_$intid"};
|
|
}
|
|
|
|
my $addcount = scalar(@toadd);
|
|
my $delcount = scalar(@todel);
|
|
|
|
if ($addcount) {
|
|
my $intcount = scalar( keys %uint ) + $addcount - $delcount;
|
|
my $maxinterests = $u->count_max_interests;
|
|
if ( $intcount > $maxinterests ) {
|
|
|
|
# let the user know they're over the limit
|
|
$rv->{toomany} = $maxinterests;
|
|
@toadd = (); # deletion still OK
|
|
}
|
|
}
|
|
|
|
$u->interest_update( add => \@toadd, del => \@todel );
|
|
$rv->{added} = $addcount;
|
|
$rv->{deleted} = $delcount;
|
|
|
|
return $rv;
|
|
}
|
|
|
|
1;
|