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