#!/opt/perl/bin/perl use strict; use URI; use AnyEvent::Impl::Perl; use IO::Handle; use JSON::Syck; use JSONConnection; use Net::IRC3::Client::Connection; use POSIX qw/strftime/; $Net::IRC3::Client::Connection::DEBUG = 1; our $CFG; our %ALIASES; our %CONNS; our %LOGS; our $JS; sub log_line { my ($server, $src, $line) = @_; my $logdir = $CFG->{log_dir} || "$ENV{HOME}/.jsonirc_logs/"; my $ts = POSIX::strftime "%F %T %Z", localtime (time); eval { unless (-e $logdir) { mkdir $logdir or die "Couldn't make directory '$logdir': $!"; } my $logfile = "$logdir/${server}" . ($src ne "" ? "_$src" : ""); unless ($LOGS{$logfile}) { open my $logfh, ">>", "$logfile" or die "Couldn't open '$logfile': $!"; $LOGS{$logfile} = $logfh; $logfh->autoflush (1); $LOGS{$logfile}->print ("---- $ts ---- starting log ----\n"); } $LOGS{$logfile}->print ("$ts: $line\n"); }; if ($@) { error_reply (undef, undef, logging_error => "Couldn't log for $server $src: $@"); } } sub load_cfg { $CFG ||= {}; return unless -e "$ENV{HOME}/.jsonircrc"; open CFGH, "<", "$ENV{HOME}/.jsonircrc" or die "Couldn't open ~/.jsonircrc: $!"; $::CFG = JSON::Syck::Load (do { local $/; }); } sub save_cfg { open CFGH, ">", "$ENV{HOME}/.jsonircrc" or die "Couldn't open for writing ~/.jsonircrc: $!"; print CFGH (JSON::Syck::Dump ($::CFG)); close CFGH; } sub to_dest_id { my ($irccon, $targ) = @_; $targ ||= ''; $irccon = $ALIASES{$irccon} || $irccon; my $uri = new URI; $uri->scheme ("jsirc"); $uri->authority ($irccon); $uri->path ($targ); "$uri" } sub unalias { my ($id) = @_; for (keys %ALIASES) { if ($id eq $ALIASES{$_}) { return $_; } } } sub lookup_connection { my ($id) = @_; my $alias = unalias ($id); return $CONNS{$alias || $id} } sub from_dest_id { my ($dest_id) = @_; my $uri = URI->new ($dest_id); my $path = ($uri->path_segments ())[1]; return (lookup_connection ($uri->authority), unalias ($uri->authority) || $uri->authority, $path); } sub connect_irc { my ($host, $port, $alias) = @_; my $irccon = "$host:$port"; my $pc = $CONNS{$irccon} = Net::IRC3::Client::Connection->new; $ALIASES{$irccon} = $alias if defined $alias; $pc->reg_cb ( channel_add => sub { my ($pc, $chan, @nicks) = @_; $chan = lc $chan; my $dest = to_dest_id ($irccon, $chan); my @ids = map { to_dest_id ($irccon, $_) } @nicks; $JS->broadcast ({ src => $dest, type => 'subid', command => 'add', ids => \@ids, timestamp => time (), }); log_line ($irccon, $chan, "$chan add: @nicks"); 1; }, channel_remove => sub { my ($pc, $chan, @nicks) = @_; $chan = lc $chan; my $dest = to_dest_id ($irccon, $chan); my @ids = map { to_dest_id ($irccon, $_) } @nicks; $JS->broadcast ({ src => $dest, type => 'subid', command => 'remove', ids => \@ids, timestamp => time (), }); log_line ($irccon, $chan, "$chan remove: @nicks"); 1; }, channel_change => sub { my ($pc, $chan, $old_nick, $new_nick) = @_; $chan = lc $chan; my $dest = to_dest_id ($irccon, $chan); $JS->broadcast ({ src => $dest, type => 'subid', command => 'change', old_id => to_dest_id ($irccon, $old_nick), new_id => to_dest_id ($irccon, $new_nick), timestamp => time (), }); log_line ($irccon, $chan, "$chan nick change: $old_nick => $new_nick"); 1; }, publicmsg => sub { my ($pc, $chan, $msg) = @_; $chan = lc $chan; my ($nick) = Net::IRC3::Util::split_prefix ($msg->{prefix}); my $dest = to_dest_id ($irccon, $chan); my $nickdest = to_dest_id ($irccon, $nick); $JS->broadcast ({ src => $dest, type => (uc ($msg->{command}) eq 'NOTICE' ? "notice" : "message"), msg_scope => "public", message => $msg->{trailing}, timestamp => time (), to => { nick => $pc->nick, id => to_dest_id ($irccon, $pc->nick) }, from => { nick => $nick, id => $nickdest }, }); log_line ($irccon, $chan, (uc ($msg->{command}) eq 'NOTICE' ? "{$nick}" : "<$nick>") . " $msg->{trailing}"); 1; }, privatemsg => sub { my ($pc, $dsgnick, $msg) = @_; my ($nick) = Net::IRC3::Util::split_prefix ($msg->{prefix}); my $dest = to_dest_id ($irccon, $nick); $JS->broadcast ({ src => $dest, type => "message", type => (uc ($msg->{command}) eq 'NOTICE' ? "notice" : "message"), msg_scope => "private", message => $msg->{trailing}, timestamp => time (), to => { nick => $pc->nick, id => to_dest_id ($irccon, $pc->nick) }, from => { nick => $nick, id => $dest }, }); log_line ($irccon, $nick, (uc ($msg->{command}) eq 'NOTICE' ? "{$nick}" : "<$nick>") . " $msg->{trailing}"); 1; }, error => sub { my ($pc, $code, $message, @params) = @_; my $name = Net::IRC3::Util::rfc_code_to_name ($code); error_reply (undef, undef, irc_error => "$name: $message", to_dest_id ($irccon)); log_line ($irccon, "", "$name: $message"); 1; }, connect => sub { info_reply (undef, connected => "Connected to $host:$port " . (defined $alias ? "(aka $alias)" : ""), to_dest_id ($irccon) ); log_line ($irccon, "", "connected"); 1; }, disconnect => sub { error_reply (undef, undef, disconnected => "Lost connection to $host:$port " . (defined $alias ? "(aka $alias)" : ""), to_dest_id ($irccon) ); log_line ($irccon, "", "disconnected"); delete $CONNS{"$host:$port"}; delete $ALIASES{"$host:$port"}; 1; } ); eval { $pc->connect ($host, $port); }; if ($@) { error_reply (undef, undef, connection_error => "Couldn't connect to $host:$port " . (defined $alias ? "(aka $alias)" : "") . ": $@", to_dest_id ($irccon) ); log_line ($irccon, "", "couldn't connect to $host:$port: $@"); delete $CONNS{"$host:$port"}; delete $ALIASES{"$host:$port"}; return; } my ($nick, $user, $real) = @{ $CFG->{userinfo}->{$alias || "$host:$port"} || $CFG->{default_userinfo} || [] }; $nick or die "No nickname in configuration given"; $user ||= $nick; $real ||= $nick; $pc->register ($nick, $user, $real); for (@{$CFG->{channels}->{$alias || "$host:$port"}}) { $pc->send_srv (JOIN => undef => $_); } } sub update_connections { for (map { /^(\S+):(\d+)/ ? [$1, $2, $_] : [] } keys %CONNS) { my ($h, $p) = @$_; unless ( grep { ($_->{host} eq $h) && (($_->{port} || 6667) == $p) && ($_->{connect}) } @{$CFG->{servers}}) { $CONNS{$_->[2]}->disconnect; } } for my $con (@{$CFG->{servers}}) { my ($host, $port) = ($con->{host}, $con->{port} || 6667); if ($con->{connect} and not $CONNS{"$host:$port"}) { eval { connect_irc ($host, $port, $con->{alias}); }; if ($@) { error_reply (undef, undef, connect_irc => "Couldn't connect to IRC server '$host:$port': $@", to_dest_id ("$host:$port")); log_line ("$host:$port", "", "couldn't connect to $host:$port: $@"); } } } } sub info_reply { my ($lid, $type, $string, $infoid) = @_; my @source = defined $infoid ? (info_id => $infoid) : (); if (defined $lid) { $JS->send_data ($lid, { type => 'info', info_type => $type, message => $string, @source }); } else { $JS->broadcast ({ type => 'info', info_type => $type, message => $string, @source }); } } sub error_reply { my ($lid, $srcpkt, $type, $string, $errid) = @_; my @source = defined $errid ? (error_id => $errid) : (); my @id = (defined $srcpkt and defined $srcpkt->{id}) ? (id => $srcpkt->{id}) : (); if (defined $lid) { $JS->send_data ($lid, { timestamp => time, type => 'error', error_type => $type, message => $string, @source, @id }); } else { $JS->broadcast ({ timestamp => time, type => 'error', error_type => $type, message => $string, @source, @id }); } } sub reply { my ($lid, $srcpkt, $type, @reply) = @_; my @id = (defined $srcpkt and defined $srcpkt->{id}) ? (id => $srcpkt->{id}) : (); if (defined $lid) { $JS->send_data ($lid, { timestamp => time, type => 'reply', reply_type => $type, @reply, @id }); } else { $JS->broadcast ({ timestamp => time, type => 'reply', reply_type => $type, @reply, @id }); } } load_cfg; my $c = AnyEvent->condvar; $JS = JSONConnection->new ( packet_cb => sub { my ($JS, $lid, $data) = @_; if ($data->{type} eq 'message') { my ($con, $irccon, $dest) = from_dest_id ($data->{dest}); unless (defined $con) { error_reply ($lid, $data, no_connection => "No connection to ID.", $data->{dest}); return } $con->send_srv (PRIVMSG => $data->{message} => $dest); $JS->broadcast ({ src => $data->{dest}, type => "message", msg_scope => $data->{msg_scope}, message => $data->{message}, is_echo => 1, timestamp => time, to => { nick => $dest, id => $data->{dest} }, from => { nick => $con->nick, id => to_dest_id ($irccon, $con->nick) }, }); log_line ($irccon, $dest, "<".$con->nick."> $data->{message}"); reply ($lid, $data, 'sent_message'); } elsif ($data->{type} eq 'command') { if ($data->{command} eq 'reload') { my $old_cfg = $::CFG; eval { load_cfg; update_connections; }; if ($@) { $::CFG = $old_cfg; error_reply ($lid, $data, reload_error => "error on reloading: $@"); } else { reply ($lid, $data, 'reloaded'); } } elsif ($data->{command} eq 'list_connections') { } elsif ($data->{command} eq 'list_ids') { } } elsif ($data->{type} eq 'raw') { my $raw = $data->{message}; my ($con, $irccon, $dest) = from_dest_id ($data->{dest}); unless (defined $con) { error_reply ($lid, $data, no_connection => "No connection to ID.", $data->{dest}); return } $con->send_raw ($raw); reply ($lid, $data, 'sent_raw_message'); } else { error_reply ($lid, $data, bad_packet => "Did not understand this packet"); } 1 }, connect_cb => sub { my ($JS, $lid) = @_; $JS->send_data ($lid, { type => "hello" }); for my $irccon (keys %CONNS) { for my $chan (keys %{$CONNS{$irccon}->channel_list}) { my $dest = to_dest_id ($irccon, lc $chan); $JS->send_data ($lid, { src => $dest, type => 'subid', command => 'list', ids => [ map { to_dest_id ($irccon, $_) } keys %{$CONNS{$irccon}->channel_list ()->{$chan}} ], timestamp => time (), }); } } }); update_connections; $JS->start_listener; $c->wait;