#!/usr/bin/perl
# rip.pl (read as "ripple"): Rudimentary IRC Program in Perl
# Features: basic command set with passthrough for the rest, multi-channels, SSL/TLS
# Non-features (by design): CTCP, (X|S|)DCC, rich TUI, config files, multi-servers
# Usage: [rlwrap] perl rip.pl -n <nick> -s <host> -p <port> [-a <pass>] [-j <chans>] [-x <proxy>]
# (use rlwrap to enable cursor keys and command history)
# Created by Luxferre in 2026, released into the public domain 

use strict; use warnings;
use IO::Select; use IO::Socket::INET; use IO::Socket::SSL;

my ($nick, $server, $port, $pass, $auto_chans, $proxy);
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; }
}
die "Usage: $0 -n|--nick <nick> -s|--server <host> -p|--port <port> [-a|-P|-w|--pass <pass>] [-j|--join <chans>] [-x|--proxy <host:port>]\n" unless $nick && $server && $port;

my ($default_chan, $delay) = (undef, 1);
if($auto_chans) { my @c = map { /^#/ ? $_ : "#$_" } split /,/, $auto_chans; $auto_chans = join(',', @c); $default_chan = $c[0]; }

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, *STDIN);
  my $stdin_fd = fileno(STDIN);
  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) {
      if(fileno($fh) == $stdin_fd) {
        my $line = <$fh>;
        if(!defined $line) { print $sock "QUIT :EOF\r\n"; exit 0; }
        chomp($line);
        next unless length $line && $registered;

        if($line =~ m{^/(\w+)\s*(.*)}i) {
          my ($cmd, $args) = (lc $1, $2);
          my ($c) = $args =~ /^#?(\S+)/; $c = "#$c" if $c && $c !~ /^#/;
          if   ($cmd eq 'quit') { print $sock "QUIT" . ($args ? " :$args" : "") . "\r\n"; exit 0; }
          elsif($cmd eq 'join') { if($c) { $default_chan = $c; print $sock "JOIN $c\r\n"; } }
          elsif($cmd eq 'part') { if($c) { print $sock "PART $c\r\n"; $default_chan = undef if $default_chan && $c eq $default_chan; } }
          elsif($cmd eq 'msg')  { my ($t, $m) = $args =~ /^(\S+)\s+(.*)/; print $sock "PRIVMSG $t :$m\r\n" if $t && $m; }
          elsif($cmd eq 'nick') { print $sock "NICK $args\r\n" if $args; $nick = $args if $args; }
          elsif($cmd eq 'chan') { $default_chan = $c if $c; }
          elsif($cmd eq 'whoami'){ print $sock "WHOIS $nick\r\n"; }
          elsif($cmd eq 'help') { print "Commands:\n  /join <chan>        Join channel\n  /part <chan>        Leave channel\n  /msg <target> <msg> Send private message\n  /nick <new_nick>    Change nick\n  /chan <chan>        Set default channel\n  /whoami             Whois on self\n  /quit [reason]      Disconnect\n"; }
          else                  { $line =~ s/^\///; print $sock "$line\r\n"; }
        }
        else {
          unless($default_chan) { warn "No channel set. Use /join or /chan first.\n"; next; }
          print $sock "PRIVMSG $default_chan :$line\r\n";
        }
      }
      else {
        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 && (my $to_join = $auto_chans // $default_chan)) {
              $joined = 1;
              print $sock "JOIN $_\r\n" for split /,/, $to_join;
            }
          }
          show($data);
        }
      }
    }
  }
}
