Compare commits
107 Commits
Author | SHA1 | Date |
---|---|---|
Arthur POULET | 1aab6b69c7 | |
Arthur Poulet | c21010a9d9 | |
Johannes Müller | d84ffb1d0c | |
Arthur POULET | ffc405d1d4 | |
Arthur POULET | 82440d9290 | |
Tatsiujin Chin | a1e382a7e6 | |
Arthur POULET | ba7e04248b | |
Arthur POULET | cb049c7312 | |
Arthur POULET | 35ffa801cf | |
Arthur POULET | bbb333ac30 | |
Arthur POULET | d906de14b0 | |
Simon Cruanes | d5bf25ffaa | |
Simon Cruanes | 03c9b1db70 | |
Arthur POULET | e89037f48d | |
Jonathan B | 26dc8556c6 | |
Arthur POULET | d682bc5560 | |
Arthur POULET | fde0e844f1 | |
Arthur POULET | b7b2a4e876 | |
Arthur POULET | aaef6c887f | |
Arthur POULET | 415bcf71de | |
Arthur POULET | 3cc038c2fb | |
Arthur POULET | 4027d3eafa | |
Arthur POULET | 2a7d37fdef | |
Arthur POULET | 7e817e1f8a | |
Arthur POULET | c7aae78d6a | |
Arthur POULET | 141f583fd7 | |
Arthur POULET | dd7ced2f43 | |
Arthur POULET | ab08e9b2aa | |
Arthur POULET | 568104bb47 | |
Arthur POULET | 547caf55a8 | |
Arthur POULET | e67bf5a095 | |
Arthur POULET | 3c5844bda9 | |
Arthur POULET | 7a6a801cd5 | |
Arthur POULET | d1846cf842 | |
Arthur POULET | e5b5689610 | |
Arthur POULET | e8e9aac275 | |
Arthur POULET | ea065a232e | |
Arthur POULET | 4fd50613f4 | |
Arthur POULET | ceacf9cd5b | |
Lucie Dispot | ed58719a10 | |
Lucie Dispot | 7a64833c03 | |
Lucie Dispot | ba087630ee | |
Lucie Dispot | fbe934b1a9 | |
Lucie Dispot | 5f1aa7deb3 | |
Lucie Dispot | eb11802889 | |
Lucie Dispot | 7cbf9b06b5 | |
Lucie Dispot | 93ddeb0fd4 | |
Lucie Dispot | db3307503b | |
Arthur POULET | e62b08f956 | |
Lucie Dispot | 1dd2d52a6a | |
Lucie Dispot | 2d690fdeba | |
Lucie Dispot | 6e2a46290d | |
Arthur POULET | 7020146105 | |
Arthur POULET | ce31088b2a | |
Lucie Dispot | a538cac160 | |
Lucie Dispot | 678d8843ab | |
Arthur POULET | c0a7a913ab | |
Arthur POULET | e4d1a8aa6a | |
Arthur POULET | ce1122cfc6 | |
Arthur POULET | f319aa9ea0 | |
Arthur POULET | 2b8375339c | |
Arthur POULET | e1f6adfe0a | |
Arthur POULET | c94fac2fc7 | |
Arthur POULET | 7f88963dc7 | |
Arthur POULET | bf3840a205 | |
Arthur POULET | 049efc67da | |
Arthur POULET | c963e0c03f | |
Arthur POULET | 9c38a632cf | |
Arthur POULET | 3bbe3053d8 | |
Arthur POULET | b29656578d | |
Arthur POULET | 8f5ad2647d | |
Arthur POULET | 69b7252352 | |
Arthur POULET | af8dbb7f74 | |
Arthur POULET | 0b7c9f20b2 | |
Lucie Dispot | f6510966a1 | |
Lucie Dispot | 2d4cbb5d34 | |
Lucie Dispot | e9a632f0ca | |
Arthur POULET | a0c42c2ed7 | |
Arthur POULET | a37f922320 | |
Lucie Dispot | 70f50c7dbf | |
Arthur POULET | d9bfe6bad5 | |
Lucie Dispot | 39ed9be176 | |
Lucie Dispot | e858e276f2 | |
Arthur POULET | cef46aa8a8 | |
Lucie Dispot | 3956eba0cc | |
Lucie Dispot | 475d808100 | |
Lucie Dispot | f703efda35 | |
Lucie Dispot | 37c09f7b2b | |
Lucie Dispot | 39da654194 | |
Lucie Dispot | a10190d42a | |
Lucie Dispot | 0e64a4636b | |
Lucie Dispot | 54f7d81054 | |
Lucie Dispot | 3aa70d77fe | |
Lucie Dispot | e607970f55 | |
Arthur POULET | c6bec9683d | |
Arthur POULET | 16ed25b8fb | |
Lucie Dispot | e09030e4ab | |
Lucie Dispot | e216f7a592 | |
Lucie Dispot | 3cc01f53db | |
Lucie Dispot | 201d3ea932 | |
Lucie Dispot | c273474f74 | |
Lucie Dispot | bb1b91a0e6 | |
Lucie Dispot | 079d6decf1 | |
Arthur POULET | 6da55c69bc | |
Arthur POULET | 923859be1b | |
Arthur POULET | acadefcb4b | |
Arthur POULET | 29433f1074 |
|
@ -0,0 +1,7 @@
|
|||
[*.cr]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
|
@ -1,13 +1,9 @@
|
|||
/doc/
|
||||
/libs/
|
||||
/.crystal/
|
||||
/lib/
|
||||
/bin/
|
||||
/.shards/
|
||||
|
||||
|
||||
# Libraries don't need dependency lock
|
||||
# Dependencies will be locked in application that uses them
|
||||
/shard.lock
|
||||
|
||||
/CrystalIrc
|
||||
/dash
|
||||
/lib/
|
||||
/docs
|
||||
|
|
13
.travis.yml
13
.travis.yml
|
@ -1 +1,14 @@
|
|||
language: crystal
|
||||
|
||||
script:
|
||||
- crystal spec
|
||||
- crystal docs
|
||||
|
||||
deploy:
|
||||
provider: pages
|
||||
skip_cleanup: true
|
||||
github_token: $GITHUB_TOKEN
|
||||
project_name: Crirc
|
||||
on:
|
||||
branch: master
|
||||
local_dir: docs
|
||||
|
|
674
LICENSE
674
LICENSE
|
@ -1,21 +1,661 @@
|
|||
The MIT License (MIT)
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (c) 2016 Arthur Poulet
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Preamble
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), 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.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey 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;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero 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 that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
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.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
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.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
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 the public, 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
|
||||
state 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) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
|
16
Makefile
16
Makefile
|
@ -1,22 +1,14 @@
|
|||
NAME=`ls src/*/ -d | cut -f2 -d'/'`
|
||||
|
||||
all: deps_opt test
|
||||
|
||||
run:
|
||||
crystal run src/$(NAME).cr
|
||||
build:
|
||||
crystal build src/$(NAME).cr --stats
|
||||
release:
|
||||
crystal build src/$(NAME).cr --stats --release
|
||||
test:
|
||||
crystal spec
|
||||
deps:
|
||||
crystal deps install
|
||||
deps_update:
|
||||
crystal deps update
|
||||
shards install
|
||||
deps_opt:
|
||||
@[ -d lib/ ] || make deps
|
||||
doc:
|
||||
crystal docs
|
||||
clean:
|
||||
rm $(NAME)
|
||||
|
||||
.PHONY: all run build release test deps deps_update doc
|
||||
.PHONY: all run build release test deps deps_update clean doc
|
||||
|
|
31
README.md
31
README.md
|
@ -1,40 +1,37 @@
|
|||
# CrystalIrc
|
||||
|
||||
A crystal library to create irc client/bots (in the future a server).
|
||||
|
||||
Works with crystal v0.23.0
|
||||
# Crirc
|
||||
|
||||
A crystal library to create irc client/bot/server.
|
||||
|
||||
## Installation
|
||||
|
||||
[![travis](https://travis-ci.org/Meoowww/CrystalIrc.svg)](https://travis-ci.org/Meoowww/CrystalIrc)
|
||||
github mirror: [![travis](https://travis-ci.org/Meoowww/Crirc.svg)](https://travis-ci.org/Meoowww/Crirc)
|
||||
|
||||
To install the lib, you will have to add the CrystalIrc dependancy to your project.
|
||||
To install the lib, you will have to add the Crirc dependency to your project.
|
||||
|
||||
Add this to your application's `shard.yml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
CrystalIrc:
|
||||
github: Meoowww/CrystalIrc
|
||||
crirc:
|
||||
git: https://git.sceptique.eu/Sceptique/Crirc
|
||||
branch: master
|
||||
```
|
||||
|
||||
Then, run ``crystal deps install`` to fetch the lib.
|
||||
|
||||
## Documentation
|
||||
|
||||
## Usage
|
||||
The documentation is built automaticaly when a commit is pushed on master on github, via Travis: <https://meoowww.github.io/Crirc/>.
|
||||
This explains the architecture and design on the library, and details the technical informations about the internal & external API.
|
||||
|
||||
- You can see a basic example: `src/dash.cr`
|
||||
- A bot (with database, plugins, etc.): [Dasshy bot](https://github.com/Meoowww/DashBot)
|
||||
Specifications (unit tests) are written into the `/spec` directory.
|
||||
|
||||
## Development
|
||||
|
||||
Open an issue if you miss an feature, or want to improve our code, or take a coffee :).
|
||||
A full implementation of a bot is published and maintained on <https://git.sceptique.eu/Sceptique/DashBot>.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork it ( https://github.com/Meoowww/CrystalIrc/fork )
|
||||
1. Fork it ( https://git.sceptique.eu/Sceptique/Crirc/fork )
|
||||
2. Create your feature branch (git checkout -b my-new-feature)
|
||||
3. Commit your changes (git commit -am 'Add some feature')
|
||||
4. Push to the branch (git push origin my-new-feature)
|
||||
|
@ -43,5 +40,5 @@ Open an issue if you miss an feature, or want to improve our code, or take a cof
|
|||
|
||||
## Contributors
|
||||
|
||||
- [Nephos](https://github.com/Nephos) Arthur Poulet - creator, maintainer
|
||||
- [Sceptique](https://git.sceptique.eu/Sceptique) Arthur Poulet - creator, maintainer
|
||||
- [Damaia](https://github.com/Lucie-Dispot) Lucie Dispot - developer
|
||||
|
|
10
shard.yml
10
shard.yml
|
@ -1,9 +1,9 @@
|
|||
name: CrystalIrc
|
||||
version: 0.1.1
|
||||
name: crirc
|
||||
version: 0.5.1
|
||||
|
||||
authors:
|
||||
- Arthur Poulet <arthur.poulet@mailoo.org>
|
||||
- Arthur Poulet <arthur.poulet@sceptique.eu>
|
||||
|
||||
crystal: 1.0.0
|
||||
|
||||
license: MIT
|
||||
crystal: 0.23.1
|
||||
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
|
||||
counter_toto = 0
|
||||
|
||||
describe CrystalIrc::Bot do
|
||||
it "Instance and basic hooking" do
|
||||
counter_toto = 0
|
||||
bot = CrystalIrc::Bot.new ip: "localhost", nick: "DashieLove"
|
||||
bot.should be_a(CrystalIrc::Bot)
|
||||
bot.on("TOTO") { |msg| msg.raw_arguments.should eq(""); counter_toto += 1 }.should be(bot)
|
||||
bot
|
||||
.on("TOTO") { |msg| msg.raw_arguments.should eq(""); counter_toto += 1 }
|
||||
.on("TOTO") { |msg| msg.raw_arguments.should eq(""); counter_toto += 1 }
|
||||
.on("TATA") { |msg| msg.raw_arguments.should eq("is true") }
|
||||
bot.handle(CrystalIrc::Message.new ":from TOTO", bot).should be(bot)
|
||||
counter_toto.should eq(3)
|
||||
bot
|
||||
.handle(CrystalIrc::Message.new ":from TATA is true", bot)
|
||||
.handle(CrystalIrc::Message.new ":from TITI", bot)
|
||||
.handle(":from TITI")
|
||||
.handle(":from TOTO")
|
||||
counter_toto.should eq(6)
|
||||
bot.on("TOTO") { |msg| msg.raw_arguments.should eq(""); counter_toto += 1 }.should be(bot)
|
||||
bot.handle(":from TOTO")
|
||||
counter_toto.should eq(10)
|
||||
expect_raises(CrystalIrc::ParsingError) { bot.handle("violation") }
|
||||
expect_raises(CrystalIrc::ParsingError) { bot.handle(":from bad") }
|
||||
end
|
||||
|
||||
it "Hooking with advanced rules" do
|
||||
counter_toto = 0
|
||||
bot = CrystalIrc::Bot.new ip: "localhost", nick: "DashieLove"
|
||||
bot
|
||||
.on("TOTO") { |msg| counter_toto += 1 }
|
||||
.on("TOTO", message: /^what(?<first>.*)/) { |msg, match|
|
||||
(match.as(Regex::MatchData))["first"] == " is love" if msg.message.to_s.size > 4
|
||||
msg.message.should eq("what is love"); counter_toto += 1
|
||||
}
|
||||
bot.handle(":s TOTO")
|
||||
counter_toto.should eq(1)
|
||||
bot.handle(":s TOTO arg :what is love")
|
||||
counter_toto.should eq(3)
|
||||
bot.handle(":s TOTO arg :this is love")
|
||||
counter_toto.should eq(4)
|
||||
end
|
||||
end
|
|
@ -1,23 +0,0 @@
|
|||
describe CrystalIrc::Client do
|
||||
it "Instance without network" do
|
||||
CrystalIrc::Client.new(ip: "localhost", port: 6667_u16, ssl: false, nick: "CrystalBot").should be_a(CrystalIrc::Client)
|
||||
CrystalIrc::Client.new(ip: "localhost", nick: "CrystalBot").should be_a(CrystalIrc::Client)
|
||||
end
|
||||
|
||||
it "chans / chan / has?" do
|
||||
cli = CrystalIrc::Client.new("nick", "localhost")
|
||||
cli.chans.size.should eq 0
|
||||
expect_raises(CrystalIrc::IrcError) { cli.chan("#chan1") }
|
||||
cli.has?("#chan1").should eq false
|
||||
cli.has?(CrystalIrc::Chan.new("#chan1")).should eq false
|
||||
|
||||
cli.chans << CrystalIrc::Chan.new("#chan1")
|
||||
cli.chans.size.should eq 1
|
||||
cli.chans << CrystalIrc::Chan.new("#chan2")
|
||||
cli.chans.size.should eq 2
|
||||
|
||||
cli.chan("#chan1").should be_a(CrystalIrc::Chan)
|
||||
cli.has?("#chan1").should eq true
|
||||
cli.has?(CrystalIrc::Chan.new("#chan1")).should eq true
|
||||
end
|
||||
end
|
|
@ -1,30 +0,0 @@
|
|||
# ::VERBOSE = true
|
||||
|
||||
describe CrystalIrc::Client do
|
||||
it "Binding test on irc.mozilla.net" do
|
||||
ENV.fetch("OFFLINE") { |v|
|
||||
next if v == "true"
|
||||
cli = CrystalIrc::Client.new ip: "irc.mozilla.org", port: 6667_u16, ssl: false, nick: "CrystalBotSpecS_#{rand 100..999}"
|
||||
cli.should be_a(CrystalIrc::Client)
|
||||
|
||||
cli.connect do |s|
|
||||
s.should be_a(CrystalIrc::IrcSender)
|
||||
spawn do
|
||||
loop do
|
||||
break if cli.closed?
|
||||
cli.gets { |msg| cli.handle msg.as(String) } rescue nil
|
||||
end
|
||||
end
|
||||
chan = CrystalIrc::Chan.new("#nyupatate")
|
||||
cli.chans.size.should eq 0
|
||||
sleep 1.5
|
||||
cli.join([chan])
|
||||
sleep 1.5
|
||||
cli.chans.size.should eq 1
|
||||
(cli.chans[0].users.size > 0).should eq true
|
||||
sleep 1.5
|
||||
end
|
||||
sleep 0.5
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,33 +0,0 @@
|
|||
# ::VERBOSE = true
|
||||
|
||||
def test_cli1(cli : CrystalIrc::Client, chan = "#nyupatate")
|
||||
cli.connect do |s|
|
||||
s.should be_a(CrystalIrc::IrcSender)
|
||||
spawn do
|
||||
loop do
|
||||
break if cli.closed?
|
||||
cli.gets { |msg| puts "> #{msg}" } rescue nil
|
||||
end
|
||||
end
|
||||
sleep 1
|
||||
chan = CrystalIrc::Chan.new(chan.as(String))
|
||||
cli.join([chan])
|
||||
cli.privmsg(target: chan, msg: "I'm a dwarf and I'm digging a hole. Diggy diggy hole.")
|
||||
sleep 1
|
||||
end
|
||||
end
|
||||
|
||||
describe CrystalIrc::Client do
|
||||
it "Test close with server" do
|
||||
CrystalIrc::Server.open(ssl: false, port: 6666_u16) do |server|
|
||||
cli = CrystalIrc::Client.new ip: "localhost", port: 6666_u16, ssl: false, nick: "CrystalBotSpecS_#{rand 100..999}"
|
||||
cli.connect do |_|
|
||||
cli.closed?.should eq(false)
|
||||
cli.close
|
||||
cli.closed?.should eq(true)
|
||||
expect_raises { cli.gets { } }
|
||||
end
|
||||
end
|
||||
sleep 0.5
|
||||
end
|
||||
end
|
|
@ -1,33 +0,0 @@
|
|||
describe CrystalIrc::Client do
|
||||
it "Test with irc.mozilla.net" do
|
||||
ENV.fetch("OFFLINE") do |v|
|
||||
next if v == "true"
|
||||
cli = CrystalIrc::Client.new ip: "irc.mozilla.org", port: 6667_u16, ssl: false, nick: "CrystalBotSpecS_#{rand 100..999}"
|
||||
cli.should be_a(CrystalIrc::Client)
|
||||
test_cli1 cli
|
||||
cli.close
|
||||
sleep 0.5
|
||||
end
|
||||
end
|
||||
|
||||
it "Instance and simple connexion with ssl" do
|
||||
ENV.fetch("OFFLINE") do |v|
|
||||
next if v == "true"
|
||||
cli = CrystalIrc::Client.new ip: "irc.mozilla.org", port: 6697_u16, ssl: true, nick: "CrystalBotSpecS_#{rand 100..999}"
|
||||
cli.connect
|
||||
cli.close
|
||||
sleep 0.5
|
||||
end
|
||||
end
|
||||
|
||||
it "Test with irc.mozilla.net with ssl" do
|
||||
ENV.fetch("OFFLINE") do |v|
|
||||
next if v == "true"
|
||||
cli = CrystalIrc::Client.new ip: "irc.mozilla.org", port: 6697_u16, ssl: true, nick: "CrystalBotSpecS_#{rand 100..999}"
|
||||
cli.should be_a(CrystalIrc::Client)
|
||||
test_cli1 cli
|
||||
cli.close
|
||||
sleep 0.5
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,27 +0,0 @@
|
|||
describe CrystalIrc::Chan do
|
||||
it "instanciation" do
|
||||
e = CrystalIrc::Chan.new "#test_works"
|
||||
e.name.should eq("#test_works")
|
||||
e = CrystalIrc::Chan.new "##test_works"
|
||||
e.name.should eq("##test_works")
|
||||
e = CrystalIrc::Chan.new("#" + "a"*49)
|
||||
e.name.should eq("#" + "a"*49)
|
||||
expect_raises(CrystalIrc::ParsingError) { CrystalIrc::Chan.new("") }
|
||||
expect_raises(CrystalIrc::ParsingError) { CrystalIrc::Chan.new("no_#_at_begin") }
|
||||
expect_raises(CrystalIrc::ParsingError) { CrystalIrc::Chan.new("#" + "a"*50) }
|
||||
expect_raises(CrystalIrc::ParsingError) { CrystalIrc::Chan.new("#spaces in the name") }
|
||||
end
|
||||
|
||||
it "user / has?" do
|
||||
c = CrystalIrc::Chan.new "#chan"
|
||||
c.users.size.should eq 0
|
||||
expect_raises(CrystalIrc::IrcError) { c.user("toto") }
|
||||
c.has?("toto").should eq false
|
||||
c.has?(CrystalIrc::User.new("toto")).should eq false
|
||||
|
||||
c.users = [CrystalIrc::User.new("toto")]
|
||||
c.user("toto").should be_a(CrystalIrc::User)
|
||||
c.has?("toto").should eq true
|
||||
c.has?(CrystalIrc::User.new("toto")).should eq true
|
||||
end
|
||||
end
|
|
@ -1,65 +0,0 @@
|
|||
def cli
|
||||
CrystalIrc::Client.new nick: "a", ip: "localhost"
|
||||
end
|
||||
|
||||
describe CrystalIrc::Message do
|
||||
it "Basics instance" do
|
||||
m = CrystalIrc::Message.new(":source CMD arg1 arg2 :message", cli)
|
||||
m.source.should eq("source")
|
||||
m.command.should eq("CMD")
|
||||
m.raw_arguments.should eq("arg1 arg2 :message")
|
||||
m.arguments.should eq(%w(arg1 arg2 message))
|
||||
m.message.should eq("message")
|
||||
m.hl.should eq("source")
|
||||
end
|
||||
|
||||
it "Instance with no message" do
|
||||
m = CrystalIrc::Message.new(":source CMD arg1 arg2", cli)
|
||||
m.source.should eq("source")
|
||||
m.command.should eq("CMD")
|
||||
m.raw_arguments.should eq("arg1 arg2")
|
||||
m.arguments.should eq(%w(arg1 arg2))
|
||||
m.message.should eq(nil)
|
||||
m.hl.should eq("source")
|
||||
end
|
||||
|
||||
it "Instance with empty message" do
|
||||
m = CrystalIrc::Message.new(":source CMD arg1 arg2 :", cli)
|
||||
m.source.should eq("source")
|
||||
m.command.should eq("CMD")
|
||||
m.raw_arguments.should eq("arg1 arg2")
|
||||
m.arguments.should eq(%w(arg1 arg2))
|
||||
m.message.should eq(nil)
|
||||
m.hl.should eq("source")
|
||||
end
|
||||
|
||||
it "Instance with no arguments" do
|
||||
m = CrystalIrc::Message.new(":source CMD :message", cli)
|
||||
m.source.should eq("source")
|
||||
m.command.should eq("CMD")
|
||||
m.raw_arguments.should eq(":message")
|
||||
m.arguments.should eq(%w(message))
|
||||
m.message.should eq("message")
|
||||
m.hl.should eq("source")
|
||||
end
|
||||
|
||||
it "Instance with no source" do
|
||||
m = CrystalIrc::Message.new("CMD arg1 arg2 :message", cli)
|
||||
m.source.should eq("0")
|
||||
m.command.should eq("CMD")
|
||||
m.raw_arguments.should eq("arg1 arg2 :message")
|
||||
m.arguments.should eq(%w(arg1 arg2 message))
|
||||
m.message.should eq("message")
|
||||
m.hl.should eq("0")
|
||||
end
|
||||
|
||||
it "PRIVMSG" do
|
||||
m = CrystalIrc::Message.new(":nik!usr@whos PRIVMSG #chan :cut my ***", cli)
|
||||
m.chan.should be_a(CrystalIrc::Chan)
|
||||
m.chan.as(CrystalIrc::Chan).name.should eq("#chan")
|
||||
m.hl.should eq("nik")
|
||||
m.source_nick.should eq("nik")
|
||||
m.source_id.should eq("usr")
|
||||
m.source_whois.should eq("whos")
|
||||
end
|
||||
end
|
|
@ -1,40 +0,0 @@
|
|||
describe CrystalIrc::User do
|
||||
it "Basic instanciation" do
|
||||
e = CrystalIrc::User.new "test-works"
|
||||
e.name.should eq("test-works")
|
||||
|
||||
# RFC ? ahahah get out the way b***
|
||||
e = CrystalIrc::User.new "test_works"
|
||||
e.name.should eq("test_works")
|
||||
|
||||
e = CrystalIrc::User.new("a"*50)
|
||||
e.name.should eq("a"*50)
|
||||
|
||||
CrystalIrc::User.new("Validity").name.should eq "Validity"
|
||||
CrystalIrc::User.new("Validity|2").name.should eq "Validity|2"
|
||||
CrystalIrc::User.new("Validity-2").name.should eq "Validity-2"
|
||||
CrystalIrc::User.new("Validity_2").name.should eq "Validity_2"
|
||||
CrystalIrc::User.new("Validity[2]").name.should eq "Validity[2]"
|
||||
CrystalIrc::User.new("Validity{2}").name.should eq "Validity{2}"
|
||||
CrystalIrc::User.new("`Validity").name.should eq "`Validity" # Not in the RFC, still accepted
|
||||
end
|
||||
|
||||
it "Instanciation error" do
|
||||
expect_raises(CrystalIrc::ParsingError) { CrystalIrc::User.new("") }
|
||||
expect_raises(CrystalIrc::ParsingError) { CrystalIrc::User.new("1a") }
|
||||
expect_raises(CrystalIrc::ParsingError) { CrystalIrc::User.new("a"*51) }
|
||||
expect_raises(CrystalIrc::ParsingError) { CrystalIrc::User.new("spaces in the name") }
|
||||
expect_raises(CrystalIrc::ParsingError) { CrystalIrc::User.new("#a") }
|
||||
end
|
||||
|
||||
it "Whois" do
|
||||
e = CrystalIrc::User.new("Dash", "here", "1.1.1.1")
|
||||
e.nick.should eq("Dash")
|
||||
e.id.should eq("here")
|
||||
e.whois.should eq("1.1.1.1")
|
||||
e = CrystalIrc::User.parse("Dash!here@1.1.1.1")
|
||||
e.name.should eq("Dash")
|
||||
e.id.should eq("here")
|
||||
e.whois.should eq("1.1.1.1")
|
||||
end
|
||||
end
|
|
@ -1,25 +0,0 @@
|
|||
describe CrystalIrc::Server do
|
||||
it "Instance" do
|
||||
s = CrystalIrc::Server.new(host: "127.0.0.1", port: 6667_u16, ssl: false)
|
||||
s.should be_a(CrystalIrc::Server)
|
||||
s.close
|
||||
CrystalIrc::Server.open(host: "127.0.0.1", port: 6667_u16, ssl: false) do |s|
|
||||
s.should be_a(CrystalIrc::Server)
|
||||
end
|
||||
end
|
||||
|
||||
it "Listen" do
|
||||
s = CrystalIrc::Server.new(host: "127.0.0.1", port: 6667_u16, ssl: false)
|
||||
spawn do
|
||||
s.accept do |cli|
|
||||
cli.should be_a(CrystalIrc::Server::Client)
|
||||
cli.send_raw ":0 NOTICE Auth :***You are connected***"
|
||||
end
|
||||
end
|
||||
TCPSocket.open("127.0.0.1", 6667) do |socket|
|
||||
got = socket.gets
|
||||
got.should eq(":0 NOTICE Auth :***You are connected***")
|
||||
end
|
||||
s.close
|
||||
end
|
||||
end
|
|
@ -1,49 +0,0 @@
|
|||
def server_process_client(s, cli)
|
||||
cli.send_raw ":0 NOTICE Auth :***You are connected***"
|
||||
begin
|
||||
loop do
|
||||
cli.gets do |str|
|
||||
STDERR.puts "server->cli.gets: #{str}" if ::VERBOSE == true
|
||||
return if str.nil?
|
||||
s.handle str, cli
|
||||
end
|
||||
end
|
||||
rescue e
|
||||
return if e.message == "Error writing file: Broken pipe"
|
||||
STDERR.puts "Error during client proccess: #{e}"
|
||||
end
|
||||
end
|
||||
|
||||
def client_fetch(cli, str)
|
||||
STDERR.puts "fetch: #{str}" if ::VERBOSE == true
|
||||
end
|
||||
|
||||
describe CrystalIrc::Server::Binding do
|
||||
it "Server binding" do
|
||||
s = CrystalIrc::Server.new(host: "127.0.0.1", port: 6667_u16, ssl: false)
|
||||
s.on("JOIN") do |msg|
|
||||
chan_name = msg.raw_arguments
|
||||
msg.command.should eq("JOIN")
|
||||
msg.raw_arguments.should eq("#toto") # chan_name
|
||||
# note: this message is already sent
|
||||
# msg.sender.send_raw ":0 NOTICE JOIN :#{chan_name}"
|
||||
end
|
||||
|
||||
spawn { spawn server_process_client(s, s.accept) }
|
||||
cli = CrystalIrc::Client.new(nick: "nick", ip: "127.0.0.1", port: 6667_u16, ssl: false)
|
||||
cli.connect
|
||||
client_fetch cli, cli.gets
|
||||
# cli.send_login
|
||||
sleep 0.5
|
||||
cli.join([CrystalIrc::Chan.new "#toto"])
|
||||
sleep 0.5
|
||||
msg = cli.gets.to_s.chomp
|
||||
client_fetch cli, msg
|
||||
msg.should eq(":0 NOTICE user :JOINED #toto")
|
||||
cli.ping
|
||||
cli.gets.to_s.chomp.should eq("PONG :0")
|
||||
cli.ping "azerty 42"
|
||||
cli.gets.to_s.chomp.should eq("PONG :azerty 42")
|
||||
s.close
|
||||
end
|
||||
end
|
|
@ -1,8 +0,0 @@
|
|||
describe CrystalIrc::Server::Client do
|
||||
it "Instance" do
|
||||
s = CrystalIrc::Server.new(host: "127.0.0.1", port: 6667_u16, ssl: false)
|
||||
c = CrystalIrc::Server::Client.new(TCPSocket.new("127.0.0.1", 6667))
|
||||
c.should be_a(CrystalIrc::Server::Client)
|
||||
s.accept { }
|
||||
end
|
||||
end
|
|
@ -1,12 +0,0 @@
|
|||
describe CrystalIrc::Nick do
|
||||
it "instanciation" do
|
||||
CrystalIrc::Nick.new("a").to_s.should eq("a")
|
||||
n = CrystalIrc::Nick.new "toto"
|
||||
n.to_s.should eq("toto")
|
||||
5.times { |i| n.next; n.to_s.should eq("toto_#{i + 1}") }
|
||||
expect_raises(CrystalIrc::ParsingError) { CrystalIrc::Nick.new("") }
|
||||
expect_raises(CrystalIrc::ParsingError) { CrystalIrc::Nick.new("a"*51) }
|
||||
expect_raises(CrystalIrc::ParsingError) { CrystalIrc::Nick.new("a@") }
|
||||
expect_raises(CrystalIrc::ParsingError) { CrystalIrc::Nick.new("a*") }
|
||||
end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
require "./spec_helper"
|
||||
|
||||
require "./CrystalIrc/*"
|
||||
require "./CrystalIrc/utils/*"
|
||||
require "./CrystalIrc/protocol/*"
|
||||
require "./CrystalIrc/server/*"
|
||||
require "./CrystalIrc/client/*"
|
|
@ -0,0 +1,18 @@
|
|||
class Crirc::Test::Binding::Handler
|
||||
include Crirc::Binding::Handler
|
||||
end
|
||||
|
||||
describe Crirc::Binding::Handler do
|
||||
it "simple test" do
|
||||
m1 = Crirc::Protocol::Message.new ":source PRIVMSG arguments :message"
|
||||
t = Crirc::Test::Binding::Handler.new
|
||||
async_test = {} of String => Bool
|
||||
t.on("PRIVMSG") { |msg, match| async_test["m1"] = true }
|
||||
t.on("PRIVMSG", message: "message") { |msg, match| async_test["m2"] = true }
|
||||
t.on("PRIVMSG", message: /message/) { |msg, match| async_test["m3"] = true }
|
||||
t.handle m1
|
||||
async_test["m1"].should be_true
|
||||
async_test["m2"].should be_true
|
||||
async_test["m3"].should be_true
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
describe Crirc::Binding::Trigger do
|
||||
it "simple test" do
|
||||
m1 = Crirc::Protocol::Message.new ":source PRIVMSG arguments :message"
|
||||
t1 = Crirc::Binding::Trigger.new "PRIVMSG"
|
||||
t2 = Crirc::Binding::Trigger.new "PRIVMSG", "source"
|
||||
t3 = Crirc::Binding::Trigger.new "PRIVMSG", "source", "arguments"
|
||||
t4 = Crirc::Binding::Trigger.new "PRIVMSG", "source", "arguments", "message"
|
||||
t1.test(m1).should be_true
|
||||
t2.test(m1).should be_true
|
||||
t3.test(m1).should be_true
|
||||
t4.test(m1).should be_true
|
||||
end
|
||||
|
||||
it "simple test" do
|
||||
m = Crirc::Protocol::Message.new ":source PRIVMSG nick :!ping me"
|
||||
t = Crirc::Binding::Trigger.new "PRIVMSG", message: /^!ping/
|
||||
t.test(m).should be_a(Regex::MatchData)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,85 @@
|
|||
module Crirc::Test::Controller::Command::Chan
|
||||
include Crirc::Controller::Command::Chan
|
||||
extend self
|
||||
|
||||
def puts(data)
|
||||
data.strip
|
||||
end
|
||||
end
|
||||
|
||||
describe Crirc::Controller::Command::Chan do
|
||||
it "join test" do
|
||||
chan = Crirc::Protocol::Chan.new "#patate"
|
||||
chan2 = Crirc::Protocol::Chan.new "#nyu"
|
||||
|
||||
Crirc::Test::Controller::Command::Chan.join({chan}).should eq("JOIN #patate")
|
||||
Crirc::Test::Controller::Command::Chan.join(chan).should eq("JOIN #patate")
|
||||
Crirc::Test::Controller::Command::Chan.join({chan, chan2}).should eq("JOIN #patate,#nyu")
|
||||
Crirc::Test::Controller::Command::Chan.join({chan}, {"bloup"}).should eq("JOIN #patate bloup")
|
||||
Crirc::Test::Controller::Command::Chan.join({chan, chan2}, {"bloup", "blip"}).should eq("JOIN #patate,#nyu bloup,blip")
|
||||
end
|
||||
|
||||
it "part test" do
|
||||
chan = Crirc::Protocol::Chan.new "#patate"
|
||||
chan2 = Crirc::Protocol::Chan.new "#nyu"
|
||||
|
||||
Crirc::Test::Controller::Command::Chan.part({chan}).should eq("PART #patate")
|
||||
Crirc::Test::Controller::Command::Chan.part(chan).should eq("PART #patate")
|
||||
Crirc::Test::Controller::Command::Chan.part({chan, chan2}).should eq("PART #patate,#nyu")
|
||||
Crirc::Test::Controller::Command::Chan.part({chan}, "I'm out").should eq("PART #patate :I'm out")
|
||||
Crirc::Test::Controller::Command::Chan.part(chan, "I'm out").should eq("PART #patate :I'm out")
|
||||
Crirc::Test::Controller::Command::Chan.part({chan, chan2}, "I'm out").should eq("PART #patate,#nyu :I'm out")
|
||||
end
|
||||
|
||||
it "misc tests" do
|
||||
target = Crirc::Protocol::User.new "nyupnyup"
|
||||
chan = Crirc::Protocol::Chan.new "#patate"
|
||||
chan2 = Crirc::Protocol::Chan.new "#nyu"
|
||||
|
||||
Crirc::Test::Controller::Command::Chan.mode(chan, "+a").should eq("MODE #patate +a")
|
||||
Crirc::Test::Controller::Command::Chan.mode(chan, "+b", target).should eq("MODE #patate +b nyupnyup")
|
||||
|
||||
Crirc::Test::Controller::Command::Chan.topic(chan).should eq("TOPIC #patate")
|
||||
Crirc::Test::Controller::Command::Chan.topic(chan, "bloup").should eq("TOPIC #patate :bloup")
|
||||
|
||||
Crirc::Test::Controller::Command::Chan.invite(chan, target).should eq("INVITE nyupnyup #patate")
|
||||
end
|
||||
|
||||
it "names test" do
|
||||
chan = Crirc::Protocol::Chan.new "#patate"
|
||||
chan2 = Crirc::Protocol::Chan.new "#nyu"
|
||||
|
||||
Crirc::Test::Controller::Command::Chan.names(nil).should eq("NAMES")
|
||||
Crirc::Test::Controller::Command::Chan.names(chan).should eq("NAMES #patate")
|
||||
Crirc::Test::Controller::Command::Chan.names({chan}).should eq("NAMES #patate")
|
||||
Crirc::Test::Controller::Command::Chan.names({chan, chan2}).should eq("NAMES #patate,#nyu")
|
||||
end
|
||||
|
||||
it "list test" do
|
||||
chan = Crirc::Protocol::Chan.new "#patate"
|
||||
chan2 = Crirc::Protocol::Chan.new "#nyu"
|
||||
|
||||
Crirc::Test::Controller::Command::Chan.list(nil).should eq("LIST")
|
||||
Crirc::Test::Controller::Command::Chan.list(chan).should eq("LIST #patate")
|
||||
Crirc::Test::Controller::Command::Chan.list({chan}).should eq("LIST #patate")
|
||||
Crirc::Test::Controller::Command::Chan.list({chan, chan2}).should eq("LIST #patate,#nyu")
|
||||
end
|
||||
|
||||
it "kick test" do
|
||||
target = Crirc::Protocol::User.new "nyupnyup"
|
||||
target2 = Crirc::Protocol::User.new "gloubi"
|
||||
chan = Crirc::Protocol::Chan.new "#patate"
|
||||
chan2 = Crirc::Protocol::Chan.new "#nyu"
|
||||
|
||||
Crirc::Test::Controller::Command::Chan.kick({chan}, {target}).should eq("KICK #patate nyupnyup")
|
||||
Crirc::Test::Controller::Command::Chan.kick(chan, target).should eq("KICK #patate nyupnyup")
|
||||
Crirc::Test::Controller::Command::Chan.kick({chan, chan2}, {target}).should eq("KICK #patate,#nyu nyupnyup")
|
||||
Crirc::Test::Controller::Command::Chan.kick({chan, chan2}, target).should eq("KICK #patate,#nyu nyupnyup")
|
||||
Crirc::Test::Controller::Command::Chan.kick({chan}, {target, target2}).should eq("KICK #patate nyupnyup,gloubi")
|
||||
Crirc::Test::Controller::Command::Chan.kick(chan, {target, target2}).should eq("KICK #patate nyupnyup,gloubi")
|
||||
Crirc::Test::Controller::Command::Chan.kick({chan, chan2}, {target, target2}).should eq("KICK #patate,#nyu nyupnyup,gloubi")
|
||||
Crirc::Test::Controller::Command::Chan.kick({chan, chan2}, {target, target2}, "Get out").should eq("KICK #patate,#nyu nyupnyup,gloubi :Get out")
|
||||
Crirc::Test::Controller::Command::Chan.kick({chan}, {target}, "Get out").should eq("KICK #patate nyupnyup :Get out")
|
||||
Crirc::Test::Controller::Command::Chan.kick(chan, target, "Get out").should eq("KICK #patate nyupnyup :Get out")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
module Crirc::Test::Controller::Command::Ping
|
||||
include Crirc::Controller::Command::Ping
|
||||
extend self
|
||||
|
||||
def puts(data)
|
||||
data.strip
|
||||
end
|
||||
end
|
||||
|
||||
describe Crirc::Controller::Command::Ping do
|
||||
it "basic test" do
|
||||
Crirc::Test::Controller::Command::Ping.ping("0").should eq("PING :0")
|
||||
Crirc::Test::Controller::Command::Ping.ping.should eq("PING :0")
|
||||
Crirc::Test::Controller::Command::Ping.pong("0").should eq("PONG :0")
|
||||
Crirc::Test::Controller::Command::Ping.pong.should eq("PONG :0")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
module Crirc::Test::Controller::Command::Talk
|
||||
include Crirc::Controller::Command::Talk
|
||||
extend self
|
||||
|
||||
def puts(data)
|
||||
data.strip
|
||||
end
|
||||
end
|
||||
|
||||
describe Crirc::Controller::Command::Talk do
|
||||
it "basic test" do
|
||||
target = Crirc::Protocol::User.new "nyupnyup"
|
||||
Crirc::Test::Controller::Command::Talk.notice(target, "This is a very important notice").should eq("NOTICE nyupnyup :This is a very important notice")
|
||||
Crirc::Test::Controller::Command::Talk.privmsg(target, "This is a test message").should eq("PRIVMSG nyupnyup :This is a test message")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
module Crirc::Test::Controller::Command::User
|
||||
include Crirc::Controller::Command::User
|
||||
extend self
|
||||
|
||||
def puts(data)
|
||||
data.strip
|
||||
end
|
||||
end
|
||||
|
||||
describe Crirc::Controller::Command::User do
|
||||
it "basic test" do
|
||||
target = Crirc::Protocol::User.new "nyupnyup"
|
||||
Crirc::Test::Controller::Command::User.whois(target).should eq("WHOIS nyupnyup")
|
||||
Crirc::Test::Controller::Command::User.whowas(target).should eq("WHOWAS nyupnyup")
|
||||
Crirc::Test::Controller::Command::User.mode(target, "abcd").should eq("MODE nyupnyup abcd")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
require "./spec_helper"
|
||||
require "./protocol/**"
|
||||
require "./binding/**"
|
||||
require "./controller/**"
|
|
@ -0,0 +1,14 @@
|
|||
describe Crirc::Protocol::Chan do
|
||||
it "instanciation" do
|
||||
e = Crirc::Protocol::Chan.new "#test_works"
|
||||
e.name.should eq("#test_works")
|
||||
e = Crirc::Protocol::Chan.new "##test_works"
|
||||
e.name.should eq("##test_works")
|
||||
e = Crirc::Protocol::Chan.new("#" + "a"*49)
|
||||
e.name.should eq("#" + "a"*49)
|
||||
expect_raises(Crirc::Protocol::Chan::ParsingError) { Crirc::Protocol::Chan.new("") }
|
||||
expect_raises(Crirc::Protocol::Chan::ParsingError) { Crirc::Protocol::Chan.new("no_#_at_begin") }
|
||||
expect_raises(Crirc::Protocol::Chan::ParsingError) { Crirc::Protocol::Chan.new("#" + "a"*50) }
|
||||
expect_raises(Crirc::Protocol::Chan::ParsingError) { Crirc::Protocol::Chan.new("#spaces in the name") }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,60 @@
|
|||
describe Crirc::Protocol::Message do
|
||||
it "Basics instance" do
|
||||
m = Crirc::Protocol::Message.new(":source CMD arg1 arg2 :message")
|
||||
m.source.should eq("source")
|
||||
m.command.should eq("CMD")
|
||||
m.raw_arguments.should eq("arg1 arg2 :message")
|
||||
m.arguments.should eq("arg1 arg2")
|
||||
m.argument_list.should eq(%w(arg1 arg2 message))
|
||||
m.message.should eq("message")
|
||||
end
|
||||
|
||||
it "Instance with no message" do
|
||||
m = Crirc::Protocol::Message.new(":source CMD arg1 arg2")
|
||||
m.source.should eq("source")
|
||||
m.command.should eq("CMD")
|
||||
m.raw_arguments.should eq("arg1 arg2")
|
||||
m.arguments.should eq("arg1 arg2")
|
||||
m.argument_list.should eq(%w(arg1 arg2))
|
||||
m.message.should eq(nil)
|
||||
end
|
||||
|
||||
it "Instance with empty message" do
|
||||
m = Crirc::Protocol::Message.new(":source CMD arg1 arg2 :")
|
||||
m.source.should eq("source")
|
||||
m.command.should eq("CMD")
|
||||
m.raw_arguments.should eq("arg1 arg2")
|
||||
m.arguments.should eq("arg1 arg2")
|
||||
m.argument_list.should eq(%w(arg1 arg2))
|
||||
m.message.should eq(nil)
|
||||
end
|
||||
|
||||
it "Instance with no arguments" do
|
||||
m = Crirc::Protocol::Message.new(":source CMD :message")
|
||||
m.source.should eq("source")
|
||||
m.command.should eq("CMD")
|
||||
m.raw_arguments.should eq(":message")
|
||||
m.arguments.should eq(nil)
|
||||
m.argument_list.should eq(%w(message))
|
||||
m.message.should eq("message")
|
||||
end
|
||||
|
||||
it "Instance with no source" do
|
||||
m = Crirc::Protocol::Message.new("CMD arg1 arg2 :message")
|
||||
m.source.should eq("0")
|
||||
m.command.should eq("CMD")
|
||||
m.raw_arguments.should eq("arg1 arg2 :message")
|
||||
m.arguments.should eq("arg1 arg2")
|
||||
m.argument_list.should eq(%w(arg1 arg2 message))
|
||||
m.message.should eq("message")
|
||||
end
|
||||
|
||||
it "PRIVMSG" do
|
||||
m = Crirc::Protocol::Message.new(":nik!usr@whos PRIVMSG #chan :cut my ***")
|
||||
# m.chan.should be_a(Crirc::Chan)
|
||||
# m.chan.as(Crirc::Chan).name.should eq("#chan")
|
||||
# m.source_nick.should eq("nik")
|
||||
# m.source_id.should eq("usr")
|
||||
# m.source_whois.should eq("whos")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,40 @@
|
|||
describe Crirc::Protocol::User do
|
||||
it "Basic instanciation" do
|
||||
e = Crirc::Protocol::User.new "test-works"
|
||||
e.name.should eq("test-works")
|
||||
|
||||
# RFC ? ahahah get out the way b***
|
||||
e = Crirc::Protocol::User.new "test_works"
|
||||
e.name.should eq("test_works")
|
||||
|
||||
e = Crirc::Protocol::User.new("a"*50)
|
||||
e.name.should eq("a"*50)
|
||||
|
||||
Crirc::Protocol::User.new("Validity").name.should eq "Validity"
|
||||
Crirc::Protocol::User.new("Validity|2").name.should eq "Validity|2"
|
||||
Crirc::Protocol::User.new("Validity-2").name.should eq "Validity-2"
|
||||
Crirc::Protocol::User.new("Validity_2").name.should eq "Validity_2"
|
||||
Crirc::Protocol::User.new("Validity[2]").name.should eq "Validity[2]"
|
||||
Crirc::Protocol::User.new("Validity{2}").name.should eq "Validity{2}"
|
||||
Crirc::Protocol::User.new("`Validity").name.should eq "`Validity" # Not in the RFC, still accepted
|
||||
end
|
||||
|
||||
it "Instanciation error" do
|
||||
expect_raises(Crirc::Protocol::User::ParsingError) { Crirc::Protocol::User.new("") }
|
||||
expect_raises(Crirc::Protocol::User::ParsingError) { Crirc::Protocol::User.new("1a") }
|
||||
expect_raises(Crirc::Protocol::User::ParsingError) { Crirc::Protocol::User.new("a"*51) }
|
||||
expect_raises(Crirc::Protocol::User::ParsingError) { Crirc::Protocol::User.new("spaces in the name") }
|
||||
expect_raises(Crirc::Protocol::User::ParsingError) { Crirc::Protocol::User.new("#a") }
|
||||
end
|
||||
|
||||
it "Whois" do
|
||||
e = Crirc::Protocol::User.new("Dash", "here", "1.1.1.1")
|
||||
e.nick.should eq("Dash")
|
||||
e.id.should eq("here")
|
||||
e.whois.should eq("1.1.1.1")
|
||||
e = Crirc::Protocol::User.parse("Dash!here@1.1.1.1")
|
||||
e.name.should eq("Dash")
|
||||
e.id.should eq("here")
|
||||
e.whois.should eq("1.1.1.1")
|
||||
end
|
||||
end
|
|
@ -1,2 +1,2 @@
|
|||
require "spec"
|
||||
require "../src/CrystalIrc"
|
||||
require "../src/crirc"
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
require "socket"
|
||||
|
||||
::VERBOSE = ENV["VERBOSE"]? == "true" || true
|
||||
|
||||
module CrystalIrc
|
||||
alias IrcSocket = (TCPSocket | OpenSSL::SSL::Socket)
|
||||
alias IrcServer = (TCPServer | OpenSSL::SSL::Socket::Server)
|
||||
|
||||
class IrcError < Exception; end
|
||||
|
||||
class NotImplementedError < IrcError; end
|
||||
|
||||
class NetworkError < IrcError; end
|
||||
|
||||
class ParsingError < IrcError
|
||||
def initialize(str : String, msg : String? = nil)
|
||||
super("Cannot parse \"#{str}\"" + (msg ? " #{msg}" : ""))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require "./CrystalIrc/utils/*"
|
||||
require "./CrystalIrc/protocol/*"
|
||||
require "./CrystalIrc/*"
|
|
@ -1,6 +0,0 @@
|
|||
require "./client"
|
||||
|
||||
class CrystalIrc::Bot < CrystalIrc::Client
|
||||
end
|
||||
|
||||
require "./bot/*"
|
|
@ -1,5 +0,0 @@
|
|||
class CrystalIrc::Bot
|
||||
def join(*chan_names : String)
|
||||
self.join chan_names.map { |chan_name| Chan.new chan_name }
|
||||
end
|
||||
end
|
|
@ -1,69 +0,0 @@
|
|||
require "./irc_sender"
|
||||
|
||||
class CrystalIrc::Client < CrystalIrc::IrcSender
|
||||
end
|
||||
|
||||
require "./client/*"
|
||||
|
||||
class CrystalIrc::Client
|
||||
include CrystalIrc::Client::Connect
|
||||
include CrystalIrc::Handler
|
||||
|
||||
@nick : Nick
|
||||
@ip : String
|
||||
@port : UInt16
|
||||
@ssl : Bool
|
||||
@socket : IrcSocket?
|
||||
@user : Nick?
|
||||
@realname : Nick?
|
||||
@domain : String?
|
||||
@pass : String?
|
||||
@irc_server : String?
|
||||
@read_timeout : UInt16
|
||||
@write_timeout : UInt16
|
||||
@keepalive : Bool
|
||||
@chans : Array(Chan)
|
||||
|
||||
getter nick, ip, port, ssl, user, realname, domain, pass, irc_server, read_timeout, write_timeout, keepalive, chans
|
||||
|
||||
# default port is 6667 or 6697 if ssl is true
|
||||
def initialize(nick : String, @ip, port = nil.as(UInt16?), @ssl = true, @user = nil, @realname = nil, @domain = nil, @pass = nil, @irc_server = nil,
|
||||
@read_timeout = 120_u16, @write_timeout = 5_u16, @keepalive = true)
|
||||
@nick = CrystalIrc::Nick.new(nick)
|
||||
@port = port || (ssl ? 6697_u16 : 6667_u16)
|
||||
@user ||= @nick
|
||||
@realname ||= @nick
|
||||
@domain ||= "0"
|
||||
@irc_server ||= "*"
|
||||
@chans = [] of Chan
|
||||
super()
|
||||
CrystalIrc::Client::Binding.attach(self)
|
||||
end
|
||||
|
||||
# The client has to call connect() before using socket.
|
||||
# If the socket is not setup, it will rase a NoConnection error
|
||||
protected def socket : IrcSocket
|
||||
raise NetworkError.new "Socket is not set. Use connect(...) before." unless @socket
|
||||
@socket.as(IrcSocket)
|
||||
end
|
||||
|
||||
def from : String
|
||||
nick.to_s
|
||||
end
|
||||
|
||||
# Search a `Chan` in the list `chans` by name (including '#')
|
||||
# If the chan is not found, then it raise an error
|
||||
def chan(chan_name : String) : Chan
|
||||
chan = self.chans.select { |e| e.name == chan_name }.first?
|
||||
raise IrcError.new("Cannot find the chan \"#{chan_name}\"") if chan.nil?
|
||||
chan.as(Chan)
|
||||
end
|
||||
|
||||
def has?(chan_name : String) : Bool
|
||||
!!chan(chan_name) rescue false
|
||||
end
|
||||
|
||||
def has?(chan : Chan) : Bool
|
||||
has?(chan.name)
|
||||
end
|
||||
end
|
|
@ -1,83 +0,0 @@
|
|||
class CrystalIrc::Client
|
||||
# Default binding on the `Client`.
|
||||
#
|
||||
# Handles the classic behavior of a client.
|
||||
# - ping
|
||||
# - join, kick, part, ...
|
||||
# - names
|
||||
# - etc.
|
||||
module Binding
|
||||
def self.attach(obj : Client)
|
||||
attach_connection(obj)
|
||||
attach_chans(obj)
|
||||
attach_other(obj)
|
||||
end
|
||||
|
||||
private def self.attach_chans(obj : Client)
|
||||
# Two cases: the client joins a chan or another user joins a chan
|
||||
obj.on("JOIN") do |msg|
|
||||
if msg.source_nick == obj.nick # the client joined
|
||||
chan = Chan.new(msg.arguments.first)
|
||||
obj.chans << chan
|
||||
else # someone else joined
|
||||
begin
|
||||
chan = obj.chan msg.arguments.first
|
||||
chan.as(Chan).users << User.new name
|
||||
rescue
|
||||
chan = Chan.new(msg.arguments.first)
|
||||
obj.chans << chan
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
obj.on("PART") do |msg|
|
||||
chan = obj.chan msg.arguments.first
|
||||
if msg.source_nick == obj.nick # the client left
|
||||
obj.chans.delete(chan)
|
||||
else # someone else left
|
||||
user = chan.user name
|
||||
chan.as(Chan).users.delete(user)
|
||||
end
|
||||
end
|
||||
|
||||
obj.on("KICK") do |msg|
|
||||
name = msg.arguments[1]
|
||||
chan = obj.chan msg.arguments.first
|
||||
if name == obj.nick # the client just got kicked
|
||||
obj.chans.delete(chan)
|
||||
else # someone else has been kicked
|
||||
user = chan.user name
|
||||
chan.as(Chan).users.delete(user)
|
||||
end
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
private def self.attach_other(obj : Client)
|
||||
obj.on("PING") do |msg|
|
||||
msg.sender.pong(msg.message)
|
||||
end
|
||||
|
||||
obj.on("353") do |msg|
|
||||
chan = obj.chan msg.arguments[2]
|
||||
chan.users = msg.arguments.last.split(" ").map do |name|
|
||||
User.new name.delete("~&@%+")
|
||||
end
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
private def self.attach_connection(obj : Client)
|
||||
obj.on("433") do |msg|
|
||||
obj.nick.next
|
||||
obj.send_login
|
||||
end
|
||||
obj.on("451") do |msg|
|
||||
obj.send_login
|
||||
end
|
||||
end
|
||||
#
|
||||
end
|
||||
end
|
|
@ -1,38 +0,0 @@
|
|||
require "openssl"
|
||||
|
||||
module CrystalIrc::Client::Connect
|
||||
# Connect the client to the server.
|
||||
#
|
||||
# It does send the login informations.
|
||||
def connect : Client
|
||||
tmp_socket = TCPSocket.new ip, port
|
||||
tmp_socket.read_timeout = read_timeout
|
||||
tmp_socket.write_timeout = write_timeout
|
||||
tmp_socket.keepalive = keepalive
|
||||
@socket = tmp_socket
|
||||
@socket = OpenSSL::SSL::Socket::Client.new(tmp_socket) if ssl
|
||||
send_login
|
||||
self
|
||||
end
|
||||
|
||||
def connect : Client
|
||||
yield connect()
|
||||
close()
|
||||
self
|
||||
end
|
||||
|
||||
def on_ready(&b) : Client
|
||||
self.on("001") { b.call }
|
||||
self
|
||||
end
|
||||
|
||||
# Sends the connecttion sequence (PASS, NICK, USER).
|
||||
# The password is sent only if specified.
|
||||
# It does not handle the errors (it is done in `binding.cr`).
|
||||
def send_login : Client
|
||||
send_raw "PASS #{pass}" if pass
|
||||
send_raw "NICK #{nick.to_s}"
|
||||
send_raw "USER #{user.to_s} \"#{domain}\" \"#{irc_server}\" :#{realname.to_s}"
|
||||
self
|
||||
end
|
||||
end
|
|
@ -1,63 +0,0 @@
|
|||
module CrystalIrc::Command::Chan
|
||||
# Formats the chans to join: #chan1,#chan2 (works with users by the same way)
|
||||
protected def format_list(chans : Enumerable(CrystalIrc::Target)) : String
|
||||
chans.map { |c| c.name }.to_set.join(",")
|
||||
end
|
||||
|
||||
# Requests to join the chan(s) chans, with password(s) passwords.
|
||||
# The passwords may be ignored if not needed.
|
||||
def join(chans : Enumerable(CrystalIrc::Chan), passwords : Enumerable(String) = [""])
|
||||
to_join = format_list(chans)
|
||||
passes = passwords.uniq.join(",")
|
||||
send_raw "JOIN #{to_join} #{passes}"
|
||||
end
|
||||
|
||||
# Requests to leave the channel(s) chans, with an optional part message msg.
|
||||
def part(chans : Enumerable(CrystalIrc::Chan), msg : String? = nil)
|
||||
to_leave = format_list(chans)
|
||||
msg = ":#{msg}" if msg
|
||||
send_raw "PART #{to_leave} #{msg}"
|
||||
end
|
||||
|
||||
# Requests to change the mode of the given channel.
|
||||
# If the mode is to be applied to an user, precise it.
|
||||
def mode(chan : CrystalIrc::Chan, flags : String, user : CrystalIrc::User? = nil)
|
||||
target = user ? user.name : ""
|
||||
send_raw "MODE #{chan.name} #{flags} #{target}"
|
||||
end
|
||||
|
||||
# Requests to change the topic of the given channel.
|
||||
# If no topic is given, requests the topic of the given channel.
|
||||
def topic(chan : CrystalIrc::Chan, topic : String)
|
||||
send_raw "TOPIC #{chan.name} :#{topic}"
|
||||
end
|
||||
|
||||
# Requests the names of the users in the given channel(s).
|
||||
# If no channel is given, requests the names of the users in every
|
||||
# known channel.
|
||||
def names(chans : Enumerable(CrystalIrc::Chan))
|
||||
target = format_list(chans)
|
||||
send_raw "NAMES #{target}"
|
||||
end
|
||||
|
||||
# Lists the channels and their topics.
|
||||
# If the chans parameter is given, lists the status of the given chans.
|
||||
def list(chans : Enumerable(CrystalIrc::Chan))
|
||||
target = format_list(chans)
|
||||
send_raw "LIST #{target}"
|
||||
end
|
||||
|
||||
# Invites the user user to the channel chan.
|
||||
def invite(chan : CrystalIrc::Chan, user : CrystalIrc::User)
|
||||
send_raw "INVITE #{user.name} #{chan.name}"
|
||||
end
|
||||
|
||||
# Kicks the user(s) users from the channel(s) chans.
|
||||
# The reason of the kick will be displayed if given as a parameter.
|
||||
def kick(chans : Enumerable(CrystalIrc::Chan), users : Enumerable(CrystalIrc::User), reason : String? = nil)
|
||||
chan = format_list(chans)
|
||||
targets = format_list(users)
|
||||
reason = ":#{reason}" if reason
|
||||
send_raw "KICK #{chan} #{targets} #{reason}"
|
||||
end
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
module CrystalIrc::Command::Ping
|
||||
def ping(message = "0")
|
||||
send_raw "PING :#{message}"
|
||||
end
|
||||
|
||||
def pong(message = "0")
|
||||
send_raw (message.nil? || message.empty?) ? "PONG" : "PONG :#{message}"
|
||||
end
|
||||
end
|
|
@ -1,11 +0,0 @@
|
|||
module CrystalIrc::Command::Talk
|
||||
# Send a notice to the given target.
|
||||
def notice(target : CrystalIrc::Target, msg : String)
|
||||
answer_raw "NOTICE #{target.name} :#{msg}"
|
||||
end
|
||||
|
||||
# Send a private message to the given target.
|
||||
def privmsg(target : CrystalIrc::Target, msg : String)
|
||||
answer_raw "PRIVMSG #{target.name} :#{msg}"
|
||||
end
|
||||
end
|
|
@ -1,16 +0,0 @@
|
|||
module CrystalIrc::Command::User
|
||||
# Requests data about the given target.
|
||||
def whois(target : CrystalIrc::User)
|
||||
send_raw "WHOIS #{target.name}"
|
||||
end
|
||||
|
||||
# Requests data bout the user who used the given name.
|
||||
def whowas(target : CrystalIrc::User)
|
||||
send_raw "WHOWAS #{target.name}"
|
||||
end
|
||||
|
||||
# Requests to set the mode of the given target to flag.
|
||||
def mode(target : CrystalIrc::User, flag : String)
|
||||
send_raw "MODE #{target.name} #{flag}"
|
||||
end
|
||||
end
|
|
@ -1,44 +0,0 @@
|
|||
abstract class CrystalIrc::HasSocket
|
||||
abstract def socket : IrcSocket
|
||||
|
||||
# Send a raw message to the socket. It should be a valid command
|
||||
# TODO: handle too large messages for IRC
|
||||
def send_raw(raw : String, output : IO? = nil) : HasSocket
|
||||
begin
|
||||
socket.puts raw
|
||||
output.puts raw if !output.nil?
|
||||
raw_display = raw.starts_with?("PASS ") ? "PASS ****" : raw
|
||||
STDOUT.puts "[#{Time.now}] #{raw_display}" if ::VERBOSE == true
|
||||
rescue e
|
||||
STDERR.puts "#{e} -> [#{Time.now}] #{raw.inspect}" if ::VERBOSE == true
|
||||
raise e
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
def answer_raw(from : String, raw : String, output : IO? = nil)
|
||||
send_raw(":#{from} #{raw}", output)
|
||||
end
|
||||
|
||||
def close
|
||||
socket.close
|
||||
end
|
||||
|
||||
def closed?
|
||||
socket.closed?
|
||||
end
|
||||
|
||||
protected def puts(e)
|
||||
socket.puts(e)
|
||||
end
|
||||
|
||||
def gets
|
||||
yield socket.gets
|
||||
end
|
||||
|
||||
def gets
|
||||
r = socket.gets
|
||||
STDOUT.puts "[#{Time.now}] #{socket}.gets() => #{r.inspect}: ok" if ::VERBOSE == true
|
||||
r
|
||||
end
|
||||
end
|
|
@ -1,15 +0,0 @@
|
|||
require "./has_socket"
|
||||
require "./command/*"
|
||||
|
||||
abstract class CrystalIrc::IrcSender < CrystalIrc::HasSocket
|
||||
include CrystalIrc::Command::Ping
|
||||
include CrystalIrc::Command::Chan
|
||||
include CrystalIrc::Command::Talk
|
||||
include CrystalIrc::Command::User
|
||||
|
||||
abstract def from : String
|
||||
|
||||
def answer_raw(raw : String, output : IO? = nil) : IrcSender
|
||||
send_raw ":#{from} #{raw}", output
|
||||
end
|
||||
end
|
|
@ -1,36 +0,0 @@
|
|||
require "./target"
|
||||
require "./user"
|
||||
|
||||
# Represent an IRC channel.
|
||||
#
|
||||
# It has a name and a list of user.
|
||||
# TODO: checkout the masks
|
||||
class CrystalIrc::Chan < CrystalIrc::Target
|
||||
@name : String
|
||||
@users : Array(User)
|
||||
|
||||
getter name, users
|
||||
setter users
|
||||
|
||||
def initialize(@name)
|
||||
raise ParsingError.new @name, "The Chan name must not be empty" if @name.empty?
|
||||
raise ParsingError.new @name, "The Chan name must begin with a \"#\"" if !@name.match(/\A\#.+\Z/)
|
||||
raise ParsingError.new @name, "The Chan name must contains at most 63 valid characters" if !@name.match(/\A(?!.{51,})(\#\#?([^[:space:],]+))\Z/)
|
||||
@users = [] of User
|
||||
end
|
||||
|
||||
# Search an `User` by name (nick).
|
||||
def user(user_name : String) : User
|
||||
user = self.users.select { |e| e.name == user_name }.first?
|
||||
raise IrcError.new("Cannot find the user \"#{user_name}\" in \"#{@name}\"") if user.nil?
|
||||
user.as(User)
|
||||
end
|
||||
|
||||
def has?(user_name : String) : Bool
|
||||
!!user(user_name) rescue false
|
||||
end
|
||||
|
||||
def has?(user : User) : Bool
|
||||
has?(user.name)
|
||||
end
|
||||
end
|
|
@ -1,102 +0,0 @@
|
|||
require "./message/*"
|
||||
|
||||
# This class is an entity to represent an IRC message.
|
||||
#
|
||||
# Every message has a sender and a command (or a code).
|
||||
# Optionaly, it may have a source, arguments, and message.
|
||||
#
|
||||
# **Rules**
|
||||
# - If no source is specified, then it will be set to "0" by default.
|
||||
# - The attribute `message` is used to represent the last argument if begin with a ':'. It it does not exist, then it will be considered `Nil`
|
||||
# - If no arguments are specified, then it will be an empty `Array(String)`. *Note: if the last one (the message) begin with a ':', then the ':' is deleted from the string.*
|
||||
# - The raw_arguments is the concatenation of arguments and message. *Note: The message, if exists, is always preceded by ':'
|
||||
class CrystalIrc::Message
|
||||
@raw : String
|
||||
@source : String
|
||||
@command : String
|
||||
@arguments : String?
|
||||
@message : String?
|
||||
@sender : CrystalIrc::IrcSender
|
||||
|
||||
getter raw, source, command, message, sender
|
||||
|
||||
# Concatenation of `arguments` and `message`. If the message exists, it is preceded by ':'
|
||||
def raw_arguments : String
|
||||
return "" if @arguments.nil? && @message.nil?
|
||||
return @arguments.to_s if @message.nil?
|
||||
return ":#{@message}" if @arguments.nil?
|
||||
return "#{@arguments} :#{@message}"
|
||||
end
|
||||
|
||||
def arguments : Array(String)
|
||||
return Array(String).new if @arguments.nil? && @message.nil?
|
||||
return (@arguments.as(String)).split(" ") if @message.nil?
|
||||
return [@message.as(String)] if @arguments.nil?
|
||||
return (@arguments.as(String)).split(" ") << (@message.as(String))
|
||||
end
|
||||
|
||||
R_SRC = "(\\:(?<src>[^[:space:]]+) )"
|
||||
R_CMD = "(?<cmd>[A-Z]+|\\d{3})"
|
||||
R_ARG_ONE = "(?:[^: ][^ ]*)"
|
||||
R_ARG = "(?: (?<arg>#{R_ARG_ONE}(?: #{R_ARG_ONE})*))"
|
||||
R_MSG = "(?: \\:(?<msg>.+)?)"
|
||||
|
||||
def initialize(@raw, @sender)
|
||||
m = raw.strip.match(/\A#{R_SRC}?#{R_CMD}#{R_ARG}?#{R_MSG}?\Z/)
|
||||
raise ParsingError.new(raw, "message invalid") if m.nil?
|
||||
@source = m["src"]? || "0"
|
||||
@command = m["cmd"] # ? || raise InvalidMessage.new("No command to parse in \"#{raw}\"")
|
||||
@arguments = m["arg"]?
|
||||
@message = m["msg"]?
|
||||
end
|
||||
|
||||
delegate source_nick, to: source
|
||||
delegate source_id, to: source
|
||||
delegate source_whois, to: source
|
||||
|
||||
def hl : String
|
||||
self.source_nick
|
||||
end
|
||||
|
||||
CHAN_COMMANDS = ["PRIVMSG", "JOIN", "NOTICE"]
|
||||
|
||||
# TODO: if a chan is associated (eg: message emit in a chan)
|
||||
def chan : Chan
|
||||
if CHAN_COMMANDS.includes?(@command) && !arguments.empty? && arguments[0][0] == '#'
|
||||
Chan.new arguments[0]
|
||||
else
|
||||
raise NotImplementedError.new "No chan availiable"
|
||||
end
|
||||
end
|
||||
|
||||
USER_COMMANDS = ["PRIVMSG", "NOTICE"]
|
||||
|
||||
# TODO: if an user is associated (eg: privmsg, ...)
|
||||
def user : User
|
||||
if USER_COMMANDS.includes?(@command) && !arguments.empty? && arguments[0][0] != '#'
|
||||
User.new arguments[0]
|
||||
else
|
||||
raise NotImplementedError.new "No user availiable"
|
||||
end
|
||||
end
|
||||
|
||||
# Channel or User which emit the message
|
||||
def target : Target
|
||||
user rescue chan rescue raise NotImplementedError.new "No chan nor user availiable"
|
||||
end
|
||||
|
||||
# Target to reply
|
||||
def reply_to : Target
|
||||
begin
|
||||
chan rescue User.parse(@source)
|
||||
rescue e
|
||||
STDERR.puts e
|
||||
raise NotImplementedError.new "No chan nor user to reply"
|
||||
end
|
||||
end
|
||||
|
||||
def reply(msg : String) : Message
|
||||
@sender.privmsg(reply_to, msg)
|
||||
self
|
||||
end
|
||||
end
|
|
@ -1,18 +0,0 @@
|
|||
# Extention of `String`.
|
||||
module CrystalIrc::Source
|
||||
def source_nick : String
|
||||
self.split("!")[0].to_s
|
||||
end
|
||||
|
||||
def source_id : String
|
||||
self.split("!")[1].to_s.split("@")[0].to_s
|
||||
end
|
||||
|
||||
def source_whois : String
|
||||
self.split("!")[1].to_s.split("@")[1].to_s
|
||||
end
|
||||
end
|
||||
|
||||
class String
|
||||
include CrystalIrc::Source
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
# A Target is a virtual entity to represent something a client can talk with.
|
||||
# In pratice, this is any entity with a name, which includes:
|
||||
# - `User`
|
||||
# - `Chan`
|
||||
abstract class CrystalIrc::Target
|
||||
abstract def name : String
|
||||
end
|
|
@ -1,36 +0,0 @@
|
|||
require "./target"
|
||||
|
||||
# Representation of an IRC client.
|
||||
#
|
||||
# It has a name (or nick), id (id@...) and an optionaly a whois.
|
||||
#
|
||||
# It does not represent a controllable client (only the class `Client` does).
|
||||
# TODO: checkout the masks
|
||||
class CrystalIrc::User < CrystalIrc::Target
|
||||
@nick : String
|
||||
@id : String
|
||||
@whois : String?
|
||||
|
||||
getter nick, id, whois
|
||||
|
||||
CHARS_SPECIAL = "[_\\|\\[\\]\\\\\\`\\^\\{\\}]"
|
||||
CHARS_ALPHA = "[a-zA-Z]"
|
||||
CHARS_NUM = "[0-9\\-]"
|
||||
CHARS_FIRST = "#{CHARS_SPECIAL}|#{CHARS_ALPHA}"
|
||||
CHARS_NEXT = "#{CHARS_FIRST}|#{CHARS_NUM}"
|
||||
def initialize(@nick, id = nil, @whois = nil)
|
||||
raise ParsingError.new @nick, "user name must not be empty" if @nick.empty?
|
||||
raise ParsingError.new @nick, "user name must contains at most 50 valid characters" if !@nick.match(/\A(?!.{51,})((#{CHARS_FIRST})((#{CHARS_NEXT})+))\Z/)
|
||||
@id = id || @nick
|
||||
end
|
||||
|
||||
def self.parse(source : String) : User
|
||||
m = source.match(/\A:?(?<name>[^!]+)!(?<id>.+)@(?<whois>.+)\Z/)
|
||||
raise ParsingError.new source, "invalid user" if m.nil?
|
||||
CrystalIrc::User.new(m["name"], m["id"], m["whois"])
|
||||
end
|
||||
|
||||
def name : String
|
||||
@nick
|
||||
end
|
||||
end
|
|
@ -1,73 +0,0 @@
|
|||
require "./irc_sender"
|
||||
|
||||
class CrystalIrc::Server < CrystalIrc::IrcSender
|
||||
end
|
||||
|
||||
require "./server/*"
|
||||
|
||||
class CrystalIrc::Server
|
||||
include CrystalIrc::Handler
|
||||
# include CrystalIrc::Server::Binding
|
||||
|
||||
@host : String
|
||||
@port : UInt16
|
||||
@socket : TCPServer
|
||||
@ssl : Bool
|
||||
@chans : Hash(String, Hash(CrystalIrc::Chan, Array(CrystalIrc::User)))
|
||||
@clients : Array(CrystalIrc::Server::Client)
|
||||
|
||||
getter host, port, socket, chans, clients
|
||||
|
||||
# TODO: maybe new should be protected
|
||||
# TODO: add ssl socket
|
||||
def initialize(@host = "127.0.0.1", @port = 6697_16, @ssl = true)
|
||||
@socket = TCPServer.new(@host, @port)
|
||||
@chans = Hash(String, Hash(CrystalIrc::Chan, Array(CrystalIrc::User))).new
|
||||
@clients = Array(CrystalIrc::Server::Client).new
|
||||
super()
|
||||
CrystalIrc::Server::Binding.attach(self)
|
||||
end
|
||||
|
||||
def self.open(host = "127.0.0.1", port = 6697_u16, ssl = true)
|
||||
s = new host, port, ssl
|
||||
begin
|
||||
yield s
|
||||
ensure
|
||||
s.close
|
||||
end
|
||||
end
|
||||
|
||||
def accept
|
||||
@socket.accept do |s|
|
||||
cli = CrystalIrc::Server::Client.new s
|
||||
begin
|
||||
@clients << cli
|
||||
yield cli
|
||||
ensure
|
||||
cli.close
|
||||
@clients.delete cli
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def accept
|
||||
cli = CrystalIrc::Server::Client.new @socket.accept
|
||||
@clients << cli
|
||||
cli
|
||||
end
|
||||
|
||||
protected def socket
|
||||
@socket
|
||||
end
|
||||
|
||||
def close
|
||||
@clients.each { |c| c.close }
|
||||
@clients.clear
|
||||
@chans.clear
|
||||
super
|
||||
end
|
||||
|
||||
def from
|
||||
"0"
|
||||
end
|
||||
end
|
|
@ -1,16 +0,0 @@
|
|||
# Default binding on the server.
|
||||
# Handles the classic behavior of a server.
|
||||
module CrystalIrc::Server::Binding
|
||||
def self.attach(obj)
|
||||
obj.on("JOIN") do |msg|
|
||||
chans = msg.raw_arguments.to_s.split(",").map { |e| CrystalIrc::Chan.new e.strip }
|
||||
# TODO: create the chan if needed
|
||||
# TODO: add the user the the chans
|
||||
# TODO: send to the chans
|
||||
# TODO: use something like msg.sender.user instead of "user"
|
||||
chans.each { |chan| msg.sender.notice CrystalIrc::User.new("user"), "JOINED #{chan.name}" }
|
||||
end.on("PING") do |msg|
|
||||
msg.sender.pong(msg.message)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,17 +0,0 @@
|
|||
require "../irc_sender"
|
||||
|
||||
# Represent a client, from the point of view of the server.
|
||||
class CrystalIrc::Server::Client < CrystalIrc::IrcSender
|
||||
@socket : TCPSocket
|
||||
|
||||
def initialize(@socket)
|
||||
end
|
||||
|
||||
protected def socket
|
||||
@socket
|
||||
end
|
||||
|
||||
def from
|
||||
"0" # TODO: find the client through the Server.users
|
||||
end
|
||||
end
|
|
@ -1,44 +0,0 @@
|
|||
# Register hooks to handle the behavior of a system based on a message.
|
||||
module CrystalIrc::Handler
|
||||
@hooks : Hash(CrystalIrc::Handler::HookRules, Array(CrystalIrc::Handler::Hook))
|
||||
|
||||
def initialize(**opts)
|
||||
super(**opts)
|
||||
@hooks = Hash(CrystalIrc::Handler::HookRules, Array(CrystalIrc::Handler::Hook)).new
|
||||
end
|
||||
|
||||
protected def hooks
|
||||
@hooks
|
||||
end
|
||||
|
||||
alias HookRule = String | Regex | Nil
|
||||
alias Hook = (CrystalIrc::Message, Regex::MatchData?) ->
|
||||
|
||||
# Register a hook on a command name (JOIN, PRIVMSG, ...) and other rules
|
||||
def on(command : String, source : HookRule = nil, arguments : HookRule = nil, message : HookRule = nil, &hook : Hook)
|
||||
rule = CrystalIrc::Bot::HookRules.new(command, source, arguments, message)
|
||||
self.hooks.fetch(rule) { self.hooks[rule] = Array(Hook).new }
|
||||
self.hooks[rule] << hook
|
||||
self
|
||||
end
|
||||
|
||||
# Handle one `Message`
|
||||
# It goes through the registred hooks, select the one to trigger.
|
||||
# Then, it execute every hooks associated, and send as parameters the current message and the regex match if possible
|
||||
# TODO: msg should NEVER be modified in the hook. (copy ? readonly ?)
|
||||
def handle(msg : CrystalIrc::Message)
|
||||
selected_hooks = self.hooks.select { |rule, hooks| rule.test(msg) }
|
||||
selected_hooks.each do |rule, hooks|
|
||||
hooks.each do |hook|
|
||||
match = msg.message && rule.message.is_a?(Regex) ? (rule.message.as(Regex)).match(msg.message.as(String)) : nil
|
||||
hook.call(msg, match)
|
||||
end
|
||||
end
|
||||
self.hooks.fetch(msg.command) { Array(Hook).new }
|
||||
self
|
||||
end
|
||||
|
||||
def handle(msg : String, sender = self)
|
||||
handle(CrystalIrc::Message.new msg, sender)
|
||||
end
|
||||
end
|
|
@ -1,37 +0,0 @@
|
|||
# Represent a Nick for Irc Clients
|
||||
class CrystalIrc::Nick
|
||||
@base : String
|
||||
@suffix : UInt16?
|
||||
|
||||
getter base
|
||||
|
||||
def initialize(base)
|
||||
@base = "*"
|
||||
self.base = base
|
||||
end
|
||||
|
||||
def reset
|
||||
@suffix = nil
|
||||
self
|
||||
end
|
||||
|
||||
def base=(v) : String
|
||||
reset
|
||||
raise ParsingError.new "user name must contains at most 50 valid characters" if !v.match(/\A(?!.{51,})(([a-zA-Z])([a-zA-Z0-9_\-\[\]\\\`\^\{\}]*))\Z/)
|
||||
@base = v
|
||||
end
|
||||
|
||||
def next : String
|
||||
@suffix ||= 0_u16
|
||||
@suffix = (@suffix.as(UInt16)) + 1_u16
|
||||
to_s
|
||||
end
|
||||
|
||||
def suffix : String
|
||||
@suffix ? "_#{@suffix}" : ""
|
||||
end
|
||||
|
||||
def to_s : String
|
||||
base + suffix
|
||||
end
|
||||
end
|
|
@ -1,3 +0,0 @@
|
|||
module CrystalIrc
|
||||
VERSION = YAML.parse({{ system("cat", "shard.yml").stringify }})["version"]
|
||||
end
|
|
@ -1,31 +0,0 @@
|
|||
require "./CrystalIrc"
|
||||
|
||||
def server_process_client(s, cli)
|
||||
cli.send_raw ":0 NOTICE Auth :***You are connected***"
|
||||
begin
|
||||
loop do
|
||||
cli.gets do |str|
|
||||
STDERR.puts "server->cli.gets: #{str}" if ::VERBOSE == true
|
||||
return if str.nil?
|
||||
begin
|
||||
s.handle str, cli
|
||||
rescue e
|
||||
STDERR.puts "Message error: #{e}"
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue e
|
||||
STDERR.puts "Error during client proccess: #{e}. Closed"
|
||||
cli.close
|
||||
end
|
||||
end
|
||||
|
||||
def start
|
||||
s = CrystalIrc::Server.new(host: "127.0.0.1", port: 6667_u16, ssl: false)
|
||||
spawn server_process_client(s, s.accept)
|
||||
loop do
|
||||
sleep 1
|
||||
end
|
||||
end
|
||||
|
||||
start
|
|
@ -0,0 +1,22 @@
|
|||
# The Crirc module contains all the object related to the project.
|
||||
# It uses 4 layers of objects:
|
||||
#
|
||||
# 1. **Network**: A network object manage a socket / I0.
|
||||
# The interface is described by `Crirc::Network::Network`.
|
||||
# 2. **Controller**: A controller belongs to a network object,
|
||||
# and handle the logic and data. Its interface is described by
|
||||
# `Crirc::Controller::Controller`.
|
||||
# 3. **Protocol**: A protocol object represent a IRC entity
|
||||
# (chan, user, message, ...).
|
||||
# 4. **Broadcast**: The `Broadcast` allows the system to send transmission to
|
||||
# several IRC entity as one.
|
||||
# 5. **Binding**: The `Binding::Handler` allows a given `Controller` to respond
|
||||
# to incoming transmissions.
|
||||
module Crirc
|
||||
end
|
||||
|
||||
require "./crirc/network/*"
|
||||
require "./crirc/controller/*"
|
||||
require "./crirc/protocol/*"
|
||||
require "./crirc/broadcast/*"
|
||||
require "./crirc/binding/*"
|
|
@ -0,0 +1,73 @@
|
|||
require "./trigger"
|
||||
|
||||
# This class is designed to be able to automaticaly respond to incoming IRC
|
||||
# messages on a set conditions.
|
||||
# The flow of this system is the following:
|
||||
# 1. With `.on()`, defines a Trigger and the associated Hook
|
||||
# 2. Call `.handle()` to process the incoming IRC message.
|
||||
#
|
||||
# ```
|
||||
# bot.on("PRIVMSG", message: /^(Hello|Hi)$/) { |msg, data| bot.reply(msg, "Hello !") }
|
||||
# while (incoming_message = io.gets) do
|
||||
# bot.handle(incoming_message)
|
||||
# end
|
||||
# ```
|
||||
module Crirc::Binding::Handler
|
||||
alias HookRule = String | Regex | Nil
|
||||
alias Hook = (Crirc::Protocol::Message, Regex::MatchData?) ->
|
||||
|
||||
# Hooks associated with `Trigger`
|
||||
getter hooks : Hash(Trigger, Array(Hook))
|
||||
|
||||
# Documentation lines for each hook
|
||||
getter docs : Hash(String, String)
|
||||
|
||||
def initialize(**opts)
|
||||
super(**opts)
|
||||
@hooks = Hash(Trigger, Array(Hook)).new
|
||||
@docs = Hash(String, String).new
|
||||
end
|
||||
|
||||
# Register a news Hook that is called when the incoming messages meet a set
|
||||
# of conditions: command name (JOIN, PRIVMSG, ...), source, arguments, message.
|
||||
#
|
||||
# - command : Condition that match exactly with the command of the incomming message.
|
||||
# - source : Condition that match with the source of the incomming message.
|
||||
# - arguments : Condition that match with the arguments of the incomming message.
|
||||
# - message : Condition that match with the message of the incomming message.
|
||||
# - doc : Documentation lines (`{short, long}`)
|
||||
# - hook : function to call if the conditions are met (with the parameters `message` and `match`).
|
||||
def on(command : String = "PRIVMSG", source : HookRule = nil, arguments : HookRule = nil, message : HookRule = nil,
|
||||
doc : {String, String}? = nil, &hook : Hook)
|
||||
rule = Trigger.new(command, source, arguments, message)
|
||||
self.hooks.fetch(rule) { self.hooks[rule] = Array(Hook).new }
|
||||
self.hooks[rule] << hook
|
||||
@docs[doc[0]] = doc[1] unless doc.nil?
|
||||
self
|
||||
end
|
||||
|
||||
# Handle one `Message`
|
||||
# It goes through the registred hooks, select the one to trigger.
|
||||
# Then, it execute every hooks associated, and send as parameters the current message and the regex match if possible
|
||||
# TODO: msg should NEVER be modified in the hook. (copy ? readonly ? struct ?)
|
||||
def handle(msg : Crirc::Protocol::Message)
|
||||
selected_hooks = self.hooks.select { |rule, hooks| rule.test(msg) }
|
||||
selected_hooks.each do |rule, hooks|
|
||||
hooks.each do |hook|
|
||||
message_to_handle = msg.message
|
||||
rule_message = rule.message
|
||||
match = if message_to_handle && rule_message.is_a?(Regex)
|
||||
rule_message.match message_to_handle
|
||||
end
|
||||
hook.call msg, match
|
||||
end
|
||||
end
|
||||
self.hooks.fetch(msg.command) { Array(Hook).new }
|
||||
self
|
||||
end
|
||||
|
||||
# Sugar for `handle` that parse the string as a `Crirc::Protocol::Message`
|
||||
def handle(msg : String)
|
||||
handle Crirc::Protocol::Message.new(msg)
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
# The `HookRules` define a set of rules.
|
||||
# The `Trigger` define a set of rules.
|
||||
# Theses rules can test an event (message) to "match" with.
|
||||
class CrystalIrc::Handler::HookRules
|
||||
class Crirc::Binding::Trigger
|
||||
@source : String | Regex | Nil
|
||||
@command : String | Regex
|
||||
@arguments : String | Regex | Nil
|
||||
|
@ -19,19 +19,24 @@ class CrystalIrc::Handler::HookRules
|
|||
def initialize(@command = "PRIVMSG", @source = nil, @arguments = nil, @message = nil)
|
||||
end
|
||||
|
||||
def test(msg : CrystalIrc::Message)
|
||||
test_command(msg) && test_source(msg) && test_raw_arguments(msg) && test_message(msg)
|
||||
# returns true if the the message match with the condition of this trigger
|
||||
def test(msg : Crirc::Protocol::Message)
|
||||
test_command(msg) && test_source(msg) && test_arguments(msg) && test_message(msg)
|
||||
end
|
||||
|
||||
private def test_command(msg : CrystalIrc::Message)
|
||||
private def test_command(msg : Crirc::Protocol::Message)
|
||||
command.is_a?(Regex) ? msg.command.to_s.match command.as(Regex) : msg.command == command
|
||||
end
|
||||
|
||||
{% for ft in ["source", "raw_arguments", "message"] %}
|
||||
private def {{ ("test_" + ft).id }}(msg : CrystalIrc::Message)
|
||||
{% for ft in ["source", "arguments", "message"] %}
|
||||
private def {{ ("test_" + ft).id }}(msg : Crirc::Protocol::Message)
|
||||
return true if {{ ft.id }}.nil?
|
||||
return false if msg.{{ ft.id }}.nil?
|
||||
{{ ft.id }}.is_a?(Regex) ? msg.{{ ft.id }}.to_s.match {{ ft.id }}.as(Regex) : msg.{{ ft.id }} == {{ ft.id }}
|
||||
if {{ ft.id }}.is_a?(Regex)
|
||||
msg.{{ ft.id }}.to_s.match {{ ft.id }}.as(Regex)
|
||||
elsif {{ ft.id }}.is_a?(String)
|
||||
msg.{{ ft.id }} == {{ ft.id }}
|
||||
end
|
||||
end
|
||||
{% end %}
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
# Allow to send data to several IRC entity as one.
|
||||
module Crirc::Broadcast
|
||||
# TODO
|
||||
abstract def puts(context : Controller::Controller, data)
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
require "../protocol/chan"
|
||||
require "./user_list"
|
||||
require "./broadcast"
|
||||
|
||||
# TODO
|
||||
# A ChanList is the associated list of `Protocol::Chan` and a `UserList`.
|
||||
# It is useful for a server that need to keep a track of all the uers
|
||||
# connected to any of its chans.
|
||||
class Crirc::ChanList
|
||||
getter chans : Hash(Protocol::Chan, UserList)
|
||||
include Broadcast
|
||||
|
||||
def initialize
|
||||
@chans = Hash(Protocol::Chan, UserList).new
|
||||
end
|
||||
|
||||
# TODO
|
||||
# Broadcast a message to the users
|
||||
def puts(context : Controller::Controller, data)
|
||||
@chans.each { |chan, userlist| userlist.puts context, data }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
require "../protocol/user"
|
||||
require "./broadcast"
|
||||
require "../controller/controller"
|
||||
|
||||
# TODO.
|
||||
# UserList is used to send message to a list of `Protocol::User`.
|
||||
#
|
||||
# ```
|
||||
# chan1 = UserList.new
|
||||
# chan1.users << user_joined
|
||||
# chan1.puts current_controller, crafted_message
|
||||
# ```
|
||||
class Crirc::UserList
|
||||
getter users : Array(Protocol::User)
|
||||
include Broadcast
|
||||
|
||||
def initialize
|
||||
@users = Array(Protocol::User)
|
||||
end
|
||||
|
||||
# TODO
|
||||
# NOTE: combine data+user
|
||||
# Broadcast a message to the users
|
||||
def puts(context : Controller::Controller, data)
|
||||
@users.each { |user| context.puts data }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,67 @@
|
|||
require "../network/client"
|
||||
require "../binding/handler"
|
||||
require "./controller"
|
||||
require "../broadcast/chan_list"
|
||||
require "./command/*"
|
||||
|
||||
class Crirc::Controller::Client
|
||||
include Crirc::Controller::Command::Ping
|
||||
include Crirc::Controller::Command::User
|
||||
include Crirc::Controller::Command::Talk
|
||||
include Crirc::Controller::Command::Chan
|
||||
|
||||
include Controller
|
||||
# It has to be the last module included so initialize { super() } works
|
||||
include Binding::Handler
|
||||
|
||||
getter network : Network::Client
|
||||
|
||||
# TODO Not used yet
|
||||
getter chanlist : ChanList
|
||||
|
||||
# delegated to the `Network`
|
||||
delegate nick, to: :network
|
||||
# delegated to the `Network`
|
||||
delegate puts, to: :network
|
||||
# delegated to the `Network`
|
||||
delegate gets, to: :network
|
||||
|
||||
# New `Client` that controls the given `Network`.
|
||||
def initialize(@network)
|
||||
super()
|
||||
@chanlist = ChanList.new
|
||||
end
|
||||
|
||||
# Initialize the connection with the IRC server (send pass, nick and user).
|
||||
def init
|
||||
puts "PASS #{@network.pass}" if @network.pass
|
||||
puts "NICK #{@network.nick.to_s}"
|
||||
puts "USER #{@network.user.to_s} \"#{@network.domain}\" \"#{@network.irc_server}\" :#{@network.realname.to_s}"
|
||||
end
|
||||
|
||||
# Start the callback when the server is ready to receive messages.
|
||||
#
|
||||
# ```
|
||||
# bot.on_ready do
|
||||
# amazing_stuff(bot)
|
||||
# end
|
||||
# ```
|
||||
def on_ready(&b) : Client
|
||||
self.on("001") { b.call }
|
||||
self
|
||||
end
|
||||
|
||||
# Reply to a given message with a privmsg.
|
||||
#
|
||||
# ```
|
||||
# bot.on("JOIN") do |msg, _|
|
||||
# nick = msg.source.source_nick
|
||||
# context.reply(msg, "Welcome to #{nick}")
|
||||
# end
|
||||
# ```
|
||||
def reply(msg, data)
|
||||
target = msg.argument_list.first
|
||||
target_object = (target[0] == '#' ? Crirc::Protocol::Chan : Crirc::Protocol::User).new(target).as(Crirc::Protocol::Target)
|
||||
self.privmsg(target_object, data)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
# The module `Command` is the scope for the IRC commands
|
||||
# defined in the standard.
|
||||
# Each of these commands is included by a `Controller`, so the `puts` function
|
||||
# is implemented by the `Controller`.
|
||||
module Crirc::Controller::Command
|
||||
abstract def puts(data)
|
||||
end
|
|
@ -0,0 +1,104 @@
|
|||
require "../command"
|
||||
|
||||
# Defines the IRC commands that are related to chans (join, part, ...).
|
||||
module Crirc::Controller::Command::Chan
|
||||
include Crirc::Controller::Command
|
||||
|
||||
# Format the chans to join: #chan1,#chan2 (works with users by the same way)
|
||||
protected def format_list(chans : Enumerable(Crirc::Protocol::Target)) : String
|
||||
chans.map { |c| c.name }.to_set.join(",")
|
||||
end
|
||||
|
||||
# Request to join the chan(s) "chans", with password(s) "passwords".
|
||||
# The passwords may be ignored if not needed.
|
||||
def join(chans : Enumerable(Crirc::Protocol::Chan), passwords : Enumerable(String) = [""])
|
||||
to_join = format_list(chans)
|
||||
passes = passwords.join(",")
|
||||
puts "JOIN #{to_join} #{passes}"
|
||||
end
|
||||
|
||||
# Overloads the join function for 1 chan.
|
||||
def join(chan : Crirc::Protocol::Chan, password : String = "")
|
||||
join({chan}, {password})
|
||||
end
|
||||
|
||||
# Request to leave the channel(s) "chans", with an optional part message "msg".
|
||||
def part(chans : Enumerable(Crirc::Protocol::Chan), msg : String? = nil)
|
||||
to_leave = format_list(chans)
|
||||
reason = ":#{msg}" if msg
|
||||
puts "PART #{to_leave} #{reason}"
|
||||
end
|
||||
|
||||
# Overloads the part function for 1 chan.
|
||||
def part(chan : Crirc::Protocol::Chan, msg : String? = nil)
|
||||
part({chan}, msg)
|
||||
end
|
||||
|
||||
# Request to change the mode of the given channel.
|
||||
# If the mode is to be applied to an user, precise it.
|
||||
def mode(chan : Crirc::Protocol::Chan, flags : String, user : Crirc::Protocol::User? = nil)
|
||||
target = user ? user.name : ""
|
||||
puts "MODE #{chan.name} #{flags} #{target}"
|
||||
end
|
||||
|
||||
# Request to change the topic of the given channel.
|
||||
# If no topic is given, requests the topic of the given channel.
|
||||
def topic(chan : Crirc::Protocol::Chan, msg : String? = nil)
|
||||
topic = ":#{msg}" if msg
|
||||
puts "TOPIC #{chan.name} #{topic}"
|
||||
end
|
||||
|
||||
# Request the names of the users in the given channel(s).
|
||||
# If no channel is given, requests the names of the users in every
|
||||
# known channel.
|
||||
def names(chans : Enumerable(Crirc::Protocol::Chan)?)
|
||||
target = format_list(chans) if chans
|
||||
puts "NAMES #{target}"
|
||||
end
|
||||
|
||||
# Overloads the names function for 1 chan.
|
||||
def names(chan : Crirc::Protocol::Chan)
|
||||
names({chan})
|
||||
end
|
||||
|
||||
# List the channels and their topics.
|
||||
# If the chans parameter is given, lists the status of the given chans.
|
||||
def list(chans : Enumerable(Crirc::Protocol::Chan?)?)
|
||||
target = format_list(chans) if chans
|
||||
puts "LIST #{target}"
|
||||
end
|
||||
|
||||
# Overloads the list function for 1 chan.
|
||||
def list(chan : Crirc::Protocol::Chan)
|
||||
puts "LIST #{chan.name}"
|
||||
end
|
||||
|
||||
# Invite the user "user" to the channel "chan".
|
||||
def invite(chan : Crirc::Protocol::Chan, user : Crirc::Protocol::User)
|
||||
puts "INVITE #{user.name} #{chan.name}"
|
||||
end
|
||||
|
||||
# Kick the users users from the channels chans.
|
||||
# The reason of the kick will be displayed if given as a parameter.
|
||||
def kick(chans : Enumerable(Crirc::Protocol::Chan), users : Enumerable(Crirc::Protocol::User), msg : String? = nil)
|
||||
chan = format_list(chans)
|
||||
targets = format_list(users)
|
||||
reason = ":#{msg}" if msg
|
||||
puts "KICK #{chan} #{targets} #{reason}"
|
||||
end
|
||||
|
||||
# Overloads the kick function for several chans, one user.
|
||||
def kick(chans : Enumerable(Crirc::Protocol::Chan), user : Crirc::Protocol::User, msg : String? = nil)
|
||||
kick(chans, {user}, msg)
|
||||
end
|
||||
|
||||
# Overloads the kick function for one chan, several users.
|
||||
def kick(chan : Crirc::Protocol::Chan, users : Enumerable(Crirc::Protocol::User), msg : String? = nil)
|
||||
kick({chan}, users, msg)
|
||||
end
|
||||
|
||||
# Overloads the kick function for one chan, one user.
|
||||
def kick(chan : Crirc::Protocol::Chan, user : Crirc::Protocol::User, msg : String? = nil)
|
||||
kick({chan}, {user}, msg)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
require "../command"
|
||||
|
||||
# Defines the IRC ping.
|
||||
module Crirc::Controller::Command::Ping
|
||||
include Crirc::Controller::Command
|
||||
|
||||
# Send a ping to check if the other end is alive
|
||||
def ping(msg : String? = "0")
|
||||
puts "PING :#{msg}"
|
||||
end
|
||||
|
||||
# Answer to a ping command with a pong
|
||||
def pong(msg : String? = "0")
|
||||
puts "PONG :#{msg}"
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
require "../command"
|
||||
|
||||
# Defines the IRC commands related to communicating between "humans".
|
||||
module Crirc::Controller::Command::Talk
|
||||
include Crirc::Controller::Command
|
||||
|
||||
# Send a notice to a target
|
||||
def notice(target : Crirc::Protocol::Target, msg : String)
|
||||
puts "NOTICE #{target.name} :#{msg}"
|
||||
end
|
||||
|
||||
# Send a message to a chan or an user
|
||||
def privmsg(target : Crirc::Protocol::Target, msg : String)
|
||||
puts "PRIVMSG #{target.name} :#{msg}"
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
require "../command"
|
||||
|
||||
# Defines the IRC commands related to the users (whois, mode).
|
||||
module Crirc::Controller::Command::User
|
||||
include Crirc::Controller::Command
|
||||
|
||||
# Request data about a given target
|
||||
def whois(target : Crirc::Protocol::Target)
|
||||
puts "WHOIS #{target.name}"
|
||||
end
|
||||
|
||||
# Request data about the target who used to have the given name
|
||||
def whowas(target : Crirc::Protocol::Target)
|
||||
puts "WHOWAS #{target.name}"
|
||||
end
|
||||
|
||||
# Request to set the mode of the user.
|
||||
# NOTE : only the user itself can change it
|
||||
# NOTE : it should also be possible to send empty flags to get the mode
|
||||
def mode(target : Crirc::Protocol::Target, flags : String)
|
||||
puts "MODE #{target.name} #{flags}"
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# A `Controller` is controlling a `Network`.
|
||||
# It is in charge to manage IRC messages at the IRC protocol level.
|
||||
module Crirc::Controller
|
||||
# Interface implemented by every `Crirc::Controller`
|
||||
module Controller
|
||||
abstract def puts(data)
|
||||
abstract def gets
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
require "../network/server"
|
||||
require "./controller"
|
||||
|
||||
# TODO
|
||||
class Crirc::Controller::Server
|
||||
include Controller
|
||||
|
||||
getter network : Network::Server
|
||||
|
||||
delegate puts, to: :network
|
||||
delegate gets, to: :network
|
||||
|
||||
def initialize(@network)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
require "../network/server_client"
|
||||
require "./controller"
|
||||
|
||||
# TODO
|
||||
# Handles the clients connected to a `Server`
|
||||
class Crirc::Controller::ServerClient
|
||||
include Controller
|
||||
|
||||
getter network : Network::ServerClient
|
||||
|
||||
delegate puts, to: :network
|
||||
delegate gets, to: :network
|
||||
|
||||
def initialize(@network)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,75 @@
|
|||
require "socket"
|
||||
require "openssl"
|
||||
require "../controller/client"
|
||||
require "./network"
|
||||
|
||||
class Crirc::Network::Client
|
||||
include Network
|
||||
|
||||
alias IrcSocket = TCPSocket | OpenSSL::SSL::Socket::Client
|
||||
|
||||
getter nick : String
|
||||
getter ip : String
|
||||
getter port : UInt16
|
||||
getter ssl : Bool
|
||||
@socket : IrcSocket?
|
||||
getter user : String
|
||||
getter realname : String
|
||||
getter domain : String?
|
||||
getter pass : String?
|
||||
getter irc_server : String?
|
||||
getter read_timeout : UInt16
|
||||
getter write_timeout : UInt16
|
||||
getter keepalive : Bool
|
||||
|
||||
# default port is 6667 or 6697 if ssl is true
|
||||
def initialize(@nick : String, @ip, port = nil.as(UInt16?), @ssl = true, user = nil, realname = nil, @domain = nil, @pass = nil, @irc_server = nil,
|
||||
@read_timeout = 120_u16, @write_timeout = 5_u16, @keepalive = true)
|
||||
@port = port.to_u16 || (ssl ? 6697_u16 : 6667_u16)
|
||||
@user = user || @nick
|
||||
@realname = realname || @nick
|
||||
@domain ||= "0"
|
||||
@irc_server ||= "*"
|
||||
end
|
||||
|
||||
def socket
|
||||
raise "Socket is not set. Add `client.connect()` before using `client.socket`" if @socket.nil?
|
||||
@socket.as(IrcSocket)
|
||||
end
|
||||
|
||||
# Connect to the target server
|
||||
def connect
|
||||
tcp_socket = TCPSocket.new(@ip, @port)
|
||||
tcp_socket.read_timeout = @read_timeout
|
||||
tcp_socket.write_timeout = @write_timeout
|
||||
tcp_socket.keepalive = @keepalive
|
||||
@socket = tcp_socket
|
||||
@socket = OpenSSL::SSL::Socket::Client.new(tcp_socket) if @ssl
|
||||
self
|
||||
end
|
||||
|
||||
# Start a new Controller::Client binded to the current object
|
||||
def start(&block)
|
||||
controller = Controller::Client.new(self)
|
||||
controller.init
|
||||
yield controller
|
||||
end
|
||||
|
||||
# Wait and fetch the next incoming message
|
||||
def gets
|
||||
socket.gets
|
||||
end
|
||||
|
||||
# Send a message to the server
|
||||
def puts(data)
|
||||
socket.puts data.strip
|
||||
socket.puts "\r\n"
|
||||
socket.flush
|
||||
end
|
||||
|
||||
# End the connection
|
||||
def close
|
||||
socket.close
|
||||
@socket = nil
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# A `Network` is controlling a I/O.
|
||||
# It is in charge to manage TCP messages at the TCP protocol level.
|
||||
module Crirc::Network
|
||||
# Interface implemented by every `Crirc::Network`.
|
||||
module Network
|
||||
abstract def puts(data)
|
||||
abstract def gets
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
class Crirc::Network::Server
|
||||
def initialize
|
||||
end
|
||||
|
||||
# Listen on the port
|
||||
def start
|
||||
end
|
||||
|
||||
# Wait for a client and yield it
|
||||
def accept
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
require "./network"
|
||||
|
||||
class Crirc::Network::ServerClient
|
||||
include Network
|
||||
|
||||
def initialize
|
||||
end
|
||||
|
||||
# Wait and fetch the next incoming message
|
||||
def gets
|
||||
end
|
||||
|
||||
# Send a message to the server
|
||||
def puts(data)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,32 @@
|
|||
require "./target"
|
||||
|
||||
# Represents an IRC channel.
|
||||
class Crirc::Protocol::Chan < Crirc::Protocol::Target
|
||||
class Motd
|
||||
getter message : String
|
||||
getter user : String
|
||||
getter timestamp : Int64
|
||||
|
||||
def initialize(@message, @user)
|
||||
@timestamp = Time.utc.to_unix
|
||||
end
|
||||
|
||||
def set_motd(@message, @user)
|
||||
@timestamp = Time.utc.to_unix
|
||||
end
|
||||
end
|
||||
|
||||
getter name : String
|
||||
property modes : String
|
||||
property motd : Motd
|
||||
|
||||
def initialize(@name)
|
||||
raise ParsingError.new "The Chan name (#{@name}) must not be empty" if @name.empty?
|
||||
raise ParsingError.new "The Chan name (#{@name}) must begin with a \"#\"" if !@name.match(/\A\#.+\Z/)
|
||||
raise ParsingError.new "The Chan name (#{@name}) must contains at most 63 valid characters" if !@name.match(/\A(?!.{51,})(\#\#?([^[:space:],]+))\Z/)
|
||||
@modes = ""
|
||||
@motd = Motd.new("", "")
|
||||
end
|
||||
|
||||
class ParsingError < Exception; end
|
||||
end
|
|
@ -0,0 +1,63 @@
|
|||
# `Message` is the object that parse the raw TCP body as a IRC message.
|
||||
#
|
||||
# Message are a IRC core part. They contain a command, the arguments, and
|
||||
# the message (last argument in the IRC protocol).
|
||||
# TODO: improve the message to appear in as the last argument. cf: fast_irc
|
||||
class Crirc::Protocol::Message
|
||||
# Raw message without parsing
|
||||
getter raw : String
|
||||
|
||||
# Source of the message (ex: "0", "abc@xyz", ...)
|
||||
getter source : String
|
||||
|
||||
# The command ("PRIVMSG", "PING", ...)
|
||||
getter command : String
|
||||
|
||||
# The arguments as a string ("user1 +0", "pingmessage", ...)
|
||||
getter arguments : String?
|
||||
|
||||
# The last argument when ":" ("This is a privmsg message", ...)
|
||||
getter message : String?
|
||||
|
||||
R_SRC = "(\\:(?<src>[^[:space:]]+) )"
|
||||
R_CMD = "(?<cmd>[A-Z]+|\\d{3})"
|
||||
R_ARG_ONE = "(?:[^: ][^ ]*)"
|
||||
R_ARG = "(?: (?<arg>#{R_ARG_ONE}(?: #{R_ARG_ONE})*))"
|
||||
R_MSG = "(?: \\:(?<msg>.+)?)"
|
||||
|
||||
def initialize(@raw)
|
||||
m = raw.strip.match(/\A#{R_SRC}?#{R_CMD}#{R_ARG}?#{R_MSG}?\Z/)
|
||||
raise ParsingError.new "The message (#{@raw}) is invalid" if m.nil?
|
||||
@source = m["src"]? || "0"
|
||||
@command = m["cmd"] # ? || raise InvalidMessage.new("No command to parse in \"#{raw}\"")
|
||||
@arguments = m["arg"]?
|
||||
@message = m["msg"]?
|
||||
end
|
||||
|
||||
# Concatenation of `arguments` and `message`.
|
||||
# If the message exists, it is preceded by ':'
|
||||
#
|
||||
# ```
|
||||
# msg.raw_arguments # => "user1 +0 :do something"
|
||||
# ```
|
||||
def raw_arguments : String
|
||||
return "" if @arguments.nil? && @message.nil?
|
||||
return @arguments.to_s if @message.nil?
|
||||
return ":#{@message}" if @arguments.nil?
|
||||
return "#{@arguments} :#{@message}"
|
||||
end
|
||||
|
||||
# The arguments formated into an Array.
|
||||
#
|
||||
# ```
|
||||
# msg.argument_list # => ["user1", "+0"]
|
||||
# ```
|
||||
def argument_list : Array(String)
|
||||
return Array(String).new if @arguments.nil? && @message.nil?
|
||||
return (@arguments.as(String)).split(" ") if @message.nil?
|
||||
return [@message.as(String)] if @arguments.nil?
|
||||
return (@arguments.as(String)).split(" ") << (@message.as(String))
|
||||
end
|
||||
|
||||
class ParsingError < Exception; end
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
# A target is a virtuel IRC entity that can receive message (`User`, `Chan`).
|
||||
abstract class Crirc::Protocol::Target
|
||||
abstract def name : String
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
require "./target"
|
||||
|
||||
# A User repretents a IRC client.
|
||||
#
|
||||
# It has a nick, id and whois.
|
||||
class Crirc::Protocol::User < Crirc::Protocol::Target
|
||||
property nick : String
|
||||
getter id : String
|
||||
getter whois : String?
|
||||
|
||||
CHARS_SPECIAL = "[_\\|\\[\\]\\\\\\`\\^\\{\\}]"
|
||||
CHARS_ALPHA = "[a-zA-Z]"
|
||||
CHARS_NUM = "[0-9\\-]"
|
||||
CHARS_FIRST = "#{CHARS_SPECIAL}|#{CHARS_ALPHA}"
|
||||
CHARS_NEXT = "#{CHARS_FIRST}|#{CHARS_NUM}"
|
||||
|
||||
def initialize(@nick, id = nil, @whois = nil)
|
||||
raise ParsingError.new "The user nick (#{@nick}) must not be empty" if @nick.empty?
|
||||
raise ParsingError.new "The user nick (#{@nick}) must contains at most 50 valid characters" if !@nick.match(/\A(?!.{51,})((#{CHARS_FIRST})((#{CHARS_NEXT})+))\Z/)
|
||||
@id = id || @nick
|
||||
end
|
||||
|
||||
def self.parse(source : String) : User
|
||||
m = source.match(/\A:?(?<name>[^!]+)!(?<id>.+)@(?<whois>.+)\Z/)
|
||||
raise ParsingError.new "The source (#{source}) is not a valid user" if m.nil?
|
||||
User.new m["name"], m["id"], m["whois"]
|
||||
end
|
||||
|
||||
def name : String
|
||||
@nick
|
||||
end
|
||||
|
||||
class ParsingError < Exception; end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
module Crirc
|
||||
VERSION = "0.4.0"
|
||||
end
|
39
src/dash.cr
39
src/dash.cr
|
@ -1,39 +0,0 @@
|
|||
require "./CrystalIrc"
|
||||
|
||||
module DashBot
|
||||
def start
|
||||
bot = CrystalIrc::Bot.new ip: "irc.mozilla.org", nick: "Dasshy", read_timeout: 300_u16
|
||||
|
||||
bot.on("JOIN") do |msg|
|
||||
if msg.hl == bot.nick.to_s
|
||||
msg.reply "Welcome everypony, what's up ? :)"
|
||||
else
|
||||
STDERR.puts "[#{Time.now}] #{msg.hl} joined the chan"
|
||||
end
|
||||
end.on("PING") do |msg|
|
||||
bot.pong(msg.message)
|
||||
end.on("PRIVMSG", message: /^!ping/) do |msg|
|
||||
msg.reply "pong #{msg.hl} (#{msg.source.to_s.source_id})"
|
||||
end
|
||||
|
||||
bot.connect.on_ready do
|
||||
bot.join("#equilibre")
|
||||
end
|
||||
|
||||
loop do
|
||||
begin
|
||||
bot.gets do |m|
|
||||
break if m.nil?
|
||||
STDERR.puts "[#{Time.now}] #{m}"
|
||||
spawn { bot.handle(m.as(String)) }
|
||||
end
|
||||
rescue IO::Timeout
|
||||
puts "Nothing happened..."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
extend self
|
||||
end
|
||||
|
||||
DashBot.start
|
|
@ -0,0 +1,51 @@
|
|||
require "./crirc"
|
||||
|
||||
# Extracts the nick from the full address of a user (nick!name@host)
|
||||
def extract_nick(address : String)
|
||||
address.split('!')[0]
|
||||
end
|
||||
|
||||
private def bind_example(bot)
|
||||
bot.on_ready do
|
||||
# Join the default chan when the bot is connected
|
||||
bot.join Crirc::Protocol::Chan.new("#equilibre2")
|
||||
end.on("JOIN") do |msg|
|
||||
# Greet message on join
|
||||
if extract_nick(msg.source) == bot.nick
|
||||
bot.reply msg, "Hello, world!"
|
||||
end
|
||||
end.on("PING") do |msg|
|
||||
# Server pong
|
||||
bot.pong(msg.message)
|
||||
end.on("PRIVMSG", message: /^!ping */, doc: {"!ping", "the bot respond by `pong nick`"}) do |msg|
|
||||
# !ping command : answer !pong to the user
|
||||
chan = msg.arguments if msg.arguments
|
||||
bot.reply msg, "pong #{extract_nick msg.source}" if chan
|
||||
end.on(message: /^!help *$/, doc: {"!help", "`!help` to list the modules\n`!help cmd` to advanced description of the cmd"}) do |msg|
|
||||
# take each documented bind (those with on("...", doc: ....)) and send the short documentation
|
||||
bot.reply msg, bot.docs.keys.join(", ")
|
||||
end.on(message: /^!help *(.*[^ ]) *$/) do |msg, match|
|
||||
# take one documented bind (those with on("...", doc: ....)) and send the long documentation
|
||||
doc = bot.docs[match.as(Regex::MatchData)[1]]?
|
||||
doc.split("\n").each { |split| bot.reply msg, split } unless doc.nil?
|
||||
end
|
||||
end
|
||||
|
||||
client = Crirc::Network::Client.new "Crircbot", "irc.mozilla.org", 6667, ssl: false
|
||||
client.connect
|
||||
client.start do |bot|
|
||||
bind_example bot
|
||||
loop do
|
||||
begin
|
||||
m = bot.gets
|
||||
puts "> #{m}"
|
||||
break if m.nil?
|
||||
spawn { bot.handle(m.as(String)) }
|
||||
rescue error
|
||||
puts error
|
||||
sleep 0.1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
client.close
|
|
@ -0,0 +1,7 @@
|
|||
require "./crirc"
|
||||
|
||||
|
||||
server = Crirc::Network::Server.new "0.0.0.0", 6667, ssl: false
|
||||
server.start
|
||||
|
||||
client.close
|
Loading…
Reference in New Issue