Mercurial > libervia-backend
comparison src/plugins/plugin_misc_text_commands.py @ 926:d609581bf74a
plugin text commands: refactoring, text now only contain main commands, and other plugin can add commands themselve:
- registerTextCommands can be called by a plugin in its __init__ method, it looks for methods starting with "cmd_" and register them as new commands
- addWhoIsCb: add a callback to /whois command, the callback can complete whois informations
- plugins parrot, XEP-0045 and XEP-0092 now manage their own commands
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 24 Mar 2014 10:57:15 +0100 |
parents | c897c8d321b3 |
children | cd150dd947e3 |
comparison
equal
deleted
inserted
replaced
925:5c78cefd233f | 926:d609581bf74a |
---|---|
16 | 16 |
17 # You should have received a copy of the GNU Affero General Public License | 17 # You should have received a copy of the GNU Affero General Public License |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 from sat.core.i18n import _ | 20 from sat.core.i18n import _ |
21 from sat.core.constants import Const as C | |
21 from sat.core.sat_main import MessageSentAndStored | 22 from sat.core.sat_main import MessageSentAndStored |
22 from twisted.words.protocols.jabber import jid | 23 from twisted.words.protocols.jabber import jid |
23 from twisted.internet import defer | 24 from twisted.internet import defer |
24 from twisted.python.failure import Failure | 25 from twisted.python.failure import Failure |
25 from logging import debug, info, warning, error | 26 from logging import debug, info, warning, error |
26 | 27 |
27 PLUGIN_INFO = { | 28 PLUGIN_INFO = { |
28 "name": "Text commands", | 29 "name": "Text commands", |
29 "import_name": "TEXT-COMMANDS", | 30 "import_name": C.TEXT_CMDS, |
30 "type": "Misc", | 31 "type": "Misc", |
31 "protocols": [], | 32 "protocols": [], |
32 "dependencies": ["XEP-0045", "EXP-PARROT", "XEP-0092"], | 33 "dependencies": [], |
33 "main": "TextCommands", | 34 "main": "TextCommands", |
34 "handler": "no", | 35 "handler": "no", |
35 "description": _("""IRC like text commands""") | 36 "description": _("""IRC like text commands""") |
36 } | 37 } |
37 | 38 |
43 | 44 |
44 def __init__(self, host): | 45 def __init__(self, host): |
45 info(_("Text commands initialization")) | 46 info(_("Text commands initialization")) |
46 self.host = host | 47 self.host = host |
47 host.trigger.add("sendMessage", self.sendMessageTrigger) | 48 host.trigger.add("sendMessage", self.sendMessageTrigger) |
49 self._commands = {} | |
50 self._whois = [] | |
51 self.registerTextCommands(self) | |
52 | |
53 def registerTextCommands(self, instance): | |
54 """ Add a text command | |
55 @param instance: instance of a class containing text commands | |
56 | |
57 """ | |
58 for attr in dir(instance): | |
59 if attr.startswith('cmd_'): | |
60 cmd = getattr(instance, attr) | |
61 if not callable(cmd): | |
62 warning(_("Skipping not callable [%s] attribute") % attr) | |
63 continue | |
64 cmd_name = attr[4:] | |
65 if not cmd_name: | |
66 warning(_("Skipping cmd_ method")) | |
67 if cmd_name in self._commands: | |
68 suff=2 | |
69 while (cmd_name + suff) in self._commands: | |
70 suff+=1 | |
71 new_name = cmd_name + suff | |
72 warning(_("Conflict for command [%(old_name)s], renaming it to [%(new_name)s]") % {'old_name': cmd_name, 'new_name': new_name}) | |
73 cmd_name = new_name | |
74 self._commands[cmd_name] = cmd | |
75 info(_("Registered text command [%s]") % cmd_name) | |
76 | |
77 def addWhoIsCb(self, callback, priority=0): | |
78 """Add a callback which give information to the /whois command | |
79 @param callback: a callback which will be called with the following arguments | |
80 - whois_msg: list of information strings to display, callback need to append its own strings to it | |
81 - target_jid: full jid from who we want informations | |
82 - profile: %(doc_profile)s | |
83 @param priority: priority of the information to show (the highest priority will be displayed first) | |
84 | |
85 """ | |
86 self._whois.append((priority, callback)) | |
87 self._whois.sort(key=lambda item: item[0]) | |
48 | 88 |
49 def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile): | 89 def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile): |
50 """ Install SendMessage command hook """ | 90 """ Install SendMessage command hook """ |
51 pre_xml_treatments.addCallback(self._sendMessageCmdHook, profile) | 91 pre_xml_treatments.addCallback(self._sendMessageCmdHook, profile) |
52 return True | 92 return True |
87 else: | 127 else: |
88 return Failure(MessageSentAndStored("text commands took over", mess_data)) | 128 return Failure(MessageSentAndStored("text commands took over", mess_data)) |
89 | 129 |
90 try: | 130 try: |
91 mess_data["unparsed"] = msg[1 + len(command):] # part not yet parsed of the message | 131 mess_data["unparsed"] = msg[1 + len(command):] # part not yet parsed of the message |
92 d = defer.maybeDeferred(getattr(self, "cmd_%s" % command), mess_data, profile) | 132 d = defer.maybeDeferred(self._commands[command], mess_data, profile) |
93 d.addCallback(retHandling) | 133 d.addCallback(retHandling) |
94 except AttributeError: | 134 except KeyError: |
95 pass | 135 pass |
96 | 136 |
97 return d or mess_data # if a command is detected, we should have a deferred, else be send the message normally | 137 return d or mess_data # if a command is detected, we should have a deferred, else be send the message normally |
98 | 138 |
99 def _getRoomJID(self, arg, service_jid): | 139 def getRoomJID(self, arg, service_jid): |
100 """Return a room jid with a shortcut | 140 """Return a room jid with a shortcut |
101 @param arg: argument: can be a full room jid (e.g.: sat@chat.jabberfr.org) | 141 @param arg: argument: can be a full room jid (e.g.: sat@chat.jabberfr.org) |
102 or a shortcut (e.g.: sat or sat@ for sat on current service) | 142 or a shortcut (e.g.: sat or sat@ for sat on current service) |
103 @param service_jid: jid of the current service (e.g.: chat.jabberfr.org) | 143 @param service_jid: jid of the current service (e.g.: chat.jabberfr.org) |
104 """ | 144 """ |
107 if arg[-1] != '@': | 147 if arg[-1] != '@': |
108 return jid.JID(arg) | 148 return jid.JID(arg) |
109 return jid.JID(arg + service_jid) | 149 return jid.JID(arg + service_jid) |
110 return jid.JID(u"%s@%s" % (arg, service_jid)) | 150 return jid.JID(u"%s@%s" % (arg, service_jid)) |
111 | 151 |
112 def _feedBack(self, message, mess_data, profile): | 152 def feedBack(self, message, mess_data, profile): |
113 """Give a message back to the user""" | 153 """Give a message back to the user""" |
114 if mess_data["type"] == 'groupchat': | 154 if mess_data["type"] == 'groupchat': |
115 _from = mess_data["to"].userhostJID() | 155 _from = mess_data["to"].userhostJID() |
116 else: | 156 else: |
117 _from = self.host.getJidNStream(profile)[0] | 157 _from = self.host.getJidNStream(profile)[0] |
118 | 158 |
119 self.host.bridge.newMessage(unicode(mess_data["to"]), message, mess_data['type'], unicode(_from), {}, profile=profile) | 159 self.host.bridge.newMessage(unicode(mess_data["to"]), message, mess_data['type'], unicode(_from), {}, profile=profile) |
120 | |
121 def cmd_nick(self, mess_data, profile): | |
122 """change nickname""" | |
123 debug("Catched nick command") | |
124 | |
125 if mess_data['type'] != "groupchat": | |
126 #/nick command does nothing if we are not on a group chat | |
127 info("Ignoring /nick command on a non groupchat message") | |
128 | |
129 return True | |
130 | |
131 nick = mess_data["unparsed"].strip() | |
132 room = mess_data["to"] | |
133 | |
134 self.host.plugins["XEP-0045"].nick(room, nick, profile) | |
135 | |
136 return False | |
137 | |
138 def cmd_join(self, mess_data, profile): | |
139 """join a new room (on the same service if full jid is not specified)""" | |
140 debug("Catched join command") | |
141 | |
142 if mess_data['type'] != "groupchat": | |
143 #/leave command does nothing if we are not on a group chat | |
144 info("Ignoring /join command on a non groupchat message") | |
145 return True | |
146 | |
147 if mess_data["unparsed"].strip(): | |
148 room = self._getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host) | |
149 nick = (self.host.plugins["XEP-0045"].getRoomNick(mess_data["to"].userhost(), profile) or | |
150 self.host.getClient(profile).jid.user) | |
151 self.host.plugins["XEP-0045"].join(room, nick, {}, profile) | |
152 | |
153 return False | |
154 | |
155 def cmd_leave(self, mess_data, profile): | |
156 """quit a room""" | |
157 debug("Catched leave command") | |
158 | |
159 if mess_data['type'] != "groupchat": | |
160 #/leave command does nothing if we are not on a group chat | |
161 info("Ignoring /leave command on a non groupchat message") | |
162 return True | |
163 | |
164 if mess_data["unparsed"].strip(): | |
165 room = self._getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host) | |
166 else: | |
167 room = mess_data["to"] | |
168 | |
169 self.host.plugins["XEP-0045"].leave(room, profile) | |
170 | |
171 return False | |
172 | |
173 def cmd_part(self, mess_data, profile): | |
174 """just a synonym of /leave""" | |
175 return self.cmd_leave(mess_data, profile) | |
176 | |
177 def cmd_title(self, mess_data, profile): | |
178 """change room's subject""" | |
179 debug("Catched title command") | |
180 | |
181 if mess_data['type'] != "groupchat": | |
182 #/leave command does nothing if we are not on a group chat | |
183 info("Ignoring /title command on a non groupchat message") | |
184 return True | |
185 | |
186 subject = mess_data["unparsed"].strip() | |
187 | |
188 if subject: | |
189 room = mess_data["to"] | |
190 self.host.plugins["XEP-0045"].subject(room, subject, profile) | |
191 | |
192 return False | |
193 | |
194 def cmd_topic(self, mess_data, profile): | |
195 """just a synonym of /title""" | |
196 return self.cmd_title(mess_data, profile) | |
197 | |
198 def cmd_parrot(self, mess_data, profile): | |
199 """activate Parrot mode between 2 entities, in both directions.""" | |
200 #TODO: these commands must not be hardcoded, an interface should be made | |
201 # to allow plugins to register simple commands like this. | |
202 | |
203 debug("Catched parrot command") | |
204 | |
205 try: | |
206 link_left_jid = jid.JID(mess_data["unparsed"].strip()) | |
207 if not link_left_jid.user or not link_left_jid.host: | |
208 raise jid.InvalidFormat | |
209 except jid.InvalidFormat: | |
210 self._feedBack("Can't activate Parrot mode for invalid jid", mess_data, profile) | |
211 return False | |
212 | |
213 link_right_jid = mess_data['to'] | |
214 | |
215 self.host.plugins["EXP-PARROT"].addParrot(link_left_jid, link_right_jid, profile) | |
216 self.host.plugins["EXP-PARROT"].addParrot(link_right_jid, link_left_jid, profile) | |
217 | |
218 self._feedBack("Parrot mode activated for %s" % (unicode(link_left_jid), ), mess_data, profile) | |
219 | |
220 return False | |
221 | |
222 def cmd_unparrot(self, mess_data, profile): | |
223 """remove Parrot mode between 2 entities, in both directions.""" | |
224 debug("Catched unparrot command") | |
225 | |
226 try: | |
227 link_left_jid = jid.JID(mess_data["unparsed"].strip()) | |
228 if not link_left_jid.user or not link_left_jid.host: | |
229 raise jid.InvalidFormat | |
230 except jid.InvalidFormat: | |
231 self._feedBack("Can't deactivate Parrot mode for invalid jid", mess_data, profile) | |
232 return False | |
233 | |
234 link_right_jid = mess_data['to'] | |
235 | |
236 self.host.plugins["EXP-PARROT"].removeParrot(link_left_jid, profile) | |
237 self.host.plugins["EXP-PARROT"].removeParrot(link_right_jid, profile) | |
238 | |
239 self._feedBack("Parrot mode deactivated for %s and %s" % (unicode(link_left_jid), unicode(link_right_jid)), mess_data, profile) | |
240 | |
241 return False | |
242 | 160 |
243 def cmd_whois(self, mess_data, profile): | 161 def cmd_whois(self, mess_data, profile): |
244 """show informations on entity""" | 162 """show informations on entity""" |
245 debug("Catched whois command") | 163 debug("Catched whois command") |
246 | 164 |
257 try: | 175 try: |
258 target_jid = jid.JID(entity) | 176 target_jid = jid.JID(entity) |
259 if not target_jid.user or not target_jid.host: | 177 if not target_jid.user or not target_jid.host: |
260 raise jid.InvalidFormat | 178 raise jid.InvalidFormat |
261 except (jid.InvalidFormat, RuntimeError): | 179 except (jid.InvalidFormat, RuntimeError): |
262 self._feedBack(_("Invalid jid, can't whois"), mess_data, profile) | 180 self.feedBack(_("Invalid jid, can't whois"), mess_data, profile) |
263 return False | 181 return False |
264 | 182 |
265 if not target_jid.resource: | 183 if not target_jid.resource: |
266 target_jid.resource = self.host.memory.getLastResource(target_jid, profile) | 184 target_jid.resource = self.host.memory.getLastResource(target_jid, profile) |
267 | 185 |
268 whois_msg = [_(u"whois for %(jid)s") % {'jid': target_jid}] | 186 whois_msg = [_(u"whois for %(jid)s") % {'jid': target_jid}] |
269 | 187 |
270 # version | 188 d = defer.succeed(None) |
271 def versionCb(version_data): | 189 for ignore, callback in self._whois: |
272 name, version, os = version_data | 190 d.addCallback(lambda ignore: callback(whois_msg, target_jid, profile)) |
273 if name: | |
274 whois_msg.append(_("Client name: %s") % name) | |
275 if version: | |
276 whois_msg.append(_("Client version: %s") % version) | |
277 if os: | |
278 whois_msg.append(_("Operating system: %s") % os) | |
279 | |
280 d = self.host.plugins['XEP-0092'].getVersion(target_jid, profile) | |
281 d.addCallback(versionCb) | |
282 | |
283 #TODO: add informations here (vcard, etc) | |
284 | 191 |
285 def feedBack(ignore): | 192 def feedBack(ignore): |
286 self._feedBack(u"\n".join(whois_msg), mess_data, profile) | 193 self.feedBack(u"\n".join(whois_msg), mess_data, profile) |
287 return False | 194 return False |
288 | 195 |
289 d.addCallback(feedBack) | 196 d.addCallback(feedBack) |
290 return d | 197 return d |
291 | 198 |
293 """show help on available commands""" | 200 """show help on available commands""" |
294 commands = filter(lambda method: method.startswith('cmd_'), dir(self)) | 201 commands = filter(lambda method: method.startswith('cmd_'), dir(self)) |
295 longuest = max([len(command) for command in commands]) | 202 longuest = max([len(command) for command in commands]) |
296 help_cmds = [] | 203 help_cmds = [] |
297 | 204 |
298 for command in commands: | 205 for command in self._commands: |
299 method = getattr(self, command) | 206 method = self._commands[command] |
300 try: | 207 try: |
301 help_str = method.__doc__.split('\n')[0] | 208 help_str = method.__doc__.split('\n')[0] |
302 except AttributeError: | 209 except AttributeError: |
303 help_str = '' | 210 help_str = '' |
304 spaces = (longuest - len(command)) * ' ' | 211 spaces = (longuest - len(command)) * ' ' |
305 help_cmds.append(" /%s: %s %s" % (command[4:], spaces, help_str)) | 212 help_cmds.append(" /%s: %s %s" % (command, spaces, help_str)) |
306 | 213 |
307 help_mess = _(u"Text commands available:\n%s") % (u'\n'.join(help_cmds), ) | 214 help_mess = _(u"Text commands available:\n%s") % (u'\n'.join(help_cmds), ) |
308 self._feedBack(help_mess, mess_data, profile) | 215 self.feedBack(help_mess, mess_data, profile) |