Mercurial > libervia-backend
comparison src/sat.tac @ 223:86d249b6d9b7
Files reorganisation
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 29 Dec 2010 01:06:29 +0100 |
parents | sat.tac@5c420b1f1df4 |
children | fd9b7834d98a |
comparison
equal
deleted
inserted
replaced
222:3198bfd66daa | 223:86d249b6d9b7 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 """ | |
5 SAT: a jabber client | |
6 Copyright (C) 2009, 2010 Jérôme Poisson (goffi@goffi.org) | |
7 | |
8 This program is free software: you can redistribute it and/or modify | |
9 it under the terms of the GNU General Public License as published by | |
10 the Free Software Foundation, either version 3 of the License, or | |
11 (at your option) any later version. | |
12 | |
13 This program is distributed in the hope that it will be useful, | |
14 but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 GNU General Public License for more details. | |
17 | |
18 You should have received a copy of the GNU General Public License | |
19 along with this program. If not, see <http://www.gnu.org/licenses/>. | |
20 """ | |
21 | |
22 CONST = { | |
23 'client_name' : u'SàT (Salut à toi)', | |
24 'client_version' : u'0.0.3D', #Please add 'D' at the end for dev versions | |
25 'local_dir' : '~/.sat' | |
26 } | |
27 | |
28 import gettext | |
29 gettext.install('sat', "i18n", unicode=True) | |
30 | |
31 from twisted.application import internet, service | |
32 from twisted.internet import glib2reactor, protocol, task | |
33 glib2reactor.install() | |
34 | |
35 from twisted.words.protocols.jabber import jid, xmlstream | |
36 from twisted.words.protocols.jabber import error as jab_error | |
37 from twisted.words.xish import domish | |
38 | |
39 from twisted.internet import reactor | |
40 import pdb | |
41 | |
42 from wokkel import client, disco, xmppim, generic, compat | |
43 | |
44 from sat.bridge.DBus import DBusBridge | |
45 import logging | |
46 from logging import debug, info, error | |
47 | |
48 import signal, sys | |
49 import os.path | |
50 | |
51 from sat.tools.memory import Memory | |
52 from sat.tools.xml_tools import tupleList2dataForm | |
53 from glob import glob | |
54 | |
55 try: | |
56 from twisted.words.protocols.xmlstream import XMPPHandler | |
57 except ImportError: | |
58 from wokkel.subprotocols import XMPPHandler | |
59 | |
60 | |
61 ### logging configuration FIXME: put this elsewhere ### | |
62 logging.basicConfig(level=logging.DEBUG, | |
63 format='%(message)s') | |
64 ### | |
65 | |
66 | |
67 sat_id = 0 | |
68 | |
69 def sat_next_id(): | |
70 global sat_id | |
71 sat_id+=1 | |
72 return "sat_id_"+str(sat_id) | |
73 | |
74 class SatXMPPClient(client.XMPPClient): | |
75 | |
76 def __init__(self, host_app, profile, user_jid, password, host=None, port=5222): | |
77 client.XMPPClient.__init__(self, user_jid, password, host, port) | |
78 self.factory.clientConnectionLost = self.connectionLost | |
79 self.__connected=False | |
80 self.profile = profile | |
81 self.host_app = host_app | |
82 | |
83 def _authd(self, xmlstream): | |
84 print "SatXMPPClient" | |
85 client.XMPPClient._authd(self, xmlstream) | |
86 self.__connected=True | |
87 info (_("********** [%s] CONNECTED **********") % self.profile) | |
88 self.streamInitialized() | |
89 self.host_app.bridge.connected(self.profile) #we send the signal to the clients | |
90 | |
91 def streamInitialized(self): | |
92 """Called after _authd""" | |
93 debug (_("XML stream is initialized")) | |
94 self.keep_alife = task.LoopingCall(self.xmlstream.send, " ") #Needed to avoid disconnection (specially with openfire) | |
95 self.keep_alife.start(180) | |
96 | |
97 self.disco = SatDiscoProtocol(self) | |
98 self.disco.setHandlerParent(self) | |
99 self.discoHandler = disco.DiscoHandler() | |
100 self.discoHandler.setHandlerParent(self) | |
101 | |
102 self.roster.requestRoster() | |
103 | |
104 self.presence.available() | |
105 | |
106 self.disco.requestInfo(jid.JID(self.host_app.memory.getParamA("Server", "Connection", profile_key=self.profile))).addCallback(self.host_app.serverDisco) #FIXME: use these informations | |
107 | |
108 def isConnected(self): | |
109 return self.__connected | |
110 | |
111 def connectionLost(self, connector, unused_reason): | |
112 self.__connected=False | |
113 info (_("********** [%s] DISCONNECTED **********") % self.profile) | |
114 try: | |
115 self.keep_alife.stop() | |
116 except AttributeError: | |
117 debug (_("No keep_alife")) | |
118 self.host_app.bridge.disconnected(self.profile) #we send the signal to the clients | |
119 | |
120 | |
121 class SatMessageProtocol(xmppim.MessageProtocol): | |
122 | |
123 def __init__(self, host): | |
124 xmppim.MessageProtocol.__init__(self) | |
125 self.host = host | |
126 | |
127 def onMessage(self, message): | |
128 debug (_(u"got message from: %s"), message["from"]) | |
129 for e in message.elements(): | |
130 if e.name == "body": | |
131 type = message['type'] if message.hasAttribute('type') else 'chat' #FIXME: check specs | |
132 self.host.bridge.newMessage(message["from"], e.children[0], type, profile=self.parent.profile) | |
133 self.host.memory.addToHistory(self.parent.jid, jid.JID(message["from"]), self.parent.jid, "chat", e.children[0]) | |
134 break | |
135 | |
136 class SatRosterProtocol(xmppim.RosterClientProtocol): | |
137 | |
138 def __init__(self, host): | |
139 xmppim.RosterClientProtocol.__init__(self) | |
140 self.host = host | |
141 | |
142 def rosterCb(self, roster): | |
143 for raw_jid, item in roster.iteritems(): | |
144 self.onRosterSet(item) | |
145 | |
146 def requestRoster(self): | |
147 """ ask the server for Roster list """ | |
148 debug("requestRoster") | |
149 self.getRoster().addCallback(self.rosterCb) | |
150 | |
151 def removeItem(self, to): | |
152 """Remove a contact from roster list""" | |
153 xmppim.RosterClientProtocol.removeItem(self, to) | |
154 #TODO: check IQ result | |
155 | |
156 #XXX: disabled (cf http://wokkel.ik.nu/ticket/56)) | |
157 #def addItem(self, to): | |
158 #"""Add a contact to roster list""" | |
159 #xmppim.RosterClientProtocol.addItem(self, to) | |
160 #TODO: check IQ result""" | |
161 | |
162 def onRosterSet(self, item): | |
163 """Called when a new/update roster item is received""" | |
164 #TODO: send a signal to frontends | |
165 item_attr = {'to': str(item.subscriptionTo), | |
166 'from': str(item.subscriptionFrom), | |
167 'ask': str(item.ask) | |
168 } | |
169 if item.name: | |
170 item_attr['name'] = item.name | |
171 info (_("new contact in roster list: %s"), item.jid.full()) | |
172 self.host.memory.addContact(item.jid, item_attr, item.groups, self.parent.profile) | |
173 self.host.bridge.newContact(item.jid.full(), item_attr, item.groups, self.parent.profile) | |
174 | |
175 def onRosterRemove(self, entity): | |
176 """Called when a roster removal event is received""" | |
177 #TODO: send a signal to frontends | |
178 print _("removing %s from roster list") % entity.full() | |
179 self.host.memory.delContact(entity, self.parent.profile) | |
180 | |
181 class SatPresenceProtocol(xmppim.PresenceClientProtocol): | |
182 | |
183 def __init__(self, host): | |
184 xmppim.PresenceClientProtocol.__init__(self) | |
185 self.host = host | |
186 | |
187 def availableReceived(self, entity, show=None, statuses=None, priority=0): | |
188 debug (_("presence update for [%(entity)s] (available, show=%(show)s statuses=%(statuses)s priority=%(priority)d)") % {'entity':entity, 'show':show, 'statuses':statuses, 'priority':priority}) | |
189 | |
190 if statuses.has_key(None): #we only want string keys | |
191 statuses["default"] = statuses[None] | |
192 del statuses[None] | |
193 | |
194 self.host.memory.addPresenceStatus(entity, show or "", | |
195 int(priority), statuses, self.parent.profile) | |
196 | |
197 #now it's time to notify frontends | |
198 self.host.bridge.presenceUpdate(entity.full(), show or "", | |
199 int(priority), statuses, self.parent.profile) | |
200 | |
201 def unavailableReceived(self, entity, statuses=None): | |
202 debug (_("presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)") % {'entity':entity, 'statuses':statuses}) | |
203 if statuses and statuses.has_key(None): #we only want string keys | |
204 statuses["default"] = statuses[None] | |
205 del statuses[None] | |
206 self.host.memory.addPresenceStatus(entity, "unavailable", 0, statuses, self.parent.profile) | |
207 | |
208 #now it's time to notify frontends | |
209 self.host.bridge.presenceUpdate(entity.full(), "unavailable", 0, statuses, self.parent.profile) | |
210 | |
211 | |
212 def available(self, entity=None, show=None, statuses=None, priority=0): | |
213 if statuses and statuses.has_key('default'): | |
214 statuses[None] = statuses['default'] | |
215 del statuses['default'] | |
216 xmppim.PresenceClientProtocol.available(self, entity, show, statuses, priority) | |
217 | |
218 def subscribedReceived(self, entity): | |
219 debug (_("subscription approved for [%s]") % entity.userhost()) | |
220 self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile) | |
221 self.host.bridge.subscribe('subscribed', entity.userhost(), self.parent.profile) | |
222 | |
223 def unsubscribedReceived(self, entity): | |
224 debug (_("unsubscription confirmed for [%s]") % entity.userhost()) | |
225 self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile) | |
226 self.host.bridge.subscribe('unsubscribed', entity.userhost(), self.parent.profile) | |
227 | |
228 def subscribeReceived(self, entity): | |
229 debug (_("subscription request for [%s]") % entity.userhost()) | |
230 self.host.memory.addWaitingSub('subscribe', entity.userhost(), self.parent.profile) | |
231 self.host.bridge.subscribe('subscribe', entity.userhost(), self.parent.profile) | |
232 | |
233 def unsubscribeReceived(self, entity): | |
234 debug (_("unsubscription asked for [%s]") % entity.userhost()) | |
235 self.host.memory.addWaitingSub('unsubscribe', entity.userhost(), self.parent.profile) | |
236 self.host.bridge.subscribe('unsubscribe', entity.userhost(), self.parent.profile) | |
237 | |
238 class SatDiscoProtocol(disco.DiscoClientProtocol): | |
239 def __init__(self, host): | |
240 disco.DiscoClientProtocol.__init__(self) | |
241 | |
242 class SatFallbackHandler(generic.FallbackHandler): | |
243 def __init__(self, host): | |
244 generic.FallbackHandler.__init__(self) | |
245 | |
246 def iqFallback(self, iq): | |
247 debug (u"iqFallback: xml = [%s], handled=%s" % (iq.toXml(), "True" if iq.handled else "False")) | |
248 generic.FallbackHandler.iqFallback(self, iq) | |
249 | |
250 class RegisteringAuthenticator(xmlstream.ConnectAuthenticator): | |
251 | |
252 def __init__(self, host, jabber_host, user_login, user_pass, answer_id): | |
253 xmlstream.ConnectAuthenticator.__init__(self, jabber_host) | |
254 self.host = host | |
255 self.jabber_host = jabber_host | |
256 self.user_login = user_login | |
257 self.user_pass = user_pass | |
258 self.answer_id = answer_id | |
259 print _("Registration asked for"),user_login, user_pass, jabber_host | |
260 | |
261 def connectionMade(self): | |
262 print "connectionMade" | |
263 | |
264 self.xmlstream.namespace = "jabber:client" | |
265 self.xmlstream.sendHeader() | |
266 | |
267 iq = compat.IQ(self.xmlstream, 'set') | |
268 iq["to"] = self.jabber_host | |
269 query = iq.addElement(('jabber:iq:register', 'query')) | |
270 _user = query.addElement('username') | |
271 _user.addContent(self.user_login) | |
272 _pass = query.addElement('password') | |
273 _pass.addContent(self.user_pass) | |
274 reg = iq.send(self.jabber_host).addCallbacks(self.registrationAnswer, self.registrationFailure) | |
275 | |
276 def registrationAnswer(self, answer): | |
277 debug (_("registration answer: %s") % answer.toXml()) | |
278 answer_type = "SUCCESS" | |
279 answer_data={"message":_("Registration successfull")} | |
280 self.host.bridge.actionResult(answer_type, self.answer_id, answer_data) | |
281 self.xmlstream.sendFooter() | |
282 | |
283 def registrationFailure(self, failure): | |
284 info (_("Registration failure: %s") % str(failure.value)) | |
285 answer_type = "ERROR" | |
286 answer_data = {} | |
287 if failure.value.condition == 'conflict': | |
288 answer_data['reason'] = 'conflict' | |
289 answer_data={"message":_("Username already exists, please choose an other one")} | |
290 else: | |
291 answer_data['reason'] = 'unknown' | |
292 answer_data={"message":_("Registration failed (%s)") % str(failure.value.condition)} | |
293 self.host.bridge.actionResult(answer_type, self.answer_id, answer_data) | |
294 self.xmlstream.sendFooter() | |
295 | |
296 | |
297 class SAT(service.Service): | |
298 | |
299 def get_next_id(self): | |
300 return sat_next_id() | |
301 | |
302 def get_const(self, name): | |
303 """Return a constant""" | |
304 if not CONST.has_key(name): | |
305 error(_('Trying to access an undefined constant')) | |
306 raise Exception | |
307 return CONST[name] | |
308 | |
309 def set_const(self, name, value): | |
310 """Save a constant""" | |
311 if CONST.has_key(name): | |
312 error(_('Trying to redefine a constant')) | |
313 raise Exception | |
314 CONST[name] = value | |
315 | |
316 def __init__(self): | |
317 #TODO: standardize callback system | |
318 | |
319 local_dir = os.path.expanduser(self.get_const('local_dir')) | |
320 if not os.path.exists(local_dir): | |
321 os.makedirs(local_dir) | |
322 | |
323 self.__waiting_conf = {} #callback called when a confirmation is received | |
324 self.__progress_cb_map = {} #callback called when a progress is requested (key = progress id) | |
325 self.__general_cb_map = {} #callback called for general reasons (key = name) | |
326 self.__private_data = {} #used for internal callbacks (key = id) | |
327 self.profiles = {} | |
328 self.plugins = {} | |
329 self.menus = {} #used to know which new menus are wanted by plugins | |
330 | |
331 self.memory=Memory(self) | |
332 self.server_features=[] #XXX: temp dic, need to be transfered into self.memory in the future | |
333 | |
334 self.bridge=DBusBridge() | |
335 self.bridge.register("getVersion", lambda: self.get_const('client_version')) | |
336 self.bridge.register("getProfileName", self.memory.getProfileName) | |
337 self.bridge.register("getProfilesList", self.memory.getProfilesList) | |
338 self.bridge.register("createProfile", self.memory.createProfile) | |
339 self.bridge.register("deleteProfile", self.memory.deleteProfile) | |
340 self.bridge.register("registerNewAccount", self.registerNewAccount) | |
341 self.bridge.register("connect", self.connect) | |
342 self.bridge.register("disconnect", self.disconnect) | |
343 self.bridge.register("getContacts", self.memory.getContacts) | |
344 self.bridge.register("getPresenceStatus", self.memory.getPresenceStatus) | |
345 self.bridge.register("getWaitingSub", self.memory.getWaitingSub) | |
346 self.bridge.register("sendMessage", self.sendMessage) | |
347 self.bridge.register("setParam", self.setParam) | |
348 self.bridge.register("getParamA", self.memory.getParamA) | |
349 self.bridge.register("getParamsUI", self.memory.getParamsUI) | |
350 self.bridge.register("getParams", self.memory.getParams) | |
351 self.bridge.register("getParamsForCategory", self.memory.getParamsForCategory) | |
352 self.bridge.register("getParamsCategories", self.memory.getParamsCategories) | |
353 self.bridge.register("getHistory", self.memory.getHistory) | |
354 self.bridge.register("setPresence", self.setPresence) | |
355 self.bridge.register("subscription", self.subscription) | |
356 self.bridge.register("addContact", self.addContact) | |
357 self.bridge.register("delContact", self.delContact) | |
358 self.bridge.register("isConnected", self.isConnected) | |
359 self.bridge.register("launchAction", self.launchAction) | |
360 self.bridge.register("confirmationAnswer", self.confirmationAnswer) | |
361 self.bridge.register("getProgress", self.getProgress) | |
362 self.bridge.register("getMenus", self.getMenus) | |
363 self.bridge.register("getMenuHelp", self.getMenuHelp) | |
364 self.bridge.register("callMenu", self.callMenu) | |
365 | |
366 self._import_plugins() | |
367 | |
368 | |
369 def _import_plugins(self): | |
370 """Import all plugins found in plugins directory""" | |
371 #TODO: manage dependencies | |
372 plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename,glob ("plugins/plugin*.py"))] | |
373 | |
374 for plug in plug_lst: | |
375 plug_path = 'plugins.'+plug | |
376 __import__(plug_path) | |
377 mod = sys.modules[plug_path] | |
378 plug_info = mod.PLUGIN_INFO | |
379 info (_("importing plugin: %s"), plug_info['name']) | |
380 self.plugins[plug_info['import_name']] = getattr(mod, plug_info['main'])(self) | |
381 if plug_info.has_key('handler') and plug_info['handler'] == 'yes': | |
382 self.plugins[plug_info['import_name']].is_handler = True | |
383 else: | |
384 self.plugins[plug_info['import_name']].is_handler = False | |
385 #TODO: test xmppclient presence and register handler parent | |
386 | |
387 def connect(self, profile_key = '@DEFAULT@'): | |
388 """Connect to jabber server""" | |
389 | |
390 profile = self.memory.getProfileName(profile_key) | |
391 if not profile_key: | |
392 error (_('Trying to connect a non-exsitant profile')) | |
393 return | |
394 | |
395 if (self.isConnected(profile)): | |
396 info(_("already connected !")) | |
397 return | |
398 current = self.profiles[profile] = SatXMPPClient(self, profile, | |
399 jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key = profile_key), profile), | |
400 self.memory.getParamA("Password", "Connection", profile_key = profile_key), | |
401 self.memory.getParamA("Server", "Connection", profile_key = profile_key), 5222) | |
402 | |
403 current.messageProt = SatMessageProtocol(self) | |
404 current.messageProt.setHandlerParent(current) | |
405 | |
406 current.roster = SatRosterProtocol(self) | |
407 current.roster.setHandlerParent(current) | |
408 | |
409 current.presence = SatPresenceProtocol(self) | |
410 current.presence.setHandlerParent(current) | |
411 | |
412 current.fallBack = SatFallbackHandler(self) | |
413 current.fallBack.setHandlerParent(current) | |
414 | |
415 current.versionHandler = generic.VersionHandler(self.get_const('client_name'), | |
416 self.get_const('client_version')) | |
417 current.versionHandler.setHandlerParent(current) | |
418 | |
419 debug (_("setting plugins parents")) | |
420 | |
421 for plugin in self.plugins.iteritems(): | |
422 if plugin[1].is_handler: | |
423 plugin[1].getHandler(profile).setHandlerParent(current) | |
424 | |
425 current.startService() | |
426 | |
427 def disconnect(self, profile_key='@DEFAULT@'): | |
428 """disconnect from jabber server""" | |
429 if (not self.isConnected(profile_key)): | |
430 info(_("not connected !")) | |
431 return | |
432 profile = self.memory.getProfileName(profile_key) | |
433 info(_("Disconnecting...")) | |
434 self.profiles[profile].stopService() | |
435 | |
436 def startService(self): | |
437 info("Salut à toi ô mon frère !") | |
438 #TODO: manage autoconnect | |
439 #self.connect() | |
440 | |
441 def stopService(self): | |
442 self.memory.save() | |
443 info("Salut aussi à Rantanplan") | |
444 | |
445 def run(self): | |
446 debug(_("running app")) | |
447 reactor.run() | |
448 | |
449 def stop(self): | |
450 debug(_("stopping app")) | |
451 reactor.stop() | |
452 | |
453 ## Misc methods ## | |
454 | |
455 def getJidNStream(self, profile_key): | |
456 """Convenient method to get jid and stream from profile key | |
457 @return: tuple (jid, xmlstream) from profile, can be None""" | |
458 profile = self.memory.getProfileName(profile_key) | |
459 if not profile or not self.profiles[profile].isConnected(): | |
460 return (None, None) | |
461 return (self.profiles[profile].jid, self.profiles[profile].xmlstream) | |
462 | |
463 def getClient(self, profile_key): | |
464 """Convenient method to get client from profile key | |
465 @return: client or None if it doesn't exist""" | |
466 profile = self.memory.getProfileName(profile_key) | |
467 if not profile: | |
468 return None | |
469 return self.profiles[profile] | |
470 | |
471 def registerNewAccount(self, login, password, server, port = 5222, id = None): | |
472 """Connect to a server and create a new account using in-band registration""" | |
473 | |
474 next_id = id or sat_next_id() #the id is used to send server's answer | |
475 serverRegistrer = xmlstream.XmlStreamFactory(RegisteringAuthenticator(self, server, login, password, next_id)) | |
476 connector = reactor.connectTCP(server, port, serverRegistrer) | |
477 serverRegistrer.clientConnectionLost = lambda conn, reason: connector.disconnect() | |
478 | |
479 return next_id | |
480 | |
481 def registerNewAccountCB(self, id, data, profile): | |
482 user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0] | |
483 password = self.memory.getParamA("Password", "Connection", profile_key=profile) | |
484 server = self.memory.getParamA("Server", "Connection", profile_key=profile) | |
485 | |
486 if not user or not password or not server: | |
487 info (_('No user or server given')) | |
488 #TODO: a proper error message must be sent to frontend | |
489 self.actionResult(id, "ERROR", {'message':_("No user, password or server given, can't register new account.")}) | |
490 return | |
491 | |
492 confirm_id = sat_next_id() | |
493 self.__private_data[confirm_id]=(id,profile) | |
494 | |
495 self.askConfirmation(confirm_id, "YES/NO", | |
496 {"message":_("Are you sure to register new account [%(user)s] to server %(server)s ?") % {'user':user, 'server':server, 'profile':profile}}, | |
497 self.regisConfirmCB) | |
498 print ("===============+++++++++++ REGISTER NEW ACCOUNT++++++++++++++============") | |
499 print "id=",id | |
500 print "data=",data | |
501 | |
502 def regisConfirmCB(self, id, accepted, data): | |
503 print _("register Confirmation CB ! (%s)") % str(accepted) | |
504 action_id,profile = self.__private_data[id] | |
505 del self.__private_data[id] | |
506 if accepted: | |
507 user = jid.parse(self.memory.getParamA("JabberID", "Connection", profile_key=profile))[0] | |
508 password = self.memory.getParamA("Password", "Connection", profile_key=profile) | |
509 server = self.memory.getParamA("Server", "Connection", profile_key=profile) | |
510 self.registerNewAccount(user, password, server, id=action_id) | |
511 else: | |
512 self.actionResult(action_id, "SUPPRESS", {}) | |
513 | |
514 def submitForm(self, action, target, fields, profile_key='@DEFAULT@'): | |
515 """submit a form | |
516 @param target: target jid where we are submitting | |
517 @param fields: list of tuples (name, value) | |
518 @return: tuple: (id, deferred) | |
519 """ | |
520 | |
521 profile = self.memory.getProfileName(profile_key) | |
522 assert(profile) | |
523 to_jid = jid.JID(target) | |
524 | |
525 iq = compat.IQ(self.profiles[profile].xmlstream, 'set') | |
526 iq["to"] = target | |
527 iq["from"] = self.profiles[profile].jid.full() | |
528 query = iq.addElement(('jabber:iq:register', 'query')) | |
529 if action=='SUBMIT': | |
530 form = tupleList2dataForm(fields) | |
531 query.addChild(form.toElement()) | |
532 elif action=='CANCEL': | |
533 query.addElement('remove') | |
534 else: | |
535 error (_("FIXME FIXME FIXME: Unmanaged action (%s) in submitForm") % action) | |
536 raise NotImplementedError | |
537 | |
538 deferred = iq.send(target) | |
539 return (iq['id'], deferred) | |
540 | |
541 ## Client management ## | |
542 | |
543 def setParam(self, name, value, category, profile_key='@DEFAULT@'): | |
544 """set wanted paramater and notice observers""" | |
545 info (_("setting param: %(name)s=%(value)s in category %(category)s") % {'name':name, 'value':value, 'category':category}) | |
546 self.memory.setParam(name, value, category, profile_key) | |
547 | |
548 def isConnected(self, profile_key='@DEFAULT@'): | |
549 """Return connection status of profile | |
550 @param profile_key: key_word or profile name to determine profile name | |
551 @return True if connected | |
552 """ | |
553 profile = self.memory.getProfileName(profile_key) | |
554 if not profile: | |
555 error (_('asking connection status for a non-existant profile')) | |
556 return | |
557 if not self.profiles.has_key(profile): | |
558 return False | |
559 return self.profiles[profile].isConnected() | |
560 | |
561 def launchAction(self, type, data, profile_key='@DEFAULT@'): | |
562 """Launch a specific action asked by client | |
563 @param type: action type (button) | |
564 @param data: needed data to launch the action | |
565 | |
566 @return: action id for result, or empty string in case or error | |
567 """ | |
568 profile = self.memory.getProfileName(profile_key) | |
569 if not profile: | |
570 error (_('trying to launch action with a non-existant profile')) | |
571 raise Exception #TODO: raise a proper exception | |
572 if type=="button": | |
573 try: | |
574 cb_name = data['callback_id'] | |
575 except KeyError: | |
576 error (_("Incomplete data")) | |
577 return "" | |
578 id = sat_next_id() | |
579 self.callGeneralCB(cb_name, id, data, profile = profile) | |
580 return id | |
581 else: | |
582 error (_("Unknown action type")) | |
583 return "" | |
584 | |
585 | |
586 ## jabber methods ## | |
587 | |
588 def sendMessage(self,to,msg,type='chat', profile_key='@DEFAULT@'): | |
589 #FIXME: check validity of recipient | |
590 profile = self.memory.getProfileName(profile_key) | |
591 assert(profile) | |
592 current_jid = self.profiles[profile].jid | |
593 debug(_("Sending jabber message to %s..."), to) | |
594 message = domish.Element(('jabber:client','message')) | |
595 message["to"] = jid.JID(to).full() | |
596 message["from"] = current_jid.full() | |
597 message["type"] = type | |
598 message.addElement("body", "jabber:client", msg) | |
599 self.profiles[profile].xmlstream.send(message) | |
600 self.memory.addToHistory(current_jid, current_jid, jid.JID(to), message["type"], unicode(msg)) | |
601 if type!="groupchat": | |
602 self.bridge.newMessage(message['from'], unicode(msg), to=message['to'], type=type, profile=profile) #We send back the message, so all clients are aware of it | |
603 | |
604 | |
605 def setPresence(self, to="", show="", priority = 0, statuses={}, profile_key='@DEFAULT@'): | |
606 """Send our presence information""" | |
607 profile = self.memory.getProfileName(profile_key) | |
608 assert(profile) | |
609 to_jid = jid.JID(to) if to else None | |
610 self.profiles[profile].presence.available(to_jid, show, statuses, priority) | |
611 | |
612 def subscription(self, subs_type, raw_jid, profile_key='@DEFAULT@'): | |
613 """Called to manage subscription | |
614 @param subs_type: subsciption type (cf RFC 3921) | |
615 @param raw_jid: unicode entity's jid | |
616 @param profile_key: profile""" | |
617 profile = self.memory.getProfileName(profile_key) | |
618 assert(profile) | |
619 to_jid = jid.JID(raw_jid) | |
620 debug (_('subsciption request [%(subs_type)s] for %(jid)s') % {'subs_type':subs_type, 'jid':to_jid.full()}) | |
621 if subs_type=="subscribe": | |
622 self.profiles[profile].presence.subscribe(to_jid) | |
623 elif subs_type=="subscribed": | |
624 self.profiles[profile].presence.subscribed(to_jid) | |
625 contact = self.memory.getContact(to_jid) | |
626 if not contact or not bool(contact['to']): #we automatically subscribe to 'to' presence | |
627 debug(_('sending automatic "to" subscription request')) | |
628 self.subscription('subscribe', to_jid.userhost()) | |
629 elif subs_type=="unsubscribe": | |
630 self.profiles[profile].presence.unsubscribe(to_jid) | |
631 elif subs_type=="unsubscribed": | |
632 self.profiles[profile].presence.unsubscribed(to_jid) | |
633 | |
634 | |
635 def addContact(self, to, profile_key='@DEFAULT@'): | |
636 """Add a contact in roster list""" | |
637 profile = self.memory.getProfileName(profile_key) | |
638 assert(profile) | |
639 to_jid=jid.JID(to) | |
640 #self.profiles[profile].roster.addItem(to_jid) XXX: disabled (cf http://wokkel.ik.nu/ticket/56)) | |
641 self.profiles[profile].presence.subscribe(to_jid) | |
642 | |
643 def delContact(self, to, profile_key='@DEFAULT@'): | |
644 """Remove contact from roster list""" | |
645 profile = self.memory.getProfileName(profile_key) | |
646 assert(profile) | |
647 to_jid=jid.JID(to) | |
648 self.profiles[profile].roster.removeItem(to_jid) | |
649 self.profiles[profile].presence.unsubscribe(to_jid) | |
650 self.bridge.contactDeleted(to, profile) | |
651 | |
652 | |
653 ## callbacks ## | |
654 | |
655 def serverDisco(self, disco): | |
656 """xep-0030 Discovery Protocol.""" | |
657 for feature in disco.features: | |
658 debug (_("Feature found: %s"),feature) | |
659 self.server_features.append(feature) | |
660 for cat, type in disco.identities: | |
661 debug (_("Identity found: [%(category)s/%(type)s] %(identity)s") % {'category':cat, 'type':type, 'identity':disco.identities[(cat,type)]}) | |
662 | |
663 | |
664 ## Generic HMI ## | |
665 | |
666 def actionResult(self, id, type, data): | |
667 """Send the result of an action | |
668 @param id: same id used with action | |
669 @param type: result type ("PARAM", "SUCCESS", "ERROR", "XMLUI") | |
670 @param data: dictionary | |
671 """ | |
672 self.bridge.actionResult(type, id, data) | |
673 | |
674 def actionResultExt(self, id, type, data): | |
675 """Send the result of an action, extended version | |
676 @param id: same id used with action | |
677 @param type: result type /!\ only "DICT_DICT" for this method | |
678 @param data: dictionary of dictionaries | |
679 """ | |
680 if type != "DICT_DICT": | |
681 error(_("type for actionResultExt must be DICT_DICT, fixing it")) | |
682 type = "DICT_DICT" | |
683 self.bridge.actionResultExt(type, id, data) | |
684 | |
685 | |
686 | |
687 def askConfirmation(self, id, type, data, cb): | |
688 """Add a confirmation callback | |
689 @param id: id used to get answer | |
690 @param type: confirmation type ("YES/NO", "FILE_TRANSFERT") | |
691 @param data: data (depend of confirmation type) | |
692 @param cb: callback called with the answer | |
693 """ | |
694 if self.__waiting_conf.has_key(id): | |
695 error (_("Attempt to register two callbacks for the same confirmation")) | |
696 else: | |
697 self.__waiting_conf[id] = cb | |
698 self.bridge.askConfirmation(type, id, data) | |
699 | |
700 | |
701 def confirmationAnswer(self, id, accepted, data): | |
702 """Called by frontends to answer confirmation requests""" | |
703 debug (_("Received confirmation answer for id [%(id)s]: %(success)s") % {'id': id, 'success':_("accepted") if accepted else _("refused")}) | |
704 if not self.__waiting_conf.has_key(id): | |
705 error (_("Received an unknown confirmation")) | |
706 else: | |
707 cb = self.__waiting_conf[id] | |
708 del self.__waiting_conf[id] | |
709 cb(id, accepted, data) | |
710 | |
711 def registerProgressCB(self, id, CB): | |
712 """Register a callback called when progress is requested for id""" | |
713 self.__progress_cb_map[id] = CB | |
714 | |
715 def removeProgressCB(self, id): | |
716 """Remove a progress callback""" | |
717 if not self.__progress_cb_map.has_key(id): | |
718 error (_("Trying to remove an unknow progress callback")) | |
719 else: | |
720 del self.__progress_cb_map[id] | |
721 | |
722 def getProgress(self, id): | |
723 """Return a dict with progress information | |
724 data['position'] : current possition | |
725 data['size'] : end_position | |
726 """ | |
727 data = {} | |
728 try: | |
729 self.__progress_cb_map[id](data) | |
730 except KeyError: | |
731 pass | |
732 #debug("Requested progress for unknown id") | |
733 return data | |
734 | |
735 def registerGeneralCB(self, name, CB): | |
736 """Register a callback called for general reason""" | |
737 self.__general_cb_map[name] = CB | |
738 | |
739 def removeGeneralCB(self, name): | |
740 """Remove a general callback""" | |
741 if not self.__general_cb_map.has_key(name): | |
742 error (_("Trying to remove an unknow general callback")) | |
743 else: | |
744 del self.__general_cb_map[name] | |
745 | |
746 def callGeneralCB(self, name, *args, **kwargs): | |
747 """Call general function back""" | |
748 try: | |
749 return self.__general_cb_map[name](*args, **kwargs) | |
750 except KeyError: | |
751 error(_("Trying to call unknown function (%s)") % name) | |
752 return None | |
753 | |
754 #Menus management | |
755 | |
756 def importMenu(self, category, name, callback, help_string = "", type = "NORMAL"): | |
757 """register a new menu for frontends | |
758 @param category: category of the menu | |
759 @param name: menu item entry | |
760 @param callback: method to be called when menuitem is selected""" | |
761 if self.menus.has_key((category,name)): | |
762 error ("Want to register a menu which already existe") | |
763 return | |
764 self.menus[(category,name,type)] = {'callback':callback, 'help_string':help_string, 'type':type} | |
765 | |
766 def getMenus(self): | |
767 """Return all menus registered""" | |
768 return self.menus.keys() | |
769 | |
770 def getMenuHelp(self, category, name, type="NORMAL"): | |
771 """return the help string of the menu""" | |
772 try: | |
773 return self.menus[(category,name,type)]['help_string'] | |
774 except KeyError: | |
775 error (_("Trying to access an unknown menu")) | |
776 return "" | |
777 | |
778 def callMenu(self, category, name, type="NORMAL", profile_key='@DEFAULT@'): | |
779 """return the help string of the menu""" | |
780 profile = self.memory.getProfileName(profile_key) | |
781 if not profile_key: | |
782 error (_('Non-exsitant profile')) | |
783 return "" | |
784 if self.menus.has_key((category,name,type)): | |
785 id = self.get_next_id() | |
786 self.menus[(category,name,type)]['callback'](id, profile) | |
787 return id | |
788 else: | |
789 error (_("Trying to access an unknown menu (%(category)s/%(name)s/%(type)s)")%{'category':category, 'name':name,'type':type}) | |
790 return "" | |
791 | |
792 | |
793 | |
794 application = service.Application('SàT') | |
795 service = SAT() | |
796 service.setServiceParent(application) |