package Bot::Flowdock::IRC; use Moose; use MooseX::NonMoose; use JSON; use List::MoreUtils 'any'; use Net::Flowdock 0.03; use Net::Flowdock::Stream; use String::Truncate 'elide'; use WWW::Shorten 'GitHub'; extends 'Bot::BasicBot'; sub FOREIGNBUILDARGS { my $class = shift; my (%args) = @_; delete $args{$_} for qw(email token key organization flow); return %args; } # XXX require this for now so that we can tell which account is the bot # (to allow us to filter those out so we don't get echos) # ideally, there would be a better way to detect this has email => ( is => 'ro', isa => 'Str', required => 1, ); has token => ( is => 'ro', isa => 'Str', required => 1, ); has key => ( is => 'ro', isa => 'Str', ); has organization => ( is => 'ro', isa => 'Str', required => 1, ); has flow => ( is => 'ro', isa => 'Str', required => 1, ); has flowdock_api => ( is => 'ro', isa => 'Net::Flowdock', lazy => 1, default => sub { my $self = shift; Net::Flowdock->new( token => $self->token, ($self->key ? (key => $self->key) : ()), ); } ); has flowdock_stream => ( is => 'ro', isa => 'Net::Flowdock::Stream', lazy => 1, default => sub { my $self = shift; Net::Flowdock::Stream->new( token => $self->token, flows => [join('/', $self->organization, $self->flow)], ); }, ); has _id_map => ( traits => ['Hash'], isa => 'HashRef[Str]', default => sub { {} }, handles => { _set_name_for_id => 'set', name_from_id => 'get', flowdock_names => 'values', }, ); has _my_id => ( is => 'rw', isa => 'Int', ); sub connected { my $self = shift; my $flow = $self->flowdock_api->get_flow({ organization => $self->organization, flow => $self->flow, }); for my $user (@{ $flow->body->{users} }) { $self->_set_name_for_id($user->{id}, $user->{nick}); if ($user->{email} eq $self->email) { $self->_my_id($user->{id}); } } } sub tick { my $self = shift; for (1..20) { my $event = $self->flowdock_stream->get_next_event; last unless $event; next if $event->{user} == $self->_my_id; my $app = $event->{app} || ''; my $type = $event->{event}; if ($app eq 'chat') { if ($type eq 'message' || $type eq 'line') { $self->flowdock_message($event); } elsif ($type eq 'status') { $self->flowdock_status($event); } elsif ($type eq 'user-edit') { $self->flowdock_user_edit($event); } else { warn "Unknown chat event type $type: " . encode_json($event); } } elsif ($app eq 'influx') { # i think this is timeline stuff if ($type eq 'vcs' && any { /^github/ } @{ $event->{tags} }) { $self->flowdock_github_event($event); } else { $self->flowdock_unknown_inbox_event($event); } } else { # we don't care activity events if ($type ne 'activity.user') { warn "Unknown event type $type for unknown app $app: " . encode_json($event); } } } return 1; } sub flowdock_message { my $self = shift; my ($event) = @_; # skip if this is a message that we just sent return if exists $event->{external_user_name}; my $name = $self->name_from_id($event->{user}); $self->_say_to_channel( $event->{content}, $name, emoted => ($event->{event} eq 'line' || $event->{event} eq 'status') ); } sub flowdock_status { my $self = shift; my ($event) = @_; $self->flowdock_message($event); } sub flowdock_user_edit { my $self = shift; my ($event) = @_; my $id = $event->{user}; my $nick = $event->{content}{user}{nick}; my $oldnick = $self->name_from_id($id); $self->_say_to_channel("$oldnick is now known as $nick"); $self->_set_name_for_id($id, $nick); } sub flowdock_github_event { my $self = shift; my ($event) = @_; my $content = $event->{content}; my $pusher = $content->{pusher}{name}; $pusher = $content->{commits}[0]{author}{username} . '(?)' if $pusher eq 'none'; # XXX ? my $commits = @{ $content->{commits} }; my $repo = $content->{repository}{name}; (my $branch = $content->{ref}) =~ s{^refs/heads/}{}; my $compare = makeashorterlink($content->{compare}); my $commit_messages = join(' / ', map { $_->{message} } @{ $content->{commits} }); my $msg = "[github] $pusher pushed $commits commit" . ($commits == 1 ? '' : 's') . " to $repo/$branch (compare $compare): $commit_messages"; $self->_say_to_channel($msg); } sub flowdock_unknown_inbox_event { my $self = shift; my ($event) = @_; warn "unknown timeline event type $event->{event}: " . encode_json($event); $self->_say_to_channel("[$event->{event}] unknown"); } sub said { my $self = shift; my ($args) = @_; my $address = $args->{address} || ''; return if $address eq 'msg'; # XXX: Bot::BasicBot does a lot of "helpful" munging of messages that we # receive. this is annoying for this use case. look into switching to raw # poco::irc at some point. my $msg = ($address ? "$address: " : '') . $args->{body}; # XXX when they allow external users to post status update events, fix this $msg = '*' . $msg . '*' if $args->{emoted}; $self->_say_to_flowdock($msg, $args->{who}); return; } around emoted => sub { my $orig = shift; my $self = shift; my ($args) = @_; $args->{emoted} = 1; return $self->$orig($args); }; sub nick_change { my $self = shift; my ($old, $new) = @_; $self->_say_to_flowdock("$old is now known as $new"); } sub _say_to_channel { my $self = shift; my ($body, $from, %params) = @_; if (defined($from)) { $body = $params{emoted} ? "* $from $body" : "<$from> $body"; } else { $body = "-!- $body"; } $body =~ s/\n/ /g; $body = elide($body, 275, { at_space => 1 }); $self->say( channel => ($self->channels)[0], body => $body, ); } sub _say_to_flowdock { my $self = shift; my ($body, $from) = @_; if (defined($from)) { $self->flowdock_api->push_chat({ external_user_name => $from, content => $body, }); } else { $self->flowdock_api->send_message({ organization => $self->organization, flow => $self->flow, event => 'status', content => $body, }); } } __PACKAGE__->meta->make_immutable; no Moose; 1;