Merge lp:~isoschiz/endroid/globalplugins into lp:endroid
- globalplugins
- Merge into trunk
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 |
Related bugs: |
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.
Ben Hutchings (ben-hutchings) wrote : Posted in a previous version of this proposal | # |
Martin Morrison (isoschiz) wrote : | # |
Ben Approved this before, so approving in proxy.
- 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
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, |
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: