aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZoffix Znet <zoffixznet@users.noreply.github.com>2016-07-26 08:50:02 -0400
committerGitHub <noreply@github.com>2016-07-26 08:50:02 -0400
commite0478c07e2096d85e20764c08c83a3d16c002e94 (patch)
tree592510005886adaadb49848d289c5c712279ecee
parente997c1b0b5ad796425abfc9f81b91947357172ce (diff)
parentcc19189ff6b74bea5211d521a59dbff0c71a0749 (diff)
Merge Rewrite 2.0 version into master
Old version should not be used anymore and 2.0 is ready to go, sans some bugs
-rw-r--r--.gitignore2
-rw-r--r--DESIGN-NOTES.md68
-rw-r--r--DESIGN/01-main.md961
-rw-r--r--DESIGN/specs-and-references.md17
-rw-r--r--README.md666
-rw-r--r--examples/bot.pl629
-rw-r--r--lib/IRC/Client.pm6360
-rw-r--r--lib/IRC/Client/Grammar.pm6 (renamed from lib/IRC/Grammar.pm6)11
-rw-r--r--lib/IRC/Client/Grammar/Actions.pm6119
-rw-r--r--lib/IRC/Client/Message.pm670
-rw-r--r--lib/IRC/Client/Plugin.pm63
-rw-r--r--lib/IRC/Client/Plugin/Debugger.pm68
-rw-r--r--lib/IRC/Client/Plugin/PingPong.pm62
-rw-r--r--lib/IRC/Grammar/Actions.pm626
-rw-r--r--lib/IRC/Parser.pm67
-rw-r--r--t/meta.t (renamed from xt/meta.t)0
-rw-r--r--t/release/01-basic.t62
-rw-r--r--t/release/02-multi-server.t75
-rw-r--r--t/release/Test/IRC/Server.pm620
-rw-r--r--t/release/servers/01-basic.pl62
20 files changed, 1649 insertions, 919 deletions
diff --git a/.gitignore b/.gitignore
index 4a5e4c7..3e54b4c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
lib/.precomp
+t/release/.precomp
+*~
diff --git a/DESIGN-NOTES.md b/DESIGN-NOTES.md
deleted file mode 100644
index eaed28b..0000000
--- a/DESIGN-NOTES.md
+++ /dev/null
@@ -1,68 +0,0 @@
-## Just some notes jotted down while reading RFCs
-
-This is only for my own use and is not meant to be of any use to anyone else.
-
-#### RFC 1459
-
-http://irchelp.org/irchelp/rfc/rfc.html
-
-Nicks can only be 9-chars long max.
-
-Channels names are strings (beginning with a ‘&’ or ‘#’ character) of length up
-to 200 characters. Apart from the the requirement that the first character being
-either ‘&’ or ‘#’; the only restriction on a channel name is that it may not
-contain any spaces (’ ‘), a control G (^G or ASCII 7), or a comma (‘,’ which is
-used as a list item separator by the protocol).
-
-A channel operator is identified by the ‘@’ symbol next to their nickname
-whenever it is associated with a channe
-
-Because of IRC’s scandanavian origin, the characters {}| are considered to be
-the lower case equivalents of the characters []\, respectively. This is a
-critical issue when determining the equivalence of two nicknames.
-
-Each IRC message may consist of up to three main parts: the prefix (optional),
-the command, and the command parameters (of which there may be up to 15). The
-prefix, command, and all parameters are separated by one (or more) ASCII space
-character(s) (0x20).
-
-***Clients should not use prefix when sending a message from themselves;***
-
-The command must either be a valid IRC command or a three (3) digit number represented in ASCII text.
-
-IRC messages are always lines of characters terminated with a CR-LF (Carriage Return - Line Feed) pair, and these messages shall ***not exceed 512 characters*** in length, counting all characters **including the trailing CR-LF**. Thus, there are 510 characters maximum allowed for the command and its parameters. There is no provision for continuation message lines. See section 7 for more details about current implementations.
-
-
-The BNF representation for this is:
-
-::=
-
-[':' <prefix> <SPACE> ] <command> <params> <crlf>
-
-::=
-
-<servername> | <nick> [ '!' <user> ] [ '@' <host> ]
-
-::=
-
-<letter> { <letter> } | <number> <number> <number>
-
-::=
-
-' ' { ' ' }
-
-::=
-
-<SPACE> [ ':' <trailing> | <middle> <params> ]
-
-::=
-
-<Any *non-empty* sequence of octets not including SPACE or NUL or CR or LF, the first of which may not be ':'>
-
-::=
-
-<Any, possibly *empty*, sequence of octets not including NUL or CR or LF>
-
-::=
-
-CR LF
diff --git a/DESIGN/01-main.md b/DESIGN/01-main.md
new file mode 100644
index 0000000..196fc1c
--- /dev/null
+++ b/DESIGN/01-main.md
@@ -0,0 +1,961 @@
+# TABLE OF CONTENTS
+- [PURPOSE](#purpose)
+- [GOALS](#goals)
+ - [Ease of Use](#ease-of-use)
+ - [Client-Generated Events](#client-generated-events)
+ - [Possibility of Non-Blocking Code](#possibility-of-non-blocking-code)
+- [DESIGN](#design)
+- [Multi-Server Interface](#multi-server-interface)
+- [Client Object](#client-object)
+ - [`$.irc` (access from inside a plugin)](#irc-access-from-inside-a-plugin)
+ - [`.new`](#new)
+ - [`.run`](#run)
+ - [`.quit`](#quit)
+ - [`.part`](#part)
+ - [`.join`](#join)
+ - [`.send`](#send)
+ - [`.nick`](#nick)
+ - [`.emit`](#emit)
+ - [`.emit-custom`](#emit-custom)
+ - [`.channel`](#channel)
+ - [`.has`](#has)
+ - [`.topic`](#topic)
+ - [`.modes`](#modes)
+ - [`.bans`](#bans)
+ - [`.names`](#names)
+- [Message Delivery](#message-delivery)
+- [Response Constants](#response-constants)
+ - [`IRC_NEXT`](#irc_next)
+ - [`IRC_DONE`](#irc_done)
+- [Message Object Interface](#message-object-interface)
+ - [`.nick`](#nick-1)
+ - [`.username`](#username)
+ - [`.host`](#host)
+ - [`.usermask`](#usermask)
+ - [`.reply`](#reply)
+- [Convenience Events](#convenience-events)
+ - [`irc-to-me`](#irc-to-me)
+ - [`irc-addressed`](#irc-addressed)
+ - [`irc-mentioned`](#irc-mentioned)
+ - [`irc-privmsg-channel`](#irc-privmsg-channel)
+ - [`irc-privmsg-me`](#irc-privmsg-me)
+ - [`irc-notice-channel`](#irc-notice-channel)
+ - [`irc-notice-me`](#irc-notice-me)
+ - [`irc-started`](#irc-started)
+ - [`irc-connected`](#irc-connected)
+ - [`irc-mode-channel`](#irc-mode-channel)
+ - [`irc-mode-user`](#irc-mode-user)
+ - [`irc-all`](#irc-all)
+- [Numeric Events](#numeric-events)
+- [Named Events](#named-events)
+ - [`irc-nick`](#irc-nick)
+ - [`irc-quit`](#irc-quit)
+ - [`irc-join`](#irc-join)
+ - [`irc-part`](#irc-part)
+ - [`irc-mode`](#irc-mode)
+ - [`irc-topic`](#irc-topic)
+ - [`irc-invite`](#irc-invite)
+ - [`irc-kick`](#irc-kick)
+ - [`irc-privmsg`](#irc-privmsg)
+ - [`irc-notice`](#irc-notice)
+- [Custom Events](#custom-events)
+
+# PURPOSE
+
+The purpose of IRC::Client is to serve as a fully-functional IRC
+client that--unlike programs like HexChat or mIRC--provide a programmatic
+interface to IRC. So, for example, to send a message to a channel, instead
+of typing a message in a message box and pressing ENTER, a method is called
+and given a string.
+
+Naturally, such an interface provides vast abilities to automate interactions
+with IRC or implement a human-friendly interface, such as HexChat or mIRC.
+
+# GOALS
+
+An implementation must achieve these goals:
+
+## Ease of Use
+
+For basic use, such as a bot that responds to triggers said in channel,
+the details of the IRC protocol must be as invisible as possible. Just as any
+user can install HexChat and join a channel and talk, similar usability has
+to be achieved by the implementation.
+
+As an example, a HexChat user can glance at the user list or channel topic
+without explicitly issuing `NAMES` or `TOPIC` IRC commands. The implementation
+should thus provide similar simplicity and provide a userlist or topic
+via a convenient method rather than explicit method to send the appropriate
+commands and the requirement of listening for the server response events.
+
+## Client-Generated Events
+
+The implementation must allow the users of the code to emit IRC and custom
+events. For example, given plugins A and B, with A performing processing
+first, plugin A can mark all `NOTICE` IRC events as handled and emit them
+as `PRIVMSG` events instead. From the point of view of second plugin B, no
+`NOTICE` commands ever happen (as they arrive to it as `PRIVMSG`).
+
+Similarly, plugin A can choose to emit custom event `FOOBAR` instead of
+`PRIVMSG`, to which plugin B can choose to respond to.
+
+## Possibility of Non-Blocking Code
+
+The implementation must allow the user to perform responses to events in
+a non-blocking manner if they choose to.
+
+# DESIGN
+
+The implementation consists of Core code responsible for maintaining the
+state of the connected client, parsing of server messages, and sending
+essential messages, as well as relating messages to and from plugins.
+
+The implementation distribution may also include several plugins that may
+be commonly needed by users. Such plugins are not enabled by default and
+the user must request their inclusion with code.
+
+# Multi-Server Interface
+
+The interface described in the rest of this document assumes a connection
+to a single server. Should the client be connected to multiple-servers at
+the time, issuing commands described will apply to *every* connected server.
+A server must be specified to issue a command to a single server.
+**Plugin authors must keep this fact in mind, when writing plugins, as
+forgetting to handle multiple servers can result in unwanted behaviour.**
+
+The same reasoning applies to the `.new` method: attributes, such as
+nicknames, usernames, etc. given without associating them with a server will
+apply to ALL connected servers. Configuration for individual servers is
+given via `:servers` named parameter as a list of `Pairs`. The key
+is the nickname of server and must be a valid method name. It's recommended
+to choose something that won't end up an actual method on the Client Object.
+It's guaranteed methods starting with `s-` will always be safe to use. The
+value is a list of pairs that can be accepted by the Client Object as named
+parameters (except for `:servers`) that specify the configuration for that
+specific server, overriding any of the non-server-specific parameters already
+set.
+
+A possible `.new` setup may look something like this:
+
+```perl6
+ my $irc = IRC::Client.new:
+ :nick<ZofBot ZofBot_ ZofBot__> # nicks to try to use on ALL servers,
+ :servers(
+ s-leliana => (
+ :server<irc.freenode.net>,
+ :channels<#perl #perl6 #perl7>
+ ),
+ s-morrigan => (
+ :server<irc.perl.org>,
+ :channels<#perl #perl-help>
+ ),
+ s-alistair => (
+ :nick<Party Party_ Party__> # nick override
+ :server<irc.perl6.pary>,
+ :channels<#perler>
+ ),
+ ),
+```
+
+Use of multiple servers is facilitated via server nicknames and using
+them as a method call to obtain the correct Client Object. For example:
+
+```perl6
+ $.irc.quit; # quits all servers
+ $.irc.s-leliana.quit; # quits only the s-leliana server
+
+ # send a message to #perl6 channel on s-morrigan server
+ $.irc.s-morrigan.send: where => '#perl6', text => 'hello';
+```
+
+The Message Object will also contain a `.server` method value of which
+is the nickname of the server from which the message arrived. In general,
+the most common way to generate messages will be using `.reply` on the Message
+Object, making the multi-server paradigm completely transparent.
+
+# Client Object
+
+Client Object represents a connected IRC client and is aware of and can
+manipulate its state, such as disconnecting, joining or parting a channel,
+or sending messages.
+
+A Client Object must support the ability to connect to multiple servers.
+The client object provides these methods:
+
+## `$.irc` (access from inside a plugin)
+
+```perl6
+ use IRC::Client::Plugin;
+ unit Plugin::Foo is IRC::Client::Plugin;
+
+ method irc-privmsg-me ($msg) {
+ $.irc.send:
+ where => '#perl6',
+ text => "$msg.nick() just sent me a secret! It's $msg.text()";
+ }
+```
+
+A plugin inherits from `IRC::Client::Plugin`, which provides `$.irc`
+attribute containing the Client Object, allowing the plugin to utilize all
+of the methods it provides.
+
+## `.new`
+
+```perl6
+ my $irc = IRC::Client.new:
+ ...
+ :plugins(
+ IRC::Client::Plugin::Factoid.new,
+ My::Plugin.new,
+ class :: is IRC::Client::Plugin {
+ method irc-privmsg-me ($msg) { $msg.repond: 'Go away!'; }
+ },
+ );
+```
+
+*Not to be used inside plugins.*
+Creates a new `IRC::Client` object. Along with the usual arguments like
+nick, username, server address, etc, takes `:plugins` argument that
+lists the plugins to include. All messages will be propagated through plugins
+in the order they are defined here.
+
+## `.run`
+
+```perl6
+ $irc.run;
+```
+
+*Not to be used inside plugins.*
+Starts the client, connecting to the server and maintaining that connection
+and not returning until an explicit `.quit` is issued. If the connection
+breaks, the client will attempt to reconnect.
+
+## `.quit`
+
+```perl6
+ $.irc.quit;
+
+ $.irc.quit: 'Reason';
+```
+
+Disconnects from the server. Takes an option string to be given to the
+server as the reson for quitting.
+
+## `.part`
+
+```per6
+ $.irc.part: '#perl6';
+
+ $.irc.part: '#perl6', 'Leaving';
+```
+
+Exits a channel. Takes two positional strings: the channel to part
+and an optional parting message. Causes the client object to discard any state
+kept for this channel.
+
+## `.join`
+
+```perl6
+ $.irc.join '#perl6', '#perl7';
+```
+
+Attempts to joins channels given as positional arguments.
+
+## `.send`
+
+```perl6
+ $.irc.send: where => '#perl6', text => 'Hello, Perl 6!';
+
+ $.irc.send: where => 'Zoffix', text => 'Hi, Zoffie!';
+
+ $.irc.send: where => 'Zoffix', text => 'Notice me, senpai!', :notice;
+```
+
+Sends a message specified by `text` argument
+either to a user or a channel specified by `:where` argument. If `Bool`
+argument `:notice` is set to true, will send a *notice* instead of regular
+message.
+
+Note that in IRC bots that respond to commands from other users a more
+typical way to reply to those commands would be by calling
+`.reply` method on the Message Object, rather than using `.send` method.
+
+
+## `.nick`
+
+```perl6
+ $.irc.nick: 'ZofBot', 'ZofBot_', 'ZofBot__';
+```
+
+Attempts to change the nick of the client. Takes one or more positional
+arguments that are a list of nicks to try.
+
+## `.emit`
+
+```perl6
+ $.irc.emit: $msg;
+
+ $.irc.emit: IRC::Client::Message::Privmsg.new:
+ nick => 'Zoffix',
+ text => 'Hello',
+ ...;
+ ...
+ method irc-privmsg ($msg) {
+ say "$msg.nick() said $msg.text()... or did they?";
+ }
+```
+
+Takes an object of any of `IRC::Client::Message::*` subclass and emits it
+as if it were a new event. That is, it will propagate through the plugin chain
+starting at the first plugin, and not the one emiting the event, and the
+plugins can't tell whether the message is self-generated or something that
+came from the server.
+
+## `.emit-custom`
+
+```perl6
+ $.irc.emit-custom: 'my-event', 'just', 'some', :args;
+```
+
+Same idea as `.emit`, except a custom event is emitted. The first positional
+argument specifies the name of the event to emit. Any other arguments
+given here will be passed as is to listener methods.
+
+## `.channel`
+
+```perl6
+ method irc-addressed ($msg) {
+ if $msg.text ~~ /'kick' \s+ $<nick>=\S+/ {
+ $msg.reply: "I don't see $<nick> up in here"
+ unless $.irc.channel($msg.channel).?has: ~$<nick>;
+ }
+
+ if $msg.text ~~ /'topic' \s+ $<channel>=\S+/ {
+ return $msg.reply: $_
+ ?? "Channel $<channel> does not exist"
+ !! "Topic in $<channel> is $_.topic()"
+ given $.irc.channel: ~$<channel>;
+ }
+ }
+```
+
+Returns an `IRC::Client::Channel` object for the channel given as positional
+argument, or `False` if no such channel seems to exist. Unless our client is
+currently *on* that channel, that existence is
+determined with `LIST` IRC command, so there will be some false negatives,
+such as when attempting to get an object for a channel with secret mode set.
+
+The Client Object tracks state for any of the joined channels, so some
+information obtainable via the Channel Object will be cached
+and retrieved from that state, whenever possible. Otherwise, a request
+to the server will be generated. Return values will be empty (empty lists
+or empty strings) when requests fail. The channel object provides the
+following methods.
+
+### `.has`
+
+```perl6
+ $.irc.channel('#perl6').has: 'Zoffix';
+```
+
+Returns `True` or `False` indicating whether a user with the given nick is
+present on the channel.
+
+### `.topic`
+
+```perl6
+ say "Topic of the channel is " ~ $.irc.channel('#perl6').topic;
+```
+
+Returns the `TOPIC` of the channel.
+
+### `.modes`
+
+```perl6
+ say $.irc.channel('#perl6').modes;
+ # ('s', 'n', 't')
+```
+
+Returns a list of single-letter codes for currently active channel modes
+on the channel. Note, this does not include any bans.
+
+### `.bans`
+
+```perl6
+ say $.irc.channel('#perl6').bans;
+ # ('*!spammer@*', 'warezbot!*@*')
+```
+
+Returns a list of currently active ban masks on the channel.
+
+### `.names`
+
+```perl6
+ say $.irc.channel('#perl6').names;
+ # ('@Zoffix', '+zoffixs-helper', 'not-zoffix')
+```
+
+Returns a list of nicks present on the channel, each potentially prefixed
+with a [channel membership prefix](https://www.alien.net.au/irc/chanmembers.html)
+
+# Message Delivery
+
+An event listener is defined by a method in a plugin class. The name
+of the method starts with `irc-` and followed by the lowercase name of the
+event. User-defined events follow the same pattern, except they start with
+`irc-custom-`:
+
+```perl6
+ use IRC::Client::Plugin;
+ unit Plugin::Foo is IRC::Client::Plugin;
+
+ # Listen to PRIVMSG IRC events:
+ method irc-privmsg ($msg) {
+ return IRC_NEXT unless $msg.channel eq '#perl6';
+ $msg.reply: 'Nice to meet you!';
+ }
+
+ # Listen to custom client-generated events:
+ method irc-custom-my-event ($some, $random, :$args) {
+ return IRC_NEXT unless $random > 5;
+ $.irc.send: where => '#perl6', text => 'Custom event triggered!';
+ }
+```
+
+An event listener receives the event message in the form of an object.
+The object must provide all the relevant information about the source
+and content of the message.
+
+The message object, where appropriate, must provide a means to send a reply
+back to the originator of the message. For example, here's a potential
+implementation of `PRIVMSG` handler that receives the message object:
+
+```perl6
+ method irc-privmsg-channel ($msg) {
+ return IRC_NEXT unless $msg.channel eq '#perl6';
+ $msg.reply: 'Nice to meet you!';
+ }
+```
+
+A plugin can send messages and emit events at will:
+
+```perl6
+ method irc-connected {
+ Supply.interval(60).tap: {
+ $.irc.send: where => '#perl6', text => 'One minute passed!!';
+ };
+ Promise.in(60*60).then: {
+ $.irc.send:
+ where => 'Zoffix',
+ text => 'I lived for one hour already!',
+ :notice;
+
+ $.irc.emit-custom: 'MY-EVENT', 'One hour passed!';
+ }
+ }
+```
+
+# Response Constants
+
+Multiple plugins can listen to the same event. The event message will be
+handed to each of the plugins in the sequence they are defined when the
+Client Object is initialized. Each handler can use predefined response
+constants to signal whether the handling of this particular event message
+should stop or continue onto the next plugin. These response constants
+are `IRC_NEXT` and `IRC_DONE` and are exported by `IRC::Client::Plugin`.
+
+## `IRC_NEXT`
+
+```perl6
+ method irc-privmsg-channel ($msg) {
+ return IRC_NEXT unless $msg.channel eq '#perl6';
+ ....
+ }
+```
+
+Signals that the message should continue to be passed on to any further
+plugins that subscribed to handle it.
+
+## `IRC_DONE`
+
+```perl6
+ method irc-privmsg-channel ($msg) {
+ return IRC_DONE if $msg.channel eq '#perl6';
+ }
+
+ # or just...
+
+ method irc-privmsg-channel ($msg) {}
+```
+
+Signals that the message has been handled and should NOT be passed on
+to any further plugins. **Note:** you don't have to explicitly return this
+value; anything other than returning `IRC_NEXT` is the same as returning
+`IRC_DONE`.
+
+
+# Message Object Interface
+
+The message object received by all non-custom events is an event-specific
+subclass of `IRC::Client::Message`. The subclass is named
+`IRC::Client::Message::$NAME`, where `$NAME` is:
+
+* *Named* and *Convenience* events use their names without `irc-` part, with any `-`
+changed to `::` and with each word written in `Title Case`. e.g.
+message object for `irc-privmsg-me` is `IRC::Client::Message::Privmsg::Me`
+* *Numeric* events always receive `IRC::Client::Message::Numeric` message
+object, regardless of the actual number of the event.
+
+Along with event-specific methods
+described under each event, the `IRC::Client::Message` offers the following
+methods:
+
+## `.nick`
+
+```perl6
+ say $msg.nick ~ " says hello";
+```
+
+Contains the nickname of the sender of the message.
+
+## `.username`
+
+```perl6
+ say $msg.nick ~ " has username " ~ $msg.username;
+```
+
+Contains the username of the sender of the message.
+
+## `.host`
+
+```perl6
+ say $msg.nick ~ " is connected from " ~ $msg.host;
+```
+
+Hostname of sender of the message.
+
+## `.usermask`
+
+```perl6
+ say $msg.usermask;
+```
+
+Nick, username, and host combined into a full usermask, e.g.
+`Zoffix!zoffix@zoffix.com`
+
+## `.reply`
+
+```perl6
+ $msg.reply: 'I love you too'
+ if $msg.text ~~ /'I love you'/;
+```
+
+Replies back to a message. For example, if we received the message as a
+private message to us, the reply will be a private message back to the
+user. Same for notices. For in-channel messages, `irc-addressed`
+and `irc-to-me` will address the sender in return, while all other in-channel
+events will not.
+
+**NOTE:** this method is only available for these events:
+
+* `irc-privmsg`
+* `irc-notice`
+* `irc-to-me`
+* `irc-addressed`
+* `irc-mentioned`
+* `irc-privmsg-channel`
+* `irc-privmsg-me`
+* `irc-notice-channel`
+* `irc-privmsg-me`
+
+# Convenience Events
+
+These sets of events do not have a corresponding IRC command defined by the
+protocol and instead are offered to make listening for a specific kind
+of events easier.
+
+## `irc-to-me`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 PRIVMSG zoffix2 :hello
+ # :zoffix!zoffix@127.0.0.1 NOTICE zoffix2 :hello
+ # :zoffix!zoffix@127.0.0.1 PRIVMSG #perl6 :zoffix2, hello
+
+ method irc-to-me ($msg) {
+ printf "%s told us `%s` using %s\n",
+ .nick, .text, .how given $msg;
+ }
+```
+
+Emitted when a user sends us a message as a private message, notice, or
+addresses us in a channel. The `.respond` method of the Message
+Object is the most convenient way to respond back to the sender of the message.
+
+The `.how` method returns a `Pair` where the key is the message type used
+(`PRIVMSG` or `NOTICE`) and the value is the addressee of that message
+(a channel or us).
+
+## `irc-addressed`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 PRIVMSG #perl6 :zoffix2, hello
+
+ method irc-addressed ($msg) {
+ printf "%s told us `%s` in channel %s\n",
+ .nick, .text, .channel given $msg;
+ }
+```
+
+Emitted when a user addresses us in a channel. Specifically, this means
+their message starts with our nickname, followed by optional comma or colon,
+followed by whitespace. That prefix will be stripped from the message.
+
+## `irc-mentioned`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 PRIVMSG #perl6 :Is zoffix2 a robot?
+
+ method irc-mentioned ($msg) {
+ printf "%s mentioned us in channel %s when they said %s\n",
+ .nick, .channel, .text given $msg;
+ }
+```
+
+Emitted when a user mentions us in a channel. Specifically, this means
+their message contains our nickname separated by a word boundary on each side.
+
+## `irc-privmsg-channel`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 PRIVMSG #perl6 :hello
+
+ method irc-privmsg-channel ($msg) {
+ printf "%s said `%s` to channel %s\n",
+ .nick, .text, .channel given $msg;
+ }
+```
+
+Emitted when a user sends a message to a channel.
+
+## `irc-privmsg-me`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 PRIVMSG zoffix2 :hey bruh
+
+ method irc-privmsg-me ($msg) {
+ printf "%s messaged us: %s\n", .nick, .text given $msg;
+ }
+```
+
+Emitted when a user sends us a private message.
+
+## `irc-notice-channel`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 NOTICE #perl6 :Notice me!
+
+ method irc-notice-channel ($msg) {
+ printf "%s sent a notice `%s` to channel %s\n",
+ .nick, .text, .channel given $msg;
+ }
+```
+
+Emitted when a user sends a notice to a channel.
+
+## `irc-notice-me`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 NOTICE zoffix2 :did you notice me?
+
+ method irc-notice-me ($msg) {
+ printf "%s sent us a notice: %s\n", .nick, .text given $msg;
+ }
+```
+
+Emitted when a user sends us a private notice.
+
+## `irc-started`
+
+```perl6
+ method irc-started {
+ $.do-some-sort-of-init-setup;
+ }
+```
+
+Emitted when the IRC client is started. Useful for doing setup work, like
+initializing database connections, etc. Note: this event will fire only once,
+even if the client reconnects to the server numerous times. Note that
+unlike most events, this event does *not* receive a Message Object.
+**IMPORTANT:** when this event fires, there's no guarantee we even started a
+connection to the server, let alone connected successfully.
+
+## `irc-connected`
+
+```perl6
+ method irc-connected {
+ $.do-some-sort-of-per-connection-setup;
+ }
+```
+
+Similar to `irc-started`, except will be emitted every time a
+*successful* connection to the server is made and we joined all
+of the requested channels. That is, we'll wait to either receive the
+full user list or error message for each of the channels we're joining.
+Note that unlike most events, this event does *not* receive a Message Object.
+
+## `irc-mode-channel`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 MODE #perl6 +o zoffix2
+ # :zoffix!zoffix@127.0.0.1 MODE #perl6 +bbb Foo!*@* Bar!*@* Ber!*@*
+
+ method irc-mode-channel ($msg) {
+ printf "Nick %s with usermask %s set mode(s) %s in channel %s\n",
+ .nick, .usermask, .modes, .channel given $msg;
+ }
+```
+
+Emitted when IRC `MODE` command is received and it's being operated on a
+channel, see `irc-mode` event for details.
+
+## `irc-mode-user`
+
+```perl6
+ # :zoffix2!f@127.0.0.1 MODE zoffix2 +w
+
+ method irc-mode-user ($msg) {
+ printf "Nick %s with usermask %s set mode(s) %s on user %s\n",
+ .nick, .usermask, .modes, .who given $msg;
+ }
+```
+
+Emitted when IRC `MODE` command is received and it's being operated on a
+user, see `irc-mode` event for details.
+
+## `irc-all`
+
+```perl6
+ method irc-all ($msg) {
+ say "Received an event: $msg.perl()";
+ return IRC_NEXT;
+ }
+```
+
+Emitted for all events and is mostly useful for debugging. The type of the
+message object received will depend on the type of the event that generated
+the message. This event will be triggered *AFTER* all other event handlers
+in the current plugin are processed.
+
+# Numeric Events
+
+Numeric IRC events can be subscribed to by defining a method with name
+`irc-` followed by the numeric code of the event (e.g. `irc-001`). The
+arguments of the event can be accessed via `.args` method that returns a
+list of strings:
+
+```perl6
+ method irc-004 ($msg) {
+ say "Here are the arguments of the RPL_MYINFO event:";
+ .say for $msg.args;
+ }
+```
+
+See [this reference](https://www.alien.net.au/irc/irc2numerics.html) for
+a detailed list of numerics and their arguments available in the wild. Note:
+the client will emit an event for any received numeric with a 3-digit
+code, regardless of whether it is listed in that reference.
+
+# Named Events
+
+## `irc-nick`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 NICK not-zoffix
+
+ method irc-nick ($msg) {
+ printf "%s changed nickname to %s\n", .nick, .new-nick given $msg;
+ }
+```
+
+[RFC 2812, 3.1.2](https://tools.ietf.org/html/rfc2812#section-3.1.2).
+Emitted when a user changes their nickname.
+
+## `irc-quit`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 QUIT :Quit: Leaving
+
+ method irc-quit ($msg) {
+ printf "%s has quit (%s)\n", .nick, .reason given $msg;
+ }
+```
+
+[RFC 2812, 3.1.7](https://tools.ietf.org/html/rfc2812#section-3.1.7).
+Emitted when a user quits the server.
+
+## `irc-join`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 JOIN :#perl6
+
+ method irc-join ($msg) {
+ printf "%s joined channel %s\n", .nick, .channel given $msg;
+ }
+```
+
+[RFC 2812, 3.2.1](https://tools.ietf.org/html/rfc2812#section-3.2.1).
+Emitted when a user joins a channel.
+
+## `irc-part`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 PART #perl6 :Leaving
+
+ method irc-part ($msg) {
+ printf "%s left channel %s (%s)\n", .nick, .channel, .reason given $msg;
+ }
+```
+
+[RFC 2812, 3.2.2](https://tools.ietf.org/html/rfc2812#section-3.2.2).
+Emitted when a user leaves a channel.
+
+## `irc-mode`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 MODE #perl6 +o zoffix2
+ # :zoffix!zoffix@127.0.0.1 MODE #perl6 +bbb Foo!*@* Bar!*@* Ber!*@*
+ # :zoffix2!f@127.0.0.1 MODE zoffix2 +w
+
+ method irc-mode ($msg) {
+ if $msg.?channel {
+ # channel mode change
+ printf "%s set mode(s) %s in channel %s\n",
+ .nick, .modes, .channel given $msg;
+ }
+ else {
+ # user mode change
+ printf "%s set mode(s) %s on user %s\n",
+ .nick, .modes, .who given $msg;
+ }
+ }
+```
+
+[RFC 2812, 3.1.5](https://tools.ietf.org/html/rfc2812#section-3.1.5)/[RFC 2812, 3.2.3](https://tools.ietf.org/html/rfc2812#section-3.2.3).
+Emitted when IRC `MODE` command is received. As the command is dual-purpose,
+the message object will have either `.channel` method available
+(for channel mode changes) or `.who` method (for user mode changes). See
+also `irc-mode-channel` and `irc-mode-user` convenience events.
+
+For channel modes, the `.modes` method returns a list of `Pair` where key
+is the mode set and the value is the argument for that mode (i.e. "limit",
+"user", or "banmask") or an empty string if the mode takes no arguments.
+
+For user modes, the `.modes` method returns a list of `Str` of the modes
+set.
+
+The received message object will be one of the subclasses of
+`IRC::Client::Message::Mode` object: `IRC::Client::Message::Mode::Channel`
+or `IRC::Client::Message::Mode::User`.
+
+## `irc-topic`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 TOPIC #perl6 :meow
+
+ method irc-topic ($msg) {
+ printf "%s set topic of channel %s to %s\n",
+ .nick, .channel, .topic given $msg;
+ }
+```
+
+[RFC 2812, 3.2.4](https://tools.ietf.org/html/rfc2812#section-3.2.4).
+Emitted when a user changes the topic of a channel.
+
+## `irc-invite`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 INVITE zoffix2 :#perl6
+
+ method irc-invite ($msg) {
+ printf "%s invited us to channel %s\n", .nick, .channel given $msg;
+ }
+```
+
+[RFC 2812, 3.2.7](https://tools.ietf.org/html/rfc2812#section-3.2.7).
+Emitted when a user invites us to a channel.
+
+## `irc-kick`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 KICK #perl6 zoffix2 :go away
+
+ method irc-kick ($msg) {
+ printf "%s kicked %s out of %s (%s)\n",
+ .nick, .who, .channel, .reason given $msg;
+ }
+```
+
+[RFC 2812, 3.2.8](https://tools.ietf.org/html/rfc2812#section-3.2.8).
+Emitted when someone kicks a user out of a channel.
+
+## `irc-privmsg`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 PRIVMSG #perl6 :hello
+ # :zoffix!zoffix@127.0.0.1 PRIVMSG zoffix2 :hey bruh
+
+ method irc-privmsg ($msg) {
+ if $msg.?channel {
+ # message sent to a channel
+ printf "%s said `%s` to channel %s\n",
+ .nick, .text, .channel given $msg;
+ }
+ else {
+ # private message
+ printf "%s messaged us: %s\n", .nick, .text given $msg;
+ }
+ }
+```
+
+[RFC 2812, 3.3.1](https://tools.ietf.org/html/rfc2812#section-3.3.1).
+Emitted when a user sends a message either to a channel
+or a private message to us. See *Convenience Events* section for a number
+of more convenient ways to listen to messages.
+
+## `irc-notice`
+
+```perl6
+ # :zoffix!zoffix@127.0.0.1 NOTICE #perl6 :Notice me!
+ # :zoffix!zoffix@127.0.0.1 NOTICE zoffix2 :did you notice me?
+
+ method irc-notice ($msg) {
+ if $msg.?channel {
+ # notice sent to a channel
+ printf "%s sent a notice `%s` to channel %s\n",
+ .nick, .text, .channel given $msg;
+ }
+ else {
+ # private notice
+ printf "%s sent us a notice: %s\n", .nick, .text given $msg;
+ }
+ }
+```
+
+[RFC 2812, 3.3.2](https://tools.ietf.org/html/rfc2812#section-3.3.2).
+Emitted when a user sends a notice either to a channel
+or a private notice to us. See *Convenience Events* section for a number
+of more convenient ways to listen to notices and messages.
+
+# Custom Events
+
+There is support for custom events. A custom event is emitted by calling
+`.emit-custom` method on the Client Object and is subscribed to via
+`irc-custom-*` methods:
+
+```perl6
+ $.irc.emit-custom: 'my-event', 'just', 'some', :args;
+ ...
+ method irc-custom-my-event ($just, $some, :$args) { }
+```
+
+No Message Object is involved in custom events.
diff --git a/DESIGN/specs-and-references.md b/DESIGN/specs-and-references.md
new file mode 100644
index 0000000..917e606
--- /dev/null
+++ b/DESIGN/specs-and-references.md
@@ -0,0 +1,17 @@
+
+# Specs
+
+* [Numerics and other awesome info](https://www.alien.net.au/irc/)
+* [RFC 1459](https://tools.ietf.org/html/rfc1459)
+* [RFC 2810](https://tools.ietf.org/html/rfc2810)
+* [RFC 2811](https://tools.ietf.org/html/rfc2811)
+* [RFC 2812](https://tools.ietf.org/html/rfc2812)
+* [RFC 2813](https://tools.ietf.org/html/rfc2813)
+* [WebIRC](https://irc.wiki/WebIRC)
+* [CTCP SPEC](http://cpansearch.perl.org/src/HINRIK/POE-Component-IRC-6.78/docs/ctcpspec.html)
+* [DCC Description](http://www.irchelp.org/irchelp/rfc/dccspec.html)
+* [DCC2](https://tools.ietf.org/id/draft-smith-irc-dcc2-negotiation-00.txt)
+
+# Future
+
+IRCv3 group: http://ircv3.net/
diff --git a/README.md b/README.md
index 4c4df78..e28417b 100644
--- a/README.md
+++ b/README.md
@@ -4,669 +4,39 @@
IRC::Client - Extendable Internet Relay Chat client
-# TABLE OF CONTENTS
-- [NAME](#name)
-- [TABLE OF CONTENTS](#table-of-contents)
-- [SYNOPSIS](#synopsis)
- - [Client script](#client-script)
- - [Custom plugins](#custom-plugins)
- - [Basic response to an IRC command:](#basic-response-to-an-irc-command)
- - [More involved handling](#more-involved-handling)
-- [DESCRIPTION](#description)
-- [BLEED BRANCH](#bleed-branch)
-- [METHODS](#methods)
- - [`new`](#new)
- - [`debug`](#debug)
- - [`host`](#host)
- - [`password`](#password)
- - [`port`](#port)
- - [`nick`](#nick)
- - [`username`](#username)
- - [`userhost`](#userhost)
- - [`userreal`](#userreal)
- - [`channels`](#channels)
- - [`plugins`](#plugins)
- - [`plugins-essential`](#plugins-essential)
- - [`run`](#run)
-- [METHODS FOR PLUGINS](#methods-for-plugins)
- - [`.notice`](#notice)
- - [`.privmsg`](#privmsg)
- - [`.respond`](#respond)
- - [`where`](#where)
- - [`what`](#what)
- - [`how`](#how)
- - [`who`](#who)
- - [`.ssay`](#ssay)
-- [INCLUDED PLUGINS](#included-plugins)
- - [IRC::Client::Plugin::Debugger](#ircclientplugindebugger)
- - [IRC::Client::Plugin::PingPong](#ircclientpluginpingpong)
-- [EXTENDING IRC::Client / WRITING YOUR OWN PLUGINS](#extending-ircclient--writing-your-own-plugins)
- - [Overview of the plugin system](#overview-of-the-plugin-system)
- - [Return value constants](#return-value-constants)
- - [`IRC_HANDLED`](#irc_handled)
- - [`IRC_NOT_HANDLED`](#irc_not_handled)
- - [Subscribing to IRC events](#subscribing-to-irc-events)
- - [Standard IRC commands](#standard-irc-commands)
- - [Special Events](#special-events)
- - [`irc-start-up`](#irc-start-up)
- - [`irc-connected`](#irc-connected)
- - [`irc-all-events`](#irc-all-events)
- - [`irc-to-me`](#irc-to-me)
- - [`irc-privmsg-me`](#irc-privmsg-me)
- - [`irc-notice-me`](#irc-notice-me)
- - [`irc-unhandled`](#irc-unhandled)
- - [Contents of the parsed IRC message](#contents-of-the-parsed-irc-message)
- - [`command`](#command)
- - [`params`](#params)
- - [`pipe`](#pipe)
- - [`who`](#who-1)
-- [REPOSITORY](#repository)
-- [BUGS](#bugs)
-- [AUTHOR](#author)
-- [LICENSE](#license)
-
# SYNOPSIS
-## Client script
-
```perl6
use IRC::Client;
- use IRC::Client::Plugin::Debugger;
+ use Pastebin::Shadowcat;
- IRC::Client.new(
+ .run with IRC::Client.new:
:host<localhost>
:channels<#perl6bot #zofbot>
:debug
- :plugins( IRC::Client::Plugin::Debugger.new )
- ).run;
-```
-
-## Custom plugins
-
-### Basic response to an IRC command:
-
-The plugin chain handling the message will stop after this plugin.
-
-```perl6
-unit class IRC::Client::Plugin::PingPong is IRC::Client::Plugin;
-method irc-ping ($irc, $e) { $irc.ssay("PONG {$irc.nick} $e<params>[0]") }
-```
-
-### More involved handling
-
-On startup, start sending message `I'm an annoying bot` to all channels
-every five seconds. We also subscribe to all events and print some debugging
-info. By returning a special constant, we tell other plugins to continue
-processing the data.
-
-```perl6
-use IRC::Client::Plugin; # import constants
-unit class IRC::Client::Plugin::Debugger is IRC::Client::Plugin;
-
-method irc-connected($irc) {
- Supply.interval( 5, 5 ).tap({
- $irc.privmsg($_, "I'm an annoying bot!")
- for $irc.channels;
- })
-}
-
-method irc-all-events ($irc, $e) {
- say "We've got a private message"
- if $e<command> eq 'PRIVMSG' and $e<params>[0] eq $irc.nick;
-
- # Store arbitrary data in the `pipe` for other plugins to use
- $e<pipe><respond-to-notice> = True
- if $e<command> eq 'PRIVMSG';
-
- say $e, :indent(4);
- return IRC_NOT_HANDLED;
-}
-
+ :plugins(
+ class { method irc-to-me ($ where /hello/) { 'Hello to you too!'} }
+ )
+ :filters(
+ -> $text where .chars > 200 {
+ 'The output is too large to show here. See: '
+ ~ Pastebin::Shadowcat.new.paste: $text;
+ }
+ );
```
# DESCRIPTION
-***Note: this is module is currently experimental. Things might change and
-new things will get added rapidly.***
-
-This modules lets you create
-[IRC clients](https://en.wikipedia.org/wiki/Internet_Relay_Chat)
-in Perl 6. The plugin system lets you work on the behaviour, without worrying
-about IRC layer.
-
-# METHODS
-
-## `new`
-
-```perl6
-my $irc = IRC::Client.new;
-```
-
-```perl6
-# Defaults are shown
-my $irc = IRC::Client.new(
- debug => False,
- host => 'localhost',
- password => 's3cret',
- port => 6667,
- nick => 'Perl6IRC',
- username => 'Perl6IRC',
- userhost => 'localhost',
- userreal => 'Perl6 IRC Client',
- channels => ['#perl6bot'],
- plugins => [],
- plugins-essential => [ IRC::Client::Plugin::PingPong.new ],
-);
-```
-
-Creates and returns a new `IRC::Client` objects. All arguments are optional
-and are as follows:
-
-### `debug`
-
-```perl6
- debug => True,
-```
-Takes `True` and `False` values. When set to `True`, debugging information
-will be printed by the modules on the STDOUT. **Defaults to:** `False`
-
-### `host`
-
-```perl6
- host => 'irc.freenode.net',
-```
-Specifies the hostname of the IRC server to connect to. **Defaults to:**
-`localhost`
-
-### `password`
-
-```perl6
- password => 's3cret',
-```
-Specifies the password for the IRC server. (on Freenode, for example, this
-is the NickServ password that identifies to services). **Defaults to:** no
-password.
-
-### `port`
-
-```perl6
- port => 7000,
-```
-Specifies the port of the IRC server to connect to. **Defaults to:** `6667`
-
-### `nick`
-
-```perl6
- nick => 'Perl6IRC',
-```
-Specifies the nick for the client to use. **Defaults to:** `Perl6IRC`
-
-### `username`
-
-```perl6
- username => 'Perl6IRC',
-```
-Specifies the username for the client to user. **Defaults to:** `Perl6IRC`
-
-### `userhost`
-
-```perl6
- userhost => 'localhost',
-```
-Specifies the hostname for the client to use when sending messages.
-**Defaults to:** `localhost` (Note: it's probably safe to leave this at
-default. Currently, this attribute is fluid and might be changed or
-removed in the future).
-
-### `userreal`
-
-```perl6
- userreal => 'Perl6 IRC Client',
-```
-Specifies the "real name" of the client. **Defaults to:** `Perl6 IRC Client`
-
-### `channels`
-
-```perl6
- channels => ['#perl6bot'],
-```
-Takes an array of channels for the client to join. **Defaults to:**
-`['#perl6bot']`
-
-### `plugins`
-
-```perl6
- plugins => [ IRC::Client::Plugin::Debug.new ],
-```
-Takes an array of IRC::Client Plugin objects. To run while the client is
-connected.
-
-### `plugins-essential`
-
-```perl6
- plugins-essential => [ IRC::Client::Plugin::PingPong.new ],
-```
-Same as `plugins`. The only difference is something will be set to
-these by default, as these plugins are assumed to be essential to proper
-working order of any IRC client. **Defaults to:**
-`[ IRC::Client::Plugin::PingPong.new ]`
-
-## `run`
-
- $irc.run;
-
-Takes no arguments. Starts the IRC client. Exits when the connection
-to the IRC server ends.
-
-# METHODS FOR PLUGINS
-
-You can make use of these `IRC::Client` methods in your plugins:
-
-## `.notice`
-
-```perl6
- $irc.notice( 'Zoffix', 'Hallo!' );
-```
-Sends a `NOTICE` message specified in the second argument
-to the user/channel specified as the first argument.
-
-## `.privmsg`
-
-```perl6
- $irc.privmsg( 'Zoffix', 'Hallo!' );
-```
-Sends a `PRIVMSG` message specified in the second argument
-to the user/channel specified as the first argument.
-
-## `.respond`
-
-```perl6
- $irc.respond:
- :where<#zofbot>
- :what("Hallo how are you?! It's been 1 hour!")
- :how<privmsg>
- :who<Zoffix>
- :when( now + 3600 )
- ;
-```
-Generates a response based on the provided arguments, which are as follows:
-
-### `where`
-
-```perl6
- $irc.respond: :where<Zoffix> ...
- $irc.respond: :where<#zofbot> ...
-```
-**Mandatory**. Takes either a nickname or a channel name where to
-send the message to.
-
-### `what`
-
-```perl6
- $irc.respond: :what('Hallo how are you?!') ...
-```
-**Mandatory**. Takes a string, which is the message to be sent.
-
-### `how`
-
-```perl6
- $irc.respond: :how<privmsg> ...
- $irc.respond: :how<notice> ...
-```
-**Optional**. Specifies whether the message should be sent using
-`PRIVMSG` or `NOTICE` IRC commands. **Valid values** are `privmsg` and `notice`
-(case-insensitive).
-
-### `who`
-
-```perl6
- $irc.respond: :who<Zoffix> ...
-```
-**Optional**. Takes a string with a nickname (which doesn't need to be
-valid in any way). If the `where` argument is set to a name of a channel,
-then the method will modify the `what` argument, by prepending
-`$who, ` to it.
-
-### `when`
+The module provides the means to create clients to communicate with
+IRC (Internet Relay Chat) servers. Has support for non-blocking responses
+and output post-processing.
-```perl6
- $irc.respond: :when(now+5) ... # respond in 5 seconds
- $irc.respond: :when(DateTime.new: :2016year :1month :2day) ...
-```
-**Optional**. Takes a `Dateish` or `Instant` value that specifies when the
-response should be generated. If omited, the response will be generated
-as soon as possible. **By default** is not specified.
-
-## `.ssay`
-
-```perl6
- $irc.ssay("Foo bar!");
-```
-Sends a message to the server, automatically appending `\r\n`. Mnemonic:
-**s**erver **say**.
-
-# INCLUDED PLUGINS
-
-Currently, this distribution comes with two IRC Client plugins:
-
-## IRC::Client::Plugin::Debugger
-
-```perl6
- use IRC::Client;
- use IRC::Client::Plugin::Debugger;
-
- IRC::Client.new(
- :host('localhost'),
- :debug,
- plugins => [ IRC::Client::Plugin::Debugger.new ]
- ).run;
-```
-
-When run, it will pretty-print all of the events received by the client. It
-does not stop plugin processing loop after handling a message.
-
-## IRC::Client::Plugin::PingPong
-
-```perl6
- use IRC::Client;
- IRC::Client.new.run; # automatically included in plugins-essential
-```
-
-This plugin makes IRC::Client respond to server's C<PING> messages and is
-included in the [`plugins-essential`](#plugins-essential) by default.
-
-# EXTENDING IRC::Client / WRITING YOUR OWN PLUGINS
-
-## Overview of the plugin system
-
-The core IRC::Client receives and parses IRC protocol messages from the
-server that it then passes through a plugin chain. The plugins declared in
-[`plugins-essential`](#plugins-essential) are executed first, followed by
-plugins in [`plugins`](#plugins). The order is the same as the order specified
-in those two lists.
-
-A plugin can return a [special constant](#return-value-constants) that
-indicates it handled the message and the plugin chain processing should stop.
-
-To subscribe to handle a particular IRC command, a plugin simply declares a
-method `irc-COMMAND`, where `COMMAND` is the name of the IRC command the
-plugin wishes to handle. There are also a couple of
-[special events](#special-events) the plugin can subscribe to, such as
-intialization during start up or when the client receives a private message
-or notice.
-
-## Return value constants
-
-```perl6
- use IRC::Client::Plugin;
- unit class IRC::Client::Plugin::Foo is IRC::Client::Plugin;
- ...
-```
-
-To make the constants available in your class, simply `use` IRC::Client::Plugin
-class.
-
-### `IRC_HANDLED`
-
-```perl6
- # Returned by default
- method irc-ping ($irc, $e) { $irc.ssay("PONG {$irc.nick} $e<params>[0]") }
-
- # Explicit return
- method irc-privmsg ($irc, $e) { return IRC_HANDLED; }
-```
-Specifies that plugin handled the message and the plugin chain processing
-should stop immediatelly. Plugins later in the chain won't know this
-message ever came. Unless you explicitly return
-[`IRC_NOT_HANDLED`](#irc_not_handled) constant, IRC::Client will assume
-`IRC_HANDLED` was returned.
-
-### `IRC_NOT_HANDLED`
-
-```perl6
- return IRC_NOT_HANDLED;
-```
-Returning this constant indicates to IRC::Client that your plugin did
-not "handle" the message and it should be propagated further down the
-plugin chain for other plugins to handle.
-
-## Subscribing to IRC events
-
-### Standard IRC commands
-
-```perl6
- method irc-privmsg ($irc, $e) { ... }
- method irc-notice ($irc, $e) { ... }
- method irc-353 ($irc, $e) { ... }
-```
-To subscribe to an IRC event, simply declare a method named `irc-command`,
-where `command` is the IRC command you want to handle, in **lower case**.
-The method takes two positional arguments: an `IRC::Client` object and
-the [parsed IRC message](#contents-of-the-parsed-irc-message).
-
-You'll likely generate a response based on the content of the parsed message
-and use one of the [METHODS FOR PLUGINS](#methods-for-plugins) to send that
-response.
-
-## Special Events
-
-```perl6
- method irc-start-up ($irc) { ... } # once per client run
- method irc-connected ($irc) { ... } # once per server connection
-
- method irc-all-events ($irc, $e) { ... }
- method irc-to-me ($irc, $e) { ... }
- method irc-privmsg-me ($irc, $e) { ... }
- method irc-notice-me ($irc, $e) { ... }
- ... # all other handlers for standard IRC commands
- method irc-unhandled ($irc, $e) { ... }
-```
-In addition to the [standard IRC commands](#standard-irc-commands), you can
-register several special cases. They're handled in the event chain in the order
-shown above (except for [`irc-start-up`](#irc-start-up) and
-[`irc-connected`](#irc-connected) that do not offect command-triggered events).
-That is, if a plugin returns [`IRC_HANDLED`](#irc_handled) after
-processing, say, [`irc-all-events`](#irc-all-events) event, its
-[`irc-notice-me`](#irc-notice-me) handler won't be triggered, even if it would
-otherwise.
-
-The available special events are as follows:
-
-### `irc-start-up`
+# DOCUMENTATION MAP
-```perl6
- method irc-start-up ($irc) { ... }
-```
-Passed IRC::Client object as the only argument. Triggered right when the
-IRC::Client is [`.run`](#run), which means most of
-[METHODS FOR PLUGINS](#methods-for-plugins) **cannot** be used, as no connection
-has been made yet. This event will be issued only once per [`.run`](#run)
-and the method's return value is discarded.
-
-### `irc-connected`
-
-```perl6
- method irc-connected ($irc) { ... }
-```
-Passed IRC::Client object as the only argument. Triggered right when we
-get a connection to the server, identify with it and issue `JOIN` commands
-to enter the channels. Note that at this point it is not guaranteed that the
-client is already in all the channels it's meant to join.
-This event will be issued only once per connection to the server
-and the method's return value is discarded.
-
-### `irc-all-events`
-
-```perl6
- method irc-all-events ($irc, $e) { ... }
-```
-Triggered for all IRC commands received, regardless of their content. As this
-method will be triggered before any others, you can use this to
-pre-process the message, for example. ***WARNING:*** **since
-[`IRC_HANDLED` constant](#irc_handled) is returned by default, if you do not
-explicitly return [`IRC_NOT_HANDLED`](#irc_not_handled), your client will
-stop handling ALL other messages
-***
-
-### `irc-to-me`
-
-```perl6
- method irc-to-me ($irc, $e, %res) {
- $irc.respond: |%res, :what("You told me: %res<what>");
- }
-```
-Triggered when: the IRC `PRIVMSG` command is received, where the recipient
-is the client (as opposed to some channel); the `NOTICE` command is
-received, or the `PRIVMSG` command is received, where the recipient is the
-channel and the message begins with client's nickname (i.e. the client
-was addressed in channel.
+* [Basics Tutorial](docs/01-basics.md)
+* [Event reference](docs/01-event-reference.md)
-Along with `IRC::Client` object and the event
-hash contained in `$e`, this method also receives an additional positional
-argument that is a hash containing the correct `$where`, `$who`, `$how`, and
-`$what` arguments to generate a response using [`.respond method`](#respond).
-You'll likely want to change the `$what` argument. You can do that by
-specifying `:what('...')` after slipping the `|%res`, as shown in the example
-above.
-
-Also, note: the supplied `$what` argument will have the client's
-nickname removed from it. Use the params in `$e` if you want the exact message
-that was said.
-
-### `irc-privmsg-me`
-
-```perl6
- method irc-privmsg-me ($irc, $e) { ... }
-```
-Triggered when the IRC `PRIVMSG` command is received, where the recipient
-is the client (as opposed to some channel).
-
-### `irc-notice-me`
-
-```perl6
- method irc-notice-me ($irc, $e) { ... }
-```
-Triggered when the IRC `NOTICE` command is received, where the recipient
-is the client (as opposed to some channel).
-
-### `irc-unhandled`
-
-```perl6
- method irc-unhandled ($irc, $e) { ... }
-```
-
-This is the same as [`irc-all-events`](#irc-all-events), except it's triggered
-**after** all other events were tried. This method can be used to catch
-any unhandled events.
-
-## Contents of the parsed IRC message
-
-```perl6
- # method irc-366 ($irc, $e) { ... }
- {
- command => "366".Str,
- params => [
- "Perl6IRC".Str,
- "#perl6bot".Str,
- "End of NAMES list".Str,
- ],
- pipe => { },
- who => {
- host => "irc.example.net".Str,
- },
- }
-
- # method irc-join ($irc, $e) { ... }
- {
- command => "JOIN".Str,
- params => [
- "#perl6bot".Str,
- ],
- pipe => { },
- who => {
- host => "localhost".Str,
- nick => "ZoffixW".Str,
- user => "~ZoffixW".Str,
- },
- }
-
- # method irc-privmsg ($irc, $e) { ... }
- {
- command => "PRIVMSG".Str,
- params => [
- "#perl6bot".Str,
- "Perl6IRC, hello!".Str,
- ],
- pipe => { },
- who => {
- host => "localhost".Str,
- nick => "ZoffixW".Str,
- user => "~ZoffixW".Str,
- },
- }
-
- # method irc-notice-me ($irc, $e) { ... }
- {
- command => "NOTICE".Str,
- params => [
- "Perl6IRC".Str,
- "you there?".Str,
- ],
- pipe => { },
- who => {
- host => "localhost".Str,
- nick => "ZoffixW".Str,
- user => "~ZoffixW".Str,
- },
- }
-```
-
-The second argument to event handlers is the parsed IRC message that is a
-hash with the following keys:
-
-### `command`
-
-```perl6
- command => "NOTICE".Str,
-```
-Contains the IRC command this message represents.
-
-### `params`
-
-```perl6
- params => [
- "Perl6IRC".Str,
- "you there?".Str,
- ],
-```
-Constains the array of parameters for the IRC command.
-
-### `pipe`
-
-```perl6
- pipe => { },
-```
-This is a special key that can be used for communication between plugins.
-While any plugin can modify any key of the parsed command's hash, the provided
-`pipe` hash is simply a means to provide some standard, agreed-upon name
-of a key to pass information around.
-
-### `who`
-
-```perl6
- #fdss
- who => {
- host => "localhost".Str,
- nick => "ZoffixW".Str,
- user => "~ZoffixW".Str,
- },
-
- who => {
- host => "irc.example.net".Str,
- },
-```
-A hash containing information on who sent the message. Messages sent by the
-server do not have `nick`/`user` keys specified.
+---
# REPOSITORY
diff --git a/examples/bot.pl6 b/examples/bot.pl6
index 524a74f..e991a8e 100644
--- a/examples/bot.pl6
+++ b/examples/bot.pl6
@@ -1,20 +1,23 @@
-use v6;
use lib 'lib';
use IRC::Client;
-use IRC::Client::Plugin::Debugger;
-class IRC::Client::Plugin::AddressedPlugin is IRC::Client::Plugin {
- method irc-addressed ($irc, $e, $where) {
- $irc.privmsg: $where[0], "$where[1], you addressed me";
+class MyPlug does IRC::Client::Plugin {
+ method irc-privmsg-channel ($msg where .text ~~ /^'say' \s+ $<cmd>=(.+)/ ) {
+ $msg.reply: "How about: $<cmd>.uc()";
}
}
my $irc = IRC::Client.new(
- :host<localhost>
- :channels<#perl6bot #zofbot>
- :debug
- :plugins(
- IRC::Client::Plugin::Debugger.new,
- IRC::Client::Plugin::AddressedPlugin.new
- )
-).run; \ No newline at end of file
+ :nick('IRCBot')
+ :debug<2>
+ :channels<#perl6 #perl7>
+ # :host<irc.freenode.net>
+ :port<6667>
+ # :servers(
+ # mine => { :port<5667> },
+
+ # inspircd => { },
+ # freenode => { :host<irc.freenode.net> },
+ # )
+ :plugins(MyPlug.new)
+).run;
diff --git a/lib/IRC/Client.pm6 b/lib/IRC/Client.pm6
index 700739c..9e3bfbd 100644
--- a/lib/IRC/Client.pm6
+++ b/lib/IRC/Client.pm6
@@ -1,174 +1,256 @@
-use v6;
-use IRC::Parser; # parse-irc
-use IRC::Client::Plugin::PingPong;
-use IRC::Client::Plugin;
unit class IRC::Client;
-has Bool:D $.debug = False;
-has Str:D $.host = 'localhost';
+use IRC::Client::Grammar;
+use IRC::Client::Grammar::Actions;
+
+my class IRC_FLAG_NEXT {};
+
+role IRC::Client::Plugin is export {
+ my IRC_FLAG_NEXT $.NEXT;
+ has $.irc is rw;
+}
+
+has Str:D $.host = 'localhost';
+has Int:D $.debug = 0;
has Str $.password;
-has Int:D $.port where 0 <= $_ <= 65535 = 6667;
-has Str:D $.nick = 'Perl6IRC';
-has Str:D $.username = 'Perl6IRC';
-has Str:D $.userhost = 'localhost';
-has Str:D $.userreal = 'Perl6 IRC Client';
-has Str:D @.channels = ['#perl6bot'];
-has IO::Socket::Async $.sock;
-has @.plugins = [];
-has @.plugins-essential = [
- IRC::Client::Plugin::PingPong.new
-];
-has @!plugs = [|@!plugins-essential, |@!plugins];
-
-method handle-event ($e) {
- $e<pipe> = {};
-
- for @!plugs.grep(*.^can: 'irc-all-events') -> $p {
- my $res = $p.irc-all-events(self, $e);
- return unless $res === IRC_NOT_HANDLED;
- }
+has Int:D $.port where 0 <= $_ <= 65535 = 6667;
+has Str:D $.nick is rw = 'Perl6IRC';
+has Str:D $.username = 'Perl6IRC';
+has Str:D $.userhost = 'localhost';
+has Str:D $.userreal = 'Perl6 IRC Client';
+has Str:D @.channels = ['#perl6'];
+has @.filters where .all ~~ Callable;
+has @.plugins;
+has %.servers;
+has Bool $!is-connected = False;
+has Lock $!lock = Lock.new;
+has Channel $!event-pipe = Channel.new;
- # Wait for END_MOTD or ERR_NOMOTD before attempting to join
- if $e<command> eq '422' | '376' {
- $.ssay("JOIN {@!channels[]}\n");
- .irc-connected: self for @!plugs.grep(*.^can: 'irc-connected');
- }
+my &colored = try {
+ require Terminal::ANSIColor;
+ &colored
+ = GLOBAL::Terminal::ANSIColor::EXPORT::DEFAULT::<&colored>;
+} // sub (Str $s, $) { $s };
- my $nick = $!nick;
- if ( ( $e<command> eq 'PRIVMSG' and $e<params>[0] eq $nick )
- or ( $e<command> eq 'NOTICE' and $e<params>[0] eq $nick )
- or ( $e<command> eq 'PRIVMSG'
- and $e<params>[1] ~~ /:i ^ $nick <[,:]> \s+/
- )
- ) {
- my %res = :where($e<who><nick> ),
- :who( $e<who><nick> ),
- :how( $e<command> ),
- :what( $e<params>[1] );
-
- %res<where> = $e<params>[0] # this message was said in the channel
- unless ( $e<command> eq 'PRIVMSG' and $e<params>[0] eq $nick )
- or ( $e<command> eq 'NOTICE' and $e<params>[0] eq $nick );
-
- %res<what>.subst-mutate: /:i ^ $nick <[,:]> \s+/, ''
- if %res<where> ~~ /^ <[#&]>/;
-
- for @!plugs.grep(*.^can: 'irc-to-me') -> $p {
- my $res = $p.irc-to-me(self, $e, %res);
- return unless $res === IRC_NOT_HANDLED;
- }
- }
+method run {
+ self!prep-servers;
+ .irc = self for @.plugins.grep: { .DEFINITE and .^can: 'irc' };
- if ( $e<command> eq 'PRIVMSG' and $e<params>[0] eq $!nick ) {
- for @!plugs.grep(*.^can: 'irc-privmsg-me') -> $p {
- my $res = $p.irc-privmsg-me(self, $e);
- return unless $res === IRC_NOT_HANDLED;
+ start {
+ my $closed = $!event-pipe.closed;
+ loop {
+ if $!event-pipe.receive -> $e {
+ $!debug and debug-print $e, :in, :server($e.server);
+ $!lock.protect: {
+ self!handle-event: $e;
+ CATCH { default { warn $_; warn .backtrace } }
+ };
+ }
+ elsif $closed { last }
}
}
- if ( $e<command> eq 'NOTICE' and $e<params>[0] eq $!nick ) {
- for @!plugs.grep(*.^can: 'irc-notice-me') -> $p {
- my $res = $p.irc-notice-me(self, $e);
- return unless $res === IRC_NOT_HANDLED;
- }
- }
+ for %!servers.kv -> $s-name, $s-conf {
+ $s-conf<promise>
+ = IO::Socket::Async.connect($s-conf<host>, $s-conf<port>).then: {
+ $!lock.protect: { $s-conf<sock> = .result; };
- my $cmd = 'irc-' ~ $e<command>.lc;
- for @!plugs.grep(*.^can: $cmd) -> $p {
- my $res = $p."$cmd"(self, $e);
- return unless $res === IRC_NOT_HANDLED;
- }
+ self!ssay: "PASS $!password", :server($s-name)
+ if $!password.defined;
+ self!ssay: "NICK $!nick", :server($s-name);
+ self!ssay:
+ "USER $!username $!username $!host :$!userreal",
+ :server($s-name);
+
+ my $left-overs = '';
+ react {
+ whenever $s-conf<sock>.Supply :bin -> $buf is copy {
+ my $str = try $buf.decode: 'utf8';
+ $str or $str = $buf.decode: 'latin-1';
+ $str = ($left-overs//'') ~ $str;
- for @!plugs.grep(*.^can: 'irc-unhandled') -> $p {
- my $res = $p.irc-unhandled(self, $e);
- return unless $res === IRC_NOT_HANDLED;
+ (my $events, $left-overs)
+ = self!parse: $str, :server($s-name);
+ $!event-pipe.send: $_ for $events.grep: *.defined;
+ }
+ CATCH { default { warn $_; warn .backtrace } }
+ }
+ $s-conf<sock>.close;
+ CATCH { default { warn $_; warn .backtrace } }
+ };
}
+ await Promise.allof: %!servers.values».<promise>;
}
-method notice (Str $who, Str $what) {
- my $msg = "NOTICE $who :$what\n";
- $!debug and "{plug-name}$msg".put;
- $!sock.print("$msg\n");
- self;
+method emit-custom (|c) {
+ $!event-pipe.send: c;
}
-method privmsg (Str $who, Str $what) {
- my $msg = "PRIVMSG $who :$what\n";
- $!debug and "{plug-name}$msg".put;
- $!sock.print("$msg\n");
- self;
+method send (:$where!, :$text!, :$server, :$notice) {
+ for $server || |%!servers.keys.sort {
+ self.send-cmd: $notice ?? 'NOTICE' !! 'PRIVMSG', $where, $text,
+ :server($_);
+ }
}
-method respond (
- Str:D :$how = 'privmsg',
- Str:D :$where is required,
- Str:D :$what is required is copy,
- Str:D :$who,
- :$when where Any|Dateish|Instant;
- # TODO: remove Any: https://rt.perl.org/Ticket/Display.html?id=127142
-) {
- $what = "$who, $what" if $who and $where ~~ /^<[#&]>/;
- my $method = $how.fc eq 'PRIVMSG'.fc ?? 'privmsg'
- !! $how.fc eq 'NOTICE'.fc ?? 'notice'
- !! fail 'Unknown :$how specified. Use PRIVMSG or NOTICE';
-
- if $when {
- Promise.at($when).then: { self."$method"($where, $what) };
- CATCH { warn .backtrace }
+method send-cmd ($cmd, *@args is copy, :$server, :$prefix = '') {
+ CATCH { default { warn $_; warn .backtrace } }
+
+ if $cmd eq 'NOTICE'|'PRIVMSG' and @!filters
+ and my @f = @!filters.grep({
+ .signature.ACCEPTS: \(@args[1])
+ or .signature.ACCEPTS: \(@args[1], where => @args[0])
+ })
+ {
+ start {
+ CATCH { default { warn $_; warn .backtrace } }
+
+ my ($where, $text) = @args;
+ for @f -> $f {
+ given $f.signature.params.elems {
+ when 1 { $text = $f($text); }
+ when 2 { ($text, $where) = $f($text, :$where) }
+ }
+ }
+ self!ssay: :$server, join ' ', $cmd, $where, ":$prefix$text";
+ }
}
else {
- self."$method"($where, $what);
+ @args[*-1] = ':' ~ @args[*-1];
+ self!ssay: :$server, join ' ', $cmd, @args;
}
- self;
}
-method run {
- .irc-start-up: self for @!plugs.grep(*.^can: 'irc-start-up');
-
- await IO::Socket::Async.connect( $!host, $!port ).then({
- $!sock = .result;
- $.ssay("PASS $!password\n") if $!password.defined;
- $.ssay("NICK $!nick\n");
- $.ssay("USER $!username $!username $!host :$!userreal\n");
-
- # my $left-overs = '';
- react {
- whenever $!sock.Supply :bin -> $buf is copy {
- my $str = try $buf.decode: 'utf8';
- $str or $str = $buf.decode: 'latin-1';
- # $str ~= $left-overs;
- $!debug and "[server {DateTime.now}] {$str}".put;
- my $events = parse-irc $str;
- for @$events -> $e {
- self.handle-event: $e;
- CATCH { warn .backtrace }
+method !prep-servers {
+ %!servers = '*' => {} unless %!servers;
+
+ for %!servers.values -> $s {
+ $s{$_} //= self."$_"()
+ for <host password port nick username userhost userreal>;
+ $s<channels> = @.channels;
+ $s<socket> = Nil;
+ }
+}
+
+method !handle-event ($e) {
+ given $e.command {
+ when '001' {
+ %!servers{ $e.server }<nick> = $e.args[0];
+ self!ssay: "JOIN $_", :server($e.server) for @.channels;
+ }
+ when 'PING' { return $e.reply; }
+ when 'JOIN' {
+ # say "Joined channel $e.channel() on $e.server()"
+ # if $e.nick eq %!servers{ $e.server }<nick>;
+ }
+ }
+
+ my $event-name = 'irc-' ~ $e.^name.subst('IRC::Client::Message::', '')
+ .lc.subst: '::', '-', :g;
+
+ my @events = flat gather {
+ given $event-name {
+ when 'irc-privmsg-channel' | 'irc-notice-channel' {
+ my $nick = $!nick;
+ if $e.text.subst-mutate: /^ $nick <[,:\s]> \s* /, '' {
+ take 'irc-addressed', ('irc-to-me' if $!is-connected);
+ }
+ elsif $e ~~ / << $nick >> / and $!is-connected {
+ take 'irc-mentioned';
}
+ take $event-name, $event-name eq 'irc-privmsg-channel'
+ ?? 'irc-privmsg' !! 'irc-notice';
+ }
+ when 'irc-privmsg-me' {
+ take $event-name, ('irc-to-me' if $!is-connected),
+ 'irc-privmsg';
+ }
+ when 'irc-notice-me' {
+ take $event-name, ('irc-to-me' if $!is-connected),
+ 'irc-notice';
+ }
+ when 'irc-mode-channel' | 'irc-mode-me' {
+ take $event-name, 'irc-mode';
+ }
+ when 'irc-numeric' {
+ if $e.command eq '001' {
+ $!is-connected = True ;
+ take 'irc-connected';
+ }
+ take 'irc-' ~ $e.command, $event-name;
}
-
- CATCH { warn .backtrace }
}
+ take 'irc-all';
+ }
+
+ EVENT: for @events -> $event {
+ debug-print "emitting `$event`", :sys
+ if $!debug >= 3 or ($!debug == 2 and not $event eq 'irc-all');
- say "Closing connection";
- $!sock.close;
+ for self!plugs-that-can($event, $e) {
+ my $res = ."$event"($e);
+ next if $res ~~ IRC_FLAG_NEXT;
+ if $res ~~ Promise {
+ $res.then: { $e.reply: $^r unless $^r ~~ Nil or $e.replied; }
+ } else {
+ $e.reply: $res unless $res ~~ Nil or $e.replied;
+ }
+ last EVENT;
- # CATCH { warn .backtrace }
- });
+ CATCH { default { warn $_, .backtrace; } }
+ }
+ }
}
-method ssay (Str:D $msg) {
- $!debug and "{plug-name}$msg".put;
- $!sock.print("$msg\n");
+method !plugs-that-can ($method, $e) {
+ gather {
+ for @!plugins -> $plug {
+ take $plug if .cando: \($plug, $e)
+ for $plug.^can: $method;
+ }
+ }
+}
+
+method !ssay (Str:D $msg, :$server = '*') {
+ $!debug and debug-print $msg, :out, :$server;
+ %!servers{ $server }<sock>.print("$msg\n");
self;
}
-#### HELPER SUBS
+method !parse (Str:D $str, :$server) {
+ return |IRC::Client::Grammar.parse(
+ $str,
+ :actions( IRC::Client::Grammar::Actions.new: :irc(self), :$server )
+ ).made;
+}
+
+sub debug-print (Str(Any) $str, :$in, :$out, :$sys, :$server) {
+ my $server-str = $server
+ ?? colored($server, 'bold white on_cyan') ~ ' ' !! '';
-sub plug-name {
- my $plug = callframe(3).file;
- my $cur = $?FILE;
- return '[core] ' if $plug eq $cur;
- $cur ~~ s/'.pm6'$//;
- $plug ~~ s:g/^ $cur '/' | '.pm6'$//;
- $plug ~~ s/'/'/::/;
- return "[$plug] ";
+ my @bits = $str.split: ' ';
+ if $in {
+ my ($pref, $cmd) = 0, 1;
+ if @bits[0] eq '❚⚠❚' {
+ @bits[0] = colored @bits[0], 'bold white on_red';
+ $pref++; $cmd++;
+ }
+ @bits[$pref] = colored @bits[$pref], 'bold magenta';
+ @bits[$cmd] = @bits[$cmd] ~~ /^ <[0..9]>**3 $/
+ ?? colored(@bits[$cmd], 'bold red')
+ !! colored(@bits[$cmd], 'bold yellow');
+ put colored('▬▬▶ ', 'bold blue' ) ~ $server-str ~ @bits.join: ' ';
+ }
+ elsif $out {
+ @bits[0] = colored @bits[0], 'bold magenta';
+ put colored('◀▬▬ ', 'bold green') ~ $server-str ~ @bits.join: ' ';
+ }
+ elsif $sys {
+ put colored(' ' x 4 ~ '↳', 'bold white') ~ ' '
+ ~ @bits.join(' ')
+ .subst: /(\`<-[`]>+\`)/, { colored(~$0, 'bold cyan') };
+ }
+ else {
+ die "Unknown debug print mode";
+ }
}
diff --git a/lib/IRC/Grammar.pm6 b/lib/IRC/Client/Grammar.pm6
index c05322c..feec9fd 100644
--- a/lib/IRC/Grammar.pm6
+++ b/lib/IRC/Client/Grammar.pm6
@@ -1,5 +1,6 @@
-unit grammar IRC::Grammar;
-token TOP { <message>+ }
+unit grammar IRC::Client::Grammar;
+token TOP { <message>+ <left-overs> }
+token left-overs { \N* }
token SPACE { ' '+ }
token message { [':' <prefix> <SPACE> ]? <command> <params> \n }
token prefix {
@@ -8,12 +9,12 @@ token message { [':' <prefix> <SPACE> ]? <command> <params> \n }
}
token servername { <host> }
token nick { <letter> [ <letter> | <number> | <special> ]* }
- token user { <-[\ \x0\r\n]>+? <before [<SPACE> | '@']>}
+ token user { <-[\ \x[0]\r\n]>+? <before [<SPACE> | '@']>}
token host { <-[\s!@]>+ }
token command { <letter>+ | <number>**3 }
token params { <SPACE>* [ ':' <trailing> | <middle> <params> ]? }
- token middle { <-[:\ \x0\r\n]> <-[\ \x0\r\n]>* }
- token trailing { <-[\x0\r\n]>* }
+ token middle { <-[:\ \x[0]\r\n]> <-[\ \x[0]\r\n]>* }
+ token trailing { <-[\x[0]\r\n]>* }
token letter { <[a..zA..Z]> }
token number { <[0..9]> }
diff --git a/lib/IRC/Client/Grammar/Actions.pm6 b/lib/IRC/Client/Grammar/Actions.pm6
new file mode 100644
index 0000000..b1fcc53
--- /dev/null
+++ b/lib/IRC/Client/Grammar/Actions.pm6
@@ -0,0 +1,119 @@
+unit class IRC::Client::Grammar::Actions;
+
+use IRC::Client::Message;
+
+has $.irc;
+has $.server;
+
+method TOP ($/) {
+ $/.make: (
+ $<message>».made,
+ ~( $<left-overs> // '' ),
+ );
+}
+
+method message ($match) {
+ my %args;
+ my $pref = $match<prefix>;
+ for qw/nick user host/ {
+ $pref{$_}.defined or next;
+ %args<who>{$_} = ~$pref{$_};
+ }
+ %args<who><host> = ~$pref<servername> if $pref<servername>.defined;
+
+ my $p = $match<params>;
+ loop {
+ %args<params>.append: ~$p<middle> if $p<middle>.defined;
+
+ if ( $p<trailing>.defined ) {
+ %args<params>.append: ~$p<trailing>;
+ last;
+ }
+ last unless $p<params>.defined;
+ $p = $p<params>;
+ }
+
+ my %msg-args =
+ command => $match<command>.uc,
+ args => %args<params>,
+ host => %args<who><host>//'',
+ irc => $!irc,
+ nick => %args<who><nick>//'',
+ server => $!server,
+ usermask => ~($match<prefix>//''),
+ username => %args<who><user>//'';
+
+ my $msg;
+ given %msg-args<command> {
+ when /^ <[0..9]>**3 $/ {
+ $msg = IRC::Client::Message::Numeric.new: |%msg-args;
+ }
+ when 'JOIN' {
+ $msg = IRC::Client::Message::Join.new:
+ :channel( %args<params>[0] ),
+ |%msg-args;
+ }
+ when 'PART' {
+ $msg = IRC::Client::Message::Part.new:
+ :channel( %args<params>[0] ),
+ |%msg-args;
+ }
+ when 'NICK' {
+ $msg = IRC::Client::Message::Nick.new:
+ :new-nick( %args<params>[0] ),
+ |%msg-args;
+ }
+ when 'NOTICE' { $msg = msg-notice %args, %msg-args }
+ when 'MODE' { $msg = msg-mode %args, %msg-args }
+ when 'PING' { $msg = IRC::Client::Message::Ping.new: |%msg-args }
+ when 'PRIVMSG' { $msg = msg-privmsg %args, %msg-args }
+ when 'QUIT' { $msg = IRC::Client::Message::Quit.new: |%msg-args }
+ default { $msg = IRC::Client::Message::Unknown.new: |%msg-args }
+ }
+
+ $match.make: $msg;
+}
+
+sub msg-privmsg (%args, %msg-args) {
+ %args<params>[0] ~~ /^<[#&]>/
+ and return IRC::Client::Message::Privmsg::Channel.new:
+ :channel( %args<params>[0] ),
+ :text( %args<params>[1] ),
+ |%msg-args;
+
+ return IRC::Client::Message::Privmsg::Me.new:
+ :text( %args<params>[1] ),
+ |%msg-args;
+}
+
+sub msg-notice (%args, %msg-args) {
+ %args<params>[0] ~~ /^<[#&]>/
+ and return IRC::Client::Message::Notice::Channel.new:
+ :channel( %args<params>[0] ),
+ :text( %args<params>[1] ),
+ |%msg-args;
+
+ return IRC::Client::Message::Notice::Me.new:
+ :text( %args<params>[1] ),
+ |%msg-args;
+}
+
+sub msg-mode (%args, %msg-args) {
+ if %args<params>[0] ~~ /^<[#&]>/ {
+ my @modes;
+ for %args<params>[1..*-1].join.comb: /\S/ {
+ state $sign;
+ /<[+-]>/ and $sign = $_ and next;
+ @modes.push: $sign => $_;
+ };
+ return IRC::Client::Message::Mode::Channel.new:
+ :channel( %args<params>[0] ),
+ :modes( @modes ),
+ |%msg-args;
+ }
+ else {
+ return IRC::Client::Message::Mode::Me.new:
+ :modes( %args<params>[1..*-1].join.comb: /<[a..zA..Z]>/ ),
+ |%msg-args;
+ }
+}
diff --git a/lib/IRC/Client/Message.pm6 b/lib/IRC/Client/Message.pm6
new file mode 100644
index 0000000..9559fd1
--- /dev/null
+++ b/lib/IRC/Client/Message.pm6
@@ -0,0 +1,70 @@
+unit package IRC::Client::Message;
+
+role IRC::Client::Message {
+ has $.irc is required;
+ has Str:D $.nick is required;
+ has Str:D $.username is required;
+ has Str:D $.host is required;
+ has Str:D $.usermask is required;
+ has Str:D $.command is required;
+ has Str:D $.server is required;
+ has $.args is required;
+
+ method Str { ":$!usermask $!command $!args[]" }
+}
+
+constant M = IRC::Client::Message;
+
+role Join does M { has $.channel; }
+role Mode does M { has @.modes; }
+role Mode::Channel does Mode { has $.channel; }
+role Mode::Me does Mode { }
+role Nick does M { has $.new-nick; }
+role Numeric does M { }
+role Part does M { has $.channel; }
+role Quit does M { }
+role Unknown does M {
+ method Str { "❚⚠❚ :$.usermask $.command $.args[]" }
+}
+
+role Ping does M {
+ method reply { $.irc.send-cmd: 'PONG', $.args, :$.server; }
+}
+
+role Privmsg does M {
+ has $.text is rw;
+ has Bool $.replied is rw = False;
+ method Str { $.text }
+}
+role Privmsg::Channel does Privmsg {
+ has $.channel;
+ method reply ($text, :$where) {
+ $.irc.send-cmd: 'PRIVMSG', $where // $.channel, $text,
+ :$.server, :prefix("$.nick, ");
+ }
+}
+role Privmsg::Me does Privmsg {
+ method reply ($text, :$where) {
+ $.irc.send-cmd: 'PRIVMSG', $where // $.nick, $text, :$.server;
+ }
+}
+
+role Notice does M {
+ has $.text is rw;
+ has Bool $.replied is rw = False;
+ method Str { $.text }
+}
+role Notice::Channel does Notice {
+ has $.channel;
+ method reply ($text, :$where) {
+ $.irc.send-cmd: 'NOTICE', $where // $.channel, $text,
+ :$.server, :prefix("$.nick, ");
+ $.replied = True;
+ }
+}
+role Notice::Me does Notice {
+ method reply ($text, :$where) {
+ $.irc.send-cmd: 'NOTICE', $where // $.nick, $text, :$.server;
+ $.replied = True;
+ }
+}
diff --git a/lib/IRC/Client/Plugin.pm6 b/lib/IRC/Client/Plugin.pm6
deleted file mode 100644
index d73d7bd..0000000
--- a/lib/IRC/Client/Plugin.pm6
+++ /dev/null
@@ -1,3 +0,0 @@
-constant IRC_HANDLED = "irc plugin handled \x1";
-constant IRC_NOT_HANDLED = "irc plugin not-handled \x2";
-unit class IRC::Client::Plugin;
diff --git a/lib/IRC/Client/Plugin/Debugger.pm6 b/lib/IRC/Client/Plugin/Debugger.pm6
deleted file mode 100644
index 13b1461..0000000
--- a/lib/IRC/Client/Plugin/Debugger.pm6
+++ /dev/null
@@ -1,8 +0,0 @@
-use Data::Dump;
-use IRC::Client::Plugin;
-unit class IRC::Client::Plugin::Debugger is IRC::Client::Plugin;
-
-method irc-all-events ($irc, $e) {
- say Dump $e, :indent(4);
- return IRC_NOT_HANDLED;
-}
diff --git a/lib/IRC/Client/Plugin/PingPong.pm6 b/lib/IRC/Client/Plugin/PingPong.pm6
deleted file mode 100644
index 2651fd6..0000000
--- a/lib/IRC/Client/Plugin/PingPong.pm6
+++ /dev/null
@@ -1,2 +0,0 @@
-unit class IRC::Client::Plugin::PingPong;
-method irc-ping ($irc, $e) { $irc.ssay("PONG {$irc.nick} $e<params>[0]") }
diff --git a/lib/IRC/Grammar/Actions.pm6 b/lib/IRC/Grammar/Actions.pm6
deleted file mode 100644
index 234e392..0000000
--- a/lib/IRC/Grammar/Actions.pm6
+++ /dev/null
@@ -1,26 +0,0 @@
-unit class IRC::Grammar::Actions;
-method TOP ($/) { $/.make: $<message>>>.made }
-method message ($/) {
- my $pref = $/<prefix>;
- my %args = command => ~$/<command>;
- for qw/nick user host/ {
- $pref{$_}.defined or next;
- %args<who>{$_} = $pref{$_}.Str;
- }
- %args<who><host> = ~$pref<servername> if $pref<servername>.defined;
-
- my $p = $/<params>;
-
- for ^100 { # bail out after 100 iterations; we're stuck
- if ( $p<middle>.defined ) {
- %args<params>.append: ~$p<middle>;
- }
- if ( $p<trailing>.defined ) {
- %args<params>.append: ~$p<trailing>;
- last;
- }
- $p = $p<params>;
- }
-
- $/.make: %args;
-}
diff --git a/lib/IRC/Parser.pm6 b/lib/IRC/Parser.pm6
deleted file mode 100644
index dda05e6..0000000
--- a/lib/IRC/Parser.pm6
+++ /dev/null
@@ -1,7 +0,0 @@
-use IRC::Grammar;
-use IRC::Grammar::Actions;
-unit class IRC::Parser;
-
-sub parse-irc (Str:D $input) is export {
- IRC::Grammar.parse($input, actions => IRC::Grammar::Actions).made // [];
-}
diff --git a/xt/meta.t b/t/meta.t
index 6e2447d..6e2447d 100644
--- a/xt/meta.t
+++ b/t/meta.t
diff --git a/t/release/01-basic.t b/t/release/01-basic.t
new file mode 100644
index 0000000..114f4c4
--- /dev/null
+++ b/t/release/01-basic.t
@@ -0,0 +1,62 @@
+use lib <lib t/release>;
+use Test;
+use Test::Notice;
+use IRC::Client;
+use Test::IRC::Server;
+
+my $Wait = (%*ENV<IRC_CLIENT_TEST_WAIT>//1) * 5;
+
+notice 'Testing connection to one server and joining two channels';
+
+diag 'Starting IRC Server';
+my $s = Test::IRC::Server.new;
+END { $s.kill };
+
+loop {
+ last if $s.out.elems >= 2;
+ sleep 0.5;
+}
+
+diag 'Starting IRC Client';
+start {
+ my $irc = IRC::Client.new(
+ :debug(%*ENV<IRC_CLIENT_DEBUG>//0)
+ :nick<IRCBot>
+ :channels<#perl6 #perl7>
+ :servers(
+ meow => { :port<5000> }
+ )
+ ).run;
+}
+
+diag 'Waiting for things to happen...';
+Promise.in($Wait).then: {$s.kill}
+await $s.promise;
+
+my $out = [
+ {:args($[[Any],]), :event("ircd_registered")},
+ {:args($[[5000, 1, "0.0.0.0"],]), :event("ircd_listener_add")},
+ {
+ :args(
+ $[["IRCBot", 1, 'time', "+i", "~Perl6IRC",
+ "simple.poco.server.irc", "simple.poco.server.irc",
+ "Perl6 IRC Client"],]
+ ),
+ :event("ircd_daemon_nick")},
+ {
+ :args($[["IRCBot!~Perl6IRC\@simple.poco.server.irc", "#perl6"],]), :event("ircd_daemon_join")
+ },
+ {
+ :args($[["IRCBot!~Perl6IRC\@simple.poco.server.irc", "#perl7"],]), :event("ircd_daemon_join")
+ }
+];
+
+# Fix time signature;
+for $s.out {
+ next unless .<event> eq 'ircd_daemon_nick';
+ .<args>[0][2] = 'time';
+}
+
+is-deeply $s.out, $out, 'Server output looks right';
+
+done-testing;
diff --git a/t/release/02-multi-server.t b/t/release/02-multi-server.t
new file mode 100644
index 0000000..d234c0b
--- /dev/null
+++ b/t/release/02-multi-server.t
@@ -0,0 +1,75 @@
+use lib <lib t/release>;
+use Test;
+use Test::Notice;
+use IRC::Client;
+use Test::IRC::Server;
+
+my $Wait = (%*ENV<IRC_CLIENT_TEST_WAIT>//1) * 5;
+
+notice 'Testing connection to four servers and joining two channels in each';
+
+diag 'Starting IRC Servers';
+my $s1 = Test::IRC::Server.new: :port<5020>;
+my $s2 = Test::IRC::Server.new: :port<5021>;
+my $s3 = Test::IRC::Server.new: :port<5022>;
+my $s4 = Test::IRC::Server.new: :port<5023>;
+END { $s1.kill; $s2.kill; $s3.kill; $s4.kill; };
+
+loop {
+ last if $s1.out.elems & $s2.out.elems & $s3.out.elems & $s4.out.elems >= 2;
+ sleep 0.5;
+}
+
+diag 'Starting IRC Client';
+start {
+ my $irc = IRC::Client.new(
+ :debug(%*ENV<IRC_CLIENT_DEBUG>//0)
+ :nick<IRCBot>
+ :channels<#perl6 #perl7>
+ :servers(
+ s1 => { :port<5020> },
+ s2 => { :port<5021>, :nick<OtherBot>, :channels<#perl7 #perl9> },
+ s3 => { :port<5022>, :channels<#perl10 #perl11> },
+ s4 => { :port<5023>, :nick<YetAnotherBot> },
+ )
+ ).run;
+}
+
+diag 'Waiting for things to happen...';
+Promise.in($Wait).then: { $s1.kill; $s2.kill; $s3.kill; $s4.kill; }
+await Promise.allof: ($s1, $s2, $s3, $s4).map: *.promise;
+
+dd $s1.out;
+diag '----';
+dd $s2.out;
+diag '----';
+dd $s3.out;
+diag '----';
+dd $s4.out;
+diag '----';
+
+#
+# my $out = [
+# {:args($[[Any],]), :event("ircd_registered")},
+# {:args($[[5000, 1, "0.0.0.0"],]), :event("ircd_listener_add")},
+# {
+# :args(
+# $[["IRCBot", 1, 'time', "+i", "~Perl6IRC",
+# "simple.poco.server.irc", "simple.poco.server.irc",
+# "Perl6 IRC Client"],]
+# ),
+# :event("ircd_daemon_nick")},
+# {
+# :args($[["IRCBot!~Perl6IRC\@simple.poco.server.irc", "#perl6"],]), :event("ircd_daemon_join")
+# }
+# ];
+#
+# # Fix time signature;
+# for $s.out {
+# next unless .<event> eq 'ircd_daemon_nick';
+# .<args>[0][2] = 'time';
+# }
+#
+# is-deeply $s.out, $out, 'Server output looks right';
+
+done-testing;
diff --git a/t/release/Test/IRC/Server.pm6 b/t/release/Test/IRC/Server.pm6
new file mode 100644
index 0000000..fed3d66
--- /dev/null
+++ b/t/release/Test/IRC/Server.pm6
@@ -0,0 +1,20 @@
+unit class Test::IRC::Server;
+
+use JSON::Fast;
+
+has $!port;
+has $!proc;
+has Promise $.promise;
+has @.out;
+
+submethod BUILD (:$!port = 5000, :$server = 't/release/servers/01-basic.pl') {
+ $!proc = Proc::Async.new: 'perl', $server, $!port;
+ $!proc.stdout.tap: {
+ %*ENV<IRC_CLIENT_DEBUG> and dd .lines;
+ @!out.append: |.lines».&from-json
+ };
+ $!proc.stderr.tap: { warn $_ };
+ $!promise = $!proc.start;
+}
+
+method kill { $!proc.kill; }
diff --git a/t/release/servers/01-basic.pl b/t/release/servers/01-basic.pl
new file mode 100644
index 0000000..086e213
--- /dev/null
+++ b/t/release/servers/01-basic.pl
@@ -0,0 +1,62 @@
+use strict;
+use warnings;
+use JSON::Meth;
+use 5.020;
+use POE qw(Component::Server::IRC);
+
+$|++;
+
+my ($Port) = @ARGV;
+
+my %config = (
+ servername => 'simple.poco.server.irc',
+ nicklen => 15,
+ network => 'SimpleNET'
+);
+
+my $pocosi = POE::Component::Server::IRC->spawn( config => \%config );
+
+POE::Session->create(
+ package_states => [
+ 'main' => [qw(_start _default)],
+ ],
+ heap => { ircd => $pocosi },
+);
+
+$poe_kernel->run();
+
+sub _start {
+ my ($kernel, $heap) = @_[KERNEL, HEAP];
+
+ $heap->{ircd}->yield('register', 'all');
+ $heap->{ircd}->add_auth(mask => '*@*');
+ $heap->{ircd}->add_listener(port => $Port);
+ $heap->{ircd}->add_operator({
+ username => 'moo',
+ password => 'fishdont',
+ });
+}
+
+sub _default {
+ my ($event, @args) = @_[ARG0 .. $#_];
+ say {
+ event => $event,
+ args => \@args,
+ }->$j;
+
+
+ # print "$event: ";
+ # for my $arg (@args) {
+ # if (ref($arg) eq 'ARRAY') {
+ # print "[", join ( ", ", @$arg ), "] ";
+ # }
+ # elsif (ref($arg) eq 'HASH') {
+ # print "{", join ( ", ", %$arg ), "} ";
+ # }
+ # else {
+ # print "'$arg' ";
+ # }
+ # }
+ #
+ # print "\n";
+ }