#! /usr/bin/env false use v6.d; use Log; use IRC::Client::Core; use IRC::Client::Handler; use IRC::Client::Message; use IRC::Client::Plugin; #| A simple IRC client, intended for automating interactions. unit class IRC::Client; #| The host to connect to. has Str $.host = '127.0.0.1'; #| The port to connect to. has Int $.port = 6697; #| Use SSL. Requires IO::Socket::Async::SSL to be installed. has Bool $.ssl = True; #| A list of channels to join on startup. has Str @.channels is rw; #| A list of acceptable nicknames to use. has Str @.nicks; #| The user to identify as. has Str $.user = 'raku'; #| The real name to identify as. has Str $.real-name = 'IRC::Client'; #| A list of plugins to use. has IRC::Client::Plugin @.plugins; #| The timeout between liveness checks (a PING to itself). has Real $.liveness-check-timeout = 30; #| The timeout between sending individual messages. has Real $.send-timeout = 0.2; #| The bot's own full prefix. has Str $.prefix is rw; #| The current nick of the bot. has Str $.nick is rw; #| Whether the bot is already connected. has Bool $.connected is rw; #| The connection with the IRC server. has $!connection; #| A supplier for incoming messages. has Supplier $!in; #| A Channel for outgoing messages. has Channel $!out; submethod TWEAK { # Due to lack of knowledge on how to properly do "protected" variables # in Raku, I'm just throwing a lot of warnings when you shouldn't be # setting something. if ($!connected) { .warning("Don't set connected on .new!") with $Log::instance } if ($!prefix) { .warning("Don't set prefix on .new!") with $Log::instance } if ($!nick) { .warning("Don't set nick on .new!") with $Log::instance } $!connected = False; } #| Start the IRC client, connecting to the server and signing on to the #| network. method start { # Sanity checks if (!@!nicks) { .error("You must specify at least one nickname") with $Log::instance; return; } # Include the core functionality plugin @!plugins.unshift(IRC::Client::Core.new); # Insert IRC::Client object into plugins for @!plugins.grep(* ~~ IRC::Client::Plugin:D) { $_.irc = self; } # Set up suppliers $!in = Supplier.new; $!out = Channel.new; # Set up a tap to handle incomding messages, outside of the react # block. This should help in keeping the react block free of # long-running code. $!in.Supply.tap(sub ($message) { try { CATCH { default { my $exception = $_ .gist .lines .map(*.trim-trailing) .join("\n") ; .critical($exception) with $Log::instance; } } .debug("Handling $message") with $Log::instance; IRC::Client::Handler.handle(IRC::Client::Message.new($message, self)); } }); # Dispatch an irc-started event. This event should occurr once per run, # to perform certain initial setups like an HTTP server. IRC::Client::Handler.dispatch(['irc-started' => self], self); # Pick the socket class my $socket = !$!ssl ?? IO::Socket::Async !! do { require ::('IO::Socket::Async::SSL'); }; # Connect to the server in a loop, to ensure automatic re-connection # upon any issue. loop { .debug("Connecting to $!host:$!port") with $Log::instance; try { CATCH { default { my $exception = $_ .gist .lines .map(*.trim-trailing) .join("\n") ; .emergency($exception) with $Log::instance; } } $!connection = await $socket.connect($!host, $!port); .debug("Connected to $!host:$!port") with $Log::instance; # Create a buffer for incoming data my $buffer; react { # Setup handling incoming messages whenever $!connection.Supply -> $message { for $message.comb -> $character { given ($character) { # When \r\n is encountered, it signifies the end of a message, # so emit whatever is in the $!buffer to the $!in Supply. when "\r\n" { .info("< $buffer") with $Log::instance; $!in.emit($buffer); $buffer = ''; } # Otherwise, add the character to the $!buffer. default { $buffer ~= $character } } } } # Setup handling outgoing messages whenever Supply.interval($!send-timeout) { if ($!connected) { with ($!out.poll) -> $message { .notice("> $message") with $Log::instance; $!connection.put($message); } } } # Simple keep-alive check, to ensure the socket # is still open and usable. whenever Supply.interval($!liveness-check-timeout).skip { if ($!connected) { self.privmsg($!nick, "PING {now.to-posix.first.subst('.', ' ')}", :ctcp); } } # Setup handling ^c whenever signal(SIGINT) { .notice('Caught ^c') with $Log::instance; self.stop('Caught ^c'); exit 0; } # Log on to the server IRC::Client::Handler.dispatch(['irc-setup' => self], self); LAST { $!connected = False; .error('Socket closed') with $Log::instance; } } } # Wait a small amount of time, in order to not blast an IRC # server with connections when something is wrong. sleep(5); } } #| Stop the IRC client, sending a QUIT to the server and closing the #| connection. method stop ( Str:D $reason = '' ) { my $message = "QUIT :$reason"; .notice("> $message") with $Log::instance; $!connected = False; $!connection.put($message); $!connection.close; } # # Backwards compatability # method run ( ) is DEPRECATED('IRC::Client.start') { self.start() } method quit ( ) is DEPRECATED('IRC::Client.stop') { self.stop() } method send ( :$where!, :$text!, :$notice ) is DEPRECATED('IRC::Client.privmsg or IRC::Client.notice') { self."{$notice ?? 'notice' !! 'privmsg'}"($where, $text) } # # Barebones messaging # #| Send a raw line to the IRC server. method send-raw ( Str:D $message, ) { $!out.send($message); } # # Convenience methods # method join ( Str:D $channel, ) { self.send-raw("JOIN $channel"); } method set-nick ( Str:D $nick, ) { $!nick = $nick; self.send-raw("NICK :$nick"); } method part ( Str:D $channel, ) { self.send-raw("PART $channel"); } multi method privmsg ( Str:D $target, Str:D $message, Bool :$ctcp where { !$_ }, ) { self.send-raw("PRIVMSG $target :$message"); } multi method privmsg ( Str:D $target, Str:D $message, Bool :$ctcp! where { $_ }, ) { self.privmsg($target, "\x[1]$message\x[1]"); } multi method notice ( Str:D $target, Str:D $message, Bool :$ctcp where { !$_ }, ) { self.send-raw("NOTICE $target :$message"); } multi method notice ( Str:D $target, Str:D $message, Bool :$ctcp where { $_ }, ) { self.notice($target, "\x[1]$message\x[1]"); } =begin pod =NAME IRC::Client =AUTHOR Patrick Spek <~tyil/raku-devel@lists.sr.ht> =VERSION 0.0.0 =head1 Synopsis =head1 Description =head1 Examples =head1 See also =end pod # vim: ft=perl6 noet