Merge lp:~ben-hutchings/endroid/roomowner into lp:endroid

Proposed by Ben Hutchings
Status: Merged
Approved by: Martin Morrison
Approved revision: 81
Merged at revision: 32
Proposed branch: lp:~ben-hutchings/endroid/roomowner
Merge into: lp:endroid
Diff against target: 5829 lines (+3896/-1001)
31 files modified
bin/endroid (+1/-1)
bin/endroid_echo (+39/-0)
debian/endroid.install (+2/-1)
etc/endroid.conf (+62/-16)
src/endroid.graphml (+1841/-0)
src/endroid.sh (+1/-1)
src/endroid/__init__.py (+66/-81)
src/endroid/config.py (+0/-159)
src/endroid/confparser.py (+179/-0)
src/endroid/cron.py (+138/-64)
src/endroid/database.py (+6/-0)
src/endroid/manhole.py (+37/-0)
src/endroid/messagehandler.py (+153/-100)
src/endroid/pluginmanager.py (+188/-230)
src/endroid/plugins/blacklist.py (+18/-14)
src/endroid/plugins/command.py (+59/-15)
src/endroid/plugins/compute/__init__.py (+2/-2)
src/endroid/plugins/correct.py (+2/-1)
src/endroid/plugins/echobot.py (+1/-1)
src/endroid/plugins/httpinterface.py (+7/-5)
src/endroid/plugins/invite.py (+121/-0)
src/endroid/plugins/memo.py (+36/-33)
src/endroid/plugins/passthebomb.py (+211/-0)
src/endroid/plugins/ratelimit.py (+3/-6)
src/endroid/plugins/remote.py (+4/-8)
src/endroid/plugins/roomowner.py (+9/-0)
src/endroid/plugins/speak.py (+4/-5)
src/endroid/plugins/whosonline.py (+3/-16)
src/endroid/rosterhandler.py (+80/-39)
src/endroid/usermanagement.py (+531/-127)
src/endroid/wokkelhandler.py (+92/-76)
To merge this branch: bzr merge lp:~ben-hutchings/endroid/roomowner
Reviewer Review Type Date Requested Status
Martin Morrison Approve
Review via email: mp+177612@code.launchpad.net

This proposal supersedes a proposal from 2013-07-29.

Commit message

Merge in changes in prep for beta release.

Description of the change

__init__.py:
 - moved plugin initialisation/config loading to PluginManager
 - added command line argument to allow ssh access via manhole (disabled by default)

config.py -> confparser.py:
 - rewrote to be more flexible
 - reads an ini-like config file into a nested dictionary, providing a get method which permits wildcard search and or syntax
 - will automatically attempt to interpret config values as python objects

cron.py:
 - removed some commented code
 - some small code formatting changes

manhole.py:
 - provides a method of accessing endroids internals via ssh using twisted.conch (provides a python interpreter with access to the endroid class instance)

messagehandler.py:
 - added a Handler class which adds a couple of nice attributes as well as a function callback. Plugins now register a handler instance rather than just a function callback which is called via handler.callback()
 - chat callbacks are now registered against groupname rather than userjid - partly to be more similar to registering with rooms and partly because it reduces the size of the messagehandler._handlers dictionary if there are many users.
 - restructured the _handlers dictionary to be {group/room : {msg type : {category : [handlers]}}}
rather than {type : {category : {room/userjid :[ handlers]}}} (this was going to help with reloading plugins in a room/group but this feature hasn't been added)
 - removed the multitude of register_*_callback/filter methods - the general register_callback function is now called directly from the Plugin class

pluginmanager.py:
 - added access to usermanagement and messagehandler to the Plugin class
 - compressed the register_*_callback functions (the switch from registering plugins against groups rather than individual users helped here) and templated the doc strings which were all pretty much identical)
 - removed another plenitude of register_*_callback methods from pluginmanager - as previously mentioned, these registrations now go straight from the Plugin class to the messagehandlers register_callback function
 - added a reload() method which currently doesn't work but hopefully will be fixed to allow plugin reloading (it currently fails on plugins which dispatch callbacks to otherplugins such as CommandPlugin, httpinterface and patternmatcher)

blacklist.py:
 - Blacklist now maintains a class _blacklist set allowing it to act globally

invite.py
 - a command plugin used to invite a list of users to a list of rooms eg invite -u me -r all invites the user sending the message to all the rooms he is registered with

roomowner.py:
 - currently serves only to configure a room that it joins and do nothing else.
 - in the future will provide many more room-management features

rosterhandler.py:
 - added the (un)availableReceived methods to inform the usermanagement class who is online/present in rooms and groups
 - NOTE - plugins are currently unaware of changes to presence - an obvious next step would be to expand the rosterhandler class to add callback methods as messaghandler has.

usermanagement.py:
 - added the Roster class - this keeps track of the users registered with and present in a room or group, providing capabilities to call functions when users are registered or deregistered (used to subscribe to contacts added to endroid's contact list and add users to a room's member list when being registered with a room)
 - added the Room class - this is just a Roster with additional information enough to join a room potentially with a password. Also contains a default room configuration dictionary and a dictionary of available-to-user configuration options.
 - re-wrote the usermanagement class from scratch using the Room and Roster classes to facilitate things. Added a number of functions to get information about users in various locations. Also functions to join and configure rooms.
  - the join room function will retry a failed join assuming that someone has stolen its nickname (a temporary nickname will be generated to join the room and the offending user will be kicked)

wokkelhandler.py
 - userJoinedRoom/userLeftROom used to let usermanagement class know who's where

endroid2.graphxml
 - a class diagram of endroid

To post a comment you must log in.
Revision history for this message
Martin Morrison (isoschiz) wrote : Posted in a previous version of this proposal

Comments in the order the files appear in the diff:

src/endroid/__init__.py

- You've changed permissions on this file. Please revert! It shouldn't be executable

- In general, it's nice to keep the module name on foreign imports (e.g. things that come from twisted). "service.Application" is a lot more obvious than "Application". Revert that import in particular.

- Probably want to increment the version number of EnDroid. ;-)

- The fields on EnDroid itself are clearer in full: rosterhandler, wokkelhandler, messagehandler, usermanagement (rather than rh, wh, um, mh).

- The initialisation code has changed - although a lot of it appears to just be changes to the names of local variables, and not to functionality. Can you revert the lines that are unchanged, to make it easier to review.

(more comments to come)

Revision history for this message
Ben Hutchings (ben-hutchings) wrote : Posted in a previous version of this proposal

Done the first four (in __init__.py)
As for the fifth - I've changed two lines back to how they were but
pretty much everything is different (I assume you're talking about the
code in startup_flow.)

Ben

PS do I just do another push to update the code?

On 29/07/2013 17:52, Martin Morrison wrote:
> Comments in the order the files appear in the diff:
>
> src/endroid/__init__.py
>
> - You've changed permissions on this file. Please revert! It shouldn't be executable
>
> - In general, it's nice to keep the module name on foreign imports (e.g. things that come from twisted). "service.Application" is a lot more obvious than "Application". Revert that import in particular.
>
> - Probably want to increment the version number of EnDroid. ;-)
>
> - The fields on EnDroid itself are clearer in full: rosterhandler, wokkelhandler, messagehandler, usermanagement (rather than rh, wh, um, mh).
>
> - The initialisation code has changed - although a lot of it appears to just be changes to the names of local variables, and not to functionality. Can you revert the lines that are unchanged, to make it easier to review.
>
> (more comments to come)

Revision history for this message
Martin Morrison (isoschiz) wrote : Posted in a previous version of this proposal
Download full text (6.1 KiB)

Fair enough - the first 5 lines was what I looked at. ;-) You have also, of course, lost the awesome HHGG quote that gets logged when initialisation is complete...

Further comments:

- Might be worth censoring the manhole dict in practice. Pick the elements that are most useful, given your experience. Might also be worth having a whole separate module with some "helper functions" that get put into the manhole dict, to make it even more useful (at this stage, no need for the extra helpers, but deferring to a module that could set them up would be useful).

- Related: instead of listening for the manhole in this module, defer entirely to the manhole module (something like "start_manhole(args, droid)").

- __main__.py made executable - also not necessary.

configparser.py

- There are some "@@@" comments in the configparser. These should be converted into something more appropriate (like actual docstrings, or potentially bug reports/feature requests in launchpad).

- Line 421: typo (Pasrer).

- [Minor: method names should be underscore_separated and not camelCase (I know existing code is wrong: new code should follow the coding standards though)]

- This module (configparser) could do with some (a lot ;-)) of docstrings, in particular explaining the expected/supported syntax for section names and aliases.

- The get method can be made more efficient:

 . Don't create lots of intermediate lists. Use generator expressions instead, and add the if to the same generator expression. (you'll need to make it into a list at the end)
 . Instead of the nested try statements, use simpler if statements. It makes the code a bit clearer.
 . A few newlines wouldn't go amiss. Some use of kwargs.pop(key, None) would also save the trys.
 . Why are the copies shallow? What if the value contains a list or dict?

- Parser.splitter should probably be Parser.SPLITTER (effectively a constant)

cron.py

- Lots of whitespace changes, without any real content. A good opportunity to improve more docstrings. ;-)

manhole.py

- def manhole_factory. Or, better, change to start_manhole as mentioned above.

- Comment about changing perhaps should be removed? For now, I think this is fine.

messagehandler.py

- Also executable (I'll stop adding this comment. Please fix all files)

- Use of im_self in the Handler class is dangerous! Assumes that plugins will only ever pass *methods* of *themselves* - both of which can quite easily not be true. This needs removing.

- The way handlers are now stored, they don't sort by priority! This is bad. Needs reverting (either by pulling priority out of the Handler object, or by doing extra work in that class to make it sort correctly).

- You've reordered the dicts in the handlers storage - why? Just means all the functions have to change.

- Apart from a rename of 'mtype' to 'place', send() and reply() are identical. Why make them look so changed? :-)

- You've altered the constructor of Message - why? Just means more changes everywhere that we create them.

- Inside the Message class, you've made __handlers not private anymore - why is that? Probably wants to be private again. Also this forms part of the external API, and is used by Comman...

Read more...

Revision history for this message
Ben Hutchings (ben-hutchings) wrote : Posted in a previous version of this proposal
Download full text (7.8 KiB)

A few comments:

On 29/07/2013 23:27, Martin Morrison wrote:
> Fair enough - the first 5 lines was what I looked at. ;-) You have also, of course, lost the awesome HHGG quote that gets logged when initialisation is complete...
The HHGG quote was nice but when room configuration moved to a plugin it
tended to fire before everything was configured so wasn't really
accurate any more.
> Further comments:
>
> - Might be worth censoring the manhole dict in practice. Pick the elements that are most useful, given your experience. Might also be worth having a whole separate module with some "helper functions" that get put into the manhole dict, to make it even more useful (at this stage, no need for the extra helpers, but deferring to a module that could set them up would be useful).
I've censored the manhole dict a bit and moved some more stuff into
manhole.py - not sure if this is what you meant but there should be room
to add helper functions in manhole.py now
> - Related: instead of listening for the manhole in this module, defer entirely to the manhole module (something like "start_manhole(args, droid)").
>
> - __main__.py made executable - also not necessary.
>
> configparser.py
>
> - There are some "@@@" comments in the configparser. These should be converted into something more appropriate (like actual docstrings, or potentially bug reports/feature requests in launchpad).
>
> - Line 421: typo (Pasrer).
>
> - [Minor: method names should be underscore_separated and not camelCase (I know existing code is wrong: new code should follow the coding standards though)]
>
> - This module (configparser) could do with some (a lot ;-)) of docstrings, in particular explaining the expected/supported syntax for section names and aliases.
>
> - The get method can be made more efficient:
>
> . Don't create lots of intermediate lists. Use generator expressions instead, and add the if to the same generator expression. (you'll need to make it into a list at the end)
I tried to make the list comprehension into a generator expression in
the .get method but I just got empy lists back - not entirely sure why
at the moment, will have more of a look this afternoon.
> . Instead of the nested try statements, use simpler if statements. It makes the code a bit clearer.
> . A few newlines wouldn't go amiss. Some use of kwargs.pop(key, None) would also save the trys.
> . Why are the copies shallow? What if the value contains a list or dict?
>
> - Parser.splitter should probably be Parser.SPLITTER (effectively a constant)
>
> cron.py
>
> - Lots of whitespace changes, without any real content. A good opportunity to improve more docstrings. ;-)
>
> manhole.py
>
> - def manhole_factory. Or, better, change to start_manhole as mentioned above.
>
> - Comment about changing perhaps should be removed? For now, I think this is fine.
>
> messagehandler.py
>
> - Also executable (I'll stop adding this comment. Please fix all files)
>
> - Use of im_self in the Handler class is dangerous! Assumes that plugins will only ever pass *methods* of *themselves* - both of which can quite easily not be true. This needs removing.
>
> - The way handlers are now stored, they don't sort by...

Read more...

lp:~ben-hutchings/endroid/roomowner updated
54. By Ben Hutchings

- Reverted ordering of messagehandler's _handler dictionary
- Fixed some more file permissions

55. By Ben Hutchings

cron: renamed parameters fun_name and name to reg_name for consitency. Renamed field name fun_name to reg_name. (it seemed strange having three different names for the same thing while writing up the API)

messagehandler: added a couple of comments in do_callback, noting that if a plugin is registered in multiple user groups it may get called multiple times.

usermanagement: renamed get_registered_users to get_users for consistency with the rest of the get_ functions

56. By Ben Hutchings

- rolled back some name changes (will be in seperate branch)
- removed an unused method (prepare_response) from messagehandler
- removed reload method from pluginmanager as it doesn't work

- fixed permissions again

Revision history for this message
Martin Morrison (isoschiz) wrote :
Download full text (5.6 KiB)

Minor point throughout the diff: lines shouldn't exceed 80 characters. There are a few places where new lines of code do. It would be good to wrap them as per the coding standards.

endroid/__init__.py

- It is interesting that you have moved the initial joining of rooms out of wokkelhandler and in to EnDroid main itself. I guess the main consequence of this is that we now call .join() before even having reached connected state. So my question is: does this also mean that we now don't re-join rooms after a disconnection?

- Above the "get_pm()" inner func in the main init is a comment that refers to rooms, when the function is used for both rooms and groups. Also the function might be better named "start_pluginmanager".

- The Twisted logging functionality should probably be predicated on a command line or configuration option.

cron.py

- Comment applies elsewhere too, but in CronSing the docstring formatting needs changing to:

"""
First line of text.
More lines.

"""

i.e. open quotes on one line, text on their own lines, and then a blank line before the line containing close quotes. Note that single line docstrings can remaing single line: """Single line text"""

messagehandler.py

- Handler has a "plugin" field that is unused.

- Instead of having the PRIORITY constants being global and then copying them into each instance of MessageHandler, make them class fields of MessageHandler itself (and every instance can see them "for free").

- In do_callback(), minor wording change: it says "if the same plugin is registered for two groups, it will be called twice". This isn't clear on the fact that different *instances* of the Plugin will be called, once in each "context" (i.e. group).

- Same function, might be worth abstracting the concept of "active names" so that the same code can handle both rooms and chat. By abstract, I mean "move into usermanagement" - because that is where this kind of logic belongs (messagehandler doesn't want to faff with these kinds of specifics).

- You removed "prepare_response()". Any reason? It was intended to be a shortcut to creating a new Message() object.

Actually, now that I think about it, maybe what would be good is if plugins could do:

self.messagehandler.Message(to, msg)

i.e. a method called "Message" on the messagehandler that wraps the Message constructor by passing in the right MessageHandler and being generally helpful where it can. Thoughts?

pluginmanager.py

- I think this is a general comment, but "room" and "group" are magic strings - i.e. enums. Would be good to define constants for them somewhere (probably usermanagement.py?)

- Maybe not for this diff, but the pluginmanager APIs should also be moved behind a self.pluginmanager object (e.g. list_plugins)

blacklist.py

- get_blacklist can be marked as a @classmethod

- You've changed from discard() to remove(). This means you'll now throw exceptions if things get a little out of sync. Probably best to keep it as discard().

invite.py

- Lacks a help string.

- You've made the commands quite... commandliney. Wouldn't it be nicer to use more English language-style commands, like the other plugins (see pubpicker!) e.g. invite {me|all|<name>+}...

Read more...

Revision history for this message
Ben Hutchings (ben-hutchings) wrote :

I had a chat with Chuck about this earlier.

Mainly it went because it was only being used by the hi5 plugin in a way
that was just a more verbose version of self.messagehandler.send_chat.

On the whole, why would a plugin need to create such a message object
outside of a .send or .reply function of some kind? The only thing you
could ultimately do with the message is send it and we already have an
API for that.

Ben

On 01/08/2013 17:11, Martin Morrison wrote:
> - You removed "prepare_response()". Any reason? It was intended to be a shortcut to creating a new Message() object.
>
> Actually, now that I think about it, maybe what would be good is if plugins could do:
>
> self.messagehandler.Message(to, msg)
>
> i.e. a method called "Message" on the messagehandler that wraps the Message constructor by passing in the right MessageHandler and being generally helpful where it can. Thoughts?

lp:~ben-hutchings/endroid/roomowner updated
57. By Ben Hutchings

- Made all changes in second half of diff notes with a couple of exceptions:
  - formatting changes (line wrap, multiline comments etc) will be done in a different diff.
  - the logic in messagehandler's do_callback method relating to rooms and groups is still there, though it should probably be moved to usermanagement at some point
  - prepare_response is still gone

58. By Ben Hutchings

- fixed room memberships (this was failing as a result of the configuration not being set properly due to a roomconfig_roomsecret parameter being passed (as None) with roomconfig_passwordprotectedroom as False) Both the configuraiton problem and the lack of an error message have been fixed.

59. By Ben Hutchings

- Users using multiple resources now seems to work fine
- Invite plugin gives more helpful error messages

60. By Ben Hutchings

- made the diff changes (added some comments/docstrings, changed a couple of names)

61. By Ben Hutchings

- renamed the usermanagement get_user_* functions to get_* (this accounts for changes in many files)
- removed some unused imports
- added some docstrings

- fixed friending/unfriending behaviour (note: this required updating wokkel to the latest version)

62. By Ben Hutchings

- fixed chat filters in usermanagement (was previously looking up a user jid rather than group names)
- added a broadcast plugin which uses filters to send endroid's messages to all available resources if a user wants.

63. By Ben Hutchings

- actually added broadcast plugin
- got rid of someunnecessary logging in messagehandler
- fixed a couple of typos/copy/paste errors in messagehandler

64. By Ben Hutchings

- made the diff changes
- a couple of doc-string modifications
- fixed some small 'bugs' in message.reply methods

65. By Ben Hutchings

- Changed some comments, added some helphints in broadcast plugin
- Updated the wokkel egg and dependency list to version 0.7.1
- Updated the sample config file to the new format
- Added the endroid_echo script to bin

66. By Ben Hutchings

- reverted contact details in conf file

67. By Ben Hutchings

- Fixed a potential bug in Cron whereby a KeyError could be raised after a restart of Endroid when Cron attempts to call a function which is no longer in its registration dictionary.
- Added getAtTimes and getTimeouts to cron which display functions schedule with doAtTime and setTimeout respectively.

68. By Ben Hutchings

- Changed a work in messagehandlers do_callback logging so that the log feedback actually makes sense (it is printed in reverse but was refereed to in the present rather than past tense)

69. By Ben Hutchings

- Added removeTask(reg_name) method to Cron which will delete all entries in the database matching reg_name
- register now has a persistent option which if False, calls removeTask before registering the function.

70. By Ben Hutchings

- Some parameter name and docstring changes as suggested by the doc review.
- Added the passthebomb plugin

71. By Ben Hutchings

- Updated the example config file
- Updated the endroid.sh script
- nick, groups and rooms are now optional in the config file
- staticmethods -> classmethods in Passthebomb plugin where appropriate

72. By Ben Hutchings

- changed 'name' paramaters to 'user' in some of usermanagement's get_ functions.
- added times to the console log in __init__

73. By Ben Hutchings

- updated endroid.install to use the latest version of wokkel

Revision history for this message
Martin Morrison (isoschiz) wrote :

Looking pretty good now! A few comments below, but nothing major.

bin/endroid:

- Given we are frigging with the PYTHONPATH anyway, we should add "~/.endroid/plugins/" to it, *before* the /usr/lib/ entry.

- This can also change to python -m endroid now that we have __main__.py

endroid.conf

- The plugins were/are listed alphabetically. Move roomowner to the right place.

- There's a missing newline at the end of the file

- It's odd to have example roomowner plugin config in here, but no other plugins. Probably move this to the roomowner docs instead.

setup.py

- This file wants to keep its +x permissions ;-)

endroid/__init__.py

- Line 2026 can go (extra LOGGING_FORMAT)

- Logging for groups and rooms is noisy (many lines). Simple change to ", ".join(rooms) will improve that.

endroid/confparser.py

- The literal_eval thing is cool. Except when it comes to bools. This should be made clear in the docstring.

- get() doesn't need to take arbitrary kwargs. If it does though, it should verify that it doesn't get any unexpected ones (swallowing get("foo", defult=True) silently is Evil)

cron.py

- The description of cron in the docstring is a bit misleading. Need to be explicit that .register() returns a Task object on which the later funcs can be called.

- There's no real need to change the table field name from fun_name to reg_name (even if it is more accurate). It just makes it not backwards compatible. Revert this.

- Docstring on register is not formatted correctly. It also doesn't really explain that it returns a Task object that can be used to get the callback called in the future.

- In removeTask instead of pop, use del.

database.py

- Database docstring not formatted correctly.

manhole.py

- start_manhole docstring not formatted correctly.

blacklist.py

- The get_blacklist() method is superfluous. Can get rid of it and access _blacklist directly

- Use discard instead of remove (list 3839)

broadcast.py

- Think this should be added separately in a different branch.

invite.py

- Plugin should be named Invite to be consistent!

- Helphints need the command keywords removing (they are already added by Command)

rosterhandler.py

- in purge_roster (in connectionInitialized) no need to pass self through - it's a closure with access to that anyway.

- You've added a set_available() method that does nothing. Remove it again.

usermanagement.py

- The default roomname shouldn't be Ensoft. EnDroid would do.

Revision history for this message
Ben Hutchings (ben-hutchings) wrote :

I'll do the doc changes in a separate branch.
Also the plugin enhancements (as they all work now I want to avoid
changing too many things).

Comments inline.

On 10/08/2013 22:12, Martin Morrison wrote:
>
> endroid/__init__.py
>
> - Line 2026 can go (extra LOGGING_FORMAT)
>
> - Logging for groups and rooms is noisy (many lines). Simple change to ", ".join(rooms) will improve that.
Will put on wiki page as an enhancement.
> endroid/confparser.py
>
> - The literal_eval thing is cool. Except when it comes to bools. This should be made clear in the docstring.
Will do in docs branch
> - get() doesn't need to take arbitrary kwargs. If it does though, it should verify that it doesn't get any unexpected ones (swallowing get("foo", defult=True) silently is Evil)
Not sure how best to do this. Will put in bugs section on wiki.
> cron.py
>
> - The description of cron in the docstring is a bit misleading. Need to be explicit that .register() returns a Task object on which the later funcs can be called.
Docs branch
>
> - There's no real need to change the table field name from fun_name to reg_name (even if it is more accurate). It just makes it not backwards compatible. Revert this.
It just makes things consistent. (Rather than having conditions like
{'fun_name' : reg_name} which make things harder to understand in an
already confusing module...

(And I thought we were going to start over with a clean database anyway?)
>
> - Docstring on register is not formatted correctly. It also doesn't really explain that it returns a Task object that can be used to get the callback called in the future.
>
> - In removeTask instead of pop, use del.
>
> database.py
>
> - Database docstring not formatted correctly.
>
> manhole.py
In doc branch
>
> - start_manhole docstring not formatted correctly.
>
> blacklist.py
>
> - The get_blacklist() method is superfluous. Can get rid of it and access _blacklist directly
>
> - Use discard instead of remove (list 3839)
These can be done in a seperate plugin enhancement branch
>
> broadcast.py
>
> - Think this should be added separately in a different branch.
Will do
>
> invite.py
>
> - Plugin should be named Invite to be consistent!
>
> - Helphints need the command keywords removing (they are already added by Command)
Again in separate plugin branch
>
> rosterhandler.py
>
> - in purge_roster (in connectionInitialized) no need to pass self through - it's a closure with access to that anyway.
>
> - You've added a set_available() method that does nothing. Remove it again.
>
> usermanagement.py
>
> - The default roomname shouldn't be Ensoft. EnDroid would do.
Done
Ben

lp:~ben-hutchings/endroid/roomowner updated
74. By Ben Hutchings

-fixed plugins not working in rooms (this came from a name conflict in usermanagement.get_nickname)
-fixed potential bug in wokkelhandler.receivedGroupChat which would occur if a resource was specified as part of the jid in the config file.

-updated blacklist and ratelimit to use the self.cron rather than Cron().get() API.

-config "pconfig" section name is now "plugin"

75. By Ben Hutchings

-actually fixed ratelimit and blacklist (and a 'bug' in ratelimit caused by the get_userhost method in usermanagement not dealing with getting a non-string item.)

-updated config file
-removed broadcast plugin to be added in another branch

-other markup notes will be put on wiki/done in different diffs

76. By Ben Hutchings

-merged slimey's fixes

77. By Ben Hutchings

-merged in memo plugin fixes + command decorator

78. By Ben Hutchings

-merged invite help fixes

79. By Chris Davidson <email address hidden>

Fix some issues with the command changes also, handle messages from groups more gracefully

80. By Chris Davidson <email address hidden>

Another command fix

81. By Ben Hutchings

-fixed a bug in configparser by which sections of config could be overwritten by subsequent sections

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=== modified file 'bin/endroid'
2--- bin/endroid 2012-08-02 10:49:57 +0000
3+++ bin/endroid 2013-08-12 17:50:48 +0000
4@@ -1,2 +1,2 @@
5 #!/bin/sh
6-PYTHONPATH="/usr/lib/endroid/dependencies/wokkel-0.7.0-py2.7.egg":"/usr/lib/endroid/plugins":"${PYTHONPATH}" python -c "import endroid; endroid.main()" $@
7+PYTHONPATH="~/.endroid/plugins/":"/usr/lib/endroid/dependencies/wokkel-0.7.1-py2.7.egg":"/usr/lib/endroid/plugins":"${PYTHONPATH}" python -m endroid $@
8
9=== added file 'bin/endroid_echo'
10--- bin/endroid_echo 1970-01-01 00:00:00 +0000
11+++ bin/endroid_echo 2013-08-12 17:50:48 +0000
12@@ -0,0 +1,39 @@
13+#!/usr/bin/python
14+
15+import os
16+import sys
17+import urllib
18+
19+KEY_ENV_VAR = 'ENDROID_REMOTE_KEY' # eg XACKJAASKJDAHD
20+JID_ENV_VAR = 'ENDROID_REMOTE_USER' # eg frodo@shire.org
21+URL_ENV_VAR = 'ENDROID_REMOTE_URL' # eg http://127.0.0.1:8880/remote/
22+
23+class UserError(Exception):
24+ pass
25+
26+try:
27+ if len(sys.argv) < 2:
28+ raise UserError("Usage: %s <Message>" % sys.argv[0])
29+
30+ if JID_ENV_VAR not in os.environ:
31+ raise UserError("Target user env var (%s) not set" % JID_ENV_VAR)
32+
33+ if KEY_ENV_VAR not in os.environ:
34+ raise UserError("Message key env var (%s) not set" % KEY_ENV_VAR)
35+
36+ if URL_ENV_VAR not in os.environ:
37+ raise UserError("Endroid URL env var (%s) not set" % URL_ENV_VAR)
38+
39+ params = {'user': os.environ[JID_ENV_VAR],
40+ 'key': os.environ[KEY_ENV_VAR],
41+ 'message': ' '.join(sys.argv[1:])}
42+
43+ f = urllib.urlopen(os.environ[URL_ENV_VAR], urllib.urlencode(params))
44+ for line in f.readlines():
45+ line = line.strip()
46+ if "Error:" in line:
47+ print line
48+
49+except UserError as e:
50+ print e
51+
52
53=== modified file 'debian/endroid.install'
54--- debian/endroid.install 2012-09-02 20:45:27 +0000
55+++ debian/endroid.install 2013-08-12 17:50:48 +0000
56@@ -1,4 +1,5 @@
57 etc/endroid.conf etc/endroid/
58 etc/init/endroid.conf etc/init/
59 bin/endroid usr/bin/
60-lib/wokkel-0.7.0-py2.7.egg usr/lib/endroid/dependencies
61+bin/endroid_echo usr/bin/
62+lib/wokkel-0.7.1-py2.7.egg usr/lib/endroid/dependencies
63
64=== modified file 'etc/endroid.conf' (properties changed: -x to +x)
65--- etc/endroid.conf 2012-09-02 21:40:31 +0000
66+++ etc/endroid.conf 2013-08-12 17:50:48 +0000
67@@ -1,26 +1,54 @@
68+# Comments will be ignored provided the hash is in the first column
69+
70 [Setup]
71 jid = marvin@hhgg.tld/planet
72-secret = secret
73-nick = Marvin
74-
75-plugins =
76+password = secret
77+# EnDroid's default nickname, defaults to the part of jid before the @
78+# nick = Marvin
79+
80+# EnDroid's full contact list. Users on this list will be added as friends,
81+# users not on this list will be removed from contacts and will be unable
82+# to communicate with EnDroid.
83+users=
84+
85+# What rooms EnDroid will attempt to create and join. Defaults to []
86+# rooms = room1@ser.ver,
87+
88+# What usergroup EnDroid will register plugins with. Defaults to 'all,'
89+# groups = all, admins
90+
91+# Detailed logging will be kept here - if something goes wrong and does not
92+# display an error in the console, look here.
93+logfile=
94+ ~/.endroid/endroid.log
95+
96+[Database]
97+dbfile=/var/lib/endroid/db/endroid.db
98+
99+# This list of settings will be applied in groups and rooms with any name (*)
100+[ group | room : *]
101+plugins=
102+# the roomowner plugin is necessary for room configuration to take place
103+ endroid.plugins.roomowner
104+# helper plugins depended on by others
105+ endroid.plugins.command
106+ endroid.plugins.httpinterface
107+ endroid.plugins.patternmatcher
108+# management plugins
109 endroid.plugins.blacklist
110+ endroid.plugins.help
111+ endroid.plugins.ratelimit
112+ endroid.plugins.unhandled
113+ endroid.plugins.whosonline
114+# others
115 endroid.plugins.chuck
116- endroid.plugins.command
117 endroid.plugins.compute
118 endroid.plugins.coolit
119 endroid.plugins.correct
120- endroid.plugins.echobot
121- endroid.plugins.help
122 endroid.plugins.memo
123- endroid.plugins.patternmatcher
124- endroid.plugins.pubpicker
125- endroid.plugins.ratelimit
126 endroid.plugins.speak
127- endroid.plugins.spell
128 endroid.plugins.theyfightcrime
129- endroid.plugins.unhandled
130- endroid.plugins.whosonline
131+# endroid.plugins.echobot # this spams everywhere
132
133 users=
134 rooms=
135@@ -28,6 +56,24 @@
136 [Database]
137 dbfile=/var/lib/endroid/db/endroid.db
138
139-[UserGroup:*]
140-
141-[Room:*]
142+[group:*]
143+
144+[room:*]
145+
146+# HTTP remote messaging feature (via 'httpinterface' and 'remote' plugins)
147+#
148+# port is the port (defaults to 8880)
149+# interface is the interface (defaults to 127.0.0.1, ie accessible only
150+# from the local host. Set to 0.0.0.0 to enable access from anywhere).
151+
152+[ group | room : * : plugin : endroid.plugins.httpinterface ]
153+#port = 8881
154+#interface = 0.0.0.0
155+
156+
157+# Wolfram Alpha compute plugin. Get an API key for free at
158+# https://developer.wolframalpha.com/portal/apisignup.html
159+# and then put it below - good for 2000 free queries/month.
160+
161+[ group | room : * : plugin : endroid.plugins.compute ]
162+#api_key = 123ABC-DE45FGJIJK
163
164=== removed file 'lib/wokkel-0.7.0-py2.7.egg'
165Binary files lib/wokkel-0.7.0-py2.7.egg 2012-08-02 10:49:57 +0000 and lib/wokkel-0.7.0-py2.7.egg 1970-01-01 00:00:00 +0000 differ
166=== added file 'lib/wokkel-0.7.1-py2.7.egg'
167Binary files lib/wokkel-0.7.1-py2.7.egg 1970-01-01 00:00:00 +0000 and lib/wokkel-0.7.1-py2.7.egg 2013-08-12 17:50:48 +0000 differ
168=== added file 'src/endroid.graphml'
169--- src/endroid.graphml 1970-01-01 00:00:00 +0000
170+++ src/endroid.graphml 2013-08-12 17:50:48 +0000
171@@ -0,0 +1,1841 @@
172+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
173+<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
174+ <!--Created by yFiles for Java 2.11-->
175+ <key for="graphml" id="d0" yfiles.type="resources"/>
176+ <key for="port" id="d1" yfiles.type="portgraphics"/>
177+ <key for="port" id="d2" yfiles.type="portgeometry"/>
178+ <key for="port" id="d3" yfiles.type="portuserdata"/>
179+ <key attr.name="url" attr.type="string" for="node" id="d4"/>
180+ <key attr.name="description" attr.type="string" for="node" id="d5"/>
181+ <key for="node" id="d6" yfiles.type="nodegraphics"/>
182+ <key attr.name="Description" attr.type="string" for="graph" id="d7"/>
183+ <key attr.name="url" attr.type="string" for="edge" id="d8"/>
184+ <key attr.name="description" attr.type="string" for="edge" id="d9"/>
185+ <key for="edge" id="d10" yfiles.type="edgegraphics"/>
186+ <graph edgedefault="directed" id="G">
187+ <data key="d7"/>
188+ <node id="n0" yfiles.foldertype="group">
189+ <data key="d4"/>
190+ <data key="d6">
191+ <y:ProxyAutoBoundsNode>
192+ <y:Realizers active="0">
193+ <y:GroupNode>
194+ <y:Geometry height="248.99004051482336" width="105.0" x="239.42528205128247" y="244.97959073517745"/>
195+ <y:Fill color="#F5F5F5" transparent="false"/>
196+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
197+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="105.0" x="0.0" y="0.0">utilities</y:NodeLabel>
198+ <y:Shape type="roundrectangle"/>
199+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
200+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
201+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
202+ </y:GroupNode>
203+ <y:GroupNode>
204+ <y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
205+ <y:Fill color="#F5F5F5" transparent="false"/>
206+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
207+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="59.02685546875" x="-4.513427734375" y="0.0">Folder 8</y:NodeLabel>
208+ <y:Shape type="roundrectangle"/>
209+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
210+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
211+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
212+ </y:GroupNode>
213+ </y:Realizers>
214+ </y:ProxyAutoBoundsNode>
215+ </data>
216+ <graph edgedefault="directed" id="n0:">
217+ <node id="n0::n0">
218+ <data key="d5"><![CDATA[Owned by Endroid]]></data>
219+ <data key="d6">
220+ <y:ShapeNode>
221+ <y:Geometry height="40.0" width="75.0" x="254.42528205128247" y="383.3142466346162"/>
222+ <y:Fill color="#3366FF" transparent="false"/>
223+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
224+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="55.369140625" x="9.8154296875" y="10.6494140625">Database<y:LabelModel>
225+ <y:SmartNodeLabelModel distance="4.0"/>
226+ </y:LabelModel>
227+ <y:ModelParameter>
228+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
229+ </y:ModelParameter>
230+ </y:NodeLabel>
231+ <y:Shape type="rectangle"/>
232+ </y:ShapeNode>
233+ </data>
234+ </node>
235+ <node id="n0::n1">
236+ <data key="d6">
237+ <y:ShapeNode>
238+ <y:Geometry height="40.0" width="75.0" x="254.42528205128247" y="438.9696312500008"/>
239+ <y:Fill color="#FFCC00" transparent="false"/>
240+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
241+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="30.009765625" x="22.4951171875" y="10.6494140625">Cron<y:LabelModel>
242+ <y:SmartNodeLabelModel distance="4.0"/>
243+ </y:LabelModel>
244+ <y:ModelParameter>
245+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
246+ </y:ModelParameter>
247+ </y:NodeLabel>
248+ <y:Shape type="rectangle"/>
249+ </y:ShapeNode>
250+ </data>
251+ </node>
252+ <node id="n0::n2">
253+ <data key="d5"><![CDATA[Owned by Endroid]]></data>
254+ <data key="d6">
255+ <y:ShapeNode>
256+ <y:Geometry height="40.0" width="75.0" x="254.42528205128247" y="332.83515110677183"/>
257+ <y:Fill color="#3366FF" transparent="false"/>
258+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
259+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="39.34375" x="17.828125" y="10.6494140625">Parser<y:LabelModel>
260+ <y:SmartNodeLabelModel distance="4.0"/>
261+ </y:LabelModel>
262+ <y:ModelParameter>
263+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
264+ </y:ModelParameter>
265+ </y:NodeLabel>
266+ <y:Shape type="rectangle"/>
267+ </y:ShapeNode>
268+ </data>
269+ </node>
270+ <node id="n0::n3">
271+ <data key="d6">
272+ <y:ShapeNode>
273+ <y:Geometry height="40.0" width="75.0" x="254.42528205128247" y="282.35605557892745"/>
274+ <y:Fill color="#3366FF" transparent="false"/>
275+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
276+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="50.03125" x="12.484375" y="10.6494140625">Manhole<y:LabelModel>
277+ <y:SmartNodeLabelModel distance="4.0"/>
278+ </y:LabelModel>
279+ <y:ModelParameter>
280+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
281+ </y:ModelParameter>
282+ </y:NodeLabel>
283+ <y:Shape type="rectangle"/>
284+ </y:ShapeNode>
285+ </data>
286+ </node>
287+ </graph>
288+ </node>
289+ <node id="n1" yfiles.foldertype="group">
290+ <data key="d4"/>
291+ <data key="d6">
292+ <y:ProxyAutoBoundsNode>
293+ <y:Realizers active="0">
294+ <y:GroupNode>
295+ <y:Geometry height="586.2619894030446" width="592.9340000000002" x="442.2021732772431" y="10.113173046875175"/>
296+ <y:Fill color="#F5F5F5" transparent="false"/>
297+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
298+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="592.9340000000002" x="0.0" y="0.0">Endroid Core</y:NodeLabel>
299+ <y:Shape type="roundrectangle"/>
300+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
301+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
302+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
303+ </y:GroupNode>
304+ <y:GroupNode>
305+ <y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
306+ <y:Fill color="#F5F5F5" transparent="false"/>
307+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
308+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="59.02685546875" x="-4.513427734375" y="0.0">Folder 9</y:NodeLabel>
309+ <y:Shape type="roundrectangle"/>
310+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
311+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
312+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
313+ </y:GroupNode>
314+ </y:Realizers>
315+ </y:ProxyAutoBoundsNode>
316+ </data>
317+ <graph edgedefault="directed" id="n1:">
318+ <node id="n1::n0" yfiles.foldertype="group">
319+ <data key="d4"/>
320+ <data key="d6">
321+ <y:ProxyAutoBoundsNode>
322+ <y:Realizers active="0">
323+ <y:GroupNode>
324+ <y:Geometry height="152.87646484375" width="252.09200000000033" x="457.2021732772431" y="276.0466120793269"/>
325+ <y:Fill color="#F5F5F5" transparent="false"/>
326+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
327+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="252.09200000000033" x="0.0" y="0.0">usermanagement.py</y:NodeLabel>
328+ <y:Shape type="roundrectangle"/>
329+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
330+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
331+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
332+ </y:GroupNode>
333+ <y:GroupNode>
334+ <y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
335+ <y:Fill color="#F5F5F5" transparent="false"/>
336+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
337+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="59.02685546875" x="-4.513427734375" y="0.0">Folder 1</y:NodeLabel>
338+ <y:Shape type="roundrectangle"/>
339+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
340+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
341+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
342+ </y:GroupNode>
343+ </y:Realizers>
344+ </y:ProxyAutoBoundsNode>
345+ </data>
346+ <graph edgedefault="directed" id="n1::n0:">
347+ <node id="n1::n0::n0">
348+ <data key="d5"><![CDATA[Owned by Endroid]]></data>
349+ <data key="d6">
350+ <y:ShapeNode>
351+ <y:Geometry height="59.0" width="123.00000000000011" x="571.2941732772433" y="335.352"/>
352+ <y:Fill color="#3366FF" transparent="false"/>
353+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
354+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="99.37890625" x="11.810546875" y="20.1494140625">UserManagement<y:LabelModel>
355+ <y:SmartNodeLabelModel distance="4.0"/>
356+ </y:LabelModel>
357+ <y:ModelParameter>
358+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
359+ </y:ModelParameter>
360+ </y:NodeLabel>
361+ <y:Shape type="rectangle"/>
362+ </y:ShapeNode>
363+ </data>
364+ </node>
365+ <node id="n1::n0::n1">
366+ <data key="d6">
367+ <y:ShapeNode>
368+ <y:Geometry height="40.0" width="58.0" x="472.2021732772431" y="373.9230769230769"/>
369+ <y:Fill color="#FFCC00" transparent="false"/>
370+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
371+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="36.009765625" x="10.9951171875" y="10.6494140625">Room<y:LabelModel>
372+ <y:SmartNodeLabelModel distance="4.0"/>
373+ </y:LabelModel>
374+ <y:ModelParameter>
375+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
376+ </y:ModelParameter>
377+ </y:NodeLabel>
378+ <y:Shape type="rectangle"/>
379+ </y:ShapeNode>
380+ </data>
381+ </node>
382+ <node id="n1::n0::n2">
383+ <data key="d6">
384+ <y:ShapeNode>
385+ <y:Geometry height="40.0" width="58.0" x="472.2021732772431" y="313.4230769230769"/>
386+ <y:Fill color="#FFCC00" transparent="false"/>
387+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
388+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="39.34375" x="9.328125" y="10.6494140625">Roster<y:LabelModel>
389+ <y:SmartNodeLabelModel distance="4.0"/>
390+ </y:LabelModel>
391+ <y:ModelParameter>
392+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
393+ </y:ModelParameter>
394+ </y:NodeLabel>
395+ <y:Shape type="rectangle"/>
396+ </y:ShapeNode>
397+ </data>
398+ </node>
399+ </graph>
400+ </node>
401+ <node id="n1::n1" yfiles.foldertype="group">
402+ <data key="d4"/>
403+ <data key="d6">
404+ <y:ProxyAutoBoundsNode>
405+ <y:Realizers active="0">
406+ <y:GroupNode>
407+ <y:Geometry height="111.37646484375" width="254.84199999999998" x="765.2941732772433" y="297.97553515625"/>
408+ <y:Fill color="#F5F5F5" transparent="false"/>
409+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
410+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="254.84199999999998" x="0.0" y="0.0">messagehandler.py</y:NodeLabel>
411+ <y:Shape type="roundrectangle"/>
412+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
413+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
414+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
415+ </y:GroupNode>
416+ <y:GroupNode>
417+ <y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
418+ <y:Fill color="#F5F5F5" transparent="false"/>
419+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
420+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="59.02685546875" x="-4.513427734375" y="0.0">Folder 2</y:NodeLabel>
421+ <y:Shape type="roundrectangle"/>
422+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
423+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
424+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
425+ </y:GroupNode>
426+ </y:Realizers>
427+ </y:ProxyAutoBoundsNode>
428+ </data>
429+ <graph edgedefault="directed" id="n1::n1:">
430+ <node id="n1::n1::n0">
431+ <data key="d5"><![CDATA[Owned by Endroid]]></data>
432+ <data key="d6">
433+ <y:ShapeNode>
434+ <y:Geometry height="59.0" width="123.0" x="780.2941732772433" y="335.352"/>
435+ <y:Fill color="#3366FF" transparent="false"/>
436+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
437+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="94.71484375" x="14.142578125" y="20.1494140625">MessageHandler<y:LabelModel>
438+ <y:SmartNodeLabelModel distance="4.0"/>
439+ </y:LabelModel>
440+ <y:ModelParameter>
441+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
442+ </y:ModelParameter>
443+ </y:NodeLabel>
444+ <y:Shape type="rectangle"/>
445+ </y:ShapeNode>
446+ </data>
447+ </node>
448+ <node id="n1::n1::n1">
449+ <data key="d6">
450+ <y:ShapeNode>
451+ <y:Geometry height="40.0" width="58.0" x="947.1361732772433" y="344.852"/>
452+ <y:Fill color="#FFCC00" transparent="false"/>
453+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
454+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="52.69140625" x="2.654296875" y="10.6494140625">Message<y:LabelModel>
455+ <y:SmartNodeLabelModel distance="4.0"/>
456+ </y:LabelModel>
457+ <y:ModelParameter>
458+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
459+ </y:ModelParameter>
460+ </y:NodeLabel>
461+ <y:Shape type="rectangle"/>
462+ </y:ShapeNode>
463+ </data>
464+ </node>
465+ </graph>
466+ </node>
467+ <node id="n1::n2" yfiles.foldertype="group">
468+ <data key="d4"/>
469+ <data key="d6">
470+ <y:ProxyAutoBoundsNode>
471+ <y:Realizers active="0">
472+ <y:GroupNode>
473+ <y:Geometry height="190.87646484375" width="276.00000000000045" x="579.2261732772433" y="47.489637890625175"/>
474+ <y:Fill color="#F5F5F5" transparent="false"/>
475+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
476+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="276.00000000000045" x="0.0" y="0.0">pluginmanager.py</y:NodeLabel>
477+ <y:Shape type="roundrectangle"/>
478+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
479+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
480+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
481+ </y:GroupNode>
482+ <y:GroupNode>
483+ <y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
484+ <y:Fill color="#F5F5F5" transparent="false"/>
485+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
486+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="59.02685546875" x="-4.513427734375" y="0.0">Folder 3</y:NodeLabel>
487+ <y:Shape type="roundrectangle"/>
488+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
489+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
490+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
491+ </y:GroupNode>
492+ </y:Realizers>
493+ </y:ProxyAutoBoundsNode>
494+ </data>
495+ <graph edgedefault="directed" id="n1::n2:">
496+ <node id="n1::n2::n0">
497+ <data key="d6">
498+ <y:ShapeNode>
499+ <y:Geometry height="59.0" width="123.00000000000011" x="717.2261732772437" y="164.36610273437518"/>
500+ <y:Fill color="#FFCC00" transparent="false"/>
501+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
502+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="84.71875" x="19.140625000000114" y="20.1494140625">PluginManager<y:LabelModel>
503+ <y:SmartNodeLabelModel distance="4.0"/>
504+ </y:LabelModel>
505+ <y:ModelParameter>
506+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
507+ </y:ModelParameter>
508+ </y:NodeLabel>
509+ <y:Shape type="rectangle"/>
510+ </y:ShapeNode>
511+ </data>
512+ </node>
513+ <node id="n1::n2::n1">
514+ <data key="d6">
515+ <y:ShapeNode>
516+ <y:Geometry height="40.0" width="87.00000000000023" x="594.2261732772433" y="84.86610273437518"/>
517+ <y:Fill color="#FFCC00" transparent="false"/>
518+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
519+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="37.357421875" x="24.821289062500114" y="10.6494140625">Plugin<y:LabelModel>
520+ <y:SmartNodeLabelModel distance="4.0"/>
521+ </y:LabelModel>
522+ <y:ModelParameter>
523+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
524+ </y:ModelParameter>
525+ </y:NodeLabel>
526+ <y:Shape type="rectangle"/>
527+ </y:ShapeNode>
528+ </data>
529+ </node>
530+ <node id="n1::n2::n2">
531+ <data key="d6">
532+ <y:ShapeNode>
533+ <y:Geometry height="40.0" width="87.00000000000023" x="594.2261732772433" y="173.86610273437518"/>
534+ <y:Fill color="#FFCC00" transparent="false"/>
535+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
536+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="64.03515625" x="11.482421875000114" y="10.6494140625">PluginMeta<y:LabelModel>
537+ <y:SmartNodeLabelModel distance="4.0"/>
538+ </y:LabelModel>
539+ <y:ModelParameter>
540+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
541+ </y:ModelParameter>
542+ </y:NodeLabel>
543+ <y:Shape type="rectangle"/>
544+ </y:ShapeNode>
545+ </data>
546+ </node>
547+ <node id="n1::n2::n3">
548+ <data key="d6">
549+ <y:ShapeNode>
550+ <y:Geometry height="40.0" width="87.00000000000023" x="735.2261732772436" y="87.17787031250018"/>
551+ <y:Fill color="#FFCC00" transparent="false"/>
552+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
553+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="68.03125" x="9.484375000000114" y="10.6494140625">PluginProxy<y:LabelModel>
554+ <y:SmartNodeLabelModel distance="4.0"/>
555+ </y:LabelModel>
556+ <y:ModelParameter>
557+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
558+ </y:ModelParameter>
559+ </y:NodeLabel>
560+ <y:Shape type="rectangle"/>
561+ </y:ShapeNode>
562+ </data>
563+ </node>
564+ </graph>
565+ </node>
566+ <node id="n1::n3">
567+ <data key="d5"><![CDATA[Owned by Endroid]]></data>
568+ <data key="d6">
569+ <y:ShapeNode>
570+ <y:Geometry height="59.0" width="123.0" x="566.7058267227567" y="522.3751624499198"/>
571+ <y:Fill color="#3366FF" transparent="false"/>
572+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
573+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="79.375" x="21.8125" y="20.1494140625">Rosterhandler<y:LabelModel>
574+ <y:SmartNodeLabelModel distance="4.0"/>
575+ </y:LabelModel>
576+ <y:ModelParameter>
577+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
578+ </y:ModelParameter>
579+ </y:NodeLabel>
580+ <y:Shape type="rectangle"/>
581+ </y:ShapeNode>
582+ </data>
583+ </node>
584+ <node id="n1::n4">
585+ <data key="d5"><![CDATA[Owned by Endroid]]></data>
586+ <data key="d6">
587+ <y:ShapeNode>
588+ <y:Geometry height="59.0" width="123.0" x="775.7058267227567" y="522.3751624499198"/>
589+ <y:Fill color="#3366FF" transparent="false"/>
590+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
591+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="85.36328125" x="18.818359375" y="20.1494140625">WokkelHandler<y:LabelModel>
592+ <y:SmartNodeLabelModel distance="4.0"/>
593+ </y:LabelModel>
594+ <y:ModelParameter>
595+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
596+ </y:ModelParameter>
597+ </y:NodeLabel>
598+ <y:Shape type="rectangle"/>
599+ </y:ShapeNode>
600+ </data>
601+ </node>
602+ </graph>
603+ </node>
604+ <node id="n2" yfiles.foldertype="group">
605+ <data key="d4"/>
606+ <data key="d6">
607+ <y:ProxyAutoBoundsNode>
608+ <y:Realizers active="0">
609+ <y:GroupNode>
610+ <y:Geometry height="111.37646484375" width="681.9340000000002" x="397.3638267227565" y="628.3983248998397"/>
611+ <y:Fill color="#F5F5F5" transparent="false"/>
612+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
613+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="681.9340000000002" x="0.0" y="0.0">Wokkel/Twisted</y:NodeLabel>
614+ <y:Shape type="roundrectangle"/>
615+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
616+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
617+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
618+ </y:GroupNode>
619+ <y:GroupNode>
620+ <y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
621+ <y:Fill color="#F5F5F5" transparent="false"/>
622+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
623+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="67.369140625" x="-8.6845703125" y="0.0">Folder 10</y:NodeLabel>
624+ <y:Shape type="roundrectangle"/>
625+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
626+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
627+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
628+ </y:GroupNode>
629+ </y:Realizers>
630+ </y:ProxyAutoBoundsNode>
631+ </data>
632+ <graph edgedefault="directed" id="n2:">
633+ <node id="n2::n0">
634+ <data key="d6">
635+ <y:ShapeNode>
636+ <y:Geometry height="59.0" width="117.50000000000011" x="785.4558267227567" y="665.7747897435897"/>
637+ <y:Fill color="#C0C0C0" transparent="false"/>
638+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
639+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="106.029296875" x="5.7353515625" y="20.1494140625">MUCClientProtocol<y:LabelModel>
640+ <y:SmartNodeLabelModel distance="4.0"/>
641+ </y:LabelModel>
642+ <y:ModelParameter>
643+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
644+ </y:ModelParameter>
645+ </y:NodeLabel>
646+ <y:Shape type="rectangle"/>
647+ </y:ShapeNode>
648+ </data>
649+ </node>
650+ <node id="n2::n1">
651+ <data key="d6">
652+ <y:ShapeNode>
653+ <y:Geometry height="59.0" width="117.50000000000011" x="946.7978267227568" y="665.7747897435897"/>
654+ <y:Fill color="#C0C0C0" transparent="false"/>
655+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
656+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="96.712890625" x="10.3935546875" y="20.1494140625">MessageProtocol<y:LabelModel>
657+ <y:SmartNodeLabelModel distance="4.0"/>
658+ </y:LabelModel>
659+ <y:ModelParameter>
660+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
661+ </y:ModelParameter>
662+ </y:NodeLabel>
663+ <y:Shape type="rectangle"/>
664+ </y:ShapeNode>
665+ </data>
666+ </node>
667+ <node id="n2::n2">
668+ <data key="d6">
669+ <y:ShapeNode>
670+ <y:Geometry height="59.0" width="117.50000000000011" x="412.3638267227565" y="665.7747897435897"/>
671+ <y:Fill color="#C0C0C0" transparent="false"/>
672+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
673+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="114.044921875" x="1.7275390625000568" y="20.1494140625">RosterClientProtocol<y:LabelModel>
674+ <y:SmartNodeLabelModel distance="4.0"/>
675+ </y:LabelModel>
676+ <y:ModelParameter>
677+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
678+ </y:ModelParameter>
679+ </y:NodeLabel>
680+ <y:Shape type="rectangle"/>
681+ </y:ShapeNode>
682+ </data>
683+ </node>
684+ <node id="n2::n3">
685+ <data key="d6">
686+ <y:ShapeNode>
687+ <y:Geometry height="59.0" width="117.50000000000011" x="573.7058267227567" y="665.7747897435897"/>
688+ <y:Fill color="#C0C0C0" transparent="false"/>
689+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
690+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="129.396484375" x="-5.9482421875" y="20.1494140625">PresenceClientProtocol<y:LabelModel>
691+ <y:SmartNodeLabelModel distance="4.0"/>
692+ </y:LabelModel>
693+ <y:ModelParameter>
694+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
695+ </y:ModelParameter>
696+ </y:NodeLabel>
697+ <y:Shape type="rectangle"/>
698+ </y:ShapeNode>
699+ </data>
700+ </node>
701+ </graph>
702+ </node>
703+ <node id="n3" yfiles.foldertype="group">
704+ <data key="d4"/>
705+ <data key="d6">
706+ <y:ProxyAutoBoundsNode>
707+ <y:Realizers active="0">
708+ <y:GroupNode>
709+ <y:Geometry height="92.37646484375" width="256.79252774439044" x="320.4179705528843" y="-649.0851333007811"/>
710+ <y:Fill color="#F5F5F5" transparent="false"/>
711+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
712+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="256.79252774439044" x="0.0" y="0.0">utilities</y:NodeLabel>
713+ <y:Shape type="roundrectangle"/>
714+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
715+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
716+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
717+ </y:GroupNode>
718+ <y:GroupNode>
719+ <y:Geometry height="50.0" width="50.0" x="299.55590940504777" y="-662.4584666341144"/>
720+ <y:Fill color="#F5F5F5" transparent="false"/>
721+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
722+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="59.02685546875" x="-4.513427734375" y="0.0">Folder 8</y:NodeLabel>
723+ <y:Shape type="roundrectangle"/>
724+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
725+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
726+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
727+ </y:GroupNode>
728+ </y:Realizers>
729+ </y:ProxyAutoBoundsNode>
730+ </data>
731+ <graph edgedefault="directed" id="n3:">
732+ <node id="n3::n0">
733+ <data key="d6">
734+ <y:ShapeNode>
735+ <y:Geometry height="40.0" width="75.0" x="335.4179705528843" y="-611.7086684570311"/>
736+ <y:Fill color="#FFCC00" transparent="false"/>
737+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
738+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="30.009765625" x="22.4951171875" y="10.6494140625">Cron<y:LabelModel>
739+ <y:SmartNodeLabelModel distance="4.0"/>
740+ </y:LabelModel>
741+ <y:ModelParameter>
742+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
743+ </y:ModelParameter>
744+ </y:NodeLabel>
745+ <y:Shape type="rectangle"/>
746+ </y:ShapeNode>
747+ </data>
748+ </node>
749+ <node id="n3::n1">
750+ <data key="d4"/>
751+ <data key="d5"><![CDATA[Owned by endroid]]></data>
752+ <data key="d6">
753+ <y:ShapeNode>
754+ <y:Geometry height="40.0" width="75.0" x="487.21049829727474" y="-611.7086684570311"/>
755+ <y:Fill color="#3366FF" transparent="false"/>
756+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
757+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="55.369140625" x="9.8154296875" y="10.6494140625">Database<y:LabelModel>
758+ <y:SmartNodeLabelModel distance="4.0"/>
759+ </y:LabelModel>
760+ <y:ModelParameter>
761+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
762+ </y:ModelParameter>
763+ </y:NodeLabel>
764+ <y:Shape type="rectangle"/>
765+ </y:ShapeNode>
766+ </data>
767+ </node>
768+ </graph>
769+ </node>
770+ <node id="n4" yfiles.foldertype="group">
771+ <data key="d4"/>
772+ <data key="d6">
773+ <y:ProxyAutoBoundsNode>
774+ <y:Realizers active="0">
775+ <y:GroupNode>
776+ <y:Geometry height="478.4713465482276" width="745.7458610777235" x="268.3931927083324" y="-498.3581735013523"/>
777+ <y:Fill color="#F5F5F5" transparent="false"/>
778+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
779+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="745.7458610777235" x="0.0" y="0.0">plugins</y:NodeLabel>
780+ <y:Shape type="roundrectangle"/>
781+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
782+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
783+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
784+ </y:GroupNode>
785+ <y:GroupNode>
786+ <y:Geometry height="50.0" width="50.0" x="249.70111127804404" y="-526.7086684570311"/>
787+ <y:Fill color="#F5F5F5" transparent="false"/>
788+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
789+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="59.02685546875" x="-4.513427734375" y="0.0">Folder 9</y:NodeLabel>
790+ <y:Shape type="roundrectangle"/>
791+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
792+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
793+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
794+ </y:GroupNode>
795+ </y:Realizers>
796+ </y:ProxyAutoBoundsNode>
797+ </data>
798+ <graph edgedefault="directed" id="n4:">
799+ <node id="n4::n0" yfiles.foldertype="group">
800+ <data key="d4"/>
801+ <data key="d6">
802+ <y:ProxyAutoBoundsNode>
803+ <y:Realizers active="0">
804+ <y:GroupNode>
805+ <y:Geometry height="185.37646484375" width="602.8558267227575" x="388.3593465544859" y="-220.26329179687474"/>
806+ <y:Fill color="#F5F5F5" transparent="false"/>
807+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
808+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="602.8558267227575" x="0.0" y="0.0">Helper Plugins</y:NodeLabel>
809+ <y:Shape type="roundrectangle"/>
810+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
811+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
812+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
813+ </y:GroupNode>
814+ <y:GroupNode>
815+ <y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
816+ <y:Fill color="#F5F5F5" transparent="false"/>
817+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
818+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="67.369140625" x="-8.6845703125" y="0.0">Folder 11</y:NodeLabel>
819+ <y:Shape type="roundrectangle"/>
820+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
821+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
822+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
823+ </y:GroupNode>
824+ </y:Realizers>
825+ </y:ProxyAutoBoundsNode>
826+ </data>
827+ <graph edgedefault="directed" id="n4::n0:">
828+ <node id="n4::n0::n0" yfiles.foldertype="group">
829+ <data key="d4"/>
830+ <data key="d6">
831+ <y:ProxyAutoBoundsNode>
832+ <y:Realizers active="0">
833+ <y:GroupNode>
834+ <y:Geometry height="111.37646484375" width="434.85582672275723" x="541.3593465544861" y="-161.26329179687474"/>
835+ <y:Fill color="#F5F5F5" transparent="false"/>
836+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
837+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="434.85582672275723" x="0.0" y="0.0">command.py</y:NodeLabel>
838+ <y:Shape type="roundrectangle"/>
839+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
840+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
841+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
842+ </y:GroupNode>
843+ <y:GroupNode>
844+ <y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
845+ <y:Fill color="#F5F5F5" transparent="false"/>
846+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
847+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="59.02685546875" x="-4.513427734375" y="0.0">Folder 4</y:NodeLabel>
848+ <y:Shape type="roundrectangle"/>
849+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
850+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
851+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
852+ </y:GroupNode>
853+ </y:Realizers>
854+ </y:ProxyAutoBoundsNode>
855+ </data>
856+ <graph edgedefault="directed" id="n4::n0::n0:">
857+ <node id="n4::n0::n0::n0">
858+ <data key="d6">
859+ <y:ShapeNode>
860+ <y:Geometry height="59.0" width="123.00000000000011" x="671.2151732772433" y="-123.88682695312474"/>
861+ <y:Fill color="#FFCC00" transparent="false"/>
862+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
863+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="92.7109375" x="15.144531250000114" y="20.1494140625">CommandPlugin<y:LabelModel>
864+ <y:SmartNodeLabelModel distance="4.0"/>
865+ </y:LabelModel>
866+ <y:ModelParameter>
867+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
868+ </y:ModelParameter>
869+ </y:NodeLabel>
870+ <y:Shape type="rectangle"/>
871+ </y:ShapeNode>
872+ </data>
873+ </node>
874+ <node id="n4::n0::n0::n1">
875+ <data key="d6">
876+ <y:ShapeNode>
877+ <y:Geometry height="40.0" width="137.0" x="824.2151732772434" y="-114.38682695312474"/>
878+ <y:Fill color="#FFCC00" transparent="false"/>
879+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
880+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="119.388671875" x="8.8056640625" y="10.6494140625">CommandPluginMeta<y:LabelModel>
881+ <y:SmartNodeLabelModel distance="4.0"/>
882+ </y:LabelModel>
883+ <y:ModelParameter>
884+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
885+ </y:ModelParameter>
886+ </y:NodeLabel>
887+ <y:Shape type="rectangle"/>
888+ </y:ShapeNode>
889+ </data>
890+ </node>
891+ <node id="n4::n0::n0::n2">
892+ <data key="d6">
893+ <y:ShapeNode>
894+ <y:Geometry height="59.0" width="84.85582672275712" x="556.3593465544861" y="-123.88682695312474"/>
895+ <y:Fill color="#FFCC00" transparent="false"/>
896+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
897+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="59.353515625" x="12.751155548878614" y="20.1494140625">Command<y:LabelModel>
898+ <y:SmartNodeLabelModel distance="4.0"/>
899+ </y:LabelModel>
900+ <y:ModelParameter>
901+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
902+ </y:ModelParameter>
903+ </y:NodeLabel>
904+ <y:Shape type="rectangle"/>
905+ </y:ShapeNode>
906+ </data>
907+ </node>
908+ </graph>
909+ </node>
910+ <node id="n4::n0::n1">
911+ <data key="d6">
912+ <y:ShapeNode>
913+ <y:Geometry height="59.0" width="123.00000000000023" x="403.3593465544859" y="-108.88682695312474"/>
914+ <y:Fill color="#FFCC00" transparent="false"/>
915+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
916+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="86.037109375" x="18.481445312500114" y="20.1494140625">PatternMatcher<y:LabelModel>
917+ <y:SmartNodeLabelModel distance="4.0"/>
918+ </y:LabelModel>
919+ <y:ModelParameter>
920+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
921+ </y:ModelParameter>
922+ </y:NodeLabel>
923+ <y:Shape type="rectangle"/>
924+ </y:ShapeNode>
925+ </data>
926+ </node>
927+ <node id="n4::n0::n2">
928+ <data key="d6">
929+ <y:ShapeNode>
930+ <y:Geometry height="59.0" width="123.00000000000023" x="403.3593465544859" y="-182.88682695312474"/>
931+ <y:Fill color="#FFCC00" transparent="false"/>
932+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
933+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="70.708984375" x="26.145507812500114" y="20.1494140625">httpInterface<y:LabelModel>
934+ <y:SmartNodeLabelModel distance="4.0"/>
935+ </y:LabelModel>
936+ <y:ModelParameter>
937+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
938+ </y:ModelParameter>
939+ </y:NodeLabel>
940+ <y:Shape type="rectangle"/>
941+ </y:ShapeNode>
942+ </data>
943+ </node>
944+ </graph>
945+ </node>
946+ <node id="n4::n1" yfiles.foldertype="group">
947+ <data key="d4"/>
948+ <data key="d6">
949+ <y:ProxyAutoBoundsNode>
950+ <y:Realizers active="0">
951+ <y:GroupNode>
952+ <y:Geometry height="143.57623027343732" width="289.5156773838148" x="709.6233764022411" y="-381.95573876953114"/>
953+ <y:Fill color="#F5F5F5" transparent="false"/>
954+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
955+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="289.5156773838148" x="0.0" y="0.0">command plugins</y:NodeLabel>
956+ <y:Shape type="roundrectangle"/>
957+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
958+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
959+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
960+ </y:GroupNode>
961+ <y:GroupNode>
962+ <y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
963+ <y:Fill color="#F5F5F5" transparent="false"/>
964+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
965+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="59.02685546875" x="-4.513427734375" y="0.0">Folder 6</y:NodeLabel>
966+ <y:Shape type="roundrectangle"/>
967+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
968+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
969+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
970+ </y:GroupNode>
971+ </y:Realizers>
972+ </y:ProxyAutoBoundsNode>
973+ </data>
974+ <graph edgedefault="directed" id="n4::n1:">
975+ <node id="n4::n1::n0">
976+ <data key="d6">
977+ <y:ShapeNode>
978+ <y:Geometry height="37.96508164062459" width="75.0" x="724.6233764022411" y="-291.3445901367184"/>
979+ <y:Fill color="#FFCC00" transparent="false"/>
980+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
981+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="38.013671875" x="18.4931640625" y="9.631954882812295">Chuck<y:LabelModel>
982+ <y:SmartNodeLabelModel distance="4.0"/>
983+ </y:LabelModel>
984+ <y:ModelParameter>
985+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
986+ </y:ModelParameter>
987+ </y:NodeLabel>
988+ <y:Shape type="rectangle"/>
989+ </y:ShapeNode>
990+ </data>
991+ </node>
992+ <node id="n4::n1::n1">
993+ <data key="d6">
994+ <y:ShapeNode>
995+ <y:Geometry height="35.0" width="71.0" x="813.9964232772422" y="-289.8620493164061"/>
996+ <y:Fill color="#FFCC00" transparent="false"/>
997+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
998+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="34.6796875" x="18.16015625" y="8.1494140625">Coolit<y:LabelModel>
999+ <y:SmartNodeLabelModel distance="4.0"/>
1000+ </y:LabelModel>
1001+ <y:ModelParameter>
1002+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1003+ </y:ModelParameter>
1004+ </y:NodeLabel>
1005+ <y:Shape type="rectangle"/>
1006+ </y:ShapeNode>
1007+ </data>
1008+ </node>
1009+ <node id="n4::n1::n2">
1010+ <data key="d6">
1011+ <y:ShapeNode>
1012+ <y:Geometry height="35.0" width="71.0" x="813.9964232772422" y="-344.57927392578114"/>
1013+ <y:Fill color="#FFCC00" transparent="false"/>
1014+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1015+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="28.6796875" x="21.16015625" y="8.1494140625">Help<y:LabelModel>
1016+ <y:SmartNodeLabelModel distance="4.0"/>
1017+ </y:LabelModel>
1018+ <y:ModelParameter>
1019+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1020+ </y:ModelParameter>
1021+ </y:NodeLabel>
1022+ <y:Shape type="rectangle"/>
1023+ </y:ShapeNode>
1024+ </data>
1025+ </node>
1026+ <node id="n4::n1::n3">
1027+ <data key="d6">
1028+ <y:ShapeNode>
1029+ <y:Geometry height="35.3441162109375" width="75.0" x="724.6233764022411" y="-344.57927392578114"/>
1030+ <y:Fill color="#FFCC00" transparent="false"/>
1031+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1032+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="32.681640625" x="21.1591796875" y="8.32147216796875">Invite<y:LabelModel>
1033+ <y:SmartNodeLabelModel distance="4.0"/>
1034+ </y:LabelModel>
1035+ <y:ModelParameter>
1036+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1037+ </y:ModelParameter>
1038+ </y:NodeLabel>
1039+ <y:Shape type="rectangle"/>
1040+ </y:ShapeNode>
1041+ </data>
1042+ </node>
1043+ <node id="n4::n1::n4">
1044+ <data key="d6">
1045+ <y:ShapeNode>
1046+ <y:Geometry height="35.0" width="84.76958363381254" x="899.3694701522434" y="-289.8620493164061"/>
1047+ <y:Fill color="#FFCC00" transparent="false"/>
1048+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1049+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="82.69140625" x="1.0390886919062723" y="8.1494140625">Theyfightcrime<y:LabelModel>
1050+ <y:SmartNodeLabelModel distance="4.0"/>
1051+ </y:LabelModel>
1052+ <y:ModelParameter>
1053+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1054+ </y:ModelParameter>
1055+ </y:NodeLabel>
1056+ <y:Shape type="rectangle"/>
1057+ </y:ShapeNode>
1058+ </data>
1059+ </node>
1060+ </graph>
1061+ </node>
1062+ <node id="n4::n2">
1063+ <data key="d6">
1064+ <y:ShapeNode>
1065+ <y:Geometry height="40.0" width="73.0" x="405.3593465544859" y="-406.8579577574118"/>
1066+ <y:Fill color="#FFCC00" transparent="false"/>
1067+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1068+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="48.009765625" x="12.4951171875" y="10.6494140625">Blacklist<y:LabelModel>
1069+ <y:SmartNodeLabelModel distance="4.0"/>
1070+ </y:LabelModel>
1071+ <y:ModelParameter>
1072+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1073+ </y:ModelParameter>
1074+ </y:NodeLabel>
1075+ <y:Shape type="rectangle"/>
1076+ </y:ShapeNode>
1077+ </data>
1078+ </node>
1079+ <node id="n4::n3">
1080+ <data key="d6">
1081+ <y:ShapeNode>
1082+ <y:Geometry height="40.0" width="75.0" x="283.3931927083324" y="-157.02827614433076"/>
1083+ <y:Fill color="#FFCC00" transparent="false"/>
1084+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1085+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="43.33984375" x="15.830078125" y="10.6494140625">Correct<y:LabelModel>
1086+ <y:SmartNodeLabelModel distance="4.0"/>
1087+ </y:LabelModel>
1088+ <y:ModelParameter>
1089+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1090+ </y:ModelParameter>
1091+ </y:NodeLabel>
1092+ <y:Shape type="rectangle"/>
1093+ </y:ShapeNode>
1094+ </data>
1095+ </node>
1096+ <node id="n4::n4">
1097+ <data key="d6">
1098+ <y:ShapeNode>
1099+ <y:Geometry height="40.0" width="75.0" x="712.1187072315686" y="-451.95573876953114"/>
1100+ <y:Fill color="#FFCC00" transparent="false"/>
1101+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1102+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="48.033203125" x="13.4833984375" y="10.6494140625">Echobot<y:LabelModel>
1103+ <y:SmartNodeLabelModel distance="4.0"/>
1104+ </y:LabelModel>
1105+ <y:ModelParameter>
1106+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1107+ </y:ModelParameter>
1108+ </y:NodeLabel>
1109+ <y:Shape type="rectangle"/>
1110+ </y:ShapeNode>
1111+ </data>
1112+ </node>
1113+ <node id="n4::n5">
1114+ <data key="d6">
1115+ <y:ShapeNode>
1116+ <y:Geometry height="40.0" width="75.0" x="405.3593465544859" y="-294.8723923552683"/>
1117+ <y:Fill color="#FFCC00" transparent="false"/>
1118+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1119+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="57.361328125" x="8.8193359375" y="10.6494140625">Pubpicker<y:LabelModel>
1120+ <y:SmartNodeLabelModel distance="4.0"/>
1121+ </y:LabelModel>
1122+ <y:ModelParameter>
1123+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1124+ </y:ModelParameter>
1125+ </y:NodeLabel>
1126+ <y:Shape type="rectangle"/>
1127+ </y:ShapeNode>
1128+ </data>
1129+ </node>
1130+ <node id="n4::n6">
1131+ <data key="d6">
1132+ <y:ShapeNode>
1133+ <y:Geometry height="40.0" width="75.0" x="403.3593465544859" y="-460.9817086576023"/>
1134+ <y:Fill color="#FFCC00" transparent="false"/>
1135+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1136+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="50.67578125" x="12.162109375" y="10.6494140625">Ratelimit<y:LabelModel>
1137+ <y:SmartNodeLabelModel distance="4.0"/>
1138+ </y:LabelModel>
1139+ <y:ModelParameter>
1140+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1141+ </y:ModelParameter>
1142+ </y:NodeLabel>
1143+ <y:Shape type="rectangle"/>
1144+ </y:ShapeNode>
1145+ </data>
1146+ </node>
1147+ <node id="n4::n7">
1148+ <data key="d6">
1149+ <y:ShapeNode>
1150+ <y:Geometry height="42.041631250000364" width="75.0" x="812.9192748397422" y="-452.9765543945313"/>
1151+ <y:Fill color="#FFCC00" transparent="false"/>
1152+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1153+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="68.693359375" x="3.1533203125" y="11.670229687500182">Roomowner<y:LabelModel>
1154+ <y:SmartNodeLabelModel distance="4.0"/>
1155+ </y:LabelModel>
1156+ <y:ModelParameter>
1157+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1158+ </y:ModelParameter>
1159+ </y:NodeLabel>
1160+ <y:Shape type="rectangle"/>
1161+ </y:ShapeNode>
1162+ </data>
1163+ </node>
1164+ <node id="n4::n8">
1165+ <data key="d5"><![CDATA[Fails to retrieve webpage, no exceptions raised or logged]]></data>
1166+ <data key="d6">
1167+ <y:ShapeNode>
1168+ <y:Geometry height="40.0" width="75.0" x="283.3931927083324" y="-99.38682695312474"/>
1169+ <y:Fill color="#FF0000" transparent="false"/>
1170+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1171+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="30.68359375" x="22.158203125" y="10.6494140625">Spell<y:LabelModel>
1172+ <y:SmartNodeLabelModel distance="4.0"/>
1173+ </y:LabelModel>
1174+ <y:ModelParameter>
1175+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1176+ </y:ModelParameter>
1177+ </y:NodeLabel>
1178+ <y:Shape type="rectangle"/>
1179+ </y:ShapeNode>
1180+ </data>
1181+ </node>
1182+ <node id="n4::n9">
1183+ <data key="d6">
1184+ <y:ShapeNode>
1185+ <y:Geometry height="40.0" width="75.0" x="909.1390537860559" y="-451.95573876953114"/>
1186+ <y:Fill color="#FFCC00" transparent="false"/>
1187+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1188+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="62.048828125" x="6.4755859375" y="10.6494140625">Unhandled<y:LabelModel>
1189+ <y:SmartNodeLabelModel distance="4.0"/>
1190+ </y:LabelModel>
1191+ <y:ModelParameter>
1192+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1193+ </y:ModelParameter>
1194+ </y:NodeLabel>
1195+ <y:Shape type="rectangle"/>
1196+ </y:ShapeNode>
1197+ </data>
1198+ </node>
1199+ <node id="n4::n10">
1200+ <data key="d6">
1201+ <y:ShapeNode>
1202+ <y:Geometry height="40.418096093750364" width="75.0" x="580.0200943509591" y="-347.11626386718757"/>
1203+ <y:Fill color="#FFCC00" transparent="false"/>
1204+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1205+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="37.33984375" x="18.830078125" y="10.858462109375182">Memo<y:LabelModel>
1206+ <y:SmartNodeLabelModel distance="4.0"/>
1207+ </y:LabelModel>
1208+ <y:ModelParameter>
1209+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1210+ </y:ModelParameter>
1211+ </y:NodeLabel>
1212+ <y:Shape type="rectangle"/>
1213+ </y:ShapeNode>
1214+ </data>
1215+ </node>
1216+ <node id="n4::n11">
1217+ <data key="d6">
1218+ <y:ShapeNode>
1219+ <y:Geometry height="37.96508164062459" width="75.11914062499989" x="579.9009537259592" y="-294.5954482421874"/>
1220+ <y:Fill color="#FFCC00" transparent="false"/>
1221+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1222+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="46.017578125" x="14.55078125" y="9.631954882812295">Remote<y:LabelModel>
1223+ <y:SmartNodeLabelModel distance="4.0"/>
1224+ </y:LabelModel>
1225+ <y:ModelParameter>
1226+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1227+ </y:ModelParameter>
1228+ </y:NodeLabel>
1229+ <y:Shape type="rectangle"/>
1230+ </y:ShapeNode>
1231+ </data>
1232+ </node>
1233+ <node id="n4::n12">
1234+ <data key="d6">
1235+ <y:ShapeNode>
1236+ <y:Geometry height="40.0" width="75.0" x="580.0200943509591" y="-451.95573876953114"/>
1237+ <y:Fill color="#FFCC00" transparent="false"/>
1238+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1239+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="38.025390625" x="18.4873046875" y="10.6494140625">Speak<y:LabelModel>
1240+ <y:SmartNodeLabelModel distance="4.0"/>
1241+ </y:LabelModel>
1242+ <y:ModelParameter>
1243+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1244+ </y:ModelParameter>
1245+ </y:NodeLabel>
1246+ <y:Shape type="rectangle"/>
1247+ </y:ShapeNode>
1248+ </data>
1249+ </node>
1250+ <node id="n4::n13">
1251+ <data key="d6">
1252+ <y:ShapeNode>
1253+ <y:Geometry height="40.0" width="75.0" x="580.0200943509591" y="-399.53600131835935"/>
1254+ <y:Fill color="#FFCC00" transparent="false"/>
1255+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1256+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="66.701171875" x="4.1494140625" y="10.6494140625">Whosonline<y:LabelModel>
1257+ <y:SmartNodeLabelModel distance="4.0"/>
1258+ </y:LabelModel>
1259+ <y:ModelParameter>
1260+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1261+ </y:ModelParameter>
1262+ </y:NodeLabel>
1263+ <y:Shape type="rectangle"/>
1264+ </y:ShapeNode>
1265+ </data>
1266+ </node>
1267+ </graph>
1268+ </node>
1269+ <node id="n5" yfiles.foldertype="group">
1270+ <data key="d4"/>
1271+ <data key="d6">
1272+ <y:ProxyAutoBoundsNode>
1273+ <y:Realizers active="0">
1274+ <y:GroupNode>
1275+ <y:Geometry height="194.87646484375" width="163.3984375" x="-21.750046674678174" y="297.95868626302183"/>
1276+ <y:Fill color="#F5F5F5" transparent="false"/>
1277+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
1278+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="163.3984375" x="0.0" y="0.0">Boxes</y:NodeLabel>
1279+ <y:Shape type="roundrectangle"/>
1280+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
1281+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
1282+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
1283+ </y:GroupNode>
1284+ <y:GroupNode>
1285+ <y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
1286+ <y:Fill color="#F5F5F5" transparent="false"/>
1287+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
1288+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="67.369140625" x="-8.6845703125" y="0.0">Folder 15</y:NodeLabel>
1289+ <y:Shape type="roundrectangle"/>
1290+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
1291+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
1292+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
1293+ </y:GroupNode>
1294+ </y:Realizers>
1295+ </y:ProxyAutoBoundsNode>
1296+ </data>
1297+ <graph edgedefault="directed" id="n5:">
1298+ <node id="n5::n0">
1299+ <data key="d6">
1300+ <y:ShapeNode>
1301+ <y:Geometry height="35.0" width="90.68359375" x="14.607375200321826" y="335.33515110677183"/>
1302+ <y:Fill color="#FFCC00" transparent="false"/>
1303+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1304+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="88.052734375" x="1.3154296875" y="8.1494140625">Endroid module<y:LabelModel>
1305+ <y:SmartNodeLabelModel distance="4.0"/>
1306+ </y:LabelModel>
1307+ <y:ModelParameter>
1308+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1309+ </y:ModelParameter>
1310+ </y:NodeLabel>
1311+ <y:Shape type="rectangle"/>
1312+ </y:ShapeNode>
1313+ </data>
1314+ </node>
1315+ <node id="n5::n1">
1316+ <data key="d6">
1317+ <y:ShapeNode>
1318+ <y:Geometry height="30.0" width="90.68359375" x="14.607375200321826" y="389.08515110677183"/>
1319+ <y:Fill color="#3366FF" transparent="false"/>
1320+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1321+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="133.3984375" x="-21.357421875" y="5.6494140625">Owned by Endroid class<y:LabelModel>
1322+ <y:SmartNodeLabelModel distance="4.0"/>
1323+ </y:LabelModel>
1324+ <y:ModelParameter>
1325+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1326+ </y:ModelParameter>
1327+ </y:NodeLabel>
1328+ <y:Shape type="rectangle"/>
1329+ </y:ShapeNode>
1330+ </data>
1331+ </node>
1332+ <node id="n5::n2">
1333+ <data key="d6">
1334+ <y:ShapeNode>
1335+ <y:Geometry height="40.0" width="91.54065915576768" x="13.750309794554141" y="437.83515110677183"/>
1336+ <y:Fill color="#C0C0C0" transparent="false"/>
1337+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1338+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="84.02734375" x="3.7566577028838424" y="10.6494140625">External library<y:LabelModel>
1339+ <y:SmartNodeLabelModel distance="4.0"/>
1340+ </y:LabelModel>
1341+ <y:ModelParameter>
1342+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1343+ </y:ModelParameter>
1344+ </y:NodeLabel>
1345+ <y:Shape type="rectangle"/>
1346+ </y:ShapeNode>
1347+ </data>
1348+ </node>
1349+ </graph>
1350+ </node>
1351+ <node id="n6" yfiles.foldertype="group">
1352+ <data key="d4"/>
1353+ <data key="d6">
1354+ <y:ProxyAutoBoundsNode>
1355+ <y:Realizers active="0">
1356+ <y:GroupNode>
1357+ <y:Geometry height="239.9237978240185" width="88.0" x="61.77179867788573" y="-460.9817086576023"/>
1358+ <y:Fill color="#F5F5F5" transparent="false"/>
1359+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
1360+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="88.0" x="0.0" y="0.0">Working?</y:NodeLabel>
1361+ <y:Shape type="roundrectangle"/>
1362+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
1363+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
1364+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
1365+ </y:GroupNode>
1366+ <y:GroupNode>
1367+ <y:Geometry height="50.0" width="50.0" x="272.4179705528843" y="-482.2961901792868"/>
1368+ <y:Fill color="#F5F5F5" transparent="false"/>
1369+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
1370+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="67.369140625" x="-8.6845703125" y="0.0">Folder 12</y:NodeLabel>
1371+ <y:Shape type="roundrectangle"/>
1372+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
1373+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
1374+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
1375+ </y:GroupNode>
1376+ </y:Realizers>
1377+ </y:ProxyAutoBoundsNode>
1378+ </data>
1379+ <graph edgedefault="directed" id="n6:">
1380+ <node id="n6::n0">
1381+ <data key="d6">
1382+ <y:ShapeNode>
1383+ <y:Geometry height="35.3441162109375" width="58.0" x="76.77179867788573" y="-423.6052438138523"/>
1384+ <y:Fill color="#FFCC00" transparent="false"/>
1385+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1386+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="29.3359375" x="14.33203125" y="8.32147216796875">Fully<y:LabelModel>
1387+ <y:SmartNodeLabelModel distance="4.0"/>
1388+ </y:LabelModel>
1389+ <y:ModelParameter>
1390+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1391+ </y:ModelParameter>
1392+ </y:NodeLabel>
1393+ <y:Shape type="rectangle"/>
1394+ </y:ShapeNode>
1395+ </data>
1396+ </node>
1397+ <node id="n6::n1">
1398+ <data key="d6">
1399+ <y:ShapeNode>
1400+ <y:Geometry height="35.3441162109375" width="58.0" x="76.77179867788573" y="-372.3149874035959"/>
1401+ <y:Fill color="#800000" transparent="false"/>
1402+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1403+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="46.6796875" x="5.66015625" y="8.32147216796875">Partially<y:LabelModel>
1404+ <y:SmartNodeLabelModel distance="4.0"/>
1405+ </y:LabelModel>
1406+ <y:ModelParameter>
1407+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1408+ </y:ModelParameter>
1409+ </y:NodeLabel>
1410+ <y:Shape type="rectangle"/>
1411+ </y:ShapeNode>
1412+ </data>
1413+ </node>
1414+ <node id="n6::n2">
1415+ <data key="d6">
1416+ <y:ShapeNode>
1417+ <y:Geometry height="35.0" width="56.908717948717936" x="77.86308072916779" y="-320.18551847831554"/>
1418+ <y:Fill color="#FF0000" transparent="false"/>
1419+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1420+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="22.673828125" x="17.117444911858968" y="8.1494140625">Not<y:LabelModel>
1421+ <y:SmartNodeLabelModel distance="4.0"/>
1422+ </y:LabelModel>
1423+ <y:ModelParameter>
1424+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1425+ </y:ModelParameter>
1426+ </y:NodeLabel>
1427+ <y:Shape type="rectangle"/>
1428+ </y:ShapeNode>
1429+ </data>
1430+ </node>
1431+ <node id="n6::n3">
1432+ <data key="d6">
1433+ <y:ShapeNode>
1434+ <y:Geometry height="35.0" width="56.908717948717936" x="77.86308072916779" y="-271.05791083358383"/>
1435+ <y:Fill color="#800080" transparent="false"/>
1436+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1437+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="52.029296875" x="2.4397105368589678" y="8.1494140625">Untested<y:LabelModel>
1438+ <y:SmartNodeLabelModel distance="4.0"/>
1439+ </y:LabelModel>
1440+ <y:ModelParameter>
1441+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1442+ </y:ModelParameter>
1443+ </y:NodeLabel>
1444+ <y:Shape type="rectangle"/>
1445+ </y:ShapeNode>
1446+ </data>
1447+ </node>
1448+ </graph>
1449+ </node>
1450+ <node id="n7" yfiles.foldertype="group">
1451+ <data key="d4"/>
1452+ <data key="d6">
1453+ <y:ProxyAutoBoundsNode>
1454+ <y:Realizers active="0">
1455+ <y:GroupNode>
1456+ <y:Geometry height="285.1195933894167" width="99.361328125" x="-69.7174060496784" y="-459.25237601787234"/>
1457+ <y:Fill color="#F5F5F5" transparent="false"/>
1458+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
1459+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="99.361328125" x="0.0" y="0.0">Arrows</y:NodeLabel>
1460+ <y:Shape type="roundrectangle"/>
1461+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
1462+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
1463+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
1464+ </y:GroupNode>
1465+ <y:GroupNode>
1466+ <y:Geometry height="50.0" width="50.0" x="-69.7174060496784" y="-459.25237601787234"/>
1467+ <y:Fill color="#F5F5F5" transparent="false"/>
1468+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
1469+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" modelName="internal" modelPosition="t" textColor="#000000" visible="true" width="67.369140625" x="-8.6845703125" y="0.0">Folder 13</y:NodeLabel>
1470+ <y:Shape type="roundrectangle"/>
1471+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
1472+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
1473+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
1474+ </y:GroupNode>
1475+ </y:Realizers>
1476+ </y:ProxyAutoBoundsNode>
1477+ </data>
1478+ <graph edgedefault="directed" id="n7:">
1479+ <node id="n7::n0">
1480+ <data key="d6">
1481+ <y:ShapeNode>
1482+ <y:Geometry height="35.0" width="58.0" x="-49.03674198717839" y="-421.87591117412234"/>
1483+ <y:Fill color="#000000" transparent="false"/>
1484+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1485+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#FFFFFF" visible="true" width="35.34765625" x="11.326171875" y="8.1494140625">Has a<y:LabelModel>
1486+ <y:SmartNodeLabelModel distance="4.0"/>
1487+ </y:LabelModel>
1488+ <y:ModelParameter>
1489+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1490+ </y:ModelParameter>
1491+ </y:NodeLabel>
1492+ <y:Shape type="rectangle"/>
1493+ </y:ShapeNode>
1494+ </data>
1495+ </node>
1496+ <node id="n7::n1">
1497+ <data key="d6">
1498+ <y:ShapeNode>
1499+ <y:Geometry height="35.0" width="58.0" x="-49.03674198717839" y="-317.98303903871204"/>
1500+ <y:Fill color="#FF0000" transparent="false"/>
1501+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1502+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="43.3515625" x="7.32421875" y="8.1494140625">Inherits<y:LabelModel>
1503+ <y:SmartNodeLabelModel distance="4.0"/>
1504+ </y:LabelModel>
1505+ <y:ModelParameter>
1506+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1507+ </y:ModelParameter>
1508+ </y:NodeLabel>
1509+ <y:Shape type="rectangle"/>
1510+ </y:ShapeNode>
1511+ </data>
1512+ </node>
1513+ <node id="n7::n2">
1514+ <data key="d6">
1515+ <y:ShapeNode>
1516+ <y:Geometry height="35.0" width="58.0" x="-49.03674198717839" y="-271.05791083358383"/>
1517+ <y:Fill color="#FF6600" transparent="false"/>
1518+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1519+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="60.68359375" x="-1.341796875" y="8.1494140625">MetaClass<y:LabelModel>
1520+ <y:SmartNodeLabelModel distance="4.0"/>
1521+ </y:LabelModel>
1522+ <y:ModelParameter>
1523+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1524+ </y:ModelParameter>
1525+ </y:NodeLabel>
1526+ <y:Shape type="rectangle"/>
1527+ </y:ShapeNode>
1528+ </data>
1529+ </node>
1530+ <node id="n7::n3">
1531+ <data key="d5"><![CDATA[Who gives config to whom]]></data>
1532+ <data key="d6">
1533+ <y:ShapeNode>
1534+ <y:Geometry height="40.0" width="57.99999999999997" x="-49.03674198717839" y="-372.42947510641716"/>
1535+ <y:Fill color="#FFFFFF" transparent="false"/>
1536+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
1537+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="38.6875" x="9.656249999999986" y="10.6494140625">Config<y:LabelModel>
1538+ <y:SmartNodeLabelModel distance="4.0"/>
1539+ </y:LabelModel>
1540+ <y:ModelParameter>
1541+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1542+ </y:ModelParameter>
1543+ </y:NodeLabel>
1544+ <y:Shape type="rectangle"/>
1545+ </y:ShapeNode>
1546+ </data>
1547+ </node>
1548+ <node id="n7::n4">
1549+ <data key="d6">
1550+ <y:ShapeNode>
1551+ <y:Geometry height="35.0" width="57.99999999999997" x="-49.03674198717839" y="-224.13278262845563"/>
1552+ <y:Fill color="#C0C0C0" transparent="false"/>
1553+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
1554+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" modelName="custom" textColor="#000000" visible="true" width="69.361328125" x="-5.680664062500014" y="8.1494140625">Unimportant<y:LabelModel>
1555+ <y:SmartNodeLabelModel distance="4.0"/>
1556+ </y:LabelModel>
1557+ <y:ModelParameter>
1558+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
1559+ </y:ModelParameter>
1560+ </y:NodeLabel>
1561+ <y:Shape type="rectangle"/>
1562+ </y:ShapeNode>
1563+ </data>
1564+ </node>
1565+ </graph>
1566+ </node>
1567+ <edge id="n1::e0" source="n1::n3" target="n1::n0::n0">
1568+ <data key="d10">
1569+ <y:PolyLineEdge>
1570+ <y:Path sx="-0.0" sy="0.0" tx="-0.0" ty="0.0"/>
1571+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1572+ <y:Arrows source="none" target="standard"/>
1573+ <y:BendStyle smoothed="false"/>
1574+ </y:PolyLineEdge>
1575+ </data>
1576+ </edge>
1577+ <edge id="n1::n0::e0" source="n1::n0::n1" target="n1::n0::n0">
1578+ <data key="d10">
1579+ <y:PolyLineEdge>
1580+ <y:Path sx="-0.0" sy="0.0" tx="-0.0" ty="0.0"/>
1581+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1582+ <y:Arrows source="none" target="standard"/>
1583+ <y:BendStyle smoothed="false"/>
1584+ </y:PolyLineEdge>
1585+ </data>
1586+ </edge>
1587+ <edge id="n1::n0::e1" source="n1::n0::n2" target="n1::n0::n0">
1588+ <data key="d10">
1589+ <y:PolyLineEdge>
1590+ <y:Path sx="-0.0" sy="0.0" tx="-0.0" ty="0.0"/>
1591+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1592+ <y:Arrows source="none" target="standard"/>
1593+ <y:BendStyle smoothed="false"/>
1594+ </y:PolyLineEdge>
1595+ </data>
1596+ </edge>
1597+ <edge id="n1::e1" source="n1::n4" target="n1::n1::n0">
1598+ <data key="d10">
1599+ <y:PolyLineEdge>
1600+ <y:Path sx="-0.0" sy="0.0" tx="-0.0" ty="0.0"/>
1601+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1602+ <y:Arrows source="none" target="standard"/>
1603+ <y:BendStyle smoothed="false"/>
1604+ </y:PolyLineEdge>
1605+ </data>
1606+ </edge>
1607+ <edge id="n1::n1::e0" source="n1::n1::n1" target="n1::n1::n0">
1608+ <data key="d10">
1609+ <y:PolyLineEdge>
1610+ <y:Path sx="-0.0" sy="0.0" tx="-0.0" ty="0.0"/>
1611+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1612+ <y:Arrows source="none" target="standard"/>
1613+ <y:BendStyle smoothed="false"/>
1614+ </y:PolyLineEdge>
1615+ </data>
1616+ </edge>
1617+ <edge id="n1::n1::e1" source="n1::n1::n0" target="n1::n1::n1">
1618+ <data key="d10">
1619+ <y:PolyLineEdge>
1620+ <y:Path sx="-0.0" sy="0.0" tx="-12.0" ty="0.0"/>
1621+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1622+ <y:Arrows source="none" target="standard"/>
1623+ <y:BendStyle smoothed="false"/>
1624+ </y:PolyLineEdge>
1625+ </data>
1626+ </edge>
1627+ <edge id="e0" source="n2::n3" target="n1::n3">
1628+ <data key="d10">
1629+ <y:PolyLineEdge>
1630+ <y:Path sx="-0.0" sy="0.0" tx="-0.0" ty="0.0"/>
1631+ <y:LineStyle color="#FF0000" type="line" width="1.0"/>
1632+ <y:Arrows source="none" target="standard"/>
1633+ <y:BendStyle smoothed="false"/>
1634+ </y:PolyLineEdge>
1635+ </data>
1636+ </edge>
1637+ <edge id="e1" source="n2::n2" target="n1::n3">
1638+ <data key="d10">
1639+ <y:PolyLineEdge>
1640+ <y:Path sx="-0.0" sy="0.0" tx="-0.0" ty="0.0"/>
1641+ <y:LineStyle color="#FF0000" type="line" width="1.0"/>
1642+ <y:Arrows source="none" target="standard"/>
1643+ <y:BendStyle smoothed="false"/>
1644+ </y:PolyLineEdge>
1645+ </data>
1646+ </edge>
1647+ <edge id="e2" source="n2::n0" target="n1::n4">
1648+ <data key="d10">
1649+ <y:PolyLineEdge>
1650+ <y:Path sx="-0.0" sy="0.0" tx="-0.0" ty="0.0"/>
1651+ <y:LineStyle color="#FF0000" type="line" width="1.0"/>
1652+ <y:Arrows source="none" target="standard"/>
1653+ <y:BendStyle smoothed="false"/>
1654+ </y:PolyLineEdge>
1655+ </data>
1656+ </edge>
1657+ <edge id="e3" source="n2::n1" target="n1::n4">
1658+ <data key="d10">
1659+ <y:PolyLineEdge>
1660+ <y:Path sx="-0.0" sy="0.0" tx="-0.0" ty="0.0"/>
1661+ <y:LineStyle color="#FF0000" type="line" width="1.0"/>
1662+ <y:Arrows source="none" target="standard"/>
1663+ <y:BendStyle smoothed="false"/>
1664+ </y:PolyLineEdge>
1665+ </data>
1666+ </edge>
1667+ <edge id="n1::n0::e2" source="n1::n0::n2" target="n1::n0::n1">
1668+ <data key="d10">
1669+ <y:PolyLineEdge>
1670+ <y:Path sx="-0.0" sy="0.0" tx="-0.0" ty="0.0"/>
1671+ <y:LineStyle color="#FF0000" type="line" width="1.0"/>
1672+ <y:Arrows source="none" target="standard"/>
1673+ <y:BendStyle smoothed="false"/>
1674+ </y:PolyLineEdge>
1675+ </data>
1676+ </edge>
1677+ <edge id="n1::e2" source="n1::n1::n0" target="n1::n2::n0">
1678+ <data key="d10">
1679+ <y:PolyLineEdge>
1680+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
1681+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1682+ <y:Arrows source="none" target="standard"/>
1683+ <y:BendStyle smoothed="false"/>
1684+ </y:PolyLineEdge>
1685+ </data>
1686+ </edge>
1687+ <edge id="n1::e3" source="n1::n0::n0" target="n1::n2::n0">
1688+ <data key="d10">
1689+ <y:PolyLineEdge>
1690+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
1691+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1692+ <y:Arrows source="none" target="standard"/>
1693+ <y:BendStyle smoothed="false"/>
1694+ </y:PolyLineEdge>
1695+ </data>
1696+ </edge>
1697+ <edge id="n1::n2::e0" source="n1::n2::n2" target="n1::n2::n1">
1698+ <data key="d10">
1699+ <y:PolyLineEdge>
1700+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
1701+ <y:LineStyle color="#FF6600" type="line" width="1.0"/>
1702+ <y:Arrows source="none" target="standard"/>
1703+ <y:BendStyle smoothed="false"/>
1704+ </y:PolyLineEdge>
1705+ </data>
1706+ </edge>
1707+ <edge id="n1::n2::e1" source="n1::n2::n1" target="n1::n2::n3">
1708+ <data key="d10">
1709+ <y:PolyLineEdge>
1710+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
1711+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1712+ <y:Arrows source="none" target="standard"/>
1713+ <y:BendStyle smoothed="false"/>
1714+ </y:PolyLineEdge>
1715+ </data>
1716+ </edge>
1717+ <edge id="n1::n2::e2" source="n1::n2::n3" target="n1::n2::n0">
1718+ <data key="d10">
1719+ <y:PolyLineEdge>
1720+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
1721+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1722+ <y:Arrows source="none" target="standard"/>
1723+ <y:BendStyle smoothed="false"/>
1724+ </y:PolyLineEdge>
1725+ </data>
1726+ </edge>
1727+ <edge id="n1::e4" source="n1::n1::n0" target="n1::n4">
1728+ <data key="d10">
1729+ <y:PolyLineEdge>
1730+ <y:Path sx="-0.0" sy="0.0" tx="-0.0" ty="0.0"/>
1731+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1732+ <y:Arrows source="none" target="standard"/>
1733+ <y:BendStyle smoothed="false"/>
1734+ </y:PolyLineEdge>
1735+ </data>
1736+ </edge>
1737+ <edge id="n1::e5" source="n1::n0::n0" target="n1::n3">
1738+ <data key="d10">
1739+ <y:PolyLineEdge>
1740+ <y:Path sx="-0.0" sy="0.0" tx="-0.0" ty="0.0"/>
1741+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1742+ <y:Arrows source="none" target="standard"/>
1743+ <y:BendStyle smoothed="false"/>
1744+ </y:PolyLineEdge>
1745+ </data>
1746+ </edge>
1747+ <edge id="n1::n2::e3" source="n1::n2::n0" target="n1::n2::n3">
1748+ <data key="d10">
1749+ <y:PolyLineEdge>
1750+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="7.866864843749568"/>
1751+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1752+ <y:Arrows source="none" target="standard"/>
1753+ <y:BendStyle smoothed="false"/>
1754+ </y:PolyLineEdge>
1755+ </data>
1756+ </edge>
1757+ <edge id="n1::n2::e4" source="n1::n2::n3" target="n1::n2::n1">
1758+ <data key="d10">
1759+ <y:PolyLineEdge>
1760+ <y:Path sx="0.0" sy="0.0" tx="-35.75000000000023" ty="0.0"/>
1761+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1762+ <y:Arrows source="none" target="standard"/>
1763+ <y:BendStyle smoothed="false"/>
1764+ </y:PolyLineEdge>
1765+ </data>
1766+ </edge>
1767+ <edge id="n1::e6" source="n1::n0::n0" target="n1::n1::n0">
1768+ <data key="d10">
1769+ <y:PolyLineEdge>
1770+ <y:Path sx="-0.0" sy="0.0" tx="-0.0" ty="0.0"/>
1771+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1772+ <y:Arrows source="none" target="standard"/>
1773+ <y:BendStyle smoothed="false"/>
1774+ </y:PolyLineEdge>
1775+ </data>
1776+ </edge>
1777+ <edge id="n0::e0" source="n0::n0" target="n0::n1">
1778+ <data key="d10">
1779+ <y:PolyLineEdge>
1780+ <y:Path sx="-0.0" sy="0.0" tx="-0.0" ty="0.0"/>
1781+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1782+ <y:Arrows source="none" target="standard"/>
1783+ <y:BendStyle smoothed="false"/>
1784+ </y:PolyLineEdge>
1785+ </data>
1786+ </edge>
1787+ <edge id="e4" source="n1::n2::n1" target="n4">
1788+ <data key="d10">
1789+ <y:PolyLineEdge>
1790+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
1791+ <y:LineStyle color="#FF0000" type="line" width="1.0"/>
1792+ <y:Arrows source="none" target="standard"/>
1793+ <y:BendStyle smoothed="false"/>
1794+ </y:PolyLineEdge>
1795+ </data>
1796+ </edge>
1797+ <edge id="e5" source="n0::n1" target="n1::n4">
1798+ <data key="d10">
1799+ <y:PolyLineEdge>
1800+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
1801+ <y:Point x="645.7396923076924" y="458.9696312500008"/>
1802+ </y:Path>
1803+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1804+ <y:Arrows source="none" target="standard"/>
1805+ <y:BendStyle smoothed="false"/>
1806+ </y:PolyLineEdge>
1807+ </data>
1808+ </edge>
1809+ <edge id="e6" source="n3::n1" target="n4::n2">
1810+ <data key="d10">
1811+ <y:PolyLineEdge>
1812+ <y:Path sx="0.0" sy="-1.1368683772161603E-13" tx="1.1368683772161603E-13" ty="-5.115907697472721E-13"/>
1813+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1814+ <y:Arrows source="none" target="standard"/>
1815+ <y:BendStyle smoothed="false"/>
1816+ </y:PolyLineEdge>
1817+ </data>
1818+ </edge>
1819+ <edge id="e7" source="n3::n1" target="n4::n10">
1820+ <data key="d10">
1821+ <y:PolyLineEdge>
1822+ <y:Path sx="0.0" sy="-1.1368683772161603E-13" tx="-3.410605131648481E-13" ty="1.7053025658242404E-13"/>
1823+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1824+ <y:Arrows source="none" target="standard"/>
1825+ <y:BendStyle smoothed="false"/>
1826+ </y:PolyLineEdge>
1827+ </data>
1828+ </edge>
1829+ <edge id="e8" source="n3::n1" target="n4::n5">
1830+ <data key="d10">
1831+ <y:PolyLineEdge>
1832+ <y:Path sx="0.0" sy="-1.1368683772161603E-13" tx="3.410605131648481E-13" ty="1.1368683772161603E-13"/>
1833+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1834+ <y:Arrows source="none" target="standard"/>
1835+ <y:BendStyle smoothed="false"/>
1836+ </y:PolyLineEdge>
1837+ </data>
1838+ </edge>
1839+ <edge id="e9" source="n3::n1" target="n4::n6">
1840+ <data key="d10">
1841+ <y:PolyLineEdge>
1842+ <y:Path sx="0.0" sy="-1.1368683772161603E-13" tx="-1.1368683772161603E-13" ty="3.410605131648481E-13"/>
1843+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1844+ <y:Arrows source="none" target="standard"/>
1845+ <y:BendStyle smoothed="false"/>
1846+ </y:PolyLineEdge>
1847+ </data>
1848+ </edge>
1849+ <edge id="e10" source="n3::n1" target="n4::n11">
1850+ <data key="d10">
1851+ <y:PolyLineEdge>
1852+ <y:Path sx="0.0" sy="-1.1368683772161603E-13" tx="0.0" ty="-5.6843418860808015E-14"/>
1853+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1854+ <y:Arrows source="none" target="standard"/>
1855+ <y:BendStyle smoothed="false"/>
1856+ </y:PolyLineEdge>
1857+ </data>
1858+ </edge>
1859+ <edge id="e11" source="n3::n0" target="n4::n6">
1860+ <data key="d10">
1861+ <y:PolyLineEdge>
1862+ <y:Path sx="-1.1368683772161603E-13" sy="-1.1368683772161603E-13" tx="-1.1368683772161603E-13" ty="3.410605131648481E-13"/>
1863+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1864+ <y:Arrows source="none" target="standard"/>
1865+ <y:BendStyle smoothed="false"/>
1866+ </y:PolyLineEdge>
1867+ </data>
1868+ </edge>
1869+ <edge id="e12" source="n3::n0" target="n4::n2">
1870+ <data key="d10">
1871+ <y:PolyLineEdge>
1872+ <y:Path sx="-1.1368683772161603E-13" sy="-1.1368683772161603E-13" tx="1.1368683772161603E-13" ty="-5.115907697472721E-13"/>
1873+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1874+ <y:Arrows source="none" target="standard"/>
1875+ <y:BendStyle smoothed="false"/>
1876+ </y:PolyLineEdge>
1877+ </data>
1878+ </edge>
1879+ <edge id="e13" source="n3" target="n0">
1880+ <data key="d10">
1881+ <y:PolyLineEdge>
1882+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
1883+ <y:Point x="208.9513425641046" y="-602.0835675455728"/>
1884+ <y:Point x="208.9513425641046" y="369.47461099258913"/>
1885+ </y:Path>
1886+ <y:LineStyle color="#000000" type="dashed" width="1.0"/>
1887+ <y:Arrows source="standard" target="standard"/>
1888+ <y:BendStyle smoothed="false"/>
1889+ </y:PolyLineEdge>
1890+ </data>
1891+ </edge>
1892+ <edge id="n3::e0" source="n3::n1" target="n3::n0">
1893+ <data key="d10">
1894+ <y:PolyLineEdge>
1895+ <y:Path sx="-0.0" sy="0.0" tx="-0.0" ty="0.0"/>
1896+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1897+ <y:Arrows source="none" target="standard"/>
1898+ <y:BendStyle smoothed="false"/>
1899+ </y:PolyLineEdge>
1900+ </data>
1901+ </edge>
1902+ <edge id="n4::e0" source="n4::n0::n0::n0" target="n4::n1">
1903+ <data key="d10">
1904+ <y:PolyLineEdge>
1905+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
1906+ <y:LineStyle color="#FF0000" type="line" width="1.0"/>
1907+ <y:Arrows source="none" target="standard"/>
1908+ <y:BendStyle smoothed="false"/>
1909+ </y:PolyLineEdge>
1910+ </data>
1911+ </edge>
1912+ <edge id="n4::n0::n0::e0" source="n4::n0::n0::n1" target="n4::n0::n0::n0">
1913+ <data key="d10">
1914+ <y:PolyLineEdge>
1915+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
1916+ <y:LineStyle color="#FF6600" type="line" width="1.0"/>
1917+ <y:Arrows source="none" target="standard"/>
1918+ <y:BendStyle smoothed="false"/>
1919+ </y:PolyLineEdge>
1920+ </data>
1921+ </edge>
1922+ <edge id="n4::n0::n0::e1" source="n4::n0::n0::n2" target="n4::n0::n0::n0">
1923+ <data key="d10">
1924+ <y:PolyLineEdge>
1925+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
1926+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1927+ <y:Arrows source="none" target="standard"/>
1928+ <y:BendStyle smoothed="false"/>
1929+ </y:PolyLineEdge>
1930+ </data>
1931+ </edge>
1932+ <edge id="n4::e1" source="n4::n0::n1" target="n4::n8">
1933+ <data key="d10">
1934+ <y:PolyLineEdge>
1935+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
1936+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1937+ <y:Arrows source="none" target="standard"/>
1938+ <y:BendStyle smoothed="false"/>
1939+ </y:PolyLineEdge>
1940+ </data>
1941+ </edge>
1942+ <edge id="n4::e2" source="n4::n0::n2" target="n4::n11">
1943+ <data key="d10">
1944+ <y:PolyLineEdge>
1945+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
1946+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1947+ <y:Arrows source="none" target="standard"/>
1948+ <y:BendStyle smoothed="false"/>
1949+ </y:PolyLineEdge>
1950+ </data>
1951+ </edge>
1952+ <edge id="e14" source="n0::n2" target="n1::n0">
1953+ <data key="d8"/>
1954+ <data key="d9"><![CDATA[From Endroid]]></data>
1955+ <data key="d10">
1956+ <y:PolyLineEdge>
1957+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
1958+ <y:LineStyle color="#000000" type="dashed" width="1.0"/>
1959+ <y:Arrows source="none" target="standard"/>
1960+ <y:BendStyle smoothed="false"/>
1961+ </y:PolyLineEdge>
1962+ </data>
1963+ </edge>
1964+ <edge id="n1::e7" source="n1::n0" target="n1::n2">
1965+ <data key="d9"><![CDATA[From UserManagement]]></data>
1966+ <data key="d10">
1967+ <y:PolyLineEdge>
1968+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
1969+ <y:LineStyle color="#000000" type="dashed" width="1.0"/>
1970+ <y:Arrows source="none" target="standard"/>
1971+ <y:BendStyle smoothed="false"/>
1972+ </y:PolyLineEdge>
1973+ </data>
1974+ </edge>
1975+ <edge id="e15" source="n1" target="n4">
1976+ <data key="d9"><![CDATA[From PluginManager]]></data>
1977+ <data key="d10">
1978+ <y:PolyLineEdge>
1979+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
1980+ <y:LineStyle color="#000000" type="dashed" width="1.0"/>
1981+ <y:Arrows source="none" target="standard"/>
1982+ <y:BendStyle smoothed="false"/>
1983+ </y:PolyLineEdge>
1984+ </data>
1985+ </edge>
1986+ <edge id="n1::e8" source="n1::n0::n0" target="n1::n4">
1987+ <data key="d9"/>
1988+ <data key="d10">
1989+ <y:PolyLineEdge>
1990+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
1991+ <y:LineStyle color="#000000" type="line" width="1.0"/>
1992+ <y:Arrows source="none" target="standard"/>
1993+ <y:BendStyle smoothed="false"/>
1994+ </y:PolyLineEdge>
1995+ </data>
1996+ </edge>
1997+ <edge id="n1::e9" source="n1::n4" target="n1::n0::n0">
1998+ <data key="d9"/>
1999+ <data key="d10">
2000+ <y:PolyLineEdge>
2001+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
2002+ <y:LineStyle color="#000000" type="line" width="1.0"/>
2003+ <y:Arrows source="none" target="standard"/>
2004+ <y:BendStyle smoothed="false"/>
2005+ </y:PolyLineEdge>
2006+ </data>
2007+ </edge>
2008+ </graph>
2009+ <data key="d0">
2010+ <y:Resources/>
2011+ </data>
2012+</graphml>
2013
2014=== modified file 'src/endroid.sh'
2015--- src/endroid.sh 2012-08-02 10:49:57 +0000
2016+++ src/endroid.sh 2013-08-12 17:50:48 +0000
2017@@ -1,2 +1,2 @@
2018 #!/bin/sh
2019-PYTHONPATH="../lib/wokkel-0.7.0-py2.7.egg":"${PYTHONPATH}" python -c "import endroid; endroid.main()" $@
2020+PYTHONPATH="../lib/wokkel-0.7.1-py2.7.egg":"${PYTHONPATH}" python -m endroid $@
2021
2022=== modified file 'src/endroid/__init__.py'
2023--- src/endroid/__init__.py 2012-09-04 15:10:26 +0000
2024+++ src/endroid/__init__.py 2013-08-12 17:50:48 +0000
2025@@ -5,7 +5,6 @@
2026 # -----------------------------------------
2027
2028 import os
2029-import os.path
2030 import sys
2031 from argparse import ArgumentParser
2032 import logging
2033@@ -14,58 +13,72 @@
2034 from twisted.internet import reactor
2035 from twisted.internet.defer import Deferred, DeferredList, inlineCallbacks
2036 from twisted.words.protocols.jabber.jid import JID
2037+from twisted.python import log # used for xml logging
2038+
2039 from wokkel.client import XMPPClient
2040
2041-from endroid.config import EnConfigParser
2042+# endroid base layer
2043 from endroid.rosterhandler import RosterHandler
2044 from endroid.wokkelhandler import WokkelHandler
2045+# top layer
2046+from endroid.usermanagement import UserManagement
2047 from endroid.messagehandler import MessageHandler
2048-from endroid.usermanagement import UserManagement
2049+# utilities
2050+from endroid.confparser import Parser
2051 from endroid.database import Database
2052-from endroid.pluginmanager import PluginManager
2053-
2054-__version__ = (1, 1)
2055-
2056-LOGGING_FORMAT = '%(asctime)-15s %(message)s'
2057+from endroid.manhole import start_manhole
2058+
2059+
2060+__version__ = (1, 2)
2061+
2062+LOGGING_FORMAT = '%(asctime)-8s %(levelname)-8s %(message)s'
2063+LOGGING_DATE_FORMAT = '%H:%M:%S'
2064+# LOGGING_FORMAT = '%(levelname)-5s: %(message)s'
2065+
2066
2067 class Endroid(object):
2068 def __init__(self, conffile, logtraffic=False):
2069 self.application = service.Application("EnDroid")
2070-
2071- self.conf = EnConfigParser(conffile)
2072-
2073- jid = self.conf['jid']
2074- self.jid = jid
2075+
2076+ self.conf = Parser(conffile)
2077+
2078+ self.jid = self.conf.get("setup", "jid")
2079 logging.info("Found JID: " + self.jid)
2080-
2081- self.secret = self.conf['secret']
2082+
2083+ self.secret = self.conf.get("setup", "password")
2084 logging.info("Found Secret: **********")
2085-
2086- self.nick = self.conf['nick']
2087- logging.info("Found Default Nick: " + self.nick)
2088-
2089- self.rooms = self.conf['rooms']
2090- for room in self.rooms:
2091+
2092+ rooms = self.conf.get("setup", "rooms", default=[])
2093+ for room in rooms:
2094 logging.info("Found Room to Join: " + room)
2095-
2096- dbfile = self.conf['database']['dbfile']
2097+
2098+ groups = self.conf.get("setup", "groups", default=['all'])
2099+ for group in groups:
2100+ logging.info("Found Group: " + group)
2101+
2102+ dbfile = self.conf.get("database", "dbfile")
2103 logging.info("Using " + dbfile + " as database file")
2104 Database.setFile(dbfile)
2105-
2106+
2107+ logfile = self.conf.get("setup", "logfile")
2108+ logging.info("Using " + logfile + " as xml log file")
2109+ log.startLogging(open(os.path.expanduser(logfile), "w+"))
2110+
2111 self.client = XMPPClient(JID(self.jid), self.secret)
2112 logging.info("Setting traffic logging to " + str(logtraffic))
2113 self.client.logTraffic = logtraffic
2114+
2115 self.client.setServiceParent(self.application)
2116-
2117- self.roster = RosterHandler()
2118- self.roster.setHandlerParent(self.client)
2119-
2120- self.wh = WokkelHandler(JID(self.jid), self.nick, self.rooms)
2121- self.wh.setHandlerParent(self.client)
2122-
2123- self.messagehandler = MessageHandler(self.wh)
2124- self.usermanagement = UserManagement(self.roster, self.wh)
2125-
2126+
2127+ self.rosterhandler = RosterHandler()
2128+ self.rosterhandler.setHandlerParent(self.client)
2129+
2130+ self.wokkelhandler = WokkelHandler()
2131+ self.wokkelhandler.setHandlerParent(self.client)
2132+
2133+ self.usermanagement = UserManagement(self.wokkelhandler, self.rosterhandler, self.conf)
2134+ self.messagehandler = MessageHandler(self.wokkelhandler, self.usermanagement)
2135+
2136 # Fire off our startup flow (once the reactor is running)
2137 reactor.callWhenRunning(self.startup_flow)
2138
2139@@ -74,57 +87,21 @@
2140 # Start the client!
2141 self.client.startService()
2142
2143- # First, we wait for the Roster and connection to Wokkel
2144- connd = Deferred()
2145- rosterd = Deferred()
2146- self.wh.set_connected_handler(connd)
2147- self.roster.set_loaded_handler(rosterd)
2148- yield DeferredList([connd, rosterd])
2149-
2150- # Ensure that everyone in the config file is in the contacts list.
2151- # And remove anyone in the roster who isn't in the contacts list
2152- # We only do this after the roster is loaded, so we don't prematurely
2153- # try to add peeps
2154- self.usermanagement.init_contacts(self.conf['users'])
2155-
2156- # Now load plugins for all the configured user groups
2157- for gname, group in self.conf['usergroups'].items():
2158- logging.info("Initialising User Group: " + gname)
2159- pm = PluginManager(self.messagehandler, self.usermanagement,
2160- userlist=group['users'])
2161- if group['plugins']:
2162- for p in group['plugins']:
2163- pm.load(p, self.conf['plugindata'][p])
2164- for p in group['plugins']:
2165- pm.init(p)
2166- else:
2167- logging.error("No Plugins Loaded For User Group: {0}".format(gname))
2168-
2169- # Then join all the rooms we are configured in (and init their plugins)
2170- roomjoinds = []
2171- for rjid, rconf in self.conf['rooms'].items():
2172- pm = PluginManager(self.messagehandler, self.usermanagement, rjid)
2173- if rconf['plugins']:
2174- for p in rconf['plugins']:
2175- pm.load(p, rconf['plugindata'][p])
2176- for p in rconf['plugins']:
2177- pm.init(p)
2178-
2179- rnick = rconf.get('nick', self.nick)
2180-
2181- logging.info("Joining Room " + rjid + " as \"" + rnick + "\"")
2182-
2183- logging.info("We have normality. I repeat, we have normality. "
2184- "Anything you still can't cope with is therefore your "
2185- "own problem.")
2186+ # wait for the wokkelhandler and rosterhandler to connect
2187+ whd = Deferred()
2188+ rhd = Deferred()
2189+ self.wokkelhandler.set_connected_handler(whd)
2190+ self.rosterhandler.set_connected_handler(rhd)
2191+ yield DeferredList([whd, rhd])
2192
2193 def run(self):
2194 reactor.run()
2195
2196+
2197 def main():
2198 parser = ArgumentParser(prog="endroid",
2199 epilog="I'm a robot. I'm not a refrigerator.",
2200- description="EnDroid: extensible XMPP Bot")
2201+ description="EnDroid: Extensible XMPP Bot")
2202 parser.add_argument("conffile", nargs='?', default="",
2203 help="Configuration file to use.")
2204 parser.add_argument("-l", "--level", type=int, default=logging.INFO,
2205@@ -133,13 +110,15 @@
2206 help="File for logging output.")
2207 parser.add_argument("-t", "--logtraffic", action='store_true',
2208 help="Additionally log all traffic.")
2209+ parser.add_argument("-m", "--manhole", nargs='+', default=None,
2210+ help="Login name, password and port for ssh access")
2211 args = parser.parse_args()
2212
2213 if args.logfile:
2214 logging.basicConfig(filename=args.logfile, level=args.level,
2215- format=LOGGING_FORMAT)
2216+ format=LOGGING_FORMAT, datefmt=LOGGING_DATE_FORMAT)
2217 else:
2218- logging.basicConfig(level=args.level, format=LOGGING_FORMAT)
2219+ logging.basicConfig(level=args.level, format=LOGGING_FORMAT, datefmt=LOGGING_DATE_FORMAT)
2220
2221 cmd = args.conffile
2222 env = os.environ.get("ENDROID_CONF", "")
2223@@ -153,8 +132,14 @@
2224 sys.exit(1)
2225
2226 logging.info("EnDroid starting up with conffile {0}".format(conffile))
2227+
2228 droid = Endroid(conffile, logtraffic=args.logtraffic)
2229- droid.run()
2230+
2231+ if args.manhole:
2232+ manhole_dict = dict([('droid', droid)] + globals().items())
2233+ start_manhole(droid, manhole_dict, args.manhole)
2234+ else:
2235+ droid.run()
2236
2237 if __name__ == "__main__":
2238 main()
2239
2240=== removed file 'src/endroid/config.py'
2241--- src/endroid/config.py 2012-08-31 12:10:51 +0000
2242+++ src/endroid/config.py 1970-01-01 00:00:00 +0000
2243@@ -1,159 +0,0 @@
2244-# -----------------------------------------
2245-# Endroid - XMPP Bot
2246-# Copyright 2012, Ensoft Ltd.
2247-# Created by Jonathan Millican
2248-# -----------------------------------------
2249-
2250-from ConfigParser import ConfigParser
2251-import re
2252-import os.path
2253-import logging
2254-import sys
2255-
2256-pluginOptions = {}
2257-
2258-class EnConfigParser(ConfigParser, dict):
2259- def _sections(self):
2260- return ConfigParser._sections(self)
2261-
2262- def __setitem__(self, key, value):
2263- super(EnConfigParser, self).__setitem__(key, value)
2264-
2265- def __delitem__(self):
2266- return super(EnConfigParser,self).__delitem__()
2267-
2268- def __iter__(self):
2269- return super(EnConfigParser,self).__iter__()
2270-
2271-
2272- def __init__(self, filename):
2273- super(EnConfigParser, self).__init__()
2274-
2275- self.optionxform = str
2276- with open(filename) as f:
2277- self.readfp(f)
2278-
2279- self['database'] = {'dbfile': self.get('Database', 'dbfile')}
2280- self['jid'] = self.get('Setup', 'jid')
2281- self['secret'] = self.get('Setup', 'secret')
2282-
2283- self['plugindata'] = {}
2284-
2285- self['nick'] = self.get('Setup', 'nick', self['jid'])
2286- self['plugins'] = self.getlist('Setup', 'plugins')
2287- self.getPluginData(self['plugins'])
2288-
2289- self['users'] = self.getlist('Setup', 'users')
2290-
2291- # Plugins can be found in various dirs
2292- for dir in self.getlist('Setup', 'plugindirs', ['~/.endroid/plugins/']):
2293- sys.path.append(os.path.expanduser(dir))
2294-
2295- defRoom = {}
2296- defRoom['nick'] = self.get('Room:*', 'nick', self['nick'])
2297- defRoom['plugins'] = self.getlist('Room:*', 'plugins', self['plugins'])
2298- self.getPluginData(defRoom['plugins'])
2299-
2300- roomlist = self.getlist('Setup', 'rooms')
2301- self['rooms'] = {}
2302- self['rooms'] = dict((k, defRoom.copy()) for k in roomlist)
2303-
2304- defUGroup = {'users': self['users'][:]} # Creates a copy of self['users']
2305- defUGroup['plugins'] = self.getlist('UserGroup:*', 'plugins', self['plugins'])
2306- self.getPluginData(defUGroup['plugins'])
2307-
2308- self['usergroups'] = {'*': defUGroup}
2309-
2310- for item in self.sections():
2311- if item[0:10] == 'UserGroup:' and item[10:] != '*':
2312- key = item[10:]
2313- logging.info("Found User Group: " + key)
2314- uGroup = {}
2315- uGroup['users'] = self.getlist(item, 'users')
2316- for userjid in uGroup['users']:
2317- if userjid in self['usergroups']['*']['users']:
2318- self['usergroups']['*']['users'].remove(userjid)
2319- logging.info("User " + userjid + " added to " + key + ", and removed from default user group.")
2320- else:
2321- uGroup['users'].remove(userjid)
2322- logging.info("User " + userjid + " not permitted in Setup section. Not being added to " + key)
2323-
2324- uGroup['plugins'] = self.getlist(item, 'plugins', self['usergroups']['*']['plugins'])
2325- self.getPluginData(uGroup['plugins'])
2326-
2327- self['usergroups'][key] = uGroup
2328-
2329- elif item[0:5] == 'Room:' and item[5:] != '*':
2330- key = item[5:]
2331- if self['rooms'].has_key(key):
2332- self['rooms'][key]['nick'] = self.get(item, 'nick', self['nick'])
2333- self['rooms'][key]['plugins'] = self.getlist(item, 'plugins', defRoom['plugins'])
2334- self.getPluginData(self['rooms'][key]['plugins'])
2335-
2336- for roomjid in self['rooms']:
2337- self['rooms'][roomjid]['plugindata'] = {}
2338- for plugin in self['rooms'][roomjid]['plugins']:
2339- self['rooms'][roomjid]['plugindata'][plugin] = self.getSpecificPluginData(plugin, "Room:" + roomjid)
2340-
2341-
2342- def getPluginData(self, pList):
2343- for plugin in pList:
2344- if not self['plugindata'].has_key(plugin):
2345- self['plugindata'][plugin] = plugin_config(plugin, self)
2346-
2347- def getSpecificPluginData(self, pName, section):
2348- self.getPluginData([pName])
2349- if os.path.exists('config/plugin-' + pName + '.cfg'):
2350- pData = self['plugindata'][pName].copy()
2351- pConf = ConfigParser()
2352- pConf.optionxform = str
2353- with open('config/plugin-' + pName + '.cfg') as f:
2354- pConf.readfp(f)
2355-
2356- if not pConf.has_section(section):
2357- return pData
2358- else:
2359- opts = pConf.options(section)
2360- for option in opts:
2361- pData[option] = pConf.get(section, option)
2362- return pData
2363- else:
2364- return self['plugindata'][pName]
2365-
2366-
2367- def get(self, section, option, default=None):
2368- if default == None:
2369- return ConfigParser.get(self, section, option)
2370- else:
2371- if self.has_option(section, option):
2372- return ConfigParser.get(self, section, option)
2373- else:
2374- return default
2375-
2376- def getlist(self, section, option, default=None):
2377- if self.has_option(section, option) or default == None:
2378- return as_list(self.get(section, option, ''))
2379- else:
2380- return default
2381-
2382-SPLITTER = re.compile("[,\n]")
2383-
2384-def as_list(value):
2385- """
2386- Given a string containing potentially multiple lines of comma-separated
2387- items, returns a list of those items. Newlines are treated as separators
2388- (even without a trailing comma), and all items are stripped of whitespace
2389- either side.
2390- """
2391- return [item.strip() for item in SPLITTER.split(value) if item.strip()]
2392-
2393-def plugin_config(plugName, mConf):
2394- if pluginOptions.has_key(plugName):
2395- return pluginOptions[plugName]
2396- confData = {}
2397- if mConf.has_section('Plugin:' + plugName):
2398- opts = mConf.options('Plugin:' + plugName)
2399- for option in opts:
2400- confData[option] = mConf.get('Plugin:' + plugName, option)
2401- pluginOptions[plugName] = confData
2402- return confData
2403
2404=== added file 'src/endroid/confparser.py'
2405--- src/endroid/confparser.py 1970-01-01 00:00:00 +0000
2406+++ src/endroid/confparser.py 2013-08-12 17:50:48 +0000
2407@@ -0,0 +1,179 @@
2408+from ConfigParser import ConfigParser
2409+from ast import literal_eval
2410+from collections import defaultdict
2411+import copy
2412+import re
2413+
2414+class Parser(object):
2415+ """Reads an ini-like configuration file into an internal dictionary.
2416+ Extra syntax:
2417+ - nested sections:
2418+ [root:child:grandchild:...]: stored as nested dictionaries with values
2419+ accessible via .get("root", "child", "grandchild", ..., "key")
2420+ - or syntax:
2421+ [root:child1|child2:...]: .get("root", "child1", ...) and .get("root", "child2"...)
2422+ will look in this section. Note that the or syntax is usable at any depth
2423+ and with any number of alternatives (so [root1|root2:child1|child2|child2:...]
2424+ is fine)
2425+ - wildcard syntax:
2426+ [root:*:grandchild:...]: .get("root", <anything>: "grandchild", ...) will
2427+ look in this section. Again the wildcard character may be used at any depth
2428+ (so [*:*:*:...] is doable)
2429+ - order of search:
2430+ the .get method will return the most specified result it can find. The order
2431+ of search is:
2432+ [foo:bar] - first
2433+ [foo:bar|baz]
2434+ [foo|far:bar]
2435+ [foo|far:bar|baz]
2436+ [foo:*]
2437+ [foo|far:*]
2438+ [*:bar]
2439+ [*:bar|baz]
2440+ [*:*] - last
2441+ - lists:
2442+ - Parser will try to identify lists by the presence interior commas and
2443+ newlines. The entry:
2444+ key = var1, var2, var3
2445+ will be returned as a list
2446+ - For a single item list, a comma must be present:
2447+ key = var1,
2448+ - Multiline lists do not need commas:
2449+ key = var1
2450+ var2
2451+ var3
2452+ as internal newlines are present
2453+ Arguments to .get:
2454+ - 'default' may be specified in which case KeyErrors will never be raised
2455+ - 'return_all' will cause get to return all possible results rather than
2456+ only the most relevant
2457+ Notes:
2458+ - All section names will have all whitespace removed and will be converted
2459+ to lower case ([Foo | BAR ] -> [foo|bar])
2460+ - Values in the config file will be parsed with literal_eval so will return
2461+ from .get as Python objects rather than strings (though if literal_eval fails
2462+ then a string will be returned)
2463+ """
2464+ SPLITTER = re.compile("[,\n]")
2465+
2466+ def __init__(self, filename=None):
2467+ self.filename = filename
2468+ self.dict = {}
2469+ self._aliases = defaultdict(list)
2470+
2471+ if filename:
2472+ self.load(filename)
2473+
2474+ def load(self, filename=None):
2475+ filename = filename or self.filename
2476+ self.read_file(filename)
2477+ self.build_dict()
2478+
2479+ def read_file(self, filename):
2480+ cp = ConfigParser()
2481+ cp.optionxform = Parser.sanitise
2482+
2483+ with open(filename) as f:
2484+ cp.readfp(f)
2485+ self.filename = filename
2486+
2487+ # transform Parser section labels into lists of sanitise label parts
2488+ # eg "foo: bar | Bar2 : BAZ" -> ["foo","bar|bar2","baz"]
2489+ sections = cp.sections()
2490+ process_tuple = Parser.process_tuple
2491+ self._parts = [map(Parser.sanitise, s.split(':')) for s in sections]
2492+ self._items = [map(process_tuple, ts) for ts in map(cp.items, sections)]
2493+
2494+ def build_dict(self):
2495+ new_dict = {}
2496+ for part_list, items_list in zip(self._parts, self._items):
2497+ d = new_dict
2498+ for part in part_list:
2499+ d = d.setdefault(part, {})
2500+ # set the value when we get to the last part
2501+ d.update(dict(items_list))
2502+
2503+ self.dict = new_dict
2504+
2505+ # register aliases (for the or syntax)
2506+ for part in [p for parts in self._parts for p in parts if '|' in p]:
2507+ for sub_p in part.split('|'):
2508+ # register the full arg against its parts if we haven't
2509+ # already so for the arg "name1|name2":
2510+ # self._aliases[name1] = ["name1|name2"]
2511+ # self._aliases[name2] = ["name1|name2"]
2512+ if not part in self._aliases[sub_p]:
2513+ self._aliases[sub_p].append(part)
2514+
2515+ def get(self, *args, **kwargs):
2516+ # note that this is quite a slow lookup function to support the wildcard
2517+ # '*' and or '|' syntax without complicating self.dict - shouldn't matter
2518+ # as it shouldn't be called too often
2519+ if not self.filename:
2520+ msg = "[{0}] lookup but no file loaded"
2521+ raise ValueError(msg.format(':'.join(args)))
2522+
2523+ dicts = [self.dict]
2524+ for arg in [a.lower() for a in args]:
2525+ # currently looking in [dict1, dict2...] - now move our focus to
2526+ # [dict1[a], dict2[a]... dict1[a|o], dict2[a|o]... dict1[*], dict2[*]...]
2527+ # in order: [arg, aliases (eg arg|other), wildcard] (so most specified
2528+ # comes first)
2529+ dicts = [d.get(key) for d in dicts
2530+ for key in [arg] + self._aliases[arg] + ['*'] if key in d]
2531+
2532+ # get the result
2533+ if 'return_all' in kwargs:
2534+ result = dicts
2535+ elif len(dicts):
2536+ result = dicts[0]
2537+ elif 'default' in kwargs:
2538+ result = kwargs['default']
2539+ else:
2540+ msg = "[{0}] not defined in {1}"
2541+ raise KeyError(msg.format(':'.join(args), self.filename))
2542+
2543+ # if the result is mutable make a copy of it to prevent accidental modification
2544+ # of the config dictionary
2545+ if isinstance(result, (dict, list)):
2546+ return copy.deepcopy(result)
2547+ else: # immutable type
2548+ return result
2549+
2550+
2551+ @staticmethod
2552+ def sanitise(string):
2553+ # remove _all_ whitespace (matched by \s) and convert to lowercase
2554+ return re.sub(r"\s", "", string).lower()
2555+
2556+ @staticmethod
2557+ def as_list(string):
2558+ # transforms strings containing interior commas or newlines into a list
2559+ return [s.strip() for s in Parser.SPLITTER.split(string) if s.strip()]
2560+
2561+ @staticmethod
2562+ def process_tuple(label_val):
2563+ # given a label, value tuple (where label and value are both strings)
2564+ # attempts to interpret value as a python object using literal_eval
2565+ # and/or as a list (in the case that value contains interior newlines
2566+ # or commas)
2567+ def process_val(value):
2568+ # if literal_eval fails (eg if value really is a string) then return
2569+ # a cleaned-up resion. Note we do not want to apply any other transformation
2570+ # to value as eg case might be important
2571+ try:
2572+ return literal_eval(value)
2573+ except:
2574+ # it is a plain string or cannot be parsed so return as string
2575+ return value.strip()
2576+
2577+ label, val = label_val
2578+ # val strings at end of lines will have an \n so must strip them
2579+ val = val.strip()
2580+ if Parser.SPLITTER.search(val): # val is a list of items
2581+ val = [process_val(v) for v in Parser.as_list(val)]
2582+ else: # it is single value
2583+ val = process_val(val)
2584+
2585+ return (label, val)
2586+
2587
2588=== modified file 'src/endroid/cron.py'
2589--- src/endroid/cron.py 2012-09-03 17:55:29 +0000
2590+++ src/endroid/cron.py 2013-08-12 17:50:48 +0000
2591@@ -11,46 +11,78 @@
2592 from pytz import timezone
2593 import logging
2594
2595+
2596 class Task(object):
2597 """
2598 Wrapper object providing direct access to a specific "task". Obtain an
2599 instance using register on the Cron singleton.
2600+
2601 """
2602- __slot__ = ('name', 'cron')
2603+ __slots__ = ('name', 'cron')
2604+
2605 def __init__(self, name, cron):
2606 self.name = name
2607 self.cron = cron
2608+
2609 def doAtTime(self, time, locality, params):
2610 return self.cron.doAtTime(time, locality, self.name, params)
2611+
2612 def setTimeout(self, timedelta, params):
2613 return self.cron.setTimeout(timedelta, self.name, params)
2614
2615+
2616 class Cron(object):
2617+ # a wrapper around the CronSing singleton
2618 cron = None
2619
2620 @staticmethod
2621 def get():
2622- if Cron.cron == None:
2623+ if Cron.cron is None:
2624 Cron.cron = CronSing()
2625 return Cron.cron
2626
2627
2628 class CronSing(object):
2629+ """
2630+ A singleton providing task scheduling facilities.
2631+ A function may be registered by calling .register(function, name)
2632+ and scheduled with either setTimeout(time, name, params) or
2633+ doAtTime(time, locality, name, params).
2634+ When it comes to be called, the function will be called with an argument
2635+ generated from params (so even if the function needs no arguments it should
2636+ provide one eg def foo(_) rather than def foo()
2637+
2638+ """
2639 def __init__(self):
2640 self.delayedcall = None
2641 self.fun_dict = {}
2642 self.db = Database('Cron')
2643+ # table for tasks which will be called after a certain amount of time
2644 if not self.db.table_exists('cron_delay'):
2645- self.db.create_table('cron_delay', ['timestamp', 'fun_name', 'params'])
2646+ self.db.create_table('cron_delay',
2647+ ['timestamp', 'reg_name', 'params'])
2648+ # table for tasks which will be called at a specific time
2649 if not self.db.table_exists('cron_datetime'):
2650- self.db.create_table('cron_datetime', ['datetime', 'locality', 'fun_name', 'params'])
2651-
2652+ self.db.create_table('cron_datetime',
2653+ ['datetime', 'locality', 'reg_name', 'params'])
2654+
2655 def seconds_until(self, td):
2656- return float((td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6)) / 10**6
2657-
2658- def register(self, fun, name):
2659- self.fun_dict.update({name: fun})
2660- return Task(name, self)
2661+ ds, ss, uss = td.days, td.seconds, td.microseconds
2662+ return float((uss + (ss + ds * 24 * 3600) * 10**6)) / 10**6
2663+
2664+ def register(self, function, reg_name, persistent=True):
2665+ """Register the callable fun against reg_name.
2666+
2667+ Allows callable to be scheduled with doAtTime or setTimeout."""
2668+ # reg_name is the key we use to access the function - we can then set the
2669+ # function to be called using setTimeout or doAtTime with regname = name
2670+
2671+ # remove any prior functions with this reg_name
2672+ if not persistent:
2673+ self.removeTask(reg_name)
2674+
2675+ self.fun_dict.update({reg_name: function})
2676+ return Task(reg_name, self)
2677
2678 def cancel(self):
2679 if self.delayedcall:
2680@@ -61,14 +93,14 @@
2681 if self.delayedcall:
2682 self.delayedcall.cancel()
2683 self.delayedcall = reactor.callLater(0, self._do_crons)
2684-
2685+
2686 def _time_left_delay(self, pickleTime):
2687 curtime = datetime.datetime.now(timezone('GMT'))
2688 dt = cPickle.loads(str(pickleTime))
2689 dt_gmt = dt.astimezone(timezone('GMT'))
2690 time_delta = dt_gmt - curtime
2691 return self.seconds_until(time_delta)
2692-
2693+
2694 def _time_left_set_time(self, pickleTime, locality):
2695 curtime = datetime.datetime.now(timezone('GMT'))
2696 dt = cPickle.loads(str(pickleTime))
2697@@ -76,89 +108,131 @@
2698 dt_gmt = dt_local.astimezone(timezone('GMT'))
2699 time_delta = dt_gmt - curtime
2700 return self.seconds_until(time_delta)
2701-
2702-
2703+
2704 def _do_crons(self):
2705 self.delayedcall = None
2706-# curtime = datetime.now(timezone('GMT'))
2707- delays = self.db.fetch('cron_delay', ['timestamp', 'fun_name', 'params', 'rowid'])
2708- set_times = self.db.fetch('cron_datetime', ['datetime', 'fun_name', 'params', 'locality', 'rowid'])
2709-
2710-# crons = [{'table':'cron_delay', 'data':data, 'time_left': self.seconds_until(cPickle.loads(str(data['timestamp'])).astimezone(timezone('GMT')) - curtime)} for data in delays ]
2711-# crons = []
2712-# for data in delays:
2713-# dt = cPickle.loads(str(data['timestamp']))
2714-# dt_gmt = dt.astimezone(timezone('GMT'))
2715-# time_delta = dt_gmt - curtime
2716-# time_left = self.seconds_until(time_delta)
2717-# crons.append({'table':'cron_delay', 'data':data, 'time_left':time_left})
2718-#
2719-# for data in set_times:
2720-# dt = cPickle.loads(str(data['datetime']))
2721-# dt_local = timezone(data['locality']).localize(dt)
2722-# dt_gmt = dt_local.astimezone(timezone('GMT'))
2723-# time_delta = dt_gmt - curtime
2724-# time_left = self.seconds_until(time_delta)
2725-# crons.append({'table':'cron_datetime', 'data':data, 'time_left':time_left})
2726-
2727-# crons_s = [{'table':'cron_datetime', 'data':data, 'time_left': self.seconds_until(timezone(data['locality']).localize(cPickle.loads(str(data['datetime']))).astimezone(timezone('GMT')) - curtime)} for data in set_times]
2728+ # retrieve the information about our two kinds of scheduled tasks from
2729+ # the database
2730+ delays = self.db.fetch('cron_delay',
2731+ ['timestamp', 'reg_name', 'params', 'rowid'])
2732+ set_times = self.db.fetch('cron_datetime',
2733+ ['datetime', 'reg_name', 'params',
2734+ 'locality', 'rowid'])
2735+
2736+ # transform the two types of data into a consistant format and combine
2737+ # data is the raw information we retrieved from self.db
2738 crons_d = [{
2739 'table': 'cron_delay',
2740 'data': data,
2741 'time_left': self._time_left_delay(data['timestamp'])
2742- } for data in delays]
2743+ } for data in delays]
2744 crons_s = [{
2745 'table': 'cron_datetime',
2746 'data': data,
2747- 'time_left': self._time_left_set_time(data['datetime'], data['locality'])
2748+ 'time_left': self._time_left_set_time(data['datetime'],
2749+ data['locality'])
2750 } for data in set_times]
2751 crons = crons_d + crons_s
2752-
2753+
2754 shortest = None
2755-
2756+
2757+ # run all crons with time_left <= 0, find smallest time_left amongst
2758+ # others and reschedule ourself to run again after this time
2759 for cron in crons:
2760 if cron['time_left'] <= 0:
2761+ # the function is ready to be called
2762+ # remove the entry from the database and call it
2763 self.db.delete(cron['table'], cron['data'])
2764- logging.info("Running Cron: " + cron['data']['fun_name'])
2765- self.fun_dict[cron['data']['fun_name']](cPickle.loads(str(cron['data']['params'])))
2766+ logging.info("Running Cron: " + cron['data']['reg_name'])
2767+ params = cPickle.loads(str(cron['data']['params']))
2768+ try:
2769+ self.fun_dict[cron['data']['reg_name']](params)
2770+ except KeyError:
2771+ # If there has been a restart we will have lost our fun_dict
2772+ # If functions have not been re-registered then we will have a problem.
2773+ logging.error("Failed to run Cron: {} not in dictionary".format(cron['data']['reg_name']))
2774 else:
2775- if (shortest == None) or cron['time_left'] < shortest:
2776+ # update the shortest time left
2777+ if (shortest is None) or cron['time_left'] < shortest:
2778 shortest = cron['time_left']
2779- if not shortest == None:
2780+ if not shortest is None: #ie there is another function to be scheduled
2781 self.delayedcall = reactor.callLater(shortest, self._do_crons)
2782
2783- def doAtTime(self, time, locality, fun_name, params):
2784+ def doAtTime(self, time, locality, reg_name, params):
2785+ """
2786+ Start a cron job to trigger the specified function ('reg_name') with the
2787+ specified arguments ('params') at time ('time', 'locality').
2788+
2789+ """
2790 lTime = timezone(locality).localize(time)
2791 gTime = lTime.astimezone(timezone('GMT'))
2792-
2793- logging.info("Cron task \"" + fun_name + "\" set for " + str(lTime) + " (" + str(gTime) + " in GMT)")
2794- t,p = self._pickleTimeParams(time, params)
2795-
2796- self.db.insert('cron_datetime', {'datetime': t, 'locality': locality, 'fun_name': fun_name, 'params': p})
2797+
2798+ fmt = "Cron task '{}' set for {} ({} GMT)"
2799+ logging.info(fmt.format(reg_name, lTime, gTime))
2800+ t, p = self._pickleTimeParams(time, params)
2801+
2802+ self.db.insert('cron_datetime', {'datetime': t, 'locality': locality,
2803+ 'reg_name': reg_name, 'params': p})
2804 self.do_crons()
2805-
2806-
2807+
2808 def setTimeout(self, timedelta, reg_name, params):
2809 """
2810- Start a cron job to trigger the specified registration ('reg_name') with the
2811- specified arguments ('params') after the specified delay ('timedelta').
2812+ Start a cron job to trigger the specified registration ('reg_name') with
2813+ specified arguments ('params') after delay ('timedelta').
2814
2815 timedelta may either be a datetime.timedelta object, or a real number
2816- representing a number of seconds to wait. Negative or 0 values will trigger
2817- near immediately.
2818+ representing a number of seconds to wait. Negative or 0 values will
2819+ trigger near immediately.
2820+
2821 """
2822 if not isinstance(timedelta, datetime.timedelta):
2823 timedelta = datetime.timedelta(seconds=timedelta)
2824- logging.info('Cron task "{0}" set to run after {1}'.format(reg_name,
2825- str(timedelta)))
2826+
2827+ fmt = 'Cron task "{0}" set to run after {1}'
2828+ logging.info(fmt.format(reg_name, str(timedelta)))
2829+
2830 time = datetime.datetime.now(timezone('GMT')) + timedelta
2831-
2832 t, p = self._pickleTimeParams(time, params)
2833-
2834- self.db.insert('cron_delay', {'timestamp': t, 'fun_name': reg_name,
2835- 'params': p})
2836+
2837+ self.db.insert('cron_delay', {'timestamp': t, 'reg_name': reg_name,
2838+ 'params': p})
2839 self.do_crons()
2840-
2841-
2842+
2843+ def removeTask(self, reg_name):
2844+ """Remove any scheduled tasks registered with reg_name."""
2845+ self.db.delete('cron_delay', {'reg_name': reg_name})
2846+ self.db.delete('cron_datetime', {'reg_name': reg_name})
2847+ self.fun_dict.pop('reg_name', None)
2848+
2849+ def getAtTimes(self):
2850+ """
2851+ Return a string showing the registration names of functions scheduled
2852+ with doAtTime and the amount of time they will be called in.
2853+
2854+ """
2855+ def get_single_string(data):
2856+ fmt = " name '{}' to run in '{}:{}:{}'"
2857+ name = data['reg_name']
2858+ delay = int(round(self._time_left_set_time(data['datetime'], data['locality'])))
2859+ return fmt.format(name, delay // 3600, (delay % 3600) // 60, delay % 60)
2860+
2861+ data = self.db.fetch('cron_datetime', ['reg_name', 'locality', 'datetime'])
2862+ return "Datetime registrations:\n" + '\n'.join(map(get_single_string, data))
2863+
2864+ def getTimeouts(self):
2865+ """
2866+ Return a string showing the registration names of functions scheduled
2867+ with setTimeout and the amount of time they will be called in.
2868+
2869+ """
2870+ def get_single_string(data):
2871+ fmt = " name '{}' to run in '{}:{}:{}'"
2872+ name = data['reg_name']
2873+ delay = int(round(self._time_left_delay(data['timestamp'])))
2874+ return fmt.format(name, delay // 3600, (delay % 3600) // 60, delay % 60)
2875+
2876+ data = self.db.fetch('cron_delay', ['reg_name', 'timestamp'])
2877+ return "Timeout registrations:\n" + '\n'.join(map(get_single_string, data))
2878+
2879 def _pickleTimeParams(self, time, params):
2880 return cPickle.dumps(time), cPickle.dumps(params)
2881
2882=== modified file 'src/endroid/database.py'
2883--- src/endroid/database.py 2012-08-09 09:06:58 +0000
2884+++ src/endroid/database.py 2013-08-12 17:50:48 +0000
2885@@ -27,6 +27,12 @@
2886 return self[EndroidUniqueID]
2887
2888 class Database(object):
2889+ """Wrapper round an sqlite3 Database
2890+
2891+ All accesses are synchronous, TODO use twisted.enterprise.adbapi to
2892+ asynchronise them.
2893+
2894+ """
2895 connection = None
2896 cursor = None
2897 file_name = None
2898
2899=== added file 'src/endroid/manhole.py'
2900--- src/endroid/manhole.py 1970-01-01 00:00:00 +0000
2901+++ src/endroid/manhole.py 2013-08-12 17:50:48 +0000
2902@@ -0,0 +1,37 @@
2903+from twisted.cred import portal, checkers
2904+from twisted.conch import manhole, manhole_ssh
2905+from twisted.internet import reactor
2906+import logging
2907+
2908+def manhole_factory(nmspace, **passwords):
2909+ realm = manhole_ssh.TerminalRealm()
2910+
2911+ def getManhole(_):
2912+ return manhole.Manhole(nmspace)
2913+
2914+ realm.chainedProtocolFactory.protocolFactory = getManhole
2915+ p = portal.Portal(realm)
2916+ p.registerChecker(
2917+ checkers.InMemoryUsernamePasswordDatabaseDontUse(**passwords)
2918+ )
2919+ f = manhole_ssh.ConchFactory(p)
2920+ return f
2921+
2922+# how to log in: ssh user_name@localhost -p port
2923+def start_manhole(droid, nmspace, cliargs):
2924+ """Start EnDroid listening for an ssh connection.
2925+
2926+ Logging in via ssh gives access to a python prompt from which EnDroid's
2927+ internals can be investigated.
2928+
2929+ Login details are specified in the cliargs tuple, which contains:
2930+ (login_name, login_password, port)
2931+
2932+ """
2933+ logging.info("Starting manhole")
2934+ # room to expand / add functionality to the manhole_dict here
2935+ manhole_dict = dict(locals().items() + globals().items())
2936+ login, password, port = cliargs
2937+ reactor.listenTCP(int(port), manhole_factory(nmspace, **{login: password}))
2938+ droid.run()
2939+
2940
2941=== modified file 'src/endroid/messagehandler.py'
2942--- src/endroid/messagehandler.py 2012-09-01 23:15:55 +0000
2943+++ src/endroid/messagehandler.py 2013-08-12 17:50:48 +0000
2944@@ -5,152 +5,203 @@
2945 # -----------------------------------------
2946
2947 import logging
2948-
2949-PRIORITY_NORMAL = 0
2950-PRIORITY_URGENT = -1
2951-PRIORITY_BULK = 1
2952+from twisted.words.protocols.jabber.jid import JID
2953+
2954+
2955+class Handler(object):
2956+ __slots__ = ("name", "priority", "callback")
2957+ def __init__(self, priority, callback):
2958+ self.name = callback.__name__
2959+ self.priority = priority
2960+ self.callback = callback
2961+
2962+ def __str__(self):
2963+ return "{}: {}".format(self.priority, self.name)
2964+
2965
2966 class MessageHandler(object):
2967- def __init__(self, wh):
2968- self._handlers = {}
2969+ """An abstraction of XMPP's message protocols."""
2970+
2971+ PRIORITY_NORMAL = 0
2972+ PRIORITY_URGENT = -1
2973+ PRIORITY_BULK = 1
2974+
2975+ def __init__(self, wh, um):
2976 self.wh = wh
2977+ self.um = um
2978+ # wh translates messages and gives them to us, needs to know who we are
2979 self.wh.set_message_handler(self)
2980-
2981- def register_callback(self, typ, cat, key, callback,
2982- including_self=False, priority=0):
2983+ self._handlers = {}
2984+
2985+ def register_callback(self, name, typ, cat, callback,
2986+ including_self=False, priority=PRIORITY_NORMAL):
2987+ """
2988+ Register a function to be called on receipt of a message of type
2989+ 'typ' (muc/chat), category 'cat' (recv, send, unhandled, *_self, *_filter)
2990+ sent from user or room 'name'.
2991+
2992+ """
2993+ # self._handlers is a dictionary of form:
2994+ # { type : { category : { room/groupname : [Handler objects]}}}
2995 typhndlrs = self._handlers.setdefault(typ, {})
2996 cathndlrs = typhndlrs.setdefault(cat, {})
2997- handlers = cathndlrs.setdefault(key, [])
2998- handlers.append((priority, callback))
2999- handlers.sort()
3000+ handlers = cathndlrs.setdefault(name, [])
3001+ handlers.append(Handler(priority, callback))
3002+ handlers.sort(key=lambda h: h.priority)
3003+
3004+ # this callback be called when we get messages sent by ourself
3005 if including_self:
3006- self.register_callback(typ, cat + "_self", key, callback,
3007+ self.register_callback(name, typ, cat + "_self", callback,
3008 priority=priority)
3009
3010- def register_muc_handler(self, rjid, handler, including_self=False, priority=0):
3011- self.register_callback("muc", "recv", rjid, handler, including_self, priority)
3012-
3013- def register_chat_handler(self, users, handler, including_self=False, priority=0):
3014- for user in users:
3015- self.register_callback("chat", "recv", user, handler, including_self, priority)
3016-
3017- def register_unhandled_muc_handler(self, rjid, handler, including_self=False,
3018- priority=0):
3019- self.register_callback("muc", "unhandled", rjid, handler, including_self, priority)
3020-
3021- def register_unhandled_chat_handler(self, users, handler, including_self=False,
3022- priority=0):
3023- for user in users:
3024- self.register_callback("chat", "unhandled", user, handler, including_self, priority)
3025-
3026- def register_muc_recv_filter(self, rjid, handler, priority=0):
3027- self.register_callback("muc", "recv_filter", rjid, handler, priority=priority)
3028-
3029- def register_chat_recv_filter(self, users, handler, priority=0):
3030- for user in users:
3031- self.register_callback("chat", "recv_filter", user, handler, priority=priority)
3032-
3033- def register_muc_send_filter(self, rjid, handler, priority=0):
3034- self.register_callback("muc", "send_filter", rjid, handler, priority=priority)
3035-
3036- def register_chat_send_filter(self, users, handler, priority=0):
3037- for user in users:
3038- self.register_callback("chat", "send_filter", user, handler, priority=priority)
3039-
3040- def do_callback(self, typ, cat, key, msg, failback):
3041- handlers = self._handlers.get(typ, {}).get(cat, {})
3042- filters = self._handlers.get(typ, {}).get(cat + "_filter", {})
3043- if key in handlers and all(f(msg) for p, f in filters.get(key, [])):
3044+ def get_handlers(self, typ, cat, name):
3045+ dct = self._handlers.get(typ, {}).get(cat, {})
3046+ if typ == 'chat': # we need to lookup name's groups
3047+ # we may have either a full jid or just a userhost,
3048+ # groups are referenced by userhost
3049+ name = self.um.get_userhost(name)
3050+ handlers = []
3051+ for name in self.um.get_groups(name):
3052+ handlers.extend(dct.get(name, []))
3053+ handlers.sort(key=lambda h: h.priority)
3054+ return handlers
3055+ else: # we are in a room so only one set of handlers to read
3056+ return dct.get(name, [])
3057+
3058+ def get_filters(self, typ, cat, name):
3059+ return self.get_handlers(typ, cat+"_filter", name)
3060+
3061+ def do_callback(self, cat, msg, failback):
3062+ if msg.place == "muc":
3063+ # get the handlers active in the room - note that these are already
3064+ # sorted (sorting is done in the register_callback method)
3065+ handlers = self.get_handlers(msg.place, cat, msg.recipient)
3066+ filters = self.get_filters(msg.place, cat, msg.recipient)
3067+ else:
3068+ # combine the handlers from each group the user is registered with
3069+ # note that if the same plugin is registered for more than one of
3070+ # the user's groups, the plugin's instance in each group will be
3071+ # called
3072+ handlers = self.get_handlers(msg.place, cat, msg.sender)
3073+ filters = self.get_filters(msg.place, cat, msg.sender)
3074+
3075+ log_list = []
3076+ if handlers and all(f.callback(msg) for f in filters):
3077 msg.set_unhandled_cb(failback)
3078- for i in handlers[key]:
3079+
3080+ for i in handlers:
3081 msg.inc_handlers()
3082- for pri, cb in handlers[key]:
3083+
3084+ log_list.append("Did {} {} handlers (priority: cb):".format(len(handlers), cat))
3085+ for handler in handlers:
3086 try:
3087- cb(msg)
3088+ handler.callback(msg)
3089+ log_list.append(str(handler))
3090 except Exception as e:
3091- logging.error("Exception occurred in callback: {0}".format(str(e)))
3092- msg.unhandled()
3093+ log_list.append("Exception in {}:\n{}".format(handler.name, e))
3094+ msg.dec_handlers()
3095 raise
3096 else:
3097 failback(msg)
3098+ if log_list:
3099+ logging.info("\n\t".join(log_list))
3100
3101 def _unhandled_muc(self, msg):
3102- self.do_callback("muc", "unhandled", msg.rjid.userhost(), msg, lambda m: None)
3103+ self.do_callback("unhandled", msg, lambda m: None)
3104
3105 def _unhandled_self_muc(self, msg):
3106- self.do_callback("muc", "unhandled_self", msg.rjid.userhost(), msg,
3107- lambda m: None)
3108+ self.do_callback("unhandled_self", msg, lambda m: None)
3109
3110+ # Do normal (recv) callbacks on msg. If no callbacks handle the message
3111+ # then call unhandled callbacks (msg's failback is set self._unhandled_...
3112+ # by the last argument to do_callback).
3113 def receive_muc(self, msg):
3114- self.do_callback("muc", "recv", msg.rjid.userhost(), msg, self._unhandled_muc)
3115+ self.do_callback("recv", msg, self._unhandled_muc)
3116
3117 def receive_self_muc(self, msg):
3118- self.do_callback("muc", "recv_self", msg.rjid.userhost(), msg,
3119- self._unhandled_self_muc)
3120+ self.do_callback("recv_self", msg, self._unhandled_self_muc)
3121
3122 def _unhandled_chat(self, msg):
3123- self.do_callback("chat", "unhandled", msg.sender.userhost(), msg,
3124- lambda m: None)
3125+ self.do_callback("unhandled", msg, lambda m: None)
3126
3127 def _unhandled_self_chat(self, msg):
3128- self.do_callback("chat", "unhandled_self", msg.sender.userhost(), msg,
3129- lambda m: None)
3130+ self.do_callback("unhandled_self", msg, lambda m: None)
3131
3132 def receive_chat(self, msg):
3133- self.do_callback("chat", "recv", msg.sender.userhost(), msg,
3134- self._unhandled_chat)
3135+ self.do_callback("recv", msg, self._unhandled_chat)
3136
3137 def receive_self_chat(self, msg):
3138- self.do_callback("chat", "recv_self", msg.sender.userhost(), msg,
3139- self._unhandled_self_chat)
3140-
3141- def send_muc(self, rjid, text, source, priority=PRIORITY_NORMAL):
3142- filters = self._handlers.get("muc", {}).get("send_filter", {})
3143- msg = Message('muc', source, text, self, recipient=rjid, rjid=rjid)
3144- if all(f(msg) for p, f in filters.get(rjid.userhost(), [])):
3145- self.wh.groupChat(rjid, text)
3146-
3147- def send_chat(self, user, text, source, priority=PRIORITY_NORMAL):
3148- filters = self._handlers.get("chat", {}).get("send_filter", {})
3149- msg = Message('chat', source, text, self, recipient=user)
3150- if all(f(msg) for p, f in filters.get(user.userhost(), [])):
3151- self.wh.chat(user, text)
3152+ self.do_callback("recv_self", msg, self._unhandled_self_chat)
3153+
3154+ def send_muc(self, room, body, source=None, priority=PRIORITY_NORMAL):
3155+ """Send muc message to room.
3156+
3157+ The message will be run through any registered filters before it is sent.
3158+
3159+ """
3160+ msg = Message('muc', source, body, self, recipient=room)
3161+ # when sending messages we check the filters registered with the
3162+ # _recipient_. Cf. when we receive messages we check filters registered
3163+ # with the _sender_.
3164+ filters = self.get_filters('muc', 'send', msg.recipient)
3165+
3166+ if all(f.callback(msg) for f in filters):
3167+ logging.info("Sending message to {}".format(room))
3168+ self.wh.groupChat(JID(room), body)
3169+ else:
3170+ logging.info("Filtered out message to {}".format(room))
3171+
3172+ def send_chat(self, user, body, source=None, priority=PRIORITY_NORMAL):
3173+ """Send chat message to person with address user.
3174+
3175+ The message will be run through any registered filters before it is sent.
3176+
3177+ """
3178+ msg = Message('chat', source, body, self, recipient=user)
3179+ filters = self.get_filters('chat', 'send', msg.recipient)
3180+
3181+ if all(f.callback(msg) for f in filters):
3182+ logging.info("Sending message to {}".format(user))
3183+ self.wh.chat(JID(user), body)
3184 else:
3185 logging.info("Filtered out message to {0}".format(user))
3186
3187-
3188+
3189 class Message(object):
3190- def __init__(self, mtype, sender, body, messagehandler,
3191- recipient=None, handlers=0, rjid=None, sendernick=None,
3192- priority=PRIORITY_NORMAL):
3193- self.mtype = mtype
3194- self.sender = sender
3195- self.sendernick = sendernick
3196+ def __init__(self, place, sender, body, messagehandler, recipient, handlers=0,
3197+ priority=MessageHandler.PRIORITY_NORMAL):
3198+ self.place = place
3199+
3200+ # sender_full is a string representing the full jid (including resource)
3201+ # of the message's sender. Used in reply methods so that if a user is
3202+ # logged in on several resources, the reply will be sent to the right one
3203+ self.sender_full = sender
3204+ # a string represeting the userhost of the message's sender. Used to
3205+ # lookup resource-independant user properties eg their registered rooms
3206+ self.sender = messagehandler.um.get_userhost(sender)
3207 self.body = body
3208 self.recipient = recipient
3209+
3210+ # a count of plugins which will try to process this message
3211 self.__handlers = handlers
3212- self.rjid = rjid
3213 self.messagehandler = messagehandler
3214 self.priority = priority
3215
3216 def send(self):
3217- if self.mtype == "chat":
3218+ if self.place == "chat":
3219 self.messagehandler.send_chat(self.recipient, self.body, self.sender)
3220- elif self.mtype == "muc":
3221+ elif self.place == "muc":
3222 self.messagehandler.send_muc(self.recipient, self.body, self.sender)
3223
3224 def reply(self, body):
3225- if self.mtype == "chat":
3226- self.messagehandler.send_chat(self.sender, body, self)
3227- elif self.mtype == "muc":
3228- self.messagehandler.send_muc(self.rjid, body, self)
3229+ if self.place == "chat":
3230+ self.messagehandler.send_chat(self.sender_full, body, self.recipient)
3231+ elif self.place == "muc":
3232+ # we send to the room (the recipient), not the message's sender
3233+ self.messagehandler.send_muc(self.recipient, body, self.recipient)
3234
3235 def reply_to_sender(self, body):
3236- self.messagehandler.send_chat(self.sender, body, self)
3237-
3238- def prepare_response(self, tojid, message, ismuc=False):
3239- return Message('muc' if ismuc else 'chat', self.recipient, message,
3240- self.messagehandler, recipient=tojid)
3241+ self.messagehandler.send_chat(self.sender_full, body, self.recipient)
3242
3243 def inc_handlers(self):
3244 self.__handlers += 1
3245@@ -158,7 +209,7 @@
3246 def dec_handlers(self):
3247 self.__handlers -= 1
3248 self.do_unhandled()
3249-
3250+
3251 def unhandled(self, *args):
3252 """
3253 Notify the message that the caller hasn't handled it. This should only
3254@@ -167,12 +218,14 @@
3255
3256 This method takes arbitrary arguments so it can be used as deferred
3257 callback or errback.
3258+
3259 """
3260 self.dec_handlers()
3261-
3262+
3263 def do_unhandled(self):
3264 if self.__handlers == 0 and hasattr(self, 'unHandledCallback'):
3265 self.unHandledCallback(self)
3266-
3267+
3268 def set_unhandled_cb(self, cb):
3269 self.unHandledCallback = cb
3270+
3271
3272=== modified file 'src/endroid/pluginmanager.py'
3273--- src/endroid/pluginmanager.py 2012-09-01 23:15:55 +0000
3274+++ src/endroid/pluginmanager.py 2013-08-12 17:50:48 +0000
3275@@ -6,8 +6,25 @@
3276
3277 import sys
3278 import logging
3279-
3280-from endroid.messagehandler import PRIORITY_NORMAL
3281+from endroid.cron import Cron
3282+
3283+msg_filter_doc = ("Register a {0} filter for {1} messages.\n"
3284+ "Filter takes endroid.messagehandler.Message and returns bool. If its\n"
3285+ "return value evaluates to False, the message is dropped.\n"
3286+ "Priority specifies the order of calling - lower numbers = called earlier.\n")
3287+
3288+msg_send_doc = ("Send a {0} message to {1}, with given text and priority.\n"
3289+ "rjid is a string representing the JID of the room to send to.\n"
3290+ "text is a string (or unicode) of the message contents.\n"
3291+ "priority one of the PRIORITY_* constants from endroid.messagehandler.\n"
3292+ "(Use with care, especially PRIORITY_URGENT, which will usually bypass any\n"
3293+ "ratelimiting or other protections put in place.)")
3294+
3295+msg_cb_doc = ("Register a callback for {0} messages.\n"
3296+ "Callback takes endroid.messagehandler.Message and may alter it arbitrarily.\n"
3297+ "Inc_self specifies whether to do the callback if EnDroid created the message.\n"
3298+ "Priority specifies the order of calling - lower numbers = called earlier.\n")
3299+
3300
3301 class PluginMeta(type):
3302 """
3303@@ -39,195 +56,142 @@
3304
3305 def __init__(cls, name, bases, dict):
3306 type.__init__(cls, name, bases, dict)
3307+ cls.modname = cls.__module__
3308 PluginMeta.registry[cls.__module__] = cls
3309
3310+
3311 class Plugin(object):
3312 """
3313 Parent class of all plugin objects within EnDroid. Plugins must subclass
3314 this type and it also represents the entry point for the plugin into the
3315 rest of EnDroid.
3316+
3317 """
3318 __metaclass__ = PluginMeta
3319-
3320- def suc_users(self):
3321- return self._pm.suc_users()
3322-
3323+
3324 def _pluginInit(self, pm, conf):
3325 self._pm = pm
3326+
3327+ self.messagehandler = pm.messagehandler
3328+ self.usermanagement = pm.usermanagement
3329+
3330+ self.place = pm.place
3331+ self.place_name = pm.name
3332 self.vars = conf
3333-
3334- # Quick and nasty integration of usermanagement without abstracting away
3335- # functions.
3336-
3337- def presence(self):
3338- return self._pm.usermanagement.presence
3339+
3340+ def _register(self, *args, **kwargs):
3341+ return self.messagehandler.register_callback(self._pm.name, *args, **kwargs)
3342
3343 # Registration methods
3344-
3345- def register_muc_callback(self, callback, including_self=False, priority=0):
3346- if self._pm.rjid:
3347- self._pm.register_muc_callback(callback, including_self=False, priority=priority)
3348-
3349- def register_chat_callback(self, callback, including_self=False, priority=0):
3350- if not self._pm.rjid:
3351- self._pm.register_chat_callback(callback, including_self=False, priority=priority)
3352-
3353- def register_unhandled_muc_callback(self, callback, including_self=False, priority=0):
3354- if self._pm.rjid:
3355- self._pm.register_unhandled_muc_callback(callback, including_self=False, priority=priority)
3356-
3357- def register_unhandled_chat_callback(self, callback, including_self=False, priority=0):
3358- if not self._pm.rjid:
3359- self._pm.register_unhandled_chat_callback(callback, including_self=False, priority=priority)
3360-
3361- def register_muc_filter(self, callback, priority=0):
3362- """
3363- Register a receive filter for MUC messages. The specified callback is
3364- called whenever a MUC message is received, allowing it to perform
3365- either transformations on the message, and even choose to drop the
3366- message completely.
3367-
3368- callback is a callable that takes a single argument, which will be of
3369- type endroid.messagehandler.Message, representing the received
3370- message. If it returns a False value (including None), then the message
3371- is dropped - no further filters will be called, and no handlers are
3372- called.
3373- priority allows a relative prioritisation of different filters. Lower
3374- priority filters are run first. Within a priority level, filters are
3375- run in the order they are registered.
3376- """
3377- if self._pm.rjid:
3378- self._pm.register_muc_filter(callback, priority)
3379-
3380- def register_chat_filter(self, callback, priority=0):
3381- """
3382- Register a receive filter for regular chat messages. The specified
3383- callback is called whenever a chat message is received, allowing it to
3384- perform either transformations on the message, and even choose to drop
3385- the message completely.
3386-
3387- callback is a callable that takes a single argument, which will be of
3388- type endroid.messagehandler.Message, representing the received
3389- message. If it returns a False value (including None), then the message
3390- is dropped - no further filters will be called, and no handlers are
3391- called.
3392- priority allows a relative prioritisation of different filters. Lower
3393- priority filters are run first. Within a priority level, filters are
3394- run in the order they are registered.
3395- """
3396- if not self._pm.rjid:
3397- self._pm.register_chat_filter(callback, priority)
3398-
3399- def register_muc_send_filter(self, callback, priority=0):
3400- """
3401- Register a send filter for MUC messages. The specified callback is
3402- called whenever a MUC message is about to be sent, allowing it to
3403- perform either transformations on the message, and even choose to drop
3404- the message completely.
3405-
3406- callback is a callable that takes a single argument, which will be of
3407- type endroid.messagehandler.Message, representing the message about to
3408- be sent. If it returns a False value (including None), then the message
3409- is dropped - no further filters will be called, and the message is not
3410- sent on to the WokkelHandler.
3411- priority allows a relative prioritisation of different filters. Lower
3412- priority filters are run first. Within a priority level, filters are
3413- run in the order they are registered.
3414- """
3415- if self._pm.rjid:
3416- self._pm.register_muc_send_filter(callback, priority)
3417-
3418- def register_chat_send_filter(self, callback, priority=0):
3419- """
3420- Register a send filter for regular chat messages. The specified
3421- callback is called whenever a chat message is about to be sent,
3422- allowing it to perform either transformations on the message, and even
3423- choose to drop the message completely.
3424-
3425- callback is a callable that takes a single argument, which will be of
3426- type endroid.messagehandler.Message, representing the message about to
3427- be sent. If it returns a False value (including None), then the message
3428- is dropped - no further filters will be called, and the message is not
3429- sent on to the WokkelHandler.
3430- priority allows a relative prioritisation of different filters. Lower
3431- priority filters are run first. Within a priority level, filters are
3432- run in the order they are registered.
3433- """
3434- if not self._pm.rjid:
3435- self._pm.register_chat_send_filter(callback, priority)
3436-
3437- # Message send methods
3438-
3439- def send_muc(self, rjid, text, priority=PRIORITY_NORMAL):
3440- """
3441- Send a MUC message to the specified room ('rjid'), with the given text
3442- and at the specified priority.
3443-
3444- rjid is a string representing the JID of the room to send to.
3445- text is a string (or unicode) with the message contents.
3446- priority specifies the priority of the message, and should be one of
3447- the PRIORITY_* constants defined in endroid.messagehandler. Use with
3448- care, especially PRIORITY_URGENT, which will usually bypass any
3449- ratelimiting or other protections put in place.
3450- """
3451- self._pm.send_muc(rjid, text, self, priority)
3452-
3453- def send_chat(self, user, text, priority=PRIORITY_NORMAL):
3454- """
3455- Send a regular chat message to the specified room ('rjid'), with the
3456- given text and at the specified priority.
3457-
3458- user is a string representing the JID of the user to send to.
3459- text is a string (or unicode) with the message contents.
3460- priority specifies the priority of the message, and should be one of
3461- the PRIORITY_* constants defined in endroid.messagehandler. Use with
3462- care, especially PRIORITY_URGENT, which will usually bypass any
3463- ratelimiting or other protections put in place.
3464- """
3465- self._pm.send_chat(user, text, self, priority)
3466+ def register_muc_callback(self, callback, inc_self=False, priority=0):
3467+ if self._pm.place != "room":
3468+ return
3469+ self._register("muc", "recv", callback, inc_self, priority)
3470+
3471+ def register_chat_callback(self, callback, inc_self=False, priority=0):
3472+ if self._pm.place != "group":
3473+ return
3474+ self._register("chat", "recv", callback, inc_self, priority)
3475+
3476+
3477+ def register_unhandled_muc_callback(self, callback, inc_self=False, priority=0):
3478+ if self._pm.place != "room":
3479+ return
3480+ self._register("muc", "unhandled", callback, inc_self, priority)
3481+
3482+ def register_unhandled_chat_callback(self, callback, inc_self=False, priority=0):
3483+ if self._pm.place != "group":
3484+ return
3485+ self._register("chat", "unhandled", callback, inc_self, priority)
3486+
3487+ register_muc_callback.__doc__ = msg_cb_doc.format("muc")
3488+ register_chat_callback.__doc__ = msg_cb_doc.format("chat")
3489+ register_unhandled_muc_callback.__doc__ = msg_cb_doc.format("unhandled muc")
3490+ register_unhandled_chat_callback.__doc__ = msg_cb_doc.format("unhandled chat")
3491+
3492+ def register_muc_filter(self, callback, inc_self=False, priority=0):
3493+ if self._pm.place != "room":
3494+ return
3495+ self._register("muc", "recv_filter", callback, inc_self, priority)
3496+
3497+ def register_chat_filter(self, callback, inc_self=False, priority=0):
3498+ if self._pm.place != "group":
3499+ return
3500+ self._register("chat", "recv_filter", callback, inc_self, priority)
3501+
3502+ def register_muc_send_filter(self, callback, inc_self=False, priority=0):
3503+ if self._pm.place != "room":
3504+ return
3505+ self._register("muc", "send_filter", callback, inc_self, priority)
3506+
3507+ def register_chat_send_filter(self, callback, inc_self=False, priority=0):
3508+ if self._pm.place != "group":
3509+ return
3510+ self._register("chat", "send_filter", callback, inc_self, priority)
3511+
3512+ register_muc_filter.__doc__ = msg_filter_doc.format("receive", "muc")
3513+ register_chat_filter.__doc__ = msg_filter_doc.format("receive", "chat")
3514+ register_muc_send_filter.__doc__ = msg_filter_doc.format("send", "muc")
3515+ register_chat_send_filter.__doc__ = msg_filter_doc.format("send", "chat")
3516
3517 # Plugin access methods
3518-
3519 def get(self, plugin_name):
3520+ """Return a plugin-like object from the plugin module plugin_name."""
3521 return self._pm.get(plugin_name)
3522
3523 def get_dependencies(self):
3524- return map(self.get, self.dependencies)
3525+ """Return an iterable of plugins this plugin depends on.
3526+
3527+ This includes indirect dependencies i.e. the dependencies of plugins this
3528+ plugin depends on and so on.
3529+
3530+ """
3531+ return (self.get(dependency) for dependency in self.dependencies)
3532
3533 def get_preferences(self):
3534- return map(self.get, self.preferences)
3535-
3536+ """Return an iterable of plugins this plugin prefers.
3537+
3538+ This includes indirect preferences i.e. the preferences of plugins this
3539+ plugin prefers and so on.
3540+
3541+ """
3542+ return (self.get(preference) for preference in self.preferences)
3543+
3544 def list_plugins(self):
3545- return self._pm.list()
3546-
3547+ """Return a list of all plugins loaded in the plugin's environment."""
3548+ return self._pm.get_plugins()
3549+
3550 def pluginLoaded(self, modname):
3551+ """Check if modname is loaded in the plugin's environment (bool)."""
3552 return self._pm.hasloaded(modname)
3553-
3554+
3555 def pluginCall(self, modname, func, *args, **kwargs):
3556+ """Directly call a method on plugin modname."""
3557 return getattr(self.get(modname), func)(*args, **kwargs)
3558
3559- # Default implementations of overridable methods
3560-
3561 def endroid_init(self):
3562 pass
3563
3564+ @property
3565+ def cron(self):
3566+ return Cron().get()
3567+
3568+
3569 dependencies = ()
3570 preferences = ()
3571
3572+
3573 class PluginProxy(object):
3574 def __init__(self, modname):
3575- logging.info(modname)
3576 __import__(modname)
3577+ # dictionary mapping module names to module objects
3578 m = sys.modules[modname]
3579-
3580 # In loading a plugin, we first look for a get_plugin() function,
3581 # then a function with the same name as the module, and finally we
3582 # just check the automatic Plugin registry for a Plugin defined in
3583 # that module.
3584 if hasattr(m, 'get_plugin'):
3585 self.module = getattr(m, 'get_plugin')()
3586- elif hasattr(m, modname.split('.')[-1]):
3587- self.module = getattr(m, modname.split('.')[-1])()
3588 else:
3589 self.module = PluginMeta.registry[modname]()
3590
3591@@ -236,10 +200,10 @@
3592 return getattr(self.module, key)
3593 else:
3594 return self
3595-
3596+
3597 def hasattr(self, key):
3598 return hasattr(self.module, key)
3599-
3600+
3601 def __call__(self, *args, **kwargs):
3602 return self
3603
3604@@ -251,113 +215,107 @@
3605 def __init__(self, value):
3606 super(ModuleNotLoadedError, self).__init__(value)
3607 self.value = value
3608+
3609 def __str__(self):
3610 return repr(self.value)
3611
3612+
3613 class PluginInitError(Exception):
3614 pass
3615
3616
3617 class PluginManager(object):
3618- def __init__(self, messagehandler, usermanagement, rjid=None, userlist=[]):
3619+ def __init__(self, messagehandler, usermanagement, place, name, config):
3620 self.messagehandler = messagehandler
3621 self.usermanagement = usermanagement
3622+
3623+ self.place = place
3624+ self.name = name
3625+
3626+ # this is a dictionary of plugin module names to pluginproxy objects
3627+ self._loaded = {}
3628+ # a dict of modnames : plugin configs
3629 self._plugins = {}
3630+ # module name to bool dictionary
3631 self.initialised = {}
3632- self.rjid = rjid
3633- self.userlist = userlist
3634-
3635- def suc_users(self):
3636- return self.userlist
3637-
3638- def register_muc_callback(self, callback, including_self=False, priority=0):
3639- self.messagehandler.register_muc_handler(self.rjid, callback,
3640- including_self, priority)
3641-
3642- def register_chat_callback(self, callback, including_self=False, priority=0):
3643- self.messagehandler.register_chat_handler(self.userlist, callback,
3644- including_self, priority)
3645-
3646- def register_unhandled_muc_callback(self, callback, including_self=False, priority=0):
3647- self.messagehandler.register_unhandled_muc_handler(self.rjid, callback,
3648- including_self, priority)
3649-
3650- def register_unhandled_chat_callback(self, callback, including_self=False, priority=0):
3651- self.messagehandler.register_unhandled_chat_handler(self.userlist, callback,
3652- including_self, priority)
3653-
3654- def register_muc_filter(self, callback, priority=0):
3655- self.messagehandler.register_muc_recv_filter(self.rjid, callback, priority)
3656-
3657- def register_chat_filter(self, callback, priority=0):
3658- self.messagehandler.register_chat_recv_filter(self.userlist, callback,
3659- priority)
3660-
3661- def register_muc_send_filter(self, callback, priority=0):
3662- self.messagehandler.register_muc_send_filter(self.rjid, callback, priority)
3663-
3664- def register_chat_send_filter(self, callback, priority=0):
3665- self.messagehandler.register_chat_send_filter(self.userlist, callback,
3666- priority)
3667-
3668- def send_muc(self, rjid, text, source, priority=PRIORITY_NORMAL):
3669- self.messagehandler.send_muc(rjid, text, source, priority)
3670-
3671- def send_chat(self, jid, text, source, priority=PRIORITY_NORMAL):
3672- self.messagehandler.send_chat(jid, text, source, priority)
3673-
3674- def list(self):
3675+
3676+ self.read_config(config)
3677+ self.loadPlugins()
3678+ self.initPlugins()
3679+
3680+ def read_config(self, conf):
3681+ def get_data(modname):
3682+ # return a tuple of (modname, modname's config)
3683+ return modname, conf.get(self.place, self.name, "plugin", modname, default={})
3684+
3685+ plugins = conf.get(self.place, self.name, "plugins")
3686+ self._plugins = dict(map(get_data, plugins))
3687+
3688+ def get_plugins(self):
3689 return self._plugins.keys()
3690-
3691- def load(self, plugin, conf):
3692- append = ""
3693- if self.rjid:
3694- append = " for room " + self.rjid
3695- elif self.userlist:
3696- append = " for users " + str(self.userlist)
3697- logging.info("Loading Plugin: " + plugin + append)
3698+
3699+ def load(self, modname):
3700+ # loads the plugin module and adds a key to self._loaded
3701+ logging.debug("\tLoading Plugin: " + modname)
3702 try:
3703- p = PluginProxy(plugin)
3704- p._pluginInit(self, conf)
3705-
3706- self._plugins[plugin] = p
3707+ p = PluginProxy(modname)
3708+ p._pluginInit(self, self._plugins[modname])
3709+
3710+ self._loaded[modname] = p
3711 except ImportError as i:
3712 logging.error(i)
3713- logging.error("**Could Not Import Plugin \"" + plugin + "\". Check That It Exists In Your PYTHONPATH.")
3714-
3715- def get(self, name):
3716- if not name in self._plugins:
3717- raise ModuleNotLoadedError(name)
3718- return self._plugins[name]
3719-
3720+ logging.error("**Could Not Import Plugin \"" + modname
3721+ + "\". Check That It Exists In Your PYTHONPATH.")
3722+
3723+ def loadPlugins(self):
3724+ logging.info("Loading Plugins for {0}".format(self.name))
3725+ plugins = self.get_plugins()
3726+ for p in plugins:
3727+ self.load(p)
3728+
3729 def hasloaded(self, name):
3730- return self._plugins.has_key(name)
3731-
3732+ return name in self._loaded
3733+
3734 def init(self, modname):
3735- logging.debug("Initialising Plugin: " + modname)
3736- if self.initialised.has_key(modname):
3737- logging.debug(modname + " Already Initialised")
3738+ logging.debug("\tInitialising Plugin: " + modname)
3739+ if modname in self.initialised:
3740+ logging.debug("\t{0} Already Initialised".format(modname))
3741 return True
3742 if not self.hasloaded(modname):
3743- logging.error("**Cannot Initialise Plugin \"" + modname + "\", It Has Not Been Imported")
3744+ logging.error("\t**Cannot Initialise Plugin \"" + modname + "\", It Has Not Been Imported")
3745 return False
3746
3747+ # deal with dependencies and preferences
3748 plugin = self.get(modname)
3749- for modname2 in plugin.dependencies:
3750- logging.debug(modname + " Depends On " + modname2 + "...")
3751- if not self.init(modname2):
3752- logging.error("**No \"" + modname2 + "\". Unloading " + modname)
3753- self._plugins.pop(modname)
3754+ for mod_dep_name in plugin.dependencies:
3755+ logging.debug("\t{0} Depends On {1}".format(modname, mod_dep_name))
3756+ if not self.init(mod_dep_name):
3757+ # can't possibly initialise us so remove us from self._loaded
3758+ logging.error("\t**No \"" + mod_dep_name + "\". Unloading " + modname)
3759+ self._loaded.pop(modname)
3760 return False
3761- for modname2 in plugin.preferences:
3762- logging.debug(modname + " Prefers To Have " + modname2 + "...")
3763- if not self.init(modname2):
3764- logging.error("**Could Not Load " + modname2)
3765+
3766+ for mod_pref_name in plugin.preferences:
3767+ logging.debug("\t{0} Prefers {1}".format(modname, mod_pref_name))
3768+ if not self.init(mod_pref_name):
3769+ logging.error("\t**Could Not Load " + mod_pref_name)
3770+ # attempt to initialise the plugin
3771 try:
3772 plugin.endroid_init()
3773 self.initialised[modname] = True
3774- logging.info("Initialised Plugin: " + modname)
3775- except PluginInitError as e:
3776- logging.error("**Error initializing \"" + modname + "\". See log for details.")
3777+ logging.info("\tInitialised Plugin: " + modname)
3778+ except PluginInitError:
3779+ logging.error("\t**Error initializing \"" + modname + "\". See log for details.")
3780 return False
3781 return True
3782+
3783+ def initPlugins(self):
3784+ logging.info("Initialising Plugins for {0}".format(self.name))
3785+ plugins = self.get_plugins()
3786+ for p in plugins:
3787+ self.init(p)
3788+
3789+ def get(self, name):
3790+ if not name in self._loaded:
3791+ raise ModuleNotLoadedError(name)
3792+ return self._loaded[name]
3793
3794=== modified file 'src/endroid/plugins/blacklist.py'
3795--- src/endroid/plugins/blacklist.py 2012-09-02 19:58:56 +0000
3796+++ src/endroid/plugins/blacklist.py 2013-08-12 17:50:48 +0000
3797@@ -7,7 +7,6 @@
3798 from endroid.pluginmanager import Plugin
3799 from endroid.database import Database
3800 from endroid.cron import Cron
3801-from endroid.config import as_list
3802
3803 # DB constants
3804 DB_NAME = "Blacklist"
3805@@ -26,14 +25,15 @@
3806 help = "Maintain a blacklist of JIDs who get ignored by EnDroid."
3807 hidden = True
3808
3809+ _blacklist = set()
3810+
3811 def endroid_init(self):
3812 """
3813 Initialise the plugin, and recover the blacklist from the DB.
3814 """
3815- self.admins = set(map(str.strip, as_list(self.vars.get("admins", ""))))
3816- self._blacklist = set()
3817+ self.admins = set(map(str.strip, self.vars.get("admins", [])))
3818
3819- self.cron = Cron.get().register(self.unblacklist, CRON_UNBLACKLIST)
3820+ self.task = self.cron.register(self.unblacklist, CRON_UNBLACKLIST)
3821
3822 self.register_muc_filter(self.checklist)
3823 self.register_chat_filter(self.checklist)
3824@@ -45,20 +45,23 @@
3825 self.db.create_table(DB_TABLE, ("userjid",))
3826 for row in self.db.fetch(DB_TABLE, ("userjid",)):
3827 self.blacklist(row["userjid"])
3828+
3829+ def get_blacklist(self):
3830+ return self._blacklist
3831
3832 def checklist(self, msg):
3833 """
3834 Receive filter callback - checks the message sender against the
3835 blacklist
3836 """
3837- return msg.sender.userhost() not in self._blacklist
3838+ return msg.sender not in self.get_blacklist()
3839
3840 def checksend(self, msg):
3841 """
3842 Send filter callback - checks the message recipient against the
3843 blacklist
3844 """
3845- return msg.recipient.userhost() not in self._blacklist
3846+ return msg.sender not in self.get_blacklist()
3847
3848 def command(self, msg):
3849 """
3850@@ -70,13 +73,14 @@
3851 while len(parts) < 3:
3852 parts.append(None)
3853 bl, cmd, user = parts[:3]
3854- if msg.sender.userhost() in self.admins and bl == "blacklist":
3855- if cmd == "add" and user not in self._blacklist:
3856+
3857+ if msg.sender in self.admins and bl == "blacklist":
3858+ if cmd == "add" and user not in self.get_blacklist():
3859 self.blacklist(user)
3860- elif cmd == "remove" and user in self._blacklist:
3861+ elif cmd == "remove" and user in self.get_blacklist():
3862 self.unblacklist(user)
3863 elif cmd == "list":
3864- msg.reply("Blacklist: " + ", ".join(self._blacklist))
3865+ msg.reply("Blacklist: " + ", ".join(self.get_blacklist() or ['None']))
3866 else:
3867 msg.unhandled()
3868 else:
3869@@ -88,15 +92,15 @@
3870 argument is passed, the user is removed after the specified number of
3871 seconds.
3872 """
3873+ self.db.delete(DB_TABLE, {"userjid": user})
3874+ self.db.insert(DB_TABLE, {"userjid": user})
3875 self._blacklist.add(user)
3876- self.db.delete(DB_TABLE, {"userjid": user})
3877- self.db.insert(DB_TABLE, {"userjid": user})
3878 if duration != 0:
3879- self.cron.setTimeout(duration, user)
3880+ self.task.setTimeout(duration, user)
3881
3882 def unblacklist(self, user):
3883 """
3884 Remove the specified user from the blacklist.
3885 """
3886- self._blacklist.discard(user)
3887 self.db.delete(DB_TABLE, {"userjid": user})
3888+ self._blacklist.remove(user)
3889
3890=== modified file 'src/endroid/plugins/command.py'
3891--- src/endroid/plugins/command.py 2012-09-02 23:03:10 +0000
3892+++ src/endroid/plugins/command.py 2013-08-12 17:50:48 +0000
3893@@ -4,13 +4,12 @@
3894 # Created by Jonathan Millican
3895 # -----------------------------------------
3896
3897-import logging
3898-
3899 from collections import namedtuple
3900 from endroid.pluginmanager import Plugin, PluginMeta
3901
3902 __all__ = (
3903 'CommandPlugin',
3904+ 'command',
3905 )
3906
3907 # A single registration
3908@@ -26,13 +25,52 @@
3909 # - subcommands is a dict of subcommand (simple str) to Handlers object
3910 Handlers = namedtuple('Handlers', ('handlers', 'subcommands'))
3911
3912+def command(wrapped=None, synonyms=(), helphint="", hidden=False, chat_only=False,
3913+ muc_only=False):
3914+ """
3915+ Decorator used to mark command functions in subclasses of CommandPlugin.
3916+
3917+ In it's simplest form, can be used to mark a function as a command:
3918+
3919+ >>> class Foo(CommandPlugin):
3920+ ... @command
3921+ ... def foo_bar(self, msg, args):
3922+ ... do_stuff_here()
3923+
3924+ This will register the command string ("foo", "bar") with the Command plugin
3925+ when the Foo plugin is initialised.
3926+
3927+ The decorator also takes a series of optional keyword arguments to control
3928+ the behaviour of the registration:
3929+
3930+ synonyms: sequence of synonyms (either strings, or sequences of strings)
3931+ helphint: a helpful hint to display in help for the command
3932+ hidden: whether the command is hidden in the help
3933+ chat_only: set to True if the command should only be registered in chat
3934+ muc_only: set to True if the command should only be registered in muc
3935+
3936+ """
3937+ def decorator(fn):
3938+ fn.is_command = True
3939+ fn.synonyms = synonyms
3940+ fn.helphint = helphint
3941+ fn.hidden = hidden
3942+ fn.chat_only = chat_only
3943+ fn.muc_only = muc_only
3944+ return fn
3945+ if wrapped is None:
3946+ return decorator
3947+ else:
3948+ return decorator(wrapped)
3949+
3950 class CommandPluginMeta(PluginMeta):
3951 """
3952 Metaclass to support simple command-driven plugins. This should not be used
3953 directly, but rather by subclassing from CommandPlugin rather than Plugin.
3954 """
3955 def __new__(meta, name, bases, dct):
3956- cmds = dict((c, f) for c, f in dct.items() if c.startswith("cmd_"))
3957+ cmds = dict((c, f) for c, f in dct.items()
3958+ if c.startswith("cmd_") or getattr(f, "is_command", False))
3959 init = dct.get('endroid_init', lambda _: None)
3960 def endroid_init(self):
3961 com = self.get('endroid.plugins.command')
3962@@ -43,7 +81,10 @@
3963 reg_fn = com.register_muc
3964 else:
3965 reg_fn = com.register_both
3966- reg_fn(getattr(self, cmd), cmd.split("_")[1:],
3967+ words = cmd.split("_")
3968+ if not getattr(fn, "is_command", False):
3969+ words = words[1:]
3970+ reg_fn(getattr(self, cmd), words,
3971 helphint=getattr(fn, "helphint", ""),
3972 hidden=getattr(fn, "hidden", False),
3973 synonyms=getattr(fn, "synonyms", ()))
3974@@ -51,17 +92,20 @@
3975 dct['endroid_init'] = endroid_init
3976 dct['dependencies'] = tuple(dct.get('dependencies', ()) +
3977 ('endroid.plugins.command',))
3978- return PluginMeta.__new__(meta, name, bases, dct)
3979+ return super(CommandPluginMeta, meta).__new__(meta, name, bases, dct)
3980
3981 class CommandPlugin(Plugin):
3982 """
3983 Parent class for simple command-driven plugins. Such plugins don't need to
3984 explicitly register their commands. Instead, they can just define methods
3985- prefixed with "cmd_" and they will automatically be registered. Any
3986- additional underscores in the method name will be converted to spaces in
3987- the registration (so cmd_foo_bar is registered as ('foo', 'bar')).
3988-
3989- In addition, certain options can be passed by adding fields to the methods:
3990+ prefixed with "cmd_" or decorated with the 'command' decorator, and they
3991+ will automatically be registered. Any additional underscores in the method
3992+ name will be converted to spaces in the registration (so cmd_foo_bar is
3993+ registered as ('foo', 'bar')).
3994+
3995+ In addition, certain options can be passed by adding fields to the methods,
3996+ or as keyword arguments to the decorator:
3997+
3998 - hidden: don't show the command in help if set to True.
3999 - synonyms: an iterable of alternative keyword sequences to register the
4000 method against. All synonyms are hidden.
4001@@ -171,19 +215,19 @@
4002 # -------------------------------------------------------------------------
4003 # Registration methods
4004
4005- def register_handler(self, callback, cmd, helphint, hidden, handlers,
4006+ def _register_handler(self, callback, cmd, helphint, hidden, handlers,
4007 synonyms=()):
4008 """
4009 Register a new handler.
4010
4011 callback is the callback handle to call.
4012 command is either a single keyword, or a sequence of keywords to match.
4013- helphint and hidden are argument to the Registration object.
4014+ helphint and hidden are arguments to the Registration object.
4015 handlers are the top-level handlers to add the registration to.
4016 """
4017 # Register any synonyms (done before we frig with the handlers)
4018 for entry in synonyms:
4019- self.register_handler(callback, entry, helphint, True, handlers)
4020+ self._register_handler(callback, entry, helphint, True, handlers)
4021
4022 # Allow simple commands to be passed as strings
4023 cmd = cmd.split() if isinstance(cmd, (str, unicode)) else cmd
4024@@ -196,13 +240,13 @@
4025 def register_muc(self, callback, command, helphint="", hidden=False,
4026 synonyms=()):
4027 """Register a new handler for MUC messages."""
4028- self.register_handler(callback, command, helphint, hidden,
4029+ self._register_handler(callback, command, helphint, hidden,
4030 self._muc_handlers, synonyms)
4031
4032 def register_chat(self, callback, command, helphint="", hidden=False,
4033 synonyms=()):
4034 """Register a new handler for chat messages."""
4035- self.register_handler(callback, command, helphint, hidden,
4036+ self._register_handler(callback, command, helphint, hidden,
4037 self._chat_handlers, synonyms)
4038
4039 def register_both(self, callback, command, helphint="", hidden=False,
4040
4041=== modified file 'src/endroid/plugins/compute/__init__.py'
4042--- src/endroid/plugins/compute/__init__.py 2012-08-09 12:39:52 +0000
4043+++ src/endroid/plugins/compute/__init__.py 2013-08-12 17:50:48 +0000
4044@@ -16,7 +16,7 @@
4045 import wap
4046
4047 # Constants for the DB values we use
4048-WOLFRAM_API_SERVER_DEFAULT = "http://api.wolframalpha.com/v1/query.jsp"
4049+WOLFRAM_API_SERVER_DEFAULT = "http://api.wolframalpha.com/v2/query"
4050
4051 MESSAGES = {
4052 'help' : """
4053@@ -44,7 +44,7 @@
4054 self.waeo = wap.WolframAlphaEngine(self.api_key, self.server)
4055 except KeyError:
4056 logging.error("ERROR: Compute: There is no API key set in config! Set 'api_key' variable"
4057- "in section '[Plugin:endroid.plugins.compute]'. Aborting plugin load.")
4058+ " in section '[Plugin:endroid.plugins.compute]'. Aborting plugin load.")
4059 raise PluginInitError("Aborted plugin initialization")
4060
4061 def help(self):
4062
4063=== modified file 'src/endroid/plugins/compute/wap.py' (properties changed: +x to -x)
4064=== modified file 'src/endroid/plugins/correct.py'
4065--- src/endroid/plugins/correct.py 2012-09-04 11:33:50 +0000
4066+++ src/endroid/plugins/correct.py 2013-08-12 17:50:48 +0000
4067@@ -58,5 +58,6 @@
4068 # This is unexpected. Probably a mistake on the user's part?
4069 msg.unhandled()
4070 else:
4071- who = msg.sendernick if msg.sendernick else "You"
4072+ sendernick = self.usermanagement.get_nickname(msg.sender, self.place_name)
4073+ who = sendernick if self.place == "muc" else "You"
4074 msg.reply("%s meant: %s" % (who, newstr))
4075
4076=== modified file 'src/endroid/plugins/echobot.py'
4077--- src/endroid/plugins/echobot.py 2012-09-02 19:58:56 +0000
4078+++ src/endroid/plugins/echobot.py 2013-08-12 17:50:48 +0000
4079@@ -7,7 +7,7 @@
4080 from endroid.pluginmanager import Plugin
4081
4082 class EchoBot(Plugin):
4083- def endroid_plugin(self):
4084+ def endroid_init(self):
4085 self.register_chat_callback(self.do_echo)
4086 self.register_muc_callback(self.do_echo)
4087
4088
4089=== modified file 'src/endroid/plugins/httpinterface.py'
4090--- src/endroid/plugins/httpinterface.py 2012-11-26 20:31:52 +0000
4091+++ src/endroid/plugins/httpinterface.py 2013-08-12 17:50:48 +0000
4092@@ -3,7 +3,6 @@
4093 # Copyright 2012, Ensoft Ltd
4094 # -----------------------------------------------------------------------------
4095
4096-import cgi
4097 import collections
4098 import re
4099
4100@@ -12,7 +11,8 @@
4101 from twisted.web.resource import Resource
4102 from twisted.web.server import Site
4103
4104-HTTP_PORT = 8880
4105+DEFAULT_HTTP_PORT = 8880
4106+DEFAULT_HTTP_INTERFACE = '127.0.0.1'
4107
4108 NOT_FOUND_TEMPLATE = """
4109 <html>
4110@@ -112,10 +112,10 @@
4111 re_src = re.escape(path_prefix) + r"(/.*)?"
4112 self.register_regex_path(plugin, callback, re_src)
4113
4114- def endroid_init(self):
4115+ def endroid_init(self, port, interface):
4116 self.root = IndexPage(self)
4117 factory = Site(self.root)
4118- reactor.listenTCP(HTTP_PORT, factory)
4119+ reactor.listenTCP(port, factory, interface=interface)
4120
4121 class HTTPInterface(Plugin):
4122 """
4123@@ -159,5 +159,7 @@
4124
4125 def endroid_init(self):
4126 if not HTTPInterface.enInited:
4127- HTTPInterface.object.endroid_init()
4128+ port = self.vars.get("port", DEFAULT_HTTP_PORT)
4129+ interface = self.vars.get("interface", DEFAULT_HTTP_INTERFACE)
4130+ HTTPInterface.object.endroid_init(port, interface)
4131 HTTPInterface.enInited = True
4132
4133=== added file 'src/endroid/plugins/invite.py'
4134--- src/endroid/plugins/invite.py 1970-01-01 00:00:00 +0000
4135+++ src/endroid/plugins/invite.py 2013-08-12 17:50:48 +0000
4136@@ -0,0 +1,121 @@
4137+from endroid.plugins.command import CommandPlugin
4138+import shlex
4139+
4140+def parse_string(string, options=None):
4141+ """
4142+ Parse a shell command like string into an args tuple and a kwargs dict.
4143+
4144+ Options is an iterable of string tuples, each tuple representing a keyword
4145+ followed by its synonyms.
4146+
4147+ Words in string will be appended to the args tuple until a keyword is
4148+ reached, at which point they will be appended to a list in kwargs.
4149+
4150+ Eg parse_string("a b c -u 1 2 3 -r 5 6", [("-u",), ("room", "-r")])
4151+ will return: ('a', 'b', 'c'), {'-u': ['1','2','3'], 'room': ['5','6']}
4152+
4153+ """
4154+ options = options or []
4155+ aliases = {}
4156+
4157+ keys = []
4158+ # build the kwargs dict with all the keywords and an aliases dict for synonyms
4159+ for option in options:
4160+ if isinstance(option, (list, tuple)):
4161+ main = option[0]
4162+ keys.append(main)
4163+ for alias in option[1:]:
4164+ aliases[alias] = main
4165+ elif isinstance(option, (str, unicode)):
4166+ keys.append(option)
4167+
4168+ args = []
4169+ kwargs = {}
4170+ current = None
4171+ # parse the string - first split into shell 'words'
4172+ parts = shlex.split(string)
4173+ # then add to args or the kwargs dictionary as appropriate
4174+ for part in parts:
4175+ # if it's a synonym get the main command, else leave it
4176+ part = aliases.get(part, part)
4177+ if part in keys:
4178+ # we have come to a keyword argument - create its list
4179+ kwargs[part] = []
4180+ # keep track of where we are sending non-keyword words to
4181+ current = kwargs[part]
4182+ elif current is not None:
4183+ # we are adding words to a keyword's list
4184+ current.append(part)
4185+ else:
4186+ # no keywords has been found yet - we are still in args
4187+ args.append(part)
4188+
4189+ return args, kwargs
4190+
4191+def replace(l, search, replace):
4192+ return [replace if item == search else item for item in l]
4193+
4194+
4195+class Inviter(CommandPlugin):
4196+ help = "Invite users to rooms"
4197+ name = "invite"
4198+ PARSE_OPTIONS = ("to", "into")
4199+
4200+ def cmd_invite_me(self, msg, arg):
4201+ """
4202+ Invite user to the rooms listed in args, or to all their rooms
4203+ if args is empty.
4204+
4205+ """
4206+ args, kwargs = parse_string(arg, self.PARSE_OPTIONS)
4207+ users = [msg.sender]
4208+ rooms = set(args + kwargs.get("to", [])) or ["all"]
4209+
4210+ results = self._do_invites(users, rooms)
4211+ msg.reply(results)
4212+ cmd_invite_me.helphint = "{to|into}? <room>+"
4213+
4214+ def cmd_invite_all(self, msg, arg):
4215+ """Invite all of a room's registered users to the room."""
4216+ users = self.usermanagement.get_available_users(self.place_name)
4217+ rooms = [self.place_name]
4218+
4219+ results = self._do_invites(users, rooms)
4220+ msg.reply(results)
4221+ cmd_invite_all.muc_only = True
4222+ cmd_invite_all.helphint = "<reason>"
4223+
4224+ def cmd_invite_users(self, msg, arg):
4225+ """Invite a list of users to a list of rooms."""
4226+ args, kwargs = parse_string(arg, self.PARSE_OPTIONS)
4227+ users = replace(args, "me", msg.sender)
4228+ rooms = kwargs.get("to", [])
4229+
4230+ results = self._do_invites(users, rooms)
4231+ msg.reply(results)
4232+ cmd_invite_users.helphint = "<user>+ {to|into} <room>+"
4233+
4234+ def _do_invites(self, users, rooms):
4235+ if 'all' in users:
4236+ if len(rooms) == 1:
4237+ users = self.usermanagement.get_available_users(rooms[0])
4238+ else:
4239+ return "Can only invite 'all' users to a single room"
4240+ if 'all' in rooms:
4241+ if len(users) == 1:
4242+ rooms = self.usermanagement.get_rooms(users[0])
4243+ if not rooms:
4244+ return "User '{}' is not registered.".format(users[0])
4245+ else:
4246+ return "Can only invite a single user to 'all' rooms"
4247+
4248+ results = []
4249+ invitations = 0
4250+ for room in rooms:
4251+ for user in users:
4252+ s, reason = self.usermanagement.invite(user, room)
4253+ if not s:
4254+ results.append("{} to {} failed: {}".format(user, room, reason))
4255+ else:
4256+ invitations += 1
4257+ return "Sent {} invitations\n".format(invitations) + '\n'.join(results)
4258
4259=== modified file 'src/endroid/plugins/memo.py'
4260--- src/endroid/plugins/memo.py 2012-09-04 15:10:26 +0000
4261+++ src/endroid/plugins/memo.py 2013-08-12 17:50:48 +0000
4262@@ -8,8 +8,7 @@
4263 See MESSAGES['usage'] for details.
4264 """
4265
4266-import endroid.messagehandler as messagehandler
4267-from endroid.pluginmanager import Plugin
4268+from endroid.plugins.command import CommandPlugin, command
4269 from endroid.database import Database
4270 from endroid.database import EndroidUniqueID
4271
4272@@ -27,11 +26,12 @@
4273 memo usage - Output this message
4274 memo [list] - List messages in inbox
4275 memo send <recipient> <message> - Send a message
4276-memo {view|delete} <message-id> -Handle a specific message""",
4277+memo {view|delete} <message-id> - Handle a specific message""",
4278
4279 'usage-send' : "memo send <recipient> <message> \t Send a message",
4280 'usage-view' : "memo view <message-id> \t View a specific message",
4281 'usage-delete' : "memo delete <message-id> \t Delete a specific message",
4282+
4283 'duplicate-id-error' : "Error: There are messages sharing the same PK id",
4284 'id-not-found-error' : "There is no message with id {0}. Try 'memo list'.",
4285 'deletion-sucess' : "Sucessfully deleted one message",
4286@@ -45,33 +45,21 @@
4287 }
4288
4289
4290-class Memo(Plugin):
4291+class Memo(CommandPlugin):
4292 name = "memo"
4293-
4294- def dependencies(self):
4295- return ('endroid.plugins.command',)
4296-
4297- def enInit(self):
4298- com = self.get('endroid.plugins.command')
4299- com.register_chat(self._handle_list, 'memo', '[list]')
4300- com.register_chat(self._handle_list, ('memo', 'list'), hidden=True)
4301- com.register_chat(self._handle_send, ('memo', 'send'),
4302- '<recipient> <message>')
4303- com.register_chat(self._handle_view, ('memo', 'view'), '<message-id>')
4304- com.register_chat(self._handle_delete, ('memo', 'delete'),
4305- '<message-id>')
4306- com.register_chat(lambda m, _: m.reply(MESSAGES['usage']),
4307- ('memo', 'usage'))
4308+ help_topics = {
4309+ '': lambda _: MESSAGES['help'],
4310+ 'usage': lambda _: MESSAGES['usage'],
4311+ 'send': lambda _: MESSAGES['usage-send'],
4312+ 'view': lambda _: MESSAGES['usage-view'],
4313+ 'delete': lambda _: MESSAGES['usage-delete'],
4314+ }
4315+
4316+ def endroid_init(self):
4317 self.db = Database(DB_NAME)
4318- self.setup_db()
4319-
4320- def setup_db(self):
4321 if not self.db.table_exists(DB_TABLE):
4322 self.db.create_table(DB_TABLE, DB_COLUMNS)
4323
4324- def help(self):
4325- return MESSAGES['help']
4326-
4327 def _message_text_summary(self, text):
4328 # First assume text is too long, try to shorten.
4329 summary = ""
4330@@ -82,8 +70,13 @@
4331 summary += word + ' '
4332 # Looks like memo is already short enough. Return it.
4333 return summary[:-1]
4334-
4335- def _handle_delete(self, msg, args):
4336+
4337+ @command
4338+ def memo_usage(self, msg, args):
4339+ msg.reply(MESSAGES['usage'])
4340+
4341+ @command(helphint="<message-id>")
4342+ def memo_delete(self, msg, args):
4343 args = args.split()
4344 if len(args) != 1 or not args[0].isdigit():
4345 msg.reply(MESSAGES['usage-delete'])
4346@@ -100,9 +93,17 @@
4347 self.db.delete(DB_TABLE, {EndroidUniqueID : int(args[0])})
4348 msg.reply(MESSAGES['deletion-sucess'])
4349
4350- def _handle_list(self, msg, args):
4351+ @command(helphint="[list]")
4352+ def memo(self, msg, args):
4353+ args = args.split()
4354+ if len(args) > 0 and args[0] in {"send", "view", "delete", "usage"}:
4355+ msg.unhandled()
4356+ return
4357+ if len(args) > 1 or (len(args) == 1 and args[0] != "list"):
4358+ msg.reply(MESSAGES['usage'])
4359+ return
4360 memos = []
4361- cond = {"recipient" : msg.sender.userhost()}
4362+ cond = {"recipient" : msg.sender}
4363 rows = self.db.fetch(DB_TABLE, DB_COLUMNS, cond)
4364 for row in rows:
4365 data = (row.id, row["sender"],
4366@@ -115,7 +116,8 @@
4367 else:
4368 msg.reply(MESSAGES['inbox-empty'])
4369
4370- def _handle_view(self, msg, args):
4371+ @command(helphint="<message-id>")
4372+ def memo_view(self, msg, args):
4373 args = args.split()
4374 if len(args) != 1 or not args[0].isdigit():
4375 msg.reply(MESSAGES['usage-view'])
4376@@ -133,18 +135,19 @@
4377 data = (row["sender"], row["text"])
4378 msg.reply(MESSAGES['view-message'].format(*data))
4379
4380- def _handle_send(self, msg, args):
4381+ @command(helphint="<recipient> <message>")
4382+ def memo_send(self, msg, args):
4383 args = args.split()
4384 if len(args) < 2:
4385 msg.reply(MESSAGES['usage-send'])
4386 return
4387
4388 recipient, memo = args[0], ' '.join(args[1:])
4389- if recipient not in self.presence().get_roster():
4390+ if recipient not in self.usermanagement.get_available_users():
4391 msg.reply(MESSAGES['bad-recipient'].format(recipient))
4392 return
4393
4394- self.db.insert(DB_TABLE, {'sender': msg.sender.userhost(),
4395+ self.db.insert(DB_TABLE, {'sender': msg.sender,
4396 'recipient': recipient,
4397 'text': memo})
4398 msg.reply(MESSAGES['sent-message'])
4399
4400=== added file 'src/endroid/plugins/passthebomb.py'
4401--- src/endroid/plugins/passthebomb.py 1970-01-01 00:00:00 +0000
4402+++ src/endroid/plugins/passthebomb.py 2013-08-12 17:50:48 +0000
4403@@ -0,0 +1,211 @@
4404+from endroid.plugins.command import CommandPlugin
4405+from endroid.cron import Cron
4406+from collections import defaultdict
4407+from endroid.database import Database
4408+
4409+DB_NAME = "PTB"
4410+DB_TABLE = "PTB"
4411+
4412+class User(object):
4413+ __slots__ = ('name', 'kills', 'shield')
4414+ def __init__(self, name, kills=0, shield=True):
4415+ self.name = name
4416+ self.kills = kills
4417+ self.shield = shield
4418+
4419+ def __repr__(self):
4420+ fmt = "User(name={}, kills={}, shield={})"
4421+ return fmt.format(self.name, self.kills, self.shield)
4422+
4423+class Bomb(object):
4424+ ID = 0
4425+ def __init__(self, source, fuse, plugin):
4426+ self.source = source # who lit the bomb?
4427+ self.user = None # our current holder
4428+ self.plugin = plugin # plugin instance we belong to
4429+ self.history = set() # who has held us?
4430+
4431+ idstring = self.get_id() # get a unique registration_name
4432+ plugin.cron.register(self.explode, idstring)
4433+ plugin.cron.setTimeout(fuse, idstring, None) # schedule detonation
4434+
4435+ # this function is called by Cron and given an argument. We don't need an
4436+ # argument so just ignore it
4437+ def explode(self, _):
4438+ # some shorthands
4439+ get_rooms = self.plugin.usermanagement.get_available_rooms
4440+ send_muc = self.plugin.messagehandler.send_muc
4441+ send_chat = self.plugin.messagehandler.send_chat
4442+
4443+ msg_explode = "!!!BOOM!!!"
4444+ msg_farexplode = "You hear a distant boom"
4445+ msg_kill = "{} was got by the bomb"
4446+
4447+ rooms = get_rooms(self.user)
4448+ for room in rooms:
4449+ # let everyone in a room with self.user here the explosion
4450+ send_muc(room, msg_explode)
4451+ send_muc(room, msg_kill.format(self.user))
4452+
4453+ # alert those who passed the bomb that it has exploded
4454+ for user in self.history:
4455+ if user == self.user:
4456+ send_chat(self.user, msg_explode)
4457+ send_chat(self.user, msg_kill.format("You"))
4458+ else:
4459+ send_chat(user, msg_farexplode)
4460+ send_chat(user, msg_kill.format(self.user))
4461+
4462+ self.plugin.register_kill(self.source)
4463+ self.plugin.bombs[self.user].discard(self)
4464+
4465+
4466+ def throw(self, user):
4467+ # remove this bomb from our current user
4468+ self.plugin.bombs[self.user].discard(self)
4469+
4470+ self.history.add(user)
4471+ self.user = user
4472+
4473+ # add it to the new user
4474+ self.plugin.bombs[self.user].add(self)
4475+
4476+ @classmethod
4477+ def get_id(cls):
4478+ # generate a unique id string to register our explode method against
4479+ result = Bomb.ID
4480+ cls.ID += 1
4481+ return "bomb" + str(result)
4482+
4483+
4484+class PassTheBomb(CommandPlugin):
4485+ help = "Pass the bomb game for EnDroid"
4486+ bombs = defaultdict(set) # users : set of bombs
4487+ users = dict() # user strings : User objects
4488+
4489+ def endroid_init(self):
4490+ self.db = Database(DB_NAME)
4491+ if not self.db.table_exists(DB_TABLE):
4492+ self.db.create_table(DB_TABLE, ('user', 'kills'))
4493+
4494+ # make a local copy of the registration database
4495+ data = self.db.fetch(DB_TABLE, ['user', 'kills'])
4496+ for dct in data:
4497+ self.users[dct['user']] = User(dct['user'], dct['kills'])
4498+
4499+ def cmd_furl_umbrella(self, msg, arg):
4500+ """This is how a user enters the game - allows them to be targeted
4501+ and to create and throw bombs"""
4502+ user = msg.sender
4503+ if not self.get_shielded(user):
4504+ msg.reply("Your umbrella is already furled!")
4505+ else:
4506+ if self.get_registered(user):
4507+ self.users[user].shield = False
4508+ else: # they are not - register them
4509+ self.db.insert(DB_TABLE, {'user': user, 'kills': 0})
4510+ self.users[user] = User(user, kills=0, shield=False)
4511+ msg.reply("You furl your umbrella!")
4512+ cmd_furl_umbrella.helphint = ("Furl your umbrella to participate in the "
4513+ "noble game of pass the bomb!")
4514+
4515+ def cmd_unfurl_umbrella(self, msg, arg):
4516+ """A user with an unfurled umbrella cannot create or receive bombs"""
4517+ user = msg.sender
4518+ if self.get_shielded(user):
4519+ msg.reply("Your umbrella is already unfurled!")
4520+ else:
4521+ # to get user must not have been shielded ie they must have furled
4522+ # so they will be in the database
4523+ self.users[user].shield = True
4524+ msg.reply("You unfurl your umbrella! No bomb can reach you now!")
4525+ cmd_unfurl_umbrella.helphint = ("Unfurl your umbrella to cower from the "
4526+ "rain of boms!")
4527+
4528+ def cmd_bomb(self, msg, arg):
4529+ """Create a bomb with a specified timer.
4530+
4531+ eg: 'bomb 1.5' for a 1.5 second fuse
4532+
4533+ """
4534+
4535+ holder = msg.sender
4536+ if self.get_shielded(holder):
4537+ return msg.reply("Your sense of honour insists that you furl your "
4538+ "umbrella before lighting the fuse")
4539+ # otherwise get a time from the first word of arg
4540+ try:
4541+ time = float(arg.split(' ', 1)[0])
4542+ # make a new bomb and throw it to its creator
4543+ Bomb(msg.sender, time, self).throw(msg.sender)
4544+ msg.reply("Sniggering evilly, you light the fuse...")
4545+ # provision for a failure to read a time float...
4546+ except ValueError:
4547+ msg.reply("You struggle with the matches")
4548+ cmd_bomb.helphint = ("Light the fuse!")
4549+
4550+ def cmd_throw(self, msg, arg):
4551+ """Throw a bomb to a user, eg: 'throw benh@ensoft.co.uk'"""
4552+ target = arg.split(' ')[0]
4553+ # we need a bomb to thrown
4554+ if not self.bombs[msg.sender]:
4555+ msg.reply("You idly throw your hat, wishing you had something "
4556+ "rounder, heavier and with more smoking fuses.")
4557+ # need our umbrella to be furled
4558+ elif self.get_shielded(msg.sender):
4559+ msg.reply("You notice that your unfurled umbrella would hinder "
4560+ "your throw.")
4561+ # check that target is online
4562+ elif not target in self.usermanagement.get_available_users():
4563+ msg.reply("You look around but cannot spot your target")
4564+ elif self.get_shielded(target): # target registered/vulnerable?
4565+ msg.reply("You see your target hunkered down under their umbrella. "
4566+ "No doubt a bomb would have little effect on that "
4567+ "monstrosity.")
4568+ else:
4569+ self.bombs[msg.sender].pop().throw(target)
4570+ msg.reply("You throw the bomb!")
4571+ self.messagehandler.send_chat(target, "A bomb lands by your feet!")
4572+ cmd_throw.helphint = ("Throw a bomb!")
4573+
4574+ def cmd_kills(self, msg, arg):
4575+ kills = self.get_kills(msg.sender)
4576+ nick = self.usermanagement.get_nickname(msg.sender,
4577+ self.place_name)
4578+ level = self.get_level(kills)
4579+
4580+ text = "{} the {} has {} kill".format(nick, level, kills)
4581+ text += ("" if kills == 1 else "s")
4582+ msg.reply(text)
4583+ cmd_kills.helphint = ("Receive and gloat over you score!")
4584+
4585+ def register_kill(self, user):
4586+ kills = self.get_kills(user)
4587+ # change the value of 'kills' to kills+1 in the row where 'user' = user
4588+ self.users[user].kills += 1
4589+ self.db.update(DB_TABLE, {'kills': kills+1}, {'user': user})
4590+
4591+ def get_kills(self, user):
4592+ return self.users[user].kills if user in self.users else 0
4593+
4594+ def get_shielded(self, user):
4595+ return self.users[user].shield if user in self.users else True
4596+
4597+ def get_registered(self, user):
4598+ return user in self.users
4599+
4600+ @staticmethod
4601+ def get_level(kills):
4602+ if kills < 5:
4603+ level = 'novice'
4604+ elif kills < 15:
4605+ level = 'apprentice'
4606+ elif kills < 35:
4607+ level = 'journeyman'
4608+ elif kills < 65:
4609+ level = 'expert'
4610+ elif kills < 100:
4611+ level = 'master'
4612+ else:
4613+ level = 'grand-master'
4614+ return level
4615
4616=== modified file 'src/endroid/plugins/ratelimit.py'
4617--- src/endroid/plugins/ratelimit.py 2012-08-31 09:59:16 +0000
4618+++ src/endroid/plugins/ratelimit.py 2013-08-12 17:50:48 +0000
4619@@ -6,11 +6,8 @@
4620
4621 from collections import defaultdict, deque
4622 import time
4623-import logging
4624
4625 from endroid.pluginmanager import Plugin
4626-from endroid.messagehandler import PRIORITY_URGENT
4627-from endroid.database import Database
4628 from endroid.cron import Cron
4629
4630 # Cron constants
4631@@ -157,7 +154,7 @@
4632
4633 self.waitingusers = set()
4634
4635- self.cron = Cron.get().register(self.sendagain, CRON_SENDAGAIN)
4636+ self.task = self.cron.register(self.sendagain, CRON_SENDAGAIN)
4637
4638 def preferences(self):
4639 """Other plugins that we could use if they are loaded."""
4640@@ -183,7 +180,7 @@
4641
4642 # Don't ratelimit things we're sending ourselves, or URGENT messages
4643 accept = (msg.sender is self or
4644- msg.priority == PRIORITY_URGENT or
4645+ msg.priority == self.messagehandler.PRIORITY_URGENT or
4646 sc.accept(msg, now))
4647 self.set_timeout(msg.recipient, now)
4648
4649@@ -231,4 +228,4 @@
4650 sc = self.limiters[user]
4651 if len(sc.queue) > 0 and not user in self.waitingusers:
4652 self.waitingusers.add(user)
4653- self.cron.setTimeout(self.fillrate / 1.0, user)
4654+ self.task.setTimeout(self.fillrate / 1.0, user)
4655
4656=== modified file 'src/endroid/plugins/remote.py'
4657--- src/endroid/plugins/remote.py 2012-11-25 00:01:10 +0000
4658+++ src/endroid/plugins/remote.py 2013-08-12 17:50:48 +0000
4659@@ -3,13 +3,10 @@
4660 # Copyright 2012, Ensoft Ltd
4661 # -----------------------------------------------------------------------------
4662
4663-import cgi
4664 import random
4665 import string
4666 import UserDict
4667
4668-from twisted.words.protocols.jabber.jid import JID
4669-
4670 from endroid.database import Database
4671 from endroid.pluginmanager import Plugin
4672
4673@@ -133,7 +130,7 @@
4674 return ''.join(random.choice(KEY_CHARS) for x in range(KEY_LENGTH))
4675
4676 def allow(self, msg, arg):
4677- jid = msg.sender.userhost()
4678+ jid = msg.sender
4679 if jid in self.keys:
4680 msg.reply("Remote notifications already allowed. (Your key is %s.)"
4681 % self.keys[jid])
4682@@ -143,7 +140,7 @@
4683 msg.reply("Remote notifications allowed. Your key is %s." % key)
4684
4685 def deny(self, msg, arg):
4686- jid = msg.sender.userhost()
4687+ jid = msg.sender
4688 if jid in self.keys:
4689 del self.keys[jid]
4690 msg.reply("Remote notifications denied.")
4691@@ -193,9 +190,8 @@
4692 if request.method == 'POST':
4693 self._validate_args(request.args)
4694
4695- self.send_chat(JID(request.args['user'][0]),
4696- "Remote notification received: %s" %
4697- request.args['message'][0])
4698+ self.messagehandler.send_chat(request.args['user'][0],
4699+ "Remote notification received: %s" % request.args['message'][0])
4700 header_msg = \
4701 "<strong>Message delivered to %s</strong> <br />" % \
4702 request.args['user'][0]
4703
4704=== added file 'src/endroid/plugins/roomowner.py'
4705--- src/endroid/plugins/roomowner.py 1970-01-01 00:00:00 +0000
4706+++ src/endroid/plugins/roomowner.py 2013-08-12 17:50:48 +0000
4707@@ -0,0 +1,9 @@
4708+from endroid.pluginmanager import Plugin
4709+
4710+class RoomOwner(Plugin):
4711+ def endroid_init(self):
4712+ if self.place == "room":
4713+ # note that configure_room internally sanitises anything it gets
4714+ # - allowing only predefined keys and ignoring the rest (so giving it
4715+ # our whole .vars dictionary is safe (and easy))
4716+ self.usermanagement.configure_room(self.place_name, self.vars)
4717
4718=== modified file 'src/endroid/plugins/speak.py'
4719--- src/endroid/plugins/speak.py 2012-08-13 20:27:02 +0000
4720+++ src/endroid/plugins/speak.py 2013-08-12 17:50:48 +0000
4721@@ -4,7 +4,6 @@
4722 # Created by Jonathan Millican
4723 # -----------------------------------------
4724
4725-from twisted.words.protocols.jabber.jid import JID
4726 from endroid.pluginmanager import Plugin
4727
4728 class Speak(Plugin):
4729@@ -20,16 +19,16 @@
4730
4731 def do_speak(self, msg, args):
4732 tojid, text = self.split(args)
4733- if tojid in self.suc_users():
4734- self.send_chat(JID(tojid), text)
4735+ if tojid in self.usermanagement.users.available:
4736+ self.messagehandler.send_chat(tojid, text, msg.sender)
4737 else:
4738 msg.reply("You can't send messages to that user. Sorry.")
4739
4740 def do_many_speak(self, msg, args):
4741 count, tojid, text = args.split(' ', 2)
4742- if tojid in self.suc_users():
4743+ if tojid in self.usermanagement.users.available:
4744 for i in range(int(count)):
4745- self.send_chat(JID(tojid), text)
4746+ self.messagehandler.send_chat(tojid, text, msg.sender)
4747 else:
4748 msg.reply("You can't send messages to that user. Sorry.")
4749
4750
4751=== modified file 'src/endroid/plugins/whosonline.py'
4752--- src/endroid/plugins/whosonline.py 2012-08-16 01:29:06 +0000
4753+++ src/endroid/plugins/whosonline.py 2013-08-12 17:50:48 +0000
4754@@ -4,7 +4,6 @@
4755 # Created by Jonathan Millican
4756 # -----------------------------------------
4757
4758-from twisted.words.protocols.jabber.jid import JID
4759 from endroid.pluginmanager import Plugin
4760
4761 class WhosOnline(Plugin):
4762@@ -19,29 +18,17 @@
4763 com.register_chat(self.show_online, 'whosonline')
4764 com.register_muc(self.show_rooms_in, 'listrooms')
4765 com.register_chat(self.show_rooms_in, 'listrooms')
4766- self.presence = self.presence()
4767
4768 def show_online(self, msg, arg):
4769 msg.reply(self.online_list())
4770
4771 def online_list(self):
4772- out = ""
4773- for i in self.presence.get_available():
4774- out = out + "\n" + i.userhost()
4775- return out
4776+ return '\n' + '\n'.join(self.usermanagement.get_available_users())
4777
4778 def show_rooms_in(self, msg, arg):
4779- if self.room_list():
4780- msg.reply(self.room_list())
4781- else:
4782- msg.reply("I'm not in any rooms")
4783+ rooms = self.usermanagement.get_available_rooms() or ["I'm not in any rooms!"]
4784+ msg.reply('\n' + '\n'.join(rooms))
4785
4786- def room_list(self):
4787- out = ""
4788- for i in self.presence.get_rooms():
4789- out = out + "\n" + i.userhost()
4790- return out
4791-
4792 def help(self):
4793 return "What? No. My name's not important. You must come with me, or you'll be late."
4794
4795
4796=== modified file 'src/endroid/rosterhandler.py'
4797--- src/endroid/rosterhandler.py 2012-08-31 19:21:34 +0000
4798+++ src/endroid/rosterhandler.py 2013-08-12 17:50:48 +0000
4799@@ -4,62 +4,103 @@
4800 # Created by Jonathan Millican
4801 # -----------------------------------------
4802
4803-import logging
4804-
4805-from wokkel.xmppim import RosterClientProtocol, PresenceProtocol
4806-
4807-class RosterHandler(RosterClientProtocol, PresenceProtocol):
4808+import logging
4809+
4810+from twisted.words.protocols.jabber.jid import JID
4811+from wokkel.xmppim import RosterClientProtocol, PresenceClientProtocol, RosterItem
4812+
4813+
4814+class RosterHandler(RosterClientProtocol, PresenceClientProtocol):
4815 def __init__(self):
4816 RosterClientProtocol.__init__(self)
4817- PresenceProtocol.__init__(self)
4818+ PresenceClientProtocol.__init__(self)
4819 self.deferred = None
4820-
4821- def set_usermanagement(self, um):
4822- self.usermanagement = um
4823-
4824+
4825+ def set_presence_handler(self, um):
4826+ self.um = um
4827+
4828 def setHandlerParent(self, parent):
4829 RosterClientProtocol.setHandlerParent(self, parent)
4830- PresenceProtocol.setHandlerParent(self, parent)
4831+ PresenceClientProtocol.setHandlerParent(self, parent)
4832
4833- def set_loaded_handler(self, d):
4834+ def set_connected_handler(self, d):
4835 self.deferred = d
4836
4837 def connectionInitialized(self):
4838 RosterClientProtocol.connectionInitialized(self)
4839- PresenceProtocol.connectionInitialized(self)
4840+ PresenceClientProtocol.connectionInitialized(self)
4841+
4842+ def purge_roster(roster, self):
4843+ # roster.keys() is a list of the users we are currently friends with.
4844+ for user in (item.userhost() for item in roster.keys()):
4845+ # if they have been removed from config since our last start
4846+ if not user in self.um.users.registered:
4847+ # get rid of them
4848+ self.um.users.deregister_user(user)
4849+ return roster # pass the roster on for further callbacks
4850+
4851 rosterd = self.reRoster()
4852+ rosterd.addCallback(purge_roster, self)
4853+
4854 if self.deferred:
4855 d, self.deferred = self.deferred, None
4856 rosterd.addCallback(d.callback)
4857 # Advertises as available - otherwise MUC will probably work
4858 # but SUC clients won't send messages on to EnDroid
4859- self.available()
4860-
4861+ self.set_available()
4862+
4863+ def set_available(self, entity=None):
4864+ super(RosterHandler, self).available(entity)
4865+
4866 def reRoster(self):
4867- d = self.getRoster()
4868- d.addCallback(self.usermanagement.got_roster)
4869- return d
4870-
4871-#######################
4872-# This version of EnDroid is designed to be bashful so
4873-# will simply ignore subscribe requests. Its contact list
4874-# is simply determined by the config file
4875-#######################
4876-# def subscribeReceived(self, presence):
4877-# PresenceProtocol.subscribeReceived(self, presence)
4878-# # Accept subscription
4879-# self.subscribed(presence.sender)
4880-# # Send available
4881-# self.available(presence.sender)
4882-# # Add them
4883-# UserManagement.add_contact(presence.sender)
4884-# self.reRoster()
4885-
4886+ return self.getRoster()
4887+
4888+ def setItem(self, name):
4889+ if isinstance(name, RosterItem):
4890+ super(RosterHandler, self).setItem(name)
4891+ elif isinstance(name, (str, unicode)):
4892+ super(RosterHandler, self).setItem(RosterItem(JID(name)))
4893+ else:
4894+ raise TypeError("RosterHandler.setItem got invalid type")
4895+
4896+ def removeItem(self, name):
4897+ logging.info("removeItem {}".format(name))
4898+ super(RosterHandler, self).removeItem(JID(name))
4899+
4900 def subscribedReceived(self, presence):
4901- PresenceProtocol.subscribedReceived(self, presence)
4902- self.available(presence.sender)
4903- self.reRoster()
4904+ PresenceClientProtocol.subscribedReceived(self, presence)
4905+ logging.info("Subscription request from {}".format(presence.userhost()))
4906+ if presence.userhost() in self.um.users.registered:
4907+ # this is generated when someone has confirmed our subscription request
4908+ # send a subscription _confirmation_ back (== authorizing in pidgin)
4909+ # (a wokkel API)
4910+ self.subscribed(presence)
4911+ # let the presence know that we are available (this does not affect
4912+ # subscription status)
4913+ self.set_available(presence)
4914
4915 def probeReceived(self, presence):
4916- PresenceProtocol.probeReceived(self, presence)
4917- self.available()
4918+ PresenceClientProtocol.probeReceived(self, presence)
4919+ self.set_available()
4920+
4921+ def availableReceived(self, entity, show=None, statuses=None, priority=0):
4922+ userhost, full_jid = entity.userhost(), entity.full()
4923+ logging.info("Available from {} '{}' priority: '{}'".format(full_jid, show, priority))
4924+ # entity has come online - update our online set
4925+ if userhost in self.um.users.registered:
4926+ self.um.users.set_available(full_jid)
4927+ # make them available in their groups
4928+ for group in self.um.get_groups(userhost):
4929+ self.um.group_rosters[group].set_available(full_jid)
4930+
4931+ def unavailableReceived(self, entity, statuses=None):
4932+ userhost, full_jid = entity.userhost(), entity.full()
4933+ logging.info("Unavailable from {}".format(full_jid))
4934+ # entity has gone offline - update our online set
4935+ if userhost in self.um.users.available:
4936+ self.um.users.set_unavailable(full_jid)
4937+ # make them unavailable in their groups
4938+ for group in self.um.get_groups(userhost):
4939+ self.um.group_rosters[group].set_unavailable(full_jid)
4940+
4941+
4942
4943=== modified file 'src/endroid/usermanagement.py'
4944--- src/endroid/usermanagement.py 2012-08-31 12:10:51 +0000
4945+++ src/endroid/usermanagement.py 2013-08-12 17:50:48 +0000
4946@@ -3,133 +3,537 @@
4947 # Copyright 2012, Ensoft Ltd.
4948 # Created by Jonathan Millican
4949 # -----------------------------------------
4950-from twisted.words.protocols.jabber.jid import JID
4951+
4952 import logging
4953-
4954-#
4955-# Note: in this file a fair bit is commented out. This is functionality
4956-# that was deliberately removed early on, but should be restored in a
4957-# later revision, once EnDroid's behaviour is more flexible. (e.g.
4958-# joining and leaving rooms dynamically, as opposed to on load-time
4959-# from the config file).
4960-#
4961-
4962-class Presence(object):
4963- """
4964- Object for plugins to access data from within UserManagement, without
4965- exposing the lower level endroid functionality to plugins
4966- """
4967- def __init__(self, usermanagement):
4968- self._usermanagement = usermanagement
4969-
4970- def get_available(self):
4971- """
4972- Returns the set of available contacts
4973- """
4974- return self._usermanagement.available
4975-
4976- def get_roster(self):
4977- """
4978- Returns the full roster
4979- """
4980- return self._usermanagement.roster
4981-
4982- def get_rooms(self):
4983- """
4984- Returns the set of rooms that EnDroid is in
4985- """
4986- return self._usermanagement.rooms
4987+from twisted.words.protocols.jabber.jid import JID, parse, InvalidFormat
4988+from twisted.words.protocols.jabber.error import StanzaError
4989+from endroid.pluginmanager import PluginManager
4990+from random import choice
4991+
4992+# we use ADJECTIVES to generate a random new nick if endroid's is taken
4993+ADJECTIVES = [
4994+ "affronted", "annoyed", "antagonized", "bitter", "chafed", "convulsed",
4995+ "cross", "displeased", "enraged", "exasperated", "ferocious", "fierce",
4996+ "fiery", "fuming", "furious", "galled", "hateful", "heated", "impassioned",
4997+ "incensed", "indignant", "inflamed", "infuriated", "irascible", "irate",
4998+ "ireful", "irritated", "maddened", "offended", "outraged", "provoked",
4999+ "raging", "resentful", "splenetic", "storming", "vexed", "wrathful"]
5000+
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches