From 06aee9d2d08703b7d4c6e56e766916749cd9e167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Bili=C5=84ski?= Date: Sun, 10 Aug 2008 13:40:49 +0000 Subject: [PATCH] Initial version of Global Events Dispatcher. Events previously generated for D-Bus support in remote_control.py go through Global Events Dispatcher now - this means any plugin can subscribe to them. Implemented D-Bus support plugin based on remote_control.py. --- plugins/dbus_plugin/__init__.py | 1 + plugins/dbus_plugin/plugin.py | 738 ++++++++++++++++++++++++++++++++ src/common/gajim.py | 1 + src/common/ged.py | 64 +++ src/gajim-remote-plugin.py | 548 ++++++++++++++++++++++++ src/gajim-remote.py | 2 +- src/gajim.py | 31 ++ src/plugins/plugin.py | 23 +- src/plugins/pluginmanager.py | 29 +- src/session.py | 6 + 10 files changed, 1437 insertions(+), 6 deletions(-) create mode 100644 plugins/dbus_plugin/__init__.py create mode 100644 plugins/dbus_plugin/plugin.py create mode 100644 src/common/ged.py create mode 100755 src/gajim-remote-plugin.py diff --git a/plugins/dbus_plugin/__init__.py b/plugins/dbus_plugin/__init__.py new file mode 100644 index 000000000..c5c296ad7 --- /dev/null +++ b/plugins/dbus_plugin/__init__.py @@ -0,0 +1 @@ +from plugin import DBusPlugin \ No newline at end of file diff --git a/plugins/dbus_plugin/plugin.py b/plugins/dbus_plugin/plugin.py new file mode 100644 index 000000000..8141d83d3 --- /dev/null +++ b/plugins/dbus_plugin/plugin.py @@ -0,0 +1,738 @@ +# -*- coding: utf-8 -*- + +## Copyright (C) 2005-2006 Yann Leboulanger +## Copyright (C) 2005-2006 Nikos Kouremenos +## Copyright (C) 2005-2006 Dimitur Kirov +## Copyright (C) 2005-2006 Andrew Sayman +## Copyright (C) 2007 Lukas Petrovicky +## Copyright (C) 2007 Julien Pivotto +## Copyright (C) 2007 Travis Shirk +## Copyright (C) 2008 Mateusz Biliński +## +## This file is part of Gajim. +## +## Gajim 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 3 only. +## +## Gajim 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. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see . +## +''' +D-BUS Support plugin. + +Based on src/remote_control.py + +:author: Mateusz Biliński +:since: 8th August 2008 +:copyright: Copyright (2008) Mateusz Biliński +:license: GPL +''' +import os +import new + +import gobject + + +from common import dbus_support +if dbus_support.supported: + import dbus + if dbus_support: + import dbus.service + import dbus.glib + +INTERFACE = 'org.gajim.dbusplugin.RemoteInterface' +OBJ_PATH = '/org/gajim/dbusplugin/RemoteObject' +SERVICE = 'org.gajim.dbusplugin' + +from common import gajim +from common import helpers +from time import time +from dialogs import AddNewContactWindow, NewChatDialog, JoinGroupchatWindow + +from plugins import GajimPlugin +from plugins.helpers import log_calls, log +from common import ged + +class DBusPlugin(GajimPlugin): + name = u'D-Bus Support' + short_name = u'dbus' + version = u'0.1' + description = u'''D-Bus support. Based on remote_control module from +Gajim core but uses new events handling system.''' + authors = [u'Mateusz Biliński '] + homepage = u'http://blog.bilinski.it' + + @log_calls('DBusPlugin') + def init(self): + self.config_dialog = None + #self.gui_extension_points = {} + #self.config_default_values = {} + + self.events_names = ['Roster', 'AccountPresence', 'ContactPresence', + 'ContactAbsence', 'ContactStatus', 'NewMessage', + 'Subscribe', 'Subscribed', 'Unsubscribed', + 'NewAccount', 'VcardInfo', 'LastStatusTime', + 'OsInfo', 'GCPresence', 'GCMessage', 'RosterInfo', + 'NewGmail'] + + self.signal_object = None + + self.events_handlers = {} + self._set_handling_methods() + + + def activate(self): + session_bus = dbus_support.session_bus.SessionBus() + + bus_name = dbus.service.BusName(SERVICE, bus=session_bus) + self.signal_object = SignalObject(bus_name) + + def deactivate(self): + self.signal_object.remove_from_connection() + self.signal_object = None + + def _set_handling_methods(self): + for event_name in self.events_names: + setattr(self, event_name, + new.instancemethod( + self._generate_handling_method(event_name), + self, + DBusPlugin)) + self.events_handlers[event_name] = (ged.POSTCORE, + getattr(self, event_name)) + + def _generate_handling_method(self, event_name): + def handler(self, arg): + print "Handler of event %s called"%(event_name) + if self.signal_object: + getattr(self.signal_object, event_name)(get_dbus_struct(arg)) + + return handler + +# type mapping + +# in most cases it is a utf-8 string +DBUS_STRING = dbus.String + +# general type (for use in dicts, where all values should have the same type) +DBUS_BOOLEAN = dbus.Boolean +DBUS_DOUBLE = dbus.Double +DBUS_INT32 = dbus.Int32 +# dictionary with string key and binary value +DBUS_DICT_SV = lambda : dbus.Dictionary({}, signature="sv") +# dictionary with string key and value +DBUS_DICT_SS = lambda : dbus.Dictionary({}, signature="ss") +# empty type (there is no equivalent of None on D-Bus, but historically gajim +# used 0 instead) +DBUS_NONE = lambda : dbus.Int32(0) + +def get_dbus_struct(obj): + ''' recursively go through all the items and replace + them with their casted dbus equivalents + ''' + if obj is None: + return DBUS_NONE() + if isinstance(obj, (unicode, str)): + return DBUS_STRING(obj) + if isinstance(obj, int): + return DBUS_INT32(obj) + if isinstance(obj, float): + return DBUS_DOUBLE(obj) + if isinstance(obj, bool): + return DBUS_BOOLEAN(obj) + if isinstance(obj, (list, tuple)): + result = dbus.Array([get_dbus_struct(i) for i in obj], + signature='v') + if result == []: + return DBUS_NONE() + return result + if isinstance(obj, dict): + result = DBUS_DICT_SV() + for key, value in obj.items(): + result[DBUS_STRING(key)] = get_dbus_struct(value) + if result == {}: + return DBUS_NONE() + return result + # unknown type + return DBUS_NONE() + +class SignalObject(dbus.service.Object): + ''' Local object definition for /org/gajim/dbus/RemoteObject. + (This docstring is not be visible, because the clients can access only the remote object.)''' + + def __init__(self, bus_name): + self.first_show = True + self.vcard_account = None + + # register our dbus API + dbus.service.Object.__init__(self, bus_name, OBJ_PATH) + + @dbus.service.signal(INTERFACE, signature='av') + def Roster(self, account_and_data): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def AccountPresence(self, status_and_account): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def ContactPresence(self, account_and_array): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def ContactAbsence(self, account_and_array): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def ContactStatus(self, account_and_array): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def NewMessage(self, account_and_array): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def Subscribe(self, account_and_array): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def Subscribed(self, account_and_array): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def Unsubscribed(self, account_and_jid): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def NewAccount(self, account_and_array): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def VcardInfo(self, account_and_vcard): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def LastStatusTime(self, account_and_array): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def OsInfo(self, account_and_array): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def GCPresence(self, account_and_array): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def GCMessage(self, account_and_array): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def RosterInfo(self, account_and_array): + pass + + @dbus.service.signal(INTERFACE, signature='av') + def NewGmail(self, account_and_array): + pass + + def raise_signal(self, signal, arg): + '''raise a signal, with a single argument of unspecified type + Instead of obj.raise_signal("Foo", bar), use obj.Foo(bar).''' + getattr(self, signal)(arg) + + @dbus.service.method(INTERFACE, in_signature='s', out_signature='s') + def get_status(self, account): + '''Returns status (show to be exact) which is the global one + unless account is given''' + if not account: + # If user did not ask for account, returns the global status + return DBUS_STRING(helpers.get_global_show()) + # return show for the given account + index = gajim.connections[account].connected + return DBUS_STRING(gajim.SHOW_LIST[index]) + + @dbus.service.method(INTERFACE, in_signature='s', out_signature='s') + def get_status_message(self, account): + '''Returns status which is the global one + unless account is given''' + if not account: + # If user did not ask for account, returns the global status + return DBUS_STRING(str(helpers.get_global_status())) + # return show for the given account + status = gajim.connections[account].status + return DBUS_STRING(status) + + def _get_account_and_contact(self, account, jid): + '''get the account (if not given) and contact instance from jid''' + connected_account = None + contact = None + accounts = gajim.contacts.get_accounts() + # if there is only one account in roster, take it as default + # if user did not ask for account + if not account and len(accounts) == 1: + account = accounts[0] + if account: + if gajim.connections[account].connected > 1: # account is connected + connected_account = account + contact = gajim.contacts.get_contact_with_highest_priority(account, + jid) + else: + for account in accounts: + contact = gajim.contacts.get_contact_with_highest_priority(account, + jid) + if contact and gajim.connections[account].connected > 1: + # account is connected + connected_account = account + break + if not contact: + contact = jid + + return connected_account, contact + + def _get_account_for_groupchat(self, account, room_jid): + '''get the account which is connected to groupchat (if not given) + or check if the given account is connected to the groupchat''' + connected_account = None + accounts = gajim.contacts.get_accounts() + # if there is only one account in roster, take it as default + # if user did not ask for account + if not account and len(accounts) == 1: + account = accounts[0] + if account: + if gajim.connections[account].connected > 1 and \ + room_jid in gajim.gc_connected[account] and \ + gajim.gc_connected[account][room_jid]: + # account and groupchat are connected + connected_account = account + else: + for account in accounts: + if gajim.connections[account].connected > 1 and \ + room_jid in gajim.gc_connected[account] and \ + gajim.gc_connected[account][room_jid]: + # account and groupchat are connected + connected_account = account + break + return connected_account + + @dbus.service.method(INTERFACE, in_signature='sss', out_signature='b') + def send_file(self, file_path, jid, account): + '''send file, located at 'file_path' to 'jid', using account + (optional) 'account' ''' + jid = self._get_real_jid(jid, account) + connected_account, contact = self._get_account_and_contact(account, jid) + + if connected_account: + if file_path[:7] == 'file://': + file_path=file_path[7:] + if os.path.isfile(file_path): # is it file? + gajim.interface.instances['file_transfers'].send_file( + connected_account, contact, file_path) + return DBUS_BOOLEAN(True) + return DBUS_BOOLEAN(False) + + def _send_message(self, jid, message, keyID, account, type = 'chat', + subject = None): + '''can be called from send_chat_message (default when send_message) + or send_single_message''' + if not jid or not message: + return DBUS_BOOLEAN(False) + if not keyID: + keyID = '' + + connected_account, contact = self._get_account_and_contact(account, jid) + if connected_account: + connection = gajim.connections[connected_account] + connection.send_message(jid, message, keyID, type, subject) + return DBUS_BOOLEAN(True) + return DBUS_BOOLEAN(False) + + @dbus.service.method(INTERFACE, in_signature='ssss', out_signature='b') + def send_chat_message(self, jid, message, keyID, account): + '''Send chat 'message' to 'jid', using account (optional) 'account'. + if keyID is specified, encrypt the message with the pgp key ''' + jid = self._get_real_jid(jid, account) + return self._send_message(jid, message, keyID, account) + + @dbus.service.method(INTERFACE, in_signature='sssss', out_signature='b') + def send_single_message(self, jid, subject, message, keyID, account): + '''Send single 'message' to 'jid', using account (optional) 'account'. + if keyID is specified, encrypt the message with the pgp key ''' + jid = self._get_real_jid(jid, account) + return self._send_message(jid, message, keyID, account, type, subject) + + @dbus.service.method(INTERFACE, in_signature='sss', out_signature='b') + def send_groupchat_message(self, room_jid, message, account): + '''Send 'message' to groupchat 'room_jid', + using account (optional) 'account'.''' + if not room_jid or not message: + return DBUS_BOOLEAN(False) + connected_account = self._get_account_for_groupchat(account, room_jid) + if connected_account: + connection = gajim.connections[connected_account] + connection.send_gc_message(room_jid, message) + return DBUS_BOOLEAN(True) + return DBUS_BOOLEAN(False) + + @dbus.service.method(INTERFACE, in_signature='ss', out_signature='b') + def open_chat(self, jid, account): + '''Shows the tabbed window for new message to 'jid', using account + (optional) 'account' ''' + if not jid: + raise MissingArgument + return DBUS_BOOLEAN(False) + jid = self._get_real_jid(jid, account) + try: + jid = helpers.parse_jid(jid) + except: + # Jid is not conform, ignore it + return DBUS_BOOLEAN(False) + + if account: + accounts = [account] + else: + accounts = gajim.connections.keys() + if len(accounts) == 1: + account = accounts[0] + connected_account = None + first_connected_acct = None + for acct in accounts: + if gajim.connections[acct].connected > 1: # account is online + contact = gajim.contacts.get_first_contact_from_jid(acct, jid) + if gajim.interface.msg_win_mgr.has_window(jid, acct): + connected_account = acct + break + # jid is in roster + elif contact: + connected_account = acct + break + # we send the message to jid not in roster, because account is + # specified, or there is only one account + elif account: + connected_account = acct + elif first_connected_acct is None: + first_connected_acct = acct + + # if jid is not a conntact, open-chat with first connected account + if connected_account is None and first_connected_acct: + connected_account = first_connected_acct + + if connected_account: + gajim.interface.new_chat_from_jid(connected_account, jid) + # preserve the 'steal focus preservation' + win = gajim.interface.msg_win_mgr.get_window(jid, + connected_account).window + if win.get_property('visible'): + win.window.focus() + return DBUS_BOOLEAN(True) + return DBUS_BOOLEAN(False) + + @dbus.service.method(INTERFACE, in_signature='sss', out_signature='b') + def change_status(self, status, message, account): + ''' change_status(status, message, account). account is optional - + if not specified status is changed for all accounts. ''' + if status not in ('offline', 'online', 'chat', + 'away', 'xa', 'dnd', 'invisible'): + return DBUS_BOOLEAN(False) + if account: + gobject.idle_add(gajim.interface.roster.send_status, account, + status, message) + else: + # account not specified, so change the status of all accounts + for acc in gajim.contacts.get_accounts(): + if not gajim.config.get_per('accounts', acc, + 'sync_with_global_status'): + continue + gobject.idle_add(gajim.interface.roster.send_status, acc, + status, message) + return DBUS_BOOLEAN(False) + + @dbus.service.method(INTERFACE, in_signature='', out_signature='') + def show_next_pending_event(self): + '''Show the window(s) with next pending event in tabbed/group chats.''' + if gajim.events.get_nb_events(): + gajim.interface.systray.handle_first_event() + + @dbus.service.method(INTERFACE, in_signature='s', out_signature='a{sv}') + def contact_info(self, jid): + '''get vcard info for a contact. Return cached value of the vcard. + ''' + if not isinstance(jid, unicode): + jid = unicode(jid) + if not jid: + raise MissingArgument + return DBUS_DICT_SV() + jid = self._get_real_jid(jid) + + cached_vcard = gajim.connections.values()[0].get_cached_vcard(jid) + if cached_vcard: + return get_dbus_struct(cached_vcard) + + # return empty dict + return DBUS_DICT_SV() + + @dbus.service.method(INTERFACE, in_signature='', out_signature='as') + def list_accounts(self): + '''list register accounts''' + result = gajim.contacts.get_accounts() + result_array = dbus.Array([], signature='s') + if result and len(result) > 0: + for account in result: + result_array.append(DBUS_STRING(account)) + return result_array + + @dbus.service.method(INTERFACE, in_signature='s', out_signature='a{ss}') + def account_info(self, account): + '''show info on account: resource, jid, nick, prio, message''' + result = DBUS_DICT_SS() + if gajim.connections.has_key(account): + # account is valid + con = gajim.connections[account] + index = con.connected + result['status'] = DBUS_STRING(gajim.SHOW_LIST[index]) + result['name'] = DBUS_STRING(con.name) + result['jid'] = DBUS_STRING(gajim.get_jid_from_account(con.name)) + result['message'] = DBUS_STRING(con.status) + result['priority'] = DBUS_STRING(unicode(con.priority)) + result['resource'] = DBUS_STRING(unicode(gajim.config.get_per( + 'accounts', con.name, 'resource'))) + return result + + @dbus.service.method(INTERFACE, in_signature='s', out_signature='aa{sv}') + def list_contacts(self, account): + '''list all contacts in the roster. If the first argument is specified, + then return the contacts for the specified account''' + result = dbus.Array([], signature='aa{sv}') + accounts = gajim.contacts.get_accounts() + if len(accounts) == 0: + return result + if account: + accounts_to_search = [account] + else: + accounts_to_search = accounts + for acct in accounts_to_search: + if acct in accounts: + for jid in gajim.contacts.get_jid_list(acct): + item = self._contacts_as_dbus_structure( + gajim.contacts.get_contacts(acct, jid)) + if item: + result.append(item) + return result + + @dbus.service.method(INTERFACE, in_signature='', out_signature='') + def toggle_roster_appearance(self): + ''' shows/hides the roster window ''' + win = gajim.interface.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())) + + @dbus.service.method(INTERFACE, in_signature='', out_signature='') + def toggle_ipython(self): + ''' shows/hides the ipython window ''' + win = gajim.ipython_window + if win: + if win.window.is_visible(): + gobject.idle_add(win.hide) + else: + win.show_all() + win.present() + else: + gajim.interface.create_ipython_window() + + @dbus.service.method(INTERFACE, in_signature='', out_signature='a{ss}') + def prefs_list(self): + prefs_dict = DBUS_DICT_SS() + def get_prefs(data, name, path, value): + if value is None: + return + key = '' + if path is not None: + for node in path: + key += node + '#' + key += name + prefs_dict[DBUS_STRING(key)] = DBUS_STRING(value[1]) + gajim.config.foreach(get_prefs) + return prefs_dict + + @dbus.service.method(INTERFACE, in_signature='', out_signature='b') + def prefs_store(self): + try: + gajim.interface.save_config() + except Exception, e: + return DBUS_BOOLEAN(False) + return DBUS_BOOLEAN(True) + + @dbus.service.method(INTERFACE, in_signature='s', out_signature='b') + def prefs_del(self, key): + if not key: + return DBUS_BOOLEAN(False) + key_path = key.split('#', 2) + if len(key_path) != 3: + return DBUS_BOOLEAN(False) + if key_path[2] == '*': + gajim.config.del_per(key_path[0], key_path[1]) + else: + gajim.config.del_per(key_path[0], key_path[1], key_path[2]) + return DBUS_BOOLEAN(True) + + @dbus.service.method(INTERFACE, in_signature='s', out_signature='b') + def prefs_put(self, key): + if not key: + return DBUS_BOOLEAN(False) + key_path = key.split('#', 2) + if len(key_path) < 3: + subname, value = key.split('=', 1) + gajim.config.set(subname, value) + return DBUS_BOOLEAN(True) + subname, value = key_path[2].split('=', 1) + gajim.config.set_per(key_path[0], key_path[1], subname, value) + return DBUS_BOOLEAN(True) + + @dbus.service.method(INTERFACE, in_signature='ss', out_signature='b') + def add_contact(self, jid, account): + if account: + if account in gajim.connections and \ + gajim.connections[account].connected > 1: + # if given account is active, use it + AddNewContactWindow(account = account, jid = jid) + else: + # wrong account + return DBUS_BOOLEAN(False) + else: + # if account is not given, show account combobox + AddNewContactWindow(account = None, jid = jid) + return DBUS_BOOLEAN(True) + + @dbus.service.method(INTERFACE, in_signature='ss', out_signature='b') + def remove_contact(self, jid, account): + jid = self._get_real_jid(jid, account) + accounts = gajim.contacts.get_accounts() + + # if there is only one account in roster, take it as default + if account: + accounts = [account] + contact_exists = False + for account in accounts: + contacts = gajim.contacts.get_contacts(account, jid) + if contacts: + gajim.connections[account].unsubscribe(jid) + for contact in contacts: + gajim.interface.roster.remove_contact(contact, account) + gajim.contacts.remove_jid(account, jid) + contact_exists = True + return DBUS_BOOLEAN(contact_exists) + + def _is_first(self): + if self.first_show: + self.first_show = False + return True + return False + + def _get_real_jid(self, jid, account = None): + '''get the real jid from the given one: removes xmpp: or get jid from nick + if account is specified, search only in this account + ''' + if account: + accounts = [account] + else: + accounts = gajim.connections.keys() + if jid.startswith('xmpp:'): + return jid[5:] # len('xmpp:') = 5 + nick_in_roster = None # Is jid a nick ? + for account in accounts: + # Does jid exists in roster of one account ? + if gajim.contacts.get_contacts(account, jid): + return jid + if not nick_in_roster: + # look in all contact if one has jid as nick + for jid_ in gajim.contacts.get_jid_list(account): + c = gajim.contacts.get_contacts(account, jid_) + if c[0].name == jid: + nick_in_roster = jid_ + break + if nick_in_roster: + # We have not found jid in roster, but we found is as a nick + return nick_in_roster + # We have not found it as jid nor as nick, probably a not in roster jid + return jid + + def _contacts_as_dbus_structure(self, contacts): + ''' get info from list of Contact objects and create dbus dict ''' + if not contacts: + return None + prim_contact = None # primary contact + for contact in contacts: + if prim_contact is None or contact.priority > prim_contact.priority: + prim_contact = contact + contact_dict = DBUS_DICT_SV() + contact_dict['name'] = DBUS_STRING(prim_contact.name) + contact_dict['show'] = DBUS_STRING(prim_contact.show) + contact_dict['jid'] = DBUS_STRING(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'] = dbus.Array([], signature='(sis)') + for contact in contacts: + resource_props = dbus.Struct((DBUS_STRING(contact.resource), + dbus.Int32(contact.priority), DBUS_STRING(contact.status))) + contact_dict['resources'].append(resource_props) + contact_dict['groups'] = dbus.Array([], signature='(s)') + for group in prim_contact.groups: + contact_dict['groups'].append((DBUS_STRING(group),)) + return contact_dict + + @dbus.service.method(INTERFACE, in_signature='', out_signature='s') + def get_unread_msgs_number(self): + return DBUS_STRING(str(gajim.events.get_nb_events())) + + @dbus.service.method(INTERFACE, in_signature='s', out_signature='b') + def start_chat(self, account): + if not account: + # error is shown in gajim-remote check_arguments(..) + return DBUS_BOOLEAN(False) + NewChatDialog(account) + return DBUS_BOOLEAN(True) + + @dbus.service.method(INTERFACE, in_signature='ss', out_signature='') + def send_xml(self, xml, account): + if account: + gajim.connections[account].send_stanza(xml) + else: + for acc in gajim.contacts.get_accounts(): + gajim.connections[acc].send_stanza(xml) + + @dbus.service.method(INTERFACE, in_signature='ssss', out_signature='') + def join_room(self, room_jid, nick, password, account): + if not account: + # get the first connected account + accounts = gajim.connections.keys() + for acct in accounts: + if gajim.account_is_connected(acct): + account = acct + break + if not account: + return + if not nick: + nick = '' + gajim.interface.instances[account]['join_gc'] = \ + JoinGroupchatWindow(account, room_jid, nick) + else: + gajim.interface.join_gc_room(account, room_jid, nick, password) + \ No newline at end of file diff --git a/src/common/gajim.py b/src/common/gajim.py index ca015d3e1..3943f2bd4 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -62,6 +62,7 @@ connections = {} # 'account name': 'account (connection.Connection) instance' verbose = False ipython_window = None plugin_manager = None +ged = None # Global Events Dispatcher h = logging.StreamHandler() f = logging.Formatter('%(asctime)s %(name)s: %(message)s', '%d %b %Y %H:%M:%S') diff --git a/src/common/ged.py b/src/common/ged.py new file mode 100644 index 000000000..07a7aea10 --- /dev/null +++ b/src/common/ged.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +## This file is part of Gajim. +## +## Gajim 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 3 only. +## +## Gajim 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. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see . +## + +''' +Global Events Dispatcher module. + +:author: Mateusz Biliński +:since: 8th August 2008 +:copyright: Copyright (2008) Mateusz Biliński +:license: GPL +''' + +from plugins.helpers import log + +PRECORE = 30 +CORE = 40 +POSTCORE = 50 + +class GlobalEventsDispatcher(object): + + def __init__(self): + self.handlers = {} + + def register_event_handler(self, event_name, priority, handler): + if event_name in self.handlers: + handlers_list = self.handlers[event_name] + i = 0 + if len(handlers_list) > 0: + while priority > handlers_list[i][0]: # sorting handlers by priority + i += 1 + + handlers_list.insert(i, (priority, handler)) + else: + self.handlers[event_name] = [(priority, handler)] + + def remove_event_handler(self, event_name, priority, handler): + if event_name in self.handlers: + try: + self.handlers[event_name].remove((priority, handler)) + except ValueError, error: + log.debug('''Function (%s) with priority "%s" never registered + as handler of event "%s". Couldn\'t remove. Error: %s''' + %(handler, priority, event_name, error)) + + def raise_event(self, event_name, *args): + #log.debug('[GED] Event: %s\nArgs: %s'%(event_name, str(args))) + if event_name in self.handlers: + for priority, handler in self.handlers[event_name]: + handler(args) + \ No newline at end of file diff --git a/src/gajim-remote-plugin.py b/src/gajim-remote-plugin.py new file mode 100755 index 000000000..c475d18eb --- /dev/null +++ b/src/gajim-remote-plugin.py @@ -0,0 +1,548 @@ +#!/usr/bin/env python +## +## Copyright (C) 2005-2006 Yann Leboulanger +## Copyright (C) 2005-2006 Nikos Kouremenos +## Copyright (C) 2005 Dimitur Kirov +## +## This file is part of Gajim. +## +## Gajim 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 3 only. +## +## Gajim 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. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see . +## + +# gajim-remote help will show you the D-BUS API of Gajim + +import sys +import os +import locale +import signal +signal.signal(signal.SIGINT, signal.SIG_DFL) # ^C exits the application + +from common import exceptions +from common import i18n + +try: + PREFERRED_ENCODING = locale.getpreferredencoding() +except: + PREFERRED_ENCODING = 'UTF-8' + +def send_error(error_message): + '''Writes error message to stderr and exits''' + print >> sys.stderr, error_message.encode(PREFERRED_ENCODING) + sys.exit(1) + +try: + if sys.platform == 'darwin': + import osx.dbus + osx.dbus.load(False) + import dbus + import dbus.service + import dbus.glib +except: + print str(exceptions.DbusNotSupported()) + sys.exit(1) + +OBJ_PATH = '/org/gajim/dbusplugin/RemoteObject' +INTERFACE = 'org.gajim.dbusplugin.RemoteInterface' +SERVICE = 'org.gajim.dbusplugin' +BASENAME = 'gajim-remote-plugin' + + +class GajimRemote: + + def __init__(self): + self.argv_len = len(sys.argv) + # define commands dict. Prototype : + # { + # 'command': [comment, [list of arguments] ] + # } + # + # each argument is defined as a tuple: + # (argument name, help on argument, is mandatory) + # + self.commands = { + 'help':[ + _('Shows a help on specific command'), + [ + #User gets help for the command, specified by this parameter + (_('command'), + _('show help on command'), False) + ] + ], + 'toggle_roster_appearance' : [ + _('Shows or hides the roster window'), + [] + ], + 'show_next_pending_event': [ + _('Pops up a window with the next pending event'), + [] + ], + 'list_contacts': [ + _('Prints a list of all contacts in the roster. Each contact ' + 'appears on a separate line'), + [ + (_('account'), _('show only contacts of the given account'), + False) + ] + + ], + 'list_accounts': [ + _('Prints a list of registered accounts'), + [] + ], + 'change_status': [ + _('Changes the status of account or accounts'), + [ +#offline, online, chat, away, xa, dnd, invisible should not be translated + (_('status'), _('one of: offline, online, chat, away, xa, dnd, invisible '), True), + (_('message'), _('status message'), False), + (_('account'), _('change status of account "account". ' + 'If not specified, try to change status of all accounts that have ' + '"sync with global status" option set'), False) + ] + ], + 'open_chat': [ + _('Shows the chat dialog so that you can send messages to a contact'), + [ + ('jid', _('JID of the contact that you want to chat with'), + True), + (_('account'), _('if specified, contact is taken from the ' + 'contact list of this account'), False) + ] + ], + 'send_chat_message':[ + _('Sends new chat message to a contact in the roster. Both OpenPGP key ' + 'and account are optional. If you want to set only \'account\', ' + 'without \'OpenPGP key\', just set \'OpenPGP key\' to \'\'.'), + [ + ('jid', _('JID of the contact that will receive the message'), True), + (_('message'), _('message contents'), True), + (_('pgp key'), _('if specified, the message will be encrypted ' + 'using this public key'), False), + (_('account'), _('if specified, the message will be sent ' + 'using this account'), False), + ] + ], + 'send_single_message':[ + _('Sends new single message to a contact in the roster. Both OpenPGP key ' + 'and account are optional. If you want to set only \'account\', ' + 'without \'OpenPGP key\', just set \'OpenPGP key\' to \'\'.'), + [ + ('jid', _('JID of the contact that will receive the message'), True), + (_('subject'), _('message subject'), True), + (_('message'), _('message contents'), True), + (_('pgp key'), _('if specified, the message will be encrypted ' + 'using this public key'), False), + (_('account'), _('if specified, the message will be sent ' + 'using this account'), False), + ] + ], + 'send_groupchat_message':[ + _('Sends new message to a groupchat you\'ve joined.'), + [ + ('room_jid', _('JID of the room that will receive the message'), True), + (_('message'), _('message contents'), True), + (_('account'), _('if specified, the message will be sent ' + 'using this account'), False), + ] + ], + 'contact_info': [ + _('Gets detailed info on a contact'), + [ + ('jid', _('JID of the contact'), True) + ] + ], + 'account_info': [ + _('Gets detailed info on a account'), + [ + ('account', _('Name of the account'), True) + ] + ], + 'send_file': [ + _('Sends file to a contact'), + [ + (_('file'), _('File path'), True), + ('jid', _('JID of the contact'), True), + (_('account'), _('if specified, file will be sent using this ' + 'account'), False) + ] + ], + 'prefs_list': [ + _('Lists all preferences and their values'), + [ ] + ], + 'prefs_put': [ + _('Sets value of \'key\' to \'value\'.'), + [ + (_('key=value'), _('\'key\' is the name of the preference, ' + '\'value\' is the value to set it to'), True) + ] + ], + 'prefs_del': [ + _('Deletes a preference item'), + [ + (_('key'), _('name of the preference to be deleted'), True) + ] + ], + 'prefs_store': [ + _('Writes the current state of Gajim preferences to the .config ' + 'file'), + [ ] + ], + 'remove_contact': [ + _('Removes contact from roster'), + [ + ('jid', _('JID of the contact'), True), + (_('account'), _('if specified, contact is taken from the ' + 'contact list of this account'), False) + + ] + ], + 'add_contact': [ + _('Adds contact to roster'), + [ + (_('jid'), _('JID of the contact'), True), + (_('account'), _('Adds new contact to this account'), False) + ] + ], + + 'get_status': [ + _('Returns current status (the global one unless account is specified)'), + [ + (_('account'), _(''), False) + ] + ], + + 'get_status_message': [ + _('Returns current status message(the global one unless account is specified)'), + [ + (_('account'), _(''), False) + ] + ], + + 'get_unread_msgs_number': [ + _('Returns number of unread messages'), + [ ] + ], + 'start_chat': [ + _('Opens \'Start Chat\' dialog'), + [ + (_('account'), _('Starts chat, using this account'), True) + ] + ], + 'send_xml': [ + _('Sends custom XML'), + [ + ('xml', _('XML to send'), True), + ('account', _('Account in which the xml will be sent; ' + 'if not specified, xml will be sent to all accounts'), + False) + ] + ], + 'handle_uri': [ + _('Handle a xmpp:/ uri'), + [ + (_('uri'), _(''), True), + (_('account'), _(''), False) + ] + ], + 'join_room': [ + _('Join a MUC room'), + [ + (_('room'), _(''), True), + (_('nick'), _(''), False), + (_('password'), _(''), False), + (_('account'), _(''), False) + ] + ], + 'check_gajim_running':[ + _('Check if Gajim is running'), + [] + ], + 'toggle_ipython' : [ + _('Shows or hides the ipython window'), + [] + ], + + } + + path = os.getcwd() + if '.svn' in os.listdir(path) or '_svn' in os.listdir(path): + # command only for svn + self.commands['toggle_ipython'] = [ + _('Shows or hides the ipython window'), + [] + ] + self.sbus = None + if self.argv_len < 2 or sys.argv[1] not in self.commands.keys(): + # no args or bad args + send_error(self.compose_help()) + self.command = sys.argv[1] + if self.command == 'help': + if self.argv_len == 3: + print self.help_on_command(sys.argv[2]).encode(PREFERRED_ENCODING) + else: + print self.compose_help().encode(PREFERRED_ENCODING) + sys.exit(0) + if self.command == 'handle_uri': + self.handle_uri() + if self.command == 'check_gajim_running': + print self.check_gajim_running() + sys.exit(0) + self.init_connection() + self.check_arguments() + + if self.command == 'contact_info': + if self.argv_len < 3: + send_error(_('Missing argument "contact_jid"')) + + try: + res = self.call_remote_method() + except exceptions.ServiceNotAvailable: + # At this point an error message has already been displayed + sys.exit(1) + else: + self.print_result(res) + + def print_result(self, res): + ''' Print retrieved result to the output ''' + if res is not None: + if self.command in ('open_chat', 'send_chat_message', 'send_single_message', 'start_chat'): + if self.command in ('send_message', 'send_single_message'): + self.argv_len -= 2 + + if res is False: + if self.argv_len < 4: + send_error(_('\'%s\' is not in your roster.\n' + 'Please specify account for sending the message.') % sys.argv[2]) + else: + send_error(_('You have no active account')) + elif self.command == 'list_accounts': + if isinstance(res, list): + for account in res: + if isinstance(account, unicode): + print account.encode(PREFERRED_ENCODING) + else: + print account + elif self.command == 'account_info': + if res: + print self.print_info(0, res, True) + elif self.command == 'list_contacts': + for account_dict in res: + print self.print_info(0, account_dict, True) + elif self.command == 'prefs_list': + pref_keys = res.keys() + pref_keys.sort() + for pref_key in pref_keys: + result = '%s = %s' % (pref_key, res[pref_key]) + if isinstance(result, unicode): + print result.encode(PREFERRED_ENCODING) + else: + print result + elif self.command == 'contact_info': + print self.print_info(0, res, True) + elif res: + print unicode(res).encode(PREFERRED_ENCODING) + + def check_gajim_running(self): + if not self.sbus: + try: + self.sbus = dbus.SessionBus() + except: + raise exceptions.SessionBusNotPresent + + test = False + if hasattr(self.sbus, 'name_has_owner'): + if self.sbus.name_has_owner(SERVICE): + test = True + elif dbus.dbus_bindings.bus_name_has_owner(self.sbus.get_connection(), + SERVICE): + test = True + return test + + def init_connection(self): + ''' create the onnection to the session dbus, + or exit if it is not possible ''' + try: + self.sbus = dbus.SessionBus() + except: + raise exceptions.SessionBusNotPresent + + from pprint import pprint + pprint(list(self.sbus.list_names())) + if not self.check_gajim_running(): + send_error(_('It seems Gajim is not running. So you can\'t use gajim-remote.')) + obj = self.sbus.get_object(SERVICE, OBJ_PATH) + interface = dbus.Interface(obj, INTERFACE) + + # get the function asked + self.method = interface.__getattr__(self.command) + + def make_arguments_row(self, args): + ''' return arguments list. Mandatory arguments are enclosed with: + '<', '>', optional arguments - with '[', ']' ''' + str = '' + for argument in args: + str += ' ' + if argument[2]: + str += '<' + else: + str += '[' + str += argument[0] + if argument[2]: + str += '>' + else: + str += ']' + return str + + def help_on_command(self, command): + ''' return help message for a given command ''' + if command in self.commands: + command_props = self.commands[command] + arguments_str = self.make_arguments_row(command_props[1]) + str = _('Usage: %s %s %s \n\t %s') % (BASENAME, command, + arguments_str, command_props[0]) + if len(command_props[1]) > 0: + str += '\n\n' + _('Arguments:') + '\n' + for argument in command_props[1]: + str += ' ' + argument[0] + ' - ' + argument[1] + '\n' + return str + send_error(_('%s not found') % command) + + def compose_help(self): + ''' print usage, and list available commands ''' + str = _('Usage: %s command [arguments]\nCommand is one of:\n' ) % BASENAME + commands = self.commands.keys() + commands.sort() + for command in commands: + str += ' ' + command + for argument in self.commands[command][1]: + str += ' ' + if argument[2]: + str += '<' + else: + str += '[' + str += argument[0] + if argument[2]: + str += '>' + else: + str += ']' + str += '\n' + return str + + def print_info(self, level, prop_dict, encode_return = False): + ''' return formated string from data structure ''' + if prop_dict is None or not isinstance(prop_dict, (dict, list, tuple)): + return '' + ret_str = '' + if isinstance(prop_dict, (list, tuple)): + ret_str = '' + spacing = ' ' * level * 4 + for val in prop_dict: + if val is None: + ret_str +='\t' + elif isinstance(val, int): + ret_str +='\t' + str(val) + elif isinstance(val, (str, unicode)): + ret_str +='\t' + val + elif isinstance(val, (list, tuple)): + res = '' + for items in val: + res += self.print_info(level+1, items) + if res != '': + ret_str += '\t' + res + elif isinstance(val, dict): + ret_str += self.print_info(level+1, val) + ret_str = '%s(%s)\n' % (spacing, ret_str[1:]) + elif isinstance(prop_dict, dict): + for key in prop_dict.keys(): + val = prop_dict[key] + spacing = ' ' * level * 4 + if isinstance(val, (unicode, int, str)): + if val is not None: + val = val.strip() + ret_str += '%s%-10s: %s\n' % (spacing, key, val) + elif isinstance(val, (list, tuple)): + res = '' + for items in val: + res += self.print_info(level+1, items) + if res != '': + ret_str += '%s%s: \n%s' % (spacing, key, res) + elif isinstance(val, dict): + res = self.print_info(level+1, val) + if res != '': + ret_str += '%s%s: \n%s' % (spacing, key, res) + if (encode_return): + try: + ret_str = ret_str.encode(PREFERRED_ENCODING) + except: + pass + return ret_str + + def check_arguments(self): + ''' Make check if all necessary arguments are given ''' + argv_len = self.argv_len - 2 + args = self.commands[self.command][1] + if len(args) < argv_len: + send_error(_('Too many arguments. \n' + 'Type "%s help %s" for more info') % (BASENAME, self.command)) + if len(args) > argv_len: + if args[argv_len][2]: + send_error(_('Argument "%s" is not specified. \n' + 'Type "%s help %s" for more info') % + (args[argv_len][0], BASENAME, self.command)) + self.arguments = [] + i = 0 + for arg in sys.argv[2:]: + i += 1 + if i < len(args): + self.arguments.append(arg) + else: + # it's latest argument with spaces + self.arguments.append(' '.join(sys.argv[i+1:])) + break + # add empty string for missing args + self.arguments += ['']*(len(args)-i) + + def handle_uri(self): + if not sys.argv[2:][0].startswith('xmpp:'): + send_error(_('Wrong uri')) + sys.argv[2] = sys.argv[2][5:] + uri = sys.argv[2:][0] + if not '?' in uri: + self.command = sys.argv[1] = 'open_chat' + return + (jid, action) = uri.split('?', 1) + sys.argv[2] = jid + if action == 'join': + self.command = sys.argv[1] = 'join_room' + # Move account parameter from position 3 to 5 + sys.argv.append('') + sys.argv.append(sys.argv[3]) + sys.argv[3] = '' + return + + sys.exit(0) + + def call_remote_method(self): + ''' calls self.method with arguments from sys.argv[2:] ''' + args = [i.decode(PREFERRED_ENCODING) for i in self.arguments] + args = [dbus.String(i) for i in args] + try: + res = self.method(*args) + return res + except Exception: + raise exceptions.ServiceNotAvailable + return None + +if __name__ == '__main__': + GajimRemote() diff --git a/src/gajim-remote.py b/src/gajim-remote.py index 9e05c576d..4e16d7acd 100755 --- a/src/gajim-remote.py +++ b/src/gajim-remote.py @@ -54,7 +54,7 @@ except: OBJ_PATH = '/org/gajim/dbus/RemoteObject' INTERFACE = 'org.gajim.dbus.RemoteInterface' SERVICE = 'org.gajim.dbus' -BASENAME = 'gajim-remote' +BASENAME = 'gajim-remote-plugin' class GajimRemote: diff --git a/src/gajim.py b/src/gajim.py index 7dfc823b8..7b9152646 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -663,6 +663,8 @@ class Interface: self.roster.fire_up_unread_messages_events(account) if self.remote_ctrl: self.remote_ctrl.raise_signal('Roster', (account, data)) + + gajim.ged.raise_event('Roster', (account, data)) def handle_event_warning(self, unused, data): #('WARNING', account, (title_text, section_text)) @@ -796,6 +798,7 @@ class Interface: self.edit_own_details(account) if self.remote_ctrl: self.remote_ctrl.raise_signal('AccountPresence', (status, account)) + gajim.ged.raise_event('AccountPresence', (status, account)) def edit_own_details(self, account): jid = gajim.get_jid_from_account(account) @@ -950,17 +953,20 @@ class Interface: if self.remote_ctrl: self.remote_ctrl.raise_signal('ContactPresence', (account, array)) + gajim.ged.raise_event('ContactPresence', (account, array)) elif old_show > 1 and new_show < 2: notify.notify('contact_disconnected', jid, account, status_message) if self.remote_ctrl: self.remote_ctrl.raise_signal('ContactAbsence', (account, array)) + gajim.ged.raise_event('ContactAbsence', (account, array)) # FIXME: stop non active file transfers elif new_show > 1: # Status change (not connected/disconnected or error (<1)) notify.notify('status_change', jid, account, [new_show, status_message]) if self.remote_ctrl: self.remote_ctrl.raise_signal('ContactStatus', (account, array)) + gajim.ged.raise_event('ContactStatus', (account, array)) else: # FIXME: Msn transport (CMSN1.2.1 and PyMSN) doesn't follow the XEP # still the case in 2008 @@ -1029,6 +1035,7 @@ class Interface: dialogs.SubscriptionRequestWindow(array[0], array[1], account, array[2]) if self.remote_ctrl: self.remote_ctrl.raise_signal('Subscribe', (account, array)) + gajim.ged.raise_event('Subscribe', (account, array)) def handle_event_subscribed(self, account, array): #('SUBSCRIBED', account, (jid, resource)) @@ -1060,11 +1067,13 @@ class Interface: gajim.connections[account].ack_subscribed(jid) if self.remote_ctrl: self.remote_ctrl.raise_signal('Subscribed', (account, array)) + gajim.ged.raise_event('Subscribed', (account, array)) def handle_event_unsubscribed(self, account, jid): gajim.connections[account].ack_unsubscribed(jid) if self.remote_ctrl: self.remote_ctrl.raise_signal('Unsubscribed', (account, jid)) + gajim.ged.raise_event('Unsubscribed', (account, jid)) contact = gajim.contacts.get_first_contact_from_jid(account, jid) if not contact: @@ -1159,6 +1168,7 @@ class Interface: if self.remote_ctrl: self.remote_ctrl.raise_signal('NewAccount', (account, array)) + gajim.ged.raise_event('NewAccount', (account, array)) def handle_event_acc_not_ok(self, account, array): #('ACC_NOT_OK', account, (reason)) @@ -1227,6 +1237,7 @@ class Interface: self.roster.draw_avatar(jid, account) if self.remote_ctrl: self.remote_ctrl.raise_signal('VcardInfo', (account, vcard)) + gajim.ged.raise_event('VcardInfo', (account, vcard)) def handle_event_last_status_time(self, account, array): # ('LAST_STATUS_TIME', account, (jid, resource, seconds, status)) @@ -1248,6 +1259,7 @@ class Interface: win.set_last_status_time() if self.remote_ctrl: self.remote_ctrl.raise_signal('LastStatusTime', (account, array)) + gajim.ged.raise_event('LastStatusTime', (account, array)) def handle_event_os_info(self, account, array): #'OS_INFO' (account, (jid, resource, client_info, os_info)) @@ -1260,6 +1272,7 @@ class Interface: win.set_os_info(array[1], array[2], array[3]) if self.remote_ctrl: self.remote_ctrl.raise_signal('OsInfo', (account, array)) + gajim.ged.raise_event('OsInfo', (account, array)) def handle_event_gc_notify(self, account, array): #'GC_NOTIFY' (account, (room_jid, show, status, nick, @@ -1321,6 +1334,7 @@ class Interface: ctrl.update_ui() if self.remote_ctrl: self.remote_ctrl.raise_signal('GCPresence', (account, array)) + gajim.ged.raise_event('GCPresence', (account, array)) def handle_event_gc_msg(self, account, array): # ('GC_MSG', account, (jid, msg, time, has_timestamp, htmlmsg, @@ -1350,6 +1364,7 @@ class Interface: if self.remote_ctrl: self.remote_ctrl.raise_signal('GCMessage', (account, array)) + gajim.ged.raise_event('GCMessage', (account, array)) def handle_event_gc_subject(self, account, array): #('GC_SUBJECT', account, (jid, subject, body, has_timestamp)) @@ -1621,6 +1636,7 @@ class Interface: self.roster.draw_contact(jid, account) if self.remote_ctrl: self.remote_ctrl.raise_signal('RosterInfo', (account, array)) + gajim.ged.raise_event('RosterInfo', (account, array)) def handle_event_bookmarks(self, account, bms): # ('BOOKMARKS', account, [{name,jid,autojoin,password,nick}, {}]) @@ -1683,6 +1699,7 @@ class Interface: if self.remote_ctrl: self.remote_ctrl.raise_signal('NewGmail', (account, array)) + gajim.ged.raise_event('NewGmail', (account, array)) def handle_event_file_request_error(self, account, array): # ('FILE_REQUEST_ERROR', account, (jid, file_props, error_msg)) @@ -3472,9 +3489,23 @@ class Interface: gobject.timeout_add_seconds(gajim.config.get( 'check_idle_every_foo_seconds'), self.read_sleepy) + # Creating Global Events Dispatcher + from common import ged + gajim.ged = ged.GlobalEventsDispatcher() + self.register_core_handlers() + # Creating plugin manager import plugins gajim.plugin_manager = plugins.PluginManager() + + def register_core_handlers(self): + ''' + Register core handlers in Global Events Dispatcher (GED). + + This part of rewriting whole evetns handling system to use GED. + Of course this can be done anywhere else. + ''' + pass if __name__ == '__main__': def sigint_cb(num, stack): diff --git a/src/plugins/plugin.py b/src/plugins/plugin.py index 53f229761..fb0651cc5 100644 --- a/src/plugins/plugin.py +++ b/src/plugins/plugin.py @@ -95,7 +95,17 @@ class GajimPlugin(object): ''' gui_extension_points = {} ''' - Extension points that plugin wants to connect with. + Extension points that plugin wants to connect with and handlers to be used. + + Keys of this string should be strings with name of GUI extension point + to handles. Values should be 2-element tuples with references to handling + functions. First function will be used to connect plugin with extpoint, + the second one to successfuly disconnect from it. Connecting takes places + when plugin is activated and extpoint already exists, or when plugin is + already activated but extpoint is being created (eg. chat window opens). + Disconnecting takes place when plugin is deactivated and extpoint exists + or when extpoint is destroyed and plugin is activate (eg. chat window + closed). ''' config_default_values = {} ''' @@ -111,6 +121,17 @@ class GajimPlugin(object): :type: {} of 2-element tuples ''' + events_handlers = {} + ''' + Dictionary with events handlers. + + Keys are event names. Values should be 2-element tuples with handler + priority as first element and reference to handler function as second + element. Priority is integer. See `ged` module for predefined priorities + like `ged.PRECORE`, `ged.CORE` or `ged.POSTCORE`. + + :type: {} with 2-element tuples + ''' @log_calls('GajimPlugin') def __init__(self): diff --git a/src/plugins/pluginmanager.py b/src/plugins/pluginmanager.py index 589d91e92..4ee6f20da 100644 --- a/src/plugins/pluginmanager.py +++ b/src/plugins/pluginmanager.py @@ -236,6 +236,22 @@ class PluginManager(object): if gui_extpoint_name in self.gui_extension_points_handlers: for handlers in self.gui_extension_points_handlers[gui_extpoint_name]: handlers[0](*args) + + def _register_events_handlers_in_ged(self, plugin): + for event_name, handler in plugin.events_handlers.iteritems(): + priority = handler[0] + handler_function = handler[1] + gajim.ged.register_event_handler(event_name, + priority, + handler_function) + + def _remove_events_handler_from_ged(self, plugin): + for event_name, handler in plugin.events_handlers.iteritems(): + priority = handler[0] + handler_function = handler[1] + gajim.ged.remove_event_handler(event_name, + priority, + handler_function) @log_calls('PluginManager') def activate_plugin(self, plugin): @@ -244,13 +260,16 @@ class PluginManager(object): :type plugin: class object of `GajimPlugin` subclass :todo: success checks should be implemented using exceptions. Such - control should also be implemented in deactivation. + control should also be implemented in deactivation. Exceptions + should be shown to user inside popup dialog, so the reason + for not activating plugin is known. ''' success = False if not plugin.active: self._add_gui_extension_points_handlers_from_plugin(plugin) self._handle_all_gui_extension_points_with_plugin(plugin) + self._register_events_handlers_in_ged(plugin) success = True @@ -279,6 +298,8 @@ class PluginManager(object): handler = gui_extpoint_handlers[1] if handler: handler(*gui_extension_point_args) + + self._remove_events_handler_from_ged(plugin) # removing plug-in from active plug-ins list plugin.deactivate() @@ -391,17 +412,17 @@ class PluginManager(object): if module: - #log.debug('Attributes processing started') + log.debug('Attributes processing started') for module_attr_name in [attr_name for attr_name in dir(module) if not (attr_name.startswith('__') or attr_name.endswith('__'))]: module_attr = getattr(module, module_attr_name) - #log.debug('%s : %s'%(module_attr_name, module_attr)) + log.debug('%s : %s'%(module_attr_name, module_attr)) try: if issubclass(module_attr, GajimPlugin) and \ not module_attr is GajimPlugin: - #log.debug('is subclass of GajimPlugin') + log.debug('is subclass of GajimPlugin') #log.debug('file_path: %s\nabspath: %s\ndirname: %s'%(file_path, os.path.abspath(file_path), os.path.dirname(os.path.abspath(file_path)))) #log.debug('file_path: %s\ndirname: %s\nabspath: %s'%(file_path, os.path.dirname(file_path), os.path.abspath(os.path.dirname(file_path)))) module_attr.__path__ = os.path.abspath(os.path.dirname(file_path)) diff --git a/src/session.py b/src/session.py index a49d16be2..a905fbfa3 100644 --- a/src/session.py +++ b/src/session.py @@ -224,6 +224,12 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): (self.conn.name, [full_jid_with_resource, msgtxt, tim, encrypted, msg_type, subject, chatstate, msg_id, composing_xep, user_nick, xhtml, form_node])) + + gajim.ged.raise_event('NewMessage', + (self.conn.name, [full_jid_with_resource, msgtxt, tim, + encrypted, msg_type, subject, chatstate, msg_id, + composing_xep, user_nick, xhtml, form_node])) + # display the message or show notification in the roster def roster_message(self, jid, msg, tim, encrypted=False, msg_type='',