Compare commits

..

95 Commits

Author SHA1 Message Date
Adrian Malacoda
45031b505f fix get_weekday() to return a better value 2018-04-30 10:30:48 -05:00
Adrian Malacoda
c2a8d5b938 use ipairs 2018-04-29 20:35:28 -05:00
Adrian Malacoda
a65c36ad9b dailies 1.1 2018-04-29 17:47:25 -05:00
Adrian Malacoda
af59c8d360 fix typo 2018-04-29 17:47:17 -05:00
Adrian Malacoda
2414eefc5f add yugipedia searcher 2018-04-29 16:23:33 -05:00
Adrian Malacoda
fb4e741386 fix issue where current_time was being reused 2018-02-27 08:10:23 -06:00
Adrian Malacoda
4ad7c33727 add lua module for sending a message on a certain day of the week 2018-02-26 23:34:53 -06:00
Adrian Malacoda
4e8e290730 allow discord channel id to have a # in front. This is mainly to work around a problem in the lua module where a string channel id becomes a float and loses precision, but to also allow for a use case where we allow channel names and not just ids. 2018-02-26 23:34:30 -06:00
Adrian Malacoda
80bcfe0580 initial stab at LuaPush implementation, doesn't currently work right now though. Also don't set type as that is a lua builtin. 2018-02-26 23:31:17 -06:00
Adrian Malacoda
44b6eecd70 make it possible to execute an lua file and code block in the same module 2018-02-25 22:49:18 -06:00
Adrian Malacoda
fec6f7f274 beef up stdin module too. This can serve two purposes, to test/interact with other modules (e.g. like a one-person chatroom) or to bridge to irc/discord. 2018-02-25 07:28:40 -06:00
Adrian Malacoda
b0c2928e78 beefed up lua scripting support, the message is now presented as a table/struct/object and lua now has access to a sender object which it can use to send messages downstream. 2018-02-25 07:04:47 -06:00
Adrian Malacoda
c6dc3f15b8 add a rudimentary irc module 2018-02-25 04:47:04 -06:00
Adrian Malacoda
906b5709d0 expand on readme 2018-02-25 03:09:53 -06:00
Adrian Malacoda
c0ee8b4d6d actually implement config reloading using notify 2018-02-25 02:52:40 -06:00
Adrian Malacoda
6fe20b8b86 reimplement reconfigure as an event that is transmitted to all modules whenever the config file changes. THis allows reconfiguration to be done in a threadsafe and relatively simple way. 2018-02-25 02:33:04 -06:00
Adrian Malacoda
9295b603aa expose a sender on the Module so we can send events directly to the module 2018-02-25 02:20:26 -06:00
Adrian Malacoda
6acffb59cc stub out config reloader thread, not currently functional 2018-02-24 19:13:09 -06:00
Adrian Malacoda
a01ad46efa make it (theoretically) possible to reconfigure a module. Might have some sort of file watcher thread which periodically checks to see if config file has been modified and reconfigures if necessary. 2018-02-24 18:54:44 -06:00
Adrian Malacoda
d9cfab7081 split off event filtering into own module 2018-02-23 00:08:13 -06:00
Adrian Malacoda
72eadd4549 add more events, dry up event filter code 2018-02-23 00:03:05 -06:00
Adrian Malacoda
93ee23c831 give the discord module the ability to recieve events and transmit messages 2018-02-22 23:43:46 -06:00
Adrian Malacoda
a37b6ab627 increment version 2018-02-22 21:44:40 -06:00
Adrian Malacoda
69c16ccd5f example of trout slap command 2018-02-22 20:55:38 -06:00
Adrian Malacoda
c774988fc9 overhaul random module to use regex instead of prefix match 2018-02-22 20:52:47 -06:00
Adrian Malacoda
240fb7f70e rules is now Optional 2018-02-22 03:19:05 -06:00
Adrian Malacoda
42fc884a58 add bulbapedia autolinker 2018-02-22 03:14:21 -06:00
Adrian Malacoda
6446bb87db there was a better way to do it 2018-02-22 03:13:03 -06:00
Adrian Malacoda
bcbc14ed17 implement a simple cache for autolink module 2018-02-22 03:07:27 -06:00
Adrian Malacoda
ca6f3391d4 simplify even more; just return an EventLoop (will probably be renamed something like Handler) 2018-02-22 02:50:49 -06:00
Adrian Malacoda
5c87b9001a simplify Envelope type, just have event loop threads generate Events which are then wrapped by Envelopes transparently 2018-02-22 02:40:22 -06:00
Adrian Malacoda
055a323d64 simplify loops, less work done in dispatcher thread 2018-02-22 02:30:54 -06:00
Adrian Malacoda
56ca5ae767 move to a model of one thread per dispatcher, instead of one main thread 2018-02-22 02:04:09 -06:00
Adrian Malacoda
fcc86a671e rearchitect event transmission so that the parents/children of each module are explicitly specified and we establish the linkages between them 2018-02-22 01:09:39 -06:00
Adrian Malacoda
3614c7eb5d update all dependencies to latest version. Need to wait for discord-rs to update though 2018-02-18 16:29:30 -06:00
Adrian Malacoda
0b289b6956 prefer if let where possible, removes empty blocks and reduces nesting 2017-05-17 21:23:00 -05:00
Adrian Malacoda
c69cc61114 prefer if let when possible 2017-05-16 21:35:05 -05:00
Adrian Malacoda
25d247f299 New filters implementation. "tags" are removed and replaced with an implementation that filters directly on the event, using an object/map instead of strings. 2017-05-11 01:29:33 -05:00
Adrian Malacoda
8cd8756722 add todo 2017-05-10 02:28:04 -05:00
Adrian Malacoda
97cc215f05 more tags 2017-05-10 02:16:05 -05:00
Adrian Malacoda
2c0e5170f4 properly implement discord channels, dry up code 2017-05-10 02:13:56 -05:00
Adrian Malacoda
bb25846566 Add GCL to interwiki list 2017-05-10 01:16:04 -05:00
Adrian Malacoda
495e3129d4 split logic for filtering out events into Subscription struct 2017-05-10 01:13:52 -05:00
Adrian Malacoda
7c26e0294a Add tag filters. This is a primitive way to screen out events that do not match a certain criteria (e.g. from user Dave) 2017-05-10 00:17:22 -05:00
Adrian Malacoda
6dda3e227f add "Logger" module which logs all received events 2017-05-09 23:39:21 -05:00
Adrian Malacoda
2baffdacd3 implement Debug for all structs, provide implementation for message senders 2017-05-09 23:36:56 -05:00
Adrian Malacoda
e8b944b836 implement run() which just forwards to event loop 2017-05-09 22:48:04 -05:00
Adrian Malacoda
9e9da11f79 begin tenquestionmarks 0.0.2. Separate Module trait into Module struct (which holds metadata and config about the module) and EventLoop trait, which implements the event loop. The constructors still return Modules, but they are structs and not boxes. 2017-05-09 22:44:33 -05:00
Adrian Malacoda
23e32f28fe update to discord 0.8.0 2017-05-09 19:50:33 -05:00
Adrian Malacoda
21b312543b yugioh rules text shouldn't be bolded 2017-03-12 18:58:20 -05:00
Adrian Malacoda
33e83d63d9 expand on mtg/ygo autolink 2017-03-12 18:48:34 -05:00
Adrian Malacoda
4888029aff Data YES (thanks Ikewise) 2017-03-05 17:18:00 +00:00
Adrian Malacoda
9112b4ada6 add some yeses to balance out the nos 2017-03-05 05:29:26 +00:00
Adrian Malacoda
8c57bd6eb7 initial working implementation of autolink module 2017-02-26 18:05:45 -06:00
Adrian Malacoda
5551ebd552 WIP for autolinker module. Currently can't use stc and tqm together because of conflicting dependencies, so this is a placeholder. 2017-02-26 17:17:22 -06:00
Adrian Malacoda
a7b9d801a1 Require echobox to have a parameter 2017-02-26 03:05:40 -06:00
Adrian Malacoda
fd1aecf4d3 add support for "general" config in the module loader. The "general" config is found under the "general" heading and is passed to each module constructor. 2017-02-26 02:44:53 -06:00
Adrian Malacoda
0a45cbb9f2 "general" as a header is reserved 2017-02-26 02:19:46 -06:00
Adrian Malacoda
d9ab75a607 Initial implementation of lua module. 2017-02-26 02:10:28 -06:00
Adrian Malacoda
41fa36cccf Transmit selfjoin event on connect 2017-02-25 23:52:43 -06:00
Adrian Malacoda
109a9131f0 Parse loglevel from environment variable 2017-02-25 23:52:31 -06:00
Adrian Malacoda
709eff63e0 add timestamp to log message 2017-02-25 23:34:01 -06:00
Adrian Malacoda
57b52772f5 set default loglevel to info 2017-02-25 23:25:09 -06:00
Adrian Malacoda
8dad1fc4aa Do not send events to their originators, or to any event not specified in the "to" list. 2017-02-25 21:31:32 -06:00
Adrian Malacoda
1fbba2554d replace Sender with ExtSender from transformable_channels. Now we can tag each outgoing envelope with the name of its sender 2017-02-25 21:11:25 -06:00
Adrian Malacoda
544974117f rename various sender/receiver variables to be clearer 2017-02-25 20:38:09 -06:00
Adrian Malacoda
4d5a412395 we're sending Envelope and transmitting Arc<Envelope>, since we're wrapping the Envelope in an Arc to transmit it, we (probably) don't need to wrap the Event in an Arc too 2017-02-25 20:33:47 -06:00
Adrian Malacoda
f22e4755d3 remove unused variable 2017-02-25 20:20:01 -06:00
Adrian Malacoda
37a9645f5b Simplify module trait by combining produce/consume event methods into a single run method that runs in the module's own thread and can produce and/or consume events. Introduce an Envelope struct that encapsulates event + to/from so we can (eventually) tag every event and also limit where events are sent (e.g. you can have a specific module configured to talk or listen only to a certain other module). 2017-02-25 20:17:46 -06:00
Adrian Malacoda
442b617f31 now with amazing echobox powers 2017-02-22 23:40:30 -06:00
Adrian Malacoda
2c893926c3 can finally commit more helpful example config 2017-02-20 15:11:13 -06:00
Adrian Malacoda
96bc25234e more event types 2017-02-19 18:21:11 -06:00
Adrian Malacoda
ce35368676 tag senders/receivers with module names so we can eventually associate individual events to module names 2017-02-19 17:31:37 -06:00
Adrian Malacoda
0f945ec604 create a dedicated Message struct and implement reply(&str) on there, since it seems to be a commonly used thing. Begin implementing helpers for command parsing. 2017-02-19 05:37:56 -06:00
Adrian Malacoda
b9d5b7916c cleanup 2017-02-19 05:06:55 -06:00
Adrian Malacoda
9b00500a77 pvn dependency 2017-02-19 04:49:26 -06:00
Adrian Malacoda
c5a88b8405 initial pvn module implementation 2017-02-19 04:49:06 -06:00
Adrian Malacoda
84d2921f8f send/receive Arc<Event> instead of Event so we don't have to clone objects all over the place. This might also enable us to be a bit more flexible with what we send with Events, and might simplify things elsewhere. 2017-02-19 02:55:30 -06:00
Adrian Malacoda
1a69349557 Add ability to specify the "playing" status string (with tenquestionmarks default) 2017-02-17 02:44:00 -06:00
Adrian Malacoda
5fc231eec6 Add logging 2017-02-17 02:38:15 -06:00
Adrian Malacoda
166805d1c2 actually add random module 2017-02-16 13:00:17 -06:00
Adrian Malacoda
26e56ebee9 Add random response module 2017-02-16 02:00:38 -06:00
Adrian Malacoda
2c87c586e2 Remove hello module 2017-02-16 01:07:19 -06:00
Adrian Malacoda
0a51c7294f rename plugin -> module 2017-02-16 01:05:33 -06:00
Adrian Malacoda
bdee07143b finally a working implementation of Discord sender 2017-02-16 00:27:53 -06:00
Adrian Malacoda
7581521b61 try arc instead of box 2017-02-16 00:16:48 -06:00
Adrian Malacoda
b238b98b82 start actually implementing discord plugin 2017-02-15 23:45:14 -06:00
Adrian Malacoda
66180578d6 alternate implementation using Box<MessageSender> and clone 2017-02-15 23:41:29 -06:00
Adrian Malacoda
5b9f1610dd attempt to flesh out send support for channel/message. Currently does not build 2017-02-15 22:41:52 -06:00
Adrian Malacoda
d414e65fd9 begin fleshing out discord module. Implement sender/channel as struct for now, there might be a performance hit from copying so much data around but we can look at optimization later 2017-02-13 22:13:33 -06:00
Adrian Malacoda
6424a7a37f Remove unused imports, stub out send() for user and channel. 2017-02-13 21:44:37 -06:00
Adrian Malacoda
9bb6887bed make plugin produce/consume events api symmetrical 2017-02-13 00:55:30 -06:00
Adrian Malacoda
edef05d123 actually write somewhat of a readme 2017-02-13 00:27:02 -06:00
Adrian Malacoda
26a6b77632 Flesh out plugins event handling, add example stdin plugin (event producer) and echo plugin (event consumer). Next step: fleshing out user/channel structs 2017-02-13 00:22:06 -06:00
Adrian Malacoda
a31b060dd3 Beginning of rust implementation for tenquestionmarks. 2017-02-08 03:25:03 -06:00
48 changed files with 2075 additions and 7460 deletions

27
BUGS
View File

@ -1,27 +0,0 @@
Investigate:
[2011-02-10 03:51:47.148968] ALL_RAW_MESSAGES: [':malacoda!~liberius@Rizon-8449AE11.satx.res.rr.com PRIVMSG #eightbar :!eightball Is eightball working?'] from irc.kickassanime.org to None
Traceback (most recent call last):
File "./tenquestionmarks.py", line 383, in <module>
tqm.loop()
File "./tenquestionmarks.py", line 125, in loop
self.ircobj.process_once()
File "./lib/irclib/irclib.py", line 214, in process_once
self.process_data(i)
File "./lib/irclib/irclib.py", line 183, in process_data
c.process_data()
File "./lib/irclib/irclib.py", line 571, in process_data
self._handle_event(Event(command, prefix, target, [m]))
File "./lib/irclib/irclib.py", line 594, in _handle_event
self.irclibobj._handle_event(self, event)
File "./lib/irclib/irclib.py", line 326, in _handle_event
if handler[1](connection, event) == "NO MORE":
File "./tenquestionmarks.py", line 79, in _dispatcher
irclib.SimpleIRCClient._dispatcher(self,connection,event)
File "./lib/irclib/irclib.py", line 1043, in _dispatcher
getattr(self, m)(c, e)
File "./tenquestionmarks.py", line 163, in on_pubmsg
if message.startswith(self.command_prefix):
TypeError: expected a character buffer object
python 2.6.6

23
Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name="tenquestionmarks"
version="0.0.3"
authors=["Adrian Malacoda <adrian.malacoda@monarch-pass.net>"]
[dependencies]
hlua = "0.4.1"
discord = "0.8.0"
toml = "0.4.5"
crossbeam = "0.3.2"
rand = "0.4.2"
log = "0.4.1"
env_logger = "0.5.3"
transformable_channels = "0.1.1"
time = "0.1"
regex = "0.2"
multimap = "0.4.0"
notify = "4.0.0"
irc = "0.11.8" # latest version which supports old openssl version (required by discord)
thread_local = "0.3"
pvn = { git = "http://gitlab.monarch-pass.net/malacoda/pvn.git" }
echobox = { git = "http://gitlab.monarch-pass.net/malacoda/echobox.git" }
stc = { git = "http://gitlab.monarch-pass.net/malacoda/stc.git" }

View File

@ -0,0 +1,17 @@
# tenquestionmarks chat bot
tenquestionmarks is an extensible, scriptable chat bot. This iteration is written in rust.
## Configuration
Configuration is done in TOML. By default, tenquestionmarks looks for `tenquestionmarks.toml`.
As of tenquestionmarks 0.0.3, tenquestionmarks supports a limited form of live configuration reloading. tenquestionmarks monitors the configuration file and, upon detecting changes, will emit a reconfiguration event to all modules to ask them to reconfigure. Reconfiguration is implemented on a per-module basis.
## Modules
tenquestionmarks is a series of modules. Modules produce events and consume events.
In this particular iteration of tenquestionmarks, there are at most two threads spawned for a module: an event loop thread (which has access to an event emitter and event receiver), and the event dispatcher thread associated with said event emitter. The event dispatcher thread takes events emitted from the event loop thread and pushes them to downstream modules' event receivers. No event dispatcher thread will be spawned for a module with no downstreams.
As of tenquestionmarks 0.0.3, each module is required to explicitly identify which modules it wishes to send and/or receive events from.
## Events
Events are things such as message, join, quit.

12
TODO
View File

@ -1,12 +0,0 @@
Modules:
- Welcome
- Help
- Log
- Stats
- Forum Integration (smf, mybb)
Make more object-oriented (i.e. user objects, channel objects instead of strings)
Documentation
Release

11
TODO.md Normal file
View File

@ -0,0 +1,11 @@
# TODO
## 0.0.2
### Filters
* Basic mechanism for filtering out messages between modules.
* Filter tags are of the type `key:value` (e.g. `type:message`, `username:Kuschelyagi`)
* Each individual filter is a stack of one or more filter values.
* Event must match AT LEAST one of these filters IN FULL in order to be accepted.
* For example, module `foo` declares `filters = [["username:Kuschelyagi", "channel:shitpost"]]`. This is a single filter with two tag conditions. This means events must match on BOTH tags to be accepted by `foo`
* If, on the other hand, module `foo` instead declares `filters = ["username:Kuschelyagi", "channel:shitpost"]` then these are separate filters and the event must only match on ONE of them.
* Other proposed filter notations:
* `filters = [{ username = "Kuschelyagi", channel = "shitpost" }]`

File diff suppressed because it is too large Load Diff

View File

@ -1,510 +0,0 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations
below.
When we speak of free software, we are referring to freedom of use,
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 and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
^L
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it
becomes a de-facto standard. To achieve this, non-free programs must
be allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
^L
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, 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 library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete 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 distribute a copy of this License along with the
Library.
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 Library or any portion
of it, thus forming a work based on the Library, 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) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
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 Library, 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 Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
^L
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you 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.
If distribution of 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 satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
^L
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at least
three years, to give the same user the materials specified in
Subsection 6a, above, for a charge no more than the cost of
performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be 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.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
^L
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library 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.
9. 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 Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
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 with
this License.
^L
11. 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 Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library 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 Library.
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.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library 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.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser 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 Library
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 Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
^L
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
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
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "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
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. 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 LIBRARY 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
LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
^L
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms
of the ordinary General Public License).
To apply these terms, attach the following notices to the library.
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 library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or
your school, if any, to sign a "copyright disclaimer" for the library,
if necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James
Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!

View File

@ -1,420 +0,0 @@
2005-12-24 Keltus <keltus@users.sourceforge.net>
* Released version 0.4.6.
* irclib.py (VERSION):
* python-irclib.spec.in:
Preparations for version 0.4.6.
2005-12-23 Keltus <keltus@users.sourceforge.net>
* dccsend:
* dccreceive:
* irclib.py:
* ircbot.py:
* irccat:
* irccat2:
* servermap:
* testbot.py:
Code modernization - String methods used instead of deprecated
string functions, keyword 'in' used for membership testing instead
of 'has_key' method, etc.
2005-12-06 Keltus <keltus@users.sourceforge.net>
* irclib.py (ServerConnection.process_data): Reversed fix from
2005-05-28. This is strange because there was a bug before and
now it's gone. Either python changed something, or the IRC
networks changed something. Confirmed by peter.
2005-11-03 Keltus <keltus@users.sourceforge.net>
* irclib.py (numeric_events): Renamed numeric code 332 from topic
to currenttopic (the message when "/topic <chan>" is sent), so it
doesn't collide with TOPIC (the message when the topic is set).
2005-08-27 Keltus <keltus@users.sourceforge.net>
* irclib.py (ServerConnection.disconnect): Fixed infinitely
recursive calls when disconnecting with a failed connection. Bug
reported by Erik Max Francis.
2005-08-18 Keltus <keltus@users.sourceforge.net>
* irclib.py: Made ServerConnection.disconnect more consistant and
changed some functions to use it instead of quit. Previously,
disconnect would ignore the quit message, but now it sends a quit
message and disconnect. Suggestion by Erik Max Francis.
* ircbot.py: Changed to use ServerConnection.disconnect instead of
ServerConnection.quit as well.
2005-05-28 Keltus <keltus@users.sourceforge.net>
* irclib.py (ServerConnection.process_data): Fixed quit arguments
to return a list rather than a list of a list. Patch from peter.
2005-05-18 Keltus <keltus@users.sourceforge.net>
* Released version 0.4.5.
* irclib.py (ServerConnection.__init__): Added self.socket = None
to be able to process events when ServerConnection is not
connected to a socket. Patch from alst.
* irclib.py (VERSION):
* python-irclib.spec.in:
Preparations for version 0.4.5.
2005-04-26 Keltus <keltus@users.sourceforge.net>
* irclib.py (IRC.__doc__): Corrected server.process_forever() to
irc.process_forever(). Suggestion by olecom.
2005-04-17 Keltus <keltus@users.sourceforge.net>
* irclib.py (ServerConnection.process_data): Moved event
translation code.
* irclib.py (ServerConnection): Reverted the 2005-01-28 change
because it breaks jump_server().
* irclib.py: minor comment changes
2005-04-03 Keltus <keltus@users.sourceforge.net>
* irclib.py (protocol_events): Added "pong" and "invite" events.
Patch from Adam Mikuta.
* irclib.py (ServerConnection.part): Added message parameter.
Patch from Adam Mikuta.
2005-02-23 Keltus <keltus@users.sourceforge.net>
* Released version 0.4.4.
* irclib.py (VERSION):
* python-irclib.spec.in:
Preparations for version 0.4.4.
2005-01-28 Keltus <keltus@users.sourceforge.net>
* irclib.py: (ServerConnection): Moved
self.irclibobj._remove_connection call from close() to
disconnect(). Patch from Alexey Nezhdanov.
2005-01-25 Keltus <keltus@users.sourceforge.net>
* irclib.py (ServerConnection.connect): closes socket if a
connection does not occur
* irclib.py (ServerConnection.connect): "Changing server" ->
"Changing servers" (more ubiquitous quit phrase)
2005-01-23 Keltus <keltus@users.sourceforge.net>
* irclib.py: Removed depreciated apply functions. python-irclib is
now compatible with Python 1.6 and above.
* testbot.py: Removed redundant extra start() call
2005-01-20 Joel Rosdahl <joel@rosdahl.net>
* Released version 0.4.3.
* Makefile: Removed more GNU make specific constructs.
2005-01-19 Joel Rosdahl <joel@rosdahl.net>
* Makefile: Don't require GNU make.
2005-01-19 Keltus <keltus@users.sourceforge.net>
* ircbot.py (IRCDict.__iter__): Added __iter__ method for IRCDict.
2005-01-17 Joel Rosdahl <joel@rosdahl.net>
* ircbot.py (IRCDict.__contains__): Added __contains__method for
IRCDict. Patch from Keltus.
(SingleServerIRCBot.on_ctcp): Corrected default decoding of CTCP
DCC CHAT. Patch from Keltus.
* irclib.py (VERSION):
* python-irclib.spec.in:
Preparations for version 0.4.3.
* debian: Removed Debian package directory since python-irclib is
in Debian now.
* ircbot.py (SingleServerIRCBot._on_namreply): Improved comment
about arguments to the function. Patch from Keltus.
(Channel.has_allow_external_messages): Renamed from
has_message_from_outside_protection. Patch from Keltus.
* irclib.py (ServerConnection.quit): Added comment about how some
IRC servers' treat QUIT messages. Patch from Keltus.
* ircbot.py (SingleServerIRCBot.jump_server): Improved jump_server
behaviour. Patch from Keltus.
2004-08-04 Joel Rosdahl <joel@rosdahl.net>
* irclib.py (ServerConnection.process_data): Added "bonus" action
event that is triggered on CTCP ACTION messages.
2004-07-09 Joel Rosdahl <joel@rosdahl.net>
* Released version 0.4.2.
* debian/rules: Remove built *.pyc files before making package.
* irclib.py (DEBUG):
* debian/changelog:
* python-irclib.spec.in:
Preparations for version 0.4.2.
* irclib.py (ServerNotConnectedError): New exception.
(ServerConnection.send_raw): Fix bug #922446, "Raise
IllegalStateException in send_raw when disconnected".
2003-10-30 Joel Rosdahl <joel@rosdahl.net>
* Released version 0.4.1.
* debian/examples: Added dccreceive and dccsend as example files
in Debian.
* python-irclib.spec.in: Likewise.
2003-10-29 Joel Rosdahl <joel@rosdahl.net>
* debian: Added Debian packaging files.
* setup.py.in: Create setup.py from setup.py.in.
* python-irclib.spec.in: RPM spec file from Gary Benson.
* testbot.py (TestBot.on_nicknameinuse): New method.
* irclib.py (ServerConnection.process_data): Record nickname when
welcome message is sent to trap nickname change triggered in a
nicknameinuse callback.
* ircbot.py (SingleServerIRCBot._on_join): Use
Connection.get_nickname instead of relying on self._nickname.
(SingleServerIRCBot._on_kick): Likewise.
(SingleServerIRCBot._on_part): And here too.
(SingleServerIRCBot._on_nick): No need to remember nickname change
here.
2003-08-31 Joel Rosdahl <joel@rosdahl.net>
* Released version 0.4.0.
Implemented DCC support (based on patches from Adam Langley and
Marco Bettio):
* irclib.py (IRC.dcc): New method.
(DCCConnectionError): New class.
(DCCConnection): New class.
(SimpleIRCClient.__init__): Added dcc_connections attribute.
(SimpleIRCClient._dcc_disconnect): New method.
(SimpleIRCClient.connect): Added localaddress and
localport parameters. The socket will be bound accordingly before
connecting.
(SimpleIRCClient.dcc_connect): New method.
(SimpleIRCClient.dcc_listen): New method.
(ip_numstr_to_quad): New function.
(ip_quad_to_numstr): New function.
* ircbot.py (SingleServerIRCBot.on_ctcp): Relay DCC CHAT CTCPs to
the on_dccchat method.
* testbot.py: Added support for accepting DCC chats and for
initiating DCC chats via a "dcc" command.
* dccreceive: New example program.
* dccsend: New example program.
* Makefile: Added dccreceive and dccsend to dist files.
Other changes:
* setup.py: Added.
* irclib.py (ServerConnection.connect, ServerConnection.user):
Send USER command according to RFC 2812.
(ServerConnection.connect): Added localaddress and
localport parameters. The socket will be bound accordingly before
connecting.
(ServerConnection.process_data): Ignore empty lines from the
server. (Patch by Jason Wies.)
(ServerConnection._get_socket): Simplified.
(ServerConnection.remove_global_handler): Added. (Patch from
Brandon Beck.)
* ircbot.py (SingleServerIRCBot.on_ctcp): Prepend VERSION reply
with VERSION. (Patch from Andrew Gaul.)
* Makefile: Added setup.py to dist files. Also create zip archive.
* README: Added requirements and installation sections.
2002-03-01 Joel Rosdahl <joel@rosdahl.net>
* Released version 0.3.4.
Corrected problems spotted by Markku Hänninen <hmm@iki.fi>:
* irccat2 (IRCCat.on_welcome): Added missing connection argument.
(IRCCat.on_join): Likewise.
(IRCCat.on_disconnect): Likewise.
* irclib.py (ServerConnection.ison): Bug fix: Join nicks by space
instead of commas.
* irclib.py (ServerConnection.whowas): Bug fix: Let the max
argument default to the empty string.
* irclib.py (numeric_events): Added new events: traceservice,
tracereconnect, tryagain, invitelist, endofinvitelist, exceptlist,
endofexceptlist, unavailresource, nochanmodes, banlistfull,
restricted and uniqopprivsneeded.
2002-02-17 Joel Rosdahl <joel@rosdahl.net>
* Released version 0.3.3.
* Makefile, README, .cvsignore: Removed documentation generated by
pythondoc. Use pydoc instead.
* servermap: Removed some excess whitespace.
* README: Mention http://python-irclib.sourceforge.net.
* Makefile (dist): Changed archive name from irclib-* to
python-irclib-*.
Changed license from GPL 2 to LGPL 2.1:
* COPYING: New license text.
* irclib.py, ircbot.py, servermap: New license header.
2001-10-21 Joel Rosdahl <joel@rosdahl.net>
* Released version 0.3.2.
* irclib.py (_parse_modes): Fixed problem found by Tom Morton: the
mode parsing code bailed out if a unary mode character didn't have
a corresponding argument.
* irclib.py (_alpha): Fixed bug found by Tom Morton: w was missing
in the alphabet used by irc_lower().
* ircbot.py: Removed redundant import of is_channel.
* servermap: Clarified copyright and license.
* irccat: Ditto.
* irccat2: Ditto.
2000-12-11 Joel Rosdahl <joel@rosdahl.net>
* Released version 0.3.1.
* irclib.py (IRC.process_once): Work-around for platform-dependent
select() on Windows systems.
* ircbot.py: Clarification of SingleServerIRCBot doc string.
2000-11-26 Joel Rosdahl <joel@rosdahl.net>
* Released version 0.3.0.
* Makefile (dist): Include ircbot.py again.
* README: Updated.
* irclib.py (ServerConnection.get_nickname): Renamed from
get_nick_name.
(ServerConnection._get_socket): Return None if not connected.
2000-11-25 Joel Rosdahl <joel@rosdahl.net>
* irclib.py (ServerConnection.process_data): all_raw_messages
instead of allrawmessages.
(IRC._handle_event): Added "all_events" event type.
(nm_to_n): Renamed from nick_from_nickmask.
(nm_to_uh): Renamed from userhost_from_nickmask.
(nm_to_h): Renamed from host_from_nickmask.
(nm_to_u): Renamed from user_from_nickmask.
(SimpleIRCClient): Created.
2000-11-22 Joel Rosdahl <joel@rosdahl.net>
* irclib.py (lower_irc_string): Use translation instead.
(ServerConnection.process_data): Split non-RFC-compliant lines a
bit more intelligently.
(ServerConnection.process_data): Removed unnecessary try/except
block.
(ServerConnection.get_server_name): Return empty server if
unknown.
(_rfc_1459_command_regexp): Tweaked a bit.
* ircbot.py: Rewritten.
2000-11-21 Joel Rosdahl <joel@rosdahl.net>
* irclib.py (IRC.process_forever): Default to processing a bit
more often.
2000-10-29 Joel Rosdahl <joel@rosdahl.net>
* Released version 0.2.4.
* Makefile (dist): Include generated documentation in
distribution.
* Makefile (doc): Make documentation.
* irclib.py: Updated documentation.
* irclib.py (is_channel): Included "!" as channel prefix.
2000-10-02 Joel Rosdahl <joel@rosdahl.net>
* Released version 0.2.3.
* irclib.py (ServerConnection.connect): Make socket.connect() work
for Python >= 1.6.
2000-09-26 Joel Rosdahl <joel@rosdahl.net>
* Released version 0.2.2.
* irclib.py (ServerConnection.user): Fixed erroneous format
string.
2000-09-24 Joel Rosdahl <joel@rosdahl.net>
* Released version 0.2.1.
* irclib.py (ServerConnection.process_data): Bug fix (didn't keep
track of nick name).
(IRC.process_once): New method.
(ServerConnection.process_data): Bug fix.
(IRC.disconnect_all): Created.
(IRC.exit): Removed.
(ServerConnection.exit): Removed.
(ServerConnection.connect): Follow RFC closer.
(ServerConnection.user): Follow RFC closer.
* ircbot.py: Removed.
* irccat (on_disconnect): Just sys.exit(0).
* servermap (on_disconnect): Just sys.exit(0).
* irclib.py: Various documentation and some clean-ups.
1999-08-21 Joel Rosdahl <joel@rosdahl.net>
* Released version 0.2.0.
* servermap: Updated to work with irclib 0.2.0.
* irccat: Updated to work with irclib 0.2.0.
* ircbot.py: Updated to work with irclib 0.2.0. The bot now
checks every minute that it is connected. If it's not, it
reconnects.
* irclib.py: Changes in how to create a ServerConnection object.
Made the code for handling disconnection hopefully more robust.
Renamed connect() to sconnect().
1999-06-19 Joel Rosdahl <joel@rosdahl.net>
* irclib.py: Released 0.1.0.

View File

@ -1,42 +0,0 @@
VERSION := `sed -n -e '/VERSION = /{s/VERSION = \(.*\), \(.*\), \(.*\)/\1.\2.\3/;p;}' <irclib.py`
DISTFILES = \
COPYING \
ChangeLog \
Makefile \
README \
dccreceive \
dccsend \
ircbot.py \
irccat \
irccat2 \
irclib.py \
python-irclib.spec \
servermap \
setup.py \
testbot.py
PACKAGENAME = python-irclib-$(VERSION)
all: $(DISTFILES)
setup.py: setup.py.in
sed 's/%%VERSION%%/'$(VERSION)'/g' setup.py.in >setup.py
python-irclib.spec: python-irclib.spec.in
sed 's/%%VERSION%%/'$(VERSION)'/g' python-irclib.spec.in >python-irclib.spec
dist: $(DISTFILES)
mkdir $(PACKAGENAME)
cp -r $(DISTFILES) $(PACKAGENAME)
tar cvzf $(PACKAGENAME).tar.gz $(PACKAGENAME)
zip -r9yq $(PACKAGENAME).zip $(PACKAGENAME)
rm -rf $(PACKAGENAME)
cvstag:
ver=$(VERSION); echo cvs tag version_`echo $$ver | sed 's/\./_/g'`
clean:
rm -rf *~ *.pyc build python-irclib.spec setup.py
.PHONY: all doc dist cvstag clean

View File

@ -1,106 +0,0 @@
irclib -- Internet Relay Chat (IRC) protocol client library
-----------------------------------------------------------
The home of irclib.py is now:
http://python-irclib.sourceforge.net
This library is intended to encapsulate the IRC protocol at a quite
low level. It provides an event-driven IRC client framework. It has
a fairly thorough support for the basic IRC protocol, CTCP and DCC
connections.
In order to understand how to make an IRC client, I'm afraid you more
or less must understand the IRC specifications. They are available
here:
http://www.irchelp.org/irchelp/rfc/
Requirements:
* Python 1.6 or newer.
Installation:
* Run "python setup.py install" or copy irclib.py and/or ircbot.py
to an appropriate Python module directory.
The main features of the IRC client framework are:
* Abstraction of the IRC protocol.
* Handles multiple simultaneous IRC server connections.
* Handles server PONGing transparently.
* Messages to the IRC server are done by calling methods on an IRC
connection object.
* Messages from an IRC server triggers events, which can be caught
by event handlers.
* Reading from and writing to IRC server sockets are normally done
by an internal select() loop, but the select()ing may be done by
an external main loop.
* Functions can be registered to execute at specified times by the
event-loop.
* Decodes CTCP tagging correctly (hopefully); I haven't seen any
other IRC client implementation that handles the CTCP
specification subtilties.
* A kind of simple, single-server, object-oriented IRC client class
that dispatches events to instance methods is included.
* DCC connection support.
Current limitations:
* The IRC protocol shines through the abstraction a bit too much.
* Data is not written asynchronously to the server (and DCC peers),
i.e. the write() may block if the TCP buffers are stuffed.
* Like most projects, documentation is lacking...
Unfortunately, this library isn't as well-documented as I would like
it to be. I think the best way to get started is to read and
understand the example program irccat, which is included in the
distribution.
The following files might be of interest:
* irclib.py
The library itself. Read the code along with comments and
docstrings to get a grip of what it does. Use it at your own risk
and read the source, Luke!
* irccat
A simple example of how to use irclib.py. irccat reads text from
stdin and writes it to a specified user or channel on an IRC
server.
* irccat2
The same as above, but using the SimpleIRCClient class.
* servermap
Another simple example. servermap connects to an IRC server,
finds out what other IRC servers there are in the net and prints
a tree-like map of their interconnections.
* testbot.py
An example bot that uses the SingleServerIRCBot class from
ircbot.py. The bot enters a channel and listens for commands in
private messages or channel traffic. It also accepts DCC
invitations and echos back sent DCC chat messages.
* dccreceive
Receives a file over DCC.
* dccsend
Sends a file over DCC.
Enjoy.
Maintainer:
keltus <keltus@users.sourceforge.net>
Original Founder:
Joel Rosdahl <joel@rosdahl.net>

View File

@ -1,77 +0,0 @@
#! /usr/bin/env python
#
# Example program using irclib.py.
#
# This program is free without restrictions; do anything you like with
# it.
#
# Joel Rosdahl <joel@rosdahl.net>
import irclib
import os
import struct
import sys
class DCCReceive(irclib.SimpleIRCClient):
def __init__(self):
irclib.SimpleIRCClient.__init__(self)
self.received_bytes = 0
def on_ctcp(self, connection, event):
args = event.arguments()[1].split()
if args[0] != "SEND":
return
self.filename = os.path.basename(args[1])
if os.path.exists(self.filename):
print "A file named", self.filename,
print "already exists. Refusing to save it."
self.connection.quit()
self.file = open(self.filename, "w")
peeraddress = irclib.ip_numstr_to_quad(args[2])
peerport = int(args[3])
self.dcc = self.dcc_connect(peeraddress, peerport, "raw")
def on_dccmsg(self, connection, event):
data = event.arguments()[0]
self.file.write(data)
self.received_bytes = self.received_bytes + len(data)
self.dcc.privmsg(struct.pack("!I", self.received_bytes))
def on_dcc_disconnect(self, connection, event):
self.file.close()
print "Received file %s (%d bytes)." % (self.filename,
self.received_bytes)
self.connection.quit()
def on_disconnect(self, connection, event):
sys.exit(0)
def main():
if len(sys.argv) != 3:
print "Usage: dccreceive <server[:port]> <nickname>"
print "\nReceives one file via DCC and then exits. The file is stored in the"
print "current directory."
sys.exit(1)
s = sys.argv[1].split(":", 1)
server = s[0]
if len(s) == 2:
try:
port = int(s[1])
except ValueError:
print "Error: Erroneous port."
sys.exit(1)
else:
port = 6667
nickname = sys.argv[2]
c = DCCReceive()
try:
c.connect(server, port, nickname)
except irclib.ServerConnectionError, x:
print x
sys.exit(1)
c.start()
if __name__ == "__main__":
main()

View File

@ -1,91 +0,0 @@
#! /usr/bin/env python
#
# Example program using irclib.py.
#
# This program is free without restrictions; do anything you like with
# it.
#
# Joel Rosdahl <joel@rosdahl.net>
import irclib
import os
import struct
import sys
class DCCSend(irclib.SimpleIRCClient):
def __init__(self, receiver, filename):
irclib.SimpleIRCClient.__init__(self)
self.receiver = receiver
self.filename = filename
self.filesize = os.path.getsize(self.filename)
self.file = open(filename)
self.sent_bytes = 0
def on_welcome(self, connection, event):
self.dcc = self.dcc_listen("raw")
self.connection.ctcp("DCC", self.receiver, "SEND %s %s %d %d" % (
os.path.basename(self.filename),
irclib.ip_quad_to_numstr(self.dcc.localaddress),
self.dcc.localport,
self.filesize))
def on_dcc_connect(self, connection, event):
if self.filesize == 0:
self.dcc.disconnect()
return
self.send_chunk()
def on_dcc_disconnect(self, connection, event):
print "Sent file %s (%d bytes)." % (self.filename, self.filesize)
self.connection.quit()
def on_dccmsg(self, connection, event):
acked = struct.unpack("!I", event.arguments()[0])[0]
if acked == self.filesize:
self.dcc.disconnect()
self.connection.quit()
elif acked == self.sent_bytes:
self.send_chunk()
def on_disconnect(self, connection, event):
sys.exit(0)
def on_nosuchnick(self, connection, event):
print "No such nickname:", event.arguments()[0]
self.connection.quit()
def send_chunk(self):
data = self.file.read(1024)
self.dcc.privmsg(data)
self.sent_bytes = self.sent_bytes + len(data)
def main():
if len(sys.argv) != 5:
print "Usage: dccsend <server[:port]> <nickname> <receiver nickname> <filename>"
print "\nSends <filename> to <receiver nickname> via DCC and then exits."
sys.exit(1)
s = sys.argv[1].split(":", 1)
server = s[0]
if len(s) == 2:
try:
port = int(s[1])
except ValueError:
print "Error: Erroneous port."
sys.exit(1)
else:
port = 6667
nickname = sys.argv[2]
receiver = sys.argv[3]
filename = sys.argv[4]
c = DCCSend(receiver, filename)
try:
c.connect(server, port, nickname)
except irclib.ServerConnectionError, x:
print x
sys.exit(1)
c.start()
if __name__ == "__main__":
main()

View File

@ -1,438 +0,0 @@
# Copyright (C) 1999--2002 Joel Rosdahl
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Joel Rosdahl <joel@rosdahl.net>
#
# $Id: ircbot.py,v 1.21 2005/12/23 18:44:43 keltus Exp $
"""ircbot -- Simple IRC bot library.
This module contains a single-server IRC bot class that can be used to
write simpler bots.
"""
import sys
from UserDict import UserDict
from irclib import SimpleIRCClient
from irclib import nm_to_n, irc_lower, all_events
from irclib import parse_channel_modes, is_channel
from irclib import ServerConnectionError
class SingleServerIRCBot(SimpleIRCClient):
"""A single-server IRC bot class.
The bot tries to reconnect if it is disconnected.
The bot keeps track of the channels it has joined, the other
clients that are present in the channels and which of those that
have operator or voice modes. The "database" is kept in the
self.channels attribute, which is an IRCDict of Channels.
"""
def __init__(self, server_list, nickname, realname, reconnection_interval=60):
"""Constructor for SingleServerIRCBot objects.
Arguments:
server_list -- A list of tuples (server, port) that
defines which servers the bot should try to
connect to.
nickname -- The bot's nickname.
realname -- The bot's realname.
reconnection_interval -- How long the bot should wait
before trying to reconnect.
dcc_connections -- A list of initiated/accepted DCC
connections.
"""
SimpleIRCClient.__init__(self)
self.channels = IRCDict()
self.server_list = server_list
if not reconnection_interval or reconnection_interval < 0:
reconnection_interval = 2**31
self.reconnection_interval = reconnection_interval
self._nickname = nickname
self._realname = realname
for i in ["disconnect", "join", "kick", "mode",
"namreply", "nick", "part", "quit"]:
self.connection.add_global_handler(i,
getattr(self, "_on_" + i),
-10)
def _connected_checker(self):
"""[Internal]"""
if not self.connection.is_connected():
self.connection.execute_delayed(self.reconnection_interval,
self._connected_checker)
self.jump_server()
def _connect(self):
"""[Internal]"""
password = None
if len(self.server_list[0]) > 2:
password = self.server_list[0][2]
try:
self.connect(self.server_list[0][0],
self.server_list[0][1],
self._nickname,
password,
ircname=self._realname)
except ServerConnectionError:
pass
def _on_disconnect(self, c, e):
"""[Internal]"""
self.channels = IRCDict()
self.connection.execute_delayed(self.reconnection_interval,
self._connected_checker)
def _on_join(self, c, e):
"""[Internal]"""
ch = e.target()
nick = nm_to_n(e.source())
if nick == c.get_nickname():
self.channels[ch] = Channel()
self.channels[ch].add_user(nick)
def _on_kick(self, c, e):
"""[Internal]"""
nick = e.arguments()[0]
channel = e.target()
if nick == c.get_nickname():
del self.channels[channel]
else:
self.channels[channel].remove_user(nick)
def _on_mode(self, c, e):
"""[Internal]"""
modes = parse_channel_modes(" ".join(e.arguments()))
t = e.target()
if is_channel(t):
ch = self.channels[t]
for mode in modes:
if mode[0] == "+":
f = ch.set_mode
else:
f = ch.clear_mode
f(mode[1], mode[2])
else:
# Mode on self... XXX
pass
def _on_namreply(self, c, e):
"""[Internal]"""
# e.arguments()[0] == "@" for secret channels,
# "*" for private channels,
# "=" for others (public channels)
# e.arguments()[1] == channel
# e.arguments()[2] == nick list
ch = e.arguments()[1]
for nick in e.arguments()[2].split():
if nick[0] == "@":
nick = nick[1:]
self.channels[ch].set_mode("o", nick)
elif nick[0] == "+":
nick = nick[1:]
self.channels[ch].set_mode("v", nick)
self.channels[ch].add_user(nick)
def _on_nick(self, c, e):
"""[Internal]"""
before = nm_to_n(e.source())
after = e.target()
for ch in self.channels.values():
if ch.has_user(before):
ch.change_nick(before, after)
def _on_part(self, c, e):
"""[Internal]"""
nick = nm_to_n(e.source())
channel = e.target()
if nick == c.get_nickname():
del self.channels[channel]
else:
self.channels[channel].remove_user(nick)
def _on_quit(self, c, e):
"""[Internal]"""
nick = nm_to_n(e.source())
for ch in self.channels.values():
if ch.has_user(nick):
ch.remove_user(nick)
def die(self, msg="Bye, cruel world!"):
"""Let the bot die.
Arguments:
msg -- Quit message.
"""
self.connection.disconnect(msg)
sys.exit(0)
def disconnect(self, msg="I'll be back!"):
"""Disconnect the bot.
The bot will try to reconnect after a while.
Arguments:
msg -- Quit message.
"""
self.connection.disconnect(msg)
def get_version(self):
"""Returns the bot version.
Used when answering a CTCP VERSION request.
"""
return "ircbot.py by Joel Rosdahl <joel@rosdahl.net>"
def jump_server(self, msg="Changing servers"):
"""Connect to a new server, possibly disconnecting from the current.
The bot will skip to next server in the server_list each time
jump_server is called.
"""
if self.connection.is_connected():
self.connection.disconnect(msg)
self.server_list.append(self.server_list.pop(0))
self._connect()
def on_ctcp(self, c, e):
"""Default handler for ctcp events.
Replies to VERSION and PING requests and relays DCC requests
to the on_dccchat method.
"""
if e.arguments()[0] == "VERSION":
c.ctcp_reply(nm_to_n(e.source()),
"VERSION " + self.get_version())
elif e.arguments()[0] == "PING":
if len(e.arguments()) > 1:
c.ctcp_reply(nm_to_n(e.source()),
"PING " + e.arguments()[1])
elif e.arguments()[0] == "DCC" and e.arguments()[1].split(" ", 1)[0] == "CHAT":
self.on_dccchat(c, e)
def on_dccchat(self, c, e):
pass
def start(self):
"""Start the bot."""
self._connect()
SimpleIRCClient.start(self)
class IRCDict:
"""A dictionary suitable for storing IRC-related things.
Dictionary keys a and b are considered equal if and only if
irc_lower(a) == irc_lower(b)
Otherwise, it should behave exactly as a normal dictionary.
"""
def __init__(self, dict=None):
self.data = {}
self.canon_keys = {} # Canonical keys
if dict is not None:
self.update(dict)
def __repr__(self):
return repr(self.data)
def __cmp__(self, dict):
if isinstance(dict, IRCDict):
return cmp(self.data, dict.data)
else:
return cmp(self.data, dict)
def __len__(self):
return len(self.data)
def __getitem__(self, key):
return self.data[self.canon_keys[irc_lower(key)]]
def __setitem__(self, key, item):
if key in self:
del self[key]
self.data[key] = item
self.canon_keys[irc_lower(key)] = key
def __delitem__(self, key):
ck = irc_lower(key)
del self.data[self.canon_keys[ck]]
del self.canon_keys[ck]
def __iter__(self):
return iter(self.data)
def __contains__(self, key):
return self.has_key(key)
def clear(self):
self.data.clear()
self.canon_keys.clear()
def copy(self):
if self.__class__ is UserDict:
return UserDict(self.data)
import copy
return copy.copy(self)
def keys(self):
return self.data.keys()
def items(self):
return self.data.items()
def values(self):
return self.data.values()
def has_key(self, key):
return irc_lower(key) in self.canon_keys
def update(self, dict):
for k, v in dict.items():
self.data[k] = v
def get(self, key, failobj=None):
return self.data.get(key, failobj)
class Channel:
"""A class for keeping information about an IRC channel.
This class can be improved a lot.
"""
def __init__(self):
self.userdict = IRCDict()
self.operdict = IRCDict()
self.voiceddict = IRCDict()
self.modes = {}
def users(self):
"""Returns an unsorted list of the channel's users."""
return self.userdict.keys()
def opers(self):
"""Returns an unsorted list of the channel's operators."""
return self.operdict.keys()
def voiced(self):
"""Returns an unsorted list of the persons that have voice
mode set in the channel."""
return self.voiceddict.keys()
def has_user(self, nick):
"""Check whether the channel has a user."""
return nick in self.userdict
def is_oper(self, nick):
"""Check whether a user has operator status in the channel."""
return nick in self.operdict
def is_voiced(self, nick):
"""Check whether a user has voice mode set in the channel."""
return nick in self.voiceddict
def add_user(self, nick):
self.userdict[nick] = 1
def remove_user(self, nick):
for d in self.userdict, self.operdict, self.voiceddict:
if nick in d:
del d[nick]
def change_nick(self, before, after):
self.userdict[after] = 1
del self.userdict[before]
if before in self.operdict:
self.operdict[after] = 1
del self.operdict[before]
if before in self.voiceddict:
self.voiceddict[after] = 1
del self.voiceddict[before]
def set_mode(self, mode, value=None):
"""Set mode on the channel.
Arguments:
mode -- The mode (a single-character string).
value -- Value
"""
if mode == "o":
self.operdict[value] = 1
elif mode == "v":
self.voiceddict[value] = 1
else:
self.modes[mode] = value
def clear_mode(self, mode, value=None):
"""Clear mode on the channel.
Arguments:
mode -- The mode (a single-character string).
value -- Value
"""
try:
if mode == "o":
del self.operdict[value]
elif mode == "v":
del self.voiceddict[value]
else:
del self.modes[mode]
except KeyError:
pass
def has_mode(self, mode):
return mode in self.modes
def is_moderated(self):
return self.has_mode("m")
def is_secret(self):
return self.has_mode("s")
def is_protected(self):
return self.has_mode("p")
def has_topic_lock(self):
return self.has_mode("t")
def is_invite_only(self):
return self.has_mode("i")
def has_allow_external_messages(self):
return self.has_mode("n")
def has_limit(self):
return self.has_mode("l")
def limit(self):
if self.has_limit():
return self.modes[l]
else:
return None
def has_key(self):
return self.has_mode("k")
def key(self):
if self.has_key():
return self.modes["k"]
else:
return None

View File

@ -1,64 +0,0 @@
#! /usr/bin/env python
#
# Example program using irclib.py.
#
# This program is free without restrictions; do anything you like with
# it.
#
# Joel Rosdahl <joel@rosdahl.net>
import irclib
import sys
def on_connect(connection, event):
if irclib.is_channel(target):
connection.join(target)
else:
while 1:
line = sys.stdin.readline()
if not line:
break
connection.privmsg(target, line)
connection.quit("Using irclib.py")
def on_join(connection, event):
while 1:
line = sys.stdin.readline()
if not line:
break
connection.privmsg(target, line)
connection.quit("Using irclib.py")
if len(sys.argv) != 4:
print "Usage: irccat <server[:port]> <nickname> <target>"
print "\ntarget is a nickname or a channel."
sys.exit(1)
def on_disconnect(connection, event):
sys.exit(0)
s = sys.argv[1].split(":", 1)
server = s[0]
if len(s) == 2:
try:
port = int(s[1])
except ValueError:
print "Error: Erroneous port."
sys.exit(1)
else:
port = 6667
nickname = sys.argv[2]
target = sys.argv[3]
irc = irclib.IRC()
try:
c = irc.server().connect(server, port, nickname)
except irclib.ServerConnectionError, x:
print x
sys.exit(1)
c.add_global_handler("welcome", on_connect)
c.add_global_handler("join", on_join)
c.add_global_handler("disconnect", on_disconnect)
irc.process_forever()

View File

@ -1,66 +0,0 @@
#! /usr/bin/env python
#
# Example program using irclib.py.
#
# This program is free without restrictions; do anything you like with
# it.
#
# Joel Rosdahl <joel@rosdahl.net>
import irclib
import sys
class IRCCat(irclib.SimpleIRCClient):
def __init__(self, target):
irclib.SimpleIRCClient.__init__(self)
self.target = target
def on_welcome(self, connection, event):
if irclib.is_channel(self.target):
connection.join(self.target)
else:
self.send_it()
def on_join(self, connection, event):
self.send_it()
def on_disconnect(self, connection, event):
sys.exit(0)
def send_it(self):
while 1:
line = sys.stdin.readline()
if not line:
break
self.connection.privmsg(self.target, line)
self.connection.quit("Using irclib.py")
def main():
if len(sys.argv) != 4:
print "Usage: irccat2 <server[:port]> <nickname> <target>"
print "\ntarget is a nickname or a channel."
sys.exit(1)
s = sys.argv[1].split(":", 1)
server = s[0]
if len(s) == 2:
try:
port = int(s[1])
except ValueError:
print "Error: Erroneous port."
sys.exit(1)
else:
port = 6667
nickname = sys.argv[2]
target = sys.argv[3]
c = IRCCat(target)
try:
c.connect(server, port, nickname)
except irclib.ServerConnectionError, x:
print x
sys.exit(1)
c.start()
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@ -1,68 +0,0 @@
Summary: A set of Python modules for IRC support.
Name: python-irclib
Version: 0.4.6
Release: 1
Group: Development/Libraries
License: LGPL
URL: http://python-irclib.sourceforge.net
Source: %{name}-%{version}.tar.gz
BuildRoot: %{_tmppath}/%{name}-root
Requires: python
BuildPrereq: python
BuildArch: noarch
%description
This library is intended to encapsulate the IRC protocol at a quite
low level. It provides an event-driven IRC client framework. It has
a fairly thorough support for the basic IRC protocol, CTCP and DCC
connections.
%prep
%setup -q
chmod 644 *
%build
python -c "import py_compile; py_compile.compile('irclib.py')"
python -c "import py_compile; py_compile.compile('ircbot.py')"
%install
[ "$RPM_BUILD_ROOT" != "/" ] && rm -rf $RPM_BUILD_ROOT
%{__mkdir_p} $RPM_BUILD_ROOT/usr/lib/python1.5/site-packages
%{__install} -m 644 irclib.py* $RPM_BUILD_ROOT/usr/lib/python1.5/site-packages
%{__install} -m 644 ircbot.py* $RPM_BUILD_ROOT/usr/lib/python1.5/site-packages
%clean
[ "$RPM_BUILD_ROOT" != "/" ] && rm -rf $RPM_BUILD_ROOT
%files
%defattr(-,root,root)
%doc README ChangeLog COPYING irccat irccat2 servermap testbot.py dccsend dccreceive
/usr/lib/python*/site-packages/*
%changelog
* Sat Dec 24 2005 Keltus <keltus@users.sourceforge.net> 0.4.6-1
- upgraded to 0.4.6
* Wed May 18 2005 Keltus <keltus@users.sourceforge.net> 0.4.5-1
- upgraded to 0.4.5
* Wed Feb 23 2005 Keltus <keltus@users.sourceforge.net> 0.4.4-1
- upgraded to 0.4.4
* Sun Jan 19 2005 Joel Rosdahl <joel@rosdahl.net> 0.4.3-1
- upgraded to 0.4.3
* Fri Jul 9 2004 Joel Rosdahl <joel@rosdahl.net> 0.4.2-1
- upgraded to 0.4.2
* Thu Oct 30 2003 Joel Rosdahl <joel@rosdahl.net> 0.4.1-1
- upgraded to 0.4.1
* Mon Sep 1 2002 Gary Benson <gary@inauspicious.org> 0.4.0-1
- upgraded to 0.4.0
* Wed Feb 20 2002 Gary Benson <gary@inauspicious.org> 0.3.4-1
- upgraded to 0.3.4
* Wed Feb 20 2002 Gary Benson <gary@inauspicious.org> 0.3.3-1
- initial revision

View File

@ -1,164 +0,0 @@
#! /usr/bin/env python
#
# Example program using irclib.py.
#
# Copyright (C) 1999-2002 Joel Rosdahl
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Joel Rosdahl <joel@rosdahl.net>
#
# servermap connects to an IRC server and finds out what other IRC
# servers there are in the net and prints a tree-like map of their
# interconnections.
#
# Example:
#
# % ./servermap irc.dal.net somenickname
# Connecting to server...
# Getting links...
#
# 26 servers (18 leaves and 8 hubs)
#
# splitrock.tx.us.dal.net
# `-vader.ny.us.dal.net
# |-twisted.ma.us.dal.net
# |-sodre.nj.us.dal.net
# |-glass.oh.us.dal.net
# |-distant.ny.us.dal.net
# | |-algo.se.eu.dal.net
# | | |-borg.se.eu.dal.net
# | | | `-ced.se.eu.dal.net
# | | |-viking.no.eu.dal.net
# | | |-inco.fr.eu.dal.net
# | | |-paranoia.se.eu.dal.net
# | | |-gaston.se.eu.dal.net
# | | | `-powertech.no.eu.dal.net
# | | `-algo-u.se.eu.dal.net
# | |-philly.pa.us.dal.net
# | |-liberty.nj.us.dal.net
# | `-jade.va.us.dal.net
# `-journey.ca.us.dal.net
# |-ion.va.us.dal.net
# |-dragons.ca.us.dal.net
# |-toronto.on.ca.dal.net
# | `-netropolis-r.uk.eu.dal.net
# | |-traced.de.eu.dal.net
# | `-lineone.uk.eu.dal.net
# `-omega.ca.us.dal.net
import irclib
import sys
if len(sys.argv) != 3:
print "Usage: servermap <server[:port]> <nickname>"
sys.exit(1)
links = []
def on_connect(connection, event):
sys.stdout.write("\nGetting links...")
sys.stdout.flush()
connection.links()
def on_passwdmismatch(connection, event):
print "Password required."
sys.exit(1)
def on_links(connection, event):
global links
links.append((event.arguments()[0],
event.arguments()[1],
event.arguments()[2]))
def on_endoflinks(connection, event):
global links
print "\n"
m = {}
for (to_node, from_node, desc) in links:
if from_node != to_node:
m[from_node] = m.get(from_node, []) + [to_node]
if connection.get_server_name() in m:
if len(m[connection.get_server_name()]) == 1:
hubs = len(m) - 1
else:
hubs = len(m)
else:
hubs = 0
print "%d servers (%d leaves and %d hubs)\n" % (len(links), len(links)-hubs, hubs)
print_tree(0, [], connection.get_server_name(), m)
connection.quit("Using irclib.py")
def on_disconnect(connection, event):
sys.exit(0)
def indent_string(level, active_levels, last):
if level == 0:
return ""
s = ""
for i in range(level-1):
if i in active_levels:
s = s + "| "
else:
s = s + " "
if last:
s = s + "`-"
else:
s = s + "|-"
return s
def print_tree(level, active_levels, root, map, last=0):
sys.stdout.write(indent_string(level, active_levels, last)
+ root + "\n")
if root in map:
list = map[root]
for r in list[:-1]:
print_tree(level+1, active_levels[:]+[level], r, map)
print_tree(level+1, active_levels[:], list[-1], map, 1)
s = sys.argv[1].split(":", 1)
server = s[0]
if len(s) == 2:
try:
port = int(s[1])
except ValueError:
print "Error: Erroneous port."
sys.exit(1)
else:
port = 6667
nickname = sys.argv[2]
irc = irclib.IRC()
sys.stdout.write("Connecting to server...")
sys.stdout.flush()
try:
c = irc.server().connect(server, port, nickname)
except irclib.ServerConnectionError, x:
print x
sys.exit(1)
c.add_global_handler("welcome", on_connect)
c.add_global_handler("passwdmismatch", on_passwdmismatch)
c.add_global_handler("links", on_links)
c.add_global_handler("endoflinks", on_endoflinks)
c.add_global_handler("disconnect", on_disconnect)
irc.process_forever()

View File

@ -1,9 +0,0 @@
#! /usr/bin/env python
from distutils.core import setup
setup(name="python-irclib",
version="0.4.6",
py_modules=["irclib", "ircbot"],
author="Joel Rosdahl",
author_email="joel@rosdahl.net",
url="http://python-irclib.sourceforge.net")

View File

@ -1,118 +0,0 @@
#! /usr/bin/env python
#
# Example program using ircbot.py.
#
# Joel Rosdahl <joel@rosdahl.net>
"""A simple example bot.
This is an example bot that uses the SingleServerIRCBot class from
ircbot.py. The bot enters a channel and listens for commands in
private messages and channel traffic. Commands in channel messages
are given by prefixing the text by the bot name followed by a colon.
It also responds to DCC CHAT invitations and echos data sent in such
sessions.
The known commands are:
stats -- Prints some channel information.
disconnect -- Disconnect the bot. The bot will try to reconnect
after 60 seconds.
die -- Let the bot cease to exist.
dcc -- Let the bot invite you to a DCC CHAT connection.
"""
from ircbot import SingleServerIRCBot
from irclib import nm_to_n, nm_to_h, irc_lower, ip_numstr_to_quad, ip_quad_to_numstr
class TestBot(SingleServerIRCBot):
def __init__(self, channel, nickname, server, port=6667):
SingleServerIRCBot.__init__(self, [(server, port)], nickname, nickname)
self.channel = channel
def on_nicknameinuse(self, c, e):
c.nick(c.get_nickname() + "_")
def on_welcome(self, c, e):
c.join(self.channel)
def on_privmsg(self, c, e):
self.do_command(e, e.arguments()[0])
def on_pubmsg(self, c, e):
a = e.arguments()[0].split(":", 1)
if len(a) > 1 and irc_lower(a[0]) == irc_lower(self.connection.get_nickname()):
self.do_command(e, a[1].strip())
return
def on_dccmsg(self, c, e):
c.privmsg("You said: " + e.arguments()[0])
def on_dccchat(self, c, e):
if len(e.arguments()) != 2:
return
args = e.arguments()[1].split()
if len(args) == 4:
try:
address = ip_numstr_to_quad(args[2])
port = int(args[3])
except ValueError:
return
self.dcc_connect(address, port)
def do_command(self, e, cmd):
nick = nm_to_n(e.source())
c = self.connection
if cmd == "disconnect":
self.disconnect()
elif cmd == "die":
self.die()
elif cmd == "stats":
for chname, chobj in self.channels.items():
c.notice(nick, "--- Channel statistics ---")
c.notice(nick, "Channel: " + chname)
users = chobj.users()
users.sort()
c.notice(nick, "Users: " + ", ".join(users))
opers = chobj.opers()
opers.sort()
c.notice(nick, "Opers: " + ", ".join(opers))
voiced = chobj.voiced()
voiced.sort()
c.notice(nick, "Voiced: " + ", ".join(voiced))
elif cmd == "dcc":
dcc = self.dcc_listen()
c.ctcp("DCC", nick, "CHAT chat %s %d" % (
ip_quad_to_numstr(dcc.localaddress),
dcc.localport))
else:
c.notice(nick, "Not understood: " + cmd)
def main():
import sys
if len(sys.argv) != 4:
print "Usage: testbot <server[:port]> <channel> <nickname>"
sys.exit(1)
s = sys.argv[1].split(":", 1)
server = s[0]
if len(s) == 2:
try:
port = int(s[1])
except ValueError:
print "Error: Erroneous port."
sys.exit(1)
else:
port = 6667
channel = sys.argv[2]
nickname = sys.argv[3]
bot = TestBot(channel, nickname, server, port)
bot.start()
if __name__ == "__main__":
main()

52
lua/dailies.lua Normal file
View File

@ -0,0 +1,52 @@
target_time = {hour=hour, min=minute, sec=second}
function get_total_day_seconds (time_table)
return (((time_table.hour * 60) + (time_table.min)) * 60) + time_table.sec
end
SECONDS_PER_DAY = get_total_day_seconds({hour=24, min=0, sec=0})
function get_sleep_duration_sec ()
current_time = os.time()
current_time_table = os.date("*t", current_time)
current_day_seconds = get_total_day_seconds(current_time_table)
target_day_seconds = get_total_day_seconds(target_time)
difference = target_day_seconds - current_day_seconds
if difference > 0 then
return difference
else
return SECONDS_PER_DAY + difference
end
end
-- Returns full weekday all lowercased (monday, tuesday, etc)
function get_weekday ()
return string.lower(os.date("%A", os.time()))
end
function sleep (sec)
return os.execute("sleep " .. tonumber(sec))
end
function run_dailies (dailies)
while true do
sleep_duration = get_sleep_duration_sec()
print("sleep for " .. sleep_duration)
if not sleep(sleep_duration) then
print("sleep exited abnormally - break")
break
end
time_table = os.date("*t", os.time())
time_table.weekday = get_weekday()
for i, fn in ipairs(dailies) do
message = fn(time_table)
if message then
print("send message " .. message)
sender:send({type = "message", channel = channel, message = message})
break
end
end
end
end

View File

View File

@ -1,28 +0,0 @@
"""An echobox is a device that, upon feeding it an input, stores it and
outputs a previously submitted input.
Usage: !echobox input"""
import random
import urllib
import urllib2
def echobox(nick,channel,tenquestionmarks,input):
"""The echobox command submits some text into an "echobox", or
a collection of quotes. After doing so, it outputs a random
quote from that colleciton."""
if input == None or input == "":
return tenquestionmarks.html("<b>Echobox error</b>: You need to type something after !echobox, because I'm too lazy to come up with something for you.")
result = ""
if "web_echobox_url" in tenquestionmarks.config()["echobox"]:
service_target = "%s?%s" % (tenquestionmarks.config()["echobox"]["web_echobox_url"],urllib.urlencode({"input": input, "nick": nick, "chan": channel}))
result = urllib2.urlopen(service_target).read()
else:
echobox = tenquestionmarks.get_json("echobox.json")
if "echoes" not in echobox:
echobox["echoes"] = []
echobox["echoes"].append(input)
tenquestionmarks.put_json("echobox.json",echobox)
result = random.choice(echobox["echoes"])
return tenquestionmarks.html("<b>Echobox</b>: %s" % (result))

View File

@ -1,7 +0,0 @@
"""Ask the eight-ball a yes/no question, and it will give you a reasonable answer.
Usage: !eightball question"""
import random
def eightball(nick,channel,tenquestionmarks,question):
return tenquestionmarks.html("<b>Eightball:</b> %s" % (random.choice(tenquestionmarks.config()["eightball"]["responses"])))

View File

@ -1,65 +0,0 @@
"""Help system for this tenquestionmarks-based bot.
Usage:
- !tqmhelp -> display an overview of all modules
- !tqmhelp module -> display help for a specific module
- !tqmhelp module command -> display help for a specific command
"""
import types
import inspect
def tqmhelp(nick,channel,tenquestionmarks,module,command=None):
output = []
bnick = tenquestionmarks.config()["nick"]
if module == "":
output.append("%s help index\n" % (bnick))
output.append("--------------------------------\n")
output.append("In this bot, commands are grouped into modules.\n")
output.append("For help with a specific module, type !tqmhelp followed by \n")
output.append("the name of the module.\n")
output.append("This bot contains the following modules:\n")
output.append("--------------------------------\n")
for submod in tenquestionmarks.modules():
output.append("<b>%s</b>\n" % (submod))
submodobj = tenquestionmarks.modules()[submod]
if not submodobj.__doc__ == None:
output.append(submodobj.__doc__)
output.append("\n--------------------------------\n")
elif not command == None:
try:
modobj = tenquestionmarks.modules()[module]
except KeyError:
return tenquestionmarks.html("<b>Help error</b>: No module named %s" % (module))
if not hasattr(modobj,command):
return tenquestionmarks.html("<b>Help error</b>: No command %s in module %s" % (command,module))
output.append("%s help for command %s.%s\n" % (bnick, module, command))
output.append("--------------------------------\n")
commandobj = getattr(modobj,command)
output.append("<b>Command %s.%s</b>\n" % (module, command))
argspec = inspect.getargspec(commandobj)
del argspec.args[0]
del argspec.args[0]
del argspec.args[0]
output.append("<b>Usage:</b> !%s.%s %s\n" % (module, command, " ".join(argspec.args)))
if not commandobj.__doc__ == None:
output.append(commandobj.__doc__)
else:
try:
modobj = tenquestionmarks.modules()[module]
except KeyError:
return tenquestionmarks.html("<b>Help error</b>: No module named %s" % (module))
output.append("%s help for module %s\n" % (bnick, module))
output.append("--------------------------------\n")
for var in vars(modobj):
varvalue = vars(modobj)[var]
if not (var.startswith("on_") or var.startswith("_")) and isinstance(varvalue,types.FunctionType):
output.append("<b>Command %s.%s</b>\n" % (module, var))
argspec = inspect.getargspec(varvalue)
del argspec.args[0]
del argspec.args[0]
del argspec.args[0]
output.append("<b>Usage:</b> !%s.%s %s\n" % (module, var, " ".join(argspec.args)))
if not varvalue.__doc__ == None:
output.append(varvalue.__doc__)
output.append("\n--------------------------------\n")
return tenquestionmarks.html("".join(output))

View File

@ -1,57 +0,0 @@
"""RSS feed aggregator. This module contains no user-facing commands."""
import sys
import os
import threading
import traceback
import feedparser
def on_connected(tenquestionmarks):
_rss_loop(tenquestionmarks,tenquestionmarks.config()["rss"]["frequency"])
def _rss_loop(tenquestionmarks, frequency=900.0):
try:
old_entries_file = os.path.join(tenquestionmarks.directory(),"old-feed-entries")
FILE = open(old_entries_file, "r")
filetext = FILE.read()
FILE.close()
except IOError:
filetext = ""
open(old_entries_file, "w").close()
filetext = filetext.decode("UTF-8")
for feed in tenquestionmarks.config()["rss"]["feeds"]:
feedname = ""
if isinstance(tenquestionmarks.config()["rss"]["feeds"],dict):
feedname = feed
feed = tenquestionmarks.config()["rss"]["feeds"][feed]
NextFeed = False
tenquestionmarks.log("refresh","Refreshing feed %s" % (feed))
d = feedparser.parse(feed)
for entry in d.entries:
title = entry.title
try:
title = title.encode("ascii")
except UnicodeEncodeError, uee:
title = tenquestionmarks.degrade_to_ascii(title)
except UnicodeDecodeError, ude:
title = tenquestionmarks.degrade_to_ascii(title)
if title in filetext:
tenquestionmarks.log("refresh","Old entry: %s" % (title))
NextFeed = True
else:
FILE = open(old_entries_file, "a")
try:
FILE.write(title + u"\n")
except Exception, e:
traceback.print_exc(file=sys.stdout)
tenquestionmarks.log("Error","%s %s" % (e,title))
FILE.close()
tenquestionmarks.queue(tenquestionmarks.config()["rss"]["format"] % ({"title": title, "link": entry.link, "feedname": feedname}))
if NextFeed:
break
def refresher(): _rss_loop(tenquestionmarks,frequency)
t = threading.Timer(frequency, refresher) # TODO: make this static
t.start()

View File

@ -1,33 +0,0 @@
#!/usr/bin/env python
from monarchpass.beedrill import Project, Action, Listener, do
class Tenquestionmarks(Project):
name = "Tenquestionmarks"
author = "Adrian Malacoda"
@Action(
title="Hello World",
command="hello"
)
def hello(self,person="world"):
print "Hello {person}!".format(person=person)
hidave = do(
action="hello",
command="hidave",
kwargs={"person": "Dave"}
)
@Action(
title="Goodbye World",
command="goodbye",
prerequisites=["hello"],
clean_directory=False
)
def goodbye(self):
print "Bye!"
@Listener(before="goodbye")
def goodbyeListener(self):
print "Listened to goodbye!"

96
src/event/filter.rs Normal file
View File

@ -0,0 +1,96 @@
use toml::value::Table;
use event::{Envelope, Event};
pub trait EventFilter: Sync + Send {
fn accept (&self, envelope: &Envelope) -> bool;
}
pub struct AttributeEventFilter {
// Attributes that can be filtered out
event_type: Option<String>,
username: Option<String>,
channel: Option<String>,
message: Option<String>
}
impl AttributeEventFilter {
pub fn new (attributes: &Table) -> AttributeEventFilter {
AttributeEventFilter {
event_type: attributes.get("type").and_then(|value| value.as_str()).map(|value| String::from(value)),
message: attributes.get("message").and_then(|value| value.as_str()).map(|value| String::from(value)),
username: attributes.get("username").and_then(|value| value.as_str()).map(|value| String::from(value)),
channel: attributes.get("channel").and_then(|value| value.as_str()).map(|value| String::from(value)),
}
}
}
impl EventFilter for AttributeEventFilter {
fn accept (&self, envelope: &Envelope) -> bool {
let mut result = true;
match &envelope.event {
&Event::Message { ref message } => {
if let Some(ref event_type) = self.event_type {
result = result && event_type == "message";
}
if let Some(ref channel_name) = self.channel {
match message.channel {
Some(ref channel) => result = result && channel_name == &channel.name,
None => result = false
}
}
if let Some(ref username) = self.username {
result = result && &message.author.name == username;
}
},
&Event::SelfJoin { ref channel } => {
if let Some(ref event_type) = self.event_type {
result = result && event_type == "selfjoin";
}
if let Some(ref channel_name) = self.channel {
result = result && channel_name == &channel.name;
}
},
&Event::SelfQuit { ref channel } => {
if let Some(ref event_type) = self.event_type {
result = result && event_type == "selfquit";
}
if let Some(ref channel_name) = self.channel {
result = result && channel_name == &channel.name;
}
},
&Event::UserJoin { ref channel, ref user } => {
if let Some(ref event_type) = self.event_type {
result = result && event_type == "userjoin";
}
if let Some(ref channel_name) = self.channel {
result = result && channel_name == &channel.name;
}
if let Some(ref username) = self.username {
result = result && &user.name == username;
}
},
&Event::UserQuit { ref channel, ref user } => {
if let Some(ref event_type) = self.event_type {
result = result && event_type == "userquit";
}
if let Some(ref channel_name) = self.channel {
result = result && channel_name == &channel.name;
}
if let Some(ref username) = self.username {
result = result && &user.name == username;
}
},
_ => {}
}
result
}
}

29
src/event/mod.rs Normal file
View File

@ -0,0 +1,29 @@
pub mod filter;
use toml::value::Table;
use {Message, Channel, User};
#[derive(Debug)]
pub enum Event {
Message { message: Message }, // A user sends a message
SelfJoin { channel: Channel }, // We join a channel
SelfQuit { channel: Channel }, // We quit a channel
UserJoin { channel: Channel, user: User }, // A user joins a channel
UserQuit { channel: Channel, user: User }, // A user quits a channel
UserKick { channel: Channel, user: User }, // A usre is kicked from a channel
UserBan { channel: Channel, user: User }, // A user is banned from a channel
TopicChange { channel: Channel }, // Channel topic is changed,
Configure { configuration: Table } // Request to reconfigure a module
}
#[derive(Debug)]
pub struct Envelope {
pub from: String,
pub event: Event,
}
impl Envelope {}

6
src/helpers/command.rs Normal file
View File

@ -0,0 +1,6 @@
pub fn split_command (input: &str) -> Option<(&str, &str)> {
match input.split_whitespace().into_iter().next() {
Some(command) => { Some((command, input[command.len()..].trim())) },
None => None
}
}

1
src/helpers/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod command;

255
src/lib.rs Normal file
View File

@ -0,0 +1,255 @@
extern crate toml;
extern crate crossbeam;
extern crate discord;
extern crate rand;
extern crate pvn;
extern crate echobox;
extern crate transformable_channels;
extern crate stc;
extern crate regex;
extern crate multimap;
extern crate irc;
extern crate thread_local;
#[macro_use]
extern crate hlua;
use std::collections::BTreeMap;
use toml::value::Table;
mod modules;
use modules::Module;
use modules::loader::{ModuleLoader, ModuleLoaderError};
mod event;
use event::Event;
use event::Envelope;
use event::filter::{EventFilter, AttributeEventFilter};
use std::sync::Arc;
use std::sync::mpsc;
use std::sync::mpsc::Sender;
use transformable_channels::mpsc::TransformableSender;
use transformable_channels::mpsc::Receiver;
use multimap::MultiMap;
mod helpers;
#[macro_use]
extern crate log;
pub struct Tenquestionmarks {
modules: BTreeMap<String, Module>,
subscriptions: MultiMap<String, Subscription>
}
impl Tenquestionmarks {
pub fn with_modules (modules: BTreeMap<String, Module>) -> Tenquestionmarks {
let mut subscriptions = MultiMap::new();
for (name, module) in modules.iter() {
for parent in module.parents() {
info!("{:?} registered as parent of {:?}", parent, name);
subscriptions.insert(parent, Subscription::new(name.to_owned(), &module));
}
for child_name in module.children() {
if let Some(ref child) = modules.get(&child_name) {
info!("{:?} registered as child of {:?}", child_name, name);
subscriptions.insert(name.clone(), Subscription::new(child_name.to_owned(), &child));
}
}
}
Tenquestionmarks {
modules: modules,
subscriptions: subscriptions
}
}
pub fn from_configuration (configuration: Table) -> Result<Tenquestionmarks, ModuleLoaderError> {
let loader = ModuleLoader::new();
let modules = loader.load_from_configuration(configuration)?;
Result::Ok(Tenquestionmarks::with_modules(modules))
}
pub fn get_module (&self, name: &str) -> Option<&Module> {
self.modules.get(name)
}
pub fn reconfigure (&self, configuration: &Table) {
for (key, module_configuration) in configuration {
if let (Some(module_configuration_table), Some(ref module)) = (module_configuration.as_table(), self.modules.get(key)) {
module.reconfigure(module_configuration_table.clone());
}
}
}
pub fn run (&self) {
crossbeam::scope(|scope| {
let mut dispatchers: BTreeMap<&str, Receiver<Envelope>> = BTreeMap::new();
// Event loop threads.
// Event loop threads consume events passed in by other modules' dispatcher threads,
// and produce events through their own dispatcher threads.
let senders: BTreeMap<&str, Sender<Arc<Envelope>>> = self.modules.iter().map(|(key, module)| {
let from = key.clone();
let (dispatcher_sender, dispatcher_receiver) = transformable_channels::mpsc::channel();
dispatchers.insert(key, dispatcher_receiver);
let mapped_sender = dispatcher_sender.map(move |event: Event| {
Envelope {
from: from.clone(),
event: event
}
});
let (module_sender, module_receiver) = mpsc::channel();
info!("Spawning event loop thread for \"{}\"", key);
scope.spawn(move || {
module.run(Box::new(mapped_sender), module_receiver);
info!("Event loop thread for \"{}\" is exiting", key);
});
module.set_sender(&module_sender);
(&key[..], module_sender)
}).collect();
// Dispatcher threads.
// Dispatcher threads transmit events produced by parent modules to child modules.
for (from, receiver) in dispatchers.into_iter() {
if let Some(subscriptions) = self.subscriptions.get_vec(from) {
let dispatcher_senders: BTreeMap<&str, (&Subscription, Sender<Arc<Envelope>>)> = senders.iter().filter_map(|(key, value)| {
subscriptions.iter().find(|subscription| subscription.name == *key)
.map(|subscription| (*key, (subscription, value.clone())))
}).collect();
info!("Spawning dispatcher thread for \"{}\"", from);
scope.spawn(move || {
loop {
match receiver.recv() {
Ok(envelope) => {
let arc_envelope = Arc::new(envelope);
for (child_name, &(subscription, ref sender)) in dispatcher_senders.iter() {
if subscription.can_handle_event(&arc_envelope) {
if let Err(err) = sender.send(arc_envelope.clone()) {
debug!("Failed to dispatch event to module \"{}\": {:?}", child_name, err);
}
}
}
},
Err(err) => {
error!("Failed to receive event from module: \"{}\": {:?}", from, err);
break;
}
}
}
info!("Dispatcher thread for \"{}\" is exiting", from);
});
}
}
});
}
}
#[derive(Debug)]
pub struct Message {
content: String,
author: User,
channel: Option<Channel>
}
impl Message {
fn reply (&self, message: &str) {
match self.channel {
Some(ref channel) => channel.send(message),
None => self.author.send(message)
}
}
}
#[derive(Debug)]
pub struct Channel {
name: String,
description: String,
topic: String,
sender: Box<MessageSender>
}
impl Channel {
pub fn send (&self, message: &str) {
self.sender.send_message(message);
}
}
#[derive(Debug)]
pub struct User {
name: String,
sender: Box<MessageSender>
}
impl User {
pub fn send (&self, message: &str) {
self.sender.send_message(message);
}
}
pub trait MessageSender : Sync + Send + std::fmt::Debug {
fn send_message (&self, _: &str) {}
}
pub struct NullMessageSender {}
impl MessageSender for NullMessageSender {}
impl std::fmt::Debug for NullMessageSender {
fn fmt (&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "NullMessageSender")
}
}
struct Subscription {
pub name: String,
pub filters: Vec<Box<EventFilter>>
}
impl Subscription {
pub fn new (name: String, module: &Module) -> Subscription {
let filters: Vec<Box<EventFilter>> = module.config.get("filters")
.and_then(|value| value.as_array())
.map(|value| value.to_vec())
.unwrap_or(vec![])
.into_iter()
.map(|value| {
match value.as_table() {
Some(table) => Some(Box::new(AttributeEventFilter::new(table)) as Box<EventFilter>),
None => None
}
})
.filter(|possible_filter| possible_filter.is_some())
.map(|possible_filter| possible_filter.unwrap())
.collect();
Subscription {
name: name,
filters: filters
}
}
pub fn can_handle_event (&self, envelope: &Envelope) -> bool {
if !self.filters.is_empty() {
for filter in &self.filters {
if filter.accept(envelope) {
return true;
}
}
debug!(
"Refusing to transmit envelope from {:?} to {:?} since envelope was filtered out",
envelope.from,
self.name
);
return false;
}
true
}
}

84
src/main.rs Normal file
View File

@ -0,0 +1,84 @@
use std::env;
use std::fs::File;
use std::io::{Read, Write};
use std::sync::mpsc;
use std::time::Duration;
extern crate tenquestionmarks;
use tenquestionmarks::Tenquestionmarks;
extern crate toml;
#[macro_use]
extern crate log;
extern crate env_logger;
extern crate time;
extern crate crossbeam;
extern crate notify;
use notify::{RecommendedWatcher, Watcher, RecursiveMode, DebouncedEvent};
fn init_logger () {
let mut builder = env_logger::Builder::new();
builder.format(|buf, record: &log::Record| {
let t = time::now();
writeln!(buf, "{} {}:{}: {}", time::strftime("%Y-%m-%d %H:%M:%S", &t).unwrap(), record.level(), record.module_path().unwrap_or("?"), record.args())
}).filter(None, log::LevelFilter::Info);;
if env::var("RUST_LOG").is_ok() {
builder.parse(&env::var("RUST_LOG").unwrap());
}
builder.init();
}
fn main () {
init_logger();
let config_file_name = env::args().nth(1).unwrap_or("tenquestionmarks.toml".into());
match Tenquestionmarks::from_configuration(read_config_from_file(&config_file_name)) {
Ok(tqm) => {
info!("tenquestionmarks initialized successfully");
crossbeam::scope(|scope| {
scope.spawn(|| {
let (notify_sender, notify_reciever) = mpsc::channel();
let mut watcher: RecommendedWatcher = Watcher::new(notify_sender, Duration::from_secs(2)).expect("Failed to create watcher");
watcher.watch(&config_file_name, RecursiveMode::NonRecursive).expect("Failed to watch config file");
loop {
if let Ok(event) = notify_reciever.recv() {
if let DebouncedEvent::Write(_) = event {
info!("Detected modified config file, reconfiguring");
tqm.reconfigure(&read_config_from_file(&config_file_name));
}
}
}
});
tqm.run();
})
},
Err(e) => error!("Failed to initialize tenquestionmarks: {:?}", e)
}
}
fn read_config_from_file (config_file_name: &str) -> toml::value::Table {
if let Ok(mut file) = File::open(&config_file_name) {
let mut contents = String::new();
if let Err(e) = file.read_to_string(&mut contents) {
panic!("Failed to open config file {}: {:?}", config_file_name, e);
} else {
match toml::from_str(&contents) {
Ok(configuration) => {
info!("Loaded configuration from: {}", config_file_name);
configuration
},
Err(error) => panic!("Failed to parse config file {}: {:?}. Config file must be a valid TOML file.", config_file_name, error)
}
}
} else {
panic!("Failed to open config file! Please specify path to a config file.");
}
}

117
src/modules/autolink.rs Normal file
View File

@ -0,0 +1,117 @@
use modules::EventLoop;
use toml::value::Table;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use Message;
use event::{Event, Envelope};
use stc::Link;
use stc::searchers::{Searcher, AggregateSearcher};
use stc::searchers::yugioh::{YugiohCard, YugiohSearcher};
use stc::searchers::mtg::{MtgCard, MtgSearcher};
use stc::searchers::mediawiki::MediawikiSearcher;
use regex::Regex;
use std::collections::BTreeMap;
pub struct AutolinkModule {}
impl AutolinkModule {
pub fn new (_: &Table, _: &Table) -> Box<EventLoop> {
Box::new(AutolinkModule {})
}
}
fn print_mtg_card (card: &MtgCard, message: &Message) {
message.reply(&format!("{}", card.image_url));
if let Some(ref cost) = card.cost {
message.reply(&format!("**{}** (**{}**)", card.name, cost));
} else {
message.reply(&format!("**{}**", card.name));
}
message.reply(&format!("**{}**", card.typeline));
if let Some(ref rules) = card.rules {
message.reply(&format!("{}", rules));
}
if let Some(ref flavor) = card.flavor {
message.reply(&format!("*{}*", flavor));
}
if let (&Some(ref power), &Some(ref toughness)) = (&card.power, &card.toughness) {
message.reply(&format!("{}/{}", power, toughness));
}
}
fn print_ygo_card (card: &YugiohCard, message: &Message) {
if let Some(ref family) = card.family {
message.reply(&format!("**{}** ({})", card.name, family));
} else {
message.reply(&format!("**{}**", card.name));
}
if let Some(ref level) = card.level {
message.reply(&format!("**Level**: {}", level));
}
if let Some(ref subtype) = card.subtype {
message.reply(&format!("**{} - {}**", card.card_type, subtype));
} else {
message.reply(&format!("**{}**", card.card_type));
}
message.reply(&format!("{}", card.text));
if let (&Some(ref atk), &Some(ref def)) = (&card.atk, &card.def) {
message.reply(&format!("{}/{}", atk, def));
}
}
fn print_any_link (link: &Link, message: &Message) {
message.reply(&format!("**Autolink:** {} -> {}", link.label(), link.url()));
}
impl EventLoop for AutolinkModule {
fn run (&self, _: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
let link_regex = Regex::new(r"\[\[([^\[\]]*)\]\]").expect("Invalid regex...");
let mut searchers = AggregateSearcher::new();
searchers.add_searcher("mtg", Box::new(MtgSearcher::new()));
searchers.add_searcher("ygo", Box::new(YugiohSearcher::new()));
searchers.add_searcher("ygp", Box::new(MediawikiSearcher::new(String::from("https://yugipedia.com/wiki/"))));
searchers.add_searcher("pk", Box::new(MediawikiSearcher::new(String::from("https://bulbapedia.bulbagarden.net/wiki/"))));
searchers.add_searcher("gp", Box::new(MediawikiSearcher::new(String::from("https://gammapedia.monarch-pass.net/wiki/"))));
searchers.add_searcher("ip", Box::new(MediawikiSearcher::new(String::from("http://infinitypedia.org/wiki/"))));
searchers.add_searcher("gcl", Box::new(MediawikiSearcher::new(String::from("https://glitchcity.info/wiki/"))));
searchers.add_searcher("wp", Box::new(MediawikiSearcher::new(String::from("https://en.wikipedia.org/wiki/"))));
let mut searcher_cache = BTreeMap::new();
loop {
match receiver.recv() {
Ok(envelope) => {
if let Event::Message { ref message } = envelope.event {
debug!("Received message from module {:?}... {:?}", envelope.from, message.content);
for cap in link_regex.captures_iter(&message.content) {
let term = cap[1].to_owned();
if let &mut Some(ref mut item) = searcher_cache.entry(term.to_owned()).or_insert_with(|| searchers.exact_search(&term)) {
print_any_link(item, message);
if let Some(card) = item.downcast_ref::<MtgCard>() {
print_mtg_card(card, message);
} else if let Some(card) = item.downcast_ref::<YugiohCard>() {
print_ygo_card(card, message);
}
}
}
}
}
Err(error) => { error!("Error {:?}", error) }
}
}
}
}

179
src/modules/discord.rs Normal file
View File

@ -0,0 +1,179 @@
use discord;
use discord::Discord;
use discord::model::{Event, PossibleServer};
use modules::EventLoop;
use toml::value::Table;
use event;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use std::fmt;
use std::fmt::{Debug, Formatter};
use crossbeam;
use {MessageSender, Message, User, Channel};
pub struct DiscordModule {
token: String,
playing: String
}
const DEFAULT_PLAYING: &'static str = "tenquestionmarks 0.0.3";
impl DiscordModule {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
let token = configuration.get("token")
.and_then(|value| value.as_str())
.unwrap_or("");
let playing = configuration.get("playing")
.and_then(|value| value.as_str())
.unwrap_or(DEFAULT_PLAYING);
Box::new(DiscordModule {
token: String::from(token),
playing: String::from(playing)
})
}
}
pub struct DiscordMessageSender {
discord: Arc<Discord>,
channel_id: discord::model::ChannelId
}
impl MessageSender for DiscordMessageSender {
fn send_message (&self, message: &str) {
debug!("Send message to channel id {:?}: {:?}", self.channel_id, message);
match self.discord.send_message(self.channel_id, message, "", false) {
Ok(message) => { debug!("Send message succeeded: {:?}", message.id); },
Err(err) => { error!("Send message failed: {:?}", err) }
}
}
}
impl Debug for DiscordMessageSender {
fn fmt (&self, formatter: &mut Formatter) -> fmt::Result {
write!(formatter, "DiscordMessageSender {{ channel_id: {:?} }}", self.channel_id)
}
}
fn make_channel_object (discord: Arc<Discord>, channel: discord::model::Channel) -> Option<Channel> {
match channel {
discord::model::Channel::Group(group) => {
Some(Channel {
name: group.name.unwrap_or_else(|| String::from("Group channel")),
description: String::from(""),
topic: String::from(""),
sender: Box::new(DiscordMessageSender {
discord: discord,
channel_id: group.channel_id
})
})
},
discord::model::Channel::Private(private_channel) => {
None
},
discord::model::Channel::Public(public_channel) => {
Some(channel_from_public_channel(discord, public_channel))
}
}
}
fn channel_from_public_channel (discord: Arc<Discord>, channel: discord::model::PublicChannel) -> Channel {
Channel {
name: channel.name,
description: String::from(""),
topic: channel.topic.unwrap_or_else(|| String::new()),
sender: Box::new(DiscordMessageSender {
discord: discord,
channel_id: channel.id
})
}
}
impl EventLoop for DiscordModule {
fn run (&self, sender: Box<ExtSender<event::Event>>, receiver: Receiver<Arc<event::Envelope>>) {
let discord = Arc::new(Discord::from_bot_token(&self.token[..]).expect("Discord module: Login failed"));
let (mut connection, _) = discord.connect().expect("Discord module: Connection failed");
info!("Playing {}", self.playing);
connection.set_game_name(self.playing.clone());
crossbeam::scope(|scope| {
let discord_sender = discord.clone();
scope.spawn(move || {
loop {
if let Ok(envelope) = receiver.recv() {
if let event::Event::Message { ref message } = envelope.event {
if let Some(ref channel) = message.channel {
let channel_string = channel.name.trim_left_matches('#');
if let Ok(channel_id) = channel_string.parse::<u64>() {
discord_sender.send_message(discord::model::ChannelId(channel_id), &message.content, "", false);
}
}
}
} else {
break;
}
}
});
loop {
let event = connection.recv_event();
debug!("Received event: {:?}", event);
match event {
Ok(Event::ServerCreate(server)) => {
match server {
PossibleServer::Online(server) => {
info!("Joined server: {}", server.name);
for channel in server.channels {
info!(" - Joined channel: {}", channel.name);
match sender.send(event::Event::SelfJoin {
channel: channel_from_public_channel(discord.clone(), channel)
}) {
Err(err) => error!("Error sending selfjoin event: {:?}", err),
Ok(_) => {}
}
}
},
_ => {}
}
},
Ok(Event::MessageCreate(message)) => {
let author = User {
name: message.author.name.clone(),
sender: Box::new(DiscordMessageSender {
discord: discord.clone(),
channel_id: message.channel_id
})
};
let message = Message {
author: author,
content: message.content,
channel: discord.get_channel(message.channel_id).ok().and_then(|channel| make_channel_object(discord.clone(), channel))
};
if let Err(err) = sender.send(event::Event::Message { message: message }) {
error!("Error sending message event: {:?}", err)
}
}
Ok(_) => {}
Err(discord::Error::Closed(code, body)) => {
error!("Gateway closed on us with code {:?}: {}", code, body);
break
}
Err(err) => error!("Received error: {:?}", err)
}
}
});
}
}

51
src/modules/echo.rs Normal file
View File

@ -0,0 +1,51 @@
use modules::EventLoop;
use toml::value::Table;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use helpers::command::split_command;
use event::{Event, Envelope};
pub struct EchoModule {
prefix: String
}
impl EchoModule {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
let prefix = configuration.get("prefix")
.and_then(|value| value.as_str())
.unwrap_or("!echo");
Box::new(EchoModule {
prefix: String::from(prefix)
})
}
}
impl EventLoop for EchoModule {
fn run(&self, _: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
loop {
match receiver.recv() {
Ok(envelope) => {
match envelope.event {
Event::Message { ref message } => {
debug!("Received message from module {:?}... {:?}", envelope.from, message.content);
match split_command(&message.content) {
Some((command, argument)) => {
if command == self.prefix {
message.reply(argument);
}
},
_ => {}
}
}
_ => ()
}
}
Err(error) => { error!("Error {:?}", error) }
}
}
}
}

67
src/modules/echobox.rs Normal file
View File

@ -0,0 +1,67 @@
use modules::EventLoop;
use toml::value::Table;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use helpers::command::split_command;
use event::{Event, Envelope};
use echobox::Echobox;
pub struct EchoboxModule {
prefix: String,
file: String
}
impl EchoboxModule {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
let prefix = configuration.get("prefix")
.and_then(|value| value.as_str())
.unwrap_or("?echobox");
let file = configuration.get("responses")
.and_then(|value| value.as_str())
.unwrap_or("echobox.db");
Box::new(EchoboxModule {
prefix: String::from(prefix),
file: String::from(file)
})
}
}
impl EventLoop for EchoboxModule {
fn run(&self, _: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
let echobox = Echobox::with_file(&self.file).unwrap();
loop {
match receiver.recv() {
Ok(envelope) => {
match envelope.event {
Event::Message { ref message } => {
debug!("Received message from module {:?}... {:?}", envelope.from, message.content);
match split_command(&message.content) {
Some((command, in_quote)) => {
if command == self.prefix {
if !in_quote.is_empty() {
match echobox.echo(in_quote) {
Ok(out_quote) => message.reply(&format!("**Echobox, quote #{}:** {}", out_quote.id, out_quote.content)),
Err(error) => { error!("Error from echobox.echo(): {:?}", error) }
}
} else {
message.reply("**Echobox:** Please enter a quote.");
}
}
},
_ => {}
}
}
_ => ()
}
}
Err(error) => { error!("Error {:?}", error) }
}
}
}
}

154
src/modules/irc.rs Normal file
View File

@ -0,0 +1,154 @@
use irc::client::prelude::*;
use irc::client::data::command::Command;
use modules::EventLoop;
use toml::value::Table;
use event;
use std::sync::{Arc, Mutex};
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use std::fmt;
use std::fmt::{Debug, Formatter};
use crossbeam;
use thread_local::CachedThreadLocal;
use {MessageSender, Message, User, Channel};
pub struct IrcHandler {
config: Config
}
const DEFAULT_NICK: &'static str = "tenquestionmarks";
impl IrcHandler {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
Box::new(IrcHandler {
config: Config {
nickname: Some(configuration.get("nickname")
.and_then(|value| value.as_str())
.unwrap_or(DEFAULT_NICK)
.to_owned()),
server: Some(configuration.get("server")
.and_then(|value| value.as_str())
.expect("Expected server in IRC config")
.to_owned()),
channels: Some(configuration.get("channels")
.and_then(|value| value.as_array())
.map(|value| value.to_vec())
.unwrap_or(vec![])
.into_iter()
.map(|value| { String::from(value.as_str().unwrap()) })
.collect()),
.. Default::default()
}
})
}
}
pub struct IrcMessageSender {
irc: IrcServerWrapper,
channel: String
}
impl MessageSender for IrcMessageSender {
fn send_message (&self, message: &str) {
debug!("Send message to channel {:?}: {:?}", self.channel, message);
match self.irc.get().send_privmsg(&self.channel, message) {
Ok(_) => { debug!("Send message succeeded"); },
Err(err) => { error!("Send message failed: {:?}", err) }
}
}
}
impl Debug for IrcMessageSender {
fn fmt (&self, formatter: &mut Formatter) -> fmt::Result {
write!(formatter, "IrcMessageSender {{ channel: {:?} }}", self.channel)
}
}
fn make_user (prefix: &Option<String>, server: &IrcServer) -> Option<User> {
prefix.as_ref().and_then(|prefix| prefix.split("!").next()).map(|name| User {
name: name.to_owned(),
sender: Box::new(IrcMessageSender {
irc: IrcServerWrapper::new(&server),
channel: name.to_owned()
})
})
}
struct IrcServerWrapper {
server: IrcServer,
thread_local: CachedThreadLocal<IrcServer>
}
impl IrcServerWrapper {
pub fn new (server: &IrcServer) -> IrcServerWrapper {
IrcServerWrapper {
server: server.clone(),
thread_local: CachedThreadLocal::new()
}
}
pub fn get (&self) -> &IrcServer {
self.thread_local.get_or(|| Box::new(self.server.clone()))
}
}
unsafe impl Sync for IrcServerWrapper {}
impl EventLoop for IrcHandler {
fn run (&self, sender: Box<ExtSender<event::Event>>, receiver: Receiver<Arc<event::Envelope>>) {
let server = IrcServer::from_config(self.config.clone()).unwrap();
server.identify().unwrap();
crossbeam::scope(|scope| {
let server_sender = server.clone();
scope.spawn(move || {
loop {
if let Ok(envelope) = receiver.recv() {
if let event::Event::Message { ref message } = envelope.event {
if let Some(ref channel) = message.channel {
server_sender.send_privmsg(&channel.name, &message.content);
}
}
} else {
break;
}
}
});
for server_message in server.iter() {
if let Ok(irc_message) = server_message {
match irc_message.command {
Command::PRIVMSG(channel, message) => {
if let Some(author) = make_user(&irc_message.prefix, &server) {
let message = Message {
author: author,
content: message,
channel: Some(Channel {
name: channel.clone(),
description: "".to_owned(),
topic: "".to_owned(),
sender: Box::new(IrcMessageSender {
irc: IrcServerWrapper::new(&server),
channel: channel.clone()
})
})
};
if let Err(err) = sender.send(event::Event::Message { message: message }) {
error!("Error sending message event: {:?}", err)
}
}
},
_ => {}
}
}
}
});
}
}

98
src/modules/loader.rs Normal file
View File

@ -0,0 +1,98 @@
use std::collections::BTreeMap;
use std::error::Error;
use std::fmt;
use toml::value::Table;
use modules::{Module, EventLoop};
use modules::discord::DiscordModule;
use modules::lua::LuaModule;
use modules::stdin::StdinModule;
use modules::echo::EchoModule;
use modules::random::RandomModule;
use modules::pvn::PvnModule;
use modules::echobox::EchoboxModule;
use modules::autolink::AutolinkModule;
use modules::logger::LoggerModule;
use modules::irc::IrcHandler;
use std::sync::{Arc, Mutex};
pub struct ModuleLoader {
types: BTreeMap<&'static str, fn(&Table, &Table) -> Box<EventLoop>>
}
impl ModuleLoader {
pub fn new () -> ModuleLoader {
let mut types = BTreeMap::new();
types.insert("discord", DiscordModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("lua", LuaModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("stdin", StdinModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("echo", EchoModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("random", RandomModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("pvn", PvnModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("echobox", EchoboxModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("autolink", AutolinkModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("logger", LoggerModule::new as fn(&Table, &Table) -> Box<EventLoop>);
types.insert("irc", IrcHandler::new as fn(&Table, &Table) -> Box<EventLoop>);
ModuleLoader {
types: types
}
}
pub fn load_from_configuration (&self, configuration: Table) -> Result<BTreeMap<String, Module>, ModuleLoaderError> {
let general_config = configuration.get("general")
.and_then(|value| value.as_table())
.map(|value| value.clone())
.unwrap_or_else(|| BTreeMap::new());
configuration.into_iter().filter(|&(ref key, _)| {
key != "general"
}).map(|(key, value)| {
match value.as_table() {
Some(table) => {
let module = self.load_single_module(&key, &general_config, table)?;
Result::Ok((key, module))
},
None => Result::Err(ModuleLoaderError { message: format!("Bad configuration parameters for module instance: {}. Configuration for a Module must be a table.", key) })
}
}).collect()
}
pub fn load_single_module (&self, name: &str, general_configuration: &Table, module_configuration: &Table) -> Result<Module, ModuleLoaderError> {
/*
* The Module type defaults to the instance name (in the tenquestionmarks configuration)
* but can explicitly be set by using the special "type" parameter.
*/
let module_type: &str = module_configuration.get("type")
.and_then(|value| value.as_str())
.unwrap_or(name);
match self.types.get(module_type) {
Some(constructor) => Result::Ok(Module {
module_type: module_type.to_owned(),
config: module_configuration.clone(),
event_loop: constructor(general_configuration, module_configuration),
sender: Mutex::new(None)
}),
None => Result::Err(ModuleLoaderError { message: format!("No such module type: {}", module_type) })
}
}
}
#[derive(Debug)]
pub struct ModuleLoaderError {
message: String
}
impl Error for ModuleLoaderError {
fn description(&self) -> &str {
&self.message[..]
}
}
impl fmt::Display for ModuleLoaderError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "ModuleLoaderError: {}", self.message)
}
}

29
src/modules/logger.rs Normal file
View File

@ -0,0 +1,29 @@
use modules::EventLoop;
use toml::value::Table;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use event::{Event, Envelope};
pub struct LoggerModule {}
impl LoggerModule {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
Box::new(LoggerModule {})
}
}
impl EventLoop for LoggerModule {
fn run(&self, _: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
loop {
match receiver.recv() {
Ok(envelope) => {
info!("Received event envelope: {:?}", envelope);
}
Err(error) => { error!("Error {:?}", error) }
}
}
}
}

201
src/modules/lua.rs Normal file
View File

@ -0,0 +1,201 @@
use modules::EventLoop;
use toml::Value;
use toml::value::Table;
use hlua;
use hlua::{Lua, LuaFunction, AnyHashableLuaValue, AnyLuaValue, AsMutLua};
use {User, Message, Channel, NullMessageSender};
use event::{Event, Envelope};
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use std::path::Path;
use std::fs::File;
use std::collections::HashMap;
pub struct LuaModule {
code: Option<String>,
file: Option<String>,
variables: Table
}
impl LuaModule {
pub fn new (_: &Table, config: &Table) -> Box<EventLoop> {
Box::new(LuaModule {
code: config.get("code").and_then(|value| value.as_str()).map(String::from),
file: config.get("file").and_then(|value| value.as_str()).map(String::from),
variables: config.clone()
})
}
}
struct TomlValueWrapper(Value);
implement_lua_read!(TomlValueWrapper);
impl TomlValueWrapper {
pub fn lua_set (self, lua: &mut Lua, key: &str) {
match self.0 {
Value::String(string) => lua.set(key, string),
Value::Integer(integer) => lua.set(key, (integer as i32)),
Value::Float(float) => lua.set(key, float),
Value::Boolean(boolean) => lua.set(key, boolean),
Value::Table(table) => {},
Value::Array(array) => {},
Value::Datetime(datetime) => lua.set(key, datetime.to_string())
}
}
}
/*impl<'lua, L: AsMutLua<'lua>> hlua::Push<L> for TomlValueWrapper {
type Err = hlua::Void;
fn push_to_lua (self, lua: L) -> Result<hlua::PushGuard<L>, (hlua::Void, L)> {
match self.0 {
Value::String(string) => string.push_to_lua(lua),
Value::Integer(integer) => (integer as i32).push_to_lua(lua),
Value::Float(float) => float.push_to_lua(lua),
Value::Boolean(boolean) => boolean.push_to_lua(lua),
Value::Table(table) => {
let hashmap: HashMap<_, _> = table.into_iter().map(|(key, value)| {
(key, TomlValueWrapper(value))
}).collect();
hashmap.push_to_lua(lua)
},
Value::Array(array) => {
let vec: Vec<_> = array.into_iter().map(TomlValueWrapper).collect();
vec.push_to_lua(lua)
},
Value::Datetime(datetime) => datetime.to_string().push_to_lua(lua)
}
}
}
impl<'lua, L: AsMutLua<'lua>> hlua::PushOne<L> for TomlValueWrapper {}*/
struct MessageWrapper {
envelope: Arc<Envelope>
}
implement_lua_read!(MessageWrapper);
implement_lua_push!(MessageWrapper, |mut metatable| {
let mut index = metatable.empty_array("__index");
index.set("content", hlua::function1(|wrapper: &MessageWrapper| {
if let Event::Message { ref message } = wrapper.envelope.event {
Some(message.content.clone())
} else {
None
}
}));
index.set("reply", hlua::function2(|wrapper: &MessageWrapper, reply: String| {
if let Event::Message { ref message } = wrapper.envelope.event {
message.reply(&reply);
}
}));
});
struct SenderWrapper {
sender: Box<ExtSender<Event>>
}
implement_lua_read!(SenderWrapper);
implement_lua_push!(SenderWrapper, |mut metatable| {
let mut index = metatable.empty_array("__index");
index.set("send", hlua::function2(|wrapper: &SenderWrapper, data: HashMap<AnyHashableLuaValue, AnyLuaValue>| {
if let Some(&AnyLuaValue::LuaString(ref event_type)) = data.get(&AnyHashableLuaValue::LuaString("type".to_owned())) {
match event_type.as_ref() {
"message" => {
wrapper.sender.send(Event::Message {
message: Message {
author: User {
name: data.get(&AnyHashableLuaValue::LuaString("username".to_owned()))
.and_then(|value| match value {
&AnyLuaValue::LuaString (ref string_value) => Some(string_value.to_owned()),
_ => None
})
.unwrap_or_else(|| "".to_owned()),
sender: Box::new(NullMessageSender {})
},
channel: data.get(&AnyHashableLuaValue::LuaString("channel".to_owned()))
.and_then(|value| match value {
&AnyLuaValue::LuaString (ref string_value) => Some(string_value.to_owned()),
&AnyLuaValue::LuaNumber (ref number_value) => Some(format!("{}", number_value)),
_ => None
}).map(|channel| Channel {
name: channel,
description: "".to_owned(),
sender: Box::new(NullMessageSender {}),
topic: "".to_owned()
}),
content: data.get(&AnyHashableLuaValue::LuaString("message".to_owned()))
.and_then(|value| match value {
&AnyLuaValue::LuaString (ref string_value) => Some(string_value.to_owned()),
_ => None
})
.expect("Invalid message event passed in")
}
});
},
_ => {}
}
}
}));
});
impl EventLoop for LuaModule {
fn run (&self, sender: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
let mut lua = Lua::new();
lua.openlibs();
for (key, value) in &self.variables {
if key != "type" {
TomlValueWrapper(value.clone()).lua_set(&mut lua, key);
}
}
lua.set("sender", SenderWrapper {
sender: Box::new(sender.clone())
});
if let Some(ref file) = self.file {
lua.execute_from_reader::<(), _>(File::open(&Path::new(file)).unwrap()).unwrap();
}
if let Some(ref code) = self.code {
lua.execute(&code).unwrap()
}
loop {
match receiver.recv() {
Ok(envelope) => {
match envelope.event {
Event::Configure { ref configuration } => {
for (key, value) in configuration {
if key != "type" {
TomlValueWrapper(value.clone()).lua_set(&mut lua, key);
}
}
},
Event::Message { ref message } => {
let on_message: Option<LuaFunction<_>> = lua.get("on_message");
match on_message {
Some(mut on_message) => {
on_message.call_with_args::<(), _, _>(MessageWrapper {
envelope: envelope.clone()
}).unwrap();
},
None => {}
}
}
_ => ()
}
}
Err(error) => { error!("Error {:?}", error) }
}
}
}
}

84
src/modules/mod.rs Normal file
View File

@ -0,0 +1,84 @@
pub mod lua;
pub mod discord;
pub mod stdin;
pub mod echo;
pub mod random;
pub mod pvn;
pub mod echobox;
pub mod autolink;
pub mod logger;
pub mod irc;
pub mod loader;
use event::{Event, Envelope};
use std::sync::{Mutex, Arc};
use std::sync::mpsc::{Sender, Receiver};
use transformable_channels::mpsc::ExtSender;
use toml::value::Table;
pub struct Module {
event_loop: Box<EventLoop>,
module_type: String,
sender: Mutex<Option<Sender<Arc<Envelope>>>>,
pub config: Table,
}
impl Module {
pub fn run (&self, sender: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
self.event_loop.run(sender, receiver);
}
pub fn reconfigure (&self, configuration: Table) {
self.send(Envelope {
from: "reconfigure".to_owned(),
event: Event::Configure {
configuration: configuration
}
});
}
pub fn send (&self, event: Envelope) {
if let Ok(sender_lock) = self.sender.lock() {
if let Some(ref sender) = *sender_lock {
sender.send(Arc::new(event));
}
}
}
pub fn set_sender (&self, sender: &Sender<Arc<Envelope>>) {
if let Ok(ref mut sender_lock) = self.sender.lock() {
**sender_lock = Some(sender.clone());
}
}
pub fn parents (&self) -> Vec<String> {
self.config.get("parents")
.and_then(|value| value.as_array())
.map(|value| value.to_vec())
.unwrap_or(vec![])
.iter()
.map(|value| value.as_str())
.filter(|value| value.is_some())
.map(|value| value.unwrap().to_owned())
.collect()
}
pub fn children (&self) -> Vec<String> {
self.config.get("children")
.and_then(|value| value.as_array())
.map(|value| value.to_vec())
.unwrap_or(vec![])
.iter()
.map(|value| value.as_str())
.filter(|value| value.is_some())
.map(|value| value.unwrap().to_owned())
.collect()
}
}
pub trait EventLoop : Sync + Send {
fn run (&self, _: Box<ExtSender<Event>>, _: Receiver<Arc<Envelope>>) {}
}

169
src/modules/pvn.rs Normal file
View File

@ -0,0 +1,169 @@
use modules::EventLoop;
use toml::value::Table;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use event::{Event, Envelope};
use Message;
use helpers::command::split_command;
use pvn::Fighter;
use pvn::pirates::{Pirate, Pirates};
use pvn::ninjas::{Ninja, Ninjas};
pub struct PvnModule {}
impl PvnModule {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
Box::new(PvnModule {})
}
}
fn split_combatants (input: &str) -> Option<(&str, &str)> {
let fighters: Vec<&str> = input.split("|").map(|item| item.trim()).collect();
if fighters.len() != 2 {
return None;
}
Option::Some((
*fighters.get(0).expect("should be exactly two fighters"),
*fighters.get(1).expect("should be exactly two fighters")
))
}
trait PvnFighter: Fighter {
fn name (&self) -> &str;
fn print (&self, message: &Message);
fn fight (&self, other: &PvnFighter, message: &Message) {
self.print(message);
other.print(message);
if self.power() == other.power() {
message.reply("**It's a tie!**");
} else if self.power() > other.power() {
message.reply(&format!("**Winner: {}!**", self.name()));
} else {
message.reply(&format!("**Winner: {}!**", other.name()));
}
}
}
impl PvnFighter for Pirate {
fn print (&self, message: &Message) {
message.reply(&format!("**{}**", self.name));
message.reply(&format!("Swashbuckling: {}", self.swashbuckling));
message.reply(&format!("Drunkenness: {}", self.drunkenness));
message.reply(&format!("Booty: {}", self.booty));
message.reply(&format!("Weapons: {}", self.weapons.join(", ")));
message.reply(&format!("**TOTAL POWER: {}**", self.power()));
}
fn name (&self) -> &str {
&self.name[..]
}
}
impl PvnFighter for Ninja {
fn print (&self, message: &Message) {
message.reply(&format!("**{}**", self.name));
message.reply(&format!("Sneakiness: {}", self.sneakiness));
message.reply(&format!("Pajamas: {}", self.pajamas));
message.reply(&format!("Pointy Things: {}", self.pointy_things));
message.reply(&format!("Weapons: {}", self.weapons.join(", ")));
message.reply(&format!("**TOTAL POWER: {}**", self.power()));
}
fn name (&self) -> &str {
&self.name[..]
}
}
struct PirateVsNinja {
pirates: Pirates,
ninjas: Ninjas
}
impl PirateVsNinja {
fn pvn_command (&mut self, argument: &str, message: &Message) {
match split_combatants(argument) {
Some((pirate_name, ninja_name)) => {
match self.pirates.get(pirate_name) {
Ok(pirate) => {
match self.ninjas.get(ninja_name) {
Ok(ninja) => {
pirate.fight(ninja, message);
},
Err(error) => {
error!("Error getting ninja: {:?}", error);
}
}
},
Err(error) => {
error!("Error getting pirate: {:?}", error);
}
}
},
None => {
message.reply("Expected two arguments of the form: {pirate} | {ninja}");
}
}
}
fn pirate_command (&mut self, name: &str, message: &Message) {
match self.pirates.get(name) {
Ok(pirate) => {
pirate.print(message);
},
Err(error) => {
error!("Error getting pirate: {:?}", error);
}
}
}
fn ninja_command (&mut self, name: &str, message: &Message) {
match self.ninjas.get(name) {
Ok(ninja) => {
ninja.print(message);
},
Err(error) => {
error!("Error getting ninja: {:?}", error);
}
}
}
}
impl EventLoop for PvnModule {
fn run(&self, _: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
let mut pvn = PirateVsNinja {
pirates: Pirates::new(),
ninjas: Ninjas::new()
};
loop {
match receiver.recv() {
Ok(envelope) => {
match envelope.event {
Event::Message { ref message } => {
let command = split_command(&message.content);
debug!("Received message from module {:?}... {:?}", envelope.from, message.content);
match command {
Some(("?pvn", argument)) => { pvn.pvn_command(argument, message) },
Some(("?pirate", name)) => { pvn.pirate_command(name, message) },
Some(("?ninja", name)) => { pvn.ninja_command(name, message) },
_ => {}
}
}
_ => ()
}
}
Err(error) => {
error!("Error {:?}", error);
}
}
}
}
}

76
src/modules/random.rs Normal file
View File

@ -0,0 +1,76 @@
use modules::EventLoop;
use toml::value::Table;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use regex;
use regex::Regex;
use event::{Event, Envelope};
use rand;
pub struct RandomModule {
initial_configuration: Table,
}
impl RandomModule {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
Box::new(RandomModule {
initial_configuration: configuration.clone()
})
}
fn pattern_from_config (configuration: &Table) -> Regex {
configuration.get("pattern")
.and_then(|value| value.as_str())
.map(String::from)
.or_else(|| configuration.get("prefix")
.and_then(|value| value.as_str())
.map(|value| format!("^{}", regex::escape(value))))
.and_then(|value| Regex::new(&value).ok())
.expect("Invalid value for pattern")
}
fn responses_from_config (configuration: &Table) -> Vec<String> {
configuration.get("responses")
.and_then(|value| value.as_array())
.map(|value| value.to_vec())
.unwrap_or(vec![])
.into_iter()
.map(|value| { String::from(value.as_str().unwrap()) })
.collect()
}
}
impl EventLoop for RandomModule {
fn run (&self, _: Box<ExtSender<Event>>, receiver: Receiver<Arc<Envelope>>) {
let mut rng = rand::thread_rng();
let mut pattern = RandomModule::pattern_from_config(&self.initial_configuration);
let mut responses = RandomModule::responses_from_config(&self.initial_configuration);
loop {
match receiver.recv() {
Ok(envelope) => {
match envelope.event {
Event::Configure { ref configuration } => {
pattern = RandomModule::pattern_from_config(configuration);
responses = RandomModule::responses_from_config(configuration);
},
Event::Message { ref message } => {
debug!("Received message from module {:?}... {:?}", envelope.from, message.content);
if let Some(captures) = pattern.captures(&message.content) {
let mut response = String::new();
captures.expand(&rand::sample(&mut rng, &responses, 1)[0], &mut response);
message.reply(&response);
}
},
_ => {}
}
}
Err(error) => { error!("Error {:?}", error) }
}
}
}
}

83
src/modules/stdin.rs Normal file
View File

@ -0,0 +1,83 @@
use std::io;
use modules::EventLoop;
use toml::value::Table;
use {MessageSender, Message, User, Channel};
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use transformable_channels::mpsc::ExtSender;
use event::{Event, Envelope};
pub struct StdinModule {
name: String,
channel: Option<String>
}
const DEFAULT_NICK: &'static str = "user";
#[derive(Debug)]
pub struct StdinMessageSender {
name: String
}
impl MessageSender for StdinMessageSender {
fn send_message (&self, message: &str) {
debug!("Send message to stdout: {:?}", message);
println!("@{}: {}", self.name, message);
}
}
impl StdinModule {
pub fn new (_: &Table, configuration: &Table) -> Box<EventLoop> {
Box::new(StdinModule {
name: configuration.get("name")
.and_then(|value| value.as_str())
.unwrap_or(DEFAULT_NICK)
.to_owned(),
channel: configuration.get("channel")
.and_then(|value| value.as_str())
.map(String::from)
})
}
}
impl EventLoop for StdinModule {
fn run(&self, sender: Box<ExtSender<Event>>, _: Receiver<Arc<Envelope>>) {
let name = self.name.clone();
let channel = self.channel.clone();
loop {
let mut input = String::new();
match io::stdin().read_line(&mut input) {
Ok(_) => {
let message = Message {
author: User {
name: name.clone(),
sender: Box::new(StdinMessageSender {
name: name.clone()
})
},
content: input,
channel: channel.as_ref().map(|channel| Channel {
name: channel.to_owned(),
description: "".to_owned(),
sender: Box::new(StdinMessageSender {
name: channel.clone()
}),
topic: "".to_owned()
})
};
match sender.send(Event::Message { message: message }) {
Err(err) => error!("Error sending message event: {:?}", err),
Ok(_) => {}
}
}
Err(error) => error!("error: {}", error),
}
}
}
}

View File

@ -1,267 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
tenquestionmarks is an extensible, modular IRC bot.
This file is governed by the following license:
Copyright (c) 2011 Adrian Malacoda, Monarch Pass
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License, version 3, as published by
the Free Software Foundation.
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/>.
Based on the IRC bot at:
http://kstars.wordpress.com/2009/09/05/a-python-irc-bot-for-keeping-up-with-arxiv-or-any-rss-feed/
Copyright 2009 by Akarsh Simha
"""
import sys
import os
import threading
import time
import optparse
import json
import traceback
import datetime
import inspect
import re
for libdir in os.listdir(os.path.join(os.path.dirname(__file__),"lib")):
sys.path.append(os.path.join(os.path.dirname(__file__),"lib",libdir))
import irclib
from monarchpass import butterfree
class Tenquestionmarks(butterfree.Base,irclib.SimpleIRCClient):
VERSION_NAME = "Tenquestionmarks"
VERSION_NUMBER = "0.16.15"
def __init__(self):
irclib.SimpleIRCClient.__init__(self)
self.msgqueue = []
butterfree.Base.__init__(self,"tenquestionmarks")
def config(self):
conf = butterfree.Base.config(self)
if "port" not in conf:
conf["port"] = 6667
return conf
def _dispatcher(self,connection,event):
irclib.SimpleIRCClient._dispatcher(self,connection,event)
self.log(event.eventtype(),"%s from %s to %s" % (event.arguments(), event.source(), event.target()))
if not self.modules() == {}:
method = "on_" + event.eventtype()
self.dispatch(method,self,event)
def version(self):
"""Creates the version string that is returned from a CTCP version
request. This can be overridden."""
return "%s %s" % (Tenquestionmarks.VERSION_NAME, Tenquestionmarks.VERSION_NUMBER)
def connect(self):
"""Connects to the IRC network given in the constructor and joins a
set of channels.
When this method completes, the on_connected event is fired for modules
to handle."""
irclib.SimpleIRCClient.connect(self,self.config()["host"], self.config()["port"], self.config()["nick"], None, self.config()["nick"], ircname=self.config()["nick"])
if "password" in self.config():
self.server.privmsg("NickServ", "identify %s" % (self.config()["password"]))
for channel in self.config()["channels"]:
self.connection.join(channel)
self.dispatch("on_connected",self)
def loop(self):
"""Starts the IRC bot's loop."""
while 1:
while len(self.msgqueue) > 0:
msg = self.msgqueue.pop()
for channel in self.config()["channels"]:
self.log("post","Posting queued message %s" % (msg))
try:
self.connection.privmsg(channel, msg)
except Exception, e:
traceback.print_exc(file=sys.stdout)
self.log("Error","%s %s" % (e,msg))
time.sleep(1) # TODO: Fix bad code
self.ircobj.process_once()
time.sleep(1) # So that we don't hog the CPU!
def queue(self, message):
"""Adds a message to the end of the bot's message queue."""
self.msgqueue.append(message.encode("UTF-8"))
def on_ctcp(self, connection, event):
"""Event handler for CTCP messages. If the CTCP message is a version request,
a version string is returned."""
self.log("ctcp","%s from %s" % (event.arguments(), event.source()))
ctcptype = event.arguments()[0]
if ctcptype == "VERSION":
self.connection.ctcp_reply(event.source().split("!")[0],self.version())
def on_privmsg(self, connection, event):
"""Event handler for messages sent directly to the bot. These
are always treated as commands."""
message = event.arguments()[0]
nick = event.source().split("!")[0]
try:
(command, arg) = message.split(" ",1)
except ValueError:
command = message
arg = ""
self.log("cmd","%s called command %s with arg %s" % (nick,command,arg))
self.call_command(nick, None, command, arg)
def on_pubmsg(self, connection, event):
"""Event handler for messages sent to a channel in which the
bot resides. If the message starts with a certain string, the
message is treated as a command."""
message = event.arguments()[0]
nick = event.source().split("!")[0]
channel = event.target()
if message.startswith(self.command_prefix):
try:
(command, arg) = message[1:].split(" ",1)
except ValueError:
command = message[1:]
arg = ""
self.log("cmd","%s called command %s in %s with arg %s" % (nick,command,channel,arg))
self.call_command(nick, channel, command, arg)
def call_command(self, nick, channel, command, arg):
"""Calls a command defined in one or more modules. This method is
called indirectly through IRC messages sent from a user, who may
optionally be sending that message publicly in a channel.
The argument is a single string. Depending on how many arguments
the command takes, this string may be broken up into a number
of strings separated by spaces.
The return value of the command is output either back to the
user or to the channel in which the command was invoked."""
command_functions = butterfree.find_functions(self.modules(),command)
if len(command_functions) > 0:
for func in command_functions:
argspec = inspect.getargspec(func)
numargs = len(argspec.args) - 3
varargs = not argspec.varargs == None
args = []
if varargs:
args = arg.split(" ")
else:
args = arg.split(" ",numargs - 1)
args.insert(0,self)
args.insert(0,channel)
args.insert(0,nick)
try:
returnvalue = func(*args)
if not returnvalue == None:
if not channel == None:
self.multiline_privmsg(channel, returnvalue)
else:
self.multiline_privmsg(nick, returnvalue)
except Exception, e:
traceback.print_exc(file=sys.stdout)
self.log("Error","Command %s caused an %s" % (command, e))
def degrade_to_ascii(self,string):
"""In order to allow as wide a range of inputs as possible, if
a Unicode string cannot be decoded, it can instead be run through
this function, which will change "fancy quotes" to regular quotes
and remove diacritics and accent marks, among other things.
To doubly sure that the string is safe for ASCII, any non-ASCII
characters are then removed after any conversion is done."""
chars = {
u"": "'",
u"": "'",
u"": '"',
u"": '"',
u"Æ": "AE",
u"À": "A",
u"Á": "A",
u"Â": "A",
u"Ã": "A",
u"Ä": "A",
u"Å": "A",
u"Ç": "C",
u"È": "E",
u"É": "E",
u"Ê": "E",
u"Ë": "E",
u"Ì": "I",
u"Í": "I",
u"Î": "I",
u"Ï": "I",
u"Ð": "D",
u"Ñ": "N",
u"Ò": "O",
u"Ó": "O",
u"Ô": "O",
u"Õ": "O",
u"Ö": "O",
u"Ø": "O",
u"Ù": "U",
u"Ú": "U",
u"Û": "U",
u"Ü": "U",
u"Ý": "Y",
u"Þ": "Th",
u"ß": "S",
u"": "-"
}
for char in chars:
string = string.replace(char,chars[char]).replace(char.lower(),chars[char].lower())
# Strip away anything non-ascii that remains
string = "".join([char for char in string if ord(char) < 128])
return string
def html(self,string):
"""Basic parser that converts between a very limited subset of
HTML and IRC control codes.
Note that this parser is very strict about the font tag. It expects
either color or color and bgcolor, but not bgcolor alone. It also wants
both color and bgcolor to be quoted and separated by no more or less than one space."""
tags = [
[re.compile("<b>(.+?)</b>"),"\x02%(0)s\x02"],
[re.compile("<u>(.+?)</u>"),"\x1f%(0)s\x1f"],
[re.compile("<font color=\"(.+?)\">(.+?)</font>"),"\x03%(0)s%(1)s\x03"],
[re.compile("<font color=\"(.+?)\" bgcolor=\"(.+?)\">(.+?)</font>"),"\x03%(0)s,%(1)s%(2)s\x03"],
[re.compile("<font bgcolor=\"(.+?)\" color=\"(.+?)\">(.+?)</font>"),"\x03%(1)s,%(0)s%2(2)s\x03"]
]
for (regex,replacement) in tags:
regex_match = regex.search(string)
while regex_match is not None:
groups_dict = {}
for i in xrange(len(regex_match.groups())):
groups_dict[str(i)] = regex_match.groups()[i]
string = string.replace(regex_match.group(0), replacement % groups_dict)
regex_match = regex.search(string)
return string
def multiline_privmsg(self, target, message):
for line in message.split("\n"):
self.connection.privmsg(target, line)
if __name__ == "__main__":
tqm = Tenquestionmarks()
tqm.connect()
tqm.loop()

View File

@ -1,383 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
tenquestionmarks is an extensible, modular IRC bot.
This file is governed by the following license:
Copyright (c) 2011 Adrian Malacoda, Monarch Pass
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License, version 3, as published by
the Free Software Foundation.
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/>.
Based on the IRC bot at:
http://kstars.wordpress.com/2009/09/05/a-python-irc-bot-for-keeping-up-with-arxiv-or-any-rss-feed/
Copyright 2009 by Akarsh Simha
"""
import sys
import os
import threading
import time
import optparse
import json
import traceback
import datetime
import inspect
import re
for libdir in os.listdir(os.path.join(os.path.dirname(__file__),"lib")):
sys.path.append(os.path.join(os.path.dirname(__file__),"lib",libdir))
import irclib
class Tenquestionmarks(irclib.SimpleIRCClient):
VERSION_NAME = "Tenquestionmarks"
VERSION_NUMBER = "0.13.23"
def __init__(self,hostname, channels, nick, command_prefix="!", password=None, port=6667, directory=None):
irclib.SimpleIRCClient.__init__(self)
self.hostname = hostname
self.port = port
self.channel_list = channels
self.nick = nick
self.password = password
if directory == None:
directory = os.path.join(os.environ.get("HOME"),".tenquestionmarks")
self.directory = directory
try:
os.makedirs(directory)
except OSError: pass
self.msgqueue = []
self.logfile = open(os.path.join(self.directory,"log.log"),"w")
self.command_prefix = command_prefix
self.config = self.get_json("options.json")
self.modules = {}
def load_modules(self,modules):
"""Loads a set of modules given by name. The modules reside in the
modules subdirectory and so are prefixed with the string "module." """
self.modules = {}
for module in modules:
self.log("module","Loading module %s" % (module))
try:
self.modules[module] = getattr(__import__("modules.%s" % (module)),module)
self.log("module","%s loaded" % (module))
except ImportError:
traceback.print_exc(file=sys.stdout)
self.log("module","No module named %s" % (module))
def _dispatcher(self,connection,event):
irclib.SimpleIRCClient._dispatcher(self,connection,event)
self.log(event.eventtype(),"%s from %s to %s" % (event.arguments(), event.source(), event.target()))
if self.modules:
method = "on_" + event.eventtype()
self._fire_method_call(method,self,event)
def _fire_method_call(self,method,*args):
if self.modules:
for module in self.modules:
module = self.modules[module]
if hasattr(module, method):
getattr(module, method)(*args)
def version(self):
"""Creates the version string that is returned from a CTCP version
request. This can be overridden."""
return "%s %s" % (Tenquestionmarks.VERSION_NAME, Tenquestionmarks.VERSION_NUMBER)
def connect(self):
"""Connects to the IRC network given in the constructor and joins a
set of channels.
When this method completes, the on_connected event is fired for modules
to handle."""
irclib.SimpleIRCClient.connect(self,self.hostname, self.port, self.nick, None, self.nick, ircname=self.nick)
if not self.password == None:
self.server.privmsg("NickServ", "identify %s" % (self.nick))
for channel in self.channel_list:
self.connection.join(channel)
self._fire_method_call("on_connected",self)
def loop(self):
"""Starts the IRC bot's loop."""
while 1:
while len(self.msgqueue) > 0:
msg = self.msgqueue.pop()
for channel in self.channel_list:
self.log("post","Posting queued message %s" % (msg))
try:
self.connection.privmsg(channel, msg)
except Exception, e:
traceback.print_exc(file=sys.stdout)
self.log("Error","%s %s" % (e,msg))
time.sleep(1) # TODO: Fix bad code
self.ircobj.process_once()
time.sleep(1) # So that we don't hog the CPU!
def queue(self, message):
"""Adds a message to the end of the bot's message queue."""
self.msgqueue.append(message.encode("UTF-8"))
def on_ctcp(self, connection, event):
"""Event handler for CTCP messages. If the CTCP message is a version request,
a version string is returned."""
self.log("ctcp","%s from %s" % (event.arguments(), event.source()))
ctcptype = event.arguments()[0]
if ctcptype == "VERSION":
self.connection.ctcp_reply(event.source().split("!")[0],self.version())
def on_privmsg(self, connection, event):
"""Event handler for messages sent directly to the bot. These
are always treated as commands."""
message = event.arguments()[0]
nick = event.source().split("!")[0]
try:
(command, arg) = message.split(" ",1)
except ValueError:
command = message
arg = ""
self.log("cmd","%s called command %s with arg %s" % (nick,command,arg))
self.call_command(nick, None, command, arg)
def on_pubmsg(self, connection, event):
"""Event handler for messages sent to a channel in which the
bot resides. If the message starts with a certain string, the
message is treated as a command."""
message = event.arguments()[0]
nick = event.source().split("!")[0]
channel = event.target()
if message.startswith(self.command_prefix):
try:
(command, arg) = message[1:].split(" ",1)
except ValueError:
command = message[1:]
arg = ""
self.log("cmd","%s called command %s in %s with arg %s" % (nick,command,channel,arg))
self.call_command(nick, channel, command, arg)
def call_command(self, nick, channel, command, arg):
"""Calls a command defined in one or more modules. This method is
called indirectly through IRC messages sent from a user, who may
optionally be sending that message publicly in a channel.
The argument is a single string. Depending on how many arguments
the command takes, this string may be broken up into a number
of strings separated by spaces.
The return value of the command is output either back to the
user or to the channel in which the command was invoked."""
command_functions = self.resolve_command(command)
if len(command_functions) > 0:
for func in command_functions:
argspec = inspect.getargspec(func)
numargs = len(argspec.args) - 3
varargs = not argspec.varargs == None
args = []
if varargs:
args = arg.split(" ")
else:
args = arg.split(" ",numargs - 1)
args.insert(0,self)
args.insert(0,channel)
args.insert(0,nick)
try:
returnvalue = func(*args)
if not returnvalue == None:
if not channel == None:
self.multiline_privmsg(channel, returnvalue)
else:
self.multiline_privmsg(nick, returnvalue)
except Exception, e:
traceback.print_exc(file=sys.stdout)
self.log("Error","Command %s caused an %s" % (command, e))
def resolve_command(self, cmdname):
"""Given a command name, traverses through the modules loaded into
the bot and finds functions that match the command name.
If given a command name by itself, this method will look through
all modules for the command. If the command name is given in
the form [module].[command], only the given module is considered."""
funcs = []
module = ""
if "." in cmdname:
(module, cmdname) = cmdname.split(".")
self.log("cmd","%s is located in module %s" % (cmdname, module))
modobj = self.modules[module]
if hasattr(modobj,cmdname):
func = getattr(modobj,cmdname)
funcs.append(func)
else:
self.log("cmd","%s is not looking for a specific module" % (cmdname))
for modname in self.modules:
modobj = self.modules[modname]
if hasattr(modobj,cmdname):
func = getattr(modobj,cmdname)
funcs.append(func)
return funcs
def log(self, operation, message):
"""Logs a message onto standard out and into a file in the bot's directory."""
try:
logmessage = u"[%s] %s: %s" % (datetime.datetime.now(), operation.upper(), message)
print logmessage
self.logfile.write(logmessage)
except Exception, e:
traceback.print_exc(file=sys.stdout)
def get_json(self,filename):
"""Retrieves a JSON object (Python dictionary) from a file in this bot's directory.
If the file does not exist, one is created and an empty dictionary
is returned."""
try:
target = open(os.path.join(options.directory,filename))
obj = json.loads(target.read())
target.close()
return obj
except IOError:
self.put_json(filename, {})
return {}
def put_json(self,filename,obj):
"""Saves a JSON object (Python dictionary) as a file in this bot's directory."""
target = open(os.path.join(options.directory,filename),"w")
target.write(json.dumps(obj))
target.close()
def degrade_to_ascii(self,string):
"""In order to allow as wide a range of inputs as possible, if
a Unicode string cannot be decoded, it can instead be run through
this function, which will change "fancy quotes" to regular quotes
and remove diacritics and accent marks, among other things.
To doubly sure that the string is safe for ASCII, any non-ASCII
characters are then removed after any conversion is done."""
chars = {
u"": "'",
u"": "'",
u"": '"',
u"": '"',
u"Æ": "AE",
u"À": "A",
u"Á": "A",
u"Â": "A",
u"Ã": "A",
u"Ä": "A",
u"Å": "A",
u"Ç": "C",
u"È": "E",
u"É": "E",
u"Ê": "E",
u"Ë": "E",
u"Ì": "I",
u"Í": "I",
u"Î": "I",
u"Ï": "I",
u"Ð": "D",
u"Ñ": "N",
u"Ò": "O",
u"Ó": "O",
u"Ô": "O",
u"Õ": "O",
u"Ö": "O",
u"Ø": "O",
u"Ù": "U",
u"Ú": "U",
u"Û": "U",
u"Ü": "U",
u"Ý": "Y",
u"Þ": "Th",
u"ß": "S",
u"": "-"
}
for char in chars:
string = string.replace(char,chars[char]).replace(char.lower(),chars[char].lower())
# Strip away anything non-ascii that remains
string = "".join([char for char in string if ord(char) < 128])
return string
def html(self,string):
"""Basic parser that converts between a very limited subset of
HTML and IRC control codes.
Note that this parser is very strict about the font tag. It expects
either color or color and bgcolor, but not bgcolor alone. It also wants
both color and bgcolor to be quoted and separated by no more or less than one space."""
tags = [
[re.compile("<b>(.+?)</b>"),"\x02%(0)s\x02"],
[re.compile("<u>(.+?)</u>"),"\x1f%(0)s\x1f"],
[re.compile("<font color=\"(.+?)\">(.+?)</font>"),"\x03%(0)s%(1)s\x03"],
[re.compile("<font color=\"(.+?)\" bgcolor=\"(.+?)\">(.+?)</font>"),"\x03%(0)s,%(1)s%(2)s\x03"],
[re.compile("<font bgcolor=\"(.+?)\" color=\"(.+?)\">(.+?)</font>"),"\x03%(1)s,%(0)s%2(2)s\x03"]
]
for (regex,replacement) in tags:
regex_match = regex.search(string)
while regex_match is not None:
groups_dict = {}
for i in xrange(len(regex_match.groups())):
groups_dict[str(i)] = regex_match.groups()[i]
string = string.replace(regex_match.group(0), replacement % groups_dict)
regex_match = regex.search(string)
return string
def multiline_privmsg(self, target, message):
for line in message.split("\n"):
self.connection.privmsg(target, line)
if __name__ == "__main__":
optparser = optparse.OptionParser()
optparser.add_option("-n", "--nick", action="store", type="string", dest="nick", help="IRC bot nick")
optparser.add_option("-s", "--host", action="store", type="string", dest="host", help="Hostname of the IRC server")
optparser.add_option("-d", "--directory", action="store", type="string", dest="directory", default=os.path.join(os.environ.get("HOME"),".tenquestionmarks"),
help="Directory where the bot stores things (default: ~/.tenquestionmarks)")
optparser.add_option("-p", "--password", action="store", type="string", dest="password", help="Nickserv password")
optparser.add_option("-c", "--channels", action="store", type="string", dest="channels", help="Comma-separated list of channels to join")
optparser.add_option("-m", "--modules", action="store", type="string", dest="modules", help="Comma-separated names of modules to load")
optparser.add_option("-r", "--command-prefix", action="store", type="string", dest="commandprefix", help="Prefix put in front of user commands (default: !)")
(options, args) = optparser.parse_args()
conf_file = open(os.path.join(options.directory,"options.json"))
conf = json.loads(conf_file.read())
conf_file.close()
if options.nick == None and "nick" in conf:
options.nick = conf["nick"]
if options.password == None and "password" in conf:
options.password = conf["password"]
if options.channels == None and "channels" in conf:
options.channels = conf["channels"]
else:
options.channels = options.channels.split(",")
if options.modules == None and "modules" in conf:
options.modules = conf["modules"]
elif not options.modules == None:
options.modules = options.modules.split(",")
if options.host == None and "host" in conf:
options.host = conf["host"]
if options.commandprefix == None and "commandprefix" in conf:
options.commandprefix = conf["commandprefix"]
tqm = Tenquestionmarks(options.host, options.channels, options.nick, options.commandprefix, options.password)
if not options.modules == None:
tqm.load_modules(options.modules)
tqm.connect()
tqm.loop()

193
tenquestionmarks.toml Normal file
View File

@ -0,0 +1,193 @@
[general]
foo = "bar"
[dailies]
type = "lua"
children = ["irc"]
file = "lua/dailies.lua"
hour = 8
minute = 0
second = 0
channel = "#eightbar"
code = """
function chrimbus (time)
if time.day == 25 and time.month == 12 then
return "https://i.imgur.com/cDiJxrV.gif"
end
end
function februaryween (time)
if time.day == 14 and time.month == 2 then
return "http://s3images.coroflot.com/user_files/individual_files/302239_CauyLQoHZTkSJkkGnr3kVbFtw.jpg"
end
end
function monday (time)
if time.weekday == "monday" then
return "https://memegenerator.net/img/instances/66733493/you-dont-hate-mondays-you-hate-capitalism.jpg"
end
end
function tuesday (time)
if time.weekday == "tuesday" then
return "https://78.media.tumblr.com/996c6866874691590558dce00b394416/tumblr_nxyfp54u121rp1193o1_1280.png"
end
end
function wednesday (time)
if time.weekday == "wednesday" then
return "http://i0.kym-cdn.com/photos/images/original/001/091/264/665.jpg"
end
end
function thursday (time)
if time.weekday == "thursday" then
if time.day == 20 then
return "http://i1.kym-cdn.com/photos/images/newsfeed/001/245/590/bd8.jpg"
else
return "https://78.media.tumblr.com/b05de5acb40dfb4eca044526eed5bbfa/tumblr_inline_p59be6mrQp1scg9wt_540.png"
end
end
end
function friday (time)
if time.weekday == "friday" then
if time.day == 13 then
return "https://www.youtube.com/watch?v=9cPDdQs7iHs"
else
return "https://www.youtube.com/watch?v=kfVsfOSbJY0"
end
end
end
run_dailies({chrimbus, februaryween, monday, tuesday, wednesday, thursday, friday})
"""
[irc]
nickname = "tenquestionmarks2"
server = "irc.rizon.net"
channels = ["#eightbar"]
#[discord]
#token = "your token here"
[stdin]
[echo]
parents = ["stdin", "discord", "irc"]
prefix = "?echo"
[no]
type = "random"
parents = ["stdin", "discord", "irc"]
prefix = "?no"
responses = [
"https://www.youtube.com/watch?v=WWaLxFIVX1s", # Darth Vader
"https://www.youtube.com/watch?v=Hwz7YN1AQmQ", # Mario
"https://www.youtube.com/watch?v=O-ycQlfOqyY", # Dr. Robotnik
"https://www.youtube.com/watch?v=FSWiMoO8zNE", # Luke Skywalker
"https://www.youtube.com/watch?v=31g0YE61PLQ", # Michael Scott
"https://www.youtube.com/watch?v=xFGfWrJR5Ck", # Captain Picard
"https://www.youtube.com/watch?v=gvdf5n-zI14", # Nope.avi
"https://www.youtube.com/watch?v=2HJxya0CWco", # Dr. Evil
"https://www.youtube.com/watch?v=HIAql1AfSSU", # Finn the Human
"https://www.youtube.com/watch?v=4LSJJeR6MEU", # Trunks
"https://www.youtube.com/watch?v=6h7clHdeg6g", # Vegeta
"https://www.youtube.com/watch?v=cYTzynLuEuk", # Freakazoid
"https://www.youtube.com/watch?v=6BoVUpSsA1A", # Ganon
"https://www.youtube.com/watch?v=Oz7b7uYG0pk", # Robbie Rotten's Clone/Brother (We Are #1)
"https://www.youtube.com/watch?v=iabC7-9YUG4", # Cleveland Brown
"https://www.youtube.com/watch?v=iGLh9hRmRcM", # Homer Simpson
"https://www.youtube.com/watch?v=zfbK_dbsCu0", # Nathan Explosion
"https://www.youtube.com/watch?v=WfpyGyb1J4I", # Eric Cartman
"https://www.youtube.com/watch?v=wOxt9PoJNkg", # Nostalgia Critic
]
[yes]
type = "random"
parents = ["stdin", "discord", "irc"]
prefix = "?yes"
responses = [
"https://www.youtube.com/watch?v=JPVaDaynNKM", # Captain Falcon
"https://www.youtube.com/watch?v=P3ALwKeSEYs", # M. Bison
"https://www.youtube.com/watch?v=FJbmB9k2Y88", # Daniel Bryan
"https://www.youtube.com/watch?v=kfk5NIG7AhY", # Plankton
"https://www.youtube.com/watch?v=5v15U2uaV6k", # Jerry's boss (Rick and Morty)
"https://www.youtube.com/watch?v=DrKmo0YAZEo", # Simpsons Yes Guy
"https://www.youtube.com/watch?v=6VU1Kb7k0cs", # Majin Vegeta
"https://www.youtube.com/watch?v=jyJyI3z_tcc", # Scouter Vegeta
"https://www.youtube.com/watch?v=jcreG-bhRRA", # Piccolo
"https://www.youtube.com/watch?v=XFDGnmQyDf4", # Algernop Krieger
"https://www.youtube.com/watch?v=gSnfdncZCYo", # Twilight Sparkle
"https://www.youtube.com/watch?v=IPjvDE-rKo0", # William Forester (YTMND)
"https://www.youtube.com/watch?v=CBuIqmpeAm0", # Finn Hudson
"https://www.youtube.com/watch?v=q6EoRBvdVPQ", # Oro (Yee Dinosaur)
"https://www.youtube.com/watch?v=j44nP2J23Jk", # Austin Powers
"https://www.youtube.com/watch?v=b4mJtqqfMrQ", # Data
]
[chk]
type = "random"
parents = ["stdin", "discord", "irc"]
prefix = "?chk"
responses = ["ack"]
[pvn]
parents = ["stdin", "discord", "irc"]
[echobox]
parents = ["stdin", "discord", "irc"]
[lua]
parents = ["stdin", "discord"]
code = """
function on_message (message)
message:reply("Lua says: " .. message:content())
end
"""
foo = "bar"
[lua2]
type = "lua"
parents = ["stdin", "discord"]
filters = [{ username = "David" }]
code = """
function on_message (message)
message:reply("Lua2 says: " .. message:content())
end
"""
[lua3]
type = "lua"
#children = ["lua", "irc"]
code = """
local clock = os.clock
function sleep(n) -- seconds
local t0 = clock()
while clock() - t0 <= n do end
end
while true do
sender:send({type = "message", channel = "#eightbar", message = "Hello world!"})
sleep(10)
end
"""
[autolink]
parents = ["stdin", "discord", "irc"]
[logger]
parents = ["stdin", "discord", "irc"]
#filters = [{ username = "Dave" }, { username = "Kevin" }]
[icced]
type = "random"
parents = ["stdin", "discord", "irc"]
pattern = "(?i)\\bicc?ed?\\b"
responses = ["Did some carbon-based lifeform just say **I C E**?"]
[trout]
type = "random"
parents = ["stdin", "discord", "irc"]
pattern = "^\\?slap (.*)"
responses = ["/me slaps $1 around a bit with a large trout", "/me slaps $1 around a bit with a large brick"]