aboutsummaryrefslogtreecommitdiff
path: root/lib/IRC/Client.rakumod
diff options
context:
space:
mode:
authorPatrick Spek <p.spek@tyil.nl>2021-05-05 11:03:32 +0200
committerPatrick Spek <p.spek@tyil.nl>2021-05-05 11:03:32 +0200
commitb6da79a0bc1289f2a6064a6b3ffcd0c2333f2c97 (patch)
tree58280e312d4a88ddc6625d845ad657f75569d3af /lib/IRC/Client.rakumod
Initial commit
Diffstat (limited to 'lib/IRC/Client.rakumod')
-rw-r--r--lib/IRC/Client.rakumod332
1 files changed, 332 insertions, 0 deletions
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