Merge lp:~ben-hutchings/endroid/roomowner into lp:endroid
- roomowner
- Merge into trunk
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 |
Related bugs: | |
Related blueprints: |
MUC Room Control Plugin
(Medium)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Morrison | Approve | ||
Review via email:
|
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/
- 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.
- 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_
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)availableRe
- 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/
endroid2.graphxml
- a class diagram of endroid
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Martin Morrison (isoschiz) wrote : Posted in a previous version of this proposal | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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/
>
> - 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.
>
> - 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)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Martin Morrison (isoschiz) wrote : Posted in a previous version of this proposal | # |
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_
- __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_
- 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...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Ben Hutchings (ben-hutchings) wrote : Posted in a previous version of this proposal | # |
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_
>
> - __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_
>
> - 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...
- 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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Martin Morrison (isoschiz) wrote : | # |
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_
- 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_
Actually, now that I think about it, maybe what would be good is if plugins could do:
self.messagehan
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>+}...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.messagehan
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_
>
> Actually, now that I think about it, maybe what would be good is if plugins could do:
>
> self.messagehan
>
> 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?
- 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_ passwordprotect edroom 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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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/
- 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/
- 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 connectionIniti
- 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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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/
>
> - 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 connectionIniti
>
> - 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
- 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.receivedGroupCh at 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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Martin Morrison (isoschiz) : | # |
Preview Diff
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' |
165 | Binary 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' |
167 | Binary 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 | + |
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)