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 |
Related bugs: |
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) |