Merge lp:~isoschiz/endroid/globalplugins into lp:endroid

Proposed by Martin Morrison
Status: Superseded
Proposed branch: lp:~isoschiz/endroid/globalplugins
Merge into: lp:endroid
Diff against target: 1587 lines (+636/-209) (has conflicts)
17 files modified
src/endroid/__init__.py (+3/-1)
src/endroid/database.py (+3/-2)
src/endroid/messagehandler.py (+127/-43)
src/endroid/pluginmanager.py (+196/-60)
src/endroid/plugins/blacklist.py (+9/-11)
src/endroid/plugins/brainyquote.py (+26/-0)
src/endroid/plugins/command.py (+25/-25)
src/endroid/plugins/coolit.py (+3/-4)
src/endroid/plugins/correct.py (+2/-3)
src/endroid/plugins/help.py (+6/-6)
src/endroid/plugins/ratelimit.py (+16/-14)
src/endroid/plugins/speak.py (+15/-26)
src/endroid/plugins/spell.py (+3/-7)
src/endroid/plugins/trains.py (+120/-0)
src/endroid/plugins/unhandled.py (+2/-2)
src/endroid/usermanagement.py (+77/-5)
src/endroid/wokkelhandler.py (+3/-0)
Text conflict in src/endroid/messagehandler.py
Text conflict in src/endroid/pluginmanager.py
Text conflict in src/endroid/plugins/ratelimit.py
Text conflict in src/endroid/usermanagement.py
To merge this branch: bzr merge lp:~isoschiz/endroid/globalplugins
Reviewer Review Type Date Requested Status
Ben Hutchings Approve
Ensoft Open Source Pending
Review via email: mp+180142@code.launchpad.net

This proposal supersedes a proposal from 2013-08-13.

This proposal has been superseded by a proposal from 2013-08-14.

Description of the change

Patch contains several changes:

- Integrate Twisted logging with built in logging so only a single log file exists

- self.database property on Plugins that provides cleaner access to per-plugin DBs.
- Fix usage of PluginProxy (now only used as a proxy for missing plugins)
- Improved error handling during plugin loading phase, including handling for circular dependencies
- Fix to use the first (and not the last) <body> present in a message.
- Migrated some plugins to newer APIs (blacklist, command, coolit, correct, help, ratelimit, speak, spell, unhandled)

- Create per-plugin UserManagement and MessageHandler classes, with simplified APIs
- New brainyquote plugin to get Quote of the Moment
- New traintimes plugin that tells you when the next train is. Is awesome.
- Start of GlobalPlugin (and generally better Place) handling. This is incomplete.

To post a comment you must log in.
Revision history for this message
Ben Hutchings (ben-hutchings) wrote :

Looks good.

A couple of _minor_ points:

1382: The namedtuple Place is unused (but will hopefully be incorporated into the rest of the code at some point...).

Line 172:
    if (unhandled, ...).count(True) > 1:
might be a clearer that than:
    if sum(1 for i in (unhandled, send_filter, recv_filter) if i) > 1:

review: Approve
lp:~isoschiz/endroid/globalplugins updated
40. By Matthew Earl

Add tee/cat equivalents of endroid_echo

41. By Martin Morrison

Patch contains several changes:

- Integrate Twisted logging with built in logging so only a single log file exists

- self.database property on Plugins that provides cleaner access to per-plugin DBs.
- Fix usage of PluginProxy (now only used as a proxy for missing plugins)
- Improved error handling during plugin loading phase, including handling for circular dependencies
- Fix to use the first (and not the last) <body> present in a message.
- Migrated some plugins to newer APIs (blacklist, command, coolit, correct, help, ratelimit, speak, spell, unhandled)

- Create per-plugin UserManagement and MessageHandler classes, with simplified APIs
- New brainyquote plugin to get Quote of the Moment
- New traintimes plugin that tells you when the next train is. Is awesome.
- Start of GlobalPlugin (and generally better Place) handling. This is incomplete.

42. By Martin Morrison

Adds support for plugins that want to register whole Resources instead of just a function.

43. By Ben Hutchings

Rosterhandler pushes up more information about available presences - now storing the priority and status of resources.

The broadcast plugin can be configured to the following levels:
 - all: broadcast to all a user's available resources
 - positive: broadcast to all a user's available resources with positive (or zero) priorities
 - max: broadcast to the user's highest priority resource
 - none: do not broadcast (leave it up to the server)

This should allow EnDroid to emulate the most common server behaviours when it comes to sending messages.

44. By Martin Morrison

Add an HTTP front page with links to any plugins with registered pages.

45. By Matthew Earl

Fix permissions for endroid remote binaries

46. By Martin Morrison

Shrink the MEGA IMAGE.

47. By Martin Morrison

Add a @task decorator for methods of Plugins that are cron callback handlers. Migrate the ratelimit plugin to use it. Note this is untested at this stage.

48. By Martin Morrison

Update the cron task decorator to use the .cron attribute of the Plugin (so it is future proof for when we don't have a single global cron anymore)

49. By Martin Morrison

Continue adding support for GlobalPlugins. Various tidy up and move to supporting Place objects throughout. Work in progress.

50. By Martin Morrison

Migrate a few more places to Places

51. By Martin Morrison

Merge in latest upstream changes

52. By Martin Morrison

Move closer to full support for GlobalPlugins:

- message and user handling now support multiple places for most operations
- Filled out the APIs for rosters access for plugins (still some missing though)
- Migrated a bunch of plugins to a bunch of the newer APIs
- Various minor improvements to plugins along the way

Key point is: some of the plugins are now declared as Global, so have had all their class variables removed. No testing done as yet though.

53. By Martin Morrison

Further migration to new APIs, including removing all the deprecated cruft from Plugin

54. By Martin Morrison

Further work on GlobalPlugins. Almost all working, except initialisation order of a GlobalPlugin depending on a regular Plugin

Unmerged revisions

54. By Martin Morrison

Further work on GlobalPlugins. Almost all working, except initialisation order of a GlobalPlugin depending on a regular Plugin

53. By Martin Morrison

Further migration to new APIs, including removing all the deprecated cruft from Plugin

52. By Martin Morrison

Move closer to full support for GlobalPlugins:

- message and user handling now support multiple places for most operations
- Filled out the APIs for rosters access for plugins (still some missing though)
- Migrated a bunch of plugins to a bunch of the newer APIs
- Various minor improvements to plugins along the way

Key point is: some of the plugins are now declared as Global, so have had all their class variables removed. No testing done as yet though.

51. By Martin Morrison

Merge in latest upstream changes

50. By Martin Morrison

Migrate a few more places to Places

49. By Martin Morrison

Continue adding support for GlobalPlugins. Various tidy up and move to supporting Place objects throughout. Work in progress.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/endroid/__init__.py'
2--- src/endroid/__init__.py 2013-08-13 17:47:42 +0000
3+++ src/endroid/__init__.py 2013-08-14 13:58:20 +0000
4@@ -63,7 +63,9 @@
5 logfile = self.conf.get("setup", "logfile",
6 default='~/.endroid/endroid.log')
7 logging.info("Using " + logfile + " as xml log file")
8- log.startLogging(open(os.path.expanduser(logfile), "w+"))
9+ observer = log.PythonLoggingObserver()
10+ observer.start()
11+ #log.startLogging(open(os.path.expanduser(logfile), "w+"))
12
13 self.client = XMPPClient(JID(self.jid), self.secret)
14 logging.info("Setting traffic logging to " + str(logtraffic))
15
16=== modified file 'src/endroid/database.py'
17--- src/endroid/database.py 2013-08-13 10:00:42 +0000
18+++ src/endroid/database.py 2013-08-14 13:58:20 +0000
19@@ -18,7 +18,8 @@
20 def __init__(self, *args, **kwargs):
21 super(TableRow, self).__init__(*args, **kwargs)
22 if not EndroidUniqueID in self:
23- raise ValueError("Cannot create table row from table with no {0} column!".format(EndroidUniqueID))
24+ raise ValueError("Cannot create table row from table with no {0} "
25+ "column!".format(EndroidUniqueID))
26
27 @property
28 def id(self):
29@@ -163,7 +164,7 @@
30 tup = tup + Database._tupleFromFieldValues(conditions)
31 Database.raw(query, tup)
32 return Database.cursor.rowcount
33-
34+
35 def empty_table(self, name):
36 """Remove all rows from table 'name'."""
37 n = Database._sanitize(self._tName(name))
38
39=== modified file 'src/endroid/messagehandler.py'
40--- src/endroid/messagehandler.py 2013-08-14 10:00:18 +0000
41+++ src/endroid/messagehandler.py 2013-08-14 13:58:20 +0000
42@@ -18,13 +18,17 @@
43 def __str__(self):
44 return "{}: {}".format(self.priority, self.name)
45
46+class Priority(object):
47+ NORMAL = 0
48+ URGENT = -1
49+ BULK = 1
50
51 class MessageHandler(object):
52 """An abstraction of XMPP's message protocols."""
53
54- PRIORITY_NORMAL = 0
55- PRIORITY_URGENT = -1
56- PRIORITY_BULK = 1
57+ PRIORITY_NORMAL = Priority.NORMAL
58+ PRIORITY_URGENT = Priority.URGENT
59+ PRIORITY_BULK = Priority.BULK
60
61 def __init__(self, wh, um):
62 self.wh = wh
63@@ -33,8 +37,8 @@
64 self.wh.set_message_handler(self)
65 self._handlers = {}
66
67- def register_callback(self, name, typ, cat, callback,
68- including_self=False, priority=PRIORITY_NORMAL):
69+ def _register_callback(self, name, typ, cat, callback,
70+ including_self=False, priority=Priority.NORMAL):
71 """
72 Register a function to be called on receipt of a message of type
73 'typ' (muc/chat), category 'cat' (recv, send, unhandled, *_self, *_filter)
74@@ -51,10 +55,10 @@
75
76 # this callback be called when we get messages sent by ourself
77 if including_self:
78- self.register_callback(name, typ, cat + "_self", callback,
79- priority=priority)
80+ self._register_callback(name, typ, cat + "_self", callback,
81+ priority=priority)
82
83- def get_handlers(self, typ, cat, name):
84+ def _get_handlers(self, typ, cat, name):
85 dct = self._handlers.get(typ, {}).get(cat, {})
86 if typ == 'chat': # we need to lookup name's groups
87 # we may have either a full jid or just a userhost,
88@@ -68,22 +72,22 @@
89 else: # we are in a room so only one set of handlers to read
90 return dct.get(name, [])
91
92- def get_filters(self, typ, cat, name):
93- return self.get_handlers(typ, cat+"_filter", name)
94+ def _get_filters(self, typ, cat, name):
95+ return self._get_handlers(typ, cat + "_filter", name)
96
97- def do_callback(self, cat, msg, failback):
98+ def _do_callback(self, cat, msg, failback=lambda m: None):
99 if msg.place == "muc":
100 # get the handlers active in the room - note that these are already
101 # sorted (sorting is done in the register_callback method)
102- handlers = self.get_handlers(msg.place, cat, msg.recipient)
103- filters = self.get_filters(msg.place, cat, msg.recipient)
104+ handlers = self._get_handlers(msg.place, cat, msg.recipient)
105+ filters = self._get_filters(msg.place, cat, msg.recipient)
106 else:
107 # combine the handlers from each group the user is registered with
108 # note that if the same plugin is registered for more than one of
109 # the user's groups, the plugin's instance in each group will be
110 # called
111- handlers = self.get_handlers(msg.place, cat, msg.sender)
112- filters = self.get_filters(msg.place, cat, msg.sender)
113+ handlers = self._get_handlers(msg.place, cat, msg.sender)
114+ filters = self._get_filters(msg.place, cat, msg.sender)
115
116 log_list = []
117 if handlers and all(f.callback(msg) for f in filters):
118@@ -109,45 +113,78 @@
119 else:
120 logging.info("Finished plugin callback - no plugins called.")
121
122- def _unhandled_muc(self, msg):
123- self.do_callback("unhandled", msg, lambda m: None)
124+ def _unhandled(self, msg):
125+ self._do_callback("unhandled", msg)
126
127- def _unhandled_self_muc(self, msg):
128- self.do_callback("unhandled_self", msg, lambda m: None)
129+ def _unhandled_self(self, msg):
130+ self._do_callback("unhandled_self", msg)
131
132 # Do normal (recv) callbacks on msg. If no callbacks handle the message
133 # then call unhandled callbacks (msg's failback is set self._unhandled_...
134- # by the last argument to do_callback).
135+ # by the last argument to _do_callback).
136 def receive_muc(self, msg):
137- self.do_callback("recv", msg, self._unhandled_muc)
138+ self._do_callback("recv", msg, self._unhandled)
139
140 def receive_self_muc(self, msg):
141- self.do_callback("recv_self", msg, self._unhandled_self_muc)
142-
143- def _unhandled_chat(self, msg):
144- self.do_callback("unhandled", msg, lambda m: None)
145-
146- def _unhandled_self_chat(self, msg):
147- self.do_callback("unhandled_self", msg, lambda m: None)
148+ self._do_callback("recv_self", msg, self._unhandled_self)
149
150 def receive_chat(self, msg):
151- self.do_callback("recv", msg, self._unhandled_chat)
152+ self._do_callback("recv", msg, self._unhandled)
153
154 def receive_self_chat(self, msg):
155+<<<<<<< TREE
156 self.do_callback("recv_self", msg, self._unhandled_self_chat)
157
158 def send_muc(self, room, body, source=None, priority=PRIORITY_NORMAL):
159 """
160 Send muc message to room.
161+=======
162+ self._do_callback("recv_self", msg, self._unhandled_self)
163+
164+ def for_plugin(self, pluginmanager, plugin):
165+ return PluginMessageHandler(self, pluginmanager, plugin)
166+
167+ # API for global plugins
168+
169+ def register(self, name, callback, priority=Priority.NORMAL, muc_only=False,
170+ chat_only=False, include_self=False, unhandled=False,
171+ send_filter=False, recv_filter=False):
172+ if sum(1 for i in (unhandled, send_filter, recv_filter) if i) > 1:
173+ raise TypeError("Only one of unhandled, send_filter or recv_filter "
174+ "may be specified")
175+ if chat_only and muc_only:
176+ raise TypeError("Only one of chat_only or muc_only may be "
177+ "specified")
178+
179+ if unhandled:
180+ cat = "unhandled"
181+ elif send_filter:
182+ cat = "send_filter"
183+ elif recv_filter:
184+ cat = "recv_filter"
185+ else:
186+ cat = "recv"
187+
188+ if not muc_only:
189+ self._register_callback(name, "chat", cat, callback,
190+ include_self, priority)
191+ if not chat_only:
192+ self._register_callback(name, "muc", cat, callback,
193+ include_self, priority)
194+
195+ def send_muc(self, room, body, source=None, priority=Priority.NORMAL):
196+ """Send muc message to room.
197+>>>>>>> MERGE-SOURCE
198
199 The message will be run through any registered filters before it is sent.
200
201 """
202+ # Verify this is a room EnDroid knows about
203 msg = Message('muc', source, body, self, recipient=room)
204 # when sending messages we check the filters registered with the
205 # _recipient_. Cf. when we receive messages we check filters registered
206 # with the _sender_.
207- filters = self.get_filters('muc', 'send', msg.recipient)
208+ filters = self._get_filters('muc', 'send', msg.recipient)
209
210 if all(f.callback(msg) for f in filters):
211 logging.info("Sending message to {}".format(room))
212@@ -155,15 +192,21 @@
213 else:
214 logging.info("Filtered out message to {}".format(room))
215
216+<<<<<<< TREE
217 def send_chat(self, user, body, source=None, priority=PRIORITY_NORMAL):
218 """
219 Send chat message to person with address user.
220+=======
221+ def send_chat(self, user, body, source=None, priority=Priority.NORMAL):
222+ """Send chat message to person with address user.
223+>>>>>>> MERGE-SOURCE
224
225 The message will be run through any registered filters before it is sent.
226
227 """
228+ # Verify user is known to EnDroid
229 msg = Message('chat', source, body, self, recipient=user)
230- filters = self.get_filters('chat', 'send', msg.recipient)
231+ filters = self._get_filters('chat', 'send', msg.recipient)
232
233 if all(f.callback(msg) for f in filters):
234 logging.info("Sending message to {}".format(user))
235@@ -172,16 +215,57 @@
236 logging.info("Filtered out message to {0}".format(user))
237
238
239+class PluginMessageHandler(object):
240+ """
241+ One of these exists per plugin, providing the API to handle messsages.
242+ """
243+ def __init__(self, messagehandler, pluginmanager, plugin):
244+ self._messagehandler = messagehandler
245+ self._pluginmanager = pluginmanager
246+ self._plugin = plugin
247+
248+ def send_muc(self, body, source=None, priority=Priority.NORMAL):
249+ if self._pluginmanager.place != "room":
250+ raise ValueError("Not in a room")
251+ self._messagehandler.send_muc(self._pluginmanager.name, body,
252+ source=source, priority=priority)
253+
254+ def send_chat(self, user, body, source=None, priority=Priority.NORMAL):
255+ if self._pluginmanager.place != "group":
256+ raise ValueError("Not in a group")
257+ if user not in self._pluginmanager.usermanagement.users(
258+ self._pluginmanager.name):
259+ raise ValueError("Target user is not in this group")
260+ # Verify user is in the group we are in
261+ self._messagehandler.send_chat(user, body,
262+ source=source, priority=priority)
263+
264+ def register(self, callback, priority=Priority.NORMAL, muc_only=False,
265+ chat_only=False, include_self=False, unhandled=False,
266+ send_filter=False, recv_filter=False):
267+ if self._pluginmanager.place == "room" and not chat_only:
268+ muc_only = True
269+ if self._pluginmanager.place == "group" and not muc_only:
270+ chat_only = True
271+ self._messagehandler.register(self._pluginmanager.name, callback,
272+ priority=priority, muc_only=muc_only,
273+ chat_only=chat_only,
274+ include_self=include_self,
275+ unhandled=unhandled,
276+ send_filter=send_filter,
277+ recv_filter=recv_filter)
278+
279 class Message(object):
280 def __init__(self, place, sender, body, messagehandler, recipient, handlers=0,
281- priority=MessageHandler.PRIORITY_NORMAL):
282+ priority=Priority.NORMAL):
283 self.place = place
284
285 # sender_full is a string representing the full jid (including resource)
286 # of the message's sender. Used in reply methods so that if a user is
287- # logged in on several resources, the reply will be sent to the right one
288+ # logged in on several resources, the reply will be sent to the right
289+ # one
290 self.sender_full = sender
291- # a string represeting the userhost of the message's sender. Used to
292+ # a string representing the userhost of the message's sender. Used to
293 # lookup resource-independant user properties eg their registered rooms
294 self.sender = messagehandler.um.get_userhost(sender)
295 self.body = body
296@@ -189,24 +273,24 @@
297
298 # a count of plugins which will try to process this message
299 self.__handlers = handlers
300- self.messagehandler = messagehandler
301+ self._messagehandler = messagehandler
302 self.priority = priority
303
304 def send(self):
305 if self.place == "chat":
306- self.messagehandler.send_chat(self.recipient, self.body, self.sender)
307+ self._messagehandler.send_chat(self.recipient, self.body, self.sender)
308 elif self.place == "muc":
309- self.messagehandler.send_muc(self.recipient, self.body, self.sender)
310+ self._messagehandler.send_muc(self.recipient, self.body, self.sender)
311
312 def reply(self, body):
313 if self.place == "chat":
314- self.messagehandler.send_chat(self.sender_full, body, self.recipient)
315+ self._messagehandler.send_chat(self.sender_full, body, self.recipient)
316 elif self.place == "muc":
317 # we send to the room (the recipient), not the message's sender
318- self.messagehandler.send_muc(self.recipient, body, self.recipient)
319+ self._messagehandler.send_muc(self.recipient, body, self.recipient)
320
321 def reply_to_sender(self, body):
322- self.messagehandler.send_chat(self.sender_full, body, self.recipient)
323+ self._messagehandler.send_chat(self.sender_full, body, self.recipient)
324
325 def inc_handlers(self):
326 self.__handlers += 1
327@@ -228,9 +312,9 @@
328 self.dec_handlers()
329
330 def do_unhandled(self):
331- if self.__handlers == 0 and hasattr(self, 'unHandledCallback'):
332- self.unHandledCallback(self)
333+ if self.__handlers == 0 and hasattr(self, '_unhandled_cb'):
334+ self._unhandled_cb(self)
335
336 def set_unhandled_cb(self, cb):
337- self.unHandledCallback = cb
338+ self._unhandled_cb = cb
339
340
341=== modified file 'src/endroid/pluginmanager.py'
342--- src/endroid/pluginmanager.py 2013-08-14 10:00:18 +0000
343+++ src/endroid/pluginmanager.py 2013-08-14 13:58:20 +0000
344@@ -7,6 +7,10 @@
345 import sys
346 import logging
347 from endroid.cron import Cron
348+from endroid.database import Database
349+
350+def deprecated(fn):
351+ return fn
352
353 msg_filter_doc = ("Register a {0} filter for {1} messages.\n"
354 "Filter takes endroid.messagehandler.Message and returns bool. If its\n"
355@@ -69,36 +73,51 @@
356 """
357 __metaclass__ = PluginMeta
358
359- def _pluginInit(self, pm, conf):
360+ def _setup(self, pm, conf):
361 self._pm = pm
362
363 self.messagehandler = pm.messagehandler
364 self.usermanagement = pm.usermanagement
365
366+ self.plugins = pm
367+ self.messages = pm.messagehandler.for_plugin(pm, self)
368+ self.rosters = pm.usermanagement.for_plugin(pm, self)
369+
370+ self._database = None
371+
372 self.place = pm.place
373 self.place_name = pm.name
374 self.vars = conf
375
376+ @property
377+ def database(self):
378+ if self._database is None:
379+ self._database = Database(self.name) # Should use place too
380+ return self._database
381+
382 def _register(self, *args, **kwargs):
383- return self.messagehandler.register_callback(self._pm.name, *args, **kwargs)
384+ return self.messagehandler._register_callback(self._pm.name, *args, **kwargs)
385
386- # Registration methods
387+ # Message Registration methods
388+ @deprecated
389 def register_muc_callback(self, callback, inc_self=False, priority=0):
390 if self._pm.place != "room":
391 return
392 self._register("muc", "recv", callback, inc_self, priority)
393
394+ @deprecated
395 def register_chat_callback(self, callback, inc_self=False, priority=0):
396 if self._pm.place != "group":
397 return
398 self._register("chat", "recv", callback, inc_self, priority)
399
400-
401+ @deprecated
402 def register_unhandled_muc_callback(self, callback, inc_self=False, priority=0):
403 if self._pm.place != "room":
404 return
405 self._register("muc", "unhandled", callback, inc_self, priority)
406
407+ @deprecated
408 def register_unhandled_chat_callback(self, callback, inc_self=False, priority=0):
409 if self._pm.place != "group":
410 return
411@@ -109,21 +128,25 @@
412 register_unhandled_muc_callback.__doc__ = msg_cb_doc.format("unhandled muc")
413 register_unhandled_chat_callback.__doc__ = msg_cb_doc.format("unhandled chat")
414
415+ @deprecated
416 def register_muc_filter(self, callback, inc_self=False, priority=0):
417 if self._pm.place != "room":
418 return
419 self._register("muc", "recv_filter", callback, inc_self, priority)
420
421+ @deprecated
422 def register_chat_filter(self, callback, inc_self=False, priority=0):
423 if self._pm.place != "group":
424 return
425 self._register("chat", "recv_filter", callback, inc_self, priority)
426
427+ @deprecated
428 def register_muc_send_filter(self, callback, inc_self=False, priority=0):
429 if self._pm.place != "room":
430 return
431 self._register("muc", "send_filter", callback, inc_self, priority)
432
433+ @deprecated
434 def register_chat_send_filter(self, callback, inc_self=False, priority=0):
435 if self._pm.place != "group":
436 return
437@@ -135,9 +158,10 @@
438 register_chat_send_filter.__doc__ = msg_filter_doc.format("send", "chat")
439
440 # Plugin access methods
441+ @deprecated
442 def get(self, plugin_name):
443 """Return a plugin-like object from the plugin module plugin_name."""
444- return self._pm.get(plugin_name)
445+ return self.plugins.get(plugin_name)
446
447 def get_dependencies(self):
448 """
449@@ -159,18 +183,22 @@
450 """
451 return (self.get(preference) for preference in self.preferences)
452
453+ @deprecated
454 def list_plugins(self):
455 """Return a list of all plugins loaded in the plugin's environment."""
456- return self._pm.get_plugins()
457+ return self.plugins.all()
458
459+ @deprecated
460 def pluginLoaded(self, modname):
461 """Check if modname is loaded in the plugin's environment (bool)."""
462- return self._pm.hasloaded(modname)
463+ return self.plugins.loaded(modname)
464
465+ @deprecated
466 def pluginCall(self, modname, func, *args, **kwargs):
467 """Directly call a method on plugin modname."""
468 return getattr(self.get(modname), func)(*args, **kwargs)
469
470+ # Overridable values/properties
471 def endroid_init(self):
472 pass
473
474@@ -178,33 +206,26 @@
475 def cron(self):
476 return Cron().get()
477
478-
479 dependencies = ()
480 preferences = ()
481
482
483+class GlobalPlugin(Plugin):
484+ def _setup(self, pm, conf):
485+ super(GlobalPlugin, self)._setup(pm, conf)
486+ self.messages = pm.messagehandler
487+ self.rosters = pm.usermanagement
488+
489+
490 class PluginProxy(object):
491 def __init__(self, modname):
492- __import__(modname)
493- # dictionary mapping module names to module objects
494- m = sys.modules[modname]
495- # In loading a plugin, we first look for a get_plugin() function,
496- # then a function with the same name as the module, and finally we
497- # just check the automatic Plugin registry for a Plugin defined in
498- # that module.
499- if hasattr(m, 'get_plugin'):
500- self.module = getattr(m, 'get_plugin')()
501- else:
502- self.module = PluginMeta.registry[modname]()
503+ self.name = modname
504
505 def __getattr__(self, key):
506- if hasattr(self.module, key):
507- return getattr(self.module, key)
508- else:
509- return self
510+ return self.__dict__.get(key, self)
511
512- def hasattr(self, key):
513- return hasattr(self.module, key)
514+ def __getitem__(self, idx):
515+ return self
516
517 def __call__(self, *args, **kwargs):
518 return self
519@@ -231,46 +252,68 @@
520 self.messagehandler = messagehandler
521 self.usermanagement = usermanagement
522
523- self.place = place
524- self.name = name
525+ self.place = place # For global, needs to be made to work with config
526+ self.name = name or "*"
527
528- # this is a dictionary of plugin module names to pluginproxy objects
529+ # this is a dictionary of plugin module names to plugin objects
530 self._loaded = {}
531- # a dict of modnames : plugin configs
532- self._plugins = {}
533- # module name to bool dictionary
534- self.initialised = {}
535-
536- self.read_config(config)
537- self.loadPlugins()
538- self.initPlugins()
539-
540- def read_config(self, conf):
541+ # a dict of modnames : plugin configs (unused?)
542+ self._plugin_cfg = {}
543+ # module name to bool dictionary (use set instead?)
544+ self._initialised = set()
545+
546+ self._read_config(config)
547+ self._load_plugins()
548+ self._init_plugins()
549+
550+ def _read_config(self, conf):
551 def get_data(modname):
552 # return a tuple of (modname, modname's config)
553 return modname, conf.get(self.place, self.name, "plugin", modname, default={})
554
555 plugins = conf.get(self.place, self.name, "plugins")
556- self._plugins = dict(map(get_data, plugins))
557-
558- def get_plugins(self):
559- return self._plugins.keys()
560-
561- def load(self, modname):
562+ logging.debug("Found the following plugins in {}/{}: {}".format(
563+ self.place, self.name, ", ".join(plugins)))
564+ self._plugin_cfg = dict(map(get_data, plugins))
565+
566+ def _load(self, modname):
567 # loads the plugin module and adds a key to self._loaded
568 logging.debug("\tLoading Plugin: " + modname)
569 try:
570- p = PluginProxy(modname)
571- p._pluginInit(self, self._plugins[modname])
572-
573- self._loaded[modname] = p
574+ __import__(modname)
575 except ImportError as i:
576 logging.error(i)
577 logging.error("**Could Not Import Plugin \"" + modname
578 + "\". Check That It Exists In Your PYTHONPATH.")
579-
580- def loadPlugins(self):
581+ return
582+ except Exception as e:
583+ logging.error(e)
584+ logging.error("**Failed to import plugin {}".format(modname))
585+ return
586+ else:
587+ # dictionary mapping module names to module objects
588+ m = sys.modules[modname]
589+
590+ try:
591+ # In loading a plugin, we first look for a get_plugin() function,
592+ # then check the automatic Plugin registry for a Plugin defined in
593+ # that module.
594+ if hasattr(m, 'get_plugin'):
595+ plugin = getattr(m, 'get_plugin')()
596+ else:
597+ plugin = PluginMeta.registry[modname]()
598+ except Exception as k:
599+ logging.error(k)
600+ logging.error("**Could not import plugin {}. Module doesn't seem to"
601+ "define a Plugin".format(modname))
602+ return
603+ else:
604+ plugin._setup(self, self._plugin_cfg[modname])
605+ self._loaded[modname] = plugin
606+
607+ def _load_plugins(self):
608 logging.info("Loading Plugins for {0}".format(self.name))
609+<<<<<<< TREE
610 plugins = self.get_plugins()
611 for p in plugins:
612 self.load(p)
613@@ -280,23 +323,86 @@
614
615 def init(self, modname):
616 if modname in self.initialised:
617+=======
618+ for p in self._plugin_cfg:
619+ self._load(p)
620+
621+ def _init_one(self, modname):
622+ logging.debug("\tInitialising Plugin: " + modname)
623+ if modname in self._initialised:
624+>>>>>>> MERGE-SOURCE
625 logging.debug("\t{0} Already Initialised".format(modname))
626 return True
627+<<<<<<< TREE
628 if not self.hasloaded(modname):
629 logging.error("\t**Cannot Initialise Plugin \"" + modname + "\", "
630 "It Has Not Been Imported")
631+=======
632+ if not self.loaded(modname):
633+ logging.error("\t**Cannot Initialise Plugin \"" + modname + "\", It Has Not Been Imported")
634+>>>>>>> MERGE-SOURCE
635 return False
636+<<<<<<< TREE
637 logging.debug("\tInitialising Plugin: " + modname)
638+=======
639+ if modname in self._initialising:
640+ logging.error("\t**Circular dependency detected. Initialising: {}"
641+ .format(", ".join(sorted(self._initialising))))
642+ return False
643+>>>>>>> MERGE-SOURCE
644
645 # deal with dependencies and preferences
646- plugin = self.get(modname)
647- for mod_dep_name in plugin.dependencies:
648- logging.debug("\t{0} Depends On {1}".format(modname, mod_dep_name))
649- if not self.init(mod_dep_name):
650- # can't possibly initialise us so remove us from self._loaded
651- logging.error("\t**No \"" + mod_dep_name + "\". Unloading " + modname)
652- self._loaded.pop(modname)
653+ # Dependencies are mandatory, so they must be loaded;
654+ # Preferences are optional, so are replaced with a PluginProxy if not
655+ # loaded. In both cases, all mentioned plugins are initialised to
656+ # make sure they are ready before this plugin starts to load them.
657+
658+ # Circular dependencies cause failures, while circular preferences are
659+ # temporarily replaced with a Proxy to break the cycle, then replaced
660+ # later (which means that during the init phase, they will not have
661+ # been available so might not be correctly used).
662+ self._initialising.add(modname)
663+
664+ try:
665+ plugin = self.get(modname)
666+ for mod_dep_name in plugin.dependencies:
667+ logging.debug("\t{} depends on {}".format(modname,
668+ mod_dep_name))
669+ if not self._init_one(mod_dep_name):
670+ # can't possibly initialise us so remove us from self._loaded
671+ logging.error('\t**No "{}". Unloading {}.'
672+ .format(mod_dep_name, modname))
673+ self._loaded.pop(modname)
674+ return False
675+
676+ for mod_pref_name in plugin.preferences:
677+ logging.debug("\t{} Prefers {}".format(modname, mod_pref_name))
678+ if mod_pref_name in self._initialising:
679+ logging.warning("\tDetected circular preference for {}. "
680+ "Continuing with proxy object in place"
681+ .format(mod_pref_name))
682+ self._loaded[mod_pref_name] = PluginProxy(mod_pref_name)
683+
684+ elif not self._init_one(mod_pref_name):
685+ logging.error("\t**Could Not Load {} required by {}".format(
686+ mod_pref_name, modname))
687+ # Create a proxy object instead
688+ self._loaded[mod_pref_name] = PluginProxy(mod_pref_name)
689+
690+ # attempt to initialise the plugin
691+ try:
692+ plugin.endroid_init()
693+ self._initialised.add(modname)
694+ logging.info("\tInitialised Plugin: " + modname)
695+ # Re-add this plugin to _loaded, in case it was temporarily
696+ # replaced by a proxy
697+ self._loaded[modname] = plugin
698+ except Exception as e:
699+ logging.error(e)
700+ logging.error('\t**Error initializing "{}". See log for details.'
701+ .format(modname))
702 return False
703+<<<<<<< TREE
704
705 for mod_pref_name in plugin.preferences:
706 logging.debug("\t{0} Prefers {1}".format(modname, mod_pref_name))
707@@ -314,12 +420,42 @@
708 return True
709
710 def initPlugins(self):
711+=======
712+ return True
713+ finally:
714+ self._initialising.discard(modname)
715+
716+ def _init_plugins(self):
717+>>>>>>> MERGE-SOURCE
718 logging.info("Initialising Plugins for {0}".format(self.name))
719- plugins = self.get_plugins()
720- for p in plugins:
721- self.init(p)
722+ # Track what we're doing to detect circular dependencies
723+ self._initialising = set()
724+ for p in self.all():
725+ self._init_one(p)
726+ del self._initialising
727+
728+ # =========================================================================
729+ # Public API for plugins
730+ #
731+
732+ def all(self):
733+ """
734+ Return an Iterator of the names of all plugins loaded in this place.
735+ """
736+ return self._loaded.keys()
737+ get_plugins = all
738+
739+ def loaded(self, name):
740+ """Returns True if the named plugin is loaded in this place."""
741+ return name in self._loaded
742+ hasLoaded = loaded
743
744 def get(self, name):
745+ """
746+ Gets the instance of the named plugin within this place.
747+
748+ Raises a ModuleNotLoadedError if the plugin is not loaded.
749+ """
750 if not name in self._loaded:
751 raise ModuleNotLoadedError(name)
752 return self._loaded[name]
753
754=== modified file 'src/endroid/plugins/blacklist.py'
755--- src/endroid/plugins/blacklist.py 2013-08-12 11:32:00 +0000
756+++ src/endroid/plugins/blacklist.py 2013-08-14 13:58:20 +0000
757@@ -35,15 +35,13 @@
758
759 self.task = self.cron.register(self.unblacklist, CRON_UNBLACKLIST)
760
761- self.register_muc_filter(self.checklist)
762- self.register_chat_filter(self.checklist)
763- self.register_chat_callback(self.command)
764- self.register_chat_send_filter(self.checksend)
765+ self.messages.register(self.checklist, recv_filter=True)
766+ self.messages.register(self.command, chat_only=True)
767+ self.messages.register(self.checksend, send_filter=True, chat_only=True)
768
769- self.db = Database(DB_NAME)
770- if not self.db.table_exists(DB_TABLE):
771- self.db.create_table(DB_TABLE, ("userjid",))
772- for row in self.db.fetch(DB_TABLE, ("userjid",)):
773+ if not self.database.table_exists(DB_TABLE):
774+ self.database.create_table(DB_TABLE, ("userjid",))
775+ for row in self.database.fetch(DB_TABLE, ("userjid",)):
776 self.blacklist(row["userjid"])
777
778 def get_blacklist(self):
779@@ -92,8 +90,8 @@
780 argument is passed, the user is removed after the specified number of
781 seconds.
782 """
783- self.db.delete(DB_TABLE, {"userjid": user})
784- self.db.insert(DB_TABLE, {"userjid": user})
785+ self.database.delete(DB_TABLE, {"userjid": user})
786+ self.database.insert(DB_TABLE, {"userjid": user})
787 self._blacklist.add(user)
788 if duration != 0:
789 self.task.setTimeout(duration, user)
790@@ -102,5 +100,5 @@
791 """
792 Remove the specified user from the blacklist.
793 """
794- self.db.delete(DB_TABLE, {"userjid": user})
795+ self.database.delete(DB_TABLE, {"userjid": user})
796 self._blacklist.remove(user)
797
798=== added file 'src/endroid/plugins/brainyquote.py'
799--- src/endroid/plugins/brainyquote.py 1970-01-01 00:00:00 +0000
800+++ src/endroid/plugins/brainyquote.py 2013-08-14 13:58:20 +0000
801@@ -0,0 +1,26 @@
802+# -----------------------------------------------------------------------------
803+# EnDroid - Brainy Quote of the moment plugin
804+# Copyright 2013, Ensoft Ltd
805+# -----------------------------------------------------------------------------
806+
807+import re
808+from HTMLParser import HTMLParser
809+from twisted.web.client import getPage
810+from endroid.plugins.command import CommandPlugin, command
811+
812+QURE = re.compile(r'<div class="bq_fq"[^>]*>\s*<p>(.*?)</p>.*?<a[^>]*>(.*?)</a>',
813+ re.S)
814+
815+class BrainyQuote(CommandPlugin):
816+ help = "Get the Quote of the Moment from brainyquote.com."
817+
818+ @command(synonyms=("brainy quote", "brainyquote"))
819+ def cmd_quote(self, msg, arg):
820+ def extract_quote(data):
821+ quote, author = map(str.strip, QURE.search(data).groups())
822+ hp = HTMLParser()
823+ msg.reply("Quote of the moment: {} -- {}".format(
824+ hp.unescape(quote), hp.unescape(author)))
825+
826+ getPage("http://www.brainyquote.com/").addCallbacks(extract_quote,
827+ msg.unhandled)
828
829=== modified file 'src/endroid/plugins/command.py'
830--- src/endroid/plugins/command.py 2013-08-12 15:17:39 +0000
831+++ src/endroid/plugins/command.py 2013-08-14 13:58:20 +0000
832@@ -127,19 +127,19 @@
833 def endroid_init(self):
834 self._muc_handlers = Handlers([], {})
835 self._chat_handlers = Handlers([], {})
836- self.register_muc_callback(self.command_muc)
837- self.register_chat_callback(self.command_chat)
838+ self.messages.register(self._command_muc, muc_only=True)
839+ self.messages.register(self._command_chat, chat_only=True)
840
841 self.help_topics = {
842- '': self.help_main,
843- 'chat': self.help_chat,
844- 'muc': self.help_muc,
845+ '': self._help_main,
846+ 'chat': self._help_chat,
847+ 'muc': self._help_muc,
848 }
849
850 # -------------------------------------------------------------------------
851 # Help methods
852
853- def help_add_regs(self, output, handlers):
854+ def _help_add_regs(self, output, handlers):
855 """
856 Add lines of help strings to the output list for each handler in the
857 given Handlers object. Then recurses down all subcommands to get their
858@@ -149,30 +149,30 @@
859 if not reg.hidden:
860 output.append(" %s %s" % (reg.command, reg.helphint))
861 for _, hdlrs in sorted(handlers.subcommands.items()):
862- self.help_add_regs(output, hdlrs)
863+ self._help_add_regs(output, hdlrs)
864
865- def help_main(self, topic):
866+ def _help_main(self, topic):
867 assert not topic
868 out = ["Commands known to me:"]
869- chat = self.help_chat(topic)
870+ chat = self._help_chat(topic)
871 if chat:
872 out.extend(["", chat])
873- muc = self.help_muc(topic)
874+ muc = self._help_muc(topic)
875 if muc:
876 out.extend(["", muc])
877 return "\n".join(out)
878
879- def help_chat(self, topic):
880+ def _help_chat(self, topic):
881 parts = []
882- self.help_add_regs(parts, self._chat_handlers)
883+ self._help_add_regs(parts, self._chat_handlers)
884 if parts:
885 return "\n".join(["Commands in Chat:"] + parts)
886 else:
887 return "No command registered in chat."
888
889- def help_muc(self, topic):
890+ def _help_muc(self, topic):
891 parts = []
892- self.help_add_regs(parts, self._muc_handlers)
893+ self._help_add_regs(parts, self._muc_handlers)
894 if parts:
895 return "\n".join(["Commands in MUC:"] + parts)
896 else:
897@@ -181,7 +181,7 @@
898 # -------------------------------------------------------------------------
899 # Command handling methods
900
901- def command(self, handlers, args, msg):
902+ def _command(self, handlers, args, msg):
903 """
904 Handle an incoming message using the given handlers; args is the
905 current remaining message string; msg is the full Message object.
906@@ -189,22 +189,22 @@
907 All handlers for the current command are called after first recursing
908 down to any subcommands that match.
909 """
910- com, arg = self.command_split(args)
911+ com, arg = self._command_split(args)
912 if com in handlers.subcommands:
913 msg.inc_handlers()
914- self.command(handlers.subcommands[com], arg, msg)
915+ self._command(handlers.subcommands[com], arg, msg)
916 for handler in handlers.handlers:
917 msg.inc_handlers()
918 handler.callback(msg, args)
919 msg.dec_handlers()
920
921- def command_muc(self, msg):
922- self.command(self._muc_handlers, msg.body, msg)
923+ def _command_muc(self, msg):
924+ self._command(self._muc_handlers, msg.body, msg)
925
926- def command_chat(self, msg):
927- self.command(self._chat_handlers, msg.body, msg)
928+ def _command_chat(self, msg):
929+ self._command(self._chat_handlers, msg.body, msg)
930
931- def command_split(self, text):
932+ def _command_split(self, text):
933 num = text.count(' ')
934 if num == 0:
935 return (text.lower(), '')
936@@ -216,7 +216,7 @@
937 # Registration methods
938
939 def _register_handler(self, callback, cmd, helphint, hidden, handlers,
940- synonyms=()):
941+ synonyms=()):
942 """
943 Register a new handler.
944
945@@ -241,13 +241,13 @@
946 synonyms=()):
947 """Register a new handler for MUC messages."""
948 self._register_handler(callback, command, helphint, hidden,
949- self._muc_handlers, synonyms)
950+ self._muc_handlers, synonyms)
951
952 def register_chat(self, callback, command, helphint="", hidden=False,
953 synonyms=()):
954 """Register a new handler for chat messages."""
955 self._register_handler(callback, command, helphint, hidden,
956- self._chat_handlers, synonyms)
957+ self._chat_handlers, synonyms)
958
959 def register_both(self, callback, command, helphint="", hidden=False,
960 synonyms=()):
961
962=== modified file 'src/endroid/plugins/coolit.py'
963--- src/endroid/plugins/coolit.py 2013-07-31 15:39:20 +0000
964+++ src/endroid/plugins/coolit.py 2013-08-14 13:58:20 +0000
965@@ -3,13 +3,12 @@
966 # Copyright 2012, Ensoft Ltd
967 # -----------------------------------------------------------------------------
968
969-from endroid.plugins.command import CommandPlugin
970+from endroid.plugins.command import CommandPlugin, command
971
972 class CoolIt(CommandPlugin):
973 help = "I'm a robot. I'm not a refrigerator."
974 hidden = True
975
976- def cmd_coolit(self, msg, args):
977+ @command(synonyms=('cool it', 'freeze'), hidden=True)
978+ def coolit(self, msg, args):
979 msg.reply(self.help)
980- cmd_coolit.hidden = True
981- cmd_coolit.synonyms = ('cool it',)
982
983=== modified file 'src/endroid/plugins/correct.py'
984--- src/endroid/plugins/correct.py 2013-08-13 15:17:45 +0000
985+++ src/endroid/plugins/correct.py 2013-08-14 13:58:20 +0000
986@@ -21,8 +21,7 @@
987
988 def endroid_init(self):
989 self.lastmsg = {}
990- self.register_chat_callback(self.heard)
991- self.register_muc_callback(self.heard)
992+ self.messages.register(self.heard)
993
994 def heard(self, msg):
995 """
996@@ -58,6 +57,6 @@
997 # This is unexpected. Probably a mistake on the user's part?
998 msg.unhandled()
999 else:
1000- sendernick = self.usermanagement.get_nickname(msg.sender, self.place_name)
1001+ sendernick = self.rosters.nickname(msg.sender_full)
1002 who = sendernick if self.place == "room" else "You"
1003 msg.reply("%s meant: %s" % (who, newstr))
1004
1005=== modified file 'src/endroid/plugins/help.py'
1006--- src/endroid/plugins/help.py 2013-07-30 11:25:48 +0000
1007+++ src/endroid/plugins/help.py 2013-08-14 13:58:20 +0000
1008@@ -18,11 +18,11 @@
1009 self.load_plugin_list()
1010
1011 def load_plugin_list(self):
1012- self.plugins = {}
1013+ self._plugins = {}
1014 for fullname in self.list_plugins():
1015 plugin = self.get(fullname)
1016 name = getattr(plugin, "name", fullname)
1017- self.plugins[name] = (fullname, plugin)
1018+ self._plugins[name] = (fullname, plugin)
1019
1020 def cmd_help(self, msg, args):
1021 msg.reply_to_sender(self.show_help_plugin("help", args))
1022@@ -43,7 +43,7 @@
1023 else:
1024 out = []
1025 out.append("Currently loaded plugins:")
1026- for name, (_, plug) in sorted(self.plugins.items()):
1027+ for name, (_, plug) in sorted(self._plugins.items()):
1028 if not getattr(plug, "hidden", False):
1029 out.append(" {0}".format(name))
1030 return "\n".join(out)
1031@@ -53,10 +53,10 @@
1032
1033 def show_help_plugin(self, name, topic=''):
1034 out = []
1035- fullname, plugin = self.plugins.get(name, (name, None))
1036+ fullname, plugin = self._plugins.get(name, (name, None))
1037 if self.pluginLoaded(fullname):
1038 # First check if it has a simple "help" property or method
1039- if plugin.hasattr("help"):
1040+ if hasattr(plugin, "help"):
1041 try:
1042 out.append(plugin.help(topic))
1043 except TypeError:
1044@@ -67,7 +67,7 @@
1045 else:
1046 out.append(str(plugin.help))
1047
1048- elif plugin.hasattr("help_topics"):
1049+ elif hasattr(plugin, "help_topics"):
1050 # Check if it is a "help_topics" dictionary, mapping topic
1051 # (first keyword) to handler function
1052 keywords = topic.strip().split(' ', 1)
1053
1054=== modified file 'src/endroid/plugins/ratelimit.py'
1055--- src/endroid/plugins/ratelimit.py 2013-08-14 10:01:49 +0000
1056+++ src/endroid/plugins/ratelimit.py 2013-08-14 13:58:20 +0000
1057@@ -10,7 +10,11 @@
1058 from collections import defaultdict, deque
1059
1060 from endroid.pluginmanager import Plugin
1061+<<<<<<< TREE
1062
1063+=======
1064+from endroid.messagehandler import Priority
1065+>>>>>>> MERGE-SOURCE
1066
1067 # Cron constants
1068 CRON_SENDAGAIN = "RateLimit_SendAgain"
1069@@ -144,8 +148,10 @@
1070 """
1071 name = "ratelimit"
1072 hidden = True
1073+ help = "Implements a Token Bucket rate limiter, per recipient JID"
1074+ preferences = ("endroid.plugins.blacklist",)
1075
1076- def enInit(self):
1077+ def endroid_init(self):
1078 """
1079 Initialise the plugin. Registers required Crons, and extracts
1080 configuration.
1081@@ -160,11 +166,8 @@
1082 self.abusecooloff = int(self.vars.get('abusecooloff', 3600))
1083 self.blacklist = self.get("endroid.plugins.blacklist")
1084
1085- self.register_muc_send_filter(self.ratelimit, priority=10)
1086- self.register_chat_send_filter(self.ratelimit, priority=10)
1087-
1088- self.register_muc_filter(self.checkabuse, priority=10)
1089- self.register_chat_filter(self.checkabuse, priority=10)
1090+ self.messages.register(self.ratelimit, priority=10, send_filter=True)
1091+ self.messages.register(self.checkabuse, priority=10, recv_filter=True)
1092
1093 # Make all the state attributes class attributes
1094 # This means that users are limited globally accross all usergroups and
1095@@ -181,14 +184,6 @@
1096
1097 self.task = self.cron.register(self.sendagain, CRON_SENDAGAIN)
1098
1099- def preferences(self):
1100- """Other plugins that we could use if they are loaded."""
1101- return ("endroid.plugins.blacklist",)
1102-
1103- def help(self):
1104- "Help string for the plugin"
1105- return "Implements a Token Bucket rate limiter, per recipient JID"
1106-
1107 def ratelimit(self, msg):
1108 """
1109 Send message filter. Rate limits based on the message recipient, using
1110@@ -203,6 +198,7 @@
1111 sc = self.limiters[msg.recipient]
1112
1113 # Don't ratelimit things we're sending ourselves, or URGENT messages
1114+<<<<<<< TREE
1115 if (msg.sender == self.name or
1116 msg.priority == self.messagehandler.PRIORITY_URGENT):
1117 accept = True
1118@@ -217,6 +213,12 @@
1119 logging.info("Ratelimiting msgs to {}".format(msg.recipient))
1120
1121 self.set_timeout(msg.recipient)
1122+=======
1123+ accept = (msg.sender is self or
1124+ msg.priority == Priority.URGENT or
1125+ sc.accept(msg, now))
1126+ self.set_timeout(msg.recipient, now)
1127+>>>>>>> MERGE-SOURCE
1128
1129 return accept
1130
1131
1132=== modified file 'src/endroid/plugins/speak.py'
1133--- src/endroid/plugins/speak.py 2013-08-06 10:32:33 +0000
1134+++ src/endroid/plugins/speak.py 2013-08-14 13:58:20 +0000
1135@@ -4,43 +4,32 @@
1136 # Created by Jonathan Millican
1137 # -----------------------------------------
1138
1139-from endroid.pluginmanager import Plugin
1140+from endroid.plugins.command import CommandPlugin
1141
1142-class Speak(Plugin):
1143+class Speak(CommandPlugin):
1144 name = "speak"
1145-
1146- def dependencies(self):
1147- return ['endroid.plugins.command']
1148-
1149- def enInit(self):
1150- com = self.get('endroid.plugins.command')
1151- com.register_chat(self.do_speak, 'speak', '<tojid> <message>')
1152- com.register_chat(self.do_many_speak, 'repeat', '<count> <tojid> <message>')
1153-
1154- def do_speak(self, msg, args):
1155- tojid, text = self.split(args)
1156- if tojid in self.usermanagement.users.available:
1157- self.messagehandler.send_chat(tojid, text, msg.sender)
1158+ help = ("Speak allows you to speak as EnDroid. Don't abuse it\n"
1159+ "Command syntax: speak <tojid> <message>")
1160+
1161+ def cmd_speak(self, msg, args):
1162+ tojid, text = self._split(args)
1163+ if tojid in self.rosters.available_users:
1164+ self.messages.send_chat(tojid, text, msg.sender)
1165 else:
1166 msg.reply("You can't send messages to that user. Sorry.")
1167+ cmd_speak.helphint = "<tojid> <message>"
1168
1169- def do_many_speak(self, msg, args):
1170+ def repeat(self, msg, args):
1171 count, tojid, text = args.split(' ', 2)
1172- if tojid in self.usermanagement.users.available:
1173+ if tojid in self.rosters.available_users:
1174 for i in range(int(count)):
1175- self.messagehandler.send_chat(tojid, text, msg.sender)
1176+ self.messages.send_chat(tojid, text, msg.sender)
1177 else:
1178 msg.reply("You can't send messages to that user. Sorry.")
1179+ repeat.helphint = "<count> <tojid> <message>"
1180
1181- def split(self, message):
1182+ def _split(self, message):
1183 if message.count(' ') == 0:
1184 return (message, '')
1185 else:
1186 return message.split(' ', 1)
1187-
1188- def help(self):
1189- return ("Speak allows you to speak as EnDroid. Don't abuse it\n"
1190- "Command syntax: speak <tojid> <message>")
1191-
1192-def get_plugin():
1193- return Speak()
1194
1195=== modified file 'src/endroid/plugins/spell.py'
1196--- src/endroid/plugins/spell.py 2013-07-30 11:25:48 +0000
1197+++ src/endroid/plugins/spell.py 2013-08-14 13:58:20 +0000
1198@@ -24,17 +24,13 @@
1199 words marked with a '(sp?)' after them.
1200 """
1201 name = "spell"
1202+ dependencies = ("endroid.plugins.patternmatcher",)
1203+ help = "Check spelling of words by typing '(sp?)' after them."
1204
1205- def enInit(self):
1206+ def endroid_init(self):
1207 pat = self.get("endroid.plugins.patternmatcher")
1208 pat.register_both(self.heard, REOBJ)
1209
1210- def dependencies(self):
1211- return ('endroid.plugins.patternmatcher',)
1212-
1213- def help(self):
1214- return "Check spelling of words by typing '(sp?)' after them."
1215-
1216 def heard(self, msg):
1217 """
1218 Checks spelling of all the matches.
1219
1220=== added file 'src/endroid/plugins/trains.py'
1221--- src/endroid/plugins/trains.py 1970-01-01 00:00:00 +0000
1222+++ src/endroid/plugins/trains.py 2013-08-14 13:58:20 +0000
1223@@ -0,0 +1,120 @@
1224+# -----------------------------------------
1225+# Endroid - Trains Live Departures
1226+# Copyright 2013, Ensoft Ltd.
1227+# Created by Martin Morrison
1228+# -----------------------------------------
1229+
1230+import re
1231+import urllib
1232+from HTMLParser import HTMLParser
1233+from twisted.web.client import getPage
1234+
1235+from endroid.plugins.command import CommandPlugin, command
1236+
1237+TIMERE_STR = r'(\d+)(?::(\d+))? *(am|pm)?'
1238+ULRE = re.compile(r'<ul class="results">(.*)', re.S)
1239+CMDRE = re.compile(r"((?:from (.*) )?to (.*?)|home)(?:(?: (arriving|leaving))?(?: (tomorrow|(?:next )?\w*day))?(?: at ({}))?)?$".format(TIMERE_STR))
1240+RESULTRE = re.compile(r"<strong> *(.*?) *</strong>")
1241+TIMERE = re.compile(TIMERE_STR)
1242+
1243+STATION_TABLE = "Stations"
1244+HOME_TABLE = "Home"
1245+
1246+class TrainTimes(CommandPlugin):
1247+ name = "traintimes"
1248+ help_topics = {
1249+ "": "When do trains leave?",
1250+ }
1251+
1252+ def endroid_init(self):
1253+ if not self.database.table_exists(STATION_TABLE):
1254+ self.database.create_table(STATION_TABLE, ("jid", "station"))
1255+ if not self.database.table_exists(HOME_TABLE):
1256+ self.database.create_table(HOME_TABLE, ("jid", "station"))
1257+
1258+ def _station_update(self, msg, args, table, jid, display):
1259+ if not args:
1260+ rows = self.database.fetch(table, ("station",),
1261+ {"jid": jid})
1262+ if rows:
1263+ msg.reply_to_sender("Your {} station is set to: {}"
1264+ .format(display, rows[0]['station']))
1265+ else:
1266+ msg.reply_to_sender("You don't have a {} station set."
1267+ .format(display))
1268+ return
1269+ self.database.delete(table, {"jid": msg.sender})
1270+ if args != "delete":
1271+ self.database.insert(table, {"jid": jid, "station": args})
1272+ msg.reply_to_sender("Your new {} station is: {}"
1273+ .format(display, args))
1274+ else:
1275+ msg.reply_to_sender("{} station deleted."
1276+ .format(display.capitalize()))
1277+
1278+ @command(helphint="{<station name>|delete}")
1279+ def nearest_station(self, msg, args):
1280+ self._station_update(msg, args, STATION_TABLE, msg.sender_full,
1281+ "nearest")
1282+
1283+ @command(helphint="{<station name>|delete}")
1284+ def home_station(self, msg, args):
1285+ self._station_update(msg, args, HOME_TABLE, msg.sender, "home")
1286+
1287+ @command(helphint="from <stn> to <stn> [[arriving|leaving] at <time>]",
1288+ synonyms=("next train",))
1289+ def train(self, msg, args):
1290+ match = CMDRE.match(args)
1291+ if not match:
1292+ msg.reply("Brain the size of a planet, but I can't parse that request")
1293+ return
1294+
1295+ def extract_results(data):
1296+ results = RESULTRE.findall(ULRE.search(data).group(1))
1297+ if results:
1298+ msg.reply(u"Trains from {} to {}: {}"
1299+ .format(src, dst,
1300+ HTMLParser().unescape(u", ".join(results))))
1301+ else:
1302+ msg.reply("Either your request is malformed, or there are no matching trains")
1303+
1304+ home, src, dst, typ, when, time, _,_,_ = match.groups()
1305+ time = self._canonical_time(time) if time is not None else time
1306+ if dst == "home" or home == "home":
1307+ rows = self.database.fetch(HOME_TABLE, ("station",),
1308+ {"jid": msg.sender})
1309+ if rows:
1310+ dst = rows[0]['station']
1311+ else:
1312+ msg.reply("You must save a home station with the 'home station'"
1313+ " command")
1314+ return
1315+ if src is None:
1316+ rows = self.database.fetch(STATION_TABLE, ("station",),
1317+ {"jid": msg.sender_full})
1318+ if rows:
1319+ src = rows[0]['station']
1320+ else:
1321+ msg.reply("You must either specify a source station, or save "
1322+ "a nearest station (with the 'nearest station' "
1323+ "command)")
1324+ return
1325+ url = "/{}/{}{}{}{}".format(
1326+ src, dst, ("/" + time) if time else "",
1327+ "a" if typ == "arriving" else "",
1328+ ("/" + when.replace(" ", "-")) if when else "")
1329+ getPage("http://www.traintimes.org.uk" + urllib.quote(url)
1330+ ).addCallbacks(extract_results, msg.unhandled)
1331+
1332+ @staticmethod
1333+ def _canonical_time(time):
1334+ match = TIMERE.match(time.strip())
1335+ assert match, "We've already checked this - how can it fail?!"
1336+ hour, minute, half = match.groups()
1337+ hour, minute = map(lambda n: int(n) if n else 0, (hour, minute))
1338+ if half and half == 'pm':
1339+ if hour <= 12:
1340+ hour += 12
1341+ if not minute:
1342+ minute = 0
1343+ return "{}:{:02}".format(hour, minute)
1344
1345=== modified file 'src/endroid/plugins/unhandled.py'
1346--- src/endroid/plugins/unhandled.py 2012-11-29 20:30:39 +0000
1347+++ src/endroid/plugins/unhandled.py 2013-08-14 13:58:20 +0000
1348@@ -12,7 +12,7 @@
1349 help = "I'm a personality prototype. You can tell, can't you...?"
1350 hidden = True
1351
1352- messages = [
1353+ _messages = [
1354 "404 Error: message not found",
1355 "Command not found, perhaps it can be found in the bottom of a locked "
1356 "filing cabinet stuck in a disused lavatory with a sign on the door "
1357@@ -39,7 +39,7 @@
1358 self.register_unhandled_muc_callback(self.unhandled)
1359
1360 def unhandled(self, msg):
1361- messages = self.messages
1362+ messages = self._messages
1363 if date.weekday(date.today()) == 3:
1364 messages = messages[:] + ["This must be a Thursday, I could never "
1365 "get the hang of Thursdays"]
1366
1367=== modified file 'src/endroid/usermanagement.py'
1368--- src/endroid/usermanagement.py 2013-08-14 10:00:18 +0000
1369+++ src/endroid/usermanagement.py 2013-08-14 13:58:20 +0000
1370@@ -9,6 +9,7 @@
1371 from twisted.words.protocols.jabber.error import StanzaError
1372 from endroid.pluginmanager import PluginManager
1373 from random import choice
1374+from collections import namedtuple
1375
1376 # we use ADJECTIVES to generate a random new nick if endroid's is taken
1377 ADJECTIVES = [
1378@@ -21,6 +22,8 @@
1379
1380 MUC = "muc#roomconfig_"
1381
1382+Place = namedtuple("Place", ("type", "name"))
1383+
1384 class Roster(object):
1385 """
1386 Provides functions for maintaining sets of users registered with and
1387@@ -187,8 +190,6 @@
1388 self.rh.set_presence_handler(self)
1389 self.wh.set_presence_handler(self)
1390
1391-
1392-
1393 def register_callback(self):
1394 # register functions to be called when we receive an 'online' or
1395 # 'offline' notifications
1396@@ -201,9 +202,14 @@
1397
1398 # given a group or room or None (our contact list), return list of users
1399 # registered/available there
1400+<<<<<<< TREE
1401 def get_users(self, name=None):
1402 """
1403 Return an iterable of users registered with 'name'.
1404+=======
1405+ def users(self, name=None):
1406+ """Return a set of users registered with 'name'.
1407+>>>>>>> MERGE-SOURCE
1408
1409 If name is None, look in contact list.
1410
1411@@ -214,10 +220,16 @@
1412 return self.group_rosters[name].registered
1413 elif name in self.room_rosters:
1414 return self.room_rosters[name].registered
1415+ get_users = users
1416
1417+<<<<<<< TREE
1418 def get_available_users(self, name=None):
1419 """
1420 Return an iterable of users present in 'name'.
1421+=======
1422+ def available_users(self, name=None):
1423+ """Return a set of users present in 'name'.
1424+>>>>>>> MERGE-SOURCE
1425
1426 If name is None, look in contact list.
1427
1428@@ -228,44 +240,69 @@
1429 return self.group_rosters[name].available
1430 elif name in self.room_rosters:
1431 return self.room_rosters[name].available
1432+ get_available_users = available_users
1433
1434 # given a user or None (us), return list of groups/rooms the user is
1435 # registered/available in
1436+<<<<<<< TREE
1437 def get_groups(self, user=None):
1438 """
1439 Return an iterable of groups 'user' is registered with.
1440+=======
1441+ def groups(self, user=None):
1442+ """Return a set of groups 'user' is registered with.
1443+>>>>>>> MERGE-SOURCE
1444
1445 If user is None, return all registered groups.
1446
1447 """
1448 return self._get_user_place(user, self.group_rosters, get_available=False)
1449+ get_groups = groups
1450
1451+<<<<<<< TREE
1452 def get_available_groups(self, user=None):
1453 """
1454 Return an iterable of groups 'user' is present in.
1455+=======
1456+ def available_groups(self, user=None):
1457+ """Return a set of groups 'user' is present in.
1458+>>>>>>> MERGE-SOURCE
1459
1460 If user is None, return all groups EnDroid is available in.
1461
1462 """
1463 return self._get_user_place(user, self.group_rosters, get_available=True)
1464+ get_available_groups = available_groups
1465
1466+<<<<<<< TREE
1467 def get_rooms(self, user=None):
1468 """
1469 Return an iterable of rooms 'user' is registered with.
1470+=======
1471+ def rooms(self, user=None):
1472+ """Return a set of rooms 'user' is registered with.
1473+>>>>>>> MERGE-SOURCE
1474
1475 If user is None, return all registered rooms.
1476
1477 """
1478 return self._get_user_place(user, self.room_rosters, get_available=False)
1479+ get_rooms = rooms
1480
1481+<<<<<<< TREE
1482 def get_available_rooms(self, user=None):
1483 """
1484 Return an iterable of rooms 'user' is present in.
1485+=======
1486+ def available_rooms(self, user=None):
1487+ """Return a set of rooms 'user' is present in.
1488+>>>>>>> MERGE-SOURCE
1489
1490 If user is None, return all rooms EnDroid is available in.
1491
1492 """
1493 return self._get_user_place(user, self.room_rosters, get_available=True)
1494+ get_available_rooms = available_rooms
1495
1496 def _get_user_place(self, user, dct, get_available):
1497 """
1498@@ -282,7 +319,7 @@
1499 else:
1500 return []
1501
1502- def get_nickname(self, user, place=None):
1503+ def nickname(self, user, place=None):
1504 """
1505 Given a user jid (user@ho.s.t) return the user's nickname in place,
1506 or if place is None (default), the user part of the jid.
1507@@ -295,8 +332,8 @@
1508 for (nick, rosteritem) in self.wh._rooms[JID(place)].roster.items():
1509 if user == rosteritem.entity.userhost():
1510 return nick
1511-
1512 return "unknown"
1513+ get_nickname = nickname
1514
1515 ### Functions for managing contact lists ###
1516
1517@@ -456,6 +493,12 @@
1518 self._pms[name] = PluginManager(self.wh.messagehandler, self, place,
1519 name, self.conf)
1520
1521+ def connected(self):
1522+ self._pms[None] = PluginManager(self.wh.messagehandler, self, "global",
1523+ None, self.conf)
1524+ # Should join all rooms and groups here
1525+ # Currently called from elsewhere
1526+
1527 def join_all_rooms(self):
1528 for room in self.get_rooms():
1529 d = self.join_room(room)
1530@@ -525,7 +568,7 @@
1531 else:
1532 reason = "User not registered in room"
1533 return (False, reason)
1534- elif room in self.get_available_rooms(user):
1535+ elif room in self.available_rooms(user):
1536 return (False, "User already in room")
1537 else:
1538 for full_jid in self.users.get_resources(user):
1539@@ -548,3 +591,32 @@
1540 except (RuntimeError, InvalidFormat, AttributeError):
1541 return item
1542 # if item is not a string or None, an AttributeError will be raised
1543+
1544+ def for_plugin(self, pluginmanager, plugin):
1545+ return PluginUserManagement(self, pluginmanager, plugin)
1546+
1547+class PluginUserManagement(object):
1548+ """
1549+ One of these exists per plugin, provide the API to handle rosters.
1550+ """
1551+ def __init__(self, usermanagement, pluginmanager, plugin):
1552+ self._usermanagement = usermanagement
1553+ self._pluginmanager = pluginmanager
1554+ self._plugin = plugin
1555+
1556+ @property
1557+ def users(self):
1558+ return self._usermanagement.users(self._pluginmanager.name)
1559+
1560+ @property
1561+ def available_users(self):
1562+ return self._usermanagement.available_users(self._pluginmanager.name)
1563+
1564+ def nickname(self, user):
1565+ return self._usermanagement.nickname(user, self._pluginmanager.name)
1566+
1567+ def invite(self, user, reason=None):
1568+ if self._pluginmanager.place != "room":
1569+ raise ValueError("Must be in a room to invite users")
1570+ return self._usermanagement.invite(user, self._pluginmanager.name,
1571+ reason=reason)
1572
1573=== modified file 'src/endroid/wokkelhandler.py'
1574--- src/endroid/wokkelhandler.py 2013-08-14 10:00:18 +0000
1575+++ src/endroid/wokkelhandler.py 2013-08-14 13:58:20 +0000
1576@@ -80,8 +80,11 @@
1577 sender = JID(message.attributes['from']).full()
1578 recipient = JID(message.attributes['to']).full()
1579 for i in message.children:
1580+ # Note: in theory multiple bodies are allowed; in practice,
1581+ # this isn't seen, so just get the first one.
1582 if getattr(i, "name", "") == 'body':
1583 body = i.children[0]
1584+ break
1585
1586 if not (body is None or sender is None or recipient is None):
1587 m = Message("chat", sender, body,

Subscribers

People subscribed via source and target branches