mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-25 05:41:12 +00:00
Bug 292492: [BUGZILLA] Integrate bzbot's ability to receive and report bugmail into Bugzilla.bm
Patch By Max Kanat-Alexander <mkanat@bugzilla.org> r=colin
This commit is contained in:
parent
0b6a9e70f8
commit
dfa659632f
@ -7,15 +7,32 @@
|
||||
package BotModules::Bugzilla;
|
||||
use vars qw(@ISA);
|
||||
@ISA = qw(BotModules);
|
||||
1;
|
||||
|
||||
use XML::LibXML;
|
||||
use Fcntl qw(:DEFAULT :flock);
|
||||
use File::Basename;
|
||||
|
||||
# For parsing bugmail.log records. Must be the same as
|
||||
# FIELD_SEPARATOR in bugmail.pl.
|
||||
use constant FIELD_SEPARATOR => '::::';
|
||||
# The log file that we read to report bug changes.
|
||||
# This will be put in the directory returned by dirname($0).
|
||||
use constant BUGMAIL_LOG => 'BotModules/.bugmail.log';
|
||||
1;
|
||||
|
||||
# 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.
|
||||
|
||||
sub Initialise {
|
||||
my $self = shift;
|
||||
my $retval = $self->SUPER::Initialise(@_);
|
||||
my ($throw_away) = $self->GetBugLog();
|
||||
$throw_away->close() if $throw_away;
|
||||
return $retval;
|
||||
}
|
||||
|
||||
# RegisterConfig - Called when initialised, should call registerVariables
|
||||
sub RegisterConfig {
|
||||
my $self = shift;
|
||||
@ -28,7 +45,25 @@ sub RegisterConfig {
|
||||
['backoffTime', 1, 1, 120],
|
||||
['ignoreCommentsTo', 1, 1, ['']],
|
||||
['ignoreCommentsFrom', 1, 1, ['|']],
|
||||
['mailIgnore', 1, 1, []],
|
||||
['skipPrefixFor', 1, 1, []],
|
||||
# The keys for productReportChannels can be in the form of 'Product'
|
||||
# or 'Product::::Component'. The value is a comma-separated list of
|
||||
# channel names.
|
||||
['productReportChannels', 1, 1, {}],
|
||||
# The fields that you want notifications about.
|
||||
['reportFields', 1, 1, ['Resolution', 'Flag', 'Attachment Flag',
|
||||
'NewBug', 'NewAttach']],
|
||||
# Except in these products, you don't want notifications about
|
||||
# certain fields (key is product name, value is comma-separated
|
||||
# list of fields).
|
||||
['productMuteFields', 1, 1, {}],
|
||||
# And in these channels, you don't want notifications about certain
|
||||
# fields (the key is the channel name and the value is a
|
||||
# comma-separated list of fields).
|
||||
['channelMuteFields', 1, 1, {}],
|
||||
# How frequently we check for new bugmail we've received, in seconds.
|
||||
['updateDelay', 1, 1, 10],
|
||||
['mutes', 1, 1, ''], # "channel channel channel"
|
||||
);
|
||||
}
|
||||
@ -40,7 +75,14 @@ sub Help {
|
||||
'' => '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\'.'
|
||||
'bugs' => q{A simple DWIM search. Not very clever. ;-)}
|
||||
. q{ Syntax: '<query string> bugs' e.g. 'mozbot bugs'.},
|
||||
'ignore' => q{Causes the bot to stop reporting all bug changes}
|
||||
. q{ made by a particular user in the current channel.}
|
||||
. q{ Syntax: 'ignore <user@domain.com>' },
|
||||
'unignore' => q{Causes the bot to un-ignore a previously ignored}
|
||||
. q{ user. See 'ignore'}
|
||||
. q{ for more details.},
|
||||
);
|
||||
if ($self->isAdmin($event)) {
|
||||
$commands{'mute'} = 'Disable watching for bug numbers in a channel. Syntax: mute bugzilla in <channel>';
|
||||
@ -49,10 +91,61 @@ sub Help {
|
||||
return \%commands;
|
||||
}
|
||||
|
||||
# Schedule - called when bot connects to a server, to install any schedulers
|
||||
# use $self->schedule($event, $delay, $times, $data)
|
||||
# where $times is 1 for a single event, -1 for recurring events,
|
||||
# and a +ve number for an event that occurs that many times.
|
||||
sub Schedule {
|
||||
my $self = shift;
|
||||
my ($event) = @_;
|
||||
$self->schedule($event, \$self->{'updateDelay'}, -1, 'Bugzilla-BugMail');
|
||||
return $self->SUPER::Schedule($event);
|
||||
}
|
||||
|
||||
sub Scheduled {
|
||||
my $self = shift;
|
||||
my ($event, @data) = @_;
|
||||
if ($data[0] eq 'Bugzilla-BugMail') {
|
||||
$self->CheckForBugMail($event);
|
||||
} else {
|
||||
return $self->SUPER::Scheduled($event, @data);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub Told {
|
||||
my $self = shift;
|
||||
my ($event, $message) = @_;
|
||||
if ($message =~ m/^ \s* # some optional whitespace
|
||||
if ($message =~ /^\s*ignore (.+)[?!.\s]*$/) {
|
||||
my $user = $1;
|
||||
# If we aren't already ignoring them...
|
||||
if (!grep($_ eq $user, @{$self->{'mailIgnore'}})) {
|
||||
push (@{$self->{'mailIgnore'}}, $user);
|
||||
$self->saveConfig();
|
||||
$self->say($event,
|
||||
"$event->{'from'}: OK, ignoring changes produced by $user.");
|
||||
}
|
||||
else {
|
||||
$self->say($event,
|
||||
"$event->{'from'}: $user is already being ignored.");
|
||||
}
|
||||
}
|
||||
elsif ($message =~ /^\s*unignore (.+)[?!.\s]*$/) {
|
||||
my $user = $1;
|
||||
my %ignoredUsers = map { $_ => 1 } @{$self->{'mailIgnore'}};
|
||||
# If we are already ignoring them...
|
||||
if ($ignoredUsers{$user}) {
|
||||
delete $ignoredUsers{$user};
|
||||
$self->{'mailIgnore'} = [keys %ignoredUsers];
|
||||
$self->saveConfig();
|
||||
$self->say($event,
|
||||
"$event->{'from'}: OK, $user is no longer being ignored.");
|
||||
}
|
||||
else {
|
||||
$self->say($event, "$event->{'from'}: $user wasn't being ignored.");
|
||||
}
|
||||
}
|
||||
elsif ($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",
|
||||
@ -391,7 +484,9 @@ sub GotURI {
|
||||
}
|
||||
|
||||
my $prefix;
|
||||
if (grep {$_ eq $event->{'from'}} @{$self->{'skipPrefixFor'}}) {
|
||||
if ( !$event->{'from'}
|
||||
|| grep {$_ eq $event->{'from'}} @{$self->{'skipPrefixFor'}} )
|
||||
{
|
||||
# they don't want to have the report prefixed with their name
|
||||
$prefix = '';
|
||||
} else {
|
||||
@ -440,6 +535,131 @@ sub GotURI {
|
||||
}
|
||||
}
|
||||
|
||||
sub CheckForBugMail {
|
||||
my $self = shift;
|
||||
my ($event) = @_;
|
||||
|
||||
my ($bug_log, $bug_file) = $self->GetBugLog();
|
||||
|
||||
my @log_lines;
|
||||
if (defined $bug_log) {
|
||||
# We need LOCK_EX because we're going to truncate it.
|
||||
flock($bug_log, LOCK_EX);
|
||||
@log_lines = $bug_log->getlines();
|
||||
$bug_log->truncate(0)
|
||||
or ($self->debug("Failed to truncate $bug_file: $!") && return);
|
||||
flock($bug_log, LOCK_UN);
|
||||
$bug_log->close() or $self->debug("Failed to close $bug_file: $!");
|
||||
$self->debug("Read " . scalar(@log_lines) . " bugmail log lines.")
|
||||
if @log_lines;
|
||||
}
|
||||
else {
|
||||
# We will have already output a more detailed error from GetBugLog.
|
||||
$self->debug("CheckForBugMail Failed: Couldn't read bugmail log.");
|
||||
return;
|
||||
}
|
||||
|
||||
# Hash to keep track of which channels we've mentioned which bug details
|
||||
# in, so we don't spew the same bug details over and over.
|
||||
my %said_bug;
|
||||
|
||||
foreach my $line (@log_lines) {
|
||||
chomp($line);
|
||||
#$self->debug("Reading log line: $line");
|
||||
my $sep = FIELD_SEPARATOR;
|
||||
$line =~ /^(.+)$sep(.+)$sep(.+)$sep(.+)$sep(.+)$sep(.*)$sep(.*)$sep(.+)$/;
|
||||
my ($bug_id, $product, $component, $who, $field, $old, $new, $message) =
|
||||
($1, $2, $3, $4, $5, $6, $7, $8);
|
||||
|
||||
# Skip this line if we never report anything for this field.
|
||||
next if !grep($_ eq $field, @{$self->{'reportFields'}});
|
||||
|
||||
my @prod_mute_fields =
|
||||
split(/\s*,\s*/, $self->{'productMuteFields'}->{$product});
|
||||
my @chan_list;
|
||||
# Don't report to these channels if this product is muted for this field.
|
||||
push (@chan_list, $self->CreateChannelList($product, $component))
|
||||
unless grep($_ eq $field, @prod_mute_fields);
|
||||
|
||||
if ($field eq 'Product') {
|
||||
my @old_mute_fields =
|
||||
split(/\s*,\s*/, $self->{'productMuteFields'}->{$old});
|
||||
push(@chan_list, $self->CreateChannelList($old, $component))
|
||||
unless grep($_ eq $field, @old_mute_fields);
|
||||
}
|
||||
elsif ($field eq 'Component') {
|
||||
my @comp_mute_fields = @prod_mute_fields;
|
||||
push(@comp_mute_fields,
|
||||
($self->{'productMuteFields'}->{$product. $sep . $component}));
|
||||
# Don't report it if the product is muted for this field, or if
|
||||
# this specific component is muted for this field.
|
||||
push(@chan_list, $self->CreateChannelList($product, $old))
|
||||
unless grep($_ eq $field, @comp_mute_fields);
|
||||
}
|
||||
|
||||
unless ($self->ignoringMailProducedBy($who)) {
|
||||
# Keep track of which channels we've told already, to avoid
|
||||
# duplicate messages.
|
||||
my %said_to;
|
||||
foreach my $channel (@chan_list) {
|
||||
my @chan_mute_fields =
|
||||
split(/\s*,\s*/, $self->{'channelMuteFields'}->{$channel});
|
||||
# Don't say it if we've said it before, or if this
|
||||
# field is muted in this channel.
|
||||
unless ( $said_to{$channel}
|
||||
|| grep($_ eq $field, @chan_mute_fields) )
|
||||
{
|
||||
# We can't use "local" here, or the target doesn't show
|
||||
# up properly in the GotURI after FetchBug.
|
||||
$event->{'target'} = $channel;
|
||||
$self->say($event, $message);
|
||||
$self->FetchBug($event, $bug_id, 'bug')
|
||||
unless $said_bug{$channel . $bug_id};
|
||||
$said_to{$channel} = 1;
|
||||
$said_bug{$channel . $bug_id} = 1;
|
||||
} # unless $said_to
|
||||
} # foreach @chan_list
|
||||
} # unless ignoringMailProducedBy
|
||||
} # foreach @log_lines
|
||||
}
|
||||
|
||||
# A helper for CheckForBugMail.
|
||||
sub CreateChannelList {
|
||||
my $self = shift;
|
||||
my ($product, $component) = @_;
|
||||
|
||||
my $chan_list = "";
|
||||
($chan_list .= $self->{'productReportChannels'}->{$product})
|
||||
if $self->{'productReportChannels'}->{$product};
|
||||
|
||||
my $prodcomp = $product . FIELD_SEPARATOR . $component;
|
||||
($chan_list .= ',' . $self->{'productReportChannels'}->{$prodcomp})
|
||||
if $self->{'productReportChannels'}->{$prodcomp};
|
||||
|
||||
return (split /\s*,\s*/, $chan_list);
|
||||
}
|
||||
|
||||
# Creates the BUGMAIL_LOG file if it doesn't exist, and returns
|
||||
# an open IO::File for it, and also the filename of that file.
|
||||
sub GetBugLog {
|
||||
my $self = shift;
|
||||
|
||||
my $file_name = dirname($0) . '/' . BUGMAIL_LOG;
|
||||
# And we generally trust $bug_log to be an OK path, so untaint it now.
|
||||
$file_name =~ /^(.*)$/;
|
||||
$file_name = $1;
|
||||
my $file = new IO::File($file_name, O_RDWR | O_CREAT, 0660)
|
||||
or $self->debug("Could not open/create $file_name for reading"
|
||||
. " incoming bugmail: $!");
|
||||
return ($file, $file_name);
|
||||
}
|
||||
|
||||
sub ignoringMailProducedBy {
|
||||
my $self = shift;
|
||||
my ($who) = @_;
|
||||
return grep($_ eq $who, @{$self->{'mailIgnore'}}) ? 1 : 0;
|
||||
}
|
||||
|
||||
sub ignoringCommentsTo {
|
||||
my $self = shift;
|
||||
my ($who) = @_;
|
||||
|
514
webtools/mozbot/BotModules/BugzillaMailHandler.pl
Executable file
514
webtools/mozbot/BotModules/BugzillaMailHandler.pl
Executable file
@ -0,0 +1,514 @@
|
||||
#!/usr/bin/perl -w
|
||||
#
|
||||
# The contents of this file are subject to the Mozilla Public
|
||||
# License Version 1.1 (the "License"); you may not use this file
|
||||
# except in compliance with the License. You may obtain a copy of
|
||||
# the License at http://www.mozilla.org/MPL/
|
||||
#
|
||||
# Software distributed under the License is distributed on an "AS
|
||||
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
|
||||
# implied. See the License for the specific language governing
|
||||
# rights and limitations under the License.
|
||||
#
|
||||
# The Original Code is the Mozilla IRC Bot
|
||||
#
|
||||
# The Initial Developer of the Original Code is Max Kanat-Alexander.
|
||||
# Portions developed by Max Kanat-Alexander are Copyright (C) 2005
|
||||
# Max Kanat-Alexander. All Rights Reserved.
|
||||
#
|
||||
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
#
|
||||
# This is loosely based off an older bugmail.pl by justdave.
|
||||
|
||||
# bugmail.pl requires that you have X-Bugzilla-Product and
|
||||
# X-Bugzilla-Component headers in your incoming email. In 2.19.2 and above,
|
||||
# this is easy. You just add two lines to your newchangedmail param:
|
||||
# X-Bugzilla-Product: %product%
|
||||
# X-Bugzilla-Component: %component%
|
||||
# If you're running 2.18, you can do the same thing, but you need to
|
||||
# apply the patch from bug 175222 <https://bugzilla.mozilla.org/show_bug.cgi?id=175222>
|
||||
# to your installation.
|
||||
|
||||
use strict;
|
||||
use Fcntl qw(:flock);
|
||||
use File::Basename;
|
||||
|
||||
use Email::Simple;
|
||||
|
||||
#####################################################################
|
||||
# Constants And Initial Setup
|
||||
#####################################################################
|
||||
|
||||
# What separates Product//Component//[Fields], etc. in a log line.
|
||||
use constant FIELD_SEPARATOR => '::::';
|
||||
|
||||
# These are fields that are multi-select fields, so when somebody
|
||||
# adds something to them, the verbs "added to " or "removed from" should
|
||||
# be used instead of the verb "changed" or "set".
|
||||
# It's a hash, where the names of the fields are the keys, and the values are 1.
|
||||
# The fields are named as they appear in the "What" part of a bugmail "changes"
|
||||
# table.
|
||||
use constant MULTI_FIELDS => {
|
||||
'CC' => 1, 'Group' => 1, 'Keywords' => 1,
|
||||
'BugsThisDependsOn' => 1, 'OtherBugsDependingOnThis' => 1,
|
||||
};
|
||||
|
||||
# Some fields have such long names for the "What" column that their names
|
||||
# wrap. Normally, our code would think that those fields were two different
|
||||
# fields. So, instead, we store a list of strings to use as an argument
|
||||
# to "grep" for the field names that we need to "unwrap."
|
||||
use constant UNWRAP_WHAT => (
|
||||
qr/^Attachment .\d+$/, qr/^Attachment .\d+ is$/, qr/^OtherBugsDep/,
|
||||
);
|
||||
|
||||
# Should be whatever Bugzilla::Util::find_wrap_point (or FindWrapPoint)
|
||||
# breaks on, in Bugzilla.
|
||||
use constant BREAKING_CHARACTERS => (' ',',','-');
|
||||
|
||||
# The maximum width, in characters, of each field of the "diffs" table.
|
||||
use constant WIDTH_WHAT => 19;
|
||||
use constant WIDTH_REMOVED => 28;
|
||||
use constant WIDTH_ADDED => 28;
|
||||
|
||||
# Our one command-line argument.
|
||||
our $debug = $ARGV[0] && $ARGV[0] eq "-d";
|
||||
|
||||
# XXX - This probably should happen in the log directory instead, but that's
|
||||
# more difficult to figure out reliably.
|
||||
my $bug_log = dirname($0) . '/.bugmail.log';
|
||||
|
||||
#####################################################################
|
||||
# Utility Functions
|
||||
#####################################################################
|
||||
|
||||
# When processing the "diffs" table in a bug, some lines wrap. This
|
||||
# function properly appends the "next" line for unwrapping to an
|
||||
# existing string.
|
||||
sub append_diffline ($$$$) {
|
||||
my ($append_to, $prev_line, $append_line, $max_width) = @_;
|
||||
my $ret_line = $append_to;
|
||||
|
||||
debug_print("Appending Line: [$append_line] Prev Line: [$prev_line]");
|
||||
debug_print("Prev Line Len: " . length($prev_line)
|
||||
. " Max Width: $max_width");
|
||||
|
||||
# If the previous line is the width of the entire column, we
|
||||
# assume that we were forcibly wrapped in the middle of a word,
|
||||
# and no space is needed. We only add the space if we were actually
|
||||
# given a non-empty string to append.
|
||||
if ($append_line && length($prev_line) != $max_width) {
|
||||
debug_print("Adding a space unless we find a breaking character.");
|
||||
# However, sometimes even if we have a very short line, if it ended
|
||||
# in a "breaking character" like '-' then we also don't need a space.
|
||||
$ret_line .= " " unless grep($prev_line =~ /$_$/, BREAKING_CHARACTERS);
|
||||
}
|
||||
$ret_line .= $append_line;
|
||||
debug_print("Appended Line: [$ret_line]");
|
||||
return $ret_line;
|
||||
}
|
||||
|
||||
# Prints a string if debugging is on. Appends a newline so you don't have to.
|
||||
sub debug_print ($) {
|
||||
(print STDERR $_[0] . "\n") if $debug;
|
||||
}
|
||||
|
||||
# Helps with generate_log for Flag messages.
|
||||
sub flag_action ($$) {
|
||||
my ($new, $old) = @_;
|
||||
|
||||
my $line = "";
|
||||
|
||||
my ($flag_name, $action, $requestee) = split_flag($new);
|
||||
debug_print("Parsing Flag Change: Name: [$flag_name] Action: [$action]")
|
||||
if $new;
|
||||
|
||||
if (!$new) {
|
||||
$line .= " cancelled $old";
|
||||
}
|
||||
elsif ($action eq '+') {
|
||||
$line .= " granted $flag_name";
|
||||
}
|
||||
elsif ($action eq '-') {
|
||||
$line .= " denied $flag_name";
|
||||
}
|
||||
else {
|
||||
$line .= " requested $flag_name from";
|
||||
if ($requestee) {
|
||||
$line .= " " . $requestee;
|
||||
}
|
||||
else {
|
||||
$line .= " the wind";
|
||||
}
|
||||
}
|
||||
|
||||
return $line;
|
||||
}
|
||||
|
||||
# Takes the $old and $new from a Flag field and returns a hash,
|
||||
# where the key is the name of the field, and the value is an
|
||||
# array, where the first item is the old flag string, and the
|
||||
# new flag string is the second item.
|
||||
sub parse_flags ($$) {
|
||||
my ($new, $old) = @_;
|
||||
|
||||
my %flags;
|
||||
foreach my $old_item (split /\s*,\s*/, $old) {
|
||||
my ($flag_name) = split_flag($old_item);
|
||||
$flags{$flag_name} = [$old_item, ''];
|
||||
}
|
||||
foreach my $new_item (split /\s*,\s*/, $new) {
|
||||
my ($flag_name) = split_flag($new_item);
|
||||
if (!exists $flags{$flag_name}) {
|
||||
$flags{$flag_name} = ['', $new_item];
|
||||
}
|
||||
else {
|
||||
$flags{$flag_name}[1] = $new_item;
|
||||
}
|
||||
}
|
||||
|
||||
return %flags;
|
||||
}
|
||||
|
||||
# Returns a list: the name of the flag, the action (+/-/?), and
|
||||
# the requestee (if that exists).
|
||||
sub split_flag ($) {
|
||||
my ($flag) = @_;
|
||||
if ($flag) {
|
||||
$flag =~ /\s*([^\?]+)(\+|-|\?)(?:\((.*)\))?$/;
|
||||
return ($1, $2, $3);
|
||||
}
|
||||
return ();
|
||||
}
|
||||
|
||||
# Cuts the whitespace off the ends of a string.
|
||||
# Lovingly borrowed from Bugzilla::Util.
|
||||
sub trim ($) {
|
||||
my ($str) = @_;
|
||||
if ($str) {
|
||||
$str =~ s/^\s+//g;
|
||||
$str =~ s/\s+$//g;
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# Main Subroutines
|
||||
#####################################################################
|
||||
|
||||
# Returns a hash, where the keys are the names of fields. The values
|
||||
# are lists, where the first item is what was removed and the second
|
||||
# item is what was added.
|
||||
sub parse_diffs ($) {
|
||||
my ($body_lines) = @_;
|
||||
my @body = @$body_lines;
|
||||
|
||||
my %changes = ();
|
||||
|
||||
# Read in the What | Removed | Added table.
|
||||
# End|of|table will never get run
|
||||
my @diff_table = grep (/^.*\|.*\|.*$/, @body);
|
||||
# The first line is the "What|Removed|Added" line, so goes away.
|
||||
shift(@diff_table);
|
||||
|
||||
my ($prev_what, $prev_added, $prev_removed);
|
||||
# We can't use foreach because we need to modify @diff_table.
|
||||
while (defined (my $line = shift @diff_table)) {
|
||||
$line =~ /^(.*)\|(.*)\|(.*)$/;
|
||||
my ($what, $removed, $added) = (trim($1), trim($2), trim($3));
|
||||
# These are used to set $prev_removed and $prev_added later.
|
||||
my ($this_removed, $this_added) = ($removed, $added);
|
||||
|
||||
debug_print("---RawLine: $what|$removed|$added\n");
|
||||
|
||||
# If we have a field name in the What field.
|
||||
if ($what) {
|
||||
# If this is a two-line "What" field...
|
||||
if( grep($what =~ $_, UNWRAP_WHAT) ) {
|
||||
# Then we need to grab the next line right now.
|
||||
my $next_line = shift @diff_table;
|
||||
debug_print("Next Line: $next_line");
|
||||
$next_line =~ /^(.*)\|(.*)\|(.*)$/;
|
||||
my ($next_what, $next_removed, $next_added) =
|
||||
(trim($1), trim($2), trim($3));
|
||||
|
||||
debug_print("Two-line What: [$what][$next_what]");
|
||||
$what = append_diffline($what, $what, $next_what,
|
||||
WIDTH_WHAT);
|
||||
if ($next_added) {
|
||||
debug_print("Two-line Added: [$added][$next_added]");
|
||||
$added = append_diffline($added, $added,
|
||||
$next_added, WIDTH_ADDED);
|
||||
}
|
||||
if ($next_removed) {
|
||||
debug_print("Two-line Removed: [$removed][$next_removed]");
|
||||
$removed = append_diffline($removed, $removed,
|
||||
$next_removed, WIDTH_REMOVED);
|
||||
}
|
||||
}
|
||||
|
||||
$changes{$what} = [$removed, $added];
|
||||
debug_print("Filed as $what: $removed => $added");
|
||||
|
||||
# We only set $prev_what if we actually had a $what to put in it.
|
||||
$prev_what = $what;
|
||||
}
|
||||
# Otherwise we're getting data from a previous What.
|
||||
else {
|
||||
my $new_removed = append_diffline($changes{$prev_what}[0],
|
||||
$prev_removed, $removed, WIDTH_REMOVED);
|
||||
my $new_added = append_diffline($changes{$prev_what}[1],
|
||||
$prev_added, $added, WIDTH_ADDED);
|
||||
|
||||
$changes{$prev_what} = [$new_removed, $new_added];
|
||||
debug_print("Filed as $prev_what: $removed => $added");
|
||||
}
|
||||
|
||||
($prev_removed, $prev_added) = ($this_removed, $this_added);
|
||||
}
|
||||
|
||||
return %changes;
|
||||
}
|
||||
|
||||
# Takes a reference to an array of lines and returns a hashref
|
||||
# containing data for a buglog entry.
|
||||
# Returns undef if the bug should not be entered into the log.
|
||||
sub parse_mail ($) {
|
||||
my ($mail_lines) = @_;
|
||||
my $mail_text = join('', @$mail_lines);
|
||||
my $email = Email::Simple->new($mail_text);
|
||||
|
||||
debug_print("Parsing Message " . $email->header('Message-ID'));
|
||||
|
||||
my $body = $email->body;
|
||||
my @body_lines = split("\n", $body);
|
||||
|
||||
my %bug_info;
|
||||
|
||||
# Bug ID
|
||||
my $subject = $email->header('Subject');
|
||||
|
||||
if ($subject !~ /^\s*\[Bug (\d+)\] /i) {
|
||||
debug_print("Not bug: $subject");
|
||||
return undef;
|
||||
}
|
||||
$bug_info{'bug_id'} = $1;
|
||||
debug_print("Bug $bug_info{bug_id} found.");
|
||||
|
||||
# Ignore Dependency mails
|
||||
# XXX - This should probably be an option in the mozbot instead
|
||||
if (my ($dep_line) =
|
||||
grep /bug (\d+), which changed state\.\s*$/, @body_lines)
|
||||
{
|
||||
debug_print("Dependency change ignored: $dep_line.");
|
||||
return undef;
|
||||
}
|
||||
|
||||
# Product
|
||||
$bug_info{'product'} = $email->header('X-Bugzilla-Product');
|
||||
unless ($bug_info{'product'}) {
|
||||
debug_print("X-Bugzilla-Product header not found.");
|
||||
return undef;
|
||||
}
|
||||
debug_print("Product '$bug_info{product}' found.");
|
||||
|
||||
# Component
|
||||
$bug_info{'component'} = $email->header('X-Bugzilla-Component');
|
||||
unless ($bug_info{'component'}) {
|
||||
debug_print("X-Bugzilla-Component header not found.");
|
||||
return undef;
|
||||
}
|
||||
debug_print("Component '$bug_info{component}' found.");
|
||||
|
||||
# New or Changed, and getting who did it.
|
||||
if ($subject =~ /^\s*\[Bug \d+\]\s*New: /i) {
|
||||
$bug_info{'new'} = 1;
|
||||
debug_print("Bug is New.");
|
||||
my ($reporter) = grep /^\s+ReportedBy:\s/, @body_lines;
|
||||
$reporter =~ s/^\s+ReportedBy:\s//;
|
||||
$bug_info{'who'} = $reporter;
|
||||
}
|
||||
elsif ( my ($changer_line) = grep /^\S+\schanged:$/, @body_lines) {
|
||||
$changer_line =~ /^(\S+)\s/;
|
||||
$bug_info{'who'} = $1;
|
||||
}
|
||||
elsif ( my ($comment_line) =
|
||||
grep /^-* Additional Comments From /, @body_lines )
|
||||
{
|
||||
$comment_line =~ /^-* Additional Comments From (\S+) /;
|
||||
$bug_info{'who'} = $1;
|
||||
} else {
|
||||
debug_print("Could not determine who made the change.");
|
||||
return undef;
|
||||
}
|
||||
debug_print("Who = $bug_info{who}");
|
||||
|
||||
# Attachment
|
||||
my $attachid;
|
||||
if (($attachid) = grep /^Created an attachment \(id=\d+\)/, @body_lines) {
|
||||
$attachid =~ /^Created an attachment \(id=(\d+)\)/;
|
||||
$bug_info{'attach_id'} = $1;
|
||||
debug_print("attach_id: $bug_info{attach_id}");
|
||||
}
|
||||
|
||||
# Duplicate
|
||||
my $dupid;
|
||||
if (($dupid) = grep /marked as a duplicate of \d+/, @body_lines) {
|
||||
$dupid =~ /marked as a duplicate of (\d+)/;
|
||||
$bug_info{'dup_of'} = $1;
|
||||
debug_print("Got dup_of: $bug_info{dup_of}");
|
||||
}
|
||||
|
||||
# Figure out where the diff table ends, and where comments start.
|
||||
my $comments_start_at = 0;
|
||||
foreach my $check_line (@body_lines) {
|
||||
last if $check_line =~ /^-* Additional Comments From /;
|
||||
$comments_start_at++;
|
||||
}
|
||||
|
||||
debug_print("Comments start at line $comments_start_at.");
|
||||
my @diff_lines = @body_lines[0 .. ($comments_start_at - 1)];
|
||||
my %diffs = parse_diffs(\@diff_lines);
|
||||
$bug_info{'diffs'} = \%diffs;
|
||||
|
||||
return \%bug_info;
|
||||
}
|
||||
|
||||
# Takes the %bug_info hash returned from parse_mail and
|
||||
# makes it into one or more lines for the bugmail log.
|
||||
# BugMail Log Lines have the following format:
|
||||
# ID::::Product::::Component::::Who::::FieldName::::OldValue::::NewValue::::message
|
||||
# OldValue and NewValue can be empty.
|
||||
# FieldName will be 'NewBug' for new bugs, and 'NewAttach' for new attachments.
|
||||
# Each line ends with a newline, except the last one.
|
||||
sub generate_log ($) {
|
||||
my ($bug_info) = @_;
|
||||
|
||||
my $prefix = $bug_info->{'bug_id'} . FIELD_SEPARATOR
|
||||
. $bug_info->{'product'} . FIELD_SEPARATOR
|
||||
. $bug_info->{'component'} . FIELD_SEPARATOR
|
||||
. $bug_info->{'who'} . FIELD_SEPARATOR;
|
||||
|
||||
my @lines;
|
||||
|
||||
# New bugs are easy to handle, so let's handle them first.
|
||||
if ($bug_info->{'new'}) {
|
||||
push(@lines, $prefix . 'NewBug' . FIELD_SEPARATOR
|
||||
# Old and New are empty.
|
||||
. FIELD_SEPARATOR . FIELD_SEPARATOR
|
||||
. "New $bug_info->{product} bug $bug_info->{bug_id}"
|
||||
. " filed by $bug_info->{who}.");
|
||||
}
|
||||
|
||||
if ($bug_info->{'attach_id'}) {
|
||||
push(@lines, $prefix . 'NewAttach' . FIELD_SEPARATOR
|
||||
# Old and New are empty.
|
||||
. FIELD_SEPARATOR . FIELD_SEPARATOR
|
||||
. "$bug_info->{'who'} added attachment $bug_info->{'attach_id'}"
|
||||
. " to bug $bug_info->{'bug_id'}.");
|
||||
}
|
||||
|
||||
# And now we handle changes by going over all the diffs, one by one.
|
||||
my %diffs = %{$bug_info->{'diffs'}};
|
||||
foreach my $field (keys %diffs) {
|
||||
my $old = $diffs{$field}[0];
|
||||
my $new = $diffs{$field}[1];
|
||||
|
||||
# For attachments, we don't want to include the bug number in
|
||||
# the output.
|
||||
$field =~ s/^(Attachment)( .)(\d+)/$1/;
|
||||
my $attach_id = $3;
|
||||
|
||||
# Flags get a *very* special handling.
|
||||
if ($field =~ /Flag$/) {
|
||||
my %flags = parse_flags($new, $old);
|
||||
foreach my $flag (keys %flags) {
|
||||
my ($old_flag, $new_flag) = @{$flags{$flag}};
|
||||
my $line = $prefix . $field . FIELD_SEPARATOR
|
||||
. $old_flag . FIELD_SEPARATOR
|
||||
. $new_flag . FIELD_SEPARATOR
|
||||
. $bug_info->{'who'};
|
||||
$line .= flag_action($new_flag, $old_flag);
|
||||
if ($field =~ /^Attachment/) {
|
||||
$line .= " for attachment $attach_id";
|
||||
}
|
||||
$line .= " on bug $bug_info->{bug_id}.";
|
||||
push(@lines, $line);
|
||||
}
|
||||
}
|
||||
|
||||
# All other, non-Flag fields.
|
||||
else {
|
||||
my $line = $prefix . $field . FIELD_SEPARATOR
|
||||
. $old . FIELD_SEPARATOR . $new . FIELD_SEPARATOR
|
||||
. $bug_info->{who};
|
||||
# Some fields require the verbs "added" and "removed", like the
|
||||
# CC field.
|
||||
if (MULTI_FIELDS->{$field}) {
|
||||
($line .= " added $new to") if $new;
|
||||
($line .= " and") if $new && $old;
|
||||
($line .= " removed $old from") if $old;
|
||||
$line .= " the $field field on bug $bug_info->{bug_id}.";
|
||||
}
|
||||
# If we didn't remove anything, only added something.
|
||||
elsif (!$old) {
|
||||
$line .= " set the $field field on bug"
|
||||
. " $bug_info->{bug_id} to $new.";
|
||||
}
|
||||
# If we didn't add anything, only removed something.
|
||||
elsif (!$new) {
|
||||
$line .= " cleared the $field '$old' from bug"
|
||||
. " $bug_info->{bug_id}.";
|
||||
}
|
||||
# If we changed a field from one value to another.
|
||||
else {
|
||||
$line .= " changed the $field on bug"
|
||||
. " $bug_info->{bug_id} from $old to $new.";
|
||||
}
|
||||
push(@lines, $line);
|
||||
}
|
||||
}
|
||||
|
||||
debug_print("Generated Log Lines.");
|
||||
debug_print("Log Line: $_") foreach (@lines);
|
||||
|
||||
return join("\n", @lines);
|
||||
}
|
||||
|
||||
# Takes a string and appends it to the buglog.
|
||||
sub append_log ($) {
|
||||
my ($string) = @_;
|
||||
|
||||
(open FILE, ">>" . $bug_log)
|
||||
or die "Couldn't open bug log file $bug_log: $!";
|
||||
debug_print("Waiting for a lock on the log...");
|
||||
flock(FILE, LOCK_EX);
|
||||
print FILE $string . "\n";
|
||||
flock(FILE, LOCK_UN);
|
||||
debug_print("Printed lines to log and unlocked file.");
|
||||
close FILE;
|
||||
}
|
||||
|
||||
|
||||
#####################################################################
|
||||
# Main Script
|
||||
#####################################################################
|
||||
|
||||
debug_print("\n\n");
|
||||
|
||||
unless (-e $bug_log) {
|
||||
print STDERR "$bug_log does not exist, so I assume that mozbot is not"
|
||||
. " running. Discarding incoming message.\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
my @mail_array = <STDIN>;
|
||||
my $bug_info = parse_mail(\@mail_array);
|
||||
|
||||
if (defined $bug_info) {
|
||||
my $log_lines = generate_log($bug_info);
|
||||
# If we got an email with just a comment, $log_lines will be empty.
|
||||
append_log($log_lines) if $log_lines;
|
||||
}
|
||||
|
||||
debug_print("All done!");
|
||||
exit;
|
91
webtools/mozbot/BotModules/BugzillaMailHandler.txt
Normal file
91
webtools/mozbot/BotModules/BugzillaMailHandler.txt
Normal file
@ -0,0 +1,91 @@
|
||||
BugzillaMailHandler.pl is a script that takes in mail from a
|
||||
Bugzilla installation and possibly reports information about that
|
||||
mail to specified channels.
|
||||
|
||||
Basically, with BugzillaMailHandler.pl, you can use MozBot to inform
|
||||
you about updates to bugs. For the Bugzilla project, we use this to
|
||||
inform us whenever a bug is filed, whenever an attachment is added,
|
||||
and whenever a bug is fixed. We also have it let us know about certain
|
||||
flags, so that we can go handle those flags quickly.
|
||||
|
||||
To use BugzillaMailHandler.pl:
|
||||
|
||||
1) Start mozbot, and load the Bugzilla.bm module.
|
||||
|
||||
2) Set up your MTA (sendmail, postfix, exim, qmail, etc.) to pipe all
|
||||
mail coming to a certain address into the script instead of a local
|
||||
mailbox.
|
||||
|
||||
Your MTA must be able to write to files owned by the user that mozbot
|
||||
is running as. For example, on my local system, my mozbot is run
|
||||
as a user called "mozbot." I run postfix, so I have postfix become
|
||||
the "mozbot" user before running BugzillaMailHandler.pl.
|
||||
|
||||
3) Now, all bugmail coming in to BugzillaMailHandler will start producing
|
||||
input in BotModules/.bugmail.log (a hidden file). Mail that isn't in
|
||||
the standard Bugzilla format will be discarded. Mails that just have
|
||||
comments, or just inform that a dependency has been RESOLVED will be
|
||||
ignored.
|
||||
|
||||
4) Now, you need to tell your bot to start reporting certain Bugzilla
|
||||
Products to certain channels. In the future, there will be a command
|
||||
for this, but for now you have to do it manually. There is a variable
|
||||
in the Bugzilla module called "productReportChannels." It's a hash --
|
||||
the keys are names of products, and the values are comma-separated
|
||||
lists of channels.
|
||||
|
||||
5) Once you set that variable, your mozbot will start reporting changes
|
||||
to the specified products, in the specified channels.
|
||||
|
||||
However, it won't report *all* changes -- it will only report the
|
||||
changes to fields that are specified in the "reportFields" variable,
|
||||
which is a list of fields. Most fields have the *name that they would
|
||||
have in a Bugzilla email*, in the "What" column of the table where
|
||||
the mail shows bug changes.
|
||||
|
||||
There are some special fields:
|
||||
|
||||
Attachment Flag - Any attachment flag change.
|
||||
NewBug - When a new bug is filed.
|
||||
NewAttach - When a new attachment is posted to a bug.
|
||||
|
||||
Now, your mozbot should be up and running and reporting the changes
|
||||
that you want!
|
||||
|
||||
Other Notes
|
||||
-----------
|
||||
|
||||
There are a few other features that you can use to fine-tune how MozBot
|
||||
reports bug changes. First, anybody (not just a bot admin) can tell the
|
||||
bot to temporarily stop reporting changes from a certain Bugzilla user:
|
||||
|
||||
ignore user@domain.com
|
||||
|
||||
And to turn back on notifications about that user:
|
||||
|
||||
unignore user@domain.com
|
||||
|
||||
There are also some variables you can use to configure how mozbot reports
|
||||
changes, and what changes he reports:
|
||||
|
||||
channelMuteFields - A hash, where the key is the name of a channel, and
|
||||
the value is a comma-separated list of Fields, just
|
||||
like they would show up in the reportFields var.
|
||||
Changes to these fields will *not* be reported in
|
||||
the specified channels, but will still be reported
|
||||
in the other channels mozbot is configured to announce
|
||||
things to.
|
||||
|
||||
productMuteFields - A hash, where the key is the name of a Product in
|
||||
Bugzilla, and the value is a comma-separated list
|
||||
of Fields, just like they would show up in the
|
||||
reportFields var.
|
||||
Changes to the specified Fields on the specified
|
||||
products will not be reported to any channel, ever.
|
||||
|
||||
updateDelay - How often mozbot checks for information in the
|
||||
.bugmail.log file. Usually you can keep this at the
|
||||
default, unless you want to increase it for some reason.
|
||||
|
||||
Questions about this functionality can be asked in #mozwebtools on
|
||||
irc.mozilla.org.
|
Loading…
Reference in New Issue
Block a user