Merge lp:~gingerchris/endroid/docs into lp:endroid

Proposed by ChrisD
Status: Merged
Approved by: Martin Morrison
Approved revision: 85
Merged at revision: 36
Proposed branch: lp:~gingerchris/endroid/docs
Merge into: lp:endroid
Prerequisite: lp:~gingerchris/endroid/bugfix
Diff against target: 2086 lines (+1681/-101)
18 files modified
README (+1/-20)
debian/endroid.install (+1/-0)
doc/Installation (+0/-16)
doc/wiki/Configuration (+63/-33)
doc/wiki/Debugging (+127/-0)
doc/wiki/GettingStarted (+40/-0)
doc/wiki/Index (+7/-4)
doc/wiki/PluginTutorial (+790/-0)
doc/wiki/Reference (+557/-0)
src/endroid.sh (+1/-1)
src/endroid/confparser.py (+3/-0)
src/endroid/cron.py (+20/-8)
src/endroid/database.py (+39/-4)
src/endroid/manhole.py (+2/-1)
src/endroid/messagehandler.py (+4/-2)
src/endroid/pluginmanager.py (+5/-3)
src/endroid/usermanagement.py (+19/-9)
src/endroid/wokkelhandler.py (+2/-0)
To merge this branch: bzr merge lp:~gingerchris/endroid/docs
Reviewer Review Type Date Requested Status
Martin Morrison Approve
Review via email: mp+180083@code.launchpad.net

Commit message

Merge in updated docs.

Description of the change

New wiki docs and a few other minor changes.

To post a comment you must log in.
Revision history for this message
Martin Morrison (isoschiz) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== renamed file 'src/README' => 'README'
2--- src/README 2012-08-06 12:41:14 +0000
3+++ README 2013-08-14 10:10:05 +0000
4@@ -15,23 +15,4 @@
5 ** You should register a Jabber ID for your EnDroid, e.g. at https://register.jabber.org/
6 ** Put config in ~/.endroid/endroid.conf e.g.
7
8-Example ~/.endroid/endroid.conf follows:
9-============================================================
10-[Setup]
11-jid=my_en_droid@jabber.org
12-secret=PASSWORD
13-nick=MyEnDroid
14-
15-plugins=endroid.plugins.command,endroid.plugins.help,endroid.plugins.patternmatcher,endroid.plugins.speak,endroid.plugins.unhandled,endroid.plugins.whosonline,endroid.plugins.memo
16-
17-users=john@domain.tld
18-rooms=test-room@conference.jabber.org
19-
20-[Database]
21-dbfile=/tmp/endroid.db
22-
23-[UserGroup:*]
24-
25-[Room:*]
26-
27-============================================================
28+See etc/endroid.conf for an example config file.
29
30=== modified file 'debian/endroid.install'
31--- debian/endroid.install 2013-08-13 11:45:37 +0000
32+++ debian/endroid.install 2013-08-14 10:10:05 +0000
33@@ -1,4 +1,5 @@
34 etc/endroid.conf etc/endroid/
35+etc/endroid.conf usr/share/doc/endroid/examples/
36 etc/init/endroid.conf etc/init/
37 bin/endroid usr/bin/
38 bin/endroid_echo usr/bin/
39
40=== removed file 'doc/Installation'
41--- doc/Installation 2012-08-27 23:19:45 +0000
42+++ doc/Installation 1970-01-01 00:00:00 +0000
43@@ -1,16 +0,0 @@
44-#acl EnsoftLander:read,write,delete,admin,revert All:read
45-
46-= Installation =
47-
48-Installing EnDroid is straightforward. Installation packages are provided (currently only for Ubuntu) [[https://launchpad.net/endroid/+download|on Launchpad]]. It is also possible to run it directly by checking out the code:
49-
50- * `bzr branch lp:endroid` - to get the latest code.
51- * Create yourself a configuration file; an example is provided in the source in `etc/endroid.conf`
52- * You'll need an XMPP account to use for the bot
53- * '''Note''': EnDroid will "unfriend" any contact not directly configured. You have been warned!
54- * cd into the `src` directory and run the `endroid.sh` script there, passing your configuration file path as an argument
55- * By default, EnDroid looks in `~/.endroid/endroid.conf`, then `/etc/endroid/endroid.conf`
56- * on platforms without `/bin/sh`, `python -m endroid` (or, for Python 2.6 and earlier, the slightly long-winded `python -c 'import endroid; endroid.main()'` can be used instead
57-
58-Note that EnDroid requires python 2.6 or later to run, and depends on Twisted, Wokkel, PyTZ and python-dateutil.
59-
60
61=== renamed file 'src/endroid.graphml' => 'doc/module-diagram.graphml'
62=== added directory 'doc/wiki'
63=== renamed file 'doc/Configuration' => 'doc/wiki/Configuration'
64--- doc/Configuration 2012-08-27 23:19:45 +0000
65+++ doc/wiki/Configuration 2013-08-14 10:10:05 +0000
66@@ -1,39 +1,69 @@
67 #acl EnsoftLander:read,write,delete,revert,admin All:read
68
69-= Configuration =
70-
71-The config file is formatted as a standard "ini" config file (using Python's standard !ConfigParser library). The sections/options are as follows (bold = required):
72+<<TableOfContents>>
73
74 == Basic Configuration ==
75
76- * '''[Setup]'''
77- * '''jid''' - the JID from which to sign in, including a resource (`username@host.tld/resource`)
78- * '''secret''' - the password for this JID
79- * nick - the default nick to use in rooms. If this is not provided then it defaults to the JID.
80- * plugins - comma-or-newline-separated default list of plugins to use. If this is not provided then it defaults to none. (example plugin: endroid.plugins.echobot)
81- * Without plugins, your EnDroid isn't going to do much. Several features that might be expected to be core are actually implemented as plugins, so you should consider at a minimum running help, unhandled, command, patternmatcher
82- * users - list of users to allow. EnDroid will add all of these to its contacts, and will unfriend anyone who it sees who is not in this list.
83- * rooms - list of rooms to join. EnDroid will only join rooms in this list, regardless of whether they have configuration information further down the file.
84-
85- * [Database]
86- * dbfile - the location of the database file. In a default installation this will be at `/var/lib/endroid/db/endroid.db`
87- * [!UserGroup:*]
88- * plugins - if this is specified, then it replaces the plugins directive in the Setup section for all single user chats. Otherwise, defaults to equal it.
89- * [Room:*]
90- * plugins - ...same, but for all rooms.
91- * nick - if specified, overrides the nick directive from the Setup section, otherwise defaults to that.
92- * [UserGroup:Group Name]
93- * users - list of all users' JIDs in the group.
94- * plugins - if this is specified, then it replaces the current default for users (i.e. that in !UserGroup:*, which equally could be taken from Setup...)
95- * [Room:Room JID]
96- * plugins - if this is specified, then it replaces the current default for rooms (i.e. that in Room:*, which equally could be taken from Setup...)
97- * nick - you get the idea
98- * [Plugin:Plugin Package]
99- * whatever options the plugin supports
100-
101-== Further Config ==
102-
103-If more detailed configuration is required, plugins can be configured on a per-room basis.
104-
105-In the same folder as the standard configuration file, the plugins can have a filename plugin-!PluginName.cfg file. Sections are [Room:RoomName], then the same directives go in here as under the [Plugin:...] sections.
106+!EnDroid reads all of its config from a single configuration file, searched for at (in order):
107+ * conffile as specified on the command file e.g. `endroid.sh <conffile>`.
108+ * `ENDROID_CONF` environment variable.
109+ * `~/.endroid/endroid.conf` (recommended)
110+ * `/etc/endroid/endroid.conf`
111+
112+An example config file can be found at `/usr/share/doc/endroid/examples/endroid.conf` or at `etc/endroid.conf` in the source.
113+The basic sections of the config file are described below:
114+
115+{{{#!python
116+# Comments are denoted by a hash character in the first column
117+
118+[Setup]
119+# EnDroid's jabber login details.
120+jid = marvin@hhgg.tld/planet
121+password = secret
122+
123+# EnDroid's default nickname. If not specified, set to the part of jid before the @
124+# nick = Marvin
125+
126+# EnDroid's full contact list. Users on this list will be added as friends,
127+# users not on this list will be removed from contacts and will be unable
128+# to communicate with EnDroid.
129+users =
130+
131+# What rooms EnDroid will attempt to create and join. Defaults to []
132+# rooms = room1@ser.ver,
133+
134+# What usergroup EnDroid will register plugins with. Defaults to ['all']
135+# groups = all, admins
136+
137+logfile = ~/.endroid/endroid.log
138+
139+[Database]
140+dbfile = ~/.endroid/endroid.db
141+
142+# a section matching groups and rooms with any name
143+[ group | room : *]
144+plugins=
145+# endroid.plugins.<your_plugin>,
146+# endroid.plugins.<other plugin your plugin depends on>,
147+# ...
148+}}}
149+
150+== Some Notes on Syntax ==
151+
152+ * !EnDroid will try to interpret values in the config file as Python objects, so `my_var = 1`.
153+ * Bools will only be converted if they are `True` or `False` (i.e. capitalised).
154+ * Lists are detected by commas or newlines:{{{#!python
155+my_list = multiple, entries, present
156+my_list2 = newlines
157+ also
158+ mean
159+ lists
160+}}}
161+ * For a list with a single item, a comma must be present at the end of the list:{{{#!python
162+expects_a_list1 = foo
163+# Will result in a string foo not a list
164+
165+expects_a_list2 = foo,
166+# Will result in a list of items with only one entry - foo
167+}}}
168
169
170=== added file 'doc/wiki/Debugging'
171--- doc/wiki/Debugging 1970-01-01 00:00:00 +0000
172+++ doc/wiki/Debugging 2013-08-14 10:10:05 +0000
173@@ -0,0 +1,127 @@
174+#acl EnsoftLander:read,write,delete,revert,admin All:read
175+
176+<<TableOfContents>>
177+
178+= Logs =
179+
180+!EnDroid currently logs to two places (this is a bug - !EnDroid currently uses `twisted.log` ''and'' `python.logging` to generate log messages - these should be combined!)
181+
182+== The Console ==
183+
184+ * This is where basic log output is displayed (generated from the `logging.[info|debug|...]` calls in the code).
185+ * The verbosity of this log can be altered with the `-l <integer>` flag, integer defaults to `1`. Lower numbers result in more logging.
186+ * If something strange happens to !EnDroid and nothing is shown here, consult the log file.
187+
188+== The Logfile ==
189+
190+ * The log file as specified in `endroid.conf` (defaults to `~/.endroid/endroid.log`):
191+ * This is where `twisted` does all its logging.
192+ * If a `Deferred` has thrown an Exception - the traceback will be in this file.
193+ * If something is going wrong and there are still no apparent errors here, use the `-t` flag to start !EnDroid and look for `bad-request` or `error` in the xml.
194+ * Any `print` statements in the code will have their output redirected here.
195+ * The location of the log file may be specified with the `-L` or `--logfile` + `<logfile>` flag.
196+
197+= Debugging =
198+
199+== Manhole ==
200+
201+Manhole provides a way of controlling !EnDroid via ssh. It gives the user a python console through which all of !EnDroid's internals may be accessed as it is running.
202+
203+To enable Manhole, pass the flag: `-m <user_name> <password> <port>` to !EnDroid on startup. Ssh access is then achieved with `ssh <user_name>@localhost -p <port>` and entering the password.
204+
205+Once inside Manhole, the user has access to the active instance of !EnDroid via `droid`. For example:
206+ * `droid.usermanagement._pms` - will return the dictionary of `{<room/group name> : <pluginmanager instance>}`
207+ * `droid.usermanagement._pms['all'].get('endroid.plugins.<your_plugin_name>')` will return the instance of `<your_plugin>` active in the `'all'` usergroup.
208+
209+A user may also define functions, import modules and generally lark around as they would in a regular python prompt. (It is almost certainly worth, for example, writing a short module with some helper
210+functions to reduce the amount of typing required in Manhole).
211+
212+=== A Usage Example ===
213+
214+Start up !EnDroid with manhole enabled:
215+{{{#!highlight python
216+me@localhost:~/endroid/src$ bash endroid.sh -t -m <user>
217+<password> <port>
218+11:06:46 INFO EnDroid starting up with conffile
219+/home/<me>/.endroid/endroid.conf
220+11:06:46 INFO Found JID: <my-endroid-jid>
221+11:06:46 INFO Found Secret: **********
222+...
223+11:06:46 INFO Starting manhole
224+...
225+}}}
226+
227+And in a separate console:
228+{{{#!highlight python
229+me@localhost:~/endroid/src$ ssh <user>@localhost -p <port>
230+admin@localhost's password: <password>
231+>>> # I'm now in a python prompt
232+>>> 1+1
233+2
234+>>> droid
235+<endroid.Endroid object at 0xabd614c>
236+>>> # I fancy sending myself a message
237+>>> droid.messagehandler.send_chat("me@myho.st", "Hi me,
238+all is well in manhole-land!")
239+>>> # I received the chat message.
240+>>> logging.info("Hi console log!")
241+>>> # console log: 11:04:28 INFO Hi console log!
242+>>> # Ctrl-D to exit
243+Connection to localhost closed.
244+me@localhost:~$
245+}}}
246+
247+=== An Import Example ===
248+
249+A useful helper module for debugging plugins.
250+
251+{{{#!highlight python
252+# src/my_helper.py
253+
254+def get_loaded(droid, room_group):
255+ """
256+ Return the _loaded dict of {plugin_name : PluginProxy object}
257+ for room_group.
258+
259+ Note that a PluginProxy object is just a wrapper round a Plugin
260+ object, and all of the plugin's attributes/functions may be accessed
261+ through it.
262+
263+ """
264+ return droid.usermanagement._pms[room_group]._loaded
265+
266+def get_instance(droid, room_group, plugin_name):
267+ """Get the instance of 'plugin_name' active in 'room_group'.
268+
269+ 'plugin_name' may be the full plugin_name e.g. 'endroid.plugins.chuck'
270+ or just the last part e.g. 'chuck'
271+
272+ """
273+ dct = get_loaded(droid, room_group)
274+ if plugin_name in dct:
275+ return dct[plugin_name]
276+ else:
277+ for key, item in dct.items():
278+ if plugin_name == key.split('.')[-1]:
279+ return item
280+ fmt = "Plugin '{}' not active in '{}'"
281+ raise KeyError(fmt.format(plugin_name, room_group))
282+}}}
283+
284+Then checking to see if our plugin's config has been properly loaded from in Manhole is easy:
285+
286+{{{#!highlight python
287+>>> import my_helper
288+>>> my_helper.get_loaded(droid, 'all').keys()
289+['endroid.plugins.unhandled', 'endroid.plugins.patternmatcher',
290+ 'endroid.plugins.httpinterface', 'endroid.plugins.roomowner',
291+ 'endroid.plugins.chuck', 'endroid.plugins.command',
292+ 'endroid.plugins.passthebomb', 'endroid.plugins.invite',
293+ 'endroid.plugins.help', 'endroid.plugins.broadcast']
294+>>> my_helper.get_instance(droid, 'all', 'chuck').vars
295+{'my_list': ['list', 'with', 'numbers', 1, 2, 3],
296+ 'my_list2': ['newlines', 'also', 'mean', 'lists'],
297+ 'my_int': 123, 'my_string': 'this is a string'}
298+>>>
299+}}}
300+
301
302=== added file 'doc/wiki/GettingStarted'
303--- doc/wiki/GettingStarted 1970-01-01 00:00:00 +0000
304+++ doc/wiki/GettingStarted 2013-08-14 10:10:05 +0000
305@@ -0,0 +1,40 @@
306+#acl EnsoftLander:read,write,delete,revert,admin All:read
307+
308+<<TableOfContents>>
309+
310+= Installation =
311+
312+Installing EnDroid is straightforward. Installation packages are provided (currently only for Ubuntu) [[https://launchpad.net/endroid/+download|on Launchpad]]. It is also possible to run it directly
313+by checking out the code using `bzr branch lp:endroid`.
314+
315+ Note: !EnDroid requires python 2.7 or later to run, and depends on Twisted, Wokkel, PyTZ and python-dateutil.
316+
317+In order to run EnDroid needs to load certain settings from its config file. An example one is installed at `/usr/share/doc/endroid/examples/endroid.conf` and it can be found in the source at
318+`etc/endroid.conf`. The settings that must be added to the config file are details of an XMPP account for !EnDroid to use (free accounts can be created easily e.g. from
319+[[http://comm.unicate.me/|comm.unicate.me]]).
320+
321+ Note: '''Do not use your own account''' as !EnDroid will cut down its contact list to users listed in the config file.
322+
323+Other settings that should be set are `users` - the list of users !EnDroid will communicate with - remember to add yourself to this and `rooms` the list of rooms !EnDroid will join. For more details
324+on configuration see [[../Configuration|configuration]].
325+
326+= Running EnDroid =
327+
328+ * If you installed !EnDroid, you should be able to run it by typing `endroid` into the console.
329+ * If you pulled the source, run !EnDroid from within the `src` directory with `./endroid.sh`
330+ * See `-h` for full list of command line arguments, some key ones are:
331+ * `<config-file>` - specify an alternative config file.
332+ * `[-l|--level] <log-level>` - specify the verbosity of the console log (lower is more verbose)
333+ * `[-L|--logfile] <log-file>` - redirect the console logging to a file.
334+ * `[-t|--logtraffic]` - if `-t` is present, `twisted` will log all the raw message xml to the file log.
335+
336+See the [[../Debugging|debugging page]] for more details on logging and `Manhole`.
337+
338+If you encounter any bugs please report them at [[https://bugs.launchpad.net/endroid|EnDroid launchpad]].
339+
340+Happy Droiding!
341+
342+= What Next? =
343+
344+Write some plugins to add some new functionality! See the [[../PluginTutorial|plugin tutorial]] to find out how easy it is.
345+
346
347=== renamed file 'doc/Index' => 'doc/wiki/Index'
348--- doc/Index 2012-08-27 23:19:45 +0000
349+++ doc/wiki/Index 2013-08-14 10:10:05 +0000
350@@ -2,12 +2,15 @@
351
352 = EnDroid =
353
354-EnDroid is a modular XMPP bot platform written in Python and built upon the Twisted framework.
355+!EnDroid is a modular XMPP bot platform written in Python and built upon the Twisted framework.
356
357 == Documentation ==
358
359- * [[EnDroid/Installation|Installation]]
360- * [[EnDroid/Configuration|Configuration]]
361- * [[EnDroid/WritingPlugins|WritingPlugins]]
362+ * [[/GettingStarted|Getting Started]]
363+ * [[/Configuration|Configuration]]
364+ * [[/PluginTutorial|Plugin Tutorial]]
365+ * [[/Reference|API Reference]]
366+ * [[/Debugging|Debugging]]
367
368 <<Include(/BluePrints)>>
369+
370
371=== added file 'doc/wiki/PluginTutorial'
372--- doc/wiki/PluginTutorial 1970-01-01 00:00:00 +0000
373+++ doc/wiki/PluginTutorial 2013-08-14 10:10:05 +0000
374@@ -0,0 +1,790 @@
375+#acl EnsoftLander:read,write,delete,revert,admin All:read
376+
377+<<TableOfContents>>
378+
379+= Introduction =
380+
381+Over the course of this tutorial we will write a plugin for !EnDroid which plays a simple pass-the-bomb game over chat.
382+
383+A user should be able to create a bomb with a specified timer, then throw it to another user. The recipient should then be able to continue the process.
384+
385+When the timer expires the bomb should 'kill' its holder and award a kill to whoever lit the fuse.
386+
387+This plugin is command orientated - that is we will be using it in chat by typing commands to !EnDroid e.g. `bomb 15` to create a bomb with a 15 second timer. This suggests we use the Command plugin
388+which simplifies this type of interaction.
389+
390+Links to the relevant sections of the !Reference page are at the bottom of each section of this tutorial.
391+
392+== Preface: Laying the ground work ==
393+
394+=== Getting EnDroid ===
395+
396+See: [[../GettingStarted#Installation|EnDroid installation]] for instructions on getting and setting up !EnDroid.
397+
398+We will be creating a plugin at `~/.endroid/plugins/passthebomb.py`. To get !EnDroid to load the plugin, make sure you have:
399+
400+{{{
401+[room | group : * ]
402+plugins = passthebomb,
403+ endroid.plugins.command,
404+# all the other plugins you want
405+}}}
406+
407+See [[../Configuration|EnDroid configuration]] for more details on the config file.
408+
409+=== Debugging ===
410+
411+It is fairly probably that at some stage something will go wrong. For information about debugging problems, see: [[../Debugging|EnDroid debugging]].
412+
413+== A Plot: CommandPlugin ==
414+
415+Firstly we will get things set up so that our plugin responds to the keywords 'bomb', 'throw' and 'kills'.
416+
417+{{{#!highlight python
418+from endroid.plugins.command import CommandPlugin
419+
420+class PassTheBomb(CommandPlugin):
421+ # The help attribute will be displayed when 'help passthebomb' is
422+ # run provided that the help plugin is enabled.
423+ help = "Pass the bomb game for EnDroid"
424+
425+ def cmd_bomb(self, msg, arg):
426+ # Create a new bomb.
427+ msg.reply("Made a new bomb with arguments '{}'".format(arg))
428+
429+ def cmd_throw(self, msg, arg):
430+ # Throw the bomb to another person (or ourself if we fancy).
431+ msg.reply("Throwing a bomb with arguments '{}'".format(arg))
432+
433+ def cmd_kills(self, msg, arg):
434+ # How many kills do we have?
435+ msg.reply("Getting killcount with arguments '{}'".format(arg))
436+}}}
437+
438+The inheritance from `CommandPlugin` means that all methods beginning with `cmd_` are automagically registered e.g. `cmd_bomb` will be called when someone types `"bomb and then whatever else they
439+want"`.
440+
441+Every `cmd_` function take two arguments:
442+ * msg - the Message object that triggered the call
443+ * arg - msg.body minus the first word (the name part of the cmd_<name> function)
444+
445+As it stands the plugin will do little except for reply to messages starting with the word 'bomb', 'throw' or 'kills' - but demonstrates simple response to commands.
446+
447+=== Aside: Plugin and CommandPlugin ===
448+
449+The general class that plugins inherit from is, unsurprisingly, `endroid.pluginmanager.Plugin` (either directly or via a subclass such as `CommandPlugin`). The `Plugin` class has access to
450+`MessageHandler` and `UserManagement` objects via the `messagehandler` and `usermanagement` attributes. These objects provide access to functionality concerned with X'''MP'''P's '''m'''essaging and
451+'''p'''resence facilities respectively.
452+
453+Additionally the `Plugin` class has methods by which a plugin can register a function with `messagehandler` such that the function will be called every time a specified type message is received, with
454+the message as its argument. The primary such functions are:
455+ * `register_muc_callback(<callable>)`
456+ * `register_chat_callback(<callable>)`
457+
458+A simple example plugin:
459+{{{#!highlight python
460+from endroid.pluginmanager import Plugin
461+
462+class JFSullivan(Plugin):
463+ def endroid_init(self):
464+ self.register_chat_callback(self.do_harass)
465+ self.register_muc_callback(self.do_harass)
466+
467+ def do_harass(self, msg):
468+ msg.reply("Purchase one of my fine umbrellas!")
469+}}}
470+This plugin registers for both muc and chat callbacks, so it's function is called whenever !EnDroid receives a muc or a chat message.
471+
472+`CommandPlugin` (`endroid.plugins.command.CommandPlugin`) inherits from `Plugin` and provides some helper features. It will automatically register class methods beginning with `cmd_`. It also provides
473+extra options such as synonyms, registering for muc or chat only and providing help messages (via the `help` plugin). For these extras it looks for attributes on methods, e.g.:
474+
475+{{{#!highlight python
476+from endroid.plugins.command import CommandPlugin
477+
478+class Umbrella(CommandPlugin):
479+ def cmd_buy(self, msg, arg):
480+ msg.reply("It will cost one thousand guineas")
481+ cmd_buy.helphint = "When buying an umbrella, I recommend sheet steel"
482+ cmd_buy.muc_only = True # Can only be called from a group chat.
483+ # cmd_buy.chat_only = True # Can only be called from a single-user chat.
484+ # messages starting with buy, purchase or acquire will call this function.
485+ cmd_buy.synonyms = ("purchase", "acquire")
486+}}}
487+
488+ '''Note:''' A function registered with the Plugin class's `register` methods will be called with a __single__ argument - the message that caused it to be called. A `cmd_` function in a
489+`CommandPlugin` derived class will be called with __two__ arguments: the message that caused it to be called, and the message's body stripped of the first word (which, as mentioned above, will have
490+been the name of the function minus the `cmd_`.
491+
492+
493+Full reference for `Plugin`:
494+ * [[../Reference#Plugin|EnDroid reference: plugin]]
495+
496+== Lighting the Fuse: Cron, Messagehandler ==
497+
498+Back to the Bomb plugin. We have got the plugin responding to commands but now we want to actually arm the bomb.
499+
500+`Cron` is !EnDroid's event scheduler. It provides methods to schedule future events, and allows persistency - i.e. events that will be remembered should !EnDroid undergo a restart. We will use it
501+explode our bombs at suitable (or unsuitable, depending on who is holding the bomb) times. The `Cron` singleton object is accessible via `self.cron`.
502+
503+The scheduling of functions with `Cron` is a two stage process. Firstly we register a callable with `Cron` against a registration string: `register(callable, registration_name)`. Once this is done, we
504+can use either `setTimeout(time, registration_name, parameters)` or `doAtTime(time, locality, registration_name, parameters)` to tell `Cron` to call the function at some point in the future
505+(localities are as used by `pytz.timezone`).
506+
507+ '''Note:''' Registration names must be __globally__ unique (across all plugins), this is the purpose of the `get_id` function in the code below.
508+
509+{{{#!highlight python
510+from endroid.cron import Cron
511+
512+class Bomb(object):
513+ ID = 0
514+ def __init__(self, source, fuse, plugin):
515+ self.source = source # The user who lit the fuse.
516+ self.user = source # Our current bearer.
517+ self.plugin = plugin # Plugin instance we belong to.
518+ self.history = set() # All our bearers.
519+
520+ idstring = self.get_id() # Get a unique registration_name.
521+ plugin.cron.register(self.explode, idstring)
522+ # Schedule detonation with parameters = None.
523+ plugin.cron.setTimeout(fuse, idstring, None)
524+
525+ # This function is called by Cron and given an argument. We don't need an
526+ # argument so just ignore it.
527+ def explode(self, _):
528+ # Last argument of send_chat is the new message's source which defaults
529+ # to None and is non-essential (it is used by the Message.reply methods
530+ # and is visible to filters but neither of these are important to us).
531+ self.plugin.messagehandler.send_chat(self.user, "BOOM!", None)
532+
533+ @classmethod
534+ def get_id(cls):
535+ # Generate a unique id string to register our explode method against.
536+ result = Bomb.ID
537+ cls.ID += 1
538+ return "bomb" + str(result)
539+
540+
541+class PassTheBomb(CommandPlugin):
542+ ...
543+ def cmd_bomb(self, msg, arg):
544+ holder = msg.sender
545+ try:
546+ # Read a time from the first word of arg.
547+ time = float(arg.split(' ', 1)[0])
548+ # A new Bomb with the message sender as the source.
549+ Bomb(msg.sender, time, self)
550+ msg.reply("Sniggering evilly, you light the fuse...")
551+ # Provision for a failure to read a time float.
552+ except ValueError:
553+ msg.reply("You struggle with the matches")
554+}}}
555+
556+Now we can type `'bomb 5'` into chat and 5 seconds later (or so) it will explode.
557+
558+We have used the `messagehandler.send_chat` function here as the `Bomb` object has no access to a `Message` object to reply to. We will see the `messagehandler.send_muc` function shortly. The
559+`MessageHandler` object is an abstraction of X'''M'''PPs '''messaging''' protocols as well as providing methods to register message callbacks (in our example these are hidden behind `CommandPlugin`)
560+and to send messages in single and multi user chats.
561+
562+Full reference for `Cron`:
563+ * [[../Reference#Cron|EnDroid reference: Cron]]
564+Full reference for `MessageHandler`:
565+ * [[../Reference#MessageHandler.2C_Message|EnDroid reference: MessageHandler]]
566+
567+== Throwing the Bomb: Usermanagement ==
568+
569+Now we will add the (important) ability to throw the bomb to another user. We will use the `UserManagement` object to check if our target is online.
570+
571+{{{#!highlight python
572+from collections import defaultdict
573+
574+class Bomb(object):
575+ ...
576+ def __init__(self, source, fuse, plugin):
577+ ... # As before.
578+ # Updating the user will now be controlled by our throw method
579+ self.user = None
580+ ...
581+
582+ def explode(self, _):
583+ self.plugin.messagehandler.send_chat(self.user, "BOOM!", self.plugin)
584+ self.plugin.bombs[self.user].discard(self) # This bomb is done
585+
586+ def throw(self, next_user):
587+ # Remove this bomb from our current user.
588+ self.plugin.bombs[self.user].discard(self)
589+
590+ self.history.add(next_user)
591+ self.user = next_user
592+
593+ # Add it to the new user.
594+ self.plugin.bombs[self.user].add(self)
595+
596+
597+class PassTheBomb(CommandPlugin):
598+ help = ...
599+ bombs = defaultdict(set) # A dictionary of which users have bombs
600+
601+ ...
602+
603+ def cmd_bomb(self, msg, arg):
604+ holder = msg.sender
605+ try:
606+ # Read a time from the first word of arg.
607+ time = float(arg.split(' ', 1)[0])
608+ # A new Bomb with the message sender as the source, passed to
609+ # msg.sender.
610+ Bomb(msg.sender, time, self).throw(msg.sender)
611+ ...
612+
613+ def cmd_throw(self, msg, arg):
614+ target = arg.split(' ')[0]
615+ # Check if the target is online.
616+ if not target in self.usermanagement.get_available_users():
617+ msg.reply("You look around but can't spot your target")
618+ # Check if we actually have a bomb.
619+ elif not self.bombs[msg.sender]:
620+ msg.reply("You idly throw your hat, wishing you had something "
621+ "more 'splodey")
622+ else:
623+ self.bombs[msg.sender].pop().throw(target)
624+ msg.reply("You throw the bomb!")
625+ # Let the target know we've thrown the bomb at him!
626+ self.messagehandler.send_chat(target, "A bomb lands by your feet!")
627+}}}
628+
629+
630+`Usermanagement` is an abstraction of the XM'''P'''Ps '''presence''' protocols. It provides a number of user management functions including:
631+ * `get_[registered|available]_users(name=None)` - returns an iterable of users registered with/available in the room/group name, or in !EnDroid's contact list if name is None (in the code above we
632+ * have used the `get_available_users(None)` to find the users online in our contact list)
633+ * `get_[?available]_[rooms|groups](user)` - returns an iterable of rooms/groups the specified user is available/registered in.
634+
635+Now let us add some more interesting explosion reporting, utilising the second of the two above methods.
636+
637+{{{#!highlight python
638+class Bomb(object):
639+ ...
640+ def explode(self, _):
641+ # Some shorthands.
642+ get_rooms = self.plugin.usermanagement.get_available_rooms
643+ send_muc = self.plugin.messagehandler.send_muc
644+ send_chat = self.plugin.messagehandler.send_chat
645+
646+ msg_explode = "!!!BOOM!!!"
647+ msg_farexplode = "You hear a distant boom"
648+ msg_kill = "{} was got by the bomb"
649+
650+ rooms = get_rooms(self.user)
651+ for room in rooms:
652+ # Let everyone in a room with self.user hear the explosion.
653+ send_muc(room, msg_explode, self.plugin)
654+ send_muc(room, msg_kill.format(self.user), self.plugin)
655+
656+ # Alert those who passed the bomb that it has exploded.
657+ for user in self.history:
658+ if user == self.user:
659+ send_chat(self.user, msg_explode, self.plugin)
660+ send_chat(self.user, msg_kill.format("You"), self.plugin)
661+ else:
662+ send_chat(user, msg_farexplode, self.plugin)
663+ send_chat(user, msg_kill.format(self.user), self.plugin)
664+
665+ self.plugin.bombs[self.user].discard(self)
666+}}}
667+
668+We broadcast the explode message in any room the victim is present in (`get_rooms(self.user)`) using `messagehandler`'s send_muc method and alert other users in the bomb's history. We also send a kill
669+notice to all users in the history (note that any grammatical imperfections are features, not bugs).
670+
671+Full reference for `UserManagement`:
672+ * [[../Reference#UserManagement|EnDroid reference: UserManagement]]
673+
674+=== Aside: Plugin Scoping ===
675+
676+Note that we have used a class variable to store the dictionary of bombs.
677+
678+In !EnDroid, a seperate instance of each plugin is instantiated in each room/for each usergroup. This means that if plugins in seperate environments want to share information, they must use class
679+variables.
680+
681+== A Criminal Record: Database ==
682+
683+`Database` is the second main utility class !EnDroid provides. It wraps an SQL database. We will use it to store kill-counts.
684+
685+{{{#!highlight python
686+from endroid.database import Database
687+
688+DB_NAME = "PTB"
689+DB_TABLE = "PTB"
690+
691+class Bomb(object):
692+ ...
693+ def explode(self, _):
694+ ...
695+ self.plugin.register_kill(self.source) # Register the kill in the database.
696+ self.plugin.bombs[self.user].discard(self)
697+
698+class PassTheBomb(CommandPlugin):
699+ ...
700+ def endroid_init(self):
701+ # Note that this will either create a new database if none with name
702+ # DB_NAME exists, or open an existing one.
703+ self.db = Database(DB_NAME)
704+ # If we haven't already setup the database then do so.
705+ if not self.db.table_exists(DB_TABLE):
706+ # Create a table with fields 'user' and 'kills'.
707+ self.db.create_table(DB_TABLE, ("user", "kills"))
708+
709+ def cmd_kills(self, msg, arg):
710+ # Retrieve the users kill count.
711+ kills = self.get_kills(msg.sender)
712+ # self.place_name is the address of the room or name of the group this
713+ # plugin is active in.
714+ nick = self.usermanagement.get_nickname(msg.sender, self.place_name)
715+ level = self.get_level(kills)
716+
717+ text = "{} the {} has {} kill".format(nick, level, kills)
718+ text += ("" if kills == 1 else "s")
719+ msg.reply(text)
720+
721+ def register_kill(self, user):
722+ kills = self.get_kills(user)
723+ if kills:
724+ # Change the value of 'kills' to kills+1 in table rows where the
725+ # field 'user' has value user.
726+ r = self.db.update(DB_TABLE, {'kills': kills+1}, {'user': user})
727+ assert r == 1 # Exactly 1 row should be updated
728+ else:
729+ # The user is not registered in the database - so create a new
730+ # registration for them with their first kill stored.
731+ self.db.insert(DB_TABLE, {'user': user, 'kills': 1})
732+
733+ def get_kills(self, user):
734+ # Look in DB_TABLE for the 'kills' field in entries whose 'user' field
735+ # is user.
736+ # Returns a list of dictionaries, each with keys: _endroid_unique_id
737+ # (always included but which we can ignore) and 'kills' - which we want.
738+ results = self.db.fetch(DB_TABLE, ['kills'], {'user': user})
739+ if len(results) == 0: # The user is not registered in the database.
740+ return 0
741+ else:
742+ # Get the first dictionary in results and extract the value of
743+ # 'kills' from it.
744+ return results[0]['kills']
745+
746+ @staticmethod
747+ def get_level(kills):
748+ if kills < 5:
749+ level = 'novice'
750+ elif kills < 15:
751+ level = 'apprentice'
752+ elif kills < 35:
753+ level = 'journeyman'
754+ elif kills < 65:
755+ level = 'expert'
756+ elif kills < 100:
757+ level = 'master'
758+ else:
759+ level = 'grand-master'
760+ return level
761+}}}
762+
763+We have used the `endroid_init` method, which is called by !EnDroid's plugin management system when a plugin is loaded. Here we use it to set up a database.
764+
765+The use of the database is fairly straightforward, in general methods take parameters table_name, options_list_or_dict and conditions_dictionary. Their parameters are internally converted into strings
766+and passed to an SQL query which does the gruntwork.
767+
768+So far, we have a fully functional plugin which does everything we set out to do. However there are still a couple of things to be done.
769+
770+Full reference for `Database`:
771+ * [[../Reference#Database|EnDroid reference: Database]]
772+
773+== Umbrellas: For when it rains (bombs) ==
774+
775+There is a problem with the plugin as it stands. If a user does not want to take part in the game (however inconceivable this may seem...) he/she has no options.
776+
777+We will remedy these problems by giving all users patented JF Sullivan umbrellas, which they can unfurl to protect themselves and furl to join in the game. By default a user's umbrella is unfurled
778+(i.e. we operate on an opt-in policy).
779+
780+When an umbrella is unfurled, the user should not be able to light a new bomb, nor throw an existing bomb, nor have a bomb thrown at them.
781+
782+Most of the changes made to the code to introduce this feature are just Python, so will not be explained in any detail.
783+
784+{{{#!highlight python
785+from collections import namedtuple
786+
787+class User(object):
788+ # A class to represent a player of the game.
789+ __slots__ = ('name', 'kills', 'shield')
790+ def __init__(self, name, kills=0, shield=True):
791+ self.name = name
792+ self.kills = kills
793+ self.shield = shield
794+
795+ def __repr__(self):
796+ return "User(name={}, kills={}, shield={})".format(self.name,
797+ self.kills,
798+ self.shield)
799+
800+...
801+
802+class PassTheBomb(CommandPlugin):
803+ ...
804+ users = dict() # A dictionary of registered game players.
805+
806+ def endroid_init(self):
807+ ...
808+ self.db.create_table(DB_TABLE, ('user', 'kills'))
809+ else:
810+ # Make a local copy of the registration database.
811+ data = self.db.fetch(DB_TABLE, ['user', 'kills'])
812+ # Data is a list of dictionaries, each one representing a row in
813+ # the database.
814+ for dct in data:
815+ self.users[dct['user']] = User(dct['user'], dct['kills'])
816+
817+ def cmd_furl_umbrella(self, msg, arg):
818+ """
819+ This is how a user enters the game - allows them to be targeted
820+ and to create and throw bombs.
821+
822+ """
823+ user = msg.sender
824+ if not self.get_shielded(user):
825+ msg.reply("Your umbrella is already furled!")
826+ else:
827+ if self.get_registered(user):
828+ self.users[user].shield = False
829+ else: # They are not - register them.
830+ self.db.insert(DB_TABLE, {'user': user, 'kills': 0})
831+ self.users[user] = User(user, kills=0, shield=False)
832+ msg.reply("You furl your umbrella!")
833+
834+ def cmd_unfurl_umbrella(self, msg, arg):
835+ """A user with an unfurled umbrella cannot create or receive bombs"""
836+ user = msg.sender
837+ if self.get_shielded(user):
838+ msg.reply("Your umbrella is already unfurled!")
839+ else:
840+ # To get user must not have been shielded ie they must have furled
841+ # so they will be in the database.
842+ self.users[user].shield = True
843+ msg.reply("You unfurl your umbrella! No bomb can reach you now!")
844+
845+
846+ def cmd_bomb(self, msg, arg):
847+ """
848+ Create a bomb with a specified timer, eg: 'bomb 1.5' for a 1.5 second
849+ fuse.
850+
851+ """
852+
853+ holder = msg.sender
854+ if self.get_shielded(holder):
855+ return msg.reply("Your sense of honour insists that you furl your "
856+ "umbrella before lighting the fuse")
857+ # Otherwise get a time from the first word of arg.
858+ ...
859+
860+ def cmd_throw(self, msg, arg):
861+ """Throw a bomb to a user, eg: 'throw benh@ensoft.co.uk'."""
862+ target = arg.split(' ')[0]
863+ if not self.bombs[msg.sender]: # Do we even have a bomb?
864+ msg.reply("You idly throw your hat, wishing you had something more"
865+ "'splodey")
866+ elif self.get_shielded(msg.sender): # Must be vulnerable while throwing.
867+ msg.reply("You notice that your unfurled umbrella would hinder "
868+ "your throw.")
869+ elif not target in self.usermanagement. ...
870+ elif self.get_shielded(target): # Target registered/vulnerable?
871+ msg.reply("You see your target hunkered down under their umbrella. "
872+ "No doubt a bomb would have no effect on that "
873+ "monstrosity.")
874+ else: ...
875+
876+ ...
877+
878+ def register_kill(self, user):
879+ ...
880+ self.users[user].kills += 1 # update our local copy
881+
882+ # Use our local copy of database information to minimise database access.
883+ def get_kills(self, user):
884+ return self.users[user].kills if user in self.users else 0
885+
886+ def get_shielded(self, user):
887+ return self.users[user].shield if user in self.users else True
888+
889+ def get_registered(self, user):
890+ return user in self.users
891+
892+ @staticmethod
893+ def get_level(kills):
894+ ...
895+}}}
896+
897+There are a few things here to note.
898+
899+First and foremost: database calls are '''synchronous''' i.e. !EnDroid will grind to a halt (briefly) each time the database is accessed. In the above code, we have kept only 'kills' recorded in the
900+database (we could have kept 'shield' there too but that would mean many more read/writes). As a result things will be faster but if !EnDroid restarts for any reason, all users umbrellas will become
901+unfurled (but this is not a particular problem).
902+
903+Secondly - it is generally a good idea to design plugins to be at least somewhat '''spam-resistent'''. In this case we have used an opt-in policy (an !EnDroid user will know nothing of the PTB plugin
904+until he/she furls his/her umbrella) and have provided an option to silence the plugin (by unfurling it again).
905+
906+Thirdly we see some more of the functionality of !CommandPlugin. The functions `cmd_[?un]furl_umbrella` will respond to messages: "[?un]furl umbrella" i.e. require two words to activate. This idea
907+extends indefinitely, it would be completely possible to create the command:
908+`cmd_go_and_buy_a_new_umbrella_mine_is_somewhat_battlescarred`
909+which would respond to the message "go and buy ...".
910+
911+= The Full Code =
912+
913+A few minor changes have been made below - mostly to do with commenting and report strings.
914+
915+{{{#!highlight python
916+from endroid.plugins.command import CommandPlugin
917+from endroid.cron import Cron
918+from collections import defaultdict, namedtuple
919+from endroid.database import Database
920+
921+DB_NAME = "PTB"
922+DB_TABLE = "PTB"
923+
924+class User(object):
925+ __slots__ = ('name', 'kills', 'shield')
926+ def __init__(self, name, kills=0, shield=True):
927+ self.name = name
928+ self.kills = kills
929+ self.shield = shield
930+
931+ def __repr__(self):
932+ return "User(name={}, kills={}, shield={})".format(self.name, self.kills, self.shield)
933+
934+class Bomb(object):
935+ ID = 0
936+ def __init__(self, source, fuse, plugin):
937+ self.source = source # Who lit the bomb?
938+ self.user = None # Our current holder.
939+ self.plugin = plugin # Plugin instance we belong to.
940+ self.history = set() # Who has held us?
941+
942+ idstring = self.get_id() # Get a unique registration_name.
943+ plugin.cron.register(self.explode, idstring)
944+ plugin.cron.setTimeout(fuse, idstring, None) # Schedule detonation.
945+
946+ # This function is called by Cron and given an argument. We don't need an
947+ # argument so just ignore it.
948+ def explode(self, _):
949+ # Some shorthands.
950+ get_rooms = self.plugin.usermanagement.get_available_rooms
951+ send_muc = self.plugin.messagehandler.send_muc
952+ send_chat = self.plugin.messagehandler.send_chat
953+
954+ msg_explode = "!!!BOOM!!!"
955+ msg_farexplode = "You hear a distant boom"
956+ msg_kill = "{} was got by the bomb"
957+
958+ rooms = get_rooms(self.user)
959+ for room in rooms:
960+ # Let everyone in a room with self.user here the explosion.
961+ send_muc(room, msg_explode)
962+ send_muc(room, msg_kill.format(self.user))
963+
964+ # Alert those who passed the bomb that it has exploded.
965+ for user in self.history:
966+ if user == self.user:
967+ send_chat(self.user, msg_explode)
968+ send_chat(self.user, msg_kill.format("You"))
969+ else:
970+ send_chat(user, msg_farexplode)
971+ send_chat(user, msg_kill.format(self.user))
972+
973+ self.plugin.register_kill(self.source)
974+ self.plugin.bombs[self.user].discard(self)
975+
976+
977+ def throw(self, user):
978+ # Remove this bomb from our current user.
979+ self.plugin.bombs[self.user].discard(self)
980+
981+ self.history.add(user)
982+ self.user = user
983+
984+ # Add it to the new user.
985+ self.plugin.bombs[self.user].add(self)
986+
987+ @classmethod
988+ def get_id(cls):
989+ # Generate a unique id string to register our explode method against.
990+ result = Bomb.ID
991+ cls.ID += 1
992+ return "bomb" + str(result)
993+
994+
995+class PassTheBomb(CommandPlugin):
996+ help = "Pass the bomb game for EnDroid"
997+ bombs = defaultdict(set) # Users : set of bombs.
998+ users = dict() # User strings : User objects.
999+
1000+ def endroid_init(self):
1001+ self.db = Database(DB_NAME)
1002+ if not self.db.table_exists(DB_TABLE):
1003+ self.db.create_table(DB_TABLE, ('user', 'kills'))
1004+ else:
1005+ # Make a local copy of the registration database.
1006+ data = self.db.fetch(DB_TABLE, ['user', 'kills'])
1007+ for dct in data:
1008+ self.users[dct['user']] = User(dct['user'], dct['kills'])
1009+
1010+ def cmd_furl_umbrella(self, msg, arg):
1011+ """
1012+ This is how a user enters the game - allows them to be targeted
1013+ and to create and throw bombs.
1014+
1015+ """
1016+ user = msg.sender
1017+ if not self.get_shielded(user):
1018+ msg.reply("Your umbrella is already furled!")
1019+ else:
1020+ if self.get_registered(user):
1021+ self.users[user].shield = False
1022+ else: # They are not - register them.
1023+ self.db.insert(DB_TABLE, {'user': user, 'kills': 0})
1024+ self.users[user] = User(user, kills=0, shield=False)
1025+ msg.reply("You furl your umbrella!")
1026+ cmd_furl_umbrella.helphint = ("Furl your umbrella to participate in the "
1027+ "noble game of pass the bomb!")
1028+
1029+ def cmd_unfurl_umbrella(self, msg, arg):
1030+ """A user with an unfurled umbrella cannot create or receive bombs."""
1031+ user = msg.sender
1032+ if self.get_shielded(user):
1033+ msg.reply("Your umbrella is already unfurled!")
1034+ else:
1035+ # To get user must not have been shielded ie they must have furled
1036+ # so they will be in the database.
1037+ self.users[user].shield = True
1038+ msg.reply("You unfurl your umbrella! No bomb can reach you now!")
1039+ cmd_unfurl_umbrella.helphint = ("Unfurl your umbrella to cower from the "
1040+ "rain of boms!")
1041+
1042+ def cmd_bomb(self, msg, arg):
1043+ """Create a bomb with a specified timer."""
1044+
1045+ holder = msg.sender
1046+ if self.get_shielded(holder):
1047+ return msg.reply("Your sense of honour insists that you furl your "
1048+ "umbrella before lighting the fuse")
1049+ # Otherwise get a time from the first word of arg.
1050+ try:
1051+ time = float(arg.split(' ', 1)[0])
1052+ # Make a new bomb and throw it to its creator.
1053+ Bomb(msg.sender, time, self).throw(msg.sender)
1054+ msg.reply("Sniggering evilly, you light the fuse...")
1055+ # Provision for a failure to read a time float...
1056+ except ValueError:
1057+ msg.reply("You struggle with the matches")
1058+ cmd_bomb.helphint = ("Light the fuse!")
1059+
1060+ def cmd_throw(self, msg, arg):
1061+ """Throw a bomb to a user, eg: 'throw benh@ensoft.co.uk'"""
1062+ target = arg.split(' ')[0]
1063+ # We need a bomb to throw.
1064+ if not self.bombs[msg.sender]:
1065+ msg.reply("You idly throw your hat, wishing you had something "
1066+ "rounder, heavier and with more smoking fuses.")
1067+ # Need our umbrella to be furled.
1068+ elif self.get_shielded(msg.sender):
1069+ msg.reply("You notice that your unfurled umbrella would hinder "
1070+ "your throw.")
1071+ # Check that target is online.
1072+ elif not target in self.usermanagement.get_available_users():
1073+ msg.reply("You look around but cannot spot your target")
1074+ elif self.get_shielded(target): # Target registered/vulnerable?
1075+ msg.reply("You see your target hunkered down under their umbrella. "
1076+ "No doubt a bomb would have little effect on that "
1077+ "monstrosity.")
1078+ else:
1079+ self.bombs[msg.sender].pop().throw(target)
1080+ msg.reply("You throw the bomb!")
1081+ self.messagehandler.send_chat(target, "A bomb lands by your feet!")
1082+ cmd_throw.helphint = ("Throw a bomb!")
1083+
1084+ def cmd_kills(self, msg, arg):
1085+ kills = self.get_kills(msg.sender)
1086+ nick = self.usermanagement.get_nickname(msg.sender,
1087+ self.place_name)
1088+ level = self.get_level(kills)
1089+
1090+ text = "{} the {} has {} kill".format(nick, level, kills)
1091+ text += ("" if kills == 1 else "s")
1092+ msg.reply(text)
1093+ cmd_kills.helphint = ("Receive and gloat over you score!")
1094+
1095+ def register_kill(self, user):
1096+ kills = self.get_kills(user)
1097+ # Change the value of 'kills' to kills+1 in the row where 'user' = user.
1098+ self.users[user].kills += 1
1099+ self.db.update(DB_TABLE, {'kills': kills+1}, {'user': user})
1100+
1101+ def get_kills(self, user):
1102+ return self.users[user].kills if user in self.users else 0
1103+
1104+ def get_shielded(self, user):
1105+ return self.users[user].shield if user in self.users else True
1106+
1107+ def get_registered(self, user):
1108+ return user in self.users
1109+
1110+ @staticmethod
1111+ def get_level(kills):
1112+ if kills < 5:
1113+ level = 'novice'
1114+ elif kills < 15:
1115+ level = 'apprentice'
1116+ elif kills < 35:
1117+ level = 'journeyman'
1118+ elif kills < 65:
1119+ level = 'expert'
1120+ elif kills < 100:
1121+ level = 'master'
1122+ else:
1123+ level = 'grand-master'
1124+ return level
1125+
1126+}}}
1127+
1128+= Extras =
1129+
1130+== Adding Configuration ==
1131+
1132+We can add configuration to our plugin by adding a section in `endroid.conf`:
1133+
1134+{{{
1135+[room | group : * : pconfig : endroid.plugins.passthebomb]
1136+my_string = this is a string
1137+my_int = 123
1138+my_list = this, has, commas, so, is, a, list
1139+my_list2 = 1
1140+ 2
1141+ 3
1142+# This will be ignored as long as # is in the first column.
1143+ 4
1144+ 5
1145+}}}
1146+
1147+These variables will now be available in the `self.vars` dictionary, which will look like this:
1148+
1149+{{{#!highlight python
1150+self.vars = {
1151+ 'my_string' : 'this is a string',
1152+ 'my_int' : 123,
1153+ 'my_list': ['this', 'has', 'commas', 'so', 'is', 'a', 'list']
1154+ 'my_list2': [1,2,3,4,5]
1155+}
1156+}}}
1157+
1158+See [[../Configuration|EnDroid configuration]] for more details on the format of the config file.
1159+
1160+= Further Reading =
1161+
1162+ * [[../Reference|EnDroid reference]]
1163+ * [[http://www.bartitsu.org/index.php/2009/05/the-umbrella-a-misunderstood-weapon/|JFSullivan]]
1164+
1165
1166=== added file 'doc/wiki/Reference'
1167--- doc/wiki/Reference 1970-01-01 00:00:00 +0000
1168+++ doc/wiki/Reference 2013-08-14 10:10:05 +0000
1169@@ -0,0 +1,557 @@
1170+#acl EnsoftLander:read,write,delete,revert,admin All:read
1171+
1172+<<TableOfContents>>
1173+
1174+This API reference will be split into three main parts.
1175+ Endroid core:: APIs for the Plugin class and classes directly accessible from that class. All plugins will be able to access all of this API with no extra work.
1176+ Utilities:: APIs provided by the Endroid modules Database (for persistent data storage) and Cron (for task scheduling) which plugins may import.
1177+ Helper Plugins:: !EnDroid plugins which are designed to be inherited from (e.g. {{{CommandPlugin}}} or 'imported' via {{{Plugin.get("endroid.plugins.<name>")}}} and provide useful extra
1178+functionality.
1179+
1180+= Glossary =
1181+ muc/chat:: A muc (Multi-User Chat) message is one sent in or to a room (group chat). A chat message is one sent to a single user.
1182+ jid:: The address of a user or room. This is a different format depending on the entity being addressed and plugins shouldn't need to know the format being used. However in case its useful it will
1183+be: {{{user@ho.s.t}}} for a single user, {{{room_name@serv.er}}} for a room and {{{room_name@serv.er/user_nickname}}} for a user in a room.
1184+
1185+= Core Endroid =
1186+
1187+API reference for the Plugin class and classes directly available to it. All plugins are expected to inherit from either {{{endroid.pluginmanager.Plugin}}} or a subclass of this class e.g.
1188+{{{endroid.plugins.CommandPlugin}}}.
1189+
1190+== Plugin ==
1191+
1192+{{{#!highlight python
1193+class Plugin(object):
1194+ """
1195+ All plugins are expected to subclass plugin in one way or another.
1196+
1197+ Attributes:
1198+ - usermanagment
1199+ - messagehandler
1200+ References to Endroid's usermanagment and messagehandler objects
1201+ through which further APIs may be called.
1202+ - cron
1203+ A reference to the Cron singleton through which task scheduling is available.
1204+
1205+ - dependencies
1206+ Iterable of plugins this plugin requires to be loaded before it can be
1207+ activated by EnDroid.
1208+
1209+ - preferences
1210+ Iterable of plugins this plugin will use if they are available but does
1211+ not require.
1212+
1213+ - place
1214+ The environment the plugin is active in - either 'room' (muc) or 'group'
1215+ (single user chat - active in a user group).
1216+ - place_name
1217+ The name of the environment the plugin is active in - either a room's
1218+ address (room@serv.er) or the name of a user group.
1219+
1220+ - vars
1221+ A dictionary of config options read from the section in EnDroid's
1222+ config file:
1223+ [ self.place_name : self.place : pconfig : <plugin_name> ]
1224+
1225+ """
1226+
1227+ def register_muc_callback(self, callback, inc_self=False, priority=PRIORITY_NORMAL):
1228+ def register_chat_callback(self, callback, inc_self=False, priority=PRIORITY_NORMAL):
1229+ """
1230+ Register a callback to be called when a muc or chat message is received.
1231+
1232+ - inc_self tells EnDroid whether it should do the callback on messages
1233+ that EnDroid has sent itself.
1234+ - priority is unused by Endroid but accessible to plugins and measures the
1235+ importance of a message (lower numbers = more important).
1236+
1237+ """
1238+
1239+ def register_unhandled_muc_callback(self, callback, inc_self=False,
1240+ priority=PRIORITY_NORMAL):
1241+ def register_unhandled_chat_callback(self, callback, inc_self=False,
1242+ priority=PRIORITY_NORMAL):
1243+ """
1244+ Unhandled callbacks are called by EnDroid when a message has not been
1245+ processed by any callbacks.
1246+
1247+ They called by a plugin via Message.unhandled(*args) (usually when the
1248+ plugin has tried to run its own callback but failed) and so the message has
1249+ not been handled.
1250+
1251+ """
1252+
1253+ # The following four methods register filter functions which take Message
1254+ # objects and return bools.
1255+ # Callable should take a message and return a bool.
1256+ # If a filter returns False, the message will be dropped. A filter
1257+ # may modify a Message object's attributes.
1258+ # Callable may alter the message by changing its attributes.
1259+
1260+ def register_muc_filter(self, callback, inc_self=False, priority=PRIORITY_NORMAL):
1261+ def register_chat_filter(self, callback, inc_self=False, priority=PRIORITY_NORMAL):
1262+ """Receive filters (can cause EnDroid to ignore the message)."""
1263+
1264+ def register_muc_send_filter(self, callback, inc_self=False, priority=PRIORITY_NORMAL):
1265+ def register_chat_send_filter(self, callback, inc_self=False, priority=PRIORITY_NORMAL):
1266+ """Send filters (can cause EnDroid to not send a message)."""
1267+
1268+ def get(self, plugin_name):
1269+ """
1270+ Load plugin called plugin_name (eg endroid.plugins.my_plugin).
1271+
1272+ This acts very much like import; it returns an object through which
1273+ plugin_name's functions can be accessed.
1274+ Note that if get is used, plugin_name should be added to the plugin
1275+ class's dependencies tuple.
1276+
1277+ """
1278+
1279+ def get_dependencies(self):
1280+ def get_preferences(self):
1281+ """
1282+ Return a full list of the plugin's dependencies/preferences.
1283+
1284+ This includes those of the plugins it depends on/prefers and so on
1285+ down the chain.
1286+
1287+ """
1288+
1289+ def list_plugins(self):
1290+ """
1291+ Get an iterable of all the plugins active in this plugin's environment
1292+ (a specific room or usergroup - not globally)."""
1293+
1294+}}}
1295+
1296+=== Plugin Scope ===
1297+
1298+Plugins are separately instantiated in each room they are configured in and for each user group. Bear in mind that if a plugin wishes to store data globally (across all rooms and user groups), a class
1299+variable should be used.
1300+
1301+Plugins can find out information about their environment via {{{self.place}}} and {{{self.place_name}}} which return the type of environment ({{{"room"}}} or {{{"group"}}}) and the name of the
1302+environment (e.g. {{{"room1@serv.er"}}} or {{{"admins"}}}) respectively.
1303+
1304+== MessageHandler, Message ==
1305+
1306+APIs for !EnDroid's message functionality.
1307+
1308+{{{#!highlight python
1309+class MessageHandler(object):
1310+ """A class abstracting XMPP's message protocols."""
1311+
1312+ def send_muc(self, room, body, source=None, priority=PRIORITY_NORMAL):
1313+ """
1314+ Send a multi-user-chat message to the room.
1315+
1316+ Source is optional. It is unused by EnDroid but visible to plugins
1317+ and filters (eg a filter may block all messages with a specified source)
1318+ Priority is not used internally but is available to plugins.
1319+ PRIORITY_NORMAL = 0, the lower the number, the higher the priority.
1320+
1321+ """
1322+
1323+ def send_chat(self, user, body, source=None, priority=PRIORITY_NORMAL):
1324+ """
1325+ Send a single-user-chat message to the user with address user.
1326+
1327+ Other arguments are the same as send_muc.
1328+
1329+ """
1330+
1331+class Message(object):
1332+ """
1333+ Object representing a single message (muc or chat).
1334+
1335+ A Message object is passed by MessageHandler to any callbacks registered
1336+ via the Plugin.register_ commands.
1337+
1338+ Attributes:
1339+ - sender - a string representing the sender's userhost.
1340+ - sender_full - a string representing the sender's full jid.
1341+ Note: sender_full is used to reply to messages so that if a user is
1342+ online on more than one resource the reply will go to the right one.
1343+
1344+ - body - the text of the message.
1345+ - recipient - a string representing the address to send the message to
1346+ - priority - a number, lower = more important (unused by EnDroid, can
1347+ be accessed by plugins).
1348+
1349+ """
1350+
1351+ def reply(self, body):
1352+ """
1353+ Reply with a single or multi-user-chat message to self.sender.
1354+
1355+ The reply will have the same type as the received message, so replying
1356+ to a message in a room will reply to the whole room.
1357+
1358+ """
1359+
1360+ def reply_to_sender(self, body):
1361+ """
1362+ Reply with a single user chat to self.sender.
1363+
1364+ Note the difference from reply - calling reply on a group message will
1365+ send a group message, calling reply_to_sender on the same message will
1366+ send a single user chat message to its sender.
1367+
1368+ """
1369+
1370+ def send(self):
1371+ """Send the Message to it's recipient."""
1372+
1373+ def unhandled(self, *args):
1374+ """
1375+ Notify the message that the caller hasn't handled it. This should only
1376+ be called by plugins that have registered as a handler (and thus have
1377+ incremented the handler count for this message).
1378+
1379+ This method takes arbitrary arguments so it can be used as deferred
1380+ callback or errback.
1381+
1382+ """
1383+}}}
1384+
1385+== UserManagement ==
1386+
1387+Note: all the get_ methods take a name string which is one of:
1388+ * The jid of a room
1389+ * The name of a usergroup
1390+ * None (in which case results will be looked for in !EnDroid's contact list)
1391+
1392+{{{#!highlight python
1393+class UserManagement(object):
1394+ """
1395+ A class abstracting XMPP's presence protocols.
1396+
1397+ In the get_ member functions, name (a userhost string e.g. user@ho.st or
1398+ room@serv.er) specifies in which room or group to run a search. If it is
1399+ None, the search will be run on EnDroid's contact list (of rooms or users
1400+ depending on the function).
1401+
1402+ """
1403+
1404+ def get_users(self, name=None):
1405+ """Return an iterable of users registered with room/group name."""
1406+
1407+ def get_available_users(self, name=None):
1408+ """Return an iterable of users available in room/group name."""
1409+
1410+ def get_groups(self, user=None):
1411+ """Return an iterable of groups the user is registered with."""
1412+
1413+ def get_available_groups(self, user=None):
1414+ """Return an iterable of groups the user is present in."""
1415+
1416+ def get_rooms(self, user=None):
1417+ """Return an iterable of rooms the user is registered with."""
1418+
1419+ def get_available_rooms(self, user=None):
1420+ """Return an iterable of rooms the user is present in."""
1421+
1422+ def get_nickname(self, user, place=None):
1423+ """
1424+ Given a user jid (user@ho.s.t) return the user's nickname in place,
1425+ or if place is None (default), the user part of the jid.
1426+
1427+ """
1428+
1429+ def invite(self, user, room, reason=None):
1430+ """
1431+ Invite a user to a room.
1432+
1433+ Will only send invitation if the user is in our contact list and online,
1434+ and if the user is registered in the room but not currently in it.
1435+
1436+ Returns a tuple (success, report-message).
1437+
1438+ Report message may be:
1439+ "User not registered"
1440+ "User not available"
1441+ "Room not registered"
1442+ "User not registered in room"
1443+ "User already in room"
1444+ "Invitation sent"
1445+ """
1446+}}}
1447+
1448+= Utilities =
1449+
1450+API reference for !EnDroid's utility classes.
1451+
1452+== Database ==
1453+
1454+A wrapper around an sqlite3 database.
1455+
1456+{{{#!highlight python
1457+class Database(object):
1458+ """
1459+ This is wrapper around an sqlite3 database. Note that all accesses
1460+ are _synchronous_, so should be minimised. (It is likely that in
1461+ the future all accesses will be made using twisted.enterprise.adbapi
1462+ which is asynchronous).
1463+
1464+ """
1465+ def __init__(self, modName):
1466+ """Create a new database with name=modName in the Database singleton."""
1467+
1468+ def create_table(self, name, fields):
1469+ """
1470+ Create a new table in the database called 'name' and containing fields
1471+ 'fields' (an iterable of strings giving field titles).
1472+
1473+ """
1474+
1475+ def table_exists(self, name):
1476+ """Check to see if a table called 'name' exists in the database."""
1477+
1478+ def insert(self, name, fields):
1479+ """
1480+ Insert a row into table 'name'.
1481+
1482+ Fields is a dictionary mapping field names (as defined in
1483+ create_table) to values.
1484+
1485+ """
1486+
1487+ def fetch(self, name, fields, conditions={}):
1488+ """
1489+ Get data from the table 'name'.
1490+
1491+ Returns a list of dictionaries mapping 'fields' to their values, one
1492+ dictionary for each row which satisfies a condition in conditions.
1493+
1494+ Conditions is a dictionary mapping field names to values. A result
1495+ will only be returned from a row if its values match those in conditions.
1496+
1497+ E.g.: conditions = {'user' : JoeBloggs}
1498+ will match only fields in rows which have JoeBloggs in the 'user' field.
1499+
1500+ """
1501+
1502+ def count(self, name, conditions):
1503+ """Return the number of rows in table 'name' which satisfy conditions."""
1504+
1505+ def delete(self, name, conditions):
1506+ """Delete rows from table 'name' which satisfy conditions."""
1507+
1508+ def update(self, name, fields, conditions):
1509+ """
1510+ Update rows in table 'name' which satisfy conditions.
1511+
1512+ Fields is a dictionary mapping the field names to their new values.
1513+
1514+ """
1515+
1516+ def empty_table(self, name):
1517+ def delete_table(self, name):
1518+ """Remove all rows from/delete table 'name'."""
1519+}}}
1520+
1521+== Cron ==
1522+
1523+!EnDroid's task scheduling service.
1524+
1525+{{{#!highlight python
1526+class Cron(object):
1527+ def get():
1528+ """
1529+ Return a reference to the Cron singleton. All below functions can be
1530+ accessed via Cron.get().<function>(args).
1531+
1532+ """
1533+
1534+class CronSing(object):
1535+ """The singleton returned by Cron.get()."""
1536+
1537+ def register(self, function, reg_name, persistent=True):
1538+ """
1539+ Register the callable 'function' against 'reg_name'. Note that 'reg_name'
1540+ must be globally unique. Allows function to be scheduled with doAtTime or
1541+ setTimeout.
1542+
1543+ If persistent is False, remoteTask(reg_name) will be called before the
1544+ the function is registered (so across a restart of EnDroid, the stored
1545+ tasks will be forgotten).
1546+
1547+ """
1548+
1549+ def doAtTime(self, time, locality, reg_name, params):
1550+ """
1551+ Schedule the callable registered against 'reg_name' to be called
1552+ with 'params' (which must be picklable) at locality time 'time'.
1553+ (Localities are from pytz.timezone).
1554+
1555+ """
1556+
1557+ def setTimeout(self, timedelta, reg_name, params):
1558+ """
1559+ Schedule the callable to be called in after 'timedelta'.
1560+
1561+ Timedelta may be a datetime.timedelta object or a real number representing
1562+ the number of seconds to wait. Negative or zero values will trigger almost
1563+ immediately.
1564+
1565+ """
1566+
1567+ def removeTask(self, reg_name):
1568+ """Remove any scheduled tasks registered with reg_name."""
1569+
1570+ def getAtTimes(self):
1571+ """
1572+ Return a string showing the registration names of functions scheduled
1573+ with doAtTime and the amount of time they will be called in.
1574+
1575+ """
1576+
1577+ def getTimeouts(self):
1578+ """
1579+ Return a string showing the registration names of functions scheduled
1580+ with setTimeout and the amount of time they will be called in.
1581+
1582+ """
1583+}}}
1584+
1585+
1586+= Helper Plugins =
1587+
1588+API reference for plugins designed to be used by other plugins.
1589+
1590+All of these plugins are located at {{{endroid.plugins.<plugin_name>}}}
1591+
1592+== Command ==
1593+
1594+Plugins should inherit from {{{CommandPlugin}}} or use {{{self.comm = self.get("endroid.plugins.command")}}} to use {{{Command}}}'s methods.
1595+
1596+{{{#!highlight python
1597+"""
1598+Helper plugin to handle command registrations by other plugins. This is
1599+the main avenue by which plugins are expected to handle incoming messages
1600+and it is expected most plugins will depend on this.
1601+
1602+"""
1603+
1604+class CommandPlugin(Plugin):
1605+ """
1606+ Parent class for simple command-driven plugins.
1607+
1608+ Such plugins don't need to explicitly register their commands. Instead, they
1609+ can just define methods prefixed with "cmd_" and they will automatically be
1610+ registered. Any additional underscores in the method name will be converted
1611+ to spaces in the registration (so cmd_foo_bar is registered as ('foo',
1612+ 'bar')).
1613+
1614+ In addition, certain options can be passed by adding fields to the methods:
1615+ - hidden: don't show the command in help if set to True.
1616+ - synonyms: an iterable of alternative keyword sequences to register the
1617+ method against. All synonyms are hidden.
1618+ - helphint: a hint to print after the keywords in help output.
1619+ - muc_only or chat_only: register for only chat or muc messages (default is
1620+ both).
1621+
1622+ """
1623+
1624+class Command(Plugin):
1625+ """
1626+ The instance of this class active in a plugin's room/group is
1627+ returned by a Plugin.get("endroid.plugins.command") call.
1628+
1629+ This class makes it possible to access the functionality of CommandPlugin
1630+ without the inheritance.
1631+
1632+ """
1633+
1634+ def register_muc(self, callback, command, helphint="", hidden=False,
1635+ synonyms=()):
1636+ """Register a new handler for MUC messages."""
1637+
1638+ def register_chat(self, callback, command, helphint="", hidden=False,
1639+ synonyms=()):
1640+ """Register a new handler for chat messages."""
1641+
1642+ def register_both(self, callback, command, helphint="", hidden=False,
1643+ synonyms=()):
1644+ """Register a handler for both MUC and chat messages."""
1645+}}}
1646+
1647+For an example usecase look at the {{{invite}}} plugins.
1648+
1649+== HTTPInterface ==
1650+
1651+Start a webserver, allow callback registrations on URL paths, and route requests to callbacks.
1652+
1653+{{{#!highlight python
1654+class HTTPInterface(Plugin):
1655+ """
1656+ The actual plugin class. This may be instantiated multiple times, but is
1657+ just a wrapper around a HTTPInterfaceSingleton object.
1658+
1659+ """
1660+
1661+ def register_regex_path(self, plugin, callback, path_regex):
1662+ """
1663+ Register a callback to be called for requests whose URI matches:
1664+ http://<server>/<plugin name>/<regex>[?<args>]
1665+
1666+ Callback arguments:
1667+ request: A twisted.web.http.Request object.
1668+
1669+ """
1670+
1671+ def register_path(self, plugin, callback, path_prefix):
1672+ """
1673+ Register a callback to be called for requests whose URI matches:
1674+ http://<server>/<plugin name>/<prefix>[?<args>]
1675+
1676+ Or:
1677+ http://<server>/<plugin name>/<prefix>/<more path>[?<args>]
1678+
1679+ Or if the prefix is the empty string:
1680+ http://<server>/<plugin name>/<more path>[?<args>]
1681+
1682+ """
1683+}}}
1684+
1685+=== Remote Plugin, endroid_echo script ===
1686+
1687+For an example usecase look at the remote plugin, this calls: {{{register_path(self, self.http_request_handler, '')}}}. This listens on {{{http://<ip address>:8880/remote/}}} by default, where a form
1688+can be filled in to send an !EnDroid user a chat message. You can change the port number with the following configuration file option:
1689+
1690+{{{
1691+# HTTP port for remote messaging
1692+[ group | room : * : pconfig : endroid.plugins.httpinterface ]
1693+http_port = 8881
1694+}}}
1695+
1696+You also need the {{{httpinterface}}} and {{{remote}}} plugins to be loaded.
1697+
1698+The {{{endroid_echo}}} script (located at {{{/bin/endroid_echo}}} provides a method of sending a message to an !EnDroid user from the command line via the {{{remote}}} plugin. A user must specify the
1699+environment variables {{{ENDROID_REMOTE_KEY}}}, {{{ENDROID_REMOTE_USER}}} and {{{ENDROID_REMOTE_URL}}} and when the script runs (via {{{endroid_echo message_here}}} it will send the the message
1700+"message_here" to the user with JID {{{ENDROID_REMOTE_USER}}} provided that they have enabled the remote plugin (by typing {{{allow remote}}} to !EnDroid and receiving their key in response) and that
1701+{{{ENDROID_REMOTE_KEY}}} matches the user's remote plugin key. The default value for {{{ENDROID_REMOTE_URL}}} would be {{{http://127.0.0.1:8880/remote/}}} .
1702+
1703+
1704+== Patternmatcher ==
1705+
1706+Message regex matching.
1707+
1708+{{{#!highlight python
1709+class PatternMatcher(Plugin):
1710+ """Plugin to simplify matching messages based on a regexp."""
1711+
1712+ def register_muc(self, callback, pattern):
1713+ def register_chat(self, callback, pattern):
1714+ """
1715+ Register a callback to be called when a message's body matches
1716+ the pattern (a regex string).
1717+
1718+ Callback will be called with the Message object as the argument.
1719+
1720+ """
1721+
1722+ def register_both(self, callback, pattern):
1723+ """Equivalent to register_muc(...); register_chat(...)."""
1724+}}}
1725+For a usecase, look at the {{{spell}}} plugin, which looks for a {{{(sp?)}}} in a message.
1726+
1727
1728=== modified file 'etc/endroid.conf' (properties changed: +x to -x)
1729=== modified file 'src/endroid.sh'
1730--- src/endroid.sh 2013-08-08 13:52:15 +0000
1731+++ src/endroid.sh 2013-08-14 10:10:05 +0000
1732@@ -1,2 +1,2 @@
1733 #!/bin/sh
1734-PYTHONPATH="../lib/wokkel-0.7.1-py2.7.egg":"${PYTHONPATH}" python -m endroid $@
1735+PYTHONPATH="../lib/wokkel-0.7.1-py2.7.egg":"${PYTHONPATH}":"~/.endroid/plugins/" python -m endroid $@
1736
1737=== modified file 'src/endroid/confparser.py'
1738--- src/endroid/confparser.py 2013-08-12 16:07:57 +0000
1739+++ src/endroid/confparser.py 2013-08-14 10:10:05 +0000
1740@@ -43,6 +43,9 @@
1741 var2
1742 var3
1743 as internal newlines are present
1744+ - booleans:
1745+ - literal_eval will convert "True" or "False" to their bool counterparts,
1746+ but "true", "false", "yes", "no" etc will remain as strings.
1747 Arguments to .get:
1748 - 'default' may be specified in which case KeyErrors will never be raised
1749 - 'return_all' will cause get to return all possible results rather than
1750
1751=== modified file 'src/endroid/cron.py'
1752--- src/endroid/cron.py 2013-08-07 16:37:48 +0000
1753+++ src/endroid/cron.py 2013-08-14 10:10:05 +0000
1754@@ -45,12 +45,17 @@
1755 class CronSing(object):
1756 """
1757 A singleton providing task scheduling facilities.
1758- A function may be registered by calling .register(function, name)
1759- and scheduled with either setTimeout(time, name, params) or
1760- doAtTime(time, locality, name, params).
1761+ A function may be registered by calling register(function, name) (returning
1762+ a Task object).
1763+ A registered function may be scheduled with either:
1764+ - setTimeout(time, name, params) / doAtTime(time, locality, name, params)
1765+ - or by calling either method on the Task object returned
1766+ by register(), omitting the name parameter.
1767+ Note that params will be pickled for storage in the database.
1768+
1769 When it comes to be called, the function will be called with an argument
1770- generated from params (so even if the function needs no arguments it should
1771- provide one eg def foo(_) rather than def foo()
1772+ unpickled from params (so even if the function needs no arguments it should
1773+ allow for one eg def foo(_) rather than def foo()).
1774
1775 """
1776 def __init__(self):
1777@@ -71,9 +76,16 @@
1778 return float((uss + (ss + ds * 24 * 3600) * 10**6)) / 10**6
1779
1780 def register(self, function, reg_name, persistent=True):
1781- """Register the callable fun against reg_name.
1782-
1783- Allows callable to be scheduled with doAtTime or setTimeout."""
1784+ """
1785+ Register the callable fun against reg_name.
1786+
1787+ Returns a Task object and allows callable to be scheduled with doAtTime
1788+ or setTimeout either on self, or on the Task object returned.
1789+
1790+ If persistent is False, any previous reigstrations against reg_name will
1791+ be deleted before the new function is registered.
1792+
1793+ """
1794 # reg_name is the key we use to access the function - we can then set the
1795 # function to be called using setTimeout or doAtTime with regname = name
1796
1797
1798=== modified file 'src/endroid/database.py'
1799--- src/endroid/database.py 2013-08-06 10:32:33 +0000
1800+++ src/endroid/database.py 2013-08-14 10:10:05 +0000
1801@@ -12,9 +12,7 @@
1802
1803
1804 class TableRow(dict):
1805- """
1806- A regular dict, plus a system 'id' attribute.
1807- """
1808+ """A regular dict, plus a system 'id' attribute."""
1809 __slots__ = ()
1810
1811 def __init__(self, *args, **kwargs):
1812@@ -27,7 +25,8 @@
1813 return self[EndroidUniqueID]
1814
1815 class Database(object):
1816- """Wrapper round an sqlite3 Database
1817+ """
1818+ Wrapper round an sqlite3 Database.
1819
1820 All accesses are synchronous, TODO use twisted.enterprise.adbapi to
1821 asynchronise them.
1822@@ -77,6 +76,11 @@
1823 return " and ".join(Database._sanitize(c) + "=?" for c in conditions) or "1"
1824
1825 def create_table(self, name, fields):
1826+ """
1827+ Create a new table in the database called 'name' and containing fields
1828+ 'fields' (an iterable of strings giving field titles).
1829+
1830+ """
1831 if any(f.startswith('_endroid') for f in fields):
1832 raise ValueError("An attempt was made to create a table with system-reserved column-name (prefix '_endroid').")
1833 n = Database._sanitize(self._tName(name))
1834@@ -85,6 +89,7 @@
1835 Database.raw(query)
1836
1837 def table_exists(self, name):
1838+ """Check to see if a table called 'name' exists in the database."""
1839 n = Database._sanitize(self._tName(name))
1840 query = "SELECT `name` FROM `sqlite_master` WHERE `type`='table' AND `name`={0};".format(n)
1841 Database.raw(query)
1842@@ -93,6 +98,13 @@
1843 return count != 0
1844
1845 def insert(self, name, fields):
1846+ """
1847+ Insert a row into table 'name'.
1848+
1849+ Fields is a dictionary mapping field names (as defined in
1850+ create_table) to values.
1851+
1852+ """
1853 n = Database._sanitize(self._tName(name))
1854 query = "INSERT INTO {0} ({1}) VALUES ({2});".format(n,
1855 Database._stringFromFieldNames(fields),
1856@@ -101,6 +113,19 @@
1857 Database.raw(query, tup)
1858
1859 def fetch(self, name, fields, conditions={}):
1860+ """
1861+ Get data from the table 'name'.
1862+
1863+ Returns a list of dictionaries mapping 'fields' to their values, one
1864+ dictionary for each row which satisfies a condition in conditions.
1865+
1866+ Conditions is a dictionary mapping field names to values. A result
1867+ will only be returned from a row if its values match those in conditions.
1868+
1869+ E.g.: conditions = {'user' : JoeBloggs}
1870+ will match only fields in rows which have JoeBloggs in the 'user' field.
1871+
1872+ """
1873 n = Database._sanitize(self._tName(name))
1874 fields = list(fields) + [EndroidUniqueID]
1875 query = "SELECT {0} FROM {1} WHERE ({2});".format(
1876@@ -112,18 +137,26 @@
1877 return rows
1878
1879 def count(self, name, conditions):
1880+ """Return the number of rows in table 'name' which satisfy conditions."""
1881 n = Database._sanitize(self._tName(name))
1882 query = "SELECT COUNT(*) FROM {0} WHERE ({1});".format(n, Database._buildConditions(conditions))
1883 r = Database.raw(query, Database._tupleFromFieldValues(conditions)).fetchall()
1884 return r[0][0]
1885
1886 def delete(self, name, conditions):
1887+ """Delete rows from table 'name' which satisfy conditions."""
1888 n = Database._sanitize(self._tName(name))
1889 query = "DELETE FROM {0} WHERE ({1});".format(n, Database._buildConditions(conditions))
1890 Database.raw(query, Database._tupleFromFieldValues(conditions))
1891 return Database.cursor.rowcount
1892
1893 def update(self, name, fields, conditions):
1894+ """
1895+ Update rows in table 'name' which satisfy conditions.
1896+
1897+ Fields is a dictionary mapping the field names to their new values.
1898+
1899+ """
1900 n = Database._sanitize(self._tName(name))
1901 query = "UPDATE {0} SET {1} WHERE ({2});".format(n, Database._buildConditions(fields), Database._buildConditions(conditions))
1902 tup = Database._tupleFromFieldValues(fields)
1903@@ -132,11 +165,13 @@
1904 return Database.cursor.rowcount
1905
1906 def empty_table(self, name):
1907+ """Remove all rows from table 'name'."""
1908 n = Database._sanitize(self._tName(name))
1909 query = "DELETE FROM {0} WHERE 1;".format(n)
1910 Database.raw(query)
1911
1912 def delete_table(self, name):
1913+ """Delete table 'name'."""
1914 n = Database._sanitize(self._tName(name))
1915 query = "DROP TABLE {0};".format(n)
1916 Database.raw(query)
1917
1918=== modified file 'src/endroid/manhole.py'
1919--- src/endroid/manhole.py 2013-08-06 10:32:33 +0000
1920+++ src/endroid/manhole.py 2013-08-14 10:10:05 +0000
1921@@ -19,7 +19,8 @@
1922
1923 # how to log in: ssh user_name@localhost -p port
1924 def start_manhole(droid, nmspace, cliargs):
1925- """Start EnDroid listening for an ssh connection.
1926+ """
1927+ Start EnDroid listening for an ssh connection.
1928
1929 Logging in via ssh gives access to a python prompt from which EnDroid's
1930 internals can be investigated.
1931
1932=== modified file 'src/endroid/messagehandler.py'
1933--- src/endroid/messagehandler.py 2013-08-14 10:10:05 +0000
1934+++ src/endroid/messagehandler.py 2013-08-14 10:10:05 +0000
1935@@ -137,7 +137,8 @@
1936 self.do_callback("recv_self", msg, self._unhandled_self_chat)
1937
1938 def send_muc(self, room, body, source=None, priority=PRIORITY_NORMAL):
1939- """Send muc message to room.
1940+ """
1941+ Send muc message to room.
1942
1943 The message will be run through any registered filters before it is sent.
1944
1945@@ -155,7 +156,8 @@
1946 logging.info("Filtered out message to {}".format(room))
1947
1948 def send_chat(self, user, body, source=None, priority=PRIORITY_NORMAL):
1949- """Send chat message to person with address user.
1950+ """
1951+ Send chat message to person with address user.
1952
1953 The message will be run through any registered filters before it is sent.
1954
1955
1956=== modified file 'src/endroid/pluginmanager.py'
1957--- src/endroid/pluginmanager.py 2013-08-14 10:10:05 +0000
1958+++ src/endroid/pluginmanager.py 2013-08-14 10:10:05 +0000
1959@@ -140,7 +140,8 @@
1960 return self._pm.get(plugin_name)
1961
1962 def get_dependencies(self):
1963- """Return an iterable of plugins this plugin depends on.
1964+ """
1965+ Return an iterable of plugins this plugin depends on.
1966
1967 This includes indirect dependencies i.e. the dependencies of plugins this
1968 plugin depends on and so on.
1969@@ -149,12 +150,13 @@
1970 return (self.get(dependency) for dependency in self.dependencies)
1971
1972 def get_preferences(self):
1973- """Return an iterable of plugins this plugin prefers.
1974+ """
1975+ Return an iterable of plugins this plugin prefers.
1976
1977 This includes indirect preferences i.e. the preferences of plugins this
1978 plugin prefers and so on.
1979
1980- """
1981+ """
1982 return (self.get(preference) for preference in self.preferences)
1983
1984 def list_plugins(self):
1985
1986=== modified file 'src/endroid/plugins/ratelimit.py' (properties changed: +x to -x)
1987=== modified file 'src/endroid/usermanagement.py'
1988--- src/endroid/usermanagement.py 2013-08-14 10:10:05 +0000
1989+++ src/endroid/usermanagement.py 2013-08-14 10:10:05 +0000
1990@@ -22,8 +22,11 @@
1991 MUC = "muc#roomconfig_"
1992
1993 class Roster(object):
1994- """Provides functions for maintaining sets of users registered with and
1995- available in a contact list, user group or room"""
1996+ """
1997+ Provides functions for maintaining sets of users registered with and
1998+ available in a contact list, user group or room.
1999+
2000+ """
2001 def __init__(self, name=None, registration_cb=None, deregistration_cb=None):
2002 self.name = name or "contacts"
2003
2004@@ -199,7 +202,8 @@
2005 # given a group or room or None (our contact list), return list of users
2006 # registered/available there
2007 def get_users(self, name=None):
2008- """Return a set of users registered with 'name'.
2009+ """
2010+ Return an iterable of users registered with 'name'.
2011
2012 If name is None, look in contact list.
2013
2014@@ -212,7 +216,8 @@
2015 return self.room_rosters[name].registered
2016
2017 def get_available_users(self, name=None):
2018- """Return a set of users present in 'name'.
2019+ """
2020+ Return an iterable of users present in 'name'.
2021
2022 If name is None, look in contact list.
2023
2024@@ -227,7 +232,8 @@
2025 # given a user or None (us), return list of groups/rooms the user is
2026 # registered/available in
2027 def get_groups(self, user=None):
2028- """Return a set of groups 'user' is registered with.
2029+ """
2030+ Return an iterable of groups 'user' is registered with.
2031
2032 If user is None, return all registered groups.
2033
2034@@ -235,7 +241,8 @@
2035 return self._get_user_place(user, self.group_rosters, get_available=False)
2036
2037 def get_available_groups(self, user=None):
2038- """Return a set of groups 'user' is present in.
2039+ """
2040+ Return an iterable of groups 'user' is present in.
2041
2042 If user is None, return all groups EnDroid is available in.
2043
2044@@ -243,7 +250,8 @@
2045 return self._get_user_place(user, self.group_rosters, get_available=True)
2046
2047 def get_rooms(self, user=None):
2048- """Return a set of rooms 'user' is registered with.
2049+ """
2050+ Return an iterable of rooms 'user' is registered with.
2051
2052 If user is None, return all registered rooms.
2053
2054@@ -251,7 +259,8 @@
2055 return self._get_user_place(user, self.room_rosters, get_available=False)
2056
2057 def get_available_rooms(self, user=None):
2058- """Return a set of rooms 'user' is present in.
2059+ """
2060+ Return an iterable of rooms 'user' is present in.
2061
2062 If user is None, return all rooms EnDroid is available in.
2063
2064@@ -496,7 +505,8 @@
2065 return d.addCallback(form_to_string)
2066
2067 def invite(self, user, room, reason=None):
2068- """Invite a user to a room.
2069+ """
2070+ Invite a user to a room.
2071
2072 Will only send invitation if the user is in our contact list and online,
2073 and if the user is registered in the room but not currently in it.
2074
2075=== modified file 'src/endroid/wokkelhandler.py'
2076--- src/endroid/wokkelhandler.py 2013-08-12 15:10:08 +0000
2077+++ src/endroid/wokkelhandler.py 2013-08-14 10:10:05 +0000
2078@@ -137,6 +137,8 @@
2079 m = Message("muc", sender_jid, message.body,
2080 self.messagehandler, room_userhost)
2081
2082+ # If the jid specified in the config file has a resource,
2083+ # then we need to reduce it to just the userhost string
2084 our_jid = self.usermanagement.get_userhost(self.jid)
2085
2086 sender_nick = self.usermanagement.get_nickname(sender_jid, room_userhost)

Subscribers

People subscribed via source and target branches