package LJ; use strict; # # name: LJ::is_friend # des: Checks to see if a user is a friend of another user. # returns: boolean; 1 if user B is a friend of user A or if A == B # args: usera, userb # des-usera: Source user hashref or userid. # des-userb: Destination user hashref or userid. (can be undef) # sub is_friend { &nodb; my ($ua, $ub) = @_[0, 1]; $ua = LJ::want_userid($ua); $ub = LJ::want_userid($ub); return 0 unless $ua && $ub; return 1 if $ua == $ub; # get group mask from the first argument to the second argument and # see if first bit is set. if it is, they're a friend. get_groupmask # is memcached and used often, so it's likely to be available quickly. return LJ::get_groupmask(@_[0, 1]) & 1; } # # name: LJ::is_banned # des: Checks to see if a user is banned from a journal. # returns: boolean; 1 if "user" is banned from "journal" # args: user, journal # des-user: User hashref or userid. # des-journal: Journal hashref or userid. # sub is_banned { &nodb; # get user and journal ids my $uid = LJ::want_userid(shift); my $jid = LJ::want_userid(shift); return 1 unless $uid && $jid; # for speed: common case is non-community posting and replies # in own journal. avoid db hit. return 0 if ($uid == $jid); # edge from journal -> user return LJ::check_rel($jid, $uid, 'B'); } sub get_groupmask { # TAG:FR:ljlib:get_groupmask my ($journal, $remote) = @_; return 0 unless $journal && $remote; my $jid = LJ::want_userid($journal); my $fid = LJ::want_userid($remote); return 0 unless $jid && $fid; my $memkey = [$jid,"frgmask:$jid:$fid"]; my $mask = LJ::MemCache::get($memkey); unless (defined $mask) { my $dbr = LJ::get_db_reader(); die "No database reader available" unless $dbr; $mask = $dbr->selectrow_array("SELECT groupmask FROM friends ". "WHERE userid=? AND friendid=?", undef, $jid, $fid); LJ::MemCache::set($memkey, $mask+0, time()+60*15); } return $mask+0; # force it to a numeric scalar } # # name: LJ::get_reluser_id # des: for [dbtable[reluser2]], numbers 1 - 31999 are reserved for # livejournal stuff, whereas numbers 32000-65535 are used for local sites. # info: If you wish to add your own hooks to this, you should define a # hook "get_reluser_id" in ljlib-local.pl. No reluser2 [special[reluserdefs]] # types can be a single character, those are reserved for # the [dbtable[reluser]] table, so we don't have namespace problems. # args: type # des-type: the name of the type you're trying to access, e.g. "hide_comm_assoc" # returns: id of type, 0 means it's not a reluser2 type # sub get_reluser_id { my $type = shift; return 0 if length $type == 1; # must be more than a single character my $val = { 'hide_comm_assoc' => 1, }->{$type}+0; return $val if $val; return 0 unless $type =~ /^local-/; return LJ::run_hook('get_reluser_id', $type)+0; } # # name: LJ::load_rel_user # des: Load user relationship information. Loads all relationships of type 'type' in # which user 'userid' participates on the left side (is the source of the # relationship). # args: db?, userid, type # des-userid: userid or a user hash to load relationship information for. # des-type: type of the relationship # returns: reference to an array of userids # sub load_rel_user { my $db = isdb($_[0]) ? shift : undef; my ($userid, $type) = @_; return undef unless $type and $userid; my $u = LJ::want_user($userid); $userid = LJ::want_userid($userid); my $typeid = LJ::get_reluser_id($type)+0; if ($typeid) { # clustered reluser2 table $db = LJ::get_cluster_reader($u); return $db->selectcol_arrayref("SELECT targetid FROM reluser2 WHERE userid=? AND type=?", undef, $userid, $typeid); } else { # non-clustered reluser global table $db ||= LJ::get_db_reader(); return $db->selectcol_arrayref("SELECT targetid FROM reluser WHERE userid=? AND type=?", undef, $userid, $type); } } # # name: LJ::load_rel_user_cache # des: Loads user relationship information of the type 'type' where user # 'targetid' participates on the left side (is the source of the relationship) # trying memcache first. The results from this sub should be # treated as inaccurate and out of date. # args: userid, type # des-userid: userid or a user hash to load relationship information for. # des-type: type of the relationship # returns: reference to an array of userids # sub load_rel_user_cache { my ($userid, $type) = @_; return undef unless $type && $userid; my $u = LJ::want_user($userid); return undef unless $u; $userid = $u->{'userid'}; my $key = [ $userid, "reluser:$userid:$type" ]; my $res = LJ::MemCache::get($key); return $res if $res; $res = LJ::load_rel_user($userid, $type); my $exp = time() + 60*30; # 30 min LJ::MemCache::set($key, $res, $exp); return $res; } # # name: LJ::load_rel_target # des: Load user relationship information. Loads all relationships of type 'type' in # which user 'targetid' participates on the right side (is the target of the # relationship). # args: db?, targetid, type # des-targetid: userid or a user hash to load relationship information for. # des-type: type of the relationship # returns: reference to an array of userids # sub load_rel_target { my $db = isdb($_[0]) ? shift : undef; my ($targetid, $type) = @_; return undef unless $type and $targetid; my $u = LJ::want_user($targetid); $targetid = LJ::want_userid($targetid); my $typeid = LJ::get_reluser_id($type)+0; if ($typeid) { # clustered reluser2 table $db = LJ::get_cluster_reader($u); return $db->selectcol_arrayref("SELECT userid FROM reluser2 WHERE targetid=? AND type=?", undef, $targetid, $typeid); } else { # non-clustered reluser global table $db ||= LJ::get_db_reader(); return $db->selectcol_arrayref("SELECT userid FROM reluser WHERE targetid=? AND type=?", undef, $targetid, $type); } } # # name: LJ::_get_rel_memcache # des: Helper function: returns memcached value for a given (userid, targetid, type) triple, if valid. # args: userid, targetid, type # des-userid: source userid, nonzero # des-targetid: target userid, nonzero # des-type: type (reluser) or typeid (rel2) of the relationship # returns: undef on failure, 0 or 1 depending on edge existence # sub _get_rel_memcache { return undef unless @LJ::MEMCACHE_SERVERS; return undef if $LJ::DISABLED{memcache_reluser}; my ($userid, $targetid, $type) = @_; return undef unless $userid && $targetid && defined $type; # memcache keys my $relkey = [$userid, "rel:$userid:$targetid:$type"]; # rel $uid->$targetid edge my $modukey = [$userid, "relmodu:$userid:$type" ]; # rel modtime for uid my $modtkey = [$targetid, "relmodt:$targetid:$type" ]; # rel modtime for targetid # do a get_multi since $relkey and $modukey are both hashed on $userid my $memc = LJ::MemCache::get_multi($relkey, $modukey); return undef unless $memc && ref $memc eq 'HASH'; # [{0|1}, modtime] my $rel = $memc->{$relkey->[1]}; return undef unless $rel && ref $rel eq 'ARRAY'; # check rel modtime for $userid my $relmodu = $memc->{$modukey->[1]}; return undef if ! $relmodu || $relmodu > $rel->[1]; # check rel modtime for $targetid my $relmodt = LJ::MemCache::get($modtkey); return undef if ! $relmodt || $relmodt > $rel->[1]; # return memcache value if it's up-to-date return $rel->[0] ? 1 : 0; } # # name: LJ::_set_rel_memcache # des: Helper function: sets memcache values for a given (userid, targetid, type) triple # args: userid, targetid, type # des-userid: source userid, nonzero # des-targetid: target userid, nonzero # des-type: type (reluser) or typeid (rel2) of the relationship # returns: 1 on success, undef on failure # sub _set_rel_memcache { return 1 unless @LJ::MEMCACHE_SERVERS; my ($userid, $targetid, $type, $val) = @_; return undef unless $userid && $targetid && defined $type; $val = $val ? 1 : 0; # memcache keys my $relkey = [$userid, "rel:$userid:$targetid:$type"]; # rel $uid->$targetid edge my $modukey = [$userid, "relmodu:$userid:$type" ]; # rel modtime for uid my $modtkey = [$targetid, "relmodt:$targetid:$type" ]; # rel modtime for targetid my $now = time(); my $exp = $now + 3600*6; # 6 hour LJ::MemCache::set($relkey, [$val, $now], $exp); LJ::MemCache::set($modukey, $now, $exp); LJ::MemCache::set($modtkey, $now, $exp); # Also, delete this key, since the contents have changed. LJ::MemCache::delete([$userid, "reluser:$userid:$type"]); return 1; } # # name: LJ::check_rel # des: Checks whether two users are in a specified relationship to each other. # args: db?, userid, targetid, type # des-userid: source userid, nonzero; may also be a user hash. # des-targetid: target userid, nonzero; may also be a user hash. # des-type: type of the relationship # returns: 1 if the relationship exists, 0 otherwise # sub check_rel { my $db = isdb($_[0]) ? shift : undef; my ($userid, $targetid, $type) = @_; return undef unless $type && $userid && $targetid; my $u = LJ::want_user($userid); $userid = LJ::want_userid($userid); $targetid = LJ::want_userid($targetid); my $typeid = LJ::get_reluser_id($type)+0; my $eff_type = $typeid || $type; my $key = "$userid-$targetid-$eff_type"; return $LJ::REQ_CACHE_REL{$key} if defined $LJ::REQ_CACHE_REL{$key}; # did we get something from memcache? my $memval = LJ::_get_rel_memcache($userid, $targetid, $eff_type); return $memval if defined $memval; # are we working on reluser or reluser2? my $table; if ($typeid) { # clustered reluser2 table $db = LJ::get_cluster_reader($u); $table = "reluser2"; } else { # non-clustered reluser table $db ||= LJ::get_db_reader(); $table = "reluser"; } # get data from db, force result to be {0|1} my $dbval = $db->selectrow_array("SELECT COUNT(*) FROM $table ". "WHERE userid=? AND targetid=? AND type=? ", undef, $userid, $targetid, $eff_type) ? 1 : 0; # set in memcache LJ::_set_rel_memcache($userid, $targetid, $eff_type, $dbval); # return and set request cache return $LJ::REQ_CACHE_REL{$key} = $dbval; } # # name: LJ::set_rel # des: Sets relationship information for two users. # args: dbs?, userid, targetid, type # des-dbs: Deprecated; optional, a master/slave set of database handles. # des-userid: source userid, or a user hash # des-targetid: target userid, or a user hash # des-type: type of the relationship # returns: 1 if set succeeded, otherwise undef # sub set_rel { &nodb; my ($userid, $targetid, $type) = @_; return undef unless $type and $userid and $targetid; my $u = LJ::want_user($userid); $userid = LJ::want_userid($userid); $targetid = LJ::want_userid($targetid); my $typeid = LJ::get_reluser_id($type)+0; my $eff_type = $typeid || $type; # working on reluser or reluser2? my ($db, $table); if ($typeid) { # clustered reluser2 table $db = LJ::get_cluster_master($u); $table = "reluser2"; } else { # non-clustered reluser global table $db = LJ::get_db_writer(); $table = "reluser"; } return undef unless $db; # set in database $db->do("REPLACE INTO $table (userid, targetid, type) VALUES (?, ?, ?)", undef, $userid, $targetid, $eff_type); return undef if $db->err; # set in memcache LJ::_set_rel_memcache($userid, $targetid, $eff_type, 1); return 1; } # # name: LJ::set_rel_multi # des: Sets relationship edges for lists of user tuples. # args: edges # des-edges: array of arrayrefs of edges to set: [userid, targetid, type]. # Where: # userid: source userid, or a user hash; # targetid: target userid, or a user hash; # type: type of the relationship. # returns: 1 if all sets succeeded, otherwise undef # sub set_rel_multi { return _mod_rel_multi({ mode => 'set', edges => \@_ }); } # # name: LJ::clear_rel_multi # des: Clear relationship edges for lists of user tuples. # args: edges # des-edges: array of arrayrefs of edges to clear: [userid, targetid, type]. # Where: # userid: source userid, or a user hash; # targetid: target userid, or a user hash; # type: type of the relationship. # returns: 1 if all clears succeeded, otherwise undef # sub clear_rel_multi { return _mod_rel_multi({ mode => 'clear', edges => \@_ }); } # # name: LJ::_mod_rel_multi # des: Sets/Clears relationship edges for lists of user tuples. # args: keys, edges # des-keys: keys: mode => {clear|set}. # des-edges: edges => array of arrayrefs of edges to set: [userid, targetid, type] # Where: # userid: source userid, or a user hash; # targetid: target userid, or a user hash; # type: type of the relationship. # returns: 1 if all updates succeeded, otherwise undef # sub _mod_rel_multi { my $opts = shift; return undef unless @{$opts->{edges}}; my $mode = $opts->{mode} eq 'clear' ? 'clear' : 'set'; my $memval = $mode eq 'set' ? 1 : 0; my @reluser = (); # [userid, targetid, type] my @reluser2 = (); foreach my $edge (@{$opts->{edges}}) { my ($userid, $targetid, $type) = @$edge; $userid = LJ::want_userid($userid); $targetid = LJ::want_userid($targetid); next unless $type && $userid && $targetid; my $typeid = LJ::get_reluser_id($type)+0; my $eff_type = $typeid || $type; # working on reluser or reluser2? push @{$typeid ? \@reluser2 : \@reluser}, [$userid, $targetid, $eff_type]; } # now group reluser2 edges by clusterid my %reluser2 = (); # cid => [userid, targetid, type] my $users = LJ::load_userids(map { $_->[0] } @reluser2); foreach (@reluser2) { my $cid = $users->{$_->[0]}->{clusterid} or next; push @{$reluser2{$cid}}, $_; } @reluser2 = (); # try to get all required cluster masters before we start doing database updates my %cache_dbcm = (); foreach my $cid (keys %reluser2) { next unless @{$reluser2{$cid}}; # return undef immediately if we won't be able to do all the updates $cache_dbcm{$cid} = LJ::get_cluster_master($cid) or return undef; } # if any error occurs with a cluster, we'll skip over that cluster and continue # trying to process others since we've likely already done some amount of db # updates already, but we'll return undef to signify that everything did not # go smoothly my $ret = 1; # do clustered reluser2 updates foreach my $cid (keys %cache_dbcm) { # array of arrayrefs: [userid, targetid, type] my @edges = @{$reluser2{$cid}}; # set in database, then in memcache. keep the two atomic per clusterid my $dbcm = $cache_dbcm{$cid}; my @vals = map { @$_ } @edges; if ($mode eq 'set') { my $bind = join(",", map { "(?,?,?)" } @edges); $dbcm->do("REPLACE INTO reluser2 (userid, targetid, type) VALUES $bind", undef, @vals); } if ($mode eq 'clear') { my $where = join(" OR ", map { "(userid=? AND targetid=? AND type=?)" } @edges); $dbcm->do("DELETE FROM reluser2 WHERE $where", undef, @vals); } # don't update memcache if db update failed for this cluster if ($dbcm->err) { $ret = undef; next; } # updates to this cluster succeeded, set memcache LJ::_set_rel_memcache(@$_, $memval) foreach @edges; } # do global reluser updates if (@reluser) { # nothing to do after this block but return, so we can # immediately return undef from here if there's a problem my $dbh = LJ::get_db_writer() or return undef; my @vals = map { @$_ } @reluser; if ($mode eq 'set') { my $bind = join(",", map { "(?,?,?)" } @reluser); $dbh->do("REPLACE INTO reluser (userid, targetid, type) VALUES $bind", undef, @vals); } if ($mode eq 'clear') { my $where = join(" OR ", map { "userid=? AND targetid=? AND type=?" } @reluser); $dbh->do("DELETE FROM reluser WHERE $where", undef, @vals); } # don't update memcache if db update failed for this cluster return undef if $dbh->err; # $_ = [userid, targetid, type] for each iteration LJ::_set_rel_memcache(@$_, $memval) foreach @reluser; } return $ret; } # # name: LJ::clear_rel # des: Deletes a relationship between two users or all relationships of a particular type # for one user, on either side of the relationship. # info: One of userid,targetid -- bit not both -- may be '*'. In that case, # if, say, userid is '*', then all relationship edges with target equal to # targetid and of the specified type are deleted. # If both userid and targetid are numbers, just one edge is deleted. # args: dbs?, userid, targetid, type # des-dbs: Deprecated; optional, a master/slave set of database handles. # des-userid: source userid, or a user hash, or '*' # des-targetid: target userid, or a user hash, or '*' # des-type: type of the relationship # returns: 1 if clear succeeded, otherwise undef # sub clear_rel { &nodb; my ($userid, $targetid, $type) = @_; return undef if $userid eq '*' and $targetid eq '*'; my $u; $u = LJ::want_user($userid) unless $userid eq '*'; $userid = LJ::want_userid($userid) unless $userid eq '*'; $targetid = LJ::want_userid($targetid) unless $targetid eq '*'; return undef unless $type && $userid && $targetid; my $typeid = LJ::get_reluser_id($type)+0; if ($typeid) { # clustered reluser2 table return undef unless $u->writer; $u->do("DELETE FROM reluser2 WHERE " . ($userid ne '*' ? "userid=$userid AND " : "") . ($targetid ne '*' ? "targetid=$targetid AND " : "") . "type=$typeid"); return undef if $u->err; } else { # non-clustered global reluser table my $dbh = LJ::get_db_writer() or return undef; my $qtype = $dbh->quote($type); $dbh->do("DELETE FROM reluser WHERE " . ($userid ne '*' ? "userid=$userid AND " : "") . ($targetid ne '*' ? "targetid=$targetid AND " : "") . "type=$qtype"); return undef if $dbh->err; } # if one of userid or targetid are '*', then we need to note the modtime # of the reluser edge from the specified id (the one that's not '*') # so that subsequent gets on rel:userid:targetid:type will know to ignore # what they got from memcache my $eff_type = $typeid || $type; if ($userid eq '*') { LJ::MemCache::set([$targetid, "relmodt:$targetid:$eff_type"], time()); } elsif ($targetid eq '*') { LJ::MemCache::set([$userid, "relmodu:$userid:$eff_type"], time()); # if neither userid nor targetid are '*', then just call _set_rel_memcache # to update the rel:userid:targetid:type memcache key as well as the # userid and targetid modtime keys } else { LJ::_set_rel_memcache($userid, $targetid, $eff_type, 0); } return 1; } 1;