mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-08 04:27:37 +00:00
10486f21a3
Patch primarily by Jake Steenhagen <jake@bugzilla.org> and Bradley Baetz <bbaetz@acm.org> r= justdave
460 lines
18 KiB
Plaintext
460 lines
18 KiB
Plaintext
# -*- Mode: perl; tab-width: 4; indent-tabs-mode: nil; -*-
|
|
################################
|
|
# Bugzilla Module #
|
|
################################
|
|
|
|
package BotModules::Bugzilla;
|
|
use vars qw(@ISA);
|
|
@ISA = qw(BotModules);
|
|
1;
|
|
|
|
use XML::LibXML;
|
|
|
|
# there is a minor error in this module: bugsHistory->$target->$bug is
|
|
# accessed even when bugsHistory->$target doesn't yet exist. XXX
|
|
|
|
# This is ported straight from techbot, so some of the code is a little convoluted. So sue me. I was lazy.
|
|
|
|
# RegisterConfig - Called when initialised, should call registerVariables
|
|
sub RegisterConfig {
|
|
my $self = shift;
|
|
$self->SUPER::RegisterConfig(@_);
|
|
$self->registerVariables(
|
|
# [ name, save?, settable? ]
|
|
['bugsURI', 1, 1, 'http://bugzilla.mozilla.org/'],
|
|
['bugsDWIMQueryDefault', 1, 1, 'short_desc_type=substring&short_desc='],
|
|
['bugsHistory', 0, 0, {}],
|
|
['backoffTime', 1, 1, 120],
|
|
['ignoreCommentsTo', 1, 1, ['']],
|
|
['ignoreCommentsFrom', 1, 1, ['|']],
|
|
['skipPrefixFor', 1, 1, []],
|
|
['mutes', 1, 1, ''], # "channel channel channel"
|
|
);
|
|
}
|
|
|
|
sub Help {
|
|
my $self = shift;
|
|
my ($event) = @_;
|
|
my %commands = (
|
|
'' => 'The Bugzilla module provides an interface to the bugzilla bug database. It will spot anyone mentioning bugs, too, and report on what they are. For example if someone says \'I think that\'s a dup of bug 5693, the :hover thing\', then this module will display information about bug 5693.',
|
|
'bug' => 'Fetches a summary of bugs from bugzilla. Expert syntax: \'bugzilla [bugnumber[,]]*[&bugzillaparameter=value]*\', bug_status: UNCONFIRMED|NEW|ASSIGNED|REOPENED; *type*=substring|; bugtype: include|exclude; order: Assignee|; chfield[from|to|value] short_desc\' long_desc\' status_whiteboard\' bug_file_loc\' keywords\'; \'_type; email[|type][1|2] [reporter|qa_contact|assigned_to|cc]',
|
|
'bug-total' => 'Same as bug (which see) but only displays the total line.',
|
|
'bugs' => 'A simple DWIM search. Not very clever. ;-) Syntax: \'<query string> bugs\' e.g. \'mozbot bugs\'.'
|
|
);
|
|
if ($self->isAdmin($event)) {
|
|
$commands{'mute'} = 'Disable watching for bug numbers in a channel. Syntax: mute bugzilla in <channel>';
|
|
$commands{'unmute'} = 'Enable watching for bug numbers in a channel. Syntax: unmute bugzilla in <channel>';
|
|
}
|
|
return \%commands;
|
|
}
|
|
|
|
sub Told {
|
|
my $self = shift;
|
|
my ($event, $message) = @_;
|
|
if ($message =~ m/^ \s* # some optional whitespace
|
|
(?:please\s+)? # an optional "please", followed optionally by either:
|
|
(?: (?:could\s+you\s+)? # 1. an optional "could you",
|
|
(?:please\s+)? # another optional "please",
|
|
show\s+me\s+ | # and the text "show me"
|
|
what\s+is\s+ | # 2. the text "what is"
|
|
what\'s\s+ )? # 3. or the text "what's"
|
|
bug (?:\s*id)?s? [\#\s]+ # a variant on "bug", "bug id", "bugids", etc
|
|
([0-9].*?| # a query string, either a number followed by some optional text, or
|
|
&.+?) # a query string, starting with a &.
|
|
(?:\s+please)? # followed by yet another optional "please"
|
|
[?!.\s]* # ending with some optional punctuation
|
|
$/osix) {
|
|
my $target = $event->{'target'};
|
|
my $bug = $1;
|
|
# Single bugs use xml.cgi, because then we get error messages
|
|
if ($bug =~ m/^\d+$/) {
|
|
$self->FetchBug($event, $bug, 'bug', 0, 0);
|
|
} else {
|
|
$self->FetchBug($event, $bug, 'bugs', 0, 0);
|
|
}
|
|
$self->{'bugsHistory'}->{$target}->{$bug} = $event->{'time'} if $bug =~ m/^[0-9]+$/os;
|
|
} elsif ($message =~ m/^\s*bug-?total\s+(.+?)\s*$/osi) {
|
|
$self->FetchBug($event, $1, 'total', 0, 0);
|
|
} elsif ($self->isAdmin($event)) {
|
|
if ($message =~ m/^\s*mute\s+bugzilla\s+in\s+(\S+?)\s*$/osi) {
|
|
$self->{'mutes'} .= " $1";
|
|
$self->saveConfig();
|
|
$self->say($event, "$event->{'from'}: Watching for bug numbers disabled in channel $1.");
|
|
} elsif ($message =~ m/^\s*unmute\s+bugzilla\s+in\s+(\S+)\s*$/osi) {
|
|
my %mutedChannels = map { $_ => 1 } split(/ /o, $self->{'mutes'});
|
|
delete($mutedChannels{$1}); # get rid of any mentions of that channel
|
|
$self->{'mutes'} = join(' ', keys(%mutedChannels));
|
|
$self->saveConfig();
|
|
$self->say($event, "$event->{'from'}: Watching for bug numbers reenabled in channel $1.");
|
|
} else {
|
|
return $self->SUPER::Told(@_);
|
|
}
|
|
} else {
|
|
return $self->SUPER::Told(@_);
|
|
}
|
|
return 0; # dealt with it...
|
|
}
|
|
|
|
sub CheckForBugs {
|
|
my $self = shift;
|
|
my ($event, $message) = @_;
|
|
if ((($event->{'channel'} eq '') or # either it was /msg'ed, or
|
|
($self->{'mutes'} !~ m/^(?:.*\s|)\Q$event->{'channel'}\E(?:|\s.*)$/si)) and # it was sent on a channel in which we aren't muted
|
|
(not $self->ignoringCommentsFrom($event->{'from'})) and # we aren't ignoring them
|
|
(not $self->ignoringCommentsTo($message))) { # and they aren't talking to someone we need to ignore
|
|
my $rest = $message;
|
|
my $bugsFound = 0;
|
|
my $bugsToFetch = '';
|
|
my $bug;
|
|
my $skipURI;
|
|
do {
|
|
if ($rest =~ m/ (?:^| # either the start of the string
|
|
[]\s,.;:\\\/=?!()<>{}[-]) # or some punctuation
|
|
bug [\s\#]* ([0-9]+) # followed a string similar to "bug # 123" (put the number in $1)
|
|
(?:[]\s,.;:\\\/=?!()<>{}[-]+ # followed optionally by some punctuation,
|
|
(.*))?$/osix) { # and everything else (which we put in $2)
|
|
$bug = $1;
|
|
$skipURI = 0;
|
|
$rest = $2;
|
|
} elsif ($rest =~ m/\Q$self->{'bugsURI'}\Eshow_bug.cgi\?id=([0-9]+)(?:[^0-9&](.*))?$/si) {
|
|
$bug = $1;
|
|
$skipURI = 1;
|
|
$rest = $2;
|
|
} else {
|
|
$bug = undef;
|
|
}
|
|
if (defined($bug)) {
|
|
$self->debug("Noticed someone mention bug $bug -- investigating...");
|
|
my $last = 0;
|
|
$last = $self->{'bugsHistory'}->{$event->{'target'}}->{$bug} if defined($self->{'bugsHistory'}->{$event->{'target'}}->{$bug});
|
|
if (($event->{'time'}-$last) > $self->{'backoffTime'}) {
|
|
$bugsToFetch .= "$bug ";
|
|
}
|
|
$self->{'bugsHistory'}->{$event->{'target'}}->{$bug} = $event->{'time'};
|
|
$bugsFound++;
|
|
}
|
|
} while (defined($bug));
|
|
if ($bugsToFetch ne '') {
|
|
$self->FetchBug($event, $bugsToFetch, 'bug', $skipURI, 1);
|
|
}
|
|
return $bugsFound;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub Heard {
|
|
my $self = shift;
|
|
my ($event, $message) = @_;
|
|
unless ($self->CheckForBugs($event, $message)) {
|
|
return $self->SUPER::Heard(@_);
|
|
}
|
|
return 0; # we've dealt with it, no need to do anything else.
|
|
}
|
|
|
|
sub Baffled {
|
|
my $self = shift;
|
|
my ($event, $message) = @_;
|
|
if ($message =~ m/^\s*(...+?)\s+bugs\s*$/osi) {
|
|
my $target = $event->{'target'};
|
|
$self->FetchBug($event, $1, 'dwim', 0, 0);
|
|
} else {
|
|
return $self->SUPER::Baffled(@_);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub Felt {
|
|
my $self = shift;
|
|
my ($event, $message) = @_;
|
|
unless ($self->CheckForBugs($event, $message)) {
|
|
return $self->SUPER::Felt(@_);
|
|
}
|
|
return 0; # we've dealt with it, no need to do anything else.
|
|
}
|
|
|
|
sub Saw {
|
|
my $self = shift;
|
|
my ($event, $message) = @_;
|
|
unless ($self->CheckForBugs($event, $message)) {
|
|
return $self->SUPER::Saw(@_);
|
|
}
|
|
return 0; # we've dealt with it, no need to do anything else.
|
|
}
|
|
|
|
sub FetchBug {
|
|
my $self = shift;
|
|
my ($event, $bugParams, $subtype, $skipURI, $skipZaroo) = @_;
|
|
my $uri;
|
|
my $type;
|
|
if ($subtype eq 'bug') {
|
|
$uri = "$self->{'bugsURI'}xml.cgi?id=".join(',',split(' ',$bugParams));
|
|
$type = 'xml';
|
|
} elsif ($subtype eq 'dwim') {
|
|
# XXX should escape query string
|
|
$uri = "$self->{'bugsURI'}buglist.cgi?format=rdf&$self->{'bugsDWIMQueryDefault'}".join(',',split(' ',$bugParams));
|
|
$subtype = 'bugs';
|
|
$type = 'buglist';
|
|
} else {
|
|
$uri = "$self->{'bugsURI'}buglist.cgi?format=rdf&bug_id=".join(',',split(' ',$bugParams));
|
|
$type = 'buglist';
|
|
}
|
|
$self->getURI($event, $uri, $type, $subtype, $skipURI, $skipZaroo);
|
|
}
|
|
|
|
sub GotURI {
|
|
my $self = shift;
|
|
my ($event, $uri, $output, $type, $subtype, $skipURI, $skipZaroo) = @_;
|
|
|
|
my @bugs;
|
|
|
|
# Bugzilla really needs a LIMIT option
|
|
my $maxRes;
|
|
if ($event->{'channel'}) {
|
|
$maxRes = 5;
|
|
} else {
|
|
$maxRes = 20;
|
|
}
|
|
my $truncated = 0;
|
|
|
|
if ($type eq 'buglist') {
|
|
# We asked for rdf, but old versions won't know how to do that
|
|
# So lets do some simple sniffing, until mozbot gives us a way
|
|
# to find out the server's returned mime type
|
|
my $format;
|
|
if ($output =~ /^<\?xml /) {
|
|
$type = 'rdf';
|
|
} else {
|
|
$type = 'html';
|
|
}
|
|
}
|
|
|
|
my $lots;
|
|
my $bugCount;
|
|
|
|
if ($type eq 'html') {
|
|
my $lots;
|
|
my @qp;
|
|
|
|
# magicness
|
|
{ no warnings; # this can go _very_ wrong easily
|
|
|
|
$lots = ($output !~ m/<FORM\s+METHOD=POST\s+ACTION="long_list.cgi">/osi); # if we got truncated, then this will be missing
|
|
|
|
# Instead of relying on being able to accurately count the
|
|
# number of bugs (which we can't do if there are more than
|
|
# 199), use the number that bugzilla tells us.
|
|
if ($output =~ /(One|\d+) bugs? found/o) {
|
|
$bugCount = $1;
|
|
if ($bugCount eq "One") {
|
|
$bugCount = 1;
|
|
}
|
|
}
|
|
|
|
$output =~ s/<\/TABLE><TABLE .+?<\/A><\/TH>//gosi;
|
|
(undef, $output) = split(/Summary<\/A><\/TH>/osi, $output);
|
|
($output, undef) = split(/<\/TABLE>/osi, $output);
|
|
$output =~ s/[\n\r]//gosi;
|
|
@qp = split(m/<TR VALIGN=TOP ALIGN=LEFT CLASS=[-A-Za-z0-9]+(?: style='.*?')?\s*?><TD>/osi, $output);
|
|
}
|
|
|
|
if (scalar(@qp) == 0) {
|
|
$bugCount = 0;
|
|
}
|
|
|
|
if (!$lots && $subtype eq 'bugs') {
|
|
if (scalar(@qp) > $maxRes) {
|
|
$truncated = 1;
|
|
@qp = @qp[0..$maxRes-1];
|
|
}
|
|
|
|
foreach (@qp) {
|
|
if ($_) {
|
|
# more magic
|
|
if (my @d = m|<A HREF="show_bug.cgi\?id=([0-9]+)">\1</A> <td class=severity><nobr>(.*?)</nobr><td class=priority><nobr>(.*?)</nobr><td class=platform><nobr>(.*?)</nobr><td class=owner><nobr>(.*?)</nobr><td class=status><nobr>(.*?)</nobr><td class=resolution><nobr>(.*?)</nobr><td class=summary>(.*)|osi) {
|
|
# bugid severity priority platform owner status resolution subject
|
|
my %bug;
|
|
($bug{'id'}, $bug{'severity'}, $bug{'priority'}, $bug{'platform'}, $bug{'owner'}, $bug{'status'}, $bug{'resolution'}, $bug{'summary'}) = @d;
|
|
push (@bugs, \%bug);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} elsif ($type eq 'xml') {
|
|
# We came from xml.cgi
|
|
my $parser = XML::LibXML->new();
|
|
my $tree = $parser->parse_string($output);
|
|
my $root = $tree->getDocumentElement;
|
|
|
|
my @xml_bugs = $root->getElementsByTagName('bug');
|
|
$bugCount = scalar(@xml_bugs);
|
|
|
|
if (scalar(@xml_bugs) > $maxRes) {
|
|
$truncated = 1;
|
|
@xml_bugs = @xml_bugs[0..$maxRes-1];
|
|
}
|
|
|
|
# OK, xml.cgi uses different names to the query stuff
|
|
# Take a deep breath, and use a mapping for the fields we
|
|
# care about
|
|
my %fieldMap = (
|
|
'bug_id' => 'id',
|
|
'bug_severity' => 'severity',
|
|
'priority' => 'priority',
|
|
'target_milestone' => 'target_milestone',
|
|
'assigned_to' => 'owner',
|
|
'bug_status' => 'status',
|
|
'resolution' => 'resolution',
|
|
'short_desc' => 'summary'
|
|
);
|
|
|
|
foreach my $xml_bug(@xml_bugs) {
|
|
my %bug = {};
|
|
my $error = $xml_bug->getAttribute('error');
|
|
if (!defined $error) {
|
|
foreach my $field (keys %fieldMap) {
|
|
my @arr = $xml_bug->getElementsByTagName($field);
|
|
if (@arr) {
|
|
my $str = $arr[0]->getFirstChild->getData();
|
|
$bug{$fieldMap{$field}} = $str;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
my @arr = $xml_bug->getElementsByTagName('bug_id');
|
|
$bug{'id'} = $arr[0]->getFirstChild->getData();
|
|
$bug{'error'} = $error;
|
|
}
|
|
push @bugs, \%bug;
|
|
}
|
|
} elsif ($type eq 'rdf') {
|
|
my $parser = XML::LibXML->new();
|
|
my $tree = $parser->parse_string($output);
|
|
my $root = $tree->getDocumentElement;
|
|
my @rdf_bugs = $root->getElementsByTagName('bz:bug');
|
|
|
|
$bugCount = scalar(@rdf_bugs);
|
|
|
|
if (scalar(@rdf_bugs) > $maxRes) {
|
|
$truncated = 1;
|
|
@rdf_bugs = @rdf_bugs[0..$maxRes-1];
|
|
}
|
|
|
|
foreach my $rdf_bug (@rdf_bugs) {
|
|
my %bug = {};
|
|
my @children = $rdf_bug->getChildnodes();
|
|
foreach my $child (@children) {
|
|
next if ($child->getLocalName() eq 'text');
|
|
my $field = $child->getLocalName();
|
|
if ($child->getFirstChild()) {
|
|
my $val = $child->getFirstChild->getData();
|
|
$bug{$field} = $val;
|
|
}
|
|
}
|
|
push @bugs, \%bug;
|
|
}
|
|
} else {
|
|
return $self->SUPER::GotURI(@_);
|
|
}
|
|
|
|
# construct the response's preamble
|
|
my $preamble;
|
|
if ($bugCount == 0 && !$skipZaroo) {
|
|
$preamble = 'Zarro boogs found.';
|
|
} else {
|
|
my $bugCountStr;
|
|
if ($bugCount) {
|
|
$bugCountStr = "$bugCount bug" . ($bugCount == 1 ? '' : 's')
|
|
. " found";
|
|
}
|
|
|
|
if ($subtype eq 'total') {
|
|
$self->say($event, $bugCountStr);
|
|
return;
|
|
}
|
|
|
|
if ($lots) {
|
|
$preamble = $bugCountStr ? "$bugCountStr, which is too many for me to handle without running out of memory."
|
|
: 'Way too many bugs found. I gave up so as to not run out of memory.';
|
|
$preamble .= "$bugCountStr Try to narrow your search or something!";
|
|
$subtype = 'lots';
|
|
} elsif ($subtype ne 'bug' && $bugCount > 1) {
|
|
$preamble = $bugCountStr;
|
|
if ($truncated) {
|
|
if ($event->{'channel'}) {
|
|
$preamble .= '. Five shown, please message me for more.';
|
|
} else {
|
|
$preamble .= '. Will only show 20 results, please use the Bugzilla query form if you want more.';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
my $prefix;
|
|
if (grep {$_ eq $event->{'from'}} @{$self->{'skipPrefixFor'}}) {
|
|
# they don't want to have the report prefixed with their name
|
|
$prefix = '';
|
|
} else {
|
|
$prefix = "$event->{'from'}: ";
|
|
}
|
|
|
|
if ($preamble) {
|
|
$self->say($event, "$prefix$preamble");
|
|
}
|
|
|
|
my $bug_link = $skipURI ? "" : "$self->{'bugsURI'}show_bug.cgi?id=";
|
|
|
|
# now send out the output
|
|
foreach my $bug (@bugs) {
|
|
if (!defined $bug->{'error'}) {
|
|
# Bugzilla doesn't give the TM by default, and we can't
|
|
# change this without using cookies, which aren't supported
|
|
# by the mozbot API. Later versions allow us to use a query param
|
|
# but we can't detect that that was accepted, which would break
|
|
# the HTML parsing
|
|
# xml.cgi gives us everything, so we can print this if we got
|
|
# results from there
|
|
# Maybe the list of columns to display could be a var, one day, after
|
|
# installations from source before Dec 2001 are no longer supported,
|
|
# or we can pass cookies
|
|
$self->say($event, $prefix .
|
|
"Bug $bug_link$bug->{'id'} " .
|
|
substr($bug->{'severity'}, 0, 3) . ", " .
|
|
$bug->{'priority'} . ", " .
|
|
($bug->{'target_milestone'} ? "$bug->{'target_milestone'}, " : "") .
|
|
$bug->{'owner'} . ", " .
|
|
substr($bug->{'status'}, 0, 4) .
|
|
($bug->{'resolution'} ? " " . $bug->{'resolution'} : "") . ", " .
|
|
substr($bug->{'summary'}, 0, 100));
|
|
} elsif ($bug->{'error'} eq 'NotFound') {
|
|
unless($skipZaroo) {
|
|
$self->say($event, $prefix . "Bug $bug->{'id'} was not found.");
|
|
}
|
|
} elsif ($bug->{'error'} eq 'NotPermitted') {
|
|
$self->say($event, $prefix . "Bug $bug_link$bug->{'id'} is not accessible");
|
|
} else {
|
|
unless($skipZaroo) {
|
|
$self->say($prefix . "Error accessing bug $bug->{'id'}: $bug->{'error'}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sub ignoringCommentsTo {
|
|
my $self = shift;
|
|
my ($who) = @_;
|
|
foreach (@{$self->{'ignoreCommentsTo'}}) {
|
|
return 1 if $who =~ m/^(?:.*[]\s,.;:\\\/=?!()<>{}[-])?\Q$_\E(?:[]\s,.;:\\\/=?!()<>{}[-].*)?$/is;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub ignoringCommentsFrom {
|
|
my $self = shift;
|
|
my ($who) = @_;
|
|
foreach (@{$self->{'ignoreCommentsFrom'}}) {
|
|
return 1 if $_ eq $who;
|
|
}
|
|
return 0;
|
|
}
|
|
|