#!/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, }, ); # # 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 # 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; } # # 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 # 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;