From 031bd4d5ec37c3c4f1d332169b3775c648c71d41 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Wed, 8 Mar 2017 15:04:29 +0000 Subject: [PATCH 01/21] use Net::GitHub::V3 in GitHub main module --- lib/Bot/BasicBot/Pluggable/Module/GitHub.pm | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm index 22e2452..d40a4cf 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm @@ -9,7 +9,7 @@ use base 'Bot::BasicBot::Pluggable::Module'; # want. use strict; -use Net::GitHub::V2; +use Net::GitHub::V3; our $VERSION = '0.04'; @@ -45,18 +45,21 @@ sub ng { # Right - assemble the params we need to give to Net::GitHub::V2 my %ngparams = ( - owner => $user, - repo => $project, +# 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; - $ngparams{token} = $token; - $ngparams{always_Authorization} = 1; +# my ($user, $token) = split /:/, $auth, 2; +# $ngparams{login} = $user; +# $ngparams{token} = $token; +# $ngparams{always_Authorization} = 1; + $ngparams{access_token} = $auth; } - return $net_github{"$user/$project"} = Net::GitHub::V2->new(%ngparams); + my $api = Net::GitHub::V3->new(%ngparams); + $api->set_default_user_repo($user, $project); + return $net_github{"$user/$project"} = $api; } From 5634d702e753d9b9ab2c94cca14bd8d14352b540 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Wed, 8 Mar 2017 15:10:33 +0000 Subject: [PATCH 02/21] update Announce feature to support multiple projects, colour, short links, and fix bug in spamming all issues on initial run --- .../Pluggable/Module/GitHub/Announce.pm | 154 +++++++++++++++--- 1 file changed, 131 insertions(+), 23 deletions(-) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm index 9e4efc8..26a1b2e 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm @@ -5,6 +5,7 @@ package Bot::BasicBot::Pluggable::Module::GitHub::Announce; use strict; +use WWW::Shorten::GitHub; use Bot::BasicBot::Pluggable::Module::GitHub; use base 'Bot::BasicBot::Pluggable::Module::GitHub'; use JSON; @@ -13,7 +14,7 @@ our $VERSION = 0.02; sub help { return < } close $fh; + } else { + $first_run = 1; + } my $seen_issues = $json ? JSON::from_json($json) : {}; # OK, for each channel, pull details of all issues from the API, and look # for changes - my $channels_and_projects = $self->channels_and_projects; + my $announce_for_channel = + $self->store->get('GitHub','announce_for_channel') || {}; channel: - for my $channel (keys %$channels_and_projects) { - my $project = $channels_and_projects->{$channel}; - my %notifications; + for my $channel (keys %$announce_for_channel) { + my $dfltproject = $self->github_project($channel) || ''; + my $dfltuser = $dfltproject && $dfltproject =~ m{^([^/]+)} ? $1 : ''; + project: + for my $project (@{ $announce_for_channel->{$channel} || [] }) { warn "Looking for issues for $project for $channel"; + my %notifications; - my $ng = $self->ng($project) or next channel; + my $ng = $self->ng($project) or next project; - my $issues = $ng->issue->list('open'); + my $issues = $ng->issue->repos_issues({state => 'open'}); # Go through all currently-open issues and look for new/reopened ones for my $issue (@$issues) { @@ -57,7 +67,8 @@ sub tick { my $details = { title => $issue->{title}, url => $issue->{html_url}, - created_by => $issue->{user}, + type => ($issue->{pull_request} ? 'pull' : 'issue'), + by => $issue->{user}{login}, }; if (my $existing = $seen_issues->{$project}{$issuenum}) { @@ -91,27 +102,87 @@ sub tick { 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} }, + my $state = 'closed'; + my $by; + if ($existing->{details}{type} eq 'pull' ) { + my $pr = $ng->pull_request->pull($issuenum); + if ($pr->{merged}) { + $state = 'merged'; + $by = $pr->{merged_by}{login}; + } + } + unless (defined $by) { + my $issue = $ng->issue->issue($issuenum); + $by = $issue->{closed_by}{login}; + } + $existing->{details}{by} = $by; + push @{ $notifications{$state} }, [ $issuenum, $existing->{details} ]; $existing->{state} = 'closed'; } } + my $in = $project eq $dfltproject ? '' : $project =~ m{^\Q$dfltuser\E/(.*)$} ? " in $1" : " in $project"; # 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} } - ); + for my $type (qw(opened reopened merged closed)) { + next unless $notifications{$type}; + my $s = scalar @{$notifications{$type}} > 1 ? 's':''; + my %nt = (issue => "\cC52Issue", pull => "\cC29Pull request"); + my %tc = ('closed' => "\cC55", 'opened' => "\cC52", reopened => "\cC52", merged => "\cC46"); + for my $t (qw(issue pull)) { + my @not = grep { $_->[1]{type} eq $t } @{ $notifications{$type} }; + $self->say( + channel => $channel, + body => "$nt{$t}$s\cC $tc{$type}$type\cC$in: " + . join ', ', map { + sprintf "\cC43%d\cC (\cC59%s\cC) by \cB%s\cB: \cC73%s\cC", + $_->[0], # issue number + @{$_->[1]}{qw(title by)}, + makeashorterlink($_->[1]{url}) + } @not + ) if @not && !$first_run; + } } - } + + my $heads = $ng->git_data->ref('heads'); + my @push_not; + for my $head (@{$heads||[]}) { + my $ref = $head->{ref}; + $ref =~ s{^refs/heads/}{}; + my $sha = $head->{object}{sha}; + my $ex = $seen_issues->{'__heads__'.$project}{$ref}; + if ($ex ne $sha) { + my $commit = $ng->git_data->commit($sha); + if ($commit && !exists $commit->{error}) { + my $title = ( split /\n+/, $commit->{commit}{message} )[0]; + my $url = $commit->{html_url}; + push @push_not, [ + $ref, + ($commit->{author}{login}||$commit->{committer}{login}||$commit->{commit}{author}{name}||$commit->{commit}{committer}{name}), + $title, + $project, + $url + ]; + } else { + push @push_not, [$ref,$sha]; + } + $seen_issues->{'__heads__'.$project}{$ref}=$sha; + } + } + if (@push_not) { + my $in = $project eq $dfltproject ? '' : $project =~ m{^\Q$dfltuser\E/(.*)$} ? " in $1" : " in $project"; + $self->say( + channel => $channel, + body => "New commits$in " . join ', ', map { + @$_>2 ? ( + sprintf "on branch %s by \cB%s\cB - \cC59%s\cC: \cC73%s\cC", + $_->[0], $_->[1], $_->[2], makeashorterlink('https://github.com/'.$_->[3].'/tree/'.$_->[0])) + : sprintf 'branch %s now at %s', @{$_}[0,1] + } @push_not + ) unless $first_run || $notifications{merged}; + } + + }} my $store_json = JSON::to_json($seen_issues); # Store the updated issue details: @@ -123,3 +194,40 @@ sub tick { } + +# Support configuring project details for a channel (potentially with auth +# details) via a msg. This is a bit too tricky to just leave the Vars module to +# handle, I think. (Note that each of the modules which inherit from us will +# get this method; one of them will catch it and handle it.) +sub said { + my ($self, $mess, $pri) = @_; + return unless $pri == 2; + return unless $mess->{address} eq 'msg'; + + if ($mess->{body} =~ m{ + ^!(? add | del )githubannounce \s+ + (? \#\S+ ) \s+ + (? \S+ ) + }xi) { + my $announce_for_channel = + $self->store->get('GitHub','announce_for_channel') || {}; + my @projects = @{ $announce_for_channel->{$+{channel}} || [] }; + if (lc $+{action} eq 'del') { + @projects = grep { lc $_ ne lc $+{project} } @projects; + } else { + unless (grep { lc $_ eq lc $+{project} } @projects) { + push @projects, $+{project}; + } + } + $announce_for_channel->{$+{channel}} = \@projects; + $self->store->set( + 'GitHub', 'announce_for_channel', $announce_for_channel + ); + + return "OK, $+{action}ed $+{project} for $+{channel}."; + + } elsif ($mess->{body} =~ /^!(?:add|del)githubannounce/i) { + return "Invalid usage. Try '!help github'"; + } + return; +} From 217b7b123b08c11058a69ec77b94c9ff93825d13 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Wed, 8 Mar 2017 15:14:20 +0000 Subject: [PATCH 03/21] implement repeat filtering, colouring, pull requests and github link lookups in EasyLinks module --- .../Pluggable/Module/GitHub/EasyLinks.pm | 272 +++++++++++++++--- 1 file changed, 239 insertions(+), 33 deletions(-) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm index a90b62b..32dfa55 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm @@ -5,24 +5,66 @@ package Bot::BasicBot::Pluggable::Module::GitHub::EasyLinks; use strict; +use WWW::Shorten::GitHub; use Bot::BasicBot::Pluggable::Module::GitHub; use base 'Bot::BasicBot::Pluggable::Module::GitHub'; use URI::Title; +use List::Util qw(min max); +use Mojo::DOM; sub help { return <{ $msg } ||= +{}; + + my $now = time; + + my $user_time = $mr->{$who} || 0; + my $other_time = $mr->{'@'} || 0; + + $mr->{'@'} = $mr->{$who} = $now; + + my $block = $user_time > $now - $mass_stop_user || $other_time > $now - $mass_stop_other; + warn "blocking " . substr(_strip_codes($msg),0,14) . ".." . max($now-$user_time,$now-$other_time) if $block; + !$block + +} + +sub _expire_mass_blocks { + my $now = time; + for my $ch (keys %mass_blocker) { + for my $msg (keys %{ $mass_blocker{$ch} }) { + if ($mass_blocker{ $ch }{ $msg }{'@'} < $now - $mass_stop_user) { + delete $mass_blocker{$ch}{$msg}; + } + } + } +} sub said { my ($self, $mess, $pri) = @_; @@ -33,32 +75,151 @@ sub said { # Loop through matching things in the message body, assembling quick links # ready to return. my @return; + unless ($mess->{body} =~ m{://git}) { match: while ($mess->{body} =~ m{ (?: \b # "Issue 42", "PR 42" or "Pull Request 42" - (? (?:issue|gh|pr|pull request) ) + (? (?:issue|pr|pull request) ) (?:\s+|-)? + \#? (? \d+) +# | +# (?:^|\s+) +# \# +# (? \d+) | # Or a commit SHA - (? [0-9a-f]{6,}) + (? [0-9a-f]{6,40}) ) # Possibly with a specific project repo ("user/repo") appeneded - (?: \s* \@ \s* (? \S+/\S+) )? + (?: (?: \s* \@ \s* | \s+ in \s+ ) (? \S+/?\S+) )? + \b }gxi ) { + # First, extract what kind of thing we're looking at, and normalise it a + # little, then go on to handle it. + my $thing = $+{thing}; + my $thingnum = $+{num}; + + if ($+{sha}) { + $thing = 'commit'; + $thingnum = $+{sha}; +# } elsif ($+{hnum}) { +# $thing = 'issue'; +# $thingnum = $+{hnum}; + } my $project = $+{project} || $self->github_project($mess->{channel}); + if ($project !~ m{/}) { + if ($self->github_project($mess->{channel}) =~ m{^(.*?)/} ) { + $project = "$1/$project"; + } else { + return; + } + } return unless $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; - # First, extract what kind of thing we're looking at, and normalise it a - # little, then go on to handle it. + + warn "OK, about to try to handle $thing $thingnum for $project"; + + # Right, handle it in the approriate way + if ($thing =~ /Issue|GH|pr|pull request/i) { + warn "Handling issue $thingnum"; + my $issue = $ng->issue->issue($thingnum); + my $pr; + if (!exists $issue->{error} && $issue->{pull_request}) { + $pr = $ng->pull_request->pull($thingnum); + if (exists $pr->{error}) { + $pr = undef; + } + } + if (exists $issue->{error}) { + push @return, $issue->{error}; + next match; + } + push @return, sprintf "%s \cC43%d\cC (\cC59%s\cC) by \cB%s\cB - \cC73%s\cC%s \{%s\cC\}", + (exists $issue->{pull_request} ? "\cC29Pull request" : "\cC52Issue"), + $thingnum, + $issue->{title}, + _dehih($issue->{user}{login}), + makeashorterlink($issue->{html_url}), + ($issue->{labels}&&@{$issue->{labels}}?" [".(join",",map{$_->{name}}@{$issue->{labels}})."]":""), +$pr&&$pr->{merged_at}?"\cC46merged on ".($pr->{merged_at}=~s/T.*//r): +$issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$issue->{state}." since ".($issue->{created_at}=~s/T.*//r); + } + + # Similarly, pull requests: +# if ($thing =~ /(?:pr|pull request)/i) { +# warn "Handling pull request $thingnum"; +# # TODO: send a pull request to add support for fetching details of +# # pull requests to Net::GitHub::V2, so we can handle PRs on private +# # repos appropriately. +# my $pull_url = "https://github.com/$project/pull/$thingnum"; +# my $title = URI::Title::title($pull_url); +# push @return, "Pull request $thingnum ($title) - $pull_url"; +# } + + # If it was a commit: + if ($thing eq 'commit') { + warn "Handling commit $thingnum"; + my $commit = $ng->git_data->commit($thingnum); + if ($commit && !exists $commit->{error}) { + my $title = ( split /\n+/, $commit->{message} )[0]; + my $url = $commit->{html_url}; + + # Currently, the URL given doesn't include the host, but that + # might perhaps change in future, so play it safe: +# $url = "https://github.com$url" unless $url =~ /^http/; +# $url =~ s{https://api.github.com/repos/(.*?)/commits/}{https://github.com/$1/commit/}; + push @return, sprintf "Commit \cC43$thingnum\cC (\cC59%s\cC) by \cB%s\cB on %s - \cC73%s", + $title, + _dehih($commit->{author}{login}||$commit->{committer}{login}||$commit->{author}{name}||$commit->{committer}{name}), + ($commit->{author}{date}=~s/T.*//r), + makeashorterlink($url); + } else { + # We purposefully don't show a message on IRC here, as we guess + # what might be a SHA, so we could be annoying saying that we + # didn't match a commit when someone said a word that just + # happened to look like it could be the start of a SHA. + warn "No commit details for $thingnum \@ $project/$thingnum"; + } + } + } + } + + unless (@return) { + match: + while ($mess->{body} =~ m{ + \b + https?://github.com/(? \S+/\S+)/ + (?: + (? (?: issues|pull ))/ + (? \d+) + (?: + (?: + /commits/ (? [0-9a-f]{6,40}) + | + /files + ) + (?: [?] [^#\s] +)? (? [#] diff\S* )? + )? + | + commit/ (? [0-9a-f]{6,40}) + (?: [?] [^#\s]+ )? (? [#] diff\S* )? + | + (? blob)/ + (? [^?#\s] +) + (?: [?] [^#\s]+ )? (? [#] L\S* ) + ) + \b + }gxi + ) { my $thing = $+{thing}; my $thingnum = $+{num}; @@ -67,47 +228,89 @@ sub said { $thingnum = $+{sha}; } + my $project = $+{project}; + return unless $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; + my $stop = $+{stop}; + + warn "OK, about to try to handle $thing $thingnum for $project"; + if ($stop) { + return unless $stop =~ s/([LR])(\d+)(?:-\1(\d+))?$//; + my ($lr, $line, $line2) = ($1, $2, $3); + next unless $line; + $line2 = $line unless defined $line2; + my $req = ($thing eq 'commit' || $thing eq 'blob') + ? HTTP::Request->new( GET => "https://github.com/$project/$thing/$thingnum") + : HTTP::Request->new( GET => "https://github.com/$project/$thing/$thingnum/files") ; + $req->accept_decodable; + my $res = $ng->_make_request($req); + my $dom = Mojo::DOM->new($res->decoded_content); + my @lines = map { + map { + my $x = $_; + $x->descendant_nodes->grep(sub{ $_->type ne "text" })->map('strip'); + $x->all_text(0) + } $dom->find("$stop$lr$_")->map('parent')->map('find', '.blob-code-inner')->flatten->each + } $line..$line2; + if (@lines) { + my $tag = "$lr$line"; + my $ret = $lines[0]; + if ($line2 > $line) { + $tag .= "-$line2"; + $ret = join ' ', map { /^\s*(.*?)\s*$/ ? $1 : $_ } @lines; + if (length $ret > 400) { + (substr $ret, 290 - 3 - 3 - length $tag) = '...'; + } + } + push @return, "$tag: $ret"; + } + next; + } + # Right, handle it in the approriate way - if ($thing =~ /Issue|GH/i) { + elsif ($thing ne 'commit') { warn "Handling issue $thingnum"; - my $issue = $ng->issue->view($thingnum); + my $issue = $ng->issue->issue($thingnum); + my $pr; + if (!exists $issue->{error} && $issue->{pull_request}) { + $pr = $ng->pull_request->pull($thingnum); + if (exists $pr->{error}) { + $pr = undef; + } + } if (exists $issue->{error}) { push @return, $issue->{error}; next match; } - push @return, sprintf "Issue %d (%s) - %s", + push @return, sprintf "%s \cC43%d\cC (\cC59%s\cC) by \cB%s\cB - \cC73%s\cC%s \{%s\cC\}", + (exists $issue->{pull_request} ? "\cC29Pull request" : "\cC52Issue"), $thingnum, $issue->{title}, - $issue->{html_url}; - } - - # Similarly, pull requests: - if ($thing =~ /(?:pr|pull request)/i) { - warn "Handling pull request $thingnum"; - # TODO: send a pull request to add support for fetching details of - # pull requests to Net::GitHub::V2, so we can handle PRs on private - # repos appropriately. - my $pull_url = "https://github.com/$project/pull/$thingnum"; - my $title = URI::Title::title($pull_url); - push @return, "Pull request $thingnum ($title) - $pull_url"; + _dehih($issue->{user}{login}), + $project, + ($issue->{labels}&&@{$issue->{labels}}?" [".(join",",map{$_->{name}}@{$issue->{labels}})."]":""), +$pr&&$pr->{merged_at}?"\cC46merged on ".($pr->{merged_at}=~s/T.*//r): +$issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$issue->{state}." since ".($issue->{created_at}=~s/T.*//r); } # If it was a commit: - if ($thing eq 'commit') { + elsif ($thing eq 'commit') { warn "Handling commit $thingnum"; - my $commit = $ng->commit->show($thingnum); + my $commit = $ng->git_data->commit($thingnum); if ($commit && !exists $commit->{error}) { my $title = ( split /\n+/, $commit->{message} )[0]; - my $url = $commit->{url}; + my $url = $commit->{html_url}; - # Currently, the URL given doesn't include the host, but that - # might perhaps change in future, so play it safe: - $url = "https://github.com$url" unless $url =~ /^http/; - push @return, sprintf "Commit $thingnum (%s) - %s", + push @return, sprintf "Commit \cC43$thingnum\cC (\cC59%s\cC) by \cB%s\cB on %s - \cC73%s", $title, - $url; + _dehih($commit->{author}{login}||$commit->{committer}{login}||$commit->{author}{name}||$commit->{committer}{name}), + ($commit->{author}{date}=~s/T.*//r), + $project; } else { # We purposefully don't show a message on IRC here, as we guess # what might be a SHA, so we could be annoying saying that we @@ -117,7 +320,10 @@ sub said { } } } + } + @return = grep { _check_mass($mess->{who}, $mess->{channel}, $_) } @return; + _expire_mass_blocks(); return join "\n", @return; } From 6e52f030f4e96826dcc0e1391fb51eaae156c1ac Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Wed, 8 Mar 2017 15:16:31 +0000 Subject: [PATCH 04/21] implement a new IssueSearch module --- MANIFEST | 1 + .../Pluggable/Module/GitHub/IssueSearch.pm | 130 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm diff --git a/MANIFEST b/MANIFEST index 0440eb8..3146d64 100644 --- a/MANIFEST +++ b/MANIFEST @@ -6,6 +6,7 @@ lib/Bot/BasicBot/Pluggable/Module/GitHub.pm lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm lib/Bot/BasicBot/Pluggable/Module/GitHub/PullRequests.pm +lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm t/00-load.t t/01-parse-config.t t/manifest.t diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm new file mode 100644 index 0000000..620043e --- /dev/null +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm @@ -0,0 +1,130 @@ +package Bot::BasicBot::Pluggable::Module::GitHub::IssueSearch; +use strict; +use WWW::Shorten::GitHub; +use Bot::BasicBot::Pluggable::Module::GitHub; +use base 'Bot::BasicBot::Pluggable::Module::GitHub'; +use LWP::Simple (); +use List::Util 'min'; +use JSON; + +sub help { + return < query [in ] +HELPMSG +} + +sub _dehih { + my $r = shift; + $r =~ s/^(.)(.*)$/$1\cB\cB$2/g; + $r +} + +sub said { + my ($self, $mess, $pri) = @_; + + return unless $pri == 2; + + return if $mess->{who} =~ /^Not-/; + return if $mess->{body} =~ m{://git}; + my $body = $mess->{body}; + + my $readdress = $mess->{channel} ne 'msg' && $body =~ s/\s+@\s+(\S+)[.]?\s*$// ? $1 : ''; + + if ($body =~ /^find \s+ + (?: (? \d+) \s+ )? + (?:(? open | closed | merged ) \s+)? + (? issue | pr | pull \s+ request )s? \s+ + (?: (?: with | matching ) \s+ )? + (? \S+ (?:\s+\S+)* ) /xi) { + my $realcount = $+{count} || 1; + my $count = min 3, $realcount; + my $expr = $+{expr}; + my $type = $+{type}; + $type = 'pr' if $type =~ /^p/i; + my $status = $+{status}; + my $project; + if ($expr =~ s/\s+ in \s+ (? \S+) \s* $//xi) { + $project = $+{project}; + } + $project ||= $self->github_project($mess->{channel}); + $expr = "is:\L$status\E $expr" if $status; + $expr =~ s{ (?:^|\s) \K ( -? ) \[ ( .+? ) \] (?=\s|$) }{ + join ' ', map { + $1 . 'label:' . ($_ =~ /\s/ ? qq{"$_"} : $_) + } split ',', $2 + }gex; + my $orig_expr = $expr; + my $search_type = 'issues'; + if ('pr' eq lc $type && $expr !~ /\b is:pr \b/xi) { + $expr = "is:pr $expr"; + $search_type = 'pulls'; + } + if ($project !~ m{/}) { + if ($self->github_project($mess->{channel}) =~ m{^(.*?)/} ) { + $project = "$1/$project"; + } else { + return; + } + } + return unless $project; + my $ng = $self->ng($project) or return; + warn "sending search repo:$project $expr"; + my $res = $ng->search->issues({q => "repo:$project $expr"}); + unless ($res && $res->{items}) { + warn "no result for query $expr."; + return; + } + my @ret; + while ($count-- && @{$res->{items}}) { + my $issue = shift @{$res->{items}}; + my $pr; + if (!exists $issue->{error} && $issue->{pull_request}) { + $pr = $ng->pull_request->pull($issue->{number}); + if (exists $pr->{error}) { + $pr = undef; + } + } + if (exists $issue->{error}) { + push @ret, $issue->{error}; + next; + } + push @ret, sprintf "%s \cC43%d\cC (\cC59%s\cC) by \cB%s\cB - \cC73%s\cC%s \{%s\cC\}", + (exists $issue->{pull_request} ? "\cC29Pull request" : "\cC52Issue"), + $issue->{number}, + $issue->{title}, + _dehih($issue->{user}{login}), + makeashorterlink($issue->{html_url}), + ($issue->{labels}&&@{$issue->{labels}}?" [".(join",",map{$_->{name}}@{$issue->{labels}})."]":""), +$pr&&$pr->{merged_at}?"\cC46merged on ".($pr->{merged_at}=~s/T.*//r): +$issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$issue->{state}." since ".($issue->{created_at}=~s/T.*//r); + } + if (@ret) { + my $info; + my $sen; + if (@{$res->{items}}) { + $sen = "and \cB" . ($res->{total_count}-@ret) . "\cB more: " . makeashorterlink("https://github.com/$project/$search_type?q=".($orig_expr=~y/ /+/r)); + } + if (@ret > 1) { + $info = join "\n", "\c_Issues matching\c_" . ($sen ? " ( $sen )" : ""), @ret; + } else { + $info = join ' ', "\c_Matching issue:\c_", @ret, $sen; + } + if ($readdress) { + my %hash = %$mess; + $hash{who} = $readdress; + $hash{address} = 1; + $self->reply(\%hash, $info); + return 1; + } + return $info; + } else { + return "Nothing found..."; + } + } + return; +} + +1; + From da7c9be3e1395f415d8f5310bb854f0701959851 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Wed, 8 Mar 2017 15:25:50 +0000 Subject: [PATCH 05/21] record dependencies --- Makefile.PL | 3 ++- README | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile.PL b/Makefile.PL index 1df03a3..f216a19 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -14,11 +14,12 @@ WriteMakefile( PREREQ_PM => { 'Test::More' => 0, 'Bot::BasicBot::Pluggable::Module' => 0, - 'Net::GitHub::V2' => 0, + 'Net::GitHub::V3' => 0, 'YAML' => 0, 'LWP::Simple' => 0, 'JSON' => 0, 'URI::Title' => 0, + 'WWW::Shorten::GitHub' => 0, }, dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, clean => { FILES => 'Bot-BasicBot-Pluggable-Module-GitHub-*' }, diff --git a/README b/README index 1e50601..2acdf39 100644 --- a/README +++ b/README @@ -10,7 +10,7 @@ DESCRIPTION interest to you. They're already in use on the Dancer project's IRC channel, and internally at my workplace, UK2. - Most communication with GitHub uses Net::GitHub::V2, and can use + Most communication with GitHub uses Net::GitHub::V3, and can use authentication with an API key for private repositories. MODULES From 3003116f9c811802b902d59947be66a23251502b Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Wed, 9 Aug 2017 14:44:20 +0200 Subject: [PATCH 06/21] fix 2 bugs: (1) status was ignored due to regex ordering (2) fix project on empty (status only) query --- lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm index 620043e..26ab740 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm @@ -42,10 +42,10 @@ sub said { my $count = min 3, $realcount; my $expr = $+{expr}; my $type = $+{type}; - $type = 'pr' if $type =~ /^p/i; my $status = $+{status}; + $type = 'pr' if $type =~ /^p/i; my $project; - if ($expr =~ s/\s+ in \s+ (? \S+) \s* $//xi) { + if ($expr =~ s/(?: ^ | \s+ ) in \s+ (? \S+) \s* $//xi) { $project = $+{project}; } $project ||= $self->github_project($mess->{channel}); From 88c2890842a0cb682e5f835ad83e24c89de7e629 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Wed, 8 Mar 2017 15:15:04 +0000 Subject: [PATCH 07/21] [local] hardcode an ignore rule for other github bots, whose nicks star in this channels with "Not-" --- lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm index 32dfa55..da9bae8 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm @@ -71,6 +71,10 @@ sub said { return unless $pri == 2; + # return if $mess->{body} =~ m{://git}; + + # do not react to other bots, identified by /^Not-/ + return if $mess->{who} =~ /^Not-/; # Loop through matching things in the message body, assembling quick links # ready to return. From 65ec5606da43ed55ac556a89cad84836d387d9e2 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Wed, 9 Aug 2017 14:46:55 +0200 Subject: [PATCH 08/21] enable # for issuenum>99 --- .../BasicBot/Pluggable/Module/GitHub/EasyLinks.pm | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm index da9bae8..01482c8 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm @@ -89,10 +89,10 @@ sub said { (?:\s+|-)? \#? (? \d+) -# | -# (?:^|\s+) -# \# -# (? \d+) + | + (?:^|\s+) + \# + (? (?!999)\d{3,}) | # Or a commit SHA (? [0-9a-f]{6,40}) @@ -110,9 +110,9 @@ sub said { if ($+{sha}) { $thing = 'commit'; $thingnum = $+{sha}; -# } elsif ($+{hnum}) { -# $thing = 'issue'; -# $thingnum = $+{hnum}; + } elsif ($+{hnum}) { + $thing = 'issue'; + $thingnum = $+{hnum}; } my $project = $+{project} || $self->github_project($mess->{channel}); From 385bdcd8053bd0936afb83fa75d7f3b7e3bb1147 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Wed, 9 Aug 2017 14:47:34 +0200 Subject: [PATCH 09/21] fix bug: shortened sha hashes would not be looked up --- .../BasicBot/Pluggable/Module/GitHub/EasyLinks.pm | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm index 01482c8..f8d1d31 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm @@ -172,7 +172,18 @@ $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i # If it was a commit: if ($thing eq 'commit') { warn "Handling commit $thingnum"; - my $commit = $ng->git_data->commit($thingnum); +# my $commit = $ng->git_data->commit($thingnum); + local $@; + my $commit = eval { $ng->repos->commit($thingnum) }; + if (!$commit && $@) { + $commit = +{ error => $@ }; + } + if ($commit->{commit}) { + if ($commit->{html_url}) { + $commit->{commit}{html_url} = $commit->{html_url}; + } + $commit = $commit->{commit}; + } if ($commit && !exists $commit->{error}) { my $title = ( split /\n+/, $commit->{message} )[0]; my $url = $commit->{html_url}; @@ -191,7 +202,7 @@ $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i # what might be a SHA, so we could be annoying saying that we # didn't match a commit when someone said a word that just # happened to look like it could be the start of a SHA. - warn "No commit details for $thingnum \@ $project/$thingnum"; + warn "No commit details for $thingnum \@ $project/$thingnum" . ($commit && ref $commit ? ": $commit->{error}" : ""); } } } From 87f240b88ebc90dcd4a5e57b74428e43806243bf Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Wed, 9 Aug 2017 14:48:13 +0200 Subject: [PATCH 10/21] feature: link to issuecomments --- .../Pluggable/Module/GitHub/EasyLinks.pm | 70 +++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm index f8d1d31..fc166d3 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm @@ -218,11 +218,15 @@ $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i (? \d+) (?: (?: - /commits/ (? [0-9a-f]{6,40}) - | - /files + (?: + /commits/ (? [0-9a-f]{6,40}) + | + /files + ) + (?: [?] [^#\s] +)? (? [#] diff\S* )? ) - (?: [?] [^#\s] +)? (? [#] diff\S* )? + | + (?
/? [#] \S* ) )? | commit/ (? [0-9a-f]{6,40}) @@ -250,10 +254,13 @@ $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i # for some reason, we can't do anything useful.) my $ng = $self->ng($project) or return; my $stop = $+{stop}; + my $details = $+{details}; + $details =~ s/.*[#]// if $details; warn "OK, about to try to handle $thing $thingnum for $project"; + # link to lines inside a blob/diff if ($stop) { return unless $stop =~ s/([LR])(\d+)(?:-\1(\d+))?$//; my ($lr, $line, $line2) = ($1, $2, $3); @@ -287,6 +294,61 @@ $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i next; } + # link to a issue comment + elsif ($details) { + next unless $details =~ /issue(comment)?-(\d+)$/; + my ($comment, $id) = ($1, $2); + warn "Handling issue $thingnum $details"; + my $issue = $ng->issue->issue($thingnum); + if (exists $issue->{error}) { + push @return, $issue->{error}; + next match; + } + + my $stitle = $issue->{title}; + if (length $stitle > 25) { + (substr $stitle, 23) = '...'; + } + my ($pre_ret, $suff_ret); + my @lines; + # issue text + unless ($comment) { + next unless $id eq $issue->{id}; + @lines = split /\R/, $issue->{body}; + $pre_ret = sprintf "%s \cC43%d\cC (%s) by \cB%s\cB -\cC ", + (exists $issue->{pull_request} ? "Pull request" : "Issue"), + $thingnum, + $stitle, + _dehih($issue->{user}{login}); + $suff_ret = ""; + } + else { + my $comment = $ng->issue->comment($id); + if (exists $comment->{error}) { + push @return, $comment->{error}; + next match; + } + @lines = split /\R/, $comment->{body}; + $pre_ret = sprintf "%s \cC43%d\cC (%s) comment by \cB%s\cB -\cC ", + (exists $issue->{pull_request} ? "Pull request" : "Issue"), + $thingnum, + $stitle, + _dehih($comment->{user}{login}); + $suff_ret = " \cBon\cB ".($comment->{created_at}=~s/T.*//r); + } + while (@lines && $lines[0] =~ /^>/) { + shift @lines; + } + my $text = join ' ', map { /^\s*(.*?)\s*$/ ? $1 : $_ } @lines; + my $maxlen = 290 - (length $pre_ret) - (length $suff_ret); + $text =~ s{\b(https?://github\.com/\S+)}{makeashorterlink($1)}ge; + if (length $text > $maxlen) { + (substr $text, $maxlen - 3) = '...'; + } + $text =~ s{\w+://\S+\.\.\.$}{...}; + push @return, "$pre_ret$text$suff_ret"; + } + # Right, handle it in the approriate way elsif ($thing ne 'commit') { warn "Handling issue $thingnum"; From 4d4f8cd9d45ab0ff6ec390f77f698b3a0513578e Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Wed, 8 Mar 2017 15:11:16 +0000 Subject: [PATCH 11/21] rewrite the announce module to work with paging and better bookkeeping --- .../Pluggable/Module/GitHub/Announce.pm | 430 ++++++++++++------ 1 file changed, 280 insertions(+), 150 deletions(-) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm index 26a1b2e..5c5289f 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm @@ -9,6 +9,9 @@ use WWW::Shorten::GitHub; use Bot::BasicBot::Pluggable::Module::GitHub; use base 'Bot::BasicBot::Pluggable::Module::GitHub'; use JSON; +use YAML qw(LoadFile DumpFile); +use Try::Tiny; +use Bot::BasicBot::Pluggable::MiscUtils qw(util_dehi util_strip_codes); our $VERSION = 0.02; @@ -18,178 +21,257 @@ Announce new/changed issues and pull requests HELPMSG } +my %issues_cache; +my %issues_lu; +my $old_issues_cache; + +sub _map_issue { + map { +{ + state => $_->{state}, + id => $_->{id}, + title => $_->{title}, + url => $_->{html_url}, + type => ($_->{pull_request} ? 'pull' : 'issue'), + by => $_->{user}{login}, + number => $_->{number}, + updated_at => $_->{updated_at}, + } } @_ +} + +sub _all_issues { + my ($ng, $args) = @_; + my @issues = $ng->issue->repos_issues($args); + my $page = 1; + while ($ng->issue->has_next_page) { + push @issues, $ng->issue->next_page; + $page++; + } + warn ".. @{[scalar @issues]} issues, $page pages" + if $page > 1; + @issues +} + +sub _merge_issues { + my ($new, $old) = @_; + my %idmap = (map { ($_->{id} => 1) } @$new); + ( @$new, + (grep { !$idmap{ $_->{id} } } @{ $old || [] }) + ) +} + +sub _update_issues { + my ($prjcache, $prjlu, $new) = @_; + $prjcache->{_issues} = [_merge_issues($new, $prjcache->{_issues})]; + %{ $prjlu } = ( map { ($_->{number} => $_) } @{ $prjcache->{_issues} } ); +} sub tick { my $self = shift; - my $issue_state_file = 'last-issues-state.json'; - my $seconds_between_checks = $self->get('poll_issues_interval') || 60 * 5; return if time - $self->get('last_issues_poll') < $seconds_between_checks; $self->set('last_issues_poll', time); - # Grab details of the issues we know about already: - # Have to handle storing & loading old issue state myself - I don't know - # why, but the bot storage doesn't want to work for this. - my $json; - my $first_run; - if (-f $issue_state_file) { - open my $fh, '<', $issue_state_file - or die "Failed to open $issue_state_file - $!"; - { local $/; $json = <$fh> } - close $fh; - } else { - $first_run = 1; - } - my $seen_issues = $json ? JSON::from_json($json) : {}; - - - # OK, for each channel, pull details of all issues from the API, and look - # for changes my $announce_for_channel = $self->store->get('GitHub','announce_for_channel') || {}; - channel: + my $conf = $self->store->get('GitHub', 'announce_config_flags') || +{}; + my %projects; for my $channel (keys %$announce_for_channel) { - my $dfltproject = $self->github_project($channel) || ''; - my $dfltuser = $dfltproject && $dfltproject =~ m{^([^/]+)} ? $1 : ''; - project: for my $project (@{ $announce_for_channel->{$channel} || [] }) { - warn "Looking for issues for $project for $channel"; - my %notifications; - - my $ng = $self->ng($project) or next project; - - my $issues = $ng->issue->repos_issues({state => '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}, - type => ($issue->{pull_request} ? 'pull' : 'issue'), - by => $issue->{user}{login}, - }; - - 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 ]; - $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. - my $state = 'closed'; - my $by; - if ($existing->{details}{type} eq 'pull' ) { - my $pr = $ng->pull_request->pull($issuenum); - if ($pr->{merged}) { - $state = 'merged'; - $by = $pr->{merged_by}{login}; + $projects{$project} = 1; + } + } + + unless ($old_issues_cache) { + if (-f "gh_announce_issues_cache.yml") { + ($old_issues_cache) = LoadFile("gh_announce_issues_cache.yml"); + } + else { + $old_issues_cache = {}; + } + } + + for my $project (sort keys %projects) { + unless ($issues_cache{$project}) { + warn "Loading issues of $project initially.."; + my $ng = $self->ng($project) or next; + $issues_cache{$project} = delete $old_issues_cache->{$project}; + $issues_cache{$project}{__time__} = time; + delete $issues_cache{$project}{_lu}; + my $prjcache = $issues_cache{$project}; + my $prjlu = $issues_lu{$project} ||= +{}; + my $since = @{$prjcache->{_issues} || []} ? + $prjcache->{_issues}[0]{updated_at} : ''; + warn ".. " . (scalar @{$prjcache->{_issues} || []}) . " cached.." . ($since ? " since $since .." : ''); + my @issues = _map_issue(_all_issues($ng, +{ state => 'all', sort => 'updated', ($since ? (since => "$since") : ()) })); + _update_issues($prjcache, $prjlu, \@issues); + my $heads = $ng->git_data->ref('heads'); + for my $head (@{$heads||[]}) { + my $ref = $head->{ref}; + $ref =~ s{^refs/heads/}{}; + $prjcache->{'__heads__'}{$ref} = $head->{object}{sha}; + } + delete $projects{$project}; + } + } + DumpFile("gh_announce_issues_cache.yml", \%issues_cache); + my %messages; + for my $project (sort keys %projects) { + next unless $issues_cache{$project}; + my $ng = $self->ng($project) or next; + my $prjcache = $issues_cache{$project}; + my $prjlu = $issues_lu{$project} ||= +{}; + my $since = @{$prjcache->{_issues}} ? + $prjcache->{_issues}[0]{updated_at} : ''; + my @issues = _map_issue(_all_issues($ng, +{ state => 'all', sort => 'updated', ($since ? (since => "$since") : ()) })); + my %notifications; + for my $issue (@issues) { + my $type; + my $details = +{ state => $issue->{state}, by => $issue->{by} }; + if (my $existing = $prjlu->{ $issue->{number} }) { + if ($existing->{state} eq $issue->{state}) { + # no change + } + elsif ($issue->{state} eq 'closed') { + # It was open before, but isn't in the list now - it must have + # been closed. + my $by; + my $state = $issue->{state}; + if ($issue->{type} eq 'pull' ) { + my $pr = $ng->pull_request->pull($issue->{number}); + if ($pr->{merged}) { + $state = 'merged'; + $by = $pr->{merged_by}{login}; + } } + unless (defined $by) { + my $issue = $ng->issue->issue($issue->{number}); + $by = $issue->{closed_by}{login}; + } + $details = +{ state => $state, by => $by }; + $type = $state; } - unless (defined $by) { - my $issue = $ng->issue->issue($issuenum); - $by = $issue->{closed_by}{login}; + elsif ($issue->{state} eq 'open') { + # It was closed before, but is now in the open feed, so it's + # been re-opened + $type = 'reopened'; } - $existing->{details}{by} = $by; - push @{ $notifications{$state} }, - [ $issuenum, $existing->{details} ]; - $existing->{state} = 'closed'; - } - } - - my $in = $project eq $dfltproject ? '' : $project =~ m{^\Q$dfltuser\E/(.*)$} ? " in $1" : " in $project"; - # Announce any changes - for my $type (qw(opened reopened merged closed)) { - next unless $notifications{$type}; - my $s = scalar @{$notifications{$type}} > 1 ? 's':''; - my %nt = (issue => "\cC52Issue", pull => "\cC29Pull request"); - my %tc = ('closed' => "\cC55", 'opened' => "\cC52", reopened => "\cC52", merged => "\cC46"); - for my $t (qw(issue pull)) { - my @not = grep { $_->[1]{type} eq $t } @{ $notifications{$type} }; - $self->say( - channel => $channel, - body => "$nt{$t}$s\cC $tc{$type}$type\cC$in: " - . join ', ', map { - sprintf "\cC43%d\cC (\cC59%s\cC) by \cB%s\cB: \cC73%s\cC", - $_->[0], # issue number - @{$_->[1]}{qw(title by)}, - makeashorterlink($_->[1]{url}) - } @not - ) if @not && !$first_run; } - } + elsif ($issue->{state} eq 'open') { + # A new issue we haven't seen before + $type = 'opened'; + } - my $heads = $ng->git_data->ref('heads'); + if ($type) { + $prjcache->{_details}{ $issue->{number} } = $details; + push @{ $notifications{$type} }, + [ $issue->{number}, $issue, $details ]; + } + } + _update_issues($prjcache, $prjlu, \@issues); my @push_not; + my $heads = $ng->git_data->ref('heads'); for my $head (@{$heads||[]}) { my $ref = $head->{ref}; $ref =~ s{^refs/heads/}{}; my $sha = $head->{object}{sha}; - my $ex = $seen_issues->{'__heads__'.$project}{$ref}; + my $ex = $prjcache->{'__heads__'}{$ref}; if ($ex ne $sha) { my $commit = $ng->git_data->commit($sha); - if ($commit && !exists $commit->{error}) { - my $title = ( split /\n+/, $commit->{commit}{message} )[0]; - my $url = $commit->{html_url}; - push @push_not, [ - $ref, - ($commit->{author}{login}||$commit->{committer}{login}||$commit->{commit}{author}{name}||$commit->{commit}{committer}{name}), - $title, - $project, - $url - ]; - } else { - push @push_not, [$ref,$sha]; + my $ignore; + my $re = $conf->{$project}{ignore_branches_re}; + if ($re) { + $ignore ||= $ref =~ /$re/; } - $seen_issues->{'__heads__'.$project}{$ref}=$sha; + unless ($ignore) { + if ($commit && !exists $commit->{error}) { + my $title = ( split /\n+/, $commit->{message} )[0]; + my $url = $commit->{html_url}; + push @push_not, [ + $ref, + ($commit->{author}{login}||$commit->{committer}{login}||$commit->{author}{name}||$commit->{committer}{name}), + $title, + $project, + $url + ]; + } + else { + push @push_not, [$ref,$sha]; + } + } + $prjcache->{'__heads__'}{$ref} = $sha; } } - if (@push_not) { + $messages{$project} = +{ notifications => \%notifications, + push_not => \@push_not }; + if (%notifications || @push_not) { + warn "Loading issues of $project " . ($since ? "since $since" : ''); + } + } + DumpFile("gh_announce_issues_cache.yml", \%issues_cache); + # try { + # OK, for each channel, pull details of all issues from the API, and look + # for changes + channel: + for my $channel (keys %$announce_for_channel) { + my $dfltproject = $self->github_project($channel) || ''; + my $dfltuser = $dfltproject && $dfltproject =~ m{^([^/]+)} ? $1 : ''; + project: + for my $project (@{ $announce_for_channel->{$channel} || [] }) { + my @bots = grep /^Not-/, $self->bot->pocoirc->channel_list($channel); + @bots = grep { $_ ne $self->bot->nick } @bots; + my %notifications = %{$messages{$project}{notifications} || +{}}; + my @push_not = @{$messages{$project}{push_not} || []}; + if (%notifications || @push_not) { + warn "Looking for issues for $project for $channel"; warn "`bots: @bots" if @bots; + } + my $in = $project eq $dfltproject ? '' : $project =~ m{^\Q$dfltuser\E/(.*)$} ? " in $1" : " in $project"; - $self->say( - channel => $channel, - body => "New commits$in " . join ', ', map { - @$_>2 ? ( - sprintf "on branch %s by \cB%s\cB - \cC59%s\cC: \cC73%s\cC", - $_->[0], $_->[1], $_->[2], makeashorterlink('https://github.com/'.$_->[3].'/tree/'.$_->[0])) - : sprintf 'branch %s now at %s', @{$_}[0,1] - } @push_not - ) unless $first_run || $notifications{merged}; + # Announce any changes + for my $type (qw(opened reopened merged closed)) { + next unless $notifications{$type}; + my $s = scalar @{$notifications{$type}} > 1 ? 's':''; + my %nt = (issue => "\cC52Issue", pull => "\cC29Pull request"); + my %tc = ('closed' => "\cC55", 'opened' => "\cC52", reopened => "\cC52", merged => "\cC46"); + for my $t (qw(issue pull)) { + my @not = grep { $_->[1]{type} eq $t } @{ $notifications{$type} }; + my @message = ( + channel => $channel, + body => "$nt{$t}$s\cC $tc{$type}$type\cC$in: " + . join ', ', map { + sprintf "\cC43%d\cC (\cC59%s\cC) by \cB%s\cB: \cC73%s\cC", + $_->[0], # issue number + $_->[1]{title}, + util_dehi($_->[2]{by}), + makeashorterlink($_->[1]{url}) + } @not + ); + warn "msg: $message[1]: ".util_strip_codes($message[3]) if @not; + $self->say(@message) if @not && !@bots; + } + } + + if (@push_not) { + my $in = $project eq $dfltproject ? '' : $project =~ m{^\Q$dfltuser\E/(.*)$} ? " in $1" : " in $project"; + my @message = ( + channel => $channel, + body => "New commits$in " . join ', ', map { + @$_>2 ? ( + sprintf "on branch %s by \cB%s\cB - \cC59%s\cC: \cC73%s\cC", + $_->[0], util_dehi($_->[1]), $_->[2], makeashorterlink('https://github.com/'.$_->[3].'/tree/'.$_->[0])) + : sprintf 'branch %s now at %s', @{$_}[0,1] + } @push_not + ); + warn "msg: $message[1]: ".util_strip_codes($message[3]); + $self->say(@message) unless @bots || $notifications{merged} || $conf->{$project}{no_heads}; + } } - - }} - - my $store_json = JSON::to_json($seen_issues); - # Store the updated issue details: - open my $storefh, '>', $issue_state_file - or die "Failed to write to $issue_state_file - $!"; - print {$storefh} $store_json; - close $storefh; + } + #} + # catch { + # warn "Exception: " . s/(\sat\s\/.*\sline\s\d+).*/$1/grs; + # } + return; } @@ -205,16 +287,19 @@ sub said { return unless $mess->{address} eq 'msg'; if ($mess->{body} =~ m{ - ^!(? add | del )githubannounce \s+ - (? \#\S+ ) \s+ - (? \S+ ) - }xi) { + ^!(? add | del )githubannounce \s+ + (? \#\S+ ) \s+ + (? \S+ ) (?:\s+ + (? .* ))? + }xi) { my $announce_for_channel = $self->store->get('GitHub','announce_for_channel') || {}; + my $conf = $self->store->get('GitHub', 'announce_config_flags') || +{}; my @projects = @{ $announce_for_channel->{$+{channel}} || [] }; if (lc $+{action} eq 'del') { @projects = grep { lc $_ ne lc $+{project} } @projects; - } else { + } + else { unless (grep { lc $_ eq lc $+{project} } @projects) { push @projects, $+{project}; } @@ -222,12 +307,57 @@ sub said { $announce_for_channel->{$+{channel}} = \@projects; $self->store->set( 'GitHub', 'announce_for_channel', $announce_for_channel - ); + ); + if ($+{action} eq 'add' && $+{flags}) { + my $flags = " $+{flags}"; + my $project = "\L$+{project}"; + while ($flags =~ /\s-(\w+)=(?:(["'])(.*?)\2|(\S+))(?=\s|$)/g) { + my $key = lc $1; + my $val = $3 || $4; + if ($val) { + $conf->{$project}{$key} = $val; + } + else { + delete $conf->{$project}{$key}; + } + } + delete $conf->{$project} + unless %{ $conf->{$project} }; + } + delete $conf->{''}; + $self->store->set( + 'GitHub', 'announce_config_flags', $conf + ); return "OK, $+{action}ed $+{project} for $+{channel}."; - } elsif ($mess->{body} =~ /^!(?:add|del)githubannounce/i) { + } + elsif ($mess->{body} =~ /^!listgithubannounce\s*$/) { + my $var = $self->store->get('GitHub', 'announce_for_channel') || +{}; + my $conf = $self->store->get('GitHub', 'announce_config_flags') || +{}; + my $ret = ''; + for my $ch (sort keys %$var) { + if (@{$var->{$ch}}) { + $ret .= $ch . ': ' . join ", ", map { + my $ret = $_; + my $proj = $_; + if ($conf->{$proj}) { + $ret .= ' ' . join ' ', + map { "-$_=".$conf->{$proj}{$_} } + grep { $conf->{$proj}{$_} } + sort keys %{$conf->{$proj}}; + } + $ret + } sort @{$var->{$ch}}; + $ret .= "\n"; + } + } + return $ret; + } + elsif ($mess->{body} =~ /^!(?:add|del|list)githubannounce/i) { return "Invalid usage. Try '!help github'"; } return; } + +1; From 199be25c5136958e40cf5ab5e6272b2e3263a7c0 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Wed, 9 Jun 2021 12:25:27 +0200 Subject: [PATCH 12/21] add !(set|del|list)github(project|auth) configuration and update help --- lib/Bot/BasicBot/Pluggable/Module/GitHub.pm | 54 ++++++++++++++++++- .../Pluggable/Module/GitHub/Announce.pm | 4 +- .../Pluggable/Module/GitHub/IssueSearch.pm | 4 +- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm index d40a4cf..0e3ffcc 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm @@ -101,6 +101,10 @@ sub channels_and_projects { return \%project_for_channel; } +sub help { + "github module; configuration: !listgithubproject, !setgithubproject [authtok], !delgithubproject , !delgithubauth " +} + # Support configuring project details for a channel (potentially with auth # details) via a msg. This is a bit too tricky to just leave the Vars module to # handle, I think. (Note that each of the modules which inherit from us will @@ -140,7 +144,55 @@ sub said { } return $message; - } elsif ($mess->{body} =~ /^!setgithubproject/i) { + } elsif ($mess->{body} =~ m{ + ^!delgithubproject \s+ + (? \#\S+ ) \s+ + (? \S+ ) + }xi) { + my $project_for_channel = + $self->store->get('GitHub','project_for_channel') || {}; + my $message = 'project not found'; + if ($project_for_channel->{$+{channel}} eq $+{project}) { + delete $project_for_channel->{$+{channel}}; + $message = "OK, project for $+{channel} deleted"; + } + $self->store->set( + 'GitHub', 'project_for_channel', $project_for_channel + ); + return $message; + } elsif ($mess->{body} =~ m{ + ^!delgithubauth \s+ + (? \S+ ) + }xi) { + my $auth_for_project = + $self->store->get('GitHub', 'auth_for_project') || {}; + my $message = 'project not found'; + if ($auth_for_project->{$+{project}}) { + delete $auth_for_project->{$+{project}}; + $message = "OK, auth for $+{project} deleted"; + } + $self->store->set( + 'GitHub', 'auth_for_project', $auth_for_project + ); + + # Invalidate any cached Net::GitHub object we might have, so the new + # settings are used + delete $net_github{$+{project}}; + return $message; + } elsif ($mess->{body} =~ m{^!listgithubproject(?:\s+|$)}xi) { + my $project_for_channel = + $self->store->get('GitHub','project_for_channel') || {}; + my $auth_for_project = + $self->store->get('GitHub', 'auth_for_project') || {}; + my $message; + for my $c (sort keys %$project_for_channel) { + $message .= "$c: $project_for_channel->{$c}\n"; + } + if (%$auth_for_project) { + $message .= "*: ".(join " ", sort keys %$auth_for_project)."\n"; + } + return $message || "no projects"; + } elsif ($mess->{body} =~ /^!(set|del|list)github(project|auth)/i) { return "Invalid usage. Try '!help github'"; } return; diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm index 5c5289f..0f5fb97 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm @@ -17,7 +17,7 @@ our $VERSION = 0.02; sub help { return < [flags], !delgithubannounce HELPMSG } @@ -355,7 +355,7 @@ sub said { return $ret; } elsif ($mess->{body} =~ /^!(?:add|del|list)githubannounce/i) { - return "Invalid usage. Try '!help github'"; + return "Invalid usage. Try '!help github::announce'"; } return; } diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm index 26ab740..c52871b 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm @@ -9,9 +9,7 @@ use JSON; sub help { return < query [in ] +Search github issues, usage: find [1-4] [open|closed|merged] query [in ] HELPMSG } From 2d89991bef5f43f376c73cfa03b931c16ceb5497 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Wed, 9 Jun 2021 12:27:23 +0200 Subject: [PATCH 13/21] add milestone display --- lib/Bot/BasicBot/Pluggable/Module/GitHub.pm | 55 +++++++++++++++++++ .../Pluggable/Module/GitHub/EasyLinks.pm | 19 +++++-- .../Pluggable/Module/GitHub/IssueSearch.pm | 3 +- 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm index 0e3ffcc..aa8d90f 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm @@ -10,6 +10,7 @@ use base 'Bot::BasicBot::Pluggable::Module'; use strict; use Net::GitHub::V3; +use Mojo::UserAgent; our $VERSION = '0.04'; @@ -62,6 +63,60 @@ sub ng { return $net_github{"$user/$project"} = $api; } +my $ua; + +sub _make_ua { + return $ua if $ua; + $ua = Mojo::UserAgent->new; + $ua->proxy->detect; + return $ua; +} + +my %_commit_branch_cache; +my $_commit_branch_cache_timeout = 60 * 60; + +sub _commit_branch { + my ($self, $pr, $commit_id) = @_; + _make_ua(); + my $base_url = $pr->{html_url}; + $base_url =~ s{/(pull|commit)/.*}{}; + my $url = "$base_url/branch_commits/$commit_id"; + my $tag; + my $time = time; + if ($_commit_branch_cache{$url} && ($_commit_branch_cache{$url}{_time} + $_commit_branch_cache_timeout) > $time) { + $tag = $_commit_branch_cache{$url}{tag}; + } else { + warn "getting: $url"; + my $res = $ua->get("$url")->res; + return "" unless $res; + my @tags = ((grep !m{[-/]}, $res->dom("ul.branches-list li.branch a")->map("text")->each), + (grep !/-/, $res->dom("ul.branches-tag-list li a")->map("text")->each)); + #my @tags = grep !/-/, $res->dom("ul li a")->map("text")->each; + if (@tags) { + $tag = $tags[-1]; + $_commit_branch_cache{$url} = { _time => $time, tag => $tag }; + } + } + return "T:\cB$tag\cB " if length $tag; + return ""; +} + +sub _pr_branch { + my ($self, $ng, $pr) = @_; + my $issue_number = $pr->{number}; + my $ie = $ng->issue; + my $commit_id; + # while (my $event = $ie->next_event($issue_number)) + for my $event ($ie->events($issue_number)) { + if ($event->{event} eq 'merged') { + $commit_id = $event->{commit_id}; + last; + } + } + return '' unless length $commit_id; + return $self->_commit_branch($pr, $commit_id); +} + # Find the name of the GitHub project for a given channel sub project_for_channel { diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm index fc166d3..586464a 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm @@ -147,13 +147,14 @@ sub said { push @return, $issue->{error}; next match; } - push @return, sprintf "%s \cC43%d\cC (\cC59%s\cC) by \cB%s\cB - \cC73%s\cC%s \{%s\cC\}", + push @return, sprintf "%s \cC43%d\cC (\cC59%s\cC) by \cB%s\cB - \cC73%s\cC%s %s\{%s\cC\}", (exists $issue->{pull_request} ? "\cC29Pull request" : "\cC52Issue"), $thingnum, $issue->{title}, _dehih($issue->{user}{login}), makeashorterlink($issue->{html_url}), ($issue->{labels}&&@{$issue->{labels}}?" [".(join",",map{$_->{name}}@{$issue->{labels}})."]":""), + ($issue->{milestone} ? "MS:\cB$issue->{milestone}{title}\cB ": ($pr?$self->_pr_branch($ng, $pr):"")), $pr&&$pr->{merged_at}?"\cC46merged on ".($pr->{merged_at}=~s/T.*//r): $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$issue->{state}." since ".($issue->{created_at}=~s/T.*//r); } @@ -182,6 +183,7 @@ $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i if ($commit->{html_url}) { $commit->{commit}{html_url} = $commit->{html_url}; } + $commit->{commit}{sha} //= $commit->{sha}; $commit = $commit->{commit}; } if ($commit && !exists $commit->{error}) { @@ -192,11 +194,13 @@ $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i # might perhaps change in future, so play it safe: # $url = "https://github.com$url" unless $url =~ /^http/; # $url =~ s{https://api.github.com/repos/(.*?)/commits/}{https://github.com/$1/commit/}; - push @return, sprintf "Commit \cC43$thingnum\cC (\cC59%s\cC) by \cB%s\cB on %s - \cC73%s", + push @return, sprintf "Commit \cC43$thingnum\cC (\cC59%s\cC) by \cB%s\cB on %s - \cC73%s\cC %s", $title, _dehih($commit->{author}{login}||$commit->{committer}{login}||$commit->{author}{name}||$commit->{committer}{name}), ($commit->{author}{date}=~s/T.*//r), - makeashorterlink($url); + makeashorterlink($url), + $self->_commit_branch($commit, $commit->{sha}), + ; } else { # We purposefully don't show a message on IRC here, as we guess # what might be a SHA, so we could be annoying saying that we @@ -364,13 +368,14 @@ $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i push @return, $issue->{error}; next match; } - push @return, sprintf "%s \cC43%d\cC (\cC59%s\cC) by \cB%s\cB - \cC73%s\cC%s \{%s\cC\}", + push @return, sprintf "%s \cC43%d\cC (\cC59%s\cC) by \cB%s\cB - \cC73%s\cC%s %s\{%s\cC\}", (exists $issue->{pull_request} ? "\cC29Pull request" : "\cC52Issue"), $thingnum, $issue->{title}, _dehih($issue->{user}{login}), $project, ($issue->{labels}&&@{$issue->{labels}}?" [".(join",",map{$_->{name}}@{$issue->{labels}})."]":""), + ($issue->{milestone} ? "MS:\cB$issue->{milestone}{title}\cB ": ($pr?$self->_pr_branch($ng, $pr):"")), $pr&&$pr->{merged_at}?"\cC46merged on ".($pr->{merged_at}=~s/T.*//r): $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$issue->{state}." since ".($issue->{created_at}=~s/T.*//r); } @@ -383,11 +388,13 @@ $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i my $title = ( split /\n+/, $commit->{message} )[0]; my $url = $commit->{html_url}; - push @return, sprintf "Commit \cC43$thingnum\cC (\cC59%s\cC) by \cB%s\cB on %s - \cC73%s", + push @return, sprintf "Commit \cC43$thingnum\cC (\cC59%s\cC) by \cB%s\cB on %s - \cC73%s\cC %s", $title, _dehih($commit->{author}{login}||$commit->{committer}{login}||$commit->{author}{name}||$commit->{committer}{name}), ($commit->{author}{date}=~s/T.*//r), - $project; + $project, + $self->_commit_branch($commit, $commit->{sha}), + ; } else { # We purposefully don't show a message on IRC here, as we guess # what might be a SHA, so we could be annoying saying that we diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm index c52871b..dca5efe 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm @@ -88,13 +88,14 @@ sub said { push @ret, $issue->{error}; next; } - push @ret, sprintf "%s \cC43%d\cC (\cC59%s\cC) by \cB%s\cB - \cC73%s\cC%s \{%s\cC\}", + push @ret, sprintf "%s \cC43%d\cC (\cC59%s\cC) by \cB%s\cB - \cC73%s\cC%s %s\{%s\cC\}", (exists $issue->{pull_request} ? "\cC29Pull request" : "\cC52Issue"), $issue->{number}, $issue->{title}, _dehih($issue->{user}{login}), makeashorterlink($issue->{html_url}), ($issue->{labels}&&@{$issue->{labels}}?" [".(join",",map{$_->{name}}@{$issue->{labels}})."]":""), + ($issue->{milestone} ? "MS:\cB$issue->{milestone}{title}\cB ": ($pr?$self->_pr_branch($ng, $pr):"")), $pr&&$pr->{merged_at}?"\cC46merged on ".($pr->{merged_at}=~s/T.*//r): $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$issue->{state}." since ".($issue->{created_at}=~s/T.*//r); } From caa86b7bac3c0ab8b077927631c387453302f310 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Thu, 17 Mar 2022 14:31:29 +0100 Subject: [PATCH 14/21] switch to Akari link shortener after git.io shutdown --- lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm | 10 ++++++---- lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm | 10 ++++++---- .../BasicBot/Pluggable/Module/GitHub/IssueSearch.pm | 8 +++++--- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm index 0f5fb97..d627a4a 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm @@ -5,16 +5,18 @@ package Bot::BasicBot::Pluggable::Module::GitHub::Announce; use strict; -use WWW::Shorten::GitHub; use Bot::BasicBot::Pluggable::Module::GitHub; use base 'Bot::BasicBot::Pluggable::Module::GitHub'; use JSON; use YAML qw(LoadFile DumpFile); use Try::Tiny; use Bot::BasicBot::Pluggable::MiscUtils qw(util_dehi util_strip_codes); +use AkariLinkShortener; our $VERSION = 0.02; - + +my $als = AkariLinkShortener->new; + sub help { return < [flags], !delgithubannounce @@ -243,7 +245,7 @@ sub tick { $_->[0], # issue number $_->[1]{title}, util_dehi($_->[2]{by}), - makeashorterlink($_->[1]{url}) + $als->shorten($_->[1]{url}) } @not ); warn "msg: $message[1]: ".util_strip_codes($message[3]) if @not; @@ -258,7 +260,7 @@ sub tick { body => "New commits$in " . join ', ', map { @$_>2 ? ( sprintf "on branch %s by \cB%s\cB - \cC59%s\cC: \cC73%s\cC", - $_->[0], util_dehi($_->[1]), $_->[2], makeashorterlink('https://github.com/'.$_->[3].'/tree/'.$_->[0])) + $_->[0], util_dehi($_->[1]), $_->[2], $als->shorten('https://github.com/'.$_->[3].'/tree/'.$_->[0])) : sprintf 'branch %s now at %s', @{$_}[0,1] } @push_not ); diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm index 586464a..2834366 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm @@ -5,12 +5,14 @@ package Bot::BasicBot::Pluggable::Module::GitHub::EasyLinks; use strict; -use WWW::Shorten::GitHub; use Bot::BasicBot::Pluggable::Module::GitHub; use base 'Bot::BasicBot::Pluggable::Module::GitHub'; use URI::Title; use List::Util qw(min max); use Mojo::DOM; +use AkariLinkShortener; + +my $als = AkariLinkShortener->new; sub help { return <{title}, _dehih($issue->{user}{login}), - makeashorterlink($issue->{html_url}), + $als->shorten($issue->{html_url}), ($issue->{labels}&&@{$issue->{labels}}?" [".(join",",map{$_->{name}}@{$issue->{labels}})."]":""), ($issue->{milestone} ? "MS:\cB$issue->{milestone}{title}\cB ": ($pr?$self->_pr_branch($ng, $pr):"")), $pr&&$pr->{merged_at}?"\cC46merged on ".($pr->{merged_at}=~s/T.*//r): @@ -198,7 +200,7 @@ $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i $title, _dehih($commit->{author}{login}||$commit->{committer}{login}||$commit->{author}{name}||$commit->{committer}{name}), ($commit->{author}{date}=~s/T.*//r), - makeashorterlink($url), + $als->shorten($url), $self->_commit_branch($commit, $commit->{sha}), ; } else { @@ -345,7 +347,7 @@ $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i } my $text = join ' ', map { /^\s*(.*?)\s*$/ ? $1 : $_ } @lines; my $maxlen = 290 - (length $pre_ret) - (length $suff_ret); - $text =~ s{\b(https?://github\.com/\S+)}{makeashorterlink($1)}ge; + $text =~ s{\b(https?://github\.com/\S+)}{$als->shorten($1)}ge; if (length $text > $maxlen) { (substr $text, $maxlen - 3) = '...'; } diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm index dca5efe..0e2e5b2 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm @@ -1,11 +1,13 @@ package Bot::BasicBot::Pluggable::Module::GitHub::IssueSearch; use strict; -use WWW::Shorten::GitHub; use Bot::BasicBot::Pluggable::Module::GitHub; use base 'Bot::BasicBot::Pluggable::Module::GitHub'; use LWP::Simple (); use List::Util 'min'; use JSON; +use AkariLinkShortener; + +my $als = AkariLinkShortener->new; sub help { return <{number}, $issue->{title}, _dehih($issue->{user}{login}), - makeashorterlink($issue->{html_url}), + $als->shorten($issue->{html_url}), ($issue->{labels}&&@{$issue->{labels}}?" [".(join",",map{$_->{name}}@{$issue->{labels}})."]":""), ($issue->{milestone} ? "MS:\cB$issue->{milestone}{title}\cB ": ($pr?$self->_pr_branch($ng, $pr):"")), $pr&&$pr->{merged_at}?"\cC46merged on ".($pr->{merged_at}=~s/T.*//r): @@ -103,7 +105,7 @@ $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i my $info; my $sen; if (@{$res->{items}}) { - $sen = "and \cB" . ($res->{total_count}-@ret) . "\cB more: " . makeashorterlink("https://github.com/$project/$search_type?q=".($orig_expr=~y/ /+/r)); + $sen = "and \cB" . ($res->{total_count}-@ret) . "\cB more: " . $als->shorten("https://github.com/$project/$search_type?q=".($orig_expr=~y/ /+/r)); } if (@ret > 1) { $info = join "\n", "\c_Issues matching\c_" . ($sen ? " ( $sen )" : ""), @ret; From 2bc733596ee52d4963b174ddfddca1faf88c4b5f Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Thu, 17 Mar 2022 14:37:58 +0100 Subject: [PATCH 15/21] [local] allow some suffixes in branches-list tag names --- lib/Bot/BasicBot/Pluggable/Module/GitHub.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm index aa8d90f..5517b17 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm @@ -90,7 +90,7 @@ sub _commit_branch { my $res = $ua->get("$url")->res; return "" unless $res; my @tags = ((grep !m{[-/]}, $res->dom("ul.branches-list li.branch a")->map("text")->each), - (grep !/-/, $res->dom("ul.branches-tag-list li a")->map("text")->each)); + (map s/~/-/r, grep !/-/, map s/-(pre\d*|an)/~$1/gr, $res->dom("ul.branches-tag-list li a")->map("text")->each)); #my @tags = grep !/-/, $res->dom("ul li a")->map("text")->each; if (@tags) { $tag = $tags[-1]; From 9e0c05294e545a07fcb930b25e40a610ca060b19 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Sun, 22 May 2022 17:30:37 +0200 Subject: [PATCH 16/21] add MULTINET support --- .../Pluggable/Module/GitHub/Announce.pm | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm index d627a4a..42d8050 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm @@ -215,17 +215,26 @@ sub tick { # OK, for each channel, pull details of all issues from the API, and look # for changes channel: - for my $channel (keys %$announce_for_channel) { + for my $netchannel (keys %$announce_for_channel) { + my $channel = $netchannel; + my $network; + if ($channel =~ s{^(\w+)/}{}) { + $network = $1; + } + next if !$network && $self->bot->can('MULTINET') && $self->bot->MULTINET; + local $self->bot->{conn_tag} = $network + if $network; + my $dfltproject = $self->github_project($channel) || ''; my $dfltuser = $dfltproject && $dfltproject =~ m{^([^/]+)} ? $1 : ''; project: - for my $project (@{ $announce_for_channel->{$channel} || [] }) { + for my $project (@{ $announce_for_channel->{$netchannel} || [] }) { my @bots = grep /^Not-/, $self->bot->pocoirc->channel_list($channel); @bots = grep { $_ ne $self->bot->nick } @bots; my %notifications = %{$messages{$project}{notifications} || +{}}; my @push_not = @{$messages{$project}{push_not} || []}; if (%notifications || @push_not) { - warn "Looking for issues for $project for $channel"; warn "`bots: @bots" if @bots; + warn "Looking for issues for $project for $netchannel"; warn "`bots: @bots" if @bots; } my $in = $project eq $dfltproject ? '' : $project =~ m{^\Q$dfltuser\E/(.*)$} ? " in $1" : " in $project"; @@ -290,7 +299,7 @@ sub said { if ($mess->{body} =~ m{ ^!(? add | del )githubannounce \s+ - (? \#\S+ ) \s+ + (? (?:\w+/)?\#\S+ ) \s+ (? \S+ ) (?:\s+ (? .* ))? }xi) { @@ -357,7 +366,7 @@ sub said { return $ret; } elsif ($mess->{body} =~ /^!(?:add|del|list)githubannounce/i) { - return "Invalid usage. Try '!help github::announce'"; + return "Invalid usage. Try 'help github::announce'"; } return; } From b7a70726a8af42712277613f399a4320f6a87153 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Mon, 23 May 2022 09:21:26 +0200 Subject: [PATCH 17/21] record dependencies --- Makefile.PL | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile.PL b/Makefile.PL index f216a19..2ce8775 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -19,7 +19,8 @@ WriteMakefile( 'LWP::Simple' => 0, 'JSON' => 0, 'URI::Title' => 0, - 'WWW::Shorten::GitHub' => 0, + 'AkariLinkShortener' => 0, + 'Mojo::UserAgent' => 0, }, dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, clean => { FILES => 'Bot-BasicBot-Pluggable-Module-GitHub-*' }, From 418866a6f026733a49942eab20ba71ccb3ef1283 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Thu, 9 Feb 2023 10:07:07 +0100 Subject: [PATCH 18/21] move link shorteners to own modules --- Makefile.PL | 1 - lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm | 7 ++----- lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm | 9 +++------ lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm | 7 ++----- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/Makefile.PL b/Makefile.PL index 2ce8775..21dff26 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -19,7 +19,6 @@ WriteMakefile( 'LWP::Simple' => 0, 'JSON' => 0, 'URI::Title' => 0, - 'AkariLinkShortener' => 0, 'Mojo::UserAgent' => 0, }, dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm index 42d8050..cae0692 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm @@ -11,12 +11,9 @@ use JSON; use YAML qw(LoadFile DumpFile); use Try::Tiny; use Bot::BasicBot::Pluggable::MiscUtils qw(util_dehi util_strip_codes); -use AkariLinkShortener; our $VERSION = 0.02; -my $als = AkariLinkShortener->new; - sub help { return < [flags], !delgithubannounce @@ -254,7 +251,7 @@ sub tick { $_->[0], # issue number $_->[1]{title}, util_dehi($_->[2]{by}), - $als->shorten($_->[1]{url}) + $self->linkshortener->shorten($_->[1]{url}) } @not ); warn "msg: $message[1]: ".util_strip_codes($message[3]) if @not; @@ -269,7 +266,7 @@ sub tick { body => "New commits$in " . join ', ', map { @$_>2 ? ( sprintf "on branch %s by \cB%s\cB - \cC59%s\cC: \cC73%s\cC", - $_->[0], util_dehi($_->[1]), $_->[2], $als->shorten('https://github.com/'.$_->[3].'/tree/'.$_->[0])) + $_->[0], util_dehi($_->[1]), $_->[2], $self->linkshortener->shorten('https://github.com/'.$_->[3].'/tree/'.$_->[0])) : sprintf 'branch %s now at %s', @{$_}[0,1] } @push_not ); diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm index 2834366..fe1af06 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm @@ -10,9 +10,6 @@ use base 'Bot::BasicBot::Pluggable::Module::GitHub'; use URI::Title; use List::Util qw(min max); use Mojo::DOM; -use AkariLinkShortener; - -my $als = AkariLinkShortener->new; sub help { return <{title}, _dehih($issue->{user}{login}), - $als->shorten($issue->{html_url}), + $self->linkshortener->shorten($issue->{html_url}), ($issue->{labels}&&@{$issue->{labels}}?" [".(join",",map{$_->{name}}@{$issue->{labels}})."]":""), ($issue->{milestone} ? "MS:\cB$issue->{milestone}{title}\cB ": ($pr?$self->_pr_branch($ng, $pr):"")), $pr&&$pr->{merged_at}?"\cC46merged on ".($pr->{merged_at}=~s/T.*//r): @@ -200,7 +197,7 @@ $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i $title, _dehih($commit->{author}{login}||$commit->{committer}{login}||$commit->{author}{name}||$commit->{committer}{name}), ($commit->{author}{date}=~s/T.*//r), - $als->shorten($url), + $self->linkshortener->shorten($url), $self->_commit_branch($commit, $commit->{sha}), ; } else { @@ -347,7 +344,7 @@ $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i } my $text = join ' ', map { /^\s*(.*?)\s*$/ ? $1 : $_ } @lines; my $maxlen = 290 - (length $pre_ret) - (length $suff_ret); - $text =~ s{\b(https?://github\.com/\S+)}{$als->shorten($1)}ge; + $text =~ s{\b(https?://github\.com/\S+)}{$self->linkshortener->shorten($1)}ge; if (length $text > $maxlen) { (substr $text, $maxlen - 3) = '...'; } diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm index 0e2e5b2..64fe84c 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/IssueSearch.pm @@ -5,9 +5,6 @@ use base 'Bot::BasicBot::Pluggable::Module::GitHub'; use LWP::Simple (); use List::Util 'min'; use JSON; -use AkariLinkShortener; - -my $als = AkariLinkShortener->new; sub help { return <{number}, $issue->{title}, _dehih($issue->{user}{login}), - $als->shorten($issue->{html_url}), + $self->linkshortener->shorten($issue->{html_url}), ($issue->{labels}&&@{$issue->{labels}}?" [".(join",",map{$_->{name}}@{$issue->{labels}})."]":""), ($issue->{milestone} ? "MS:\cB$issue->{milestone}{title}\cB ": ($pr?$self->_pr_branch($ng, $pr):"")), $pr&&$pr->{merged_at}?"\cC46merged on ".($pr->{merged_at}=~s/T.*//r): @@ -105,7 +102,7 @@ $issue->{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i my $info; my $sen; if (@{$res->{items}}) { - $sen = "and \cB" . ($res->{total_count}-@ret) . "\cB more: " . $als->shorten("https://github.com/$project/$search_type?q=".($orig_expr=~y/ /+/r)); + $sen = "and \cB" . ($res->{total_count}-@ret) . "\cB more: " . $self->linkshortener->shorten("https://github.com/$project/$search_type?q=".($orig_expr=~y/ /+/r)); } if (@ret > 1) { $info = join "\n", "\c_Issues matching\c_" . ($sen ? " ( $sen )" : ""), @ret; From 1d504e74ad9f9e07ede6f1d31aebf59a2afe7193 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Tue, 1 Apr 2025 22:46:53 +0200 Subject: [PATCH 19/21] make poll interval configurable --- lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm index cae0692..4bd3cd6 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/Announce.pm @@ -24,6 +24,11 @@ my %issues_cache; my %issues_lu; my $old_issues_cache; +sub init { + my $self = shift; + $self->config({'user_poll_issues_interval' => 0}); +} + sub _map_issue { map { +{ state => $_->{state}, @@ -67,7 +72,7 @@ sub _update_issues { sub tick { my $self = shift; - my $seconds_between_checks = $self->get('poll_issues_interval') || 60 * 5; + my $seconds_between_checks = 60 * ( $self->get('user_poll_issues_interval') || 5 ); return if time - $self->get('last_issues_poll') < $seconds_between_checks; $self->set('last_issues_poll', time); From c56c51c7a2b3df6b22127357f8208ecd7b07be24 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Tue, 1 Apr 2025 22:46:39 +0200 Subject: [PATCH 20/21] find diffs in github js only website --- .../Pluggable/Module/GitHub/EasyLinks.pm | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm index fe1af06..ffd91fe 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub/EasyLinks.pm @@ -10,6 +10,7 @@ use base 'Bot::BasicBot::Pluggable::Module::GitHub'; use URI::Title; use List::Util qw(min max); use Mojo::DOM; +use Mojo::JSON qw(from_json); sub help { return <{closed_at}?"\cC55closed on ".($issue->{closed_at}=~s/T.*//r):"\cC52".$i $x->all_text(0) } $dom->find("$stop$lr$_")->map('parent')->map('find', '.blob-code-inner')->flatten->each } $line..$line2; + unless (@lines) { + # see if it has lines present as embedded json + eval { + my $es = $dom->at('script[data-target="react-app.embeddedData"]')->text; + my $js = from_json($es); + if ($js->{payload}{blob}{rawLines}) { + @lines = $js->{payload}{blob}{rawLines}->@[ ($line-1)..($line2-1) ]; + } + 1; } || warn "no embedded json\n"; + } + unless (@lines) { + # see if it has github fragment + eval { + my $frag = $dom->at('include-fragment[src*="/diffs?"]')->{src}; + die "no absolute path" unless $frag =~ /^\//; + my $res = $ng->_make_request(HTTP::Request->new( GET => "https://github.com$frag" )); + my $dom = Mojo::DOM->new($res->decoded_content); + @lines = map { + map { + my $x = $_; + $x->descendant_nodes->grep(sub{ $_->type ne "text" })->map('strip'); + $x->all_text(0) + } $dom->find("$stop$lr$_")->map('parent')->map('find', '.blob-code-inner')->flatten->each + } $line..$line2; + 1; } || warn "no github fragment\n"; + } if (@lines) { my $tag = "$lr$line"; my $ret = $lines[0]; From 05cf8c6ca509502657374dbc42074b1e591aa342 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Tue, 1 Apr 2025 22:46:08 +0200 Subject: [PATCH 21/21] add support for codeberg issue/pr polling --- lib/Bot/BasicBot/Pluggable/Module/GitHub.pm | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm b/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm index 5517b17..42df2e9 100644 --- a/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm +++ b/lib/Bot/BasicBot/Pluggable/Module/GitHub.pm @@ -38,9 +38,10 @@ sub ng { } return unless $user && $project; + my $id = "$user/$project"; # If we've already got a suitable Net::GitHub::V2 object, use it: - if (my $ng = $net_github{"$user/$project"}) { + if (my $ng = $net_github{$id}) { return $ng; } @@ -58,9 +59,16 @@ sub ng { # $ngparams{always_Authorization} = 1; $ngparams{access_token} = $auth; } + my $endpoint; + if ($user =~ /:/) { + ($endpoint, $user) = split /:/, $user, 2; + } + if ($endpoint) { + $ngparams{api_url} = "https://$endpoint/api/v1"; + } my $api = Net::GitHub::V3->new(%ngparams); $api->set_default_user_repo($user, $project); - return $net_github{"$user/$project"} = $api; + return $net_github{$id} = $api; } my $ua;