root/trunk/src/remote_control.py

Revision 10549, 22.3 kB (checked in by asterix, 6 weeks ago)

revert thorstenp patches for now. They introduce bugs.

  • Property svn:eol-style set to LF
Line 
1# -*- coding:utf-8 -*-
2## src/remote_control.py
3##
4## Copyright (C) 2005-2006 Andrew Sayman <lorien420 AT myrealbox.com>
5##                         Dimitur Kirov <dkirov AT gmail.com>
6##                         Nikos Kouremenos <kourem AT gmail.com>
7## Copyright (C) 2005-2008 Yann Leboulanger <asterix AT lagaule.org>
8## Copyright (C) 2006-2007 Travis Shirk <travis AT pobox.com>
9## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
10## Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
11##                    Julien Pivotto <roidelapluie AT gmail.com>
12## Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
13##
14## This file is part of Gajim.
15##
16## Gajim is free software; you can redistribute it and/or modify
17## it under the terms of the GNU General Public License as published
18## by the Free Software Foundation; version 3 only.
19##
20## Gajim is distributed in the hope that it will be useful,
21## but WITHOUT ANY WARRANTY; without even the implied warranty of
22## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23## GNU General Public License for more details.
24##
25## You should have received a copy of the GNU General Public License
26## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
27##
28
29import gobject
30import os
31
32from common import gajim
33from common import helpers
34from time import time
35from dialogs import AddNewContactWindow, NewChatDialog, JoinGroupchatWindow
36
37from common import dbus_support
38if dbus_support.supported:
39        import dbus
40        if dbus_support:
41                import dbus.service
42                import dbus.glib
43
44INTERFACE = 'org.gajim.dbus.RemoteInterface'
45OBJ_PATH = '/org/gajim/dbus/RemoteObject'
46SERVICE = 'org.gajim.dbus'
47
48# type mapping
49
50# in most cases it is a utf-8 string
51DBUS_STRING = dbus.String
52
53# general type (for use in dicts, where all values should have the same type)
54DBUS_BOOLEAN = dbus.Boolean
55DBUS_DOUBLE = dbus.Double
56DBUS_INT32 = dbus.Int32
57# dictionary with string key and binary value
58DBUS_DICT_SV = lambda : dbus.Dictionary({}, signature="sv")
59# dictionary with string key and value
60DBUS_DICT_SS = lambda : dbus.Dictionary({}, signature="ss")
61# empty type (there is no equivalent of None on D-Bus, but historically gajim
62# used 0 instead)
63DBUS_NONE = lambda : dbus.Int32(0)
64
65def get_dbus_struct(obj):
66        ''' recursively go through all the items and replace
67        them with their casted dbus equivalents
68        '''
69        if obj is None:
70                return DBUS_NONE()
71        if isinstance(obj, (unicode, str)):
72                return DBUS_STRING(obj)
73        if isinstance(obj, int):
74                return DBUS_INT32(obj)
75        if isinstance(obj, float):
76                return DBUS_DOUBLE(obj)
77        if isinstance(obj, bool):
78                return DBUS_BOOLEAN(obj)
79        if isinstance(obj, (list, tuple)):
80                result = dbus.Array([get_dbus_struct(i) for i in obj],
81                        signature='v')
82                if result == []:
83                        return DBUS_NONE()
84                return result
85        if isinstance(obj, dict):
86                result = DBUS_DICT_SV()
87                for key, value in obj.items():
88                        result[DBUS_STRING(key)] = get_dbus_struct(value)
89                if result == {}:
90                        return DBUS_NONE()
91                return result
92        # unknown type
93        return DBUS_NONE() 
94
95class Remote:
96        def __init__(self):
97                self.signal_object = None
98                session_bus = dbus_support.session_bus.SessionBus()
99
100                bus_name = dbus.service.BusName(SERVICE, bus=session_bus)
101                self.signal_object = SignalObject(bus_name)
102
103        def raise_signal(self, signal, arg):
104                if self.signal_object:
105                        getattr(self.signal_object, signal)(get_dbus_struct(arg))
106
107
108class SignalObject(dbus.service.Object):
109        ''' Local object definition for /org/gajim/dbus/RemoteObject.
110        (This docstring is not be visible, because the clients can access only the remote object.)'''
111
112        def __init__(self, bus_name):
113                self.first_show = True
114                self.vcard_account = None
115
116                # register our dbus API
117                dbus.service.Object.__init__(self, bus_name, OBJ_PATH)
118
119        @dbus.service.signal(INTERFACE, signature='av')
120        def Roster(self, account_and_data):
121                pass
122
123        @dbus.service.signal(INTERFACE, signature='av')
124        def AccountPresence(self, status_and_account):
125                pass
126
127        @dbus.service.signal(INTERFACE, signature='av')
128        def ContactPresence(self, account_and_array):
129                pass
130
131        @dbus.service.signal(INTERFACE, signature='av')
132        def ContactAbsence(self, account_and_array):
133                pass
134
135        @dbus.service.signal(INTERFACE, signature='av')
136        def ContactStatus(self, account_and_array):
137                pass
138
139        @dbus.service.signal(INTERFACE, signature='av')
140        def NewMessage(self, account_and_array):
141                pass
142
143        @dbus.service.signal(INTERFACE, signature='av')
144        def Subscribe(self, account_and_array):
145                pass
146
147        @dbus.service.signal(INTERFACE, signature='av')
148        def Subscribed(self, account_and_array):
149                pass
150
151        @dbus.service.signal(INTERFACE, signature='av')
152        def Unsubscribed(self, account_and_jid):
153                pass
154
155        @dbus.service.signal(INTERFACE, signature='av')
156        def NewAccount(self, account_and_array):
157                pass
158
159        @dbus.service.signal(INTERFACE, signature='av')
160        def VcardInfo(self, account_and_vcard):
161                pass
162
163        @dbus.service.signal(INTERFACE, signature='av')
164        def LastStatusTime(self, account_and_array):
165                pass
166
167        @dbus.service.signal(INTERFACE, signature='av')
168        def OsInfo(self, account_and_array):
169                pass
170
171        @dbus.service.signal(INTERFACE, signature='av')
172        def GCPresence(self, account_and_array):
173                pass
174
175        @dbus.service.signal(INTERFACE, signature='av')
176        def GCMessage(self, account_and_array):
177                pass
178
179        @dbus.service.signal(INTERFACE, signature='av')
180        def RosterInfo(self, account_and_array):
181                pass
182
183        @dbus.service.signal(INTERFACE, signature='av')
184        def NewGmail(self, account_and_array):
185                pass
186
187        def raise_signal(self, signal, arg):
188                '''raise a signal, with a single argument of unspecified type
189                Instead of obj.raise_signal("Foo", bar), use obj.Foo(bar).'''
190                getattr(self, signal)(arg)
191
192        @dbus.service.method(INTERFACE, in_signature='s', out_signature='s')
193        def get_status(self, account):
194                '''Returns status (show to be exact) which is the global one
195                unless account is given'''
196                if not account:
197                        # If user did not ask for account, returns the global status
198                        return DBUS_STRING(helpers.get_global_show())
199                # return show for the given account
200                index = gajim.connections[account].connected
201                return DBUS_STRING(gajim.SHOW_LIST[index])
202
203        @dbus.service.method(INTERFACE, in_signature='s', out_signature='s')
204        def get_status_message(self, account):
205                '''Returns status which is the global one
206                unless account is given'''
207                if not account:
208                        # If user did not ask for account, returns the global status
209                        return DBUS_STRING(str(helpers.get_global_status()))
210                # return show for the given account
211                status = gajim.connections[account].status
212                return DBUS_STRING(status)
213
214        def _get_account_and_contact(self, account, jid):
215                '''get the account (if not given) and contact instance from jid'''
216                connected_account = None
217                contact = None
218                accounts = gajim.contacts.get_accounts()
219                # if there is only one account in roster, take it as default
220                # if user did not ask for account
221                if not account and len(accounts) == 1:
222                        account = accounts[0]
223                if account:
224                        if gajim.connections[account].connected > 1: # account is connected
225                                connected_account = account
226                                contact = gajim.contacts.get_contact_with_highest_priority(account,
227                                        jid)
228                else:
229                        for account in accounts:
230                                contact = gajim.contacts.get_contact_with_highest_priority(account,
231                                        jid)
232                                if contact and gajim.connections[account].connected > 1:
233                                        # account is connected
234                                        connected_account = account
235                                        break
236                if not contact:
237                        contact = jid
238
239                return connected_account, contact
240
241        def _get_account_for_groupchat(self, account, room_jid):
242                '''get the account which is connected to groupchat (if not given)
243                or check if the given account is connected to the groupchat'''
244                connected_account = None
245                accounts = gajim.contacts.get_accounts()
246                # if there is only one account in roster, take it as default
247                # if user did not ask for account
248                if not account and len(accounts) == 1:
249                        account = accounts[0]
250                if account:
251                        if gajim.connections[account].connected > 1 and \
252                        room_jid in gajim.gc_connected[account] and \
253                        gajim.gc_connected[account][room_jid]:
254                                # account and groupchat are connected
255                                connected_account = account
256                else:
257                        for account in accounts:
258                                if gajim.connections[account].connected > 1 and \
259                                room_jid in gajim.gc_connected[account] and \
260                                gajim.gc_connected[account][room_jid]:
261                                        # account and groupchat are connected
262                                        connected_account = account
263                                        break
264                return connected_account
265
266        @dbus.service.method(INTERFACE, in_signature='sss', out_signature='b')
267        def send_file(self, file_path, jid, account):
268                '''send file, located at 'file_path' to 'jid', using account
269                (optional) 'account' '''
270                jid = self._get_real_jid(jid, account)
271                connected_account, contact = self._get_account_and_contact(account, jid)
272
273                if connected_account:
274                        if file_path.startswith('file://'):
275                                file_path=file_path[7:]
276                        if os.path.isfile(file_path): # is it file?
277                                gajim.interface.instances['file_transfers'].send_file(
278                                        connected_account, contact, file_path)
279                                return DBUS_BOOLEAN(True)
280                return DBUS_BOOLEAN(False)
281
282        def _send_message(self, jid, message, keyID, account, type_ = 'chat',
283        subject = None):
284                '''can be called from send_chat_message (default when send_message)
285                or send_single_message'''
286                if not jid or not message:
287                        return DBUS_BOOLEAN(False)
288                if not keyID:
289                        keyID = ''
290
291                connected_account, contact = self._get_account_and_contact(account, jid)
292                if connected_account:
293                        connection = gajim.connections[connected_account]
294                        connection.send_message(jid, message, keyID, type_, subject)
295                        return DBUS_BOOLEAN(True)
296                return DBUS_BOOLEAN(False)
297
298        @dbus.service.method(INTERFACE, in_signature='ssss', out_signature='b')
299        def send_chat_message(self, jid, message, keyID, account):
300                '''Send chat 'message' to 'jid', using account (optional) 'account'.
301                if keyID is specified, encrypt the message with the pgp key '''
302                jid = self._get_real_jid(jid, account)
303                return self._send_message(jid, message, keyID, account)
304
305        @dbus.service.method(INTERFACE, in_signature='sssss', out_signature='b')
306        def send_single_message(self, jid, subject, message, keyID, account):
307                '''Send single 'message' to 'jid', using account (optional) 'account'.
308                if keyID is specified, encrypt the message with the pgp key '''
309                jid = self._get_real_jid(jid, account)
310                return self._send_message(jid, message, keyID, account, type, subject)
311
312        @dbus.service.method(INTERFACE, in_signature='sss', out_signature='b')
313        def send_groupchat_message(self, room_jid, message, account):
314                '''Send 'message' to groupchat 'room_jid',
315                using account (optional) 'account'.'''
316                if not room_jid or not message:
317                        return DBUS_BOOLEAN(False)
318                connected_account = self._get_account_for_groupchat(account, room_jid)
319                if connected_account:
320                        connection = gajim.connections[connected_account]
321                        connection.send_gc_message(room_jid, message)
322                        return DBUS_BOOLEAN(True)
323                return DBUS_BOOLEAN(False)
324
325        @dbus.service.method(INTERFACE, in_signature='ss', out_signature='b')
326        def open_chat(self, jid, account):
327                '''Shows the tabbed window for new message to 'jid', using account
328                (optional) 'account' '''
329                if not jid:
330                        raise MissingArgument
331                        return DBUS_BOOLEAN(False)
332                jid = self._get_real_jid(jid, account)
333                try:
334                        jid = helpers.parse_jid(jid)
335                except Exception:
336                        # Jid is not conform, ignore it
337                        return DBUS_BOOLEAN(False)
338
339                if account:
340                        accounts = [account]
341                else:
342                        accounts = gajim.connections.keys()
343                        if len(accounts) == 1:
344                                account = accounts[0]
345                connected_account = None
346                first_connected_acct = None
347                for acct in accounts:
348                        if gajim.connections[acct].connected > 1: # account is  online
349                                contact = gajim.contacts.get_first_contact_from_jid(acct, jid)
350                                if gajim.interface.msg_win_mgr.has_window(jid, acct):
351                                        connected_account = acct
352                                        break
353                                # jid is in roster
354                                elif contact:
355                                        connected_account = acct
356                                        break
357                                # we send the message to jid not in roster, because account is
358                                # specified, or there is only one account
359                                elif account: 
360                                        connected_account = acct
361                                elif first_connected_acct is None:
362                                        first_connected_acct = acct
363
364                # if jid is not a conntact, open-chat with first connected account
365                if connected_account is None and first_connected_acct:
366                        connected_account = first_connected_acct
367
368                if connected_account:
369                        gajim.interface.new_chat_from_jid(connected_account, jid)
370                        # preserve the 'steal focus preservation'
371                        win = gajim.interface.msg_win_mgr.get_window(jid,
372                                connected_account).window
373                        if win.get_property('visible'):
374                                win.window.focus()
375                        return DBUS_BOOLEAN(True)
376                return DBUS_BOOLEAN(False)
377
378        @dbus.service.method(INTERFACE, in_signature='sss', out_signature='b')
379        def change_status(self, status, message, account):
380                ''' change_status(status, message, account). account is optional -
381                if not specified status is changed for all accounts. '''
382                if status not in ('offline', 'online', 'chat', 
383                        'away', 'xa', 'dnd', 'invisible'):
384                        return DBUS_BOOLEAN(False)
385                if account:
386                        gobject.idle_add(gajim.interface.roster.send_status, account,
387                                status, message)
388                else:
389                        # account not specified, so change the status of all accounts
390                        for acc in gajim.contacts.get_accounts():
391                                if not gajim.config.get_per('accounts', acc,
392                                'sync_with_global_status'):
393                                        continue
394                                gobject.idle_add(gajim.interface.roster.send_status, acc,
395                                        status, message)
396                return DBUS_BOOLEAN(False)
397
398        @dbus.service.method(INTERFACE, in_signature='', out_signature='')
399        def show_next_pending_event(self):
400                '''Show the window(s) with next pending event in tabbed/group chats.'''
401                if gajim.events.get_nb_events():
402                        gajim.interface.systray.handle_first_event()
403
404        @dbus.service.method(INTERFACE, in_signature='s', out_signature='a{sv}')
405        def contact_info(self, jid):
406                '''get vcard info for a contact. Return cached value of the vcard.
407                '''
408                if not isinstance(jid, unicode):
409                        jid = unicode(jid)
410                if not jid:
411                        raise MissingArgument
412                        return DBUS_DICT_SV()
413                jid = self._get_real_jid(jid)
414
415                cached_vcard = gajim.connections.values()[0].get_cached_vcard(jid)
416                if cached_vcard:
417                        return get_dbus_struct(cached_vcard)
418
419                # return empty dict
420                return DBUS_DICT_SV()
421
422        @dbus.service.method(INTERFACE, in_signature='', out_signature='as')
423        def list_accounts(self):
424                '''list register accounts'''
425                result = gajim.contacts.get_accounts()
426                result_array = dbus.Array([], signature='s')
427                if result and len(result) > 0:
428                        for account in result:
429                                result_array.append(DBUS_STRING(account))
430                return result_array
431
432        @dbus.service.method(INTERFACE, in_signature='s', out_signature='a{ss}')
433        def account_info(self, account):
434                '''show info on account: resource, jid, nick, prio, message'''
435                result = DBUS_DICT_SS()
436                if account in gajim.connections:
437                        # account is valid
438                        con = gajim.connections[account]
439                        index = con.connected
440                        result['status'] = DBUS_STRING(gajim.SHOW_LIST[index])
441                        result['name'] = DBUS_STRING(con.name)
442                        result['jid'] = DBUS_STRING(gajim.get_jid_from_account(con.name))
443                        result['message'] = DBUS_STRING(con.status)
444                        result['priority'] = DBUS_STRING(unicode(con.priority))
445                        result['resource'] = DBUS_STRING(unicode(gajim.config.get_per(
446                                'accounts', con.name, 'resource')))
447                return result
448
449        @dbus.service.method(INTERFACE, in_signature='s', out_signature='aa{sv}')
450        def list_contacts(self, account):
451                '''list all contacts in the roster. If the first argument is specified,
452                then return the contacts for the specified account'''
453                result = dbus.Array([], signature='aa{sv}')
454                accounts = gajim.contacts.get_accounts()
455                if len(accounts) == 0:
456                        return result
457                if account:
458                        accounts_to_search = [account]
459                else:
460                        accounts_to_search = accounts
461                for acct in accounts_to_search:
462                        if acct in accounts:
463                                for jid in gajim.contacts.get_jid_list(acct):
464                                        item = self._contacts_as_dbus_structure(
465                                                gajim.contacts.get_contacts(acct, jid))
466                                        if item:
467                                                result.append(item)
468                return result
469
470        @dbus.service.method(INTERFACE, in_signature='', out_signature='')
471        def toggle_roster_appearance(self):
472                ''' shows/hides the roster window '''
473                win = gajim.interface.roster.window
474                if win.get_property('visible'):
475                        gobject.idle_add(win.hide)
476                else:
477                        win.present()
478                        # preserve the 'steal focus preservation'
479                        if self._is_first():
480                                win.window.focus()
481                        else:
482                                win.window.focus(long(time()))
483
484        @dbus.service.method(INTERFACE, in_signature='', out_signature='')
485        def toggle_ipython(self):
486                ''' shows/hides the ipython window '''
487                win = gajim.ipython_window
488                if win:
489                        if win.window.is_visible():
490                                gobject.idle_add(win.hide)
491                        else:
492                                win.show_all()
493                                win.present()
494                else:
495                        gajim.interface.create_ipython_window()
496
497        @dbus.service.method(INTERFACE, in_signature='', out_signature='a{ss}')
498        def prefs_list(self):
499                prefs_dict = DBUS_DICT_SS()
500                def get_prefs(data, name, path, value):
501                        if value is None:
502                                return
503                        key = ''
504                        if path is not None:
505                                for node in path:
506                                        key += node + '#'
507                        key += name
508                        prefs_dict[DBUS_STRING(key)] = DBUS_STRING(value[1])
509                gajim.config.foreach(get_prefs)
510                return prefs_dict
511
512        @dbus.service.method(INTERFACE, in_signature='', out_signature='b')
513        def prefs_store(self):
514                try:
515                        gajim.interface.save_config()
516                except Exception, e:
517                        return DBUS_BOOLEAN(False)
518                return DBUS_BOOLEAN(True)
519
520        @dbus.service.method(INTERFACE, in_signature='s', out_signature='b')
521        def prefs_del(self, key):
522                if not key:
523                        return DBUS_BOOLEAN(False)
524                key_path = key.split('#', 2)
525                if len(key_path) != 3:
526                        return DBUS_BOOLEAN(False)
527                if key_path[2] == '*':
528                        gajim.config.del_per(key_path[0], key_path[1])
529                else:
530                        gajim.config.del_per(key_path[0], key_path[1], key_path[2])
531                return DBUS_BOOLEAN(True)
532
533        @dbus.service.method(INTERFACE, in_signature='s', out_signature='b')
534        def prefs_put(self, key):
535                if not key:
536                        return DBUS_BOOLEAN(False)
537                key_path = key.split('#', 2)
538                if len(key_path) < 3:
539                        subname, value = key.split('=', 1)
540                        gajim.config.set(subname, value)
541                        return DBUS_BOOLEAN(True)
542                subname, value = key_path[2].split('=', 1)
543                gajim.config.set_per(key_path[0], key_path[1], subname, value)
544                return DBUS_BOOLEAN(True)
545
546        @dbus.service.method(INTERFACE, in_signature='ss', out_signature='b')
547        def add_contact(self, jid, account):
548                if account:
549                        if account in gajim.connections and \
550                                gajim.connections[account].connected > 1:
551                                # if given account is active, use it
552                                AddNewContactWindow(account = account, jid = jid)
553                        else:
554                                # wrong account
555                                return DBUS_BOOLEAN(False)
556                else:
557                        # if account is not given, show account combobox
558                        AddNewContactWindow(account = None, jid = jid)
559                return DBUS_BOOLEAN(True)
560
561        @dbus.service.method(INTERFACE, in_signature='ss', out_signature='b')
562        def remove_contact(self, jid, account):
563                jid = self._get_real_jid(jid, account)
564                accounts = gajim.contacts.get_accounts()
565
566                # if there is only one account in roster, take it as default
567                if account:
568                        accounts = [account]
569                contact_exists = False
570                for account in accounts:
571                        contacts = gajim.contacts.get_contacts(account, jid)
572                        if contacts:
573                                gajim.connections[account].unsubscribe(jid)
574                                for contact in contacts:
575                                        gajim.interface.roster.remove_contact(contact, account)
576                                gajim.contacts.remove_jid(account, jid)
577                                contact_exists = True
578                return DBUS_BOOLEAN(contact_exists)
579
580        def _is_first(self):
581                if self.first_show:
582                        self.first_show = False
583                        return True
584                return False
585
586        def _get_real_jid(self, jid, account = None):
587                '''get the real jid from the given one: removes xmpp: or get jid from nick
588                if account is specified, search only in this account
589                '''
590                if account:
591                        accounts = [account]
592                else:
593                        accounts = gajim.connections.keys()
594                if jid.startswith('xmpp:'):
595                        return jid[5:] # len('xmpp:') = 5
596                nick_in_roster = None # Is jid a nick ?
597                for account in accounts:
598                        # Does jid exists in roster of one account ?
599                        if gajim.contacts.get_contacts(account, jid):
600                                return jid
601                        if not nick_in_roster:
602                                # look in all contact if one has jid as nick
603                                for jid_ in gajim.contacts.get_jid_list(account):
604                                        c = gajim.contacts.get_contacts(account, jid_)
605                                        if c[0].name == jid:
606                                                nick_in_roster = jid_
607                                                break
608                if nick_in_roster:
609                        # We have not found jid in roster, but we found is as a nick
610                        return nick_in_roster
611                # We have not found it as jid nor as nick, probably a not in roster jid
612                return jid
613
614        def _contacts_as_dbus_structure(self, contacts):
615                ''' get info from list of Contact objects and create dbus dict '''
616                if not contacts:
617                        return None
618                prim_contact = None # primary contact
619                for contact in contacts:
620                        if prim_contact is None or contact.priority > prim_contact.priority:
621                                prim_contact = contact
622                contact_dict = DBUS_DICT_SV()
623                contact_dict['name'] = DBUS_STRING(prim_contact.name)
624                contact_dict['show'] = DBUS_STRING(prim_contact.show)
625                contact_dict['jid'] = DBUS_STRING(prim_contact.jid)
626                if prim_contact.keyID:
627                        keyID = None
628                        if len(prim_contact.keyID