#!/usr/bin/perl
#
use strict;
package LJ::Support;
use vars qw(@SUPPORT_PRIVS);
use Digest::MD5 qw(md5_hex);
use lib "$ENV{LJHOME}/cgi-bin";
require "sysban.pl";
# Constants
my $SECONDS_IN_DAY = 3600 * 24;
@SUPPORT_PRIVS = (qw/supportclose
supporthelp
supportdelete
supportread
supportviewinternal
supportmakeinternal
supportmovetouch
supportviewscreened
supportviewstocks
supportchangesummary/);
#
# name: LJ::Support::slow_query_dbh
# des: Retrieve a database handle to be used for support-related
# slow queries... defaults to 'slow' role but can be
# overriden by [ljconfig[support_slow_roles]].
# args: none
# returns: master database handle.
#
sub slow_query_dbh
{
return LJ::get_dbh(@LJ::SUPPORT_SLOW_ROLES);
}
## pass $id of zero or blank to get all categories
sub load_cats
{
my ($id) = @_;
my $hashref = {};
$id += 0;
my $where = $id ? "WHERE spcatid=$id" : "";
my $dbr = LJ::get_db_reader();
my $sth = $dbr->prepare("SELECT * FROM supportcat $where");
$sth->execute;
$hashref->{$_->{'spcatid'}} = $_ while ($_ = $sth->fetchrow_hashref);
return $hashref;
}
sub load_email_to_cat_map
{
my $map = {};
my $dbr = LJ::get_db_reader();
my $sth = $dbr->prepare("SELECT * FROM supportcat ORDER BY sortorder DESC");
$sth->execute;
while (my $sp = $sth->fetchrow_hashref) {
next unless ($sp->{'replyaddress'});
$map->{$sp->{'replyaddress'}} = $sp;
}
return $map;
}
sub calc_points
{
my ($sp, $secs) = @_;
my $base = $sp->{_cat}->{'basepoints'} || 1;
$secs = int($secs / (3600*6));
my $total = ($base + $secs);
if ($total > 10) { $total = 10; }
return $total;
}
sub init_remote
{
my $remote = shift;
return unless $remote;
LJ::load_user_privs($remote, @SUPPORT_PRIVS);
}
sub has_any_support_priv {
my $u = shift;
return 0 unless $u;
foreach my $support_priv (@SUPPORT_PRIVS) {
return 1 if LJ::check_priv($u, $support_priv);
}
return 0;
}
# given all the categories, maps a catkey into a cat
sub get_cat_by_key
{
my ($cats, $cat) = @_;
foreach (keys %$cats) {
if ($cats->{$_}->{'catkey'} eq $cat) {
return $cats->{$_};
}
}
return undef;
}
sub get_cat_by_id
{
my ($cats, $id) = @_;
foreach (keys %$cats) {
if ($cats->{$_}->{'spcatid'} == $id) {
return $cats->{$_};
}
}
return undef;
}
sub filter_cats
{
my $remote = shift;
my $cats = shift;
return grep {
can_read_cat($_, $remote);
} sorted_cats($cats);
}
sub sorted_cats
{
my $cats = shift;
return sort { $a->{'catname'} cmp $b->{'catname'} } values %$cats;
}
# takes raw support request record and puts category info in it
# so it can be used in other functions like can_*
sub fill_request_with_cat
{
my ($sp, $cats) = @_;
$sp->{_cat} = $cats->{$sp->{'spcatid'}};
}
sub is_poster {
my ($sp, $remote, $auth) = @_;
if ($sp->{'reqtype'} eq "user") {
return 1 if $remote && $remote->id == $sp->{'requserid'};
} else {
if ($remote) {
return 1 if lc($remote->email_raw) eq lc($sp->{'reqemail'});
} else {
return 1 if $auth && $auth eq mini_auth($sp);
}
}
return 0;
}
sub can_see_helper
{
my ($sp, $remote) = @_;
if ($sp->{_cat}->{'hide_helpers'}) {
if (can_help($sp, $remote)) {
return 1;
}
if (LJ::check_priv($remote, "supportviewinternal", $sp->{_cat}->{'catkey'})) {
return 1;
}
if (LJ::check_priv($remote, "supportviewscreened", $sp->{_cat}->{'catkey'})) {
return 1;
}
return 0;
}
return 1;
}
sub can_read
{
my ($sp, $remote, $auth) = @_;
return (is_poster($sp, $remote, $auth) ||
can_read_cat($sp->{_cat}, $remote));
}
sub can_read_cat
{
my ($cat, $remote) = @_;
return unless ($cat);
return ($cat->{'public_read'} ||
LJ::check_priv($remote, "supportread", $cat->{'catkey'}));
}
*can_bounce = \&can_close_cat;
*can_lock = \&can_close_cat;
# if they can close in this category
sub can_close_cat
{
my ($sp, $remote) = @_;
return 1 if $sp->{_cat}->{public_read} && LJ::check_priv($remote, 'supportclose', '');
return 1 if LJ::check_priv($remote, 'supportclose', $sp->{_cat}->{catkey});
return 0;
}
# if they can close this particular request
sub can_close
{
my ($sp, $remote, $auth) = @_;
return 1 if $sp->{_cat}->{user_closeable} && is_poster($sp, $remote, $auth);
return can_close_cat($sp, $remote);
}
# if they can reopen a request
sub can_reopen {
my ($sp, $remote, $auth) = @_;
return 1 if is_poster($sp, $remote, $auth);
return can_close_cat($sp, $remote);
}
sub can_append
{
my ($sp, $remote, $auth) = @_;
if (is_poster($sp, $remote, $auth)) { return 1; }
return 0 unless $remote;
return 0 unless $remote->{'statusvis'} eq "V";
if ($sp->{_cat}->{'allow_screened'}) { return 1; }
if (can_help($sp, $remote)) { return 1; }
return 0;
}
sub is_locked
{
my $sp = shift;
my $spid = ref $sp ? $sp->{spid} : $sp+0;
return undef unless $spid;
my $props = LJ::Support::load_props($spid);
return $props->{locked} ? 1 : 0;
}
sub lock
{
my $sp = shift;
my $spid = ref $sp ? $sp->{spid} : $sp+0;
return undef unless $spid;
my $dbh = LJ::get_db_writer();
$dbh->do("REPLACE INTO supportprop (spid, prop, value) VALUES (?, 'locked', 1)", undef, $spid);
}
sub unlock
{
my $sp = shift;
my $spid = ref $sp ? $sp->{spid} : $sp+0;
return undef unless $spid;
my $dbh = LJ::get_db_writer();
$dbh->do("DELETE FROM supportprop WHERE spid = ? AND prop = 'locked'", undef, $spid);
}
# privilege policy:
# supporthelp with no argument gives you all abilities in all public_read categories
# supporthelp with a catkey arg gives you all abilities in that non-public_read category
# supportread with a catkey arg is required to view requests in a non-public_read category
# all other privs work like:
# no argument = global, where category is public_read or user has supportread on that category
# argument = local, priv applies in that category only if it's public or user has supportread
sub support_check_priv
{
my ($sp, $remote, $priv) = @_;
return 1 if can_help($sp, $remote);
return 0 unless can_read_cat($sp->{_cat}, $remote);
return 1 if LJ::check_priv($remote, $priv, '') && $sp->{_cat}->{public_read};
return 1 if LJ::check_priv($remote, $priv, $sp->{_cat}->{catkey});
return 0;
}
# different calling convention than above. this matches LJ::check_priv
sub category_check_priv
{
my ($remote, $priv, $cat) = @_;
my $sp = get_cat_by_key(load_cats(), $cat)
or return 0;
return support_check_priv($sp, $remote, $priv);
}
# can they read internal comments? if they're a helper or have
# extended supportread (with a plus sign at the end of the category key)
sub can_read_internal
{
my ($sp, $remote) = @_;
return 1 if LJ::Support::support_check_priv($sp, $remote, 'supportviewinternal');
return 1 if LJ::check_priv($remote, "supportread", $sp->{_cat}->{catkey}."+");
return 0;
}
sub can_make_internal
{
return LJ::Support::support_check_priv(@_, 'supportmakeinternal');
}
sub can_read_screened
{
return LJ::Support::support_check_priv(@_, 'supportviewscreened');
}
sub can_perform_actions
{
return LJ::Support::support_check_priv(@_, 'supportmovetouch');
}
sub can_change_summary
{
return LJ::Support::support_check_priv(@_, 'supportchangesummary');
}
sub can_see_stocks
{
return LJ::Support::support_check_priv(@_, 'supportviewstocks');
}
sub can_help
{
my ($sp, $remote) = @_;
if ($sp->{_cat}->{'public_read'}) {
if ($sp->{_cat}->{'public_help'}) {
return 1;
}
if (LJ::check_priv($remote, "supporthelp", "")) { return 1; }
}
my $catkey = $sp->{_cat}->{'catkey'};
if (LJ::check_priv($remote, "supporthelp", $catkey)) { return 1; }
return 0;
}
sub load_props
{
my $spid = shift;
return unless $spid;
my %props = (); # prop => value
my $dbr = LJ::get_db_reader();
my $sth = $dbr->prepare("SELECT prop, value FROM supportprop WHERE spid=?");
$sth->execute($spid);
while (my ($prop, $value) = $sth->fetchrow_array) {
$props{$prop} = $value;
}
return \%props;
}
sub prop
{
my ($spid, $propname) = @_;
my $props = LJ::Support::load_props($spid);
return $props->{$propname} || undef;
}
sub set_prop
{
my ($spid, $propname, $propval) = @_;
# TODO:
# -- delete on 'undef' propval
# -- allow setting of multiple
my $dbh = LJ::get_db_writer()
or die "couldn't contact global master";
$dbh->do("REPLACE INTO supportprop (spid, prop, value) VALUES (?,?,?)",
undef, $spid, $propname, $propval);
die $dbh->errstr if $dbh->err;
return 1;
}
# $loadreq is used by /abuse/report.bml and
# ljcmdbuffer.pl to signify that the full request
# should not be loaded. To simplify code going live,
# Whitaker and I decided to not try and merge it
# into the new $opts hash.
# $opts->{'db_force'} loads the request from a
# global master. Needed to prevent a race condition
# where the request may not have replicated to slaves
# in the time needed to load an auth code.
sub load_request
{
my ($spid, $loadreq, $opts) = @_;
my $sth;
$spid += 0;
# load the support request
my $db = $opts->{'db_force'} ? LJ::get_db_writer() : LJ::get_db_reader();
$sth = $db->prepare("SELECT * FROM support WHERE spid=$spid");
$sth->execute;
my $sp = $sth->fetchrow_hashref;
return undef unless $sp;
# load the category the support requst is in
$sth = $db->prepare("SELECT * FROM supportcat WHERE spcatid=$sp->{'spcatid'}");
$sth->execute;
$sp->{_cat} = $sth->fetchrow_hashref;
# now load the user's request text, if necessary
if ($loadreq) {
$sp->{body} = $db->selectrow_array("SELECT message FROM supportlog WHERE spid = ? AND type = 'req'",
undef, $sp->{spid});
}
return $sp;
}
sub load_response
{
my $splid = shift;
my $sth;
$splid += 0;
# load the support request. we hit the master because we generally
# only invoke this when we want the freshest version of the row.
# (ie, approving a response changes its type from screened to
# answer ... then we fetch the row again and make decisions on its type.
# so we want the authoritative version)
my $dbh = LJ::get_db_writer();
$sth = $dbh->prepare("SELECT * FROM supportlog WHERE splid=$splid");
$sth->execute;
my $res = $sth->fetchrow_hashref;
return $res;
}
sub get_answer_types
{
my ($sp, $remote, $auth) = @_;
my @ans_type;
if (is_poster($sp, $remote, $auth)) {
push @ans_type, ("comment", "More information");
return @ans_type;
}
if (can_help($sp, $remote)) {
push @ans_type, ("screened" => "Screened Response",
"answer" => "Answer",
"comment" => "Comment or Question");
} elsif ($sp->{_cat}->{'allow_screened'}) {
push @ans_type, ("screened" => "Screened Response");
}
if (can_make_internal($sp, $remote) &&
! $sp->{_cat}->{'public_help'})
{
push @ans_type, ("internal" => "Internal Comment / Action");
}
if (can_bounce($sp, $remote)) {
push @ans_type, ("bounce" => "Bounce to Email & Close");
}
return @ans_type;
}
sub file_request
{
my $errors = shift;
my $o = shift;
my $email = $o->{'reqtype'} eq "email" ? $o->{'reqemail'} : "";
my $log = { 'uniq' => $o->{'uniq'},
'email' => $email };
my $userid = 0;
unless ($email) {
if ($o->{'reqtype'} eq "user") {
my $u = LJ::load_userid($o->{'requserid'});
$userid = $u->{'userid'};
$log->{'user'} = $u->user;
$log->{'email'} = $u->email_raw;
unless ($u->is_person || $u->is_identity) {
push @$errors, "You cannot submit support requests from non-user accounts.";
}
if (LJ::sysban_check('support_user', $u->{'user'})) {
return LJ::sysban_block($userid, "Support request blocked based on user", $log);
}
$email = $u->email_raw || $o->{'reqemail'};
}
}
if (LJ::sysban_check('support_email', $email)) {
return LJ::sysban_block($userid, "Support request blocked based on email", $log);
}
if (LJ::sysban_check('support_uniq', $o->{'uniq'})) {
return LJ::sysban_block($userid, "Support request blocked based on uniq", $log);
}
my $reqsubject = LJ::trim($o->{'subject'});
my $reqbody = LJ::trim($o->{'body'});
# remove the auth portion of any see_request.bml links
$reqbody =~ s/(see_request\.bml.+?)\&auth=\w+/$1/ig;
unless ($reqsubject) {
push @$errors, "You must enter a problem summary.";
}
unless ($reqbody) {
push @$errors, "You did not enter a support request.";
}
my $cats = LJ::Support::load_cats();
push @$errors, $BML::ML{'error.invalid.support.category'} unless $cats->{$o->{'spcatid'}+0};
if (@$errors) { return 0; }
if (LJ::is_enabled("support_request_language")) {
$o->{'language'} = undef unless grep { $o->{'language'} eq $_ } (@LJ::LANGS, "xx");
$reqsubject = "[$o->{'language'}] $reqsubject" if $o->{'language'} && $o->{'language'} !~ /^en_/;
}
my $dbh = LJ::get_db_writer();
my $dup_id = 0;
my $qsubject = $dbh->quote($reqsubject);
my $qbody = $dbh->quote($reqbody);
my $qreqtype = $dbh->quote($o->{'reqtype'});
my $qrequserid = $o->{'requserid'}+0;
my $qreqname = $dbh->quote($o->{'reqname'});
my $qreqemail = $dbh->quote($o->{'reqemail'});
my $qspcatid = $o->{'spcatid'}+0;
my $scat = $cats->{$qspcatid};
# make the authcode
my $authcode = LJ::make_auth_code(15);
my $qauthcode = $dbh->quote($authcode);
my $md5 = md5_hex("$qreqname$qreqemail$qsubject$qbody");
my $sth;
$dbh->do("LOCK TABLES support WRITE, duplock WRITE");
unless ($o->{ignore_dup_check}) {
$sth = $dbh->prepare("SELECT dupid FROM duplock WHERE realm='support' AND reid=0 AND userid=$qrequserid AND digest='$md5'");
$sth->execute;
($dup_id) = $sth->fetchrow_array;
if ($dup_id) {
$dbh->do("UNLOCK TABLES");
return $dup_id;
}
}
my ($urlauth, $url, $spid); # used at the bottom
my $sql = "INSERT INTO support (spid, reqtype, requserid, reqname, reqemail, state, authcode, spcatid, subject, timecreate, timetouched, timeclosed, timelasthelp) VALUES (NULL, $qreqtype, $qrequserid, $qreqname, $qreqemail, 'open', $qauthcode, $qspcatid, $qsubject, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 0)";
$sth = $dbh->prepare($sql);
$sth->execute;
if ($dbh->err) {
my $error = $dbh->errstr;
$dbh->do("UNLOCK TABLES");
push @$errors, "Database error: (report this)
$error";
return 0;
}
$spid = $dbh->{'mysql_insertid'};
$dbh->do("INSERT INTO duplock (realm, reid, userid, digest, dupid, instime) VALUES ('support', 0, $qrequserid, '$md5', $spid, NOW())")
unless $o->{ignore_dup_check};
$dbh->do("UNLOCK TABLES");
unless ($spid) {
push @$errors, "Database error: (report this)
Didn't get a spid.";
return 0;
}
# save meta-data for this request
my @data;
my $add_data = sub {
my $q = $dbh->quote($_[1]);
return unless $q && $q ne 'NULL';
push @data, "($spid, '$_[0]', $q)";
};
if (LJ::is_enabled("support_request_language") && $o->{language} ne "xx") {
$add_data->($_, $o->{$_}) foreach qw(uniq useragent language);
} else {
$add_data->($_, $o->{$_}) foreach qw(uniq useragent);
}
$dbh->do("INSERT INTO supportprop (spid, prop, value) VALUES " . join(',', @data));
$dbh->do("INSERT INTO supportlog (splid, spid, timelogged, type, faqid, userid, message) ".
"VALUES (NULL, $spid, UNIX_TIMESTAMP(), 'req', 0, $qrequserid, $qbody)");
# set the flag on the support request and the support request on the flag
if ($o->{flagid}) {
LJ::Support::set_prop($spid, "contentflagid", $o->{flagid});
LJ::ContentFlag->set_supportid($o->{flagid}, $spid);
}
my $body;
my $miniauth = mini_auth({ 'authcode' => $authcode });
$url = "$LJ::SITEROOT/support/see_request.bml?id=$spid";
$urlauth = "$url&auth=$miniauth";
$body = "Your $LJ::SITENAME support request regarding \"$o->{'subject'}\" has been filed and will be answered as soon as possible. Your request tracking number is $spid.\n\n";
$body .= "You can track your request's progress or add information here:\n\n ";
$body .= $urlauth . "\n\n";
if ($scat->{user_closeable}) {
$body .= "If you figure out the problem before somebody gets back to you, please cancel your request by clicking this:\n\n ";
$body .= "$LJ::SITEROOT/support/act.bml?close;$spid;$authcode";
}
# disable auto-replies for the entire category, or per request
unless ($scat->{'no_autoreply'} || $o->{'no_autoreply'}) {
LJ::send_mail({
'to' => $email,
'from' => $LJ::BOGUS_EMAIL,
'fromname' => "$LJ::SITENAME Support",
'charset' => 'utf-8',
'subject' => "Support Request \#$spid",
'body' => $body
});
}
support_notify({ spid => $spid, type => 'new' });
# and we're done
return $spid;
}
sub append_request
{
my $sp = shift; # support request to be appended to.
my $re = shift; # hashref of attributes of response to be appended
my $sth;
# $re->{'body'}
# $re->{'type'} (req, answer, comment, internal, screened)
# $re->{'faqid'}
# $re->{'remote'} (remote if known)
# $re->{'uniq'} (uniq of remote)
my $remote = $re->{'remote'};
my $posterid = $remote ? $remote->{'userid'} : 0;
# check for a sysban
my $log = { 'uniq' => $re->{'uniq'} };
if ($remote) {
$log->{'user'} = $remote->user;
$log->{'email'} = $remote->email_raw;
if (LJ::sysban_check('support_user', $remote->{'user'})) {
return LJ::sysban_block($remote->{'userid'}, "Support request blocked based on user", $log);
}
if (LJ::sysban_check('support_email', $remote->email_raw)) {
return LJ::sysban_block($remote->{'userid'}, "Support request blocked based on email", $log);
}
}
if (LJ::sysban_check('support_uniq', $re->{'uniq'})) {
my $userid = $remote ? $remote->{'userid'} : 0;
return LJ::sysban_block($userid, "Support request blocked based on uniq", $log);
}
my $message = $re->{'body'};
$message =~ s/^\s+//;
$message =~ s/\s+$//;
my $dbh = LJ::get_db_writer();
my $qmessage = $dbh->quote($message);
my $qtype = $dbh->quote($re->{'type'});
my $qfaqid = $re->{'faqid'}+0;
my $quserid = $posterid+0;
my $spid = $sp->{'spid'}+0;
my $sql = "INSERT INTO supportlog (splid, spid, timelogged, type, faqid, userid, message) VALUES (NULL, $spid, UNIX_TIMESTAMP(), $qtype, $qfaqid, $quserid, $qmessage)";
$dbh->do($sql);
my $splid = $dbh->{'mysql_insertid'};
if ($posterid) {
# add to our index of recently replied to support requests per-user.
$dbh->do("INSERT IGNORE INTO support_youreplied (userid, spid) VALUES (?, ?)", undef,
$posterid, $spid);
die $dbh->errstr if $dbh->err;
# and also lazily clean out old stuff:
$sth = $dbh->prepare("SELECT s.spid FROM support s, support_youreplied yr ".
"WHERE yr.userid=? AND yr.spid=s.spid AND s.state='closed' ".
"AND s.timeclosed < UNIX_TIMESTAMP() - 3600*72");
$sth->execute($posterid);
my @to_del;
push @to_del, $_ while ($_) = $sth->fetchrow_array;
if (@to_del) {
my $in = join(", ", map { $_ + 0 } @to_del);
$dbh->do("DELETE FROM support_youreplied WHERE userid=? AND spid IN ($in)",
undef, $posterid);
}
}
support_notify({ spid => $spid, splid => $splid, type => 'update' });
return $splid;
}
# userid may be undef/0 in the setting to zero case
sub set_points
{
my ($spid, $userid, $points) = @_;
my $dbh = LJ::get_db_writer();
if ($points) {
$dbh->do("REPLACE INTO supportpoints (spid, userid, points) ".
"VALUES (?,?,?)", undef, $spid, $userid, $points);
} else {
$userid ||= $dbh->selectrow_array("SELECT userid FROM supportpoints WHERE spid=?",
undef, $spid);
$dbh->do("DELETE FROM supportpoints WHERE spid=?", undef, $spid);
}
$dbh->do("REPLACE INTO supportpointsum (userid, totpoints, lastupdate) ".
"SELECT userid, SUM(points), UNIX_TIMESTAMP() FROM supportpoints ".
"WHERE userid=? GROUP BY 1", undef, $userid) if $userid;
# clear caches
if ($userid) {
my $u = LJ::load_userid($userid);
delete $u->{_supportpointsum} if $u;
my $memkey = [$userid, "supportpointsum:$userid"];
LJ::MemCache::delete($memkey);
}
}
sub touch_request
{
my ($spid) = @_;
# no touching if the request is locked
return 0 if LJ::Support::is_locked($spid);
my $dbh = LJ::get_db_writer();
$dbh->do("UPDATE support".
" SET state='open', timeclosed=0, timetouched=UNIX_TIMESTAMP()".
" WHERE spid=?",
undef, $spid)
or return 0;
set_points($spid, undef, 0);
return 1;
}
sub mail_response_to_user
{
my $sp = shift;
my $splid = shift;
$splid += 0;
my $res = load_response($splid);
my $email;
if ($sp->{'reqtype'} eq "email") {
$email = $sp->{'reqemail'};
} else {
my $u = LJ::load_userid($sp->{'requserid'});
$email = $u->email_raw || $sp->{'reqemail'};
}
my $spid = $sp->{'spid'}+0;
my $faqid = $res->{'faqid'}+0;
my $type = $res->{'type'};
# don't mail internal comments (user shouldn't see) or
# screened responses (have to wait for somebody to approve it first)
return if ($type eq "internal" || $type eq "screened");
# the only way it can be zero is if it's a reply to an email, so it's
# problem the person replying to their own request, so we don't want
# to mail them:
return unless ($res->{'userid'});
# also, don't send them their own replies:
return if ($sp->{'requserid'} == $res->{'userid'});
my $body = "";
my $dbh = LJ::get_db_writer();
my $what = $type eq "answer" ? "an answer to" : "a comment on";
$body .= "Below is $what your support question regarding \"$sp->{'subject'}\"\n";
my $miniauth = mini_auth($sp);
$body .= "($LJ::SITEROOT/support/see_request.bml?id=$spid&auth=$miniauth).\n\n";
$body .= "="x70 . "\n\n";
if ($faqid) {
my $faqname = "";
my $sth = $dbh->prepare("SELECT question FROM faq WHERE faqid=$faqid");
$sth->execute;
($faqname) = $sth->fetchrow_array;
if ($faqname) {
$body .= "FAQ REFERENCE: $faqname\n";
$body .= "$LJ::SITEROOT/support/faqbrowse.bml?faqid=$faqid&view=full";
$body .= "\n\n";
}
}
$body .= "$res->{'message'}\n\n";
if ($sp->{_cat}->{user_closeable}) {
$body .= "Did this answer your question?\nYES:\n";
$body .= "$LJ::SITEROOT/support/act.bml?close;$spid;$sp->{'authcode'}";
$body .= ";$splid" if $type eq "answer";
$body .= "\nNO:\n$LJ::SITEROOT/support/see_request.bml?id=$spid&auth=$miniauth\n\n";
}
$body .= "If you are having problems using any of the links in this email, please try copying and pasting the *entire* link into your browser's address bar rather than clicking on it.";
my $fromemail;
if ($sp->{_cat}->{'replyaddress'}) {
my $miniauth = mini_auth($sp);
$fromemail = $sp->{_cat}->{'replyaddress'};
# insert mini-auth stuff:
my $rep = "+${spid}z$miniauth\@";
$fromemail =~ s/\@/$rep/;
} else {
$fromemail = $LJ::BOGUS_EMAIL;
$body .= "\n\nReplies to this address are not monitored. To reply to your request, use the links above.";
}
LJ::send_mail({
'to' => $email,
'from' => $fromemail,
'fromname' => "$LJ::SITENAME Support",
'charset' => 'utf-8',
'subject' => "Re: $sp->{'subject'}",
'body' => $body
});
if ($type eq "answer") {
$dbh->do("UPDATE support SET timelasthelp=UNIX_TIMESTAMP() WHERE spid=$spid");
}
}
sub mini_auth
{
my $sp = shift;
return substr($sp->{'authcode'}, 0, 4);
}
#
# name: LJ::Support::get_support_by_daterange
# des: Get all the [dbtable[support]] rows based on a date range.
# args: date1, date2
# des-date1: YYYY-MM-DD of beginning date of range
# des-date2: YYYY-MM-DD of ending date of range
# returns: HashRef of support rows by support id
#
sub get_support_by_daterange {
my ($date1, $date2) = @_;
# Build the query out based on the dates specified
my $time1 = LJ::mysqldate_to_time($date1);
my $time2 = LJ::mysqldate_to_time($date2) + $SECONDS_IN_DAY;
# Convert from times to IDs because support.timecreate isn't indexed
my ($start_id, $end_id) = LJ::DB::time_range_to_ids
(table => 'support',
roles => ['slow'],
idcol => 'spid',
timecol => 'timecreate',
starttime => $time1,
endtime => $time2,
);
# Generate the SQL. Include time fields to be safe
my $sql = "SELECT * FROM support "
. "WHERE spid >= ? AND spid <= ? "
. " AND timecreate >= ? AND timecreate < ?";
# Get the results from the database
my $dbh = LJ::Support::slow_query_dbh()
or return "Database unavailable";
my $sth = $dbh->prepare($sql);
$sth->execute($start_id, $end_id, $time1, $time2);
die $dbh->errstr if $dbh->err;
$sth->{mysql_use_result} = 1;
# Loop over the results, generating a hash by Support ID
my %result_hash = ();
while (my $row = $sth->fetchrow_hashref) {
$result_hash{$row->{spid}} = $row;
}
return \%result_hash;
}
#
# name: LJ::Support::get_support_by_ids
# des: Get all the [dbtable[support]] rows based on a list of Support IDs
# args: support_ids_ref
# des-support_ids_ref: ArrayRef of Support IDs.
# returns: ArrayRef of support rows
#
sub get_support_by_ids {
my ($support_ids_ref) = @_;
my %result_hash = ();
return \%result_hash unless @$support_ids_ref;
# Build the query out based on the dates specified
my $support_ids_bind = join ',', map { '?' } @$support_ids_ref;
my $sql = "SELECT * FROM support "
. "WHERE spid IN ($support_ids_bind)";
# Get the results from the database
my $dbh = LJ::Support::slow_query_dbh()
or return "Database unavailable";
my $sth = $dbh->prepare($sql);
$sth->execute(@$support_ids_ref);
die $dbh->errstr if $dbh->err;
$sth->{mysql_use_result} = 1;
# Loop over the results, generating a hash by Support ID
while (my $row = $sth->fetchrow_hashref) {
$result_hash{$row->{spid}} = $row;
}
return \%result_hash;
}
#
# name: LJ::Support::get_supportlogs
# des: Get all the [dbtable[supportlog]] rows for a list of Support IDs.
# args: support_ids_ref
# des-support_ids_ref: ArrayRef of Support IDs.
# returns: HashRef of supportlog rows by support id.
#
sub get_supportlogs {
my $support_ids_ref = shift;
my %result_hash = ();
return \%result_hash unless @$support_ids_ref;
# Build the query out based on the dates specified
my $spid_bind = join ',', map { '?' } @$support_ids_ref;
my $sql = "SELECT * FROM supportlog WHERE spid IN ($spid_bind) ";
# Get the results from the database
my $dbh = LJ::Support::slow_query_dbh()
or return "Database unavailable";
my $sth = $dbh->prepare($sql);
$sth->execute(@$support_ids_ref);
die $dbh->errstr if $dbh->err;
$sth->{mysql_use_result} = 1;
# Loop over the results, generating a hash by Support ID
while (my $row = $sth->fetchrow_hashref) {
push @{$result_hash{$row->{spid}}}, $row;
}
return \%result_hash;
}
#
# name: LJ::Support::get_touch_supportlogs_by_user_and_date
# des: Get all touch (non-req) supportlogs based on User ID and Date Range.
# args: userid, date1, date2
# des-userid: User ID to filter on, or Undef for all users.
# des-date1: YYYY-MM-DD of beginning date of range
# des-date2: YYYY-MM-DD of ending date of range
# returns: Support HashRef of Support Logs Array, sorted by log time.
#
sub get_touch_supportlogs_by_user_and_date {
my ($userid, $date1, $date2) = @_;
# Build the query out based on the dates specified
my $time1 = LJ::mysqldate_to_time($date1);
my $time2 = LJ::mysqldate_to_time($date2) + $SECONDS_IN_DAY;
# Convert from times to IDs because supportlog.timelogged isn't indexed
my ($start_id, $end_id) = LJ::DB::time_range_to_ids
(table => 'supportlog',
roles => \@LJ::SUPPORT_SLOW_ROLES,
idcol => 'splid',
timecol => 'timelogged',
starttime => $time1,
endtime => $time2,
);
# Generate the SQL. Include time fields to be safe
my $sql = "SELECT * FROM supportlog"
. " WHERE type <> 'req' "
. " AND splid >= ? AND splid <= ?"
. " AND timelogged >= ? AND timelogged < ?"
. ($userid ? " AND userid = ?" : '');
# Get the results from the database
my $dbh = LJ::Support::slow_query_dbh()
or return "Database unavailable";
my $sth = $dbh->prepare($sql);
my @parms = ($start_id, $end_id, $time1, $time2);
push @parms, $userid if $userid;
$sth->execute(@parms);
die $dbh->errstr if $dbh->err;
$sth->{mysql_use_result} = 1;
# Store the query results in an array
my @results;
while (my $row = $sth->fetchrow_hashref) {
push @results, $row;
}
# Sort logs by time
@results = sort {$a->{timelogged} <=> $b->{timelogged}} @results;
# Loop over the results, generating an array that's hashed by Support ID
my %result_hash = ();
foreach my $row (@results) {
push @{$result_hash{$row->{spid}}}, $row;
}
return \%result_hash;
}
sub support_notify {
my $params = shift;
my $sclient = LJ::theschwartz() or
return 0;
my $h = $sclient->insert("LJ::Worker::SupportNotify", $params);
return $h ? 1 : 0;
}
package LJ::Worker::SupportNotify;
use base 'TheSchwartz::Worker';
sub work {
my ($class, $job) = @_;
my $a = $job->arg;
# load basic stuff common to both paths
my $type = $a->{type};
my $spid = $a->{spid}+0;
my $load_body = $type eq 'new' ? 1 : 0;
my $sp = LJ::Support::load_request($spid, $load_body, { force => 1 }); # force from master
# we're only going to be reading anyway, but these jobs
# sometimes get processed faster than replication allows,
# causing the message not to load from the reader
my $dbr = LJ::get_db_writer();
# now branch a bit to select the right user information
my $level = $type eq 'new' ? "'new', 'all'" : "'all'";
my $data = $dbr->selectcol_arrayref("SELECT userid FROM supportnotify " .
"WHERE spcatid=? AND level IN ($level)", undef, $sp->{_cat}{spcatid});
my $userids = LJ::load_userids(@$data);
# 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};
foreach my $u (values %$userids) {
next unless $u->is_visible;
next unless $u->{status} eq "A";
push @emails, $u->email_raw;
}
} 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 $u (values %$userids) {
next unless $u->is_visible;
next unless $u->{status} eq "A";
next if $posterid == $u->id;
next if $rtype eq 'screened' &&
!LJ::Support::can_read_screened($sp, $u);
next if $rtype eq 'internal' &&
!LJ::Support::can_read_internal($sp, $u);
push @emails, $u->email_raw;
}
}
# 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;
$job->completed;
return 1;
}
sub keep_exit_status_for { 0 }
sub grab_for { 30 }
sub max_retries { 5 }
sub retry_delay {
my ($class, $fails) = @_;
return 30;
}
1;