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

Proposed by Martin Morrison
Status: Merged
Approved by: Martin Morrison
Approved revision: no longer in the source branch.
Merged at revision: 41
Proposed branch: lp:~isoschiz/endroid/globalplugins
Merge into: lp:endroid
Diff against target: 1618 lines (+608/-268)
17 files modified
src/endroid/__init__.py (+26/-18)
src/endroid/database.py (+3/-2)
src/endroid/messagehandler.py (+123/-50)
src/endroid/pluginmanager.py (+182/-87)
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 (+7/-16)
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 (+53/-11)
src/endroid/wokkelhandler.py (+3/-0)
To merge this branch: bzr merge lp:~isoschiz/endroid/globalplugins
Reviewer Review Type Date Requested Status
Martin Morrison Approve
Ben Hutchings Pending
Review via email: mp+180216@code.launchpad.net

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

Commit message

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.

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 : Posted in a previous version of this proposal

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
Revision history for this message
Martin Morrison (isoschiz) wrote :

Ben Approved this before, so approving in proxy.

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.

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

Subscribers

People subscribed via source and target branches