From b6da79a0bc1289f2a6064a6b3ffcd0c2333f2c97 Mon Sep 17 00:00:00 2001 From: Patrick Spek Date: Wed, 5 May 2021 11:03:32 +0200 Subject: Initial commit --- lib/IRC/Client.rakumod | 332 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 lib/IRC/Client.rakumod (limited to 'lib/IRC/Client.rakumod') diff --git a/lib/IRC/Client.rakumod b/lib/IRC/Client.rakumod new file mode 100644 index 0000000..166873c --- /dev/null +++ b/lib/IRC/Client.rakumod @@ -0,0 +1,332 @@ +#! /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); + + # 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 -- cgit v1.1