diff --git a/scripts/gajim-remote.py b/scripts/gajim-remote.py new file mode 100755 index 000000000..b6388f6a6 --- /dev/null +++ b/scripts/gajim-remote.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +## scripts/gajim-remote.py +## +## Gajim Team: +## - Yann Le Boulanger +## - Vincent Hanquez +## - Nikos Kouremenos +## +## This file was initially written by Dimitur Kirov +## +## Copyright (C) 2003-2005 Gajim Team +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +# gajim-remote help will show you the 'dbus' api +# That api is also usable, and the code to use it follows.. + +import sys +import gtk +import gobject + +def send_error(error_message): + sys.stderr.write(error_message+'\n') + sys.stderr.flush() + sys.exit(1) + +try: + import dbus +except: + send_error('Dbus is not supported.\n') + +_version = getattr(dbus, 'version', (0, 20, 0)) + +OBJ_PATH = '/org/gajim/dbus/RemoteObject' +INTERFACE = 'org.gajim.dbus.RemoteInterface' +SERVICE = 'org.gajim.dbus' +commands = ['help', 'show_roster', 'show_waiting', 'list_contacts', + 'list_accounts', 'change_status', 'new_message', 'send_message', + 'contact_info'] + +if _version[1] >= 41: + import dbus.service + import dbus.glib + +def compose_help(): + str = 'Usage: '+ sys.argv[0] + ' command [arguments]\n' + str += 'Command must be one of:\n' + for command in commands: + str += '\t' + command +'\n' + return str + +def show_vcard_info(*args, **keyword): + if _version[1] >= 30: + print args[0] + else: + if args and len(args) >= 5: + print args[4].get_args_list() + + # remove_signal_receiver is broken in lower versions, + # so we leave the leak - nothing can be done + if _version[1] >= 41: + sbus.remove_signal_receiver(show_vcard_info, 'VcardInfo', INTERFACE, + SERVICE, OBJ_PATH) + + gtk.main_quit() + +def gtk_quit(): + if _version[1] >= 41: + sbus.remove_signal_receiver(show_vcard_info, 'VcardInfo', INTERFACE, + SERVICE, OBJ_PATH) + gtk.main_quit() + + +argv_len = len(sys.argv) + +if argv_len < 2: + send_error('Usage: ' + sys.argv[0] + ' command [arguments]') + +if sys.argv[1] not in commands: + send_error(compose_help()) + +command = sys.argv[1] + +if command == 'help': + print compose_help() + sys.exit() + +try: + sbus = dbus.SessionBus() +except: + send_error('Session bus is not available.\n') + + +if _version[1] >= 30 and _version[1] <= 42: + object = sbus.get_object(SERVICE, OBJ_PATH) + interface = dbus.Interface(object, INTERFACE) +elif _version[1] < 30: + service = sbus.get_service(SERVICE) + interface = service.get_object(OBJ_PATH, INTERFACE) +else: + send_error('Unknow dbus version: '+ _version) + +method = interface.__getattr__(sys.argv[1]) # get the function asked + +if command == 'contact_info': + if argv_len < 3: + send_error("Missing argument \'contact_jid'") + try: + id = sbus.add_signal_receiver(show_vcard_info, 'VcardInfo', + INTERFACE, SERVICE, OBJ_PATH) + except: + send_error('Service not available') + gobject.timeout_add(5000, gtk_quit) + gtk.main() + +#FIXME: gajim-remote.py change_status help to inform what it does with optional arg (account). the same for rest of methods that accept args + +#FIXME - didn't find more clever way for the below 8 lines of code. +# method(sys.argv[2:]) doesn't work, cos sys.argv[2:] is a tuple +try: + if argv_len == 2: + res = method() + elif argv_len == 3: + res = method(str(sys.argv[2])) + elif argv_len == 4: + res = method(sys.argv[2], sys.argv[3]) + elif argv_len == 5: + res = method(sys.argv[2], sys.argv[3], sys.argv[4]) + if res: + print res +except: + send_error('Service not available') diff --git a/src/common/config.py b/src/common/config.py index 4773b1fb8..85bd4ea97 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -116,6 +116,7 @@ class Config: 'search_engine': [opt_str, 'http://www.google.com/search?&q='], 'dictionary_url': [opt_str, 'http://dictionary.reference.com/search?q='], 'always_english_wikipedia': [opt_bool, False], + 'use_dbus': [opt_bool, False], # allow control via dbus service } __options_per_key = { diff --git a/src/gajim.py b/src/gajim.py index ec6363829..e2b29055f 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -174,6 +174,8 @@ class Interface: #('ROSTER', account, array) self.roster.mklists(data, account) self.roster.draw_roster() + if self.remote: + self.remote.raise_signal('Roster', (account, data)) def handle_event_warning(self, unused, data): #('WARNING', account, (title_text, section_text)) @@ -208,6 +210,8 @@ class Interface: else: self.allow_notifications[account] = False self.roster.on_status_changed(account, status) + if self.remote: + self.remote.raise_signal('AccountPresence', (status, account)) def handle_event_notify(self, account, array): #('NOTIFY', account, (jid, status, message, resource, priority, keyID, @@ -306,6 +310,8 @@ class Interface: instance = dialogs.PopupNotificationWindow(self, _('Contact Signed In'), jid, account) self.roster.popup_notification_windows.append(instance) + if self.remote: + self.remote.raise_signal('ContactPresence', (account, array)) elif old_show > 1 and new_show < 2: if gajim.config.get_per('soundevents', 'contact_disconnected', @@ -324,6 +330,8 @@ class Interface: instance = dialogs.PopupNotificationWindow(self, _('Contact Signed Out'), jid, account) self.roster.popup_notification_windows.append(instance) + if self.remote: + self.remote.raise_signal('ContactAbsence', (account, array)) elif self.windows[account]['gc'].has_key(ji): #it is a groupchat presence @@ -332,6 +340,8 @@ class Interface: self.windows[account]['gc'][ji].chg_contact_status(ji, resource, array[1], array[2], array[6], array[7], array[8], array[9], array[10], array[11], array[12], account) + if self.remote: + self.remote.raise_signal('GCPresence', (account, array)) def handle_event_msg(self, account, array): #('MSG', account, (contact, msg, time, encrypted, msg_type, subject)) @@ -394,6 +404,8 @@ class Interface: if gajim.config.get_per('soundevents', 'next_message_received', 'enabled') and not first: self.play_sound('next_message_received') + if self.remote: + self.remote.raise_signal('NewMessage', (account, array)) def handle_event_msgerror(self, account, array): #('MSGERROR', account, (jid, error_code, error_msg, msg, time)) @@ -438,6 +450,8 @@ class Interface: def handle_event_subscribe(self, account, array): #('SUBSCRIBE', account, (jid, text)) dialogs.SubscriptionRequestWindow(self, array[0], array[1], account) + if self.remote: + self.remote.raise_signal('Subscribe', (account, array)) def handle_event_subscribed(self, account, array): #('SUBSCRIBED', account, (jid, resource)) @@ -466,10 +480,14 @@ class Interface: dialogs.InformationDialog(_('Authorization accepted'), _('The contact "%s" has authorized you to see his status.') % jid).get_response() + if self.remote: + self.remote.raise_signal('Subscribed', (account, array)) def handle_event_unsubscribed(self, account, jid): dialogs.InformationDialog(_('Contact "%s" removed subscription from you') % jid, _('You will always see him as offline.')).get_response() + if self.remote: + self.remote.raise_signal('Unsubscribed', (account, array)) def handle_event_agent_info(self, account, array): #('AGENT_INFO', account, (agent, identities, features, items)) @@ -522,6 +540,8 @@ class Interface: if self.windows.has_key('accounts'): self.windows['accounts'].init_accounts() self.roster.draw_roster() + if self.remote: + self.remote.raise_signal('NewAccount', (account, array)) def handle_event_quit(self, p1, p2): self.roster.quit_gtkgui_plugin() @@ -550,6 +570,8 @@ class Interface: if self.windows[account]['infos'].has_key(array[0]): self.windows[account]['infos'][array[0]].set_os_info(array[1], \ array[2], array[3]) + if self.remote: + self.remote.raise_signal('OsInfo', (account, array)) def handle_event_gc_msg(self, account, array): #('GC_MSG', account, (jid, msg, time)) @@ -565,6 +587,8 @@ class Interface: #message from someone self.windows[account]['gc'][jid].print_conversation(array[1], jid, \ jids[1], array[2]) + if self.remote: + self.remote.raise_signal('GCMessage', (account, array)) def handle_event_gc_subject(self, account, array): #('GC_SUBJECT', account, (jid, subject)) @@ -610,6 +634,8 @@ class Interface: if array[4]: user.groups = array[4] self.roster.draw_contact(jid, account) + if self.remote: + self.remote.raise_signal('RosterInfo', (account, array)) def handle_event_bookmarks(self, account, bms): #('BOOKMARKS', account, [{name,jid,autojoin,password,nick}, {}]) @@ -836,7 +862,7 @@ class Interface: for account in gajim.config.get_per('accounts'): gajim.connections[account] = common.connection.Connection(account) - + if gtk.pygtk_version >= (2, 6, 0): gtk.about_dialog_set_email_hook(self.on_launch_browser_mailer, 'mail') gtk.about_dialog_set_url_hook(self.on_launch_browser_mailer, 'url') @@ -859,6 +885,12 @@ class Interface: gajim.last_message_time[a] = {} self.roster = roster_window.RosterWindow(self) + if gajim.config.get('use_dbus'): + import remote_control + self.remote = remote_control.Remote(self) + else: + self.remote = None + path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps/gajim.png') pix = gtk.gdk.pixbuf_new_from_file(path_to_file) gtk.window_set_default_icon(pix) # set the icon to all newly opened windows @@ -882,7 +914,6 @@ class Interface: self.systray = systray.Systray(self) if self.systray_capabilities and gajim.config.get('trayicon'): self.show_systray() - if gajim.config.get('check_for_new_version'): check_for_new_version.Check_for_new_version_dialog(self) diff --git a/src/remote_control.py b/src/remote_control.py new file mode 100644 index 000000000..20ef57908 --- /dev/null +++ b/src/remote_control.py @@ -0,0 +1,353 @@ +## roster_window.py +## +## Gajim Team: +## - Yann Le Boulanger +## - Vincent Hanquez +## - Nikos Kouremenos +## +## Copyright (C) 2003-2005 Gajim Team +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +import gtk +import gobject + +from common import gajim +from time import time + +try: + import dbus +except: + pass + +_version = getattr(dbus, 'version', (0, 20, 0)) + +if _version >= (0, 41, 0): + import dbus.service + import dbus.glib # cause dbus 0.35+ doesn't return signal replies without it + DbusPrototype = dbus.service.Object +else: + DbusPrototype = dbus.Object + +INTERFACE = 'org.gajim.dbus.RemoteInterface' +OBJ_PATH = '/org/gajim/dbus/RemoteObject' +SERVICE = 'org.gajim.dbus' + +class Remote: + def __init__(self, plugin): + self.signal_object = None + if 'dbus' not in globals(): + print 'DBUS python bindings are missing in this computer.' + print 'DBUS capabilities of Gajim cannot be used' + return None + try: + session_bus = dbus.SessionBus() + except: + # FIXME: some status why remote is not supported + # When dbus 0.2.x is obsolete + return None + + if _version[1] >= 41: + service = dbus.service.BusName(SERVICE, bus=session_bus) + self.signal_object = SignalObject(service, plugin) + elif _version[1] <= 40: + service=dbus.Service(SERVICE, session_bus) + self.signal_object = SignalObject(service, plugin) + + def raise_signal(self, signal, arg): + if self.signal_object: + self.signal_object.raise_signal(signal, repr(arg)) + + +class SignalObject(DbusPrototype): + _version = getattr(dbus, 'version', (0, 20, 0)) + + def __init__(self, service, plugin): + self.plugin = plugin + self.first_show = True + self.contacts = self.plugin.roster.contacts + self.vcard_account = None + + # register our dbus API + if _version[1] >= 41: + DbusPrototype.__init__(self, service, OBJ_PATH) + elif _version[1] >= 30: + DbusPrototype.__init__(self, OBJ_PATH, service) + else: + DbusPrototype.__init__(self, OBJ_PATH, service, + [ self.show_roster, + self.show_waiting, + self.list_contacts, + self.list_accounts, + self.change_status, + self.new_message, + self.send_message, + self.contact_info + ]) + + def raise_signal(self, signal, arg): + ''' raise a signal, with a single string message ''' + if _version[1] >=30: + from dbus import dbus_bindings + message = dbus_bindings.Signal(OBJ_PATH, INTERFACE, signal) + iter = message.get_iter(True) + iter.append(arg) + self._connection.send(message) + else: + self.emit_signal(INTERFACE, signal, arg) + + + # signals + def VcardInfo(self, *vcard): + pass + + def send_message(self, *args): + ''' send_message(jid, message, keyID=None, account=None) + send 'message' to 'jid', using account (optional) 'account'. + if keyID is specified, encrypt the message with the pgp key ''' + jid, message, keyID, account = self._get_real_arguments(args, 4) + if not jid or not message: + return None # or raise error + if not keyID: + keyID = '' + if account: + self.plugin.connections[account].send_message(jid, message, keyID) + else: + for account in self.contacts.keys(): + if self.contacts[account].has_key(jid): + gajim.connections[account].send_message(jid, + message, keyID) + return True + return False + + def new_message(self, *args): + ''' new_message(jid, account=None) -> shows the tabbed window for new + message to 'jid', using account(optional) 'account ' ''' + jid, account = self._get_real_arguments(args, 2) + if not jid: + # FIXME: raise exception for missing argument (dbus0.3+) + return None + if account: + accounts = [account] + else: + accounts = self.contacts.keys() + + for account in accounts: + if self.plugin.windows[account]['chats'].has_key(jid): + self.plugin.windows[account]['chats'][jid].set_active_tab(jid) + break + elif self.contacts[account].has_key(jid): + self.plugin.roster.new_chat(self.contacts[account][jid][0], + account) + jid_data = self.plugin.windows[account]['chats'][jid] + jid_data.set_active_tab(jid) + jid_data.window.present() + # preserve the "steal focus preservation" + if self._is_first(): + jid_data.window.window.focus() + else: + jid_data.window.window.focus(long(time())) + break + + def change_status(self, *args, **keywords): + ''' change_status(status, message, account). account is optional - + if not specified status is changed for all accounts. ''' + status, message, account = self._get_real_arguments(args, 3) + if status not in ('offline', 'online', 'chat', + 'away', 'xa', 'dnd', 'invisible'): + # FIXME: raise exception for bad status (dbus0.3+) + return None + if account: + gobject.idle_add(self.plugin.roster.send_status, account, + status, message) + else: + # account not specified, so change the status of all accounts + for acc in self.contacts.keys(): + gobject.idle_add(self.plugin.roster.send_status, acc, + status, message) + return None + + def show_waiting(self, *args): + ''' Show the window(s) with waiting messages/chats. ''' + #FIXME: when systray is disabled this method does nothing. + if len(self.plugin.systray.jids) != 0: + account = self.plugin.systray.jids[0][0] + jid = self.plugin.systray.jids[0][1] + acc = self.plugin.windows[account] + jid_tab = None + if acc['gc'].has_key(jid): + jid_tab = acc['gc'][jid] + elif acc['chats'].has_key(jid): + jid_tab = acc['chats'][jid] + else: + self.plugin.roster.new_chat( + self.contacts[account][jid][0], account) + jid_tab = acc['chats'][jid] + if jid_tab: + jid_tab.set_active_tab(jid) + jid_tab.window.present() + # preserve the "steal focus preservation" + if self._is_first(): + jid_tab.window.window.focus() + else: + jid_tab.window.window.focus(long(time())) + + + def contact_info(self, *args): + ''' get vcard info for a contact. The second argument is optional and + stands for the account, to which this contact belongs. This method + return nothing. You have to register the 'VcartInfo' signal to get the + real vcard. ''' + jid, account = self._get_real_arguments(args, 2) + if not jid: + # FIXME: raise exception for missing argument (0.3+) + return None + if account: + accounts = [account] + else: + accounts = self.contacts.keys() + iq = None + + for account in accounts: + if self.contacts[account].has_key(jid): + self.vcard_account = account + gajim.connections[account].register_handler('VCARD', + self._receive_vcard) + iq = gajim.connections[account].request_vcard(jid) + break + return None + + def list_accounts(self, *args): + ''' list register accounts ''' + if self.contacts: + result = self.contacts.keys() + if result and len(result) > 0: + return result + return None + + + def list_contacts(self, *args): + ''' list all contacts in the roster. If the first argument is specified, + then return the contacts for the specified account ''' + [for_account] = self._get_real_arguments(args, 1) + result = [] + if not self.contacts or len(self.contacts) == 0: + return None + if for_account: + if self.contacts.has_key(for_account): + for jid in self.contacts[for_account]: + item = self._serialized_contacts( + self.contacts[for_account][jid]) + if item: + result.append(item) + else: + # "for_account: is not recognised:", + # FIXME: there can be a return status for this [0.3+] + return None + else: + for account in self.contacts: + for jid in self.contacts[account]: + item = self._serialized_contacts(self.contacts[account][jid]) + if item: + result.append(item) + # dbus 0.40 does not support return result as empty list + if result == []: + return None + return result + + def show_roster(self, *args): + ''' shows/hides the roster window ''' + win = self.plugin.roster.window + if win.get_property('visible'): + gobject.idle_add(win.hide) + else: + win.present() + # preserve the "steal focus preservation" + if self._is_first(): + win.window.focus() + else: + win.window.focus(long(time())) + + def _is_first(self): + if self.first_show: + self.first_show = False + return True + return False + + def _receive_vcard(self,account, array): + if self.vcard_account: + gajim.connections[self.vcard_account].unregister_handler('VCARD', + self._receive_vcard) + self.unregistered_vcard = None + if _version[1] >=30: + self.VcardInfo(repr(array)) + else: + self.emit_signal(INTERFACE, 'VcardInfo', + repr(array)) + + def _get_real_arguments(self, args, desired_length): + # supresses the first "message" argument, which is set in dbus 0.23 + if _version[1] == 20: + args=args[1:] + if desired_length > 0: + args = list(args) + args.extend([None] * (desired_length - len(args))) + args = args[:desired_length] + return args + + def _serialized_contacts(self, contacts): + ''' get info from list of Contact objects and create a serialized + dict for sending it over dbus ''' + if not contacts: + return None + prim_contact = None # primary contact + for contact in contacts: + if prim_contact == None or contact.priority > prim_contact.priority: + prim_contact = contact + contact_dict = {} + contact_dict['name'] = prim_contact.name + contact_dict['show'] = prim_contact.show + contact_dict['jid'] = prim_contact.jid + if prim_contact.keyID: + keyID = None + if len(prim_contact.keyID) == 8: + keyID = prim_contact.keyID + elif len(prim_contact.keyID) == 16: + keyID = prim_contact.keyID[8:] + if keyID: + contact_dict['openpgp'] = keyID + contact_dict['resources'] = [] + for contact in contacts: + contact_dict['resources'].append(tuple([contact.resource, + contact.priority, contact.status])) + return repr(contact_dict) + + + if _version[1] >= 30 and _version[1] <= 40: + method = dbus.method + signal = dbus.signal + elif _version[1] >= 41: + method = dbus.service.method + signal = dbus.service.signal + + if _version[1] >= 30: + # prevent using decorators, because they are not supported + # on python < 2.4 + # FIXME: use decorators when python2.3 is OOOOOOLD + show_roster = method(INTERFACE)(show_roster) + list_contacts = method(INTERFACE)(list_contacts) + list_accounts = method(INTERFACE)(list_accounts) + show_waiting = method(INTERFACE)(show_waiting) + change_status = method(INTERFACE)(change_status) + new_message = method(INTERFACE)(new_message) + contact_info = method(INTERFACE)(contact_info) + send_message = method(INTERFACE)(send_message) + VcardInfo = signal(INTERFACE)(VcardInfo)