[Bps-public-commit] smokingit branch, slack, created. ff669ec4cdd0a2ff2e430050f09e8b6428060efb

Alex Vandiver alexmv at bestpractical.com
Tue Dec 9 15:55:55 EST 2014


The branch, slack has been created
        at  ff669ec4cdd0a2ff2e430050f09e8b6428060efb (commit)

- Log -----------------------------------------------------------------
commit 92ecb92197a286683d68c98f40ace891ac289163
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Dec 9 02:14:26 2014 -0500

    Switch to slack

diff --git a/Makefile.PL b/Makefile.PL
index 1568efc..d1b2829 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -5,8 +5,8 @@ version     '0.01';
 requires    'Jifty' => '1.01209';
 requires    'Git::PurePerl';
 requires    'Plack::Middleware::CrossOrigin';
-requires    'IM::Engine';
-requires    'String::IRC';
+requires    'AnyEvent::WebSocket::Client';
+requires    'AnyEvent::HTTP';
 
 # requires Web::Hippie from git://github.com/alexmv/Web-Hippie.git
 
diff --git a/bin/anna b/bin/anna
index 7d05831..095a5ab 100755
--- a/bin/anna
+++ b/bin/anna
@@ -6,17 +6,20 @@ use warnings;
 
 use Jifty;
 BEGIN { Jifty->new( no_request => 1 ); };
-use Smokingit::IRC;
+use Smokingit::Slack;
 
-my %config = %{ Jifty->config->app('irc') || {} };
+my %config = %{ Jifty->config->app('slack') || {} };
 unless (%config) {
-    print "IRC is not configured; edit your site_config.yml\n";
+    print "Slack is not configured; edit your site_config.yml\n";
     exit;
 }
 
-unless ($config{host}) {
-    print "No host specified for IRC; edit your site_config.yml\n";
+unless ($config{token}) {
+    print "No token specified for Slack; edit your site_config.yml\n";
     exit;
 }
 
-Smokingit::IRC->new->run;
+my $client = Smokingit::Slack->new;
+$client->run;
+
+AE::cv->recv;
diff --git a/lib/Smokingit.pm b/lib/Smokingit.pm
index 9873527..5be13a8 100644
--- a/lib/Smokingit.pm
+++ b/lib/Smokingit.pm
@@ -4,6 +4,8 @@ use warnings;
 package Smokingit;
 use Cache::Memcached;
 
+our $VERSION = '1.00';
+
 our( $MEMCACHED );
 
 sub start {
diff --git a/lib/Smokingit/IRC.pm b/lib/Smokingit/Slack.pm
similarity index 58%
rename from lib/Smokingit/IRC.pm
rename to lib/Smokingit/Slack.pm
index 5eb1442..528850d 100644
--- a/lib/Smokingit/IRC.pm
+++ b/lib/Smokingit/Slack.pm
@@ -1,147 +1,259 @@
 use strict;
 use warnings;
 
-package Smokingit::IRC;
-use String::IRC;
+package Smokingit::Slack;
+use AnyEvent::WebSocket::Client;
+use AnyEvent::HTTP;
+use LWP::UserAgent;
+use JSON;
 
 use Moose;
-extends 'IM::Engine';
-
-has '+interface_args' => (
-    required => 0,
-    default  => sub {
-        my %config = %{ Jifty->config->app('irc') || {} };
-        return {
-            protocol => 'IRC',
-            credentials => {
-                server   => $config{host},
-                port     => $config{port},
-                nick     => $config{nick} || 'anna',
-                channels => [$config{channel}],
-            },
-        };
-    },
+
+has 'name' => (
+    is      => 'rw',
+    isa     => 'Str',
+);
+has 'slack_properties' => (
+    is      => 'rw',
+    isa     => 'HashRef',
+    default => sub { {} },
+);
+
+has 'connection' => (
+    is      => 'rw',
+    isa     => 'Maybe[AnyEvent::WebSocket::Connection]',
 );
 
-sub BUILD {
+has 'next_id' => (
+    is      => 'rw',
+    isa     => 'Int',
+    default => 1,
+);
+sub get_id {
     my $self = shift;
+    my $id = $self->next_id;
+    $self->next_id( $id + 1 );
+    return $id;
+}
+has 'pending_reply' => (
+    is      => 'rw',
+    isa     => 'HashRef',
+    default => sub { {} },
+);
 
-    $self->interface->incoming_callback(
-        sub { $self->incoming(@_) },
-    );
+has 'ping'      => ( is => 'rw', );
+has 'reconnect' => ( is => 'rw', );
 
-    $self->interface->irc->reg_cb(
-        registered => sub {
-            my $sub = Jifty->bus->new_listener;
-            $sub->subscribe(Jifty->bus->topic("test_result"));
-            $sub->poll( sub { $self->test_progress(@_) } );
 
-            my $out = IM::Engine::Outgoing::IRC::Channel->new(
-                channel => Jifty->config->app('irc')->{channel},
-                message => "I'm going to ban so hard",
-                command => "NOTICE",
-            );
-            $self->interface->send_message($out);
-        },
-    );
+sub run {
+    my $self = shift;
+
+    my $token = Jifty->config->app('slack')->{token};
+    $self->reconnect(undef);
+
+    # XXX use AnyEvent::HTTP
+    http_request GET => "https://slack.com/api/rtm.start?token=" . $token,
+        headers => {"user-agent" => "smokingit/$Smokingit::VERSION"},
+        timeout => 30,
+        sub {
+            my ($body, $hdr) = @_;
+            die "Slack API request failed: ".$hdr->{Reason} . "\n" . $body
+                unless $hdr->{Status} =~ /^2/;
+
+            my $data = eval { decode_json( $body ) };
+            die "Failed to decode API response: $body"
+                unless $data;
+
+            die "API response failed: $body"
+                unless $data->{ok};
+
+            $self->name( $data->{self}{name} );
+            $self->slack_properties( $data->{self} );
+
+            my $client = AnyEvent::WebSocket::Client->new;
+            Jifty->log->info( "Connecting to ".$data->{url} );
+            $client->connect( $data->{url} )->cb( sub {
+                # This will die if the connection attempt fails
+                $self->connection( eval { shift->recv } );
+                if ($@) {
+                    Jifty->log->warn("Failed to connect to websocket: $@; retrying in 5s");
+                    $self->reconnect( AE::timer( 5, 0, sub { $self->run } ) );
+                }
+
+                my $sub = Jifty->bus->new_listener;
+                $sub->subscribe(Jifty->bus->topic("test_result"));
+                $sub->poll( sub { $self->test_progress(@_) } );
+
+                $self->connection->on( each_message => sub {$self->each_message(@_)});
+                $self->connection->on( finish       => sub {$self->finish(@_)});
+
+                $self->heartbeat;
+            } );
+        };
 }
 
-sub error_reply {
-    my($incoming, $msg) = @_;
-    return $incoming->reply(
-        String::IRC->new( $msg )->maroon->stringify,
-    );
+sub send {
+    my $self = shift;
+    my (%msg) = @_;
+
+    $msg{id} = $self->get_id;
+
+    my $done = AnyEvent->condvar;
+    $self->pending_reply->{$msg{id}} = $done;
+
+    Jifty->log->debug( "Sending: ".encode_json(\%msg) );
+
+    $self->connection->send( AnyEvent::WebSocket::Message->new(
+        body => encode_json( \%msg )
+    ) );
+
+    unless (defined wantarray) {
+        $done->cb( sub {
+            my ($body) = $_[0]->recv;
+            Jifty->log->warn( "$msg{type} $msg{id} failed: ".encode_json($body->{error}) )
+                unless $body->{ok};
+        });
+    }
+    return $done;
 }
 
-sub incoming {
+sub send_to {
     my $self = shift;
-    my $incoming = shift;
+    my ($channel, $msg) = @_;
+    $msg =~ s/&/&/g;
+    $msg =~ s/</</g;
+    $msg =~ s/>/>/g;
+    $self->send( type => "message", channel => $channel, text => $msg );
+}
+
+sub heartbeat {
+    my $self = shift;
+
+    $self->ping( AE::timer( 10, 10, sub {
+        $self->send( type => "ping", ok => 1 );
+    } ) );
+}
+
+sub each_message {
+    my $self = shift;
+    my ($c, $m) = @_;
+
     Jifty::Record->flush_cache if Jifty::Record->can('flush_cache');
 
-    # Skip messages from the system
-    if ($incoming->sender->name =~ /\./) {
-        warn $incoming->sender->name . ": " .
-            $incoming->message;
-        return;
-    } elsif ($incoming->command eq "NOTICE") {
-        # NOTICE's are required to never trigger auto-replies
-        return;
+    if ($m->is_close) {
+        Jifty->log->warn("Websocket closed by remote server");
+    } elsif ($m->is_text) {
+        my $body = eval { decode_json($m->body) };
+        if ($@) {
+            Jifty->log->warn("Failed to decode body: ". $m->body);
+            return;
+        }
+
+        return unless $self->connection;
+        $self->heartbeat;
+
+        Jifty->log->debug( "Got: ".encode_json($body) );
+        if ($body->{reply_to}) {
+            my $call = delete $self->pending_reply->{$body->{reply_to}};
+            $call->send( $body ) if $call;
+        } else {
+            my $call = $self->can( "recv_" . $body->{type} );
+            $call->( $self, $body ) if $call;
+        }
     }
+}
+
+sub finish {
+    my $self = shift;
+
+    $self->connection( undef );
+    $self->ping( undef );
+
+    Jifty->log->warn( "Disconnected from websocket; reconnecting in 5s..." );
+    $self->reconnect( AE::timer( 5, 0, sub { $self->run } ) );
+}
+
+sub recv_message {
+    my $self = shift;
 
-    my $msg = $incoming->message;
-    $msg =~ s/\s*$//;
-    my $nick = $self->interface->irc->nick;
-    return if $incoming->isa("IM::Engine::Incoming::IRC::Channel")
+    my ($body) = @_;
+    return if $body->{hidden};
+
+    my $nick = $self->name;
+
+    my $msg = $body->{text};
+    return if $body->{channel} =~ /^C/
         and not $msg =~ s/^\s*$nick(?:\s*[:,])?\s*(?:please\s+)?//i;
 
     if ($msg =~ /^(?:re)?test\s+(.*)/) {
-        return $self->do_test($incoming, $1);
+        return $self->do_test($body->{channel}, $1);
     } elsif ($msg =~ /^status\s+(?:of\s+)?(.*)/) {
-        return $self->do_status($incoming, $1);
+        return $self->do_status($body->{channel}, $1);
     } elsif ($msg =~ /^(?:re)?sync(?:\s+(.*))?/) {
-        return $self->do_sync($incoming, $1);
+        return $self->do_sync($body->{channel}, $1);
     } elsif ($msg =~ /^queued?(?:\s+(.*))?/) {
-        return $self->do_queued($incoming, $1);
+        return $self->do_queued($body->{channel}, $1);
     } else {
-        return $incoming->reply( "What?" );
+        $self->send_to( $body->{channel} => "What?" );
     }
 }
 
 sub do_test {
     my $self = shift;
-    my ($incoming, $what) = @_;
+    my ($channel, $what) = @_;
     my $action = Smokingit::Action::Test->new(
         current_user => Smokingit::CurrentUser->superuser,
         arguments    => { commit => $what },
     );
     $action->validate;
-    return error_reply(
-        $incoming => $action->result->field_error("commit"),
+    return $self->send_to(
+        $channel => $action->result->field_error("commit"),
     ) unless $action->result->success;
 
     $action->run;
-    return error_reply(
-        $incoming => $action->result->error,
+    return $self->send_to(
+        $channel => $action->result->error,
     ) if $action->result->error;
 
-    return $incoming->reply( $action->result->message );
+    return $self->send_to(
+        $channel => $action->result->message
+    );
 }
 
 sub do_status {
     my $self     = shift;
-    my $incoming = shift;
-    my $what     = $self->lookup_commitish($incoming, @_);
-    if ($what->isa("Smokingit::Model::Commit")) {
-        my $msg = $what->short_sha . " is " . $what->status;
-        $msg = $what->short_sha . " is " . $self->describe_fail($what)
-            if $what->status eq "failing";
+    my $channel  = shift;
+    my $what = $self->lookup_commitish($channel, @_) or return;
 
-        $msg .= "; " . $self->queue_status($what)
-            if $what->status eq "queued";
+    my $msg = $what->short_sha . " is " . $what->status;
+    $msg = $what->short_sha . " is " . $self->describe_fail($what)
+        if $what->status eq "failing";
 
-        $msg .= " - " .  Jifty->web->url(path => "/test/".$what->short_sha);
+    $msg .= "; " . $self->queue_status($what)
+        if $what->status eq "queued";
 
-        return $incoming->reply( $msg );
-    } else {
-        return $what;
-    }
+    $msg .= " - " .  Jifty->web->url(path => "/test/".$what->short_sha);
+
+    $self->send_to( $channel => $msg );
 }
 
 sub lookup_commitish {
     my $self = shift;
-    my ($incoming, $what) = @_;
+    my ($channel, $what) = @_;
     if ($what =~ s/^\s*([a-fA-F0-9]{5,})\s*$/lc $1/e) {
         my $commits = Smokingit::Model::CommitCollection->new;
         $commits->limit( column => "sha", operator => "like", value => "$what%" );
         my @matches = @{ $commits->items_array_ref };
         if (not @matches) {
-            return error_reply(
-                $incoming => "No such SHA!"
+            $self->send_to(
+                $channel => "No such SHA!"
             );
+            return;
         } elsif (@matches > 1) {
-            return error_reply(
-                $incoming => "Found ".(@matches+0)." matching SHAs!",
+            $self->send_to(
+                $channel => "Found ".(@matches+0)." matching SHAs!",
             );
+            return;
         }
 
         return $matches[0];
@@ -154,29 +266,32 @@ sub lookup_commitish {
             my $project_obj = Smokingit::Model::Project->new;
             $project_obj->load_by_cols( name => $project );
             if (not $project_obj->id) {
-                return error_reply(
-                    $incoming => "No such project $project!",
+                $self->send_to(
+                    $channel => "No such project $project!",
                 );
+                return;
             }
             $branches->limit( column => "project_id", value => $project_obj->id );
         }
 
         my @matches = @{ $branches->items_array_ref };
         if (not @matches) {
-            return error_reply(
-                $incoming => "No branch $branch found",
+            $self->send_to(
+                $channel => "No branch $branch found",
             );
+            return;
         } elsif (@matches > 1) {
             @matches = map {$_->project->name} @matches;
-            return error_reply(
-                $incoming => "Found $branch in ".
+            $self->send_to(
+                $channel => "Found $branch in ".
                     join(", ", @matches).
                     ".  Try, $matches[0]:$branch"
             );
+            return;
         }
 
         # Need to re-parse if this got any updates
-        return $self->lookup_commitish($incoming, $what)
+        return $self->lookup_commitish($channel, $what)
             if $matches[0]->as_superuser->sync;
 
         return $matches[0]->current_commit;
@@ -185,37 +300,40 @@ sub lookup_commitish {
 
 sub do_sync {
     my $self = shift;
-    my ($incoming, $what) = @_;
+    my ($channel, $what) = @_;
 
     if (defined $what and $what =~ /^\s*(.*?)\s*$/) {
         $what = $1;
         my $project = Smokingit::Model::Project->new;
         $project->load_by_cols( name => $what );
         if (not $project->id) {
-            return error_reply(
-                $incoming => "No such project $what!",
-            );
+            $self->send_to( $channel => "No such project $what!" );
+        } else {
+            my @results = $project->as_superuser->sync;
+            if (@results) {
+                $self->send_to( $channel => join("; ", @results));
+            } else {
+                $self->send_to( $channel => "No changes" );
+            }
         }
-        my @results = $project->as_superuser->sync;
-        return $incoming->reply("No changes") unless @results;
-        return $incoming->reply(join("; ", @results));
     } else {
         my $projects = Smokingit::Model::ProjectCollection->new;
         $projects->unlimit;
         while (my $p = $projects->next) {
             $p->as_superuser->sync;
         }
-        return $incoming->reply("Synchronized ".$projects->count." projects");
+        $self->send_to(
+            $channel => "Synchronized ".$projects->count." projects"
+        );
     }
 }
 
 sub do_queued {
     my $self = shift;
-    my ($incoming, $what) = @_;
+    my ($channel, $what) = @_;
 
     if ($what) {
-        $what = $self->lookup_commitish($incoming, $what);
-        return $what unless $what->isa("Smokingit::Model::Commit");
+        $what = $self->lookup_commitish($channel, $what) or return;
     }
 
     my $queued = Smokingit::Model::SmokeResultCollection->queued;
@@ -225,7 +343,7 @@ sub do_queued {
     $msg .= join(" ", ";", $what->short_sha, $self->queue_status($what, $queued))
         if $what;
 
-    return $incoming->reply($msg);
+    $self->send_to( $channel => $msg );
 }
 
 sub queue_status {
@@ -266,12 +384,7 @@ sub test_progress {
         my $message = $self->do_analyze($smoke);
         return unless $message;
 
-        my $out = IM::Engine::Outgoing::IRC::Channel->new(
-            channel => Jifty->config->app('irc')->{channel},
-            message => $message,
-            command => "NOTICE",
-        );
-        $self->interface->send_message($out);
+        $self->send_to( Jifty->config->app('slack')->{channel} => $message );
     };
     warn "$@" if $@;
 }
@@ -366,7 +479,7 @@ sub do_analyze {
     if (not $smoke->configuration->auto) {
         my ($status) = $commit->status($smoke);
         if ($status eq "passing") {
-            $status = String::IRC->new("passes tests")->green;
+            $status = "passes tests";
         } else {
             my $fails = Smokingit::Model::SmokeFileResultCollection->new;
             $fails->limit(
@@ -379,7 +492,7 @@ sub do_analyze {
             );
             $status = "is failing " . enum(", ", sort map {$_->filename} @{$fails->items_array_ref});
             my $url = Jifty->web->url(path => "/test/".$commit->short_sha);
-            $status = String::IRC->new($status)->red . " - $url";
+            $status .= " - $url";
         }
         return $smoke->configuration->name . " of ".$commit->short_sha . " on ".$smoke->branch_name
             ." $status";
@@ -409,17 +522,15 @@ sub do_analyze {
     if (($branch->first_commit and $commit->sha eq $branch->first_commit->sha)
             or not @tested_parents) {
         if ($commit->status eq "passing") {
-            return "New branch $branchname " .
-              String::IRC->new("passes tests")->green;
+            return "New branch $branchname passes tests";
         } else {
             return "$author pushed a new branch $branchname which is " .
-              String::IRC->new($self->describe_fail($commit))->red . " - $url";
+              "$commit - $url";
         }
     } elsif ($commit->is_merge){
         my $mergename = $commit->is_merge;
         if ($commit->status eq "passing") {
-            return "Merged $mergename into $branchname, " .
-              String::IRC->new("passes tests")->green;
+            return "Merged $mergename into $branchname, passes tests";
         }
 
         # So the merge commit is fail: there are four possibilities,
@@ -430,30 +541,29 @@ sub do_analyze {
         my $branch_good = $branch_commit->status eq "passing";
 
         if ($trunk_good and $branch_good) {
-            return "$author merged $mergename into $branchname, which is " .
-                String::IRC->new($self->describe_fail($commit))->red .
+            return "$author merged $mergename into $branchname, which is $commit" .
                 ", although both parents were passing - $url";
         } elsif ($trunk_good and not $branch_good) {
             return "$author merged $mergename (".
-              String::IRC->new($self->describe_fail($commit))->red.
+              $self->describe_fail($branch_commit).
               ") into $branchname, which is now ".
-              String::IRC->new($self->describe_fail($commit))->red . " - $url";
+              $self->describe_fail($commit) . " - $url";
         } elsif (not $trunk_good and not $branch_good) {
             return "$author merged $mergename (".
-              String::IRC->new($self->describe_fail($commit))->red.
+              $self->describe_fail($branch_commit).
               ") into $branchname, which is still ".
-              String::IRC->new($self->describe_fail($commit))->red . " - $url";
+              $self->describe_fail($commit) . " - $url";
         } else {
             return "$author merged $mergename".
               " into $branchname, which is still ".
-              String::IRC->new($self->describe_fail($commit))->red . " - $url";
+              $self->describe_fail($commit) . " - $url";
         }
     } elsif ($commit->status ne "passing") {
         # A new commit on an existing branch, which fails tests.  Let's
         # check if this is better or worse than the previous commit.
         if (@tested_parents == grep {$_->status eq "passing"} @tested_parents) {
             return "$branchname by $author began ".
-                String::IRC->new($self->describe_fail($commit))->red .
+                $self->describe_fail($commit) .
                 " as of ".$commit->short_sha. " - $url";
         } else {
             # Was failing, still failing?  Let's not spam about it
@@ -462,8 +572,7 @@ sub do_analyze {
     } elsif (grep {$_->status ne "passing"} @tested_parents) {
         # A new commit on an existing branch, which passes tests but
         # whose parents didn't!
-        return "$branchname by $author now ".
-            String::IRC->new("passes tests")->green .
+        return "$branchname by $author now passes tests".
             " as of ".$commit->short_sha;
     } else {
         # A commit which passes, and whose parents all passed.  Go them?

commit ff669ec4cdd0a2ff2e430050f09e8b6428060efb
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Dec 9 15:39:29 2014 -0500

    Resolve channel name to id

diff --git a/lib/Smokingit/Slack.pm b/lib/Smokingit/Slack.pm
index 528850d..c8c5b74 100644
--- a/lib/Smokingit/Slack.pm
+++ b/lib/Smokingit/Slack.pm
@@ -18,6 +18,11 @@ has 'slack_properties' => (
     isa     => 'HashRef',
     default => sub { {} },
 );
+has 'channels' => (
+    is      => 'rw',
+    isa     => 'HashRef',
+    default => sub { {} },
+);
 
 has 'connection' => (
     is      => 'rw',
@@ -70,6 +75,10 @@ sub run {
             $self->name( $data->{self}{name} );
             $self->slack_properties( $data->{self} );
 
+            my %channels;
+            $channels{$_->{id}} = $_ for @{ $data->{channels} };
+            $self->channels( \%channels );
+
             my $client = AnyEvent::WebSocket::Client->new;
             Jifty->log->info( "Connecting to ".$data->{url} );
             $client->connect( $data->{url} )->cb( sub {
@@ -384,7 +393,13 @@ sub test_progress {
         my $message = $self->do_analyze($smoke);
         return unless $message;
 
-        $self->send_to( Jifty->config->app('slack')->{channel} => $message );
+        my $channel = Jifty->config->app('slack')->{channel};
+        unless ($channel =~ /^C/) {
+            ($channel) = map {$_->{id}}
+                grep {$_->{name} eq $channel}
+                values %{ $self->channels };
+        }
+        $self->send_to( $channel => $message );
     };
     warn "$@" if $@;
 }

-----------------------------------------------------------------------


More information about the Bps-public-commit mailing list