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)