summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AUTHORS20
-rw-r--r--ChangeLog121
-rw-r--r--GPL2.txt340
-rw-r--r--GPL3.txt674
-rw-r--r--HACKING25
-rw-r--r--INSTALL16
-rw-r--r--Makefile146
-rw-r--r--README104
-rw-r--r--TODO5
-rw-r--r--announcements/people/aurelien/phrases2
-rw-r--r--announcements/people/aurelien/seen0
-rw-r--r--announcements/people/aurelien/settings2
-rw-r--r--announcements/people/fauno/phrases9
-rw-r--r--announcements/people/fauno/seen0
-rw-r--r--announcements/people/fauno/settings2
-rw-r--r--announcements/people/mtjm/phrases1
-rw-r--r--announcements/people/mtjm/seen0
-rw-r--r--announcements/people/mtjm/settings2
-rw-r--r--announcements/people/xylon/phrases1
-rw-r--r--announcements/people/xylon/seen0
-rw-r--r--announcements/people/xylon/settings2
-rw-r--r--bot_settings.sh296
-rw-r--r--bot_settings.sh.example296
-rw-r--r--bug_tracker_change_detector68
-rw-r--r--contrib/README6
-rw-r--r--contrib/init-scripts/envbot.debian48
-rw-r--r--contrib/modules/m_bugzilla.sh231
-rw-r--r--contrib/modules/m_calc.sh74
-rw-r--r--contrib/modules/m_convert.sh117
-rw-r--r--contrib/modules/m_eix.sh101
-rw-r--r--contrib/modules/m_eval.sh55
-rw-r--r--contrib/modules/m_helloworld.sh193
-rw-r--r--contrib/modules/m_perl/__main__.sh74
-rw-r--r--contrib/modules/m_perl/safe_eval.pl25
-rw-r--r--data/faq.txt.example4
-rw-r--r--data/quotes.txt.example.pqf570
-rw-r--r--doc/ConfigChanges.txt17
-rw-r--r--doc/bot_settings.sh.example.in296
-rw-r--r--doc/coding-standards.txt100
-rw-r--r--doc/envbot.152
-rw-r--r--doc/factoids.sql15
-rw-r--r--doc/karma.sql14
-rw-r--r--doc/module_api2.txt383
-rw-r--r--doc/seen.sql15
-rw-r--r--doc/transports_api.txt101
-rwxr-xr-xenvbot42
-rw-r--r--hack_of_all_hacks165
-rw-r--r--lib/access.sh99
-rw-r--r--lib/channels.sh125
-rw-r--r--lib/commands.sh265
-rw-r--r--lib/config.sh219
-rw-r--r--lib/debug.sh84
-rw-r--r--lib/feedback.sh59
-rw-r--r--lib/hack_of_all_hacks54
-rw-r--r--lib/hash.sh312
-rw-r--r--lib/log.sh285
-rw-r--r--lib/main.sh589
-rw-r--r--lib/misc.sh267
-rw-r--r--lib/modules.sh447
-rw-r--r--lib/numerics.sh348
-rw-r--r--lib/parse.sh100
-rw-r--r--lib/send.sh178
-rw-r--r--lib/server.sh337
-rw-r--r--lib/time.sh110
-rw-r--r--logs/1318590352/main.log12
-rw-r--r--logs/1318590811/main.log12
-rw-r--r--logs/1318591023/main.log12
-rw-r--r--logs/1318593271/main.log12
-rw-r--r--logs/1318593379/main.log12
-rw-r--r--logs/1318593584/main.log12
-rw-r--r--logs/1318593636/main.log12
-rw-r--r--modules/m_assign_mode.sh228
-rw-r--r--modules/m_autojoin.sh63
-rw-r--r--modules/m_check_numerics.sh45
-rw-r--r--modules/m_commands.sh103
-rw-r--r--modules/m_ctcp.sh91
-rw-r--r--modules/m_dice.sh84
-rw-r--r--modules/m_die.sh75
-rw-r--r--modules/m_dumpvars.sh54
-rw-r--r--modules/m_factoids.sh461
-rw-r--r--modules/m_faq.sh138
-rw-r--r--modules/m_help.sh151
-rw-r--r--modules/m_join.sh92
-rw-r--r--modules/m_karma.sh252
-rw-r--r--modules/m_kick_ban.sh145
-rw-r--r--modules/m_modules.sh178
-rw-r--r--modules/m_nicktracking.sh316
-rw-r--r--modules/m_ping.sh87
-rw-r--r--modules/m_quote.sh85
-rw-r--r--modules/m_rehash.sh74
-rw-r--r--modules/m_say.sh98
-rw-r--r--modules/m_seen.sh209
-rw-r--r--modules/m_sendraw.sh53
-rw-r--r--modules/m_services.sh87
-rw-r--r--modules/m_sqlite3.sh89
-rw-r--r--modules/m_umodes.sh63
-rw-r--r--modules/m_uptime.sh56
-rw-r--r--tools/bashdoc/ChangeLog22
-rwxr-xr-xtools/bashdoc/bashdoc.sh726
-rwxr-xr-xtools/build_numerics.sh102
-rw-r--r--tools/numerics.txt215
-rw-r--r--transport/dev-tcp.sh122
-rw-r--r--transport/gnutls.sh121
-rw-r--r--transport/netcat.sh130
-rw-r--r--transport/openssl.sh126
-rw-r--r--transport/socat.sh184
106 files changed, 13917 insertions, 0 deletions
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..433a68a
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,20 @@
+Main developer:
+ Arvid Norlander <anmaster [AT] sourceforge no spam DOT net>
+
+Other developers:
+ EmErgE
+
+Thanks to:
+ Remo Ford <remoford [AT] gmail no spam DOT com>
+ for helping with testing and many good ideas as well as some contributions.
+ likewhoa
+ for providing domain name.
+ Cyrus Lopez
+ for providing hosting.
+ Matt Richards (eggy)
+ for testing and finding bugs.
+ Vsevolod Kozlov
+ Thanks for contributions in core and modules.
+
+Guy who hacked it up and made it into pbot-ng:
+ Joseph Graham <joe@t67.eu> (hacker alias xylon)
diff --git a/ChangeLog b/ChangeLog
new file mode 100644
index 0000000..e408d68
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,121 @@
+Hi I'm not maintaining this file so I leave the rest verbatim. --xylon
+
+ChangeLog
+=========
+This is a overview of changes users may care about. Detailed changelog can be
+done using bzr (does not work in tarballs, you need a development checkout for this):
+ bzr log -rtag:tag.. --short
+Example (list changes compared to 0.1-beta1)
+ bzr log -rtag:0.1-beta1.. --short
+
+
+0.1-beta1
+---------
+A lot is new in this release. Some of the highlights include:
+ * Help command.
+ * Coloured log output.
+ * Modules that is split over several files in a subdirectory.
+ See contrib/modules/m_perl for an example.
+ * Lots of speed improvements by avoiding subshells when possible and such.
+ * Checks in subshells that module loading will work before actually loading it.
+ This is to avoid crashes on syntax errors in modules and such.
+ * A new centralised bot command dispatching system that is easier to use
+ than the old on_PRIVMSG and having every module parse it by itself.
+ * API documentation is now auto generated from source code using bashdoc.
+ * Extended and reworked internal API.
+ * New more flexible module API.
+ * Many new modules.
+ * Core:
+ * commands - List commands from each module.
+ * ctcp - Responds to CTCPs send to the bot.
+ * dice - Roll a dice.
+ * help - Help command.
+ * karma - Karma module (the common ++/-- stuff)
+ * nicktracking - tracks nicks <-> host mapping for channels the bot is in.
+ * ping - Provide some latency info and such.
+ * uptime - Tell the uptime of the bot.
+ * Contrib (these either need extra deps or are not supported by the developers):
+ * convert - Convert between different units (needs GNU or BSD units installed).
+ * perl - Run perl code (not supported because it may not be safe).
+ * Lots of bugfixes.
+
+The rest of this change log entry is mainly for developers wanting to port their
+code from 0.0.1 to 0.1:
+ * 0.0.1 modules won't work without change, the both the module and core APIs
+ have changed a lot. For example on_PRIVMSG should not be used now most of the
+ time, instead see commands_register function.
+ See doc/module_api2.txt and the auto generated API docs for details.
+ * Quite a few Core API functions have changed parameter format:
+ A lot now take "out parameters" instead of using $() construct. This is
+ because subshells (caused by the $() construct) are slow.
+ * Sadly one feature from trunk were too unstable to make it into 0.1, it will
+ hopefully be in the next version. This feature was periodic events. This is
+ the reason for some odd code related to transports status in this version
+ ($transport_status and transport_alive to be specific).
+
+
+0.0.1
+-----
+475 Updated man page.
+472,474 Fixed bash version check.
+471 Fixed an incorrect regular expression in lib/main.sh
+470 Made list_contains use grep -F instead.
+469 Fixed typo in ebuild.
+
+
+0.0.1-rc1
+---------
+465 Backported fix to remove eval from various places.
+463-464 Fixed bug in modules/m_kick_ban.sh that made bot loose part of kick reason sometimes.
+462 Fixed bug in modules/m_join.sh that caused it to never send a reason on part.
+459-461 Added Gentoo ebuild for envbot.
+457 Made it work correctly on FreeBSD
+456 Fixed bug 29 (Channels not rejoined after ping timeout) and similar issue in modules/m_services.sh (ghost needed state not reset).
+453 Fixed for one aspect of bug 11.
+
+
+0.0.1-beta5
+-----------
+450 Merged r451 from trunk: Fix broken misc_clean_spaces (was broken after beta4).
+443 Made parse_hostmask_*() faster and added some missing quotes.
+441 Found that an INVITE hook was missing, added it.
+430-435 Added more numerics.
+429 Workaround for bug #21 added (Own nick desync during connect with ghost).
+
+
+0.0.1-beta4
+-----------
+424 Config version update: Added $config_log_raw. Defines if we should log raw lines or not (affects both STDOUT and logfiles)
+420,423 misc_clean_spaces was slow, changed to inline ways of stripping spaces.
+416 Got rid of deprecated parse_get_colon_arg function.
+415 Added some code to check for stuff that matches no hook, and found that there is no umode change hook because of this. Fixed.
+411 Made assignment of factoids work if there is more than one separator in string, yes this may be slower but it works.
+410 Fixed bug in factoids with forget command not reporting "I didn't have a factoid matching..." when none was found.
+
+
+0.0.1-beta3
+-----------
+407 Make error messages in transports more verbose.
+402-406 Make m_calc.sh check it's arguments on IRC better to make it secure.
+401 Fix bug that made modules_load break if a dependency failed to load.
+398 Made main envbot a wrapper script that sanitise the environment and then executes main bot.
+395 Some more functions are now internal in lib/log.sh, modules should use the log level versions.
+393 m_autojoin was slowing down rehash more than needed, fixed that.
+392 Fixed: Could not unload rehash, module_rehash_UNLOAD returned 127!
+391 Fixed bug that caused FINALISE hooks to never run
+383 Add bash version check (it turned out bash-3.1 and older was not supported).
+
+
+0.0.1-beta2
+-----------
+379 Actually more than one space between parameters on IRC is possible, yes that sucks, and no sane server would send it, but just to be on the safe side
+377 Added this ChangeLog.
+376 Fixed potential security problem in SQL cleaning code in module_sqlite3_clean_string (I can't find a way to abuse this so I don't think the risk is very high)
+375 Added access_log_action()
+373-375 Introduced more advanced logging API with several levels (info, warn, error and fatal)
+371 Optimised log_write() in lib/log.sh (something like 20 times as fast now if my testing is correct)
+
+
+0.0.1-beta1
+-----------
+First version, no ChangeLog as there is nothing to older to list changes against.
diff --git a/GPL2.txt b/GPL2.txt
new file mode 100644
index 0000000..3912109
--- /dev/null
+++ b/GPL2.txt
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ 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
+this service 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.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+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
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ 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
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/GPL3.txt b/GPL3.txt
new file mode 100644
index 0000000..94a9ed0
--- /dev/null
+++ b/GPL3.txt
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is 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. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ 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.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ 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 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. Use with the GNU Affero General Public License.
+
+ 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 Affero 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 special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU 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 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 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 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 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ 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 GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/HACKING b/HACKING
new file mode 100644
index 0000000..4422a56
--- /dev/null
+++ b/HACKING
@@ -0,0 +1,25 @@
+Hi, this file was made by xyon.
+
+Basically I made this bot by getting envbot and hacking it. When I first ran
+envbot it didn't do anything and I couldn't be bothered to read the
+documentation of how to use/add modules so i just opened up the main file and
+started hacking it up.
+
+I modified lib/main.sh to source `hack_of_all_hacks' file once per minute and
+I put all the code in `l33t_codes' function in that file. So you can modify/add
+more code without having to restart the bot.
+
+Originally the bot was kind of; event driven, it could only respond to stuff
+that happens in the channel but couldn't say stuff `out of the blue', so in
+order to add the bug tracker announcer feature I had to make some fundamental
+changes to stuff, so I hacked up transport/dev-tcp.sh. There are several
+transport modes, each in a different file in that dir, designed to work on
+different systems, this is the one it uses on my deb server. If you wanted to
+run pbot-ng on a system that needs a different transport mode then the `saying
+stuff out of the blue' functionality would break unless the relevent transport
+file was modified also.
+
+Also some of the settings in bot_settings.sh won't work because I kind of
+hard-coded some things like #parabola into the `hack_of_all_hacks' file.
+
+To run the bot do: ./envbot & ./bug_tracker_change_detector
diff --git a/INSTALL b/INSTALL
new file mode 100644
index 0000000..a7fbf4a
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,16 @@
+Installing envbot
+=================
+
+Installation isn't needed, you can run it from source directory.
+Installation is possible though, but not recommended.
+
+All you have to do is to get the bot running is:
+* Run:
+ make
+* Copy bot_settings.sh.example to bot_settings.sh
+* Edit bot_settings.sh to set correct nick, IRC server, service password, and so on.
+* Run:
+ ./envbot
+
+To install use something like (change the paths to fit your system):
+make install DESTDIR=/ PREFIX=/home/user/envbot
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..5a07f2b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+# This file is used to generate some, uh, generated files.
+# Also some other tasks
+
+# Useful targets:
+# all: Builds numerics file and generates example config
+# man: Generates man page using help2man
+# apidocs: Generates API docs from code for public functions
+# apidocs-all: Generates API docs from code for public and internal functions.
+# install: Installs to a DESTDIR (don't confuse with DISTDIR),
+# See variables below.
+# dist-dir: Generates a clean checkout of current version, ready to be
+# tared up. Can only be done in a bzr branch/checkout
+
+ENVBOT_VERSION = 0.1-beta1
+
+# For make dest-dir, defaults
+DISTDIR ?= dist
+# For make install, defaults
+DESTDIR ?= DEST
+PREFIX ?= /usr/local
+BINDIR ?= $(PREFIX)/bin
+CONFDIR ?= $(PREFIX)/etc
+LIBDIR ?= $(PREFIX)/lib
+DATADIR ?= $(PREFIX)/share
+MANDIR ?= $(DATADIR)/man
+# And now for actual place of stuff
+ENVBOT_LIBDIR ?= $(LIBDIR)/envbot
+ENVBOT_TRANSPORTDIR ?= $(ENVBOT_LIBDIR)/transport
+ENVBOT_LIBRARYDIR ?= $(ENVBOT_LIBDIR)/lib
+ENVBOT_MODULESDIR ?= $(ENVBOT_LIBDIR)/modules
+ENVBOT_DATADIR ?= $(DATADIR)/envbot/data
+ENVBOT_LOGDIR ?= $(DATADIR)/envbot/logs
+ENVBOT_CONFDIR ?= $(CONFDIR)/envbot
+ENVBOT_DOCDIR ?= $(DATADIR)/doc/envbot-$(ENVBOT_VERSION)
+
+# Now for some commands
+INSTALL ?= install -p
+SED ?= sed
+RM ?= rm
+
+all: numerics config
+
+config:
+ $(SED) "s|@@moddir@@|modules|;s|@@transportdir@@|transport|;s|@@datadir@@|data|;s|@@logdir@@|logs|" doc/bot_settings.sh.example.in > bot_settings.sh.example
+
+numerics:
+ tools/build_numerics.sh > lib/numerics.sh
+
+# Used by developers to update man page.
+man:
+ help2man -NS envbot -n 'An advanced modular IRC bot in bash' "/usr/bin/env bash envbot" > doc/envbot.1
+
+clean:
+ $(RM) -vf *~ */*~ */*/*~ */*/*/*~ bot_settings.sh.example
+
+cleandocs:
+ $(RM) -rf doc/api/private-core
+ $(RM) -rf doc/api/public-core
+ $(RM) -rf doc/api/private-modules
+ $(RM) -rf doc/api/public-modules
+
+cleanlogs:
+ $(RM) -vrf logs/*
+
+apidocs-private:
+ ./tools/bashdoc/bashdoc.sh -p "envbot Core API (private functions) for "$(ENVBOT_VERSION) -o doc/api/private-core lib/*.sh
+ ./tools/bashdoc/bashdoc.sh -p "envbot module-provided API (private functions) for "$(ENVBOT_VERSION) -o doc/api/private-modules modules/*.sh
+
+apidocs-public:
+ ./tools/bashdoc/bashdoc.sh -e "Type=API" -p "envbot Core API for "$(ENVBOT_VERSION) -o doc/api/public-core lib/*.sh
+ ./tools/bashdoc/bashdoc.sh -e "Type=API" -p "envbot module-provided API for "$(ENVBOT_VERSION) -o doc/api/public-modules modules/*.sh
+
+apidocs: apidocs-public
+
+apidocs-all: apidocs-private apidocs-public
+
+checkvars:
+ @if [ "$(ENV_USERNAME)" = "" ]; then \
+ echo "Please call this script with the ENV_USERNAME environment variable set"; \
+ exit 1; \
+ fi
+ @if [ "$(ENV_PATH)" = "" ]; then \
+ echo "Please call this script with the ENV_PATH environment variable set"; \
+ exit 1; \
+ fi
+
+apidocs-upload: checkvars
+ rsync -hhzcrv --progress --delete --stats -e ssh doc/api/ $(ENV_USERNAME)@envbot.org:$(ENV_PATH)/
+
+dist-dir:
+ $(RM) -rf $(DISTDIR)
+ bzr export $(DISTDIR)
+
+install: cleandocs all apidocs-public
+ @echo "#########################################################################"
+ @echo "# #"
+ @echo "# Installing... Note that running from source directory is recommended! #"
+ @echo "# #"
+ @echo "#########################################################################"
+ $(INSTALL) -d $(DESTDIR)$(PREFIX) $(DESTDIR)$(BINDIR)
+ $(INSTALL) -d $(DESTDIR)$(ENVBOT_LIBDIR) $(DESTDIR)$(ENVBOT_CONFDIR)
+ $(INSTALL) -d $(DESTDIR)$(ENVBOT_DATADIR) $(DESTDIR)$(ENVBOT_TRANSPORTDIR)
+ $(INSTALL) -d $(DESTDIR)$(ENVBOT_LIBRARYDIR) $(DESTDIR)$(ENVBOT_MODULESDIR)
+ $(INSTALL) -d $(DESTDIR)$(ENVBOT_DOCDIR) $(DESTDIR)$(MANDIR)/man1
+ $(INSTALL) -d $(DESTDIR)$(ENVBOT_LOGDIR) $(DESTDIR)$(ENVBOT_DOCDIR)/api
+ $(INSTALL) -d $(DESTDIR)$(ENVBOT_DOCDIR)/api/core $(DESTDIR)$(ENVBOT_DOCDIR)/api/modules
+ $(INSTALL) -m 644 lib/*.sh $(DESTDIR)$(ENVBOT_LIBRARYDIR)
+ $(INSTALL) -m 644 modules/*.sh $(DESTDIR)$(ENVBOT_MODULESDIR)
+ $(INSTALL) -m 644 transport/*.sh $(DESTDIR)$(ENVBOT_TRANSPORTDIR)
+ $(INSTALL) -m 644 README AUTHORS GPL3.txt $(DESTDIR)$(ENVBOT_DOCDIR)
+ $(INSTALL) -m 644 doc/*.sql $(DESTDIR)$(ENVBOT_DOCDIR)
+ $(INSTALL) -m 644 doc/*.txt $(DESTDIR)$(ENVBOT_DOCDIR)
+ $(INSTALL) -m 644 doc/api/public-core/*.html $(DESTDIR)$(ENVBOT_DOCDIR)/api/core/
+ $(INSTALL) -m 644 doc/api/public-core/*.css $(DESTDIR)$(ENVBOT_DOCDIR)/api/core/
+ $(INSTALL) -m 644 doc/api/public-modules/*.html $(DESTDIR)$(ENVBOT_DOCDIR)/api/modules/
+ $(INSTALL) -m 644 doc/api/public-modules/*.css $(DESTDIR)$(ENVBOT_DOCDIR)/api/modules/
+ $(INSTALL) -m 644 doc/envbot.1 $(DESTDIR)$(MANDIR)/man1
+ $(INSTALL) -m 644 data/{faq.txt.example,quotes.txt.example.pqf} $(DESTDIR)$(ENVBOT_DATADIR)
+ $(SED) "s|^library_dir=.*|library_dir='$(ENVBOT_LIBRARYDIR)'|;s|^config_file=.*|config_file='$(ENVBOT_CONFDIR)/bot_settings.sh'|" envbot > envbot.tmp
+ $(INSTALL) envbot.tmp $(DESTDIR)$(BINDIR)/envbot
+ $(RM) envbot.tmp
+ $(SED) "s|@@moddir@@|$(ENVBOT_MODULESDIR)|;s|@@transportdir@@|$(ENVBOT_TRANSPORTDIR)|;s|@@datadir@@|$(ENVBOT_DATADIR)|;s|@@logdir@@|$(ENVBOT_LOGDIR)|" doc/bot_settings.sh.example.in > bot_settings.tmp
+ $(INSTALL) -m 644 bot_settings.tmp $(DESTDIR)$(ENVBOT_CONFDIR)/bot_settings.sh.example
+ $(RM) bot_settings.tmp
+
+.PHONY: all apidocs apidocs-private apidocs-public checkvars apidocs-upload numerics clean cleanlogs cleandocs dist-dir
diff --git a/README b/README
new file mode 100644
index 0000000..afab6e8
--- /dev/null
+++ b/README
@@ -0,0 +1,104 @@
+Read the HACKING file to find out what changes xylon made to the bot, the rest
+of this file I leave verbatim.
+
+envbot - A modular IRC bot in bash
+==================================
+envbot is a modular IRC bot coded in bash.
+http://envbot.org/trac
+
+Features include:
+ * SSL
+ * IPv6
+ * Transport (SSL, whatever) to server are also modules
+ * Modularity
+ * Loading, unloading and reloading of modules at runtime
+ * Rehashing configuration at runtime
+ * Advanced access control
+See http://envbot.org/trac/query?status=new&status=assigned&status=reopened&type=enchantment&order=priority for other planed features
+
+
+Installing
+----------
+See the file INSTALL.
+
+
+License
+-------
+envbot is licensed under GPL version 3, with the exceptions of:
+ data/quotes.txt.example.pqf:
+ The source (games-misc/fortune-mod-pqf/fortune-mod-pqf-6.0.ebuild from Gentoo portage)
+ says it is under GPL-2.
+ tools/bashdoc/bashdoc.sh
+ GPL-2. bashdoc is a heavily updated and modified version of bashdoc from
+ the sourcemage project.
+
+Dependencies
+------------
+* bash - version 3.2.10 or later should work fine, but not tested on anything below 3.2.17
+* Standard POSIX tools. Should be included on any recent and sane Linux
+ distro.
+
+Some transports and module have extra dependencies. Note that when it says
+"you need the program" just having the library won't work. You actually need
+a program with this name.
+Transports:
+ dev-tcp:
+ The bash you use must support the pseudo device /dev/tcp. Debian is known
+ to disable this. Most other distros are sane and have it on.
+ netcat:
+ You need the program netcat.
+ This is for Debian users and others with a broken distro. If your
+ distro supports it use dev-tcp transport instead.
+ I have only tested with GNU netcat. (http://netcat.sourceforge.net/)
+ Supports binding to a specific IP.
+ gnutls:
+ You need the program gnutls-cli. (http://www.gnutls.org/)
+ openssl:
+ You need the program openssl. (http://www.openssl.org/)
+ socat:
+ You need the program socat. (http://www.dest-unreach.org/socat/)
+ Note that while socat support IPv4, IPv6, SSL and non-SSL it doesn't
+ support both SSL and IPv6 at the same time if the version of socat
+ is lower than 1.5.
+ Supports binding to a specific IP.
+Modules:
+ sqlite3
+ You need the program sqlite3. (http://www.sqlite.org/)
+ factoids
+ This depends on the sqlite3 module and therefore have the
+ same dependencies as it
+ seen
+ This depends on the sqlite3 module and therefore have the
+ same dependencies as it
+
+
+Contributed modules
+-------------------
+These are extra modules in contrib/modules. They are not really
+supported by the developers. Information about dependencies and
+extra configuration options is in each contrib module.
+
+If you want to use a contrib module the recommended way is:
+ cd modules
+ ln -s ../contrib/modules/m_modulename.sh
+That way if the contrib module is updated you will get the new
+version automatically.
+
+
+Feedback
+--------
+We (the developers) would love to get feedback on what you like/dislike with
+envbot, what features you want, and what you use it for.
+
+Please also report any bugs you find at http://envbot.org/trac/simpleticket
+(no login needed, but please enter your email so we can contact you if we
+need more details about your problem).
+
+
+Contacting developers
+---------------------
+You can reach us on IRC.
+1) Server: irc.kuonet-ng.org
+ Channel: #envbot
+2) Server: irc.securitychat.org
+ Channel: #envbot
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..7602c02
--- /dev/null
+++ b/TODO
@@ -0,0 +1,5 @@
+Hi I'm not maintaining this file so I leave the rest verbatim. --xylon
+
+TODO list
+---------
+TODO list has been moved to our issue tracker at http://envbot.org/trac/query
diff --git a/announcements/people/aurelien/phrases b/announcements/people/aurelien/phrases
new file mode 100644
index 0000000..53756c5
--- /dev/null
+++ b/announcements/people/aurelien/phrases
@@ -0,0 +1,2 @@
+Hi crusher
+*BEHOLD: DEATH CRUSHER POWER FIST!! VEHEMENT EVANGELIST OF SOFTWARE FREEDOM!!*
diff --git a/announcements/people/aurelien/seen b/announcements/people/aurelien/seen
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/announcements/people/aurelien/seen
diff --git a/announcements/people/aurelien/settings b/announcements/people/aurelien/settings
new file mode 100644
index 0000000..438bdd5
--- /dev/null
+++ b/announcements/people/aurelien/settings
@@ -0,0 +1,2 @@
+enabled=yes
+locked=no
diff --git a/announcements/people/fauno/phrases b/announcements/people/fauno/phrases
new file mode 100644
index 0000000..fe3d28f
--- /dev/null
+++ b/announcements/people/fauno/phrases
@@ -0,0 +1,9 @@
+Hi Pixelface
+Hi Pixelman
+Hi Spiceboy
+Hi Spicepixel
+Hi Pixelspice
+Hi Pixelbrain
+Hi Pixelpenis
+Hi Pixelhumour
+*ALL HAIL FAUNO!! KING OF PARABOLA!!*
diff --git a/announcements/people/fauno/seen b/announcements/people/fauno/seen
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/announcements/people/fauno/seen
diff --git a/announcements/people/fauno/settings b/announcements/people/fauno/settings
new file mode 100644
index 0000000..438bdd5
--- /dev/null
+++ b/announcements/people/fauno/settings
@@ -0,0 +1,2 @@
+enabled=yes
+locked=no
diff --git a/announcements/people/mtjm/phrases b/announcements/people/mtjm/phrases
new file mode 100644
index 0000000..6912180
--- /dev/null
+++ b/announcements/people/mtjm/phrases
@@ -0,0 +1 @@
+*ALL HAIL MTJM!! ULTIMATE ÃœBER-L33T OF PARABOLA!!*
diff --git a/announcements/people/mtjm/seen b/announcements/people/mtjm/seen
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/announcements/people/mtjm/seen
diff --git a/announcements/people/mtjm/settings b/announcements/people/mtjm/settings
new file mode 100644
index 0000000..438bdd5
--- /dev/null
+++ b/announcements/people/mtjm/settings
@@ -0,0 +1,2 @@
+enabled=yes
+locked=no
diff --git a/announcements/people/xylon/phrases b/announcements/people/xylon/phrases
new file mode 100644
index 0000000..25dc0b4
--- /dev/null
+++ b/announcements/people/xylon/phrases
@@ -0,0 +1 @@
+*ALL HAIL XYLON!! SUPREME OVERLORD OF THE STAR SYSTEM!!*
diff --git a/announcements/people/xylon/seen b/announcements/people/xylon/seen
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/announcements/people/xylon/seen
diff --git a/announcements/people/xylon/settings b/announcements/people/xylon/settings
new file mode 100644
index 0000000..438bdd5
--- /dev/null
+++ b/announcements/people/xylon/settings
@@ -0,0 +1,2 @@
+enabled=yes
+locked=no
diff --git a/bot_settings.sh b/bot_settings.sh
new file mode 100644
index 0000000..2207364
--- /dev/null
+++ b/bot_settings.sh
@@ -0,0 +1,296 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+
+# Variables marked with (*) will take effect at a rehash as well.
+
+# What version this config is at. This is used to check
+# if your config needs updating.
+config_version=17
+
+####################
+# General settings #
+####################
+
+# Nick to use
+config_firstnick="pbots_friend"
+# Nick if first is in use
+config_secondnick="pbots_friend_"
+# Nick if second is in use
+config_thirdnick="pbots_friend__"
+
+config_ident='rfc3092'
+config_gecos='ietf.org/rfc/rfc3092'
+
+
+###################
+# Server settings #
+###################
+
+# Server to use
+config_server='ipv6.chat.freenode.net'
+# What port to use. Normally 6667 works for non SSL connections.
+config_server_port='6667'
+# If 1 use SSL, not all transport modules support this.
+config_server_ssl=0
+# Accept invalid server certificates?
+# Note that some SSL modules (openssl for example) just
+# print any errors and continues anyway
+config_server_ssl_accept_invalid=1
+# Be verbose when connecting?
+# Not all SSL modules support it. The ones that doesn't
+# support it will just ignore it.
+# Be aware that this is mainly for debugging of SSL transport
+# modules as it is possible the verbose output may confuse the bot!
+config_server_ssl_verbose=0
+# If not empty try to bind to this IP when connecting, useful
+# to select vhost. Not all transport modules support this.
+config_server_bind=""
+# If this is empty don't use a server password.
+config_server_passwd=""
+
+
+##################
+# Access control #
+##################
+
+# (*) Access regexes
+# Without at least one set, the bot won't start
+# "owner" is a special capab that grants all other access.
+# The first access entry must be an owner.
+#
+# Scope is a regular expression of channels where access is effective.
+# A /msg (like say to a non channel) will get the scope MSG
+# Anything affecting global state will have the scope "GLOBAL"
+#
+# There can be several access masks matching same host (to allow
+# for different scope/capab combinations).
+config_access_mask[1]='^xylon!user@2001:ba8:1f1:f216::5$'
+config_access_capab[1]='owner'
+config_access_scope[1]='.*'
+
+# Some more access examples:
+
+#config_access_mask[2]='^Someone!whatever@customer-1234\.isp\.com$'
+#config_access_capab[2]='say kick'
+#config_access_scope[2]='#channel'
+
+#config_access_mask[3]='^Someone!whatever@customer-1234\.isp\.com$'
+#config_access_capab[3]='facoid_admin'
+#config_access_scope[3]='GLOBAL'
+
+
+############
+# Commands #
+############
+
+# (*) A regular expression of prefixes we should listen to.
+config_commands_listenregex="(;|${config_firstnick}[:,] )"
+
+# (*) Should we treat any message in /msg as a command even
+# if it doesn't have the the listen prefix?
+config_commands_private_always=1
+
+
+############
+# Feedback #
+############
+
+# (*) How to treat unknown commands:
+# Valid values:
+# 0 = Ignore them (drop command, do nothing).
+# 1 = Return error to sender.
+# 2 = Pass them on to modules that may handle "generic" PRIVMSG.
+# Note that non-commands always get passed on to "generic" PRIVMSG handling modules.
+config_feedback_unknown_commands=1
+
+
+###########
+# Logging #
+###########
+
+# Directory for log files
+config_log_dir="logs"
+# (*) Should we log raw data or not?
+# Can be 1 (log) or 0 (don't log)
+config_log_raw=1
+# (*) Should we always log to STDOUT as well?
+# Note that this doesn't mean it will not log at all if set to 0.
+# It will still log errors and other important log messages to STDOUT.
+# This is the same as --verbose.
+# Can be 1 (log) or 0 (don't log)
+config_log_stdout=0
+# (*) When logging to STDOUT should we use colors?
+config_log_colors=1
+
+
+#############
+# Transport #
+#############
+
+# Transport module. You should select exactly one.
+# The recommended non-SSL module is dev-tcp.
+# The recommended SSL module is gnutls.
+config_transport='dev-tcp'
+#config_transport='netcat' # Not well tested, for Debian users and other
+ # with broken distros.
+#config_transport='gnutls'
+#config_transport='socat' # Not well tested
+#config_transport='openssl' # Not well tested
+
+# netcat options
+# MAKE SURE THEY ARE CORRECT if you use netcat.
+#config_transport_netcat_path='/usr/bin/netcat'
+
+# socat options
+# MAKE SURE THEY ARE CORRECT if you use socat.
+#
+# This tells if to use IPv6 or IPv4 to connect.
+# socat doesn't support automatic choice of this.
+# Note that socat versions below 1.5 does not
+# support using IPv6 and SSL at the same time.
+# This can be either "ipv4" or "ipv6".
+#config_transport_socat_protocol_family=ipv4
+
+# Where are transports stored, this can be a relative path from the
+# main script of the bot, or an absolute path.
+config_transport_dir="transport"
+
+
+###########
+# Modules #
+###########
+
+
+# What modules to load on startup, space separated list
+# For a list of modules see the modules dir.
+# Note that the order of the modules may be important
+#
+# The list should normally start with "modules rehash services umodes autojoin"
+# Some modules should be placed last. "factoids" and "karma are such modules.
+config_modules="modules rehash services umodes autojoin ctcp"
+
+# Where are modules stored, this can be a relative path from the
+# main script of the bot, or an absolute path.
+config_modules_dir="modules"
+
+
+############################
+# Module specific settings #
+############################
+
+#####################################################################
+# Services module
+#
+# (*) NickServ password
+config_module_services_nickserv_passwd='nickserv password here'
+# (*) Name of NickServ
+# Normally this is correct.
+config_module_services_nickserv_name='NickServ'
+# (*) Service style. Supported are: generic, atheme
+# For the default server (irc.kuonet-ng.org) use atheme
+# Otherwise try generic, that will work with atheme too but
+# some features will be disabled.
+config_module_services_style='atheme'
+# (*) Use server side aliases
+# Try 1 first, if the bot fails to identify try 0
+config_module_services_server_alias=1
+
+
+#####################################################################
+# FAQ module
+#
+# (*) Location of FAQ items.
+config_module_faq_file='data/faq.txt'
+
+
+####################################################################
+# Quote module
+#
+# (*) Location of quotes file
+config_module_quotes_file='data/quotes.txt'
+
+
+#####################################################################
+# AutoJoin module.
+#
+# Channels to autojoin on connect
+config_module_autojoin_channels[1]='#parabola'
+# A channel can have a key as showed in the example below
+config_module_autojoin_channels[2]='#otherchannel channelkey'
+
+
+#####################################################################
+# Umodes module.
+#
+# (*) Default umodes to set on connect. Also set these at a rehash.
+config_module_umodes_default_umodes="+isB-w"
+
+
+#####################################################################
+# CTCP module
+#
+# (*) What to reply to VERSION requests.
+config_module_ctcp_version_reply="envbot $envbot_version"
+
+
+#####################################################################
+# SQLite3 module
+#
+# (*) Location of SQLite3 database file
+#config_module_sqlite3_database='data/envbot.db'
+
+
+#####################################################################
+# Factoids module
+#
+# (*) Table name for factoids in SQLite3 database
+#config_module_factoids_table='factoids'
+
+
+#####################################################################
+# Karma module
+#
+# (*) Table name for karma data in SQLite3 database
+#config_module_karma_table='karma'
+
+
+#####################################################################
+# Seen module
+#
+# (*) Table name for seen data in SQLite3 database
+#config_module_seen_table='seen'
+
+
+#####################################################################
+# Vote module.
+#
+# (*) The channel where vote mode will work.
+# Note that this is a regular expression that will be
+# surronded by ^ and $ when matching.
+#config_module_vote_channel='#mychannel'
+# (*) How long a vote is open before it is closed (in seconds).
+#config_module_vote_timeout='900'
+
+
+#####################################################################
+# For contrib modules please see the contrib module in question
+# for information on what variables it uses.
diff --git a/bot_settings.sh.example b/bot_settings.sh.example
new file mode 100644
index 0000000..62ae724
--- /dev/null
+++ b/bot_settings.sh.example
@@ -0,0 +1,296 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+
+# Variables marked with (*) will take effect at a rehash as well.
+
+# What version this config is at. This is used to check
+# if your config needs updating.
+config_version=17
+
+####################
+# General settings #
+####################
+
+# Nick to use
+config_firstnick="pbot_exterminator"
+# Nick if first is in use
+config_secondnick="pbot_exterminator_"
+# Nick if second is in use
+config_thirdnick="pbot_exterminator__"
+
+config_ident='rfc3092'
+config_gecos='ietf.org/rfc/rfc3092'
+
+
+###################
+# Server settings #
+###################
+
+# Server to use
+config_server='ipv6.chat.freenode.net'
+# What port to use. Normally 6667 works for non SSL connections.
+config_server_port='6667'
+# If 1 use SSL, not all transport modules support this.
+config_server_ssl=0
+# Accept invalid server certificates?
+# Note that some SSL modules (openssl for example) just
+# print any errors and continues anyway
+config_server_ssl_accept_invalid=1
+# Be verbose when connecting?
+# Not all SSL modules support it. The ones that doesn't
+# support it will just ignore it.
+# Be aware that this is mainly for debugging of SSL transport
+# modules as it is possible the verbose output may confuse the bot!
+config_server_ssl_verbose=0
+# If not empty try to bind to this IP when connecting, useful
+# to select vhost. Not all transport modules support this.
+config_server_bind=""
+# If this is empty don't use a server password.
+config_server_passwd=""
+
+
+##################
+# Access control #
+##################
+
+# (*) Access regexes
+# Without at least one set, the bot won't start
+# "owner" is a special capab that grants all other access.
+# The first access entry must be an owner.
+#
+# Scope is a regular expression of channels where access is effective.
+# A /msg (like say to a non channel) will get the scope MSG
+# Anything affecting global state will have the scope "GLOBAL"
+#
+# There can be several access masks matching same host (to allow
+# for different scope/capab combinations).
+config_access_mask[1]='^xylon!user@2001:ba8:1f1:f216::5$'
+config_access_capab[1]='owner'
+config_access_scope[1]='.*'
+
+# Some more access examples:
+
+#config_access_mask[2]='^Someone!whatever@customer-1234\.isp\.com$'
+#config_access_capab[2]='say kick'
+#config_access_scope[2]='#channel'
+
+#config_access_mask[3]='^Someone!whatever@customer-1234\.isp\.com$'
+#config_access_capab[3]='facoid_admin'
+#config_access_scope[3]='GLOBAL'
+
+
+############
+# Commands #
+############
+
+# (*) A regular expression of prefixes we should listen to.
+config_commands_listenregex="(;|${config_firstnick}[:,] )"
+
+# (*) Should we treat any message in /msg as a command even
+# if it doesn't have the the listen prefix?
+config_commands_private_always=0
+
+
+############
+# Feedback #
+############
+
+# (*) How to treat unknown commands:
+# Valid values:
+# 0 = Ignore them (drop command, do nothing).
+# 1 = Return error to sender.
+# 2 = Pass them on to modules that may handle "generic" PRIVMSG.
+# Note that non-commands always get passed on to "generic" PRIVMSG handling modules.
+config_feedback_unknown_commands=1
+
+
+###########
+# Logging #
+###########
+
+# Directory for log files
+config_log_dir="logs"
+# (*) Should we log raw data or not?
+# Can be 1 (log) or 0 (don't log)
+config_log_raw=0
+# (*) Should we always log to STDOUT as well?
+# Note that this doesn't mean it will not log at all if set to 0.
+# It will still log errors and other important log messages to STDOUT.
+# This is the same as --verbose.
+# Can be 1 (log) or 0 (don't log)
+config_log_stdout=0
+# (*) When logging to STDOUT should we use colors?
+config_log_colors=1
+
+
+#############
+# Transport #
+#############
+
+# Transport module. You should select exactly one.
+# The recommended non-SSL module is dev-tcp.
+# The recommended SSL module is gnutls.
+config_transport='dev-tcp'
+#config_transport='netcat' # Not well tested, for Debian users and other
+ # with broken distros.
+#config_transport='gnutls'
+#config_transport='socat' # Not well tested
+#config_transport='openssl' # Not well tested
+
+# netcat options
+# MAKE SURE THEY ARE CORRECT if you use netcat.
+#config_transport_netcat_path='/usr/bin/netcat'
+
+# socat options
+# MAKE SURE THEY ARE CORRECT if you use socat.
+#
+# This tells if to use IPv6 or IPv4 to connect.
+# socat doesn't support automatic choice of this.
+# Note that socat versions below 1.5 does not
+# support using IPv6 and SSL at the same time.
+# This can be either "ipv4" or "ipv6".
+#config_transport_socat_protocol_family=ipv4
+
+# Where are transports stored, this can be a relative path from the
+# main script of the bot, or an absolute path.
+config_transport_dir="transport"
+
+
+###########
+# Modules #
+###########
+
+
+# What modules to load on startup, space separated list
+# For a list of modules see the modules dir.
+# Note that the order of the modules may be important
+#
+# The list should normally start with "modules rehash services umodes autojoin"
+# Some modules should be placed last. "factoids" and "karma are such modules.
+config_modules="modules rehash services umodes autojoin ctcp"
+
+# Where are modules stored, this can be a relative path from the
+# main script of the bot, or an absolute path.
+config_modules_dir="modules"
+
+
+############################
+# Module specific settings #
+############################
+
+#####################################################################
+# Services module
+#
+# (*) NickServ password
+config_module_services_nickserv_passwd='nickserv password here'
+# (*) Name of NickServ
+# Normally this is correct.
+config_module_services_nickserv_name='NickServ'
+# (*) Service style. Supported are: generic, atheme
+# For the default server (irc.kuonet-ng.org) use atheme
+# Otherwise try generic, that will work with atheme too but
+# some features will be disabled.
+config_module_services_style='atheme'
+# (*) Use server side aliases
+# Try 1 first, if the bot fails to identify try 0
+config_module_services_server_alias=1
+
+
+#####################################################################
+# FAQ module
+#
+# (*) Location of FAQ items.
+#config_module_faq_file='data/faq.txt'
+
+
+####################################################################
+# Quote module
+#
+# (*) Location of quotes file
+#config_module_quotes_file='data/quotes.txt'
+
+
+#####################################################################
+# AutoJoin module.
+#
+# Channels to autojoin on connect
+config_module_autojoin_channels[1]='#botwars'
+# A channel can have a key as showed in the example below
+config_module_autojoin_channels[2]='#otherchannel channelkey'
+
+
+#####################################################################
+# Umodes module.
+#
+# (*) Default umodes to set on connect. Also set these at a rehash.
+config_module_umodes_default_umodes="+isB-w"
+
+
+#####################################################################
+# CTCP module
+#
+# (*) What to reply to VERSION requests.
+config_module_ctcp_version_reply="envbot $envbot_version"
+
+
+#####################################################################
+# SQLite3 module
+#
+# (*) Location of SQLite3 database file
+#config_module_sqlite3_database='data/envbot.db'
+
+
+#####################################################################
+# Factoids module
+#
+# (*) Table name for factoids in SQLite3 database
+#config_module_factoids_table='factoids'
+
+
+#####################################################################
+# Karma module
+#
+# (*) Table name for karma data in SQLite3 database
+#config_module_karma_table='karma'
+
+
+#####################################################################
+# Seen module
+#
+# (*) Table name for seen data in SQLite3 database
+#config_module_seen_table='seen'
+
+
+#####################################################################
+# Vote module.
+#
+# (*) The channel where vote mode will work.
+# Note that this is a regular expression that will be
+# surronded by ^ and $ when matching.
+#config_module_vote_channel='#mychannel'
+# (*) How long a vote is open before it is closed (in seconds).
+#config_module_vote_timeout='900'
+
+
+#####################################################################
+# For contrib modules please see the contrib module in question
+# for information on what variables it uses.
diff --git a/bug_tracker_change_detector b/bug_tracker_change_detector
new file mode 100644
index 0000000..2f64518
--- /dev/null
+++ b/bug_tracker_change_detector
@@ -0,0 +1,68 @@
+#! /bin/bash
+
+while true
+do
+ log_file=bug_sums
+
+ temp_file=$( mktemp )
+
+ changes="/tmp/un-provoked-message-store"
+
+ for url in $( curl --compressed "https://bugs.parabolagnulinux.org/bugs/issue?@pagesize=99999" 2> /dev/null | grep -E 'href="issue[[:digit:]]+' | cut -d '"' -f 2 )
+ do
+ tfile="$( mktemp )"
+
+ try_count=1
+
+ # Get the URL and make sure it's not empty.
+ until curl --compressed "https://bugs.parabolagnulinux.org/bugs/${url}" > "${tfile}" 2> /dev/null && (( $( wc -l "${tfile}" 2> /dev/null | cut -d ' ' -f 1 ) ))
+ do
+ # The time we sleep doubles each time up to a maximum of 512
+ # seconds, before restarting the entire script.
+ sleep "${try_count}"
+
+ if (( try_count < 512 ))
+ then
+ try_count=$(( try_count * 2 ))
+ else
+ continue 2
+ fi
+ done
+
+ echo "${url} $( md5sum < ${tfile} | cut -d ' ' -f 1 )" >> "${temp_file}"
+
+ rm "${tfile}"
+ done
+
+# Check that the log file is not empty as a sanity check.
+ if (( $( wc -l "${log_file}" 2> /dev/null | cut -d ' ' -f 1 ) ))
+ then
+ cat "${temp_file}" |
+ while read -r line
+ do
+ bug_number="${line%% *}"
+ # If this bug is not in the log file then it must be new.
+ if { ! grep "${bug_number}" "${log_file}" > /dev/null ; }
+ then
+ tdir="$( mktemp -d )"
+ curl --compressed "https://bugs.parabolagnulinux.org/bugs/${bug_number}" 2> /dev/null | csplit -f "${tdir}/xx" - '%<title>%1'
+ bug_title=$( head -1 ${tdir}/xx* )
+ rm -r "${tdir}"
+ echo "${bug_number} created: https://bugs.parabolagnulinux.org/bugs/${bug_number} (${bug_title})" >> "${changes}"
+ # It is in the log file so now we check if the entire line is there,
+ # because if it's not then the md5sum must have changed.
+ elif { ! grep "${line}" "${log_file}" > /dev/null ; }
+ then
+ tdir="$( mktemp -d )"
+ curl --compressed "https://bugs.parabolagnulinux.org/bugs/${bug_number}" 2> /dev/null | csplit -f "${tdir}/xx" - '%<title>%1'
+ bug_title=$( head -1 ${tdir}/xx* )
+ rm -r "${tdir}"
+ echo "${bug_number} changed: https://bugs.parabolagnulinux.org/bugs/${bug_number} (${bug_title})" >> "${changes}"
+ fi
+ done
+ fi
+
+ mv "${temp_file}" "${log_file}"
+
+ sleep 5m
+done
diff --git a/contrib/README b/contrib/README
new file mode 100644
index 0000000..2c6b29a
--- /dev/null
+++ b/contrib/README
@@ -0,0 +1,6 @@
+contrib modules and scripts
+===========================
+This directory contain stuff that the normal developers doesn't want to support
+(even though they may have coded it)
+
+See each file for description
diff --git a/contrib/init-scripts/envbot.debian b/contrib/init-scripts/envbot.debian
new file mode 100644
index 0000000..971bd09
--- /dev/null
+++ b/contrib/init-scripts/envbot.debian
@@ -0,0 +1,48 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007 Remo Ford #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+
+ENVBOT_DIR="/path/to/envbot"
+ENVBOT_OPTIONS=""
+ENVBOT_USER="envbot"
+PID_FILE="/var/run/envbot.pid"
+
+test -f /lib/lsb/init-functions || exit 1
+. /lib/lsb/init-functions
+
+case "$1" in
+ start)
+ log_begin_msg "Starting envbot..."
+ start-stop-daemon -b -d $ENVBOT_DIR --start -m -p $PID_FILE -c $ENVBOT_USER --exec $ENVBOT_DIR/envbot $ENVBOT_OPTIONS || log_end_msg 1
+ log_end_msg 0
+ ;;
+ stop)
+ log_begin_msg "Stopping envbot..."
+ start-stop-daemon --stop --quiet -p $PID_FILE || log_end_msg 1
+ log_end_msg 0
+ ;;
+ *)
+ log_success_msg "Usage: /etc/init.d/envbot {start|stop}"
+ exit 1
+ ;;
+esac
+
+exit 0
diff --git a/contrib/modules/m_bugzilla.sh b/contrib/modules/m_bugzilla.sh
new file mode 100644
index 0000000..c428cdc
--- /dev/null
+++ b/contrib/modules/m_bugzilla.sh
@@ -0,0 +1,231 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Check bugs using the program bugz and return output from it.
+## @pybugz bugz is a tool to search Gentoo bug reports (or other bugzillas)<br />
+## @pybugz From eix pybugz:<br />
+## @pybugz Description: Command line interface to (Gentoo) Bugzilla
+## @Dependencies This module therefore depends on:<br />
+## @Dependencies pybugz
+## @Config_variables To set bugzilla to use something like this in config:<br />
+## @Config_variables <pre>config_module_bugzilla_tracker_name[0]='gentoo'
+## @Config_variables config_module_bugzilla_tracker_url[0]='https://bugs.gentoo.org/'</pre>
+## @Config_variables Must end in trailing slash! Also the first entry will be the default.<br />
+## @Config_variables You also need to specify flood limiting<br />
+## @Config_variables (how often in seconds)<br />
+## @Config_variables <tt>config_module_bugzilla_rate='10'</tt>
+#---------------------------------------------------------------------
+
+module_bugzilla_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='after_load'
+ commands_register "$1" 'bugs_search' 'bugs search' || return 1
+ commands_register "$1" 'bug' || return 1
+ helpentry_module_bugzilla_description="Search in bugzilla bug trackers."
+
+ helpentry_bugzilla_bugs_search_syntax='[-t <tracker>] [-(all|closed)] <pattern>'
+ helpentry_bugzilla_bugs_search_description='Search for <pattern> in <tracker> (or the default tracker).'
+
+ helpentry_bugzilla_bug_syntax='[-t <tracker>] <id>'
+ helpentry_bugzilla_bug_description='Look up the bug with <id> in <tracker> (or the default tracker).'
+}
+
+module_bugzilla_UNLOAD() {
+ unset module_bugzilla_last_query module_bugzilla_default_bugtracker
+ unset module_bugzilla_parse_config module_bugzilla_find_tracker
+ hash_reset 'bugzilla_tracker'
+}
+
+module_bugzilla_REHASH() {
+ module_bugzilla_parse_config
+}
+
+#---------------------------------------------------------------------
+## Initialize the hash of name -> url mapping
+## @Type Private
+#---------------------------------------------------------------------
+module_bugzilla_parse_config() {
+ hash_reset 'bugzilla_tracker'
+ local index
+ for index in "${!config_module_bugzilla_tracker_name[@]}"; do
+ hash_set 'bugzilla_tracker' \
+ "${config_module_bugzilla_tracker_name[index]}" \
+ "${config_module_bugzilla_tracker_url[index]}"
+ done
+ module_bugzilla_default_bugtracker="${config_module_bugzilla_tracker_url[0]}"
+}
+
+#---------------------------------------------------------------------
+## Find what tracker to use from the parameters.
+## @param Name of parameter variable.
+## @param Name of bugtracker variable.
+## @Type Private
+#---------------------------------------------------------------------
+module_bugzilla_find_tracker() {
+ if [[ "${!1}" =~ ^(-(tracker|t)\ +([A-Za-z0-9]+)\ +)(.+) ]]; then
+ local tindex="${BASH_REMATCH[3]}"
+ # Store result back in variable.
+ printf -v "$1" '%s' "${BASH_REMATCH[4]}"
+ local turl
+ hash_get 'bugzilla_tracker' "$tindex" 'turl'
+ if [[ $turl ]]; then
+ printf -v "$2" '%s' "$turl"
+ else
+ feedback_generic_error "$sendernick" "bugs search" "No such bug tracker found."
+ fi
+ else
+ printf -v "$2" '%s' "${module_bugzilla_default_bugtracker}"
+ fi
+}
+
+# Called after module has loaded.
+# Check for bugz
+module_bugzilla_after_load() {
+ if ! hash bugz > /dev/null 2>&1; then
+ log_error "Couldn't find bugz command line tool. The bugzilla module depend on that tool (emerge pybugz to get it on Gentoo)."
+ return 1
+ fi
+ if [[ -z ${config_module_bugzilla_tracker_url[0]} ]]; then
+ log_error "Please set at least config_module_bugzilla_url[0] in config."
+ return 1
+ fi
+ if [[ -z ${config_module_bugzilla_tracker_name[0]} ]]; then
+ log_error "Please set at least config_module_bugzilla_name[0] in config."
+ return 1
+ fi
+ if [[ -z $config_module_bugzilla_rate ]]; then
+ log_error "Please set config_module_bugzilla_rate in config."
+ return 1
+ fi
+ module_bugzilla_parse_config
+ unset module_bugzilla_last_query
+ module_bugzilla_last_query='0'
+}
+
+module_bugzilla_handler_bugs_search() {
+ # Accept this anywhere, unless someone can give a good reason not to.
+ local sender="$1"
+ local channel="$2"
+ local sendernick=
+ parse_hostmask_nick "$sender" 'sendernick'
+ # If it isn't in a channel send message back to person who send it,
+ # otherwise send in channel
+ if ! [[ $2 =~ ^# ]]; then
+ channel="$sendernick"
+ fi
+ local parameters="$3"
+ local bugtracker
+ module_bugzilla_find_tracker 'parameters' 'bugtracker'
+ if [[ "$parameters" =~ ^(-(all|closed)\ +)?(.+) ]]; then
+ local mode="${BASH_REMATCH[2]}"
+ local pattern="${BASH_REMATCH[@]: -1}"
+ # Simple flood limiting
+ if time_check_interval "$module_bugzilla_last_query" "$config_module_bugzilla_rate"; then
+ time_get_current 'module_bugzilla_last_query'
+ local bugs_parameters=""
+ if [[ $mode = "all" ]]; then
+ bugs_parameters="-s all"
+ elif [[ $mode = "closed" ]]; then
+ bugs_parameters="-s CLOSED -s RESOLVED"
+ fi
+ log_info_file bugzilla.log "$sender made the bot run pybugz search on \"$pattern\""
+ # We unset TERM because otherwise bugz output some control codes
+ local result="$(unset TERM; ulimit -t 4; bugz -fqb "$bugtracker" search $bugs_parameters "$pattern")"
+ local lines="$(wc -l <<< "$result")"
+ local header footer
+ # Some odd formatting chars are always returned (in some versions of pybugz), so we can't check for empty string.
+ if [[ ${#result} -le 10 ]]; then
+ header="No bugs matching \"$pattern\" found"
+ elif [[ $lines -gt 1 ]]; then
+ header="First bug matching \"$pattern\": "
+ footer=" ($lines more bugs found)"
+ else
+ header="One bug matching \"$pattern\" found: "
+ fi
+ if [[ $(head -n 1 <<< "$result") =~ \ ([0-9]+)\ +([^ ]+)\ +(.*)$ ]]; then
+ local pretty_result="${format_bold}${bugtracker}${BASH_REMATCH[1]}${format_bold} ${format_bold}Description${format_bold}: ${BASH_REMATCH[3]} ${format_bold}Assigned To${format_bold}: ${BASH_REMATCH[2]}"
+ fi
+ send_msg "$channel" "${header}${pretty_result}${footer}"
+ else
+ log_error_file bugzilla.log "FLOOD DETECTED in bugzilla module"
+ fi
+ else
+ feedback_bad_syntax "$sendernick" "bugs search" "[-t tracker] [-(all|closed)] <pattern>"
+ fi
+}
+
+module_bugzilla_handler_bug() {
+ # Accept this anywhere, unless someone can give a good reason not to.
+ local sender="$1"
+ local channel="$2"
+ local sendernick=
+ parse_hostmask_nick "$sender" 'sendernick'
+ # If it isn't in a channel send message back to person who send it,
+ # otherwise send in channel
+ if ! [[ $2 =~ ^# ]]; then
+ channel="$sendernick"
+ fi
+ local parameters="$3"
+ local bugtracker
+ module_bugzilla_find_tracker 'parameters' 'bugtracker'
+ # Extract bug ID
+ if [[ "$parameters" =~ ^([0-9]+) ]]; then
+ local id="${BASH_REMATCH[1]}"
+ # Simple flood limiting
+ if time_check_interval "$module_bugzilla_last_query" "$config_module_bugzilla_rate"; then
+ time_get_current 'module_bugzilla_last_query'
+ log_info_file bugzilla.log "$sender made the bot check with pybugz for bug \"$id\""
+ # We unset TERM because otherwise bugz output some control codes
+ local result="$(unset TERM; ulimit -t 4; bugz -fqb "$bugtracker" get -n "$id" | grep -E 'Title|Status|Resolution')"
+ local resultread pretty_result
+ local title status resolution
+ # Read the data out of the multiline result.
+ while read -r resultread; do
+ if [[ $resultread =~ ^Title[\ :]+([^ ].*) ]]; then
+ title="${BASH_REMATCH[1]}"
+ elif [[ $resultread =~ ^Status[\ :]+([^ ].*) ]]; then
+ status="${BASH_REMATCH[1]}"
+ elif [[ $resultread =~ ^Resolution[\ :]+([^ ].*) ]]; then
+ resolution="${BASH_REMATCH[1]}"
+ fi
+ done <<< "$result"
+ # Yes this is a bit of a mess
+ if [[ "$title" ]]; then
+ # This info is always here
+ pretty_result="${format_bold}Bug $id${format_bold} (${format_bold}Status${format_bold} $status"
+ # The resolution may not exist, add it if it does.
+ if [[ $resolution ]]; then
+ pretty_result+=", ${format_bold}Resolution${format_bold} $resolution"
+ fi
+ # And add the title in. Does not depend on if resolution exist.
+ pretty_result+="): $title (${bugtracker}${id})"
+ else
+ pretty_result="Bug $id not found"
+ fi
+ send_msg "$channel" "${pretty_result}"
+ else
+ log_error_file bugzilla.log "FLOOD DETECTED in bugzilla module"
+ fi
+ else
+ feedback_bad_syntax "$sendernick" "bug" "[-t tracker] <id>"
+ fi
+}
diff --git a/contrib/modules/m_calc.sh b/contrib/modules/m_calc.sh
new file mode 100644
index 0000000..fa1d98d
--- /dev/null
+++ b/contrib/modules/m_calc.sh
@@ -0,0 +1,74 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 EmErgE <halt.system@gmail.com> #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Calculate with bc
+## @Dependencies This module depends on bc
+## @Dependencies (http://www.gnu.org/software/bc/bc.html)
+#---------------------------------------------------------------------
+
+module_calc_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ if ! hash bc > /dev/null 2>&1; then
+ log_error "Couldn't find \"bc\" command line tool. The calc module depend on that tool."
+ return 1
+ fi
+ commands_register "$1" 'calc' || return 1
+ helpentry_module_calc_description="Simple calculator module."
+ helpentry_calc_calc_syntax='<expression>'
+ helpentry_calc_calc_description='Try to calculate <expression> using bc.'
+}
+
+module_calc_UNLOAD() {
+ return 0
+}
+
+module_calc_REHASH() {
+ return 0
+}
+
+module_calc_handler_calc() {
+ local sender="$1"
+ local channel="$2"
+ local sendernick=
+ parse_hostmask_nick "$sender" 'sendernick'
+ # If it isn't in a channel send message back to person who send it,
+ # otherwise send in channel
+ if ! [[ $2 =~ ^# ]]; then
+ channel="$sendernick"
+ fi
+ local parameters="$3"
+
+ # Sanity check on parameters
+ parameters="$(tr -d '\n\r\t' <<< "$parameters")"
+ if grep -Eq "scale=|read|while|if|for|break|continue|print|return|define|[e|j] *\(" <<< "$parameters"; then
+ send_msg "$channel" "${sendernick}: Can't calculate that, it contains a potential unsafe/very slow function."
+ elif [[ $parameters =~ \^[0-9]{4,} ]]; then
+ send_msg "$channel" "${sendernick}: Some too large numbers."
+ else
+ # Force some security guards
+ local myresult="$(ulimit -t 4; echo "$parameters" | bc -l 2>&1 | head -n 1)"
+ send_msg "$channel" "${sendernick}: $myresult"
+ fi
+
+}
diff --git a/contrib/modules/m_convert.sh b/contrib/modules/m_convert.sh
new file mode 100644
index 0000000..3910379
--- /dev/null
+++ b/contrib/modules/m_convert.sh
@@ -0,0 +1,117 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Convert values with units
+## @Dependencies This module depends on units
+## @Dependencies (http://www.gnu.org/software/units/units.html)
+#---------------------------------------------------------------------
+
+module_convert_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ if ! hash units > /dev/null 2>&1; then
+ log_error "Couldn't find \"units\" command line tool. The convert module depend on that tool."
+ return 1
+ fi
+ # Is it GNU units?
+ if units --help > /dev/null 2>&1; then
+ module_convert_gnu=1
+ else
+ module_convert_gnu=0
+ fi
+ commands_register "$1" 'convert' || return 1
+ helpentry_module_convert_description="Convert between different units."
+ helpentry_convert_convert_syntax='<value> <unit> [to] <unit>'
+ helpentry_convert_convert_description='Convert the value from one unit to another.'
+}
+
+module_convert_UNLOAD() {
+ return 0
+}
+
+module_convert_REHASH() {
+ return 0
+}
+
+module_convert_handler_convert() {
+ local sender="$1"
+ local channel="$2"
+ local sendernick=
+ parse_hostmask_nick "$sender" 'sendernick'
+ # If it isn't in a channel send message back to person who send it,
+ # otherwise send in channel
+ if ! [[ $2 =~ ^# ]]; then
+ channel="$sendernick"
+ fi
+ local parameters="$3"
+ # Format: convert <value> <in unit> <out unit>
+ if [[ "$parameters" =~ ^([-0-9.]+)\ +([a-zA-Z0-9^/*]+)\ +(to\ +)?([a-zA-Z0-9^/*]+) ]]; then
+ local value="${BASH_REMATCH[1]}"
+ local inunit="${BASH_REMATCH[2]}"
+ local outunit="${BASH_REMATCH[@]: -1}"
+ # Construct expression of value and inunit,
+ # needed because of temperature
+ case $inunit in
+ C|F|K)
+ # This only work on GNU units.
+ if [[ $module_convert_gnu = 1 ]]; then
+ local inexpr="temp${inunit}($value)"
+ else
+ local inexpr="$value deg${inunit}"
+ fi
+ ;;
+ *)
+ local inexpr="$value $inunit"
+ ;;
+ esac
+ # Out: Temperature
+ case $outunit in
+ C|F|K)
+ # This only work on GNU units
+ if [[ $module_convert_gnu = 1 ]]; then
+ local outexpr="temp${outunit}"
+ else
+ local outexpr="deg${outunit}"
+ fi
+ local outunit="degrees $outunit"
+ ;;
+ *)
+ local outexpr="$outunit"
+ ;;
+ esac
+
+ # Need to do the local separately or return code will be messed up.
+ local myresult
+ # Force some security guards
+ # We can't use -t, that doesn't work on *BSD units...
+ # so we use awk to get interesting lines.
+ # Then check pipestatus to give nice return code
+ myresult="$(ulimit -t 4; units -q "$inexpr" "$outexpr" 2>&1 | awk '/^\t[0-9]+/ {print $1} /\*/ {print $2} /[Ee]rror|[Uu]nknown/'; [[ ${PIPESTATUS[0]} -eq 0 ]] || exit 1)"
+ if [[ $? -eq 0 ]]; then
+ send_msg "$channel" "${sendernick}: $myresult $outunit"
+ else
+ send_msg "$channel" "${sendernick}: Error: $myresult"
+ fi
+ else
+ feedback_bad_syntax "$sendernick" "convert" "<value> <in unit> [to] <out unit>"
+ fi
+}
diff --git a/contrib/modules/m_eix.sh b/contrib/modules/m_eix.sh
new file mode 100644
index 0000000..46ba964
--- /dev/null
+++ b/contrib/modules/m_eix.sh
@@ -0,0 +1,101 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Check eix and return output from it.
+## @eix eix is a tool to search Gentoo packages<br />
+## @eix From eix eix:<br />
+## @eix <tt> Description: Small utility for searching ebuilds with indexing for fast results</tt>
+## @Dependencies This module therefore depends on:<br />
+## @Dependencies Gentoo<br />
+## @Dependencies eix<br />
+## @Config_variables You need to specify flood limiting in config.<br />
+## @Config_variables (how often in seconds)<br />
+## @Config_variables <tt>config_module_eix_rate='5'</tt><br />
+#---------------------------------------------------------------------
+
+module_eix_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='after_load'
+ commands_register "$1" 'eix' || return 1
+ helpentry_module_eix_description="Search in Gentoo package database."
+
+ helpentry_eix_eix_syntax='<pattern>'
+ helpentry_eix_eix_description='Search for wildcard pattern in the local copy of the Gentoo package database.'
+}
+
+module_eix_UNLOAD() {
+ unset module_eix_format_string module_eix_last_query
+}
+
+module_eix_REHASH() {
+ return 0
+}
+
+# Called after module has loaded.
+# Check for eix and config being sane.
+module_eix_after_load() {
+ # Check (silently) for eix
+ if ! hash eix >/dev/null 2>&1; then
+ log_error "Couldn't find \"eix\" command line tool. The eix module depend on that tool."
+ return 1
+ fi
+ if [[ -z $config_module_eix_rate ]]; then
+ log_error_file eix.log "YOU MUST SET config_module_eix_rate IN YOUR CONFIG IN ORDER TO USE THE EIX MODULE"
+ return 1
+ fi
+ # Flood limiting.
+ unset module_eix_last_query
+ module_eix_last_query='0'
+}
+
+#---------------------------------------------------------------------
+## eix format string.
+## @Type Private
+#---------------------------------------------------------------------
+module_eix_format_string="<category>/${format_bold}<name>${format_bold} \(<availableversionsshort>\) \(${format_bold}<homepage>${format_bold}\): <description>"
+
+module_eix_handler_eix() {
+ # Accept this anywhere, unless someone can give a good reason not to.
+ local sender="$1"
+ local channel="$2"
+ # If it isn't in a channel send message back to person who send it,
+ # otherwise send in channel
+ if ! [[ $2 =~ ^# ]]; then
+ parse_hostmask_nick "$sender" 'channel'
+ fi
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(.+) ]]; then
+ local pattern="${BASH_REMATCH[1]}"
+ # Simple flood limiting
+ if time_check_interval "$module_eix_last_query" "$config_module_eix_rate"; then
+ time_get_current 'module_eix_last_query'
+ log_info_file eix.log "$sender made the bot run eix on \"$pattern\""
+ send_msg "$channel" "$(ulimit -t 4; EIX_PRINT_IUSE='false' eix -pSCxs --format "$module_eix_format_string" "$pattern" | head -n 1)"
+ else
+ log_error_file eix.log "FLOOD DETECTED in eix module"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "eix" "<pattern>"
+ fi
+}
diff --git a/contrib/modules/m_eval.sh b/contrib/modules/m_eval.sh
new file mode 100644
index 0000000..c3a03c4
--- /dev/null
+++ b/contrib/modules/m_eval.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Allow owners to make the bot eval any code<br />
+## THIS IS FOR DEBUGGING ONLY!!!! Don't use it in other cases
+#---------------------------------------------------------------------
+
+module_eval_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ commands_register "$1" 'eval' || return 1
+ helpentry_module_eval_description="Eval command for developers debugging the bot. Don't use if you don't know what you are doing."
+
+ helpentry_eval_eval_syntax='<expression>'
+ helpentry_eval_eval_description='Evaluate <expression> in global scope.'
+}
+
+module_eval_UNLOAD() {
+ return 0
+}
+
+module_eval_REHASH() {
+ return 0
+}
+
+module_eval_handler_eval() {
+ # Accept anywhere
+ local sender="$1"
+ if access_check_owner "$sender"; then
+ local parameters="$3"
+ access_log_action "$sender" "did eval with: $parameters"
+ eval "$parameters"
+ else
+ access_fail "$sender" "eval a command" "owner"
+ fi
+}
diff --git a/contrib/modules/m_helloworld.sh b/contrib/modules/m_helloworld.sh
new file mode 100644
index 0000000..25b6a10
--- /dev/null
+++ b/contrib/modules/m_helloworld.sh
@@ -0,0 +1,193 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Example module meant to help people who want to make modules for envbot
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## This is called to get a list of hooks that the module provides.
+## Use the hook after_load to do other things
+## @Type Module hook
+## @Stdout A list of hooks.
+#---------------------------------------------------------------------
+module_helloworld_INIT() {
+ modinit_API='2'
+ # Set modinit_HOOKS to the hooks we have.
+ modinit_HOOKS='after_load'
+ # Register commands, each command handler will have a name like:
+ # module_modulename_handler_function
+ # Example: module_helloworld_handler_hi
+ # If command name and function name are the same you can skip
+ # command name.
+ commands_register "$1" 'hi' || return 1
+ # Here the function name and command name can't be the same,
+ # as the command name got space in it. Note that a command can
+ # be at most two words.
+ commands_register "$1" 'hello_world' 'hello world' || return 1
+ helpentry_module_helloworld_description="This is an example module."
+
+ helpentry_helloworld_hi_syntax='<target> <message>'
+ helpentry_helloworld_hi_description='Send a greeting to <target> (nick or channel) with the <message>.'
+
+ helpentry_helloworld_helloworld_syntax='<message>'
+ helpentry_helloworld_helloworld_description='Send a greeting to the current scope with the one word <message>.'
+}
+
+#---------------------------------------------------------------------
+## Here we do anything needed to unload the module.
+## @Type Module hook
+## @return 0 Unloaded correctly
+## @return 1 Failed to unload. On this the bot will quit.
+## @Note This function is NOT called when the bot is exiting. To check for that
+## @Note use the FINALISE hook!
+#---------------------------------------------------------------------
+module_helloworld_UNLOAD() {
+ # Here we unset any functions and variables that we have defined
+ # except the hook functions.
+ unset module_helloworld_variable module_helloworld_function
+}
+
+#---------------------------------------------------------------------
+## Here do anything needed at rehash
+## @Type Module hook
+## @return 0 Rehashed correctly
+## @return 1 Non fatal error for the bot itself. The bot will call UNLOAD on the module.
+## @return 2 Fatal error of some kind. On this the bot will quit.
+#---------------------------------------------------------------------
+module_helloworld_REHASH() {
+ # We don't have anything to do here.
+ return 0
+}
+
+#---------------------------------------------------------------------
+## Called after all the hooks are added for the module.
+## @Type Module hook
+## @return 0 Unloaded correctly
+## @return 1 Failed. On this the bot will call unload on the module.
+#---------------------------------------------------------------------
+module_helloworld_after_load() {
+ # Set a global variable, this can't be done in INIT.
+ # Remember to unset all global variables on UNLOAD!
+ module_helloworld_variable="world!"
+}
+
+#---------------------------------------------------------------------
+## This logs "hello world" as an informative level log item
+## when called
+## @Type Private
+## @Note Note that this is a custom function used by
+## @Note some other part of the script
+#---------------------------------------------------------------------
+module_helloworld_function() {
+ # Lets use the variable defined above!
+ log_info "Hello $module_helloworld_variable"
+}
+
+#---------------------------------------------------------------------
+## Called on the command "hello world"
+## @Type Function handler
+## @param From who (n!u@h)
+## @param To who (channel or botnick)
+## @param The parameters to the command
+#---------------------------------------------------------------------
+module_helloworld_handler_hello_world() {
+ local sender="$1"
+ local target
+ # If it isn't in a channel send message back to person who send it,
+ # otherwise send in channel
+ if [[ $2 =~ ^# ]]; then
+ target="$2"
+ else
+ # parse_hostmask_nick gets the nick from a hostmask.
+ parse_hostmask_nick "$sender" 'target'
+ fi
+
+ local parameters="$3"
+ # Check if the syntax for the parameters is correct!
+ # Lets check for one parameter without spaces
+ if [[ "$parameters" =~ ^([^ ]+) ]]; then
+ # Store the bit in the first group of the regex into
+ # the variable message
+ local message="${BASH_REMATCH[1]}"
+ # Send a hello world message:
+ send_msg "$target" "Hello world! I had the parameter $message"
+ else
+ # So the regex for matching parameters didn't work, lets provide
+ # the user with some feedback!
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "hello world" "<message> # Where message is one word!"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Called on the command "hi"
+## @Type Function handler
+## @param From who (n!u@h)
+## @param To who (channel or botnick)
+## @param The parameters to the command
+#---------------------------------------------------------------------
+module_helloworld_handler_hi() {
+ local sender="$1"
+
+ local parameters="$3"
+ # Two parameters, one is single word, the other matches to
+ # end of line.
+ if [[ "$parameters" =~ ^([^ ]+)\ (.+) ]]; then
+ # Store the groups in some variables.
+ local target_channel="${BASH_REMATCH[1]}"
+ local message="${BASH_REMATCH[2]}"
+ # This is used for the access check below.
+ # Check if target is a channel or nick.
+ local scope
+ if [[ $target_channel =~ ^# ]]; then
+ scope="$target_channel"
+ else
+ scope="MSG"
+ fi
+
+ # Lets check for access.
+ # First variable is capability to check for
+ # Second variable is the hostmask of the sender of the message
+ # Third variable is the scope, that we set above.
+ if access_check_capab "hi" "$sender" "$scope"; then
+ # Such important events for security as a "hi" should
+ # really get logged even if it fails! ;)
+ access_log_action "$sender" "made the hi channel \"$message\" in/to \"$target_channel\""
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ send_msg "${target_channel}" "Hi $target_channel! $sendernick wants you to know ${message}"
+ # As an example also call our function.
+ module_helloworld_function
+ else
+ # Lets tell the sender they lack access!
+ # access_fail will send a PRIVMSG to the sender saying permission denied
+ # and also log the failed attempt.
+ access_fail "$sender" "make the bot hi" "hi"
+ fi
+ else
+ # As above, provide feedback about bad syntax.
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "hi" "<target> <message> # Where target is a nick or channel"
+ fi
+}
diff --git a/contrib/modules/m_perl/__main__.sh b/contrib/modules/m_perl/__main__.sh
new file mode 100644
index 0000000..36906a5
--- /dev/null
+++ b/contrib/modules/m_perl/__main__.sh
@@ -0,0 +1,74 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 EmErgE <halt.system@gmail.com> #
+# Copyright (C) 2007-2008 Vsevolod Kozlov #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Evaluate with perl
+## @Dependencies This module depends on perl
+## @Dependencies (http://www.perl.org/about.html)
+## @Note This may not be safe!
+#---------------------------------------------------------------------
+
+module_perl_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ if ! hash perl > /dev/null 2>&1; then
+ log_error "Couldn't find \"perl\" binary. The perl module depends on it."
+ return 1
+ fi
+ module_perl_working_dir="$MODULE_BASE_PATH"
+ commands_register "$1" 'perl' || return 1
+ helpentry_module_perl_description="Execute perl code."
+
+ helpentry_perl_perl_syntax='<code>'
+ helpentry_perl_perl_description='Execute perl code.'
+}
+
+module_perl_UNLOAD() {
+ unset module_perl_working_dir
+ unset module_perl_handler_perl
+ return 0
+}
+
+module_perl_REHASH() {
+ return 0
+}
+
+module_perl_handler_perl() {
+ local sender="$1"
+ local channel="$2"
+ local sendernick=
+ parse_hostmask_nick "$sender" 'sendernick'
+ if access_check_capab "perl_eval" "$sender" "$channel"; then
+ # If it isn't in a channel send message back to person who send it,
+ # otherwise send in channel
+ if ! [[ $2 =~ ^# ]]; then
+ channel="$sendernick"
+ fi
+ local parameters="$3"
+ # Extremely Safe Perl Evaluation
+ local myresult="$(perl "${module_perl_working_dir}/safe_eval.pl" "$parameters")"
+ send_msg "$channel" "${sendernick}: $myresult"
+ else
+ access_fail "$sender" "make the bot evalute perl expressions" "perl_eval"
+ fi
+}
diff --git a/contrib/modules/m_perl/safe_eval.pl b/contrib/modules/m_perl/safe_eval.pl
new file mode 100644
index 0000000..1e6489f
--- /dev/null
+++ b/contrib/modules/m_perl/safe_eval.pl
@@ -0,0 +1,25 @@
+#!/usr/bin/perl
+use strict;
+use Safe;
+
+my $expr = shift;
+
+my $cpt = new Safe;
+
+#Basic variable IO and traversal
+
+$cpt->permit(':base_core');
+
+$SIG{ALRM} = sub {
+ die "Alarm";
+};
+
+alarm(4);
+
+my $ret = $cpt->reval($expr);
+
+if ($@) {
+ print $@;
+} else {
+ print $ret;
+}
diff --git a/data/faq.txt.example b/data/faq.txt.example
new file mode 100644
index 0000000..ee371d1
--- /dev/null
+++ b/data/faq.txt.example
@@ -0,0 +1,4 @@
+This is an example FAQ item
+This is another FAQ item
+Oh wow a third one!
+Forth FAQ item
diff --git a/data/quotes.txt.example.pqf b/data/quotes.txt.example.pqf
new file mode 100644
index 0000000..54c9f25
--- /dev/null
+++ b/data/quotes.txt.example.pqf
@@ -0,0 +1,570 @@
+"But you read a lot of books, I'm thinking. Hard to have faith, ain't it, when you've read too many books?" -- (Terry Pratchett, Carpe Jugulum)
+"Nac mac Feegle wha hae!" -- (Terry Pratchett, Carpe Jugulum)
+In Ghat they believe in vampire watermelons, although folklore is silent about *what* they believe about vampire watermelons. Possibly they suck back. -- (Terry Pratchett, Carpe Jugulum)
+Perdita thought that not obeying rules was somehow *cool*. Agnes thought that rules like "Don't fall into this huge pit of spikes" were there for a purpose. -- (Terry Pratchett, Carpe Jugulum)
+Lancre operated on the feudal system, which was to say, everyone feuded all the time and handed on the fight to their descendants. -- (Terry Pratchett, Carpe Jugulum)
+"I name you ... Esmeralda Margaret Note Spelling of Lancre!" -- (Terry Pratchett, Carpe Jugulum)
+There are many rhymes about magpies, but none of them is very reliable because they are not the ones the magpies know themselves. -- (Terry Pratchett, Carpe Jugulum)
+One or two of the old barrows had been exposed over the years, their huge stones attracting their own folklore. If you left your unshod horse at one of them overnight and placed sixpence on the stone, in the morning the sixpence would be gone and you'd never see your horse again, either... -- (Terry Pratchett, Carpe Jugulum)
+Fewer birds could sit more meekly than the Lancre wowhawk, or lappet-faced worrier, a carnivore permanently on the lookout for the vegetarian option. -- (Terry Pratchett, Carpe Jugulum)
+- "Remember -- that which does not kill us can only make us stronger." - "And that which *does* kill us leaves us *dead*!" -- (Terry Pratchett, Carpe Jugulum)
+Rincewind is one of those people who gets in the way of his own happiness. If it was raining kisses he'd be the only person with an umbrella. -- (Terry Pratchett, CIX Pratchett Conference)
+He was always at a loss when people acted like this. When machines went funny you just oiled them or prodded them or, if nothing else worked, hit them with a hammer. Nomes didn't respond well to this treatment. -- (Terry Pratchett, Diggers)
+"Nah," he said, eventually. "I've looked at the colours on flowers. They're definitely built-in." -- (Terry Pratchett, Diggers)
+"There is nothing that can be in our way, for this is Jekub, that Laughs at Barriers, and says brrm-brrm." -- From the Book Of Nome, Jekub, Chap. 3, v. V (Terry Pratchett, Diggers)
+The trouble with having an open mind, of course, is that people will insist on coming along and trying to put things in it. -- (Terry Pratchett, Diggers)
+- "Do you know, humans think the world was made by a sort of big human?" - "Get away?" - "It took a week." - "I expect it had some help, then,' said Dorcas. -- The Nomes discuss religion (Terry Pratchett, Diggers)
+If broomsticks were cars, this one would be a split-window Morris Minor. -- (Terry Pratchett, Equal Rites)
+Still, it was a relief to get away from that macabre sight. Gander considered that gnolls didn't look any better inside than out. He hated their guts. -- (Terry Pratchett, Equal Rites)
+"While I'm still confused and uncertain, it's on a much higher plane, d'you see, and at least I know I'm bewildered about the really fundamental and important facts of the universe." Treatle nodded. "I hadn't looked at it like that," he said, "But you're absolutely right. He's really pushed back the boundaries of ignorance." -- Discworld scientists at work (Terry Pratchett, Equal Rites)
+They both savoured the strange warm glow of being much more ignorant than ordinary people, who were only ignorant of ordinary things. -- Discworld scientists at work (Terry Pratchett, Equal Rites)
+They may have been ugly. they may have been evil. But when it came to poetry in motion, the Things had all the grace and coordination of a deck-chair. -- Meet the creatures from the Dungeon Dimensions (Terry Pratchett, Equal Rites)
+"They say there's dwarf mines under the Ramtops," she said inconsequentially. "My, but them little buggers is in for a surprise." -- Granny reflects on Esk's methods of lighting a fire. (Terry Pratchett, Equal Rites)
+For animals, the entire universe has been neatly divided into things to (a) mate with, (b) eat, (c) run away from, and (d) rocks. -- (Terry Pratchett, Equal Rites)
+No enemies had ever taken Ankh-Morpock. Well *technically* they had, quite often; the city welcomed free-spending barbarian invaders, but somehow the puzzled raiders found, after a few days, that they didn't own their horses any more, and within a couple of months they were just another minority group with its own graffiti and food shops. -- (Terry Pratchett, Eric)
+Old Tom was the single cracked bronze bell in the University bell tower. The clapper dropped out shortly after it was cast, but the bell still tolled out some tremendously sonorous silences every hour. -- (Terry Pratchett, Eric)
+Rincewind had been told that death was just like going into another room. The difference is, when you shout, "Where's my clean socks?", no-one answers. -- (Terry Pratchett, Eric)
+It was true about the time measurement as well. The Tezumen had realized long ago that everything was steadily getting worse and, having a terrible little-mindedness, had developed a complex system to keep track of how much worse each succeeding day was. -- (Terry Pratchett, Eric)
+"These people were not only cheering, they were throwing flowers and hats. The hats were made of stone, but the thought was there." -- Life among the primitive Discworld tribes (Terry Pratchett, Eric)
+While working his way along a wall he came to a huge door, which artistically portrayed a group of prisoners apparently being given a complete medical check-up [footnote: From a distance it did, anyway. Close to, no]. -- Rincewind visits the Tezumen tribe (Terry Pratchett, Eric)
+- "There's a door" - "Where does it go?" - "It stays where it is, I think." -- (Terry Pratchett, Eric)
+The trouble is that things *never* get better, they just stay the same, only more so. -- (Terry Pratchett, Eric)
+- "So we're surrounded by absolutely nothing. There's a word for it. It's what you get when there's nothing left and everything's been used up." - "Yes. I think it's called the bill." -- (Terry Pratchett, Eric)
+- "What're quantum mechanics?" - "I don't know. People who repair quantums, I suppose." -- (Terry Pratchett, Eric)
+The librarian was, ex officio, a member of the college council. No-one had been able to find any rule about orang-utans being barred, although they had surreptiously looked very hard for one. -- Unseen University politics at work (Terry Pratchett, Eric)
+I HOPE WE ARE NOT GOING TO HAVE ANY OF THIS 'FOUL FIEND' BUSINESS AGAIN. -- Death gets summoned by the college council (Terry Pratchett, Eric)
+There had been some desultory talk about putting up a statue to Rincewind but, by the curious alchemy that tends to apply in these sensitive issues, this quickly became a plaque, then a note on the Roll of Honour, and finally a motion of censure for being improperly dressed. -- Unseen University politics at work (Terry Pratchett, Eric)
+Any wizard bright enough to survive for five minutes was also bright enough to realise that if there was any power in demonology, then it lay with the demons. Using it for your own purposes would be like trying to beat mice to death with a rattlesnake. -- Why summoning demons is a Bad Idea (Terry Pratchett, Eric)
+The gods of the Disc have never bothered much about judging the souls of the dead, and so people only go to hell if that's where they believe, in their deepest heart, that they deserve to go. Which they won't do if they don't know about it. This explains why it is so important to shoot missionaries on sight. -- (Terry Pratchett, Eric)
+"You mean mysterious ancient races of Amazonian princesses who subject all male prisoners to strange and exhausting progenitative rites?" said Eric, his glasses beginning to fog. -- (Terry Pratchett, Eric)
+The consensus seemed to be that if really large numbers of men were sent to storm the mountain, then enough might survive the rocks to take the citadel. This is essentially the basis of all military thinking. -- (Terry Pratchett, Eric)
+The sergeant put on the poker face which has been handed down from NCO to NCO ever since one protoamphibian told another, lower ranking protoamphibian to muster a squad of newts and Take That Beach. -- (Terry Pratchett, Eric)
+- "What shall I do?" - "Well, if you see anything crawl out of the sea and try to breathe, you could try telling it not to bother." -- Rincewind and Eric at the Beginning of Time (Terry Pratchett, Eric)
+"Multiple exclamation marks," he went on, shaking his head, "are a sure sign of a diseased mind." -- Something that Terry feels strongly about, because a similar quote also appears in "Reaper Man" (Terry Pratchett, Eric)
+The Supreme Life President of Hell wrote: "What business are we in???" He thought for a bit, and then carefully wrote, underneath: "We are in the damnation business!!!" -- (Terry Pratchett, Eric)
+"Bingeley bingeley beep!" -- (Terry Pratchett, Feet of Clay)
+...He hated the very idea of the world being divided into the shaved and the shavers. Or those who wore the shiny boots and those who cleaned the mud off them. Every time he saw Willikins the butler fold his, Vimes's, clothes, he suppressed a terrible urge to kick the butler's shiny backside as an affront to the dignity of man. -- (Terry Pratchett, Feet of Clay)
+I AM DEATH, NOT TAXES. *I* TURN UP ONLY ONCE. -- (Terry Pratchett, Feet of Clay)
+Slab: Jus' say "AarrghaarrghpleeassennononoUGH" -- Detritus' war on drugs (Terry Pratchett, Feet of Clay)
+And, while it was regarded as pretty good evidence of criminality to be living in a slum, for some reason owning a whole street of them merely got you invited to the very best social occasions. -- (Terry Pratchett, Feet of Clay)
+There were no public health laws in Ankh-Morpork. It would be like installing smoke detectors in Hell. -- (Terry Pratchett, Feet of Clay)
+"Just because someone's a member of an ethnic minority doesn't mean they're not a nasty small-minded little jerk [...]" -- (Terry Pratchett, Feet of Clay)
+You never ever volunteered. Not even if a sergant stood there and said, "We need someone to drink alcohol, bottles of, and make love, passionate, to women, for the use of." There was *always* a snag. If a choir of angels asked for volunteers for Paradise to step forward, Nobby knew enough to take one smart pace to the rear. -- (Terry Pratchett, Feet of Clay)
+"Today Is A Good Day For Someone Else To Die!" -- (Terry Pratchett, Feet of Clay)
+Rumour is information distilled so finely that it can filter through anything. It does not need doors and windows -- sometimes it does not need people. It can exist free and wild, running from ear to ear without ever touching lips. -- (Terry Pratchett, Feet of Clay)
+In all, I've had seventeen demands for your badge. Some want parts of your body attached. Why did you have to upset everybody? -- Lord Vetinari reproves Vimes. (Terry Pratchett, Feet of Clay)
+It was Carrot who'd suggested to the Patrician that hardened criminals should be given the chance to "serve the community" by redecorating the homes of the elderly, lending a new terror to old age and, given Ankh-Morpork's crime rate, leading to at least one old lady having her front room wallpapered so many times in six months that now she could only get in sideways. -- (Terry Pratchett, Feet of Clay)
+It was hard enough to kill a vampire. You could stake them down and turn them into dust and ten years later someone drops a drop of blood in the wrong place and *guess who's back*? They returned more times than raw broccoli. -- (Terry Pratchett, Feet of Clay)
+Many people, meeting Aziraphale for the first time, formed three impressions: that he was English, that he was intelligent, and that he was gayer than a tree full of monkeys on nitrous oxide. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+Kids! Bringing about Armageddon can be dangerous. Do not attempt it in your home. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+"You can't second-guess ineffability, I always say." -- (Terry Pratchett & Neil Gaiman, Good Omens)
+He was currently wondering vaguely who Moey and Chandon were. -- Crowley listens to his favourite rock group (Terry Pratchett & Neil Gaiman, Good Omens)
+He'd been particularly pleased with Manchester. -- Crowley contemplating his achievements (Terry Pratchett & Neil Gaiman, Good Omens)
+The only time Crowley had bought petrol was once in 1967, to get the free James Bond bullet-hole-in-the-windscreen transfers, which he rather fancied at the time. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+He wondered reflectively what would happen if you asked a nun where the Gents was. Probably the Pope sent you a sharp note or something. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+Humans suffering from a conflict of signals aren't the best people to be holding guns, especially when they've just witnessed a natural childbirth, which definitely looked an un-American way of bringing new citizens into the world. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+Of course he was all in favour of Armageddon in *general* terms. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+"You see a wile, you thwart. Am I right?" -- Crowley the demon and Aziraphale the angel in conversation (Terry Pratchett & Neil Gaiman, Good Omens)
+On those occasions when the angel managed to get his mind into the twentieth century, it always gravitated to 1950. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+They drove back through the dawn, while the cassette player played J. S. Bach's Mass in B Minor, vocals by F. Mercury. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+"Art thou a witch, *viva espana*?" -- The Spanish Inquisition (Tadfield version) in action (Terry Pratchett & Neil Gaiman, Good Omens)
+Anathema didn't only believe in ley-lines, but in seals, whales, bicycles, rainforests, whole grain in loves, recycled paper, white South Africans out of South Africa, and Americans out of practically everywhere down to and including Long Island. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+Shadwell hated all southerners and, by inference, was standing at the North Pole. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+DON'T THINK OF IT AS DYING, said Death. JUST THINK OF IT AS LEAVING EARLY TO AVOID THE RUSH. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+The Kappamaki, a whaling research ship, was currently researching the question: How many whales can you catch in one week? -- (Terry Pratchett & Neil Gaiman, Good Omens)
+The kraken stirs. And ten billion sushi dinners cry out for vengeance. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+"?" he said. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+Madame Tracy had even removed most of the Major Arcana from her Tarot card pack, because their appearance tended to upset people. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+I DON'T CARE WHAT IT SAYS, said the tall biker in the helmet, I NEVER LAID A FINGER ON HIM. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+Voodoo is a very interesting religion for the whole family, even those members of it who are dead. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+"Jesus won't cut you off before you're through With him you won't never get a crossed line, And when your bill comes it'll all be properly itemised He's the telephone repairman on the switchboard of my life. The phone line to the saviour's always free of interference He's in at any hour, day or night And when you call J-E-S-U-S you always call toll-free He's the telephone repairman on the switchboard of my life."
+-- (Terry Pratchett & Neil Gaiman, Good Omens)
+- "ALL YOU CAN HOPE FOR IS THE MERCY OF HELL." - "Yeah?" - "JUST OUR LITTLE JOKE." - "Ngk," said Crowley. -- Crowley in conversation with his superiors (Terry Pratchett & Neil Gaiman, Good Omens)
+Death and Famine and War and Pollution continued biking towards Tadfield. And Grievous Bodily Harm, Cruelty To Animals, Things Not Working Properly Even After You've Given Them A Good Thumping but secretly No Alcohol Lager, and Really Cool People travelled with them. -- The eight Bikers of the Apocalypse (Terry Pratchett & Neil Gaiman, Good Omens)
+Thud. Thud. Thud. Splat. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+"Did any of them kids have some space alien with a face like a friendly turd in a bike basket?" -- (Terry Pratchett & Neil Gaiman, Good Omens)
+Crowley had been extremely impressed with the warranties offered by the computer industry, and had in fact sent a bundle Below to the department that drew up the Immortal Soul agreements, with a yellow memo form attached just saying: "Learn, guys." -- Crowley is a demon, in case you don't know (Terry Pratchett & Neil Gaiman, Good Omens)
+R. P. Tyler was not, however, satisfied simply with being vouchsafed the difference between right and wrong. He felt it his bounden duty to tell the world. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+"This isn't how I imagined it, chaps," said War. "I haven't been waiting for thousands of years just to fiddle around with bits of wire. It's not what you'd call *dramatic*. Albrecht Duerer didn't waste his time doing woodcuts of the Four Button-Pressers of the Apocalypse, I do know that." -- Armageddon delayed by technical difficulties (Terry Pratchett & Neil Gaiman, Good Omens)
+"I don't see why it matters what is written. Not when it's about people. It can always be crossed out." -- (Terry Pratchett & Neil Gaiman, Good Omens)
+In the Beginning
+It was a nice day. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+If you take the small view, the universe is just something small and round, like those water-filled balls which produce a miniature snowstorm when you shake them. Although, unless the ineffable plan is a lot more ineffable than it's given credit for, it does not have a large plastic snowman at the bottom. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+- "You're Hells Angels, then? What chapter are you from?" - REVELATIONS, CHAPTER SIX. -- Death in conversation with a biker (Terry Pratchett & Neil Gaiman, Good Omens)
+The lorry blocked the road. And the corrugated iron blocked the road. And a thirty-foot-high pile of fish blocked the road. It was one of the most effectively blocked roads the sergeant had ever seen. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+Crowley was in Hell's bad books. Not that Hell has any other kind. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+God does not play dice with the universe: He plays an ineffable game of His own devising, which might be compared, from the perspective of any of the other players [i.e. everybody], to being involved in an obscure and complex variant of poker in a pitch-dark room, with blank cards, for infinite stakes, with a Dealer who won't tell you the rules, and who *smiles all the time*. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+It wasn't a dark and stormy night. It should have been, but there's the weather for you. For every mad scientist who's had a convenient thunderstorm just on the night his Great Work is complete and lying on the slab, there have been dozens who've sat around aimlessly under the peaceful stars while Igor clocks up the overtime. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+Many phenomena - wars, plagues, sudden audits - have been advanced as evidence for the hidden hand of Satan in the affairs of Man, but whenever students of demonology get together the M25 London orbital motorway is generally agreed to be among the top contenders for exhibit A. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+Sister Mary headed through the night-time hospital with the Adversary, Destroyer of Kings, Angel of the Bottomless Pit, Great Beast that is called Dragon, Prince of This World, Father of Lies, Spawn of Satan and Lord of Darkness safely in her arms. She found a bassinet and laid him down in it. He gurgled. She gave him a tickle. -- The antichrist is born (Terry Pratchett & Neil Gaiman, Good Omens)
+Mr Young hadn't had to quiet a screaming baby for years. He'd never been much good at it to start with. He'd always respected Sir Winston Churchill, and patting small versions of him on the bottom had always seemed ungracious. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+The ducks in St James's Park are so used to being fed bread by secret agents meeting clandestinely that they have developed their own Pavlovian reaction. Put a St James's Park duck in a laboratory cage and show it a picture of two men -- one usually wearing a coat with a fur collar, the other something sombre with a scarf -- and it'll look up expectantly. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+A man threw himself through the window, a knife between his teeth, a Kalashnikov automatic rifle in one hand, a grenade in the other. "I glaim gis oteg in der gaing og der --" he paused. He tooke the knife out of his teeth and began again. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+Jaime had never realised that trees made a sound when they grew, and no-one else had realised it either, because the sound is made over hundreds of years in waves of twenty-four hours from peak to peak. Speed it up, and the sound a tree makes is *vrooom*. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+... walking like a man carrying a thermos flask of something that might cause, if he dropped it or even thought about dropping it, the sort of explosion that impels grey-beards to make statements like "And where this crater is now, once stood the city of Wah-Shing-Ton", in SF B-movies. -- Crowley gets out the Holy Water (Terry Pratchett & Neil Gaiman, Good Omens)
+She'd stopped reading the kind of women's magazine that talked about romance and knitting and started reading the kind of women's magazine that talked about orgasms, but apart from making a mental note to have one if ever the occasion presented itself she dismissed them as only romance and knitting in a new form. -- (Terry Pratchett & Neil Gaiman, Good Omens)
+"[...] a number of offences of murder by means of a blunt instrument, to whit, a dragon, and many further offences of generalized abetting [...]" -- (Terry Pratchett, Guards! Guards!)
+"Have another drink, not-Corporal Nobby?" said Sergeant Colon unsteadily. "I do not mind if I do, not-Sgt Colon," said Nobby. -- The joys of working undercover (Terry Pratchett, Guards! Guards!)
+"'E's fighting in there!" he stuttered, grabbing the captain's arm. "All by himself?" said the captain. "No, with everyone!" shouted Nobby, hopping from one foot to the other. -- Making Friends and Hitting People (Terry Pratchett, Guards! Guards!)
+FABRICATI DIEM, PVNC. -- The motto of the Ankh-Morpork City Watch (Terry Pratchett, Guards! Guards!)
+"Pour encourjay lays ortras." -- (Terry Pratchett, Guards! Guards!)
+"This is Lord Mountjoy Quickfang Winterforth IV, the hottest dragon in the city. It could burn your head clean off." -- Captain Vimes addresses a band of rioters (Terry Pratchett, Guards! Guards!)
+A good bookshop is just a genteel Black Hole that knows how to read. -- (Terry Pratchett, Guards! Guards!)
+There was a thoughtful pause in the conversation as the assembled Brethren mentally divided the universe into the deserving and the undeserving, and put themselves on the appropriate side. -- The Elucidated Brethren see the light (Terry Pratchett, Guards! Guards!)
+All dwarfs have beards and wear up to twelve layers of clothing. Gender is more or less optional. -- (Terry Pratchett, Guards! Guards!)
+All dwarfs are by nature dutiful, serious, literate, obedient and thoughtful people whose only minor failing is a tendency, after one drink, to rush at enemies screaming "Arrrrrrgh!" and axing their legs off at the knee. -- (Terry Pratchett, Guards! Guards!)
+People who are rather more than six feet tall and nearly as broad across the shoulders often have uneventful journeys. People jump out at them from behind rocks then say things like, "Oh. Sorry. I thought you were someone else." -- Carrot travels to Ankh-Morpork (Terry Pratchett, Guards! Guards!)
+He nodded to the troll which was employed by the Drum as a splatter [footnote: Like a bouncer, but trolls use more force]. -- Nobby takes Carrot for a drink in The Mended Drum (Terry Pratchett, Guards! Guards!)
+It was possibly the most circumspect advance in the history of military manoeuvres, right down at the bottom end of the scale that things like the Charge of the Light Brigade are at the top of. -- The City Watch takes action (Terry Pratchett, Guards! Guards!)
+Lady Ramkin's bosom rose and fell like an empire. -- (Terry Pratchett, Guards! Guards!)
+It's a metaphor of human bloody existence, a dragon. And if that wasn't bad enough, it's also a bloody great hot flying thing. -- Captain Vimes ponders his problems (Terry Pratchett, Guards! Guards!)
+The three rules of the Librarians of Time and Space are: 1) Silence; 2) Books must be returned no later than the date last shown; and 3) Do not interfere with the nature of causality. -- (Terry Pratchett, Guards! Guards!)
+A number of religions in Ankh-Morpork still practiced human sacrifice, except that they didn't really need to practice any more because they had got so good at it. -- (Terry Pratchett, Guards! Guards!)
+Thunder rolled. ... It rolled a six. -- (Terry Pratchett, Guards! Guards!)
+"Right, you bastards, you're... you're geography" -- (Terry Pratchett, Guards! Guards!)
+"The significant owl hoots in the night." -- (Terry Pratchett, Guards! Guards!)
+"Real children don't go hoppity-skip unless they are on drugs." -- Susan, the ultimate sensible governess (Terry Pratchett, Hogfather)
+Getting an education was a bit like a communicable sexual disease. It made you unsuitable for a lot of jobs and then you had the urge to pass it on. -- (Terry Pratchett, Hogfather)
+She'd become a governess. It was one of the few jobs a known lady could do. And she'd taken to it well. She'd sworn that if she did indeed ever find herself dancing on rooftops with chimney sweeps she'd beat herself to death with her own umbrella. -- (Terry Pratchett, Hogfather)
+"ER...HO. HO. HO." -- Death makes a career move (Terry Pratchett, Hogfather)
+Biers was where the undead drank. And when Igor the barman was asked for a Bloody Mary, he didn't mix a metaphor. -- (Terry Pratchett, Hogfather)
+"Did you check the list?" YES. TWICE. ARE YOU SURE THAT'S ENOUGH? -- He's gonna find out... (Terry Pratchett, Hogfather)
+"That statement is either so deep it would take a lifetime to fully comprehend every particle of its meaning, or it is a load of absolute tosh. Which is it, I wonder?" -- (Terry Pratchett, Hogfather)
+"Real stupidity beats artificial intelligence every time." -- Bursar 1 - Hex 0 (Terry Pratchett, Hogfather)
+*Glingleglingleglingle.* -- (Terry Pratchett, Hogfather)
+Everything starts somewhere, though many physicists disagree. But people have always been dimly aware of the problem with the start of things. They wonder how the snowplough driver gets to work, or how the makers of dictionaries look up the spelling of words. -- (Terry Pratchett, Hogfather)
+We took pity on him because he'd lost both parents at an early age. I think that, on reflection, we should have wondered a bit more about that. -- Lord Downey reflects on Mister Teatime (Terry Pratchett, Hogfather)
+It's a sad and terrible thing that high-born folk really have thought that the servants would be totally fooled if spirits were put into decanters that were cunningly labelled *backwards*. And also throughout history the more politically conscious butler has taken it on trust, and with rather more justification, that his employers will not notice if the whisky is topped up with eniru. -- (Terry Pratchett, Hogfather)
+The truth may be out there, but lies are inside your head. -- (Terry Pratchett, Hogfather)
++++ Divide By Cucumber Error. Please Reinstall Universe And Reboot +++ -- (Terry Pratchett, Hogfather)
+"Millennium hand and shrimp." -- (Terry Pratchett, Hogfather)
+Rincewind could scream for mercy in nineteen languages, and just scream in another forty-four. -- (Terry Pratchett, Interesting Times)
+After the stampede the artist Three Solid Frogs got to his feet, retrieved his brush from his nostril, pulled his easel out of a tree, and tried to think placid thoughts. -- (Terry Pratchett, Interesting Times)
+Just because it's not nice doesn't mean it's not miraculous. -- (Terry Pratchett, Interesting Times)
+++?????++ Out of Cheese Error. Redo From Start. -- (Terry Pratchett, Interesting Times)
+"Luck is my middle name," said Rincewind, indistinctly. "Mind you, my first name is Bad." -- (Terry Pratchett, Interesting Times)
+Natural selection saw to it that professional heroes who at a crucial moment tended to ask themselves questions like "What is my purpose in life?" very quickly lacked both. -- (Terry Pratchett, Interesting Times)
+"Stercus, stercus, stercus, moriturus sum." -- (Terry Pratchett, Interesting Times)
+The Emperor had all the qualifications for a corpse except, as it were, the most vital one. -- (Terry Pratchett, Interesting Times)
+"I know about people who talk about suffering for the common good. It's never bloody them! When you hear a man shouting "Forward, brave comrades!" you'll see he's the one behind the bloody big rock and the one wearing the only really arrow-proof helmet!" -- Rincewind gives a speech on politics. (Terry Pratchett, Interesting Times)
+Many an ancient lord's last words had been, "You can't kill me because I've got magic aaargh." -- Magic armour is not all it's cracked up to be. (Terry Pratchett, Interesting Times)
+[...] Vimes's grin was as funny as the one that moves very fast towards drowning men. And has a fin on top. -- (Terry Pratchett, Jingo)
+"Taxation, gentlemen, is very much like dairy farming. The task is to extract the maximum amount of milk with the minimum of moo. And I am afraid to say that these days all I get is moo." -- (Terry Pratchett, Jingo)
+She sighed again. She was familiar with the syndrome. They *said* they wanted a soulmate and helpmeet but sooner or later the list would include a skin like silk and a chest fit for a herd of cows. -- (Terry Pratchett, Jingo)
+One of the universal rules of happiness is: always be wary of any helpful item that weighs less than its operating manual. -- (Terry Pratchett, Jingo)
+"Give a man a fire and he's warm for a day, but set fire to him and he's warm for the rest of his life." -- (Terry Pratchett, Jingo)
+"*Veni, vici*...Vetinari." -- (Terry Pratchett, Jingo)
+And there was nothing finer than a wizard dressed up formally, until someone could find a way of inflating a Bird of Paradise, possibly by using an elastic band and some kind of gas. -- (Terry Pratchett, Jingo)
+"'Chapter Fifteen, Elementary Necromancy'", she read out loud. "'Lesson One: Correct Use of Shovel...'" -- (Terry Pratchett, Jingo)
+"One o'clock pee em! Hello, Insert Name Here!" -- The Dis-organizer (Terry Pratchett, Jingo)
+He had the look of a lawn mower just after the grass had organised a workers' collective. There was a definite suggestion that, deep inside, he knew this was not really happening. It could not be happening because this sort of thing did not happen. Any contradictory evidence could be safely ignored. -- (Terry Pratchett, Jingo)
+It was so much easier to blame it on Them. It was bleakly depressing to think that They were Us. If it was Them, then nothing was anyone's fault. If it was us, what did that make Me? After all, I'm one of Us. I must be. I've certainly never thought of myself as one of Them. *No one* ever thinks of themselves as one of Them. We're always one of Us. It's Them that do the bad things. -- (Terry Pratchett, Jingo)
+Bigmac wasn't an athlete. If there was an Olympic Sick Note event, he would've won the 100 metres I've Got Asthma, the half marathon Lurk in the Changing Rooms, and the freestyle Got to Go to the Doctor. -- (Terry Pratchett, Johnny and the Bomb)
+"Is that *you*?" said a female voice. It wasn't exactly an unpleasant one, but it had a sharp penetrating quality. It seemed to be saying that if you *weren't* you, then it was *your* fault. Johnny recognized it instantly. It was the voice of someone who dialled the wrong numbers and then complained that the phone was answered by people she didn't want to speak to. -- (Terry Pratchett, Johnny and the Bomb)
+When all else failed, she tried being reasonable. -- (Terry Pratchett, Johnny and the Bomb)
+"You're not allowed to call them dinosaurs anymore." said Yo-less. "It's speciesist. You have to call them pre-petroleum persons." -- (Terry Pratchett, Johnny and the Bomb)
+*No-one* liked the Joshua N'Clement block. There were two schools of thought about what should be done with it. The people who lived there thought everyone should be taken out en then the block should be blown up, and the people who lived *near* the block just wanted it blown up. -- (Terry Pratchett, Johnny and the Dead)
+There was a new library in the Civic Centre. It was so new it didn't even have librarians. It had Assistant Information Officers. -- (Terry Pratchett, Johnny and the Dead)
+Johnny had seen films of American shopping malls. They must have different sorts of people in America, he'd thought. They all looked cool, all the girls were beautiful, and the place wasn't crowded with little kamikaze grandmothers. -- (Terry Pratchett, Johnny and the Dead)
+"You've got a lot of time for abstract thought when you've got your hand stuck up a dead badger." -- (Terry Pratchett, Johnny and the Dead)
+The figures the telescope was producing were all that was left of an exploding star twenty million years ago. A billion small rubbery things on two planets who had been getting on with life in a quiet sort of way had been totally destroyed, but they were certainly helping Adrian get his Ph.D. and, who knows, they might have thought it all worthwhile if anyone had asked them. -- (Terry Pratchett, Johnny and the Dead)
+"I believe it's very hard to have fun in Iceland without fish being involved in some way." -- Looking for a good place to party (Terry Pratchett, Johnny and the Dead)
+- "America?" said Mrs Liberty. "Won't we get scalped?" - "Good grief, no!" said William Stickers, who was a bit more up to date about the world. - "*Probably* not," said Mr Fletcher, who had been watching the news lately and was even more up to date than William Stickers. -- Still looking for a good place to party (Terry Pratchett, Johnny and the Dead)
+Sergeant Comely was working on the general assumption that where you got lots of people gathered together, something illegal was bound to happen sooner or later. -- (Terry Pratchett, Johnny and the Dead)
+Mrs. Nugent was the Johnson's next door neighbour, and known to be unreasonable on subjects like Madonna played at full volume at 3 a.m. -- (Terry Pratchett, Johnny and the Dead)
+"Why bother with such a big stone arch?" "It's just showing off. There's probably a sticker on the back saying 'My Other Grave Is A Porch'". -- (Terry Pratchett, Johnny and the Dead)
+Granddad was superstitious about books. He thought that if you had enough of them around, education leaked out, like radioactivity. -- (Terry Pratchett, Johnny and the Dead)
+"Are you a physicist?" "Me? I don't know anything about science!" "Marvellous! Ideal qualification!" -- (Terry Pratchett, Johnny and the Dead)
+Suicide was against the law. Johnny had wondered why. It meant that if you missed, or the gas ran out, or the rope broke, you could get locked up in prison to show you that life was really very jolly and thoroughly worth living. -- (Terry Pratchett, Johnny and the Dead)
+"Oook!"
+"Oook?"
+I MUST SAY THESE ARE VERY GOOD BISCUITS. HOW DO THEY GET THE BITS OF CHOCOLATE IN? -- Death has a snack (Terry Pratchett, Lords and Ladies)
+Nanny Ogg never did any housework herself, but she was the cause of housework in other people. -- (Terry Pratchett, Lords and Ladies)
+Verence would rather cut his own leg off than put a witch in prison, since it'd save trouble in the long run and probably be less painful. -- (Terry Pratchett, Lords and Ladies)
+I LIKE TO THINK I AM A PICKER-UP OF UNCONSIDERED TRIFLES. Death grinned hopefully. -- (Terry Pratchett, Lords and Ladies)
+Mustrum Ridcully did a lot for rare species. For one thing, he kept them rare. -- (Terry Pratchett, Lords and Ladies)
+Using a metaphor in front of a man as unimaginative as Ridcully was like a red flag to a bu-- was like putting something very annoying in front of someone who was annoyed by it. -- (Terry Pratchett, Lords and Ladies)
+The thing about iron is that you generally don't have to think fast in dealing with it. -- (Terry Pratchett, Lords and Ladies)
+Nanny Ogg looked under her bed in case there was a man there. Well, you never knew your luck. -- (Terry Pratchett, Lords and Ladies)
+The chieftain had been turned into a pumpkin although, in accordance with the rules of universal humour, he still had his hat on. -- (Terry Pratchett, Lords and Ladies)
+For Magrat, stepping into a man's bedroom was like an explorer stepping on to that part of the map marked Here Be Dragons. -- (Terry Pratchett, Lords and Ladies)
+"I wants your body, Mrs Ogg." -- Casanunda makes his move (Terry Pratchett, Lords and Ladies)
+"I know she's in there," said Verence, holding his crown in his hands in the famous Ai-Senor-Mexican-Bandits-Have-Raided-Our-Village position. -- (Terry Pratchett, Lords and Ladies)
+In fact, the mere act of opening the box will determine the state of the cat, although in this case there were three determinate states the cat could be in: these being Alive, Dead, and Bloody Furious. -- Schrodinger's Moggy explained (Terry Pratchett, Lords and Ladies)
+The shortest unit of time in the multiverse is the New York Second, defined as the period of time between the traffic lights turning green and the cab behind you honking. -- (Terry Pratchett, Lords and Ladies)
+"Serve 'em right for not inviting me to their weddings." -- Ridcully contemplates the Trousers of Time (Terry Pratchett, Lords and Ladies)
+"Hah, I can just see a real playsmith putting *donkeys* in a play!" -- (Terry Pratchett, Lords and Ladies)
+"Do not meddle in the affairs of wizards, especially simian ones. They are not all that subtle." -- (Terry Pratchett, Lords and Ladies)
+"Go ahead, bake my quiche" -- Magrat instructs the castle cook (Terry Pratchett, Lords and Ladies)
+In the Beginning there was nothing, which exploded. -- (Terry Pratchett, Lords and Ladies)
+Remember, A Dragon is For Life, Not Just for Hogswatchnight -- Motto of The Sunshine Home for Sick Dragons in Morphic Street, Please Leave Donations of Coal by Side Door. (Terry Pratchett, Lords and Ladies)
+The place looked as though it had been visited by Gengiz Cohen [footnote: hence the term "wholesale destruction"]. -- (Terry Pratchett, Lords and Ladies)
+"This is a lovely party," said the Bursar to a chair, "I wish I was here." -- The Bursar is a man under a *lot* of stress (Terry Pratchett, Lords and Ladies)
+No matter what she did with her hair it took about three minutes for it to tangle itself up again, like a garden hosepipe in a shed [Which, no matter how carefully coiled, will always uncoil overnight and tie the lawnmower to the bicycles]. -- (Terry Pratchett, Lords and Ladies)
+He married that Palliard girl, remember? The one with the air-cooled teeth? -- (Terry Pratchett, Lords and Ladies)
+And the child had a permanently runny nose and ought to be provided with a handkerchief or, failing that, a cork. -- (Terry Pratchett, Lords and Ladies)
+It was here that the thaum, hitherto believed to be the smallest possible particle of magic, was succesfully demonstrated to be made up of /resons/ (Lit.: 'Thing-ies') or reality fragments. Currently research indicates that each reson is itself made up of a combination of at least five 'flavours', known as 'up', 'down', 'sideways', 'sex appeal' and 'peppermint'. -- (Terry Pratchett, Lords and Ladies)
+A heap of discarded garments by the bed suggested that Verence had mastered the art of hanging up clothes as practised by half the population of the world, and that he had equally had difficulty with the complex topological manoeuvres necessary to turn the socks the right way out. -- (Terry Pratchett, Lords and Ladies)
+Chain-mail isn't much defence against an arrow. It certainly isn't when the arrow is being aimed between your eyes. -- (Terry Pratchett, Lords and Ladies)
+It's not enough to be able to pick up a sword. You have to know which end to poke into the enemy. -- (Terry Pratchett, Lords and Ladies)
+The Monks of Cool, whose tiny and exclusive monastery is hidden in a really cool and laid-back valley in the lower Ramtops, have a passing-out test for a novice. He is taken into a room full of all types of clothing and asked: Yo, my son, which of these is the most stylish thing to wear? And the correct answer is: Hey, whatever I select. -- (Terry Pratchett, Lords and Ladies)
+The person on the other side was a young woman. Very obviously a young woman. There was no possible way that she could have been mistaken for a young man in any language, especially Braille. -- The goddess with the nice earrings (Terry Pratchett, Maskerade)
+Nanny Ogg found herself embarrassed to even think about this, and this was unusual because embarrassment normally came as naturally to Nanny as altruism comes to a cat. -- (Terry Pratchett, Maskerade)
+People who didn't need people needed people around to know that they were the kind of people who didn't need people. -- (Terry Pratchett, Maskerade)
+He had a unique stride: it looked as though his body was being dragged forward and his legs had to flail around underneath it, landing wherever they could find room. It wasn't so much a walk as a collapse, indefinitely postponed. -- (Terry Pratchett, Maskerade)
+She'd even given herself a middle initial - X - which stood for "someone who has a cool and exciting middle name". -- (Terry Pratchett, Maskerade)
+"What sort of person," said Salzella patiently, "sits down and *writes* a maniacal laugh? And all those exclamation marks, you notice? Five? A sure sign of someone who wears his underpants on his head. Opera can do that to a man." -- (Terry Pratchett, Maskerade)
+"The singers all loathe the sight of one another, the chorus despises the singers, they both hate the orchestra, and everyone fears the conductor; the staff on one prompt side won't talk to the staff on the opposite prompt side, the dancers are all crazed from hunger in any case..." -- (Terry Pratchett, Maskerade)
+Most people in Lancre, as the saying goes, went to bed with the chickens and got up with the cows. [footnote: Er. That is to say, they went to bed at the same time as the chickens went to bed, and got up at the same time as the cows got up. Loosely worded sayings can really cause misunderstandings.] -- (Terry Pratchett, Maskerade)
+"...my father is the Emperor of Klatch and my mother is a small tray of raspberry puddings." -- Agnes tells Christine about herself (Terry Pratchett, Maskerade)
+Instead, people would take pains to tell her that beauty was only skin-deep, as if a man ever fell for an attractive pair of kidneys. -- (Terry Pratchett, Maskerade)
+A day ago the future had looked aching and desolate, and now it looked full of surprises and terror and bad things happening to people... If she had anything to do with it anyway. -- Granny Weatherwax commits optimism (Terry Pratchett, Maskerade)
+- "There have been...accidents." - "What kind of accidents?" - "The kind of accidents you prefer to call...accidents." -- (Terry Pratchett, Maskerade)
+- "But I don't *believe* in reincarnation!" he protested. - SQUEAK. - And this, Mr Pounder understood with absolute rodent clarity, meant: Reincarnation believes in *you*. -- (Terry Pratchett, Maskerade)
+It was done far more often than the audiences ever realized -- when singers had a sore throat, or had completely dried, or had turned up so drunk they could barely stand, or, in one notorious instance many years previously, had died in the interval and subsequently sung their famous aria by means of a broom-handle stuck up their back and their jaw operated with a piece of string. -- (Terry Pratchett, Maskerade)
+After you'd known Christine for any length of time, you found yourself fighting a desire to look into her ear to see if you could spot daylight coming the other way. -- (Terry Pratchett, Maskerade)
+The pre-luncheon drinks were going quite well, Mr Bucket thought. Everyone was making polite conversation and absolutely no one had been killed up to the present moment. -- (Terry Pratchett, Maskerade)
+Nanny could get a statue to cry on her shoulder and say what it really thought about pigeons. -- (Terry Pratchett, Maskerade)
+Greebo could, in fact, commit sexual harrassment simply by sitting very quietly in the next room. -- (Terry Pratchett, Maskerade)
+It is the fate of all banisters worth sliding down that there is something nasty waiting at the far end. -- (Terry Pratchett, Maskerade)
+If the Creator had said, "Let there be light" in Ankh-Morpork, he'd have gotten no further because of all the people saying "What colour?" -- (Terry Pratchett, Men at Arms)
+From the back, Vetinari looked like a carnivorous flamingo. -- (Terry Pratchett, Men at Arms)
+Cuddy had only been a guard for a few days, but already he had absorbed one important and basic fact: it is almost impossible for anyone to be in a street without breaking the law. -- (Terry Pratchett, Men at Arms)
+The Battle of Koom Valley is the only one known to history where both sides ambushed each other. -- (Terry Pratchett, Men at Arms)
+Carrot was two metres tall but he'd been brought up as a dwarf, and then further up as a human. -- (Terry Pratchett, Men at Arms)
+"Young Edward thinks that there is no lake of blood too big to wade through to put a rightful king on a throne, no deed too base in defence of a crown. A romantic, in fact." -- Lord Rust describing Edward d'Eath (Terry Pratchett, Men at Arms)
+The Ramkins were more highly bred than a hilltop bakery, whereas Corporal Nobbs had been disqualified from the human race for shoving. -- (Terry Pratchett, Men at Arms)
+He was said to have the body of a twenty-five year old, although no one knew where he kept it. -- The Life and Times of Corporal Nobbs (Terry Pratchett, Men at Arms)
+"Pride is all very well, but a sausage is a sausage." -- Gaspode, of course (Terry Pratchett, Men at Arms)
+The river Ankh is probably the only river in the universe on which the investigators can chalk the outline of the corpse. -- (Terry Pratchett, Men at Arms)
+The Alchemist's Guild is opposite the Gambler's Guild. Usually. Sometimes it's above it, or below it, or falling in bits around it. -- (Terry Pratchett, Men at Arms)
+Sham Harga had run a succesful eatery for many years by always smiling, never extending credit, and realizing that most of his customers wanted meals properly balanced between the four food groups: sugar, starch, grease and burnt crunchy bits. -- (Terry Pratchett, Men at Arms)
+The Patrician relaxed, in a way which only then drew gentle attention to the foregoing moment of tension. -- (Terry Pratchett, Men at Arms)
+Sometimes it's better to light a flamethrower than curse the darkness. -- (Terry Pratchett, Men at Arms)
+Being a werewolf meant having the dexterity and jaw power to instantly rip out a man's jugular. It was a trick of her father's that had always annoyed her mother, especially when he did it just before meals. -- (Terry Pratchett, Men at Arms)
+The door was still ajar, but there was a tentative tap on it which said, in a kind of metaphorical morse code, that the tapper could see very well that Carrot was in his room with a scantily clad woman and was trying to knock without actually being heard. -- (Terry Pratchett, Men at Arms)
+"It's got three keyboards and a hundred extra knobs, including twelve with '?' on them." -- The Unseen University Organ, as designed by B. S. Johnson (Terry Pratchett, Men at Arms)
+- BIG FIDO? - "Yes?" - HEEL. -- (Terry Pratchett, Men at Arms)
+The Librarian of Unseen University had unilaterally decided to aid comprehension by producing an Orang-utan/Human Dictionary. He'd been working on it for three months. It wasn't easy. He'd got as far as "Oook". -- (Terry Pratchett, Men at Arms)
+- "It could be a torture chamber or a dungeon or a hideous pit or anything!" - "It's just a student's bedroom, sergeant." - "You see?" -- (Terry Pratchett, Men at Arms)
+The maze was so small that people got lost looking *for* it. -- Bloody Stupid Johnson at his finest (Terry Pratchett, Men at Arms)
+"It would seem that you have no useful skill or talent whatsoever," he said. "Have you thought of going into teaching?" -- (Terry Pratchett, Mort)
+Only one creature could have duplicated the expressions on their faces, and that would be a pigeon who has heard not only that Lord Nelson has got down off his column but has also been seen buying a 12-bore repeater and a box of cartridges. -- (Terry Pratchett, Mort)
+- "My granny says that dying is like going to sleep," Mort added, a shade hopefully. - I WOULDN'T KNOW. I HAVE DONE NEITHER. -- (Terry Pratchett, Mort)
+- "Pardon me for living, I'm sure." - NO-ONE GETS PARDONED FOR LIVING. -- (Terry Pratchett, Mort)
+Although the scythe isn't pre-eminent among the weapons of war, anyone who has been on the wrong end of, say, a peasants' revolt will know that in skilled hands it is fearsome. -- (Terry Pratchett, Mort)
+"Do not meddle in the affairs of wizards because a refusal often offends, I read somewhere." -- (Terry Pratchett, Mort)
+"When a man is tired of Ankh-Morpork, he is tired of ankle-deep slurry." -- (Terry Pratchett, Mort)
+I DON'T KNOW ABOUT YOU, he said, BUT I COULD MURDER A CURRY. -- Death addresses his new apprentice (Terry Pratchett, Mort)
+Poets have tried to describe Ankh-Morpork. They have failed. Perhaps it's the sheer zestful vitality of the place, or maybe it's just that a city with a million inhabitants and no sewers is rather robust for poets, who prefer daffodils and no wonder. -- (Terry Pratchett, Mort)
+It is a fact that although the Death of the Discworld is, in his own words, an ANTHROPOMORPHIC PERSONIFICATION, he long ago gave up using the traditional skeletal horses, because of the bother of having to stop all the time to wire bits back on. -- (Terry Pratchett, Mort)
+"You're dead," he said. Keli waited. She couldn't think of any suitable reply. "I'm not" lacked a certain style, while "Is it serious?" seemed somehow too frivolous. -- Princess Keli in trouble (Terry Pratchett, Mort)
+The thing between Death's triumphant digits was a fly from the dawn of time. It was the fly in the primordial soup. It had bred on mammoth turds. It wasn't a fly that bangs on window panes, it was a fly that drills through walls. -- Death goes fishing (Terry Pratchett, Mort)
+Ankh-Morpork had dallied with many forms of government and had ended up with that form of democracy known as One Man, One Vote. The Patrician was the Man; he had the Vote. -- Discworld politics explained (Terry Pratchett, Mort)
+- I USHERED SOULS INTO THE NEXT WORLD. I WAS THE GRAVE OF ALL HOPE. I WAS THE ULTIMATE REALITY. I WAS THE ASSASSIN AGAINST WHOM NO LOCK WOULD HOLD. - "Yes, point taken, but do you have any particular skills?" -- Death consults a job broker (Terry Pratchett, Mort)
+- "Sodomy non sapiens," said Albert under his breath. - "What does that mean?" - "Means I'm buggered if I know." -- Mort and Albert are facing a problem (Terry Pratchett, Mort)
+Women's clothes were not a subject that preoccupied Cutwell much -- in fact, usually when he thought about women his mental pictures seldom included any clothes at all -- but the vision in front of him really did take his breath away. -- Princess Keli prepares for her coronation (Terry Pratchett, Mort)
+"You won't get away with this," said Cutwell. He thought for a bit and added, "Well, you will probably get away with it, but you'll feel bad about it on your deathbed and you'll wish -- " He stopped talking. -- Cutwell tries to reason with the Duke of Sto Helit (Terry Pratchett, Mort)
+"What do people like to drink here, then?" The landlord looked sideways at his customers, a clever trick given that they were directly in front of him. -- Mort goes out for a drink (Terry Pratchett, Mort)
+"You like it?" he said to Mort, in pretty much the same tone of voice people used when they said to St George, "You killed a *what*?" -- Mort tastes scrumble for the first time (Terry Pratchett, Mort)
+The Librarian had seen many weird things in his time, but that had to be the 57th strangest. [footnote: he had a tidy mind] -- (Terry Pratchett, Moving Pictures)
+"Woof bloody woof." -- Gaspode the Wonder Dog (Terry Pratchett, Moving Pictures)
+No-one would have believed, in the final years of the Century of the Fruitbat, that Discworld affairs were being watched keenly and impatiently by intelligences greater than Man's, or at least much nastier; that their affairs were being scrutinised and studied as a man with a three-day appetite might study the All-You-Can-Gobble-For-A-Dollar menu outside Harga's House of Ribs... -- (Terry Pratchett, Moving Pictures)
+It was the sort of thing you expected in the Street of Alchemists. The neighbours *preferred* explosions, which were at least identifiable and soon over. They were better than the smells, which crept up on you. -- (Terry Pratchett, Moving Pictures)
+"Meat pies! Hot sausages! Inna bun! So fresh the pig h'an't noticed they're gone!" -- Genuine pig portion packages (Terry Pratchett, Moving Pictures)
+The Archchancellor's most important job, as the Bursar saw it, was to sign things, preferably, from the Bursar's point of view, without reading them first. -- Middle management explained (Terry Pratchett, Moving Pictures)
+By and large, the only skill the alchemists of Ankh-Morpork had discovered so far was the ability to turn gold into less gold. -- (Terry Pratchett, Moving Pictures)
+Most alchemists were nervous, in any case; it came from not knowing what the crucible of bubbling stuff they were experimenting with was going to do next. -- (Terry Pratchett, Moving Pictures)
+"If you put butter and salt on it, it tastes like salty butter." -- Popcorn comes to the Discworld (Terry Pratchett, Moving Pictures)
+"Students?" barked the Archchancellor. "Yes, Master. You know? They're the thinner ones with the pale faces? Because we're a *university*? They come with the whole thing, like rats --" -- (Terry Pratchett, Moving Pictures)
+Of course, it is very important to be sober when you take an exam. Many worthwhile careers in the street-cleansing, fruit-picking and subway-guitar-playing industries have been founded on a lack of understanding of this simple fact. -- (Terry Pratchett, Moving Pictures)
+And then you bit onto them, and learned once again that Cut-me-own-Throat Dibbler could find a use for bits of an animal that the animal didn't know it had got. Dibbler had worked out that with enough fried onions and mustard people would eat *anything*. -- A fact McDonalds knows about as well (Terry Pratchett, Moving Pictures)
+"The thing is that Mr. Dibbler can even sell sausages to people who have bought them off him *before*." -- Now *that's* marketing (Terry Pratchett, Moving Pictures)
+- "You pay for it before you eat it? What happens if it's dreadful?" - "That's why." -- (Terry Pratchett, Moving Pictures)
+"One minute I'm just another rabbit and happy about it, next minute *whazaam*, I'm thinking. That's a major drawback if you're looking for happiness as a rabbit, let me tell you. You want grass and sex, not thoughts like 'What's it all about, when you get right down to it?'" -- (Terry Pratchett, Moving Pictures)
+"I'm a cat person, myself," she said, vaguely. A low-level voice said: "Yeah? Yeah? Wash in your own spit, do you?" -- It's a dog's life (Terry Pratchett, Moving Pictures)
+"Why's it called Ming?" said the Archchancellor, on cue. The Bursar tapped the pot. It went *ming*. -- Discworld archeology revealed (Terry Pratchett, Moving Pictures)
+- "I thought swords had to be straight." - "Perhaps they start out straight and go bendy with use. A lot of things do." -- Odour-eaters, right? Yeah, she means odour-eaters. (Terry Pratchett, Moving Pictures)
+Azhural raised his staff. "It's fifteen hundred miles to Ankh-Morpork," he said. "We've got three hundred and sixty-three elephants, fifty carts of forage, the monsoon's about to break and we're wearing... we're wearing... sort of things, like glass, only dark... dark glass things on our eyes..." -- (Terry Pratchett, Moving Pictures)
+People who used magic without knowing what they were doing usually came to a sticky end. All over the entire room, sometimes. -- (Terry Pratchett, Moving Pictures)
+"He's in love," said Gaspode. "It's very tricky." "Yeah, I know how it is," said the cat sympathetically. "People throwing old boots and things at you." -- (Terry Pratchett, Moving Pictures)
+"In a word -- im-possible!" "That's two words," said Dibbler. -- (Terry Pratchett, Moving Pictures)
+"I'm vice-president of Throwing Out People Mr Dibbler Doesn't like the Face of." -- (Terry Pratchett, Moving Pictures)
+- "It looks worse than you can imagine!" - "I can imagine some pretty bad things!" - "That's why I said *worse*!" -- (Terry Pratchett, Moving Pictures)
+"Woof?" -- (Terry Pratchett, Moving Pictures)
+"Could have bin worse, mister. I could have said 'miaow'." -- Our hero meets Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"Worlds only harmonica-playing dog. Tuppence." -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"Seems I can't get me 'ead down these days without rescuin' people or foilin' robbers or sunnink." -- It's a wonder dog's life (Terry Pratchett, Moving Pictures)
+"How's he in the mysterious senses department?" -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"Walk a mile on these paws and call me a liar." -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"Woof. In tones of low menace." -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"There's nothin' wrong with bein' a son of a bitch." -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"I thought it was going to be bucket-of-water time myself." -- Gaspode's way of saying "I'm sorry, was I intruding?" (Terry Pratchett, Moving Pictures)
+"I can explain it in Dog, but you only listen in Human." -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"I mean are we talking Thicko City here or what?" -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"I wouldn't give it to a dog, and I *am* one." -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"Does that look like ten per cent to you, Victor?" -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"... Percy the Pup here with a cold nose, bright eyes, glossy coat and the brains of a stunned herring." -- Gaspode the wonder dog on 'Laddie' (Terry Pratchett, Moving Pictures)
+"Maybe you should loosen her clothing or something." -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"I could send you a bone with a file in it, only you'd eat it." -- (Terry Pratchett, Moving Pictures)
+"Well, 'scuse me. I was jus' tryin' to save the world." -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"If gharstely creatures from before the Dawna Time starts wavin' at you from under your bed, jus' you don't come complainin' to me," -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"Idiot I may be, but tied up I ain't." -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"And Howondaland Smith, Balrog Hunter, practic'ly eats the dark for his tea." -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"Messin' around with girls in thrall to Creatures from the Void never works out, take my word for it." -- Gaspode gives Victor some practical advice (Terry Pratchett, Moving Pictures)
+"Growl, growl." -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"I can bite your leg if you like." -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"I expect I've saved the day, right?" -- Gaspode the wonder dog (Terry Pratchett, Moving Pictures)
+"Can't sing. Can't dance. Can handle a sword a little." -- Victor's resume (Terry Pratchett, Moving Pictures)
+"Did I hear things, or can that little dog speak?" said Dibbler. "He says he can't," said Victor. Dibbler hesitated. "Well," he said, "I suppose he should know." -- Dibbler meets Gaspode the Wonder Dog (Terry Pratchett, Moving Pictures)
+In retrospect, Victor was always a little unclear about those next few minutes. That's the way it goes. The moments that change your life are the ones that happen suddenly, like the one where you die. -- (Terry Pratchett, Moving Pictures)
+"When Mister Safety Catch Is Not On, Mister Crossbow Is Not Your Friend." -- Detritus learns about weapons safety (Terry Pratchett, Night Watch)
+He hated being thought of as one of those people that wore stupid ornamental armour. It was gilt by association. -- (Terry Pratchett, Night Watch)
+"Don't put your trust in revolutions. They always come around again. That's why they're called revolutions. People die, and nothing changes." -- (Terry Pratchett, Night Watch)
+The Assassin moved quietly from roof to roof until he was well away from the excitement around the Watch House.
+His movements could be called cat-like, except that he did not stop to spray urine up against things. -- (Terry Pratchett, Night Watch)
+No-one could sit in that chair. It was full of old T-shirts and books and supper plates and junk. There was a deep sock layer and possibly the Lost Strawberry Yoghurt. No-one could sit down there without special equipment. -- (Terry Pratchett, Only You Can Save Mankind)
+One day, if he could master GCSE maths and reliably pick up a soldering iron by the end that wasn't hot, he was going to be a Big Man in computers. -- (Terry Pratchett, Only You Can Save Mankind)
+"I saw a film where there was an alien crawling around inside a spaceship's air ducts and it could come out wherever it liked," said Johnny reproachfully. "Doubtless it had a map," said the Captain. -- (Terry Pratchett, Only You Can Save Mankind)
+Basically, there were two sides to the world. There was the entire computer games software industry engaged in a tremendous effort to stamp out piracy, and there was Wobbler. Currently, Wobbler was in front. -- (Terry Pratchett, Only You Can Save Mankind)
+Wobbler thought that California was where good people went when they died. -- (Terry Pratchett, Only You Can Save Mankind)
+If Not You, Who Else? -- (Terry Pratchett, Only You Can Save Mankind)
+"We got a talk about it at school. There's lots of stuff most girls can't do, but you've got to pretend they can, so that more of them will." -- Sexism explained (Terry Pratchett, Only You Can Save Mankind)
+Bigmac's brother was reliably believed to be in the job of moving video recorders around in an informal way. -- (Terry Pratchett, Only You Can Save Mankind)
+He microwaved himself something called a Pour-On Genuine Creole Lasagne, which said it served four portions. It did if you were dwarfs. -- (Terry Pratchett, Only You Can Save Mankind)
+On Earth, No-one Can Hear You Say "Um". -- (Terry Pratchett, Only You Can Save Mankind)
+"Stuck? You're an *alien*," said Johnny. "Aliens don't get stuck in air ducts. It's practically a well-known fact." -- (Terry Pratchett, Only You Can Save Mankind)
+"If we find a cat I'm going to kick it!" -- (Terry Pratchett, Only You Can Save Mankind)
+What our ancestors would really be thinking, if they were alive today, is: "Why is it so dark in here?" -- (Terry Pratchett, Pyramids)
+All assassins had a full-length mirror in their rooms, because it would be a terrible insult to anyone to kill them when you were badly dressed. -- (Terry Pratchett, Pyramids)
+"Ibid you already know." -- More Discworld philosophers (Terry Pratchett, Pyramids)
+The Ephebians made wine out of anything they could put in a bucket, and ate anything that couldn't climb out of one. -- (Terry Pratchett, Pyramids)
+Nature abhors dimensional abnormalities, and seals them neatly away so that they don't upset people. Nature, in fact, abhors a lot of things, including vacuums, ships called the "Marie Celeste", and the chuck keys for electric drills. -- (Terry Pratchett, Pyramids)
+There was not a lot that could be done to make Morpork a worse place. A direct hit by a meteorite, for example, would count as gentrification. -- (Terry Pratchett, Pyramids)
+- "Therefore I will have dinner sent in," said the priest. "It will be roast chicken." - "I hate chicken." Dios smiled. "No sire. On wednesdays the King always enjoys chicken, sire." -- (Terry Pratchett, Pyramids)
+"What is this thing, anyway?" said the Dean, inspecting the implement in his hands. "It's called a shovel," said the Senior Wrangler. "I've seen the gardeners use them. You stick the sharp end in the ground. Then it gets a bit technical." -- (Terry Pratchett, Reaper Man)
+No one was avoiding him, it was just that an apparent random Brownian motion was gently moving everyone away. -- (Terry Pratchett, Reaper Man)
+"Dock-a-loodle-fod!" -- Dyslexic roosters are a sad sight (Terry Pratchett, Reaper Man)
+People have believed for hundreds of years that newts in a well mean that the water's fresh and drinkable, and *in all that time* never asked themselves whether the newts got out to go to the lavatory. -- (Terry Pratchett, Reaper Man)
+He'd never realized that, deep down inside, what he really wanted to do was make things go splat. -- (Terry Pratchett, Reaper Man)
+DROP THE SCYTHE, AND TURN AROUND SLOWLY. -- Dirty Death (Terry Pratchett, Reaper Man)
+Five exclamation marks, the sure sign of an insane mind. -- (Terry Pratchett, Reaper Man)
+It is traditional, when loading wire trolleys, to put the most fragile items at the bottom. -- (Terry Pratchett, Reaper Man)
+No matter how fast light travels it finds the darkness has always got there first, and is waiting for it. -- (Terry Pratchett, Reaper Man)
+I EXPECT, he said, THAT YOU COULD MURDER A PIECE OF CHEESE? -- Death talks to the Death of Rats (Terry Pratchett, Reaper Man)
+On the fabled hidden continent of Xxxx, somewhere near the rim, there is a lost colony of wizards who wear corks around their pointy hats and live on nothing but prawns. -- (Terry Pratchett, Reaper Man)
+"Bonsai!" -- (Terry Pratchett, Reaper Man)
+"Chap with a whip got as far as the big sharp spikes last week," said the low priest. -- Life in the Temple of Offler (Terry Pratchett, Reaper Man)
+"You know," said Windle, "it's a wonderful afterlife." -- (Terry Pratchett, Reaper Man)
+WHAT CAN THE HARVEST HOPE FOR, IF NOT FOR THE CARE OF THE REAPER MAN? -- Death appeals to Azrael (Terry Pratchett, Reaper Man)
+- "Have you any last words?" - YES. I DON'T WANT TO GO. - "Well. Succinct, anyway." -- Death at the other end of the scythe, for once (Terry Pratchett, Reaper Man)
+Mrs Evadne Cake was a medium, verging on small. -- (Terry Pratchett, Reaper Man)
+"Chain letters," said the Tyrant. "The Chain Letter to the Ephebians. Forget Your Gods. Be Subjugated. Learn to Fear. Do not break the chain -- the last people who did woke up one morning to find fifty thousand armed men on their lawn." -- (Terry Pratchett, Small Gods)
+"It's a god-eat-god world." -- (Terry Pratchett, Small Gods)
+"You can't trample infidels when you're a tortoise. I mean, all you could do is give them a meaningful look." -- (Terry Pratchett, Small Gods)
+"That's right," he said. "We're philosophers. We think, therefore we am." -- (Terry Pratchett, Small Gods)
+His philosophy was a mixture of three famous schools -- the Cynics, the Stoics and the Epicureans -- and summed up all three of them in his famous phrase, "You can't trust any bugger further than you can throw him, and there's nothing you can do about it, so let's have a drink." -- We meet Dydactylos the philosopher (Terry Pratchett, Small Gods)
+Dhblah sidled closer. This was not hard. Dhblah sidled everywhere. *Crabs* thought he walked sideways. -- (Terry Pratchett, Small Gods)
+One day a tortoise will learn how to fly. -- (Terry Pratchett, Small Gods)
+History, contrary to popular theories, *is* kings and dates and battles. -- (Terry Pratchett, Small Gods)
+And it came to pass that in time the Great God Om spake unto Brutha, the Chosen One: "Psst!" -- (Terry Pratchett, Small Gods)
+Brother Preptil, the master of the music, had described Brutha's voice as putting him in mind of a disappointed vulture arriving too late at the dead donkey. -- (Terry Pratchett, Small Gods)
+"There's very good eating on one of these, you know." -- Eyeing the tortoise for tea (Terry Pratchett, Small Gods)
+"Pets are always a great help in times of stress. And in times of starvation too, o'course." -- (Terry Pratchett, Small Gods)
+Words are the litmus paper of the minds. If you find yourself in the power of someone who will use the word "commence" in cold blood, go somewhere else very quickly. But if they say "Enter", don't stop to pack. -- (Terry Pratchett, Small Gods)
+The labyrinth of Ephebe is ancient and full of one hundred and one amazing things you can do with hidden springs, razor-sharp knives, and falling rocks. -- (Terry Pratchett, Small Gods)
+"Ah. Philosophy," said Om. -- (Terry Pratchett, Small Gods)
+"Not a man to mince words. People, yes. But not words." -- (Terry Pratchett, Small Gods)
+SQUEAK. -- The Death of Rats (Terry Pratchett, Small Gods)
+Bishops move diagonally. That's why they often turn up where the kings don't expect them to be. -- (Terry Pratchett, Small Gods)
+"Eureka," he said. "Going to have a bath then?" -- Philosophy in action (Terry Pratchett, Small Gods)
+Cuius testiculos habes, habeas cardia et cerebellum. -- (Terry Pratchett, Small Gods)
+"Are you a philosopher? Where's your sponge?" -- (Terry Pratchett, Small Gods)
+REMIND ME AGAIN, he said, HOW THE LITTLE HORSE-SHAPED ONES MOVE. -- Death on symbolic last games (Terry Pratchett, Small Gods)
+"Go on, do Deformed Rabbit... it's my favourite." -- Shadow puppets are so cute (Terry Pratchett, Small Gods)
+"Oh, a very useful philosophical animal, your average tortoise. Outrunning metaphorical arrows, beating hares in races... very handy." -- (Terry Pratchett, Small Gods)
+Gravity is a habit that is hard to shake off. -- (Terry Pratchett, Small Gods)
+The trouble with being a god is that you've got no one to pray to. -- (Terry Pratchett, Small Gods)
+There are hardly any excesses of the most crazed psychopath that cannot easily be duplicated by a normal kindly family man who just comes in to work every day and has a job to do. -- (Terry Pratchett, Small Gods)
+The people who really run organizations are usually found several levels down, where it is still possible to get things done. -- (Terry Pratchett, Small Gods)
+Guilt was the grease in which the wheels of the authority turned. -- (Terry Pratchett, Small Gods)
+Most gods find it hard to walk and think at the same time. -- (Terry Pratchett, Small Gods)
+When the least they could do to you was everything, then the most they could do to you suddenly held no terror. -- (Terry Pratchett, Small Gods)
+"What's a philosopher ?" said Brutha. "Someone who's bright enough to find a job with no heavy lifting," said a voice in his head. -- (Terry Pratchett, Small Gods)
+"Slave is an Ephebian word. In Om we have no word for slave," said Vorbis. "So I understand," said the Tyrant. "I imagine that fish have no word for water." -- (Terry Pratchett, Small Gods)
+"He says gods like to see an atheist around. Gives them something to aim at." -- (Terry Pratchett, Small Gods)
+"You're not one of us." "I don't think I'm one of them, either," said Brutha. "I'm one of mine." -- (Terry Pratchett, Small Gods)
+Simony's eyes gleamed with the gleam of a man who had seen the future and found it covered with armour plating. -- (Terry Pratchett, Small Gods)
+"All holy piety in public, and all peeled grapes and self-indulgence in private." -- (Terry Pratchett, Small Gods)
+When you can flatten entire cities at a whim, a tendency towards quiet reflection and seeing-things-from-the-other-fellow's-point- of-view is seldom necessary. -- (Terry Pratchett, Small Gods)
+"Take it from me, whenever you see a bunch of buggers puttering around talking about truth and beauty and the best way of attacking Ethics, you can bet your sandals it's all because dozens of other poor buggers are doing all the real work around the place." -- (Terry Pratchett, Small Gods)
+"Why do you bother with him? He's had thousands of people killed!" "Yes, but perhaps he thought that you wanted it." -- (Terry Pratchett, Small Gods)
+The figures looked more or less human. And they were engaged in religion. You could tell by the knives (it's not murder if you do it for a god). -- (Terry Pratchett, Small Gods)
+The trouble was that he was talking in philosophy, but they were listening in gibberish. -- (Terry Pratchett, Small Gods)
+"He's muffed it," said Simony. "he could have done *anything* with them. And he just told them the facts. You can't inspire people with facts. They need a cause. They need a symbol." -- (Terry Pratchett, Small Gods)
+"You can't find a hermit to teach you herming, because of course that rather spoils the whole thing." -- (Terry Pratchett, Small Gods)
+Om began to feel the acute depression that steals over every realist in the presence of an optimist. -- (Terry Pratchett, Small Gods)
+"All the other prophets came back with commandments!" "Where they get them?" "I ... suppose they made them up." "You get them from the same place." -- (Terry Pratchett, Small Gods)
+Brutha tried to nod, and thought: I'm on everyone's side. It'd be nice if, just for once, someone was on mine. -- (Terry Pratchett, Small Gods)
+Probably the last man who knew how it worked had been tortured to death years before. Or as soon as it was installed. Killing the creator was a traditional method of patent protection. -- (Terry Pratchett, Small Gods)
+Give anyone a lever long enough and they can change the world. It's unreliable levers that are the problem. -- (Terry Pratchett, Small Gods)
+"Now we've got a truth to die for!" "No. Men should die for lies. But the truth is too precious to die for." -- (Terry Pratchett, Small Gods)
+YOU HAVE PERHAPS HEARD THE PHRASE THAT HELL IS OTHER PEOPLE? "Yes. Yes, of course." Death nodded. IN TIME, he said, YOU WILL LEARN THAT IT IS WRONG. -- (Terry Pratchett, Small Gods)
+"I used to think that *I* was stupid, and then I met philosophers." -- (Terry Pratchett, Small Gods)
+"I like the idea of democracy. You have to have someone everyone distrusts," said Brutha. "That way, everyone's happy." -- (Terry Pratchett, Small Gods)
+Until an unfortunate axe incident, Gloria had been captain of the school basketball team. -- (Terry Pratchett, Soul Music)
+The man gave a shrug which indicated that, although the world did indeed have many problems, this was one of them that was not his. -- (Terry Pratchett, Soul Music)
+"Scum," said Crash, his voice low with resigned menace, "you've bought a leopard, haven't you?" -- (Terry Pratchett, Soul Music)
+"Of course, just because we've heard a spine-chilling, blood-curdling scream of the sort to make your very marrow freeze in your bones doesn't automatically mean there's anything wrong." -- (Terry Pratchett, Soul Music)
+It is said that whosoever the gods wish to destroy, they first make mad. In fact, whosoever the gods wish to destroy, they first hand the equivalent of a stick with a fizzing fuse and Acme Dynamite Company written on the side. It's more interesting, and doesn't take so long. -- (Terry Pratchett, Soul Music)
+The question seldom addressed is *where* Medusa had snakes. Underarm hair is an even more embarassing problem when it keeps biting the top of the deodorant bottle. -- (Terry Pratchett, Soul Music)
+People came to Ankh-Morpork to seek their fortune. Unfortunately, other people sought it too. -- (Terry Pratchett, Soul Music)
+The class was learning about some revolt in which some peasants had wanted to stop being peasants and, since the nobles had won, had stopped being peasants *really quickly*. -- (Terry Pratchett, Soul Music)
+The hippo of recollection stirred in the muddy waters of the mind. -- (Terry Pratchett, Soul Music)
+SNH, SNH, SNH. -- (Terry Pratchett, Soul Music)
+Smoke was coming out of the stricken piano. The Librarian's hands were walking through the keys like Casanunda in a nunnery. -- (Terry Pratchett, Soul Music)
+They looked at one another in incomprehension, two minds driving opposite ways up a narrow street and waiting for the other man to reverse first. -- (Terry Pratchett, Soul Music)
+The students were staring at her in the manner of those who have heard of the species 'female' but have never expected to get this close to one. -- (Terry Pratchett, Soul Music)
+"I'm mean and turf and I'm mean and turf and I'm mean and turf and I'm mean and turf, And me an' my friends can walk towards you with our hats on backwards in a menacing way, Yo!" -- (Terry Pratchett, Soul Music)
+The Patrician was a pragmatist. He never tried to fix things that worked. Things that didn't work, however, got broken. -- (Terry Pratchett, Soul Music)
+"What duck?" -- (Terry Pratchett, Soul Music)
+There was a roar like the scream of a camel who has just seen two bricks. -- (Terry Pratchett, Soul Music)
+"Yes," said the skull. "Quit while you're a head, that's what I say." -- (Terry Pratchett, Soul Music)
+He did of course sometimes have people horribly tortured to death, but this was considered to be perfectly acceptable behaviour for a civic ruler and generally approved of by the overhelming majority of citizens. [footnote: The overhelming majority of citizens being defined in this case as everyone not currently hanging upside down over a scorpion pit] -- (Terry Pratchett, Sourcery)
+There were a few seconds of total silence as everyone waited to see what would happen next. And then Nijel uttered the battle cry that Rincewind would never quite forget to the end of his life. "Erm," he said, "excuse me..." -- (Terry Pratchett, Sourcery)
+Of course, Ankh-Morpork's citizens had always claimed that the river water was incredibly pure. Any water that had passed through so many kidneys, they reasoned, had to be very pure indeed. -- (Terry Pratchett, Sourcery)
+The subject of wizards and sex is a complicated one, but as has already been indicated it does, in essence, boil down to this: when it comes to wine, women and song, wizards are allowed to get drunk and croon as much as they like. -- (Terry Pratchett, Sourcery)
+The vermine is a small black and white relative of the lemming, found in the cold Hublandish regions. Its skin is rare and highly valued, especially by the vermine itself; the selfish little bastard will do anything rather than let go of it. -- Discworld wildlife (Terry Pratchett, Sourcery)
+"It's going to look pretty good, then, isn't it," said War testily, "the One Horseman and Three Pedestrians of the Apocralypse." -- The Four Horsemen of the Apocralypse encounter unexpected difficulties (Terry Pratchett, Sourcery)
+It wasn't blood in general he couldn't stand the sight of, it was just his blood in particular that was so upsetting. -- (Terry Pratchett, Sourcery)
+"I'm not going to ride on a magic carpet!" he hissed. "I'm afraid of grounds." "You mean heights," said Conina. "And stop being silly." "I know what I mean! It's the grounds that kill you!" -- (Terry Pratchett, Sourcery)
+It became apparent that one reason why the Ice Giants were known as the Ice Giants was because they were, well, giants. The other was that they were made of ice. -- (Terry Pratchett, Sourcery)
+"I meant," said Iplsore bitterly, "what is there in this world that makes living worthwhile?" Death thought about it. "CATS," he said eventually, "CATS ARE NICE." -- Death is obviously not a dog person (Terry Pratchett, Sourcery)
+Death was Nature's way of telling you to slow down. -- (Terry Pratchett, Strata)
+The sky spun again as Marco turned the ship so that 'down' was where long tradition had always put it, in the region of the feet. -- (Terry Pratchett, Strata)
+He didn't look mad, but they never did. -- (Terry Pratchett, Strata)
+"You can think and you can fight, but the world's always movin', and if you wanna stay ahead you gotta dance." -- (Terry Pratchett, The Amazing Maurice and his Educated Rodents)
+The important thing about adventures, thought Mr Bunnsy, was that they shouldn't be so long as to make you miss mealtimes. -- (Terry Pratchett, The Amazing Maurice and his Educated Rodents)
+Humans, eh? Think they're lords of creation. Not like us cats. We *know* we are. Ever see a cat feed a human? Case proven. -- (Terry Pratchett, The Amazing Maurice and his Educated Rodents)
+A good plan isn't one where someone wins, it's where nobody thinks they've *lost*. -- (Terry Pratchett, The Amazing Maurice and his Educated Rodents)
+"You can think and you can fight, but the world's always movin', and if you wanna stay ahead you gotta dance." -- (Terry Pratchett, The Amazing Maurice and his Educated Rodents)
+On the fifth day the Governor of the town called all the tribal chieftains to an audience in the market square, to hear their grievances. He didn't always do anything about them, but at least they got *heard*, and he nodded a lot, and everyone felt better about it at least until they got home. This is politics. -- Carpet politics are very similar to Discworld politics (Terry Pratchett, The Carpet People)
+"He's a man of few words, and he doesn't know what either of them mean," people said, but not when he was within hearing. -- (Terry Pratchett, The Carpet People)
+Tourist, Rincewind decided, meant "idiot". -- (Terry Pratchett, The Colour of Magic)
+"Reflected-sound-of-underground-spirits?" -- Economics explained (Terry Pratchett, The Colour of Magic)
+"Let's just say that if complete and utter chaos was lightning, he'd be the sort to stand on a hilltop in a thunderstorm wearing wet copper armour and shouting 'All gods are bastards'." -- Rincewind discussing Twoflower (Terry Pratchett, The Colour of Magic)
+"I perceive a possibility of an immediate chronological sequence of events which includes a violence," said Three. He stepped back, "I express preference for a chronological sequence of events which precludes a violence." -- (Terry Pratchett, The Dark Side of the Sun)
+The one positive thing you could say about the bread products around him was that they were probably as edible now as they were on the day they were baked. *Forged* was a better term. Dwarf bread was made as a meal of last resort and also as a weapon and a currency. Dwarfs were not, as far as Vimes knew, religious in any way, but the way they thought about bread came close. -- (Terry Pratchett, The Fifth Elephant)
+You did something because it had always been done, and the explanation was "but we've always done it this way." A million dead people can't have been wrong, can they? -- (Terry Pratchett, The Fifth Elephant)
+There was no such thing as a dwarfish female pronoun or, once the children were on solids, any such thing as women's work. -- (Terry Pratchett, The Fifth Elephant)
+He wasn't strictly aware of it, but he treated even geography as if he was investigating a crime (did you see who carved out the valley? Would you recognize that glacier if you saw it again?) -- (Terry Pratchett, The Fifth Elephant)
+He was aware that a wise man should always respect the folkways of others, to use Carrot's happy phrase, but Vimes often had difficulty with this idea. For one thing, there were people in the world whose folkways consisted of gutting other people like clams and this was not a procedure that commanded, in Vimes, any kind of respect at all. -- (Terry Pratchett, The Fifth Elephant)
+It was funny how people were people everywhere you went, even if the people concerned weren't the people the people who made up the phrase "people are people everywhere" had traditionally thought of as people. And even if you weren't virtuous, as you had been brought up to understand the term, you did like to see virtue in other people, provided it didn?t cost you anything. -- (Terry Pratchett, The Fifth Elephant)
+A marriage is always made up of two people who are prepared to swear that only the other one snores. -- (Terry Pratchett, The Fifth Elephant)
+As castles went, this one looked as though it could be taken by a small squad of not very efficient soldiers. For defence, putting a blanket over your head might be marginally safer. -- (Terry Pratchett, The Fifth Elephant)
+She moved like someone who had grown used to her body and, in general, looked like what Vimes had heard described as "a woman of a certain age." He'd never been quite certain what age that was. -- (Terry Pratchett, The Fifth Elephant)
+"When it's time to stop living, I will certainly make Death my number one choice!" -- (Terry Pratchett, The Last Continent)
+In the fetid fleapit of Rincewind's brain the projectionist of memory put on reel two. Recollection began to flicker. -- (Terry Pratchett, The Last Continent)
+Daggy stepped forward, but only comparatively; in fact, his mates had all, without discussion, taken one step backwards in the choreography of caution. -- (Terry Pratchett, The Last Continent)
+PEOPLE'S WHOLE LIVES *DO* PASS IN FRONT OF THEIR EYES BEFORE THEY DIE. THE PROCESS IS CALLED 'LIVING'. -- (Terry Pratchett, The Last Continent)
+"Nulli Sheilae sanguineae" -- (Terry Pratchett, The Last Continent)
+"All bastards are bastards, but some bastards is *bastards*." -- (Terry Pratchett, The Last Continent)
+They say the heat and the flies here can drive a man insane. But you don't have to believe that, and nor does that bright mauve elephant that just cycled past. -- (Terry Pratchett, The Last Continent)
+Ridcully was to management what King Herod was to the Bethlehem Playgroup Association. His mental approach to it could be visualised as a sort of business flowchart with, at the top, a circle entitled "Me, who does the telling" and, connected below it by a line, a large circle entitled "Everyone else". -- (Terry Pratchett, The Last Continent)
+"When You're Up to Your Ass in Alligators, Today Is the First Day of the Rest of Your Life." -- Management slogan, Ridcully-style (Terry Pratchett, The Last Continent)
+Rincewind had always been happy to think of himself as a racist. The One Hundred Meters, the Mile, the Marathon -- he'd run them all. -- (Terry Pratchett, The Last Continent)
+Too many people, when listing all the perils to be found in the search for lost treasure or ancient wisdom, had forgotten to put at the top of the list 'the man who arrived just before you'. -- (Terry Pratchett, The Last Hero)
+- "DID YOU SAY HUMANS PLAY IT FOR FUN?" - "Some of them get to be very good at it, yes. I'm only an amateur, I'm afraid" - "BUT THEY ONLY LIVE EIGHTY OR NINETY YEARS!" -- The joys of bridge (Terry Pratchett, The Light Fantastic)
+It looked like the sort of book described in library catalogues as "slightly foxed", although it would be more honest to admit that it looked as though it had beed badgered, wolved and possibly beared as well. -- Ah, but has it been hedgehogged? (Terry Pratchett, The Light Fantastic)
+"The knuckles! The horrible knuckles!" -- (Terry Pratchett, The Light Fantastic)
+I WAS AT A PARTY, he added, a shade reproachfully. -- Death is summoned by the Wizards (Terry Pratchett, The Light Fantastic)
+A Thaum is the basic unit of magical strength. It has been universally established as the amount of magic needed to create one small white pigeon or three normal sized billiard balls. -- (Terry Pratchett, The Light Fantastic)
+He moved in a way that suggested he was attempting the world speed record for the nonchalant walk. -- (Terry Pratchett, The Light Fantastic)
+- "What is it that a man may call the greatest things in life?" - "Hot water, good dentishtry and shoft lavatory paper." -- Cohen the Barbarian in conversation with Discworld nomads (Terry Pratchett, The Light Fantastic)
+The old shaman said carefully, "You didn't just see two men go through upside down on a broomstick, shouting and screaming at each other, did you?" The boy looked at him levelly. "Certainly not," he said. The old man heaved a sigh of relief. "Thank goodness for that," he said. "Neither did I." -- Rincewind and Twoflower take up broomstick flying (Terry Pratchett, The Light Fantastic)
+Something small and distant broke through the cloud layer, trailing shreds of vapour. In the stratospheric calm the sounds of bickering came sharp and clear. "You said you could fly one of these things!" "No I didn't; I just said *you* couldn't!" -- Rincewind and Twoflower attempt broomstick flying (Terry Pratchett, The Light Fantastic)
+"Early to rise, early to bed, makes a man healthy, wealthy and dead." -- Apparently Terry nicked this from James Thurber. Still a good quote, though. (Terry Pratchett, The Light Fantastic)
+The druid stiffened. "*Nice?*" he said. "A triumph of the silicon chunk, a miracle of modern masonic technology -- *nice*?" "Oh, yes," said Twoflower, to whom sarcasm was merely a seven letter word beginning with S. -- (Terry Pratchett, The Light Fantastic)
+"Shut up and tell me what that other idiot ish doing!" "No, but look, if I've got to shut up, how can I --" The knife at his throat became a hot streak of pain and Rincewind decided to give logic a miss. -- Cohen the Barbarian interrogates Rincewind (Terry Pratchett, The Light Fantastic)
+"Students made it long ago," said Rincewind. "Handy way in and out after lights out." "Ah," said Twoflower, "I *understand*. Over the wall and out to brightly-lit tavernas to drink and sing and recite poetry, yes?" "Nearly right except for the singings and the poetry, yes," said Rincewind. -- (Terry Pratchett, The Light Fantastic)
+- "Pull me up, then," he hinted. - "I think that might be sort of difficult," grunted Twoflower. "I don't actually think I can do it, in fact." - "What are you holding on to, then?" - "You." - "I mean besides me." - "What do you mean, besides you?" said Twoflower. -- (Terry Pratchett, The Light Fantastic)
+There were no flies on C.M.O.T Dibbler. He would have charged them rent. -- (Terry Pratchett, The Truth)
+The world is made up of four elements: Earth, Air, Fire and Water. This is a fact well known even to Corporal Nobbs. It's also wrong. There's a fifth element, and generally it's called Surprise. -- (Terry Pratchett, The Truth)
+There are, it has been said, two types of people in the world. There are those who, when presented with a glass that is exactly half full, say: this glass is half full. And then there are those who say: this glass is half empty.
+The world *belongs*, however, to those who can look at the glass and say: What's up with this glass? Excuse me? Excuse *me*? This is my glass? I don't *think* so. My glass was full! *And* it was a bigger glass! -- (Terry Pratchett, The Truth)
+It was a puzzle why things were always dragged kicking and screaming. No one ever seemed to want to, for example, lead them gently by the hand. -- (Terry Pratchett, The Truth)
+"The Truth Shall Make Ye Fret" -- (Terry Pratchett, The Truth)
+A lie can run round the world before the truth has got its boots on. -- (Terry Pratchett, The Truth)
+"He's going to go totally Librarian-poo." -- Gaspode about Vimes (Terry Pratchett, The Truth)
+"But surely dogs can't talk--" Sacharissa began. "Oh dear oh dear oh dear," said Gaspode, "Did I *say* I was talking?" "Well, not in so many words--" -- Sacharissa meets Gaspode the Wonder Dog (Terry Pratchett, The Truth)
+WHO KNOWS WHAT EVIL LURKS IN THE HEART OF MEN? The Death of Rats looked up from the feast of potato. SQUEAK, he said. Death waved a hand dismissively. WELL, YES, OBVIOUSLY *ME*, he said. I JUST WONDERED IF THERE WAS ANYONE ELSE. -- (Terry Pratchett, The Truth)
+"Some people are heroes. And some people jot down notes." -- (Terry Pratchett, The Truth)
+Our garden was debated territory between five local cats, and we'd heard that the best way to keep other cats out of the garden was to have one yourself. A moment's rational thought here will spot the slight flaw in this reasoning. -- (Terry Pratchett, The Unadulterated Cat)
+Boot-faced cats aren't born but made, often because they've tried to outstare or occasionally rape a speeding car and have been repaired by a vet who just pulled all the bits together and stuck the stitches in where there was room. -- (Terry Pratchett, The Unadulterated Cat)
+Cats don't hunt seals. They would if they knew what they were and where to find them. But they don't, so that's all right. -- (Terry Pratchett, The Unadulterated Cat)
+It's an interesting fact that fewer than 17
+of Real cats end their lives with the same name they started with. Much family effort goes into selecting one at the start ("She looks like a Winnifred to *me*"), and the as the years roll by it suddenly finds itself being called Meepo or Ratbag. -- (Terry Pratchett, The Unadulterated Cat)
+Next comes the realist phase ("After all, from a purely geometrical point of view a cat is only a tube with a door at the top."). -- Getting Real cats to take medication can be a problem (Terry Pratchett, The Unadulterated Cat)
+Everyone's heard of Erwin Schrodinger's famous thought experiment. You put a cat in a box with a bottle of poison, which many people would suggest is about as far as you need to go. -- (Terry Pratchett, The Unadulterated Cat)
+Consider the situation. There you are, forehead like a set of balconies, worrying about the long-term effects of all this new 'fire' stuff on the environment, you're being chased and eaten by most of the planet's large animals, and suddenly tiny versions of one of the worst of them wanders into the cave and starts to purr. -- Why humans like cats (Terry Pratchett, The Unadulterated Cat)
+"Zoology, eh? That's a big word, isn't it?" "No, actually it isn't," said Tiffany. "Patronizing is a big word. Zoology is really quite short." -- (Terry Pratchett, The Wee Free Men)
+"Nac Mac Feegle! The Wee Free Men! Nae king! Nae quin! Nae laird! *We willna be fooled again!*" -- (Terry Pratchett, The Wee Free Men)
+"Do not act incautiously when confronting little bald wrinkly smiling men!" -- Rule One (Terry Pratchett, Thief of Time)
+"Sometimes I really think people ought to have to pass a *proper* exam before they're allowed to be parents. Not just the practical, I mean." -- (Terry Pratchett, Thief of Time)
+"No running with scythes!" -- (Terry Pratchett, Thief of Time)
+Look, that's why there's rules, understand? So that you *think* before you break 'em. -- (Terry Pratchett, Thief of Time)
+"Bikkit!" -- (Terry Pratchett, Thief of Time)
+Even with nougat you can have a perfect moment. -- (Terry Pratchett, Thief of Time)
+- "Outside! What's it like?" - "Well -- It's sort of big" -- (Terry Pratchett, Truckers)
+The Store was having its last sale. It was holding a Grand Final Clearance of specially selected sparks, and flames to suit every pocket. -- (Terry Pratchett, Truckers)
+It's a small step for a man, but a giant leap for nomekind. -- (Terry Pratchett, Truckers)
+Granny's remedies, made from simple, honest, and generally nearly poisonous herbs and roots, were amazing things. After one dose of stomache-ache jollop, you made sure you never complained of stomach ache ever again. In its way, it was a sort of cure. -- No, not that Granny. The other one. (Terry Pratchett, Truckers)
+"No, no, no, what you do is, you get a gnu, then you point it at the driver and someone says, 'Look out, he's got a gnu!' and you say, 'Take us where we want to go or I'll fire this gnu at you!'" -- "Host Age at 10,000 Feet", Nome style (Terry Pratchett, Truckers)
+There was a polite beeping from the Thing. "You may be interested to know," it said, "that we've broken the sound barrier." Masklin turned wearily to the others. "All right, own up. Who broke it?" -- (Terry Pratchett, Wings)
+Beyond the top of the sky was the place the Thing had called the universe. It contained -- according to the Thing -- everything and nothing. And there was very little everything and more nothing than anyone could imagine. -- (Terry Pratchett, Wings)
+- "The other humans around it are trying to explain to it what a planet is." - "Doesn't it know?" - "Many humans don't. Mistervicepresident is one of them." -- (Terry Pratchett, Wings)
+AIRPORTS: A place where people hurry up and wait. -- From A Scientific Encyclopedia for the Enquiring Young Nome by Angalo de Haberdasheri (Terry Pratchett, Wings)
+"I thought jet planes were just trucks with more wings and less wheels." -- (Terry Pratchett, Wings)
+Nomes live ten times faster than humans. They're harder to see than a high- speed mouse. That's one reason why most humans hardly ever see them. The other is that humans are very good at not seeing things they know aren't there. And, since sensible humans know that there are no such things as people four inches high, a nome who doesn't want to be seen probably won't be seen. -- (Terry Pratchett, Wings)
+And then there were the frogs. Very, very small frogs. They had such a tiny life cycle it still had trainer wheels on it. -- (Terry Pratchett, Wings)
+And this had been the way things were for as far back as the frogs could remember [footnote: About three seconds. Frogs don't have good memories]. -- (Terry Pratchett, Wings)
+"You get more air close to the ground," said Angalo. "I read that in a book. You get lots of air low down, and not much when you go up." "Why not?" said Gurder. "Dunno. It's frightened of heights, I guess." -- The nomes discuss science (Terry Pratchett, Wings)
+CONCORDE: It goes twice as fast as a bullet and you get smoked salmon. -- From A Scientific Encyclopedia for the Enquiring Young Nome by Angalo de Haberdasheri (Terry Pratchett, Wings)
+It had been in a pocket diary, and the names of the faraway places written on it were like magic -- Africa, Australia, China, Equator, Printed in Hong Kong, Iceland... -- (Terry Pratchett, Wings)
+They stared at the branch. There wasn't just one flower out there, there were dozens, although the frogs weren't able to think like this because frogs can't count beyond one. They saw lots of ones. -- (Terry Pratchett, Wings)
+They stared at them. Staring is one of the few things frogs are good at. Thinking isn't. -- (Terry Pratchett, Wings)
+It would be nice to say that the tiny frogs thought long and hard about the new flower, about life in the old flower, about the need to explore, about the possibility that the world was bigger than a pool with petals around the edge. In fact, what they thought was: "._._.mipmip._._.mipmip._._.mipmip". -- (Terry Pratchett, Wings)
+HOTELS: A place where TRAVELLING HUMANS are parked at night. Other humans bring them food, including the famous BACON, LETTUCE AND TOMATO SANDWICH. There are beds and towels and special things that rain on people to get them clean. -- From A Scientific Encyclopedia for the Enquiring Young Nome by Angalo de Haberdasheri (Terry Pratchett, Wings)
+- "Very clever idea, though." - "What is?" - "Asking the questions when people arrive. If anyone was coming here to do some subversive overthrowing, everyone'd be down on him like a pound of bricks as soon as he answered 'Yes'." - "It's a sneaky trick, isn't it," said Angalo, in an admiring tone of voice. -- The nomes encounter American customs regulations (Terry Pratchett, Wings)
+- "What's the human singing about, Thing?" said Masklin. - "It is a little difficult to follow. However, it appears that the singer wishes it to be known that he did something his way." - "Did what?" - "Insufficient data at this point. But whatever it was, he did it at a) each step along life's highway and b) not in a shy way..." -- (Terry Pratchett, Wings)
+SATELLITES: They are in SPACE and stay there by going so fast that they are never in one place enough to fall down. TELEVISIONS are bounced off them. They are part of SCIENCE. -- From A Scientific Encyclopedia for the Enquiring Young Nome by Angalo de Haberdasheri (Terry Pratchett, Wings)
+SCIENCE: A way of finding things out and then making them work. Science explains what is happening around us the whole time. So does RELIGION, but science is better because it comes up with more understandable excuses when it is wrong. There is a lot more Science than you think. -- From A Scientific Encyclopedia for the Enquiring Young Nome by Angalo de Haberdasheri (Terry Pratchett, Wings)
+"All right," said Masklin. "But you're not to fly down low again and try to read the signposts. Every time you do that, humans rush into the streets and we get lots of shouting on the radio." "That's right." said the Thing. "People are bound to get excited when they see a ten-million-ton starship trying to fly down the street." -- (Terry Pratchett, Wings)
+The Yen Buddhists are the richest religious sect in the universe. They hold that the accumulation of money is a great evil and a burden to the soul. They therefore, regardless of personal hazard, see it as their unpleasant duty to acquire as much as possible in order to reduce the risk to innocent people. -- (Terry Pratchett, Witches Abroad)
+Asking someone to repeat a phrase you'd not only heard very clearly but were also exceedingly angry about was around Defcon II in the lexicon of squabble. -- (Terry Pratchett, Witches Abroad)
+- "We've got a lot of experience of not having any experience" - "But the point is... the point is... the point is we've not been experienced for a lot longer than you." -- Stop being so negative (Terry Pratchett, Witches Abroad)
+The only way housework could be done in this place was with a shovel or, for preference, a match. -- (Terry Pratchett, Witches Abroad)
+People didn't hit you over the head with farmhouses back home. -- Nanny Ogg gets homesick (Terry Pratchett, Witches Abroad)
+Racism was not a problem on the Discworld, because -- what with trolls and dwarfs and so on -- speciesism was more interesting. Black and white lived in perfect harmony and ganged up on green. -- (Terry Pratchett, Witches Abroad)
+Nanny Ogg quite liked cooking, provided there were other people around to do things like chop up the vegetables and wash the dishes afterwards. -- Home Pragmatics (Terry Pratchett, Witches Abroad)
+"Emberella," thought Magrat. "I'm fairy godmothering a girl who sounds like something you put up in the rain." -- (Terry Pratchett, Witches Abroad)
+Magrat was annoyed. She was also frightened, which made her even more annoyed. It was hard for people when Magrat was annoyed. It was like being attacked by damp tissue. -- (Terry Pratchett, Witches Abroad)
+Nanny Ogg looked him up and down or, at least, down and further down. "You're a dwarf," she said. -- Nanny Ogg meets Casanunda (Terry Pratchett, Witches Abroad)
+- "'S called the Vieux River." - "Yes?" - "Know what that means?" - "No." - "The Old (Masculine) River," said Nanny. - "Yes?" - "Words have sex in foreign parts," said Nanny hopefully. -- (Terry Pratchett, Witches Abroad)
+"You can't go around building a better world for people. Only people can build a better world for people. Otherwise it's just a cage." -- (Terry Pratchett, Witches Abroad)
+Greebo's technique was unscientific and wouldn't have stood a chance against any decent swordmanship, but on his side was the fact that it is almost impossible to develop decent swordmanship when you seem to have run into a food mixer that is biting your ear off. -- (Terry Pratchett, Witches Abroad)
+Genua had once controlled the river mouth and taxed its traffic in a way that couldn't be called piracy because it was done by the city government. -- Local-body politics explained (Terry Pratchett, Witches Abroad)
+"Baths is unhygienic," Granny declared. "You know I've never agreed with baths. Sittin' around in your own dirt like that." -- Taking personal hygiene to new limits (Terry Pratchett, Witches Abroad)
+The calender of the Theocracy of Muntab counts down, not up. No-one knows why, but it might not be a good idea to hang around and find out. -- (Terry Pratchett, Wyrd Sisters)
+The duke had a mind that ticked like a clock and, like a clock, it regularly went cuckoo. -- (Terry Pratchett, Wyrd Sisters)
+"There must be a hundred silver dollars in here," moaned Boggis, waving a purse. "I mean, that's not my league. That's not my class. I can't handle that sort of money. You've got to be in the Guild of Lawyers or something to steal that much." -- (Terry Pratchett, Wyrd Sisters)
+"I'd like to know if I could compare you to a summer's day. Because -- well, June 12th was quite nice, and..." -- (Terry Pratchett, Wyrd Sisters)
+"'Tis not right, a woman going into such places by herself." Granny nodded. She thoroughly approved of such sentiments so long as there was, of course, no suggestion that they applied to her. -- (Terry Pratchett, Wyrd Sisters)
+Above the hearth was a huge pokerwork sign saying "Mother". No tyrant in the whole history of the world had ever achieved a domination so complete. -- (Terry Pratchett, Wyrd Sisters)
+"A man could go far, knowing his rights like you do," said Granny. "But right now he should go home." -- (Terry Pratchett, Wyrd Sisters)
+"I daresay," said Granny, pushing the Fool aside and stepping over a writhing taproot. "If anyone locked *me* in a dungeon, there'd be screams." -- (Terry Pratchett, Wyrd Sisters)
+"He didn't take any notice!" whispered Tomjon. "A born critic," said the dwarf. -- Discworld stage actors in conversation (Terry Pratchett, Wyrd Sisters)
+"Actors," said Granny, witheringly. "As if the world weren't full of enough history without inventing more." -- (Terry Pratchett, Wyrd Sisters)
+In fact, no gods anywhere play chess. They prefer simple, vicious games, where you Do Not Achieve Transcendence but Go Straight to Oblivion; a key to the understanding of all religion is that a god's idea of amusement is Snakes and Ladders with greased rungs. -- (Terry Pratchett, Wyrd Sisters)
+"Yes, bugger all that." said Nanny. "Let's curse somebody." -- Even Nanny Ogg gets upset occasionally (Terry Pratchett, Wyrd Sisters)
+On nights such as these the gods, as has already been pointed out, play games other than chess with the fates of mortals and the thrones of kings. It is important to remember that they always cheat, right up to the end... -- (Terry Pratchett, Wyrd Sisters)
diff --git a/doc/ConfigChanges.txt b/doc/ConfigChanges.txt
new file mode 100644
index 0000000..f52e894
--- /dev/null
+++ b/doc/ConfigChanges.txt
@@ -0,0 +1,17 @@
+Any configuration file changes and how to update should be listed here.
+
+Config version 17
+-----------------
+ * Added config_log_colors
+
+Config version 16
+-----------------
+ * Added config_commands_private_always
+ * Renamed config_listenregex to config_commands_listenregex
+ * Renamed config_unknown_commands to config_feedback_unknown_commands.
+
+
+Config version 15
+-----------------
+This file was added at this point. To convert from older versions to this
+one, please read example config and add what is missing.
diff --git a/doc/bot_settings.sh.example.in b/doc/bot_settings.sh.example.in
new file mode 100644
index 0000000..9e8d5a9
--- /dev/null
+++ b/doc/bot_settings.sh.example.in
@@ -0,0 +1,296 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+
+# Variables marked with (*) will take effect at a rehash as well.
+
+# What version this config is at. This is used to check
+# if your config needs updating.
+config_version=17
+
+####################
+# General settings #
+####################
+
+# Nick to use
+config_firstnick="BOTNICK"
+# Nick if first is in use
+config_secondnick="BOTNICK_"
+# Nick if second is in use
+config_thirdnick="BOTNICK__"
+
+config_ident='rfc3092'
+config_gecos='ietf.org/rfc/rfc3092'
+
+
+###################
+# Server settings #
+###################
+
+# Server to use
+config_server='irc.kuonet-ng.org'
+# What port to use. Normally 6667 works for non SSL connections.
+config_server_port='6667'
+# If 1 use SSL, not all transport modules support this.
+config_server_ssl=0
+# Accept invalid server certificates?
+# Note that some SSL modules (openssl for example) just
+# print any errors and continues anyway
+config_server_ssl_accept_invalid=1
+# Be verbose when connecting?
+# Not all SSL modules support it. The ones that doesn't
+# support it will just ignore it.
+# Be aware that this is mainly for debugging of SSL transport
+# modules as it is possible the verbose output may confuse the bot!
+config_server_ssl_verbose=0
+# If not empty try to bind to this IP when connecting, useful
+# to select vhost. Not all transport modules support this.
+config_server_bind=""
+# If this is empty don't use a server password.
+config_server_passwd=""
+
+
+##################
+# Access control #
+##################
+
+# (*) Access regexes
+# Without at least one set, the bot won't start
+# "owner" is a special capab that grants all other access.
+# The first access entry must be an owner.
+#
+# Scope is a regular expression of channels where access is effective.
+# A /msg (like say to a non channel) will get the scope MSG
+# Anything affecting global state will have the scope "GLOBAL"
+#
+# There can be several access masks matching same host (to allow
+# for different scope/capab combinations).
+#config_access_mask[1]='^The!owner@shellhost\.net$'
+#config_access_capab[1]='owner'
+#config_access_scope[1]='.*'
+
+# Some more access examples:
+
+#config_access_mask[2]='^Someone!whatever@customer-1234\.isp\.com$'
+#config_access_capab[2]='say kick'
+#config_access_scope[2]='#channel'
+
+#config_access_mask[3]='^Someone!whatever@customer-1234\.isp\.com$'
+#config_access_capab[3]='facoid_admin'
+#config_access_scope[3]='GLOBAL'
+
+
+############
+# Commands #
+############
+
+# (*) A regular expression of prefixes we should listen to.
+config_commands_listenregex="(;|${config_firstnick}[:,] )"
+
+# (*) Should we treat any message in /msg as a command even
+# if it doesn't have the the listen prefix?
+config_commands_private_always=0
+
+
+############
+# Feedback #
+############
+
+# (*) How to treat unknown commands:
+# Valid values:
+# 0 = Ignore them (drop command, do nothing).
+# 1 = Return error to sender.
+# 2 = Pass them on to modules that may handle "generic" PRIVMSG.
+# Note that non-commands always get passed on to "generic" PRIVMSG handling modules.
+config_feedback_unknown_commands=1
+
+
+###########
+# Logging #
+###########
+
+# Directory for log files
+config_log_dir="@@logdir@@"
+# (*) Should we log raw data or not?
+# Can be 1 (log) or 0 (don't log)
+config_log_raw=0
+# (*) Should we always log to STDOUT as well?
+# Note that this doesn't mean it will not log at all if set to 0.
+# It will still log errors and other important log messages to STDOUT.
+# This is the same as --verbose.
+# Can be 1 (log) or 0 (don't log)
+config_log_stdout=0
+# (*) When logging to STDOUT should we use colors?
+config_log_colors=1
+
+
+#############
+# Transport #
+#############
+
+# Transport module. You should select exactly one.
+# The recommended non-SSL module is dev-tcp.
+# The recommended SSL module is gnutls.
+config_transport='dev-tcp'
+#config_transport='netcat' # Not well tested, for Debian users and other
+ # with broken distros.
+#config_transport='gnutls'
+#config_transport='socat' # Not well tested
+#config_transport='openssl' # Not well tested
+
+# netcat options
+# MAKE SURE THEY ARE CORRECT if you use netcat.
+#config_transport_netcat_path='/usr/bin/netcat'
+
+# socat options
+# MAKE SURE THEY ARE CORRECT if you use socat.
+#
+# This tells if to use IPv6 or IPv4 to connect.
+# socat doesn't support automatic choice of this.
+# Note that socat versions below 1.5 does not
+# support using IPv6 and SSL at the same time.
+# This can be either "ipv4" or "ipv6".
+#config_transport_socat_protocol_family=ipv4
+
+# Where are transports stored, this can be a relative path from the
+# main script of the bot, or an absolute path.
+config_transport_dir="@@transportdir@@"
+
+
+###########
+# Modules #
+###########
+
+
+# What modules to load on startup, space separated list
+# For a list of modules see the modules dir.
+# Note that the order of the modules may be important
+#
+# The list should normally start with "modules rehash services umodes autojoin"
+# Some modules should be placed last. "factoids" and "karma are such modules.
+config_modules="modules rehash services umodes autojoin ctcp"
+
+# Where are modules stored, this can be a relative path from the
+# main script of the bot, or an absolute path.
+config_modules_dir="@@moddir@@"
+
+
+############################
+# Module specific settings #
+############################
+
+#####################################################################
+# Services module
+#
+# (*) NickServ password
+config_module_services_nickserv_passwd='nickserv password here'
+# (*) Name of NickServ
+# Normally this is correct.
+config_module_services_nickserv_name='NickServ'
+# (*) Service style. Supported are: generic, atheme
+# For the default server (irc.kuonet-ng.org) use atheme
+# Otherwise try generic, that will work with atheme too but
+# some features will be disabled.
+config_module_services_style='atheme'
+# (*) Use server side aliases
+# Try 1 first, if the bot fails to identify try 0
+config_module_services_server_alias=1
+
+
+#####################################################################
+# FAQ module
+#
+# (*) Location of FAQ items.
+#config_module_faq_file='@@datadir@@/faq.txt'
+
+
+####################################################################
+# Quote module
+#
+# (*) Location of quotes file
+#config_module_quotes_file='@@datadir@@/quotes.txt'
+
+
+#####################################################################
+# AutoJoin module.
+#
+# Channels to autojoin on connect
+config_module_autojoin_channels[1]='#channel'
+# A channel can have a key as showed in the example below
+config_module_autojoin_channels[2]='#otherchannel channelkey'
+
+
+#####################################################################
+# Umodes module.
+#
+# (*) Default umodes to set on connect. Also set these at a rehash.
+config_module_umodes_default_umodes="+isB-w"
+
+
+#####################################################################
+# CTCP module
+#
+# (*) What to reply to VERSION requests.
+config_module_ctcp_version_reply="envbot $envbot_version"
+
+
+#####################################################################
+# SQLite3 module
+#
+# (*) Location of SQLite3 database file
+#config_module_sqlite3_database='@@datadir@@/envbot.db'
+
+
+#####################################################################
+# Factoids module
+#
+# (*) Table name for factoids in SQLite3 database
+#config_module_factoids_table='factoids'
+
+
+#####################################################################
+# Karma module
+#
+# (*) Table name for karma data in SQLite3 database
+#config_module_karma_table='karma'
+
+
+#####################################################################
+# Seen module
+#
+# (*) Table name for seen data in SQLite3 database
+#config_module_seen_table='seen'
+
+
+#####################################################################
+# Vote module.
+#
+# (*) The channel where vote mode will work.
+# Note that this is a regular expression that will be
+# surronded by ^ and $ when matching.
+#config_module_vote_channel='#mychannel'
+# (*) How long a vote is open before it is closed (in seconds).
+#config_module_vote_timeout='900'
+
+
+#####################################################################
+# For contrib modules please see the contrib module in question
+# for information on what variables it uses.
diff --git a/doc/coding-standards.txt b/doc/coding-standards.txt
new file mode 100644
index 0000000..3e4253c
--- /dev/null
+++ b/doc/coding-standards.txt
@@ -0,0 +1,100 @@
+Coding standards
+================
+This document describe the coding standards used in envbot.
+
+Indention
+---------
+Indention is done with tabs, one tab char for each indention level.
+Do adjust the code with spaces if you need to break a long line.
+Example:
+ thisFunctionGotLotsOfParameters foo bar quux loooooooooooooooooooooooooooongParameter
+ some more parameters
+That is, first indent with tabs to base indention level, then use spaces
+to adjust.
+
+
+Spaces
+------
+Trailing spaces
+ Forbidden.
+Spaces around operators:
+ Depend on situation.
+ Examples:
+ myvar="this is a value"
+ if [[ "$myvar" = "something" ]]; then
+ As you can see it depends on where it is used.
+ Generally it should be used everywhere except assignment like
+ "=" and "=+".
+
+
+Newlines
+--------
+There should not be newlines before a { or other block separator.
+For example this is correct:
+ if [[ $a = $b ]]; then
+ runStuff
+ fi
+But this is incorrect:
+ if [[ $a = $b ]]
+ then
+ runStuff
+ fi
+
+Trailing newlines: There should be a single
+ending newline at the end of the file, no
+trailing newlines.
+
+
+Functions
+---------
+Functions should be declared without the function keyword
+and with the () following the name of the function, without
+space between the function name and the (). After the () there
+should be one space and then the opening bracket.
+Example:
+myNiceFunction() {
+ runStuff
+}
+
+
+Functions: Returning values
+---------------------------
+Return values using "printf -v" construct. Avoid subshells (for
+performance reasons). One exception to this is the case of the
+function only calling an external program, then just subshell
+the function let the external programs output go to STDOUT of the
+function.
+Example of printf -v construct (first parameter for this function
+is the name of the variable to return in):
+ myfunction() {
+ printf -v "$1" '%s' "something we computed"
+ }
+Using return code should only be used when returned value is
+an error code. Example:
+ otherfunction() {
+ if [[ "$1" = "foo" ]]; then
+ # Success!
+ return 0
+ else
+ # Fail
+ return 1
+ fi
+ }
+
+
+Comments
+--------
+Comments are free form inside function, function comment should
+be coded in bashdoc style.
+(TODO: add description of that or link to that)
+
+Comments should be indented to same level as surrounding code,
+unless the comment is commented out code. Commented out code
+should be avoided in committed code, but if it is needed, the
+comment should be at the start of the line.
+
+
+if
+--
+When testing in if use the [[ ]] construct. Do NOT under ANY
+circumstances use the [ ] construct.
diff --git a/doc/envbot.1 b/doc/envbot.1
new file mode 100644
index 0000000..cd09d00
--- /dev/null
+++ b/doc/envbot.1
@@ -0,0 +1,52 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.36.
+.TH ENVBOT "1" "July 2008" "envbot" "User Commands"
+.SH NAME
+envbot \- An advanced modular IRC bot in bash
+.SH SYNOPSIS
+.B envbot
+[\fIOPTION\fR]...
+.SH DESCRIPTION
+envbot is an advanced modular IRC bot coded in bash.
+.SH OPTIONS
+.TP
+\fB\-c\fR, \fB\-\-config\fR file
+Use file instead of the default as config file.
+.TP
+\fB\-l\fR, \fB\-\-libdir\fR directory
+Use directory instead of the default as library directory.
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+Force verbose output even if config_log_stdout is 0.
+.TP
+\fB\-d\fR, \fB\-\-debug\fR
+Enable debugging code. Most likely pointless to anyone
+except envbot developers or module developers.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+Display this help and exit
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+Output version information and exit
+.PP
+Note that envbot can't handle short versions of options being written together like
+\fB\-vv\fR currently.
+.PP
+Exit status is 0 if OK, 1 if minor problems, 2 if serious trouble.
+.SH EXAMPLES
+.TP
+envbot
+Runs envbot with default options.
+.TP
+envbot \fB\-c\fR bot.config
+Runs envbot with the config bot.config.
+.SH AUTHOR
+Written by Arvid Norlander and EmErgE.
+.SH "REPORTING BUGS"
+Report bugs to http://envbot.org/trac/simpleticket
+.SH COPYRIGHT
+Copyright \(co 2007-2008 Arvid Norlander
+.br
+Copyright \(co 2007-2008 EmErgE
+.br
+This is free software; see the source for copying conditions. There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/doc/factoids.sql b/doc/factoids.sql
new file mode 100644
index 0000000..ecf2732
--- /dev/null
+++ b/doc/factoids.sql
@@ -0,0 +1,15 @@
+/*
+ * This is how to create a factoid database
+ * Use this file like this:
+ * sqlite3 -batch data/envbot.db < doc/factoids.sql
+ *
+ * If you use another table name, change below in
+ * both places
+ */
+DROP TABLE IF EXISTS factoids;
+CREATE TABLE factoids (
+ name TEXT UNIQUE NOT NULL PRIMARY KEY,
+ value TEXT NOT NULL,
+ who TEXT NOT NULL,
+ is_locked INTEGER NOT NULL DEFAULT 0
+);
diff --git a/doc/karma.sql b/doc/karma.sql
new file mode 100644
index 0000000..63f16f7
--- /dev/null
+++ b/doc/karma.sql
@@ -0,0 +1,14 @@
+/*
+ * This is how to create a factoid database
+ * Use this file like this:
+ * sqlite3 -batch data/envbot.db < doc/karma.sql
+ *
+ * If you use another table name, change below in
+ * both places
+ */
+DROP TABLE IF EXISTS karma;
+CREATE TABLE karma (
+ target TEXT UNIQUE NOT NULL PRIMARY KEY,
+ rating INTEGER NOT NULL,
+ is_locked INTEGER NOT NULL DEFAULT 0
+);
diff --git a/doc/module_api2.txt b/doc/module_api2.txt
new file mode 100644
index 0000000..1249a79
--- /dev/null
+++ b/doc/module_api2.txt
@@ -0,0 +1,383 @@
+Module API
+==========
+
+This file contains info needed to write a module.
+
+File name
+---------
+There are two ways to store modules: single file and directory.
+Single file:
+ The module is a single file.
+ The file name should follow this pattern:
+ m_modulename.sh
+ Example:
+ m_faq.sh
+Directory:
+ The module is a directory that contains several files.
+ The directory name should follow this pattern:
+ m_modulename
+ Example:
+ m_faq
+ This directory should contain a file called __main__.sh
+ that the bot will load when loading the module. This file
+ may then load other files from the directory as needed.
+
+Other than that there are no differences between the two ways to store
+modules.
+
+
+Variables
+---------
+All bash variables in the module that are global MUST follow the format
+module_modulename_variablename to prevent overwriting other modules, or
+global bot variables. FAQ module may use: module_faq_last_query for example
+as a variable.
+
+Variables that are local to the function (using the local directive) may have
+other names. Don't forget variables created in for loops and such
+(for foo in bar bash; do echo $foo; done the $foo variable is global here,
+ do local foo on the line before it)
+
+
+Functions and hooks
+-------------------
+Hooks are functions with special names, as listed below in the section "The hooks".
+
+Each function or hook should start with the module_ followed by the module name.
+So if the module is faq.sh the modules should start with module_faq_ like module_faq_INIT.
+
+All bash functions in the module MUST follow the format
+module_modulename_functionname to prevent overwriting other modules, or
+global bot functions.
+
+If not mentioned otherwise the hook is optional.
+Functions marked with TODO may change, get renamed or removed and may not work
+even in current code.
+
+
+The hooks
+---------
+module_modulename_INIT (REQUIRED)
+ Called directly after loading the module.
+ Parameters:
+ $1 = The module name.
+ None
+ Exported variables to this function:
+ MODULE_BASE_PATH
+ Useful for multifile modules, then it is the path to the base directory of
+ the module (without ending slash). If the module consists of a single file
+ it is the path to the module file.
+ Return status:
+ If return status isn't 0 the module is considered as failed to load.
+ Return on in variables (these will get unset after they have been processed):
+ modinit_HOOKS:
+ In this variable should be returned a space separated list of the optional functions
+ that the module implements. FAQ would return "before_connect on_PRIVMSG"
+ modinit_API:
+ Return 2 if you follow this module API.
+ Notes:
+ Check for dependencies (external commands and other modules) in this function,
+ as well as register any commands.
+
+
+module_modulename_UNLOAD (REQUIRED)
+ Called to make the module unload itself.
+ In this function it should clean up after itself and unset all
+ it's functions and variables except the hook functions.
+ The unload function and other hook functions will be undefined
+ after unload finished.
+ The module may or may not get reloaded after this.
+ Parameters:
+ None
+ Return status:
+ 0 = Unloaded correctly
+ 1 = Failed to unload. On this the bot will quit.
+ Notes:
+ This function is NOT called when the bot is exiting. To check for that
+ use the FINALISE hook!
+
+
+module_modulename_REHASH (REQUIRED)
+ Called to make the module reparse any config options.
+ Parameters:
+ None
+ Return status:
+ 0 = Rehashed correctly
+ 1 = Non fatal error for the bot itself. The bot will call UNLOAD on the module.
+ 2 = Fatal error of some kind. On this the bot will quit.
+
+
+module_modulename_FINALISE
+ Called directly before the bot quits. The bot has already closed
+ the connection to the IRC server at this point. This is for saving
+ stuff.
+ Parameters:
+ None
+ Return status:
+ Not checked.
+
+
+module_modulename_after_load
+ Called after all the hooks are added for the module.
+ Parameters:
+ None
+ Return status:
+ 0 = Unloaded correctly
+ 1 = Failed. On this the bot will call unload on the module.
+ Notes:
+ This is useful for stuff that needs to echo log messages
+ as that can't be done in modulename_INIT.
+
+
+module_modulename_before_connect
+ Called before the bot connected.
+ Parameters:
+ None
+ Return status:
+ Not checked.
+
+
+module_modulename_on_connect
+ Called for each line that the bot
+ receives from the server during connecting.
+ Parameters:
+ $1 = Raw line.
+ Return status:
+ Not checked.
+
+
+module_modulename_after_connect
+ Called after the bot connected.
+ Parameters:
+ None
+ Return status:
+ Not checked.
+
+
+module_modulename_on_module_UNLOAD
+ Called when *ANOTHER* module is got unloaded.
+ This is meant for a provider module that must unregister some entry
+ at unload. For example m_help.sh would be such a module.
+ Parameters:
+ $1 = The module that got unloaded.
+ Return code:
+ Not checked.
+
+
+module_modulename_on_PRIVMSG
+ Called when bot gets a PRIVMSG
+ Parameters:
+ $1 = From who (n!u@h)
+ $2 = To who (channel or botnick)
+ $3 = The message
+ Return code:
+ 0 = pass it on to next module
+ 1 = I have taken care of this, and don't
+ consult other modules about this.
+
+
+module_modulename_on_NOTICE
+ Called when bot gets a NOTICE
+ Parameters:
+ $1 = From who (n!u@h)
+ $2 = To who (channel or botnick)
+ $3 = The message
+ Return code:
+ 0 = pass it on to next module
+ 1 = I have taken care of this, and don't
+ consult other modules about this.
+
+
+module_modulename_on_INVITE (new in 0.0.1-beta5)
+ Called when bot gets an INVITE
+ Parameters:
+ $1 = From who (n!u@h)
+ $2 = Target nick (probably our own nick).
+ $3 = Target channel.
+ Return code:
+ Not checked.
+
+
+module_modulename_on_NICK
+ Called when someone changes nick in a channel the bot is in
+ Parameters:
+ $1 = From who (n!u@h)
+ $2 = New nick
+ Return status:
+ Not checked.
+ Reason: A module with a list of users could get desynced if it didn't
+ get the nick change.
+
+
+module_modulename_on_JOIN
+ Called when someone joins a channel the bot is in
+ Parameters:
+ $1 = Who did it (n!u@h)
+ $2 = What channel
+ Return status:
+ Not checked.
+ Reason: A module with a list of users could get desynced if it didn't
+ get the join.
+
+
+module_modulename_on_PART
+ Called when someone parts a channel the bot is in
+ Parameters:
+ $1 = Who did it (n!u@h)
+ $2 = What channel
+ $3 = A reason if one exists.
+ Return status:
+ Not checked.
+ Reason: A module with a list of users could get desynced if it didn't
+ get the part.
+
+
+module_modulename_on_KICK
+ Called when someone parts a channel the bot is in
+ Parameters:
+ $1 = Who did it (n!u@h).
+ $2 = What channel.
+ $3 = Who got kicked.
+ $4 = A reason if one exists.
+ Return status:
+ Not checked.
+ Reason: A module with a list of users could get desynced if it didn't
+ get the kick.
+
+
+module_modulename_on_QUIT
+ Called when someone quits
+ Parameters:
+ $1 = Who did it (n!u@h).
+ $2 = A reason if one exists.
+ Return status:
+ Not checked.
+ Reason: A module with a list of users could get desynced if it didn't
+ get the quit.
+
+
+module_modulename_on_TOPIC
+ Called when a topic is set or on channel join
+ Parameters:
+ $1 = Who did it (n!u@h).
+ $2 = What channel.
+ $3 = New topic.
+ Return status:
+ Not checked.
+ Reason: We don't want to get desynced modules.
+
+
+module_modulename_on_channel_MODE
+ Called when someone changes a mode in the channel
+ Parameters:
+ $1 = Who did it (n!u@h).
+ $2 = On what channel.
+ $3 = The mode change with it's parameters.
+ Return status:
+ Not checked.
+ Reason: A module with a list of users could get desynced if it didn't
+ get the mode change.
+
+
+module_modulename_on_user_MODE (new in 0.0.1-beta4)
+ Called when someone changes a user mode (most likely on ourself)
+ Parameters:
+ $1 = Who did it (n!u@h).
+ $2 = What target (nick).
+ $3 = The mode change with it's parameters.
+ Return status:
+ Not checked.
+ Reason: There is no point in checking this.
+
+
+module_modulename_on_numeric
+ Called on any numeric after connect.
+ Parameters:
+ $1 = The numeric.
+ $2 = It's data.
+ Return status:
+ 0 = pass it on to next module
+ 1 = I have taken care of this, and don't
+ consult other modules about this.
+ Notes:
+ In most cases you DON'T want to return 1 for this. If you do be very very careful.
+
+
+module_modulename_on_PONG
+ Called when bot receives a PONG from server
+ Parameters:
+ $1 = Sender
+ $2 = Second server.
+ $3 = The data.
+ Return status:
+ Not checked.
+
+
+module_modulename_on_server_ERROR
+ # ERROR :Closing link (rfc3092@1.2.3.4) [Killed (AnMaster (testing a kill on inspircd))]
+ # ERROR :Closing Link: [1.2.3.4] (Throttled: Reconnecting too fast) -Email staff@example.com for more information.
+ # ERROR :Closing Link: envbot[host.name.se] Brain ([hurricane.KuoNET.org] Local kill by Brain (testing kill on unrealircd))
+ # ERROR :Closing Link: envbot[host.name.se] hurricane.KuoNET.org (Killed (Brain (testing kill on unrealircd, remote one)))
+ Called when the bot get an ERROR from the server.
+ Parameters:
+ $1 = The ERROR message.
+ Return status:
+ Not checked.
+ Reason: There isn't much point in swallowing this.
+
+
+module_modulename_on_KILL
+ Called when the bot get a KILL message from the server.
+ Parameters:
+ $1 = The sender of the kill.
+ $2 = The target of the kill.
+ $3 = The KILL path.
+ $4 = The reason.
+ Return status:
+ Not checked.
+ Reason: Risk for desync of modules.
+
+
+module_modulename_before_disconnect
+ Called when the bot is about to disconnect from server.
+ The bot may or may not reconnect after this depending on what caused this.
+ the disconnect.
+ Parameters:
+ None
+ Return status:
+ Not checked.
+ Notes:
+ This won't be called if the disconnect wasn't planned, like
+ a ping timeout, getting killed, and so on.
+
+
+module_modulename_after_disconnect
+ Called when the bot gets disconnected from server.
+ The bot may or may not reconnect after this depending on what caused
+ the disconnect.
+ Parameters:
+ None
+ Return status:
+ Not checked.
+ Reason: A module may need clean up, don't let it desync.
+
+
+module_modulename_on_RAW
+ Called when the bot gets *ANY* line from the server after the initial connect.
+ With this a module can hook onto something not supported by a more specific hook.
+ The downsides:
+ * Potentially speed as it has to be called for any message.
+ * You have to parse more on your own.
+ * You can desync other modules! Imagine a module that keep a list of
+ users in the channel. If you swallow a message about a join, part,
+ quit, nickchange, or other ones it may get desynced!
+ * Even worse, you can desync the bot itself if you swallow NICK, JOIN
+ PART, KICK or possibly others. Or make it ping out by swallowing PING.
+ Parameters:
+ $1 = The raw line
+ Return status:
+ 0 = pass it on to next module
+ 1 = I have taken care of this, and don't
+ consult other modules about this.
+ For raw this means no normal hooks
+ will be called on the line either.
diff --git a/doc/seen.sql b/doc/seen.sql
new file mode 100644
index 0000000..f10e5d0
--- /dev/null
+++ b/doc/seen.sql
@@ -0,0 +1,15 @@
+/*
+ * This is how to create a factoid database
+ * Use this file like this:
+ * sqlite3 -batch data/envbot.db < doc/seen.sql
+ *
+ * If you use another table name, change below in
+ * both places
+ */
+DROP TABLE IF EXISTS seen;
+CREATE TABLE seen (
+ nick TEXT UNIQUE NOT NULL PRIMARY KEY,
+ channel TEXT NOT NULL,
+ timestamp TEXT NOT NULL,
+ message TEXT NOT NULL
+);
diff --git a/doc/transports_api.txt b/doc/transports_api.txt
new file mode 100644
index 0000000..881ae58
--- /dev/null
+++ b/doc/transports_api.txt
@@ -0,0 +1,101 @@
+Transport modules
+=================
+
+Transport modules is a feature that allows different ways to connect to the server.
+They may support different systems and/or different features.
+
+
+File descriptors
+================
+If a module need to use a file descriptor it should use FD 3 and 4.
+If more than two are needed, the module can try higher but that may be used
+for other things in the future.
+
+
+Configuration
+=============
+Additional configuration variables for a module should follow this naming scheme:
+config_transport_transportname_variablename
+Example:
+config_transport_stunnel_path
+
+
+Variables
+=========
+The module defines a variable called $transport_supports
+It is a space separated list of supported features. Current ones:
+ * ipv4 - Supports IPv4
+ * ipv6 - Supports IPv6
+ * nossl - Supports making non-SSL connections
+ * ssl - Supports SSL
+ * bind - Supports using a specific IP when connecting
+This variable only needs to be set after transport_check_support has run
+
+
+Functions
+=========
+The function names below should not be unique for the module like with
+normal modules. You can only use one transport module at a time.
+
+
+transport_check_support()
+ Check if all the stuff needed to use this transport is available
+ on this system.
+ Return status:
+ 0 = Yes
+ 1 = No
+
+
+transport_connect()
+ Try to connect
+ Return status:
+ 0 = Connection successful
+ 1 = Connection failed
+ Parameters:
+ $1 = Hostname/IP
+ $2 = Port
+ $3 = If 1 use SSL. If the module does not support it, just ignore it.
+ $3 = IP to bind to if any and if supported
+ If the module does not support it, just ignore it.
+
+
+transport_disconnect()
+ Called to close connection
+ Parameters:
+ None
+ Return status:
+ Not checked.
+ Notes:
+ The module must handle this getting called even when not connected or when
+ partly connected (it should clean up any program the module is running in
+ the background on a ping time out for example).
+
+
+transport_alive()
+ Called to check if connection is still alive.
+ Return status
+ 0 If connection is still alive.
+ 1 If it isn't alive.
+ Notes:
+ This function should be low overhead, it gets called quite often.
+
+
+
+transport_read_line()
+ Return a line in the variable $line.
+ Return status:
+ 0 = Success
+ 1 = Connection has failed (timeout or whatever)
+ Notes:
+ The transport module should remove any trailing \r and/or \n (CR and/or LF)
+ from the string before it returns it.
+
+
+transport_write_line()
+ Send a line
+ Parameters:
+ $* = send this
+ Return status:
+ Not checked.
+ Notes:
+ The transport module should add a \n (LF) to the end of the data it get.
diff --git a/envbot b/envbot
new file mode 100755
index 0000000..a3de9e7
--- /dev/null
+++ b/envbot
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## This is a wrapper that runs envbot in a sane setup.<br />
+## Using a wrapper is really the simplest way to do it.
+#---------------------------------------------------------------------
+
+# Default config file
+config_file="bot_settings.sh"
+# Default place to load libraries from
+# Also where to load main envbot from. We won't accept any other location
+# for main.sh, but main.sh will for library scripts.
+library_dir="lib"
+
+# This work-around is needed on FreeBSD
+bash="$(type -p bash)"
+
+# Later arguments on the command line will override the defaults.
+
+# env -i: Clean environment
+# TERM="$TERM": But keep TERM
+# bash --norc: Don't read profile/bashrc
+exec env -i TERM="$TERM" REALHOME="$HOME" "$bash" --norc "${library_dir}/main.sh" --config "$config_file" --libdir "$library_dir" "$@"
diff --git a/hack_of_all_hacks b/hack_of_all_hacks
new file mode 100644
index 0000000..0c4e026
--- /dev/null
+++ b/hack_of_all_hacks
@@ -0,0 +1,165 @@
+#! /bin/bash
+
+# This function reads tags of xml in the same way read normally reads lines.
+function rdom
+{
+ local IFS=\>
+ read -d \< element content
+}
+
+
+function l33t_codes
+{
+ person="${sender%%!*}"
+
+ ##########################
+ # Shared libraries error #
+ ##########################
+
+ if [[ ${line} == *"error while loading shared libraries"* ]]
+ then
+ send_msg '#parabola' "${person}: please report a bug, specifying exact error message, package of the failing command and architecture"
+ fi
+
+ ##########
+ # Repeat #
+ ##########
+
+ # If two different people say the same thing in a row then say it again
+ # for fun.
+ if [[ ${line##*PRIVMSG} == ${lastline} ]] && [[ ${person} != ${lastsender} ]]
+ then
+ send_msg '#parabola' "${line##*PRIVMSG}"
+ fi
+
+ lastline="${line##*PRIVMSG}"
+ lastsender="${person}"
+
+ #################
+ # Announcements #
+ #################
+
+ # Remove any forward slashes.
+ personoslash="${person//\/}"
+
+ # Make this person a folder if they don't already have one.
+ if ! [[ -d "announcements/people/${personoslash}" ]]
+ then
+ mkdir -p "announcements/people/${personoslash}"
+ touch "announcements/people/${personoslash}/phrases"
+ cat << EOF > "announcements/people/${personoslash}/settings"
+enabled=yes
+locked=no
+EOF
+ fi
+
+ the_time_now=$(date +%s)
+
+ # If this person has announcements enabled and there is at least one phrase
+ # in their file and their seen log exists.
+ if grep 'enabled=yes' "announcements/people/${personoslash}/settings" > \
+ /dev/null && (( $( wc -l "announcements/people/${personoslash}/phrases" | cut -d ' ' -f 1 ) )) && [[ -f announcements/people/${personoslash}/seen ]]
+ then
+ # Check if they were last present more than three hours ago.
+ if (( ( $( stat -c %Y announcements/people/${personoslash}/seen ) +
+ 10800 ) < the_time_now ))
+ then
+ send_msg '#parabola' \
+ "$( shuf "announcements/people/${personoslash}/phrases" | head -1 )"
+ fi
+
+ fi
+
+ # Record that the person has been seen, and when.
+ touch "announcements/people/${personoslash}/seen"
+
+ ########
+ # Seen #
+ ########
+
+ # This one depends on the previous to record the last time the person was
+ # seen and create the `the_time_now' var.
+ if [[ ${line} == *"pbots_friend: when did you last see"* ]]
+ then
+ subject="${line##*pbots_friend: when did you last see }"
+ subject="${subject%?}"
+ subject="${subject%% *}"
+ if [[ -f announcements/people/${subject}/seen ]]
+ then
+ seconds_ago_seen="$(( the_time_now - $( stat -c %Y announcements/people/${subject}/seen ) ))"
+ minutes_ago_seen="$(( ( the_time_now - $( stat -c %Y announcements/people/${subject}/seen ) ) / 60 ))"
+ hours_ago_seen="$(( ( the_time_now - $( stat -c %Y announcements/people/${subject}/seen ) ) / 3600 ))"
+ days_ago_seen="$(( ( the_time_now - $( stat -c %Y announcements/people/${subject}/seen ) ) / 86400 ))"
+ months_ago_seen="$(( ( the_time_now - $( stat -c %Y announcements/people/${subject}/seen ) ) / 2592000 ))"
+ years_ago_seen="$(( ( the_time_now - $( stat -c %Y announcements/people/${subject}/seen ) ) / 946080000 ))"
+ if (( seconds_ago_seen < 120 ))
+ then
+ send_msg '#parabola' "I last saw ${subject} speak ${seconds_ago_seen} seconds ago."
+ elif (( minutes_ago_seen < 120 ))
+ then
+ send_msg '#parabola' "I last saw ${subject} speak ${minutes_ago_seen} minutes ago."
+ elif (( hours_ago_seen < 48 ))
+ then
+ send_msg '#parabola' "I last saw ${subject} speak ${hours_ago_seen} hours ago."
+ elif (( days_ago_seen < 60 ))
+ then
+ send_msg '#parabola' "I last saw ${subject} speak ${days_ago_seen} days ago."
+ elif (( months_ago_seen < 24 ))
+ then
+ send_msg '#parabola' "I last saw ${subject} speak ${months_ago_seen} months ago."
+ else
+ send_msg '#parabola' "I last saw ${subject} speak ${years_ago_seen} years ago."
+ fi
+ else
+ send_msg '#parabola' "I never saw ${subject} speak."
+ fi
+ fi
+
+ ######################
+ # Echo injected data #
+ ######################
+
+ # Should send a message like: echo 'This is the message' > /tmp/un-provoked-message-store
+
+ line_half_filtered="${line##*PRIVMSG}"
+ line_filtered=${line_half_filtered##* :}
+
+ if [[ ${personoslash} == tlCJ99mfZl ]]
+ then
+ send_msg '#parabola' "${line_filtered}"
+ fi
+
+ # TODO: add a birthday announcement feature, cointoss feature, timer
+ # feature.
+
+ #####################
+ # Page title getter #
+ #####################
+
+ if [[ ${line} =~ http://[^\ ]+ ]] || [[ ${line} =~ https://[^\ ]+ ]]
+ then
+ url_to_get="${BASH_REMATCH}"
+
+ the_title=$(
+ curl -L --compressed "${url_to_get}" 2> /dev/null |
+ while rdom
+ do
+ if [[ ${element} = title ]]
+ then
+ echo "${content}" | sed 's/&mdash;/—/g ; s/&lt;/</g ; s/&gt;/>/g ; s/&amp;/&/g ; s/&quot;/"/g'
+ fi
+ done
+ )
+
+ if ! [[ -z ${the_title} ]]
+ then
+ send_msg '#parabola' "The title of that page is: \`${the_title}'"
+ fi
+ fi
+
+ #########
+ # Tests #
+ #########
+
+ #echo "${line}"
+}
diff --git a/lib/access.sh b/lib/access.sh
new file mode 100644
index 0000000..28fa358
--- /dev/null
+++ b/lib/access.sh
@@ -0,0 +1,99 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Access control library.
+#---------------------------------------------------------------------
+
+
+#---------------------------------------------------------------------
+## Check for owner access.
+## @Type API
+## @param n!u@h mask
+## @return 0 If access was granted
+## @return 1 If access was denied.
+#---------------------------------------------------------------------
+access_check_owner() {
+ debug_log_caller "$@"
+ security_assert_argc 1 1 "$@" || {
+ log_error "Aiie! Access denied because of incorrect function call!"
+ return 1
+ }
+ local index
+ for index in ${!config_access_mask[*]}; do
+ if [[ "$1" =~ ${config_access_mask[$index]} ]] && list_contains "config_access_capab[$index]" 'owner'; then
+ return 0
+ fi
+ done
+ return 1
+}
+
+#---------------------------------------------------------------------
+## Check for access in scope.
+## @Type API
+## @param Capability to check for.
+## @param n!u@h mask
+## @param What scope
+## @return 0 If access was granted
+## @return 1 If access was denied.
+#---------------------------------------------------------------------
+access_check_capab() {
+ debug_log_caller "$@"
+ security_assert_argc 3 3 "$@" || {
+ log_error "Aiie! Access denied because of incorrect function call!"
+ return 1
+ }
+ local index
+ for index in ${!config_access_mask[*]}; do
+ if [[ "$2" =~ ${config_access_mask[$index]} ]] && \
+ [[ "$3" =~ ${config_access_scope[$index]} ]]; then
+ if list_contains "config_access_capab[$index]" "$1" || \
+ list_contains "config_access_capab[$index]" "owner"; then
+ return 0
+ fi
+ fi
+ done
+ return 1
+}
+
+#---------------------------------------------------------------------
+## Used to log actions like "did a rehash" if access was granted.
+## @Type API
+## @param n!u@h mask
+## @param What happened.
+#---------------------------------------------------------------------
+access_log_action() {
+ log_info_file owner.log "$1 performed the restricted action: $2"
+}
+
+#---------------------------------------------------------------------
+## Return error message about failed access to someone, and log it
+## @Type API
+## @param n!u@h mask
+## @param What they tried to do
+## @param What capability they need
+#---------------------------------------------------------------------
+access_fail() {
+ log_error_file access.log "$1 tried to \"$2\" but lacks access."
+ local nick=
+ parse_hostmask_nick "$sender" 'nick'
+ send_notice "$nick" "Permission denied. You need the capability \"$3\" to do this action."
+}
diff --git a/lib/channels.sh b/lib/channels.sh
new file mode 100644
index 0000000..df38c55
--- /dev/null
+++ b/lib/channels.sh
@@ -0,0 +1,125 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Channel management.
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Space separated list of current channels
+## @Type API
+#---------------------------------------------------------------------
+channels_current=""
+
+#---------------------------------------------------------------------
+## Join a channel
+## @Type API
+## @param The channel to join.
+## @param Is a channel key, if any.
+#---------------------------------------------------------------------
+channels_join() {
+ local channel="$1"
+ local key=""
+ [[ -n "$2" ]] && key=" $2"
+ send_raw "JOIN ${channel}${key}"
+}
+
+#---------------------------------------------------------------------
+## Part a channel
+## @Type API
+## @param The channel to part
+## @param Is a reason.
+#---------------------------------------------------------------------
+channels_part() {
+ local channel="$1"
+ local reason=""
+ [[ -n "$2" ]] && reason=" :$2"
+ send_raw "PART ${channel}${reason}"
+}
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Internal function!
+## Adds channels to the list
+## @Type Private
+## @param The channel to add
+#---------------------------------------------------------------------
+channels_add() {
+ channels_current+=" $1"
+}
+
+#---------------------------------------------------------------------
+## Internal function!
+## Removes channels to the list
+## @Type Private
+## @param The channel to remove
+#---------------------------------------------------------------------
+channels_remove() {
+ list_remove channels_current "$1" channels_current
+}
+
+#---------------------------------------------------------------------
+## Check if we parted, called from main loop
+## @Type Private
+## @param n!u@h mask
+## @param Channel parted.
+## @param Reason (ignored).
+#---------------------------------------------------------------------
+channels_handle_part() {
+ local whoparted=
+ parse_hostmask_nick "$1" 'whoparted'
+ if [[ $whoparted == $server_nick_current ]]; then
+ channels_remove "$2"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Check if we got kicked, called from main loop
+## @Type Private
+## @param n!u@h mask of kicker
+## @param Channel kicked from.
+## @param Nick of kicked user
+## @param Reason (ignored).
+#---------------------------------------------------------------------
+channels_handle_kick() {
+ local whogotkicked="$3"
+ if [[ $whogotkicked == $server_nick_current ]]; then
+ channels_remove "$2"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Check if we joined, called from main loop
+## @Type Private
+## @param n!u@h mask
+## @param Channel joined.
+#---------------------------------------------------------------------
+channels_handle_join() {
+ local whojoined=
+ parse_hostmask_nick "$1" 'whojoined'
+ if [[ $whojoined == $server_nick_current ]]; then
+ channels_add "$2"
+ fi
+}
diff --git a/lib/commands.sh b/lib/commands.sh
new file mode 100644
index 0000000..b914a58
--- /dev/null
+++ b/lib/commands.sh
@@ -0,0 +1,265 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Handle registering of commands
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## List of commands (maps to function for the command), a hash
+## @Note Dummy variable to document the fact that it is a hash.
+## @Type Private
+#---------------------------------------------------------------------
+commands_list=''
+
+#---------------------------------------------------------------------
+## List of functions (by module), a hash
+## @Note Dummy variable to document the fact that it is a hash.
+## @Type Private
+#---------------------------------------------------------------------
+commands_modules_functions=''
+
+#---------------------------------------------------------------------
+## List of commands (by function), a hash
+## @Note Dummy variable to document the fact that it is a hash.
+## @Type Private
+#---------------------------------------------------------------------
+commands_function_commands=''
+
+#---------------------------------------------------------------------
+## List of commands (by module)
+## @Note Dummy variable to document the fact that it is a hash.
+## @Type Private
+#---------------------------------------------------------------------
+commands_module_commands=''
+
+#---------------------------------------------------------------------
+## List of modules (by command)
+## @Note Dummy variable to document the fact that it is a hash.
+## @Type Private
+#---------------------------------------------------------------------
+commands_commands_module=''
+
+#---------------------------------------------------------------------
+## Comma separated list of all commands
+## @Type Semi-private
+#---------------------------------------------------------------------
+commands_commands=''
+
+# Just unset dummy variables.
+unset commands_list commands_modules_functions commands_function_commands commands_module_commands commands_module_commands
+
+#---------------------------------------------------------------------
+## Register a command.
+## @Type API
+## @param Module name
+## @param Function name (Part after module_modulename_handler_)
+## @param Command name (on IRC, may contain spaces) (optional, defaults to same as function name, that is $2)
+## @return 0 If successful
+## @return 1 If failed for other reason
+## @return 2 If invalid command name
+## @return 3 If the command already exists (maybe from some other module)
+## @return 4 If the function already exists for other command.
+## @return 5 If the function in question is not declared.
+#---------------------------------------------------------------------
+commands_register() {
+ # Speed isn't that important here, it is only called at module load after all.
+ local module="$1"
+ local function_name="$2"
+ local command_name="$3"
+ # Command name is optional
+ if [[ -z $command_name ]]; then
+ command_name="$function_name"
+ fi
+ # Check for valid command name
+ if ! [[ $command_name =~ ^[a-zA-Z0-9] ]]; then
+ log_error "commands_register_command: Module \"$module\" gave invalid command name \"$command_name\". First char of command must be alphanumeric."
+ return 2
+ fi
+ if ! [[ $command_name =~ ^[a-zA-Z0-9][^\ ,]*( [^, ]+)?$ ]]; then
+ log_error "commands_register_command: Module \"$module\" gave invalid command name \"$command_name\". A command can be at most 2 words and should have no trailing white space and may not contain a \",\" (comma)."
+ return 2
+ fi
+ # Bail out if command is already registered.
+ if hash_exists 'commands_list' "$command_name"; then
+ log_error "commands_register_command: Failed to register command from \"$module\": a command with the name \"$command_name\" already exists."
+ return 3
+ fi
+ # Bail out if the function already is mapped to some other command
+ if hash_exists 'commands_function_commands' "$function_name"; then
+ log_error "commands_register_command: Failed to register command from \"$module\": the function is already registered under another command name."
+ return 4
+ fi
+
+ # Does the function itself exist?
+ local full_function_name="module_${module}_handler_${function_name}"
+ if ! declare -F | grep -qe "^declare -f ${full_function_name}$"; then
+ log_error "commands_register_command: Failed to register command from \"$module\": the function $full_function_name does not exist"
+ return 5
+ fi
+ # So it was valid. Lets add it then.
+
+ # Store in module -> function mapping.
+ hash_append 'commands_modules_functions' "$module" "$function_name" || {
+ log_error "commands_register_command: module -> commands mapping failed: mod=\"$module\" func=\"$function_name\"."
+ return 1
+ }
+ # Store in command -> function mapping
+ hash_set 'commands_list' "$command_name" "$full_function_name" || {
+ log_error "commands_register_command: command -> function mapping failed: cmd=\"$command_name\" full_func=\"$full_function_name\"."
+ return 1
+ }
+ # Store in function -> command mapping
+ hash_set 'commands_function_commands' "$function_name" "$command_name" || {
+ log_error "commands_register_command: function -> command mapping failed: func=\"$function_name\" cmd=\"$command_name\"."
+ return 1
+ }
+ # Store in command -> module mapping
+ hash_set 'commands_commands_module' "$command_name" "$module" || {
+ log_error "commands_register_command: command -> module mapping failed: cmd=\"$command_name\" mod=\"$module\"."
+ return 1
+ }
+ # Store in module -> commands mapping (ick!)
+ hash_append 'commands_module_commands' "$module" "$command_name" ',' || {
+ log_error "commands_register_command: module -> command mapping failed: mod=\"$module\" cmd=\"$command_name\"."
+ }
+ # Store in comma-separated command list
+ if [[ $commands_commands ]]; then
+ commands_commands+=",$command_name" || return 1
+ else
+ commands_commands="$command_name" || return 1
+ fi
+}
+
+#---------------------------------------------------------------------
+## Get what module provides a command.
+## @param Command to find.
+## @param Variable to return in
+## @Type Semi-private
+#---------------------------------------------------------------------
+commands_provides() {
+ hash_get "commands_commands_module" "$1" "$2"
+}
+
+#---------------------------------------------------------------------
+## Get what commands exist in a module.
+## @param Command to find.
+## @param Variable to return comma separated list in
+## @Type Semi-private
+#---------------------------------------------------------------------
+commands_in_module() {
+ hash_get "commands_module_commands" "$1" "$2"
+}
+
+#---------------------------------------------------------------------
+## Will remove all commands from a module and unset the functions in question.
+## @Type Private
+## @param Module
+## @return 0 If successful (or no commands exist for module)
+## @return 1 If error
+## @return 2 If fatal error
+#---------------------------------------------------------------------
+commands_unregister() {
+ local module="$1"
+ # Are there any commands for the module?
+ hash_exists 'commands_modules_functions' "$module" || {
+ return 0
+ }
+ local function_name full_function_name command_name functions
+ # Get list of functions
+ hash_get 'commands_modules_functions' "$module" 'functions' || return 2
+ # Iterate through the functions
+ for function_name in $functions; do
+ # Get command name
+ hash_get 'commands_function_commands' "$function_name" 'command_name' || return 2
+ # Unset from function -> command hash
+ hash_unset 'commands_function_commands' "$function_name" || return 2
+ # Unset from command -> function hash
+ hash_unset 'commands_list' "$command_name" || return 2
+ # Unset from command -> module mapping
+ hash_unset 'commands_commands_module' "$command_name" || return 2
+ # Remove from command list.
+ list_remove 'commands_commands' "$command_name" 'commands_commands' "," || return 1
+ # Unset help strings (if any):
+ unset helpentry_${module}_${function_name}_syntax
+ unset helpentry_${module}_${function_name}_description
+ # Unset function itself.
+ full_function_name="module_${module}_handler_${function_name}"
+ unset "$full_function_name" || return 2
+ done
+ # Unset the module -> commands mapping.
+ hash_unset 'commands_module_commands' "$module" || return 2
+ # Finally unset module -> functions mapping.
+ hash_unset 'commands_modules_functions' "$module" || return 2
+}
+
+#---------------------------------------------------------------------
+## Process a line finding what command it would be
+## @Type Private
+## @param Sender
+## @param Target
+## @param Query
+## @return 0 If not a command
+## @return 1 If it indeed was a command that we therefore handled.
+## @return 2 A command but that didn't exist.
+#---------------------------------------------------------------------
+commands_call_command() {
+ local regex="${config_commands_listenregex}"
+ # Not on a channel?
+ if [[ ! $2 =~ ^# ]]; then
+ # Should we treat it as a command anyway?
+ if [[ $config_commands_private_always == 1 ]]; then
+ local regex="(${config_commands_listenregex})?"
+ fi
+ fi
+ # Check if it is a command.
+ # (${config_commands_listenregex}, followed by an alphanumeric char.)
+ if [[ "$3" =~ ^${regex}([a-zA-Z0-9].*) ]]; then
+ local data="${BASH_REMATCH[@]: -1}"
+ # Right, get the parts of the command
+ if [[ $data =~ ^([a-zA-Z0-9][^ ]*)( [^, ]+)?( .*)? ]]; then
+ local firstword="${BASH_REMATCH[1]}"
+ local secondword="${BASH_REMATCH[2]}"
+ local parameters="${BASH_REMATCH[3]}"
+
+ local function=
+ # Check for two word commands first.
+ hash_get 'commands_list' "${firstword}${secondword}" 'function'
+ if [[ -z "$function" ]]; then
+ # Maybe one word then?
+ hash_get 'commands_list' "$firstword" 'function'
+ if [[ "$function" ]]; then
+ parameters="${secondword}${parameters}"
+ # No, not that either
+ else
+ return 2
+ fi
+ fi
+
+ # So we got a command, now lets run it
+ # (strip leading white spaces) from parameters.
+ "$function" "$1" "$2" "${parameters## }"
+ return 1
+ fi
+ return 2
+ fi
+ return 0
+}
diff --git a/lib/config.sh b/lib/config.sh
new file mode 100644
index 0000000..0b344e7
--- /dev/null
+++ b/lib/config.sh
@@ -0,0 +1,219 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Configuration management
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Rehash config file.
+## @Type API
+## @return 0 Success.
+## @return 2 Not same config version.
+## @return 3 Failed to source. The bot should not be in an undefined state.
+## @return 4 Config validation on faked source failed. The bot should not be in an undefined state.
+## @return 5 Failed to source. The bot may be in an undefined state.
+## @Note If config validation fails at REAL source, the bot may quit. However this should never happen.
+#---------------------------------------------------------------------
+config_rehash() {
+ local new_conf_ver="$(grep -E '^config_version=' "$config_file")"
+ if ! [[ $new_conf_ver =~ ^config_version=$config_current_version ]]; then
+ log_error "REHASH: Not same config version. Rehash aborted."
+ return 2
+ fi
+ # Try sourceing in a subshell first to catch errors
+ # without causing bot to break
+ ( source "$config_file" )
+ if [[ $? -ne 0 ]]; then
+ log_error "REHASH: Failed faked source. Rehash aborted. (TIP: Check for syntax errors in config and any message above this message.)"
+ return 3
+ fi
+ # HACK: Subshell, then unset all but two config_ variables (one is readonly, the other is needed to validate)
+ # Then source config file and run validation on it.
+ ( unset -v $(sed 's/ *config_current_version */ /g;s/ *config_file */ /g' <<<"${!config_*}")
+ source "$config_file"
+ config_validate && config_validate_transport )
+ if [[ $? -ne 0 ]]; then
+ log_error "REHASH: Failed config validation on new config. Rehash aborted."
+ return 4
+ fi
+ # Source for real if that worked
+ source "$config_file"
+ if [[ $? -ne 0 ]]; then
+ log_error "REHASH: Failed real source. BOT MAY BE IN UNDEFINED STATE."
+ return 5
+ fi
+ # Lets force command line -v, it may have been overwritten by config.
+ if [[ $force_verbose -eq 1 ]]; then
+ config_log_stdout='1'
+ fi
+ local status
+ modules_load_from_config
+ for module in $modules_loaded; do
+ module_${module}_REHASH
+ status=$?
+ if [[ $status -eq 1 ]]; then
+ log_error "Rehash of ${module} failed, trying to unload it."
+ modules_unload "${module}" || {
+ log_fatal "Unloading of ${module} after failed rehash failed."
+ bot_quit "Fatal error in unload of module that failed to rehash"
+ }
+ fi
+ if [[ $status -eq 2 ]]; then
+ log_fatal "Rehash of ${module} failed in a FATAL way. Quitting"
+ bot_quit "Fatal error in rehash of module"
+ fi
+ done
+ log_info_stdout "Rehash successful"
+}
+
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## This will call logging if logging is setup,
+## otherwise just print to STDOUT, with prefix
+## @Type Private
+#---------------------------------------------------------------------
+config_dolog_fatal() {
+ if [[ $log_file ]]; then
+ log_fatal "$1"
+ else
+ echo "FATAL ERROR: $1"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Returns an error if the variable in question is empty/not set
+## @Note Works only for non-array variables
+## @Type Private
+## @param Variable name
+## @param Extra error line(s) to append (optional, one parameter for each extra line)
+#---------------------------------------------------------------------
+config_validate_check_exists() {
+ if [[ -z "${!1}" ]]; then
+ config_dolog_fatal "YOU MUST SET $1 IN THE CONFIG"
+ shift
+ # Do the rest of the messages
+ local line=
+ for line in "$@"; do
+ config_dolog_fatal "$line"
+ done
+ envbot_quit 2
+ fi
+}
+
+
+#---------------------------------------------------------------------
+## Validate config file
+## @Type Private
+#---------------------------------------------------------------------
+config_validate() {
+ # Note: normal logging is not initialized yet at this point,
+ # so we use config_dolog_fatal, that calls normal logging in case
+ # logging is loaded (like rehash).
+
+ # General settings
+ config_validate_check_exists config_firstnick
+ config_validate_check_exists config_ident
+ config_validate_check_exists config_gecos
+
+ # Server settings
+ config_validate_check_exists config_server
+ config_validate_check_exists config_server_port
+ config_validate_check_exists config_server_ssl
+
+ # Logging
+ config_validate_check_exists config_log_dir
+ config_validate_check_exists config_log_stdout
+ config_validate_check_exists config_log_raw
+ config_validate_check_exists config_log_colors
+
+ # Commands
+ config_validate_check_exists config_commands_listenregex
+ config_validate_check_exists config_commands_private_always
+
+ # Feedback
+ config_validate_check_exists config_feedback_unknown_commands
+
+ # Access
+ if [[ -z "${config_access_mask[1]}" ]]; then
+ config_dolog_fatal "YOU MUST SET AT LEAST ONE OWNER IN EXAMPLE CONFIG"
+ config_dolog_fatal "AND THAT OWNER MUST BE THE FIRST ONE (config_access_mask[1] that is)."
+ envbot_quit 1
+ fi
+ if ! list_contains "config_access_capab[1]" "owner"; then
+ config_dolog_fatal "YOU MUST SET AT LEAST ONE OWNER IN EXAMPLE CONFIG"
+ config_dolog_fatal "AND THAT OWNER MUST BE THE FIRST ONE (config_access_capab[1] that is)."
+ envbot_quit 1
+ fi
+
+ # Transports
+ config_validate_check_exists "config_transport_dir"
+ if [[ ! -d "${config_transport_dir}" ]]; then
+ config_dolog_fatal "The transport directory ${config_transport_dir} doesn't seem to exist"
+ envbot_quit 2
+ fi
+ config_validate_check_exists "config_transport"
+ if [[ ! -r "${config_transport_dir}/${config_transport}.sh" ]]; then
+ config_dolog_fatal "The transport ${config_transport} doesn't seem to exist"
+ envbot_quit 2
+ fi
+
+ # Modules
+ config_validate_check_exists config_modules_dir
+ if ! [[ -d "$config_modules_dir" ]]; then
+ if ! list_contains transport_supports "bind"; then
+ config_dolog_fatal "$config_modules_dir DOES NOT EXIST OR IS NOT A DIRECTORY."
+ envbot_quit 1
+ fi
+ fi
+ config_validate_check_exists config_modules
+}
+
+#---------------------------------------------------------------------
+## Validate some settings from config file that can only be done after
+## transport was loaded.
+## @Type Private
+#---------------------------------------------------------------------
+config_validate_transport() {
+ # At this point logging is enabled, we can use it.
+ if [[ $config_server_ssl -ne 0 ]]; then
+ if ! list_contains transport_supports "ssl"; then
+ log_fatal "THIS TRANSPORT DOES NOT SUPORT SSL"
+ envbot_quit 1
+ fi
+ else
+ if ! list_contains transport_supports "nossl"; then
+ log_fatal "THIS TRANSPORT REQUIRES SSL"
+ envbot_quit 1
+ fi
+ fi
+ if [[ "$config_server_bind" ]]; then
+ if ! list_contains transport_supports "bind"; then
+ log_fatal "THIS TRANSPORT DOES NOT SUPORT BINDING AN IP"
+ envbot_quit 1
+ fi
+ fi
+}
diff --git a/lib/debug.sh b/lib/debug.sh
new file mode 100644
index 0000000..106a329
--- /dev/null
+++ b/lib/debug.sh
@@ -0,0 +1,84 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Functions used during development for debugging.
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Debugging function to check that right number of parameters were
+## provided.
+## @param Lowest allowed count of parameters.
+## @param Higest allowed count of parameters. (Optional, defaults to same as lower)
+#---------------------------------------------------------------------
+debug_assert_argc() {
+ [[ $envbot_debugging ]] || return 0
+ if [[ ${BASH_ARGC[1]} -lt $1 || ${BASH_ARGC[1]} -gt ${2:-$1} ]]; then
+ log_debug "${FUNCNAME[1]} should have had $1 parameters but had ${BASH_ARGC[1]} instead"
+ log_debug "${FUNCNAME[1]} was called from ${BASH_SOURCE[2]}:${BASH_LINENO[1]} ${FUNCNAME[2]}."
+ return 1
+ fi
+}
+
+#---------------------------------------------------------------------
+## Reports who called function and with what arguments.
+## @Type API
+## @param Should be "$@" at first line of function.
+#---------------------------------------------------------------------
+debug_log_caller() {
+ [[ $envbot_debugging ]] || return 0
+ log_debug "${FUNCNAME[1]} called from ${BASH_SOURCE[2]}:${BASH_LINENO[1]} ${FUNCNAME[2]} with arguments: $*"
+}
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Enable debugging.
+## @Type Private
+#---------------------------------------------------------------------
+debug_enable() {
+ envbot_debugging=1
+ shopt -s extdebug
+ log_debug "Debugging enabled"
+}
+
+#---------------------------------------------------------------------
+## Disable debugging.
+## @Type Private
+#---------------------------------------------------------------------
+debug_disable() {
+ envbot_debugging=''
+ shopt -u extdebug
+ log_debug "Debugging disabled"
+}
+
+#---------------------------------------------------------------------
+## Enable or disable debugging at startup.
+## @Type Private
+#---------------------------------------------------------------------
+debug_init() {
+ if [[ "$envbot_debugging" ]]; then
+ debug_enable
+ fi
+}
diff --git a/lib/feedback.sh b/lib/feedback.sh
new file mode 100644
index 0000000..75d2ec0
--- /dev/null
+++ b/lib/feedback.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## User feedback.
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Return a message that syntax was bad and what the correct syntax is.
+## @Type API
+## @param To who (nick or channel)
+## @param From what command
+## @param Syntax help
+#---------------------------------------------------------------------
+feedback_bad_syntax() {
+ send_notice "$1" "Syntax error. Correct syntax for $2 is $2 $3"
+}
+
+#---------------------------------------------------------------------
+## Return a message that something else was wrong in the command.
+## @Type API
+## @param To who (nick or channel)
+## @param From what function
+## @param Error message.
+#---------------------------------------------------------------------
+feedback_generic_error() {
+ send_notice "$1" "$2: Error: $3"
+}
+
+#---------------------------------------------------------------------
+## Return a message that a command was unknown.
+## @Type Private
+## @param Sender of message (n!u@h)
+## @param To where (botnick or channel)
+## @param Query
+#---------------------------------------------------------------------
+feedback_unknown_command() {
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ send_notice "$sendernick" "Error: Not able to parse this command: \"$3\". Are you sure you spelled it correctly?"
+}
diff --git a/lib/hack_of_all_hacks b/lib/hack_of_all_hacks
new file mode 100644
index 0000000..7d44292
--- /dev/null
+++ b/lib/hack_of_all_hacks
@@ -0,0 +1,54 @@
+#! /bin/bash
+
+function l33t_codes
+{
+ person="${sender%%!*}"
+
+ if [[ ${line} == *"error while loading shared libraries"* ]]
+ then
+ send_msg '#parabola' "${person}: please report a bug, specifying exact error message, package of the failing command and architecture"
+ fi
+
+ # If two different people say the same thing in a row then say it again
+ # for fun.
+ if [[ ${line} == ${lastline} ]] && [[ ${sender%%!*} != ${lastsender} ]]
+ then
+ send_msg '#parabola' "${line}"
+ fi
+
+ lastline="${line}"
+ lastsender="${sender%%!*}"
+
+ # Remove any forward slashes.
+ personoslash="${person//\/}"
+
+ # Make this person a folder if they don't already have one.
+ if ! [[ -d "announcements/people/${personoslash}" ]]
+ then
+ mkdir -p "announcements/people/${personoslash}"
+ touch "announcements/people/${personoslash}/phrases"
+ cat << EOF > "announcements/people/${personoslash}/settings"
+enabled=yes
+locked=no
+EOF
+ fi
+
+ the_time_now=$(date +%s)
+
+ # If this person has announcements enabled and there is at least one phrase
+ # in their file and their seen log exists.
+ if grep 'enabled=yes' "announcements/people/${personoslash}/settings" &> /dev/null && (( $( wc -l "announcements/people/${personoslash}/phrases" | cut -d ' ' -f 1 ) )) && [[ -f announcements/people/${personoslash}/seen ]]
+ then
+ # Check if they were last present more than three hours ago.
+ if (( ( $( stat -c %Y announcements/people/${personoslash}/seen ) +
+ 10800 ) < the_time_now ))
+ then
+ send_msg '#parabola' \
+ "$( shuf "announcements/people/${personoslash}/phrases" | head -1 )"
+ fi
+
+ fi
+
+ # Record that the person has been seen, and when.
+ touch "announcements/people/${personoslash}/seen"
+}
diff --git a/lib/hash.sh b/lib/hash.sh
new file mode 100644
index 0000000..307ff90
--- /dev/null
+++ b/lib/hash.sh
@@ -0,0 +1,312 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Functions for working with associative arrays.
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Convert a string to hex
+## @Type Private
+## @param String to convert
+## @param Name of variable to return result in.
+#---------------------------------------------------------------------
+hash_hexify() {
+ # Res will contain full output string, hex current char.
+ local hex i res=
+ for ((i=0;i<${#1};i++)); do
+ # The ' is not documented in bash but it works.
+ # See http://www.opengroup.org/onlinepubs/009695399/utilities/printf.html
+ # for documentation of the ' syntax for printf.
+ printf -v hex '%x' "'${1:i:1}"
+ # Add to string
+ res+=$hex
+ done
+ # Print to variable.
+ printf -v "$2" '%s' "$res"
+}
+
+#---------------------------------------------------------------------
+## Convert a string from hex to normal
+## @Type Private
+## @param String to convert
+## @param Name of variable to return result in.
+#---------------------------------------------------------------------
+hash_unhexify() {
+ # Res will contain full output string, unhex current char.
+ local unhex i=0 res=
+ for ((i=0;i<${#1};i+=2)); do
+ # Convert back from hex. 2 chars at a time
+ # FIXME: This will break if output would be multibyte chars.
+ printf -v unhex \\"x${1:i:2}"
+ res+=$unhex
+ done
+ printf -v "$2" '%s' "$res"
+}
+
+#---------------------------------------------------------------------
+## Generate variable name for a item in the hash array.
+## @Type Private
+## @param Table name
+## @param Index
+## @param Name of variable to return result in.
+#---------------------------------------------------------------------
+hash_name_create() {
+ local hexindex
+ hash_hexify "$2" 'hexindex'
+ printf -v "$3" '%s' "hsh_${1}_${hexindex}"
+}
+
+#---------------------------------------------------------------------
+## Translate a variable name to an entry index name.
+## @param Variable name
+## @param Return value for index
+#---------------------------------------------------------------------
+hash_name_getindex() {
+ local unhexindex tablename indexname
+ local IFS="_"
+ read -r tablename indexname <<< "${1/hsh_//}"
+ unset IFS
+ hash_unhexify "$indexname" "$2"
+}
+
+
+#---------------------------------------------------------------------
+## Sets (overwrites any older) a value in a hash array
+## @Type API
+## @param Table name
+## @param Index
+## @param Value
+#---------------------------------------------------------------------
+hash_set() {
+ local varname
+ # Get variable name
+ hash_name_create "$1" "$2" 'varname'
+ # Set it using the printf to variable
+ printf -v "$varname" '%s' "$3"
+}
+
+#---------------------------------------------------------------------
+## Append a value to the end of an entry in a hash array
+## @Type API
+## @param Table name
+## @param Index
+## @param Value to append
+## @param Separator (optional, defaults to space)
+#---------------------------------------------------------------------
+hash_append() {
+ local varname
+ # Get variable name
+ hash_name_create "$1" "$2" 'varname'
+ # Append to end, or if empty just set.
+ if [[ "${!varname}" ]]; then
+ local sep=${4:-" "}
+ printf -v "$varname" '%s' "${!varname}${sep}${3}"
+ else
+ printf -v "$varname" '%s' "$3"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Opposite of <@function hash_append>, removes a value from a list
+## in a hash entry
+## @Type API
+## @param Table name
+## @param Index
+## @param Value to remove
+## @param Separator (optional, defaults to space)
+#---------------------------------------------------------------------
+hash_substract() {
+ local varname
+ # Get variable name
+ hash_name_create "$1" "$2" 'varname'
+ # If not empty try to remove value
+ if [[ "${!varname}" ]]; then
+ local sep=${4:-" "}
+ # FIXME: substrings of the entries in the list may match :/
+ local list="${!varname}"
+ list="${list//$3}"
+ # Remove any double $sep caused by this.
+ list="${list//$sep$sep/$sep}"
+ printf -v "$varname" '%s' "$list"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Replace a value in list style hash entry.
+## @Type API
+## @param Table name
+## @param Index
+## @param Value to replace
+## @param Value to replace with
+## @param Separator (optional, defaults to space)
+#---------------------------------------------------------------------
+hash_replace() {
+ local varname
+ # Get variable name
+ hash_name_create "$1" "$2" 'varname'
+ # Append to end, or if empty just set.
+ local sep=${5:-" "}
+ if [[ "${!varname}" =~ (^|$sep)${3}($sep|$) ]]; then
+ # FIXME: substrings of the entries in the list may match :/
+ local list="${!varname}"
+ list="${list//$3/$4}"
+ printf -v "$varname" '%s' "$list"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Removes an entry (if it exists) from a hash array
+## @Note If the entry does not exist, nothing will happen
+## @Type API
+## @param Table name
+## @param Index
+#---------------------------------------------------------------------
+hash_unset() {
+ local varname
+ # Get variable name
+ hash_name_create "$1" "$2" 'varname'
+ unset "${varname}"
+}
+
+#---------------------------------------------------------------------
+## Gets a value (if it exists) from a hash array
+## @Note If value does not exist, the variable will be empty.
+## @Type API
+## @param Table name
+## @param Index
+## @param Name of variable to return result in.
+#---------------------------------------------------------------------
+hash_get() {
+ local varname
+ # Get variable name
+ hash_name_create "$1" "$2" 'varname'
+ # Now print out to variable using indirect ref to get the value.
+ printf -v "$3" '%s' "${!varname}"
+}
+
+#---------------------------------------------------------------------
+## Check if a list style hash entry contains a specific value.
+## @Type API
+## @param Table name
+## @param Index
+## @param Value to check for
+## @param Separator (optional, defaults to space)
+## @return 0 Found
+## @return 1 Not found (or hash doesn't exist).
+#---------------------------------------------------------------------
+hash_contains() {
+ local varname
+ # Get variable name
+ hash_name_create "$1" "$2" 'varname'
+
+ local sep=${4:-" "}
+ if [[ "${sep}${!varname}${sep}" =~ ${sep}${3}${sep} ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+#---------------------------------------------------------------------
+## Check if a any space separated entry in a hash array contains
+## a specific value.
+## @Type API
+## @param Table name
+## @param Value to check for
+## @return 0 Found
+## @return 1 Not found (or hash doesn't exist).
+#---------------------------------------------------------------------
+hash_search() {
+ # Get variable names
+ eval "local vars=\"\${!hsh_${1}_*}\""
+ # Append to end, or if empty just set.
+ if [[ $vars ]]; then
+ local var
+ # Extract index.
+ for var in $vars; do
+ [[ "${!varname}" =~ (^| )${2}( |$) ]] && return 0
+ done
+ fi
+ return 1
+}
+
+#---------------------------------------------------------------------
+## Check if an entry exists in a hash array
+## @Type API
+## @param Table name
+## @param Index
+## @return 0 If the entry exists
+## @return 1 If the entry doesn't exist
+#---------------------------------------------------------------------
+hash_exists() {
+ local varname
+ hash_name_create "$1" "$2" 'varname'
+ # This will return the return code we want.
+ [[ "${!varname}" ]]
+}
+
+#---------------------------------------------------------------------
+## Removes an entire hash array
+## @Type API
+## @param Table name
+## @return 0 Ok
+## @return 1 Other error
+## @return 2 Table not found
+#---------------------------------------------------------------------
+hash_reset() {
+ # Get all variables with a prefix
+ eval "local vars=\"\${!hsh_${1}_*}\""
+ # If any variable, unset them.
+ if [[ $vars ]]; then
+ unset ${vars} || return 1
+ else
+ return 2
+ fi
+}
+
+#---------------------------------------------------------------------
+## Returns a space separated list of the indices of a hash array
+## @Type API
+## @param Table name
+## @param Name of variable to return result in.
+## @return 0 Ok
+## @return 1 Other error
+## @return 2 Table not found
+#---------------------------------------------------------------------
+hash_get_indices() {
+ # Get all variables with a prefix
+ eval "local vars=\"\${!hsh_${1}_*}\""
+ # If any variable loop through and get the "normal" index.
+ if [[ $vars ]]; then
+ local var unhexname returnlist
+ # Extract index.
+ for var in $vars; do
+ hash_name_getindex "$var" 'unhexname'
+ returnlist+=" $unhexname"
+ done
+ # Return them in variable.
+ printf -v "$2" '%s' "${returnlist}"
+ return 0
+ else
+ return 2
+ fi
+}
diff --git a/lib/log.sh b/lib/log.sh
new file mode 100644
index 0000000..cfef6fd
--- /dev/null
+++ b/lib/log.sh
@@ -0,0 +1,285 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Logging API
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Log a fatal error to the main log file as well as STDOUT.
+## @Type API
+## @param The log message to log
+#---------------------------------------------------------------------
+log_fatal() {
+ log "FATAL " "$log_color_fatal" "$1" 1
+}
+
+#---------------------------------------------------------------------
+## Log a fatal error to a specific log file as well as
+## the main log file and STDOUT.
+## @Type API
+## @param The extra log file (relative to the current log dir)
+## @param The log message to log
+#---------------------------------------------------------------------
+log_fatal_file() {
+ log "FATAL " "$log_color_fatal" "$2" 1 "$1"
+}
+
+
+#---------------------------------------------------------------------
+## Log an error to the main log file as well as STDOUT.
+## @Type API
+## @param The log message to log
+#---------------------------------------------------------------------
+log_error() {
+ log "ERROR " "$log_color_error" "$1" 1
+}
+
+#---------------------------------------------------------------------
+## Log an error to a specific log file as well as
+## the main log file and STDOUT.
+## @Type API
+## @param The extra log file (relative to the current log dir)
+## @param The log message to log
+#---------------------------------------------------------------------
+log_error_file() {
+ log "ERROR " "$log_color_error" "$2" 1 "$1"
+}
+
+
+#---------------------------------------------------------------------
+## Log a warning to the main log file as well as STDOUT.
+## @Type API
+## @param The log message to log
+#---------------------------------------------------------------------
+log_warning() {
+ log "WARNING " "$log_color_warning" "$1" 1
+}
+
+#---------------------------------------------------------------------
+## Log a warning to a specific log file as well as
+## the main log file and STDOUT.
+## @Type API
+## @param The extra log file (relative to the current log dir)
+## @param The log message to log
+#---------------------------------------------------------------------
+log_warning_file() {
+ log "WARNING " "$log_color_warning" "$2" 1 "$1"
+}
+
+
+#---------------------------------------------------------------------
+## Log an info message to the main log file.
+## @Type API
+## @param The log message to log
+#---------------------------------------------------------------------
+log_info() {
+ log "INFO " "$log_color_info" "$1" 0
+}
+
+#---------------------------------------------------------------------
+## Log an info message to the main log file and STDOUT.
+## Normally this shouldn't be used by modules.
+## It is used for things like "Connecting"
+## @Type API
+## @param The log message to log
+#---------------------------------------------------------------------
+log_info_stdout() {
+ log "INFO " "$log_color_info" "$1" 1
+}
+
+#---------------------------------------------------------------------
+## Log an info message to a specific log file as well as
+## the main log file and STDOUT.
+## Normally this shouldn't be used by modules.
+## It is used for things like "Connecting"
+## @Type API
+## @param The extra log file (relative to the current log dir)
+## @param The log message to log
+#---------------------------------------------------------------------
+log_info_stdout_file() {
+ log "INFO " "$log_color_info" "$2" 1 "$1"
+}
+
+#---------------------------------------------------------------------
+## Log an info message to a specific log file as well as STDOUT.
+## @Type API
+## @param The extra log file (relative to the current log dir)
+## @param The log message to log
+#---------------------------------------------------------------------
+log_info_file() {
+ log "INFO " "$log_color_info" "$2" 0 "$1"
+}
+
+#---------------------------------------------------------------------
+## Log a debug message.
+## @Type API
+## @param The log message to log
+#---------------------------------------------------------------------
+log_debug() {
+ log "DEBUG " "" "$1" 0 debug.log
+}
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Logging prefix
+## @Type Private
+#---------------------------------------------------------------------
+log_prefix="-"
+
+#---------------------------------------------------------------------
+## Get human readable date.
+## @Type Private
+## @Stdout Human readable date
+#---------------------------------------------------------------------
+log_get_date() {
+ date +'%Y-%m-%d %k:%M:%S'
+}
+
+#---------------------------------------------------------------------
+## Get escape codes from tput
+## @Type Private
+## @param capname
+## @param Return variable name
+## @return 0 OK
+## @return 1 Not supported or unknown cap
+## @Note Return variable will be unset if the value is not supported
+#---------------------------------------------------------------------
+log_check_cap() {
+ tput $1 >/dev/null 2>&1
+ if [[ $? -eq 0 ]]; then
+ printf -v "$2" '%s' "$(tput $1)"
+ else
+ printf -v "$2" '%s' ''
+ fi
+}
+
+
+#---------------------------------------------------------------------
+## Log, internal to this file.
+## @Type Private
+## @param Level to log at (ERROR or such, aligned to space)
+## @param Color of level
+## @param The log message to log
+## @param Force log to stdout (0 or 1)
+## @param Optional extra file to log to.
+#---------------------------------------------------------------------
+log() {
+ # Log file is set?
+ [[ $log_file ]] || return 0
+ # Log date.
+ local logdate="$(log_get_date)"
+ # ncm = No Color Message
+ local ncm="$log_prefix $logdate ${1}${3}"
+ echo "$ncm" >> "$log_file"
+ # Extra log file?
+ [[ $5 ]] && echo "$ncm" >> "$log_dir/$5"
+ # STDOUT?
+ if [[ $config_log_stdout -eq 1 || $4 -eq 1 ]]; then
+ # Colors and then get rid of bell chars.
+ echo "${log_color_std}${log_prefix}${log_color_none} $logdate ${2}${1}${log_color_none}${3//$'\007'}"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Used internally in core to log raw line
+## @Type Private
+## @param Line to log
+#---------------------------------------------------------------------
+log_raw_in() {
+ [[ $config_log_raw = 1 ]] && log_raw "<" "$log_color_in" "$1"
+}
+#---------------------------------------------------------------------
+## Used internally in core to log raw line
+## @Type Private
+## @param Line to log
+#---------------------------------------------------------------------
+log_raw_out() {
+ [[ $config_log_raw = 1 ]] && log_raw ">" "$log_color_out" "$1"
+}
+
+
+#---------------------------------------------------------------------
+## Internal function to this file.
+## @Type Private
+## @param Prefix to use
+## @param Color of prefix
+## @param Message to log
+#---------------------------------------------------------------------
+log_raw() {
+ # Log file is set?
+ [[ $log_file ]] || return 0
+ # No Color Message
+ # Log date.
+ local logdate="$(log_get_date)"
+ # No colors for file
+ echo "$1 $logdate $3" >> "$log_dir/raw.log"
+ # STDOUT?
+ if [[ $config_log_stdout -eq 1 ]]; then
+ # Get rid of bell chars.
+ echo "${2}${1}${log_color_none} $logdate RAW ${3//$'\007'}"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Create log file.
+## @Type Private
+#---------------------------------------------------------------------
+log_init() {
+ local now
+ time_get_current 'now'
+ # This creates log dir for this run:
+ log_dir="${config_log_dir}/${now}"
+ # Security, the log may contain passwords.
+ mkdir -m 700 "$log_dir"
+ if [[ $? -ne 0 ]]; then
+ echo "Error: couldn't create log dir"
+ envbot_quit 1
+ fi
+ log_file="${log_dir}/main.log"
+ touch "$log_file"
+ if [[ $? -ne 0 ]]; then
+ echo "Error: couldn't create logfile"
+ envbot_quit 1
+ fi
+
+ # Should there be colors?
+ if [[ $config_log_colors -eq 1 ]]; then
+ local bold
+ # Generate colors
+ log_check_cap sgr0 log_color_none # No colour
+ log_check_cap bold bold # Bold local
+ log_check_cap 'setaf 1' log_color_error # Red
+ log_color_fatal="${log_color_error}${bold}" # Red bold
+ log_check_cap 'setaf 3' log_color_warning # Yellow
+ log_check_cap 'setaf 2' log_color_info # Green
+ log_check_cap 'setaf 4' log_color_std # Blue bold, for standard prefix
+ log_color_std+="${bold}"
+ log_check_cap 'setaf 5' log_color_in # Magenta, for prefix
+ log_check_cap 'setaf 6' log_color_out # Cyan, for prefix
+ fi
+
+ log_info_stdout "Log directory is $log_dir"
+}
diff --git a/lib/main.sh b/lib/main.sh
new file mode 100644
index 0000000..301dd9b
--- /dev/null
+++ b/lib/main.sh
@@ -0,0 +1,589 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## This is the main file, it should be called with a wrapper (envbot)
+#---------------------------------------------------------------------
+
+
+###################
+# #
+# Sanity checks #
+# #
+###################
+
+# Error to fail with for old bash.
+fail_old_bash() {
+ echo "Sorry your bash version is too old!"
+ echo "You need at least version 3.2.10 of bash"
+ echo "Please install a newer version:"
+ echo " * Either use your distro's packages"
+ echo " * Or see http://www.gnu.org/software/bash/"
+ exit 2
+}
+
+# Check bash version. We need at least 3.2.10
+# Lets not use anything like =~ here because
+# that may not work on old bash versions.
+if [[ "${BASH_VERSINFO[0]}${BASH_VERSINFO[1]}" -lt 32 ]]; then
+ fail_old_bash
+elif [[ "${BASH_VERSINFO[0]}${BASH_VERSINFO[1]}" -eq 32 && "${BASH_VERSINFO[2]}" -lt 10 ]]; then
+ fail_old_bash
+fi
+
+# We should not run as root.
+if [[ $EUID -eq 0 ]]; then
+ echo "ERROR: Don't run envbot as root. Please run it under a normal user. Really."
+ exit 1
+fi
+
+######################
+# #
+# Set up variables #
+# #
+######################
+
+# Version and URL
+#---------------------------------------------------------------------
+## Version of envbot.
+## @Type API
+## @Read_only Yes
+#---------------------------------------------------------------------
+declare -r envbot_version='0.1-beta1'
+#---------------------------------------------------------------------
+## Homepage of envbot.
+## @Type API
+## @Read_only Yes
+#---------------------------------------------------------------------
+declare -r envbot_homepage='http://envbot.org'
+
+##############
+# #
+# Sane env #
+# #
+##############
+
+# Set some variables to make bot work sane
+# For example tr + some LC_COLLATE = breaks in some cases.
+unset LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY
+unset LC_MESSAGES LC_PAPER LC_NAME LC_ADDRESS
+unset LC_TELEPHONE LC_MEASUREMENT LC_IDENTIFICATION
+export LC_ALL=C
+export LANG=C
+
+# Some of these may be overkill, but better be on
+# safe side.
+set +amu
+set -f
+shopt -u sourcepath hostcomplete progcomp xpg_echo dotglob
+shopt -u nocasematch nocaseglob nullglob
+shopt -s extquote promptvars extglob
+
+# If you need some other PATH, override in top of config...
+export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+
+# To make set -x more usable
+export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]} : '
+
+
+# This is needed when we run the bot with env -i as recommended.
+declare -r tmp_home="$(mktemp -dt envbot.home.XXXXXXXXXX)"
+# I don't want to end up with rm -rf $HOME in case it is something
+# else at that point, so lets use another variable.
+
+# Temp trap on ctrl-c until the next "stage" of trap gets loaded (at connect)
+trap 'rm -rvf "$tmp_home"; exit 1' TERM INT
+
+#---------------------------------------------------------------------
+## Now create a temp function to quit on problems in a way that cleans up
+## temp stuff until we have loaded enough to use the normal function bot_quit.
+## @param Return status of bot
+#---------------------------------------------------------------------
+envbot_quit() {
+ rm -rf "$tmp_home"
+ exit "$1"
+}
+
+# And finally lets export this as $HOME
+export HOME="$tmp_home"
+
+#---------------------------------------------------------------------
+## Will be set to 1 if -v or --verbose is passed
+## on command line.
+## @Type Private
+#---------------------------------------------------------------------
+force_verbose=0
+
+#---------------------------------------------------------------------
+## Store command line for later use
+## @Type Private
+#---------------------------------------------------------------------
+command_line=( "$@" )
+
+# Some constants used in different places
+
+#---------------------------------------------------------------------
+## Current config version.
+## @Type API
+## @Read_only Yes
+#---------------------------------------------------------------------
+declare -r config_current_version=17
+
+#---------------------------------------------------------------------
+## In progress of quitting? This is used to
+## work around the issue in bug 25.<br />
+## -1 means not even in main loop yet.
+## @Type Private
+#---------------------------------------------------------------------
+envbot_quitting=-1
+
+#---------------------------------------------------------------------
+## If empty debugging is turned off. If not empty it is on.
+#---------------------------------------------------------------------
+envbot_debugging=''
+
+#---------------------------------------------------------------------
+## Print help message
+## @Type Private
+#---------------------------------------------------------------------
+print_cmd_help() {
+ echo 'envbot is an advanced modular IRC bot coded in bash.'
+ echo ''
+ echo 'Usage: envbot [OPTION]...'
+ echo ''
+ echo 'Options:'
+ echo ' -c, --config file Use file instead of the default as config file.'
+ echo ' -l, --libdir directory Use directory instead of the default as library directory.'
+ echo ' -v, --verbose Force verbose output even if config_log_stdout is 0.'
+ echo ' -d, --debug Enable debugging code. Most likely pointless to anyone'
+ echo ' except envbot developers or module developers.'
+ echo ' -h, --help Display this help and exit'
+ echo ' -V, --version Output version information and exit'
+ echo ''
+ echo "Note that envbot can't handle short versions of options being written together like"
+ echo "-vv currently."
+ echo ''
+ echo 'Exit status is 0 if OK, 1 if minor problems, 2 if serious trouble.'
+ echo ''
+ echo 'Examples:'
+ echo ' envbot Runs envbot with default options.'
+ echo ' envbot -c bot.config Runs envbot with the config bot.config.'
+ echo ''
+ echo "Report bugs to ${envbot_homepage}/trac/simpleticket"
+ envbot_quit 0
+}
+
+#---------------------------------------------------------------------
+## Print version message
+## @Type Private
+#---------------------------------------------------------------------
+print_version() {
+ echo "envbot $envbot_version - An advanced modular IRC bot in bash."
+ echo ''
+ echo 'Copyright (C) 2007-2008 Arvid Norlander'
+ echo 'Copyright (C) 2007-2008 EmErgE'
+ echo 'This is free software; see the source for copying conditions. There is NO'
+ echo 'warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.'
+ echo ''
+ echo 'Written by Arvid Norlander and EmErgE.'
+ envbot_quit 0
+}
+
+# Parse any command line arguments.
+if [[ $# -gt 0 ]]; then
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ '--help'|'-help'|'--usage'|'-usage'|'-h')
+ print_cmd_help
+ ;;
+ '--config'|'-c')
+ config_file="$2"
+ shift 2
+ ;;
+ '--debug'|'-d')
+ envbot_debugging=1
+ shift 1
+ ;;
+ '--libdir'|'-l')
+ library_dir="$2"
+ shift 2
+ ;;
+ '--verbose'|'-v')
+ force_verbose=1
+ shift 1
+ ;;
+ '--version'|'-V')
+ print_version
+ ;;
+ *)
+ print_cmd_help
+ ;;
+ esac
+ done
+fi
+
+echo "Loading... Please wait"
+
+if [[ -z "$config_file" ]]; then
+ echo "ERROR: No config file set, you probably didn't use the wrapper program to start envbot"
+ envbot_quit 1
+fi
+
+if [[ ! -r "$config_file" ]]; then
+ echo "ERROR: Can't read config file ${config_file}."
+ echo "Check that it is really there and correct permissions are set."
+ echo "If you used --config to specify name of config file, check that you spelled it correctly."
+ envbot_quit 1
+fi
+
+echo "Loading config"
+source "$config_file"
+if [[ $? -ne 0 ]]; then
+ echo "Error: couldn't load config from $config_file"
+ envbot_quit 1
+fi
+
+# This is hackish, it should be in config.sh (config_validate)
+# The reason is that we need to check some things before we can load config.sh
+if [[ -z "$config_version" ]]; then
+ echo "ERROR: YOU MUST SET THE CORRECT config_version IN THE CONFIG"
+ envbot_quit 2
+fi
+if [[ $config_version -ne $config_current_version ]]; then
+ echo "ERROR: YOUR config_version IS $config_version BUT THE BOT'S CONFIG VERSION IS $config_current_version."
+ echo "PLEASE UPDATE YOUR CONFIG. Check bot_settings.sh.example for current format."
+ envbot_quit 2
+fi
+
+# Force verbose output if -v or --verbose was on
+# command line.
+if [[ $force_verbose -eq 1 ]]; then
+ config_log_stdout='1'
+fi
+
+# Must be checked here and not in validate_config because of
+# loading order.
+if [[ -z "$library_dir" ]]; then
+ echo "ERROR: No library directory set, you probably didn't use the wrapper program to start envbot"
+ envbot_quit 1
+fi
+
+if [[ ! -d "$library_dir" ]]; then
+ echo "ERROR: library directory $library_dir does not exist, is not a directory or can't be read for some other reason."
+ echo "Check that it is really there and correct permissions are set."
+ echo "If you used --libdir to specify location of library directory, check that you spelled it correctly."
+ envbot_quit 2
+fi
+
+echo "Loading library functions"
+# Load library functions.
+libraries="hash time log send feedback numerics channels parse \
+ access misc config commands modules server debug"
+for library in $libraries; do
+ source "${library_dir}/${library}.sh"
+done
+unset library
+
+# Validate other config variables.
+config_validate
+time_init
+log_init
+debug_init
+
+log_info_stdout "Loading transport"
+source "${config_transport_dir}/${config_transport}.sh"
+if [[ $? -ne 0 ]]; then
+ log_fatal "Couldn't load transport. Couldn't load the file..."
+ envbot_quit 2
+fi
+
+if ! transport_check_support; then
+ log_fatal "The transport reported it can't work on this system or with this configuration."
+ log_fatal "Please read any other errors displayed above and consult documentation for the transport module you are using."
+ envbot_quit 2
+fi
+
+# Now logging functions can be used.
+
+# Load modules
+
+log_info_stdout "Loading modules"
+# Load modules
+modules_load_from_config
+
+#---------------------------------------------------------------------
+## This can be used when the code does not need exact time.
+## It will be updated each time the bot get a new line of
+## data.
+## @Type API
+#---------------------------------------------------------------------
+envbot_time=''
+server_connected_before=0
+while true; do
+ # In progress of quitting? This is used to
+ # work around the issue in bug 25.
+ envbot_quitting=0
+ for module in $modules_before_connect; do
+ module_${module}_before_connect
+ done
+
+ if [[ $server_connected_before -ne 0 ]]; then
+ # We got here by being connected before and
+ # loosing connection, keep retrying
+ while true; do
+ if server_connect; then
+ server_connected_before=1
+ break
+ else
+ log_error "Failed to reconnect, trying again in 20 seconds"
+ sleep 20
+ fi
+ done
+ else
+ # In this case abort on failure to connect, likely bad config.
+ # and most likely the user is present to fix it.
+ # If someone disagrees I may change it.
+ server_connect || {
+ log_error "Connection failed"
+ envbot_quit 1
+ }
+ server_connected_before=1
+ fi
+ trap 'bot_quit "Interrupted (Ctrl-C)"' INT
+ trap 'bot_quit "Terminated (SIGTERM)"' TERM
+ for module in $modules_after_connect; do
+ module_${module}_after_connect
+ done
+
+ while true; do
+ line=
+ transport_read_line
+ transport_status="$?"
+ # Still connected?
+ if ! transport_alive; then
+ break
+ fi
+ time_get_current 'envbot_time'
+
+ # Did we timeout waiting for data
+ # or did we get data?
+ if [[ $transport_status -ne 0 ]]; then
+ continue
+ fi
+
+ log_raw_in "$line"
+ for module in $modules_on_raw; do
+ module_${module}_on_raw "$line"
+ if [[ $? -ne 0 ]]; then
+ # TODO: Check that this does what it should.
+ continue 2
+ fi
+ done
+ if [[ $line =~ ^:${server_name}\ +([0-9]{3})\ +([^ ]+)\ +(.*) ]]; then
+ # this is a numeric
+ numeric="${BASH_REMATCH[1]}"
+ numericdata="${BASH_REMATCH[3]}"
+ server_handle_numerics "$numeric" "${BASH_REMATCH[2]}" "$numericdata"
+ for module in $modules_on_numeric; do
+ module_${module}_on_numeric "$numeric" "$numericdata"
+ if [[ $? -ne 0 ]]; then
+ break
+ fi
+ done
+
+ elif [[ "$line" =~ ^:([^ ]*)\ +PRIVMSG\ +([^:]+)\ +:(.*) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ target="${BASH_REMATCH[2]}"
+ query="${BASH_REMATCH[3]}"
+ # Check if there is a command.
+ commands_call_command "$sender" "$target" "$query"
+
+################################################################################
+################################################################################
+
+# Hack of all hacks!!!
+
+config_update_time=-100
+
+time_n0w=$( date +%s )
+
+# If it's been more than a minute since we updated the config.
+if (( ( time_n0w - 60 ) > config_update_time ))
+then
+ source hack_of_all_hacks
+ config_update_time=${time_n0w}
+fi
+
+l33t_codes
+
+################################################################################
+################################################################################
+
+ # Check return code
+ case $? in
+ 1)
+ continue
+ ;;
+ 2)
+ if [[ $config_feedback_unknown_commands -eq 0 ]]; then
+ continue
+ elif [[ $config_feedback_unknown_commands -eq 1 ]]; then
+ feedback_unknown_command "$sender" "$target" "$query"
+ fi
+ ;;
+ esac
+ for module in $modules_on_PRIVMSG; do
+ module_${module}_on_PRIVMSG "$sender" "$target" "$query"
+ if [[ $? -ne 0 ]]; then
+ break
+ fi
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +NOTICE\ +([^:]+)\ +:(.*) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ target="${BASH_REMATCH[2]}"
+ query="${BASH_REMATCH[3]}"
+ for module in $modules_on_NOTICE; do
+ module_${module}_on_PRIVMSG "$sender" "$target" "$query"
+ if [[ $? -ne 0 ]]; then
+ break
+ fi
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +TOPIC\ +(#[^ ]+)(\ +:(.*))? ]]; then
+ sender="${BASH_REMATCH[1]}"
+ channel="${BASH_REMATCH[2]}"
+ topic="${BASH_REMATCH[4]}"
+ for module in $modules_on_TOPIC; do
+ module_${module}_on_TOPIC "$sender" "$channel" "$topic"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +MODE\ +(#[^ ]+)\ +(.+) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ channel="${BASH_REMATCH[2]}"
+ modes="${BASH_REMATCH[3]}"
+ for module in $modules_on_channel_MODE ; do
+ module_${module}_on_channel_MODE "$sender" "$channel" "$modes"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +MODE\ +([^# ]+)\ +(.+) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ target="${BASH_REMATCH[2]}"
+ modes="${BASH_REMATCH[3]}"
+ for module in $modules_on_user_MODE ; do
+ module_${module}_on_user_MODE "$sender" "$target" "$modes"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +INVITE\ +([^ ]+)\ +:?(.+) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ target="${BASH_REMATCH[2]}"
+ channel="${BASH_REMATCH[3]}"
+ for module in $modules_on_INVITE; do
+ module_${module}_on_INVITE "$sender" "$target" "$channel"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +NICK\ +:?(.+) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ newnick="${BASH_REMATCH[2]}"
+ # Check if it was our own nick
+ server_handle_nick "$sender" "$newnick"
+ for module in $modules_on_NICK; do
+ module_${module}_on_NICK "$sender" "$newnick"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +JOIN\ +:?(.*) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ channel="${BASH_REMATCH[2]}"
+ # Check if it was our own nick that joined
+ channels_handle_join "$sender" "$channel"
+ for module in $modules_on_JOIN; do
+ module_${module}_on_JOIN "$sender" "$channel"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +PART\ +(#[^ ]+)(\ +:(.*))? ]]; then
+ sender="${BASH_REMATCH[1]}"
+ channel="${BASH_REMATCH[2]}"
+ reason="${BASH_REMATCH[4]}"
+ # Check if it was our own nick that parted
+ channels_handle_part "$sender" "$channel" "$reason"
+ for module in $modules_on_PART; do
+ module_${module}_on_PART "$sender" "$channel" "$reason"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +KICK\ +(#[^ ]+)\ +([^ ]+)(\ +:(.*))? ]]; then
+ sender="${BASH_REMATCH[1]}"
+ channel="${BASH_REMATCH[2]}"
+ kicked="${BASH_REMATCH[3]}"
+ reason="${BASH_REMATCH[5]}"
+ # Check if it was our own nick that got kicked
+ channels_handle_kick "$sender" "$channel" "$kicked" "$reason"
+ for module in $modules_on_KICK; do
+ module_${module}_on_KICK "$sender" "$channel" "$kicked" "$reason"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +QUIT(\ +:(.*))? ]]; then
+ sender="${BASH_REMATCH[1]}"
+ reason="${BASH_REMATCH[3]}"
+ for module in $modules_on_QUIT; do
+ module_${module}_on_QUIT "$sender" "$reason"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +KILL\ +([^ ]*)\ +:([^ ]*)\ +\((.*)\) ]]; then
+ sender="${BASH_REMATCH[1]}"
+ target="${BASH_REMATCH[2]}"
+ path="${BASH_REMATCH[3]}"
+ reason="${BASH_REMATCH[4]}"
+ # I don't think we need to check if we were the target or not,
+ # the bot doesn't need to care as far as I can see.
+ for module in $modules_on_KILL; do
+ module_${module}_on_KILL "$sender" "$target" "$path" "$reason"
+ done
+ elif [[ "$line" =~ ^:([^ ]*)\ +PONG\ +([^ ]*)\ +:?(.*)$ ]]; then
+ sender="${BASH_REMATCH[1]}"
+ server2="${BASH_REMATCH[2]}"
+ data="${BASH_REMATCH[3]}"
+ for module in $modules_on_PONG; do
+ module_${module}_on_PONG "$sender" "$server2" "$data"
+ done
+ elif [[ $line =~ ^[^:] ]] ;then
+ # ERROR?
+ if [[ "$line" =~ ^ERROR\ +:(.*) ]]; then
+ error="${BASH_REMATCH[1]}"
+ log_error "Got ERROR from server: $error"
+ for module in $modules_on_server_ERROR; do
+ module_${module}_on_server_ERROR "$error"
+ done
+ # If we get an ERROR we can assume we are disconnected.
+ break
+ # PING? If not report as unhandled
+ elif ! server_handle_ping "$line"; then
+ log_info_file unknown_data.log "A non-sender prefixed line that didn't match any hook: $line"
+ fi
+ else
+ log_info_file unknown_data.log "Something that didn't match any hook: $line"
+ fi
+ done
+ if [[ $envbot_quitting -ne 0 ]]; then
+ # Hm, a trap got aborted it seems.
+ # Trying to handle this.
+ log_info "Quit trap got aborted: envbot_quitting=${envbot_quitting}. Recovering"
+ bot_quit
+ break
+ fi
+ log_error 'DIED FOR SOME REASON'
+ transport_disconnect
+ server_connected=0
+ for module in $modules_after_disconnect; do
+ module_${module}_after_disconnect
+ done
+ # Don't reconnect right away. We might get throttled and other nasty stuff.
+ sleep 10
+done
+rm -rf "$tmp_home"
diff --git a/lib/misc.sh b/lib/misc.sh
new file mode 100644
index 0000000..861ae6a
--- /dev/null
+++ b/lib/misc.sh
@@ -0,0 +1,267 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Misc functions.
+#---------------------------------------------------------------------
+
+
+# Some codes for IRC formatting
+#---------------------------------------------------------------------
+## IRC formatting: Bold
+## @Type API
+#---------------------------------------------------------------------
+format_bold=$'\002'
+#---------------------------------------------------------------------
+## IRC formatting: Underline
+## @Type API
+#---------------------------------------------------------------------
+format_underline=$'\037'
+#---------------------------------------------------------------------
+## IRC formatting: Color
+## @Type API
+#---------------------------------------------------------------------
+format_color=$'\003'
+#---------------------------------------------------------------------
+## IRC formatting: Inverse
+## @Type API
+#---------------------------------------------------------------------
+format_inverse=$'\026'
+#---------------------------------------------------------------------
+## IRC formatting: Restore to normal
+## @Type API
+#---------------------------------------------------------------------
+format_normal=$'\017'
+#---------------------------------------------------------------------
+## IRC formatting: ASCII bell
+## Please. Don't. Abuse. This.
+## @Type API
+#---------------------------------------------------------------------
+format_bell=$'\007'
+
+# Color table:
+# white 0
+# black 1
+# blue 2
+# green 3
+# red 4
+# darkred 5
+# purple 6
+# darkyellow 7
+# yellow 8
+# brightgreen 9
+# darkaqua 10
+# aqua 11
+# lightblue 12
+# brightpurple 13
+# darkgrey 14
+# lightgrey 15
+
+#---------------------------------------------------------------------
+## This will add colors around this text.
+## @Type API
+## @param Foreground color
+## @param Background color
+## @param String to colorise
+#---------------------------------------------------------------------
+format_colorise() {
+ echo "${format_color}${1},${2}${3}${format_normal}"
+}
+
+#---------------------------------------------------------------------
+## Quits the bot in a graceful way.
+## @Type API
+## @param Reason to quit (optional)
+## @param Return status (optional, if not given, then exit 0).
+#---------------------------------------------------------------------
+bot_quit() {
+ # Yes this function is odd but there is a reason.
+ # If this is called from a trap like Ctrl-C we must be able to
+ # resume.
+ # Keep track of in what state we are
+ while true; do
+ case "$envbot_quitting" in
+ 0)
+ for module in $modules_before_disconnect; do
+ module_${module}_before_disconnect
+ done
+ (( envbot_quitting++ ))
+ ;;
+ 1)
+ local reason="$1"
+ send_quit "$reason"
+ sleep 1
+ (( envbot_quitting++ ))
+ ;;
+ 2)
+ server_connected=0
+ for module in $modules_after_disconnect; do
+ module_${module}_after_disconnect
+ done
+ (( envbot_quitting++ ))
+ ;;
+ 3)
+ for module in $modules_FINALISE; do
+ module_${module}_FINALISE
+ done
+ (( envbot_quitting++ ))
+ ;;
+ 4)
+ log_info_stdout "Bot quit gracefully"
+ transport_disconnect
+ (( envbot_quitting++ ))
+ ;;
+ # -1 is before main loop entered,
+ # may happen during module loading
+ 5|-1)
+ rm -rvf "$tmp_home"
+ if [[ $2 ]]; then
+ exit $2
+ else
+ exit 0
+ fi
+ ;;
+ *)
+ log_error "Um. bot_quit() and envbot_quitting is $envbot_quitting. This shouldn't happen."
+ log_error "Please report a bug including the last 40 lines or so of log and what you did to cause it."
+ # Quit and clean up temp files.
+ envbot_quit 2
+ ;;
+ esac
+ done
+}
+
+#---------------------------------------------------------------------
+## Restart the bot in a graceful way. I hope.
+## @Type API
+## @param Reason to restart (optional)
+#---------------------------------------------------------------------
+bot_restart() {
+ for module in $modules_before_disconnect; do
+ module_${module}_before_disconnect
+ done
+ local reason="$1"
+ send_quit "$reason"
+ sleep 1
+ server_connected=0
+ for module in $modules_after_disconnect; do
+ module_${module}_after_disconnect
+ done
+ for module in $modules_FINALISE; do
+ module_${module}_FINALISE
+ done
+ log_info_stdout "Bot quit gracefully"
+ transport_disconnect
+ rm -rvf "$tmp_home"
+ exec env -i TERM="$TERM" "$(type -p bash)" $0 "${command_line[@]}"
+}
+
+
+#---------------------------------------------------------------------
+## Strip leading/trailing spaces.
+## @Type API
+## @Note Before this function was deprecated, but it has been recoded
+## @Note in a much faster way. This version is not compatible with old
+## @Note version.
+## @param String to strip
+## @param Variable to return in
+#---------------------------------------------------------------------
+misc_clean_spaces() {
+ # Fastest way that is still secure
+ local array
+ read -ra array <<< "$1"
+ printf -v "$2" '%s' "${array[*]}"
+}
+
+#---------------------------------------------------------------------
+## Strip leading/trailing separator.
+## @Type API
+## @param String to strip
+## @param Variable to return in
+## @param Separator
+#---------------------------------------------------------------------
+misc_clean_delimiter() {
+ local sep="$3" array
+ local IFS="$sep"
+ # Fastest way that is still secure
+ read -ra array <<< "$1"
+ local tmp="${array[*]}"
+ printf -v "$2" '%s' "${tmp#${sep}}"
+}
+
+#---------------------------------------------------------------------
+## Remove a value from a space (or other delimiter) separated list.
+## @Type API
+## @param List to remove from.
+## @param Value to remove.
+## @param Variable to return new list in.
+## @param Separator (optional, defaults to space)
+#---------------------------------------------------------------------
+list_remove() {
+ local sep=${4:-" "}
+ local oldlist="${sep}${!1}${sep}"
+ local newlist="${oldlist//${sep}${2}${sep}/${sep}}"
+ misc_clean_delimiter "$newlist" "$3" "$sep" # Get rid of the unneeded spaces.
+}
+
+#---------------------------------------------------------------------
+## Checks if a space separated list contains a value.
+## @Type API
+## @param List to check.
+## @param Value to check for.
+## @return 0 If found.
+## @return 1 If not found.
+#---------------------------------------------------------------------
+list_contains() {
+ [[ " ${!1} " = *" $2 "* ]]
+}
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Like debug_assert_argc but works without debugging on.
+## For use in sensitive functions in core.
+## @Type Private
+## @param Minimum count of parameters
+## @param Maximum count of parameters
+## @param All the rest of the parameters as "$@"
+## @Example For example this could be called as:
+## @Example <pre>
+## @Example foo() {
+## @Example security_assert_argc 2 2 "$@"
+## @Example ... rest of function ...
+## @Example }
+## @Example </pre>
+#---------------------------------------------------------------------
+security_assert_argc() {
+ local min="$1" max="$2"
+ shift 2
+ if [[ $# -lt $min || $# -gt $max ]]; then
+ log_error "Security sensitive function ${FUNCNAME[1]} should have had between $min and $max parameters but had $# instead."
+ log_error "Security sensitive function ${FUNCNAME[1]} was called from ${BASH_SOURCE[2]}:${BASH_LINENO[1]} ${FUNCNAME[2]} with these parameters: $*"
+ log_error "This should be reported as a bug."
+ return 1
+ fi
+ return 0
+}
diff --git a/lib/modules.sh b/lib/modules.sh
new file mode 100644
index 0000000..10428e6
--- /dev/null
+++ b/lib/modules.sh
@@ -0,0 +1,447 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Modules management
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## List of loaded modules. Don't change from other code.
+## @Type Semi-private
+#---------------------------------------------------------------------
+modules_loaded=""
+
+#---------------------------------------------------------------------
+## Current module API version.
+#---------------------------------------------------------------------
+declare -r modules_current_API=2
+
+
+#---------------------------------------------------------------------
+## Call from after_load with a list of modules that you depend on
+## @Type API
+## @param What module you are calling from.
+## @param Space separated list of modules you depend on
+## @return 0 Success
+## @return 1 Other error. You should return 1 from after_load.
+## @return 2 One or several of the dependencies could found. You should return 1 from after_load.
+## @return 3 Not all of the dependencies could be loaded (modules exist but did not load correctly). You should return 1 from after_load.
+#---------------------------------------------------------------------
+modules_depends_register() {
+ local callermodule="$1"
+ local dep
+ for dep in $2; do
+ if [[ $dep == $callermodule ]]; then
+ log_error_file modules.log "To the module author of $callermodule: You can't list yourself as a dependency of yourself!"
+ log_error_file modules.log "Aborting!"
+ return 1
+ fi
+ if ! list_contains "modules_loaded" "$dep"; then
+ log_info_file modules.log "Loading dependency of $callermodule: $dep"
+ modules_load "$dep"
+ local status="$?"
+ if [[ $status -eq 4 ]]; then
+ return 2
+ elif [[ $status -ne 0 ]]; then
+ return 3
+ fi
+ fi
+ if list_contains "modules_depends_${dep}" "$callermodule"; then
+ log_warning_file modules.log "Dependency ${callermodule} already listed as depending on ${dep}!?"
+ fi
+ # Use printf not eval here.
+ local listname="modules_depends_${dep}"
+ printf -v "modules_depends_${dep}" '%s' "${!listname} $callermodule"
+ done
+}
+
+#---------------------------------------------------------------------
+## Call from after_load or INIT with a list of modules that you
+## depend on optionally.
+## @Type API
+## @param What module you are calling from.
+## @param The module you want to depend on optionally.
+## @return 0 Success, module loaded
+## @return 1 User didn't list it as loaded, don't use the features in question
+## @return 2 Other error. You should return 1 from after_load.
+## @return 3 One or several of the dependencies could found. You should return 1 from after_load.
+## @return 4 Not all of the dependencies could be loaded (modules exist but did not load correctly). You should return 1 from after_load.
+#---------------------------------------------------------------------
+modules_depends_register_optional() {
+ local callermodule="$1"
+ local dep="$2"
+ if ! list_contains "modules_loaded" "$dep"; then
+ # So not loaded, now we need to find out if we should load it or not
+ # We use $config_modules for it
+ if ! list_contains 'config_modules' "$dep"; then
+ log_info_file modules.log "Optional dependency of $callermodule ($dep) not loaded."
+ return 1
+ fi
+ log_info_file modules.log "Loading optional dependency of $callermodule: ($dep)"
+ fi
+ # Ah we should load it then? Call modules_depends_register
+ modules_depends_register "$@"
+}
+
+
+#---------------------------------------------------------------------
+## Semi internal!
+## List modules that depend on another module.
+## @Type Semi-private
+## @param Module to check
+## @Stdout List of modules that depend on this.
+#---------------------------------------------------------------------
+modules_depends_list_deps() {
+ # This is needed to be able to use indirect refs
+ local deplistname="modules_depends_${1}"
+ # Clean out spaces, fastest way
+ echo ${!deplistname}
+}
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+# See doc/module_api.txt instead #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Used by unload to unregister from depends system
+## (That is: remove from list of "depended on by" of other modules)
+## @Type Private
+## @param Module to unregister
+#---------------------------------------------------------------------
+modules_depends_unregister() {
+ local module newval
+ for module in $modules_loaded; do
+ if list_contains "modules_depends_${module}" "$1"; then
+ list_remove "modules_depends_${module}" "$1" "modules_depends_${module}"
+ fi
+ done
+}
+
+#---------------------------------------------------------------------
+## Check if a module can be unloaded
+## @Type Private
+## @param Name of module to check
+## @return Can be unloaded
+## @return Is needed by some other module.
+#---------------------------------------------------------------------
+modules_depends_can_unload() {
+ # This is needed to be able to use indirect refs
+ local deplistname="modules_depends_${1}"
+ # Not empty/only whitespaces?
+ if ! [[ ${!deplistname} =~ ^\ *$ ]]; then
+ return 1
+ fi
+ return 0
+}
+
+#---------------------------------------------------------------------
+## Add hooks for a module
+## @Type Private
+## @param Module name
+## @param MODULE_BASE_PATH, exported to INIT as a part of the API
+## @return 0 Success
+## @return 1 module_modulename_INIT returned non-zero
+## @return 2 Module wanted to register an unknown hook.
+#---------------------------------------------------------------------
+modules_add_hooks() {
+ local module="$1"
+ local modinit_HOOKS
+ local modinit_API
+ local MODULE_BASE_PATH="$2"
+ module_${module}_INIT "$module"
+ [[ $? -ne 0 ]] && { log_error_file modules.log "Failed to get initialize module \"$module\""; return 1; }
+ # Check if it didn't set any modinit_API, in that case it is a API 1 module.
+ if [[ -z $modinit_API ]]; then
+ log_error "Please upgrade \"$module\" to new module API $modules_current_API. This old API is obsolete and no longer supported."
+ return 1
+ elif [[ $modinit_API -ne $modules_current_API ]]; then
+ log_error "Current module API version is $modules_current_API, but the API version of \"$module\" is $module_API."
+ return 1
+ fi
+
+ local hook
+ for hook in $modinit_HOOKS; do
+ case $hook in
+ "FINALISE")
+ modules_FINALISE+=" $module"
+ ;;
+ "after_load")
+ modules_after_load+=" $module"
+ ;;
+ "before_connect")
+ modules_before_connect+=" $module"
+ ;;
+ "on_connect")
+ modules_on_connect+=" $module"
+ ;;
+ "after_connect")
+ modules_after_connect+=" $module"
+ ;;
+ "before_disconnect")
+ modules_before_disconnect+=" $module"
+ ;;
+ "after_disconnect")
+ modules_after_disconnect+=" $module"
+ ;;
+ "on_module_UNLOAD")
+ modules_on_module_UNLOAD+=" $module"
+ ;;
+ "on_server_ERROR")
+ modules_on_server_ERROR+=" $module"
+ ;;
+ "on_NOTICE")
+ modules_on_NOTICE+=" $module"
+ ;;
+ "on_PRIVMSG")
+ modules_on_PRIVMSG+=" $module"
+ ;;
+ "on_TOPIC")
+ modules_on_TOPIC+=" $module"
+ ;;
+ "on_channel_MODE")
+ modules_on_channel_MODE+=" $module"
+ ;;
+ "on_user_MODE")
+ modules_on_user_MODE+=" $module"
+ ;;
+ "on_INVITE")
+ modules_on_INVITE+=" $module"
+ ;;
+ "on_JOIN")
+ modules_on_JOIN+=" $module"
+ ;;
+ "on_PART")
+ modules_on_PART+=" $module"
+ ;;
+ "on_KICK")
+ modules_on_KICK+=" $module"
+ ;;
+ "on_QUIT")
+ modules_on_QUIT+=" $module"
+ ;;
+ "on_KILL")
+ modules_on_KILL+=" $module"
+ ;;
+ "on_NICK")
+ modules_on_NICK+=" $module"
+ ;;
+ "on_numeric")
+ modules_on_numeric+=" $module"
+ ;;
+ "on_PONG")
+ modules_on_PONG+=" $module"
+ ;;
+ "on_raw")
+ modules_on_raw+=" $module"
+ ;;
+ *)
+ log_error_file modules.log "Unknown hook $hook requested. Module may malfunction. Module will be unloaded"
+ return 2
+ ;;
+ esac
+ done
+}
+
+#---------------------------------------------------------------------
+## List of all the optional hooks.
+## @Type Private
+#---------------------------------------------------------------------
+modules_hooks="FINALISE after_load before_connect on_connect after_connect before_disconnect after_disconnect on_module_UNLOAD on_server_ERROR on_NOTICE on_PRIVMSG on_TOPIC on_channel_MODE on_user_MODE on_INVITE on_JOIN on_PART on_KICK on_QUIT on_KILL on_NICK on_numeric on_PONG on_raw"
+
+#---------------------------------------------------------------------
+## Unload a module
+## @Type Private
+## @param Module name
+## @return 0 Unloaded
+## @return 2 Module not loaded
+## @return 3 Can't unload, some other module depends on this.
+## @Note If the unload fails for other reasons the bot will quit.
+#---------------------------------------------------------------------
+modules_unload() {
+ local module="$1"
+ local hook newval to_unset
+ if ! list_contains "modules_loaded" "$module"; then
+ log_warning_file modules.log "No such module as $1 is loaded."
+ return 2
+ fi
+ if ! modules_depends_can_unload "$module"; then
+ log_error_file modules.log "Can't unload $module because these module(s) depend(s) on it: $(modules_depends_list_deps "$module")"
+ return 3
+ fi
+
+ # Remove hooks from list first in case unloading fails so we can do quit hooks if something break.
+ for hook in $modules_hooks; do
+ # List so we can unset.
+ if list_contains "modules_${hook}" "$module"; then
+ to_unset+=" module_${module}_${hook}"
+ fi
+ list_remove "modules_${hook}" "$module" "modules_${hook}"
+ done
+ commands_unregister "$module" || {
+ log_fatal_file modules.log "Could not unregister commands for ${module}"
+ bot_quit "Fatal error in module unload, please see log"
+ }
+ module_${module}_UNLOAD || {
+ log_fatal_file modules.log "Could not unload ${module}, module_${module}_UNLOAD returned ${?}!"
+ bot_quit "Fatal error in module unload, please see log"
+ }
+ unset module_${module}_UNLOAD
+ unset module_${module}_INIT
+ unset module_${module}_REHASH
+ # Unset from list created above.
+ for hook in $to_unset; do
+ unset "$hook" || {
+ log_fatal_file modules.log "Could not unset the hook $hook of module $module!"
+ bot_quit "Fatal error in module unload, please see log"
+ }
+ done
+ modules_depends_unregister "$module"
+ list_remove "modules_loaded" "$module" "modules_loaded"
+
+ # Call any hooks for unloading modules.
+ local othermodule
+ for othermodule in $modules_on_module_UNLOAD; do
+ module_${othermodule}_on_module_UNLOAD "$module"
+ done
+
+ # Unset help string
+ unset helpentry_module_${module}_description
+
+ return 0
+}
+
+#---------------------------------------------------------------------
+## Generate awk script to validate module functions.
+## @param Module name
+## @Type Private
+## @return 0 If the file is OK
+## @return 1 If the file lacks one of more of the functions.
+#---------------------------------------------------------------------
+modules_check_function() {
+ local module="$1"
+ # This is a one liner. Well mostly. ;)
+ # We check that the needed functions exist.
+ awk "function check_found() { if (init && unload && rehash) exit 0 }
+ /^declare -f module_${module}_INIT$/ { init=1; check_found() }
+ /^declare -f module_${module}_UNLOAD$/ { unload=1; check_found() }
+ /^declare -f module_${module}_REHASH$/ { rehash=1; check_found() }
+ END { if (! (init && unload && rehash)) exit 1 }"
+}
+
+#---------------------------------------------------------------------
+## Load a module
+## @Type Private
+## @param Name of module to load
+## @return 0 Loaded Ok
+## @return 1 Other errors
+## @return 2 Module already loaded
+## @return 3 Failed to source it in safe subshell
+## @return 4 Failed to source it
+## @return 5 No such module
+## @return 6 Getting hooks failed
+## @return 7 after_load failed
+## @Note If the load fails in a fatal way the bot will quit.
+#---------------------------------------------------------------------
+modules_load() {
+ local module="$1"
+ if list_contains "modules_loaded" "$module"; then
+ log_warning_file modules.log "Module ${module} is already loaded."
+ return 2
+ fi
+ # modulebase is exported as MODULE_BASE_PATH
+ # with ${config_modules_dir} prepended to the
+ # INIT function, useful for multi-file
+ # modules, but available for other modules too.
+ local modulefilename modulebase
+ if [[ -f "${config_modules_dir}/m_${module}.sh" ]]; then
+ modulefilename="m_${module}.sh"
+ modulebase="${modulefilename}"
+ elif [[ -d "${config_modules_dir}/m_${module}" && -f "${config_modules_dir}/m_${module}/__main__.sh" ]]; then
+ modulefilename="m_${module}/__main__.sh"
+ modulebase="m_${module}"
+ else
+ log_error_file modules.log "No such module as ${module} exists."
+ return 5
+ fi
+ ( source "${config_modules_dir}/${modulefilename}" )
+ if [[ $? -ne 0 ]]; then
+ log_error_file modules.log "Could not load ${module}, failed to source it in safe subshell."
+ return 3
+ fi
+ ( source "${config_modules_dir}/${modulefilename}" && declare -F ) | modules_check_function "$module"
+ if [[ $? -ne 0 ]]; then
+ log_error_file modules.log "Could not load ${module}, it lacks some important functions it should have."
+ return 3
+ fi
+ source "${config_modules_dir}/${modulefilename}"
+ if [[ $? -eq 0 ]]; then
+ modules_loaded+=" $module"
+ modules_add_hooks "$module" "${config_modules_dir}/${modulebase}" || \
+ {
+ log_error_file modules.log "Hooks failed for $module"
+ # Try to unload.
+ modules_unload "$module" || {
+ log_fatal_file modules.log "Failed Unloading of $module (that failed to load)."
+ bot_quit "Fatal error in module unload of failed module load, please see log"
+ }
+ return 6
+ }
+ if grep -qw "$module" <<< "$modules_after_load"; then
+ module_${module}_after_load
+ if [[ $? -ne 0 ]]; then
+ modules_unload ${module} || {
+ log_fatal_file modules.log "Unloading of $module that failed after_load failed."
+ bot_quit "Fatal error in module unload of failed module load (after_load), please see log"
+ }
+ return 7
+ fi
+ fi
+ else
+ log_error_file modules.log "Could not load ${module}, failed to source it."
+ return 4
+ fi
+}
+
+#---------------------------------------------------------------------
+## Load modules from the config
+## @Type Private
+#---------------------------------------------------------------------
+modules_load_from_config() {
+ local module
+ IFS=" "
+ for module in $modules_loaded; do
+ if ! list_contains config_modules "$module"; then
+ modules_unload "$module"
+ fi
+ done
+ unset IFS
+ for module in $config_modules; do
+ if [[ -f "${config_modules_dir}/m_${module}.sh" || -d "${config_modules_dir}/m_${module}" ]]; then
+ if ! list_contains modules_loaded "$module"; then
+ modules_load "$module"
+ fi
+ else
+ log_warning_file modules.log "$module doesn't exist! Removing it from list"
+ fi
+ done
+}
diff --git a/lib/numerics.sh b/lib/numerics.sh
new file mode 100644
index 0000000..bfe3d33
--- /dev/null
+++ b/lib/numerics.sh
@@ -0,0 +1,348 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+
+###########################################################################
+# #
+# WARNING THIS FILE IS AUTOGENERATED. ANY CHANGES WILL BE OVERWRITTEN! #
+# See the source in tools/numerics.txt for comments about some numerics #
+# This file was generated with tools/build_numerics.sh #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Auto-generated list of numerics from tools/numerics.txt<br />
+## This file contains a list of numerics that we currently use.
+## It is therefore incomplete.<br />
+## Because the list of variables in this file is so long, please see
+## it's source for more details.
+#---------------------------------------------------------------------
+
+##########################
+# Name -> number mapping #
+##########################
+
+numeric_RPL_WELCOME='001'
+numeric_RPL_YOURHOST='002'
+numeric_RPL_CREATED='003'
+numeric_RPL_MYINFO='004'
+numeric_RPL_ISUPPORT='005'
+numeric_RPL_MAP='006'
+numeric_RPL_MAPEND='007'
+numeric_RPL_SNOMASK='008'
+numeric_RPL_TRACEUSER='205'
+numeric_RPL_STATSCLINE='213'
+numeric_RPL_ENDOFSTATS='219'
+numeric_RPL_UMODEIS='221'
+numeric_RPL_STATSELINE='223'
+numeric_RPL_RULES='232'
+numeric_RPL_STATSUPTIME='242'
+numeric_RPL_STATSCONN='250'
+numeric_RPL_LUSERCLIENT='251'
+numeric_RPL_LUSEROP='252'
+numeric_RPL_LUSERUNKNOWN='253'
+numeric_RPL_LUSERCHANNELS='254'
+numeric_RPL_LUSERME='255'
+numeric_RPL_ADMINME='256'
+numeric_RPL_ADMINLOC1='257'
+numeric_RPL_ADMINLOC2='258'
+numeric_RPL_ADMINEMAIL='259'
+numeric_RPL_TRYAGAIN='263'
+numeric_RPL_LOCALUSERS='265'
+numeric_RPL_GLOBALUSERS='266'
+numeric_RPL_SILELIST='271'
+numeric_RPL_ENDOFSILELIST='272'
+numeric_RPL_AWAY='301'
+numeric_RPL_USERHOST='302'
+numeric_RPL_ISON='303'
+numeric_RPL_TEXT='304'
+numeric_RPL_UNAWAY='305'
+numeric_RPL_UNAWAY='306'
+numeric_RPL_WHOISREGNICK='307'
+numeric_RPL_RULESSTART='308'
+numeric_RPL_ENDOFRULES='309'
+numeric_RPL_WHOISHELPOP='310'
+numeric_RPL_WHOISUSER='311'
+numeric_RPL_WHOISSERVER='312'
+numeric_RPL_WHOISOPERATOR='313'
+numeric_RPL_WHOWASUSER='314'
+numeric_RPL_ENDOFWHO='315'
+numeric_RPL_WHOISIDLE='317'
+numeric_RPL_ENDOFWHOIS='318'
+numeric_RPL_WHOISCHANNELS='319'
+numeric_RPL_WHOISSPECIAL='320'
+numeric_RPL_LISTSTART='321'
+numeric_RPL_LIST='322'
+numeric_RPL_LISTEND='323'
+numeric_RPL_CHANNELMODEIS='324'
+numeric_RPL_CREATIONTIME='329'
+numeric_RPL_WHOISACCOUNT='330'
+numeric_RPL_NOTOPIC='331'
+numeric_RPL_TOPIC='332'
+numeric_RPL_TOPICWHOTIME='333'
+numeric_RPL_USERIP='340'
+numeric_RPL_INVITING='341'
+numeric_RPL_INVITELIST='346'
+numeric_RPL_ENDOFINVITELIST='347'
+numeric_RPL_EXCEPTLIST='348'
+numeric_RPL_ENDOFEXCEPTLIST='349'
+numeric_RPL_VERSION='351'
+numeric_RPL_WHOREPLY='352'
+numeric_RPL_NAMREPLY='353'
+numeric_RPL_LINKS='364'
+numeric_RPL_ENDOFLINKS='365'
+numeric_RPL_ENDOFNAMES='366'
+numeric_RPL_BANLIST='367'
+numeric_RPL_ENDOFBANLIST='368'
+numeric_RPL_ENDOFWHOWAS='369'
+numeric_RPL_INFO='371'
+numeric_RPL_MOTD='372'
+numeric_RPL_ENDOFINFO='374'
+numeric_RPL_MOTDSTART='375'
+numeric_RPL_ENDOFMOTD='376'
+numeric_RPL_WHOISHOST='378'
+numeric_RPL_YOUREOPER='381'
+numeric_RPL_REHASHING='382'
+numeric_RPL_TIME='391'
+numeric_RPL_HOSTHIDDEN='396'
+numeric_ERR_NOSUCHNICK='401'
+numeric_ERR_NOSUCHSERVER='402'
+numeric_ERR_NOSUCHCHANNEL='403'
+numeric_ERR_CANNOTSENDTOCHAN='404'
+numeric_ERR_TOOMANYCHANNELS='405'
+numeric_ERR_WASNOSUCHNICK='406'
+numeric_ERR_TOOMANYTARGETS='407'
+numeric_ERR_NOTEXTTOSEND='412'
+numeric_ERR_TOOMANYMATCHES='416'
+numeric_ERR_UNKNOWNCOMMAND='421'
+numeric_ERR_NOMOTD='422'
+numeric_ERR_ERRONEUSNICKNAME='432'
+numeric_ERR_NICKNAMEINUSE='433'
+numeric_ERR_NICKTOOFAST='438'
+numeric_ERR_USERNOTINCHANNEL='441'
+numeric_ERR_NOTONCHANNEL='442'
+numeric_ERR_USERONCHANNEL='443'
+numeric_ERR_SUMMONDISABLED='445'
+numeric_ERR_USERSDISABLED='446'
+numeric_ERR_NONICKCHANGE='447'
+numeric_ERR_NOTFORHALFOPS='460'
+numeric_ERR_NEEDMOREPARAMS='461'
+numeric_ERR_ALREADYREGISTERED='462'
+numeric_ERR_ONLYSERVERSCANCHANGE='468'
+numeric_ERR_LINKCHANNEL='470'
+numeric_ERR_CHANNELISFULL='471'
+numeric_ERR_UNKNOWNMODE='472'
+numeric_ERR_INVITEONLYCHAN='473'
+numeric_ERR_BANNEDFROMCHAN='474'
+numeric_ERR_BADCHANNELKEY='475'
+numeric_ERR_NEEDREGGEDNICK='477'
+numeric_ERR_BANLISTFULL='478'
+numeric_ERR_CANNOTKNOCK='480'
+numeric_ERR_NOPRIVILEGES='481'
+numeric_ERR_CHANOPRIVSNEEDED='482'
+numeric_ERR_ATTACKDENY='484'
+numeric_ERR_SECUREONLYCHAN='489'
+numeric_ERR_ALLMUSTUSESSL='490'
+numeric_ERR_NOOPERHOST='491'
+numeric_ERR_NOREJOINONKICK='495'
+numeric_ERR_CHANOWNPRIVNEEDED='499'
+numeric_ERR_UMODEUNKNOWNFLAG='501'
+numeric_ERR_USERSDONTMATCH='502'
+numeric_RPL_LOGON='600'
+numeric_RPL_LOGOFF='601'
+numeric_RPL_WATCHOFF='602'
+numeric_RPL_NOWON='604'
+numeric_RPL_NOWOFF='605'
+numeric_RPL_WATCHLIST='606'
+numeric_RPL_ENDOFWATCHLIST='607'
+numeric_RPL_WHOISSECURE='671'
+numeric_RPL_MODULES='900'
+numeric_RPL_ENDOFMODULES='901'
+numeric_RPL_COMMANDS='902'
+numeric_RPL_ENDOFCOMMANDS='903'
+numeric_ERR_CENSORED='936'
+numeric_ERR_ALREDYCENSORED='937'
+numeric_ERR_NOTCENSORED='938'
+numeric_ERR_SPAMFILTERLISTFULL='939'
+numeric_RPL_ENDOFSPAMFILTER='940'
+numeric_RPL_SPAMFILTER='941'
+numeric_ERR_INVALIDNICK='942'
+numeric_RPL_SILENCEREMOVED='950'
+numeric_RPL_SILENCEADDED='951'
+numeric_ERR_ALREADYSILENCE='952'
+numeric_ERR_CANNOTDOCOMMAND='972'
+numeric_ERR_CANNOTCHANGECHANMODE='974'
+
+##########################
+# Number -> name mapping #
+##########################
+
+numerics[1]='RPL_WELCOME'
+numerics[2]='RPL_YOURHOST'
+numerics[3]='RPL_CREATED'
+numerics[4]='RPL_MYINFO'
+numerics[5]='RPL_ISUPPORT'
+numerics[6]='RPL_MAP'
+numerics[7]='RPL_MAPEND'
+numerics[8]='RPL_SNOMASK'
+numerics[205]='RPL_TRACEUSER'
+numerics[213]='RPL_STATSCLINE'
+numerics[219]='RPL_ENDOFSTATS'
+numerics[221]='RPL_UMODEIS'
+numerics[223]='RPL_STATSELINE'
+numerics[232]='RPL_RULES'
+numerics[242]='RPL_STATSUPTIME'
+numerics[250]='RPL_STATSCONN'
+numerics[251]='RPL_LUSERCLIENT'
+numerics[252]='RPL_LUSEROP'
+numerics[253]='RPL_LUSERUNKNOWN'
+numerics[254]='RPL_LUSERCHANNELS'
+numerics[255]='RPL_LUSERME'
+numerics[256]='RPL_ADMINME'
+numerics[257]='RPL_ADMINLOC1'
+numerics[258]='RPL_ADMINLOC2'
+numerics[259]='RPL_ADMINEMAIL'
+numerics[263]='RPL_TRYAGAIN'
+numerics[265]='RPL_LOCALUSERS'
+numerics[266]='RPL_GLOBALUSERS'
+numerics[271]='RPL_SILELIST'
+numerics[272]='RPL_ENDOFSILELIST'
+numerics[301]='RPL_AWAY'
+numerics[302]='RPL_USERHOST'
+numerics[303]='RPL_ISON'
+numerics[304]='RPL_TEXT'
+numerics[305]='RPL_UNAWAY'
+numerics[306]='RPL_UNAWAY'
+numerics[307]='RPL_WHOISREGNICK'
+numerics[308]='RPL_RULESSTART'
+numerics[309]='RPL_ENDOFRULES'
+numerics[310]='RPL_WHOISHELPOP'
+numerics[311]='RPL_WHOISUSER'
+numerics[312]='RPL_WHOISSERVER'
+numerics[313]='RPL_WHOISOPERATOR'
+numerics[314]='RPL_WHOWASUSER'
+numerics[315]='RPL_ENDOFWHO'
+numerics[317]='RPL_WHOISIDLE'
+numerics[318]='RPL_ENDOFWHOIS'
+numerics[319]='RPL_WHOISCHANNELS'
+numerics[320]='RPL_WHOISSPECIAL'
+numerics[321]='RPL_LISTSTART'
+numerics[322]='RPL_LIST'
+numerics[323]='RPL_LISTEND'
+numerics[324]='RPL_CHANNELMODEIS'
+numerics[329]='RPL_CREATIONTIME'
+numerics[330]='RPL_WHOISACCOUNT'
+numerics[331]='RPL_NOTOPIC'
+numerics[332]='RPL_TOPIC'
+numerics[333]='RPL_TOPICWHOTIME'
+numerics[340]='RPL_USERIP'
+numerics[341]='RPL_INVITING'
+numerics[346]='RPL_INVITELIST'
+numerics[347]='RPL_ENDOFINVITELIST'
+numerics[348]='RPL_EXCEPTLIST'
+numerics[349]='RPL_ENDOFEXCEPTLIST'
+numerics[351]='RPL_VERSION'
+numerics[352]='RPL_WHOREPLY'
+numerics[353]='RPL_NAMREPLY'
+numerics[364]='RPL_LINKS'
+numerics[365]='RPL_ENDOFLINKS'
+numerics[366]='RPL_ENDOFNAMES'
+numerics[367]='RPL_BANLIST'
+numerics[368]='RPL_ENDOFBANLIST'
+numerics[369]='RPL_ENDOFWHOWAS'
+numerics[371]='RPL_INFO'
+numerics[372]='RPL_MOTD'
+numerics[374]='RPL_ENDOFINFO'
+numerics[375]='RPL_MOTDSTART'
+numerics[376]='RPL_ENDOFMOTD'
+numerics[378]='RPL_WHOISHOST'
+numerics[381]='RPL_YOUREOPER'
+numerics[382]='RPL_REHASHING'
+numerics[391]='RPL_TIME'
+numerics[396]='RPL_HOSTHIDDEN'
+numerics[401]='ERR_NOSUCHNICK'
+numerics[402]='ERR_NOSUCHSERVER'
+numerics[403]='ERR_NOSUCHCHANNEL'
+numerics[404]='ERR_CANNOTSENDTOCHAN'
+numerics[405]='ERR_TOOMANYCHANNELS'
+numerics[406]='ERR_WASNOSUCHNICK'
+numerics[407]='ERR_TOOMANYTARGETS'
+numerics[412]='ERR_NOTEXTTOSEND'
+numerics[416]='ERR_TOOMANYMATCHES'
+numerics[421]='ERR_UNKNOWNCOMMAND'
+numerics[422]='ERR_NOMOTD'
+numerics[432]='ERR_ERRONEUSNICKNAME'
+numerics[433]='ERR_NICKNAMEINUSE'
+numerics[438]='ERR_NICKTOOFAST'
+numerics[441]='ERR_USERNOTINCHANNEL'
+numerics[442]='ERR_NOTONCHANNEL'
+numerics[443]='ERR_USERONCHANNEL'
+numerics[445]='ERR_SUMMONDISABLED'
+numerics[446]='ERR_USERSDISABLED'
+numerics[447]='ERR_NONICKCHANGE'
+numerics[460]='ERR_NOTFORHALFOPS'
+numerics[461]='ERR_NEEDMOREPARAMS'
+numerics[462]='ERR_ALREADYREGISTERED'
+numerics[468]='ERR_ONLYSERVERSCANCHANGE'
+numerics[470]='ERR_LINKCHANNEL'
+numerics[471]='ERR_CHANNELISFULL'
+numerics[472]='ERR_UNKNOWNMODE'
+numerics[473]='ERR_INVITEONLYCHAN'
+numerics[474]='ERR_BANNEDFROMCHAN'
+numerics[475]='ERR_BADCHANNELKEY'
+numerics[477]='ERR_NEEDREGGEDNICK'
+numerics[478]='ERR_BANLISTFULL'
+numerics[480]='ERR_CANNOTKNOCK'
+numerics[481]='ERR_NOPRIVILEGES'
+numerics[482]='ERR_CHANOPRIVSNEEDED'
+numerics[484]='ERR_ATTACKDENY'
+numerics[489]='ERR_SECUREONLYCHAN'
+numerics[490]='ERR_ALLMUSTUSESSL'
+numerics[491]='ERR_NOOPERHOST'
+numerics[495]='ERR_NOREJOINONKICK'
+numerics[499]='ERR_CHANOWNPRIVNEEDED'
+numerics[501]='ERR_UMODEUNKNOWNFLAG'
+numerics[502]='ERR_USERSDONTMATCH'
+numerics[600]='RPL_LOGON'
+numerics[601]='RPL_LOGOFF'
+numerics[602]='RPL_WATCHOFF'
+numerics[604]='RPL_NOWON'
+numerics[605]='RPL_NOWOFF'
+numerics[606]='RPL_WATCHLIST'
+numerics[607]='RPL_ENDOFWATCHLIST'
+numerics[671]='RPL_WHOISSECURE'
+numerics[900]='RPL_MODULES'
+numerics[901]='RPL_ENDOFMODULES'
+numerics[902]='RPL_COMMANDS'
+numerics[903]='RPL_ENDOFCOMMANDS'
+numerics[936]='ERR_CENSORED'
+numerics[937]='ERR_ALREDYCENSORED'
+numerics[938]='ERR_NOTCENSORED'
+numerics[939]='ERR_SPAMFILTERLISTFULL'
+numerics[940]='RPL_ENDOFSPAMFILTER'
+numerics[941]='RPL_SPAMFILTER'
+numerics[942]='ERR_INVALIDNICK'
+numerics[950]='RPL_SILENCEREMOVED'
+numerics[951]='RPL_SILENCEADDED'
+numerics[952]='ERR_ALREADYSILENCE'
+numerics[972]='ERR_CANNOTDOCOMMAND'
+numerics[974]='ERR_CANNOTCHANGECHANMODE'
+
+# End of generated file.
diff --git a/lib/parse.sh b/lib/parse.sh
new file mode 100644
index 0000000..6abb9ab
--- /dev/null
+++ b/lib/parse.sh
@@ -0,0 +1,100 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Data parsing
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Get parts of hostmask.
+## @Note In most cases you should use one of
+## @Note <@function parse_hostmask_nick>, <@function parse_hostmask_ident>
+## @Note or <@function parse_hostmask_host>. Only use this function
+## @Note if you want all several parts.
+## @Type API
+## @param n!u@h mask
+## @param Variable to return nick in
+## @param Variable to return ident in
+## @param Variable to return host in
+#---------------------------------------------------------------------
+parse_hostmask() {
+ if [[ $1 =~ ^([^ !]+)!([^ @]+)@([^ ]+) ]]; then
+ printf -v "$2" '%s' "${BASH_REMATCH[1]}"
+ printf -v "$3" '%s' "${BASH_REMATCH[2]}"
+ printf -v "$4" '%s' "${BASH_REMATCH[3]}"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Get nick from hostmask
+## @Type API
+## @param n!u@h mask
+## @param Variable to return result in
+#---------------------------------------------------------------------
+parse_hostmask_nick() {
+ if [[ $1 =~ ^([^ !]+)! ]]; then
+ printf -v "$2" '%s' "${BASH_REMATCH[1]}"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Get ident from hostmask
+## @Type API
+## @param n!u@h mask
+## @param Variable to return result in
+#---------------------------------------------------------------------
+parse_hostmask_ident() {
+ if [[ $1 =~ ^[^\ !]+!([^ @]+)@ ]]; then
+ printf -v "$2" '%s' "${BASH_REMATCH[1]}"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Get host from hostmask
+## @Type API
+## @param n!u@h mask
+## @param Variable to return result in
+#---------------------------------------------------------------------
+parse_hostmask_host() {
+ if [[ $1 =~ ^[^\ !]+![^\ @]+@([^ ]+) ]]; then
+ printf -v "$2" '%s' "${BASH_REMATCH[1]}"
+ fi
+}
+
+#---------------------------------------------------------------------
+## This is used to get data out of 005.
+## @Type API
+## @param Name of data to get
+## @param Variable to return result (if any result) in
+## @return 0 If found otherwise 1
+## @Note That if the variable doesn't have any data,
+## @Note but still exist it will return nothing on STDOUT
+## @Note but 0 as error code
+#---------------------------------------------------------------------
+parse_005() {
+ if [[ $server_005 =~ ${1}(=([^ ]+))? ]]; then
+ if [[ ${BASH_REMATCH[2]} ]]; then
+ printf -v "$2" '%s' "${BASH_REMATCH[2]}"
+ fi
+ return 0
+ fi
+ return 1
+}
diff --git a/lib/send.sh b/lib/send.sh
new file mode 100644
index 0000000..2b3d488
--- /dev/null
+++ b/lib/send.sh
@@ -0,0 +1,178 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Functions for sending data to server
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Simple flood limiting.
+## Note that this doesn't handle this very well:<br />
+## seconds:milliseconds message<br />
+## 01:999 message<br />
+## 02:001 other message<br />
+## Then they get too close.<br />
+## I think this won't flood us off though.<br />
+## @Type Private
+#---------------------------------------------------------------------
+send_last=0
+
+#---------------------------------------------------------------------
+## Send a "raw" line to the server.
+## @Type API
+## @param Line to send
+#---------------------------------------------------------------------
+send_raw() {
+ # Do the flood limiting
+ local now=
+ time_get_current 'now'
+ if [[ "$send_last" == "$now" ]]; then
+ sleep 1
+ fi
+ time_get_current 'send_last'
+ send_raw_flood "$*"
+}
+
+#---------------------------------------------------------------------
+## Send a PRIVMSG
+## @Type API
+## @param Who (channel or nick)
+## @param Message
+#---------------------------------------------------------------------
+send_msg() {
+ # Don't do anything if no message
+ [[ -z $2 ]] && return 0
+ send_raw "PRIVMSG ${1} :${2}"
+}
+
+#---------------------------------------------------------------------
+## Send a NOTICE
+## @Type API
+## @param Who (channel or nick)
+## @param Message
+#---------------------------------------------------------------------
+send_notice() {
+ # Don't do anything if no message
+ [[ -z $2 ]] && return 0
+ send_raw "NOTICE ${1} :${2}"
+}
+
+#---------------------------------------------------------------------
+## Send a CTCP
+## @Type API
+## @param Who (channel or nick)
+## @param Message
+#---------------------------------------------------------------------
+send_ctcp() {
+ # Don't do anything if no message
+ [[ -z $2 ]] && return 0
+ send_msg "$1" $'\1'"${2}"$'\1'
+}
+
+#---------------------------------------------------------------------
+## Send a NCTCP (ctcp reply)
+## @Type API
+## @param Who (channel or nick)
+## @param Message
+#---------------------------------------------------------------------
+send_nctcp() {
+ # Don't do anything if no message
+ [[ -z $2 ]] && return 0
+ send_notice "$1" $'\1'"${2}"$'\1'
+}
+
+#---------------------------------------------------------------------
+## Send a NICK to change nick
+## @Type API
+## @param New nick
+#---------------------------------------------------------------------
+send_nick() {
+ send_raw "NICK ${1}"
+}
+
+#---------------------------------------------------------------------
+## Send a MODE to change umodes.
+## @Type API
+## @param Modes to send
+#---------------------------------------------------------------------
+send_umodes() {
+ send_raw "MODE $server_nick_current $1"
+}
+
+#---------------------------------------------------------------------
+## Send a MODE to change channel modes.
+## @Type API
+## @param Target channel
+## @param Modes to set
+#---------------------------------------------------------------------
+send_modes() {
+ send_raw "MODE $1 $2"
+}
+
+#---------------------------------------------------------------------
+## Send a TOPIC to change channel topic.
+## @Type API
+## @param Channel to change topic of
+## @param New topic.
+#---------------------------------------------------------------------
+send_topic() {
+ send_raw "TOPIC $1 :$2"
+}
+
+#---------------------------------------------------------------------
+## This is semi-internal only
+## This may flood ourself off. Use send_raw instead in most cases.
+## Also this doesn't log the actual line, so used for passwords.
+## @Type API
+## @param What to log instead (example could be: "NickServ IDENTIFY (password)")
+## @param The line to send
+#---------------------------------------------------------------------
+send_raw_flood_nolog() {
+ log_raw_out "<hidden line from logs>: $1"
+ transport_write_line "$2"$'\r'
+}
+
+#---------------------------------------------------------------------
+## This is semi-internal only
+## This may flood ourself off. Use send_raw instead in most cases.
+## Same syntax as send_raw
+## @Type Semi-private
+#---------------------------------------------------------------------
+send_raw_flood() {
+ log_raw_out "$*"
+ transport_write_line "$*"$'\r'
+}
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Module authors: use the wrapper: bot_quit in misc.sh instead!
+## @Type Private
+## @param If set, a quit reason
+#---------------------------------------------------------------------
+send_quit() {
+ local reason=""
+ [[ -n "$1" ]] && reason=" :$1"
+ send_raw_flood "QUIT${reason}"
+}
diff --git a/lib/server.sh b/lib/server.sh
new file mode 100644
index 0000000..348cb73
--- /dev/null
+++ b/lib/server.sh
@@ -0,0 +1,337 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Server connection.
+#---------------------------------------------------------------------
+
+# Server info variables
+#---------------------------------------------------------------------
+## Name of server (example: server1.example.net)
+## @Type API
+#---------------------------------------------------------------------
+server_name=""
+#---------------------------------------------------------------------
+## The 004 received from the server.
+## @Type API
+#---------------------------------------------------------------------
+server_004=""
+#---------------------------------------------------------------------
+## The 005 received from the server. Use parse_005 to get data out of this.
+## @Type API
+## @Note See http://www.irc.org/tech_docs/005.html for an incomplete list of 005 values.
+#---------------------------------------------------------------------
+server_005=""
+# NAMES output with UHNAMES and NAMESX
+# :photon.kuonet-ng.org 353 envbot = #bots :@%+AnMaster!AnMaster@staff.kuonet-ng.org @ChanServ!ChanServ@services.kuonet-ng.org bashbot!rfc3092@1F1794B2:769091B3
+# NAMES output with NAMESX only:
+# :hurricane.KuoNET.org 353 envbot = #test :bashbot ~@Brain ~@EmErgE &@AnMaster/kng
+#---------------------------------------------------------------------
+## 1 if UHNAMES enabled, otherwise 0
+## @Type API
+#---------------------------------------------------------------------
+server_UHNAMES=0
+#---------------------------------------------------------------------
+## 1 if NAMESX enabled, otherwise 0
+## @Type API
+#---------------------------------------------------------------------
+server_NAMESX=0
+# These are passed in a slightly odd way in 005 so we do them here.
+#---------------------------------------------------------------------
+## The mode char (if any) for ban excepts (normally +e)
+## @Type API
+#---------------------------------------------------------------------
+server_EXCEPTS=""
+#---------------------------------------------------------------------
+## The mode char (if any) for invite excepts (normally +I)
+## @Type API
+#---------------------------------------------------------------------
+server_INVEX=""
+
+# In case we don't get a 005, make some sane defaults.
+#---------------------------------------------------------------------
+## List channel modes supported by server.
+## @Type API
+#---------------------------------------------------------------------
+server_CHMODES_LISTMODES="b"
+#---------------------------------------------------------------------
+## "Always parameters" channel modes supported by server.
+## @Type API
+#---------------------------------------------------------------------
+server_CHMODES_ALWAYSPARAM="k"
+#---------------------------------------------------------------------
+## "Parameter on set" channel modes supported by server.
+## @Type API
+#---------------------------------------------------------------------
+server_CHMODES_PARAMONSET="l"
+#---------------------------------------------------------------------
+## Simple channel modes supported by server.
+## @Type API
+#---------------------------------------------------------------------
+server_CHMODES_SIMPLE="imnpst"
+#---------------------------------------------------------------------
+## Prefix channel modes supported by server.
+## @Type API
+#---------------------------------------------------------------------
+server_PREFIX_modes="ov"
+#---------------------------------------------------------------------
+## Channel prefixes supported by server.
+## @Type API
+#---------------------------------------------------------------------
+server_PREFIX_prefixes="@+"
+
+#---------------------------------------------------------------------
+## What is our current nick?
+## @Type API
+#---------------------------------------------------------------------
+server_nick_current=""
+#---------------------------------------------------------------------
+## 1 if we are connected, otherwise 0
+## @Type API
+#---------------------------------------------------------------------
+server_connected=0
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Get some common data out of 005, the whole will also be saved to
+## $server_005 for any module to use via parse_005().
+## This function is for cases that needs special action, like NAMESX
+## and UHNAMES.
+## This should be called directly after receiving a part of the 005!
+## @Type Private
+## @param The last part of the 005.
+#---------------------------------------------------------------------
+server_handle_005() {
+ # Example from freenode:
+ # :heinlein.freenode.net 005 envbot IRCD=dancer CAPAB CHANTYPES=# EXCEPTS INVEX CHANMODES=bdeIq,k,lfJD,cgijLmnPQrRstz CHANLIMIT=#:20 PREFIX=(ov)@+ MAXLIST=bdeI:50 MODES=4 STATUSMSG=@ KNOCK NICKLEN=16 :are supported by this server
+ # :heinlein.freenode.net 005 envbot SAFELIST CASEMAPPING=ascii CHANNELLEN=30 TOPICLEN=450 KICKLEN=450 KEYLEN=23 USERLEN=10 HOSTLEN=63 SILENCE=50 :are supported by this server
+ local line="$1"
+ if [[ $line =~ EXCEPTS(=([^ ]+))? ]]; then
+ # Some, but not all also send what char the modes for EXCEPTS is.
+ # If it isn't sent, lets guess it is +e
+ if [[ ${BASH_REMATCH[2]} ]]; then
+ server_EXCEPTS="${BASH_REMATCH[2]}"
+ else
+ server_EXCEPTS="e"
+ fi
+ fi
+ if [[ $line =~ INVEX(=([^ ]+))? ]]; then
+ # Some, but not all also send what char the modes for INVEX is.
+ # If it isn't sent, lets guess it is +I
+ if [[ ${BASH_REMATCH[2]} ]]; then
+ server_INVEX="${BASH_REMATCH[2]}"
+ else
+ server_INVEX="I"
+ fi
+ fi
+ if [[ $line =~ PREFIX=(\(([^ \)]+)\)([^ ]+)) ]]; then
+ server_PREFIX="${BASH_REMATCH[1]}"
+ server_PREFIX_modes="${BASH_REMATCH[2]}"
+ server_PREFIX_prefixes="${BASH_REMATCH[3]}"
+ fi
+ if [[ $line =~ CHANMODES=([^ ,]+),([^ ,]+),([^ ,]+),([^ ,]+) ]]; then
+ server_CHMODES_LISTMODES="${BASH_REMATCH[1]}"
+ server_CHMODES_ALWAYSPARAM="${BASH_REMATCH[2]}"
+ server_CHMODES_PARAMONSET="${BASH_REMATCH[3]}"
+ server_CHMODES_SIMPLE="${BASH_REMATCH[4]}"
+ fi
+ # Enable NAMESX is supported.
+ if [[ $line =~ NAMESX ]]; then
+ log_info "Enabled NAMESX support"
+ send_raw_flood "PROTOCTL NAMESX"
+ server_NAMESX=1
+ fi
+ # Enable UHNAMES if it is there.
+ if [[ $line =~ UHNAMES ]]; then
+ log_info "Enabled UHNAMES support"
+ send_raw_flood "PROTOCTL UHNAMES"
+ server_UHNAMES=1
+ fi
+}
+
+#---------------------------------------------------------------------
+## Respond to PING from server.
+## @Type Private
+## @param Raw line
+## @return 0 If it was a PING
+## @return 1 If it was not a PING
+#---------------------------------------------------------------------
+server_handle_ping() {
+ if [[ "$1" =~ ^PING\ *:(.*) ]] ;then
+ send_raw "PONG :${BASH_REMATCH[1]}"
+ return 0
+ fi
+ return 1
+}
+
+#---------------------------------------------------------------------
+## Handle numerics from server.
+## @Type Private
+## @param Numeric
+## @param Target (self)
+## @param Data
+#---------------------------------------------------------------------
+server_handle_numerics() {
+ # Slight sanity check
+ if [[ "$2" != "$server_nick_current" ]]; then
+ log_warning 'Own nick desynced!'
+ log_warning "It should be $server_nick_current but server says it is $2"
+ log_warning "Correcting own nick and lets hope that doesn't break anything"
+ server_nick_current="$2"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Handle NICK messages from server
+## @Type Private
+## @param Sender
+## @param New nick
+#---------------------------------------------------------------------
+server_handle_nick() {
+ local oldnick=
+ parse_hostmask_nick "$1" 'oldnick'
+ if [[ $oldnick == $server_nick_current ]]; then
+ server_nick_current="$2"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Handle nick in use.
+## @Type Private
+#---------------------------------------------------------------------
+server_handle_nick_in_use() {
+ if [[ $on_nick -eq 3 ]]; then
+ log_error "Third nick is ALSO in use. I give up"
+ bot_quit 2
+ elif [[ $on_nick -eq 2 ]]; then
+ log_warning "Second nick is ALSO in use, trying third"
+ send_nick "$config_thirdnick"
+ server_nick_current="$config_thirdnick"
+ on_nick=3
+ else
+ log_info_stdout "First nick is in use, trying second"
+ send_nick "$config_secondnick"
+ on_nick=2
+ # FIXME: THIS IS HACKISH AND MAY BREAK
+ server_nick_current="$config_secondnick"
+ fi
+ sleep 1
+}
+
+#---------------------------------------------------------------------
+## Connect to IRC server.
+## @Type Private
+#---------------------------------------------------------------------
+server_connect() {
+ server_connected=0
+ on_nick=1
+ # Clear current channels:
+ channels_current=""
+ # HACK: Clean up if we are aborted, replaced after connect with one that sends QUIT
+ trap 'transport_disconnect; rm -rvf "$tmp_home"; exit 1' TERM INT
+ log_info_stdout "Connecting to \"${config_server}:${config_server_port}\"..."
+ transport_connect "$config_server" "$config_server_port" "$config_server_ssl" "$config_server_bind" || return 1
+
+ [[ $config_server_passwd ]] && send_raw_flood_nolog "PASS $config_server_passwd"
+ log_info_stdout "logging in as $config_firstnick..."
+ send_nick "$config_firstnick"
+ # FIXME: THIS IS HACKISH AND MAY BREAK
+ server_nick_current="$config_firstnick"
+ # If a server password is set, send it.
+ send_raw_flood "USER $config_ident 0 * :${config_gecos}"
+ while true; do
+ line=
+ transport_read_line
+ local transport_status="$?"
+ # Still connected?
+ if ! transport_alive; then
+ return 1
+ fi
+ # Did we timeout waiting for data
+ # or did we get data?
+ if [[ $transport_status -ne 0 ]]; then
+ continue
+ fi
+ # Check with modules first, needed so we don't skip them.
+ for module in $modules_on_connect; do
+ module_${module}_on_connect "$line"
+ done
+ if [[ "$line" =~ ^:[^\ ]+\ +${numeric_RPL_MOTD} ]]; then
+ continue
+ fi
+ log_raw_in "$line"
+ if [[ "$line" =~ ^:[^\ ]+\ +([0-9]{3})\ +([^ ]+)\ +(.*) ]]; then
+ local numeric="${BASH_REMATCH[1]}"
+ # We use this to check below for our own nick.
+ local numericnick="${BASH_REMATCH[2]}"
+ local data="${BASH_REMATCH[3]}"
+ case "$numeric" in
+ "$numeric_RPL_MOTDSTART")
+ log_info "Motd is not displayed in log";
+ ;;
+ "$numeric_RPL_YOURHOST")
+ if [[ $line =~ ^:([^ ]+) ]]; then # just to get the server name, this should always be true
+ server_name="${BASH_REMATCH[1]}"
+ fi
+ ;;
+ "$numeric_RPL_WELCOME")
+ # This should work
+ server_nick_current="$numericnick"
+ ;;
+ # We don't care about these and don't want to show it as unhandled.
+ "$numeric_RPL_CREATED"|"$numeric_RPL_LUSERCLIENT"|"$numeric_RPL_LUSEROP"|"$numeric_RPL_LUSERUNKNOWN"|"$numeric_RPL_LUSERCHANNELS"|"$numeric_RPL_LUSERME"|"$numeric_RPL_LOCALUSERS"|"$numeric_RPL_GLOBALUSERS"|"$numeric_RPL_STATSCONN")
+ continue
+ ;;
+ "$numeric_RPL_MYINFO")
+ server_004="$data"
+ server_004=$(tr -d $'\r\n' <<< "$server_004") # Get rid of ending newline
+ ;;
+ "$numeric_RPL_ISUPPORT")
+ server_005+=" $data"
+ server_005=$(tr -d $'\r\n' <<< "$server_005") # Get rid of newlines
+ server_005="${server_005/ :are supported by this server/}" # Get rid of :are supported by this server
+ server_handle_005 "$line"
+ ;;
+ "$numeric_ERR_NICKNAMEINUSE"|"$numeric_ERR_ERRONEUSNICKNAME")
+ server_handle_nick_in_use
+ ;;
+ "$numeric_RPL_ENDOFMOTD"|"$numeric_ERR_NOMOTD")
+ sleep 1
+ log_info_stdout 'Connected'
+ server_connected=1
+ break
+ ;;
+ *)
+ if [[ -z "${numerics[10#${numeric}]}" ]]; then
+ log_info_file unknown_data.log "Unknown numeric during connect: $numeric Data: $data"
+ else
+ log_info_file unknown_data.log "Known but not handled numeric during connect: $numeric Data: $data"
+ fi
+ ;;
+ esac
+ fi
+ server_handle_ping "$line"
+ done
+}
diff --git a/lib/time.sh b/lib/time.sh
new file mode 100644
index 0000000..befce82
--- /dev/null
+++ b/lib/time.sh
@@ -0,0 +1,110 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Functions for working with time.
+#---------------------------------------------------------------------
+
+#---------------------------------------------------------------------
+## Check if a set time has passed
+## @Type API
+## @param Unix timestamp to check against
+## @param Number of seconds
+## @return 0 If at least the given number of seconds has passed
+## @return 1 If it hasn't
+#---------------------------------------------------------------------
+time_check_interval() {
+ local newtime=
+ time_get_current 'newtime'
+ (( ( newtime - $1 ) > $2 ))
+}
+
+
+#---------------------------------------------------------------------
+## Get current time (seconds since 1970-01-01 00:00:00 UTC)
+## @Type API
+## @param Variable to return current timestamp in
+#---------------------------------------------------------------------
+time_get_current() {
+ printf -v "$1" '%s' "$(( time_initial + SECONDS ))"
+}
+
+
+#---------------------------------------------------------------------
+## Returns how long a time interval is in a human readable format.
+## @Type API
+## @param Time interval
+## @param Variable to return result in.
+## @Note Modified version of function posted by goedel at
+## @Note http://forum.bash-hackers.org/index.php?topic=59.0
+#---------------------------------------------------------------------
+time_format_difference() {
+ local tdiv=$1
+ local tmod i
+ local output=""
+
+ for ((i=0; i < ${#time_format_units[@]}; ++i)); do
+ # n means no limit.
+ if [[ ${time_format_unitspan[i]} == n ]]; then
+ tmod=$tdiv
+ else
+ (( tmod = tdiv % time_format_unitspan[i] ))
+ (( tdiv = tdiv / time_format_unitspan[i] ))
+ fi
+ output="$tmod${time_format_units[i]} $output"
+ [[ $tdiv = 0 ]] && break
+ done
+
+ printf -v "$2" '%s' "${output% }"
+}
+
+###########################################################################
+# Internal functions to core or this file below this line! #
+# Module authors: go away #
+###########################################################################
+
+#---------------------------------------------------------------------
+## Array used for time_format_difference
+## @Type Private
+#---------------------------------------------------------------------
+declare -r time_format_units=( s min h d mon )
+#---------------------------------------------------------------------
+## Array used for time_format_difference
+## @Type Private
+## @Note n means no limit.
+#---------------------------------------------------------------------
+declare -r time_format_unitspan=( 60 60 24 30 n )
+
+#---------------------------------------------------------------------
+## Initial timestamp that we use to get current time later on.
+## @Type Private
+#---------------------------------------------------------------------
+time_initial=''
+
+#---------------------------------------------------------------------
+## Set up time variables
+## @Type Private
+#---------------------------------------------------------------------
+time_init() {
+ # Set up initial env
+ time_initial="$(date -u +%s)"
+ SECONDS=0
+}
diff --git a/logs/1318590352/main.log b/logs/1318590352/main.log
new file mode 100644
index 0000000..9bbd74a
--- /dev/null
+++ b/logs/1318590352/main.log
@@ -0,0 +1,12 @@
+- 2011-10-14 12:05:52 INFO Log directory is logs/1318590352
+- 2011-10-14 12:05:52 INFO Loading transport
+- 2011-10-14 12:05:52 INFO Loading modules
+- 2011-10-14 12:05:53 INFO Connecting to "ipv6.chat.freenode.net:6667"...
+- 2011-10-14 12:05:55 INFO logging in as pbot_exterminator...
+- 2011-10-14 12:05:59 INFO Motd is not displayed in log
+- 2011-10-14 12:05:59 INFO Identifying...
+- 2011-10-14 12:06:01 INFO Connected
+- 2011-10-14 12:06:01 INFO Setting umodes: +isB-w
+- 2011-10-14 12:06:01 INFO Joining #botwars
+- 2011-10-14 12:06:04 INFO Joining #otherchannel channelkey
+- 2011-10-14 12:09:59 INFO Bot quit gracefully
diff --git a/logs/1318590811/main.log b/logs/1318590811/main.log
new file mode 100644
index 0000000..8e7814a
--- /dev/null
+++ b/logs/1318590811/main.log
@@ -0,0 +1,12 @@
+- 2011-10-14 12:13:32 INFO Log directory is logs/1318590811
+- 2011-10-14 12:13:32 INFO Loading transport
+- 2011-10-14 12:13:32 INFO Loading modules
+- 2011-10-14 12:13:33 INFO Connecting to "ipv6.chat.freenode.net:6667"...
+- 2011-10-14 12:13:35 INFO logging in as pbot_exterminator...
+- 2011-10-14 12:13:39 INFO Motd is not displayed in log
+- 2011-10-14 12:13:39 INFO Identifying...
+- 2011-10-14 12:13:42 INFO Connected
+- 2011-10-14 12:13:42 INFO Setting umodes: +isB-w
+- 2011-10-14 12:13:42 INFO Joining #botwars
+- 2011-10-14 12:13:45 INFO Joining #otherchannel channelkey
+- 2011-10-14 12:16:13 INFO Bot quit gracefully
diff --git a/logs/1318591023/main.log b/logs/1318591023/main.log
new file mode 100644
index 0000000..171153c
--- /dev/null
+++ b/logs/1318591023/main.log
@@ -0,0 +1,12 @@
+- 2011-10-14 12:17:04 INFO Log directory is logs/1318591023
+- 2011-10-14 12:17:04 INFO Loading transport
+- 2011-10-14 12:17:04 INFO Loading modules
+- 2011-10-14 12:17:05 INFO Connecting to "ipv6.chat.freenode.net:6667"...
+- 2011-10-14 12:17:07 INFO logging in as pbot_smasher...
+- 2011-10-14 12:17:10 INFO Motd is not displayed in log
+- 2011-10-14 12:17:11 INFO Identifying...
+- 2011-10-14 12:17:13 INFO Connected
+- 2011-10-14 12:17:13 INFO Setting umodes: +isB-w
+- 2011-10-14 12:17:13 INFO Joining #botwars
+- 2011-10-14 12:17:16 INFO Joining #otherchannel channelkey
+- 2011-10-14 12:54:23 INFO Bot quit gracefully
diff --git a/logs/1318593271/main.log b/logs/1318593271/main.log
new file mode 100644
index 0000000..a50c909
--- /dev/null
+++ b/logs/1318593271/main.log
@@ -0,0 +1,12 @@
+- 2011-10-14 12:54:32 INFO Log directory is logs/1318593271
+- 2011-10-14 12:54:32 INFO Loading transport
+- 2011-10-14 12:54:32 INFO Loading modules
+- 2011-10-14 12:54:33 INFO Connecting to "ipv6.chat.freenode.net:6667"...
+- 2011-10-14 12:54:33 INFO logging in as pbot_smasher...
+- 2011-10-14 12:54:34 INFO Motd is not displayed in log
+- 2011-10-14 12:54:34 INFO Identifying...
+- 2011-10-14 12:54:36 INFO Connected
+- 2011-10-14 12:54:36 INFO Setting umodes: +isB-w
+- 2011-10-14 12:54:36 INFO Joining #botwars
+- 2011-10-14 12:54:39 INFO Joining #otherchannel channelkey
+- 2011-10-14 12:56:17 INFO Bot quit gracefully
diff --git a/logs/1318593379/main.log b/logs/1318593379/main.log
new file mode 100644
index 0000000..3d706ae
--- /dev/null
+++ b/logs/1318593379/main.log
@@ -0,0 +1,12 @@
+- 2011-10-14 12:56:19 INFO Log directory is logs/1318593379
+- 2011-10-14 12:56:19 INFO Loading transport
+- 2011-10-14 12:56:19 INFO Loading modules
+- 2011-10-14 12:56:20 INFO Connecting to "ipv6.chat.freenode.net:6667"...
+- 2011-10-14 12:56:21 INFO logging in as pbot_smasher...
+- 2011-10-14 12:56:21 INFO Motd is not displayed in log
+- 2011-10-14 12:56:22 INFO Identifying...
+- 2011-10-14 12:56:24 INFO Connected
+- 2011-10-14 12:56:24 INFO Setting umodes: +isB-w
+- 2011-10-14 12:56:24 INFO Joining #botwars
+- 2011-10-14 12:56:27 INFO Joining #otherchannel channelkey
+- 2011-10-14 12:59:39 INFO Bot quit gracefully
diff --git a/logs/1318593584/main.log b/logs/1318593584/main.log
new file mode 100644
index 0000000..611bee8
--- /dev/null
+++ b/logs/1318593584/main.log
@@ -0,0 +1,12 @@
+- 2011-10-14 12:59:44 INFO Log directory is logs/1318593584
+- 2011-10-14 12:59:44 INFO Loading transport
+- 2011-10-14 12:59:44 INFO Loading modules
+- 2011-10-14 12:59:45 INFO Connecting to "ipv6.chat.freenode.net:6667"...
+- 2011-10-14 12:59:45 INFO logging in as pbot_smasher...
+- 2011-10-14 12:59:46 INFO Motd is not displayed in log
+- 2011-10-14 12:59:46 INFO Identifying...
+- 2011-10-14 12:59:48 INFO Connected
+- 2011-10-14 12:59:49 INFO Setting umodes: +isB-w
+- 2011-10-14 12:59:49 INFO Joining #botwars
+- 2011-10-14 12:59:52 INFO Joining #otherchannel channelkey
+- 2011-10-14 13:00:32 INFO Bot quit gracefully
diff --git a/logs/1318593636/main.log b/logs/1318593636/main.log
new file mode 100644
index 0000000..4805a2f
--- /dev/null
+++ b/logs/1318593636/main.log
@@ -0,0 +1,12 @@
+- 2011-10-14 13:00:36 INFO Log directory is logs/1318593636
+- 2011-10-14 13:00:36 INFO Loading transport
+- 2011-10-14 13:00:36 INFO Loading modules
+- 2011-10-14 13:00:37 INFO Connecting to "ipv6.chat.freenode.net:6667"...
+- 2011-10-14 13:00:37 INFO logging in as pbot_smasher...
+- 2011-10-14 13:00:38 INFO Motd is not displayed in log
+- 2011-10-14 13:00:38 INFO Identifying...
+- 2011-10-14 13:00:40 INFO Connected
+- 2011-10-14 13:00:40 INFO Setting umodes: +isB-w
+- 2011-10-14 13:00:40 INFO Joining #botwars
+- 2011-10-14 13:00:43 INFO Joining #otherchannel channelkey
+- 2011-10-14 13:00:57 INFO Bot quit gracefully
diff --git a/modules/m_assign_mode.sh b/modules/m_assign_mode.sh
new file mode 100644
index 0000000..7fb4ea1
--- /dev/null
+++ b/modules/m_assign_mode.sh
@@ -0,0 +1,228 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# Copyright (C) 2007-2008 EmErgE <halt.system@gmail.com> #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Channel modes
+#---------------------------------------------------------------------
+
+module_assign_mode_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ commands_register "$1" 'op' || return 1
+ commands_register "$1" 'deop' || return 1
+ commands_register "$1" 'halfop' || return 1
+ commands_register "$1" 'dehalfop' || return 1
+ commands_register "$1" 'voice' || return 1
+ commands_register "$1" 'devoice' || return 1
+ commands_register "$1" 'protect' || return 1
+ commands_register "$1" 'deprotect' || return 1
+ commands_register "$1" 'topic' || return 1
+ helpentry_module_assign_mode_description="Provides op, deop and related commands."
+
+ local help_cmd
+ for help_cmd in op deop halfop halfdeop voice devoice protect deprotect; do
+ printf -v "helpentry_assign_mode_${help_cmd}_syntax" '<#channel> <nick>'
+ printf -v "helpentry_assign_mode_${help_cmd}_description" "$(tr 'a-z' 'A-Z' <<< "${help_cmd:0:1}")${help_cmd:1} <nick> in <#channel>."
+ done
+
+ helpentry_assign_mode_topic_syntax='<#channel> <new topic>'
+ helpentry_assign_mode_topic_description='Change topic to <new topic> in <#channel>'
+}
+
+module_assign_mode_UNLOAD() {
+ return 0
+}
+
+module_assign_mode_REHASH() {
+ return 0
+}
+
+module_assign_mode_handler_op() {
+ local sender="$1"
+ local sendon_channel="$2"
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(#[^ ]+)\ ([^ ]+) ]]; then
+ local channel="${BASH_REMATCH[1]}"
+ local nick="${BASH_REMATCH[2]}"
+ if access_check_capab "op" "$sender" "$channel"; then
+ send_modes "$channel" "+o $nick"
+ else
+ access_fail "$sender" "make the bot op somebody" "op"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "op" "<#channel> <nick>"
+ fi
+}
+
+module_assign_mode_handler_deop() {
+ local sender="$1"
+ local sendon_channel="$2"
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(#[^ ]+)\ ([^ ]+) ]]; then
+ local channel="${BASH_REMATCH[1]}"
+ local nick="${BASH_REMATCH[2]}"
+ if access_check_capab "op" "$sender" "$channel"; then
+ send_modes "$channel" "-o $nick"
+ else
+ access_fail "$sender" "make the bot deop somebody" "op"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "deop" "<#channel> <nick>"
+ fi
+}
+
+module_assign_mode_handler_halfop() {
+ local sender="$1"
+ local sendon_channel="$2"
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(#[^ ]+)\ ([^ ]+) ]]; then
+ local channel="${BASH_REMATCH[1]}"
+ local nick="${BASH_REMATCH[2]}"
+ if access_check_capab "halfop" "$sender" "$channel"; then
+ send_modes "$channel" "+h $nick"
+ else
+ access_fail "$sender" "make the bot halfop somebody" "halfop"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "halfop" "<#channel> <nick>"
+ fi
+}
+
+module_assign_mode_handler_dehalfop() {
+ local sender="$1"
+ local sendon_channel="$2"
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(#[^ ]+)\ ([^ ]+) ]]; then
+ local channel="${BASH_REMATCH[1]}"
+ local nick="${BASH_REMATCH[2]}"
+ if access_check_capab "halfop" "$sender" "$channel"; then
+ send_modes "$channel" "-h $nick"
+ else
+ access_fail "$sender" "make the bot dehalfop somebody" "halfop"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "dehalfop" "<#channel> <nick>"
+ fi
+}
+
+module_assign_mode_handler_voice() {
+ local sender="$1"
+ local sendon_channel="$2"
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(#[^ ]+)\ ([^ ]+) ]]; then
+ local channel="${BASH_REMATCH[1]}"
+ local nick="${BASH_REMATCH[2]}"
+ if access_check_capab "voice" "$sender" "$channel"; then
+ send_modes "$channel" "+v $nick"
+ else
+ access_fail "$sender" "make the bot give voice to somebody" "voice"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "voice" "<#channel> <nick>"
+ fi
+}
+
+module_assign_mode_handler_devoice() {
+ local sender="$1"
+ local sendon_channel="$2"
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(#[^ ]+)\ ([^ ]+) ]]; then
+ local channel="${BASH_REMATCH[1]}"
+ local nick="${BASH_REMATCH[2]}"
+ if access_check_capab "voice" "$sender" "$channel"; then
+ send_modes "$channel" "-v $nick"
+ else
+ access_fail "$sender" "make the bot take voice from somebody" "voice"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "devoice" "<#channel> <nick>"
+ fi
+}
+
+module_assign_mode_handler_protect() {
+ local sender="$1"
+ local sendon_channel="$2"
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(#[^ ]+)\ ([^ ]+) ]]; then
+ local channel="${BASH_REMATCH[1]}"
+ local nick="${BASH_REMATCH[2]}"
+ if access_check_capab "protect" "$sender" "$channel"; then
+ send_modes "$channel" "+a $nick"
+ else
+ access_fail "$sender" "make the bot protect somebody" "protect"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "protect" "<#channel> <nick>"
+ fi
+}
+
+module_assign_mode_handler_deprotect() {
+ local sender="$1"
+ local sendon_channel="$2"
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(#[^ ]+)\ ([^ ]+) ]]; then
+ local channel="${BASH_REMATCH[1]}"
+ local nick="${BASH_REMATCH[2]}"
+ if access_check_capab "protect" "$sender" "$channel"; then
+ send_modes "$channel" "-a $nick"
+ else
+ access_fail "$sender" "make the bot deprotect somebody" "protect"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "deprotect" "<#channel> <nick>"
+ fi
+}
+
+module_assign_mode_handler_topic() {
+ local sender="$1"
+ local sendon_channel="$2"
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(#[^ ]+)\ (.+) ]]; then
+ local channel="${BASH_REMATCH[1]}"
+ local message="${BASH_REMATCH[2]}"
+ if access_check_capab "topic" "$sender" "$channel"; then
+ send_topic "$channel" "$message"
+ else
+ access_fail "$sender" "change the topic" "topic"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "topic" "<#channel> <new topic>"
+ fi
+}
diff --git a/modules/m_autojoin.sh b/modules/m_autojoin.sh
new file mode 100644
index 0000000..468bffc
--- /dev/null
+++ b/modules/m_autojoin.sh
@@ -0,0 +1,63 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## This module does autojoin after connect.
+#---------------------------------------------------------------------
+
+module_autojoin_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='after_connect'
+ helpentry_module_autojoin_description="Provides support for autojoining channels."
+}
+
+module_autojoin_UNLOAD() {
+ unset module_autojoin_join_from_config
+}
+
+module_autojoin_REHASH() {
+ module_autojoin_join_from_config
+ return 0
+}
+
+#---------------------------------------------------------------------
+## Autojoin channels from config.
+## @Type Private
+#---------------------------------------------------------------------
+module_autojoin_join_from_config() {
+ local channel
+ for channel in "${config_module_autojoin_channels[@]}"; do
+ # No quotes around channel because second word of it may be a key
+ # and list_contains just uses the first 2 arguments so a
+ # third one will be ignored.
+ if ! list_contains "channels_current" $channel; then
+ log_info "Joining $channel"
+ # No quotes here because then second argument can be a key
+ channels_join $channel
+ sleep 2
+ fi
+ done
+}
+
+# Called after bot has connected
+module_autojoin_after_connect() {
+ module_autojoin_join_from_config
+}
diff --git a/modules/m_check_numerics.sh b/modules/m_check_numerics.sh
new file mode 100644
index 0000000..44c97f5
--- /dev/null
+++ b/modules/m_check_numerics.sh
@@ -0,0 +1,45 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## For debugging, report any unknown numerics.
+#---------------------------------------------------------------------
+
+module_check_numerics_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='on_numeric'
+ helpentry_module_check_numerics_description="Debugging module to check if any numeric we get is unknown."
+}
+
+module_check_numerics_UNLOAD() {
+ return 0
+}
+
+module_check_numerics_REHASH() {
+ return 0
+}
+
+module_check_numerics_on_numeric() {
+ # Make sure it is in base 10 here.
+ if [[ -z "${numerics[10#${1}]}" ]]; then
+ log_warning_file unknown_data.log "Unknown numeric $1 Data: $2"
+ fi
+}
diff --git a/modules/m_commands.sh b/modules/m_commands.sh
new file mode 100644
index 0000000..ca1f759
--- /dev/null
+++ b/modules/m_commands.sh
@@ -0,0 +1,103 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# Copyright (C) 2007-2008 Vsevolod Kozlov #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Command-related utility commands
+#---------------------------------------------------------------------
+
+module_commands_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ commands_register "$1" 'provides' || return 1
+ commands_register "$1" 'commands' || return 1
+ helpentry_module_commands_description="Provides a set of command-related commands."
+
+ helpentry_commands_provides_syntax='<command>'
+ helpentry_commands_provides_description='Shows which module provides command <command>'
+
+ helpentry_commands_commands_syntax='[<module>]'
+ helpentry_commands_commands_description='Lists commands available in <module>. If module name is not given, lists all commands'
+}
+
+module_commands_UNLOAD() {
+ return 0
+}
+
+module_commands_REHASH() {
+ return 0
+}
+
+module_commands_handler_provides() {
+ local sender="$1"
+ local parameters="$3"
+ if [[ $parameters =~ ^([a-zA-Z0-9][^ ]*)( [^ ]+)? ]]; then # regex suggested by AnMaster
+ local command_name="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
+ local target
+ if [[ $2 =~ ^# ]]; then
+ target="$2"
+ else
+ parse_hostmask_nick "$sender" 'target'
+ fi
+ local module_name
+ commands_provides "$command_name" module_name
+ if [[ -z $module_name ]]; then # No such command
+ send_msg "$target" "Command \"$command_name\" does not exist."
+ else
+ send_msg "$target" "Command \"$command_name\" is provided by module \"$module_name\""
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "provides" "<modulename>"
+ fi
+}
+
+module_commands_handler_commands() {
+ local parameters="$3"
+ local target
+ if [[ $2 =~ ^# ]]; then
+ target="$2"
+ else
+ parse_hostmask_nick "$1" 'target'
+ fi
+ if [[ -z $parameters ]]; then
+ send_msg "$target" "${format_bold}Available commands${format_bold}: ${commands_commands//,/, }"
+ else
+ # So we got a parameter
+ local module_name
+ if [[ $parameters =~ ^([^ ]+)\ *$ ]]; then
+ module_name="${BASH_REMATCH[1]}"
+ else
+ send_notice "$target" "\"$parameters\" is not a valid module name"
+ return 1
+ fi
+ local commands_in_module
+ commands_in_module "$module_name" 'commands_in_module'
+ if [[ $commands_in_module ]]; then
+ send_msg "$target" "${format_bold}Available commands (in module \"$module_name\")${format_bold}: ${commands_in_module//,/, }"
+ elif list_contains "modules_loaded" "$module_name"; then
+ send_notice "$target" "Module \"$module_name\" provides no commands"
+ else
+ send_notice "$target" "Module \"$module_name\" is not loaded"
+ fi
+ fi
+}
diff --git a/modules/m_ctcp.sh b/modules/m_ctcp.sh
new file mode 100644
index 0000000..dfbe874
--- /dev/null
+++ b/modules/m_ctcp.sh
@@ -0,0 +1,91 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Handle CTCP
+#---------------------------------------------------------------------
+
+module_ctcp_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='after_load on_PRIVMSG'
+ helpentry_module_ctcp_description="Answers CTCP requests."
+}
+
+module_ctcp_UNLOAD() {
+ return 0
+}
+
+module_ctcp_REHASH() {
+ return 0
+}
+
+module_ctcp_after_load() {
+ if [[ -z $config_module_ctcp_version_reply ]]; then
+ log_error "VERSION reply (config_module_ctcp_version_reply) must be set in config to use CTCP module."
+ return 1
+ fi
+}
+
+# Called on a PRIVMSG
+#
+# $1 = from who (n!u@h)
+# $2 = to who (channel or botnick)
+# $3 = the message
+module_ctcp_on_PRIVMSG() {
+ local sender="$1"
+ local query="$3"
+ # We can't use regex here. For some unknown reason bash drops \001 from
+ # regex.
+ if [[ $query = $'\001'* ]]; then
+ # Get rid of \001 in the string.
+ local data="${query//$'\001'}"
+ local ctcp_command ctcp_parameters
+ # Split it up into command and any parameters.
+ read -r ctcp_command ctcp_parameters <<< "$data"
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ case "$ctcp_command" in
+ "CLIENTINFO")
+ send_nctcp "$sendernick" "CLIENTINFO CLIENTINFO PING SOURCE TIME VERSION"
+ ;;
+ "PING")
+ send_nctcp "$sendernick" "PING $ctcp_parameters"
+ ;;
+ "SOURCE")
+ send_nctcp "$sendernick" "SOURCE http://envbot.org"
+ ;;
+ "TIME")
+ send_nctcp "$sendernick" "TIME $(date +'%Y-%m-%d %k:%M:%S')"
+ ;;
+ "VERSION")
+ send_nctcp "$sendernick" "VERSION $config_module_ctcp_version_reply"
+ ;;
+ *)
+ # So we didn't handle this CTCP? Return 0 then, someone else may want it.
+ return 0
+ ;;
+ esac
+ # See above. We didn't fall back to not handle it and did not return
+ # so therefore we must have handled it.
+ return 1
+ fi
+ return 0
+}
diff --git a/modules/m_dice.sh b/modules/m_dice.sh
new file mode 100644
index 0000000..7c4c6ab
--- /dev/null
+++ b/modules/m_dice.sh
@@ -0,0 +1,84 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# Copyright (C) 2007-2008 Vsevolod Kozlov #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Rolls dies.
+#---------------------------------------------------------------------
+
+module_dice_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ commands_register "$1" 'roll' || return 1
+ helpentry_module_dice_description="Rolls dies for you."
+
+ helpentry_dice_roll_syntax="<dies>d<sides>"
+ helpentry_dice_roll_description="Rolls <dies> dies, each <sides> sides."
+}
+
+module_dice_UNLOAD() {
+ return 0
+}
+
+module_dice_REHASH() {
+ return 0
+}
+
+module_dice_handler_roll() {
+ local sender="$1"
+ local parameters="$3"
+ if [[ $parameters =~ ^([0-9]+)d([0-9]+)$ ]]; then
+ local how_much_times="${BASH_REMATCH[1]}"
+ local how_many_sides="${BASH_REMATCH[2]}"
+ local target=
+ if [[ $2 =~ ^# ]]; then
+ target="$2"
+ else
+ parse_hostmask_nick "$sender" 'target'
+ fi
+ local insane=0
+ # Chech if number of dies and sides are sane.
+ if (( ($how_many_sides < 2 || $how_many_sides > 100)
+ || ($how_much_times < 1 || $how_much_times > 100) )); then
+ log_warning "Tried to roll $how_much_times dies $how_many_sides sides each!"
+ log_warning "This is above the allowed maximum or below the allowed minimum, and was aborted."
+ send_msg "$target" "You can't roll that."
+ return 0
+ fi
+ # Roll $how_much_times dies, each with $how_many_sides sides.
+ local result=""
+ local total=0
+ for (( i=0; $i < $how_much_times; i+=1 )); do
+ local rolled=$(( ($RANDOM % $how_many_sides) + 1 ))
+ result+="$rolled, "
+ ((total += $rolled))
+ done
+ result=${result%, }
+ if [[ $how_much_times != 1 ]]; then
+ result+=" with the grand total of $total"
+ fi
+ send_msg "$target" "You rolled ${result}."
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "roll" "<dies>d<sides>"
+ fi
+}
diff --git a/modules/m_die.sh b/modules/m_die.sh
new file mode 100644
index 0000000..199089a
--- /dev/null
+++ b/modules/m_die.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# Copyright (C) 2007-2008 EmErgE <halt.system@gmail.com> #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Quit the bot.
+#---------------------------------------------------------------------
+
+module_die_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ commands_register "$1" 'die' || return 1
+ commands_register "$1" 'restart' || return 1
+ helpentry_module_die_description="Commands to shut down and restart bot."
+ helpentry_die_die_syntax='[reason]'
+ helpentry_die_die_description='Quit with an optional quit reason.'
+ helpentry_die_restart_syntax='[reason]'
+ helpentry_die_restart_description='Disconnect the bot with an optional quit reason, then rerun itself.'
+}
+
+module_die_UNLOAD() {
+ return 0
+}
+
+module_die_REHASH() {
+ return 0
+}
+
+module_die_handler_die() {
+ local sender="$1"
+ if access_check_owner "$sender"; then
+ local parameters="$3"
+ access_log_action "$sender" "made the bot die with reason: $parameters"
+ local reason=
+ if [[ $parameters ]]; then
+ reason=": $parameters"
+ fi
+ bot_quit "Dying ($sender)$reason"
+ else
+ access_fail "$sender" "make the bot die" "owner"
+ fi
+}
+
+module_die_handler_restart() {
+ local sender="$1"
+ if access_check_owner "$sender"; then
+ local parameters="$3"
+ access_log_action "$sender" "made the bot restart with reason: $parameters"
+ local reason=
+ if [[ $parameters ]]; then
+ reason=": $parameters"
+ fi
+ bot_restart "Restarting ($sender)$reason"
+ else
+ access_fail "$sender" "make the bot restart" "owner"
+ fi
+}
diff --git a/modules/m_dumpvars.sh b/modules/m_dumpvars.sh
new file mode 100644
index 0000000..77a9c14
--- /dev/null
+++ b/modules/m_dumpvars.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Debug module, dump all variables to console.
+#---------------------------------------------------------------------
+
+module_dumpvars_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ commands_register "$1" 'dumpvars' || return 1
+ helpentry_module_dumpvars_description="Debugging module to dump all variables in the bot."
+ helpentry_dumpvars_dumpvars_syntax=''
+ helpentry_dumpvars_dumpvars_description='Dump all variables to STDOUT.'
+}
+
+module_dumpvars_UNLOAD() {
+ return 0
+}
+
+module_dumpvars_REHASH() {
+ return 0
+}
+
+module_dumpvars_handler_dumpvars() {
+ local sender="$1"
+ if access_check_owner "$sender"; then
+ # This is hackish, we only display
+ # lines unique to "file" 1.
+ # Also remove one variable that may fill our scrollback.
+ access_log_action "$sender" "a dump of variables"
+ comm -2 -3 <(declare) <(declare -f) | grep -Ev '^module_quote_quotes'
+ else
+ access_fail "$sender" "dump variables to STDOUT" "owner"
+ fi
+}
diff --git a/modules/m_factoids.sh b/modules/m_factoids.sh
new file mode 100644
index 0000000..ba164ed
--- /dev/null
+++ b/modules/m_factoids.sh
@@ -0,0 +1,461 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Simple factoids module using SQLite3
+#---------------------------------------------------------------------
+
+module_factoids_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='after_load on_PRIVMSG'
+ commands_register "$1" 'learn' || return 1
+ commands_register "$1" 'forget' || return 1
+ commands_register "$1" 'lock_factoid' 'lock factoid' || return 1
+ commands_register "$1" 'unlock_factoid' 'unlock factoid' || return 1
+ commands_register "$1" 'whatis' || return 1
+ commands_register "$1" 'factoid_stats' 'factoid stats' || return 1
+ helpentry_module_factoids_description="Provides a factoid database."
+
+ helpentry_factoids_learn_syntax='<key> (as|is|are|=) <value>'
+ helpentry_factoids_learn_description='Teach the bot a new factoid.'
+
+ helpentry_factoids_forget_syntax='<key>'
+ helpentry_factoids_forget_description='Make the bot forget the factoid <key>.'
+
+ helpentry_factoids_lock_factoid_syntax='<key>'
+ helpentry_factoids_lock_factoid_description='Prevent normal users from changing the factoid <key>.'
+
+ helpentry_factoids_unlock_factoid_syntax='<key>'
+ helpentry_factoids_unlock_factoid_description='Allow changes to a previously locked factoid <key>.'
+
+ helpentry_factoids_whatis_syntax='<key>'
+ helpentry_factoids_whatis_description='Look up the factoid <key>.'
+
+ helpentry_factoids_factoid_stats_syntax=''
+ helpentry_factoids_factoid_stats_description='Report some statistics on the factoid database.'
+}
+
+
+module_factoids_UNLOAD() {
+ # Ok this is a LOT. I hope I got all...
+ unset module_factoids_set module_factoids_remove module_factoids_parse_assignment
+ unset module_factoids_parse_key module_factoids_parse_value
+ unset module_factoids_set_INSERT_or_UPDATE module_factoids_send_factoid
+ unset module_factoids_get_count module_factoids_get_locked_count
+ unset module_factoids_is_locked module_factoids_lock module_factoids_unlock
+ unset module_factoids_SELECT module_factoids_INSERT module_factoids_UPDATE module_factoids_DELETE
+}
+
+
+module_factoids_REHASH() {
+ return 0
+}
+
+
+# Called after module has loaded.
+module_factoids_after_load() {
+ modules_depends_register "factoids" "sqlite3" || {
+ # This error reporting is hackish, will fix later.
+ if ! list_contains "modules_loaded" "sqlite3"; then
+ log_error "The factoids module depends upon the SQLite3 module being loaded."
+ fi
+ return 1
+ }
+ if [[ -z $config_module_factoids_table ]]; then
+ log_error "Factiods table (config_module_factoids_table) must be set in config if you want to use factoids module."
+ return 1
+ fi
+ if ! module_sqlite3_table_exists "$config_module_factoids_table"; then
+ log_error "factoids module: $config_module_factoids_table does not exist in the database file."
+ log_error "factoids module: See comment in doc/factoids.sql for how to create the table."
+ fi
+}
+
+
+#---------------------------------------------------------------------
+## Get an item from DB
+## @Type Private
+## @param Key
+## @Stdout The result of the database query.
+#---------------------------------------------------------------------
+module_factoids_SELECT() {
+ #$ sqlite3 -list data/factoids.sqlite "SELECT value from factoids WHERE name='factoids';"
+ #A system that stores useful bits of information
+ module_sqlite3_exec_sql "SELECT value FROM $config_module_factoids_table WHERE name='$(module_sqlite3_clean_string "$1")';"
+}
+
+
+#---------------------------------------------------------------------
+## Insert a new item into DB
+## @Type Private
+## @param key
+## @param value
+## @param hostmask of person who added it
+#---------------------------------------------------------------------
+module_factoids_INSERT() {
+ module_sqlite3_exec_sql \
+ "INSERT INTO $config_module_factoids_table (name, value, who) VALUES('$(module_sqlite3_clean_string "$1")', '$(module_sqlite3_clean_string "$2")', '$(module_sqlite3_clean_string "$3")');"
+}
+
+
+#---------------------------------------------------------------------
+## Change the item in DB
+## @Type Private
+## @param key
+## @param new value
+## @param hostmask of person who changed it
+#---------------------------------------------------------------------
+module_factoids_UPDATE() {
+ module_sqlite3_exec_sql \
+ "UPDATE $config_module_factoids_table SET value='$(module_sqlite3_clean_string "$2")', who='$(module_sqlite3_clean_string "$3")' WHERE name='$(module_sqlite3_clean_string "$1")';"
+}
+
+
+#---------------------------------------------------------------------
+## Remove an item
+## @Type Private
+## @param key
+#---------------------------------------------------------------------
+module_factoids_DELETE() {
+ module_sqlite3_exec_sql "DELETE FROM $config_module_factoids_table WHERE name='$(module_sqlite3_clean_string "$1")';"
+}
+
+
+#---------------------------------------------------------------------
+## How many factoids are there
+## @Type Private
+## @Stdout Count of factoids.
+#---------------------------------------------------------------------
+module_factoids_get_count() {
+ module_sqlite3_exec_sql "SELECT COUNT(name) FROM $config_module_factoids_table;"
+}
+
+
+#---------------------------------------------------------------------
+## How many locked factoids are there
+## @Type Private
+## @Stdout Count of locked factoids.
+#---------------------------------------------------------------------
+module_factoids_get_locked_count() {
+ module_sqlite3_exec_sql "SELECT COUNT(name) FROM $config_module_factoids_table WHERE is_locked='1';"
+}
+
+
+#---------------------------------------------------------------------
+## Check if factoid is locked or not.
+## @Type Private
+## @param key
+## @return 0 locked
+## @return 1 not locked
+#---------------------------------------------------------------------
+module_factoids_is_locked() {
+ local lock="$(module_sqlite3_exec_sql "SELECT is_locked FROM $config_module_factoids_table WHERE name='$(module_sqlite3_clean_string "$1")';")"
+ if [[ $lock == "1" ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+
+#---------------------------------------------------------------------
+## Lock a factoid against changes from non-owners
+## @Type Private
+## @param key
+#---------------------------------------------------------------------
+module_factoids_lock() {
+ module_sqlite3_exec_sql "UPDATE $config_module_factoids_table SET is_locked='1' WHERE name='$(module_sqlite3_clean_string "$1")';"
+}
+
+
+#---------------------------------------------------------------------
+## Unlock a factoid from protection against non-owners
+## @Type Private
+## @param key
+#---------------------------------------------------------------------
+module_factoids_unlock() {
+ module_sqlite3_exec_sql "UPDATE $config_module_factoids_table SET is_locked='0' WHERE name='$(module_sqlite3_clean_string "$1")';"
+}
+
+
+#---------------------------------------------------------------------
+## Wrapper, call either INSERT or UPDATE
+## @Type Private
+## @param key
+## @param value
+## @param hostmask of person set it
+#---------------------------------------------------------------------
+module_factoids_set_INSERT_or_UPDATE() {
+ if [[ $(module_factoids_SELECT "$1") ]]; then
+ module_factoids_UPDATE "$1" "$2" "$3"
+ else
+ module_factoids_INSERT "$1" "$2" "$3"
+ fi
+}
+
+
+#---------------------------------------------------------------------
+## Wrapper, call either INSERT or UPDATE
+## @Type Private
+## @param key
+## @param value
+## @param sender
+## @param channel
+#---------------------------------------------------------------------
+module_factoids_set() {
+ local key="$1"
+ local value="$2"
+ local sender="$3"
+ local channel="$4"
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ if module_factoids_is_locked "$key"; then
+ if access_check_capab "factoid_admin" "$sender" "GLOBAL"; then
+ module_factoids_set_INSERT_or_UPDATE "$key" "$value" "$sender"
+ send_msg "$channel" "Ok ${sendernick}, I will remember, $key is $value"
+ else
+ access_fail "$sender" "change a locked factoid" "factoid_admin"
+ fi
+ else
+ module_factoids_set_INSERT_or_UPDATE "$key" "$value" "$sender"
+ send_msg "$channel" "Ok ${sendernick}, I will remember, $key is $value"
+ fi
+}
+
+
+#---------------------------------------------------------------------
+## Wrapper, check access
+## @Type Private
+## @param key
+## @param sender
+## @param channel
+#---------------------------------------------------------------------
+module_factoids_remove() {
+ local key="$1"
+ local sender="$2"
+ local channel="$3"
+ local value="$(module_factoids_SELECT "$(tr '[:upper:]' '[:lower:]' <<< "$key")")"
+ if [[ "$value" ]]; then
+ if module_factoids_is_locked "$key"; then
+ if access_check_capab "factoid_admin" "$sender" "GLOBAL"; then
+ module_factoids_DELETE "$key"
+ send_msg "$channel" "I forgot $key"
+ else
+ access_fail "$sender" "remove a locked factoid" "factoid_admin"
+ fi
+ else
+ module_factoids_DELETE "$key"
+ send_msg "$channel" "I forgot $key"
+ fi
+ else
+ send_msg "$channel" "I didn't have a factoid matching \"$key\""
+ fi
+}
+
+
+#---------------------------------------------------------------------
+## Send the factoid:
+## @Type Private
+## @param To where (channel or nick)
+## @param What factoid.
+#---------------------------------------------------------------------
+module_factoids_send_factoid() {
+ local channel="$1"
+ local key="$2"
+ local value="$(module_factoids_SELECT "$(tr '[:upper:]' '[:lower:]' <<< "$key")")"
+ if [[ "$value" ]]; then
+ if [[ $value =~ ^\<REPLY\>\ *(.*) ]]; then
+ send_msg "$channel" "${BASH_REMATCH[1]}"
+ elif [[ $value =~ ^\<ACTION\>\ *(.*) ]]; then
+ send_ctcp "$channel" "ACTION ${BASH_REMATCH[1]}"
+ else
+ send_msg "$channel" "$key is $value"
+ fi
+ else
+ send_msg "$channel" "I don't know what \"$key\" is."
+ fi
+}
+
+
+#---------------------------------------------------------------------
+## Parse assignment:
+## @Type Private
+## @param String to parse
+## @Note Will return using Global variables
+## @Globals $module_factoids_parse_key $module_factoids_parse_value
+#---------------------------------------------------------------------
+module_factoids_parse_assignment() {
+ local word key value
+ # Have we hit a separator yet?
+ local state=0
+ while read -rd ' ' word; do
+ case "$state" in
+ 0)
+ # If state is 1 the rest is value
+ if [[ "$word" =~ ^(as|is|are|=)$ ]]; then
+ state=1
+ else
+ key+=" $word"
+ fi
+ ;;
+ 1)
+ value+=" $word"
+ ;;
+ esac
+ # Extra space at end is intended, to make read work correctly.
+ done <<< "$1 "
+ # And clean spaces, fastest way
+ read -ra module_factoids_parse_key <<< "$key"
+ read -ra module_factoids_parse_value <<< "$value"
+}
+
+
+module_factoids_handler_learn() {
+ local sender="$1"
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ local channel="$2"
+ if ! [[ $2 =~ ^# ]]; then
+ channel="$sendernick"
+ fi
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(.+)\ (as|is|are|=)\ (.+) ]]; then
+ # Do the actual parsing elsewhere:
+ module_factoids_parse_assignment "$parameters"
+ local key="${module_factoids_parse_key[*]}"
+ local value="${module_factoids_parse_value[*]}"
+ unset module_factoids_parse_key module_factoids_parse_value
+ module_factoids_set "$(tr '[:upper:]' '[:lower:]' <<< "$key")" "$value" "$sender" "$channel"
+ else
+ feedback_bad_syntax "$sendernick" "learn" "<key> (as|is|are|=) <value>"
+ fi
+ return 1
+}
+
+module_factoids_handler_forget() {
+ local sender="$1"
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ local channel="$2"
+ if ! [[ $2 =~ ^# ]]; then
+ channel="$sendernick"
+ fi
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(.+) ]]; then
+ local key="${BASH_REMATCH[1]}"
+ module_factoids_remove "$(tr '[:upper:]' '[:lower:]' <<< "$key")" "$sender" "$channel"
+ else
+ feedback_bad_syntax "$sendernick" "forget" "<key>"
+ fi
+}
+
+module_factoids_handler_lock_factoid() {
+ local sender="$1"
+ if access_check_capab "factoid_admin" "$sender" "GLOBAL"; then
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ local channel="$2"
+ if ! [[ $2 =~ ^# ]]; then
+ channel="$sendernick"
+ fi
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(.+) ]]; then
+ local key="${BASH_REMATCH[1]}"
+ module_factoids_lock "$(tr '[:upper:]' '[:lower:]' <<< "$key")"
+ send_msg "$channel" "Ok ${sendernick}, the factoid \"$key\" is now protected from changes"
+ else
+ feedback_bad_syntax "$sendernick" "lock" "<key>"
+ fi
+ else
+ access_fail "$sender" "lock a factoid" "factoid_admin"
+ fi
+}
+
+module_factoids_handler_unlock_factoid() {
+ local sender="$1"
+ if access_check_capab "factoid_admin" "$sender" "GLOBAL"; then
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ local channel="$2"
+ if ! [[ $2 =~ ^# ]]; then
+ channel="$sendernick"
+ fi
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(.+) ]]; then
+ local key="${BASH_REMATCH[1]}"
+ module_factoids_unlock "$(tr '[:upper:]' '[:lower:]' <<< "$key")"
+ send_msg "$channel" "Ok ${sendernick}, the factoid \"$key\" is no longer protected from changes"
+ else
+ feedback_bad_syntax "$sendernick" "lock" "<key>"
+ fi
+ else
+ access_fail "$sender" "lock a factoid" "factoid_admin"
+ fi
+}
+
+module_factoids_handler_whatis() {
+ local sender="$1"
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ local channel="$2"
+ if ! [[ $2 =~ ^# ]]; then
+ channel="$sendernick"
+ fi
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(.+) ]]; then
+ local key="${BASH_REMATCH[1]}"
+ module_factoids_send_factoid "$channel" "$key"
+ else
+ feedback_bad_syntax "$sendernick" "whatis" "<key>"
+ fi
+}
+
+module_factoids_handler_factoid_stats() {
+ local sender="$1"
+ local channel="$2"
+ if ! [[ $2 =~ ^# ]]; then
+ parse_hostmask_nick "$sender" 'channel'
+ fi
+ local count="$(module_factoids_get_count)"
+ local lockedcount="$(module_factoids_get_locked_count)"
+ if [[ "$count" ]]; then
+ send_msg "$channel" "There are $count items in my factoid database. $lockedcount of the factoids are locked."
+ fi
+}
+
+module_factoids_on_PRIVMSG() {
+ local sender="$1"
+ local channel="$2"
+ if ! [[ $2 =~ ^# ]]; then
+ parse_hostmask_nick "$sender" 'channel'
+ fi
+ local query="$3"
+ # Answer question in channel if we got a factoid.
+ if [[ "$query" =~ ^((what|where|who|why|how)\ )?((is|are|were|was|to|can I find)\ )?([^\?]+)\?? ]]; then
+ local key="${BASH_REMATCH[@]: -1}"
+ local value="$(module_factoids_SELECT "$(tr '[:upper:]' '[:lower:]' <<< "$key")")"
+ if [[ "$value" ]]; then
+ module_factoids_send_factoid "$channel" "$key"
+ return 1
+ fi
+ fi
+ return 0
+}
diff --git a/modules/m_faq.sh b/modules/m_faq.sh
new file mode 100644
index 0000000..f1557dc
--- /dev/null
+++ b/modules/m_faq.sh
@@ -0,0 +1,138 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Simple FAQ module
+#---------------------------------------------------------------------
+
+module_faq_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='after_load'
+ commands_register "$1" 'faq' || return 1
+ helpentry_module_faq_description="FAQ from a file."
+
+ helpentry_faq_faq_syntax='[number|string]'
+ helpentry_faq_faq_description='Show the <number>th faq item or search for a <string> in all the faq items.'
+}
+
+module_faq_UNLOAD() {
+ unset module_faq_array module_faq_last_query_item
+ unset module_faq_load module_faq_last_query_time
+}
+
+module_faq_REHASH() {
+ module_faq_load
+}
+
+#---------------------------------------------------------------------
+## Load or reload FAQ items
+## @Type Private
+#---------------------------------------------------------------------
+module_faq_load() {
+ local i=0
+ unset module_faq_array
+ if [[ -z "$config_module_faq_file" ]]; then
+ log_error "faq module: You need to set config_module_faq_file in your config!"
+ return 1
+ elif [[ -r "$config_module_faq_file" ]]; then
+ while read -d $'\n' line ;do
+ # Skip empty lines
+ if [[ "$line" ]]; then
+ (( i++ ))
+ module_faq_array[$i]="$line"
+ fi
+ done < "${config_module_faq_file}"
+ log_info 'Loaded FAQ items'
+ return 0
+ else
+ log_error "faq module: Cannot load '${config_module_faq_file}'. File doesn't exist or can't be read."
+ return 1
+ fi
+}
+
+# Called after module has loaded.
+module_faq_after_load() {
+ module_faq_last_query_item='null'
+ module_faq_last_query_time='null'
+ module_faq_load
+}
+
+# Called on a PRIVMSG
+#
+# $1 = from who (n!u@h)
+# $2 = to who (channel or botnick)
+# $3 = the message
+module_faq_handler_faq() {
+ local sender="$1"
+ local channel="$2"
+ # If it isn't in a channel send message back to person who send it,
+ # otherwise send in channel
+ if ! [[ $2 =~ ^# ]]; then
+ parse_hostmask_nick "$sender" 'channel'
+ fi
+ local query="$3"
+ if [[ "$query" ]]; then
+ if [[ "$query" == "reload" ]]; then
+ if access_check_capab "faq_admin" "$sender" "GLOBAL"; then
+ send_msg "$channel" "Reloading FAQ items..."
+ module_faq_load
+ send_msg "$channel" "Done."
+ else
+ access_fail "$sender" "reload faq items" "faq_admin"
+ fi
+ return 0
+ fi
+ # Is it a flood? Then 1.
+ local ok=0
+ if [[ "$module_faq_last_query_item" == "$line" ]]; then
+ time_check_interval "$module_faq_last_query_time" 60 || ok=1
+ fi
+ if [[ $ok -eq 0 ]] ; then # Must be at least 1 min old or different query...
+ time_get_current 'module_faq_last_query_time'
+ # Update anti-flood variables
+ module_faq_last_query_item="$line"
+ module_faq_last_query="$query_time"
+
+ if [[ "$query" =~ ^\ *([0-9]+)\ *$ ]]; then
+ local index="${BASH_REMATCH[1]}"
+ if [[ "${module_faq_array[$index]}" ]]; then
+ send_msg "$channel" "${module_faq_array[$index]}"
+ else
+ send_msg "$channel" "That FAQ item doesn't exist"
+ fi
+ # Check length of search to be at least 3 chars
+ elif [[ "${#query}" -ge 3 ]] ; then
+ local i=0
+ while [[ $i -lt "${#module_faq_array[*]}" ]] ; do
+ (( i++ ))
+ # FIXME: This code is hard to read.
+ # This module needs rewriting...
+ if grep -qiFm 1 "$query" <<< "${module_faq_array[$i]}" ; then
+ send_msg "$channel" "${module_faq_array[$i]}"
+ break 1
+ fi
+ done
+ fi
+ else
+ log_error "FLOOD DETECTED in FAQ module"
+ fi
+ fi
+}
diff --git a/modules/m_help.sh b/modules/m_help.sh
new file mode 100644
index 0000000..3fdd9b9
--- /dev/null
+++ b/modules/m_help.sh
@@ -0,0 +1,151 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# Copyright (C) 2007-2008 Vsevolod Kozlov #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Provides help command.
+#---------------------------------------------------------------------
+
+module_help_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ commands_register "$1" 'help' || return 1
+ commands_register "$1" 'modinfo' || return 1
+ helpentry_module_help_description="Provides help and information for commands and modules."
+
+ helpentry_help_help_syntax='<command>'
+ helpentry_help_help_description='Displays help for command <command>'
+
+ helpentry_help_modinfo_syntax='<module>'
+ helpentry_help_modinfo_description='Displays a description for module <module>'
+}
+
+module_help_UNLOAD() {
+ unset module_help_fetch_module_function_data
+ unset module_help_fetch_module_data
+}
+
+module_help_REHASH() {
+ return 0
+}
+
+module_help_fetch_module_function_data() {
+ local module_name="$1"
+ local function_name="$2"
+ local target_syntax="$3"
+ local target_description="$4"
+
+ local varname_syntax="helpentry_${module_name}_${function_name}_syntax"
+ local varname_description="helpentry_${module_name}_${function_name}_description"
+ if [[ -z ${!varname_description} ]]; then
+ return 1
+ fi
+
+ printf -v "$target_description" '%s' "${!varname_description}"
+
+ if [[ ${!varname_syntax} ]]; then
+ printf -v "$target_syntax" '%s' " ${!varname_syntax}"
+ fi
+}
+
+module_help_fetch_module_data() {
+ local module_name="$1"
+ local target_description="$2"
+
+ local varname_description="helpentry_module_${module_name}_description"
+ if [[ -z ${!varname_description} ]]; then
+ return 1
+ fi
+
+ printf -v "$target_description" '%s' "${!varname_description}"
+}
+
+module_help_handler_help() {
+ local sender="$1"
+ local parameters="$3"
+ if [[ $parameters =~ ^([a-zA-Z0-9][^ ]*)( [^ ]+)? ]]; then
+ local command_name="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
+ # Look where we will reply to. We will not reply in the channel, even if the request was made in a channel, unless appropriate option is set
+ local target
+ if [[ $2 =~ ^# && $config_module_help_reply_in_channel == 1 ]]; then
+ target="$2"
+ else
+ parse_hostmask_nick "$sender" 'target'
+ fi
+ # Get the module name the command belongs to.
+ local module_name=
+ commands_provides "$command_name" 'module_name'
+ # Extract the function name.
+ local function_name=
+ hash_get 'commands_list' "$command_name" 'function_name'
+ if [[ $function_name =~ ^module_${module_name}_handler_(.+)$ ]]; then
+ function_name="${BASH_REMATCH[1]}"
+ fi
+ # Finally get the data for a specific function in specific module.
+ local syntax=
+ local description=
+ module_help_fetch_module_function_data "$module_name" "$function_name" syntax description || {
+ send_notice "$target" "Sorry, no help for ${format_bold}${command_name}${format_bold}"
+ return
+ }
+ # And send it back to the user.
+ if [[ $config_module_help_reply_in_one_line == 1 ]]; then
+ send_notice "$target" "${format_bold}${command_name}${format_bold}$syntax -- $description"
+ else
+ send_notice "$target" "${format_bold}${command_name}${format_bold}$syntax"
+ send_notice "$target" "$description"
+ fi
+ else
+ local sendernick=
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "help" "<command>"
+ fi
+}
+
+module_help_handler_modinfo() {
+ local sender="$1"
+ local parameters="$3"
+ if [[ $parameters =~ ^([^ ]+) ]]; then
+ local module_name="${BASH_REMATCH[1]}"
+ # See module_help_handler_help
+ local target
+ if [[ $2 =~ ^# && $config_module_help_reply_in_channel == 1 ]]; then
+ target="$2"
+ else
+ parse_hostmask_nick "$sender" 'target'
+ fi
+ local description=
+ module_help_fetch_module_data "$module_name" description || {
+ send_notice "$target" "Sorry, no information for module ${format_bold}${module_name}${format_bold}"
+ return
+ }
+ if [[ $config_module_help_reply_in_one_line == 1 ]]; then
+ send_notice "$target" "${format_bold}${module_name}${format_bold} -- $description"
+ else
+ send_notice "$target" "${format_bold}${module_name}${format_bold}"
+ send_notice "$target" "$description"
+ fi
+ else
+ local sendernick=
+ parse_hostmask_nick "$sender" sendernick
+ feedback_bad_syntax "$sendernick" "modinfo" "<module>"
+ fi
+}
diff --git a/modules/m_join.sh b/modules/m_join.sh
new file mode 100644
index 0000000..3151eef
--- /dev/null
+++ b/modules/m_join.sh
@@ -0,0 +1,92 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# Copyright (C) 2007-2008 EmErgE <halt.system@gmail.com> #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Join/part
+#---------------------------------------------------------------------
+
+module_join_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ commands_register "$1" 'join' || return 1
+ commands_register "$1" 'part' || return 1
+ helpentry_module_join_description="Join/part commands."
+
+ helpentry_join_join_syntax='<#channel> [<key>]'
+ helpentry_join_join_description='Join a <#channel>, with an optional channel <key>.'
+
+ helpentry_join_part_syntax='<#channel> [<reason>]"'
+ helpentry_join_part_description='Part a <#channel> with an optional <reason>.'
+}
+
+module_join_UNLOAD() {
+ return 0
+}
+
+module_join_REHASH() {
+ return 0
+}
+
+module_join_handler_part() {
+ local sender="$1"
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(#[^ ]+)(\ (.+))? ]]; then
+ local channel="${BASH_REMATCH[1]}"
+ local reason="${BASH_REMATCH[3]}"
+ if access_check_capab "join" "$sender" "$channel"; then
+ if [[ -z "$reason" ]]; then
+ channels_part "$channel"
+ else
+ channels_part "$channel" "$reason"
+ fi
+ else
+ access_fail "$sender" "make the bot part channel" "join"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "part" "<#channel> [<reason>]"
+ fi
+}
+
+module_join_handler_join() {
+ local sender="$1"
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(#[^ ]+)(\ [^ ]+)? ]]; then
+ local channel="${BASH_REMATCH[1]}"
+ local key="${BASH_REMATCH[2]}"
+ if access_check_capab "join" "$sender" "$channel"; then
+ key="${key## }"
+ if [[ -z "$key" ]]; then
+ channels_join "${channel}"
+ else
+ channels_join "${channel}" "$key"
+ fi
+ else
+ access_fail "$sender" "make the join channel" "join"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "join" "<#channel> [<key>]"
+ fi
+}
diff --git a/modules/m_karma.sh b/modules/m_karma.sh
new file mode 100644
index 0000000..8d2aed8
--- /dev/null
+++ b/modules/m_karma.sh
@@ -0,0 +1,252 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Karma module
+#---------------------------------------------------------------------
+
+module_karma_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='after_load on_PRIVMSG'
+ commands_register "$1" 'karma' || return 1
+ helpentry_module_karma_description="Provides karma support. Use ++ and -- after a string in a channel to change karma."
+ helpentry_karma_karma_syntax='<string>'
+ helpentry_karma_karma_description='Get current karma for <string>.'
+}
+
+module_karma_UNLOAD() {
+ unset module_karma_SELECT
+ unset module_karma_INSERT module_karma_UPDATE module_karma_set_INSERT_or_UPDATE
+ unset module_karma_substract module_karma_add module_karma_check
+ unset module_karma_is_nick
+ unset module_karma_check_return
+ return 0
+}
+
+module_karma_REHASH() {
+ return 0
+}
+
+module_karma_after_load() {
+ modules_depends_register "karma" "sqlite3" || {
+ # This error reporting is hackish, will fix later.
+ if ! list_contains "modules_loaded" "sqlite3"; then
+ log_error "The karma module depends upon the SQLite3 module being loaded."
+ fi
+ return 1
+ }
+ if [[ -z $config_module_karma_table ]]; then
+ log_error "Karma table (config_module_karma_table) must be set in config to use the karma module."
+ return 1
+ fi
+ if ! module_sqlite3_table_exists "$config_module_karma_table"; then
+ log_error "karma module: $config_module_karma_table does not exist in the database file."
+ log_error "karma module: See comment in doc/karma.sql for how to create the table."
+ fi
+}
+
+#---------------------------------------------------------------------
+## Get an item from DB
+## @Type Private
+## @param key
+## @Stdout The result of the database query.
+#---------------------------------------------------------------------
+module_karma_SELECT() {
+ module_sqlite3_exec_sql "SELECT rating FROM $config_module_karma_table WHERE target='$(module_sqlite3_clean_string "$1")';"
+}
+
+
+#---------------------------------------------------------------------
+## Insert a new item into DB
+## @Type Private
+## @param key
+## @param karma
+#---------------------------------------------------------------------
+module_karma_INSERT() {
+ module_sqlite3_exec_sql \
+ "INSERT INTO $config_module_karma_table (target, rating) VALUES('$(module_sqlite3_clean_string "$1")', '$(module_sqlite3_clean_string "$2")');"
+}
+
+
+#---------------------------------------------------------------------
+## Change the item in DB
+## @Type Private
+## @param key
+## @param karma
+#---------------------------------------------------------------------
+module_karma_UPDATE() {
+ module_sqlite3_exec_sql \
+ "UPDATE $config_module_karma_table SET rating='$(module_sqlite3_clean_string "$2")' WHERE target='$(module_sqlite3_clean_string "$1")';"
+}
+
+#---------------------------------------------------------------------
+## Wrapper, call either INSERT or UPDATE
+## @Type Private
+## @param key
+## @param karma
+#---------------------------------------------------------------------
+module_karma_set_INSERT_or_UPDATE() {
+ if [[ $(module_karma_SELECT "$1") ]]; then
+ module_karma_UPDATE "$1" "$2"
+ else
+ module_karma_INSERT "$1" "$2"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Remove 1 from key
+## @Type Private
+## @param key to remove from.
+#---------------------------------------------------------------------
+module_karma_substract() {
+ # Clean spaces and convert to lower case
+ local keyarray
+ read -ra keyarray <<< "$1"
+ local key="$(tr '[:upper:]' '[:lower:]' <<< "${keyarray[*]}")"
+ local old="$(module_karma_SELECT "$key")"
+ # -1 + any old value (yes looks backwards but works)
+ local new=-1
+ if [[ "$old" ]]; then
+ (( new += old ))
+ fi
+ module_karma_set_INSERT_or_UPDATE "$key" "$new"
+}
+
+#---------------------------------------------------------------------
+## Add 1 from key
+## @Type Private
+## @param key to add to.
+#---------------------------------------------------------------------
+module_karma_add() {
+ # Clean spaces and convert to lower case
+ local keyarray
+ read -ra keyarray <<< "$1"
+ local key="$(tr '[:upper:]' '[:lower:]' <<< "${keyarray[*]}")"
+ local old="$(module_karma_SELECT "$key")"
+ # 1 + any old value
+ local new=1
+ if [[ "$old" ]]; then
+ (( new += old ))
+ fi
+ module_karma_set_INSERT_or_UPDATE "$key" "$new"
+}
+
+#---------------------------------------------------------------------
+## Return karma value for key
+## The result is returned in $module_karma_check_return
+## @Type Private
+## @param key to return karma for
+## @Globals $module_karma_check_return
+#---------------------------------------------------------------------
+module_karma_check() {
+ # Clean spaces and convert to lower case
+ local keyarray
+ read -ra keyarray <<< "$1"
+ local key="$(tr '[:upper:]' '[:lower:]' <<< "${keyarray[*]}")"
+ module_karma_check_return="$(module_karma_SELECT "$key")"
+ if [[ -z "$module_karma_check_return" ]]; then
+ module_karma_check_return=0
+ fi
+}
+
+#---------------------------------------------------------------------
+## Check if the key is the nick of sender.
+## @Type Private
+## @param key
+## @param sender
+## @return 0 If nick and key are same
+## @return 1 Otherwise
+#---------------------------------------------------------------------
+module_karma_is_nick() {
+ local keyarray
+ read -ra keyarray <<< "$1"
+ local key="$(tr '[:upper:]' '[:lower:]' <<< "${keyarray[*]}")"
+ local sendernick
+ parse_hostmask_nick "$2" 'sendernick'
+ local nickarray
+ read -ra nickarray <<< "$(tr '[:upper:]' '[:lower:]' <<< "$sendernick")"
+ local nick="${nickarray[*]}"
+ if [[ "$key" = "$nick" ]]; then
+ return 0
+ fi
+ return 1
+}
+
+
+# Called on a PRIVMSG
+#
+# $1 = from who (n!u@h)
+# $2 = to who (channel or botnick)
+# $3 = the message
+module_karma_on_PRIVMSG() {
+ local sender="$1"
+ local query="$3"
+ local sendon_channel
+ # If it isn't in a channel send message back to person who sent it,
+ # otherwise send in channel
+ if [[ $2 =~ ^# ]]; then
+ sendon_channel="$2"
+ # An item must begin with an alphanumeric char.
+ if [[ "$query" =~ ^([a-zA-Z0-9].*)\+\+$ ]]; then
+ local key="${BASH_REMATCH[1]}"
+ if module_karma_is_nick "$key" "$sender"; then
+ send_msg "$sendon_channel" "You can't change karma of yourself."
+ else
+ module_karma_add "$key"
+ fi
+ elif [[ "$query" =~ ^([a-zA-Z0-9].*)--$ ]]; then
+ local key="${BASH_REMATCH[1]}"
+ if module_karma_is_nick "$key" "$sender"; then
+ send_msg "$sendon_channel" "You can't change karma of yourself."
+ else
+ module_karma_substract "$key"
+ fi
+ fi
+ else
+ parse_hostmask_nick "$sender" 'sendon_channel'
+ # Karma is only possible in channels
+ if [[ "$query" =~ ^[a-zA-Z0-9].*(--|\+\+)$ ]]; then
+ send_notice "$sendon_channel" "You can only change karma in channels."
+ return 1
+ fi
+ fi
+ return 0
+}
+
+module_karma_handler_karma() {
+ local sender="$1"
+ local sendon_channel
+ if [[ $2 =~ ^# ]]; then
+ sendon_channel="$2"
+ else
+ parse_hostmask_nick "$sender" 'sendon_channel'
+ fi
+ local parameters="$3"
+ if [[ $parameters =~ ^(.+)$ ]]; then
+ local key="${BASH_REMATCH[1]}"
+ module_karma_check "$key"
+ send_msg "$sendon_channel" "Karma for $key is $module_karma_check_return"
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "karma" "<string>"
+ fi
+}
diff --git a/modules/m_kick_ban.sh b/modules/m_kick_ban.sh
new file mode 100644
index 0000000..d234c62
--- /dev/null
+++ b/modules/m_kick_ban.sh
@@ -0,0 +1,145 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# Copyright (C) 2007-2008 EmErgE <halt.system@gmail.com> #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Kicking and banning.
+#---------------------------------------------------------------------
+
+module_kick_ban_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='after_load after_connect on_numeric'
+ unset module_kick_ban_next_unset module_kick_ban_timed_bans
+ commands_register "$1" 'kick' || return 1
+ commands_register "$1" 'ban' || return 1
+ helpentry_module_kick_ban_description="Provides kick and ban commands."
+
+ helpentry_kick_ban_kick_syntax='[<#channel>] <nick> <reason>'
+ helpentry_kick_ban_kick_description='Kick someone from a channel. Channel parameter only needed if not sent in a channel.'
+
+ helpentry_kick_ban_ban_syntax='<#channel> <nick> [<duration>]'
+ helpentry_kick_ban_ban_description='Ban someone from a channel. Duration is optional and defaults to infinite.'
+}
+
+module_kick_ban_UNLOAD() {
+ unset module_kick_ban_TBAN_supported
+}
+
+module_kick_ban_REHASH() {
+ return 0
+}
+
+# Lets check if TBAN is supported
+# :photon.kuonet-ng.org 461 envbot TBAN :Not enough parameters.
+# :photon.kuonet-ng.org 304 envbot :SYNTAX TBAN <channel> <duration> <banmask>
+module_kick_ban_after_connect() {
+ module_kick_ban_TBAN_supported=0
+ send_raw "TBAN"
+}
+
+# HACK: If module is loaded after connect, module_kick_ban_after_connect won't
+# get called, therefore lets check if we are connected here and check for
+# TBAN here if that is the case.
+module_kick_ban_after_load() {
+ if [[ $server_connected -eq 1 ]]; then
+ module_kick_ban_TBAN_supported=0
+ send_raw "TBAN"
+ fi
+}
+
+module_kick_ban_on_numeric() {
+ if [[ $1 == $numeric_ERR_NEEDMOREPARAMS ]]; then
+ if [[ "$2" =~ ^TBAN\ : ]]; then
+ module_kick_ban_TBAN_supported=1
+ fi
+ fi
+}
+
+module_kick_ban_handler_kick() {
+ # Accept this anywhere, unless someone can give a good reason not to.
+ local sender="$1"
+ local sendon_channel="$2"
+ local parameters="$3"
+ if [[ $parameters =~ ^((#[^ ]+)\ )(.*) ]]; then
+ local channel="${BASH_REMATCH[2]}"
+ parameters="${BASH_REMATCH[3]}"
+ else
+ if ! [[ $channel =~ ^# ]]; then
+ if [[ $sendon_channel =~ ^# ]]; then
+ local channel="$sendon_channel"
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "kick" "[<#channel>] <nick> <reason> # Channel must be send when the message is not sent in a channel"
+ return 0
+ fi
+ fi
+ fi
+
+ if [[ "$parameters" =~ ^([^ ]+)\ (.+) ]]; then
+ local nick="${BASH_REMATCH[1]}"
+ local kickmessage="${BASH_REMATCH[2]}"
+ if access_check_capab "kick" "$sender" "$channel"; then
+ send_raw "KICK $channel $nick :$kickmessage"
+ access_log_action "$sender" "kicked $nick from $channel with kick message: $kickmessage"
+ else
+ access_fail "$sender" "make the bot kick somebody" "kick"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "kick" "[<#channel>] <nick> <reason> # Channel must be send when the message is not sent in a channel"
+ fi
+}
+
+module_kick_ban_handler_ban() {
+ local sender="$1"
+ local sendon_channel="$2"
+ local parameters="$3"
+ if [[ "$parameters" =~ ^(#[^ ]+)\ ([^ ]+)(\ ([0-9]+))? ]]; then
+ local channel="${BASH_REMATCH[1]}"
+ local nick="${BASH_REMATCH[2]}"
+ # Optional parameter.
+ local duration="${BASH_REMATCH[4]}"
+ if access_check_capab "ban" "$sender" "$channel"; then
+ if [[ $duration ]]; then
+ # send_modes "$channel" "+b" get_hostmask $nick <-- not implemented yet
+ if [[ $module_kick_ban_TBAN_supported -eq 1 ]]; then
+ send_raw "TBAN $channel $duration $nick"
+ else
+ send_modes "$channel" "+b $nick"
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ send_notice "$sendernick" "Sorry ban will not be timed, this IRCd didn't support TBAN command when I checked before."
+ fi
+ else
+ send_modes "$channel" "+b $nick"
+ fi
+ access_log_action "$sender" "banned $nick from $channel"
+ else
+ access_fail "$sender" "make the bot ban somebody" "ban"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "ban" "<#channel> <nick> [<duration>]"
+ fi
+}
diff --git a/modules/m_modules.sh b/modules/m_modules.sh
new file mode 100644
index 0000000..62b3917
--- /dev/null
+++ b/modules/m_modules.sh
@@ -0,0 +1,178 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Manage (load/unload/list) modules.
+#---------------------------------------------------------------------
+
+module_modules_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ commands_register "$1" 'modload' || return 1
+ commands_register "$1" 'modunload' || return 1
+ commands_register "$1" 'modreload' || return 1
+ commands_register "$1" 'modlist' || return 1
+ helpentry_module_modules_description="Exposes the internal module loading and unloading support to owners."
+
+ helpentry_modules_modload_syntax='<module name>'
+ helpentry_modules_modload_description='Try to load a module.'
+
+ helpentry_modules_modunload_syntax='<module name>'
+ helpentry_modules_modunload_description='Try to unload a module.'
+
+ helpentry_modules_modreload_syntax='<module name>'
+ helpentry_modules_modreload_description='Try to unload and reload a module.'
+
+ helpentry_modules_modlist_syntax=''
+ helpentry_modules_modlist_description='List currently loaded moudules.'
+
+}
+
+module_modules_UNLOAD() {
+ unset module_modules_doload module_modules_dounload
+}
+
+module_modules_REHASH() {
+ return 0
+}
+
+#---------------------------------------------------------------------
+## Load a module
+## @param Module to load
+## @param Sender nick
+#---------------------------------------------------------------------
+module_modules_doload() {
+ local target_module="$1"
+ local sendernick="$2"
+ modules_load "$target_module"
+ local status_message status=$?
+ case $status in
+ 0) status_message="Loaded \"$target_module\" successfully" ;;
+ 2) status_message="Module \"$target_module\" is already loaded" ;;
+ 3) status_message="Failed to source \"$target_module\" in safe subshell, see log for details" ;;
+ 4) status_message="Failed to source \"$target_module\"" ;;
+ 5) status_message="Module \"$target_module\" could not be found" ;;
+ 6) status_message="Getting hooks from \"$target_module\" failed" ;;
+ 7) status_message="after_load failed for \"$target_module\", see log for details" ;;
+ *) status_message="Unknown error (code $status) for \"$target_module\"" ;;
+ esac
+ send_notice "$sendernick" "$status_message"
+ return $status
+}
+
+#---------------------------------------------------------------------
+## Unload a module
+## @param Module to unload
+## @param Sender nick
+#---------------------------------------------------------------------
+module_modules_dounload() {
+ local target_module="$1"
+ local sendernick="$2"
+ if [[ $target_module == modules ]]; then
+ send_msg "$sendernick" \
+ "You can't unload/reload the modules module using itself. (The hackish way would be to use the eval module for this.)"
+ return 1
+ fi
+ modules_unload "$target_module"
+ local status_message status=$?
+ case $status in
+ 0) status_message="Unloaded \"$target_module\" successfully" ;;
+ 2) status_message="Module \"$target_module\" is not loaded" ;;
+ 3) status_message="Module \"$target_module\" can't be unloaded, some these module(s) depend(s) on it: $(modules_depends_list_deps "$target_module")" ;;
+ *) status_message="Unknown error (code $status) for \"$target_module\"" ;;
+ esac
+ send_notice "$sendernick" "$status_message"
+ return $status
+}
+
+module_modules_handler_modload() {
+ # Accept this anywhere, unless someone can give a good reason not to.
+ local sender="$1"
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ local parameters="$3"
+ if [[ "$parameters" =~ ^([^ ]+) ]]; then
+ local target_module="${BASH_REMATCH[1]}"
+ if access_check_owner "$sender"; then
+ access_log_action "$sender" "loaded the module $target_module"
+ module_modules_doload "$target_module" "$sendernick"
+ else
+ access_fail "$sender" "load a module" "owner"
+ fi
+ else
+ feedback_bad_syntax "$sendernick" "modload" "<module name>"
+ fi
+}
+
+module_modules_handler_modunload() {
+ local sender="$1"
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ local parameters="$3"
+ if [[ "$parameters" =~ ^([^ ]+) ]]; then
+ local target_module="${BASH_REMATCH[1]}"
+ if access_check_owner "$sender"; then
+ access_log_action "$sender" "unloaded the module $target_module"
+ module_modules_dounload "$target_module" "$sendernick"
+ else
+ access_fail "$sender" "unload a module" "owner"
+ fi
+ else
+ feedback_bad_syntax "$sendernick" "modunload" "<module name>"
+ fi
+}
+
+module_modules_handler_modreload() {
+ local sender="$1"
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ local parameters="$3"
+ if [[ "$parameters" =~ ^([^ ]+) ]]; then
+ local target_module="${BASH_REMATCH[1]}"
+ if access_check_owner "$sender"; then
+ access_log_action "$sender" "reloaded the module $target_module"
+ module_modules_dounload "$target_module" "$sendernick"
+ if [[ $? = 0 ]]; then
+ module_modules_doload "$target_module" "$sendernick"
+ else
+ send_notice "$sendernick" "Reload of $target_module failed because it could not be unloaded."
+ fi
+ else
+ access_fail "$sender" "reload a module" "owner"
+ fi
+ else
+ feedback_bad_syntax "$sendernick" "modreload" "<module name>"
+ fi
+}
+
+module_modules_handler_modlist() {
+ local sender="$1"
+ local parameters="$3"
+ local target
+ if [[ $2 =~ ^# ]]; then
+ target="$2"
+ else
+ parse_hostmask_nick "$sender" 'target'
+ fi
+ local modlist="${modules_loaded## }"
+ modlist="${modlist%% }"
+ send_msg "$target" "${format_bold}Modules currently loaded${format_bold}: ${modlist// / }"
+}
diff --git a/modules/m_nicktracking.sh b/modules/m_nicktracking.sh
new file mode 100644
index 0000000..34b6873
--- /dev/null
+++ b/modules/m_nicktracking.sh
@@ -0,0 +1,316 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Provides nick tracking API for other modules.
+#---------------------------------------------------------------------
+
+module_nicktracking_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='after_load before_connect on_numeric on_NICK on_QUIT on_KICK on_PART on_JOIN'
+ helpentry_module_nicktracking_description="Provides nicktracking backend for other modules."
+}
+
+module_nicktracking_UNLOAD() {
+ unset module_nicktracking_channels
+ hash_reset module_nicktracking_channels_nicks
+ hash_reset module_nicktracking_nicks
+ # Private functions
+ unset module_nicktracking_clear_nick module_nicktracking_clear_chan
+ unset module_nicktracking_parse_names
+ unset module_nicktracking_add_channel_nick
+ unset module_nicktracking_remove_channel_nick
+ # API functions
+ unset module_nicktracking_get_hostmask_by_nick
+ unset module_nicktracking_get_channel_nicks
+ return 0
+}
+
+module_nicktracking_REHASH() {
+ return 0
+}
+
+
+#################
+# API functions #
+#################
+
+#---------------------------------------------------------------------
+## Return hostmask of a nick
+## @Type API
+## @param Nick to find hostmask for
+## @param Variable to return hostmask in
+## @Note If no nick is found (or data about the nick
+## @Note is missing currently), the return variable will be empty.
+#---------------------------------------------------------------------
+module_nicktracking_get_hostmask_by_nick() {
+ hash_get 'module_nicktracking_nicks' "$(tr '[:upper:]' '[:lower:]' <<< "$1")" "$2"
+}
+
+#---------------------------------------------------------------------
+## Return list of nicks on a channel
+## @Type API
+## @param Channel to check
+## @param Variable to return space separated list in
+## @return 0 Channel data exists.
+## @return 1 We don't track this channel.
+#---------------------------------------------------------------------
+module_nicktracking_get_channel_nicks() {
+ if list_contains 'module_nicktracking_channels' "$1"; then
+ hash_get 'module_nicktracking_channels_nicks' "$1" "$2"
+ return 0
+ else
+ return 1
+ fi
+}
+
+
+#####################
+# Private functions #
+#####################
+
+#---------------------------------------------------------------------
+## Check if a nick should be removed
+## @Type Private
+## @param Nick to check
+#---------------------------------------------------------------------
+module_nicktracking_clear_nick() {
+ # If not on a channel any more, remove knowledge about nick.
+ if ! hash_search 'module_nicktracking_channels_nicks' "$1"; then
+ hash_unset 'module_nicktracking_nicks' "$1"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Clear a channel (if we part it or such)
+## @Type Private
+## @param Channel name
+#---------------------------------------------------------------------
+module_nicktracking_clear_chan() {
+ list_remove 'module_nicktracking_channels' "$1" 'module_nicktracking_channels'
+ # Get list and then unset it.
+ local nicks=
+ hash_get 'module_nicktracking_channels_nicks' "$1" 'nicks'
+ hash_unset 'module_nicktracking_channels_nicks' "$1"
+ # Sigh, this isn't fast I know...
+ local nick
+ for nick in $nicks; do
+ module_nicktracking_clear_nick "$nick"
+ done
+}
+
+#---------------------------------------------------------------------
+## Parse RPL_NAMREPLY data.
+## @Type Private
+## @param NAMES data
+#---------------------------------------------------------------------
+module_nicktracking_parse_names() {
+ if [[ $1 =~ ^[=*@]?\ *(#[^ ]+)\ +:(.+) ]]; then
+ local channel="${BASH_REMATCH[1]}"
+ local nicks="${BASH_REMATCH[2]}"
+ local entry nick realnick
+ # Loop through the entries
+ for entry in $nicks; do
+ # This will work both with and without NAMESX
+ if [[ $entry =~ [$server_PREFIX_prefixes]*([^ ]+) ]]; then
+ nick="${BASH_REMATCH[1]}"
+ # Is UHNAMES enabled?
+ # If yes lets take care of hostmask.
+ if [[ $server_UHNAMES -eq 1 ]]; then
+ parse_hostmask_nick "$nick" 'realnick'
+ realnick="$(tr '[:upper:]' '[:lower:]' <<< "$realnick")"
+ hash_set 'module_nicktracking_nicks' "$realnick" "$nick"
+ # Add to nick list of channel if not in list
+ hash_contains 'module_nicktracking_channels_nicks' "$channel" "$realnick" || \
+ hash_append 'module_nicktracking_channels_nicks' "$channel" "$realnick"
+ else
+ realnick="$(tr '[:upper:]' '[:lower:]' <<< "$nick")"
+ # Add to nick list of channel if not in list
+ hash_contains 'module_nicktracking_channels_nicks' "$channel" "$realnick" || \
+ hash_append 'module_nicktracking_channels_nicks' "$channel" "$realnick"
+ fi
+ else
+ log_error_file unknown_data.log "module_nicktracking_parse_names: Uh uh, regex for inner loop is bad, couldn't parse: $nick"
+ log_error_file unknown_data.log "module_nicktracking_parse_names: Please report a bug with the above message"
+ fi
+ done
+ else
+ log_error_file unknown_data.log "module_nicktracking_parse_names: Uh uh, outer regex is bad, couldn't parse: $1"
+ log_error_file unknown_data.log "module_nicktracking_parse_names: Please report a bug with the above message"
+ fi
+ return 0
+}
+
+#---------------------------------------------------------------------
+## Parse RPL_WHOREPLY data.
+## @Type Private
+## @param WHO data
+#---------------------------------------------------------------------
+module_nicktracking_parse_who() {
+ # Read the who data into an array then extract the data from the array.
+ local whodata
+ read -ra whodata <<< "$1"
+ local channel="${whodata[0]}"
+ local ident="${whodata[1]}"
+ local host="${whodata[2]}"
+ local nick="${whodata[4]}"
+ local lowernick="$(tr '[:upper:]' '[:lower:]' <<< "$nick")"
+ # Set the hash tables
+ hash_set 'module_nicktracking_nicks' "$lowernick" "${nick}!${ident}@${host}"
+ # We don't want to add twice
+ hash_contains 'module_nicktracking_channels_nicks' "$channel" "$lowernick" || \
+ hash_append 'module_nicktracking_channels_nicks' "$channel" "$lowernick"
+}
+
+
+#---------------------------------------------------------------------
+## Add a nick to a channel
+## @Type Private
+## @param Channel
+## @param Hostmask
+## @param Nick
+#---------------------------------------------------------------------
+module_nicktracking_add_channel_nick() {
+ local nick="$(tr '[:upper:]' '[:lower:]' <<< "$3")"
+ hash_append 'module_nicktracking_channels_nicks' "$1" "$nick"
+ hash_set 'module_nicktracking_nicks' "$nick" "$2"
+}
+
+#---------------------------------------------------------------------
+## Remove a nick from a channel
+## @Type Private
+## @param Channel
+## @param Nick
+#---------------------------------------------------------------------
+module_nicktracking_remove_channel_nick() {
+ local nick="$(tr '[:upper:]' '[:lower:]' <<< "$2")"
+ hash_substract 'module_nicktracking_channels_nicks' "$1" "$nick"
+ module_nicktracking_clear_nick "$nick"
+}
+
+
+#########
+# Hooks #
+#########
+
+module_nicktracking_after_load() {
+ # Handle case of loading while bot is running
+ if [[ $server_connected -eq 1 ]]; then
+ module_nicktracking_channels="$channels_current"
+ local channel
+ for channel in $module_nicktracking_channels; do
+ send_raw "NAMES $channel"
+ # We have to send a WHO #channel if servers doesn't support UHNAMES.
+ if [[ $server_UHNAMES -eq 0 ]]; then
+ send_raw "WHO $channel"
+ fi
+ done
+ fi
+}
+
+module_nicktracking_before_connect() {
+ # Reset state.
+ unset module_nicktracking_channels
+ hash_reset module_nicktracking_channels_nicks
+ hash_reset module_nicktracking_nicks
+ return 0
+}
+
+
+##########################
+# Message handling hooks #
+##########################
+
+module_nicktracking_on_numeric() {
+ case $1 in
+ "$numeric_RPL_NAMREPLY")
+ # TODO: Parse NAMES
+ module_nicktracking_parse_names "$2"
+ ;;
+ "$numeric_RPL_WHOREPLY")
+ module_nicktracking_parse_who "$2"
+ ;;
+ esac
+}
+
+module_nicktracking_on_NICK() {
+ local oldnick oldident oldhost oldentry
+ parse_hostmask "$1" 'oldnick' 'oldident' 'oldhost'
+ local oldlowercase="$(tr '[:upper:]' '[:lower:]' <<< "$oldnick")"
+ local newlowercase="$(tr '[:upper:]' '[:lower:]' <<< "$2")"
+ # Remove old and add new.
+ hash_get 'module_nicktracking_nicks' "$oldlowercase" 'oldentry'
+ hash_unset 'module_nicktracking_nicks' "$oldlowercase"
+ hash_set 'module_nicktracking_nicks' "$newlowercase" "${2}!${oldident}@${oldhost}"
+ local channel
+ # Loop through the channels
+ for channel in $module_nicktracking_channels; do
+ hash_replace 'module_nicktracking_channels_nicks' "$channel" "$oldnick" "$newlowercase"
+ done
+ return 0
+}
+
+module_nicktracking_on_QUIT() {
+ local whoquit=
+ parse_hostmask_nick "$1" 'whoquit'
+ local nick="$(tr '[:upper:]' '[:lower:]' <<< "$whoquit")"
+ hash_unset 'module_nicktracking_nicks' "$nick"
+ local channel
+ # Remove from channel
+ for channel in $module_nicktracking_channels; do
+ hash_substract 'module_nicktracking_channels_nicks' "$channel" "$nick"
+ done
+}
+
+module_nicktracking_on_KICK() {
+ local whogotkicked="$3"
+ if [[ $whogotkicked == $server_nick_current ]]; then
+ module_nicktracking_clear_chan "$2"
+ else
+ module_nicktracking_remove_channel_nick "$2" "$whogotkicked"
+ fi
+}
+
+module_nicktracking_on_PART() {
+ # Check if it was us
+ local whoparted=
+ parse_hostmask_nick "$1" 'whoparted'
+ if [[ $whoparted == $server_nick_current ]]; then
+ module_nicktracking_clear_chan "$2"
+ else
+ module_nicktracking_remove_channel_nick "$2" "$whoparted"
+ fi
+}
+
+module_nicktracking_on_JOIN() {
+ local whojoined=
+ parse_hostmask_nick "$1" 'whojoined'
+ if [[ $whojoined == $server_nick_current ]]; then
+ module_nicktracking_channels+=" $2"
+ hash_set 'module_nicktracking_channels_nicks' "$2" "$server_nick_current"
+ # We have to send a WHO #channel if servers doesn't support UHNAMES.
+ if [[ $server_UHNAMES -eq 0 ]]; then
+ send_raw "WHO $2"
+ fi
+ else
+ module_nicktracking_add_channel_nick "$2" "$1" "$whojoined"
+ fi
+}
diff --git a/modules/m_ping.sh b/modules/m_ping.sh
new file mode 100644
index 0000000..6dd1532
--- /dev/null
+++ b/modules/m_ping.sh
@@ -0,0 +1,87 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Keeps track of latency
+#---------------------------------------------------------------------
+
+# TODO: Redo with stored "on pong info".
+
+module_ping_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='on_PONG'
+ commands_register "$1" 'ping' || return 1
+ commands_register "$1" 'latency' || return 1
+ helpentry_module_ping_description="Provides latency tracking."
+
+ helpentry_ping_ping_syntax=''
+ helpentry_ping_ping_description='Respond to sender with "PONG!"'
+
+ helpentry_ping_latency_syntax=''
+ helpentry_ping_latency_description='Report current latency to server.'
+}
+
+module_ping_UNLOAD() {
+ return 0
+}
+
+module_ping_REHASH() {
+ return 0
+}
+
+module_ping_on_PONG() {
+ # Is data time_sender?
+ if [[ $3 =~ ([0-9]+)_(#?[A-Za-z0-9][^ ]+) ]]; then
+ local time="${BASH_REMATCH[1]}"
+ local target="${BASH_REMATCH[2]}"
+ local latency
+ (( latency = envbot_time - $time ))
+ local msg=
+ case $latency in
+ 0) msg="less than one second" ;;
+ 1) msg="1 second" ;;
+ *) msg="$latency seconds" ;;
+ esac
+ send_msg "$target" "Latency is $msg"
+ fi
+}
+
+module_ping_handler_ping() {
+ local target
+ local sender_nick
+ parse_hostmask_nick "$1" 'sender_nick'
+ if [[ $2 =~ ^# ]]; then
+ target="$2"
+ else
+ target="$sender_nick"
+ fi
+ send_msg "$target" "$sender_nick: PONG!"
+}
+
+module_ping_handler_latency() {
+ local target
+ if [[ $2 =~ ^# ]]; then
+ target="$2"
+ else
+ parse_hostmask_nick "$1" 'target'
+ fi
+ send_raw "PING :${envbot_time}_${target}"
+}
diff --git a/modules/m_quote.sh b/modules/m_quote.sh
new file mode 100644
index 0000000..51c4752
--- /dev/null
+++ b/modules/m_quote.sh
@@ -0,0 +1,85 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 EmErgE <halt.system@gmail.com> #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Quotes module
+#---------------------------------------------------------------------
+
+module_quote_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='after_load'
+ commands_register "$1" 'quote' || return 1
+ helpentry_module_quote_description="Provides command for random quotes from a file."
+
+ helpentry_quote_quote_syntax=''
+ helpentry_quote_quote_description='Return a random quote.'
+}
+
+module_quote_UNLOAD() {
+ unset module_quote_load
+ unset module_quote_quotes
+}
+
+module_quote_REHASH() {
+ module_quote_load
+}
+
+#---------------------------------------------------------------------
+## Load quotes from file
+## @Type Private
+#---------------------------------------------------------------------
+module_quote_load() {
+ local i=0 line
+ unset module_quote_quotes
+ if [[ -z "$config_module_quotes_file" ]]; then
+ log_error "quotes module: You need to set config_module_quotes_file in your config!"
+ return 1
+ elif [[ -r "$config_module_quotes_file" ]]; then
+ local IFS=$'\n'
+ module_quote_quotes=( $(<"${config_module_quotes_file}") )
+ unset IFS
+ log_info 'Loaded Quotes.'
+ return 0
+ else
+ log_error "quotes module: Quotes failed to load: Cannot load \"$config_module_quotes_file\". File doesn't exist."
+ return 1
+ fi
+}
+
+module_quote_after_load() {
+ # Return code from last command in a function
+ # will be return code for the function by default.
+ module_quote_load
+}
+
+module_quote_handler_quote() {
+ local sender="$1"
+ local channel="$2"
+ # If it isn't in a channel send message back to person who send it,
+ # otherwise send in channel
+ if ! [[ $2 =~ ^# ]]; then
+ parse_hostmask_nick "$sender" 'channel'
+ fi
+ local number="$RANDOM"
+ (( number %= ${#module_quote_quotes[*]} ))
+ send_msg "$channel" "${module_quote_quotes[$number]}"
+}
diff --git a/modules/m_rehash.sh b/modules/m_rehash.sh
new file mode 100644
index 0000000..4449512
--- /dev/null
+++ b/modules/m_rehash.sh
@@ -0,0 +1,74 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Rehashing
+#---------------------------------------------------------------------
+
+module_rehash_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ commands_register "$1" 'rehash' || return 1
+ helpentry_module_rehash_description="Exposes the internal rehash support to bot owners."
+
+ helpentry_rehash_rehash_syntax=''
+ helpentry_rehash_rehash_description='Reload configuration file.'
+}
+
+module_rehash_UNLOAD() {
+ unset module_rehash_dorehash
+}
+
+module_rehash_REHASH() {
+ return 0
+}
+
+#---------------------------------------------------------------------
+## Rehash config
+## @Type Private
+## @param Sender
+#---------------------------------------------------------------------
+module_rehash_dorehash() {
+ local sender="$1" status_message
+ config_rehash
+ local status=$?
+ case $status in
+ 0) status_message="Rehash successful. (Also any loaded modules not listed in config have been unloaded.)" ;;
+ 2) status_message="The new config is not the same version as the bot. Rehash won't work." ;;
+ 3) status_message="Failed to source it, but the bot should not be in an undefined state." ;;
+ 4) status_message="Configuration validation on new config failed, but the bot should not be in an undefined state." ;;
+ 5) status_message="Failed to source it and the bot may be in an undefined state." ;;
+ *) status_message="Unknown error (code $status)" ;;
+ esac
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ send_notice "$sendernick" "$status_message"
+}
+
+module_rehash_handler_rehash() {
+ local sender="$1"
+ if access_check_owner "$sender"; then
+ access_log_action "$sender" "did a rehash"
+ module_rehash_dorehash "$sender"
+ else
+ access_fail "$sender" "load a module" "owner"
+ fi
+}
diff --git a/modules/m_say.sh b/modules/m_say.sh
new file mode 100644
index 0000000..104b30c
--- /dev/null
+++ b/modules/m_say.sh
@@ -0,0 +1,98 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Allow owners to make to bot say something
+#---------------------------------------------------------------------
+
+module_say_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ commands_register "$1" 'say' || return 1
+ commands_register "$1" 'act' || return 1
+ helpentry_module_say_description="Provides say and act commands."
+
+ helpentry_say_act_syntax='<target> <message>'
+ helpentry_say_act_description='Send a <message> to <target> (nick or channel).'
+
+ helpentry_say_act_syntax='<target> <message>'
+ helpentry_say_act_description='Peform the <message> as a /me to <target> (nick or channel).'
+}
+
+module_say_UNLOAD() {
+ return 0
+}
+
+module_say_REHASH() {
+ return 0
+}
+
+module_say_handler_say() {
+ local sender="$1"
+ local parameters="$3"
+ if [[ "$parameters" =~ ^([^ ]+)\ (.+) ]]; then
+ local target="${BASH_REMATCH[1]}"
+ local message="${BASH_REMATCH[2]}"
+ local scope
+ # Is it a channel?
+ if [[ $target =~ ^# ]]; then
+ scope="$target"
+ else
+ scope="MSG"
+ fi
+ if access_check_capab "say" "$sender" "$scope"; then
+ access_log_action "$sender" "made the bot say \"$message\" in/to \"$target\""
+ send_msg "$target" "$message"
+ else
+ access_fail "$sender" "make the bot talk with say" "say"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "say" "<target> <message> # Where target is a nick or channel"
+ fi
+}
+
+module_say_handler_act() {
+ local sender="$1"
+ local parameters="$3"
+ if [[ "$parameters" =~ ^([^ ]+)\ (.+) ]]; then
+ local target="${BASH_REMATCH[1]}"
+ local message="${BASH_REMATCH[2]}"
+ local scope
+ # Is it a channel?
+ if [[ $target =~ ^# ]]; then
+ scope="$target"
+ else
+ scope="MSG"
+ fi
+ if access_check_capab "say" "$sender" "$scope"; then
+ access_log_action "$sender" "made the bot act \"$message\" in/to \"$target\""
+ send_ctcp "$target" "ACTION ${message}"
+ else
+ access_fail "$sender" "make the bot act" "say"
+ fi
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "act" "<target> <message> # Where target is a nick or channel"
+ fi
+}
diff --git a/modules/m_seen.sh b/modules/m_seen.sh
new file mode 100644
index 0000000..5ecfa8d
--- /dev/null
+++ b/modules/m_seen.sh
@@ -0,0 +1,209 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Simple seen module using SQLite3
+#---------------------------------------------------------------------
+
+module_seen_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='after_load on_PRIVMSG'
+ commands_register "$1" 'seen' || return 1
+ helpentry_module_seen_description="Provides last seen information."
+
+ helpentry_seen_seen_syntax='<nick>'
+ helpentry_seen_seen_description='Report when the bot last saw <nick>.'
+}
+
+module_seen_UNLOAD() {
+ unset module_seen_exec_sql module_seen_SELECT module_seen_INSERT module_seen_UPDATE
+ unset module_seen_set_INSERT_or_UPDATE
+ unset module_seen_store module_seen_find
+}
+
+module_seen_REHASH() {
+ return 0
+}
+
+
+# Called after module has loaded.
+module_seen_after_load() {
+ modules_depends_register "seen" "sqlite3" || {
+ # This error reporting is hackish, will fix later.
+ if ! list_contains "modules_loaded" "sqlite3"; then
+ log_error "The seen module depends upon the SQLite3 module being loaded."
+ fi
+ return 1
+ }
+ if [[ -z $config_module_seen_table ]]; then
+ log_error "\"Seen table\" (config_module_seen_table) must be set in config."
+ return 1
+ fi
+ if ! module_sqlite3_table_exists "$config_module_seen_table"; then
+ log_error "seen module: $config_module_seen_table does not exist in the database file."
+ log_error "seen module: See comment in doc/seen.sql for how to create the table."
+ fi
+}
+
+#---------------------------------------------------------------------
+## Get the data about nick
+## @Type Private
+## @param The nick
+## @Stdout The result of the database query.
+#---------------------------------------------------------------------
+module_seen_SELECT() {
+ module_sqlite3_exec_sql "SELECT timestamp, channel, message FROM $config_module_seen_table WHERE nick='$(module_sqlite3_clean_string "$1")';"
+}
+
+#---------------------------------------------------------------------
+## Insert a new item into DB
+## @Type Private
+## @param Nick
+## @param Channel
+## @param Timestamp
+## @param Query
+#---------------------------------------------------------------------
+module_seen_INSERT() {
+ module_sqlite3_exec_sql \
+ "INSERT INTO $config_module_seen_table (nick, channel, timestamp, message) VALUES('$(module_sqlite3_clean_string "$1")', '$(module_sqlite3_clean_string "$2")', '$(module_sqlite3_clean_string "$3")', '$(module_sqlite3_clean_string "$4")');"
+}
+
+#---------------------------------------------------------------------
+## Change the item in DB
+## @Type Private
+## @param Nick
+## @param Channel
+## @param Timestamp
+## @param Message
+#---------------------------------------------------------------------
+module_seen_UPDATE() {
+ module_sqlite3_exec_sql \
+ "UPDATE $config_module_seen_table SET channel='$(module_sqlite3_clean_string "$2")', timestamp='$(module_sqlite3_clean_string "$3")', message='$(module_sqlite3_clean_string "$4")' WHERE nick='$(module_sqlite3_clean_string "$1")';"
+}
+
+#---------------------------------------------------------------------
+## Wrapper, call either INSERT or UPDATE
+## @Type Private
+## @param Nick
+## @param Channel
+## @param Timestamp
+## @param Message
+#---------------------------------------------------------------------
+module_seen_set_INSERT_or_UPDATE() {
+ if [[ $(module_seen_SELECT "$1") ]]; then
+ module_seen_UPDATE "$1" "$2" "$3" "$4"
+ else
+ module_seen_INSERT "$1" "$2" "$3" "$4"
+ fi
+}
+
+#---------------------------------------------------------------------
+## Store a line
+## @Type Private
+## @param Sender
+## @param Channel
+## @param Timestamp
+## @param Query
+#---------------------------------------------------------------------
+module_seen_store() {
+ # Clean spaces, fastest way for this
+ local query
+ read -ra query <<< "$4"
+ local sendernick
+ parse_hostmask_nick "$1" 'sendernick'
+ module_seen_set_INSERT_or_UPDATE "$(echo -n "$sendernick" | tr '[:upper:]' '[:lower:]')" "$2" "$3" "${query[*]}"
+}
+
+#---------------------------------------------------------------------
+## Look up a nick and send info to a channel/nick
+## @Type Private
+## @param Sender
+## @param Channel
+## @param Nick to look up
+#---------------------------------------------------------------------
+module_seen_find() {
+ local sender="$1"
+ local channel="$2"
+ local nick="$(tr '[:upper:]' '[:lower:]' <<< "$3")"
+ local sender_nick=
+ parse_hostmask_nick "$sender" 'sender_nick'
+ # Classical ones. We just HAVE to do them.
+ if [[ "$nick" == "$(tr '[:upper:]' '[:lower:]' <<< "$server_nick_current")" ]]; then
+ send_msg "$channel" "$sender_nick, you found me!"
+ return 0
+ elif [[ "$nick" == "$(tr '[:upper:]' '[:lower:]' <<< "$sender_nick")" ]]; then
+ send_ctcp "$channel" "ACTION holds up a mirror for $sender_nick"
+ return 0
+ fi
+ local match="$(module_seen_SELECT "$nick")"
+ if [[ $match ]]; then
+ # So we got a match
+ # Lets use regex
+ if [[ $match =~ ([0-9]+)\|(#[^ |]+)\|(.*) ]]; then
+ local found_timestamp="${BASH_REMATCH[1]}"
+ local found_channel="${BASH_REMATCH[2]}"
+ local found_message="${BASH_REMATCH[3]}"
+ if [[ $found_message =~ ^ACTION\ (.*) ]]; then
+ found_message="* $3 ${BASH_REMATCH[1]}"
+ fi
+ local difference frmtdiff
+ time_get_current 'difference'
+ (( difference -= found_timestamp ))
+ time_format_difference "$difference" 'frmtdiff'
+ send_msg "$channel" "$3 was last seen $frmtdiff ago in $found_channel saying \"$found_message\""
+ fi
+ else
+ send_msg "$channel" "Sorry, I have not seen $3."
+ fi
+}
+
+module_seen_on_PRIVMSG() {
+ local sender="$1"
+ local channel="$2"
+ local query="$3"
+ # If in channel, store
+ if [[ $channel =~ ^# ]]; then
+ local now=
+ time_get_current 'now'
+ module_seen_store "$sender" "$channel" "$now" "$query"
+ # If not in channel respond to any commands in /msg
+ else
+ parse_hostmask_nick "$sender" 'channel'
+ fi
+}
+
+module_seen_handler_seen() {
+ local sender="$1"
+ local channel="$2"
+ if ! [[ $2 =~ ^# ]]; then
+ parse_hostmask_nick "$sender" 'channel'
+ fi
+ # Lets look up messages
+ local parameters="$3"
+ if [[ "$parameters" =~ ^([^ ]+) ]]; then
+ local nick="${BASH_REMATCH[1]}"
+ module_seen_find "$sender" "$channel" "$nick"
+ else
+ local sendernick
+ parse_hostmask_nick "$sender" 'sendernick'
+ feedback_bad_syntax "$sendernick" "seen" "<nick>"
+ fi
+}
diff --git a/modules/m_sendraw.sh b/modules/m_sendraw.sh
new file mode 100644
index 0000000..e94c083
--- /dev/null
+++ b/modules/m_sendraw.sh
@@ -0,0 +1,53 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Allow owners to make bot send any line.
+## THIS IS FOR DEBUGGING MAINLY.
+#---------------------------------------------------------------------
+
+module_sendraw_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ commands_register "$1" 'raw' || return 1
+ helpentry_module_sendraw_description="Provides raw command to send raw data."
+ helpentry_sendraw_raw_syntax='<line>'
+ helpentry_sendraw_raw_description='Send the <line> to the IRC server.'
+}
+
+module_sendraw_UNLOAD() {
+ return 0
+}
+
+module_sendraw_REHASH() {
+ return 0
+}
+
+module_sendraw_handler_raw() {
+ local sender="$1"
+ if access_check_capab "sendraw" "$sender" "GLOBAL"; then
+ local parameters="$3"
+ access_log_action "$sender" "make the bot send a raw line: $parameters"
+ send_raw "$parameters"
+ else
+ access_fail "$sender" "send a raw line" "sendraw"
+ fi
+}
diff --git a/modules/m_services.sh b/modules/m_services.sh
new file mode 100644
index 0000000..47d688e
--- /dev/null
+++ b/modules/m_services.sh
@@ -0,0 +1,87 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Identify to NickServ
+#---------------------------------------------------------------------
+
+module_services_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='on_connect after_load after_disconnect'
+ helpentry_module_services_description="Provides support for identifying with services."
+}
+
+module_services_UNLOAD() {
+ unset module_services_ghost module_services_nickserv_command
+}
+
+module_services_REHASH() {
+ return 0
+}
+
+module_services_after_load() {
+ module_services_ghost=0
+ if [[ $config_module_services_server_alias -eq 0 ]]; then
+ module_services_nickserv_command="PRIVMSG $config_module_services_nickserv_name :"
+ else
+ module_services_nickserv_command="$config_module_services_nickserv_name "
+ fi
+}
+
+# Called for each line on connect
+module_services_on_connect() {
+ local line="$1"
+ if [[ "$line" =~ ^:[^\ ]+\ +([0-9]{3})\ +([^ ]+)\ +(.*) ]]; then
+ local numeric="${BASH_REMATCH[1]}"
+ local numeric="${BASH_REMATCH[1]}"
+ # Check if this is a numeric we will handle.
+ case "$numeric" in
+ "$numeric_ERR_NICKNAMEINUSE"|"$numeric_ERR_ERRONEUSNICKNAME")
+ module_services_ghost=1
+ ;;
+ "$numeric_RPL_ENDOFMOTD"|"$numeric_ERR_NOMOTD")
+ if [[ $config_module_services_style == 'atheme' ]]; then
+ send_raw_flood_nolog "NickServ IDENTIFY (password)" "${module_services_nickserv_command}IDENTIFY $config_firstnick $config_module_services_nickserv_passwd"
+ fi
+ if [[ $module_services_ghost == 1 ]]; then
+ log_info_stdout "Recovering ghost"
+ send_raw_flood_nolog "NickServ GHOST (password)" "${module_services_nickserv_command}GHOST $config_firstnick $config_module_services_nickserv_passwd"
+ # Try to release too, just in case.
+ send_raw_flood_nolog "NickServ RELEASE (password)" "${module_services_nickserv_command}RELEASE $config_firstnick $config_module_services_nickserv_passwd"
+ sleep 2
+ send_nick "$config_firstnick"
+ # HACK: This is a workaround for bug #21
+ server_nick_current="$config_firstnick"
+ fi
+ log_info_stdout "Identifying..."
+ if [[ $config_module_services_style != 'atheme' ]]; then
+ send_raw_flood_nolog "NickServ IDENTIFY (password)" "${module_services_nickserv_command}IDENTIFY $config_module_services_nickserv_passwd"
+ fi
+ sleep 1
+ ;;
+ esac
+ fi
+}
+
+module_services_after_disconnect() {
+ # Reset state.
+ module_services_ghost=0
+}
diff --git a/modules/m_sqlite3.sh b/modules/m_sqlite3.sh
new file mode 100644
index 0000000..4cb8ef2
--- /dev/null
+++ b/modules/m_sqlite3.sh
@@ -0,0 +1,89 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## This module allows other modules to access a SQLite3 database in a
+## "simple" way.
+#---------------------------------------------------------------------
+
+module_sqlite3_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='after_load'
+ helpentry_module_sqlite3_description="Provides sqlite3 database backend for other modules."
+}
+
+module_sqlite3_UNLOAD() {
+ unset module_sqlite3_clean_string module_sqlite3_exec_sql module_sqlite3_table_exists
+}
+
+module_sqlite3_REHASH() {
+ return 0
+}
+
+# Called after module has loaded.
+module_sqlite3_after_load() {
+ # Check (silently) for sqlite3
+ if ! hash sqlite3 > /dev/null 2>&1; then
+ log_error "Couldn't find sqlite3 command line tool. The sqlite3 module depend on that tool."
+ return 1
+ fi
+ if [[ -z $config_module_sqlite3_database ]]; then
+ log_error "You must set config_module_sqlite3_database in your config to use the sqlite3 module."
+ return 1
+ fi
+ if ! [[ -r $config_module_sqlite3_database ]]; then
+ log_error "sqlite3 module: Database file doesn't exist or can't be read!"
+ log_error "sqlite3 module: To create one follow the comments in one (or several) of the sql files in the doc directory."
+ return 1
+ fi
+}
+
+#---------------------------------------------------------------------
+## Make string safe for SQLite3.
+## @Type API
+## @param String to clean
+## @Note IMPORTANT FOR SECURITY!: Only use the result inside single
+## @Note quotes ('), NEVER inside double quotes (").
+## @Note The output isn't safe for that.
+#---------------------------------------------------------------------
+module_sqlite3_clean_string() {
+ sed "s/'/''/g" <<< "$1"
+}
+
+#---------------------------------------------------------------------
+## Run the query against the data base.
+## @Type API
+## @param Query to run
+#---------------------------------------------------------------------
+module_sqlite3_exec_sql() {
+ sqlite3 -list "$config_module_sqlite3_database" "$1"
+}
+
+#---------------------------------------------------------------------
+## Check if a table exists in the database file.
+## @Type API
+## @param The table name to check for
+## @return 0 If table exists
+## @return 1 If table doesn't exist.
+#---------------------------------------------------------------------
+module_sqlite3_table_exists() {
+ sqlite3 -list "$config_module_sqlite3_database" ".tables" | grep -qw "$1"
+}
diff --git a/modules/m_umodes.sh b/modules/m_umodes.sh
new file mode 100644
index 0000000..0a508f9
--- /dev/null
+++ b/modules/m_umodes.sh
@@ -0,0 +1,63 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Set umodes when connecting
+#---------------------------------------------------------------------
+
+module_umodes_INIT() {
+ modinit_API='2'
+ modinit_HOOKS='after_connect after_load'
+ helpentry_module_umodes_description="Provides support for setting umodes on connect."
+}
+
+module_umodes_UNLOAD() {
+ unset module_umodes_set_modes
+}
+
+module_umodes_REHASH() {
+ module_umodes_set_modes
+ return 0
+}
+
+#---------------------------------------------------------------------
+## Set the umodes
+## @Type Private
+#---------------------------------------------------------------------
+module_umodes_set_modes() {
+ if [[ $config_module_umodes_default_umodes ]]; then
+ log_info "Setting umodes: $config_module_umodes_default_umodes"
+ send_umodes "$config_module_umodes_default_umodes"
+ fi
+}
+
+# Called after bot has connected
+module_umodes_after_connect() {
+ module_umodes_set_modes
+}
+
+# Called after bot has connected
+module_umodes_after_load() {
+ # Check if connected first
+ if [[ $server_connected -eq 1 ]]; then
+ module_umodes_set_modes
+ fi
+}
diff --git a/modules/m_uptime.sh b/modules/m_uptime.sh
new file mode 100644
index 0000000..834773b
--- /dev/null
+++ b/modules/m_uptime.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# Copyright (C) 2007-2008 Vsevolod Kozlov #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Bot's uptime command.
+#---------------------------------------------------------------------
+
+module_uptime_INIT() {
+ modinit_API='2'
+ modinit_HOOKS=''
+ commands_register "$1" 'uptime' || return 1
+ helpentry_module_uptime_description="Provides a command to show bot's uptime."
+
+ helpentry_uptime_uptime_syntax=''
+ helpentry_uptime_uptime_description='Shows the uptime for the bot.'
+}
+
+module_uptime_UNLOAD() {
+ return 0
+}
+
+module_uptime_REHASH() {
+ return 0
+}
+
+module_uptime_handler_uptime() {
+ local sender="$1"
+ local formatted_time=
+ time_format_difference $SECONDS formatted_time
+ local target=
+ if [[ $2 =~ ^# ]]; then
+ target="$2"
+ else
+ parse_hostmask_nick "$sender" target
+ fi
+ send_msg "$target" "The bot has been up for $formatted_time."
+}
diff --git a/tools/bashdoc/ChangeLog b/tools/bashdoc/ChangeLog
new file mode 100644
index 0000000..5fcf8cb
--- /dev/null
+++ b/tools/bashdoc/ChangeLog
@@ -0,0 +1,22 @@
+Sun Oct 14 17:24:19 CEST 2007; Arvid Norlander
+ The last two days I have changed so much on bashdoc that I lost count of it
+ Some nice things:
+ Cleaned up code.
+ XHTML and CSS.
+ Made bashdoc able to document variables.
+ Fix many bugs.
+
+Sun Aug 13 13:08:12 PDT 2006; Unknown
+ Fix bug which caused either extra garbage around function links in src or
+ skipped some interlinks.
+ Release bashdoc-0.1.8
+
+Sun Jan 22 16:22:45 EST 2006; Unknown
+ ChangeLog: Started
+ src2html: changed sed to fix the weird invalid range end erors
+ Changes suggested by Jaka Kranjc
+ generate-smgl-docs: Add handy example doc generation
+ script, written by and for SourceMage
+ generate-smgl-docs: Patch by Jaka Kranjc
+ generate-smgl-docs: Add -q to bashdoc call to limit useless output
+ Release: bashdoc-0.1.7
diff --git a/tools/bashdoc/bashdoc.sh b/tools/bashdoc/bashdoc.sh
new file mode 100755
index 0000000..821f9b3
--- /dev/null
+++ b/tools/bashdoc/bashdoc.sh
@@ -0,0 +1,726 @@
+#!/usr/bin/env bash
+# -*- coding: utf-8 -*-
+#--------------------------
+## @Synopsis Reads specialy formated shell scripts and creates docs
+## @Copyright Copyright 2003, Paul Mahon
+## @Copyright Copyright 2007, Arvid Norlander
+## @License GPL v2
+## Parses comments between lines of '#---'
+## Lines to be parsed start with ##. All tags start with @.
+## Lines without a tag are considered simple description of the section.
+## If the line following the comment block doesn't start with 'function'
+## the it's assumed that the comment is for the whole file. Only the first
+## non-function comment block will be used, the other will be ignored.
+## <p>
+## Multiple identical tags are allowed, the contents are appended and separated
+## with a space. @param tags are treated specials and are assumed to be in order.
+## <p>
+## There is an additional &lt;@function FUNCTION_NAME&gt; tag that can be embeded
+## in any bashdoc comment. It will be transformed into a link to that function.
+## Note, this will only work for functions that are defined in the same script.
+## <p><pre>
+## Usage: [OPTIONS] [--] script [ script ...]
+## -p, --project project Name of the project
+## -o, --output directory Specifies the directory you want the resulting html to go into
+## -c, --nocss Do not write default CSS file.
+## -e, --exclusive tag Only output if the block has this tag
+## -q, --quiet Quiet the output
+## -h, --help Display this help and exit
+## -V, --version Output version information and exit
+## -- No more arguments, only scripts
+## script The script you want documented
+##</pre>
+##
+#--------------------------
+
+# Make env sane
+unset LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY
+unset LC_MESSAGES LC_PAPER LC_NAME LC_ADDRESS
+unset LC_TELEPHONE LC_MEASUREMENT LC_IDENTIFICATION
+export LC_ALL=C
+export LANG=C
+
+# Check bash version. We need at least 3.2.x
+# Lets not use anything like =~ here because
+# that may not work on old bash versions.
+if [[ "$(awk -F. '{print $1 $2}' <<< $BASH_VERSION)" -lt 32 ]]; then
+ echo "Sorry your bash version is too old!"
+ echo "You need at least version 3.2 of bash"
+ echo "Please install a newer version:"
+ echo " * Either use your distro's packages"
+ echo " * Or see http://www.gnu.org/software/bash/"
+ exit 2
+fi
+
+# To make set -x more usable
+export PS4='(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]} : '
+
+VERSION="0.1.8"
+HEADERS="<!-- Generated by bashdoc version $VERSION, on $(date +'%Y-%m-%d'). -->
+<link rel=\"stylesheet\" href=\"style.css\" type=\"text/css\" />
+<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />"
+
+GOOD=$'\e[32;01m'
+WARN=$'\e[33;01m'
+BAD=$'\e[31;01m'
+NORMAL=$'\e[0m'
+
+#--------------------------
+## Output error message
+## @param Message
+## @Stderr Formated message
+#--------------------------
+print_error () {
+ echo -e " ${BAD}*${NORMAL} $*" >&2
+}
+#--------------------------
+## Output warning message
+## @param Message
+## @Stderr Formated message
+#--------------------------
+print_warn () {
+ echo -e " ${WARN}*${NORMAL} $*" >&2
+}
+#--------------------------
+## Output info message
+## @param Message
+## @Stderr Formated message
+#--------------------------
+print_info () {
+ echo -e " ${GOOD}*${NORMAL} $*" >&2
+}
+#--------------------------
+## Output debug message
+## @param Message
+## @Stderr Formated message
+#--------------------------
+print_debug () {
+ echo -e " $*" >&2
+}
+
+#--------------------------
+## @Arguments -r: recursive, -o [directory]: output html
+## Parses arguments for this script
+## @Gobals RECURSIVE, OUT_DIR
+#--------------------------
+function args()
+{
+ local retVal=0
+ QUIET=0
+ while true ; do
+ case $1 in
+ -p|--project)
+ PROJECT="$2"
+ (( retVal+=2 ))
+ shift 2
+ ;;
+ -o|--output)
+ OUT_DIR="$2"
+ (( retVal+=2 ))
+ shift 2
+ ;;
+ -c|--nocss)
+ NOCSS="1"
+ (( retVal+=2 ))
+ shift 1
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ -V|--version)
+ version
+ exit 0
+ ;;
+ -e|--exclusive)
+ EXCLUSIVE="${2%%=*}"
+ EXCLUSIVE_VAL="${2#*=}"
+ (( retVal+=2 ))
+ shift 2
+ ;;
+ -q|--quiet)
+ (( QUIET+=1 ))
+ (( retVal+=1 ))
+ shift 1
+ ;;
+ --)
+ (( retVal++ ))
+ return $retVal
+ ;;
+ -*)
+ usage
+ exit 0
+ ;;
+ *)
+ [[ -e $1 ]] && return $retVal
+ echo "$1 doesn't exist."
+ usage
+ exit 1
+ ;;
+ esac
+ done
+}
+
+#-------------------------
+## Version for this script
+## @Stdout Version information
+#-------------------------
+function version()
+{
+ echo "bashdoc $VERSION - Generate HTML documentation from bash scripts"
+ echo ''
+ echo 'Copyright (C) 2003 Paul Mahon'
+ echo 'Copyright (C) 2007 Arvid Norlander'
+ echo 'This is free software; see the source for copying conditions. There is NO'
+ echo 'warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.'
+ echo ''
+ echo 'Written by Paul Mahon and modified by Arvid Norlander'
+}
+
+#-------------------------
+## Usage for this script
+## @Stdout Usage information
+#-------------------------
+function usage()
+{
+cat <<- EOF
+bashdoc generates HTML documentation from bash scripts.
+
+Usage: $(basename $0) [OPTIONS] [--] script [script ...]
+
+Options:
+ -p, --project project Name of the project
+ -o, --output directory Specifies the directory you want the resulting html to go into
+ -c, --nocss Do not write default CSS file.
+ -e, --exclusive tag Only output if the block has this tag
+ -q, --quiet Quiet the output
+ -h, --help Display this help and exit
+ -V, --version Output version information and exit
+ -- No more arguments, only scripts
+ script The script you want documented
+
+Examples:
+ bashdoc.sh -p bashdoc -o docs/ bashdoc.sh Generate documentation for this program.
+ bashdoc.sh -p appname -o docs/ -e Type=API someapp.sh Generate documentation for someapp.sh,
+ exclude items that do not include the tag
+ @Type API
+EOF
+}
+
+
+#--------------------------
+## Reads until it has read an entire comment block. A block starts with
+## <br><pre>#---</pre></br>
+## Alone on a line, and continues until the next
+## <br><pre>#---</pre></br>
+## All comment lines inside should have ## at the start or they
+## will be ignored.
+##
+## @return 0 Possibly more blocks
+## @return 1 Unexpected end of file
+## @return 2 Expected end of file, no more blocks
+## @Stdin Reads a chunk
+## @Stdout Block with starting '##' removed
+## @Globals paramDesc, retDesc, desc, block, split, out_comment_block
+#--------------------------
+function get_comment_block()
+{
+ local inComment commentBlock lastLine=""
+ commentBlock=""
+ while read LINE ; do
+ (( srcLine++ ))
+ if [[ ${LINE:0:4} == '#---' ]] ; then
+ if [[ $inComment ]] ; then
+ out_comment_block="$commentBlock"
+ # I'm not sure why this is needed but it fixes incorrect line number
+ (( srcLine++ ))
+ return 0
+ else
+ inComment=yes
+ fi
+ elif [[ ${LINE:0:2} != '##' ]] && [[ $inComment ]] ; then
+ [[ $QUIET -lt 1 ]] && print_warn "Line $srcLine of $FILE isn't a doc comment! Ignoring."
+ [[ $QUIET -lt 1 ]] && print_warn "Line in question is: $LINE"
+ elif [[ $inComment ]] ; then
+ commentBlock="$commentBlock"$'\n'${LINE####}
+ fi
+ done
+ #If we make it out here, we hit the end of the file
+ if [[ $commentBlock ]] ; then
+ #If there is a comment block started, then it never ended
+ [[ $QUIET -lt 2 ]] && print_error "Unfinished comment block:"
+ [[ $QUIET -lt 2 ]] && print_error "$commentBlock"
+ return 1
+ else
+ return 2
+ fi
+}
+
+
+#-----------------------
+## Parses the comments from stdin. Also reads the (non-commented)
+## function name. Mostly uses <@function parse_block> and
+## <@function output_parsed_block> to do the read work.
+## @Stdin Reads line after comment block
+## @Globals paramDesc, retDesc, desc, block, split, out_comment_block
+#-----------------------
+function parse_comments()
+{
+
+ #We use a lot of $( echo ... ) in here to trim the blanks
+
+ local funcLine funcName
+ paramDesc=()
+ retDesc=()
+ local FIRST_BLOCK="yes"
+ local skipRead
+ local outBlock=""
+ local lastOutBlock=""
+ srcLine=0
+ # 1 = function
+ # 2 = variable
+ itemtype=0
+ while true ; do
+ paramNames=()
+ paramDesc=()
+ split=()
+ retDesc=()
+ desc=""
+ itemtype=0
+ unset out_comment_block
+ get_comment_block
+ [[ $? -gt 0 ]] && break
+ block="$out_comment_block"
+
+ if [[ $skipRead ]] ; then
+ skipRead=""
+ else
+ funcLine=""
+ funcName=""
+ read funcLine
+ fi
+ # Is it a (global) variable?
+ # Check before function to catch arrays.
+ if [[ ${funcLine} =~ ^(declare -r +)?([a-zA-Z_][a-zA-Z0-9_]*)=.+$ ]]; then
+ varName="${BASH_REMATCH[@]: -1}"
+ itemtype=2
+ # Is it a function?
+ elif [[ ${funcLine%%[[:blank:]]*} == function ]] || [[ ${funcLine} =~ ^[^\ ]+\ *\(\)\ *\{?$ ]]; then
+ funcName=$( echo ${funcLine#function} )
+ funcName=$( echo ${funcName%%()*} )
+ itemtype=1
+ fi
+ if [[ $funcName ]] || [[ $varName ]] || [[ $FIRST_BLOCK ]] ; then
+ # Only bother with this block if it is a function block or
+ # the first script block
+
+ #This fills in paramDesc[*], tag_*, retDesc
+ parse_block
+ lastOutBlock="$outBlock"
+ outBlock=$(output_parsed_block)
+
+ if [[ $FIRST_BLOCK ]] && [[ ! $funcName ]] && [[ ! $varName ]]; then
+ FIRST_BLOCK=""
+ fi
+
+ if [[ $EXCLUSIVE ]] ; then
+ # If this is first block, include it anyway.
+ if [[ $funcName ]] || [[ $varName ]]; then
+ local i="tag_${EXCLUSIVE}"
+ if [[ ${!i} != $EXCLUSIVE_VAL ]] ; then
+ if [[ $itemtype = 2 ]]; then
+ funcName="$varName"
+ fi
+ print_debug "$funcName block ignored, no $EXCLUSIVE=$EXCLUSIVE_VAL tag."
+ # Code duplication but hard to avoid
+ for i in ${!tag_*} ; do
+ unset $i
+ done
+ continue
+ fi
+ fi
+ fi
+
+ for i in ${!tag_*} ; do
+ unset $i
+ done
+
+ if [[ $funcName ]]; then
+ FUNC_LIST="$FUNC_LIST $funcName"
+ elif [[ $varName ]]; then
+ VAR_LIST="$VAR_LIST $varName"
+ fi
+ unset funcName varName
+ echo "$outBlock"
+
+ else
+ [[ $QUIET -lt 2 ]] && print_warn "Ignoring non-first non-function/variable comment block"
+ [[ $QUIET -lt 1 ]] && print_warn "$block"
+ fi
+ done
+}
+
+#---------------------
+## Create HTML from the non-special tags
+## @param var or func (is this for a variable or function)
+## @Stdout HTMLized tags
+#---------------------
+function output_parsed_tags() {
+ local i
+ for i in ${!tag_*} ; do
+ # Convert _ in tags to space. Looks better.
+ echo " <h3 class=\"othertag ${1}othertag ${i/tag_/tag-}\">$(sed 's/_/ /g' <<< "${i#tag_}")</h3>"
+ # This may be fun, allow special formatting by tag.
+ echo " <p class=\"othertag ${1}othertag ${i/tag_/tag-}\">"
+ echo " ${!i}"
+ echo " </p>"
+ unset $i
+ done
+}
+
+#---------------------
+## Outputs the parsed information in a nice pretty format.
+## @Stdout formated documentation
+## @Globals paramDesc, retDesc, desc, block, split
+#---------------------
+function output_parsed_block()
+{
+ echo "<hr />"
+ if [[ $itemtype -eq 1 ]] && [[ $funcName ]]; then
+ echo "<!-- Block for $funcName -->"
+ echo " <h2 id=\"$funcName\" class=\"function\">function <strong>$funcName</strong>()</h2>"
+ echo " <h3>Parameters:</h3>"
+ echo " <ul class=\"paramerters\">"
+ if [[ ${#paramDesc[*]} -gt 0 ]] ; then
+ for(( i=0; i<"${#paramDesc[@]}"; i++ )) ; do
+ echo " <li class=\"paramerters\">\$$[i+1]: ${paramDesc[i]}</li>"
+ done
+ else
+ echo "<li>None</li>"
+ fi
+ echo " </ul>"
+ if [[ ${#retDesc[*]} -gt 0 ]] ; then
+ echo " <h3>Returns:</h3>"
+ echo " <ul class=\"returns\">"
+ for(( i=0; i<"${#retDesc[@]}"; i++ )) ; do
+ echo " <li class=\"returns\">${retDesc[i]}</li>"
+ done
+ echo " </ul>"
+ fi
+
+ output_parsed_tags func
+ [[ $desc ]] && echo "<h3>Description</h3><p class=\"description funcdescription\">$desc</p>"
+ elif [[ $itemtype -eq 2 ]]; then
+ echo "<!-- Block for $varName -->"
+ echo " <h2 id=\"$varName\" class=\"variable\">variable <strong>$varName</strong></h2>"
+ output_parsed_tags var
+ [[ $desc ]] && echo "<h3>Description</h3><p class=\"description vardescription\">$desc</p>"
+ else
+ echo '<!-- Header for whole script -->'
+ echo "<h1>$FILE</h1>"
+ echo " <p class=\"filedescription\">$desc</p>"
+ echo "$desc" >> $SCRIPT_DESC
+
+ for i in ${!tag_*} ; do
+ echo " <h3 class=\"fileothertag ${i/tag_/tag-}\">${i#tag_}</h3>"
+ echo " <p class=\"fileothertag ${i/tag_/tag-}\">${!i}</p>"
+ unset $i
+ done
+ fi
+
+}
+
+#---------------
+## Does the real work of the parsing. Tags start with @. Special
+## tags are @return and @param. Doc lines without a tag are
+## considered description.
+## @Globals paramDesc, retDesc, desc, block, split
+#---------------
+function parse_block()
+{
+ local tag
+ local backIFS="$IFS"
+ IFS=$'\n'
+ for LINE in $block; do
+ IFS="$backIFS"
+ LINE=$( echo $LINE )
+ if [[ ${LINE:0:1} == '@' ]] ; then
+ split_tag split $LINE
+ case ${split} in
+ @param)
+ #paramNames[${#paramNames[*]}]=${split[1]}
+ paramDesc=( "${paramDesc[@]}" "${split[1]}" )
+ ;;
+ @return)
+ retDesc=( "${retDesc[@]}" "${split[1]}" )
+ ;;
+ @*)
+ tag=${split[0]#@}
+ local i="tag_${tag}"
+ if [[ ${!i} ]] ; then
+ local varname="tag_${tag}"
+ eval "tag_${tag}=\"\${!varname}"$'\n'"\${split[1]}\""
+ else
+ eval "tag_${tag}=\"\${split[1]}\""
+ fi
+ ;;
+ *)
+ print_error "We shouldn't get here... it was a tag, but not a tag?"
+ ;;
+ esac
+ else
+ desc="$desc"$'\n'"$LINE"
+ fi
+ done
+ IFS="$backIFS"
+}
+
+#----------------
+## Splits a line that starts with a tag into tag and data.
+## @param Variable you want the result put into. Array is format is ( tag, data ).
+## @param Tag
+## @param Data
+## @Globals The variable in $1 will get the results
+#----------------
+function split_tag()
+{
+ local out="${1}" ; shift
+ local tag=$( echo ${1} ) ; shift
+# local key=$( echo ${1} ) ; shift
+ local value=$( echo $* )
+ eval "$out=( \"\$tag\" \"\${value}\" )"
+}
+
+#--------------------
+## Outputs a header for script pages
+## @Stdout html header
+## @param Script name
+#--------------------
+function script_header()
+{
+cat <<- EOF > $OUT_FILE
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+ <head>
+ $HEADERS
+ <title>$1 - $PROJECT</title>
+ </head>
+ <body>
+ <p class="right">
+ <a href="script_list.html">Script Index</a>
+ </p>
+EOF
+}
+
+# Initialise project variables
+OUT_DIR=$( dirname $0 )
+NOCSS=0
+args "$@"
+shift $?
+[[ $OUT_DIR ]] || OUT_DIR="."
+
+# Create output directory in case it doesn't exist
+mkdir -p "$OUT_DIR" || {
+ print_error "Failed to create output directory."
+ exit 1
+}
+
+if [[ $NOCSS = 0 ]]; then
+ print_info "Writing CSS"
+ # Copy stylesheet to output directory.
+ cat <<- EOF > "${OUT_DIR}/style.css"
+ /* Based on Trac CSS */
+ body {
+ background: #fff;
+ color: #000;
+ margin: 10px;
+ padding: 0;
+ }
+ body, th, td {
+ font: normal 13px verdana,arial,'Bitstream Vera Sans',helvetica,sans-serif;
+ }
+ h1, h2, h3, h4 {
+ font-family: arial,verdana,'Bitstream Vera Sans',helvetica,sans-serif;
+ font-weight: bold;
+ letter-spacing: -0.018em;
+ }
+ h1 { font-size: 19px; margin: .15em 1em 0 0 }
+ h2 { font-size: 16px; font-weight: normal; }
+ h3 { font-size: 14px }
+ hr { border: none; border-top: 1px solid #ccb; margin: 2em 0 }
+ address { font-style: normal }
+ img { border: none }
+ tt { white-space: pre }
+ :link, :visited {
+ text-decoration: none;
+ color: #b00;
+ border-bottom: 1px dotted #bbb;
+ }
+ :link:hover, :visited:hover {
+ background-color: #eee;
+ color: #555;
+ }
+ h1 :link, h1 :visited ,h2 :link, h2 :visited, h3 :link, h3 :visited,
+ h4 :link, h4 :visited, h5 :link, h5 :visited, h6 :link, h6 :visited {
+ color: inherit;
+ }
+ /* Partly own stuff: */
+ .nav body {
+ margin: 0;
+ padding: 0;
+ background: inherit;
+ color: inherit;
+ }
+ .nav ul { font-size: 11px; list-style: none; margin: 0; padding: 0; text-align: left }
+ .nav li {
+ display: block;
+ padding: 0;
+ margin: 0;
+ white-space: nowrap;
+ }
+ /* Own stuff */
+ .nav-header {
+ font-weight: bold;
+ }
+ .right { text-align: right }
+ .tag-Deprecated { color: #e00; }
+ EOF
+else
+ print_warn "Not writing a stylesheet. You will need to add your own by hand afterwards."
+fi
+
+while [[ $# -gt 0 ]] ; do
+
+ #Initialise vars for this src
+ FILE=$1
+ [[ ! -f $FILE ]] || [[ ! -r $FILE ]] && {
+ print_error "$FILE is not a file or is not readable, skipping."
+ shift
+ continue
+ }
+ print_info "Parsing $FILE"
+ shift
+ OUT_FILE=${FILE#/} #Remove leading /
+ OUT_FILE="$OUT_DIR/${OUT_FILE//\//.}.html"
+ FUNC_FILE="${OUT_FILE%.html}.funcs"
+ VAR_FILE="${OUT_FILE%.html}.vars"
+ SCRIPT_DESC="${OUT_FILE%.html}.desc"
+ # Store real name (reuse in script list)
+ REAL_NAME_FILE="${OUT_FILE%.html}.name"
+ echo -n "${FILE#/}" > "$REAL_NAME_FILE"
+
+ FUNC_LIST=""
+ VAR_LIST=""
+
+ #Start this src's html file
+ script_header "$FILE"
+
+ # Parse and write out function list
+ {
+ parse_comments < $FILE
+ echo "$FUNC_LIST" > $FUNC_FILE
+ echo "$VAR_LIST" > $VAR_FILE
+ # Convert references like <@function file,functioname> into links
+ } | sed -e 's!<@[[:blank:]]*function \([^,>]*\)[[:blank:]]*>!<a href="#\1">\1</a>!g' \
+ -e 's!<@[[:blank:]]*function \([^,>]*\),[[:blank:]]*\([^>]*\)[[:blank:]]*>!<a href="\1#\2">\1</a>!g' >> $OUT_FILE
+ #Close off the html for this src
+ cat <<- EOF >> $OUT_FILE
+ </body>
+ </html>
+ EOF
+
+done #Go on to next src
+
+#Now for tying the scripts all together
+pushd $OUT_DIR >/dev/null
+
+print_info "Writing function list"
+# Start page that will have all the function calls
+cat <<- EOF > function_list.html
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+ <head>
+ $HEADERS
+ <title>Functions of $PROJECT</title>
+ </head>
+ <body class="nav">
+ <ul class="nav">
+EOF
+
+echo "<li class=\"nav nav-header\">Functions</li>" >> function_list.html
+# Merge function lists of all sources, sort by function name
+for i in *.funcs ; do
+ for f in $( cat $i ) ; do
+ echo "$f <li class=\"nav nav-function\"><a href=\"${i%.funcs}.html#$f\" target=\"main\">$f</a></li>"
+ done
+done | sort | cut -d' ' -f2- >> function_list.html
+
+echo "<li class=\"nav nav-header\">Variables</li>" >> function_list.html
+for i in *.vars ; do
+ for v in $( cat $i ) ; do
+ echo "$v <li class=\"nav nav-variable\"><a href=\"${i%.vars}.html#$v\" target=\"main\">$v</a></li>"
+ done
+done | sort | cut -d' ' -f2- >> function_list.html
+
+# Close off the html for the global function list
+cat <<- EOF >> function_list.html
+ </ul>
+ </body>
+</html>
+EOF
+
+print_info "Writing script list"
+# Start the list of scripts
+TITLE="Scripts"
+[[ $PROJECT ]] && TITLE="$PROJECT Script Documentation"
+cat <<- EOF > script_list.html
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+ <head>
+ $HEADERS
+ <title>Scripts of $PROJECT</title>
+ </head>
+ <body>
+ <h1>$TITLE</h1>
+ <hr />
+ <dl>
+EOF
+
+# List all the sources + descriptions, sort by script dir/name
+for i in *.name ; do
+ name=${i%.name}
+ echo "${name} $(cat "$i")"
+done | sort | while read LINE realname; do
+ echo "<dt><a href=\"${LINE}.html\">$realname</a></dt>"
+ echo "<dd>"
+ cat ${LINE}.desc 2>/dev/null || { [[ $QUIET -lt 2 ]] && print_warn "$LINE has no description."; }
+ echo "</dd>"
+done >> script_list.html
+
+# Close off the html for the global script list
+cat <<- EOF >> script_list.html
+ </dl>
+ </body>
+</html>
+EOF
+
+print_info "Writing index file"
+# Create the index file for the whole shbang
+cat <<- EOF > index.html
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+ <head>
+ $HEADERS
+ <title>BashDoc - $PROJECT</title>
+ </head>
+ <frameset cols="25%,*">
+ <frame src="function_list.html" name="function_list" />
+ <frame src="script_list.html" name="main" />
+ </frameset>
+</html>
+EOF
+
+# Remove the temporary .desc and .name files, leave the .func and .vars files, someone may want them later.
+rm *.desc
+rm *.name
+popd >/dev/null
diff --git a/tools/build_numerics.sh b/tools/build_numerics.sh
new file mode 100755
index 0000000..cb225a7
--- /dev/null
+++ b/tools/build_numerics.sh
@@ -0,0 +1,102 @@
+#!/usr/bin/env bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Generate list of numerics from the numerics.txt<br />
+## Output to STDOUT.<br />
+## Run this using make numerics in the main directory.
+#---------------------------------------------------------------------
+
+# Clean up env, just in case.
+unset LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY
+unset LC_MESSAGES LC_PAPER LC_NAME LC_ADDRESS
+unset LC_TELEPHONE LC_MEASUREMENT LC_IDENTIFICATION
+export LC_ALL=C
+export LANG=C
+
+cat << EOF
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+
+###########################################################################
+# #
+# WARNING THIS FILE IS AUTOGENERATED. ANY CHANGES WILL BE OVERWRITTEN! #
+# See the source in tools/numerics.txt for comments about some numerics #
+# This file was generated with tools/build_numerics.sh #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## Auto-generated list of numerics from tools/numerics.txt<br />
+## This file contains a list of numerics that we currently use.
+## It is therefore incomplete.<br />
+## Because the list of variables in this file is so long, please see
+## it's source for more details.
+#---------------------------------------------------------------------
+
+##########################
+# Name -> number mapping #
+##########################
+
+EOF
+# The numerics above are special case, otherwise bash strips leading 0.
+
+# Yes a bash file with .txt..
+source tools/numerics.txt || { echo 'Failed to source.' >&2; exit 1; }
+
+for index in ${!numeric[*]}; do
+ printf "numeric_%s='%03i'\n" "${numeric[$index]}" "$index"
+done
+
+# Same special case as above.
+cat << EOF
+
+##########################
+# Number -> name mapping #
+##########################
+
+EOF
+for index in ${!numeric[*]}; do
+ echo "numerics[$index]='${numeric[$index]}'"
+done
+
+cat << EOF
+
+# End of generated file.
+EOF
diff --git a/tools/numerics.txt b/tools/numerics.txt
new file mode 100644
index 0000000..1835bf6
--- /dev/null
+++ b/tools/numerics.txt
@@ -0,0 +1,215 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an irc bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+# This file contains a list of numerics that we *currently use*.
+# It is therefore incomplete.
+
+# Sources:
+# RFC 1459
+# RFC 2812
+# http://www.alien.net.au/irc/irc2numerics.html
+# http://www.inspircd.org/wiki/List_Of_Numerics
+
+# These are from RFC 1459, if not mentioned otherwise
+
+##########################
+# Number -> name mapping #
+##########################
+
+# During connect, these are sent. They are NOT part of RFC 1459.
+# For some format of the parameters varies between servers.
+numeric[1]=RPL_WELCOME # "Welcome to <network>"
+numeric[2]=RPL_YOURHOST # "Your host is <servername>, running version <ver>"
+numeric[3]=RPL_CREATED
+numeric[4]=RPL_MYINFO # "<servername> <version> <available user modes> <available channel modes>"
+numeric[5]=RPL_ISUPPORT # Not in any RFC. See http://www.irc.org/tech_docs/005.html for incomplete list.
+
+numeric[6]=RPL_MAP # Not from any RFC
+numeric[7]=RPL_MAPEND # Not from any RFC
+numeric[8]=RPL_SNOMASK # Not from any RFC, used on Unreal
+
+# Command replies.
+numeric[205]=RPL_TRACEUSER
+numeric[213]=RPL_STATSCLINE
+numeric[219]=RPL_ENDOFSTATS
+numeric[221]=RPL_UMODEIS
+numeric[223]=RPL_STATSELINE # Not from any RFC. Conflicting use on different IRCds.
+numeric[232]=RPL_RULES # Unreal usage. Conflicting use on different IRCds.
+numeric[242]=RPL_STATSUPTIME
+numeric[250]=RPL_STATSCONN # Not from any RFC. Conflicting use on different IRCds.
+numeric[251]=RPL_LUSERCLIENT
+numeric[252]=RPL_LUSEROP
+numeric[253]=RPL_LUSERUNKNOWN
+numeric[254]=RPL_LUSERCHANNELS
+numeric[255]=RPL_LUSERME
+numeric[256]=RPL_ADMINME
+numeric[257]=RPL_ADMINLOC1
+numeric[258]=RPL_ADMINLOC2
+numeric[259]=RPL_ADMINEMAIL
+numeric[263]=RPL_TRYAGAIN
+numeric[265]=RPL_LOCALUSERS # Not from any RFC.
+numeric[266]=RPL_GLOBALUSERS # Not from any RFC.
+numeric[271]=RPL_SILELIST # Not from any RFC
+numeric[272]=RPL_ENDOFSILELIST # Not from any RFC
+
+numeric[301]=RPL_AWAY
+numeric[302]=RPL_USERHOST
+numeric[303]=RPL_ISON
+numeric[304]=RPL_TEXT # Not from any RFC I think. And on InspIRCd this is used for syntax hints.
+numeric[305]=RPL_UNAWAY
+numeric[306]=RPL_UNAWAY
+numeric[307]=RPL_WHOISREGNICK # Not from any RFC. Used on Unreal.
+numeric[308]=RPL_RULESSTART # Unreal usage. Conflicting use on different IRCds.
+numeric[309]=RPL_ENDOFRULES # Unreal usage. Conflicting use on different IRCds.
+numeric[310]=RPL_WHOISHELPOP # Unreal usage. Conflicting use on different IRCds.
+numeric[311]=RPL_WHOISUSER
+numeric[312]=RPL_WHOISSERVER
+numeric[313]=RPL_WHOISOPERATOR
+numeric[314]=RPL_WHOWASUSER
+numeric[315]=RPL_ENDOFWHO
+numeric[317]=RPL_WHOISIDLE
+numeric[318]=RPL_ENDOFWHOIS
+numeric[319]=RPL_WHOISCHANNELS
+numeric[320]=RPL_WHOISSPECIAL # Not from any RFC. Numeric called other things on other ircds.
+ # RPL_WHOISSPECIAL is what it is called on Unreal.
+ # Used for "connecting using ssl" on InspIRCd. Also used for SWHOIS.
+ # On hyperion used for "is identified to services".
+numeric[321]=RPL_LISTSTART
+numeric[322]=RPL_LIST
+numeric[323]=RPL_LISTEND
+numeric[324]=RPL_CHANNELMODEIS
+numeric[329]=RPL_CREATIONTIME # Not from any RFC. Used on InspIRCd at least.
+numeric[330]=RPL_WHOISACCOUNT # Not from any RFC. This is how it is used on InspIRCd with m_services_account.
+numeric[331]=RPL_NOTOPIC
+numeric[332]=RPL_TOPIC
+numeric[333]=RPL_TOPICWHOTIME # Not from any RFC.
+numeric[340]=RPL_USERIP # Not from any RFC.
+numeric[341]=RPL_INVITING
+numeric[346]=RPL_INVITELIST
+numeric[347]=RPL_ENDOFINVITELIST
+numeric[348]=RPL_EXCEPTLIST
+numeric[349]=RPL_ENDOFEXCEPTLIST
+numeric[351]=RPL_VERSION
+numeric[352]=RPL_WHOREPLY
+numeric[353]=RPL_NAMREPLY
+numeric[364]=RPL_LINKS
+numeric[365]=RPL_ENDOFLINKS
+numeric[366]=RPL_ENDOFNAMES
+numeric[367]=RPL_BANLIST
+numeric[368]=RPL_ENDOFBANLIST
+numeric[369]=RPL_ENDOFWHOWAS
+numeric[371]=RPL_INFO
+numeric[372]=RPL_MOTD
+numeric[374]=RPL_ENDOFINFO
+numeric[375]=RPL_MOTDSTART
+numeric[376]=RPL_ENDOFMOTD
+numeric[378]=RPL_WHOISHOST
+numeric[381]=RPL_YOUREOPER
+numeric[382]=RPL_REHASHING
+numeric[391]=RPL_TIME
+numeric[396]=RPL_HOSTHIDDEN # Not from any RFC.
+
+
+# Errors
+numeric[401]=ERR_NOSUCHNICK
+numeric[402]=ERR_NOSUCHSERVER
+numeric[403]=ERR_NOSUCHCHANNEL
+numeric[404]=ERR_CANNOTSENDTOCHAN
+numeric[405]=ERR_TOOMANYCHANNELS
+numeric[406]=ERR_WASNOSUCHNICK
+numeric[407]=ERR_TOOMANYTARGETS
+numeric[412]=ERR_NOTEXTTOSEND
+numeric[416]=ERR_TOOMANYMATCHES # Not from any RFC.
+numeric[421]=ERR_UNKNOWNCOMMAND
+numeric[422]=ERR_NOMOTD
+numeric[432]=ERR_ERRONEUSNICKNAME # Bad/forbidden nickname
+numeric[433]=ERR_NICKNAMEINUSE # Nick in use
+numeric[438]=ERR_NICKTOOFAST # Not from any RFC. Used on ircu and Unreal (at least).
+numeric[441]=ERR_USERNOTINCHANNEL
+numeric[442]=ERR_NOTONCHANNEL
+numeric[443]=ERR_USERONCHANNEL
+numeric[445]=ERR_SUMMONDISABLED # Yep, most (all?) do nowdays :)
+numeric[446]=ERR_USERSDISABLED # Yep, most (all?) do nowdays :)
+numeric[447]=ERR_NONICKCHANGE # Not from any RFC.
+numeric[460]=ERR_NOTFORHALFOPS # Not from any RFC. Unreal got this at least.
+numeric[461]=ERR_NEEDMOREPARAMS
+numeric[462]=ERR_ALREADYREGISTERED
+numeric[468]=ERR_ONLYSERVERSCANCHANGE # Not from any RFC.
+numeric[470]=ERR_LINKCHANNEL # Not from any RFC.
+ # InspIRCd example: :#channel has become full, so you are automatically being transferred to the linked channel #otherchannel
+numeric[471]=ERR_CHANNELISFULL
+numeric[472]=ERR_UNKNOWNMODE
+numeric[473]=ERR_INVITEONLYCHAN
+numeric[474]=ERR_BANNEDFROMCHAN
+numeric[475]=ERR_BADCHANNELKEY
+numeric[477]=ERR_NEEDREGGEDNICK # Not from any RFC.
+numeric[478]=ERR_BANLISTFULL
+numeric[480]=ERR_CANNOTKNOCK # Not from any RFC.
+numeric[481]=ERR_NOPRIVILEGES
+numeric[482]=ERR_CHANOPRIVSNEEDED
+numeric[484]=ERR_ATTACKDENY # Name on Unreal. No idea use on Unreal.
+ # InspIRCd: 484 <nick> <channel> :Can't kick user <nick> from channel (+Q set)
+numeric[489]=ERR_SECUREONLYCHAN # Not from any RFC. Used on Unreal and InspIRCd at least.
+numeric[490]=ERR_ALLMUSTUSESSL # InspIRCd specific numeric. I made up this name, I don't know correct name.
+ # 490 <nick> <channel> :all members of the channel must be connected via SSL
+numeric[491]=ERR_NOOPERHOST
+numeric[495]=ERR_NOREJOINONKICK # InspIRCd specific numeric. I made up this name, I don't know correct name.
+ # 495 <nick> <channel> :You cannot rejoin this channel yet after being kicked (+J)
+numeric[499]=ERR_CHANOWNPRIVNEEDED # Not from any RFC. Unreal got this at least.
+
+numeric[501]=ERR_UMODEUNKNOWNFLAG # Some send this for unknown umodes. not all.
+numeric[502]=ERR_USERSDONTMATCH # Trying to change mode for other user.
+
+# Others. Not from any RFC but semi standard.
+numeric[600]=RPL_LOGON # Unreal, InspIRCd and more
+numeric[601]=RPL_LOGOFF # Unreal, InspIRCd and more
+numeric[602]=RPL_WATCHOFF # Unreal, InspIRCd and more
+numeric[604]=RPL_NOWON # Unreal, InspIRCd and more
+numeric[605]=RPL_NOWOFF # Unreal, InspIRCd and more
+numeric[606]=RPL_WATCHLIST # Unreal, InspIRCd and more
+numeric[607]=RPL_ENDOFWATCHLIST # Unreal, InspIRCd and more
+
+numeric[671]=RPL_WHOISSECURE # Used on Unreal for ssl clients.
+
+
+# IRCd specific, these are InspIRCd ones unless said otherwise.
+# As we can't include more than one meaning for every numeric conflicting
+# ones may be added as comments. Modules depending on 9xx numerics should use the raw value.
+# As I mainly use InspIRCd I prioritize those. ;)
+numeric[900]=RPL_MODULES
+numeric[901]=RPL_ENDOFMODULES
+numeric[902]=RPL_COMMANDS # 902 <nick> :<command> <module name> <minimum parameters>
+numeric[903]=RPL_ENDOFCOMMANDS # 903 <nick> :End of COMMANDS list
+numeric[936]=ERR_CENSORED # 936 <nick> <channel> <word> :Your message contained a censored word, and was blocked
+numeric[937]=ERR_ALREDYCENSORED # 937 <nick> <channel> :The word %s is already on the spamfilter list
+numeric[938]=ERR_NOTCENSORED # 938 <nick> <channel> :No such spamfilter word is set
+numeric[939]=ERR_SPAMFILTERLISTFULL # 939 <nick> <channel> :Channel spamfilter list is full
+numeric[940]=RPL_ENDOFSPAMFILTER # 940 <nick> <channel> :End of channel spamfilter list
+numeric[941]=RPL_SPAMFILTER # 941 <nick> <channel> <spamfilter>
+numeric[942]=ERR_INVALIDNICK # 942 <nick> <nick> :Invalid user specified.
+numeric[950]=RPL_SILENCEREMOVED # 950 <nick> <nick> :Removed <nick>!*@* from silence list
+numeric[951]=RPL_SILENCEADDED # 951 <nick> <nick> :Added <nick>!*@* to silence list
+numeric[952]=ERR_ALREADYSILENCE # 952 <nick> <nick> :<nick> is already on your silence list
+numeric[972]=ERR_CANNOTDOCOMMAND # Unreal uses 972 (ERR_CANNOTDOCOMMAND) for umode +q, and other failed kicks.
+ # According to http://www.alien.net.au/irc/irc2numerics.html:
+ # "Works similarly to all of KineIRCd's CANNOT* numerics. This one indicates that a
+ # command could not be performed for an arbitrary reason. For example, a halfop trying to kick an op."
+numeric[974]=ERR_CANNOTCHANGECHANMODE # Unreal uses 974 (ERR_CANNOTCHANGECHANMODE ?) for ERR_ALLMUSTUSESSL.
diff --git a/transport/dev-tcp.sh b/transport/dev-tcp.sh
new file mode 100644
index 0000000..a19b2b3
--- /dev/null
+++ b/transport/dev-tcp.sh
@@ -0,0 +1,122 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## A transport module using /dev/tcp
+#---------------------------------------------------------------------
+
+# A list of features supported
+# These are used: ipv4, ipv6, ssl, nossl, bind
+transport_supports="ipv4 ipv6 nossl"
+
+# Check if all the stuff needed to use this transport is available
+# Return status
+# 0 yes
+# 1 no
+transport_check_support() {
+ # If anyone can tell me how to check if /dev/tcp is supported
+ # without trying to make a connection (that could fail for so
+ # many other reasons), please contact me.
+ echo "NOTE: It is possible that this transport is not supported on your system"
+ echo " However, there is no way it can be checked except trying to connect."
+ echo " If you see an error below try netcat or socat transport instead."
+ return 0
+}
+
+# Try to connect
+# Parameters
+# $1 hostname/IP
+# $2 port
+# $3 If 1 use SSL. If the module does not support it, just ignore it.
+# $4 IP to bind to if any and if supported
+# If the module does not support it, just ignore it.
+# Return status
+# 0 if Ok
+# 1 if connection failed
+transport_connect() {
+ exec 3<&-
+ exec 3<> "/dev/tcp/${1}/${2}"
+ time_get_current 'transport_lastvalidtime'
+}
+
+# Called to close connection
+# No parameters, no return code check
+transport_disconnect() {
+ exec 3<&-
+ # To force code to consider this disconnected.
+ transport_lastvalidtime=0
+}
+
+# Return status
+# 0 If connection is still alive
+# 1 If it isn't.
+transport_alive() {
+ local newtime=
+ time_get_current 'newtime'
+ (( newtime - transport_lastvalidtime > 300 )) && return 1
+ return 0
+}
+
+# Return a line in the variable line.
+# Return status
+# 0 If Ok
+# 1 If connection failed
+transport_read_line() {
+ while true
+ do
+ ze_length="$( wc -l '/tmp/un-provoked-message-store' 2> /dev/null )"
+
+ if [[ -r /tmp/un-provoked-message-store ]] &&
+ [[ -w /tmp/un-provoked-message-store ]] && (( ${ze_length%% *} ))
+ then
+ read -r line < /tmp/un-provoked-message-store
+ line=':tlCJ99mfZl!~user@2001:ba8:1f1:f216::5 PRIVMSG #parabola :'"${line}"
+
+ if (( ${ze_length%% *} < 2 ))
+ then
+ echo -n > /tmp/un-provoked-message-store
+ else
+ tail -n +2 /tmp/un-provoked-message-store |
+ sponge /tmp/un-provoked-message-store
+ fi
+
+ break
+ else
+ read -t 5 -ru 3 line
+ the_return_code="${?}"
+ (( the_return_code == 0 )) && break
+ (( the_return_code > 128 )) && continue
+ (( the_return_code -ne 0 )) && return
+ fi
+ done
+
+ time_get_current 'transport_lastvalidtime'
+ line=${line//$'\r'/}
+}
+
+
+# Send a line
+# Parameters
+# $* send this
+# Return code not checked.
+transport_write_line() {
+ echo "$*" >&3
+}
diff --git a/transport/gnutls.sh b/transport/gnutls.sh
new file mode 100644
index 0000000..3df8ebe
--- /dev/null
+++ b/transport/gnutls.sh
@@ -0,0 +1,121 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## A transport module using gnutls-cli
+#---------------------------------------------------------------------
+
+# A list of features supported
+# These are used: ipv4, ipv6, ssl, nossl, bind
+transport_supports="ipv4 ipv6 ssl"
+
+# Check if all the stuff needed to use this transport is available
+# Return status
+# 0 yes
+# 1 no
+transport_check_support() {
+ hash gnutls-cli >/dev/null 2>&1 || {
+ log_fatal "Can't find gnutls-cli (needed for this transport)"
+ return 1
+ }
+ hash mkfifo >/dev/null 2>&1 || {
+ log_fatal "Can't find mkfifo (needed for this transport)"
+ return 1
+ }
+ return 0
+}
+
+# Try to connect
+# Parameters
+# $1 hostname/IP
+# $2 port
+# $3 If 1 use SSL. If the module does not support it, just ignore it.
+# $4 IP to bind to if any and if supported
+# If the module does not support it, just ignore it.
+# Return status
+# 0 if Ok
+# 1 if connection failed
+transport_connect() {
+ transport_tmp_dir_file="$(mktemp -dt envbot.gnutls.XXXXXXXXXX)" || return 1
+ # To keep this simple, from client perspective.
+ # We WRITE to out and READ from in
+ mkfifo "${transport_tmp_dir_file}/in"
+ mkfifo "${transport_tmp_dir_file}/out"
+ exec 3<&-
+ exec 4<&-
+ local myargs
+ [[ $config_server_ssl_accept_invalid -eq 1 ]] && myargs="--insecure"
+ gnutls-cli "$1" -p "$2" $myargs < "${transport_tmp_dir_file}/out" > "${transport_tmp_dir_file}/in" &
+ transport_pid="$!"
+ echo "$transport_pid" >> "${transport_tmp_dir_file}/pid"
+ exec 3>"${transport_tmp_dir_file}/out"
+ exec 4<"${transport_tmp_dir_file}/in"
+ # To be able to wait for error.
+ sleep 2
+ kill -0 "$transport_pid" >/dev/null 2>&1 || return 1
+ time_get_current 'transport_lastvalidtime'
+}
+
+# Called to close connection
+# No parameters, no return code check
+transport_disconnect() {
+ # It might not be running.
+ kill "$(< "${transport_tmp_dir_file}/pid")" >/dev/null 2>&1
+ rm -rf "${transport_tmp_dir_file}"
+ exec 3<&-
+ exec 4<&-
+ # To force code to consider this disconnected.
+ transport_lastvalidtime=0
+}
+
+# Return status
+# 0 If connection is still alive
+# 1 If it isn't.
+transport_alive() {
+ kill -0 "$transport_pid" >/dev/null 2>&1 || return 1
+ local newtime=
+ time_get_current 'newtime'
+ (( $newtime - $transport_lastvalidtime > 300 )) && return 1
+ return 0
+}
+
+# Return a line in the variable line.
+# Return status
+# 0 If Ok
+# 1 If connection failed
+transport_read_line() {
+ read -ru 4 line
+ # Fail.
+ if [[ $? -ne 0 ]]; then
+ return 1
+ else
+ time_get_current 'transport_lastvalidtime'
+ fi
+ line=${line//$'\r'/}
+}
+
+# Send a line
+# Parameters
+# $* send this
+# Return code not checked.
+transport_write_line() {
+ kill -0 "$transport_pid" >/dev/null 2>&1 && echo "$*" >&3
+}
diff --git a/transport/netcat.sh b/transport/netcat.sh
new file mode 100644
index 0000000..49637e1
--- /dev/null
+++ b/transport/netcat.sh
@@ -0,0 +1,130 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## A transport module using netcat
+#---------------------------------------------------------------------
+
+# A list of features supported
+# These are used: ipv4, ipv6, ssl, nossl, bind
+# Yes I know some versions of netcat support encryption and some
+# other ones support IPv6. I used GNU netcat and I couldn't find
+# a way to detect what is supported in current netcat.
+# Also those other netcat variants require you to pass some command
+# line argument to enable use of IPv6. (nc6 doesn't)
+# netcat got to many problems, use either dev-tcp or socat for
+# non-SSL transport really!
+transport_supports="ipv4 nossl bind"
+
+# Check if all the stuff needed to use this transport is available
+# Return status
+# 0 yes
+# 1 no
+transport_check_support() {
+ [[ -x "$config_transport_netcat_path" ]] || {
+ log_fatal "Can't find netcat (needed for this transport)"
+ return 1
+ }
+ hash mkfifo >/dev/null 2>&1 || {
+ log_fatal "Can't find mkfifo (needed for this transport)"
+ return 1
+ }
+ return 0
+}
+
+# Try to connect
+# Parameters
+# $1 hostname/IP
+# $2 port
+# $3 If 1 use SSL. If the module does not support it, just ignore it.
+# $4 IP to bind to if any and if supported
+# If the module does not support it, just ignore it.
+# Return status
+# 0 if Ok
+# 1 if connection failed
+transport_connect() {
+ transport_tmp_dir_file="$(mktemp -dt envbot.netcat.XXXXXXXXXX)" || return 1
+ # To keep this simple, from client perspective.
+ # We WRITE to out and READ from in
+ mkfifo "${transport_tmp_dir_file}/in"
+ mkfifo "${transport_tmp_dir_file}/out"
+ exec 3<&-
+ exec 4<&-
+ local myargs
+ if [[ $4 ]]; then
+ myargs="-s $4"
+ fi
+ "$config_transport_netcat_path" "$1" "$2" < "${transport_tmp_dir_file}/out" > "${transport_tmp_dir_file}/in" &
+ transport_pid="$!"
+ echo "$transport_pid" >> "${transport_tmp_dir_file}/pid"
+ exec 3>"${transport_tmp_dir_file}/out"
+ exec 4<"${transport_tmp_dir_file}/in"
+ # To be able to wait for error.
+ sleep 2
+ kill -0 "$transport_pid" >/dev/null 2>&1 || return 1
+ time_get_current 'transport_lastvalidtime'
+}
+
+# Called to close connection
+# No parameters, no return code check
+transport_disconnect() {
+ # It might not be running.
+ kill "$(< "${transport_tmp_dir_file}/pid")" >/dev/null 2>&1
+ rm -rf "${transport_tmp_dir_file}"
+ exec 3<&-
+ exec 4<&-
+ # To force code to consider this disconnected.
+ transport_lastvalidtime=0
+}
+
+# Return status
+# 0 If connection is still alive
+# 1 If it isn't.
+transport_alive() {
+ kill -0 "$transport_pid" >/dev/null 2>&1 || return 1
+ local newtime=
+ time_get_current 'newtime'
+ (( newtime - transport_lastvalidtime > 300 )) && return 1
+ return 0
+}
+
+# Return a line in the variable line.
+# Return status
+# 0 If Ok
+# 1 If connection failed
+transport_read_line() {
+ read -ru 4 line
+ # Fail.
+ if [[ $? -ne 0 ]]; then
+ return 1
+ else
+ time_get_current 'transport_lastvalidtime'
+ fi
+ line=${line//$'\r'/}
+}
+
+# Send a line
+# Parameters
+# $* send this
+# Return code not checked.
+transport_write_line() {
+ kill -0 "$transport_pid" >/dev/null 2>&1 && echo "$*" >&3
+}
diff --git a/transport/openssl.sh b/transport/openssl.sh
new file mode 100644
index 0000000..8e42247
--- /dev/null
+++ b/transport/openssl.sh
@@ -0,0 +1,126 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## A transport module using openssl s_client
+#---------------------------------------------------------------------
+
+# A list of features supported
+# These are used: ipv4, ipv6, ssl, nossl, bind
+transport_supports="ipv4 ipv6 ssl"
+
+# Check if all the stuff needed to use this transport is available
+# Return status
+# 0 yes
+# 1 no
+transport_check_support() {
+ hash openssl >/dev/null 2>&1 || {
+ log_fatal "Can't find openssl (needed for this transport)"
+ return 1
+ }
+ hash mkfifo >/dev/null 2>&1 || {
+ log_fatal "Can't find mkfifo (needed for this transport)"
+ return 1
+ }
+ return 0
+}
+
+# Try to connect
+# Parameters
+# $1 hostname/IP
+# $2 port
+# $3 If 1 use SSL. If the module does not support it, just ignore it.
+# $4 IP to bind to if any and if supported
+# If the module does not support it, just ignore it.
+# Return status
+# 0 if Ok
+# 1 if connection failed
+transport_connect() {
+ transport_tmp_dir_file="$(mktemp -dt envbot.openssl.XXXXXXXXXX)" || return 1
+ # To keep this simple, from client perspective.
+ # We WRITE to out and READ from in
+ mkfifo "${transport_tmp_dir_file}/in"
+ mkfifo "${transport_tmp_dir_file}/out"
+ exec 3<&-
+ exec 4<&-
+ local myargs
+ if [[ $config_server_ssl_accept_invalid -eq 1 ]]; then
+ myargs="-verify 0"
+ else
+ myargs="-verify 10"
+ fi
+ [[ $config_server_ssl_verbose -ne 1 ]] && myargs+=" -quiet"
+ openssl s_client -connect "$1:$2" $myargs < "${transport_tmp_dir_file}/out" > "${transport_tmp_dir_file}/in" &
+ transport_pid="$!"
+ echo "$transport_pid" >> "${transport_tmp_dir_file}/pid"
+ exec 3>"${transport_tmp_dir_file}/out"
+ exec 4<"${transport_tmp_dir_file}/in"
+ # To be able to wait for error.
+ sleep 2
+ kill -0 "$transport_pid" >/dev/null 2>&1 || return 1
+ time_get_current 'transport_lastvalidtime'
+}
+
+# Called to close connection
+# No parameters, no return code check
+transport_disconnect() {
+ # It might not be running.
+ kill "$(< "${transport_tmp_dir_file}/pid")" >/dev/null 2>&1
+ rm -rf "${transport_tmp_dir_file}"
+ exec 3<&-
+ exec 4<&-
+ # To force code to consider this disconnected.
+ transport_lastvalidtime=0
+}
+
+# Return status
+# 0 If connection is still alive
+# 1 If it isn't.
+transport_alive() {
+ kill -0 "$transport_pid" >/dev/null 2>&1 || return 1
+ local newtime=
+ time_get_current 'newtime'
+ (( newtime - transport_lastvalidtime > 300 )) && return 1
+ return 0
+}
+
+# Return a line in the variable line.
+# Return status
+# 0 If Ok
+# 1 If connection failed
+transport_read_line() {
+ read -ru 4 line
+ # Fail.
+ if [[ $? -ne 0 ]]; then
+ return 1
+ else
+ time_get_current 'transport_lastvalidtime'
+ fi
+ line=${line//$'\r'/}
+}
+
+# Send a line
+# Parameters
+# $* send this
+# Return code not checked.
+transport_write_line() {
+ kill -0 "$transport_pid" >/dev/null 2>&1 && echo "$*" >&3
+}
diff --git a/transport/socat.sh b/transport/socat.sh
new file mode 100644
index 0000000..2958708
--- /dev/null
+++ b/transport/socat.sh
@@ -0,0 +1,184 @@
+#!/bin/bash
+# -*- coding: utf-8 -*-
+###########################################################################
+# #
+# envbot - an IRC bot in bash #
+# Copyright (C) 2007-2008 Arvid Norlander #
+# #
+# This program is free software: you can redistribute it and/or modify #
+# it under the terms of the GNU General Public License as published by #
+# the Free Software Foundation, either version 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 General Public License for more details. #
+# #
+# You should have received a copy of the GNU General Public License #
+# along with this program. If not, see <http://www.gnu.org/licenses/>. #
+# #
+###########################################################################
+#---------------------------------------------------------------------
+## A transport module using socat
+#---------------------------------------------------------------------
+
+# A list of features supported
+# This is set in transport_check_support
+transport_check_support=''
+
+# Check if all the stuff needed to use this transport is available
+# Return status
+# 0 Yes
+# 1 No
+transport_check_support() {
+ hash socat >/dev/null 2>&1 || {
+ log_fatal "Can't find socat (needed for this transport)"
+ return 1
+ }
+ hash mkfifo >/dev/null 2>&1 || {
+ log_fatal "Can't find mkfifo (needed for this transport)"
+ return 1
+ }
+ # Build transport_supports
+ local features="$(socat -V | grep -E 'socat version|define')"
+ # These seems to always be supported?
+ transport_supports="nossl bind"
+ if grep -q WITH_IP4 <<< "$features"; then
+ transport_supports+=" ipv4"
+ fi
+ if grep -q WITH_IP6 <<< "$features"; then
+ transport_supports+=" ipv6"
+ fi
+ if grep -q WITH_OPENSSL <<< "$features"; then
+ transport_supports+=" ssl"
+ fi
+ if [[ -z $config_transport_socat_protocol_family ]]; then
+ log_fatal "You need to set config_transport_socat_protocol_family in your config to either ipv4 or ipv6."
+ return 1
+ fi
+ # Check for older version
+ if grep -q "socat version 1.4" <<< "$features"; then
+ # SSL + IPv6 is not supported with socat-1.4.x
+ if [[ $config_server_ssl -ne 0 ]]; then
+ # list_remove is not yet loaded so we can't use that here...
+ transport_supports="$(sed "s/ipv6//" <<< "$transport_supports")"
+ fi
+ # This is to be sure socat-1.4.x works
+ # Modules should normally never set config_* in them
+ # This is an exception.
+ if [[ -z $config_transport_socat_protocol_family ]]; then
+ config_transport_socat_protocol_family="ipv4"
+ fi
+ # Remember version to find what workaround to use in transport_connect()
+ transport_socat_is_14="1"
+ else
+ transport_socat_is_14="0"
+ fi
+ return 0
+}
+
+# Try to connect
+# Parameters
+# $1 hostname/IP
+# $2 port
+# $3 If 1 use SSL. If the module does not support it, just ignore it.
+# $4 IP to bind to if any and if supported
+# If the module does not support it, just ignore it.
+# Return status
+# 0 if Ok
+# 1 if connection failed
+transport_connect() {
+ transport_tmp_dir_file="$(mktemp -dt envbot.socat.XXXXXXXXXX)" || return 1
+ # To keep this simple, from client perspective.
+ # We WRITE to out and READ from in
+ mkfifo "${transport_tmp_dir_file}/in"
+ mkfifo "${transport_tmp_dir_file}/out"
+ exec 3<&-
+ exec 4<&-
+ local addrargs socatnewargs
+ if [[ $3 -eq 1 ]]; then
+ addrargs="OPENSSL"
+ # HACK: Support IPv6 with SSL if socat is new enough.
+ if [[ $transport_socat_is_14 -eq 0 ]]; then
+ if [[ $config_transport_socat_protocol_family = "ipv6" ]]; then
+ socatnewargs=",pf=ip6"
+ elif [[ $config_transport_socat_protocol_family = "ipv4" ]]; then
+ socatnewargs=",pf=ip4"
+ fi
+ fi
+ elif [[ $config_transport_socat_protocol_family = "ipv6" ]]; then
+ addrargs="TCP6"
+ elif [[ $config_transport_socat_protocol_family = "ipv4" ]]; then
+ addrargs="TCP4"
+ fi
+ # Add in hostname and port.
+ addrargs+=":${1}:${2}"
+ # Should we bind an IP? Then lets do that.
+ if [[ $4 ]]; then
+ addrargs+=",bind=$4"
+ fi
+ # If version 1.5 or later add in extra args
+ if [[ $transport_socat_is_14 -eq 0 ]]; then
+ addrargs+="${socatnewargs}"
+ fi
+ # If we use SSL check if we should verify.
+ if [[ $3 -eq 1 && $config_server_ssl_accept_invalid -eq 1 ]]; then
+ addrargs+=",verify=0"
+ fi
+ socat STDIO "$addrargs" < "${transport_tmp_dir_file}/out" > "${transport_tmp_dir_file}/in" &
+ transport_pid="$!"
+ echo "$transport_pid" >> "${transport_tmp_dir_file}/pid"
+ exec 3>"${transport_tmp_dir_file}/out"
+ exec 4<"${transport_tmp_dir_file}/in"
+ # To be able to wait for error.
+ sleep 2
+ kill -0 "$transport_pid" >/dev/null 2>&1 || return 1
+ time_get_current 'transport_lastvalidtime'
+}
+
+# Called to close connection
+# No parameters, no return code check
+transport_disconnect() {
+ # It might not be running.
+ kill "$(< "${transport_tmp_dir_file}/pid")" >/dev/null 2>&1
+ rm -rf "${transport_tmp_dir_file}"
+ exec 3<&-
+ exec 4<&-
+ # To force code to consider this disconnected.
+ transport_lastvalidtime=0
+}
+
+# Return status
+# 0 If connection is still alive
+# 1 If it isn't.
+transport_alive() {
+ kill -0 "$transport_pid" >/dev/null 2>&1 || return 1
+ local newtime=
+ time_get_current 'newtime'
+ (( newtime - transport_lastvalidtime > 300 )) && return 1
+ return 0
+}
+
+# Return a line in the variable line.
+# Return status
+# 0 If Ok
+# 1 If connection failed
+transport_read_line() {
+ read -ru 4 line
+ # Fail.
+ if [[ $? -ne 0 ]]; then
+ return 1
+ else
+ time_get_current 'transport_lastvalidtime'
+ fi
+ line=${line//$'\r'/}
+}
+
+# Send a line
+# Parameters
+# $* send this
+# Return code not checked.
+transport_write_line() {
+ kill -0 "$transport_pid" >/dev/null 2>&1 && echo "$*" >&3
+}