commit a8f1ec70b68583ee34c276564cb53bea55e0a32b from: lex0de date: Thu Sep 3 16:31:59 2026 UTC Connect Crybot to Supernets commit - d38bceff4a0824695b93b2bebc22a9e73c6b8fd6 commit + a8f1ec70b68583ee34c276564cb53bea55e0a32b blob - 316d1678ba5d5811bcadd191187443b5b8f12c5c blob + 24496ef559b4f745d5bafcfb6ab50c4f9678f968 --- crybot.pl +++ crybot.pl @@ -9,6 +9,7 @@ use autodie qw(:socket :io); 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; @@ -79,6 +80,7 @@ if ( $config{feed_state_file} !~ m{^/} ) { $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); @@ -102,13 +104,82 @@ my %COMMANDS = ( 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( @@ -252,8 +323,8 @@ sub join_channels { } 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; @@ -261,7 +332,7 @@ sub event_loop { 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}; } @@ -281,14 +352,33 @@ sub event_loop { } 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; } @@ -393,7 +483,7 @@ sub handle_channel_text { } sub poll_feeds { - my ( $sock, $config ) = @_; + my ( $connections, $config ) = @_; return unless @{ $config->{feed_sources} // [] }; my $state = $config->{feed_state} //= {}; @@ -445,9 +535,11 @@ sub poll_feeds { 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 @@ -88,4 +88,31 @@ subtest 'link is accepted when guid is absent' => sub ['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;