diff options
-rw-r--r-- | DESIGN-NOTES.md | 68 | ||||
-rw-r--r-- | DESIGN/01-main.md | 70 | ||||
-rw-r--r-- | DESIGN/specs-and-references.md | 15 | ||||
-rw-r--r-- | lib/IRC/Client.pm6 | 174 | ||||
-rw-r--r-- | lib/old-IRC/Client.pm6 | 174 | ||||
-rw-r--r-- | lib/old-IRC/Client/Plugin.pm6 (renamed from lib/IRC/Client/Plugin.pm6) | 0 | ||||
-rw-r--r-- | lib/old-IRC/Client/Plugin/Debugger.pm6 (renamed from lib/IRC/Client/Plugin/Debugger.pm6) | 0 | ||||
-rw-r--r-- | lib/old-IRC/Client/Plugin/PingPong.pm6 (renamed from lib/IRC/Client/Plugin/PingPong.pm6) | 0 | ||||
-rw-r--r-- | lib/old-IRC/Grammar.pm6 (renamed from lib/IRC/Grammar.pm6) | 0 | ||||
-rw-r--r-- | lib/old-IRC/Grammar/Actions.pm6 (renamed from lib/IRC/Grammar/Actions.pm6) | 0 | ||||
-rw-r--r-- | lib/old-IRC/Parser.pm6 (renamed from lib/IRC/Parser.pm6) | 0 | ||||
-rw-r--r-- | t/meta.t (renamed from xt/meta.t) | 0 |
12 files changed, 259 insertions, 242 deletions
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..16ecf24 --- /dev/null +++ b/DESIGN/01-main.md @@ -0,0 +1,70 @@ +# PURPOSE + +The purpose of IRC::Client is to provide 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. + +## Core + +### 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 program may have multiple Client Objects, but each of them can be connected +only to one IRC server. + +A relevant Client Object must be easily accessible to the user of the +implementation. This includes user's plugins responsible for handling +events. + +### diff --git a/DESIGN/specs-and-references.md b/DESIGN/specs-and-references.md new file mode 100644 index 0000000..12c1f23 --- /dev/null +++ b/DESIGN/specs-and-references.md @@ -0,0 +1,15 @@ + +# Specs + +* [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) + +# Future + +IRCv3 group: http://ircv3.net/ diff --git a/lib/IRC/Client.pm6 b/lib/IRC/Client.pm6 index 700739c..e69de29 100644 --- a/lib/IRC/Client.pm6 +++ b/lib/IRC/Client.pm6 @@ -1,174 +0,0 @@ -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'; -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; - } - - # 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 $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; - } - } - - 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; - } - } - - 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; - } - } - - my $cmd = 'irc-' ~ $e<command>.lc; - for @!plugs.grep(*.^can: $cmd) -> $p { - my $res = $p."$cmd"(self, $e); - return unless $res === IRC_NOT_HANDLED; - } - - for @!plugs.grep(*.^can: 'irc-unhandled') -> $p { - my $res = $p.irc-unhandled(self, $e); - return unless $res === IRC_NOT_HANDLED; - } -} - -method notice (Str $who, Str $what) { - my $msg = "NOTICE $who :$what\n"; - $!debug and "{plug-name}$msg".put; - $!sock.print("$msg\n"); - self; -} - -method privmsg (Str $who, Str $what) { - my $msg = "PRIVMSG $who :$what\n"; - $!debug and "{plug-name}$msg".put; - $!sock.print("$msg\n"); - self; -} - -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 } - } - else { - self."$method"($where, $what); - } - 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 } - } - } - - CATCH { warn .backtrace } - } - - say "Closing connection"; - $!sock.close; - - # CATCH { warn .backtrace } - }); -} - -method ssay (Str:D $msg) { - $!debug and "{plug-name}$msg".put; - $!sock.print("$msg\n"); - self; -} - -#### HELPER SUBS - -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] "; -} diff --git a/lib/old-IRC/Client.pm6 b/lib/old-IRC/Client.pm6 new file mode 100644 index 0000000..700739c --- /dev/null +++ b/lib/old-IRC/Client.pm6 @@ -0,0 +1,174 @@ +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'; +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; + } + + # 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 $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; + } + } + + 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; + } + } + + 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; + } + } + + my $cmd = 'irc-' ~ $e<command>.lc; + for @!plugs.grep(*.^can: $cmd) -> $p { + my $res = $p."$cmd"(self, $e); + return unless $res === IRC_NOT_HANDLED; + } + + for @!plugs.grep(*.^can: 'irc-unhandled') -> $p { + my $res = $p.irc-unhandled(self, $e); + return unless $res === IRC_NOT_HANDLED; + } +} + +method notice (Str $who, Str $what) { + my $msg = "NOTICE $who :$what\n"; + $!debug and "{plug-name}$msg".put; + $!sock.print("$msg\n"); + self; +} + +method privmsg (Str $who, Str $what) { + my $msg = "PRIVMSG $who :$what\n"; + $!debug and "{plug-name}$msg".put; + $!sock.print("$msg\n"); + self; +} + +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 } + } + else { + self."$method"($where, $what); + } + 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 } + } + } + + CATCH { warn .backtrace } + } + + say "Closing connection"; + $!sock.close; + + # CATCH { warn .backtrace } + }); +} + +method ssay (Str:D $msg) { + $!debug and "{plug-name}$msg".put; + $!sock.print("$msg\n"); + self; +} + +#### HELPER SUBS + +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] "; +} diff --git a/lib/IRC/Client/Plugin.pm6 b/lib/old-IRC/Client/Plugin.pm6 index d73d7bd..d73d7bd 100644 --- a/lib/IRC/Client/Plugin.pm6 +++ b/lib/old-IRC/Client/Plugin.pm6 diff --git a/lib/IRC/Client/Plugin/Debugger.pm6 b/lib/old-IRC/Client/Plugin/Debugger.pm6 index 13b1461..13b1461 100644 --- a/lib/IRC/Client/Plugin/Debugger.pm6 +++ b/lib/old-IRC/Client/Plugin/Debugger.pm6 diff --git a/lib/IRC/Client/Plugin/PingPong.pm6 b/lib/old-IRC/Client/Plugin/PingPong.pm6 index 2651fd6..2651fd6 100644 --- a/lib/IRC/Client/Plugin/PingPong.pm6 +++ b/lib/old-IRC/Client/Plugin/PingPong.pm6 diff --git a/lib/IRC/Grammar.pm6 b/lib/old-IRC/Grammar.pm6 index c05322c..c05322c 100644 --- a/lib/IRC/Grammar.pm6 +++ b/lib/old-IRC/Grammar.pm6 diff --git a/lib/IRC/Grammar/Actions.pm6 b/lib/old-IRC/Grammar/Actions.pm6 index 234e392..234e392 100644 --- a/lib/IRC/Grammar/Actions.pm6 +++ b/lib/old-IRC/Grammar/Actions.pm6 diff --git a/lib/IRC/Parser.pm6 b/lib/old-IRC/Parser.pm6 index dda05e6..dda05e6 100644 --- a/lib/IRC/Parser.pm6 +++ b/lib/old-IRC/Parser.pm6 |