Merge lp:~verterok/lalita/zmq-proxy into lp:lalita
- zmq-proxy
- Merge into trunk
Proposed by
Guillermo Gonzalez
Status: | Merged |
---|---|
Approved by: | Facundo Batista |
Approved revision: | 179 |
Merged at revision: | 173 |
Proposed branch: | lp:~verterok/lalita/zmq-proxy |
Merge into: | lp:lalita |
Diff against target: |
665 lines (+497/-9) 11 files modified
docs/tutorial_en.rst (+5/-0) docs/tutorial_sp.rst (+4/-0) lalita.cfg.sample (+4/-0) lalita/core/tests/test_dispatcher.py (+3/-2) lalita/core/tests/test_events.py (+2/-1) lalita/core/tests/test_ircbot.py (+3/-3) lalita/core/tests/test_metacommands.py (+2/-1) lalita/plugins/tests/helper.py (+8/-2) lalita/plugins/tests/test_zmq_proxy.py (+204/-0) lalita/plugins/zmq_plugins/example.py (+52/-0) lalita/plugins/zmq_proxy.py (+210/-0) |
To merge this branch: | bzr merge lp:~verterok/lalita/zmq-proxy |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Facundo Batista | Approve | ||
Review via email: mp+113858@code.launchpad.net |
Commit message
ZeroMQ proxy plugin, converts lalita into a irc <-> ZeroMQ bridge (to create plugins in external processes)
Description of the change
Add a plugin that converts lalita into a irc <-> ZeroMQ bridge, this allow to create plugins in external processes.
To post a comment you must log in.
lp:~verterok/lalita/zmq-proxy
updated
- 178. By Guillermo Gonzalez
-
add info about zmq-proxy plugin in the docs.
Revision history for this message
Guillermo Gonzalez (verterok) wrote : | # |
lp:~verterok/lalita/zmq-proxy
updated
- 179. By Guillermo Gonzalez
-
fix typo
Revision history for this message
Facundo Batista (facundo) wrote : | # |
Awesome, thanks!
review:
Approve
lp:~verterok/lalita/zmq-proxy
updated
- 180. By Guillermo Gonzalez
-
add copyright notice
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'docs/tutorial_en.rst' | |||
2 | --- docs/tutorial_en.rst 2011-04-11 02:06:18 +0000 | |||
3 | +++ docs/tutorial_en.rst 2012-07-09 13:48:35 +0000 | |||
4 | @@ -633,6 +633,11 @@ | |||
5 | 633 | - url.py: Recollects all the URLs that are mentioned on the different channels, | 633 | - url.py: Recollects all the URLs that are mentioned on the different channels, |
6 | 634 | and you can search them afterwards. | 634 | and you can search them afterwards. |
7 | 635 | 635 | ||
8 | 636 | - zmq_proxy.py: It's a IRC-ZeroMQ <http://www.zeromq.org/> proxy/bridge, that | ||
9 | 637 | publish all lalita events to a PUB/SUB ZeroMQ socket and listen for commands | ||
10 | 638 | in other socket (in json format). See zmq_plugins/example.py for an example | ||
11 | 639 | plugin using this. | ||
12 | 640 | |||
13 | 636 | 641 | ||
14 | 637 | Advanced configuration | 642 | Advanced configuration |
15 | 638 | ====================== | 643 | ====================== |
16 | 639 | 644 | ||
17 | === modified file 'docs/tutorial_sp.rst' | |||
18 | --- docs/tutorial_sp.rst 2011-04-11 02:06:18 +0000 | |||
19 | +++ docs/tutorial_sp.rst 2012-07-09 13:48:35 +0000 | |||
20 | @@ -670,6 +670,10 @@ | |||
21 | 670 | - url.py: Va juntando todas las URLs que se van mencionando en los | 670 | - url.py: Va juntando todas las URLs que se van mencionando en los |
22 | 671 | distintos canales, y luego nos permite buscar en las mismas. | 671 | distintos canales, y luego nos permite buscar en las mismas. |
23 | 672 | 672 | ||
24 | 673 | - zmq_proxy.py: Es un proxy/bridge entre IRC y ZeroMQ <http://www.zeromq.org/> | ||
25 | 674 | que publica todos los eventos de lalita en un socket pub/sub de ZeroMQ y | ||
26 | 675 | escucha 'comandos' en otro socket (en formato json). | ||
27 | 676 | Ver zmq_plugins/example.py para un ejemplo de plugin en otro proceso. | ||
28 | 673 | 677 | ||
29 | 674 | ConfiguraciĆ³n avanzada | 678 | ConfiguraciĆ³n avanzada |
30 | 675 | ====================== | 679 | ====================== |
31 | 676 | 680 | ||
32 | === modified file 'lalita.cfg.sample' | |||
33 | --- lalita.cfg.sample 2011-04-11 02:05:31 +0000 | |||
34 | +++ lalita.cfg.sample 2012-07-09 13:48:35 +0000 | |||
35 | @@ -55,6 +55,10 @@ | |||
36 | 55 | # 'url.Url': { }, | 55 | # 'url.Url': { }, |
37 | 56 | 'lalita.plugins.photo.Photo': { }, | 56 | 'lalita.plugins.photo.Photo': { }, |
38 | 57 | 'lalita.plugins.misc.Ping': { }, | 57 | 'lalita.plugins.misc.Ping': { }, |
39 | 58 | 'zmq_proxy.ZMQPlugin':{ | ||
40 | 59 | 'events_address':'tcp://127.0.0.1:9090', | ||
41 | 60 | 'bot_address':'tcp://127.0.0.1:9091', | ||
42 | 61 | }, | ||
43 | 58 | }, | 62 | }, |
44 | 59 | ), | 63 | ), |
45 | 60 | 'example': dict ( | 64 | 'example': dict ( |
46 | 61 | 65 | ||
47 | === modified file 'lalita/core/tests/test_dispatcher.py' | |||
48 | --- lalita/core/tests/test_dispatcher.py 2010-06-22 00:12:42 +0000 | |||
49 | +++ lalita/core/tests/test_dispatcher.py 2012-07-09 13:48:35 +0000 | |||
50 | @@ -2,6 +2,7 @@ | |||
51 | 2 | # License: GPL v3 | 2 | # License: GPL v3 |
52 | 3 | # For further info, see LICENSE file | 3 | # For further info, see LICENSE file |
53 | 4 | 4 | ||
54 | 5 | import logging | ||
55 | 5 | import re | 6 | import re |
56 | 6 | 7 | ||
57 | 7 | from collections import defaultdict | 8 | from collections import defaultdict |
58 | @@ -26,7 +27,7 @@ | |||
59 | 26 | ) | 27 | ) |
60 | 27 | 28 | ||
61 | 28 | ircbot_factory = ircbot.IRCBotFactory(server) | 29 | ircbot_factory = ircbot.IRCBotFactory(server) |
63 | 29 | ircbot.logger.setLevel("error") | 30 | ircbot.logger.setLevel(logging.ERROR) |
64 | 30 | self.bot = ircbot.IrcBot() | 31 | self.bot = ircbot.IrcBot() |
65 | 31 | self.bot.factory = ircbot_factory | 32 | self.bot.factory = ircbot_factory |
66 | 32 | self.bot.config = ircbot_factory.config | 33 | self.bot.config = ircbot_factory.config |
67 | @@ -213,7 +214,7 @@ | |||
68 | 213 | if name == name.upper()] | 214 | if name == name.upper()] |
69 | 214 | for event in supported_events: | 215 | for event in supported_events: |
70 | 215 | self.disp.register(event, self.helper.f) | 216 | self.disp.register(event, self.helper.f) |
72 | 216 | if (event in dispatcher.USER_POS and | 217 | if (event in dispatcher.USER_POS and |
73 | 217 | dispatcher.USER_POS[event] is not None): | 218 | dispatcher.USER_POS[event] is not None): |
74 | 218 | self.disp.push(event, 'user', 'channel', 'msg') | 219 | self.disp.push(event, 'user', 'channel', 'msg') |
75 | 219 | elif (event in dispatcher.CHANNEL_POS and | 220 | elif (event in dispatcher.CHANNEL_POS and |
76 | 220 | 221 | ||
77 | === modified file 'lalita/core/tests/test_events.py' | |||
78 | --- lalita/core/tests/test_events.py 2010-06-20 16:34:35 +0000 | |||
79 | +++ lalita/core/tests/test_events.py 2012-07-09 13:48:35 +0000 | |||
80 | @@ -2,6 +2,7 @@ | |||
81 | 2 | # License: GPL v3 | 2 | # License: GPL v3 |
82 | 3 | # For further info, see LICENSE file | 3 | # For further info, see LICENSE file |
83 | 4 | 4 | ||
84 | 5 | import logging | ||
85 | 5 | 6 | ||
86 | 6 | from twisted.trial.unittest import TestCase as TwistedTestCase | 7 | from twisted.trial.unittest import TestCase as TwistedTestCase |
87 | 7 | from twisted.internet import defer | 8 | from twisted.internet import defer |
88 | @@ -21,7 +22,7 @@ | |||
89 | 21 | def write(*a): pass | 22 | def write(*a): pass |
90 | 22 | 23 | ||
91 | 23 | ircbot_factory = ircbot.IRCBotFactory(server) | 24 | ircbot_factory = ircbot.IRCBotFactory(server) |
93 | 24 | ircbot.logger.setLevel("error") | 25 | ircbot.logger.setLevel(logging.ERROR) |
94 | 25 | bot = ircbot.IrcBot() | 26 | bot = ircbot.IrcBot() |
95 | 26 | bot.factory = ircbot_factory | 27 | bot.factory = ircbot_factory |
96 | 27 | bot.msg = lambda *a:None | 28 | bot.msg = lambda *a:None |
97 | 28 | 29 | ||
98 | === modified file 'lalita/core/tests/test_ircbot.py' | |||
99 | --- lalita/core/tests/test_ircbot.py 2010-06-16 11:45:13 +0000 | |||
100 | +++ lalita/core/tests/test_ircbot.py 2012-07-09 13:48:35 +0000 | |||
101 | @@ -47,7 +47,7 @@ | |||
102 | 47 | def setUp(self): | 47 | def setUp(self): |
103 | 48 | server["log_config"] = {} | 48 | server["log_config"] = {} |
104 | 49 | ircbot_factory = ircbot.IRCBotFactory(server) | 49 | ircbot_factory = ircbot.IRCBotFactory(server) |
106 | 50 | ircbot.logger.setLevel("error") | 50 | ircbot.logger.setLevel(logging.ERROR) |
107 | 51 | self.bot = bot = ircbot.IrcBot() | 51 | self.bot = bot = ircbot.IrcBot() |
108 | 52 | bot.factory = ircbot_factory | 52 | bot.factory = ircbot_factory |
109 | 53 | bot.config = ircbot_factory.config | 53 | bot.config = ircbot_factory.config |
110 | @@ -129,7 +129,7 @@ | |||
111 | 129 | 129 | ||
112 | 130 | def setUp(self): | 130 | def setUp(self): |
113 | 131 | ircbot_factory = ircbot.IRCBotFactory(server) | 131 | ircbot_factory = ircbot.IRCBotFactory(server) |
115 | 132 | ircbot.logger.setLevel("error") | 132 | ircbot.logger.setLevel(logging.ERROR) |
116 | 133 | self.bot = bot = ircbot.IrcBot() | 133 | self.bot = bot = ircbot.IrcBot() |
117 | 134 | bot.factory = ircbot_factory | 134 | bot.factory = ircbot_factory |
118 | 135 | bot.config = ircbot_factory.config | 135 | bot.config = ircbot_factory.config |
119 | @@ -168,7 +168,7 @@ | |||
120 | 168 | 168 | ||
121 | 169 | def setUp(self): | 169 | def setUp(self): |
122 | 170 | ircbot_factory = ircbot.IRCBotFactory(server.copy()) | 170 | ircbot_factory = ircbot.IRCBotFactory(server.copy()) |
124 | 171 | ircbot.logger.setLevel("error") | 171 | ircbot.logger.setLevel(logging.ERROR) |
125 | 172 | self.bot = bot = ircbot.IrcBot() | 172 | self.bot = bot = ircbot.IrcBot() |
126 | 173 | bot.factory = ircbot_factory | 173 | bot.factory = ircbot_factory |
127 | 174 | bot.config = ircbot_factory.config | 174 | bot.config = ircbot_factory.config |
128 | 175 | 175 | ||
129 | === modified file 'lalita/core/tests/test_metacommands.py' | |||
130 | --- lalita/core/tests/test_metacommands.py 2010-01-17 19:03:28 +0000 | |||
131 | +++ lalita/core/tests/test_metacommands.py 2012-07-09 13:48:35 +0000 | |||
132 | @@ -4,6 +4,7 @@ | |||
133 | 4 | # License: GPL v3 | 4 | # License: GPL v3 |
134 | 5 | # For further info, see LICENSE file | 5 | # For further info, see LICENSE file |
135 | 6 | 6 | ||
136 | 7 | import logging | ||
137 | 7 | import unittest | 8 | import unittest |
138 | 8 | 9 | ||
139 | 9 | from collections import defaultdict | 10 | from collections import defaultdict |
140 | @@ -11,7 +12,7 @@ | |||
141 | 11 | from lalita import dispatcher, events, ircbot | 12 | from lalita import dispatcher, events, ircbot |
142 | 12 | 13 | ||
143 | 13 | ircbot_factory = ircbot.IRCBotFactory(dict(log_config="error", channels=defaultdict(lambda: {}))) | 14 | ircbot_factory = ircbot.IRCBotFactory(dict(log_config="error", channels=defaultdict(lambda: {}))) |
145 | 14 | ircbot.logger.setLevel("error") | 15 | ircbot.logger.setLevel(logging.ERROR) |
146 | 15 | bot = ircbot.IrcBot() | 16 | bot = ircbot.IrcBot() |
147 | 16 | bot.factory = ircbot_factory | 17 | bot.factory = ircbot_factory |
148 | 17 | bot.config = ircbot_factory.config | 18 | bot.config = ircbot_factory.config |
149 | 18 | 19 | ||
150 | === modified file 'lalita/plugins/tests/helper.py' | |||
151 | --- lalita/plugins/tests/helper.py 2010-03-08 13:52:37 +0000 | |||
152 | +++ lalita/plugins/tests/helper.py 2012-07-09 13:48:35 +0000 | |||
153 | @@ -2,6 +2,7 @@ | |||
154 | 2 | # License: GPL v3 | 2 | # License: GPL v3 |
155 | 3 | # For further info, see LICENSE file | 3 | # For further info, see LICENSE file |
156 | 4 | 4 | ||
157 | 5 | import logging | ||
158 | 5 | import unittest | 6 | import unittest |
159 | 6 | 7 | ||
160 | 7 | from lalita import ircbot | 8 | from lalita import ircbot |
161 | @@ -27,8 +28,8 @@ | |||
162 | 27 | (plugin_name, config) = server_plugin | 28 | (plugin_name, config) = server_plugin |
163 | 28 | self.test_server["plugins"] = { plugin_name: config } | 29 | self.test_server["plugins"] = { plugin_name: config } |
164 | 29 | 30 | ||
167 | 30 | self.test_server["log_config"] = { plugin_name: "error" } | 31 | self.test_server["log_config"] = { plugin_name: "ERROR" } |
168 | 31 | ircbot.logger.setLevel("error") | 32 | ircbot.logger.setLevel(logging.ERROR) |
169 | 32 | ircbot_factory = ircbot.IRCBotFactory(self.test_server) | 33 | ircbot_factory = ircbot.IRCBotFactory(self.test_server) |
170 | 33 | self.bot = ircbot.IrcBot() | 34 | self.bot = ircbot.IrcBot() |
171 | 34 | self.bot.factory = ircbot_factory | 35 | self.bot.factory = ircbot_factory |
172 | @@ -61,6 +62,11 @@ | |||
173 | 61 | else: | 62 | else: |
174 | 62 | raise ValueError("The searched plugin does not exist!") | 63 | raise ValueError("The searched plugin does not exist!") |
175 | 63 | 64 | ||
176 | 65 | def tearDown(self): | ||
177 | 66 | if hasattr(self, 'bot') and hasattr(self.bot, 'dispatcher'): | ||
178 | 67 | self.bot.dispatcher.shutdown() | ||
179 | 68 | |||
180 | 69 | |||
181 | 64 | def assertMessageInAnswer(self, message_idx, expected): | 70 | def assertMessageInAnswer(self, message_idx, expected): |
182 | 65 | """assert the content of message with index: message_idx in self.answer | 71 | """assert the content of message with index: message_idx in self.answer |
183 | 66 | and handle unexpected errors properly.""" | 72 | and handle unexpected errors properly.""" |
184 | 67 | 73 | ||
185 | === added file 'lalita/plugins/tests/test_zmq_proxy.py' | |||
186 | --- lalita/plugins/tests/test_zmq_proxy.py 1970-01-01 00:00:00 +0000 | |||
187 | +++ lalita/plugins/tests/test_zmq_proxy.py 2012-07-09 13:48:35 +0000 | |||
188 | @@ -0,0 +1,204 @@ | |||
189 | 1 | # -*- coding: utf8 -*- | ||
190 | 2 | |||
191 | 3 | # Copyright 2010 laliputienses | ||
192 | 4 | # License: GPL v3 | ||
193 | 5 | # For further info, see LICENSE file | ||
194 | 6 | import json | ||
195 | 7 | import time | ||
196 | 8 | |||
197 | 9 | from twisted.trial.unittest import TestCase as TwistedTestCase | ||
198 | 10 | from twisted.internet import defer, reactor, error | ||
199 | 11 | |||
200 | 12 | from lalita import events | ||
201 | 13 | from lalita.core.dispatcher import USER_POS, CHANNEL_POS | ||
202 | 14 | |||
203 | 15 | from .helper import PluginTest | ||
204 | 16 | from lalita.plugins.zmq_proxy import EVENT_MAP, PluginProcess | ||
205 | 17 | |||
206 | 18 | try: | ||
207 | 19 | import zmq, txzmq | ||
208 | 20 | zmq_available = True | ||
209 | 21 | except ImportError: | ||
210 | 22 | zmq_available = False | ||
211 | 23 | |||
212 | 24 | |||
213 | 25 | class TestZMQPlugin(TwistedTestCase, PluginTest): | ||
214 | 26 | |||
215 | 27 | if not zmq_available: | ||
216 | 28 | skip = "pyzmq and txzmq required." | ||
217 | 29 | |||
218 | 30 | def setUp(self): | ||
219 | 31 | super(TestZMQPlugin, self).setUp() | ||
220 | 32 | self.init(server_plugin=("lalita.plugins.zmq_proxy.ZMQPlugin", | ||
221 | 33 | {"events_address":"inproc://pub_addr", | ||
222 | 34 | "bot_address":"inproc://sub_addr"})) | ||
223 | 35 | self.ctx = zmq.Context.instance() | ||
224 | 36 | self.sub = self.ctx.socket(zmq.SUB) | ||
225 | 37 | while True: | ||
226 | 38 | try: | ||
227 | 39 | self.sub.connect("inproc://pub_addr") | ||
228 | 40 | except zmq.ZMQError: | ||
229 | 41 | continue | ||
230 | 42 | else: | ||
231 | 43 | break | ||
232 | 44 | self.sub.setsockopt(zmq.SUBSCRIBE, "") | ||
233 | 45 | self.cmd = self.ctx.socket(zmq.PUB) | ||
234 | 46 | while True: | ||
235 | 47 | try: | ||
236 | 48 | self.cmd.connect("inproc://sub_addr") | ||
237 | 49 | except zmq.ZMQError: | ||
238 | 50 | time.sleep(0.2) | ||
239 | 51 | continue | ||
240 | 52 | else: | ||
241 | 53 | break | ||
242 | 54 | |||
243 | 55 | def tearDown(self): | ||
244 | 56 | self.sub.close() | ||
245 | 57 | self.cmd.close() | ||
246 | 58 | self.plugin.shutdown() | ||
247 | 59 | return super(TestZMQPlugin, self).tearDown() | ||
248 | 60 | |||
249 | 61 | @defer.inlineCallbacks | ||
250 | 62 | def test_say_action(self): | ||
251 | 63 | """Say something via zmq.""" | ||
252 | 64 | called = [] | ||
253 | 65 | self.patch(self.plugin, 'say', lambda *a: called.append(a)) | ||
254 | 66 | msg = {'action':'say', 'to_whom':"channel", "msg":"hola", "args":[]} | ||
255 | 67 | self.cmd.send(json.dumps(msg)) | ||
256 | 68 | d = defer.Deferred() | ||
257 | 69 | reactor.callLater(0.2, lambda: d.callback(None)) | ||
258 | 70 | yield d | ||
259 | 71 | self.assertEqual(called[0], (msg['to_whom'].encode('utf-8'), msg['msg'].decode("utf-8"))) | ||
260 | 72 | |||
261 | 73 | def test_event_handler(self): | ||
262 | 74 | """Handle all events.""" | ||
263 | 75 | called = [] | ||
264 | 76 | self.patch(self.plugin, 'publish', lambda *a: called.append(a)) | ||
265 | 77 | tested_events = { | ||
266 | 78 | events.CONNECTION_MADE:(), | ||
267 | 79 | events.CONNECTION_LOST:(), | ||
268 | 80 | events.SIGNED_ON:(), | ||
269 | 81 | events.JOINED:("channel",), | ||
270 | 82 | events.PRIVATE_MESSAGE:("channel",), | ||
271 | 83 | events.TALKED_TO_ME:("channel", "user"), | ||
272 | 84 | events.PUBLIC_MESSAGE:("channel", "user"), | ||
273 | 85 | events.ACTION:("channel", "user"), | ||
274 | 86 | events.JOIN:("channel", "user"), | ||
275 | 87 | events.LEFT:("channel", "user"), | ||
276 | 88 | events.QUIT:("user",), | ||
277 | 89 | events.KICK:("channel", "user"), | ||
278 | 90 | events.RENAME:("user",) | ||
279 | 91 | } | ||
280 | 92 | for event, fixed_args in tested_events.items(): | ||
281 | 93 | msg = "a message" | ||
282 | 94 | args = fixed_args + (msg,) | ||
283 | 95 | self.disp.push(event, *args) | ||
284 | 96 | kwargs = {} | ||
285 | 97 | if CHANNEL_POS[event] is not None: | ||
286 | 98 | kwargs['channel'] = args[CHANNEL_POS[event]] | ||
287 | 99 | if USER_POS[event] is not None: | ||
288 | 100 | kwargs['user'] = args[USER_POS[event]] | ||
289 | 101 | kwargs['args'] = [msg] | ||
290 | 102 | self.assertIn((EVENT_MAP[event][0], kwargs), called) | ||
291 | 103 | |||
292 | 104 | |||
293 | 105 | class TestPluginProcess(TwistedTestCase, PluginTest): | ||
294 | 106 | """Tests for PluginProcess.""" | ||
295 | 107 | |||
296 | 108 | class TestPlugin(PluginProcess): | ||
297 | 109 | def __init__(self, addr1, addr2, called, config=None): | ||
298 | 110 | self.called = called | ||
299 | 111 | super(TestPluginProcess.TestPlugin, self).__init__(addr1, addr2, | ||
300 | 112 | config=config) | ||
301 | 113 | |||
302 | 114 | def init(self, config): | ||
303 | 115 | self.config = config | ||
304 | 116 | self.called.append(("init", config)) | ||
305 | 117 | |||
306 | 118 | def _connect(self, events_address, bot_address): | ||
307 | 119 | self.ctx = zmq.Context.instance() | ||
308 | 120 | self.sub_socket = self.ctx.socket(zmq.SUB) | ||
309 | 121 | while True: | ||
310 | 122 | try: | ||
311 | 123 | self.sub_socket.connect(events_address) | ||
312 | 124 | except zmq.ZMQError: | ||
313 | 125 | time.sleep(0.1) | ||
314 | 126 | continue | ||
315 | 127 | else: | ||
316 | 128 | break | ||
317 | 129 | self.sub_socket.setsockopt(zmq.SUBSCRIBE, "") | ||
318 | 130 | self.bot_socket = self.ctx.socket(zmq.PUB) | ||
319 | 131 | while True: | ||
320 | 132 | try: | ||
321 | 133 | self.bot_socket.connect(bot_address) | ||
322 | 134 | except zmq.ZMQError: | ||
323 | 135 | time.sleep(0.1) | ||
324 | 136 | continue | ||
325 | 137 | else: | ||
326 | 138 | break | ||
327 | 139 | |||
328 | 140 | def setUp(self): | ||
329 | 141 | super(TestPluginProcess, self).setUp() | ||
330 | 142 | events_address = "inproc://pub_addr" | ||
331 | 143 | bot_address = "inproc://sub_addr" | ||
332 | 144 | self.init(server_plugin=("lalita.plugins.zmq_proxy.ZMQPlugin", | ||
333 | 145 | {"events_address":events_address, | ||
334 | 146 | "bot_address":bot_address})) | ||
335 | 147 | self.called = [] | ||
336 | 148 | try: | ||
337 | 149 | self.zmq_plugin = TestPluginProcess.TestPlugin(events_address, | ||
338 | 150 | bot_address, | ||
339 | 151 | self.called) | ||
340 | 152 | except Exception, e: | ||
341 | 153 | import traceback; | ||
342 | 154 | traceback.print_exc() | ||
343 | 155 | |||
344 | 156 | def tearDown(self): | ||
345 | 157 | self.zmq_plugin.bot_socket.close() | ||
346 | 158 | self.zmq_plugin.sub_socket.close() | ||
347 | 159 | self.plugin.shutdown() | ||
348 | 160 | return super(TestPluginProcess, self).tearDown() | ||
349 | 161 | |||
350 | 162 | def test_init(self): | ||
351 | 163 | """Test init is called.""" | ||
352 | 164 | self.assertEquals(self.called, [("init", None)]) | ||
353 | 165 | |||
354 | 166 | def test_register(self): | ||
355 | 167 | """Register to en event.""" | ||
356 | 168 | matcher = lambda a: True | ||
357 | 169 | func = lambda *a: True | ||
358 | 170 | self.zmq_plugin.register(events.JOINED, func, matcher) | ||
359 | 171 | self.assertIn(events.JOINED, self.zmq_plugin._events) | ||
360 | 172 | self.assertEquals(self.zmq_plugin._events[events.JOINED], (func, matcher)) | ||
361 | 173 | |||
362 | 174 | def test_register_command(self): | ||
363 | 175 | func = lambda a: None | ||
364 | 176 | self.patch(self.zmq_plugin, '_send', self.called.append) | ||
365 | 177 | self.zmq_plugin.register_command(func, "command") | ||
366 | 178 | self.assertIn("irc.command", self.zmq_plugin._events) | ||
367 | 179 | self.assertEquals(self.zmq_plugin._events["irc.command"][0][0], func) | ||
368 | 180 | self.assertIn({'action':'register_command', | ||
369 | 181 | 'command':["command"]}, self.called) | ||
370 | 182 | |||
371 | 183 | def test_register_commands(self): | ||
372 | 184 | func = lambda a: None | ||
373 | 185 | self.patch(self.zmq_plugin, '_send', self.called.append) | ||
374 | 186 | self.zmq_plugin.register_command(func, "command") | ||
375 | 187 | self.assertIn("irc.command", self.zmq_plugin._events) | ||
376 | 188 | self.assertEquals(self.zmq_plugin._events["irc.command"][0][0], func) | ||
377 | 189 | self.assertIn({'action':'register_command', | ||
378 | 190 | 'command':["command"]}, self.called) | ||
379 | 191 | func1 = lambda a: None | ||
380 | 192 | self.zmq_plugin.register_command(func1, "command1") | ||
381 | 193 | self.assertEqual(len(self.zmq_plugin._events["irc.command"]), 2) | ||
382 | 194 | self.assertEquals(self.zmq_plugin._events["irc.command"][1][0], func1) | ||
383 | 195 | self.assertIn({'action':'register_command', | ||
384 | 196 | 'command':["command1"]}, self.called) | ||
385 | 197 | |||
386 | 198 | |||
387 | 199 | |||
388 | 200 | def test_say(self): | ||
389 | 201 | self.patch(self.zmq_plugin, '_send', self.called.append) | ||
390 | 202 | self.zmq_plugin.say("me", "message") | ||
391 | 203 | self.assertIn({'action':'say', 'to_whom':'me', 'msg':"message", | ||
392 | 204 | 'args':()}, self.called) | ||
393 | 0 | 205 | ||
394 | === added directory 'lalita/plugins/zmq_plugins' | |||
395 | === added file 'lalita/plugins/zmq_plugins/example.py' | |||
396 | --- lalita/plugins/zmq_plugins/example.py 1970-01-01 00:00:00 +0000 | |||
397 | +++ lalita/plugins/zmq_plugins/example.py 2012-07-09 13:48:35 +0000 | |||
398 | @@ -0,0 +1,52 @@ | |||
399 | 1 | # -*- coding: utf8 -*- | ||
400 | 2 | # Copyright 2009-2012 laliputienses | ||
401 | 3 | # License: GPL v3 | ||
402 | 4 | # For further info, see LICENSE file | ||
403 | 5 | |||
404 | 6 | from lalita.plugins.zmq_proxy import PluginProcess | ||
405 | 7 | |||
406 | 8 | |||
407 | 9 | class Example(PluginProcess): | ||
408 | 10 | """Example zmq-based plugin.""" | ||
409 | 11 | |||
410 | 12 | def init(self, config): | ||
411 | 13 | self.logger.info("Configuring Example Plugin!") | ||
412 | 14 | # register the commands | ||
413 | 15 | self.register_command(self.cmd_example, "example") | ||
414 | 16 | self.register_command(self.cmd_example1, "example1") | ||
415 | 17 | self.register("irc.private_message", self.example_priv) | ||
416 | 18 | self.register("irc.talked_to_me", self.cmd_example) | ||
417 | 19 | |||
418 | 20 | def example_priv(self, user, command, *args): | ||
419 | 21 | """Just say something.""" | ||
420 | 22 | self.say(user, "This is an example plugin.") | ||
421 | 23 | |||
422 | 24 | def cmd_example(self, user, channel, command, *args): | ||
423 | 25 | """Just say something.""" | ||
424 | 26 | self.logger.debug("command %s from %s (args: %s)", command, user, args) | ||
425 | 27 | self.say(channel, "This is an example plugin.") | ||
426 | 28 | |||
427 | 29 | def cmd_example1(self, user, channel, command, *args): | ||
428 | 30 | """Just say something.""" | ||
429 | 31 | self.logger.debug("command %s from %s (args: %s)", command, user, args) | ||
430 | 32 | self.say(channel, "Another example.") | ||
431 | 33 | |||
432 | 34 | |||
433 | 35 | if __name__ == "__main__": | ||
434 | 36 | import optparse | ||
435 | 37 | parser = optparse.OptionParser() | ||
436 | 38 | parser.add_option("-s", "--events-address", dest="events_address", | ||
437 | 39 | default="tcp://127.0.0.1:9090") | ||
438 | 40 | parser.add_option("-b", "--bot-address", dest="bot_address", | ||
439 | 41 | default="tcp://127.0.0.1:9091") | ||
440 | 42 | options, args = parser.parse_args() | ||
441 | 43 | import logging | ||
442 | 44 | logging.basicConfig() | ||
443 | 45 | logging.getLogger().setLevel(logging.DEBUG) | ||
444 | 46 | |||
445 | 47 | try: | ||
446 | 48 | Example(options.events_address, options.bot_address).run() | ||
447 | 49 | except: | ||
448 | 50 | import traceback; | ||
449 | 51 | traceback.print_exc() | ||
450 | 52 | |||
451 | 0 | 53 | ||
452 | === added file 'lalita/plugins/zmq_proxy.py' | |||
453 | --- lalita/plugins/zmq_proxy.py 1970-01-01 00:00:00 +0000 | |||
454 | +++ lalita/plugins/zmq_proxy.py 2012-07-09 13:48:35 +0000 | |||
455 | @@ -0,0 +1,210 @@ | |||
456 | 1 | # | ||
457 | 2 | # Copyright 2009-2012 laliputienses | ||
458 | 3 | # License: GPL v3 | ||
459 | 4 | # For further info, see LICENSE file | ||
460 | 5 | # | ||
461 | 6 | """ZeroMQ proxy plugin.""" | ||
462 | 7 | |||
463 | 8 | import json | ||
464 | 9 | import logging | ||
465 | 10 | import re | ||
466 | 11 | |||
467 | 12 | import zmq | ||
468 | 13 | |||
469 | 14 | from txzmq.pubsub import ZmqSubConnection | ||
470 | 15 | from txzmq import ZmqFactory, ZmqEndpoint | ||
471 | 16 | |||
472 | 17 | from lalita import Plugin | ||
473 | 18 | from lalita.core import events | ||
474 | 19 | |||
475 | 20 | |||
476 | 21 | EVENT_MAP = { | ||
477 | 22 | events.CONNECTION_MADE:("irc.connection_made", None, None), | ||
478 | 23 | events.CONNECTION_LOST:("irc.connection_lost", None, None), | ||
479 | 24 | events.SIGNED_ON:("irc.signed_on", None, None), | ||
480 | 25 | events.JOINED:("irc.joined", 0, None), | ||
481 | 26 | events.PRIVATE_MESSAGE:("irc.private_message", None, 0), | ||
482 | 27 | events.TALKED_TO_ME:("irc.talked_to_me", 1, 0), | ||
483 | 28 | events.COMMAND:("irc.command", 1, 0), | ||
484 | 29 | events.PUBLIC_MESSAGE:("irc.public_message", 1, 0), | ||
485 | 30 | events.ACTION:("irc.action", 1, 0), | ||
486 | 31 | events.JOIN:("irc.join", 1, 0), | ||
487 | 32 | events.LEFT:("irc.left", 1, 0), | ||
488 | 33 | events.QUIT:("irc.quit", None, 0), | ||
489 | 34 | events.KICK:("irc.kick", 1, 0), | ||
490 | 35 | events.RENAME:("irc.rename", None, 0), | ||
491 | 36 | } | ||
492 | 37 | |||
493 | 38 | |||
494 | 39 | class BotConnection(ZmqSubConnection): | ||
495 | 40 | """Bot zmq connection.""" | ||
496 | 41 | |||
497 | 42 | def __init__(self, plugin, factory, endpoint): | ||
498 | 43 | ZmqSubConnection.__init__(self, factory, endpoint) | ||
499 | 44 | self.plugin = plugin | ||
500 | 45 | |||
501 | 46 | def messageReceived(self, *a, **kw): | ||
502 | 47 | return ZmqSubConnection.messageReceived(self, *a, **kw) | ||
503 | 48 | |||
504 | 49 | def gotMessage(self, message): | ||
505 | 50 | info = json.loads(message) | ||
506 | 51 | if info['action'] == "say": | ||
507 | 52 | if isinstance(info['to_whom'], unicode): | ||
508 | 53 | self.plugin.say(info['to_whom'].encode('utf-8'), | ||
509 | 54 | info['msg'], *info['args']) | ||
510 | 55 | else: | ||
511 | 56 | self.plugin.say(info['to_whom'], info['msg'], *info['args']) | ||
512 | 57 | elif info["action"] == "register_command": | ||
513 | 58 | self.plugin.register_command(info['command']) | ||
514 | 59 | else: | ||
515 | 60 | self.plugin.log.error("Invalid Action %s", message) | ||
516 | 61 | |||
517 | 62 | |||
518 | 63 | class EventHandler(object): | ||
519 | 64 | """Bridge of lalita event's to zmq messages.""" | ||
520 | 65 | |||
521 | 66 | def __init__(self, plugin, event_name, channel_pos, user_pos): | ||
522 | 67 | self.name = event_name | ||
523 | 68 | self.channel_pos = channel_pos | ||
524 | 69 | self.user_pos = user_pos | ||
525 | 70 | self.plugin = plugin | ||
526 | 71 | self.im_self = plugin | ||
527 | 72 | self.im_func = self.__call__ | ||
528 | 73 | self.logger = plugin.logger | ||
529 | 74 | |||
530 | 75 | def __call__(self, *args): | ||
531 | 76 | self.logger.debug("event: %s", args) | ||
532 | 77 | msg_args = {} | ||
533 | 78 | if self.channel_pos is not None: | ||
534 | 79 | msg_args['channel'] = args[self.channel_pos] | ||
535 | 80 | if self.user_pos is not None: | ||
536 | 81 | msg_args['user'] = args[self.user_pos] | ||
537 | 82 | msg_args['args'] = [a for i, a in enumerate(args) \ | ||
538 | 83 | if i != self.channel_pos and i != self.user_pos] | ||
539 | 84 | self.logger.debug("publishing: %s - %s", self.name, msg_args) | ||
540 | 85 | self.plugin.publish(self.name, msg_args) | ||
541 | 86 | |||
542 | 87 | |||
543 | 88 | class ZMQPlugin(Plugin): | ||
544 | 89 | """ZeroMQ plugin.""" | ||
545 | 90 | |||
546 | 91 | def init(self, config): | ||
547 | 92 | self._plugins = {} | ||
548 | 93 | self.config = config | ||
549 | 94 | pub_address = self.config['events_address'] | ||
550 | 95 | cmd_address = self.config['bot_address'] | ||
551 | 96 | self.commands = set() # hold all the commands | ||
552 | 97 | self.ctx = zmq.Context.instance() | ||
553 | 98 | self.pub_socket = self.ctx.socket(zmq.PUB) | ||
554 | 99 | self.pub_socket.bind(pub_address) | ||
555 | 100 | # callback/command socket | ||
556 | 101 | zmq_factory = ZmqFactory(context=self.ctx) | ||
557 | 102 | rpc_endpoint = ZmqEndpoint("bind", cmd_address) | ||
558 | 103 | self.cmd_socket = BotConnection(self, zmq_factory, rpc_endpoint) | ||
559 | 104 | self.cmd_socket.subscribe("") | ||
560 | 105 | for event, info in EVENT_MAP.items(): | ||
561 | 106 | if event != events.COMMAND: | ||
562 | 107 | self.register(event, EventHandler(self, *info), re.compile(".*")) | ||
563 | 108 | |||
564 | 109 | def shutdown(self): | ||
565 | 110 | if hasattr(self, 'cmd_socket'): | ||
566 | 111 | self.cmd_socket.shutdown() | ||
567 | 112 | if hasattr(self, 'pub_socket'): | ||
568 | 113 | self.pub_socket.close() | ||
569 | 114 | self.ctx.term() | ||
570 | 115 | for proc in self._plugins.values(): | ||
571 | 116 | proc.loseConnection() | ||
572 | 117 | |||
573 | 118 | def register_command(self, commands): | ||
574 | 119 | """Register a list of commands.""" | ||
575 | 120 | if not self.commands.intersection(set(commands)): | ||
576 | 121 | for command in commands: | ||
577 | 122 | self.commands.add(command) | ||
578 | 123 | self.register(events.COMMAND, | ||
579 | 124 | EventHandler(self, *EVENT_MAP[events.COMMAND]), | ||
580 | 125 | commands) | ||
581 | 126 | # else, already registered. | ||
582 | 127 | |||
583 | 128 | def publish(self, name, msg_args): | ||
584 | 129 | """Publish a message/event.""" | ||
585 | 130 | self.pub_socket.send(name, zmq.SNDMORE) | ||
586 | 131 | self.pub_socket.send(json.dumps(msg_args)) | ||
587 | 132 | |||
588 | 133 | |||
589 | 134 | class PluginProcess(object): | ||
590 | 135 | """Base class for ZeroMQ plugins.""" | ||
591 | 136 | |||
592 | 137 | def __init__(self, events_address, bot_address, config=None): | ||
593 | 138 | self.ctx = None | ||
594 | 139 | self.sub_socket = None | ||
595 | 140 | self.cmd_socket = None | ||
596 | 141 | self.config = config or {} | ||
597 | 142 | self.logger = logging.getLogger("zmq_plugin.%s" % | ||
598 | 143 | (self.__class__.__name__,)) | ||
599 | 144 | self._connect(events_address, bot_address) | ||
600 | 145 | # setup the event handler | ||
601 | 146 | self._events = {} | ||
602 | 147 | self.init(config) | ||
603 | 148 | |||
604 | 149 | def _connect(self, events_address, bot_address): | ||
605 | 150 | self.ctx = zmq.Context.instance() | ||
606 | 151 | self.sub_socket = self.ctx.socket(zmq.SUB) | ||
607 | 152 | self.sub_socket.connect(events_address) | ||
608 | 153 | self.sub_socket.setsockopt(zmq.SUBSCRIBE, "irc") | ||
609 | 154 | # create the bot socket | ||
610 | 155 | self.bot_socket = self.ctx.socket(zmq.PUB) | ||
611 | 156 | self.bot_socket.connect(bot_address) | ||
612 | 157 | |||
613 | 158 | def init(self, config): | ||
614 | 159 | """Subclass responsability.""" | ||
615 | 160 | pass | ||
616 | 161 | |||
617 | 162 | def _send(self, info): | ||
618 | 163 | self.bot_socket.send(json.dumps(info)) | ||
619 | 164 | |||
620 | 165 | def register_command(self, function, command_name): | ||
621 | 166 | """Register a command.""" | ||
622 | 167 | self._events.setdefault('irc.command', [])\ | ||
623 | 168 | .append((function, lambda a: command_name in a)) | ||
624 | 169 | new_msg = {'action':'register_command', 'command':[command_name]} | ||
625 | 170 | self._send(new_msg) | ||
626 | 171 | |||
627 | 172 | def register(self, event, function, matcher=None): | ||
628 | 173 | """Register a event handler.""" | ||
629 | 174 | self._events[event] = (function, matcher) | ||
630 | 175 | |||
631 | 176 | def say(self, to_whom, msg, *args): | ||
632 | 177 | """Say something.""" | ||
633 | 178 | new_msg = {'action':'say', 'to_whom':to_whom, "msg":msg, "args":args} | ||
634 | 179 | self._send(new_msg) | ||
635 | 180 | |||
636 | 181 | def run(self): | ||
637 | 182 | """Main loop""" | ||
638 | 183 | while True: | ||
639 | 184 | # block waiting for a message | ||
640 | 185 | match = False | ||
641 | 186 | event = self.sub_socket.recv() | ||
642 | 187 | payload = json.loads(self.sub_socket.recv()) | ||
643 | 188 | if event == "irc.command": | ||
644 | 189 | commands = self._events[event] | ||
645 | 190 | for handler, matcher in commands: | ||
646 | 191 | if matcher(payload['args']): | ||
647 | 192 | match = True | ||
648 | 193 | break | ||
649 | 194 | else: | ||
650 | 195 | match = True | ||
651 | 196 | try: | ||
652 | 197 | handler, matcher = self._events[event] | ||
653 | 198 | |||
654 | 199 | except KeyError: | ||
655 | 200 | self.logger.error("No handler for %s", event) | ||
656 | 201 | continue | ||
657 | 202 | if match: | ||
658 | 203 | user = payload.get('user') | ||
659 | 204 | channel = payload.get('channel') | ||
660 | 205 | args = [a for a in [user, channel] + payload['args'] \ | ||
661 | 206 | if a is not None] | ||
662 | 207 | handler(*args) | ||
663 | 208 | else: | ||
664 | 209 | self.logger.debug("No match for %s", payload) | ||
665 | 210 |
added info in the docs/*