commit - d38bceff4a0824695b93b2bebc22a9e73c6b8fd6
commit + a8f1ec70b68583ee34c276564cb53bea55e0a32b
blob - 316d1678ba5d5811bcadd191187443b5b8f12c5c
blob + 24496ef559b4f745d5bafcfb6ab50c4f9678f968
--- crybot.pl
+++ crybot.pl
use Socket qw(AF_INET PF_INET SOCK_STREAM inet_aton inet_ntoa sockaddr_in);
use IO::Select;
+use List::Util qw(first);
use IO::Socket::SSL;
use Digest::MD5 qw(md5_hex);
use IP::Country::Fast;
$config{feed_state_file} = File::Spec->catfile( $Bin, $config{feed_state_file} );
}
$config{feed_state} = load_feed_state( $config{feed_state_file} );
+$config{servers} = [ build_server_configs( \%config ) ];
$ua->agent('Cryogenix IRC Bot');
$ua->timeout(5);
main() unless caller;
sub main {
- my $sock = connect_to_server(\%config);
- register_with_server($sock, \%config);
- identify_services($sock, \%config);
- join_channels($sock, \%config);
- event_loop($sock, \%config);
+ my @connections;
+
+ for my $server ( @{ $config{servers} } ) {
+ my $sock = eval { connect_to_server($server) };
+ if ( !$sock ) {
+ my $error = $@ || 'connection failed';
+ warn "IRC connection to $server->{server} failed: $error";
+ die $error if $server->{required};
+ next;
+ }
+
+ my $registered = eval {
+ register_with_server( $sock, $server );
+ identify_services( $sock, $server );
+ join_channels( $sock, $server );
+ 1;
+ };
+ if ( !$registered ) {
+ my $error = $@ || 'registration failed';
+ warn "IRC registration on $server->{server} failed: $error";
+ close $sock;
+ die $error if $server->{required};
+ next;
+ }
+
+ push @connections, { config => $server, sock => $sock };
+ }
+
+ die "No IRC connections available\n" unless @connections;
+ event_loop( \@connections, \%config );
}
+sub build_server_configs {
+ my ($config) = @_;
+ my $supernets = {
+ key => 'supernets',
+ required => 0,
+ server => $ENV{CRYBOT_SUPERNETS_SERVER} // 'irc.supernets.org',
+ port => $ENV{CRYBOT_SUPERNETS_PORT} // 6697,
+ nick => $ENV{CRYBOT_SUPERNETS_NICK} // $config->{nick},
+ altnick => $ENV{CRYBOT_SUPERNETS_ALTNICK} // $config->{altnick},
+ login => $ENV{CRYBOT_SUPERNETS_LOGIN} // $config->{login},
+ version => $ENV{CRYBOT_SUPERNETS_VERSION} // $config->{version},
+ pass => defined $ENV{CRYBOT_SUPERNETS_PASSWORD}
+ ? $ENV{CRYBOT_SUPERNETS_PASSWORD}
+ : $config->{pass},
+ sasl_user => defined $ENV{CRYBOT_SUPERNETS_SASL_USER}
+ ? $ENV{CRYBOT_SUPERNETS_SASL_USER}
+ : $config->{sasl_user},
+ sasl_pass => defined $ENV{CRYBOT_SUPERNETS_SASL_PASS}
+ ? $ENV{CRYBOT_SUPERNETS_SASL_PASS}
+ : $config->{sasl_pass},
+ channels => [ uniq( $ENV{CRYBOT_SUPERNETS_CHANNEL} // '#supernets' ) ],
+ };
+ my $primary = {
+ key => 'libera',
+ required => 1,
+ server => $config->{server},
+ port => $config->{port},
+ nick => $config->{nick},
+ altnick => $config->{altnick},
+ login => $config->{login},
+ version => $config->{version},
+ pass => $config->{pass},
+ sasl_user => $config->{sasl_user},
+ sasl_pass => $config->{sasl_pass},
+ channels => [ uniq( $config->{channel}, '#cryogenix' ) ],
+ };
+
+ return map {
+ my %server = %{$config};
+ @server{ keys %{$_} } = values %{$_};
+ \%server;
+ } ( $primary, $supernets );
+}
+
sub connect_to_server {
my ($config) = @_;
IO::Socket::SSL->new(
}
sub event_loop {
- my ( $sock, $config ) = @_;
- my $selector = IO::Select->new($sock);
+ my ( $connections, $config ) = @_;
+ my $selector = IO::Select->new( map { $_->{sock} } @{$connections} );
my $have_feeds = @{ $config->{feed_sources} // [] } ? 1 : 0;
my $next_poll = time;
my $now = time;
if ( $have_feeds && $now >= $next_poll ) {
- eval { poll_feeds( $sock, $config ); };
+ eval { poll_feeds( $connections, $config ); };
warn "Feed poll failed: $@" if $@;
$next_poll = $now + $config->{feed_poll_interval};
}
}
if (@ready) {
- my $raw = <$sock>;
- last unless defined $raw;
- chomp $raw;
- $raw =~ s/\r$//;
- debug_log( $config, "<< $raw" );
- handle_line( $sock, $config, $raw );
+ for my $sock (@ready) {
+ my $fd = fileno $sock;
+ my $connection = first { fileno( $_->{sock} ) == $fd } @{$connections};
+ next unless $connection;
+
+ my $raw = <$sock>;
+ if ( !defined $raw ) {
+ $selector->remove($sock);
+ @{$connections} = grep { fileno( $_->{sock} ) != $fd } @{$connections};
+ close $sock;
+ next;
+ }
+
+ chomp $raw;
+ $raw =~ s/\r$//;
+ debug_log( $connection->{config}, "<< $raw" );
+ eval {
+ handle_line( $sock, $connection->{config}, $raw );
+ 1;
+ } or warn "IRC input failed on $connection->{config}{server}: $@";
+ }
}
+ if ( !@{$connections} ) {
+ die "All IRC connections closed\n";
+ }
+
if ( !@ready && !$have_feeds ) {
next;
}
}
sub poll_feeds {
- my ( $sock, $config ) = @_;
+ my ( $connections, $config ) = @_;
return unless @{ $config->{feed_sources} // [] };
my $state = $config->{feed_state} //= {};
my $label = $feed->{label} // 'Update';
my $message = sprintf '[%s] %s - %s', $label, $title, $link;
- for my $chan ( @{ $config->{channels} // [] } ) {
- next unless $chan;
- irc_print( $sock, "PRIVMSG $chan :$message" );
+ for my $connection ( @{$connections} ) {
+ for my $chan ( @{ $connection->{config}{channels} // [] } ) {
+ next unless $chan;
+ irc_print( $connection->{sock}, "PRIVMSG $chan :$message" );
+ }
}
}
}
blob - 7eb77fbc249364e48a0ebd2e7a82bee57b5c136c
blob + 33bfa26f84742fa9ae5d8a2cfa1f181a7eccaecf
--- t/feed_polling.t
+++ t/feed_polling.t
['https://example.invalid/new'], 'link-only item is announced' );
};
+subtest 'separate server configurations keep channels isolated' => sub {
+ local $ENV{CRYBOT_SUPERNETS_SERVER} = 'irc.supernets.org';
+ local $ENV{CRYBOT_SUPERNETS_PORT} = 6697;
+ local $ENV{CRYBOT_SUPERNETS_CHANNEL} = '#supernets';
+
+ my @servers = build_server_configs(
+ {
+ server => 'irc.libera.chat',
+ port => 6697,
+ nick => 'GCHQ',
+ altnick => 'GCHQ2',
+ login => 'GCHQ',
+ version => 'GCHQ Bot',
+ channel => '#cryogenix',
+ pass => 'test-password',
+ }
+ );
+
+ is( scalar @servers, 2, 'both server configurations are present' );
+ is( $servers[0]{server}, 'irc.libera.chat', 'primary server is preserved' );
+ is_deeply( $servers[0]{channels}, ['#cryogenix'],
+ 'primary channel remains on the primary server' );
+ is( $servers[1]{server}, 'irc.supernets.org', 'Supernets server is configured' );
+ is_deeply( $servers[1]{channels}, ['#supernets'],
+ 'Supernets channel is isolated to its server' );
+};
+
done_testing;