Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #4 from throughnothing/multiple-projects

Support Multiple projects per channel
  • Loading branch information...
commit c0fec2b7d3ae7a892f12069e8998cb63a77666ca 2 parents f8216b0 + 543aa9b
@bigpresh authored
View
2  Makefile.PL
@@ -16,7 +16,7 @@ WriteMakefile(
'Bot::BasicBot::Pluggable::Module' => 0,
'Net::GitHub::V2' => 0,
'YAML' => 0,
- 'LWP::Simple' => 0,
+ 'LWP::UserAgent' => 0,
'JSON' => 0,
'URI::Title' => 0,
},
View
174 lib/Bot/BasicBot/Pluggable/Module/GitHub.pm
@@ -28,50 +28,61 @@ sub ng {
# Work out the repo we're using - we may have been given "user/repo", or, if
# we were given a channel name, look up the default repo for that channel
- my ($user,$project);
+ my ($user, $project);
if ($channelorproject =~ m{/}) {
($user, $project) = split '/', $channelorproject, 2;
} else {
- my $chanproject = $self->project_for_channel($channelorproject);
- ($user, $project) = split '/', $chanproject, 2;
+ my $chanprojects = $self->project_for_channel($channelorproject) || [];
+ ($user, $project) = split '/', @$chanprojects[0], 2;
}
return unless $user && $project;
- # If we've already got a suitable Net::GitHub::V2 object, use it:
if (my $ng = $net_github{"$user/$project"}) {
return $ng;
}
- # Right - assemble the params we need to give to Net::GitHub::V2
my %ngparams = (
owner => $user,
repo => $project,
);
-
# If authentication is needed, add that in too:
if (my $auth = $self->auth_for_project("$user/$project")) {
- my ($user, $token) = split /:/, $auth, 2;
- $ngparams{login} = $user;
+ my ($auth_user, $token) = split /:/, $auth, 2;
+ $ngparams{login} = $auth_user;
$ngparams{token} = $token;
$ngparams{always_Authorization} = 1;
}
+
return $net_github{"$user/$project"} = Net::GitHub::V2->new(%ngparams);
}
-# Find the name of the GitHub project for a given channel
-sub project_for_channel {
+# Find all of the GitHub projects for a given channel
+sub projects_for_channel {
my ($self, $channel) = @_;
- my $project_for_channel =
- $self->store->get('GitHub', 'project_for_channel');
- return $project_for_channel->{$channel};
+ my $projects_for_channel =
+ $self->store->get('GitHub', 'projects_for_channel');
+ return $projects_for_channel->{$channel};
}
# Alias for backwards compatibility
sub github_project {
my $self = shift;
- $self->project_for_channel(@_);
+ my $projects = $self->projects_for_channel(@_);
+ return @$projects[0] if $projects;
+ return undef;
+}
+
+# Accept a search term and return the first project that matches
+sub search_projects {
+ my ($self, $channel, $search) = @_;
+ my $projects = $self->projects_for_channel($channel);
+ return unless $projects;
+
+ for (@$projects) {
+ return $_ if $_ =~ /$search/i;
+ }
}
# Find auth details to use to access a channel's project
@@ -80,22 +91,27 @@ sub auth_for_project {
my $auth_for_project =
$self->store->get('GitHub', 'auth_for_project');
- return $auth_for_project->{$project};
-}
+ if ($auth_for_project->{$project}) {
+ return $auth_for_project->{$project};
+ } else {
+ # Return the default auth, if set
+ return $self->store->get('GitHub', 'default_auth');
+ }
+}
-# For each channel the bot is in, call project_for_channel() to find out what
-# project is appropriate for that channel, and return a hashref of
-# channel => project (leaving out channels for which no project is defined)
+# For each channel the bot is in, call projects_for_channel() to find out what
+# projects are appropriate for that channel, and return a hashref of
+# channel => projects (leaving out channels for which no project is defined)
sub channels_and_projects {
my $self = shift;
- my %project_for_channel;
+ my %projects_for_channel;
for my $channel ($self->bot->channels) {
- if (my $project = $self->project_for_channel($channel)) {
- $project_for_channel{$channel} = $project;
+ if (my $projects = $self->projects_for_channel($channel)) {
+ $projects_for_channel{$channel} = $projects;
}
}
- return \%project_for_channel;
+ return \%projects_for_channel;
}
# Support configuring project details for a channel (potentially with auth
@@ -108,37 +124,115 @@ sub said {
return unless $mess->{address} eq 'msg';
if ($mess->{body} =~ m{
- ^!setgithubproject \s+
+ ^!setgithubprojects \s+
(?<channel> \#\S+ ) \s+
- (?<project> \S+ )
- ( \s+ (?<auth> \S+ ) )?
+ (\S+\/\S+)+
}xi) {
- my $project_for_channel =
- $self->store->get('GitHub','project_for_channel') || {};
- $project_for_channel->{$+{channel}} = $+{project};
+ my $channel = $+{channel};
+ my $message = "OK, set projects for $channel: ";
+ my $projects_for_channel =
+ $self->store->get('GitHub','projects_for_channel') || {};
+ $projects_for_channel->{$channel} = []; #set to empty
+
+ while($mess->{body} =~ m{
+ \b(?<project> \S+\/\S+ )
+ }gxi)
+ {
+ my $project = $+{project};
+ push(@{$projects_for_channel->{$channel}}, $project);
+ # Invalidate any cached Net::GitHub object we might have,
+ # so the new settings are used
+ delete $net_github{$project};
+ $message .= " $project";
+ }
+
$self->store->set(
- 'GitHub', 'project_for_channel', $project_for_channel
+ 'GitHub', 'projects_for_channel', $projects_for_channel
);
+ return $message;
+ } elsif ($mess->{body} =~ /^!setgithubproject/i) {
+ return "Invalid usage. Try '!help github'";
+ } elsif ($mess->{body} =~ m{
+ ^!setdefaultauth \s+
+ (?<auth> \S+ )
+ }xi) {
+ $self->store->set('GitHub', 'default_auth', $+{auth});
+ return "Set default auth credentials for all projects";
+ } elsif ($mess->{body} =~ /^!setdefaultauth/i) {
+ return "Invalid usage. Try '!help github'";
+ } elsif ($mess->{body} =~ m{
+ ^!setauthforproject \s+
+ (?<project> \S+\/\S+) \s+
+ (?<auth> \S+ )
+ }xi) {
+ my ($project, $auth) = ($+{project}, $+{auth});
my $auth_for_project =
$self->store->get('GitHub', 'auth_for_project') || {};
- $auth_for_project->{$+{project}} = $+{auth};
+ $auth_for_project->{$project} = $auth;
$self->store->set(
'GitHub', 'auth_for_project', $auth_for_project
);
+ return "Set auth credentials for $project";
+ } elsif ($mess->{body} =~ /^!setauthforproject/i) {
+ return "Invalid usage. Try '!help github'";
+ } elsif ($mess->{body} =~ m{
+ ^!addgithubproject \s+
+ (?<channel> \#\S+ ) \s+
+ (?<project> \S+\/\S+)+
+ }xi){
+ my ($channel, $project) = ($+{channel}, $+{project});
+ my $projects_for_channel =
+ $self->store->get('GitHub','projects_for_channel') || {};
+ my $projects = $projects_for_channel->{$channel} || [];
+
+ # Check if project already exists
+ for (@$projects){
+ return "Project $_ already exists for channel $channel!"
+ if $_ eq $project;
+ }
- # Invalidate any cached Net::GitHub object we might have, so the new
- # settings are used
- delete $net_github{$+{project}};
+ push(@$projects, $project);
+ # Invalidate any cached Net::GitHub object we might have,
+ # so the new settings are used
+ delete $net_github{$project};
+ $projects_for_channel->{$channel} = $projects;
+ $self->store->set(
+ 'GitHub', 'projects_for_channel', $projects_for_channel
+ );
- my $message = "OK, project for $+{channel} set to $+{project}";
- if ($+{auth}) {
- $message .= " (using auth details supplied)";
+ return "OK, set project for $channel: $project";
+ } elsif ($mess->{body} =~ m{
+ ^!deletegithubproject \s+
+ (?<channel> \#\S+ ) \s+
+ (?<project> \S+\/\S+)+
+ }xi) {
+ my ($channel, $project) = ($+{channel}, $+{project});
+ my $projects_for_channel =
+ $self->store->get('GitHub','projects_for_channel') || {};
+ my $projects = $projects_for_channel->{$channel} || [];
+
+ # Check if project already exists
+ for (1..@$projects){
+ if ($projects->[$_] eq $project){
+ delete $projects->[$_];
+ delete $net_github{$project};
+ }
}
- return $message;
+ $projects_for_channel->{$channel} = $projects;
+ $self->store->set(
+ 'GitHub', 'projects_for_channel', $projects_for_channel
+ );
- } elsif ($mess->{body} =~ /^!setgithubproject/i) {
- return "Invalid usage. Try '!help github'";
+ return "OK, deleted project for $channel: $project";
+ } elsif ($mess->{body} =~ /^!showgithubprojects/i){
+ my $message;
+ my $projects_for_channel =
+ $self->store->get('GitHub','projects_for_channel') || {};
+ foreach my $key (keys $projects_for_channel){
+ $message .= "$key: @{$projects_for_channel->{$key}}\n";
+ }
+ return $message || "No GitHub projects set!";
}
return;
}
View
130 lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm
@@ -38,78 +38,80 @@ sub tick {
my $seen_issues = $json ? JSON::from_json($json) : {};
- # OK, for each channel, pull details of all issues from the API, and look
- # for changes
+ # OK, for each channel, pull details of all issues from the API
+ # and look for changes
my $channels_and_projects = $self->channels_and_projects;
- channel:
for my $channel (keys %$channels_and_projects) {
- my $project = $channels_and_projects->{$channel};
- my %notifications;
- warn "Looking for issues for $project for $channel";
-
- my $ng = $self->ng($project) or next channel;
-
- my $issues = $ng->issue->list('open');
-
- # Go through all currently-open issues and look for new/reopened ones
- for my $issue (@$issues) {
- my $issuenum = $issue->{number};
- my $details = {
- title => $issue->{title},
- url => $issue->{html_url},
- created_by => $issue->{user},
- };
-
- if (my $existing = $seen_issues->{$project}{$issuenum}) {
- if ($existing->{state} eq 'closed') {
- # It was closed before, but is now in the open feed, so it's
- # been re-opened
- push @{ $notifications{reopened} },
+ project:
+ for my $project (@{$channels_and_projects->{$channel}}){
+ my %notifications;
+ warn "Looking for issues for $project for $channel";
+
+ my $ng = $self->ng($project) or next project;
+
+ my $issues = $ng->issue->list('open');
+
+ # Go through all currently-open issues and
+ # look for new/reopened ones
+ for my $issue (@$issues) {
+ my $issuenum = $issue->{number};
+ my $details = {
+ title => $issue->{title},
+ url => $issue->{html_url},
+ created_by => $issue->{user},
+ };
+
+ if (my $existing = $seen_issues->{$project}{$issuenum}) {
+ if ($existing->{state} eq 'closed') {
+ # It was closed before, but is now in the open feed,
+ # so it's been re-opened
+ push @{ $notifications{reopened} },
+ [ $issuenum, $details ];
+ $existing->{state} = 'open';
+ }
+ } else {
+ # A new issue we haven't seen before
+ push @{ $notifications{opened} },
[ $issuenum, $details ];
- $existing->{state} = 'open';
+ $seen_issues->{$project}{$issuenum} = {
+ state => 'open',
+ details => $details,
+ };
}
- } else {
- # A new issue we haven't seen before
- push @{ $notifications{opened} },
- [ $issuenum, $details ];
- $seen_issues->{$project}{$issuenum} = {
- state => 'open',
- details => $details,
- };
}
- }
- # Now, go through ones we already know about - if we knew about them,
- # and they were open, but weren't in the list of open issues we fetched
- # above, they must now be closed
- for my $issuenum (keys %{ $seen_issues->{$project} }) {
- my $existing = $seen_issues->{$project}{$issuenum};
- my $current = grep {
- $_->{number} == $issuenum
- } @$issues;
-
- if ($existing->{state} eq 'open' && !$current) {
- # It was open before, but isn't in the list now - it must have
- # been closed.
- push @{ $notifications{closed} },
- [ $issuenum, $existing->{details} ];
- $existing->{state} = 'closed';
+ # Now, go through ones we already know about - if we knew
+ # about them, and they were open, but weren't in the list
+ # of open issues we fetched above, they must now be closed
+ for my $issuenum (keys %{ $seen_issues->{$project} }) {
+ my $existing = $seen_issues->{$project}{$issuenum};
+ my $current = grep {
+ $_->{number} == $issuenum
+ } @$issues;
+
+ if ($existing->{state} eq 'open' && !$current) {
+ # It was open before, but isn't in the list now
+ # it must have been closed.
+ push @{ $notifications{closed} },
+ [ $issuenum, $existing->{details} ];
+ $existing->{state} = 'closed';
+ }
}
- }
- # Announce any changes
- for my $type (keys %notifications) {
- my $s = scalar $notifications{$type} > 1 ? 's':'';
-
- $self->say(
- channel => $channel,
- body => "Issue$s $type : "
- . join ', ', map {
- sprintf "%d (%s) by %s : %s",
- $_->[0], # issue number
- @{$_->[1]}{qw(title created_by url)}
- } @{ $notifications{$type} }
- );
+ # Announce any changes
+ for my $type (keys %notifications) {
+ my $s = scalar $notifications{$type} > 1 ? 's':'';
+
+ $self->say(
+ channel => $channel,
+ body => "($project) Issue$s $type : "
+ . join ', ', map {
+ sprintf "%d (%s) by %s : %s",
+ $_->[0], # issue number
+ @{$_->[1]}{qw(title created_by url)}
+ } @{ $notifications{$type} }
+ );
+ }
}
}
View
6 lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm
@@ -37,6 +37,8 @@ sub said {
while ($mess->{body} =~ m{
(?:
\b
+ # Term to search for in project names
+ (?<search> \S+ )? \s*
# "Issue 42", "PR 42" or "Pull Request 42"
(?<thing> (?:issue|gh|pr|pull request) )
(?:\s+|-)?
@@ -53,6 +55,10 @@ sub said {
my $project = $+{project} || $self->github_project($mess->{channel});
return unless $project;
+ # Search through all the projects to see if the search word matches
+ $project = $self->search_projects($mess->{channel}, $+{search})
+ || $project;
+
# Get the Net::GitHub::V2 object we'll be using. (If we don't get one,
# for some reason, we can't do anything useful.)
my $ng = $self->ng($project) or return;
View
44 lib/Bot/BasicBot/Pluggable/Module/GitHub/PullRequests.pm
@@ -7,7 +7,7 @@ package Bot::BasicBot::Pluggable::Module::GitHub::PullRequests;
use strict;
use Bot::BasicBot::Pluggable::Module::GitHub;
use base 'Bot::BasicBot::Pluggable::Module::GitHub';
-use LWP::Simple ();
+use LWP::UserAgent;
use JSON;
sub help {
@@ -30,9 +30,16 @@ sub said {
return unless $pri == 2;
if ($mess->{body} =~ /^!pr (?: \s+ (\S+))?/xi) {
- my $check_projects = $1;
- $check_projects ||= $self->get('user_github_project');
- if (!$check_projects) {
+ my $search = $1;
+
+ my $project = $+{project} || $self->github_project($mess->{channel});
+ return unless $project;
+
+ # Search through all the projects to see if the search word matches
+ $project = $self->search_projects($mess->{channel}, $search)
+ || $project;
+
+ if (!$project) {
$self->reply(
$mess,
"No project(s) to check; either specify"
@@ -42,13 +49,12 @@ sub said {
);
return 1;
}
- for my $project (split /,/, $check_projects) {
- my $prs = $self->_get_pull_request_count($project);
- $self->say(
- channel => $mess->{channel},
- body => "Open pull requests for $project : $prs",
- );
- }
+
+ my $prs = $self->_get_pull_request_count($project);
+ $self->say(
+ channel => $mess->{channel},
+ body => "Open pull requests for $project : $prs",
+ );
return 1; # "swallow" this message
}
return 0; # This message didn't interest us
@@ -57,10 +63,20 @@ sub said {
sub _get_pull_request_count {
my ($self, $project) = @_;
+
my $url = "http://github.com/api/v2/json/pulls/" . $project;
- my $json = LWP::Simple::get($url)
- or return "Unknown - error fetching $url";
- my $pulls = JSON::from_json($json)
+ my $ua = LWP::UserAgent->new;
+ my $req = HTTP::Request->new(GET => $url);
+
+ # Auth if necessary
+ if (my $auth = $self->auth_for_project($project)) {
+ my ($user, $token) = split /:/, $auth, 2;
+ $req->authorization_basic("$user/token", "$token");
+ }
+
+ my $res = $ua->request($req) or return "Unknown - error fetching $url";
+
+ my $pulls = JSON::from_json($res->content)
or return "Unknown - error parsing API response";
my %pulls_by_author;
View
28 t/01-parse-config.t
@@ -19,6 +19,11 @@ sub get {
return unless $namespace eq 'GitHub';
return $self->{settings}{$key};
}
+sub set {
+ my ($self, $namespace, $key, $value) = @_;
+ return unless $namespace eq 'GitHub';
+ $self->{settings}{$key} = $value;
+}
# Subclass to override fetching of config setting from store
@@ -40,15 +45,15 @@ use Bot::BasicBot::Pluggable::Module::GitHub;
package main;
-use Test::More tests => 4;
+use Test::More tests => 6;
my $plugin = MockBot->new;
# Set some projects for channels, then we can test we get the right info back
$plugin->{_store} = MockStore->new({
- project_for_channel => {
- '#foo' => 'someuser/foo',
- '#bar' => 'bobby/tables',
+ projects_for_channel => {
+ '#foo' => [ 'someuser/foo' ],
+ '#bar' => [ 'bobby/tables', 'tom/drinks' ] ,
},
auth_for_project => {
'bobby/tables' => 'bobby:tables',
@@ -57,11 +62,16 @@ $plugin->{_store} = MockStore->new({
-is($plugin->project_for_channel('#foo'), 'someuser/foo',
+is_deeply ($plugin->projects_for_channel('#foo'), [ 'someuser/foo' ],
'Got expected project for a channel'
);
-is($plugin->project_for_channel('#fake'), undef,
+is_deeply ($plugin->projects_for_channel('#bar'),
+ [ 'bobby/tables', 'tom/drinks' ],
+ 'Got expected projects for a channel'
+);
+
+is($plugin->projects_for_channel('#fake'), undef,
'Got undef project for non-configured channel'
);
@@ -72,3 +82,9 @@ is($plugin->auth_for_project('fake/project'), undef,
'Got undef auth info for non-configured project'
);
+# Test default_auth if set
+$plugin->{_store}->set('GitHub', 'default_auth', 'default:auth');
+
+is($plugin->auth_for_project('someuser/foo'), 'default:auth',
+ 'Got expected default auth info for a project'
+);
Please sign in to comment.
Something went wrong with that request. Please try again.