#!/usr/bin/perl
# rib.pl: Rudimentary IRC Bot in Perl
# Usage: perl rib.pl -n <nick> -s <host> -p <port> [-a <pass>] [-j <chans>] [-o <ops>] [-x <proxy>] [-f]
# Features: auto-joins channels, grants op (+o) to designated nicks, daemonizes by default
# Created by Luxferre in 2026, released into the public domain

use strict; use warnings; use POSIX qw(setsid);
use IO::Select; use IO::Socket::INET; use IO::Socket::SSL;

my ($nick, $server, $port, $pass, $auto_chans, $proxy, $op_nicks, $fg);
while(@ARGV) {
  my $o = shift;
  if   ($o =~ /^-(n|-nick)$/)        { $nick = shift; }
  elsif($o =~ /^-(s|-server)$/)      { $server = shift; }
  elsif($o =~ /^-(p|-port)$/)        { $port = shift; }
  elsif($o =~ /^-(a|P|w|-pass)$/)    { $pass = shift; }
  elsif($o =~ /^-(j|-join)$/)        { $auto_chans = shift; }
  elsif($o =~ /^-(x|-proxy|-socks)$/){ $proxy = shift; }
  elsif($o =~ /^-(o|-op|-ops)$/)     { $op_nicks = shift; }
  elsif($o =~ /^-(f|-foreground)$/)  { $fg = 1; }
}
die "Usage: $0 -n|--nick <nick> -s|--server <host> -p|--port <port> [-a|-P|-w|--pass <pass>] [-j|--join <chans>] [-o|--op|--ops <nicks>] [-x|--proxy <host:port>] [-f|--foreground]\n" unless $nick && $server && $port;

if($auto_chans) { my @c = map { /^#/ ? $_ : "#$_" } split /,/, $auto_chans; $auto_chans = join(',', @c); }
my %ops = map { (s/^[~\@\%+&!]+//r) => 1 } grep { length } split /\s*,\s*/, lc($op_nicks // '');

unless($fg) {
  fork() && exit(0); POSIX::setsid() or die "Cannot start new session: $!";
  open(STDIN, '<', '/dev/null'); open(STDOUT, '>', '/dev/null'); open(STDERR, '>', '/dev/null');
}

my $delay = 1;

sub show {
  my $l = shift;
  print(($l =~ /^\:\S+\s+\d+\s+\S+\s*\:?(.*)/)              ? "$1\n" :
        ($l =~ /^\:([^!]+)!\S+\s+PRIVMSG\s+\S+\s+:(.*)/)  ? "<$1> $2\n" :
        ($l =~ /^\:([^!]+)!\S+\s+(JOIN|PART)\s+:?#?(\S+)/)? "* $1 " . lc($2) . "ed #$3\n" :
        ($l =~ /^\:([^!]+)!\S+\s+QUIT(?:\s+\:(.*))?/)     ? "* $1 quit (reason: " . ($2 // "") . ")\n" :
        ($l =~ /^(?::\S+\s+)?NOTICE\s+\S+\s+:(.*)/)        ? "- $1 -\n" :
        ($l =~ /^(?::\S+\s+)?(.*)/)                        ? "$1\n" : "");
}

sub connect_sock {
  my ($host, $port, $proxy, $is_ssl) = @_;
  if($proxy) {
    my ($ph, $pp) = $proxy =~ /^(?:(.*):)?(\d+)$/ ? ($1 || '127.0.0.1', $2) : ('127.0.0.1', 1080);
    my $s = IO::Socket::INET->new(PeerHost => $ph, PeerPort => $pp, Proto => 'tcp') or return undef;
    print $s "\x05\x01\x00"; $s->sysread(my $b, 2); return undef unless defined $b && $b eq "\x05\x00";
    print $s "\x05\x01\x00\x03" . chr(length $host) . $host . pack('n', $port);
    $s->sysread($b, 4); return undef unless defined $b && substr($b, 0, 2) eq "\x05\x00";
    my $a = ord(substr($b, 3, 1)); $s->sysread($b, $a == 1 ? 6 : $a == 4 ? 18 : do { $s->sysread(my $l, 1); ord($l) + 2 });
    return $is_ssl ? IO::Socket::SSL->start_SSL($s, SSL_verify_mode => 0, SSL_hostname => $host) : $s;
  }
  my $class = $is_ssl ? 'IO::Socket::SSL' : 'IO::Socket::INET';
  return $class->new(PeerHost => $host, PeerPort => $port, ($is_ssl ? (SSL_verify_mode => 0) : (Proto => 'tcp')));
}

OUTER: while(1) {
  my $is_ssl = ($port == 6697 || $ENV{USE_SSL});
  my $sock   = connect_sock($server, $port, $proxy, $is_ssl)
    or (print("Connect failed: " . ($@ || $! || "proxy error") . ". Reconnecting in ${delay}s...\n"), sleep($delay), $delay *= 2, next OUTER);
  $sock->autoflush(1);

  print "Connected to $server:$port\n";
  print $sock "CAP LS 302\r\nCAP END\r\n" . ($pass ? "PASS $pass\r\n" : "") . "NICK $nick\r\nUSER $nick 0 * :$nick\r\n";

  my $sel = IO::Select->new($sock);
  my ($registered, $joined, $recv_buf) = (0, 0, '');

  while(1) {
    my @ready = $sel->can_read(0.1);
    push @ready, $sock if $sock->can('pending') && $sock->pending() && !grep { $_ == $sock } @ready;

    foreach my $fh(@ready) {
      while(1) {
        my $read = $sock->sysread(my $tmp, 4096);
        close($sock), print("Connection lost. Reconnecting in ${delay}s...\n"), sleep($delay), $delay *= 2, next OUTER if !$read;
        $recv_buf .= $tmp;
        last unless $sock->can('pending') && $sock->pending();
      }

      while($recv_buf =~ s/^(.*?)\r?\n//) {
        my $data = $1;
        if($data =~ /^(?::\S+\s+)?PING\s+(.*)/i) { print $sock "PONG $1\r\n"; next; }
        if($data =~ /^\:\S+\s+(?:001|376|422)\s+/) {
          show("Registration complete.") if !$registered;
          $registered = 1; $delay = 1;
          if(!$joined && $auto_chans) { $joined = 1; print $sock "JOIN $_\r\n" for split /,/, $auto_chans; }
        }
        elsif($data =~ /^\:([^! ]+)(?:!\S+)?\s+JOIN\s+:?(#\S+)/i) {
          my ($u, $c) = ($1, $2); $u =~ s/^[~\@\%+&!]+//;
          print $sock "MODE $c +o $u\r\n" if defined $u && length $u && $ops{lc($u)} && lc($u) ne lc($nick);
        }
        elsif($data =~ /^\:\S+\s+353\s+.*?(#\S+)\s+:(.*)/i) {
          my ($c, $ns) = ($1, $2 // '');
          for(split /\s+/, $ns) { s/^[~\@\%+&!]+//; print $sock "MODE $c +o $_\r\n" if length && $ops{lc($_)} && lc($_) ne lc($nick); }
        }
        show($data);
      }
    }
  }
}
