The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Net::Async::IRC::Introduction - an introduction

INTRODUCTION

Hello, World

This first example is the "hello world" of IRC; a script that connects to the server and immediately sends a hello message to a preconfigured user.

This program starts with the usual boilerplate for any IO::Async-based program; namely loading the required modules and creating a containing IO::Async::Loop instance. It then constructs the actual Net::Async::IRC object and adds it to this containing loop. As these actions are standard to every program, they won't be repeated in later examples; just presumed to have already taken place:

   use strict;
   use warnings;

   use Future::AsyncAwait;

   use IO::Async::Loop;
   use Net::Async::IRC;

   my $loop = IO::Async::Loop->new;

   my $irc = Net::Async::IRC->new;
   $loop->add( $irc );

Now this is created, we can move on to the specifics of this example. As it's a tiny example script, we'll just hard-code the parameters for the message. A larger program of course would read these from somewhere better - a config file, commandline arguments, etc...

   my $SERVER = "irc.example.net";
   my $NICK = "MyNick";
   my $TARGET = "TargetNick";

Finally we can connect to the IRC server and send the message:

   await $irc->login(
      host => $SERVER,
      nick => $NICK,
   );

   await $irc->do_PRIVMSG(
      target => $TARGET,
      text   => "Hello, World"
   );

The program calls "login" in Net::Async::IRC, which connects the client to the given IRC server and logs in as the given nick. This method returns a Future instance to represent its eventual completion, giving us an easy way to sequence further code after it. After login is complete, the next task is simply to send the message. This is done with the PRIVMSG IRC command as wrapped by "do_PRIVMSG" in Net::Async::IRC. This takes the message target name and text string.

The trailing call to "get" in Future makes the script stop here waiting for this chain of futures to actually complete. Without this, the returned future would simply be lost (as the "then" in Future method appears in void context), and the second stage of code within it would probably never get called. In later examples we'll see other techniques, but for now every constructed future will simply be forced by calling get on it. If either of these stages fails, it will cause the get call to throw an exception instead.

Once this is sent, the script terminates, closing its connection to the server.

Receiving Messages

As a second example, lets now consider also how we handle messages that arrive from IRC.

   $irc->configure(
      on_message_PRIVMSG => sub {
         my ( $irc, $message, $hints ) = @_;
         return unless $hints->{prefix_nick_folded} eq
                       $irc->casefold_name( $TARGET );
   
         print "The user said: $hints->{text}\n";
      }
   );

   await $irc->login(
      host => $SERVER,
      nick => $NICK,
   );

   await $irc->do_PRIVMSG(
      target => $TARGET,
      text   => "Hello, what's your name?"
   );
   
   $loop->run;

Here we have used the configure method to attach an event handler to the on_message_PRIVMSG event. This handler code ignores any messages except from the user we are interested in, and simply prints the contents of those we are interested in to the terminal.

Like all IRC message handlers, this one is passed both the plain IRC message itself (in $message as an instance of Protocol::IRC::Message), and the hints hash (in $hints as a plain hash reference). While the code could operate on the message instance itself, this requries knowing that, in this case, the text field happens to be at $message->arg(1). An easier and more powerful way to work on incoming messages is to use fields of the hint hash. In this case, the text comes in a field named simply text. In this case for PRIVMSG there aren't too many other fields of interest, but for some message types, especially server numeric replies, the hints hash will contain lots of additional detail parsed out of the raw message, so it's always worth looking there first. It's entirely reasonable for a program never to actually inspect the $message object itself; it is passed only for completeness, or in case the hints parsing has failed to recognise some server-extended detail about it and such raw access is required.

Having established this event handler, we can then log in and send a message to the target user, similar to the first example. Instead of stopping the script entirely afterwards, we need to ensure that the program keeps running after this initial start so it can continue to receive messages. To do that we enter the main "run" in IO::Async::Loop method, which will wait indefinitely, processing any events that are received.

Case-folded Names

The use of the "folded" strings ensures that this code can correctly cope with any odd case-folding rules the IRC server has. By comparison, both of the following lines are incorrect, and may cause missed messages on some servers:

   return unless $hints->{prefix_name} eq $TARGET;   # don't do this

   return unless lc $hints->{prefix_name} eq lc $TARGET;  # don't do this

The first does not case-fold the string at all, so will fail in the case of User vs user. The second attempts to solve this, but does not take account of the odd case-folding logic most IRC servers have, in which the characters [\] are "uppercase" versions of {|}. The "casefold_name" in Protocol::IRC method is provided as a server-aware alternative to lc(), which handles this. A correct implementation could be written:

   return unless $irc->casefold_name( $hints->{prefix_name} ) eq
                 $irc->casefold_name( $TARGET );

However, since this is a very common pattern, the hints hash conveniently supplies already-folded strings for any name or nick fields it finds. Furthermore, as the case folded version of the target name won't change after startup, we could store that initially to save re-calculating it at every event:

   await $irc->login(
      host => $SERVER,
      nick => $NICK,
   );

   my $target_folded = $irc->casefold_name( $TARGET );

   $irc->configure(
      on_message_PRIVMSG => sub {
         my ( undef, $message, $hints ) = @_;
         return unless $hints->{prefix_nick_folded} eq $target_folded;

         print "The user said: $hints->{text}\n";
      }
   );

PRIVMSG vs text and CTCPs

This example has used the basic on_message_PRIVMSG event. A better version would be to use on_message_text instead. This is a synthesized event created on receipt of either PRIVMSG or NOTICE, and itself handles details like CTCP parsing, freeing the user code from having to handle it). For example, the plain PRIVMSG event will get quite confused by an incoming CTCP ACTION, such as is created by most IRC clients by the /me command. Instead, we can handle that by attaching a handler specifically for CTCP ACTION:

   $irc->configure(
      on_message_text => sub {
         my ( undef, $message, $hints ) = @_;
         return unless $hints->{prefix_nick_folded} eq $target_folded;

         print "The user said: $hints->{text}\n";
      },
      on_message_ctcp_ACTION => sub {
         my ( undef, $message, $hints ) = @_;
         return unless $hints->{prefix_nick_folded} eq $target_folded;

         print "The user acted: $hints->{ctcp_args}\n";
      },
   );

This second handlers is invoked on receipt of a PRIVMSG containing a CTCP ACTION. The first is only invoked on receipt of a plain PRIVMSG that doesn't contain a CTCP subcommand.

TODO

Encodings

AUTHOR

Paul Evans <leonerd@leonerd.org.uk>