package LJ; use Class::Autouse qw ( LJ::EmbedModule ); # # name: LJ::get_posts_raw # des: Gets raw post data (text and props) efficiently from clusters. # info: Fetches posts from clusters, trying memcache and slaves first if available. # returns: hashref with keys 'text', 'prop', and values being hashrefs # with keys "jid:jitemid". values of that are as follows: # text: [ $subject, $body ], props: { ... }; # replycount scalar returned within props. # # args: opts?, id+ # des-opts: An optional hashref of options: # - memcache_only: Don't fall back on the database. # - prop_only: Retrieve only props, for efficiensy. # des-id: An arrayref of [ clusterid, ownerid, itemid ]. # sub get_posts_raw { my $opts = ref $_[0] eq "HASH" ? shift : {}; my $ret = {}; my $sth; LJ::load_props('log'); # throughout this function, the concept of an "id" # is the key to identify a single post. # it is of the form "$jid:$jitemid". # build up a list for each cluster of what we want to get, # as well as a list of all the keys we want from memcache. my %cids; # cid => 1 my $needtext; # text needed: $cid => $id => 1 my $needprop; # props needed: $cid => $id => 1 my $needrc; # replycounts needed: $cid => $id => 1 my @mem_keys; # if we're loading entries for a friends page, # silently failing to load a cluster is acceptable. # but for a single user, we want to die loudly so they don't think # we just lost their journal. my $single_user; # because the memcache keys for logprop don't contain # which cluster they're in, we also need a map to get the # cid back from the jid so we can insert into the needfoo hashes. # the alternative is to not key the needfoo hashes on cluster, # but that means we need to grep out each cluster's jids when # we do per-cluster queries on the databases. my %cidsbyjid; foreach my $post (@_) { my ($cid, $jid, $jitemid) = @{$post}; my $id = "$jid:$jitemid"; #!!! if (not defined $single_user) { $single_user = $jid; } elsif ($single_user and $jid != $single_user) { # multiple users $single_user = 0; } $cids{$cid} = 1; $cidsbyjid{$jid} = $cid; unless ($opts->{prop_only}) { $needtext->{$cid}{$id} = 1; push @mem_keys, [$jid, "logtext:$cid:$id"]; $needrc->{$cid}{$id} = 1; push @mem_keys, [$jid, "rp:$id"]; } $needprop->{$cid}{$id} = 1; push @mem_keys, [$jid, "logprop:$id"]; } # first, check memcache. my $mem = LJ::MemCache::get_multi(@mem_keys) || {}; while (my ($k, $v) = each %$mem) { next unless defined $v; next unless $k =~ /(\w+):(?:\d+:)?(\d+):(\d+)/; my ($type, $jid, $jitemid) = ($1, $2, $3); my $cid = $cidsbyjid{$jid}; my $id = "$jid:$jitemid"; if ($type eq "logtext") { delete $needtext->{$cid}{$id}; $ret->{text}{$id} = $v; } elsif ($type eq "logprop" && ref $v eq "HASH") { delete $needprop->{$cid}{$id}; $ret->{prop}{$id} = $v; } elsif ($type eq "rp") { delete $needrc->{$cid}{$id}; $ret->{replycount}{$id} = $v; } } # we may be done already. return $ret if $opts->{memcache_only}; return $ret unless values %$needtext or values %$needprop or values %$needrc; # otherwise, hit the database. foreach my $cid (keys %cids) { # for each cluster, get the text/props we need from it. my $cneedtext = $needtext->{$cid} || {}; my $cneedprop = $needprop->{$cid} || {}; my $cneedrc = $needrc->{$cid} || {}; next unless %$cneedtext or %$cneedprop or %$cneedrc; my $make_in = sub { my @in; foreach my $id (@_) { my ($jid, $jitemid) = map { $_ + 0 } split(/:/, $id); push @in, "(journalid=$jid AND jitemid=$jitemid)"; } return join(" OR ", @in); }; # now load from each cluster. my $fetchtext = sub { my $db = shift; return unless %$cneedtext; my $in = $make_in->(keys %$cneedtext); $sth = $db->prepare("SELECT journalid, jitemid, subject, event ". "FROM logtext2 WHERE $in"); $sth->execute; while (my ($jid, $jitemid, $subject, $event) = $sth->fetchrow_array) { LJ::text_uncompress(\$event); my $id = "$jid:$jitemid"; my $val = [ $subject, $event ]; $ret->{text}{$id} = $val; LJ::MemCache::add([$jid, "logtext:$cid:$id"], $val, 7200); delete $cneedtext->{$id}; } }; my $fetchprop = sub { my $db = shift; return unless %$cneedprop; my $in = $make_in->(keys %$cneedprop); $sth = $db->prepare("SELECT journalid, jitemid, propid, value ". "FROM logprop2 WHERE $in"); $sth->execute; my %gotid; while (my ($jid, $jitemid, $propid, $value) = $sth->fetchrow_array) { my $id = "$jid:$jitemid"; my $propname = $LJ::CACHE_PROPID{'log'}->{$propid}{name}; $ret->{prop}{$id}{$propname} = $value; $gotid{$id} = 1; } foreach my $id (keys %gotid) { my ($jid, $jitemid) = map { $_ + 0 } split(/:/, $id); LJ::MemCache::add([$jid, "logprop:$id"], $ret->{prop}{$id}); #7200 delete $cneedprop->{$id}; } }; my $fetchrc = sub { my $db = shift; return unless %$cneedrc; my $in = $make_in->(keys %$cneedrc); $sth = $db->prepare("SELECT journalid, jitemid, replycount FROM log2 WHERE $in"); $sth->execute; while (my ($jid, $jitemid, $rc) = $sth->fetchrow_array) { my $id = "$jid:$jitemid"; $ret->{replycount}{$id} = $rc; LJ::MemCache::add([$jid, "rp:$id"], $rc); delete $cneedrc->{$id}; } }; my $dberr = sub { die "Couldn't connect to database" if $single_user; next; }; # run the fetch functions on the proper databases, with fallbacks if necessary. my ($dbcm, $dbcr); if (@LJ::MEMCACHE_SERVERS or $opts->{use_master}) { $dbcm ||= LJ::get_cluster_master($cid) or $dberr->(); $fetchtext->($dbcm) if %$cneedtext; $fetchprop->($dbcm) if %$cneedprop; $fetchrc->($dbcm) if %$cneedrc; } else { $dbcr ||= LJ::get_cluster_reader($cid); if ($dbcr) { $fetchtext->($dbcr) if %$cneedtext; $fetchprop->($dbcr) if %$cneedprop; $fetchrc->($dbcr) if %$cneedrc; } # if we still need some data, switch to the master. if (%$cneedtext or %$cneedprop) { $dbcm ||= LJ::get_cluster_master($cid) or $dberr->(); $fetchtext->($dbcm); $fetchprop->($dbcm); $fetchrc->($dbcm); } } # and finally, if there were no errors, # insert into memcache the absence of props # for all posts that didn't have any props. foreach my $id (keys %$cneedprop) { my ($jid, $jitemid) = map { $_ + 0 } split(/:/, $id); LJ::MemCache::add([$jid, "logprop:$id"], {} ); } } # move replycount into prop : we could not do this before prop are set because prop hash will rewrite replycount... while (my ($k, $v) = each %{$ret->{"replycount"}||{}}) { $ret->{prop}{$k}{replycount} = $ret->{replycount}{$k}; } return $ret; } # # returns a row from log2, trying memcache # accepts $u + $jitemid # returns hash with: posterid, eventtime, logtime, # security, allowmask, journalid, jitemid, anum. sub get_log2_row { my ($u, $jitemid, $db) = @_; my $jid = $u->{'userid'}; my $memkey = [$jid, "log2:$jid:$jitemid"]; my ($row, $item); $row = LJ::MemCache::get($memkey); if ($row) { @$item{'posterid', 'eventtime', 'logtime', 'allowmask', 'ditemid'} = unpack("NNNNN", $row); $item->{'security'} = ($item->{'allowmask'} == 0 ? 'private' : ($item->{'allowmask'} == 2**31 ? 'public' : 'usemask')); $item->{'journalid'} = $jid; @$item{'jitemid', 'anum'} = ($item->{'ditemid'} >> 8, $item->{'ditemid'} % 256); $item->{'eventtime'} = LJ::mysql_time($item->{'eventtime'}, 1); $item->{'logtime'} = LJ::mysql_time($item->{'logtime'}, 1); return $item; } $db = LJ::get_cluster_def_reader($u) unless $db; return undef unless $db; my $sql = "SELECT posterid, eventtime, logtime, security, allowmask, " . "anum FROM log2 WHERE journalid=? AND jitemid=?"; $item = $db->selectrow_hashref($sql, undef, $jid, $jitemid); return undef unless $item; $item->{'journalid'} = $jid; $item->{'jitemid'} = $jitemid; $item->{'ditemid'} = $jitemid*256 + $item->{'anum'}; my ($sec, $eventtime, $logtime); $sec = $item->{'allowmask'}; $sec = 0 if $item->{'security'} eq 'private'; $sec = 2**31 if $item->{'security'} eq 'public'; $eventtime = LJ::mysqldate_to_time($item->{'eventtime'}, 1); $logtime = LJ::mysqldate_to_time($item->{'logtime'}, 1); $row = pack("NNNNN", $item->{'posterid'}, $eventtime, $logtime, $sec, $item->{'ditemid'}); LJ::MemCache::set($memkey, $row); return $item; } # local function. # Get 8 weeks worth of recent items, in rlogtime order, using memcache. # accepts ($jid, $clusterid, $timeupdate, $notafter, $notbefore). # - $notafter - max value for rlogtime # - $notbefore - min value for rlogtime, optional # - $timeupdate is the timeupdate for this user, as far as the caller knows, # in UNIX time. # returns arrayref of the following: # [$rlogtime, $posterid, $eventtime, $allowmask, $ditemid] sub get_log2_recent_log { my ($jid, $cid, $timeupdate, $notafter, $notbefore) = @_; my $DATAVER = "3"; # 1 char my $memkey = [$jid, "log2lt:$jid"]; my $lockkey = $memkey->[1]; my ($rows, $ret); $rows = LJ::MemCache::get($memkey); $ret = []; my $rows_decode = sub { return 0 unless $rows && substr($rows, 0, 1) eq $DATAVER; my $tu = unpack("N", substr($rows, 1, 4)); # if update time we got from upstream is newer than recorded # here, this data from memcache is unreliable return 0 if $timeupdate > $tu; my $n = (length($rows) - 5 )/20; for (my $i=0; $i<$n; $i++) { #was: pack("NNNNN", $rlogtime, $posterid, $eventtime, $allowmask, $ditemid); my @item = unpack("NNNNN", substr($rows, $i*20+5, 20)); next if $notbefore and $item[0] < $notbefore; # rlogtime last if $notafter and $item[0] > $notafter; # rlogtime push @$ret, \@item; } return 1; }; return $ret if $rows_decode->(); #clear otherwise: LJ::MemCache::delete($memkey) if $rows; $rows = ""; my $db = LJ::get_cluster_def_reader($cid); # if we use slave or didn't get some data, don't store in memcache my $dont_store = 0; unless ($db) { $db = LJ::get_cluster_reader($cid); $dont_store = 1; return undef unless $db; } # get reliable log2lt data from the db my $max_age = $LJ::MAX_FRIENDS_VIEW_AGE || 3600*24*56; # 8 weeks default my $sql = "SELECT rlogtime, posterid, eventtime, jitemid, " . "security, allowmask, anum, replycount FROM log2 " . "USE INDEX (rlogtime) WHERE journalid=? AND " . "rlogtime <= ($LJ::EndOfTime - UNIX_TIMESTAMP()) + $max_age"; my $sth = $db->prepare($sql); $sth->execute($jid); my @row; while (my @arr = $sth->fetchrow_array) { push @row, \@arr; } @row = sort { $a->[0] <=> $b->[0] } @row; #rlogtime my $i = 0; foreach (@row) { my ($rlogtime, $posterid, $eventtime, $jitemid, $security, $allowmask, $anum, $replycount) = @$_; #in $sql order! $eventtime = LJ::mysqldate_to_time($eventtime, 1); $allowmask = 0 if $security eq 'private'; $allowmask = 2**31 if $security eq 'public'; my $ditemid = $jitemid*256 + $anum; $rows .= pack("NNNNN", $rlogtime, $posterid, $eventtime, $allowmask, $ditemid); unless (($notafter and $rlogtime > $notafter) || ($notbefore and $rlogtime < $notbefore)) { push @$ret, [$rlogtime, $posterid, $eventtime, $allowmask, $ditemid]; } if ($i++ < 50) { LJ::MemCache::add([$jid, "rp:$jid:$jitemid"], $replycount); } if ($i > $LJ::MAX_SCROLLBACK_FRIENDS_SINGLE_USER_ACTIVITY) { last; #limit log2lt: to a reasonable size } } $rows = $DATAVER . pack("N", $timeupdate) . $rows; LJ::MemCache::add($memkey, $rows, $timeupdate + $max_age) unless $dont_store; return $ret; } # public function, called from get_friend_items() sub get_log2_recent_user { my $opts = shift; my $ret = []; my $journalid = $opts->{'userid'}; my $clusterid = $opts->{'clusterid'}; my $log = LJ::get_log2_recent_log($journalid, $clusterid, $opts->{'timeupdate'}, $opts->{'notafter'}, $opts->{'notbefore'}); my $left = $opts->{'itemshow'}; my $remote = $opts->{'remote'}; my $remoteid = $remote->{'userid'}; my $valid_remote_journaltype = $remote->{'journaltype'} eq "P" || $remote->{'journaltype'} eq "I"; my $mask; foreach my $i (@$log) { last unless $left; ## filter security and provide proper format for the caller: my ($rlogtime, $posterid, $eventtime, $allowmask, $ditemid) = @$i; my $security = $allowmask == 0 ? 'private' : ($allowmask == 2**31 ? 'public' : 'usemask'); next unless $remote || $security eq 'public'; next if $security eq 'private' and $journalid != $remoteid; if ($security eq 'usemask') { next unless $valid_remote_journaltype; my $permit = ($journalid == $remoteid); unless ($permit) { $mask = LJ::get_groupmask($journalid, $remoteid) unless defined $mask; $permit = $allowmask+0 & $mask+0; } next unless $permit; } my ($jitemid, $anum) = ($ditemid >> 8, $ditemid % 256); push @$ret, [$rlogtime, $jitemid, $posterid, $eventtime, $anum, $ditemid, $security, $journalid]; $left--; } return $ret; } # called from go.bml sub get_itemid_near2 { my ($u, $ditemid, $direction) = @_; $ditemid += 0; my $jitemid = $ditemid >> 8; my ($inc, $order); if ($direction eq "next") { ($inc, $order) = (1, "DESC"); } elsif ($direction eq "prev") { ($inc, $order) = (-1, "ASC"); } else { return 0; } my $dbr = LJ::get_cluster_reader($u); my $jid = $u->{'userid'}+0; # $remote nujen dlia svjaznosti ssylok. my $remote = LJ::get_remote(); my $mask; my $item; while (1) { $jitemid += $inc; # TODO: try get_log2_row() #($anum, $security, $allowmask) = $dbr->selectrow_array( # "SELECT anum, security, allowmask ". # " FROM log2 WHERE journalid=$jid AND jitemid=$jitemid"); $item = get_log2_row($u, $jitemid, $dbr); last unless $item; my $security = $item->{'security'}; # usually exits from the first try, unless security: last if $security eq 'public'; last if $security eq 'private' && $remote && $jid == $remote->{'userid'}; if ($security eq 'usemask' && $remote) { next unless $remote->{'journaltype'} eq "P" || $remote->{'journaltype'} eq "I"; my $permit = ($jid == $remote->{'userid'}); unless ($permit) { $mask = LJ::get_groupmask($jid, $remote->{'userid'}) unless defined $mask; $permit = $item->{'allowmask'} & $mask+0; } last if $permit; } next; } return $item->{'jitemid'}*256 + $item->{'anum'} if ($item); #return 0; # deleted posts can be a problem # old, unused code, use as fallback $jitemid = $ditemid >> 8; my $field = $u->{'journaltype'} eq "P" ? "revttime" : "rlogtime"; my $stime = $dbr->selectrow_array("SELECT $field FROM log2 WHERE ". "journalid=$jid AND jitemid=$jitemid"); return 0 unless $stime; my $day = 86400; foreach my $distance ($day, $day*7, $day*30, $day*90) { my ($one_away, $further) = ($stime - $inc, $stime - $inc*$distance); if ($further < $one_away) { # swap them, BETWEEN needs lower number first ($one_away, $further) = ($further, $one_away); } my ($id, $anum) = $dbr->selectrow_array("SELECT jitemid, anum FROM log2 WHERE journalid=$jid ". "AND $field BETWEEN $one_away AND $further ". "ORDER BY $field $order LIMIT 1"); if ($id) { return wantarray() ? ($id, $anum) : ($id*256 + $anum); } } return 0; } sub set_logprop { my ($u, $jitemid, $hashref, $logprops) = @_; # hashref to set, hashref of what was done $jitemid += 0; my $uid = $u->{'userid'} + 0; my $kill_mem = 0; my $del_ids; my $ins_values; while (my ($k, $v) = each %{$hashref||{}}) { my $prop = LJ::get_prop("log", $k); next unless $prop; $kill_mem = 1; if ($v) { $ins_values .= "," if $ins_values; $ins_values .= "($uid, $jitemid, $prop->{'id'}, " . $u->quote($v) . ")"; $logprops->{$k} = $v; } else { $del_ids .= "," if $del_ids; $del_ids .= $prop->{'id'}; } } $u->do("REPLACE INTO logprop2 (journalid, jitemid, propid, value) ". "VALUES $ins_values") if $ins_values; $u->do("DELETE FROM logprop2 WHERE journalid=? AND jitemid=? ". "AND propid IN ($del_ids)", undef, $u->{'userid'}, $jitemid) if $del_ids; LJ::MemCache::delete([$uid,"logprop:$uid:$jitemid"]) if $kill_mem; } # # name: LJ::load_log_props2 # class: # des: # info: # args: db?, uuserid, listref, hashref # des-: # returns: # sub load_log_props2 { my $db = isdb($_[0]) ? shift @_ : undef; my ($uuserid, $listref, $hashref) = @_; my $userid = want_userid($uuserid); return unless ref $hashref eq "HASH"; my %needprops; my %needrc; my %rc; my @memkeys; foreach (@$listref) { my $id = $_+0; $needprops{$id} = 1; $needrc{$id} = 1; push @memkeys, [$userid, "logprop:$userid:$id"]; push @memkeys, [$userid, "rp:$userid:$id"]; } return unless %needprops || %needrc; my $mem = LJ::MemCache::get_multi(@memkeys) || {}; while (my ($k, $v) = each %$mem) { next unless $k =~ /(\w+):(\d+):(\d+)/; if ($1 eq 'logprop') { next unless ref $v eq "HASH"; delete $needprops{$3}; $hashref->{$3} = $v; } if ($1 eq 'rp') { delete $needrc{$3}; $rc{$3} = $v; } } foreach (keys %rc) { $hashref->{$_}{'replycount'} = $rc{$_}; } return unless %needprops || %needrc; unless ($db) { my $u = LJ::load_userid($userid); $db = @LJ::MEMCACHE_SERVERS ? LJ::get_cluster_def_reader($u) : LJ::get_cluster_reader($u); return unless $db; } if (%needprops) { LJ::load_props("log"); my $in = join(",", keys %needprops); my $sth = $db->prepare("SELECT jitemid, propid, value FROM logprop2 ". "WHERE journalid=? AND jitemid IN ($in)"); $sth->execute($userid); while (my ($jitemid, $propid, $value) = $sth->fetchrow_array) { $hashref->{$jitemid}->{$LJ::CACHE_PROPID{'log'}->{$propid}->{'name'}} = $value; } foreach my $id (keys %needprops) { LJ::MemCache::add([$userid,"logprop:$userid:$id"], $hashref->{$id} || {}); #7200 } } if (%needrc) { my $in = join(",", keys %needrc); my $sth = $db->prepare("SELECT jitemid, replycount FROM log2 WHERE journalid=? AND jitemid IN ($in)"); $sth->execute($userid); while (my ($jitemid, $rc) = $sth->fetchrow_array) { $hashref->{$jitemid}->{'replycount'} = $rc; LJ::MemCache::add([$userid, "rp:$userid:$jitemid"], $rc); } } } # # name: LJ::delete_entry # des: Deletes a user's journal entry # args: uuserid, jitemid, quick?, anum? # des-uuserid: Journal itemid or $u object of journal to delete entry from # des-jitemid: Journal itemid of item to delete. # des-quick: Optional boolean. If set, only [dbtable[log2]] table # is deleted from and the rest of the content is deleted # later using [func[LJ::cmd_buffer_add]]. # des-anum: The log item's anum, which'll be needed to delete lazily # some data in tables which includes the anum, but the # log row will already be gone so we'll need to store it for later. # returns: boolean; 1 on success, 0 on failure. # sub delete_entry { my ($uuserid, $jitemid, $quick, $anum) = @_; my $jid = LJ::want_userid($uuserid); my $u = ref $uuserid ? $uuserid : LJ::load_userid($jid); $jitemid += 0; my $and = ""; if (defined $anum) { $and = "AND anum=" . ($anum+0); } my $dc = $u->log2_do(undef, "DELETE FROM log2 WHERE journalid=$jid AND jitemid=$jitemid $and"); return 0 unless $dc; LJ::MemCache::delete([$jid, "log2:$jid:$jitemid"]); LJ::MemCache::decr([$jid, "log2ct:$jid"]); LJ::memcache_kill($jid, "dayct"); # delete tags LJ::Tags::delete_logtags($u, $jitemid); # if this is running the second time (started by the cmd buffer), # the log2 row will already be gone and we shouldn't check for it. if ($quick) { return 1 if $dc < 1; # already deleted? return LJ::cmd_buffer_add($u->{clusterid}, $jid, "delitem", { 'itemid' => $jitemid, 'anum' => $anum, }); } # delete from clusters foreach my $t (qw(logtext2 logprop2 logsec2)) { $u->do("DELETE FROM $t WHERE journalid=$jid AND jitemid=$jitemid"); } $u->dudata_set('L', $jitemid, 0); # delete all comments LJ::Talk::delete_all_comments($u, 'L', $jitemid); # clean unused cache - LJ::MemCache::delete([$jid, "logtext:$u->{clusterid}:$jid:$jitemid"]); LJ::MemCache::delete([$jid, "logprop:$jid:$jitemid"]); LJ::MemCache::delete([$jid, "rss:$jid"]); return 1; } # # name: LJ::mark_entry_as_spam # class: web # des: Copies an entry in a community into the global spamreports table # args: journalu, jitemid # des-journalu: User object of journal (community) entry was posted in. # des-jitemid: ID of this entry. # returns: 1 for success, 0 for failure # sub mark_entry_as_spam { my ($journalu, $jitemid) = @_; $journalu = LJ::want_user($journalu); $jitemid += 0; return 0 unless $journalu && $jitemid; my $dbcr = LJ::get_cluster_def_reader($journalu); my $dbh = LJ::get_db_writer(); return 0 unless $dbcr && $dbh; my $item = LJ::get_log2_row($journalu, $jitemid); return 0 unless $item; # step 1: get info we need my $logtext = LJ::get_logtext2($journalu, $jitemid); my ($subject, $body, $posterid) = ($logtext->{$jitemid}[0], $logtext->{$jitemid}[1], $item->{posterid}); return 0 unless $body; # step 2: insert into spamreports $dbh->do('INSERT INTO spamreports (reporttime, posttime, journalid, posterid, subject, body, report_type) ' . 'VALUES (UNIX_TIMESTAMP(), UNIX_TIMESTAMP(?), ?, ?, ?, ?, \'entry\')', undef, $item->{logtime}, $journalu->{userid}, $posterid, $subject, $body); return 0 if $dbh->err; return 1; } # replycount_do # input: $u, $jitemid, $action, $value # action is one of: "init", "incr", "decr" # $value is amount to incr/decr, 1 by default sub replycount_do { my ($u, $jitemid, $action, $value) = @_; $value = 1 unless defined $value; my $uid = $u->{'userid'}; my $memkey = [$uid, "rp:$uid:$jitemid"]; # "init" is easiest and needs no lock (called before the entry is live) if ($action eq 'init') { LJ::MemCache::set($memkey, 0); return 1; } return 0 unless $action eq 'decr' || $action eq 'incr'; return 0 unless $u->writer; if ($action eq 'decr') { $value = - $value; } $u->do("UPDATE log2 SET replycount=replycount+$value WHERE journalid=$uid AND jitemid=$jitemid"); my $rc = $u->selectrow_array("SELECT replycount FROM log2 WHERE journalid=$uid AND jitemid=$jitemid"); LJ::MemCache::set($memkey, $rc) if defined $rc; LJ::MemCache::delete("/comments/$jitemid/$uid"); LJ::Talk::update_commentalter($u, $jitemid); # timestamp return 1; } # # name: LJ::get_logtext2 # des: Efficiently retrieves a large number of journal entry text, trying first # slave database servers for recent items, then the master in # cases of old items the slaves have already disposed of. See also: # [func[LJ::get_talktext2]]. # args: u, opts?, jitemid* # returns: hashref with keys being jitemids, values being [ $subject, $body ] # des-opts: Optional hashref of special options. Currently only 'usemaster' # key is supported, which always returns a definitive copy, # and not from a cache or slave database. # des-jitemid: List of jitemids to retrieve the subject & text for. # sub get_logtext2 { my $u = shift; my $clusterid = $u->{'clusterid'}; my $journalid = $u->{'userid'}+0; my $opts = ref $_[0] ? shift : {}; # return structure. my $lt = {}; return $lt unless $clusterid; # keep track of itemids we still need to load. my %need; my @mem_keys; foreach (@_) { my $id = $_+0; $need{$id} = 1; push @mem_keys, [$journalid,"logtext:$clusterid:$journalid:$id"]; } # pass 0: memory, avoiding databases unless ($opts->{'usemaster'}) { my $mem = LJ::MemCache::get_multi(@mem_keys) || {}; while (my ($k, $v) = each %$mem) { next unless $v; $k =~ /:(\d+):(\d+):(\d+)/; delete $need{$3}; $lt->{$3} = $v; } } return $lt unless %need; # pass 1 (slave) and pass 2 (master) foreach my $pass (1, 2) { next unless %need; next if $pass == 1 && $opts->{'usemaster'}; my $db = $pass == 1 ? LJ::get_cluster_reader($clusterid) : LJ::get_cluster_def_reader($clusterid); next unless $db; my $jitemid_in = join(", ", keys %need); my $sth = $db->prepare("SELECT jitemid, subject, event FROM logtext2 ". "WHERE journalid=$journalid AND jitemid IN ($jitemid_in)"); $sth->execute; while (my ($id, $subject, $event) = $sth->fetchrow_array) { LJ::text_uncompress(\$event); unless ($opts->{'text-only'}) { LJR::Distributed::sign_imported_entry ($journalid, $id, \$event); } my $val = [ $subject, $event ]; $lt->{$id} = $val; LJ::MemCache::add([$journalid,"logtext:$clusterid:$journalid:$id"], $val, 7200); delete $need{$id}; } } return $lt; } # # name: LJ::load_talk_props2 # des: Retrieves comments properties. # info: # args: # des-: # returns: # sub load_talk_props2 { my ($u, $listref, $hashref) = @_; my $userid = $u->{'userid'}+0; $hashref = {} unless ref $hashref eq "HASH"; my %need; my @memkeys; foreach (@$listref) { my $id = $_+0; $need{$id} = 1; push @memkeys, [$userid,"talkprop:$userid:$id"]; } return $hashref unless %need; my $mem = LJ::MemCache::get_multi(@memkeys) || {}; while (my ($k, $v) = each %$mem) { next unless $k =~ /(\d+):(\d+)/ && ref $v eq "HASH"; delete $need{$2}; $hashref->{$2}->{$_[0]} = $_[1] while @_ = each %$v; } return $hashref unless %need; my $db; if (@LJ::MEMCACHE_SERVERS) { $db = @LJ::MEMCACHE_SERVERS ? LJ::get_cluster_def_reader($u) : LJ::get_cluster_reader($u); return $hashref unless $db; } LJ::load_props("talk"); my $in = join(',', keys %need); my $sth = $db->prepare("SELECT jtalkid, tpropid, value FROM talkprop2 ". "WHERE journalid=? AND jtalkid IN ($in)"); $sth->execute($userid); while (my ($jtalkid, $propid, $value) = $sth->fetchrow_array) { my $p = $LJ::CACHE_PROPID{'talk'}->{$propid}; next unless $p; $hashref->{$jtalkid}->{$p->{'name'}} = $value; } foreach my $id (keys %need) { LJ::MemCache::add([$userid,"talkprop:$userid:$id"], $hashref->{$id} || {}, 3600); } return $hashref; } # # name: LJ::get_talktext2 # des: Retrieves comments text. Tries slave servers first, then master. # info: Efficiently retreives batches of comment text. Will try alternate # servers first. See also [func[LJ::get_logtext2]]. # returns: Hashref with the talkids as keys, values being [ $subject, $event ]. # args: u, opts?, jtalkids # des-opts: A hashref of options. 'onlysubjects' will only retrieve subjects. # des-jtalkids: A list of talkids to get text for. # sub get_talktext2 { my $u = shift; my $clusterid = $u->{'clusterid'}; my $journalid = $u->{'userid'}+0; my $opts = ref $_[0] ? shift : {}; # return structure. my $lt = {}; return $lt unless $clusterid; # keep track of itemids we still need to load. my %need; my @mem_keys; foreach (@_) { my $id = $_+0; push @mem_keys, [$journalid,"talktext:$clusterid:$journalid:$id"]; } # try the memory cache my $mem = LJ::MemCache::get_multi(@mem_keys) || {}; foreach (@_) { my $id = $_+0; my $v = $mem->{"talktext:$clusterid:$journalid:$id"}; if (defined $v) { $lt->{$id} = $v; } else { $need{$id} = 1; } } return $lt unless %need; # pass 1 (slave) and pass 2 (master) foreach my $pass (1, 2) { next unless %need; my $db = $pass == 1 ? LJ::get_cluster_reader($clusterid) : LJ::get_cluster_def_reader($clusterid); next unless $db; my $in = join(",", keys %need); my $sth = $db->prepare("SELECT jtalkid, subject, body FROM talktext2 ". "WHERE journalid=$journalid AND jtalkid IN ($in)"); $sth->execute; while (my ($id, $subject, $body) = $sth->fetchrow_array) { LJ::text_uncompress(\$body); $lt->{$id} = [ $subject, $body ]; LJ::MemCache::add([$journalid,"talktext:$clusterid:$journalid:$id"], [$subject, $body], 3600); delete $need{$id}; } } return $lt; } # # name: LJ::item_link # class: component # des: Returns URL to view an individual journal item. # info: The returned URL may have an ampersand in it. In an HTML/XML attribute, # these must first be escaped by, say, [func[LJ::ehtml]]. This # function doesn't return it pre-escaped because the caller may # use it in, say, a plain-text email message. # args: u, itemid, anum? # des-itemid: Itemid of entry to link to. # des-anum: If present, $u is assumed to be on a cluster and itemid is assumed # to not be a $ditemid already, and the $itemid will be turned into one # by multiplying by 256 and adding $anum. # returns: scalar; unescaped URL string # sub item_link { my ($u, $itemid, $anum, @args) = @_; my $ditemid = $itemid*256 + $anum; # XXX: should have an option of returning a url with escaped (&) # or non-escaped (&) arguments. a new link object would be best. my $args = @args ? "?" . join("&", @args) : ""; return LJ::journal_base($u) . "/$ditemid.html$args"; } # # name: LJ::expand_embedded # class: # des: # info: # args: # des-: # returns: # sub expand_embedded { &nodb; my ($u, $ditemid, $remote, $eventref, %opts) = @_; LJ::Poll::show_polls($ditemid, $remote, $eventref); LJ::EmbedModule->expand_entry($u, $eventref, %opts); LJ::run_hooks("expand_embedded", $u, $ditemid, $remote, $eventref, %opts); } # # name: LJ::item_toutf8 # des: convert one item's subject, text and props to UTF8. # item can be an entry or a comment (in which cases props can be # left empty, since there are no 8bit talkprops). # args: u, subject, text, props # des-u: user hashref of the journal's owner # des-subject: ref to the item's subject # des-text: ref to the item's text # des-props: hashref of the item's props # returns: nothing. # sub item_toutf8 { my ($u, $subject, $text, $props) = @_; return unless $LJ::UNICODE; my $convert = sub { my $rtext = shift; my $error = 0; my $res = LJ::text_convert($$rtext, $u, \$error); if ($error) { LJ::text_out($rtext); } else { $$rtext = $res; }; return; }; $convert->($subject); $convert->($text); foreach(keys %$props) { $convert->(\$props->{$_}); } return; } # Called from get_friend_items, get_recent_items, get_journal_item, few others. # # A single place to pickup text, props, tags, convert utf-8, and fill items' properies. # $items: arrayref; # $u: user object, or hashref of user objects by uid (= multiowner); # $opts fields: multiowner, only_subject, props_only. # # TODO: should be optimized / rewtitten from scratch. # # Usage: fill_items_with_text_props(\@items, $u); # fill_items_with_text_props(\@friend_items, $opts->{'friends_u'}, {'multiowner' => 1}); # sub fill_items_with_text_props { my ($items, $u, $opts) = @_; if ($opts->{'multiowner'}) { # $u - hashref of user objects by ownerid # required fields: ownerid, itemid my @ids; foreach (@$items) { push @ids, [ $u->{$_->{'ownerid'}}->{'clusterid'}, $_->{'ownerid'}, $_->{'itemid'} ]; } # load the text and props of the entries my $res = LJ::get_posts_raw({}, @ids); #key {text or prop}{"$ownerid:$itemid"} # load tags my $tags; $tags = LJ::Tags::get_logtagsmulti(\@ids); #key "$ownerid:$itemid" foreach (@$items) { $_->{'text'} = $res->{text}{"$_->{'ownerid'}:$_->{'itemid'}"}; $_->{'props'} = $res->{prop}{"$_->{'ownerid'}:$_->{'itemid'}"}; if ($LJ::UNICODE && $_->{'props'}->{'unknown8bit'}) { # artefact, very very small fraction of old items affected LJ::item_toutf8($u->{$_->{'ownerid'}}, \$_->{'text'}->[0], \$_->{'text'}->[1], $_->{'props'}); ###$_->{'props'}->{'unknown8bit'} = 0; #no change: memcache logtext,logprop always in sync with db print STDERR "Fixing item_toutf8 in friend_items $_->{'ownerid'} $_->{'itemid'} \n"; } if ($tags) { # $taglist = [ split(/\s*,\s*/, $_->{'props'}->{taglist}) ]; #unknown8bit ? edittags? my @taglist = values %{$tags->{"$_->{'ownerid'}:$_->{'itemid'}"}}; #$kwid => $kw @taglist = sort { $a cmp $b } @taglist; $_->{'props'}->{'tags'} = \@taglist; } } } else { #single owner: $u - user object # required fields: itemid my @ids; foreach (@$items) { push @ids, [ $u->{'clusterid'}, $u->{'userid'}, $_->{'itemid'} ]; } # load the text and props of the entries my $res; if ($opts->{'props_only'}) { $res = LJ::get_posts_raw({'prop_only' => 1}, @ids); } else { $res = LJ::get_posts_raw({}, @ids); } # load tags my $tags; $tags = LJ::Tags::get_logtagsmulti(\@ids) unless $opts->{'only_subject'} || $opts->{'props_only'}; foreach (@$items) { $_->{'text'} = $res->{text}{"$u->{'userid'}:$_->{'itemid'}"}; $_->{'props'} = $res->{prop}{"$u->{'userid'}:$_->{'itemid'}"}; if ($LJ::UNICODE && $_->{'props'}->{'unknown8bit'} && $logtext) { # artefact, very very small fraction of old items affected LJ::item_toutf8($u, \$_->{'text'}->[0], \$_->{'text'}->[1], $_->{'props'}); ###$_->{'props'}->{'unknown8bit'} = 0; #no change: memcache logtext,logprop always in sync with db print STDERR "Fixing item_toutf8 in recent_items $u->{'userid'} $_->{'itemid'} \n"; } if ($tags) { # $taglist = [ split(/\s*,\s*/, $_->{'props'}->{taglist}) ]; my @taglist = values %{$tags->{"$u->{'userid'}:$_->{'itemid'}"}}; #$kwid => $kw @taglist = sort { $a cmp $b } @taglist; $_->{'props'}->{'tags'} = \@taglist; } } } #foreach (@$items) { #TODO #my $subject = $_->{'text'}->[0]; # see if we have a subject and clean it #if ($subject) { # $subject =~ s/[\r\n]/ /g; # LJ::CleanHTML::clean_subject_all(\$subject); # $_->{'text'}->[0] = $subject; #} #} } 1;