summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Build.PL28
-rw-r--r--Changes69
-rw-r--r--LICENSE379
-rw-r--r--MANIFEST25
-rw-r--r--META.json53
-rw-r--r--META.yml33
-rw-r--r--Makefile.PL18
-rw-r--r--README222
-rw-r--r--examples/SYNOPSIS.pl23
-rw-r--r--examples/client.pl69
-rw-r--r--examples/intro-01-helloworld.pl24
-rw-r--r--examples/intro-02-whatsyourname.pl43
-rw-r--r--lib/Net/Async/IRC.pm530
-rw-r--r--lib/Net/Async/IRC/Introduction.pod185
-rw-r--r--lib/Net/Async/IRC/Protocol.pm412
-rw-r--r--t/00use.t11
-rw-r--r--t/30client-connect.t107
-rw-r--r--t/31client-cap.t100
-rw-r--r--t/32client-encoding.t84
-rw-r--r--t/33client-nick.t100
-rw-r--r--t/40methods-basic.t40
-rw-r--r--t/50client-pingpong.t86
-rw-r--r--t/99pod.t11
-rw-r--r--t/privkey.pem15
-rw-r--r--t/server.pem17
25 files changed, 2684 insertions, 0 deletions
diff --git a/Build.PL b/Build.PL
new file mode 100644
index 0000000..f5be62c
--- /dev/null
+++ b/Build.PL
@@ -0,0 +1,28 @@
+use strict;
+use warnings;
+
+use Module::Build;
+
+my $build = Module::Build->new(
+ module_name => 'Net::Async::IRC',
+ requires => {
+ 'IO::Async::Loop' => '0.54', # ->connect Future
+ 'IO::Async::Stream' => '0.59', # Protocol deprecation
+ 'Protocol::IRC::Client' => '0.12',
+ 'Time::HiRes' => 0,
+ 'perl' => '5.010', # //, mro c3
+ },
+ test_requires => {
+ 'IO::Async::OS' => 0,
+ 'IO::Async::Test' => 0.14,
+ 'Test::Fatal' => 0,
+ 'Test::More' => '0.88', # done_testing
+ },
+ auto_configure_requires => 0, # Don't add M::B to configure_requires
+ license => 'perl',
+ create_makefile_pl => 'traditional',
+ create_license => 1,
+ create_readme => 1,
+);
+
+$build->create_build_script;
diff --git a/Changes b/Changes
new file mode 100644
index 0000000..6bd9f87
--- /dev/null
+++ b/Changes
@@ -0,0 +1,69 @@
+Revision history for Net-Async-IRC
+
+0.11 2017/03/04 11:14:11
+ [CHANGES]
+ * Protocol::IRC is now moved to its own distribution
+ * Expanded documentation, including the start of an introduction
+ tutorial
+
+0.10 2014/06/12 02:02:58
+ [CHANGES]
+ * Ignore received blank lines
+ * Allow capture of IRC parse errors as custom error handling
+
+ [BUGFIXES]
+ * Ensure that connection close during login counts as a failure for
+ pending login futures
+
+0.09 2014/01/21 12:21:41
+ [CHANGES]
+ * Removed now-dead NaIRC::Message subclass
+
+ [BUGFIXES]
+ * Ensure that handled gated commands don't appear as unhnandled to
+ the default 'on_message' handler
+ * Fix return EXPR and EXPR operator precedence (RT87260)
+ * Correctly set internal nick state when logging in after an
+ ERR_NICKINUSE error (RT90487)
+ * MSWin32 lacks a getpwnam() - use Win32::LoginName() instead
+
+0.08 2014/01/20 01:52:18
+ [CHANGES]
+ * Directly subclass IO::Async::Stream instead of IO::Async::Protocol
+ * Implement IRCv3.1 CAP negotiation
+ * Much improved handling of command/response gating
+ * Implement 'whois' message gate
+ * Futures-first documentation and testing
+
+0.07 CHANGES:
+ * Much splitting of non-async logic out of NaIRC into Protocol::IRC
+ tree
+ * Added name aliases for server numerics
+ * Dispatch message handler methods for numerics to names first,
+ before raw numbers
+
+0.06 CHANGES:
+ * Renamed Net::Async::IRC::Message to Protocol::IRC::Message, as the
+ first step of the split to Protocol::IRC
+ * Implement IRCv3 message tags
+
+0.05 CHANGES:
+ * Bugfix for ->connect() with service => undef
+ * Some more numerics
+ * New model for storing numerics in source code
+
+0.04 CHANGES:
+ * Split lower-level code into new Net::Async::IRC::Protocol module
+ * Use IO::Async::Protocol->connect from 0.34
+
+0.03 BUGFIXES:
+ * Fix failures due to IO::Async::Test or ::Loop no longer loading
+ IO::Async::Stream; load it explicitly where needed
+
+0.02 CHANGES:
+ * Some more numerics
+ * Capture named args from more WHOIS numerics
+ * base on IO::Async::Protocol::Stream
+
+0.01 First version, released on an unsuspecting world.
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8a8e8e5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,379 @@
+This software is copyright (c) 2017 by Paul Evans <leonerd@leonerd.org.uk>.
+
+This is free software; you can redistribute it and/or modify it under
+the same terms as the Perl 5 programming language system itself.
+
+Terms of the Perl programming language system itself
+
+a) the GNU General Public License as published by the Free
+ Software Foundation; either version 1, or (at your option) any
+ later version, or
+b) the "Artistic License"
+
+--- The GNU General Public License, Version 1, February 1989 ---
+
+This software is Copyright (c) 2017 by Paul Evans <leonerd@leonerd.org.uk>.
+
+This is free software, licensed under:
+
+ The GNU General Public License, Version 1, February 1989
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 1, February 1989
+
+ Copyright (C) 1989 Free Software Foundation, Inc.
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The license agreements of most software companies try to keep users
+at the mercy of those companies. By contrast, our General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. The
+General Public License applies to the Free Software Foundation's
+software and to any other program whose authors commit to using it.
+You can use it for your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Specifically, the General Public License is designed to make
+sure that you have the freedom to give away or sell copies of free
+software, that you receive source code or can get it if you want it,
+that you can change the software or use pieces of it in new free
+programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of a such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must tell them their rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any program or other work which
+contains a notice placed by the copyright holder saying it may be
+distributed under the terms of this General Public License. The
+"Program", below, refers to any such program or work, and a "work based
+on the Program" means either the Program or any work containing the
+Program or a portion of it, either verbatim or with modifications. Each
+licensee is addressed as "you".
+
+ 1. You may copy and distribute verbatim copies of the Program's source
+code as you receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice and
+disclaimer of warranty; keep intact all the notices that refer to this
+General Public License and to the absence of any warranty; and give any
+other recipients of the Program a copy of this General Public License
+along with the Program. You may charge a fee for the physical act of
+transferring a copy.
+
+ 2. You may modify your copy or copies of the Program or any portion of
+it, and copy and distribute such modifications under the terms of Paragraph
+1 above, provided that you also do the following:
+
+ a) cause the modified files to carry prominent notices stating that
+ you changed the files and the date of any change; and
+
+ b) cause the whole of any work that you distribute or publish, that
+ in whole or in part contains the Program or any part thereof, either
+ with or without modifications, to be licensed at no charge to all
+ third parties under the terms of this General Public License (except
+ that you may choose to grant warranty protection to some or all
+ third parties, at your option).
+
+ c) If the modified program normally reads commands interactively when
+ run, you must cause it, when started running for such interactive use
+ in the simplest and most usual way, to print or display an
+ announcement including an appropriate copyright notice and a notice
+ that there is no warranty (or else, saying that you provide a
+ warranty) and that users may redistribute the program under these
+ conditions, and telling the user how to view a copy of this General
+ Public License.
+
+ d) You may charge a fee for the physical act of transferring a
+ copy, and you may at your option offer warranty protection in
+ exchange for a fee.
+
+Mere aggregation of another independent work with the Program (or its
+derivative) on a volume of a storage or distribution medium does not bring
+the other work under the scope of these terms.
+
+ 3. You may copy and distribute the Program (or a portion or derivative of
+it, under Paragraph 2) in object code or executable form under the terms of
+Paragraphs 1 and 2 above provided that you also do one of the following:
+
+ a) accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of
+ Paragraphs 1 and 2 above; or,
+
+ b) accompany it with a written offer, valid for at least three
+ years, to give any third party free (except for a nominal charge
+ for the cost of distribution) a complete machine-readable copy of the
+ corresponding source code, to be distributed under the terms of
+ Paragraphs 1 and 2 above; or,
+
+ c) accompany it with the information you received as to where the
+ corresponding source code may be obtained. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form alone.)
+
+Source code for a work means the preferred form of the work for making
+modifications to it. For an executable file, complete source code means
+all the source code for all modules it contains; but, as a special
+exception, it need not include source code for modules which are standard
+libraries that accompany the operating system on which the executable
+file runs, or for standard header files or definitions files that
+accompany that operating system.
+
+ 4. You may not copy, modify, sublicense, distribute or transfer the
+Program except as expressly provided under this General Public License.
+Any attempt otherwise to copy, modify, sublicense, distribute or transfer
+the Program is void, and will automatically terminate your rights to use
+the Program under this License. However, parties who have received
+copies, or rights to use copies, from you under this General Public
+License will not have their licenses terminated so long as such parties
+remain in full compliance.
+
+ 5. By copying, distributing or modifying the Program (or any work based
+on the Program) you indicate your acceptance of this license to do so,
+and all its terms and conditions.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the original
+licensor to copy, distribute or modify the Program subject to these
+terms and conditions. You may not impose any further restrictions on the
+recipients' exercise of the rights granted herein.
+
+ 7. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of the license which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+the license, you may choose any version ever published by the Free Software
+Foundation.
+
+ 8. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ Appendix: How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to humanity, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these
+terms.
+
+ To do so, attach the following notices to the program. It is safest to
+attach them to the start of each source file to most effectively convey
+the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) 19yy <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 1, or (at your option)
+ any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) 19xx name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the
+appropriate parts of the General Public License. Of course, the
+commands you use may be called something other than `show w' and `show
+c'; they could even be mouse-clicks or menu items--whatever suits your
+program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ program `Gnomovision' (a program to direct compilers to make passes
+ at assemblers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
+--- The Artistic License 1.0 ---
+
+This software is Copyright (c) 2017 by Paul Evans <leonerd@leonerd.org.uk>.
+
+This is free software, licensed under:
+
+ The Artistic License 1.0
+
+The Artistic License
+
+Preamble
+
+The intent of this document is to state the conditions under which a Package
+may be copied, such that the Copyright Holder maintains some semblance of
+artistic control over the development of the package, while giving the users of
+the package the right to use and distribute the Package in a more-or-less
+customary fashion, plus the right to make reasonable modifications.
+
+Definitions:
+
+ - "Package" refers to the collection of files distributed by the Copyright
+ Holder, and derivatives of that collection of files created through
+ textual modification.
+ - "Standard Version" refers to such a Package if it has not been modified,
+ or has been modified in accordance with the wishes of the Copyright
+ Holder.
+ - "Copyright Holder" is whoever is named in the copyright or copyrights for
+ the package.
+ - "You" is you, if you're thinking about copying or distributing this Package.
+ - "Reasonable copying fee" is whatever you can justify on the basis of media
+ cost, duplication charges, time of people involved, and so on. (You will
+ not be required to justify it to the Copyright Holder, but only to the
+ computing community at large as a market that must bear the fee.)
+ - "Freely Available" means that no fee is charged for the item itself, though
+ there may be fees involved in handling the item. It also means that
+ recipients of the item may redistribute it under the same conditions they
+ received it.
+
+1. You may make and give away verbatim copies of the source form of the
+Standard Version of this Package without restriction, provided that you
+duplicate all of the original copyright notices and associated disclaimers.
+
+2. You may apply bug fixes, portability fixes and other modifications derived
+from the Public Domain or from the Copyright Holder. A Package modified in such
+a way shall still be considered the Standard Version.
+
+3. You may otherwise modify your copy of this Package in any way, provided that
+you insert a prominent notice in each changed file stating how and when you
+changed that file, and provided that you do at least ONE of the following:
+
+ a) place your modifications in the Public Domain or otherwise make them
+ Freely Available, such as by posting said modifications to Usenet or an
+ equivalent medium, or placing the modifications on a major archive site
+ such as ftp.uu.net, or by allowing the Copyright Holder to include your
+ modifications in the Standard Version of the Package.
+
+ b) use the modified Package only within your corporation or organization.
+
+ c) rename any non-standard executables so the names do not conflict with
+ standard executables, which must also be provided, and provide a separate
+ manual page for each non-standard executable that clearly documents how it
+ differs from the Standard Version.
+
+ d) make other distribution arrangements with the Copyright Holder.
+
+4. You may distribute the programs of this Package in object code or executable
+form, provided that you do at least ONE of the following:
+
+ a) distribute a Standard Version of the executables and library files,
+ together with instructions (in the manual page or equivalent) on where to
+ get the Standard Version.
+
+ b) accompany the distribution with the machine-readable source of the Package
+ with your modifications.
+
+ c) accompany any non-standard executables with their corresponding Standard
+ Version executables, giving the non-standard executables non-standard
+ names, and clearly documenting the differences in manual pages (or
+ equivalent), together with instructions on where to get the Standard
+ Version.
+
+ d) make other distribution arrangements with the Copyright Holder.
+
+5. You may charge a reasonable copying fee for any distribution of this
+Package. You may charge any fee you choose for support of this Package. You
+may not charge a fee for this Package itself. However, you may distribute this
+Package in aggregate with other (possibly commercial) programs as part of a
+larger (possibly commercial) software distribution provided that you do not
+advertise this Package as a product of your own.
+
+6. The scripts and library files supplied as input to or produced as output
+from the programs of this Package do not automatically fall under the copyright
+of this Package, but belong to whomever generated them, and may be sold
+commercially, and may be aggregated with this Package.
+
+7. C or perl subroutines supplied by you and linked into this Package shall not
+be considered part of this Package.
+
+8. The name of the Copyright Holder may not be used to endorse or promote
+products derived from this software without specific prior written permission.
+
+9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
+MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+
+The End
+
diff --git a/MANIFEST b/MANIFEST
new file mode 100644
index 0000000..79703e9
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,25 @@
+Build.PL
+Changes
+examples/client.pl
+examples/intro-01-helloworld.pl
+examples/intro-02-whatsyourname.pl
+examples/SYNOPSIS.pl
+lib/Net/Async/IRC.pm
+lib/Net/Async/IRC/Introduction.pod
+lib/Net/Async/IRC/Protocol.pm
+LICENSE
+Makefile.PL
+MANIFEST This list of files
+META.json
+META.yml
+README
+t/00use.t
+t/30client-connect.t
+t/31client-cap.t
+t/32client-encoding.t
+t/33client-nick.t
+t/40methods-basic.t
+t/50client-pingpong.t
+t/99pod.t
+t/privkey.pem
+t/server.pem
diff --git a/META.json b/META.json
new file mode 100644
index 0000000..f7f672a
--- /dev/null
+++ b/META.json
@@ -0,0 +1,53 @@
+{
+ "abstract" : "use IRC with C<IO::Async>",
+ "author" : [
+ "Paul Evans <leonerd@leonerd.org.uk>"
+ ],
+ "dynamic_config" : 1,
+ "generated_by" : "Module::Build version 0.422",
+ "license" : [
+ "perl_5"
+ ],
+ "meta-spec" : {
+ "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
+ "version" : "2"
+ },
+ "name" : "Net-Async-IRC",
+ "prereqs" : {
+ "runtime" : {
+ "requires" : {
+ "IO::Async::Loop" : "0.54",
+ "IO::Async::Stream" : "0.59",
+ "Protocol::IRC::Client" : "0.12",
+ "Time::HiRes" : "0",
+ "perl" : "5.010"
+ }
+ },
+ "test" : {
+ "requires" : {
+ "IO::Async::OS" : "0",
+ "IO::Async::Test" : "0.14",
+ "Test::Fatal" : "0",
+ "Test::More" : "0.88"
+ }
+ }
+ },
+ "provides" : {
+ "Net::Async::IRC" : {
+ "file" : "lib/Net/Async/IRC.pm",
+ "version" : "0.11"
+ },
+ "Net::Async::IRC::Protocol" : {
+ "file" : "lib/Net/Async/IRC/Protocol.pm",
+ "version" : "0.11"
+ }
+ },
+ "release_status" : "stable",
+ "resources" : {
+ "license" : [
+ "http://dev.perl.org/licenses/"
+ ]
+ },
+ "version" : "0.11",
+ "x_serialization_backend" : "JSON::PP version 2.27400"
+}
diff --git a/META.yml b/META.yml
new file mode 100644
index 0000000..eaa2d58
--- /dev/null
+++ b/META.yml
@@ -0,0 +1,33 @@
+---
+abstract: 'use IRC with C<IO::Async>'
+author:
+ - 'Paul Evans <leonerd@leonerd.org.uk>'
+build_requires:
+ IO::Async::OS: '0'
+ IO::Async::Test: '0.14'
+ Test::Fatal: '0'
+ Test::More: '0.88'
+dynamic_config: 1
+generated_by: 'Module::Build version 0.422, CPAN::Meta::Converter version 2.150005'
+license: perl
+meta-spec:
+ url: http://module-build.sourceforge.net/META-spec-v1.4.html
+ version: '1.4'
+name: Net-Async-IRC
+provides:
+ Net::Async::IRC:
+ file: lib/Net/Async/IRC.pm
+ version: '0.11'
+ Net::Async::IRC::Protocol:
+ file: lib/Net/Async/IRC/Protocol.pm
+ version: '0.11'
+requires:
+ IO::Async::Loop: '0.54'
+ IO::Async::Stream: '0.59'
+ Protocol::IRC::Client: '0.12'
+ Time::HiRes: '0'
+ perl: '5.010'
+resources:
+ license: http://dev.perl.org/licenses/
+version: '0.11'
+x_serialization_backend: 'CPAN::Meta::YAML version 0.018'
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644
index 0000000..6feaa8d
--- /dev/null
+++ b/Makefile.PL
@@ -0,0 +1,18 @@
+# Note: this file was auto-generated by Module::Build::Compat version 0.4220
+require 5.010;
+use ExtUtils::MakeMaker;
+WriteMakefile
+(
+ 'NAME' => 'Net::Async::IRC',
+ 'VERSION_FROM' => 'lib/Net/Async/IRC.pm',
+ 'PREREQ_PM' => {
+ 'IO::Async::Loop' => '0.54',
+ 'IO::Async::Stream' => '0.59',
+ 'Protocol::IRC::Client' => '0.12',
+ 'Time::HiRes' => 0
+ },
+ 'INSTALLDIRS' => 'site',
+ 'EXE_FILES' => [],
+ 'PL_FILES' => {}
+)
+;
diff --git a/README b/README
new file mode 100644
index 0000000..ac9ede8
--- /dev/null
+++ b/README
@@ -0,0 +1,222 @@
+NAME
+
+ Net::Async::IRC - use IRC with IO::Async
+
+SYNOPSIS
+
+ use IO::Async::Loop;
+ use Net::Async::IRC;
+
+ my $loop = IO::Async::Loop->new;
+
+ my $irc = Net::Async::IRC->new(
+ on_message_text => sub {
+ my ( $self, $message, $hints ) = @_;
+
+ print "$hints->{prefix_name} says: $hints->{text}\n";
+ },
+ );
+
+ $loop->add( $irc );
+
+ $irc->login(
+ nick => "MyName",
+ host => "irc.example.org",
+ )->get;
+
+ $irc->do_PRIVMSG( target => "YourName", text => "Hello world!" );
+
+ $loop->run;
+
+DESCRIPTION
+
+ This object class implements an asynchronous IRC client, for use in
+ programs based on IO::Async.
+
+ Most of the actual IRC message handling behaviour is implemented by the
+ parent class Net::Async::IRC::Protocol.
+
+ Most of the behaviour related to being an IRC client is implemented by
+ the parent class Protocol::IRC::Client.
+
+ The following documentation may make mention of these above two parent
+ classes; the reader should make reference to them when required.
+
+PARAMETERS
+
+ The following named parameters may be passed to new or configure:
+
+ nick => STRING
+
+ user => STRING
+
+ realname => STRING
+
+ Connection details. See also connect, login.
+
+ If user is not supplied, it will default to either $ENV{LOGNAME} or
+ the current user's name as supplied by getpwuid() or
+ Win32::LoginName().
+
+ If unconnected, changing these properties will set the default values
+ to use when logging in.
+
+ If logged in, changing the nick property is equivalent to calling
+ change_nick. Changing the other properties will not take effect until
+ the next login.
+
+ use_caps => ARRAY of STRING
+
+ Attempts to negotiate IRC v3.1 CAP at connect time. The array gives
+ the names of capabilities which will be requested, if the server
+ supports them.
+
+METHODS
+
+ The following methods documented with a trailing call to ->get return
+ Future instances.
+
+ connect
+
+ $irc = $irc->connect( %args )->get
+
+ Connects to the IRC server. This method does not perform the complete
+ IRC login sequence; for that see instead the login method. The returned
+ Future will yield the $irc instance itself, to make chaining easier.
+
+ host => STRING
+
+ Hostname of the IRC server.
+
+ service => STRING or NUMBER
+
+ Optional. Port number or service name of the IRC server. Defaults to
+ 6667.
+
+ Any other arguments are passed into the underlying IO::Async::Loop
+ connect method.
+
+ $irc->connect( %args )
+
+ The following additional arguments are used to provide continuations
+ when not returning a Future.
+
+ on_connected => CODE
+
+ Continuation to invoke once the connection has been established.
+ Usually used by the login method to perform the actual login
+ sequence.
+
+ $on_connected->( $irc )
+
+ on_error => CODE
+
+ Continuation to invoke in the case of an error preventing the
+ connection from taking place.
+
+ $on_error->( $errormsg )
+
+ login
+
+ $irc = $irc->login( %args )->get
+
+ Logs in to the IRC network, connecting first using the connect method
+ if required. Takes the following named arguments:
+
+ nick => STRING
+
+ user => STRING
+
+ realname => STRING
+
+ IRC connection details. Defaults can be set with the new or configure
+ methods.
+
+ pass => STRING
+
+ Server password to connect with.
+
+ Any other arguments that are passed, are forwarded to the connect
+ method if it is required; i.e. if login is invoked when not yet
+ connected to the server.
+
+ $irc->login( %args )
+
+ The following additional arguments are used to provide continuations
+ when not returning a Future.
+
+ on_login => CODE
+
+ A continuation to invoke once login is successful.
+
+ $on_login->( $irc )
+
+ change_nick
+
+ $irc->change_nick( $newnick )
+
+ Requests to change the nick. If unconnected, the change happens
+ immediately to the stored defaults. If logged in, sends a NICK command
+ to the server, which may suceed or fail at a later point.
+
+IRC v3.1 CAPABILITIES
+
+ The following methods relate to IRC v3.1 capabilities negotiations.
+
+ caps_supported
+
+ $caps = $irc->caps_supported
+
+ Returns a HASH whose keys give the capabilities listed by the server as
+ supported in its CAP LS response. If the server ignored the CAP
+ negotiation then this method returns undef.
+
+ cap_supported
+
+ $supported = $irc->cap_supported( $cap )
+
+ Returns a boolean indicating if the server supports the named
+ capability.
+
+ caps_enabled
+
+ $caps = $irc->caps_enabled
+
+ Returns a HASH whose keys give the capabilities successfully enabled by
+ the server as part of the CAP REQ login sequence. If the server ignored
+ the CAP negotiation then this method returns undef.
+
+ cap_enabled
+
+ $enabled = $irc->cap_enabled( $cap )
+
+ Returns a boolean indicating if the client successfully enabled the
+ named capability.
+
+MESSAGE-WRAPPING METHODS
+
+ The following methods are all inherited from Protocol::IRC::Client but
+ are mentioned again for convenient. For further details see the
+ documentation in the parent module.
+
+ In particular, each method returns a Future instance.
+
+ do_PRIVMSG
+
+ do_NOTICE
+
+ $irc->do_PRIVMSG( target => $target, text => $text )->get
+
+ $irc->do_NOTICE( target => $target, text => $text )->get
+
+ Sends a PRIVMSG or NOITICE command.
+
+SEE ALSO
+
+ * http://tools.ietf.org/html/rfc2812 - Internet Relay Chat: Client
+ Protocol
+
+AUTHOR
+
+ Paul Evans <leonerd@leonerd.org.uk>
+
diff --git a/examples/SYNOPSIS.pl b/examples/SYNOPSIS.pl
new file mode 100644
index 0000000..6349599
--- /dev/null
+++ b/examples/SYNOPSIS.pl
@@ -0,0 +1,23 @@
+use IO::Async::Loop;
+use Net::Async::IRC;
+
+my $loop = IO::Async::Loop->new;
+
+my $irc = Net::Async::IRC->new(
+ on_message_text => sub {
+ my ( $self, $message, $hints ) = @_;
+
+ print "$hints->{prefix_name} says: $hints->{text}\n";
+ },
+);
+
+$loop->add( $irc );
+
+$irc->login(
+ nick => "MyName",
+ host => "irc.example.org",
+)->get;
+
+$irc->do_PRIVMSG( target => "YourName", text => "Hello world!" );
+
+$loop->run;
diff --git a/examples/client.pl b/examples/client.pl
new file mode 100644
index 0000000..3296713
--- /dev/null
+++ b/examples/client.pl
@@ -0,0 +1,69 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Data::Dump 'pp';
+use Getopt::Long;
+
+use Future::Utils qw( repeat );
+use IO::Async::Loop;
+use Net::Async::IRC;
+
+GetOptions(
+ 'server|s=s' => \my $SERVER,
+ 'nick|n=s' => \my $NICK,
+ 'port|p=i' => \my $PORT,
+ 'SSL|S' => \my $SSL,
+) or exit 1;
+
+require IO::Async::SSL if $SSL;
+
+my $loop = IO::Async::Loop->new;
+
+my $irc = Net::Async::IRC->new(
+ on_message => sub {
+ my ( $self, $command, $message, $hints ) = @_;
+ return if $hints->{handled};
+
+ printf "<<%s>>: %s\n", $command, join( " ", $message->args );
+ print "| $_\n" for split m/\n/, pp( $hints );
+
+ return 1;
+ },
+);
+$loop->add( $irc );
+
+$PORT //= ( $SSL ? 6697 : 6667 );
+
+$irc->connect(
+ host => $SERVER,
+ service => $PORT,
+ ( $SSL ?
+ ( extensions => ['SSL'],
+ SSL_verify_mode => 0 ) :
+ () ),
+)->get;
+
+print "Connected...\n";
+
+$irc->login(
+ nick => $NICK,
+)->get;
+
+print "Now logged in...\n";
+
+my $stdin = IO::Async::Stream->new_for_stdin( on_read => sub {} );
+$loop->add( $stdin );
+
+my $eof;
+( repeat {
+ $stdin->read_until( "\n" )->on_done( sub {
+ ( my $line, $eof ) = @_;
+ return if $eof;
+
+ chomp $line;
+ my $message = Protocol::IRC::Message->new_from_line( $line );
+ $irc->send_message( $message );
+ });
+} while => sub { !$_[0]->failure and !$eof } )->get;
diff --git a/examples/intro-01-helloworld.pl b/examples/intro-01-helloworld.pl
new file mode 100644
index 0000000..85dd867
--- /dev/null
+++ b/examples/intro-01-helloworld.pl
@@ -0,0 +1,24 @@
+use strict;
+use warnings;
+
+use IO::Async::Loop;
+use Net::Async::IRC;
+
+my $loop = IO::Async::Loop->new;
+
+my $irc = Net::Async::IRC->new;
+$loop->add( $irc );
+
+my $SERVER = "irc.example.net";
+my $NICK = "MyNick";
+my $TARGET = "TargetNick";
+
+$irc->login(
+ host => $SERVER,
+ nick => $NICK,
+)->then( sub {
+ $irc->do_PRIVMSG(
+ target => $TARGET,
+ text => "Hello, World"
+ );
+})->get;
diff --git a/examples/intro-02-whatsyourname.pl b/examples/intro-02-whatsyourname.pl
new file mode 100644
index 0000000..fc1a4b3
--- /dev/null
+++ b/examples/intro-02-whatsyourname.pl
@@ -0,0 +1,43 @@
+use strict;
+use warnings;
+
+use IO::Async::Loop;
+use Net::Async::IRC;
+
+my $loop = IO::Async::Loop->new;
+
+my $irc = Net::Async::IRC->new;
+$loop->add( $irc );
+
+my $SERVER = "irc.example.net";
+my $NICK = "MyNick";
+my $TARGET = "TargetNick";
+
+$irc->login(
+ host => $SERVER,
+ nick => $NICK,
+)->get;
+
+my $target_folded = $irc->casefold_name( $TARGET );
+
+$irc->configure(
+ on_message_text => sub {
+ my ( undef, $message, $hints ) = @_;
+ return unless $hints->{prefix_nick_folded} eq $target_folded;
+
+ print "The user said: $hints->{text}\n";
+ },
+ on_message_ctcp_ACTION => sub {
+ my ( undef, $message, $hints ) = @_;
+ return unless $hints->{prefix_nick_folded} eq $target_folded;
+
+ print "The user acted: $hints->{ctcp_args}\n";
+ },
+);
+
+$irc->do_PRIVMSG(
+ target => $TARGET,
+ text => "Hello, what's your name?"
+);
+
+$loop->run;
diff --git a/lib/Net/Async/IRC.pm b/lib/Net/Async/IRC.pm
new file mode 100644
index 0000000..8ae539f
--- /dev/null
+++ b/lib/Net/Async/IRC.pm
@@ -0,0 +1,530 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2008-2017 -- leonerd@leonerd.org.uk
+
+package Net::Async::IRC;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.11';
+
+# We need to use C3 MRO to make the ->isupport etc.. methods work properly
+use mro 'c3';
+use base qw( Net::Async::IRC::Protocol Protocol::IRC::Client );
+
+use Carp;
+
+use Socket qw( SOCK_STREAM );
+
+use constant HAVE_MSWIN32 => ( $^O eq "MSWin32" );
+
+=head1 NAME
+
+C<Net::Async::IRC> - use IRC with C<IO::Async>
+
+=head1 SYNOPSIS
+
+ use IO::Async::Loop;
+ use Net::Async::IRC;
+
+ my $loop = IO::Async::Loop->new;
+
+ my $irc = Net::Async::IRC->new(
+ on_message_text => sub {
+ my ( $self, $message, $hints ) = @_;
+
+ print "$hints->{prefix_name} says: $hints->{text}\n";
+ },
+ );
+
+ $loop->add( $irc );
+
+ $irc->login(
+ nick => "MyName",
+ host => "irc.example.org",
+ )->get;
+
+ $irc->do_PRIVMSG( target => "YourName", text => "Hello world!" );
+
+ $loop->run;
+
+=head1 DESCRIPTION
+
+This object class implements an asynchronous IRC client, for use in programs
+based on L<IO::Async>.
+
+Most of the actual IRC message handling behaviour is implemented by the parent
+class L<Net::Async::IRC::Protocol>.
+
+Most of the behaviour related to being an IRC client is implemented by the
+parent class L<Protocol::IRC::Client>.
+
+The following documentation may make mention of these above two parent
+classes; the reader should make reference to them when required.
+
+=cut
+
+sub new
+{
+ my $class = shift;
+ my %args = @_;
+
+ my $on_closed = delete $args{on_closed};
+
+ return $class->SUPER::new(
+ %args,
+
+ on_closed => sub {
+ my $self = shift;
+
+ if( $self->{on_login_f} ) {
+ $_->fail( "Closed" ) for @{ $self->{on_login_f} };
+ undef $self->{on_login_f};
+ }
+
+ $on_closed->( $self ) if $on_closed;
+ },
+ );
+}
+
+sub _init
+{
+ my $self = shift;
+ $self->SUPER::_init( @_ );
+
+ $self->{user} = $ENV{LOGNAME} ||
+ ( HAVE_MSWIN32 ? Win32::LoginName() : getpwuid($>) );
+
+ $self->{realname} = "Net::Async::IRC client $VERSION";
+}
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=over 8
+
+=item nick => STRING
+
+=item user => STRING
+
+=item realname => STRING
+
+Connection details. See also C<connect>, C<login>.
+
+If C<user> is not supplied, it will default to either C<$ENV{LOGNAME}> or the
+current user's name as supplied by C<getpwuid()> or C<Win32::LoginName()>.
+
+If unconnected, changing these properties will set the default values to use
+when logging in.
+
+If logged in, changing the C<nick> property is equivalent to calling
+C<change_nick>. Changing the other properties will not take effect until the
+next login.
+
+=item use_caps => ARRAY of STRING
+
+Attempts to negotiate IRC v3.1 CAP at connect time. The array gives the names
+of capabilities which will be requested, if the server supports them.
+
+=back
+
+=cut
+
+sub configure
+{
+ my $self = shift;
+ my %args = @_;
+
+ for (qw( user realname use_caps )) {
+ $self->{$_} = delete $args{$_} if exists $args{$_};
+ }
+
+ if( exists $args{nick} ) {
+ $self->_set_nick( delete $args{nick} );
+ }
+
+ $self->SUPER::configure( %args );
+}
+
+=head1 METHODS
+
+The following methods documented with a trailing call to C<< ->get >> return
+L<Future> instances.
+
+=cut
+
+=head2 connect
+
+ $irc = $irc->connect( %args )->get
+
+Connects to the IRC server. This method does not perform the complete IRC
+login sequence; for that see instead the C<login> method. The returned
+L<Future> will yield the C<$irc> instance itself, to make chaining easier.
+
+=over 8
+
+=item host => STRING
+
+Hostname of the IRC server.
+
+=item service => STRING or NUMBER
+
+Optional. Port number or service name of the IRC server. Defaults to 6667.
+
+=back
+
+Any other arguments are passed into the underlying C<IO::Async::Loop>
+C<connect> method.
+
+ $irc->connect( %args )
+
+The following additional arguments are used to provide continuations when not
+returning a Future.
+
+=over 8
+
+=item on_connected => CODE
+
+Continuation to invoke once the connection has been established. Usually used
+by the C<login> method to perform the actual login sequence.
+
+ $on_connected->( $irc )
+
+=item on_error => CODE
+
+Continuation to invoke in the case of an error preventing the connection from
+taking place.
+
+ $on_error->( $errormsg )
+
+=back
+
+=cut
+
+# TODO: Most of this needs to be moved into an abstract Net::Async::Connection role
+sub connect
+{
+ my $self = shift;
+ my %args = @_;
+
+ # Largely for unit testing
+ return $self->{connect_f} ||= Future->new->done( $self ) if
+ $self->read_handle;
+
+ my $on_error = delete $args{on_error};
+
+ $args{service} ||= "6667";
+
+ return $self->{connect_f} ||= $self->SUPER::connect(
+ %args,
+
+ on_resolve_error => sub {
+ my ( $msg ) = @_;
+ chomp $msg;
+
+ if( $args{on_resolve_error} ) {
+ $args{on_resolve_error}->( $msg );
+ }
+ elsif( $on_error ) {
+ $on_error->( "Cannot resolve - $msg" );
+ }
+ },
+
+ on_connect_error => sub {
+ if( $args{on_connect_error} ) {
+ $args{on_connect_error}->( @_ );
+ }
+ elsif( $on_error ) {
+ $on_error->( "Cannot connect" );
+ }
+ },
+ )->on_fail( sub { undef $self->{connect_f} } );
+}
+
+=head2 login
+
+ $irc = $irc->login( %args )->get
+
+Logs in to the IRC network, connecting first using the C<connect> method if
+required. Takes the following named arguments:
+
+=over 8
+
+=item nick => STRING
+
+=item user => STRING
+
+=item realname => STRING
+
+IRC connection details. Defaults can be set with the C<new> or C<configure>
+methods.
+
+=item pass => STRING
+
+Server password to connect with.
+
+=back
+
+Any other arguments that are passed, are forwarded to the C<connect> method if
+it is required; i.e. if C<login> is invoked when not yet connected to the
+server.
+
+ $irc->login( %args )
+
+The following additional arguments are used to provide continuations when not
+returning a Future.
+
+=over 8
+
+=item on_login => CODE
+
+A continuation to invoke once login is successful.
+
+ $on_login->( $irc )
+
+=back
+
+=cut
+
+sub login
+{
+ my $self = shift;
+ my %args = @_;
+
+ my $nick = delete $args{nick} || $self->{nick} or croak "Need a login nick";
+ my $user = delete $args{user} || $self->{user} or croak "Need a login user";
+ my $realname = delete $args{realname} || $self->{realname};
+ my $pass = delete $args{pass};
+
+ if( !defined $self->{nick} ) {
+ $self->_set_nick( $nick );
+ }
+
+ my $on_login = delete $args{on_login};
+ !defined $on_login or ref $on_login eq "CODE" or
+ croak "Expected 'on_login' to be a CODE reference";
+
+ return $self->{login_f} ||= $self->connect( %args )->then( sub {
+ $self->send_message( "CAP", undef, "LS" ) if $self->{use_caps};
+
+ $self->send_message( "PASS", undef, $pass ) if defined $pass;
+ $self->send_message( "USER", undef, $user, "0", "*", $realname );
+ $self->send_message( "NICK", undef, $nick );
+
+ my $f = $self->loop->new_future;
+
+ push @{ $self->{on_login_f} }, $f;
+ $f->on_done( $on_login ) if $on_login;
+
+ return $f;
+ })->on_fail( sub { undef $self->{login_f} } );
+}
+
+=head2 change_nick
+
+ $irc->change_nick( $newnick )
+
+Requests to change the nick. If unconnected, the change happens immediately
+to the stored defaults. If logged in, sends a C<NICK> command to the server,
+which may suceed or fail at a later point.
+
+=cut
+
+sub change_nick
+{
+ my $self = shift;
+ my ( $newnick ) = @_;
+
+ if( !$self->is_connected ) {
+ $self->_set_nick( $newnick );
+ }
+ else {
+ $self->send_message( "NICK", undef, $newnick );
+ }
+}
+
+############################
+# Message handling methods #
+############################
+
+=head1 IRC v3.1 CAPABILITIES
+
+The following methods relate to IRC v3.1 capabilities negotiations.
+
+=cut
+
+sub on_message_cap_LS
+{
+ my $self = shift;
+ my ( $message, $hints ) = @_;
+
+ my $supported = $self->{caps_supported} = $hints->{caps};
+
+ my @request = grep { $supported->{$_} } @{$self->{use_caps}};
+
+ if( @request ) {
+ $self->{caps_enabled} = { map { $_ => undef } @request };
+ $self->send_message( "CAP", undef, "REQ", join( " ", @request ) );
+ }
+ else {
+ $self->send_message( "CAP", undef, "END" );
+ }
+
+ return 1;
+}
+
+*on_message_cap_ACK = *on_message_cap_NAK = \&_on_message_cap_reply;
+sub _on_message_cap_reply
+{
+ my $self = shift;
+ my ( $message, $hints ) = @_;
+ my $ack = $hints->{verb} eq "ACK";
+
+ $self->{caps_enabled}{$_} = $ack for keys %{ $hints->{caps} };
+
+ # Are any outstanding
+ !defined and return 1 for values %{ $self->{caps_enabled} };
+
+ $self->send_message( "CAP", undef, "END" );
+ return 1;
+}
+
+=head2 caps_supported
+
+ $caps = $irc->caps_supported
+
+Returns a HASH whose keys give the capabilities listed by the server as
+supported in its C<CAP LS> response. If the server ignored the C<CAP>
+negotiation then this method returns C<undef>.
+
+=cut
+
+sub caps_supported
+{
+ my $self = shift;
+ return $self->{caps_supported};
+}
+
+=head2 cap_supported
+
+ $supported = $irc->cap_supported( $cap )
+
+Returns a boolean indicating if the server supports the named capability.
+
+=cut
+
+sub cap_supported
+{
+ my $self = shift;
+ my ( $cap ) = @_;
+ return !!$self->{caps_supported}{$cap};
+}
+
+=head2 caps_enabled
+
+ $caps = $irc->caps_enabled
+
+Returns a HASH whose keys give the capabilities successfully enabled by the
+server as part of the C<CAP REQ> login sequence. If the server ignored the
+C<CAP> negotiation then this method returns C<undef>.
+
+=cut
+
+sub caps_enabled
+{
+ my $self = shift;
+ return $self->{caps_enabled};
+}
+
+=head2 cap_enabled
+
+ $enabled = $irc->cap_enabled( $cap )
+
+Returns a boolean indicating if the client successfully enabled the named
+capability.
+
+=cut
+
+sub cap_enabled
+{
+ my $self = shift;
+ my ( $cap ) = @_;
+ return !!$self->{caps_enabled}{$cap};
+}
+
+sub on_message_NICK
+{
+ my $self = shift;
+ my ( $message, $hints ) = @_;
+
+ if( $hints->{prefix_is_me} ) {
+ $self->_set_nick( $hints->{new_nick} );
+ return 1;
+ }
+
+ return 0;
+}
+
+sub on_message_RPL_WELCOME
+{
+ my $self = shift;
+ my ( $message ) = @_;
+
+ # set our nick to be what the server logged us in as
+ $self->_set_nick( $message->{args}[0] );
+
+ if( $self->{on_login_f} and @{ $self->{on_login_f} } ) {
+ my @futures = @{ $self->{on_login_f} };
+ undef $self->{on_login_f};
+
+ foreach my $f ( @futures ) {
+ $f->done( $self );
+ }
+ }
+
+ # Don't eat it
+ return 0;
+}
+
+=head1 MESSAGE-WRAPPING METHODS
+
+The following methods are all inherited from L<Protocol::IRC::Client> but are
+mentioned again for convenient. For further details see the documentation in
+the parent module.
+
+In particular, each method returns a L<Future> instance.
+
+=cut
+
+=head2 do_PRIVMSG
+
+=head2 do_NOTICE
+
+ $irc->do_PRIVMSG( target => $target, text => $text )->get
+
+ $irc->do_NOTICE( target => $target, text => $text )->get
+
+Sends a C<PRIVMSG> or C<NOITICE> command.
+
+=cut
+
+=head1 SEE ALSO
+
+=over 4
+
+=item *
+
+L<http://tools.ietf.org/html/rfc2812> - Internet Relay Chat: Client Protocol
+
+=back
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/lib/Net/Async/IRC/Introduction.pod b/lib/Net/Async/IRC/Introduction.pod
new file mode 100644
index 0000000..cab5776
--- /dev/null
+++ b/lib/Net/Async/IRC/Introduction.pod
@@ -0,0 +1,185 @@
+=head1 NAME
+
+Net::Async::IRC::Introduction - an introduction
+
+=head1 INTRODUCTION
+
+=head2 Hello, World
+
+This first example is the "hello world" of IRC; a script that connects to the
+server and immediately sends a hello message to a preconfigured user.
+
+This program starts with the usual boilerplate for any L<IO::Async>-based
+program; namely loading the required modules and creating a containing
+L<IO::Async::Loop> instance. It then constructs the actual L<Net::Async::IRC>
+object and adds it to this containing loop. As these actions are standard to
+every program, they won't be repeated in later examples; just presumed to have
+already taken place:
+
+ use strict;
+ use warnings;
+
+ use IO::Async::Loop;
+ use Net::Async::IRC;
+
+ my $loop = IO::Async::Loop->new;
+
+ my $irc = Net::Async::IRC->new;
+ $loop->add( $irc );
+
+Now this is created, we can move on to the specifics of this example. As it's
+a tiny example script, we'll just hard-code the parameters for the message. A
+larger program of course would read these from somewhere better - a config
+file, commandline arguments, etc...
+
+ my $SERVER = "irc.example.net";
+ my $NICK = "MyNick";
+ my $TARGET = "TargetNick";
+
+Finally we can connect to the IRC server and send the message:
+
+ $irc->login(
+ host => $SERVER,
+ nick => $NICK,
+ )->then( sub {
+ $irc->do_PRIVMSG(
+ target => $TARGET,
+ text => "Hello, World"
+ );
+ })->get;
+
+The program calls L<Net::Async::IRC/login>, which connects the client to the
+given IRC server and logs in as the given nick. This method returns a
+L<Future> instance to represent its eventual completion, giving us an easy way
+to sequence further code after it. After login is complete, the next task is
+simply to send the message. This is done with the C<PRIVMSG> IRC command as
+wrapped by L<Net::Async::IRC/do_PRIVMSG>. This takes taking the message target
+name and text string.
+
+The trailing call to L<Future/get> makes the script stop here waiting for this
+chain of futures to actually complete. Without this, the returned future would
+simply be lost (as the L<Future/then> method appears in void context), and the
+second stage of code within it would probably never get called. In later
+examples we'll see other techniques, but for now every constructed future will
+simply be forced by calling C<get> on it. If either of these stages fails, it
+will cause the C<get> call to throw an exception instead.
+
+Once this is sent, the script terminates, closing its connection to the server.
+
+=head2 RECEIVING MESSAGES
+
+As a second example, lets now consider also how we handle messages that arrive
+from IRC.
+
+ $irc->configure(
+ on_message_PRIVMSG => sub {
+ my ( $irc, $message, $hints ) = @_;
+ return unless $hints->{prefix_nick_folded} eq
+ $irc->casefold_name( $TARGET );
+
+ print "The user said: $hints->{text}\n";
+ }
+ );
+
+ $irc->login(
+ host => $SERVER,
+ nick => $NICK,
+ )->then( sub {
+ $irc->do_PRIVMSG(
+ target => $TARGET,
+ text => "Hello, what's your name?"
+ );
+ })->get;
+
+ $loop->run;
+
+Here we have used the C<configure> method to attach an event handler to the
+C<on_message_PRIVMSG> event. This handler code ignores any messages except
+from the user we are interested in, and simply prints the contents of those we
+are interested in to the terminal.
+
+Having established this event handler, we can then log in and send a message
+to the target user, similar to the first example. Instead of stopping the
+script entirely afterwards, we need to ensure that the program keeps running
+after this initial start so it can continue to receive messages. To do that we
+enter the main L<IO::Async::Loop/run> method, which will wait indefinitely,
+processing any events that are received.
+
+=head2 Case-folded Names
+
+The use of the "folded" strings ensures that this code can correctly cope with
+any odd case-folding rules the IRC server has. By comparison, both of the
+following lines are incorrect, and may cause missed messages on some servers:
+
+ return unless $hints->{prefix_name} eq $TARGET; # don't do this
+
+ return unless lc $hints->{prefix_name} eq lc $TARGET; # don't do this
+
+The first does not case-fold the string at all, so will fail in the case of
+C<User> vs C<user>. The second attempts to solve this, but does not take
+account of the odd case-folding logic most IRC servers have, in which the
+characters C<[\]> are "uppercase" versions of C<{|}>. The
+L<Protocol::IRC/casefold_name> method is provided as a server-aware
+alternative to C<lc()>, which handles this. A correct implementation could be
+written:
+
+ return unless $irc->casefold_name( $hints->{prefix_name} ) eq
+ $irc->casefold_name( $TARGET );
+
+However, since this is a very common pattern, the hints hash conveniently
+supplies already-folded strings for any name or nick fields it finds.
+Furthermore, as the case folded version of the target name won't change after
+startup, we could store that initially to save re-calculating it at every
+event:
+
+ $irc->login(
+ host => $SERVER,
+ nick => $NICK,
+ )->get;
+
+ my $target_folded = $irc->casefold_name( $TARGET );
+
+ $irc->configure(
+ on_message_PRIVMSG => sub {
+ my ( undef, $message, $hints ) = @_;
+ return unless $hints->{prefix_nick_folded} eq $target_folded;
+
+ print "The user said: $hints->{text}\n";
+ }
+ );
+
+=head2 C<PRIVMSG> vs C<text> and CTCPs
+
+This example has used the basic C<on_message_PRIVMSG> event. A better version
+would be to use C<on_message_text> instead. This is a synthesized event
+created on receipt of either C<PRIVMSG> or C<NOTICE>, and itself handles
+details like C<CTCP> parsing, freeing the user code from having to handle it).
+For example, the plain C<PRIVMSG> event will get quite confused by an incoming
+C<CTCP ACTION>, such as is created by most IRC clients by the C</me> command.
+Instead, we can handle that by attaching a handler specifically for
+C<CTCP ACTION>:
+
+ $irc->configure(
+ on_message_text => sub {
+ my ( undef, $message, $hints ) = @_;
+ return unless $hints->{prefix_nick_folded} eq $target_folded;
+
+ print "The user said: $hints->{text}\n";
+ },
+ on_message_ctcp_ACTION => sub {
+ my ( undef, $message, $hints ) = @_;
+ return unless $hints->{prefix_nick_folded} eq $target_folded;
+
+ print "The user acted: $hints->{ctcp_args}\n";
+ },
+ );
+
+This second handlers is invoked on receipt of a C<PRIVMSG> containing a
+C<CTCP ACTION>. The first is only invoked on receipt of a plain C<PRIVMSG>
+that doesn't contain a C<CTCP> subcommand.
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
diff --git a/lib/Net/Async/IRC/Protocol.pm b/lib/Net/Async/IRC/Protocol.pm
new file mode 100644
index 0000000..e21c5d6
--- /dev/null
+++ b/lib/Net/Async/IRC/Protocol.pm
@@ -0,0 +1,412 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2010-2015 -- leonerd@leonerd.org.uk
+
+package Net::Async::IRC::Protocol;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.11';
+
+use base qw( IO::Async::Stream Protocol::IRC );
+
+use Carp;
+
+use Protocol::IRC::Message;
+
+use Encode qw( find_encoding );
+use Time::HiRes qw( time );
+
+use IO::Async::Timer::Countdown;
+
+=head1 NAME
+
+C<Net::Async::IRC::Protocol> - send and receive IRC messages
+
+=head1 DESCRIPTION
+
+This subclass of L<IO::Async::Stream> implements an established IRC
+connection that has already completed its inital login sequence and is ready
+to send and receive IRC messages. It handles base message sending and
+receiving, and implements ping timers. This class provides most of the
+functionality required for sending and receiving IRC commands and responses
+by mixing in from L<Protocol::IRC>.
+
+Objects of this type would not normally be constructed directly. For IRC
+clients, see L<Net::Async::IRC> which is a subclass of it. All the events,
+parameters, and methods documented below are relevant there.
+
+=cut
+
+=head1 EVENTS
+
+The following events are invoked, either using subclass methods or C<CODE>
+references in parameters:
+
+=head2 $handled = on_message
+
+=head2 $handled = on_message_MESSAGE
+
+Invoked on receipt of a valid IRC message. See C<MESSAGE HANDLING> below.
+
+=head2 on_irc_error $err
+
+Invoked on receipt of an invalid IRC message if parsing fails. C<$err> is the
+error message text. If left unhandled, any parse error will result in the
+connection being immediataely closed, followed by the exception being
+re-thrown.
+
+=head2 on_ping_timeout
+
+Invoked if the peer fails to respond to a C<PING> message within the given
+timeout.
+
+=head2 on_pong_reply $lag
+
+Invoked when the peer successfully sends a C<PONG> reply response to a C<PING>
+message. C<$lag> is the response time in (fractional) seconds.
+
+=cut
+
+=head1 PARAMETERS
+
+The following named parameters may be passed to C<new> or C<configure>:
+
+=over 8
+
+=item on_message => CODE
+
+=item on_message_MESSAGE => CODE
+
+=item on_irc_error => CODE
+
+=item on_ping_timeout => CODE
+
+=item on_pong_reply => CODE
+
+C<CODE> references for event handlers.
+
+=item pingtime => NUM
+
+Amount of quiet time, in seconds, after a message is received from the peer,
+until a C<PING> will be sent to check it is still alive.
+
+=item pongtime => NUM
+
+Timeout, in seconds, after sending a C<PING> message, to wait for a C<PONG>
+response.
+
+=item encoding => STRING
+
+If supplied, sets an encoding to use to encode outgoing messages and decode
+incoming messages.
+
+=back
+
+=cut
+
+=head1 CONSTRUCTOR
+
+=cut
+
+=head2 new
+
+ $irc = Net::Async::IRC::Protocol->new( %args )
+
+Returns a new instance of a C<Net::Async::IRC::Protocol> object. This object
+represents a IRC connection to a peer.
+
+=cut
+
+sub new
+{
+ my $class = shift;
+ my %args = @_;
+
+ my $on_closed = delete $args{on_closed};
+
+ return $class->SUPER::new(
+ %args,
+
+ on_closed => sub {
+ my $self = shift;
+
+ my $loop = $self->get_loop;
+
+ $self->{pingtimer}->stop;
+ $self->{pongtimer}->stop;
+
+ $on_closed->( $self ) if $on_closed;
+
+ undef $self->{connect_f};
+ undef $self->{login_f};
+ },
+ );
+}
+
+sub _init
+{
+ my $self = shift;
+ $self->SUPER::_init( @_ );
+
+ my $pingtime = 60;
+ my $pongtime = 10;
+
+ $self->{pingtimer} = IO::Async::Timer::Countdown->new(
+ delay => $pingtime,
+
+ on_expire => sub {
+ my $now = time();
+
+ $self->send_message( "PING", undef, "$now" );
+
+ $self->{ping_send_time} = $now;
+
+ $self->{pongtimer}->start;
+ },
+ );
+ $self->add_child( $self->{pingtimer} );
+
+ $self->{pongtimer} = IO::Async::Timer::Countdown->new(
+ delay => $pongtime,
+
+ on_expire => sub {
+ $self->{on_ping_timeout}->( $self ) if $self->{on_ping_timeout};
+ },
+ );
+ $self->add_child( $self->{pongtimer} );
+}
+
+# for Protocol::IRC
+sub encoder
+{
+ my $self = shift;
+ return $self->{encoder};
+}
+
+sub configure
+{
+ my $self = shift;
+ my %args = @_;
+
+ $self->{$_} = delete $args{$_} for grep m/^on_message/, keys %args;
+
+ for (qw( on_ping_timeout on_pong_reply on_irc_error )) {
+ $self->{$_} = delete $args{$_} if exists $args{$_};
+ }
+
+ if( exists $args{pingtime} ) {
+ $self->{pingtimer}->configure( delay => delete $args{pingtime} );
+ }
+
+ if( exists $args{pongtime} ) {
+ $self->{pongtimer}->configure( delay => delete $args{pongtime} );
+ }
+
+ if( exists $args{encoding} ) {
+ my $encoding = delete $args{encoding};
+ my $obj = find_encoding( $encoding );
+ defined $obj or croak "Cannot handle an encoding of '$encoding'";
+ $self->{encoder} = $obj;
+ }
+
+ $self->SUPER::configure( %args );
+}
+
+sub incoming_message
+{
+ my $self = shift;
+ my ( $message ) = @_;
+
+ my @shortargs = ( $message->arg( 0 ) );
+ push @shortargs, $message->arg( 1 ) if $message->command =~ m/^\d+$/;
+ push @shortargs, "..." if $message->args > 1;
+
+ $self->debug_printf( "COMMAND ${\ $message->command } @shortargs" );
+
+ return $self->SUPER::incoming_message( @_ );
+}
+
+=head1 METHODS
+
+=cut
+
+=head2 is_connected
+
+ $connect = $irc->is_connected
+
+Returns true if a connection to the peer is established. Note that even
+after a successful connection, the connection may not yet logged in to. See
+also the C<is_loggedin> method.
+
+=cut
+
+sub is_connected
+{
+ my $self = shift;
+ return 0 unless my $connect_f = $self->{connect_f};
+ return $connect_f->is_ready && !$connect_f->failure;
+}
+
+=head2 is_loggedin
+
+ $loggedin = $irc->is_loggedin
+
+Returns true if the full login sequence has been performed on the connection
+and it is ready to use.
+
+=cut
+
+sub is_loggedin
+{
+ my $self = shift;
+ return 0 unless my $login_f = $self->{login_f};
+ return $login_f->is_ready && !$login_f->failure;
+}
+
+sub on_read
+{
+ my $self = shift;
+ my ( $buffref, $eof ) = @_;
+
+ my $pingtimer = $self->{pingtimer};
+
+ $pingtimer->is_running ? $pingtimer->reset : $pingtimer->start;
+
+ eval {
+ $self->Protocol::IRC::on_read( $$buffref );
+ 1;
+ } and return 0;
+
+ my $e = "$@"; chomp $e;
+
+ $self->maybe_invoke_event( on_irc_error => $e )
+ and return 0;
+
+ $self->close_now;
+ die "$e\n";
+}
+
+=head2 nick
+
+ $nick = $irc->nick
+
+Returns the current nick in use by the connection.
+
+=cut
+
+sub _set_nick
+{
+ my $self = shift;
+ ( $self->{nick} ) = @_;
+ $self->{nick_folded} = $self->casefold_name( $self->{nick} );
+}
+
+sub nick
+{
+ my $self = shift;
+ return $self->{nick};
+}
+
+=head2 nick_folded
+
+ $nick_folded = $irc->nick_folded
+
+Returns the current nick in use by the connection, folded by C<casefold_name>
+for convenience.
+
+=cut
+
+sub nick_folded
+{
+ my $self = shift;
+ return $self->{nick_folded};
+}
+
+=head1 MESSAGE HANDLING
+
+Every incoming message causes a sequence of message handling to occur. First,
+the message is parsed, and a hash of data about it is created; this is called
+the hints hash. The message and this hash are then passed down a sequence of
+potential handlers.
+
+Each handler indicates by return value, whether it considers the message to
+have been handled. Processing of the message is not interrupted the first time
+a handler declares to have handled a message. Instead, the hints hash is
+marked to say it has been handled. Later handlers can still inspect the
+message or its hints, using this information to decide if they wish to take
+further action.
+
+A message with a command of C<COMMAND> will try handlers in following places:
+
+=over 4
+
+=item 1.
+
+A CODE ref in a parameter called C<on_message_COMMAND>
+
+ $on_message_COMMAND->( $irc, $message, \%hints )
+
+=item 2.
+
+A method called C<on_message_COMMAND>
+
+ $irc->on_message_COMMAND( $message, \%hints )
+
+=item 3.
+
+A CODE ref in a parameter called C<on_message>
+
+ $on_message->( $irc, 'COMMAND', $message, \%hints )
+
+=item 4.
+
+A method called C<on_message>
+
+ $irc->on_message( 'COMMAND', $message, \%hints )
+
+=back
+
+As this message handling ability is provided by C<Protocol::IRC>, more details
+about how it works and how to use it can be found at
+L<Protocol::IRC/MESSAGE HANDLING>.
+
+Additionally, some types of messages receive further processing by
+C<Protocol::IRC> and in turn cause new types of events to be invoked. These
+are further documented by L<Protocol::IRC/INTERNAL MESSAGE HANDLING>.
+
+=cut
+
+sub invoke
+{
+ my $self = shift;
+ my $retref = $self->maybe_invoke_event( @_ ) or return undef;
+ return $retref->[0];
+}
+
+sub on_message_PONG
+{
+ my $self = shift;
+ my ( $message, $hints ) = @_;
+
+ return 1 unless $self->{pongtimer}->is_running;
+
+ my $lag = time - $self->{ping_send_time};
+
+ $self->{current_lag} = $lag;
+ $self->{on_pong_reply}->( $self, $lag ) if $self->{on_pong_reply};
+
+ $self->{pongtimer}->stop;
+
+ return 1;
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;
diff --git a/t/00use.t b/t/00use.t
new file mode 100644
index 0000000..8bb8681
--- /dev/null
+++ b/t/00use.t
@@ -0,0 +1,11 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+
+use_ok( "Net::Async::IRC" );
+use_ok( "Net::Async::IRC::Protocol" );
+
+done_testing;
diff --git a/t/30client-connect.t b/t/30client-connect.t
new file mode 100644
index 0000000..bee0327
--- /dev/null
+++ b/t/30client-connect.t
@@ -0,0 +1,107 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use IO::Async::Test;
+use IO::Async::Loop;
+use IO::Async::Listener;
+
+use Net::Async::IRC;
+
+my $CRLF = "\x0d\x0a"; # because \r\n isn't portable
+
+my $loop = IO::Async::Loop->new();
+testing_loop( $loop );
+
+my $client;
+my $listener = IO::Async::Listener->new(
+ on_stream => sub {
+ ( undef, $client ) = @_;
+ },
+);
+$loop->add( $listener );
+
+$listener->listen(
+ addr => { family => "inet" },
+)->get;
+
+my @errors;
+
+my $irc = Net::Async::IRC->new(
+ user => "defaultuser",
+ realname => "Default Real name",
+
+ on_message => sub { "IGNORE" },
+
+ on_irc_error => sub {
+ my $self = shift;
+ my ( $err ) = @_;
+
+ push @errors, $err;
+ },
+);
+
+$loop->add( $irc );
+
+ok( !$irc->is_connected, 'not $irc->is_connected' );
+
+$irc->connect(
+ addr => {
+ family => "inet",
+ ip => $listener->read_handle->sockhost,
+ port => $listener->read_handle->sockport,
+ },
+)->get;
+
+ok( $irc->is_connected, '$irc->is_connected' );
+ok( !$irc->is_loggedin, 'not $irc->is_loggedin' );
+
+wait_for { $client };
+$client->configure( on_read => sub { 0 } ); # using read futures
+$loop->add( $client );
+
+# Now see if we can send a message
+$irc->send_message( "HELLO", undef, "world" );
+
+my $read_f;
+
+$read_f = $client->read_until( $CRLF );
+wait_for { $read_f->is_ready };
+
+is( scalar $read_f->get, "HELLO world$CRLF", 'Server stream after initial client message' );
+
+my $logged_in = 0;
+
+my $login_f = $irc->login(
+ nick => "MyNick",
+
+ on_login => sub { $logged_in = 1 },
+);
+
+$read_f = $client->read_until( qr/$CRLF.*$CRLF/ );
+wait_for { $read_f->is_ready };
+
+is( scalar $read_f->get,
+ "USER defaultuser 0 * :Default Real name$CRLF" .
+ "NICK MyNick$CRLF",
+ 'Server stream after login' );
+
+$client->write( ":irc.example.com 001 MyNick :Welcome to IRC MyNick!defaultuser\@your.host.here$CRLF" );
+
+wait_for { $login_f->is_ready };
+
+ok( !$login_f->failure, 'Client logs in without failure' );
+
+ok( $logged_in, 'Client receives logged in event' );
+ok( $irc->is_connected, '$irc->is_connected' );
+ok( $irc->is_loggedin, '$irc->is_loggedin' );
+
+$client->write( ":something invalid-here$CRLF" );
+
+wait_for { scalar @errors };
+
+ok( defined shift @errors, 'on_error invoked' );
+
+done_testing;
diff --git a/t/31client-cap.t b/t/31client-cap.t
new file mode 100644
index 0000000..f2035ad
--- /dev/null
+++ b/t/31client-cap.t
@@ -0,0 +1,100 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use IO::Async::Test;
+use IO::Async::Loop;
+use IO::Async::Stream;
+
+use Net::Async::IRC;
+
+my $CRLF = "\x0d\x0a"; # because \r\n isn't portable
+
+my $loop = IO::Async::Loop->new();
+testing_loop( $loop );
+
+my ( $S1, $S2 ) = IO::Async::OS->socketpair() or die "Cannot create socket pair - $!";
+
+# Normal CAP login
+{
+ my $irc = Net::Async::IRC->new(
+ handle => $S1,
+ use_caps => [qw( multi-prefix )],
+ );
+ $loop->add( $irc );
+
+ my $login_f = $irc->login(
+ nick => "MyNick",
+ user => "me",
+ realname => "My real name",
+ );
+
+ my $serverstream = "";
+ wait_for_stream { $serverstream =~ m/(?:.*$CRLF){3}/ } $S2 => $serverstream;
+
+ is( $serverstream, "CAP LS$CRLF" .
+ "USER me 0 * :My real name$CRLF" .
+ "NICK MyNick$CRLF", 'Server stream negotiates CAP' );
+ $serverstream = "";
+
+ $S2->syswrite( ':irc.example.com CAP * LS :multi-prefix sasl' . $CRLF );
+
+ wait_for_stream { $serverstream =~ m/.*$CRLF/ } $S2 => $serverstream;
+
+ is( $serverstream, "CAP REQ multi-prefix$CRLF", 'Client requests caps' );
+ $serverstream = "";
+
+ is_deeply( $irc->caps_supported,
+ { 'multi-prefix' => 1,
+ 'sasl' => 1 },
+ '$irc->caps_supported' );
+ ok( $irc->cap_supported( "multi-prefix" ), '$irc->cap_supported' );
+
+ $S2->syswrite( ':irc.example.com CAP * ACK :multi-prefix' . $CRLF );
+
+ wait_for_stream { $serverstream =~ m/.*$CRLF/ } $S2 => $serverstream;
+
+ is( $serverstream, "CAP END$CRLF", 'Client finishes CAP' );
+
+ is_deeply( $irc->caps_enabled,
+ { 'multi-prefix' => 1 },
+ '$irc->caps_enabled' );
+ ok( $irc->cap_enabled( "multi-prefix" ), '$irc->cap_enabled' );
+
+ $S2->syswrite( ':irc.example.com 001 MyNick :Welcome to IRC MyNick!me@your.host' . $CRLF );
+
+ wait_for { $login_f->is_ready };
+ $login_f->get;
+
+ $loop->remove( $irc );
+}
+
+# CAP ignored by server
+{
+ my $irc = Net::Async::IRC->new(
+ handle => $S1,
+ use_caps => [qw( multi-prefix )],
+ );
+ $loop->add( $irc );
+
+ my $login_f = $irc->login(
+ nick => "MyNick",
+ user => "me",
+ realname => "My real name",
+ );
+
+ my $serverstream = "";
+ wait_for_stream { $serverstream =~ m/(?:.*$CRLF){3}/ } $S2 => $serverstream;
+
+ $S2->syswrite( ':irc.example.com 001 MyNick :Welcome to IRC MyNick!me@your.host' . $CRLF );
+
+ wait_for { $login_f->is_ready };
+ $login_f->get;
+
+ is( $irc->caps_supported, undef, '$irc->caps_supported undef for CAPless server' );
+ is( $irc->caps_enabled, undef, '$irc->caps_enabled undef for CAPless server' );
+}
+
+done_testing;
diff --git a/t/32client-encoding.t b/t/32client-encoding.t
new file mode 100644
index 0000000..7110ab7
--- /dev/null
+++ b/t/32client-encoding.t
@@ -0,0 +1,84 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use utf8;
+
+use Test::More;
+use IO::Async::Test;
+use IO::Async::Loop;
+use IO::Async::Listener;
+
+use Encode qw( encode_utf8 );
+
+use Net::Async::IRC;
+
+my $CRLF = "\x0d\x0a"; # because \r\n isn't portable
+
+my $loop = IO::Async::Loop->new();
+testing_loop( $loop );
+
+SKIP: foreach my $SSL ( 0, 1 ) {
+ if( $SSL ) {
+ eval { require IO::Async::SSL } or skip "No IO::Async::SSL", 1;
+ }
+
+ my $client;
+ my $listener = IO::Async::Listener->new(
+ on_stream => sub {
+ ( undef, $client ) = @_;
+ },
+ );
+ $loop->add( $listener );
+
+ $listener->listen(
+ addr => { family => "inet" },
+ ( $SSL ?
+ ( extensions => [ 'SSL' ],
+ SSL_key_file => "t/privkey.pem",
+ SSL_cert_file => "t/server.pem", ) :
+ () ),
+ )->get;
+
+ my $irc = Net::Async::IRC->new(
+ user => "defaultuser",
+ realname => "Default Real name",
+
+ encoding => "UTF-8",
+
+ on_message => sub { "IGNORE" },
+
+ on_irc_error => sub {},
+ );
+ $loop->add( $irc );
+
+ $irc->connect(
+ addr => {
+ family => "inet",
+ ip => $listener->read_handle->sockhost,
+ port => $listener->read_handle->sockport,
+ },
+ ( $SSL ?
+ ( extensions => [ 'SSL' ],
+ SSL_verify_mode => 0 ) :
+ () ),
+ )->get;
+
+ wait_for { $client };
+ $client->configure( on_read => sub { 0 } ); # using read futures
+ $loop->add( $client );
+
+ $irc->send_message( "PRIVMSG", undef, "target", "Ĉu vi ĉi tio vidas?" );
+
+ my $read_f = $client->read_until( $CRLF );
+ wait_for { $read_f->is_ready };
+
+ is( scalar $read_f->get, encode_utf8( "PRIVMSG target :Ĉu vi ĉi tio vidas?$CRLF" ),
+ 'Stream is encoded over ' . ( $SSL ? "SSL" : "plaintext" ) );
+
+ $loop->remove( $irc );
+ $loop->remove( $client );
+ $loop->remove( $listener );
+}
+
+done_testing;
diff --git a/t/33client-nick.t b/t/33client-nick.t
new file mode 100644
index 0000000..078f5dc
--- /dev/null
+++ b/t/33client-nick.t
@@ -0,0 +1,100 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use IO::Async::Test;
+use IO::Async::OS;
+use IO::Async::Loop;
+use IO::Async::Stream;
+
+use Net::Async::IRC;
+
+my $CRLF = "\x0d\x0a"; # because \r\n isn't portable
+
+my $loop = IO::Async::Loop->new();
+testing_loop( $loop );
+
+my ( $S1, $S2 ) = IO::Async::OS->socketpair() or die "Cannot create socket pair - $!";
+
+my $in_use = 0;
+my $err_nick = 0;
+my $irc = Net::Async::IRC->new(
+ handle => $S1,
+
+ user => "defaultuser",
+ realname => "Default Real name",
+
+ nick => "AlreadyUsedNick",
+
+ on_message_ERR_NICKNAMEINUSE => sub { shift->change_nick( "1stNick" ); $in_use = 1; },
+ on_message_ERR_ERRONEUSNICKNAME => sub { shift->change_nick( "FirstNickTOOLONG" ); $err_nick = 1; },
+ on_message => sub { "IGNORE" },
+);
+
+$loop->add( $irc );
+
+is( $irc->nick, "AlreadyUsedNick", 'Initial nick is set' );
+
+ok( $irc->is_nick_me( "AlreadyUsedNick" ), 'Client recognises initial nick' );
+ok( !$irc->is_nick_me( "SomeoneElse" ), 'Client does not recognise other nick' );
+
+my $login_f = $irc->login;
+
+my $serverstream = "";
+
+wait_for_stream { $serverstream =~ m/$CRLF.*$CRLF/ } $S2 => $serverstream;
+
+is( $serverstream, "USER defaultuser 0 * :Default Real name$CRLF" .
+ "NICK AlreadyUsedNick$CRLF", 'Server stream after attempt to login with nick already in use' );
+
+$S2->syswrite( ":irc.example.com 433 * AlreadyUsedNick :Nickname is already in use$CRLF" );
+
+wait_for { $in_use };
+
+ok( $in_use, 'Client recieves ERR_NICKNAMEINUSE error' );
+
+$S2->syswrite( ":irc.example.com 432 * 1stNick :Erroneous nickname$CRLF" );
+
+wait_for { $err_nick };
+
+ok( $err_nick, 'Client recieves ERR_ERRONEUSNICK error' );
+
+$S2->syswrite( ":irc.example.com 001 FirstNick :Welcome to IRC FirstNick!defaultuser\@your.host.here$CRLF" );
+
+wait_for { $login_f->is_ready };
+$login_f->get;
+
+is( $irc->nick, "FirstNick", 'Nick was updated correctly even after multiple errors' );
+
+$serverstream = "";
+
+wait_for_stream { $serverstream =~ m/$CRLF/ } $S2 => $serverstream;
+
+is( $serverstream, "NICK 1stNick$CRLF" .
+ "NICK FirstNickTOOLONG$CRLF", 'Server stream after login' );
+
+$irc->change_nick( "SecondNick" );
+
+is( $irc->nick, "FirstNick", 'Nick still old until server confirms' );
+
+ok( $irc->is_nick_me( "FirstNick" ), 'Client recognises still old nick' );
+ok( !$irc->is_nick_me( "SecondNick" ), 'Client does not recognise new nick' );
+
+$serverstream = "";
+
+wait_for_stream { $serverstream =~ m/$CRLF/ } $S2 => $serverstream;
+
+is( $serverstream, "NICK SecondNick$CRLF", 'Server stream after NICK command' );
+
+$S2->syswrite( ":FirstNick!defaultuser\@your.host.here NICK SecondNick$CRLF" );
+
+wait_for { not $irc->is_nick_me( "FirstNick" ) };
+
+is( $irc->nick, "SecondNick", 'Object now confirms new nick' );
+
+ok( !$irc->is_nick_me( "FirstNick" ), 'Client no longer recognises old nick' );
+ok( $irc->is_nick_me( "SecondNick" ), 'Client now recognises new nick' );
+
+done_testing;
diff --git a/t/40methods-basic.t b/t/40methods-basic.t
new file mode 100644
index 0000000..dd36500
--- /dev/null
+++ b/t/40methods-basic.t
@@ -0,0 +1,40 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use IO::Async::Test;
+use IO::Async::Loop;
+use IO::Async::Stream;
+
+use Net::Async::IRC;
+
+my $CRLF = "\x0d\x0a"; # because \r\n isn't portable
+
+my $loop = IO::Async::Loop->new();
+testing_loop( $loop );
+
+my ( $S1, $S2 ) = IO::Async::OS->socketpair() or die "Cannot create socket pair - $!";
+
+my $irc = Net::Async::IRC->new(
+ handle => $S1,
+);
+$loop->add( $irc );
+
+# privmsg
+{
+ my $f = $irc->do_PRIVMSG( target => "#target", text => "Your message here" );
+
+ isa_ok( $f, "Future", '$f' );
+
+ my $serverstream = "";
+ wait_for_stream { $serverstream =~ m/(?:.*$CRLF)/ } $S2 => $serverstream;
+
+ is( $serverstream, "PRIVMSG #target :Your message here$CRLF",
+ '->privmsg' );
+
+ ok( $f->is_ready, '$f is ready' );
+}
+
+done_testing;
diff --git a/t/50client-pingpong.t b/t/50client-pingpong.t
new file mode 100644
index 0000000..4f715bf
--- /dev/null
+++ b/t/50client-pingpong.t
@@ -0,0 +1,86 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+
+use Time::HiRes qw(); # Empty import, just there to let IO::Async and Net::Async::IRC use it
+
+use IO::Async::Test;
+use IO::Async::OS;
+use IO::Async::Loop;
+use IO::Async::Stream;
+
+use Net::Async::IRC;
+
+my $CRLF = "\x0d\x0a"; # because \r\n isn't portable
+
+my $loop = IO::Async::Loop->new();
+testing_loop( $loop );
+
+my ( $S1, $S2 ) = IO::Async::OS->socketpair() or die "Cannot create socket pair - $!";
+
+my $lag;
+my $pingout;
+
+my $irc = Net::Async::IRC->new(
+ handle => $S1,
+ on_message => sub { "IGNORE" },
+
+ pingtime => 2,
+ pongtime => 1,
+
+ on_pong_reply => sub { $lag = $_[1] },
+ on_ping_timeout => sub { $pingout = 1 },
+);
+
+$loop->add( $irc );
+
+# This is all tricky timing-related code. Pay attention
+
+# First [the server] will send three messages, separated by 1sec, and assert
+# that the client didn't send a PING
+
+my $serverstream = "";
+
+my $msgcount = 0;
+
+sub tick {
+ $msgcount++;
+ $S2->syswrite( "HELLO client$CRLF" );
+
+ $loop->enqueue_timer(
+ delay => 1,
+ code => \&tick
+ ) if $msgcount < 3;
+}
+
+tick();
+
+wait_for_stream { $msgcount == 3 } $S2 => $serverstream;
+
+is( $serverstream, "", 'client quiet after server noise' );
+
+# Now [the server] will be quiet and assert that the client sends a PING
+
+wait_for_stream { $serverstream =~ m/$CRLF/ } $S2 => $serverstream;
+
+like( $serverstream, qr/^PING .*$CRLF$/, 'client sent PING after server idle' );
+
+# Now lets be a good server and reply to the PING
+my ( $pingarg ) = $serverstream =~ m/^PING (.*)$CRLF$/;
+$S2->syswrite( ":irc.example.com PONG $pingarg$CRLF" );
+
+undef $lag;
+wait_for { defined $lag };
+
+ok( $lag >= 0 && $lag <= 1, 'client acknowledges PONG reply' );
+
+# Now [the server] won't reply to a PING at all, and hope for an event to note
+# that it failed
+
+wait_for { defined $pingout };
+ok( $pingout, 'client reports PING timeout' );
+
+done_testing;
diff --git a/t/99pod.t b/t/99pod.t
new file mode 100644
index 0000000..eb319fb
--- /dev/null
+++ b/t/99pod.t
@@ -0,0 +1,11 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+
+eval "use Test::Pod 1.00";
+plan skip_all => "Test::Pod 1.00 required for testing POD" if $@;
+
+all_pod_files_ok();
diff --git a/t/privkey.pem b/t/privkey.pem
new file mode 100644
index 0000000..a7eb3d0
--- /dev/null
+++ b/t/privkey.pem
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQDddpSdw0V5emmrozEq3LkTiILDUeUk4MiTmoug1bcfgbQwld4r
+U/Dyl1oT8bonEzIcf7002HAQmhPae+9XdQOxnqyzamRBNwuypO895g5EPswTh9Rr
+PohJse3qWs4yVP1PVeatBwxIvug+6+0XXAymNBVJmQfcCbLX4i/b27NtmwIDAQAB
+AoGAcz93XZY1/F6oyQo21wBgS/r5WZ2vqn5TwwRk70DoeDvuQm5rXI7lT8lVthVQ
+c284373V/782ql0UQdnHFvMtBPT14fPdfysBSFIwjPdAZMG6EqTtYy30o8Hk1N34
+CcBTqS4nt+MvxW3xdvQd/hVZgoWRbdCZ6p11Ky9ylmJgt6kCQQD3cRkKNjeF//8j
+eG/L0OykpTivy0peDCWOZCyRIME45+L/eYaYKMdhQ4YNeaguMC2Z8GrbXf2oRZce
+t2jxn6tdAkEA5R92e5jC3dT+S1SCCSzdr1+IGF8PF5EnPCGtQMl+pfCleAo/aiPK
+pM2lmoUaOoMj8j655mq5gdUxxshPFl7lVwJBAJmo2D3pMU27jbt/PR263lnYaH1y
+pvoEXQYx2yM8zgECr4qq8xRmrnoOLp8Ln48fSBJCpHkZwz3OCWx/xWHXH9kCQEH+
+3wTYyoBVAm42SEJWTwBdtvi2IMW8BJ4YYSwBHd60QyUhZoSvDIaNyX6JijWCYo87
+LBbHdOmFvBGyzrz11n8CQCmlyhmF2xe1xUrYnGgnfIj29KPFmJik2qeDTfxACv4Z
+MzPtOWOEdZjc5h6JTnQTl0fcko35l5FaUeflvw2uBGM=
+-----END RSA PRIVATE KEY-----
diff --git a/t/server.pem b/t/server.pem
new file mode 100644
index 0000000..54b5b51
--- /dev/null
+++ b/t/server.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICsDCCAhmgAwIBAgIJAOLBB28kRrw6MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTAxMTIxMjIwMjM5WhcNMTAxMjIxMjIwMjM5WjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
+gQDddpSdw0V5emmrozEq3LkTiILDUeUk4MiTmoug1bcfgbQwld4rU/Dyl1oT8bon
+EzIcf7002HAQmhPae+9XdQOxnqyzamRBNwuypO895g5EPswTh9RrPohJse3qWs4y
+VP1PVeatBwxIvug+6+0XXAymNBVJmQfcCbLX4i/b27NtmwIDAQABo4GnMIGkMB0G
+A1UdDgQWBBQKCmQV0xTMGtYoalfHFbpDr3kgszB1BgNVHSMEbjBsgBQKCmQV0xTM
+GtYoalfHFbpDr3kgs6FJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt
+U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAOLBB28k
+Rrw6MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAIjrc8INv1WxIq0kV
+yDEmcBeot1RRiCQJJxy3xq6eZZcTkT+YvEVrR/hOWPGL0qFInltBKcp0To0w+Esz
+SQfvieWW1U/aAfcBNJ26HRyzh8N98ZST9k4LlDJbneHB8McF1G5n/D71wmHm1llh
+cIX3gRpAkOW5gnjXUYpgsviJxUQ=
+-----END CERTIFICATE-----