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)