]> git.somenet.org - irc/bugbot.git/blob - BotModules/Bugzilla.bm
GITOLITE.txt
[irc/bugbot.git] / BotModules / Bugzilla.bm
1 # -*- Mode: perl; tab-width: 4; indent-tabs-mode: nil; -*-
2 # vim: syntax=perl
3 ################################
4 # Bugzilla Module              #
5 ################################
6
7
8 package BotModules::Bugzilla;
9 use vars qw(@ISA);
10 @ISA = qw(BotModules);
11
12 use XML::LibXML;
13 use Fcntl qw(:DEFAULT :flock);
14 use File::Basename;
15
16 # For parsing bugmail.log records. Must be the same as 
17 # FIELD_SEPARATOR in bugmail.pl.
18 use constant FIELD_SEPARATOR => '::::';
19 # The log file that we read to report bug changes.
20 # This will be put in the directory returned by dirname($0).
21 use constant BUGMAIL_LOG => 'BotModules/.bugmail.log';
22 1;
23
24 # there is a minor error in this module: bugsHistory->$target->$bug is
25 # accessed even when bugsHistory->$target doesn't yet exist. XXX
26
27 # This is ported straight from techbot, so some of the code is a little convoluted. So sue me. I was lazy.
28
29 sub Initialise {
30     my $self = shift;
31     my $retval = $self->SUPER::Initialise(@_);
32     my ($throw_away) = $self->GetBugLog();
33     $throw_away->truncate(0) if $throw_away;
34     $throw_away->close() if $throw_away;
35     return $retval;
36 }
37
38 # RegisterConfig - Called when initialised, should call registerVariables
39 sub RegisterConfig {
40     my $self = shift;
41     $self->SUPER::RegisterConfig(@_);
42     $self->registerVariables(
43       # [ name, save?, settable? ]
44         ['bugsURI', 1, 1, 'https://bugzilla.mozilla.org/'], 
45         ['bugsDWIMQueryDefault', 1, 1, 'short_desc_type=substring&short_desc='], 
46         ['bugsDWIMQueryChannelDefault', 1, 1, {}],
47         ['bugsHistory', 0, 0, {}], 
48         ['backoffTime', 1, 1, 120], 
49         ['ignoreCommentsTo', 1, 1, ['']],
50         ['ignoreCommentsFrom', 1, 1, ['|']],
51         ['mailIgnore', 1, 1, []],
52         ['skipPrefixFor', 1, 1, []],
53         # The keys for productReportChannels can be in the form of 'Product'
54         # or 'Product::::Component'. The value is a comma-separated list of
55         # channel names.
56         ['productReportChannels', 1, 1, {}],
57         # The fields that you want notifications about.
58         ['reportFields', 1, 1, ['Resolution', 'Flag', 'Attachment Flag', 
59                                 'NewBug', 'NewAttach']],
60         # Except in these products, you don't want notifications about
61         # certain fields (key is product name, value is comma-separated
62         # list of fields).
63         ['productMuteFields', 1, 1, {}],
64         # And in these channels, you don't want notifications about certain
65         # fields (the key is the channel name and the value is a 
66         # comma-separated list of fields).
67         ['channelMuteFields', 1, 1, {}],
68         # How frequently we check for new bugmail we've received, in seconds.
69         ['updateDelay', 1, 1, 10],
70         # List of products for which component of new bugs is reported instead
71         # of only the product. Can also restrict to specific components
72         # by using Product::::Component syntax and always report components
73         # by using 'all'.
74         ['reportComponent', 1, 1, ['all']],
75         ['mutes', 1, 1, ''],  # "channel channel channel"
76         # Optionally skip fetching the bug details for automatic notifications
77         ['reportBugDetails', 1, 1, 1]
78     );
79 }
80
81 sub Help {
82     my $self = shift;
83     my ($event) = @_;
84     my %commands = (
85         '' => '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.',
86         '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]',
87         'bug-total' => 'Same as bug (which see) but only displays the total line.',
88         'bugs' => q{A simple DWIM search. Not very clever. ;-)}
89                 . q{ Syntax: '<query string> bugs' e.g. 'mozbot bugs'.},
90         'ignore' => q{Causes the bot to stop reporting all bug changes}
91                   . q{ made by a particular user in the current channel.}
92                   . q{ Syntax: 'ignore <user@domain.com>' },
93         'unignore' => q{Causes the bot to un-ignore a previously ignored}
94                     . q{ user. See 'ignore'}
95                     . q{ for more details.},
96     );
97     if ($self->isAdmin($event)) {
98         $commands{'mute'} = 'Disable watching for bug numbers in a channel. Syntax: mute bugzilla in <channel>';
99         $commands{'unmute'} = 'Enable watching for bug numbers in a channel. Syntax: unmute bugzilla in <channel>';
100     }
101     return \%commands;
102 }
103
104 # Schedule - called when bot connects to a server, to install any schedulers
105 # use $self->schedule($event, $delay, $times, $data)
106 # where $times is 1 for a single event, -1 for recurring events,
107 # and a +ve number for an event that occurs that many times.
108 sub Schedule {
109     my $self = shift;
110     my ($event) = @_;
111     $self->schedule($event, \$self->{'updateDelay'}, -1, 'Bugzilla-BugMail');
112     return $self->SUPER::Schedule($event);
113 }
114
115 sub Scheduled {
116     my $self = shift;
117     my ($event, @data) = @_;
118     if ($data[0] eq 'Bugzilla-BugMail') {
119         $self->CheckForBugMail($event);
120     } else {
121         return $self->SUPER::Scheduled($event, @data);
122     }
123     return 0;
124 }
125
126 sub Told {
127     my $self = shift;
128     my ($event, $message) = @_;
129     if ($message =~ /^\s*ignore (.+)[?!.\s]*$/) {
130         my $user = $1;
131         # If we aren't already ignoring them...
132         if (!grep($_ eq $user, @{$self->{'mailIgnore'}})) {
133             push (@{$self->{'mailIgnore'}}, $user);
134             $self->saveConfig();
135             $self->say($event, 
136                 "$event->{'from'}: OK, ignoring changes produced by $user.");
137         }
138         else {
139             $self->say($event, 
140                 "$event->{'from'}: $user is already being ignored.");
141         }
142     }
143     elsif ($message =~ /^\s*unignore (.+)[?!.\s]*$/) {
144         my $user = $1;
145         my %ignoredUsers = map { $_ => 1 } @{$self->{'mailIgnore'}};
146         # If we are already ignoring them...
147         if ($ignoredUsers{$user}) {
148             delete $ignoredUsers{$user};
149             $self->{'mailIgnore'} = [keys %ignoredUsers];
150             $self->saveConfig();
151             $self->say($event, 
152                 "$event->{'from'}: OK, $user is no longer being ignored.");
153         }
154         else {
155             $self->say($event, "$event->{'from'}: $user wasn't being ignored.");
156         }
157     }
158     elsif ($message =~ m/^ \s*                   # some optional whitespace
159                         (?:please\s+)?           # an optional "please", followed optionally by either:
160                         (?: (?:could\s+you\s+)?  # 1. an optional "could you",
161                             (?:please\s+)?       #    another optional "please",
162                             show\s+me\s+ |       #    and the text "show me"
163                             what\s+is\s+ |       # 2. the text "what is"
164                             what\'s\s+     )?    # 3. or the text "what's"
165                         bug (?:\s*id)?s? [\#\s]+ # a variant on "bug", "bug id", "bugids", etc
166                         ([0-9].*?|               # a query string, either a number followed by some optional text, or
167                          &.+?)                   # a query string, starting with a &.
168                         (?:\s+please)?           # followed by yet another optional "please"
169                         [?!.\s]*                 # ending with some optional punctuation
170                       $/osix) {
171         my $target = $event->{'target'};
172         my $bug = $1;
173         # Single bugs use xml.cgi, because then we get error messages
174         if ($bug =~ m/^\d+$/) {
175             $self->FetchBug($event, $bug, 'bug', {'sayAlways' => 1});
176         } else {
177             $self->FetchBug($event, $bug, 'bugs', {'sayAlways' => 1});
178         }
179         $self->{'bugsHistory'}->{$target}->{$bug} = $event->{'time'} if $bug =~ m/^\d+$/os;
180     } elsif ($message =~ m/^\s*bug-?total\s+(.+?)\s*$/osi) {
181         $self->FetchBug($event, $1, 'total');
182     } elsif ($self->isAdmin($event)) {
183         if ($message =~ m/^\s*mute\s+bugzilla\s+in\s+(\S+?)\s*$/osi) {
184             $self->{'mutes'} .= " $1";
185             $self->saveConfig();
186             $self->say($event, "$event->{'from'}: Watching for bug numbers disabled in channel $1.");
187         } elsif ($message =~ m/^\s*unmute\s+bugzilla\s+in\s+(\S+)\s*$/osi) {
188             my %mutedChannels = map { $_ => 1 } split(/ /o, $self->{'mutes'});
189             delete($mutedChannels{$1}); # get rid of any mentions of that channel
190             $self->{'mutes'} = join(' ', keys(%mutedChannels));
191             $self->saveConfig();
192             $self->say($event, "$event->{'from'}: Watching for bug numbers reenabled in channel $1.");
193         } else {
194             return $self->SUPER::Told(@_);
195         }
196     } else {
197         return $self->SUPER::Told(@_);
198     }
199     return 0; # dealt with it...
200 }
201
202 sub CheckForBugs {
203     my $self = shift;
204     my ($event, $message) = @_;
205     if ((($event->{'channel'} eq '') or # either it was /msg'ed, or
206          ($self->{'mutes'} !~ m/^(?:.*\s|)\Q$event->{'channel'}\E(?:|\s.*)$/si)) and # it was sent on a channel in which we aren't muted
207         (not $self->ignoringCommentsFrom($event->{'from'})) and # we aren't ignoring them
208         (not $self->ignoringCommentsTo($message))) { # and they aren't talking to someone we need to ignore
209         my $rest = $message;
210         my $bugsFound = 0;
211         my $bugsToFetch = '';
212         my $bug;
213         my $skipURI;
214         do {
215             if ($rest =~ m/ (?:^|                        # either the start of the string
216                                []\s,.;:\\\/=?!()<>{}[-]) # or some punctuation
217                             bug [\s\#]* ([0-9]+)         # followed a string similar to "bug # 123" (put the number in $1)
218                             (?:[]\s,.;:\\\/=?!()<>{}[-]+ # followed optionally by some punctuation,
219                             (.*))?$/osix) {              # and everything else (which we put in $2)
220                 $bug = $1;
221                 $skipURI = 0;
222                 $rest = $2;
223             } elsif ($rest =~ m/\Q$self->{'bugsURI'}\Eshow_bug.cgi\?id=([0-9]+)(?:[^0-9&](.*))?$/si) {
224                 $bug = $1;
225                 $skipURI = 1;
226                 $rest = $2;
227             } else {
228                 $bug = undef;
229             }
230             if (defined($bug)) {
231                 $self->debug("Noticed someone mention bug $bug -- investigating...");
232                 $bugsToFetch .= "$bug ";
233                 $bugsFound++;
234             }
235         } while (defined($bug));
236         if ($bugsToFetch ne '') {
237             $self->FetchBug($event, $bugsToFetch, 'bug', {'skipURI' => $skipURI, 'skipZaroo' =>1});
238         }
239         return $bugsFound;
240     }
241     return 0;
242 }
243
244 sub Heard {
245     my $self = shift;
246     my ($event, $message) = @_;
247     unless ($self->CheckForBugs($event, $message)) {
248         return $self->SUPER::Heard(@_);
249     }
250     return 0; # we've dealt with it, no need to do anything else.
251 }
252
253 sub Baffled {
254     my $self = shift;
255     my ($event, $message) = @_;
256     if ($message =~ m/^\s*(...+?)\s+bugs\s*$/osi) {
257         my $target = $event->{'target'};
258         $self->FetchBug($event, $1, 'dwim');
259     } else {
260         return $self->SUPER::Baffled(@_);
261     }
262     return 0;
263 }
264
265 sub Felt {
266     my $self = shift;
267     my ($event, $message) = @_;
268     unless ($self->CheckForBugs($event, $message)) {
269         return $self->SUPER::Felt(@_);
270     }
271     return 0; # we've dealt with it, no need to do anything else.
272 }
273
274 sub Saw {
275     my $self = shift;
276     my ($event, $message) = @_;
277     unless ($self->CheckForBugs($event, $message)) {
278         return $self->SUPER::Saw(@_);
279     }
280     return 0; # we've dealt with it, no need to do anything else.
281 }
282
283 sub FetchBug {
284     my $self = shift;
285     my ($event, $bugParams, $subtype, $params) = @_;
286     my $skipURI = exists($params->{'skipURI'}) ? $params->{'skipURI'} : 0;
287     my $skipZaroo = exists($params->{'skipZaroo'}) ? $params->{'skipZaroo'} : 0;
288     my $sayAlways = exists($params->{'sayAlways'}) ? $params->{'sayAlways'} :  0;
289     my $uri;
290     my $type;
291     my @bugs = split(' ', $bugParams);
292     my @ids = ();
293     foreach my $bug (@bugs) {
294         if($sayAlways || $self->needToFetchBug($event->{'target'}, $event->{'time'}, $bug)) {
295             push @ids, $bug;
296             $self->{'bugsHistory'}->{$event->{'target'}}->{$bug} = $event->{'time'} if $bug =~ m/^\d+$/os;
297         }
298     }
299     return unless @ids;
300     if ($subtype eq 'bug') {
301         # Code taken from Bugzilla's xml.cgi
302         $uri = "$self->{'bugsURI'}show_bug.cgi?ctype=xml&excludefield=long_desc&excludefield=attachmentdata&excludefield=cc".join('', map { $_ = "&id=" . $_ } @ids);
303         $type = 'xml';
304     } elsif ($subtype eq 'dwim') {
305         # XXX should escape query string
306         my $DWIMdefaultQuery = $self->{'bugsDWIMQueryDefault'};
307         if (exists $self->{'bugsDWIMQueryChannelDefault'}->{$event->{'channel'}}) {
308             $DWIMdefaultQuery = $self->{'bugsDWIMQueryChannelDefault'}->{$event->{'channel'}};
309         }
310         $uri = "$self->{'bugsURI'}buglist.cgi?format=rdf&$DWIMdefaultQuery".join(',',@ids);
311         $subtype = 'bugs';
312         $type = 'buglist';
313     } else {
314         $uri = "$self->{'bugsURI'}buglist.cgi?format=rdf&bug_id=".join(',',@ids);
315         $type = 'buglist';
316     }
317     $self->getURI($event, $uri, $type, $subtype, $skipURI, $skipZaroo);
318 }
319
320 sub GotURI {
321     my $self = shift;
322     my ($event, $uri, $output, $type, $subtype, $skipURI, $skipZaroo) = @_;
323
324     my @bugs;
325
326     # Bugzilla really needs a LIMIT option
327     my $maxRes;
328     if ($event->{'channel'}) {
329         $maxRes = 5;
330     } else {
331         $maxRes = 20;
332     }
333     my $truncated = 0;
334
335     if ($type eq 'buglist') {
336         # We asked for rdf, but old versions won't know how to do that
337         # So lets do some simple sniffing, until mozbot gives us a way
338         # to find out the server's returned mime type
339         my $format;
340         if ($output =~ /^<\?xml /) {
341             $type = 'rdf';
342         } else {
343             $type = 'html';
344         }
345     }
346
347     my $lots;
348     my $bugCount;
349
350     if ($type eq 'html') {
351         my $lots;
352         my @qp;
353
354         # magicness
355         { no warnings; # this can go _very_ wrong easily
356
357           $lots = ($output !~ m/<FORM\s+METHOD=POST\s+ACTION="long_list.cgi">/osi); # if we got truncated, then this will be missing
358
359           # Instead of relying on being able to accurately count the
360           # number of bugs (which we can't do if there are more than
361           # 199), use the number that bugzilla tells us.
362           if ($output =~ /(One|\d+) bugs? found/o) {
363               $bugCount = $1;
364               if ($bugCount eq "One") {
365                   $bugCount = 1;
366               }
367           }
368
369           $output =~ s/<\/TABLE><TABLE .+?<\/A><\/TH>//gosi;
370           (undef, $output) = split(/Summary<\/A><\/TH>/osi, $output);
371           ($output, undef) = split(/<\/TABLE>/osi, $output);
372           $output =~ s/[\n\r]//gosi;
373           @qp = split(m/<TR VALIGN=TOP ALIGN=LEFT CLASS=[-A-Za-z0-9]+(?: style='.*?')?\s*?><TD>/osi, $output);
374         }
375
376         if (scalar(@qp) == 0) {
377             $bugCount = 0;
378         }
379
380         if (!$lots && $subtype eq 'bugs') {
381             if (scalar(@qp) > $maxRes) {
382                 $truncated = 1;
383                 @qp = @qp[0..$maxRes-1];
384             }
385
386             foreach (@qp) {
387                 if ($_) {
388                     # more magic
389                     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) {
390                         # bugid severity priority platform owner status resolution subject
391                         my %bug;
392                         ($bug{'id'}, $bug{'severity'}, $bug{'priority'}, $bug{'platform'}, $bug{'owner'}, $bug{'status'}, $bug{'resolution'}, $bug{'summary'}) = @d;
393                         push (@bugs, \%bug);
394                     }
395                 }
396             }
397         }
398     } elsif ($type eq 'xml') {
399         # We came from xml.cgi
400         my $parser = XML::LibXML->new();
401         my $tree = $parser->parse_string($output);
402         my $root = $tree->getDocumentElement;
403
404         my @xml_bugs = $root->getElementsByTagName('bug');
405         $bugCount = scalar(@xml_bugs);
406
407         if (scalar(@xml_bugs) > $maxRes) {
408             $truncated = 1;
409             @xml_bugs = @xml_bugs[0..$maxRes-1];
410         }
411
412         # OK, xml.cgi uses different names to the query stuff
413         # Take a deep breath, and use a mapping for the fields we
414         # care about
415         my %fieldMap = (
416                         'bug_id' => 'id',
417                         'bug_severity' => 'severity',
418                         'priority' => 'priority',
419                         'target_milestone' => 'target_milestone',
420                         'assigned_to' => 'owner',
421                         'bug_status' => 'status',
422                         'resolution' => 'resolution',
423                         'short_desc' => 'summary'
424                        );
425
426         foreach my $xml_bug(@xml_bugs) {
427             my %bug = {};
428             my $error = $xml_bug->getAttribute('error');
429             if (!defined $error) {
430                 foreach my $field (keys %fieldMap) {
431                     my @arr = $xml_bug->getElementsByTagName($field);
432                     if (@arr) {
433                         my $firstChild = $arr[0]->getFirstChild();
434                         if (defined $firstChild) {
435                             $bug{$fieldMap{$field}} = $firstChild->getData();
436                         }
437                     }
438                 }
439             }
440             else {
441                 my @arr = $xml_bug->getElementsByTagName('bug_id');
442                 $bug{'id'} = $arr[0]->getFirstChild->getData();
443                 $bug{'error'} = $error;
444             }
445             push @bugs, \%bug;
446         }
447     } elsif ($type eq 'rdf') {
448         my $parser = XML::LibXML->new();
449         my $tree = $parser->parse_string($output);
450         my $root = $tree->getDocumentElement;
451         my @rdf_bugs = $root->getElementsByTagName('bz:bug');
452
453         $bugCount = scalar(@rdf_bugs);
454
455         if (scalar(@rdf_bugs) > $maxRes) {
456             $truncated = 1;
457             @rdf_bugs = @rdf_bugs[0..$maxRes-1];
458         }
459
460         foreach my $rdf_bug (@rdf_bugs) {
461             my %bug = {};
462             my @children = $rdf_bug->getChildnodes();
463             foreach my $child (@children) {
464                 next if ($child->getLocalName() eq 'text');
465                 my $field = $child->getLocalName();
466                 if ($child->getFirstChild()) {
467                     my $val = $child->getFirstChild->getData();
468                     $bug{$field} = $val;
469                 }
470             }
471             push @bugs, \%bug;
472         }
473     } else {
474         return $self->SUPER::GotURI(@_);
475     }
476
477     # construct the response's preamble
478     my $preamble;
479     if ($bugCount == 0 && !$skipZaroo) {
480         $preamble = 'Zarro boogs found.';
481     } else {
482         my $bugCountStr;
483         if ($bugCount) {
484             $bugCountStr = "$bugCount bug" . ($bugCount == 1 ? '' : 's')
485               . " found";
486         }
487
488         if ($subtype eq 'total') {
489             $self->say($event, $bugCountStr);
490             return;
491         }
492
493         if ($lots) {
494             $preamble = $bugCountStr ? "$bugCountStr, which is too many for me to handle without running out of memory."
495               : 'Way too many bugs found. I gave up so as to not run out of memory.';
496             $preamble .= "$bugCountStr Try to narrow your search or something!";
497             $subtype = 'lots';
498         } elsif ($subtype ne 'bug' && $bugCount > 1) {
499             $preamble = $bugCountStr;
500             if ($truncated) {
501                 if ($event->{'channel'}) {
502                     $preamble .= '. Five shown, please message me for more.';
503                 } else {
504                     $preamble .= '. Will only show 20 results, please use the Bugzilla query form if you want more.';
505                 }
506             }
507         }
508     }
509
510     my $prefix;
511     if ( !$event->{'from'}
512          || grep {$_ eq $event->{'from'}} @{$self->{'skipPrefixFor'}} )
513     {
514         # they don't want to have the report prefixed with their name
515         $prefix = '';
516     } else {
517         $prefix = "$event->{'from'}: ";
518     }
519
520     if ($preamble) {
521         $self->say($event, "$prefix$preamble");
522     }
523
524     my $bug_link = $skipURI ? "" : "$self->{'bugsURI'}show_bug.cgi?id=";
525
526     # now send out the output
527     foreach my $bug (@bugs) {
528         if (!defined $bug->{'error'}) {
529             # Bugzilla doesn't give the TM by default, and we can't
530             # change this without using cookies, which aren't supported
531             # by the mozbot API. Later versions allow us to use a query param
532             # but we can't detect that that was accepted, which would break
533             # the HTML parsing
534             # xml.cgi gives us everything, so we can print this if we got
535             # results from there
536             # Maybe the list of columns to display could be a var, one day, after
537             # installations from source before Dec 2001 are no longer supported,
538             # or we can pass cookies
539             $self->say($event, $prefix .
540                        "Bug $bug_link$bug->{'id'} " .
541                        substr($bug->{'severity'} || $bug->{'bug_severity'}, 0, 3) . ", " .
542                        $bug->{'priority'} . ", " .
543                        ($bug->{'target_milestone'} ? "$bug->{'target_milestone'}, " : "") .
544                        ($bug->{'owner'} || $bug->{'assigned_to'}) . ", " .
545                        substr($bug->{'status'} || $bug->{'bug_status'},  0, 4) .
546                        ($bug->{'resolution'} ? " " . $bug->{'resolution'} : "") . ", " .
547                        substr($bug->{'summary'} || $bug->{'short_desc'} || $bug->{'short_short_desc'}, 0, 100));
548         } elsif ($bug->{'error'} eq 'NotFound') {
549             unless($skipZaroo) {
550                 $self->say($event, $prefix . "Bug $bug->{'id'} was not found.");
551             }
552         } elsif ($bug->{'error'} eq 'NotPermitted') {
553             $self->say($event, $prefix . "Bug $bug_link$bug->{'id'} is not accessible");
554         } else {
555             unless($skipZaroo) {
556                 $self->say($prefix . "Error accessing bug $bug->{'id'}: $bug->{'error'}");
557             }
558         }
559     }
560 }
561
562 sub CheckForBugMail {
563     my $self = shift;
564     my ($event) = @_;
565
566     my ($bug_log, $bug_file) = $self->GetBugLog();
567
568     my @log_lines;
569     if (defined $bug_log) {
570         # We need LOCK_EX because we're going to truncate it.
571         flock($bug_log, LOCK_EX);
572         @log_lines = $bug_log->getlines();
573         $bug_log->truncate(0)
574             or ($self->debug("Failed to truncate $bug_file: $!") && return);
575         flock($bug_log, LOCK_UN);
576         $bug_log->close() or $self->debug("Failed to close $bug_file: $!");
577         $self->debug("Read " . scalar(@log_lines) . " bugmail log lines.")
578             if @log_lines;
579     }
580     else {
581         # We will have already output a more detailed error from GetBugLog.
582         $self->debug("CheckForBugMail Failed: Couldn't read bugmail log.");
583         return;
584     }
585
586     # Hash to keep track of which channels we've mentioned which bug details 
587     # in, so we don't spew the same bug details over and over.
588     my %said_bug;
589
590     foreach my $line (@log_lines) {
591         chomp($line);
592         #$self->debug("Reading log line: $line");
593         my $sep = FIELD_SEPARATOR;
594         $line =~ /^(.+)$sep(.+)$sep(.+)$sep(.+)$sep(.+)$sep(.*)$sep(.*)$sep(.+)$/;
595         my ($bug_id, $product, $component, $who, $field, $old, $new, $message) =
596             ($1, $2, $3, $4, $5, $6, $7, $8);
597
598         # Skip this line if we never report anything for this field.
599         next if !grep($_ eq $field, @{$self->{'reportFields'}});
600
601         my @prod_mute_fields = 
602             split(/\s*,\s*/, $self->{'productMuteFields'}->{$product});
603         my @chan_list;
604         # Don't report to these channels if this product is muted for this field.
605         push (@chan_list, $self->CreateChannelList($product, $component))
606             unless grep($_ eq $field, @prod_mute_fields);
607
608         if ($field eq 'Product') {
609             my @old_mute_fields = 
610                 split(/\s*,\s*/, $self->{'productMuteFields'}->{$old});
611             push(@chan_list, $self->CreateChannelList($old, $component))
612                 unless grep($_ eq $field, @old_mute_fields);
613         }
614         elsif ($field eq 'Component') {
615             my @comp_mute_fields = @prod_mute_fields;
616             push(@comp_mute_fields, 
617                 ($self->{'productMuteFields'}->{$product. $sep . $component}));
618             # Don't report it if the product is muted for this field, or if
619             # this specific component is muted for this field.
620             push(@chan_list, $self->CreateChannelList($product, $old))
621                 unless grep($_ eq $field, @comp_mute_fields);
622         }
623         # Enable Mozbot to report both product and component of new bugs.
624         if (grep(lc($_) eq 'all', @{$self->{'reportComponent'}}) ||
625             grep(lc($_) eq lc($product), @{$self->{'reportComponent'}}) ||
626             grep(lc($_) eq lc($product.$sep.$component), @{$self->{'reportComponent'}})) {
627             $message =~ s/^New $product bug/New $product - $component bug/i;
628         }
629         unless ($self->ignoringMailProducedBy($who)) {
630             # Keep track of which channels we've told already, to avoid
631             # duplicate messages.
632             my %said_to;
633             foreach my $channel (@chan_list) {
634                 my @chan_mute_fields = 
635                     split(/\s*,\s*/, $self->{'channelMuteFields'}->{$channel});
636                 # Don't say it if we've said it before, or if this
637                 # field is muted in this channel.
638                 unless ( $said_to{$channel} 
639                          || grep($_ eq $field, @chan_mute_fields) ) 
640                 {
641                     # We can't use "local" here, or the target doesn't show
642                     # up properly in the GotURI after FetchBug.
643                     $event->{'target'} = $channel;
644                     $self->say($event, $message);
645                     my $bugids = "";
646                     # Special case for "duplicate of messages"
647                     if ($message =~ /DUPLICATE of bug (\d+)/) {
648                         my $dup_id = $1;
649                         $bugids = $dup_id unless $said_bug{$channel . $dup_id};
650                         $said_bug{$channel . $dup_id} = 1;
651                     }
652                     # Fetch bugs mentioned for dependent field changes
653                     if ($field eq 'OtherBugsDependingOnThis'
654                         || $field eq 'BugsThisDependsOn') {
655                         foreach my $id (split(/,/, $old . $new)) {
656                             $bugids = $id . " " . $bugids
657                                 unless $said_bug{$channel . $id};
658                             $said_bug{$channel . $id} = 1;
659                         }
660                     }
661                     if (! $said_bug{$channel . $bug_id}) {
662                         $bugids = $bug_id . " " . $bugids;
663                     }
664                     if ($bugids ne '') {
665                         if ($self->{'reportBugDetails'}) {
666                             $self->FetchBug($event, $bugids, 'bug');
667                         }
668                     }
669                     $said_to{$channel} = 1;
670                     $said_bug{$channel . $bug_id} = 1;
671                 } # unless $said_to
672             } # foreach @chan_list
673         } # unless ignoringMailProducedBy
674     } # foreach @log_lines
675 }
676
677 # A helper for CheckForBugMail.
678 sub CreateChannelList {
679     my $self = shift;
680     my ($product, $component) = @_;
681
682     my $chan_list = "";
683     ($chan_list .= $self->{'productReportChannels'}->{$product})
684         if $self->{'productReportChannels'}->{$product};
685
686     my $prodcomp = $product . FIELD_SEPARATOR . $component;
687     ($chan_list .= ',' . $self->{'productReportChannels'}->{$prodcomp})
688         if $self->{'productReportChannels'}->{$prodcomp};
689
690     return (split /\s*,\s*/, $chan_list);
691 }
692
693 # Creates the BUGMAIL_LOG file if it doesn't exist, and returns
694 # an open IO::File for it, and also the filename of that file.
695 sub GetBugLog {
696     my $self = shift;
697
698     my $file_name = dirname($0) . '/' . BUGMAIL_LOG;
699     # And we generally trust $bug_log to be an OK path, so untaint it now.
700     $file_name =~ /^(.*)$/;
701     $file_name = $1;
702     my $file = new IO::File($file_name, O_RDWR | O_CREAT, 0660)
703             or $self->debug("Could not open/create $file_name for reading" 
704                             . " incoming bugmail: $!");
705     return ($file, $file_name);
706 }
707
708 sub ignoringMailProducedBy {
709     my $self = shift;
710     my ($who) = @_;
711     return grep($_ eq $who, @{$self->{'mailIgnore'}}) ? 1 : 0;
712 }
713
714 sub ignoringCommentsTo {
715     my $self = shift;
716     my ($who) = @_;
717     foreach (@{$self->{'ignoreCommentsTo'}}) {
718         next unless $_; # Ignore blanks, happens when the array is empty (?)
719         return 1 if $who =~ m/^(?:.*[]\s,.;:\\\/=?!()<>{}[-])?\Q$_\E(?:[]\s,.;:\\\/=?!()<>{}[-].*)?$/is;
720     }
721     return 0;
722 }
723
724 sub ignoringCommentsFrom {
725     my $self = shift;
726     my ($who) = @_;
727     foreach (@{$self->{'ignoreCommentsFrom'}}) {
728         return 1 if $_ eq $who;
729     }
730     return 0;
731 }
732
733 sub needToFetchBug {
734     my ($self, $target, $time, $bug) = @_;
735     my $last = 0;
736     if (defined($self->{'bugsHistory'}->{$target}->{$bug})) {
737         $last = $self->{'bugsHistory'}->{$target}->{$bug};
738     }
739     if (($time-$last) > $self->{'backoffTime'}) {
740        return 1;
741     }
742     return 0;
743 }