diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | DESIGN-NOTES.md | 68 | ||||
-rw-r--r-- | DESIGN/01-main.md | 961 | ||||
-rw-r--r-- | DESIGN/specs-and-references.md | 17 | ||||
-rw-r--r-- | README.md | 666 | ||||
-rw-r--r-- | examples/bot.pl6 | 29 | ||||
-rw-r--r-- | lib/IRC/Client.pm6 | 360 | ||||
-rw-r--r-- | lib/IRC/Client/Grammar.pm6 (renamed from lib/IRC/Grammar.pm6) | 11 | ||||
-rw-r--r-- | lib/IRC/Client/Grammar/Actions.pm6 | 119 | ||||
-rw-r--r-- | lib/IRC/Client/Message.pm6 | 70 | ||||
-rw-r--r-- | lib/IRC/Client/Plugin.pm6 | 3 | ||||
-rw-r--r-- | lib/IRC/Client/Plugin/Debugger.pm6 | 8 | ||||
-rw-r--r-- | lib/IRC/Client/Plugin/PingPong.pm6 | 2 | ||||
-rw-r--r-- | lib/IRC/Grammar/Actions.pm6 | 26 | ||||
-rw-r--r-- | lib/IRC/Parser.pm6 | 7 | ||||
-rw-r--r-- | t/meta.t (renamed from xt/meta.t) | 0 | ||||
-rw-r--r-- | t/release/01-basic.t | 62 | ||||
-rw-r--r-- | t/release/02-multi-server.t | 75 | ||||
-rw-r--r-- | t/release/Test/IRC/Server.pm6 | 20 | ||||
-rw-r--r-- | t/release/servers/01-basic.pl | 62 |
20 files changed, 1649 insertions, 919 deletions
@@ -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/ @@ -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/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"; + } |