ljr/livejournal/cgi-bin/ljcmdbuffer.pl

323 lines
10 KiB
Perl
Raw Normal View History

2019-02-05 21:49:12 +00:00
#!/usr/bin/perl
#
use strict;
require "$ENV{LJHOME}/cgi-bin/ljlib.pl";
require "$ENV{LJHOME}/cgi-bin/supportlib.pl";
require "$ENV{LJHOME}/cgi-bin/ljmail.pl";
package LJ::Cmdbuffer;
# built-in commands
%LJ::Cmdbuffer::cmds =
(
# delete journal entries
delitem => {
run => \&LJ::Cmdbuffer::_delitem,
},
# ping weblogs.com with updates? takes a $u argument
weblogscom => {
too_old => 60*60*2, # 2 hours old = qbufferd not running?
once_per_user => 1,
run => \&LJ::Cmdbuffer::_weblogscom,
},
# emails that previously failed to send
send_mail => {
arg_format => 'raw',
run => \&LJ::Cmdbuffer::_send_mail,
},
# notify fotobilder of dirty friends
dirty => {
once_per_user => 1,
kill_mem_size => 50_000, # bytes
kill_job_ct => 250, # calls to LJ::Cmdbuffer::flush
run => \&LJ::Cmdbuffer::_dirty,
},
# send notifications for support requests
support_notify => {
too_old => 60*60*2, # after two hours, notification seems kinda pointless
run => \&LJ::Cmdbuffer::_support_notify,
},
);
# <LJFUNC>
# name: LJ::Cmdbuffer::flush
# des: flush up to 500 rows of a given command type from the cmdbuffer table
# args: dbh, db, cmd, userid?
# des-dbh: master database handle
# des-db: database cluster master
# des-cmd: a command type registered in %LJ::Cmdbuffer::cmds
# des-userid: optional userid to which flush should be constrained
# returns: 1 on success, 0 on failure
# </LJFUNC>
sub LJ::Cmdbuffer::flush
{
my ($dbh, $db, $cmd, $userid) = @_;
return 0 unless $cmd;
my $mode = "run";
if ($cmd =~ s/:(\w+)//) {
$mode = $1;
}
my $code = $LJ::Cmdbuffer::cmds{$cmd} ?
$LJ::Cmdbuffer::cmds{$cmd}->{$mode} : $LJ::HOOKS{"cmdbuf:$cmd:$mode"}->[0];
return 0 unless $code;
# start/finish modes
if ($mode ne "run") {
$code->($dbh);
return 1;
}
# 0 = never too old
my $too_old = LJ::Cmdbuffer::get_property($cmd, 'too_old') || 0;
# 0 == okay to run more than once per user
my $once_per_user = LJ::Cmdbuffer::get_property($cmd, 'once_per_user') || 0;
# 'url' = urlencode, 'raw' = don't urlencode
my $arg_format = LJ::Cmdbuffer::get_property($cmd, 'arg_format') || 'url';
my $clist;
my $loop = 1;
my $where = "cmd=" . $dbh->quote($cmd);
if ($userid) {
$where .= " AND journalid=" . $dbh->quote($userid);
}
my $LIMIT = 500;
while ($loop &&
($clist = $db->selectall_arrayref("SELECT cbid, UNIX_TIMESTAMP() - UNIX_TIMESTAMP(instime), journalid ".
"FROM cmdbuffer ".
"WHERE $where ORDER BY cbid LIMIT $LIMIT")) &&
$clist && @$clist)
{
my @too_old;
my @cbids;
# citem: [ cbid, age, journalid ]
foreach my $citem (@$clist) {
if ($too_old && $citem->[1] > $too_old) {
push @too_old, $citem->[0];
} else {
push @cbids, $citem->[0];
}
}
if (@too_old) {
local $" = ",";
$db->do("DELETE FROM cmdbuffer WHERE cbid IN (@too_old)");
}
foreach my $cbid (@cbids) {
my $got_lock = $db->selectrow_array("SELECT GET_LOCK('cbid-$cbid',10)");
return 0 unless $got_lock;
# sadly, we have to do another query here to verify the job hasn't been
# done by another thread. (otherwise we could've done it above, instead
# of just getting the id)
my $c = $db->selectrow_hashref("SELECT cbid, journalid, cmd, instime, args " .
"FROM cmdbuffer WHERE cbid=?", undef, $cbid);
next unless $c;
if ($arg_format eq "url") {
my $a = {};
LJ::decode_url_string($c->{'args'}, $a);
$c->{'args'} = $a;
}
# otherwise, arg_format eq "raw"
# run handler
$code->($dbh, $db, $c);
# if this task is to be run once per user, go ahead and delete any jobs
# for this user of this type and remove them from the queue
my $wh = "cbid=$cbid";
if ($once_per_user) {
$wh = "cmd=" . $db->quote($cmd) . " AND journalid=" . $db->quote($c->{journalid});
@$clist = grep { $_->[2] != $c->{journalid} } @$clist;
}
$db->do("DELETE FROM cmdbuffer WHERE $wh");
$db->do("SELECT RELEASE_LOCK('cbid-$cbid')");
}
$loop = 0 unless scalar(@$clist) == $LIMIT;
}
return 1;
}
# <LJFUNC>
# name: LJ::Cmdbuffer::get_property
# des: get a property of an async job type, either built-in or site-specific
# args: cmd, prop
# des-cmd: a registered async job type
# des-prop: the property name to look up
# returns: value of property (whatever it may be) on success, undef on failure
# </LJFUNC>
sub get_property {
my ($cmd, $prop) = @_;
return undef unless $cmd && $prop;
if (my $c = $LJ::Cmdbuffer::cmds{$cmd}) {
return $c->{$prop};
}
if (LJ::are_hooks("cmdbuf:$cmd:$prop")) {
return LJ::run_hook("cmdbuf:$cmd:$prop");
}
return undef;
}
sub _delitem {
my ($dbh, $db, $c) = @_;
my $a = $c->{'args'};
return LJ::delete_entry($c->{'journalid'}, $a->{'itemid'},
0, $a->{'anum'});
}
sub _weblogscom {
# user, title, url
my ($dbh, $db, $c) = @_;
my $a = $c->{'args'};
eval {
eval "use XMLRPC::Lite;";
unless ($@) {
XMLRPC::Lite
->new( proxy => "http://rpc.weblogs.com/RPC2",
timeout => 5 )
->call('weblogUpdates.ping', # xml-rpc method call
LJ::ehtml($a->{'title'}) . " \@ $LJ::SITENAMESHORT",
$a->{'url'},
"$LJ::SITEROOT/misc/weblogs-change.bml?user=$a->{'user'}");
}
};
return 1;
}
sub _send_mail {
my ($dbh, $db, $c) = @_;
my $msg = Storable::thaw($c->{'args'});
return LJ::send_mail($msg, "async");
}
sub _dirty {
my ($dbh, $db, $c) = @_;
my $a = $c->{args};
my $what = $a->{what};
if ($what eq 'friends') {
eval {
eval qq{
use RPC::XML;
use RPC::XML::Client;
};
unless ($@) {
my $u = LJ::load_userid($c->{journalid});
my %req = ( user => $u->{user} );
# fill in groups info
LJ::fill_groups_xmlrpc($u, \%req);
my $res = RPC::XML::Client
->new("$LJ::FB_SITEROOT/interface/xmlrpc")
->send_request('FB.XMLRPC.groups_push',
# FIXME: don't be lazy with the smart_encode
# FIXME: log useful errors from outcome
RPC::XML::smart_encode(\%req));
}
};
}
return 1;
}
sub _support_notify {
my ($dbh, $db, $c) = @_;
# load basic stuff common to both paths
my $a = $c->{args};
my $type = $a->{type};
my $spid = $a->{spid}+0;
my $sp = LJ::Support::load_request($spid, $type eq 'new' ? 1 : 0); # 1 means load body
my $dbr = LJ::get_db_reader();
# now branch a bit to select the right user information
my ($select, $level) = $type eq 'new' ?
('u.email', "'new', 'all'") :
('u.email, u.userid, u.user', "'all'");
my $data = $dbr->selectall_arrayref("SELECT $select FROM supportnotify sn, user u " .
"WHERE sn.userid=u.userid AND sn.spcatid=? " .
"AND sn.level IN ($level)", undef, $sp->{_cat}{spcatid});
# prepare the email
my $body;
my @emails;
if ($type eq 'new') {
$body = "A $LJ::SITENAME support request has been submitted regarding the following:\n\n";
$body .= "Category: $sp->{_cat}{catname}\n";
$body .= "Subject: $sp->{subject}\n\n";
$body .= "You can track its progress or add information here:\n\n";
$body .= "$LJ::SITEROOT/support/see_request.bml?id=$spid";
$body .= "\n\nIf you do not wish to receive notifications of incoming support requests, you may change your notification settings here:\n\n";
$body .= "$LJ::SITEROOT/support/changenotify.bml";
$body .= "\n\n" . "="x70 . "\n\n";
$body .= $sp->{body};
# just copy this out
push @emails, $_->[0] foreach @$data;
} elsif ($type eq 'update') {
# load the response we want to stuff in the email
my ($resp, $rtype, $posterid) =
$dbr->selectrow_array("SELECT message, type, userid FROM supportlog WHERE spid = ? AND splid = ?",
undef, $sp->{spid}, $a->{splid}+0);
# build body
$body = "A follow-up to the request regarding \"$sp->{subject}\" has ";
$body .= "been submitted. You can track its progress or add ";
$body .= "information here:\n\n ";
$body .= "$LJ::SITEROOT/support/see_request.bml?id=$spid";
$body .= "\n\n" . "="x70 . "\n\n";
$body .= $resp;
# now see who this should be sent to
foreach my $erow (@$data) {
next if $posterid == $erow->[1];
next if $rtype eq 'screened' &&
!LJ::Support::can_read_screened($sp, LJ::make_remote($erow->[2], $erow->[1]));
next if $rtype eq 'internal' &&
!LJ::Support::can_read_internal($sp, LJ::make_remote($erow->[2], $erow->[1]));
push @emails, $erow->[0];
}
}
# send the email
LJ::send_mail({
bcc => join(', ', @emails),
from => $LJ::BOGUS_EMAIL,
fromname => "$LJ::SITENAME Support",
charset => 'utf-8',
subject => ($type eq 'update' ? 'Re: ' : '') . "Support Request \#$spid",
body => $body,
wrap => 1,
}) if @emails;
return 1;
}
1;