diff --git a/Makefile.am b/Makefile.am
index 4e70251bd..14ddf443d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,4 +1,4 @@
-SUBDIRS = src data po icons
+SUBDIRS = src data plugins po icons
ACLOCAL_AMFLAGS = -I m4
diff --git a/configure.ac b/configure.ac
index 38a181c37..768e5b966 100644
--- a/configure.ac
+++ b/configure.ac
@@ -65,14 +65,17 @@ AC_ARG_ENABLE(site-packages,
instead of DATADIR/gajim/src.])]
,
AC_SUBST([gajim_srcdir], [\${pkgpythondir}])
+AC_SUBST([gajim_pluginsdir], [\${pkgpythondir}])
,
AC_SUBST([gajim_srcdir], [\${datadir}/\${PACKAGE}/src])
+AC_SUBST([gajim_pluginsdir], [\${datadir}/\${PACKAGE}/plugins])
)
AS_AC_EXPAND(GAJIM_SRCDIR, "${gajim_srcdir}")
AS_AC_EXPAND(PKGDATADIR, "${datadir}/${PACKAGE}")
AS_AC_EXPAND(DOCDIR, "${docdir}")
AS_AC_EXPAND(LOCALEDIR, "${localedir}")
+AS_AC_EXPAND(GAJIM_PLUGINSDIR, "${gajim_pluginsdir}")
AC_SUBST(VERSION)
AC_SUBST(PACKAGE)
@@ -94,6 +97,7 @@ AC_CONFIG_FILES([
scripts/gajim-remote:scripts/gajim.in
scripts/gajim-history-manager:scripts/gajim.in
po/Makefile.in
+ plugins/Makefile
])
AC_OUTPUT
echo "
@@ -101,6 +105,7 @@ echo "
Installation:
Prefix ........... ${prefix}
Python modules ... ${GAJIM_SRCDIR}
+ Plugins .. ....... ${GAJIM_PLUGINSDIR}
Documentation .... ${DOCDIR}
Others ........... ${PKGDATADIR}
*****************************"
diff --git a/data/gui/groups_post_window.ui b/data/gui/groups_post_window.ui
index ae4706854..28fb44a21 100644
--- a/data/gui/groups_post_window.ui
+++ b/data/gui/groups_post_window.ui
@@ -20,7 +20,7 @@
GTK_FILL
@@ -31,7 +31,7 @@
1
diff --git a/data/gui/plugins_window.ui b/data/gui/plugins_window.ui
new file mode 100644
index 000000000..4560c2013
--- /dev/null
+++ b/data/gui/plugins_window.ui
@@ -0,0 +1,644 @@
+
+
+
+
+
+
+ Plug-in decription should be displayed here. This text will be erased during PluginsWindow initialization.
+
+
diff --git a/data/gui/roster_window.ui b/data/gui/roster_window.ui
index d8e1428ec..b48b2ad20 100644
--- a/data/gui/roster_window.ui
+++ b/data/gui/roster_window.ui
@@ -54,8 +54,8 @@
Join _Group Chat...
- TrueTrue
+ Trueimage3Falseaccelgroup1
@@ -69,8 +69,8 @@
Add _Contact...
- TrueTrue
+ Trueimage4Falseaccelgroup1
@@ -159,6 +159,16 @@
+
+
+ P_lugins
+ True
+ True
+ image13
+ False
+
+
+
@@ -437,4 +447,9 @@
gtk-properties1
+
+ True
+ gtk-disconnect
+ 1
+
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
new file mode 100644
index 000000000..bd4d7d15d
--- /dev/null
+++ b/plugins/Makefile.am
@@ -0,0 +1,10 @@
+INCLUDES = \
+ $(PYTHON_INCLUDES)
+
+gajimpluginsdir = $(gajim_pluginsdir)
+nobase_dist_gajimplugins_PYTHON = \
+ $(srcdir)/*.py \
+ $(srcdir)/*/*.py \
+ $(srcdir)/*/*.ui
+
+MAINTAINERCLEANFILES = Makefile.in
diff --git a/plugins/acronyms_expander.py b/plugins/acronyms_expander.py
new file mode 100644
index 000000000..12ad677b0
--- /dev/null
+++ b/plugins/acronyms_expander.py
@@ -0,0 +1,102 @@
+# -*- 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 .
+##
+
+'''
+Acronyms expander plugin.
+
+:author: Mateusz Biliński
+:since: 9th June 2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+import sys
+
+import gtk
+import gobject
+
+from plugins import GajimPlugin
+from plugins.helpers import log, log_calls
+
+class AcronymsExpanderPlugin(GajimPlugin):
+ name = u'Acronyms Expander'
+ short_name = u'acronyms_expander'
+ version = u'0.1'
+ description = u'''Replaces acronyms (or other strings) with given expansions/substitutes.'''
+ authors = [u'Mateusz Biliński ']
+ homepage = u'http://blog.bilinski.it'
+
+ @log_calls('AcronymsExpanderPlugin')
+ def init(self):
+ self.config_dialog = None
+
+ self.gui_extension_points = {
+ 'chat_control_base': (self.connect_with_chat_control_base,
+ self.disconnect_from_chat_control_base)
+ }
+
+ self.config_default_values = {
+ 'INVOKER': (' ', _('')),
+ 'ACRONYMS': ({'RTFM': 'Read The Friendly Manual',
+ '/slap': '/me slaps',
+ 'PS-': 'plug-in system',
+ 'G-': 'Gajim',
+ 'GNT-': 'http://trac.gajim.org/newticket',
+ 'GW-': 'http://trac.gajim.org/',
+ 'GTS-': 'http://trac.gajim.org/report',
+ },
+ _('')),
+ }
+
+ @log_calls('AcronymsExpanderPlugin')
+ def textbuffer_live_acronym_expander(self, tb):
+ """
+ @param tb gtk.TextBuffer
+ """
+ #assert isinstance(tb,gtk.TextBuffer)
+ ACRONYMS = self.config['ACRONYMS']
+ INVOKER = self.config['INVOKER']
+ t = tb.get_text(tb.get_start_iter(), tb.get_end_iter())
+ #log.debug('%s %d'%(t, len(t)))
+ if t and t[-1] == INVOKER:
+ #log.debug('changing msg text')
+ base, sep, head=t[:-1].rpartition(INVOKER)
+ log.debug('%s | %s | %s'%(base, sep, head))
+ if head in ACRONYMS:
+ head = ACRONYMS[head]
+ #log.debug('head: %s'%(head))
+ t = ''.join((base, sep, head, INVOKER))
+ #log.debug("setting text: '%s'"%(t))
+ gobject.idle_add(tb.set_text, t)
+
+ @log_calls('AcronymsExpanderPlugin')
+ def connect_with_chat_control_base(self, chat_control):
+ d = {}
+ tv = chat_control.msg_textview
+ tb = tv.get_buffer()
+ h_id = tb.connect('changed', self.textbuffer_live_acronym_expander)
+ d['h_id'] = h_id
+
+ chat_control.acronyms_expander_plugin_data = d
+
+ return True
+
+ @log_calls('AcronymsExpanderPlugin')
+ def disconnect_from_chat_control_base(self, chat_control):
+ d = chat_control.acronyms_expander_plugin_data
+ tv = chat_control.msg_textview
+ tv.get_buffer().disconnect(d['h_id'])
diff --git a/plugins/banner_tweaks/__init__.py b/plugins/banner_tweaks/__init__.py
new file mode 100644
index 000000000..a328f68ee
--- /dev/null
+++ b/plugins/banner_tweaks/__init__.py
@@ -0,0 +1,2 @@
+
+from plugin import BannerTweaksPlugin
diff --git a/plugins/banner_tweaks/config_dialog.ui b/plugins/banner_tweaks/config_dialog.ui
new file mode 100644
index 000000000..1994c1c9d
--- /dev/null
+++ b/plugins/banner_tweaks/config_dialog.ui
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+ True
+ 9
+ vertical
+ 4
+
+
+ Display status icon
+ True
+ True
+ False
+ If checked, status icon will be displayed in chat window banner.
+ True
+
+
+
+ False
+ 0
+
+
+
+
+ Display status message of contact
+ True
+ True
+ False
+ If checked, status message of contact will be displayed in chat window banner.
+ True
+
+
+
+ False
+ 1
+
+
+
+
+ Display resource name of contact
+ True
+ True
+ False
+ If checked, resource name of contact will be displayed in chat window banner.
+ True
+
+
+
+ False
+ 2
+
+
+
+
+ Use small fonts for contact name and resource name
+ True
+ True
+ False
+ If checked, smaller font will be used to display resource name and contact name in chat window banner.
+ True
+
+
+
+ False
+ 3
+
+
+
+
+
+
diff --git a/plugins/banner_tweaks/plugin.py b/plugins/banner_tweaks/plugin.py
new file mode 100644
index 000000000..121606aa4
--- /dev/null
+++ b/plugins/banner_tweaks/plugin.py
@@ -0,0 +1,205 @@
+# -*- 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 .
+##
+
+'''
+Adjustable chat window banner.
+
+Includes tweaks to make it compact.
+
+Based on patch by pb in ticket #4133:
+http://trac.gajim.org/attachment/ticket/4133/gajim-chatbanneroptions-svn10008.patch
+
+:author: Mateusz Biliński
+:since: 30 July 2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+import sys
+
+import gtk
+import gobject
+import message_control
+from common import i18n
+from common import gajim
+from common import helpers
+
+from plugins import GajimPlugin
+from plugins.helpers import log, log_calls
+from plugins.gui import GajimPluginConfigDialog
+
+class BannerTweaksPlugin(GajimPlugin):
+ name = u'Banner Tweaks'
+ short_name = u'banner_tweaks'
+ version = u'0.1'
+ description = u'''Allows user to tweak chat window banner appearance (eg. make it compact).
+
+Based on patch by pb in ticket #4133:
+http://trac.gajim.org/attachment/ticket/4133'''
+ authors = [u'Mateusz Biliński ']
+ homepage = u'http://blog.bilinski.it'
+
+ @log_calls('BannerTweaksPlugin')
+ def init(self):
+ self.config_dialog = BannerTweaksPluginConfigDialog(self)
+
+ self.gui_extension_points = {
+ 'chat_control_base_draw_banner': (self.chat_control_base_draw_banner_called,
+ self.chat_control_base_draw_banner_deactivation)
+ }
+
+ self.config_default_values = {
+ 'show_banner_image': (True, _('If True, Gajim will display a status icon in the banner of chat windows.')),
+ 'show_banner_online_msg': (True, _('If True, Gajim will display the status message of the contact in the banner of chat windows.')),
+ 'show_banner_resource': (False, _('If True, Gajim will display the resource name of the contact in the banner of chat windows.')),
+ 'banner_small_fonts': (False, _('If True, Gajim will use small fonts for contact name and resource name in the banner of chat windows.')),
+ 'old_chat_avatar_height': (52, _('chat_avatar_height value before plugin was activated')),
+ }
+
+ @log_calls('BannerTweaksPlugin')
+ def activate(self):
+ self.config['old_chat_avatar_height'] = gajim.config.get('chat_avatar_height')
+ #gajim.config.set('chat_avatar_height', 28)
+
+ @log_calls('BannerTweaksPlugin')
+ def deactivate(self):
+ gajim.config.set('chat_avatar_height', self.config['old_chat_avatar_height'])
+
+ @log_calls('BannerTweaksPlugin')
+ def chat_control_base_draw_banner_called(self, chat_control):
+ if not self.config['show_banner_online_msg']:
+ chat_control.banner_status_label.hide()
+ chat_control.banner_status_label.set_no_show_all(True)
+ status_text = ''
+ chat_control.banner_status_label.set_markup(status_text)
+
+ if not self.config['show_banner_image']:
+ banner_status_img = chat_control.xml.get_object('banner_status_image')
+ banner_status_img.clear()
+
+ # TODO: part below repeats a lot of code from ChatControl.draw_banner_text()
+ # This could be rewritten using re module: getting markup text from
+ # banner_name_label and replacing some elements based on plugin config.
+ # Would it be faster?
+ if self.config['show_banner_resource'] or self.config['banner_small_fonts']:
+ banner_name_label = chat_control.xml.get_object('banner_name_label')
+ label_text = banner_name_label.get_label()
+
+ contact = chat_control.contact
+ jid = contact.jid
+
+ name = contact.get_shown_name()
+ if chat_control.resource:
+ name += '/' + chat_control.resource
+ elif contact.resource and self.config['show_banner_resource']:
+ name += '/' + contact.resource
+
+ if chat_control.TYPE_ID == message_control.TYPE_PM:
+ name = _('%(nickname)s from group chat %(room_name)s') %\
+ {'nickname': name, 'room_name': chat_control.room_name}
+ name = gobject.markup_escape_text(name)
+
+ # We know our contacts nick, but if another contact has the same nick
+ # in another account we need to also display the account.
+ # except if we are talking to two different resources of the same contact
+ acct_info = ''
+ for account in gajim.contacts.get_accounts():
+ if account == chat_control.account:
+ continue
+ if acct_info: # We already found a contact with same nick
+ break
+ for jid in gajim.contacts.get_jid_list(account):
+ other_contact_ = \
+ gajim.contacts.get_first_contact_from_jid(account, jid)
+ if other_contact_.get_shown_name() == chat_control.contact.get_shown_name():
+ acct_info = ' (%s)' % \
+ gobject.markup_escape_text(chat_control.account)
+ break
+
+ font_attrs, font_attrs_small = chat_control.get_font_attrs()
+ if self.config['banner_small_fonts']:
+ font_attrs = font_attrs_small
+
+ st = gajim.config.get('displayed_chat_state_notifications')
+ cs = contact.chatstate
+ if cs and st in ('composing_only', 'all'):
+ if contact.show == 'offline':
+ chatstate = ''
+ elif contact.composing_xep == 'XEP-0085':
+ if st == 'all' or cs == 'composing':
+ chatstate = helpers.get_uf_chatstate(cs)
+ else:
+ chatstate = ''
+ elif contact.composing_xep == 'XEP-0022':
+ if cs in ('composing', 'paused'):
+ # only print composing, paused
+ chatstate = helpers.get_uf_chatstate(cs)
+ else:
+ chatstate = ''
+ else:
+ # When does that happen ? See [7797] and [7804]
+ chatstate = helpers.get_uf_chatstate(cs)
+
+ label_text = '%s%s %s' % \
+ (font_attrs, name, font_attrs_small, acct_info, chatstate)
+ else:
+ # weight="heavy" size="x-large"
+ label_text = '%s%s' % \
+ (font_attrs, name, font_attrs_small, acct_info)
+
+ banner_name_label.set_markup(label_text)
+
+ @log_calls('BannerTweaksPlugin')
+ def chat_control_base_draw_banner_deactivation(self, chat_control):
+ pass
+ #chat_control.draw_banner()
+
+class BannerTweaksPluginConfigDialog(GajimPluginConfigDialog):
+ def init(self):
+ self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path(
+ 'config_dialog.ui')
+ self.xml = gtk.Builder()
+ self.xml.set_translation_domain(i18n.APP)
+ self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH,
+ ['banner_tweaks_config_vbox'])
+ self.config_vbox = self.xml.get_object('banner_tweaks_config_vbox')
+ self.child.pack_start(self.config_vbox)
+
+ self.show_banner_image_checkbutton = self.xml.get_object('show_banner_image_checkbutton')
+ self.show_banner_online_msg_checkbutton = self.xml.get_object('show_banner_online_msg_checkbutton')
+ self.show_banner_resource_checkbutton = self.xml.get_object('show_banner_resource_checkbutton')
+ self.banner_small_fonts_checkbutton = self.xml.get_object('banner_small_fonts_checkbutton')
+
+ self.xml.connect_signals(self)
+
+ def on_run(self):
+ self.show_banner_image_checkbutton.set_active(self.plugin.config['show_banner_image'])
+ self.show_banner_online_msg_checkbutton.set_active(self.plugin.config['show_banner_online_msg'])
+ self.show_banner_resource_checkbutton.set_active(self.plugin.config['show_banner_resource'])
+ self.banner_small_fonts_checkbutton.set_active(self.plugin.config['banner_small_fonts'])
+
+ def on_show_banner_image_checkbutton_toggled(self, button):
+ self.plugin.config['show_banner_image'] = button.get_active()
+
+ def on_show_banner_online_msg_checkbutton_toggled(self, button):
+ self.plugin.config['show_banner_online_msg'] = button.get_active()
+
+ def on_show_banner_resource_checkbutton_toggled(self, button):
+ self.plugin.config['show_banner_resource'] = button.get_active()
+
+ def on_banner_small_fonts_checkbutton_toggled(self, button):
+ self.plugin.config['banner_small_fonts'] = button.get_active()
diff --git a/plugins/dbus_plugin/__init__.py b/plugins/dbus_plugin/__init__.py
new file mode 100644
index 000000000..3851c6bb9
--- /dev/null
+++ b/plugins/dbus_plugin/__init__.py
@@ -0,0 +1 @@
+from plugin import DBusPlugin
diff --git a/plugins/dbus_plugin/plugin.py b/plugins/dbus_plugin/plugin.py
new file mode 100644
index 000000000..c34e0ad0c
--- /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:
+ INTERFACE = 'org.gajim.dbusplugin.RemoteInterface'
+ OBJ_PATH = '/org/gajim/dbusplugin/RemoteObject'
+ SERVICE = 'org.gajim.dbusplugin'
+
+ import dbus.service
+ import dbus.glib
+ # 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)
+
+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()
+
+ @log_calls('DBusPlugin')
+ 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)
+
+ @log_calls('DBusPlugin')
+ def deactivate(self):
+ self.signal_object.remove_from_connection()
+ self.signal_object = None
+
+ @log_calls('DBusPlugin')
+ 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
diff --git a/plugins/events_dump/__init__.py b/plugins/events_dump/__init__.py
new file mode 100644
index 000000000..de174c1b9
--- /dev/null
+++ b/plugins/events_dump/__init__.py
@@ -0,0 +1 @@
+from plugin import EventsDumpPlugin
diff --git a/plugins/events_dump/plugin.py b/plugins/events_dump/plugin.py
new file mode 100644
index 000000000..3e816ae3a
--- /dev/null
+++ b/plugins/events_dump/plugin.py
@@ -0,0 +1,129 @@
+# -*- 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 .
+##
+'''
+Events Dump plugin.
+
+Dumps info about selected events to console.
+
+:author: Mateusz Biliński
+:since: 10th August 2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+import new
+from pprint import pformat
+
+from plugins import GajimPlugin
+from plugins.helpers import log_calls, log
+from common import ged
+
+class EventsDumpPlugin(GajimPlugin):
+ name = u'Events Dump'
+ short_name = u'events_dump'
+ version = u'0.1'
+ description = u'''Dumps info about selected events to console.'''
+ authors = [u'Mateusz Biliński ']
+ homepage = u'http://blog.bilinski.it'
+
+ @log_calls('EventsDumpPlugin')
+ def init(self):
+ self.config_dialog = None
+ #self.gui_extension_points = {}
+ #self.config_default_values = {}
+ events_from_old_dbus_support = [
+ 'Roster', 'AccountPresence', 'ContactPresence',
+ 'ContactAbsence', 'ContactStatus', 'NewMessage',
+ 'Subscribe', 'Subscribed', 'Unsubscribed',
+ 'NewAccount', 'VcardInfo', 'LastStatusTime',
+ 'OsInfo', 'GCPresence', 'GCMessage', 'RosterInfo',
+ 'NewGmail']
+
+ events_from_src_gajim = [
+ 'ROSTER', 'WARNING', 'ERROR',
+ 'INFORMATION', 'ERROR_ANSWER', 'STATUS',
+ 'NOTIFY', 'MSGERROR', 'MSGSENT', 'MSGNOTSENT',
+ 'SUBSCRIBED', 'UNSUBSCRIBED', 'SUBSCRIBE',
+ 'AGENT_ERROR_INFO', 'AGENT_ERROR_ITEMS',
+ 'AGENT_REMOVED', 'REGISTER_AGENT_INFO',
+ 'AGENT_INFO_ITEMS', 'AGENT_INFO_INFO',
+ 'QUIT', 'NEW_ACC_CONNECTED',
+ 'NEW_ACC_NOT_CONNECTED', 'ACC_OK', 'ACC_NOT_OK',
+ 'MYVCARD', 'VCARD', 'LAST_STATUS_TIME', 'OS_INFO',
+ 'GC_NOTIFY', 'GC_MSG', 'GC_SUBJECT', 'GC_CONFIG',
+ 'GC_CONFIG_CHANGE', 'GC_INVITATION',
+ 'GC_AFFILIATION', 'GC_PASSWORD_REQUIRED',
+ 'BAD_PASSPHRASE', 'ROSTER_INFO', 'BOOKMARKS',
+ 'CON_TYPE', 'CONNECTION_LOST', 'FILE_REQUEST',
+ 'GMAIL_NOTIFY', 'FILE_REQUEST_ERROR',
+ 'FILE_SEND_ERROR', 'STANZA_ARRIVED', 'STANZA_SENT',
+ 'HTTP_AUTH', 'VCARD_PUBLISHED',
+ 'VCARD_NOT_PUBLISHED', 'ASK_NEW_NICK', 'SIGNED_IN',
+ 'METACONTACTS', 'ATOM_ENTRY', 'FAILED_DECRYPT',
+ 'PRIVACY_LISTS_RECEIVED', 'PRIVACY_LIST_RECEIVED',
+ 'PRIVACY_LISTS_ACTIVE_DEFAULT',
+ 'PRIVACY_LIST_REMOVED', 'ZC_NAME_CONFLICT',
+ 'PING_SENT', 'PING_REPLY', 'PING_ERROR',
+ 'SEARCH_FORM', 'SEARCH_RESULT',
+ 'RESOURCE_CONFLICT', 'PEP_CONFIG',
+ 'UNIQUE_ROOM_ID_UNSUPPORTED',
+ 'UNIQUE_ROOM_ID_SUPPORTED', 'SESSION_NEG',
+ 'GPG_PASSWORD_REQUIRED', 'SSL_ERROR',
+ 'FINGERPRINT_ERROR', 'PLAIN_CONNECTION',
+ 'PUBSUB_NODE_REMOVED', 'PUBSUB_NODE_NOT_REMOVED']
+
+ network_events_from_core = ['raw-message-received',
+ 'raw-iq-received',
+ 'raw-pres-received']
+
+ network_events_generated_in_nec = [
+ 'customized-message-received',
+ 'more-customized-message-received',
+ 'modify-only-message-received',
+ 'enriched-chat-message-received']
+
+ self.events_names = []
+ self.events_names += network_events_from_core
+ self.events_names += network_events_generated_in_nec
+
+ self.events_handlers = {}
+ self._set_handling_methods()
+
+ @log_calls('EventsDumpPlugin')
+ def activate(self):
+ pass
+
+ @log_calls('EventsDumpPlugin')
+ def deactivate(self):
+ pass
+
+ @log_calls('EventsDumpPlugin')
+ 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,
+ EventsDumpPlugin))
+ self.events_handlers[event_name] = (ged.POSTCORE,
+ getattr(self, event_name))
+
+ def _generate_handling_method(self, event_name):
+ def handler(self, *args):
+ print "Event '%s' occured. Arguments: %s\n\n===\n"%(event_name, pformat(args))
+
+ return handler
diff --git a/plugins/google_translation/__init__.py b/plugins/google_translation/__init__.py
new file mode 100644
index 000000000..dc2c3bc36
--- /dev/null
+++ b/plugins/google_translation/__init__.py
@@ -0,0 +1 @@
+from plugin import GoogleTranslationPlugin
diff --git a/plugins/google_translation/plugin.py b/plugins/google_translation/plugin.py
new file mode 100644
index 000000000..a20664060
--- /dev/null
+++ b/plugins/google_translation/plugin.py
@@ -0,0 +1,118 @@
+# -*- 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 .
+##
+'''
+Google Translation plugin.
+
+Translates (currently only incoming) messages using Google Translate.
+
+:note: consider this as proof-of-concept
+:author: Mateusz Biliński
+:since: 25th August 2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+import re
+import urllib2
+import new
+from pprint import pformat
+
+from common import helpers
+from common import gajim
+
+from plugins import GajimPlugin
+from plugins.helpers import log_calls, log
+from common import ged
+from common import nec
+
+class GoogleTranslationPlugin(GajimPlugin):
+ name = u'Google Translation'
+ short_name = u'google_translation'
+ version = u'0.1'
+ description = u'''Translates (currently only incoming) messages using Google Translate.'''
+ authors = [u'Mateusz Biliński ']
+ homepage = u'http://blog.bilinski.it'
+
+ @log_calls('GoogleTranslationPlugin')
+ def init(self):
+ self.config_dialog = None
+ #self.gui_extension_points = {}
+ self.config_default_values = {'from_lang' : (u'en', _(u'Language of text to be translated')),
+ 'to_lang' : (u'fr', _(u'Language to which translation will be made')),
+ 'user_agent' : (u'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.12) Gecko/20080213 Firefox/2.0.0.11',
+ _(u'User Agent data to be used with urllib2 when connecting to Google Translate service'))}
+
+ #self.events_handlers = {}
+
+ self.events = [GoogleTranslateMessageReceivedEvent]
+
+ self.translated_text_re = \
+ re.compile(r'google.language.callbacks.id100\(\'22\',{"translatedText":"(?P[^"]*)"}, 200, null, 200\)')
+
+ @log_calls('GoogleTranslationPlugin')
+ def translate_text(self, text, from_lang, to_lang):
+ text = self.prepare_text_for_url(text)
+ headers = { 'User-Agent' : self.config['user_agent'] }
+ translation_url = u'http://www.google.com/uds/Gtranslate?callback=google.language.callbacks.id100&context=22&q=%(text)s&langpair=%(from_lang)s%%7C%(to_lang)s&key=notsupplied&v=1.0'%locals()
+
+ request = urllib2.Request(translation_url, headers=headers)
+ response = urllib2.urlopen(request)
+ results = response.read()
+
+ translated_text = self.translated_text_re.search(results).group('text')
+
+ return translated_text
+
+ @log_calls('GoogleTranslationPlugin')
+ def prepare_text_for_url(self, text):
+ '''
+ Converts text so it can be used within URL as query to Google Translate.
+ '''
+
+ # There should be more replacements for plugin to work in any case:
+ char_replacements = { ' ' : '%20',
+ '+' : '%2B'}
+
+ for char, replacement in char_replacements.iteritems():
+ text = text.replace(char, replacement)
+
+ return text
+
+ @log_calls('GoogleTranslationPlugin')
+ def activate(self):
+ pass
+
+ @log_calls('GoogleTranslationPlugin')
+ def deactivate(self):
+ pass
+
+class GoogleTranslateMessageReceivedEvent(nec.NetworkIncomingEvent):
+ name = 'google-translate-message-received'
+ base_network_events = ['raw-message-received']
+
+ def generate(self):
+ msg_type = self.base_event.xmpp_msg.attrs.get('type', None)
+ if msg_type == u'chat':
+ msg_text = "".join(self.base_event.xmpp_msg.kids[0].data)
+ if msg_text:
+ from_lang = self.plugin.config['from_lang']
+ to_lang = self.plugin.config['to_lang']
+ self.base_event.xmpp_msg.kids[0].setData(
+ self.plugin.translate_text(msg_text, from_lang, to_lang))
+
+ return False # We only want to modify old event, not emit another,
+ # so we return False here.
diff --git a/plugins/length_notifier/__init__.py b/plugins/length_notifier/__init__.py
new file mode 100644
index 000000000..67c8c614a
--- /dev/null
+++ b/plugins/length_notifier/__init__.py
@@ -0,0 +1,2 @@
+
+from length_notifier import LengthNotifierPlugin
diff --git a/plugins/length_notifier/config_dialog.ui b/plugins/length_notifier/config_dialog.ui
new file mode 100644
index 000000000..f06bfe115
--- /dev/null
+++ b/plugins/length_notifier/config_dialog.ui
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+ True
+ 9
+ 3
+ 2
+ 7
+ 5
+
+
+ True
+ Message length at which notification is invoked.
+ 0
+ Message length:
+
+
+ GTK_FILL
+ GTK_FILL
+
+
+
+
+ True
+ Background color of text entry field in chat window when notification is invoked.
+ 0
+ Notification color:
+
+
+ 1
+ 2
+ GTK_FILL
+ GTK_FILL
+
+
+
+
+ True
+ JabberIDs that plugin should be used with (eg. restrict only to one microblogging bot). Use comma (without space) as separator. If empty plugin is used with every JID.
+ 0
+ JabberIDs to include:
+
+
+ 2
+ 3
+ GTK_FILL
+ GTK_FILL
+
+
+
+
+ True
+ True
+ JabberIDs that plugin should be used with (eg. restrict only to one microblogging bot). Use comma (without space) as separator. If empty plugin is used with every JID.
+
+
+
+
+ 1
+ 2
+ 2
+ 3
+ GTK_FILL
+
+
+
+
+ True
+
+
+ True
+ True
+ True
+ Background color of text entry field in chat window when notification is invoked.
+ 0
+ Pick a color for notification
+ #000000000000
+
+
+
+ False
+ False
+ 0
+
+
+
+
+ True
+
+
+
+
+
+ 1
+
+
+
+
+ 1
+ 2
+ 1
+ 2
+ GTK_FILL
+
+
+
+
+
+ True
+
+
+ True
+ True
+ Message length at which notification is invoked.
+ 6
+ True
+ True
+
+
+
+ False
+ False
+ 0
+
+
+
+
+ True
+
+
+
+
+
+ 1
+
+
+
+
+ 1
+ 2
+ GTK_FILL
+ GTK_FILL
+
+
+
+
+
+
diff --git a/plugins/length_notifier/length_notifier.py b/plugins/length_notifier/length_notifier.py
new file mode 100644
index 000000000..cde132208
--- /dev/null
+++ b/plugins/length_notifier/length_notifier.py
@@ -0,0 +1,161 @@
+# -*- 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 .
+##
+
+'''
+Message length notifier plugin.
+
+:author: Mateusz Biliński
+:since: 1st June 2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+import sys
+
+import gtk
+from common import i18n
+
+from plugins import GajimPlugin
+from plugins.helpers import log, log_calls
+from plugins.gui import GajimPluginConfigDialog
+
+class LengthNotifierPlugin(GajimPlugin):
+ name = u'Message Length Notifier'
+ short_name = u'length_notifier'
+ version = u'0.1'
+ description = u'''Highlights message entry field in chat window when given length of message is exceeded.'''
+ authors = [u'Mateusz Biliński ']
+ homepage = u'http://blog.bilinski.it'
+
+ @log_calls('LengthNotifierPlugin')
+ def init(self):
+ self.config_dialog = LengthNotifierPluginConfigDialog(self)
+
+ self.gui_extension_points = {
+ 'chat_control' : (self.connect_with_chat_control,
+ self.disconnect_from_chat_control)
+ }
+
+ self.config_default_values = {'MESSAGE_WARNING_LENGTH' : (140, _('Message length at which notification is invoked.')),
+ 'WARNING_COLOR' : ('#F0DB3E', _('Background color of text entry field in chat window when notification is invoked.')),
+ 'JIDS' : ([], _('JabberIDs that plugin should be used with (eg. restrict only to one microblogging bot). If empty plugin is used with every JID. [not implemented]'))
+ }
+
+ @log_calls('LengthNotifierPlugin')
+ def textview_length_warning(self, tb, chat_control):
+ tv = chat_control.msg_textview
+ d = chat_control.length_notifier_plugin_data
+ t = tb.get_text(tb.get_start_iter(), tb.get_end_iter())
+ if t:
+ len_t = len(t)
+ #print("len_t: %d"%(len_t))
+ if len_t>self.config['MESSAGE_WARNING_LENGTH']:
+ if not d['prev_color']:
+ d['prev_color'] = tv.style.copy().base[gtk.STATE_NORMAL]
+ tv.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse(self.config['WARNING_COLOR']))
+ elif d['prev_color']:
+ tv.modify_base(gtk.STATE_NORMAL, d['prev_color'])
+ d['prev_color'] = None
+
+ @log_calls('LengthNotifierPlugin')
+ def connect_with_chat_control(self, chat_control):
+ jid = chat_control.contact.jid
+ if self.jid_is_ok(jid):
+ d = {'prev_color' : None}
+ tv = chat_control.msg_textview
+ tb = tv.get_buffer()
+ h_id = tb.connect('changed', self.textview_length_warning, chat_control)
+ d['h_id'] = h_id
+
+ t = tb.get_text(tb.get_start_iter(), tb.get_end_iter())
+ if t:
+ len_t = len(t)
+ if len_t>self.config['MESSAGE_WARNING_LENGTH']:
+ d['prev_color'] = tv.style.copy().base[gtk.STATE_NORMAL]
+ tv.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse(self.config['WARNING_COLOR']))
+
+ chat_control.length_notifier_plugin_data = d
+
+ return True
+
+ return False
+
+ @log_calls('LengthNotifierPlugin')
+ def disconnect_from_chat_control(self, chat_control):
+ try:
+ d = chat_control.length_notifier_plugin_data
+ tv = chat_control.msg_textview
+ tv.get_buffer().disconnect(d['h_id'])
+ if d['prev_color']:
+ tv.modify_base(gtk.STATE_NORMAL, d['prev_color'])
+ except AttributeError, error:
+ pass
+ #log.debug('Length Notifier Plugin was (probably) never connected with this chat window.\n Error: %s' % (error))
+
+ @log_calls('LengthNotifierPlugin')
+ def jid_is_ok(self, jid):
+ if jid in self.config['JIDS'] or not self.config['JIDS']:
+ return True
+
+ return False
+
+class LengthNotifierPluginConfigDialog(GajimPluginConfigDialog):
+ def init(self):
+ self.GTK_BUILDER_FILE_PATH = self.plugin.local_file_path(
+ 'config_dialog.ui')
+ self.xml = gtk.Builder()
+ self.xml.set_translation_domain(i18n.APP)
+ self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH,
+ ['length_notifier_config_table'])
+ self.config_table = self.xml.get_object('length_notifier_config_table')
+ self.child.pack_start(self.config_table)
+
+ self.message_length_spinbutton = self.xml.get_object(
+ 'message_length_spinbutton')
+ self.message_length_spinbutton.get_adjustment().set_all(140, 0, 500, 1,
+ 10, 0)
+ self.notification_colorbutton = self.xml.get_object(
+ 'notification_colorbutton')
+ self.jids_entry = self.xml.get_object('jids_entry')
+
+ self.xml.connect_signals(self)
+
+ def on_run(self):
+ self.message_length_spinbutton.set_value(self.plugin.config['MESSAGE_WARNING_LENGTH'])
+ self.notification_colorbutton.set_color(gtk.gdk.color_parse(self.plugin.config['WARNING_COLOR']))
+ #self.jids_entry.set_text(self.plugin.config['JIDS'])
+ self.jids_entry.set_text(','.join(self.plugin.config['JIDS']))
+
+ @log_calls('LengthNotifierPluginConfigDialog')
+ def on_message_length_spinbutton_value_changed(self, spinbutton):
+ self.plugin.config['MESSAGE_WARNING_LENGTH'] = spinbutton.get_value()
+
+ @log_calls('LengthNotifierPluginConfigDialog')
+ def on_notification_colorbutton_color_set(self, colorbutton):
+ self.plugin.config['WARNING_COLOR'] = colorbutton.get_color().to_string()
+
+ @log_calls('LengthNotifierPluginConfigDialog')
+ def on_jids_entry_changed(self, entry):
+ text = entry.get_text()
+ if len(text)>0:
+ self.plugin.config['JIDS'] = entry.get_text().split(',')
+ else:
+ self.plugin.config['JIDS'] = []
+
+ @log_calls('LengthNotifierPluginConfigDialog')
+ def on_jids_entry_editing_done(self, entry):
+ pass
diff --git a/plugins/new_events_example/__init__.py b/plugins/new_events_example/__init__.py
new file mode 100644
index 000000000..523d43e14
--- /dev/null
+++ b/plugins/new_events_example/__init__.py
@@ -0,0 +1 @@
+from plugin import NewEventsExamplePlugin
diff --git a/plugins/new_events_example/plugin.py b/plugins/new_events_example/plugin.py
new file mode 100644
index 000000000..ff40dd56f
--- /dev/null
+++ b/plugins/new_events_example/plugin.py
@@ -0,0 +1,147 @@
+# -*- 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 .
+##
+'''
+New Events Example plugin.
+
+Demonstrates how to use Network Events Controller to generate new events
+based on existing one.
+
+:author: Mateusz Biliński
+:since: 15th August 2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+import new
+from pprint import pformat
+
+from common import helpers
+from common import gajim
+
+from plugins import GajimPlugin
+from plugins.helpers import log_calls, log
+from common import ged
+from common import nec
+
+class NewEventsExamplePlugin(GajimPlugin):
+ name = u'New Events Example'
+ short_name = u'new_events_example'
+ version = u'0.1'
+ description = u'''Shows how to generate new network events based on existing one using Network Events Controller.'''
+ authors = [u'Mateusz Biliński ']
+ homepage = u'http://blog.bilinski.it'
+
+ @log_calls('NewEventsExamplePlugin')
+ def init(self):
+ self.config_dialog = None
+ #self.gui_extension_points = {}
+ #self.config_default_values = {}
+
+ self.events_handlers = {'raw-message-received' :
+ (ged.POSTCORE,
+ self.raw_message_received),
+ 'customized-message-received' :
+ (ged.POSTCORE,
+ self.customized_message_received),
+ 'enriched-chat-message-received' :
+ (ged.POSTCORE,
+ self.enriched_chat_message_received)}
+
+ self.events = [CustomizedMessageReceivedEvent,
+ MoreCustomizedMessageReceivedEvent,
+ ModifyOnlyMessageReceivedEvent,
+ EnrichedChatMessageReceivedEvent]
+
+ def enriched_chat_message_received(self, event_object):
+ pass
+ #print "Event '%s' occured. Event object: %s\n\n===\n"%(event_object.name,
+ #event_object)
+
+ def raw_message_received(self, event_object):
+ pass
+ #print "Event '%s' occured. Event object: %s\n\n===\n"%(event_object.name,
+ #event_object)
+
+ def customized_message_received(self, event_object):
+ pass
+ #print "Event '%s' occured. Event object: %s\n\n===\n"%(event_object.name,
+ #event_object
+
+ @log_calls('NewEventsExamplePlugin')
+ def activate(self):
+ pass
+
+ @log_calls('NewEventsExamplePlugin')
+ def deactivate(self):
+ pass
+
+class CustomizedMessageReceivedEvent(nec.NetworkIncomingEvent):
+ name = 'customized-message-received'
+ base_network_events = ['raw-message-received']
+
+ def generate(self):
+ return True
+
+class MoreCustomizedMessageReceivedEvent(nec.NetworkIncomingEvent):
+ '''
+ Shows chain of custom created events.
+
+ This one is based on custom 'customized-messsage-received'.
+ '''
+ name = 'more-customized-message-received'
+ base_network_events = ['customized-message-received']
+
+ def generate(self):
+ return True
+
+class ModifyOnlyMessageReceivedEvent(nec.NetworkIncomingEvent):
+ name = 'modify-only-message-received'
+ base_network_events = ['raw-message-received']
+
+ def generate(self):
+ msg_type = self.base_event.xmpp_msg.attrs.get('type', None)
+ if msg_type == u'chat':
+ msg_text = "".join(self.base_event.xmpp_msg.kids[0].data)
+ self.base_event.xmpp_msg.kids[0].setData(
+ u'%s [MODIFIED BY CUSTOM NETWORK EVENT]'%(msg_text))
+
+ return False
+
+class EnrichedChatMessageReceivedEvent(nec.NetworkIncomingEvent):
+ '''
+ Generates more friendly (in use by handlers) network event for
+ received chat message.
+ '''
+ name = 'enriched-chat-message-received'
+ base_network_events = ['raw-message-received']
+
+ def generate(self):
+ msg_type = self.base_event.xmpp_msg.attrs.get('type', None)
+ if msg_type == u'chat':
+ self.xmpp_msg = self.base_event.xmpp_msg
+ self.conn = self.base_event.conn
+ self.from_jid = helpers.get_full_jid_from_iq(self.xmpp_msg)
+ self.from_jid_without_resource = gajim.get_jid_without_resource(self.from_jid)
+ self.account = self.base_event.account
+ self.from_nickname = gajim.get_contact_name_from_jid(
+ self.account,
+ self.from_jid_without_resource)
+ self.msg_text = "".join(self.xmpp_msg.kids[0].data)
+
+ return True
+
+ return False
diff --git a/plugins/roster_buttons/__init__.py b/plugins/roster_buttons/__init__.py
new file mode 100644
index 000000000..48754d57c
--- /dev/null
+++ b/plugins/roster_buttons/__init__.py
@@ -0,0 +1,4 @@
+
+__all__ = ['RosterButtonsPlugin']
+
+from plugin import RosterButtonsPlugin
diff --git a/plugins/roster_buttons/plugin.py b/plugins/roster_buttons/plugin.py
new file mode 100644
index 000000000..a75fd8c6c
--- /dev/null
+++ b/plugins/roster_buttons/plugin.py
@@ -0,0 +1,86 @@
+# -*- 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 .
+##
+
+'''
+Roster buttons plug-in.
+
+:author: Mateusz Biliński
+:since: 14th June 2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+import sys
+
+import gtk
+from common import i18n
+from common import gajim
+
+from plugins import GajimPlugin
+from plugins.helpers import log, log_calls
+
+class RosterButtonsPlugin(GajimPlugin):
+ name = u'Roster Buttons'
+ short_name = u'roster_buttons'
+ version = u'0.1'
+ description = u'''Adds quick action buttons to roster window.'''
+ authors = [u'Mateusz Biliński ']
+ homepage = u'http://blog.bilinski.it'
+
+ @log_calls('RosterButtonsPlugin')
+ def init(self):
+ self.GTK_BUILDER_FILE_PATH = self.local_file_path('roster_buttons.ui')
+ self.roster_vbox = gajim.interface.roster.xml.get_object('roster_vbox2')
+ self.show_offline_contacts_menuitem = gajim.interface.roster.xml.get_object('show_offline_contacts_menuitem')
+
+ self.config_dialog = None
+
+ @log_calls('RosterButtonsPlugin')
+ def activate(self):
+ self.xml = gtk.Builder()
+ self.xml.set_translation_domain(i18n.APP)
+ self.xml.add_objects_from_file(self.GTK_BUILDER_FILE_PATH,
+ ['roster_buttons_buttonbox'])
+ self.buttonbox = self.xml.get_object('roster_buttons_buttonbox')
+
+ self.roster_vbox.pack_start(self.buttonbox, expand=False)
+ self.roster_vbox.reorder_child(self.buttonbox, 0)
+ self.xml.connect_signals(self)
+
+ @log_calls('RosterButtonsPlugin')
+ def deactivate(self):
+ self.roster_vbox.remove(self.buttonbox)
+
+ self.buttonbox = None
+ self.xml = None
+
+ @log_calls('RosterButtonsPlugin')
+ def on_roster_button_1_clicked(self, button):
+ #gajim.interface.roster.on_show_offline_contacts_menuitem_activate(None)
+ self.show_offline_contacts_menuitem.set_active(not self.show_offline_contacts_menuitem.get_active())
+
+ @log_calls('RosterButtonsPlugin')
+ def on_roster_button_2_clicked(self, button):
+ pass
+
+ @log_calls('RosterButtonsPlugin')
+ def on_roster_button_3_clicked(self, button):
+ pass
+
+ @log_calls('RosterButtonsPlugin')
+ def on_roster_button_4_clicked(self, button):
+ pass
diff --git a/plugins/roster_buttons/roster_buttons.ui b/plugins/roster_buttons/roster_buttons.ui
new file mode 100644
index 000000000..b91b0d2a6
--- /dev/null
+++ b/plugins/roster_buttons/roster_buttons.ui
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+ True
+ True
+ spread
+
+
+ 1
+ True
+ True
+ True
+
+
+
+ False
+ False
+ 0
+
+
+
+
+ 2
+ True
+ True
+ True
+
+
+
+ False
+ False
+ 1
+
+
+
+
+ 3
+ True
+ True
+ True
+
+
+
+ False
+ False
+ 2
+
+
+
+
+ 4
+ True
+ True
+ True
+
+
+
+ False
+ False
+ 3
+
+
+
+
+
+
diff --git a/plugins/snarl_notifications/PySnarl.py b/plugins/snarl_notifications/PySnarl.py
new file mode 100755
index 000000000..c3c657e56
--- /dev/null
+++ b/plugins/snarl_notifications/PySnarl.py
@@ -0,0 +1,772 @@
+"""
+A python version of the main functions to use Snarl
+(http://www.fullphat.net/snarl)
+
+Version 1.0
+
+This module can be used in two ways. One is the normal way
+the other snarl interfaces work. This means you can call snShowMessage
+and get an ID back for manipulations.
+
+The other way is there is a class this module exposes called SnarlMessage.
+This allows you to keep track of the message as a python object. If you
+use the send without specifying False as the argument it will set the ID
+to what the return of the last SendMessage was. This is of course only
+useful for the SHOW message.
+
+Requires one of:
+ pywin32 extensions from http://pywin32.sourceforge.net
+ ctypes (included in Python 2.5, downloadable for earlier versions)
+
+Creator: Sam Listopad II (samlii@users.sourceforge.net)
+
+Copyright 2006-2008 Samuel Listopad II
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not
+use this file except in compliance with the License. You may obtain a copy
+of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required
+by applicable law or agreed to in writing, software distributed under the
+License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
+OF ANY KIND, either express or implied. See the License for the specific
+language governing permissions and limitations under the License.
+"""
+
+import array, struct
+
+def LOWORD(dword):
+ """Return the low WORD of the passed in integer"""
+ return dword & 0x0000ffff
+#get the hi word
+def HIWORD(dword):
+ """Return the high WORD of the passed in integer"""
+ return dword >> 16
+
+class Win32FuncException(Exception):
+ def __init__(self, value):
+ self.value = value
+
+ def __str__(self):
+ return repr(self.value)
+
+class Win32Funcs:
+ """Just a little class to hide the details of finding and using the
+correct win32 functions. The functions may throw a UnicodeEncodeError if
+there is not a unicode version and it is sent a unicode string that cannot
+be converted to ASCII."""
+ WM_USER = 0x400
+ WM_COPYDATA = 0x4a
+ #Type of String the functions are expecting.
+ #Used like function(myWin32Funcs.strType(param)).
+ __strType = str
+ #FindWindow function to use
+ __FindWindow = None
+ #FindWindow function to use
+ __FindWindowEx = None
+ #SendMessage function to use
+ __SendMessage = None
+ #SendMessageTimeout function to use
+ __SendMessageTimeout = None
+ #IsWindow function to use
+ __IsWindow = None
+ #RegisterWindowMessage to use
+ __RegisterWindowMessage = None
+ #GetWindowText to use
+ __GetWindowText = None
+
+ def FindWindow(self, lpClassName, lpWindowName):
+ """Wraps the windows API call of FindWindow"""
+ if lpClassName is not None:
+ lpClassName = self.__strType(lpClassName)
+ if lpWindowName is not None:
+ lpWindowName = self.__strType(lpWindowName)
+ return self.__FindWindow(lpClassName, lpWindowName)
+
+ def FindWindowEx(self, hwndParent, hwndChildAfter, lpClassName, lpWindowName):
+ """Wraps the windows API call of FindWindow"""
+ if lpClassName is not None:
+ lpClassName = self.__strType(lpClassName)
+ if lpWindowName is not None:
+ lpWindowName = self.__strType(lpWindowName)
+ return self.__FindWindowEx(hwndParent, hwndChildAfter, lpClassName, lpWindowName)
+
+ def SendMessage(self, hWnd, Msg, wParam, lParam):
+ """Wraps the windows API call of SendMessage"""
+ return self.__SendMessage(hWnd, Msg, wParam, lParam)
+
+ def SendMessageTimeout(self, hWnd, Msg,
+ wParam, lParam, fuFlags,
+ uTimeout, lpdwResult = None):
+ """Wraps the windows API call of SendMessageTimeout"""
+ idToRet = None
+ try:
+ idFromMsg = array.array('I', [0])
+ result = idFromMsg.buffer_info()[0]
+ response = self.__SendMessageTimeout(hWnd, Msg, wParam,
+ lParam, fuFlags,
+ uTimeout, result)
+ if response == 0:
+ raise Win32FuncException, "SendMessageTimeout TimedOut"
+
+ idToRet = idFromMsg[0]
+ except TypeError:
+ idToRet = self.__SendMessageTimeout(hWnd, Msg, wParam,
+ lParam, fuFlags,
+ uTimeout)
+
+ if lpdwResult is not None and lpdwResult.typecode == 'I':
+ lpdwResult[0] = idToRet
+
+ return idToRet
+
+ def IsWindow(self, hWnd):
+ """Wraps the windows API call of IsWindow"""
+ return self.__IsWindow(hWnd)
+
+ def RegisterWindowMessage(self, lpString):
+ """Wraps the windows API call of RegisterWindowMessage"""
+ return self.__RegisterWindowMessage(self.__strType(lpString))
+
+ def GetWindowText(self, hWnd, lpString = None, nMaxCount = None):
+ """Wraps the windows API call of SendMessageTimeout"""
+ text = ''
+ if hWnd == 0:
+ return text
+
+ if nMaxCount is None:
+ nMaxCount = 1025
+
+ try:
+ arrayType = 'c'
+ if self.__strType == unicode:
+ arrayType = 'u'
+ path_string = array.array(arrayType, self.__strType('\x00') * nMaxCount)
+ path_buffer = path_string.buffer_info()[0]
+ result = self.__GetWindowText(hWnd,
+ path_buffer,
+ nMaxCount)
+ if result > 0:
+ if self.__strType == unicode:
+ text = path_string[0:result].tounicode()
+ else:
+ text = path_string[0:result].tostring()
+ except TypeError:
+ text = self.__GetWindowText(hWnd)
+
+ if lpString is not None and lpString.typecode == 'c':
+ lpdwResult[0:len(text)] = array.array('c', str(text));
+
+ if lpString is not None and lpString.typecode == 'u':
+ lpdwResult[0:len(text)] = array.array('u', unicode(text));
+
+ return text
+
+ def __init__(self):
+ """Load up my needed functions"""
+ # First see if they already have win32gui imported. If so use it.
+ # This has to be checked first since the auto check looks for ctypes
+ # first.
+ try:
+ self.__FindWindow = win32gui.FindWindow
+ self.__FindWindowEx = win32gui.FindWindowEx
+ self.__GetWindowText = win32gui.GetWindowText
+ self.__IsWindow = win32gui.IsWindow
+ self.__SendMessage = win32gui.SendMessage
+ self.__SendMessageTimeout = win32gui.SendMessageTimeout
+ self.__RegisterWindowMessage = win32gui.RegisterWindowMessage
+ self.__strType = unicode
+
+ #Something threw a NameError, most likely the win32gui lines
+ #so do auto check
+ except NameError:
+ try:
+ from ctypes import windll
+ self.__FindWindow = windll.user32.FindWindowW
+ self.__FindWindowEx = windll.user32.FindWindowExW
+ self.__GetWindowText = windll.user32.GetWindowTextW
+ self.__IsWindow = windll.user32.IsWindow
+ self.__SendMessage = windll.user32.SendMessageW
+ self.__SendMessageTimeout = windll.user32.SendMessageTimeoutW
+ self.__RegisterWindowMessage = windll.user32.RegisterWindowMessageW
+ self.__strType = unicode
+
+ #FindWindowW wasn't found, look for FindWindowA
+ except AttributeError:
+ try:
+ self.__FindWindow = windll.user32.FindWindowA
+ self.__FindWindowEx = windll.user32.FindWindowExA
+ self.__GetWindowText = windll.user32.GetWindowTextA
+ self.__IsWindow = windll.user32.IsWindow
+ self.__SendMessage = windll.user32.SendMessageA
+ self.__SendMessageTimeout = windll.user32.SendMessageTimeoutA
+ self.__RegisterWindowMessage = windll.user32.RegisterWindowMessageA
+ # Couldn't find either so Die and tell user why.
+ except AttributeError:
+ import sys
+ sys.stderr.write("Your Windows TM setup seems to be corrupt."+
+ " No FindWindow found in user32.\n")
+ sys.stderr.flush()
+ sys.exit(3)
+
+ except ImportError:
+ try:
+ import win32gui
+ self.__FindWindow = win32gui.FindWindow
+ self.__FindWindowEx = win32gui.FindWindowEx
+ self.__GetWindowText = win32gui.GetWindowText
+ self.__IsWindow = win32gui.IsWindow
+ self.__SendMessage = win32gui.SendMessage
+ self.__SendMessageTimeout = win32gui.SendMessageTimeout
+ self.__RegisterWindowMessage = win32gui.RegisterWindowMessage
+ self.__strType = unicode
+
+ except ImportError:
+ import sys
+ sys.stderr.write("You need to have either"+
+ " ctypes or pywin32 installed.\n")
+ sys.stderr.flush()
+ #sys.exit(2)
+
+
+myWin32Funcs = Win32Funcs()
+
+
+SHOW = 1
+HIDE = 2
+UPDATE = 3
+IS_VISIBLE = 4
+GET_VERSION = 5
+REGISTER_CONFIG_WINDOW = 6
+REVOKE_CONFIG_WINDOW = 7
+REGISTER_ALERT = 8
+REVOKE_ALERT = 9
+REGISTER_CONFIG_WINDOW_2 = 10
+GET_VERSION_EX = 11
+SET_TIMEOUT = 12
+
+EX_SHOW = 32
+
+GLOBAL_MESSAGE = "SnarlGlobalMessage"
+GLOBAL_MSG = "SnarlGlobalEvent"
+
+#Messages That may be received from Snarl
+SNARL_LAUNCHED = 1
+SNARL_QUIT = 2
+SNARL_ASK_APPLET_VER = 3
+SNARL_SHOW_APP_UI = 4
+
+SNARL_NOTIFICATION_CLICKED = 32 #notification was right-clicked by user
+SNARL_NOTIFICATION_CANCELLED = SNARL_NOTIFICATION_CLICKED #Name clarified
+SNARL_NOTIFICATION_TIMED_OUT = 33
+SNARL_NOTIFICATION_ACK = 34 #notification was left-clicked by user
+
+#Snarl Test Message
+WM_SNARLTEST = myWin32Funcs.WM_USER + 237
+
+M_ABORTED = 0x80000007L
+M_ACCESS_DENIED = 0x80000009L
+M_ALREADY_EXISTS = 0x8000000CL
+M_BAD_HANDLE = 0x80000006L
+M_BAD_POINTER = 0x80000005L
+M_FAILED = 0x80000008L
+M_INVALID_ARGS = 0x80000003L
+M_NO_INTERFACE = 0x80000004L
+M_NOT_FOUND = 0x8000000BL
+M_NOT_IMPLEMENTED = 0x80000001L
+M_OK = 0x00000000L
+M_OUT_OF_MEMORY = 0x80000002L
+M_TIMED_OUT = 0x8000000AL
+
+ErrorCodeRev = {
+ 0x80000007L : "M_ABORTED",
+ 0x80000009L : "M_ACCESS_DENIED",
+ 0x8000000CL : "M_ALREADY_EXISTS",
+ 0x80000006L : "M_BAD_HANDLE",
+ 0x80000005L : "M_BAD_POINTER",
+ 0x80000008L : "M_FAILED",
+ 0x80000003L : "M_INVALID_ARGS",
+ 0x80000004L : "M_NO_INTERFACE",
+ 0x8000000BL : "M_NOT_FOUND",
+ 0x80000001L : "M_NOT_IMPLEMENTED",
+ 0x00000000L : "M_OK",
+ 0x80000002L : "M_OUT_OF_MEMORY",
+ 0x8000000AL : "M_TIMED_OUT"
+ }
+
+class SnarlMessage(object):
+ """The main Snarl interface object.
+
+ ID = Snarl Message ID for most operations. See SDK for more info
+ as to other values to put here.
+ type = Snarl Message Type. Valid values are : SHOW, HIDE, UPDATE,
+ IS_VISIBLE, GET_VERSION, REGISTER_CONFIG_WINDOW, REVOKE_CONFIG_WINDOW
+ all which are constants in the PySnarl module.
+ timeout = Timeout in seconds for the Snarl Message
+ data = Snarl Message data. This is dependant upon message type. See SDK
+ title = Snarl Message title.
+ text = Snarl Message text.
+ icon = Path to the icon to display in the Snarl Message.
+ """
+ __msgType = 0
+ __msgID = 0
+ __msgTimeout = 0
+ __msgData = 0
+ __msgTitle = ""
+ __msgText = ""
+ __msgIcon = ""
+ __msgClass = ""
+ __msgExtra = ""
+ __msgExtra2 = ""
+ __msgRsvd1 = 0
+ __msgRsvd2 = 0
+ __msgHWnd = 0
+
+ lastKnownHWnd = 0
+
+ def getType(self):
+ """Type Attribute getter."""
+ return self.__msgType
+ def setType(self, value):
+ """Type Attribute setter."""
+ if( isinstance(value, (int, long)) ):
+ self.__msgType = value
+ type = property(getType, setType, doc="The Snarl Message Type")
+
+ def getID(self):
+ """ID Attribute getter."""
+ return self.__msgID
+ def setID(self, value):
+ """ID Attribute setter."""
+ if( isinstance(value, (int, long)) ):
+ self.__msgID = value
+ ID = property(getID, setID, doc="The Snarl Message ID")
+
+ def getTimeout(self):
+ """Timeout Attribute getter."""
+ return self.__msgTimeout
+ def updateTimeout(self, value):
+ """Timeout Attribute setter."""
+ if( isinstance(value, (int, long)) ):
+ self.__msgTimeout = value
+ timeout = property(getTimeout, updateTimeout,
+ doc="The Snarl Message Timeout")
+
+ def getData(self):
+ """Data Attribute getter."""
+ return self.__msgData
+ def setData(self, value):
+ """Data Attribute setter."""
+ if( isinstance(value, (int, long)) ):
+ self.__msgData = value
+ data = property(getData, setData, doc="The Snarl Message Data")
+
+ def getTitle(self):
+ """Title Attribute getter."""
+ return self.__msgTitle
+ def setTitle(self, value):
+ """Title Attribute setter."""
+ if( isinstance(value, basestring) ):
+ self.__msgTitle = value
+ title = property(getTitle, setTitle, doc="The Snarl Message Title")
+
+ def getText(self):
+ """Text Attribute getter."""
+ return self.__msgText
+ def setText(self, value):
+ """Text Attribute setter."""
+ if( isinstance(value, basestring) ):
+ self.__msgText = value
+ text = property(getText, setText, doc="The Snarl Message Text")
+
+ def getIcon(self):
+ """Icon Attribute getter."""
+ return self.__msgIcon
+ def setIcon(self, value):
+ """Icon Attribute setter."""
+ if( isinstance(value, basestring) ):
+ self.__msgIcon = value
+ icon = property(getIcon, setIcon, doc="The Snarl Message Icon")
+
+ def getClass(self):
+ """Class Attribute getter."""
+ return self.__msgClass
+ def setClass(self, value):
+ """Class Attribute setter."""
+ if( isinstance(value, basestring) ):
+ self.__msgClass = value
+ msgclass = property(getClass, setClass, doc="The Snarl Message Class")
+
+ def getExtra(self):
+ """Extra Attribute getter."""
+ return self.__msgExtra
+ def setExtra(self, value):
+ """Extra Attribute setter."""
+ if( isinstance(value, basestring) ):
+ self.__msgExtra = value
+ extra = property(getExtra, setExtra, doc="Extra Info for the Snarl Message")
+
+ def getExtra2(self):
+ """Extra2 Attribute getter."""
+ return self.__msgExtra2
+ def setExtra2(self, value):
+ """Extra2 Attribute setter."""
+ if( isinstance(value, basestring) ):
+ self.__msgExtra2 = value
+ extra2 = property(getExtra2, setExtra2,
+ doc="More Extra Info for the Snarl Message")
+
+ def getRsvd1(self):
+ """Rsvd1 Attribute getter."""
+ return self.__msgRsvd1
+ def setRsvd1(self, value):
+ """Rsvd1 Attribute setter."""
+ if( isinstance(value, (int, long)) ):
+ self.__msgRsvd1 = value
+ rsvd1 = property(getRsvd1, setRsvd1, doc="The Snarl Message Field Rsvd1")
+
+ def getRsvd2(self):
+ """Rsvd2 Attribute getter."""
+ return self.__msgRsvd2
+ def setRsvd2(self, value):
+ """Rsvd2 Attribute setter."""
+ if( isinstance(value, (int, long)) ):
+ self.__msgRsvd2 = value
+ rsvd2 = property(getRsvd2, setRsvd2, doc="The Snarl Message Field Rsvd2")
+
+ def getHwnd(self):
+ """hWnd Attribute getter."""
+ return self.__msgHWnd
+ def setHwnd(self, value):
+ """hWnd Attribute setter."""
+ if( isinstance(value, (int, long)) ):
+ self.__msgHWnd = value
+
+ hWnd = property(getHwnd, setHwnd, doc="The hWnd of the window this message is being sent from")
+
+
+ def __init__(self, title="", text="", icon="", msg_type=1, msg_id=0):
+ self.__msgTimeout = 0
+ self.__msgData = 0
+ self.__msgClass = ""
+ self.__msgExtra = ""
+ self.__msgExtra2 = ""
+ self.__msgRsvd1 = 0
+ self.__msgRsvd2 = 0
+ self.__msgType = msg_type
+ self.__msgText = text
+ self.__msgTitle = title
+ self.__msgIcon = icon
+ self.__msgID = msg_id
+
+ def createCopyStruct(self):
+ """Creates the struct to send as the copyData in the message."""
+ return struct.pack("ILLL1024s1024s1024s1024s1024s1024sLL",
+ self.__msgType,
+ self.__msgID,
+ self.__msgTimeout,
+ self.__msgData,
+ self.__msgTitle.encode('utf-8'),
+ self.__msgText.encode('utf-8'),
+ self.__msgIcon.encode('utf-8'),
+ self.__msgClass.encode('utf-8'),
+ self.__msgExtra.encode('utf-8'),
+ self.__msgExtra2.encode('utf-8'),
+ self.__msgRsvd1,
+ self.__msgRsvd2
+ )
+ __lpData = None
+ __cds = None
+
+ def packData(self, dwData):
+ """This packs the data in the necessary format for a
+WM_COPYDATA message."""
+ self.__lpData = None
+ self.__cds = None
+ item = self.createCopyStruct()
+ self.__lpData = array.array('c', item)
+ lpData_ad = self.__lpData.buffer_info()[0]
+ cbData = self.__lpData.buffer_info()[1]
+ self.__cds = array.array('c',
+ struct.pack("IIP",
+ dwData,
+ cbData,
+ lpData_ad)
+ )
+ cds_ad = self.__cds.buffer_info()[0]
+ return cds_ad
+
+ def reset(self):
+ """Reset this SnarlMessage to the default state."""
+ self.__msgType = 0
+ self.__msgID = 0
+ self.__msgTimeout = 0
+ self.__msgData = 0
+ self.__msgTitle = ""
+ self.__msgText = ""
+ self.__msgIcon = ""
+ self.__msgClass = ""
+ self.__msgExtra = ""
+ self.__msgExtra2 = ""
+ self.__msgRsvd1 = 0
+ self.__msgRsvd2 = 0
+
+
+ def send(self, setid=True):
+ """Send this SnarlMessage to the Snarl window.
+Args:
+ setid - Boolean defining whether or not to set the ID
+ of this SnarlMessage to the return value of
+ the SendMessage call. Default is True to
+ make simple case of SHOW easy.
+ """
+ hwnd = myWin32Funcs.FindWindow(None, "Snarl")
+ if myWin32Funcs.IsWindow(hwnd):
+ if self.type == REGISTER_CONFIG_WINDOW or self.type == REGISTER_CONFIG_WINDOW_2:
+ self.hWnd = self.data
+ try:
+ response = myWin32Funcs.SendMessageTimeout(hwnd,
+ myWin32Funcs.WM_COPYDATA,
+ self.hWnd, self.packData(2),
+ 2, 500)
+ except Win32FuncException:
+ return False
+
+ idFromMsg = response
+ if setid:
+ self.ID = idFromMsg
+ return True
+ else:
+ return idFromMsg
+ print "No snarl window found"
+ return False
+
+ def hide(self):
+ """Hide this message. Type will revert to type before calling hide
+to allow for better reuse of object."""
+ oldType = self.__msgType
+ self.__msgType = HIDE
+ retVal = bool(self.send(False))
+ self.__msgType = oldType
+ return retVal
+
+ def isVisible(self):
+ """Is this message visible. Type will revert to type before calling
+hide to allow for better reuse of object."""
+ oldType = self.__msgType
+ self.__msgType = IS_VISIBLE
+ retVal = bool(self.send(False))
+ self.__msgType = oldType
+ return retVal
+
+ def update(self, title=None, text=None, icon=None):
+ """Update this message with given title and text. Type will revert
+to type before calling hide to allow for better reuse of object."""
+ oldType = self.__msgType
+ self.__msgType = UPDATE
+ if text:
+ self.__msgText = text
+ if title:
+ self.__msgTitle = title
+ if icon:
+ self.__msgIcon = icon
+ retVal = self.send(False)
+ self.__msgType = oldType
+ return retVal
+
+ def setTimeout(self, timeout):
+ """Set the timeout in seconds of the message"""
+ oldType = self.__msgType
+ oldData = self.__msgData
+ self.__msgType = SET_TIMEOUT
+ #self.timeout = timeout
+ #self.__msgData = self.__msgTimeout
+ self.__msgData = timeout
+ retVal = self.send(False)
+ self.__msgType = oldType
+ self.__msgData = oldData
+ return retVal
+
+ def show(self, timeout=None, title=None,
+ text=None, icon=None,
+ replyWindow=None, replyMsg=None, msgclass=None, soundPath=None):
+ """Show a message"""
+ oldType = self.__msgType
+ oldTimeout = self.__msgTimeout
+ self.__msgType = SHOW
+ if text:
+ self.__msgText = text
+ if title:
+ self.__msgTitle = title
+ if timeout:
+ self.__msgTimeout = timeout
+ if icon:
+ self.__msgIcon = icon
+ if replyWindow:
+ self.__msgID = replyMsg
+ if replyMsg:
+ self.__msgData = replyWindow
+ if soundPath:
+ self.__msgExtra = soundPath
+ if msgclass:
+ self.__msgClass = msgclass
+
+ if ((self.__msgClass and self.__msgClass != "") or
+ (self.__msgExtra and self.__msgExtra != "")):
+ self.__msgType = EX_SHOW
+
+
+ retVal = bool(self.send())
+ self.__msgType = oldType
+ self.__msgTimeout = oldTimeout
+ return retVal
+
+
+def snGetVersion():
+ """ Get the version of Snarl that is running as a tuple. (Major, Minor)
+
+If Snarl is not running or there was an error it will
+return False."""
+ msg = SnarlMessage(msg_type=GET_VERSION)
+ version = msg.send(False)
+ if not version:
+ return False
+ return (HIWORD(version), LOWORD(version))
+
+def snGetVersionEx():
+ """ Get the internal version of Snarl that is running.
+
+If Snarl is not running or there was an error it will
+return False."""
+ sm = SnarlMessage(msg_type=GET_VERSION_EX)
+ verNum = sm.send(False)
+ if not verNum:
+ return False
+ return verNum
+
+def snGetGlobalMessage():
+ """Get the Snarl global message id from windows."""
+ return myWin32Funcs.RegisterWindowMessage(GLOBAL_MSG)
+
+def snShowMessage(title, text, timeout=0, iconPath="",
+ replyWindow=0, replyMsg=0):
+ """Show a message using Snarl and return its ID. See SDK for arguments."""
+ sm = SnarlMessage( title, text, iconPath, msg_id=replyMsg)
+ sm.data = replyWindow
+ if sm.show(timeout):
+ return sm.ID
+ else:
+ return False
+
+def snShowMessageEx(msgClass, title, text, timeout=0, iconPath="",
+ replyWindow=0, replyMsg=0, soundFile=None, hWndFrom=None):
+ """Show a message using Snarl and return its ID. See SDK for arguments.
+ One added argument is hWndFrom that allows one to make the messages appear
+ to come from a specific window. This window should be the one you registered
+ earlier with RegisterConfig"""
+ sm = SnarlMessage( title, text, iconPath, msg_id=replyMsg)
+ sm.data = replyWindow
+ if hWndFrom is not None:
+ sm.hWnd = hWndFrom
+ else:
+ sm.hWnd = SnarlMessage.lastKnownHWnd
+ if sm.show(timeout, msgclass=msgClass, soundPath=soundFile):
+ return sm.ID
+ else:
+ return False
+
+def snUpdateMessage(msgId, msgTitle, msgText, icon=None):
+ """Update a message"""
+ sm = SnarlMessage(msg_id=msgId)
+ if icon:
+ sm.icon = icon
+ return sm.update(msgTitle, msgText)
+
+def snHideMessage(msgId):
+ """Hide a message"""
+ return SnarlMessage(msg_id=msgId).hide()
+
+def snSetTimeout(msgId, timeout):
+ """Update the timeout of a message already shown."""
+ sm = SnarlMessage(msg_id=msgId)
+ return sm.setTimeout(timeout)
+
+def snIsMessageVisible(msgId):
+ """Returns True if the message is visible False otherwise."""
+ return SnarlMessage(msg_id=msgId).isVisible()
+
+def snRegisterConfig(replyWnd, appName, replyMsg):
+ """Register a config window. See SDK for more info."""
+ global lastRegisteredSnarlMsg
+ sm = SnarlMessage(msg_type=REGISTER_CONFIG_WINDOW,
+ title=appName,
+ msg_id=replyMsg)
+ sm.data = replyWnd
+ SnarlMessage.lastKnownHWnd = replyWnd
+
+ return sm.send(False)
+
+def snRegisterConfig2(replyWnd, appName, replyMsg, icon):
+ """Register a config window. See SDK for more info."""
+ global lastRegisteredSnarlMsg
+ sm = SnarlMessage(msg_type=REGISTER_CONFIG_WINDOW_2,
+ title=appName,
+ msg_id=replyMsg,
+ icon=icon)
+ sm.data = replyWnd
+ SnarlMessage.lastKnownHWnd = replyWnd
+ return sm.send(False)
+
+def snRegisterAlert(appName, classStr) :
+ """Register an alert for an already registered config. See SDK for more info."""
+ sm = SnarlMessage(msg_type=REGISTER_ALERT,
+ title=appName,
+ text=classStr)
+ return sm.send(False)
+
+def snRevokeConfig(replyWnd):
+ """Revoke a config window"""
+ sm = SnarlMessage(msg_type=REVOKE_CONFIG_WINDOW)
+ sm.data = replyWnd
+ if replyWnd == SnarlMessage.lastKnownHWnd:
+ SnarlMessage.lastKnownHWnd = 0
+ return sm.send(False)
+
+def snGetSnarlWindow():
+ """Returns the hWnd of the snarl window"""
+ return myWin32Funcs.FindWindow(None, "Snarl")
+
+def snGetAppPath():
+ """Returns the application path of the currently running snarl window"""
+ app_path = None
+ snarl_handle = snGetSnarlWindow()
+ if snarl_handle != 0:
+ pathwin_handle = myWin32Funcs.FindWindowEx(snarl_handle,
+ 0,
+ "static",
+ None)
+ if pathwin_handle != 0:
+ try:
+ result = myWin32Funcs.GetWindowText(pathwin_handle)
+ app_path = result
+ except Win32FuncException:
+ pass
+
+
+ return app_path
+
+def snGetIconsPath():
+ """Returns the path to the icons of the program"""
+ s = snGetAppPath()
+ if s is None:
+ return ""
+ else:
+ return s + "etc\\icons\\"
+
+def snSendTestMessage(data=None):
+ """Sends a test message to Snarl. Used to make sure the
+api is connecting"""
+ param = 0
+ command = 0
+ if data:
+ param = struct.pack("I", data)
+ command = 1
+ myWin32Funcs.SendMessage(snGetSnarlWindow(), WM_SNARLTEST, command, param)
diff --git a/plugins/snarl_notifications/__init__.py b/plugins/snarl_notifications/__init__.py
new file mode 100644
index 000000000..b504dfd50
--- /dev/null
+++ b/plugins/snarl_notifications/__init__.py
@@ -0,0 +1 @@
+from plugin import SnarlNotificationsPlugin
diff --git a/plugins/snarl_notifications/plugin.py b/plugins/snarl_notifications/plugin.py
new file mode 100644
index 000000000..3e21acb77
--- /dev/null
+++ b/plugins/snarl_notifications/plugin.py
@@ -0,0 +1,90 @@
+# -*- 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 .
+##
+'''
+Events notifications using Snarl
+
+Fancy events notifications under Windows using Snarl infrastructure.
+
+:note: plugin is at proof-of-concept state.
+
+:author: Mateusz Biliński
+:since: 15th August 2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+import new
+from pprint import pformat
+
+#import PySnarl
+
+from common import gajim
+from plugins import GajimPlugin
+from plugins.helpers import log_calls, log
+from common import ged
+
+class SnarlNotificationsPlugin(GajimPlugin):
+ name = u'Snarl Notifications'
+ short_name = u'snarl_notifications'
+ version = u'0.1'
+ description = u'''Shows events notification using Snarl (http://www.fullphat.net/) under Windows. Snarl needs to be installed in system.
+PySnarl bindings are used (http://code.google.com/p/pysnarl/).'''
+ authors = [u'Mateusz Biliński ']
+ homepage = u'http://blog.bilinski.it'
+
+ @log_calls('SnarlNotificationsPlugin')
+ def init(self):
+ self.config_dialog = None
+ #self.gui_extension_points = {}
+ #self.config_default_values = {}
+
+ self.events_handlers = {'NewMessage' : (ged.POSTCORE, self.newMessage)}
+
+ @log_calls('SnarlNotificationsPlugin')
+ def activate(self):
+ pass
+
+ @log_calls('SnarlNotificationsPlugin')
+ def deactivate(self):
+ pass
+
+ @log_calls('SnarlNotificationsPlugin')
+ def newMessage(self, args):
+ event_name = "NewMessage"
+ data = args
+ account = data[0]
+ jid = data[1][0]
+ jid_without_resource = gajim.get_jid_without_resource(jid)
+ msg = data[1][1]
+ msg_type = data[1][4]
+ if msg_type == 'chat':
+ nickname = gajim.get_contact_name_from_jid(account,
+ jid_without_resource)
+ elif msg_type == 'pm':
+ nickname = gajim.get_resource_from_jid(jid)
+
+ print "Event '%s' occured. Arguments: %s\n\n===\n"%(event_name, pformat(args))
+ print "Event '%s' occured. Arguments: \naccount = %s\njid = %s\nmsg = %s\nnickname = %s"%(
+ event_name, account, jid, msg, nickname)
+
+
+ #if PySnarl.snGetVersion() != False:
+ #(major, minor) = PySnarl.snGetVersion()
+ #print "Found Snarl version",str(major)+"."+str(minor),"running."
+ #PySnarl.snShowMessage(nickname, msg[:20]+'...')
+ #else:
+ #print "Sorry Snarl does not appear to be running"
diff --git a/src/Makefile.am b/src/Makefile.am
index 29333e3f2..79b8d19b1 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -10,7 +10,8 @@ nobase_dist_gajimsrc_PYTHON = \
$(srcdir)/common/xmpp/*.py \
$(srcdir)/common/zeroconf/*.py \
$(srcdir)/command_system/*.py \
- $(srcdir)/command_system/implementation/*.py
+ $(srcdir)/command_system/implementation/*.py \
+ $(srcdir)/plugins/*.py
dist-hook:
rm -f $(distdir)/ipython_view.py
diff --git a/src/chat_control.py b/src/chat_control.py
index 2de943187..370a9730a 100644
--- a/src/chat_control.py
+++ b/src/chat_control.py
@@ -155,6 +155,8 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
"""
self.draw_banner_text()
self._update_banner_state_image()
+ gajim.plugin_manager.gui_extension_point('chat_control_base_draw_banner',
+ self)
def draw_banner_text(self):
"""
@@ -409,6 +411,10 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
self.command_hits = []
self.last_key_tabs = False
+ # PluginSystem: adding GUI extension point for ChatControlBase
+ # instance object (also subclasses, eg. ChatControl or GroupchatControl)
+ gajim.plugin_manager.gui_extension_point('chat_control_base', self)
+
# This is bascially a very nasty hack to surpass the inability
# to properly use the super, because of the old code.
CommandTools.__init__(self)
@@ -448,6 +454,12 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
i += 1
menu.show_all()
+ def shutdown(self):
+ # PluginSystem: removing GUI extension points connected with ChatControlBase
+ # instance object
+ gajim.plugin_manager.remove_gui_extension_point('chat_control_base', self)
+ gajim.plugin_manager.remove_gui_extension_point('chat_control_base_draw_banner', self)
+
def on_msg_textview_populate_popup(self, textview, menu):
"""
Override the default context menu and we prepend an option to switch
@@ -1589,6 +1601,10 @@ class ChatControl(ChatControlBase):
else:
img.hide()
+ # PluginSystem: adding GUI extension point for this ChatControl
+ # instance object
+ gajim.plugin_manager.gui_extension_point('chat_control', self)
+
def _update_jingle(self, jingle_type):
if jingle_type not in ('audio', 'video'):
return
@@ -2459,7 +2475,13 @@ class ChatControl(ChatControlBase):
self.reset_kbd_mouse_timeout_vars()
def shutdown(self):
- # Send 'gone' chatstate
+ # PluginSystem: calling shutdown of super class (ChatControlBase) to let it remove
+ # it's GUI extension points
+ super(ChatControl, self).shutdown()
+ # PluginSystem: removing GUI extension points connected with ChatControl
+ # instance object
+ gajim.plugin_manager.remove_gui_extension_point('chat_control', self) # Send 'gone' chatstate
+
self.send_chatstate('gone', self.contact)
self.contact.chatstate = None
self.contact.our_chatstate = None
diff --git a/src/common/check_paths.py b/src/common/check_paths.py
index 33368ebe1..00a50e6fe 100644
--- a/src/common/check_paths.py
+++ b/src/common/check_paths.py
@@ -268,6 +268,8 @@ def check_and_possibly_create_paths():
MY_CONFIG = configpaths.gajimpaths['MY_CONFIG']
MY_CACHE = configpaths.gajimpaths['MY_CACHE']
+ PLUGINS_CONFIG_PATH = gajim.PLUGINS_CONFIG_DIR
+
if not os.path.exists(MY_DATA):
create_path(MY_DATA)
elif os.path.isfile(MY_DATA):
@@ -333,6 +335,13 @@ def check_and_possibly_create_paths():
print _('Gajim will now exit')
sys.exit()
+ if not os.path.exists(PLUGINS_CONFIG_PATH):
+ create_path(PLUGINS_CONFIG_PATH)
+ elif os.path.isfile(PLUGINS_CONFIG_PATH):
+ print _('%s is a file but it should be a directory') % PLUGINS_CONFIG_PATH
+ print _('Gajim will now exit')
+ sys.exit()
+
def create_path(directory):
head, tail = os.path.split(directory)
if not os.path.exists(head):
diff --git a/src/common/config.py b/src/common/config.py
index 7f82a7258..3473d6f7e 100644
--- a/src/common/config.py
+++ b/src/common/config.py
@@ -455,6 +455,9 @@ class Config:
'roster': [opt_str, '', _("'yes', 'no' or ''")],
'urgency_hint': [opt_bool, False],
}, {}),
+ 'plugins': ({
+ 'active': [opt_bool, False, _('State whether plugins should be activated on exit (this is saved on Gajim exit). This option SHOULD NOT be used to (de)activate plug-ins. Use GUI instead.')],
+ },{}),
}
statusmsg_default = {
diff --git a/src/common/configpaths.py b/src/common/configpaths.py
index 465e76676..74027994d 100644
--- a/src/common/configpaths.py
+++ b/src/common/configpaths.py
@@ -140,7 +140,8 @@ class ConfigPaths:
d = {'MY_DATA': '', 'LOG_DB': u'logs.db', 'MY_CACERTS': u'cacerts.pem',
'MY_EMOTS': u'emoticons', 'MY_ICONSETS': u'iconsets',
- 'MY_MOOD_ICONSETS': u'moods', 'MY_ACTIVITY_ICONSETS': u'activities'}
+ 'MY_MOOD_ICONSETS': u'moods', 'MY_ACTIVITY_ICONSETS': u'activities',
+ 'PLUGINS_USER': u'plugins'}
for name in d:
self.add(name, TYPE_DATA, windowsify(d[name]))
@@ -155,6 +156,8 @@ class ConfigPaths:
self.add('DATA', None, os.path.join(basedir, windowsify(u'data')))
self.add('ICONS', None, os.path.join(basedir, windowsify(u'icons')))
self.add('HOME', None, fse(os.path.expanduser('~')))
+ self.add('PLUGINS_BASE', None, os.path.join(basedir,
+ windowsify(u'plugins')))
try:
self.add('TMP', None, fse(tempfile.gettempdir()))
except IOError, e:
@@ -172,14 +175,17 @@ class ConfigPaths:
conffile = windowsify(u'config')
pidfile = windowsify(u'gajim')
secretsfile = windowsify(u'secrets')
+ pluginsconfdir = windowsify(u'pluginsconfig')
if len(profile) > 0:
conffile += u'.' + profile
pidfile += u'.' + profile
secretsfile += u'.' + profile
+ pluginsconfdir += u'.' + profile
pidfile += u'.pid'
self.add('CONFIG_FILE', TYPE_CONFIG, conffile)
self.add('PID_FILE', TYPE_CACHE, pidfile)
self.add('SECRETS_FILE', TYPE_DATA, secretsfile)
+ self.add('PLUGINS_CONFIG_DIR', TYPE_CONFIG, pluginsconfdir)
gajimpaths = ConfigPaths()
diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py
index 968550131..61321086a 100644
--- a/src/common/connection_handlers.py
+++ b/src/common/connection_handlers.py
@@ -41,6 +41,7 @@ from calendar import timegm
import datetime
import common.xmpp
+import common.caps_cache as capscache
from common import helpers
from common import gajim
@@ -50,7 +51,10 @@ from common.pubsub import ConnectionPubSub
from common.pep import ConnectionPEP
from common.protocol.caps import ConnectionCaps
from common.protocol.bytestream import ConnectionSocks5Bytestream
-import common.caps_cache as capscache
+from common import ged
+from common import nec
+from common.nec import NetworkEvent
+from plugins import GajimPlugin
if gajim.HAVE_FARSIGHT:
from common.jingle import ConnectionJingle
else:
@@ -552,6 +556,9 @@ class ConnectionVcard:
def _IqCB(self, con, iq_obj):
id_ = iq_obj.getID()
+ gajim.nec.push_incoming_event(NetworkEvent('raw-iq-received',
+ conn=con, xmpp_iq=iq_obj))
+
# Check if we were waiting a timeout for this id
found_tim = None
for tim in self.awaiting_timeouts:
@@ -808,33 +815,16 @@ class ConnectionHandlersBase:
def _ErrorCB(self, con, iq_obj):
log.debug('ErrorCB')
- jid_from = helpers.get_full_jid_from_iq(iq_obj)
- jid_stripped, resource = gajim.get_room_and_nick_from_fjid(jid_from)
id_ = unicode(iq_obj.getID())
if id_ in self.last_ids:
- self.dispatch('LAST_STATUS_TIME', (jid_stripped, resource, -1, ''))
- self.last_ids.remove(id_)
- return
+ gajim.nec.push_incoming_event(LastResultReceivedEvent(None,
+ conn=self, iq_obj=iq_obj))
+ return True
def _LastResultCB(self, con, iq_obj):
log.debug('LastResultCB')
- qp = iq_obj.getTag('query')
- seconds = qp.getAttr('seconds')
- status = qp.getData()
- try:
- seconds = int(seconds)
- except Exception:
- return
- id_ = iq_obj.getID()
- if id_ in self.groupchat_jids:
- who = self.groupchat_jids[id_]
- del self.groupchat_jids[id_]
- else:
- who = helpers.get_full_jid_from_iq(iq_obj)
- if id_ in self.last_ids:
- self.last_ids.remove(id_)
- jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
- self.dispatch('LAST_STATUS_TIME', (jid_stripped, resource, seconds, status))
+ gajim.nec.push_incoming_event(LastResultReceivedEvent(None, conn=self,
+ iq_obj=iq_obj))
def get_sessions(self, jid):
"""
@@ -1004,6 +994,9 @@ ConnectionCaps, ConnectionHandlersBase, ConnectionJingle):
self.gmail_last_tid = None
self.gmail_last_time = None
+ gajim.ged.register_event_handler('http-auth-received', ged.CORE,
+ self._nec_http_auth_received)
+
def build_http_auth_answer(self, iq_obj, answer):
if not self.connection or self.connected < 2:
return
@@ -1018,33 +1011,33 @@ ConnectionCaps, ConnectionHandlersBase, ConnectionJingle):
common.xmpp.protocol.ERR_NOT_AUTHORIZED)
self.connection.send(err)
+ def _nec_http_auth_received(self, obj):
+ if obj.conn.name != self.name:
+ return
+ if obj.opt in ('yes', 'no'):
+ obj.conn.build_http_auth_answer(obj.iq_obj, obj.opt)
+ return True
+
def _HttpAuthCB(self, con, iq_obj):
log.debug('HttpAuthCB')
- opt = gajim.config.get_per('accounts', self.name, 'http_auth')
- if opt in ('yes', 'no'):
- self.build_http_auth_answer(iq_obj, opt)
- else:
- id_ = iq_obj.getTagAttr('confirm', 'id')
- method = iq_obj.getTagAttr('confirm', 'method')
- url = iq_obj.getTagAttr('confirm', 'url')
- msg = iq_obj.getTagData('body') # In case it's a message with a body
- self.dispatch('HTTP_AUTH', (method, url, id_, iq_obj, msg))
+ gajim.nec.push_incoming_event(HttpAuthReceivedEvent(None, conn=self,
+ iq_obj=iq_obj))
raise common.xmpp.NodeProcessed
def _ErrorCB(self, con, iq_obj):
log.debug('ErrorCB')
- ConnectionHandlersBase._ErrorCB(self, con, iq_obj)
- jid_from = helpers.get_full_jid_from_iq(iq_obj)
- jid_stripped, resource = gajim.get_room_and_nick_from_fjid(jid_from)
+ if ConnectionHandlersBase._ErrorCB(self, con, iq_obj):
+ return
id_ = unicode(iq_obj.getID())
if id_ in self.version_ids:
- self.dispatch('OS_INFO', (jid_stripped, resource, '', ''))
- self.version_ids.remove(id_)
+ gajim.nec.push_incoming_event(VersionResultReceivedEvent(None,
+ conn=self, iq_obj=iq_obj))
return
if id_ in self.entity_time_ids:
- self.dispatch('ENTITY_TIME', (jid_stripped, resource, ''))
- self.entity_time_ids.remove(id_)
+ gajim.nec.push_incoming_event(LastResultReceivedEvent(None,
+ conn=self, iq_obj=iq_obj))
return
+ jid_from = helpers.get_full_jid_from_iq(iq_obj)
errmsg = iq_obj.getErrorMsg()
errcode = iq_obj.getErrorCode()
self.dispatch('ERROR_ANSWER', (id_, jid_from, errmsg, errcode))
@@ -1135,7 +1128,8 @@ ConnectionCaps, ConnectionHandlersBase, ConnectionJingle):
log.warn('Invalid JID: %s, ignoring it' % conf.getAttr('jid'))
continue
- if bm not in self.bookmarks:
+ bm_jids = [b['jid'] for b in self.bookmarks]
+ if bm['jid'] not in bm_jids:
self.bookmarks.append(bm)
if storage_type == 'xml':
# We got a bookmark that was not in pubsub
@@ -1210,25 +1204,8 @@ ConnectionCaps, ConnectionHandlersBase, ConnectionJingle):
def _VersionResultCB(self, con, iq_obj):
log.debug('VersionResultCB')
- client_info = ''
- os_info = ''
- qp = iq_obj.getTag('query')
- if qp.getTag('name'):
- client_info += qp.getTag('name').getData()
- if qp.getTag('version'):
- client_info += ' ' + qp.getTag('version').getData()
- if qp.getTag('os'):
- os_info += qp.getTag('os').getData()
- id_ = iq_obj.getID()
- if id_ in self.groupchat_jids:
- who = self.groupchat_jids[id_]
- del self.groupchat_jids[id_]
- else:
- who = helpers.get_full_jid_from_iq(iq_obj)
- jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
- if id_ in self.version_ids:
- self.version_ids.remove(id_)
- self.dispatch('OS_INFO', (jid_stripped, resource, client_info, os_info))
+ gajim.nec.push_incoming_event(VersionResultReceivedEvent(None,
+ conn=self, iq_obj=iq_obj))
def _TimeCB(self, con, iq_obj):
log.debug('TimeCB')
@@ -1260,50 +1237,8 @@ ConnectionCaps, ConnectionHandlersBase, ConnectionJingle):
def _TimeRevisedResultCB(self, con, iq_obj):
log.debug('TimeRevisedResultCB')
- time_info = ''
- qp = iq_obj.getTag('time')
- if not qp:
- # wrong answer
- return
- tzo = qp.getTag('tzo').getData()
- if tzo.lower() == 'z':
- tzo = '0:0'
- tzoh, tzom = tzo.split(':')
- utc_time = qp.getTag('utc').getData()
- ZERO = datetime.timedelta(0)
- class UTC(datetime.tzinfo):
- def utcoffset(self, dt):
- return ZERO
- def tzname(self, dt):
- return "UTC"
- def dst(self, dt):
- return ZERO
-
- class contact_tz(datetime.tzinfo):
- def utcoffset(self, dt):
- return datetime.timedelta(hours=int(tzoh), minutes=int(tzom))
- def tzname(self, dt):
- return "remote timezone"
- def dst(self, dt):
- return ZERO
-
- try:
- t = datetime.datetime.strptime(utc_time, '%Y-%m-%dT%H:%M:%SZ')
- t = t.replace(tzinfo=UTC())
- time_info = t.astimezone(contact_tz()).strftime('%c')
- except ValueError, e:
- log.info('Wrong time format: %s' % str(e))
-
- id_ = iq_obj.getID()
- if id_ in self.groupchat_jids:
- who = self.groupchat_jids[id_]
- del self.groupchat_jids[id_]
- else:
- who = helpers.get_full_jid_from_iq(iq_obj)
- jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who)
- if id_ in self.entity_time_ids:
- self.entity_time_ids.remove(id_)
- self.dispatch('ENTITY_TIME', (jid_stripped, resource, time_info))
+ gajim.nec.push_incoming_event(TimeResultReceivedEvent(None,
+ conn=self, iq_obj=iq_obj))
def _gMailNewMailCB(self, con, gm):
"""
@@ -1329,97 +1264,23 @@ ConnectionCaps, ConnectionHandlersBase, ConnectionJingle):
self.connection.send(iq)
raise common.xmpp.NodeProcessed
- def _gMailQueryCB(self, con, gm):
+ def _gMailQueryCB(self, con, iq_obj):
"""
Called when we receive results from Querying the server for mail messages
in gmail account
"""
- if not gm.getTag('mailbox'):
- return
- self.gmail_url = gm.getTag('mailbox').getAttr('url')
- if gm.getTag('mailbox').getNamespace() == common.xmpp.NS_GMAILNOTIFY:
- newmsgs = gm.getTag('mailbox').getAttr('total-matched')
- if newmsgs != '0':
- # there are new messages
- gmail_messages_list = []
- if gm.getTag('mailbox').getTag('mail-thread-info'):
- gmail_messages = gm.getTag('mailbox').getTags('mail-thread-info')
- for gmessage in gmail_messages:
- unread_senders = []
- for sender in gmessage.getTag('senders').getTags('sender'):
- if sender.getAttr('unread') != '1':
- continue
- if sender.getAttr('name'):
- unread_senders.append(sender.getAttr('name') + '< ' + \
- sender.getAttr('address') + '>')
- else:
- unread_senders.append(sender.getAttr('address'))
-
- if not unread_senders:
- continue
- gmail_subject = gmessage.getTag('subject').getData()
- gmail_snippet = gmessage.getTag('snippet').getData()
- tid = int(gmessage.getAttr('tid'))
- if not self.gmail_last_tid or tid > self.gmail_last_tid:
- self.gmail_last_tid = tid
- gmail_messages_list.append({ \
- 'From': unread_senders, \
- 'Subject': gmail_subject, \
- 'Snippet': gmail_snippet, \
- 'url': gmessage.getAttr('url'), \
- 'participation': gmessage.getAttr('participation'), \
- 'messages': gmessage.getAttr('messages'), \
- 'date': gmessage.getAttr('date')})
- self.gmail_last_time = int(gm.getTag('mailbox').getAttr(
- 'result-time'))
-
- jid = gajim.get_jid_from_account(self.name)
- log.debug(('You have %s new gmail e-mails on %s.') % (newmsgs, jid))
- self.dispatch('GMAIL_NOTIFY', (jid, newmsgs, gmail_messages_list))
- raise common.xmpp.NodeProcessed
+ log.debug('gMailQueryCB')
+ gajim.nec.push_incoming_event(GMailQueryReceivedEvent(None,
+ conn=self, iq_obj=iq_obj))
+ raise common.xmpp.NodeProcessed
def _rosterItemExchangeCB(self, con, msg):
"""
XEP-0144 Roster Item Echange
"""
- exchange_items_list = {}
- jid_from = helpers.get_full_jid_from_iq(msg)
- items_list = msg.getTag('x').getChildren()
- if not items_list:
- return
- action = items_list[0].getAttr('action')
- if action == None:
- action = 'add'
- for item in msg.getTag('x',
- namespace=common.xmpp.NS_ROSTERX).getChildren():
- try:
- jid = helpers.parse_jid(item.getAttr('jid'))
- except common.helpers.InvalidFormat:
- log.warn('Invalid JID: %s, ignoring it' % item.getAttr('jid'))
- continue
- name = item.getAttr('name')
- contact = gajim.contacts.get_contact(self.name, jid)
- groups = []
- same_groups = True
- for group in item.getTags('group'):
- groups.append(group.getData())
- # check that all suggested groups are in the groups we have for this
- # contact
- if not contact or group not in contact.groups:
- same_groups = False
- if contact:
- # check that all groups we have for this contact are in the
- # suggested groups
- for group in contact.groups:
- if group not in groups:
- same_groups = False
- if contact.sub in ('both', 'to') and same_groups:
- continue
- exchange_items_list[jid] = []
- exchange_items_list[jid].append(name)
- exchange_items_list[jid].append(groups)
- if exchange_items_list:
- self.dispatch('ROSTERX', (action, exchange_items_list, jid_from))
+ log.debug('rosterItemExchangeCB')
+ gajim.nec.push_incoming_event(RosterItemExchangeEvent(None,
+ conn=self, iq_obj=msg))
raise common.xmpp.NodeProcessed
def _messageCB(self, con, msg):
@@ -1427,6 +1288,10 @@ ConnectionCaps, ConnectionHandlersBase, ConnectionJingle):
Called when we receive a message
"""
log.debug('MessageCB')
+
+ gajim.nec.push_incoming_event(NetworkEvent('raw-message-received',
+ conn=con, xmpp_msg=msg, account=self.name))
+
mtype = msg.getType()
# check if the message is a roster item exchange (XEP-0144)
@@ -1782,6 +1647,8 @@ ConnectionCaps, ConnectionHandlersBase, ConnectionJingle):
"""
Called when we receive a presence
"""
+ gajim.nec.push_incoming_event(NetworkEvent('raw-pres-received',
+ conn=con, xmpp_pres=prs))
ptype = prs.getType()
if ptype == 'available':
ptype = None
@@ -2449,3 +2316,262 @@ ConnectionCaps, ConnectionHandlersBase, ConnectionJingle):
con.RegisterHandler('presence', self._StanzaArrivedCB)
con.RegisterHandler('message', self._StanzaArrivedCB)
con.RegisterHandler('unknown', self._StreamCB, 'urn:ietf:params:xml:ns:xmpp-streams', xmlns='http://etherx.jabber.org/streams')
+
+class HelperEvent:
+ def get_jid_resource(self):
+ if self.id_ in self.conn.groupchat_jids:
+ self.fjid = self.conn.groupchat_jids[self.id_]
+ del self.conn.groupchat_jids[self.id_]
+ else:
+ self.fjid = helpers.get_full_jid_from_iq(self.iq_obj)
+ self.jid, self.resource = gajim.get_room_and_nick_from_fjid(self.fjid)
+
+ def get_id(self):
+ self.id_ = self.iq_obj.getID()
+
+class HttpAuthReceivedEvent(nec.NetworkIncomingEvent):
+ name = 'http-auth-received'
+ base_network_events = []
+
+ def generate(self):
+ if not self.conn:
+ self.conn = self.base_event.conn
+ if not self.iq_obj:
+ self.iq_obj = self.base_event.xmpp_iq
+
+ self.opt = gajim.config.get_per('accounts', self.conn.name, 'http_auth')
+ self.iq_id = self.iq_obj.getTagAttr('confirm', 'id')
+ self.method = self.iq_obj.getTagAttr('confirm', 'method')
+ self.url = self.iq_obj.getTagAttr('confirm', 'url')
+ # In case it's a message with a body
+ self.msg = self.iq_obj.getTagData('body')
+ return True
+
+class LastResultReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
+ name = 'last-result-received'
+ base_network_events = []
+
+ def generate(self):
+ if not self.conn:
+ self.conn = self.base_event.conn
+ if not self.iq_obj:
+ self.iq_obj = self.base_event.xmpp_iq
+
+ self.get_id()
+ self.get_jid_resource()
+ if self.id_ in self.conn.last_ids:
+ self.conn.last_ids.remove(self.id_)
+
+ self.status = ''
+ self.seconds = -1
+
+ if self.iq_obj.getType() == 'error':
+ return True
+
+ qp = self.iq_obj.getTag('query')
+ sec = qp.getAttr('seconds')
+ self.status = qp.getData()
+ try:
+ self.seconds = int(sec)
+ except Exception:
+ return
+
+ return True
+
+class VersionResultReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
+ name = 'version-result-received'
+ base_network_events = []
+
+ def generate(self):
+ if not self.conn:
+ self.conn = self.base_event.conn
+ if not self.iq_obj:
+ self.iq_obj = self.base_event.xmpp_iq
+
+ self.get_id()
+ self.get_jid_resource()
+ if self.id_ in self.conn.version_ids:
+ self.conn.version_ids.remove(self.id_)
+
+ self.client_info = ''
+ self.os_info = ''
+
+ if self.iq_obj.getType() == 'error':
+ return True
+
+ qp = self.iq_obj.getTag('query')
+ if qp.getTag('name'):
+ self.client_info += qp.getTag('name').getData()
+ if qp.getTag('version'):
+ self.client_info += ' ' + qp.getTag('version').getData()
+ if qp.getTag('os'):
+ self.os_info += qp.getTag('os').getData()
+
+ return True
+
+class TimeResultReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
+ name = 'time-result-received'
+ base_network_events = []
+
+ def generate(self):
+ if not self.conn:
+ self.conn = self.base_event.conn
+ if not self.iq_obj:
+ self.iq_obj = self.base_event.xmpp_iq
+
+ self.get_id()
+ self.get_jid_resource()
+ if self.id_ in self.conn.entity_time_ids:
+ self.conn.entity_time_ids.remove(self.id_)
+
+ self.time_info = ''
+
+ if self.iq_obj.getType() == 'error':
+ return True
+
+ qp = self.iq_obj.getTag('time')
+ if not qp:
+ # wrong answer
+ return
+ tzo = qp.getTag('tzo').getData()
+ if tzo.lower() == 'z':
+ tzo = '0:0'
+ tzoh, tzom = tzo.split(':')
+ utc_time = qp.getTag('utc').getData()
+ ZERO = datetime.timedelta(0)
+ class UTC(datetime.tzinfo):
+ def utcoffset(self, dt):
+ return ZERO
+ def tzname(self, dt):
+ return "UTC"
+ def dst(self, dt):
+ return ZERO
+
+ class contact_tz(datetime.tzinfo):
+ def utcoffset(self, dt):
+ return datetime.timedelta(hours=int(tzoh), minutes=int(tzom))
+ def tzname(self, dt):
+ return "remote timezone"
+ def dst(self, dt):
+ return ZERO
+
+ try:
+ t = datetime.datetime.strptime(utc_time, '%Y-%m-%dT%H:%M:%SZ')
+ t = t.replace(tzinfo=UTC())
+ self.time_info = t.astimezone(contact_tz()).strftime('%c')
+ except ValueError, e:
+ log.info('Wrong time format: %s' % str(e))
+ return
+
+ return True
+
+class GMailQueryReceivedEvent(nec.NetworkIncomingEvent):
+ name = 'gmail-notify'
+ base_network_events = []
+
+ def generate(self):
+ if not self.conn:
+ self.conn = self.base_event.conn
+ if not self.iq_obj:
+ self.iq_obj = self.base_event.xmpp_iq
+
+ if not self.iq_obj.getTag('mailbox'):
+ return
+ mb = self.iq_obj.getTag('mailbox')
+ if not mb.getAttr('url'):
+ return
+ self.conn.gmail_url = mb.getAttr('url')
+ if mb.getNamespace() != common.xmpp.NS_GMAILNOTIFY:
+ return
+ self.newmsgs = mb.getAttr('total-matched')
+ if not self.newmsgs:
+ return
+ if self.newmsgs == '0':
+ return
+ # there are new messages
+ self.gmail_messages_list = []
+ if mb.getTag('mail-thread-info'):
+ gmail_messages = mb.getTags('mail-thread-info')
+ for gmessage in gmail_messages:
+ unread_senders = []
+ for sender in gmessage.getTag('senders').getTags(
+ 'sender'):
+ if sender.getAttr('unread') != '1':
+ continue
+ if sender.getAttr('name'):
+ unread_senders.append(sender.getAttr('name') + \
+ '< ' + sender.getAttr('address') + '>')
+ else:
+ unread_senders.append(sender.getAttr('address'))
+
+ if not unread_senders:
+ continue
+ gmail_subject = gmessage.getTag('subject').getData()
+ gmail_snippet = gmessage.getTag('snippet').getData()
+ tid = int(gmessage.getAttr('tid'))
+ if not self.conn.gmail_last_tid or \
+ tid > self.conn.gmail_last_tid:
+ self.conn.gmail_last_tid = tid
+ self.gmail_messages_list.append({
+ 'From': unread_senders,
+ 'Subject': gmail_subject,
+ 'Snippet': gmail_snippet,
+ 'url': gmessage.getAttr('url'),
+ 'participation': gmessage.getAttr('participation'),
+ 'messages': gmessage.getAttr('messages'),
+ 'date': gmessage.getAttr('date')})
+ self.conn.gmail_last_time = int(mb.getAttr('result-time'))
+
+ self.jid = gajim.get_jid_from_account(self.name)
+ log.debug(('You have %s new gmail e-mails on %s.') % (self.newmsgs,
+ self.jid))
+ return True
+
+class RosterItemExchangeEvent(nec.NetworkIncomingEvent, HelperEvent):
+ name = 'roster-item-exchange-received'
+ base_network_events = []
+
+ def generate(self):
+ if not self.conn:
+ self.conn = self.base_event.conn
+ if not self.iq_obj:
+ self.iq_obj = self.base_event.xmpp_iq
+
+ self.get_jid_resource()
+ self.exchange_items_list = {}
+ items_list = msg.getTag('x').getChildren()
+ if not items_list:
+ return
+ self.action = items_list[0].getAttr('action')
+ if self.action is None:
+ self.action = 'add'
+ for item in msg.getTag('x', namespace=common.xmpp.NS_ROSTERX).\
+ getChildren():
+ try:
+ jid = helpers.parse_jid(item.getAttr('jid'))
+ except common.helpers.InvalidFormat:
+ log.warn('Invalid JID: %s, ignoring it' % item.getAttr('jid'))
+ continue
+ name = item.getAttr('name')
+ contact = gajim.contacts.get_contact(self.conn.name, jid)
+ groups = []
+ same_groups = True
+ for group in item.getTags('group'):
+ groups.append(group.getData())
+ # check that all suggested groups are in the groups we have for this
+ # contact
+ if not contact or group not in contact.groups:
+ same_groups = False
+ if contact:
+ # check that all groups we have for this contact are in the
+ # suggested groups
+ for group in contact.groups:
+ if group not in groups:
+ same_groups = False
+ if contact.sub in ('both', 'to') and same_groups:
+ continue
+ self.exchange_items_list[jid] = []
+ self.exchange_items_list[jid].append(name)
+ self.exchange_items_list[jid].append(groups)
+ if exchange_items_list:
+ return True
diff --git a/src/common/gajim.py b/src/common/gajim.py
index a44029de5..cfd4419af 100644
--- a/src/common/gajim.py
+++ b/src/common/gajim.py
@@ -68,6 +68,8 @@ connections = {} # 'account name': 'account (connection.Connection) instance'
ipython_window = None
ged = None # Global Events Dispatcher
+nec = None # Network Events Controller
+plugin_manager = None # Plugins Manager
log = logging.getLogger('gajim')
@@ -88,6 +90,9 @@ TMP = gajimpaths['TMP']
DATA_DIR = gajimpaths['DATA']
ICONS_DIR = gajimpaths['ICONS']
HOME_DIR = gajimpaths['HOME']
+PLUGINS_DIRS = [gajimpaths['PLUGINS_BASE'],
+ gajimpaths['PLUGINS_USER']]
+PLUGINS_CONFIG_DIR = gajimpaths['PLUGINS_CONFIG_DIR']
try:
LANG = locale.getdefaultlocale()[0] # en_US, fr_FR, el_GR etc..
diff --git a/src/common/ged.py b/src/common/ged.py
index 92c54a268..485176fa2 100644
--- a/src/common/ged.py
+++ b/src/common/ged.py
@@ -30,6 +30,9 @@ log = logging.getLogger('gajim.common.ged')
PRECORE = 30
CORE = 40
POSTCORE = 50
+GUI1 = 60
+GUI2 = 70
+POSTGUI = 80
class GlobalEventsDispatcher(object):
@@ -61,4 +64,5 @@ class GlobalEventsDispatcher(object):
log.debug('%s\nArgs: %s'%(event_name, str(args)))
if event_name in self.handlers:
for priority, handler in self.handlers[event_name]:
- handler(*args, **kwargs)
+ if handler(*args, **kwargs):
+ return
diff --git a/src/common/nec.py b/src/common/nec.py
new file mode 100644
index 000000000..ec41b05e3
--- /dev/null
+++ b/src/common/nec.py
@@ -0,0 +1,134 @@
+# -*- 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 .
+##
+
+'''
+Network Events Controller.
+
+:author: Mateusz Biliński
+:since: 10th August 2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+from pprint import pformat
+
+#from plugins.helpers import log
+from common import gajim
+
+class NetworkEventsController(object):
+
+ def __init__(self):
+ self.incoming_events_generators = {}
+ '''
+ Keys: names of events
+ Values: list of class objects that are subclasses
+ of `NetworkIncomingEvent`
+ '''
+
+ def register_incoming_event(self, event_class):
+ for base_event_name in event_class.base_network_events:
+ event_list = self.incoming_events_generators.setdefault(base_event_name, [])
+ if not event_class in event_list:
+ event_list.append(event_class)
+
+ def unregister_incoming_event(self, event_class):
+ for base_event_name in event_class.base_network_events:
+ if base_event_name in self.incoming_events_generators:
+ self.incoming_events_generators[base_event_name].remove(event_class)
+
+ def register_outgoing_event(self, event_class):
+ pass
+
+ def unregister_outgoing_event(self, event_class):
+ pass
+
+ def push_incoming_event(self, event_object):
+ if event_object.generate():
+ if self._generate_events_based_on_incoming_event(event_object):
+ gajim.ged.raise_event(event_object.name, event_object)
+
+ def push_outgoing_event(self, event_object):
+ pass
+
+ def _generate_events_based_on_incoming_event(self, event_object):
+ '''
+ :return: True if even_object should be dispatched through Global
+ Events Dispatcher, False otherwise. This can be used to replace
+ base events with those that more data computed (easier to use
+ by handlers).
+ :note: replacing mechanism is not implemented currently, but will be
+ based on attribute in new network events object.
+ '''
+ base_event_name = event_object.name
+ if base_event_name in self.incoming_events_generators:
+ for new_event_class in self.incoming_events_generators[base_event_name]:
+ new_event_object = new_event_class(None, base_event=event_object)
+ if new_event_object.generate():
+ if self._generate_events_based_on_incoming_event(new_event_object):
+ gajim.ged.raise_event(new_event_object.name, new_event_object)
+ return True
+
+class NetworkEvent(object):
+ name = ''
+
+ def __init__(self, new_name, **kwargs):
+ if new_name:
+ self.name = new_name
+
+ self._set_kwargs_as_attributes(**kwargs)
+
+ self.init()
+
+ def init(self):
+ pass
+
+
+ def generate(self):
+ '''
+ Generates new event (sets it's attributes) based on event object.
+
+ Base event object name is one of those in `base_network_events`.
+
+ Reference to base event object is stored in `self.base_event` attribute.
+
+ Note that this is a reference, so modifications to that event object
+ are possible before dispatching to Global Events Dispatcher.
+
+ :return: True if generated event should be dispatched, False otherwise.
+ '''
+ return True
+
+ def _set_kwargs_as_attributes(self, **kwargs):
+ for k, v in kwargs.iteritems():
+ setattr(self, k, v)
+
+ def __str__(self):
+ return ' Attributes: %s'%(pformat(self.__dict__))
+
+ def __repr__(self):
+ return ' Attributes: %s'%(pformat(self.__dict__))
+
+
+class NetworkIncomingEvent(NetworkEvent):
+ base_network_events = []
+ '''
+ Names of base network events that new event is going to be generated on.
+ '''
+
+
+class NetworkOutgoingEvent(NetworkEvent):
+ pass
\ No newline at end of file
diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py
index ceaa49c9a..eb06c6a28 100644
--- a/src/common/xmpp/protocol.py
+++ b/src/common/xmpp/protocol.py
@@ -717,7 +717,7 @@ class Message(Protocol):
if xmllang:
dom = NodeBuilder('%s' % (NS_XHTML, xmllang, val)).getDom()
else:
- dom = NodeBuilder('%s, 0' % (NS_XHTM, val)).getDom()
+ dom = NodeBuilder('%s, 0' % (NS_XHTML, val)).getDom()
if self.getTag('html'):
self.getTag('html').addChild(node=dom)
else:
diff --git a/src/gajim-remote-plugin.py b/src/gajim-remote-plugin.py
new file mode 100755
index 000000000..e9acaf8f1
--- /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 96895561e..0f9cec821 100644
--- a/src/gajim-remote.py
+++ b/src/gajim-remote.py
@@ -58,7 +58,7 @@ except Exception:
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/groupchat_control.py b/src/groupchat_control.py
index 75a85c541..9296667f3 100644
--- a/src/groupchat_control.py
+++ b/src/groupchat_control.py
@@ -1669,6 +1669,10 @@ class GroupchatControl(ChatControlBase):
del win._controls[self.account][self.contact.jid]
def shutdown(self, status='offline'):
+ # PluginSystem: calling shutdown of super class (ChatControlBase)
+ # to let it remove it's GUI extension points
+ super(GroupchatControl, self).shutdown()
+
# Preventing autorejoin from being activated
self.autorejoin = False
diff --git a/src/gui_interface.py b/src/gui_interface.py
index 1fb6c3043..ac6d4cd3f 100644
--- a/src/gui_interface.py
+++ b/src/gui_interface.py
@@ -148,23 +148,24 @@ class Interface:
self.instances['change_nick_dialog'] = dialogs.ChangeNickDialog(
account, room_jid, title, prompt)
- def handle_event_http_auth(self, account, data):
+ def handle_event_http_auth(self, obj):
#('HTTP_AUTH', account, (method, url, transaction_id, iq_obj, msg))
- def response(account, iq_obj, answer):
- gajim.connections[account].build_http_auth_answer(iq_obj, answer)
+ def response(account, answer):
+ obj.conn.build_http_auth_answer(obj.iq_obj, answer)
- def on_yes(is_checked, account, iq_obj):
- response(account, iq_obj, 'yes')
+ def on_yes(is_checked, obj):
+ response(obj, 'yes')
+ account = obj.conn.name
sec_msg = _('Do you accept this request?')
if gajim.get_number_of_connected_accounts() > 1:
sec_msg = _('Do you accept this request on account %s?') % account
- if data[4]:
- sec_msg = data[4] + '\n' + sec_msg
+ if obj.msg:
+ sec_msg = obj.msg + '\n' + sec_msg
dialog = dialogs.YesNoDialog(_('HTTP (%(method)s) Authorization for '
- '%(url)s (id: %(id)s)') % {'method': data[0], 'url': data[1],
- 'id': data[2]}, sec_msg, on_response_yes=(on_yes, account, data[3]),
- on_response_no=(response, account, data[3], 'no'))
+ '%(url)s (id: %(id)s)') % {'method': obj.method, 'url': obj.url,
+ 'id': obj.iq_id}, sec_msg, on_response_yes=(on_yes, obj),
+ on_response_no=(response, obj, 'no'))
def handle_event_error_answer(self, account, array):
#('ERROR_ANSWER', account, (id, jid_from, errmsg, errcode))
@@ -825,57 +826,24 @@ class Interface:
if self.remote_ctrl:
self.remote_ctrl.raise_signal('VcardInfo', (account, vcard))
- def handle_event_last_status_time(self, account, array):
+ def handle_event_last_status_time(self, obj):
# ('LAST_STATUS_TIME', account, (jid, resource, seconds, status))
- tim = array[2]
- if tim < 0:
+ if obj.seconds < 0:
# Ann error occured
return
- win = None
- if array[0] in self.instances[account]['infos']:
- win = self.instances[account]['infos'][array[0]]
- elif array[0] + '/' + array[1] in self.instances[account]['infos']:
- win = self.instances[account]['infos'][array[0] + '/' + array[1]]
- c = gajim.contacts.get_contact(account, array[0], array[1])
+ account = obj.conn.name
+ c = gajim.contacts.get_contact(account, obj.jid, obj.resource)
if c: # c can be none if it's a gc contact
- if array[3]:
- c.status = array[3]
+ if obj.status:
+ c.status = obj.status
self.roster.draw_contact(c.jid, account) # draw offline status
- last_time = time.localtime(time.time() - tim)
+ last_time = time.localtime(time.time() - obj.seconds)
if c.show == 'offline':
c.last_status_time = last_time
else:
c.last_activity_time = last_time
- if win:
- win.set_last_status_time()
- if self.roster.tooltip.id and self.roster.tooltip.win:
- self.roster.tooltip.update_last_time(last_time)
- if self.remote_ctrl:
- self.remote_ctrl.raise_signal('LastStatusTime', (account, array))
-
- def handle_event_os_info(self, account, array):
- #'OS_INFO' (account, (jid, resource, client_info, os_info))
- win = None
- if array[0] in self.instances[account]['infos']:
- win = self.instances[account]['infos'][array[0]]
- elif array[0] + '/' + array[1] in self.instances[account]['infos']:
- win = self.instances[account]['infos'][array[0] + '/' + array[1]]
- if win:
- win.set_os_info(array[1], array[2], array[3])
- if self.remote_ctrl:
- self.remote_ctrl.raise_signal('OsInfo', (account, array))
-
- def handle_event_entity_time(self, account, array):
- #'ENTITY_TIME' (account, (jid, resource, time_info))
- win = None
- if array[0] in self.instances[account]['infos']:
- win = self.instances[account]['infos'][array[0]]
- elif array[0] + '/' + array[1] in self.instances[account]['infos']:
- win = self.instances[account]['infos'][array[0] + '/' + array[1]]
- if win:
- win.set_entity_time(array[1], array[2])
- if self.remote_ctrl:
- self.remote_ctrl.raise_signal('EntityTime', (account, array))
+ if self.roster.tooltip.id and self.roster.tooltip.win:
+ self.roster.tooltip.update_last_time(last_time)
def handle_event_gc_notify(self, account, array):
#'GC_NOTIFY' (account, (room_jid, show, status, nick,
@@ -1321,45 +1289,42 @@ class Interface:
notify.popup(event_type, jid, account, 'file-send-error', path,
event_type, file_props['name'])
- def handle_event_gmail_notify(self, account, array):
- jid = array[0]
- gmail_new_messages = int(array[1])
- gmail_messages_list = array[2]
- if gajim.config.get('notify_on_new_gmail_email'):
- path = gtkgui_helpers.get_icon_path('gajim-new_email_recv', 48)
- title = _('New mail on %(gmail_mail_address)s') % \
- {'gmail_mail_address': jid}
- text = i18n.ngettext('You have %d new mail conversation',
- 'You have %d new mail conversations', gmail_new_messages,
- gmail_new_messages, gmail_new_messages)
+ def handle_event_gmail_notify(self, obj):
+ jid = obj.jid
+ gmail_new_messages = int(obj.newmsgs)
+ gmail_messages_list = obj.gmail_messages_list
+ if not gajim.config.get('notify_on_new_gmail_email'):
+ return
+ path = gtkgui_helpers.get_icon_path('gajim-new_email_recv', 48)
+ title = _('New mail on %(gmail_mail_address)s') % \
+ {'gmail_mail_address': jid}
+ text = i18n.ngettext('You have %d new mail conversation',
+ 'You have %d new mail conversations', gmail_new_messages,
+ gmail_new_messages, gmail_new_messages)
- if gajim.config.get('notify_on_new_gmail_email_extra'):
- cnt = 0
- for gmessage in gmail_messages_list:
- # FIXME: emulate Gtalk client popups. find out what they
- # parse and how they decide what to show each message has a
- # 'From', 'Subject' and 'Snippet' field
- if cnt >= 5:
- break
- senders = ',\n '.join(reversed(gmessage['From']))
- text += _('\n\nFrom: %(from_address)s\nSubject: '
- '%(subject)s\n%(snippet)s') % \
- {'from_address': senders,
- 'subject': gmessage['Subject'],
- 'snippet': gmessage['Snippet']}
- cnt += 1
+ if gajim.config.get('notify_on_new_gmail_email_extra'):
+ cnt = 0
+ for gmessage in gmail_messages_list:
+ # FIXME: emulate Gtalk client popups. find out what they
+ # parse and how they decide what to show each message has a
+ # 'From', 'Subject' and 'Snippet' field
+ if cnt >= 5:
+ break
+ senders = ',\n '.join(reversed(gmessage['From']))
+ text += _('\n\nFrom: %(from_address)s\nSubject: '
+ '%(subject)s\n%(snippet)s') % {'from_address': senders,
+ 'subject': gmessage['Subject'],
+ 'snippet': gmessage['Snippet']}
+ cnt += 1
- command = gajim.config.get('notify_on_new_gmail_email_command')
- if command:
- Popen(command, shell=True)
+ command = gajim.config.get('notify_on_new_gmail_email_command')
+ if command:
+ Popen(command, shell=True)
- if gajim.config.get_per('soundevents', 'gmail_received', 'enabled'):
- helpers.play_sound('gmail_received')
- notify.popup(_('New E-mail'), jid, account, 'gmail',
- path_to_image=path, title=title, text=text)
-
- if self.remote_ctrl:
- self.remote_ctrl.raise_signal('NewGmail', (account, array))
+ if gajim.config.get_per('soundevents', 'gmail_received', 'enabled'):
+ helpers.play_sound('gmail_received')
+ notify.popup(_('New E-mail'), jid, obj.conn.name, 'gmail',
+ path_to_image=path, title=title, text=text)
def handle_event_file_request_error(self, account, array):
# ('FILE_REQUEST_ERROR', account, (jid, file_props, error_msg))
@@ -1870,9 +1835,10 @@ class Interface:
if 'pep_services' in self.instances[account]:
self.instances[account]['pep_services'].config(data[0], data[1])
- def handle_event_roster_item_exchange(self, account, data):
+ def handle_event_roster_item_exchange(self, obj):
# data = (action in [add, delete, modify], exchange_list, jid_from)
- dialogs.RosterItemExchangeWindow(account, data[0], data[1], data[2])
+ dialogs.RosterItemExchangeWindow(obj.conn.name, obj.action,
+ obj.exchange_items_list, obj.fjid)
def handle_event_unique_room_id_supported(self, account, data):
"""
@@ -2122,9 +2088,6 @@ class Interface:
'ACC_OK': [self.handle_event_acc_ok],
'MYVCARD': [self.handle_event_myvcard],
'VCARD': [self.handle_event_vcard],
- 'LAST_STATUS_TIME': [self.handle_event_last_status_time],
- 'OS_INFO': [self.handle_event_os_info],
- 'ENTITY_TIME': [self.handle_event_entity_time],
'GC_NOTIFY': [self.handle_event_gc_notify],
'GC_MSG': [self.handle_event_gc_msg],
'GC_SUBJECT': [self.handle_event_gc_subject],
@@ -2140,12 +2103,10 @@ class Interface:
'CON_TYPE': [self.handle_event_con_type],
'CONNECTION_LOST': [self.handle_event_connection_lost],
'FILE_REQUEST': [self.handle_event_file_request],
- 'GMAIL_NOTIFY': [self.handle_event_gmail_notify],
'FILE_REQUEST_ERROR': [self.handle_event_file_request_error],
'FILE_SEND_ERROR': [self.handle_event_file_send_error],
'STANZA_ARRIVED': [self.handle_event_stanza_arrived],
'STANZA_SENT': [self.handle_event_stanza_sent],
- 'HTTP_AUTH': [self.handle_event_http_auth],
'VCARD_PUBLISHED': [self.handle_event_vcard_published],
'VCARD_NOT_PUBLISHED': [self.handle_event_vcard_not_published],
'ASK_NEW_NICK': [self.handle_event_ask_new_nick],
@@ -2166,7 +2127,6 @@ class Interface:
'SEARCH_FORM': [self.handle_event_search_form],
'SEARCH_RESULT': [self.handle_event_search_result],
'RESOURCE_CONFLICT': [self.handle_event_resource_conflict],
- 'ROSTERX': [self.handle_event_roster_item_exchange],
'PEP_CONFIG': [self.handle_event_pep_config],
'UNIQUE_ROOM_ID_UNSUPPORTED': \
[self.handle_event_unique_room_id_unsupported],
@@ -2186,7 +2146,12 @@ class Interface:
'JINGLE_DISCONNECTED': [self.handle_event_jingle_disconnected],
'JINGLE_ERROR': [self.handle_event_jingle_error],
'PEP_RECEIVED': [self.handle_event_pep_received],
- 'CAPS_RECEIVED': [self.handle_event_caps_received]
+ 'CAPS_RECEIVED': [self.handle_event_caps_received],
+ 'gmail-notify': [self.handle_event_gmail_notify],
+ 'http-auth-received': [self.handle_event_http_auth],
+ 'last-result-received': [self.handle_event_last_status_time],
+ 'roster-item-exchange-received': \
+ [self.handle_event_roster_item_exchange],
}
def register_core_handlers(self):
@@ -2197,7 +2162,7 @@ class Interface:
"""
for event_name, event_handlers in self.handlers.iteritems():
for event_handler in event_handlers:
- gajim.ged.register_event_handler(event_name, ged.CORE,
+ gajim.ged.register_event_handler(event_name, ged.GUI1,
event_handler)
################################################################################
@@ -3246,6 +3211,10 @@ class Interface:
self.show_systray()
self.roster = roster_window.RosterWindow()
+ # Creating plugin manager
+ import plugins
+ gajim.plugin_manager = plugins.PluginManager()
+
self.roster._before_fill()
for account in gajim.connections:
gajim.connections[account].load_roster_from_db()
@@ -3276,7 +3245,6 @@ class Interface:
pass
gobject.timeout_add_seconds(5, remote_init)
-
def __init__(self):
gajim.interface = self
gajim.thread_interface = ThreadInterface
@@ -3398,6 +3366,9 @@ class Interface:
# Creating Global Events Dispatcher
gajim.ged = ged.GlobalEventsDispatcher()
+ # Creating Network Events Controller
+ from common import nec
+ gajim.nec = nec.NetworkEventsController()
self.create_core_handlers_list()
self.register_core_handlers()
diff --git a/src/message_control.py b/src/message_control.py
index d09cb446b..700ac9485 100644
--- a/src/message_control.py
+++ b/src/message_control.py
@@ -38,7 +38,7 @@ TYPE_PM = 'pm'
####################
-class MessageControl:
+class MessageControl(object):
"""
An abstract base widget that can embed in the gtk.Notebook of a
MessageWindow
diff --git a/src/message_window.py b/src/message_window.py
index 4dff5394c..8678930d8 100644
--- a/src/message_window.py
+++ b/src/message_window.py
@@ -298,9 +298,10 @@ _('Do you really want to close them all?'),
self.notebook.show_all()
else:
self.window.show_all()
- # NOTE: we do not call set_control_active(True) since we don't know whether
- # the tab is the active one.
+ # NOTE: we do not call set_control_active(True) since we don't know
+ # whether the tab is the active one.
self.show_title()
+ gobject.idle_add(control.msg_textview.grab_focus)
def on_tab_eventbox_button_press_event(self, widget, event, child):
if event.button == 3: # right click
@@ -511,6 +512,7 @@ _('Do you really want to close them all?'),
ctrl_page = self.notebook.page_num(ctrl.widget)
self.notebook.set_current_page(ctrl_page)
self.window.present()
+ gobject.idle_add(ctrl.msg_textview.grab_focus)
def remove_tab(self, ctrl, method, reason = None, force = False):
"""
diff --git a/src/plugins/__init__.py b/src/plugins/__init__.py
new file mode 100644
index 000000000..0d5d18fda
--- /dev/null
+++ b/src/plugins/__init__.py
@@ -0,0 +1,30 @@
+# -*- 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 .
+##
+
+'''
+Main file of plugins package.
+
+:author: Mateusz Biliński
+:since: 05/30/2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+from pluginmanager import PluginManager
+from plugin import GajimPlugin
+
+__all__ = ['PluginManager', 'GajimPlugin']
diff --git a/src/plugins/gui.py b/src/plugins/gui.py
new file mode 100644
index 000000000..ab0281e1f
--- /dev/null
+++ b/src/plugins/gui.py
@@ -0,0 +1,220 @@
+# -*- 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 .
+##
+
+'''
+GUI classes related to plug-in management.
+
+:author: Mateusz Biliński
+:since: 6th June 2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+__all__ = ['PluginsWindow']
+
+import pango
+import gtk, gobject
+
+import gtkgui_helpers
+from common import gajim
+
+from plugins.helpers import log_calls, log
+
+class PluginsWindow(object):
+ '''Class for Plugins window'''
+
+ @log_calls('PluginsWindow')
+ def __init__(self):
+ '''Initialize Plugins window'''
+ self.xml = gtkgui_helpers.get_gtk_builder('plugins_window.ui')
+ self.window = self.xml.get_object('plugins_window')
+ self.window.set_transient_for(gajim.interface.roster.window)
+
+ widgets_to_extract = ('plugins_notebook',
+ 'plugin_name_label',
+ 'plugin_version_label',
+ 'plugin_authors_label',
+ 'plugin_homepage_linkbutton',
+ 'plugin_description_textview',
+ 'uninstall_plugin_button',
+ 'configure_plugin_button',
+ 'installed_plugins_treeview')
+
+ for widget_name in widgets_to_extract:
+ setattr(self, widget_name, self.xml.get_object(widget_name))
+
+ attr_list = pango.AttrList()
+ attr_list.insert(pango.AttrWeight(pango.WEIGHT_BOLD, 0, -1))
+ self.plugin_name_label.set_attributes(attr_list)
+
+ self.installed_plugins_model = gtk.ListStore(gobject.TYPE_PYOBJECT,
+ gobject.TYPE_STRING,
+ gobject.TYPE_BOOLEAN)
+ self.installed_plugins_treeview.set_model(self.installed_plugins_model)
+
+ renderer = gtk.CellRendererText()
+ col = gtk.TreeViewColumn(_('Plugin'), renderer, text=1)
+ self.installed_plugins_treeview.append_column(col)
+
+ renderer = gtk.CellRendererToggle()
+ renderer.set_property('activatable', True)
+ renderer.connect('toggled', self.installed_plugins_toggled_cb)
+ col = gtk.TreeViewColumn(_('Active'), renderer, active=2)
+ self.installed_plugins_treeview.append_column(col)
+
+ # connect signal for selection change
+ selection = self.installed_plugins_treeview.get_selection()
+ selection.connect('changed',
+ self.installed_plugins_treeview_selection_changed)
+ selection.set_mode(gtk.SELECTION_SINGLE)
+
+ self._clear_installed_plugin_info()
+
+ self.fill_installed_plugins_model()
+
+ self.xml.connect_signals(self)
+
+ self.plugins_notebook.set_current_page(0)
+
+ self.window.show_all()
+ gtkgui_helpers.possibly_move_window_in_current_desktop(self.window)
+
+ @log_calls('PluginsWindow')
+ def installed_plugins_treeview_selection_changed(self, treeview_selection):
+ model, iter = treeview_selection.get_selected()
+ if iter:
+ plugin = model.get_value(iter, 0)
+ plugin_name = model.get_value(iter, 1)
+ is_active = model.get_value(iter, 2)
+
+ self._display_installed_plugin_info(plugin)
+ else:
+ self._clear_installed_plugin_info()
+
+ def _display_installed_plugin_info(self, plugin):
+ self.plugin_name_label.set_text(plugin.name)
+ self.plugin_version_label.set_text(plugin.version)
+ self.plugin_authors_label.set_text(", ".join(plugin.authors))
+ self.plugin_homepage_linkbutton.set_uri(plugin.homepage)
+ self.plugin_homepage_linkbutton.set_label(plugin.homepage)
+ self.plugin_homepage_linkbutton.set_property('sensitive', True)
+
+ desc_textbuffer = self.plugin_description_textview.get_buffer()
+ desc_textbuffer.set_text(plugin.description)
+ self.plugin_description_textview.set_property('sensitive', True)
+ self.uninstall_plugin_button.set_property('sensitive', True)
+ if plugin.config_dialog is None:
+ self.configure_plugin_button.set_property('sensitive', False)
+ else:
+ self.configure_plugin_button.set_property('sensitive', True)
+
+ def _clear_installed_plugin_info(self):
+ self.plugin_name_label.set_text('')
+ self.plugin_version_label.set_text('')
+ self.plugin_authors_label.set_text('')
+ self.plugin_homepage_linkbutton.set_uri('')
+ self.plugin_homepage_linkbutton.set_label('')
+ self.plugin_homepage_linkbutton.set_property('sensitive', False)
+
+ desc_textbuffer = self.plugin_description_textview.get_buffer()
+ desc_textbuffer.set_text('')
+ self.plugin_description_textview.set_property('sensitive', False)
+ self.uninstall_plugin_button.set_property('sensitive', False)
+ self.configure_plugin_button.set_property('sensitive', False)
+
+ @log_calls('PluginsWindow')
+ def fill_installed_plugins_model(self):
+ pm = gajim.plugin_manager
+ self.installed_plugins_model.clear()
+ self.installed_plugins_model.set_sort_column_id(1, gtk.SORT_ASCENDING)
+
+ for plugin in pm.plugins:
+ self.installed_plugins_model.append([plugin,
+ plugin.name,
+ plugin.active])
+
+ @log_calls('PluginsWindow')
+ def installed_plugins_toggled_cb(self, cell, path):
+ is_active = self.installed_plugins_model[path][2]
+ plugin = self.installed_plugins_model[path][0]
+
+ if is_active:
+ gajim.plugin_manager.deactivate_plugin(plugin)
+ else:
+ gajim.plugin_manager.activate_plugin(plugin)
+
+ self.installed_plugins_model[path][2] = not is_active
+
+ @log_calls('PluginsWindow')
+ def on_plugins_window_destroy(self, widget):
+ '''Close window'''
+ del gajim.interface.instances['plugins']
+
+ @log_calls('PluginsWindow')
+ def on_close_button_clicked(self, widget):
+ self.window.destroy()
+
+ @log_calls('PluginsWindow')
+ def on_configure_plugin_button_clicked(self, widget):
+ #log.debug('widget: %s'%(widget))
+ selection = self.installed_plugins_treeview.get_selection()
+ model, iter = selection.get_selected()
+ if iter:
+ plugin = model.get_value(iter, 0)
+ plugin_name = model.get_value(iter, 1)
+ is_active = model.get_value(iter, 2)
+
+
+ result = plugin.config_dialog.run(self.window)
+
+ else:
+ # No plugin selected. this should never be reached. As configure
+ # plugin button should only be clickable when plugin is selected.
+ # XXX: maybe throw exception here?
+ pass
+
+ @log_calls('PluginsWindow')
+ def on_uninstall_plugin_button_clicked(self, widget):
+ pass
+
+
+class GajimPluginConfigDialog(gtk.Dialog):
+
+ @log_calls('GajimPluginConfigDialog')
+ def __init__(self, plugin, **kwargs):
+ gtk.Dialog.__init__(self, '%s %s'%(plugin.name, _('Configuration')), **kwargs)
+ self.plugin = plugin
+ self.add_button('gtk-close', gtk.RESPONSE_CLOSE)
+
+ self.child.set_spacing(3)
+
+ self.init()
+
+ @log_calls('GajimPluginConfigDialog')
+ def run(self, parent=None):
+ self.set_transient_for(parent)
+ self.on_run()
+ self.show_all()
+ result = super(GajimPluginConfigDialog, self).run()
+ self.hide()
+ return result
+
+ def init(self):
+ pass
+
+ def on_run(self):
+ pass
diff --git a/src/plugins/helpers.py b/src/plugins/helpers.py
new file mode 100644
index 000000000..6b62fa3ec
--- /dev/null
+++ b/src/plugins/helpers.py
@@ -0,0 +1,140 @@
+# -*- 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 .
+##
+
+'''
+Helper code related to plug-ins management system.
+
+:author: Mateusz Biliński
+:since: 30th May 2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+__all__ = ['log', 'log_calls', 'Singleton']
+
+import logging
+log = logging.getLogger('gajim.plugin_system')
+'''
+Logger for code related to plug-in system.
+
+:type: logging.Logger
+'''
+
+consoleloghandler = logging.StreamHandler()
+#consoleloghandler.setLevel(1)
+consoleloghandler.setFormatter(
+ logging.Formatter('%(levelname)s: %(message)s'))
+ #logging.Formatter('%(asctime)s %(name)s: %(levelname)s: %(message)s'))
+#log.setLevel(logging.DEBUG)
+log.addHandler(consoleloghandler)
+log.propagate = False
+
+import functools
+
+class log_calls(object):
+ '''
+ Decorator class for functions to easily log when they are entered and left.
+ '''
+
+ filter_out_classes = ['GajimPluginConfig', 'PluginManager',
+ 'GajimPluginConfigDialog', 'PluginsWindow']
+ '''
+ List of classes from which no logs should be emited when methods are called,
+ eventhough `log_calls` decorator is used.
+ '''
+
+ def __init__(self, classname='', log=log):
+ '''
+ :Keywords:
+ classname : str
+ Name of class to prefix function name (if function is a method).
+ log : logging.Logger
+ Logger to use when outputing debug information on when function has
+ been entered and when left. By default: `plugins.helpers.log`
+ is used.
+ '''
+
+ self.full_func_name = ''
+ '''
+ Full name of function, with class name (as prefix) if given
+ to decorator.
+
+ Otherwise, it's only function name retrieved from function object
+ for which decorator was called.
+
+ :type: str
+ '''
+ self.log_this_class = True
+ '''
+ Determines whether wrapper of given function should log calls of this
+ function or not.
+
+ :type: bool
+ '''
+
+ if classname:
+ self.full_func_name = classname+'.'
+
+ if classname in self.filter_out_classes:
+ self.log_this_class = False
+
+ def __call__(self, f):
+ '''
+ :param f: function to be wrapped with logging statements
+
+ :return: given function wrapped by *log.debug* statements
+ :rtype: function
+ '''
+
+ self.full_func_name += f.func_name
+ if self.log_this_class:
+ @functools.wraps(f)
+ def wrapper(*args, **kwargs):
+
+ log.debug('%(funcname)s() '%{
+ 'funcname': self.full_func_name})
+ result = f(*args, **kwargs)
+ log.debug('%(funcname)s() '%{
+ 'funcname': self.full_func_name})
+ return result
+ else:
+ @functools.wraps(f)
+ def wrapper(*args, **kwargs):
+ result = f(*args, **kwargs)
+ return result
+
+ return wrapper
+
+class Singleton(type):
+ '''
+ Singleton metaclass.
+ '''
+ def __init__(cls, name, bases, dic):
+ super(Singleton, cls).__init__(name, bases, dic)
+ cls.instance=None
+
+ def __call__(cls,*args,**kw):
+ if cls.instance is None:
+ cls.instance=super(Singleton, cls).__call__(*args,**kw)
+ #log.debug('%(classname)s - new instance created'%{
+ #'classname' : cls.__name__})
+ else:
+ pass
+ #log.debug('%(classname)s - returning already existing instance'%{
+ #'classname' : cls.__name__})
+
+ return cls.instance
diff --git a/src/plugins/plugin.py b/src/plugins/plugin.py
new file mode 100644
index 000000000..19ca1ce5e
--- /dev/null
+++ b/src/plugins/plugin.py
@@ -0,0 +1,234 @@
+# -*- 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 .
+##
+
+'''
+Base class for implementing plugin.
+
+:author: Mateusz Biliński
+:since: 1st June 2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+import os
+
+from common import gajim
+
+from plugins.helpers import log_calls, log
+from plugins.gui import GajimPluginConfigDialog
+
+
+class GajimPlugin(object):
+ '''
+ Base class for implementing Gajim plugins.
+ '''
+ name = u''
+ '''
+ Name of plugin.
+
+ Will be shown in plugins management GUI.
+
+ :type: unicode
+ '''
+ short_name = u''
+ '''
+ Short name of plugin.
+
+ Used for quick indentification of plugin.
+
+ :type: unicode
+
+ :todo: decide whether we really need this one, because class name (with
+ module name) can act as such short name
+ '''
+ version = u''
+ '''
+ Version of plugin.
+
+ :type: unicode
+
+ :todo: decide how to compare version between each other (which one
+ is higher). Also rethink: do we really need to compare versions
+ of plugins between each other? This would be only useful if we detect
+ same plugin class but with different version and we want only the newest
+ one to be active - is such policy good?
+ '''
+ description = u''
+ '''
+ Plugin description.
+
+ :type: unicode
+
+ :todo: should be allow rich text here (like HTML or reStructuredText)?
+ '''
+ authors = []
+ '''
+ Plugin authors.
+
+ :type: [] of unicode
+
+ :todo: should we decide on any particular format of author strings?
+ Especially: should we force format of giving author's e-mail?
+ '''
+ homepage = u''
+ '''
+ URL to plug-in's homepage.
+
+ :type: unicode
+
+ :todo: should we check whether provided string is valid URI? (Maybe
+ using 'property')
+ '''
+ gui_extension_points = {}
+ '''
+ 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 = {}
+ '''
+ Default values for keys that should be stored in plug-in config.
+
+ This dict is used when when someone calls for config option but it has not
+ been set yet.
+
+ Values are tuples: (default_value, option_description). The first one can
+ be anything (this is the advantage of using shelve/pickle instead of
+ custom-made config I/O handling); the second one should be unicode (gettext
+ can be used if need and/or translation is planned).
+
+ :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
+ '''
+ events = []
+ '''
+ New network event classes to be registered in Network Events Controller.
+
+ :type: [] of `nec.NetworkIncomingEvent` or `nec.NetworkOutgoingEvent`
+ subclasses.
+ '''
+
+ @log_calls('GajimPlugin')
+ def __init__(self):
+ self.config = GajimPluginConfig(self)
+ '''
+ Plug-in configuration dictionary.
+
+ Automatically saved and loaded and plug-in (un)load.
+
+ :type: `plugins.plugin.GajimPluginConfig`
+ '''
+ self.load_config()
+ self.config_dialog = GajimPluginConfigDialog(self)
+ self.init()
+
+ @log_calls('GajimPlugin')
+ def save_config(self):
+ self.config.save()
+
+ @log_calls('GajimPlugin')
+ def load_config(self):
+ self.config.load()
+
+ def __eq__(self, plugin):
+ if self.short_name == plugin.short_name:
+ return True
+
+ return False
+
+ def __ne__(self, plugin):
+ if self.short_name != plugin.short_name:
+ return True
+
+ return False
+
+ @log_calls('GajimPlugin')
+ def local_file_path(self, file_name):
+ return os.path.join(self.__path__, file_name)
+
+ @log_calls('GajimPlugin')
+ def init(self):
+ pass
+
+ @log_calls('GajimPlugin')
+ def activate(self):
+ pass
+
+ @log_calls('GajimPlugin')
+ def deactivate(self):
+ pass
+
+import shelve
+import UserDict
+
+class GajimPluginConfig(UserDict.DictMixin):
+ @log_calls('GajimPluginConfig')
+ def __init__(self, plugin):
+ self.plugin = plugin
+ self.FILE_PATH = os.path.join(gajim.PLUGINS_CONFIG_DIR, self.plugin.short_name)
+ #log.debug('FILE_PATH = %s'%(self.FILE_PATH))
+ self.data = None
+ self.load()
+
+ @log_calls('GajimPluginConfig')
+ def __getitem__(self, key):
+ if not key in self.data:
+ self.data[key] = self.plugin.config_default_values[key][0]
+ self.save()
+
+ return self.data[key]
+
+ @log_calls('GajimPluginConfig')
+ def __setitem__(self, key, value):
+ self.data[key] = value
+ self.save()
+
+ def keys(self):
+ return self.data.keys()
+
+ @log_calls('GajimPluginConfig')
+ def save(self):
+ self.data.sync()
+ #log.debug(str(self.data))
+
+ @log_calls('GajimPluginConfig')
+ def load(self):
+ self.data = shelve.open(self.FILE_PATH)
+
+class GajimPluginException(Exception):
+ pass
+
+class GajimPluginInitError(GajimPluginException):
+ pass
diff --git a/src/plugins/pluginmanager.py b/src/plugins/pluginmanager.py
new file mode 100644
index 000000000..a0708c89c
--- /dev/null
+++ b/src/plugins/pluginmanager.py
@@ -0,0 +1,456 @@
+# -*- 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 .
+##
+
+'''
+Plug-in management related classes.
+
+:author: Mateusz Biliński
+:since: 30th May 2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+__all__ = ['PluginManager']
+
+import os
+import sys
+import fnmatch
+
+from common import gajim
+from common import nec
+
+from plugins.helpers import log, log_calls, Singleton
+from plugins.plugin import GajimPlugin
+
+class PluginManager(object):
+ '''
+ Main plug-in management class.
+
+ Currently:
+ - scans for plugins
+ - activates them
+ - handles GUI extension points, when called by GUI objects after plugin
+ is activated (by dispatching info about call to handlers in plugins)
+
+ :todo: add more info about how GUI extension points work
+ :todo: add list of available GUI extension points
+ :todo: implement mechanism to dynamically load plugins where GUI extension
+ points have been already called (i.e. when plugin is activated
+ after GUI object creation). [DONE?]
+ :todo: implement mechanism to dynamically deactive plugins (call plugin's
+ deactivation handler) [DONE?]
+ :todo: when plug-in is deactivated all GUI extension points are removed
+ from `PluginManager.gui_extension_points_handlers`. But when
+ object that invoked GUI extension point is abandoned by Gajim, eg.
+ closed ChatControl object, the reference to called GUI extension
+ points is still in `PluginManager.gui_extension_points`. These
+ should be removed, so that object can be destroyed by Python.
+ Possible solution: add call to clean up method in classes
+ 'destructors' (classes that register GUI extension points)
+ '''
+
+ __metaclass__ = Singleton
+
+ #@log_calls('PluginManager')
+ def __init__(self):
+ self.plugins = []
+ '''
+ Detected plugin classes.
+
+ Each class object in list is `GajimPlugin` subclass.
+
+ :type: [] of class objects
+ '''
+ self.active_plugins = []
+ '''
+ Instance objects of active plugins.
+
+ These are object instances of classes held `plugins`, but only those
+ that were activated.
+
+ :type: [] of `GajimPlugin` based objects
+ '''
+ self.gui_extension_points = {}
+ '''
+ Registered GUI extension points.
+ '''
+
+ self.gui_extension_points_handlers = {}
+ '''
+ Registered handlers of GUI extension points.
+ '''
+
+ for path in gajim.PLUGINS_DIRS:
+ self.add_plugins(PluginManager.scan_dir_for_plugins(path))
+
+ #log.debug('plugins: %s'%(self.plugins))
+
+ self._activate_all_plugins_from_global_config()
+
+ #log.debug('active: %s'%(self.active_plugins))
+
+ @log_calls('PluginManager')
+ def _plugin_has_entry_in_global_config(self, plugin):
+ if gajim.config.get_per('plugins', plugin.short_name) is None:
+ return False
+ else:
+ return True
+
+ @log_calls('PluginManager')
+ def _create_plugin_entry_in_global_config(self, plugin):
+ gajim.config.add_per('plugins', plugin.short_name)
+
+ @log_calls('PluginManager')
+ def add_plugin(self, plugin_class):
+ '''
+ :todo: what about adding plug-ins that are already added? Module reload
+ and adding class from reloaded module or ignoring adding plug-in?
+ '''
+ plugin = plugin_class()
+
+ if plugin not in self.plugins:
+ if not self._plugin_has_entry_in_global_config(plugin):
+ self._create_plugin_entry_in_global_config(plugin)
+
+ self.plugins.append(plugin)
+ plugin.active = False
+ else:
+ log.info('Not loading plugin %s v%s from module %s (identified by short name: %s). Plugin already loaded.'%(
+ plugin.name, plugin.version, plugin.__module__, plugin.short_name))
+
+ @log_calls('PluginManager')
+ def add_plugins(self, plugin_classes):
+ for plugin_class in plugin_classes:
+ self.add_plugin(plugin_class)
+
+ @log_calls('PluginManager')
+ def gui_extension_point(self, gui_extpoint_name, *args):
+ '''
+ Invokes all handlers (from plugins) for particular GUI extension point
+ and adds it to collection for further processing (eg. by plugins not active
+ yet).
+
+ :param gui_extpoint_name: name of GUI extension point.
+ :type gui_extpoint_name: unicode
+ :param args: parameters to be passed to extension point handlers
+ (typically and object that invokes `gui_extension_point`; however,
+ this can be practically anything)
+ :type args: tuple
+
+ :todo: GUI extension points must be documented well - names with
+ parameters that will be passed to handlers (in plugins). Such
+ documentation must be obeyed both in core and in plugins. This
+ is a loosely coupled approach and is pretty natural in Python.
+
+ :bug: what if only some handlers are successfully connected? we should
+ revert all those connections that where successfully made. Maybe
+ call 'self._deactivate_plugin()' or sth similar.
+ Looking closer - we only rewrite tuples here. Real check should be
+ made in method that invokes gui_extpoints handlers.
+ '''
+
+ self._add_gui_extension_point_call_to_list(gui_extpoint_name, *args)
+ self._execute_all_handlers_of_gui_extension_point(gui_extpoint_name, *args)
+
+ @log_calls('PluginManager')
+ def remove_gui_extension_point(self, gui_extpoint_name, *args):
+ '''
+ Removes GUI extension point from collection held by `PluginManager`.
+
+ From this point this particular extension point won't be visible
+ to plugins (eg. it won't invoke any handlers when plugin is activated).
+
+ GUI extension point is removed completely (there is no way to recover it
+ from inside `PluginManager`).
+
+ Removal is needed when instance object that given extension point was
+ connect with is destroyed (eg. ChatControl is closed or context menu
+ is hidden).
+
+ Each `PluginManager.gui_extension_point` call should have a call of
+ `PluginManager.remove_gui_extension_point` related to it.
+
+ :note: in current implementation different arguments mean different
+ extension points. The same arguments and the same name mean
+ the same extension point.
+ :todo: instead of using argument to identify which extpoint should be
+ removed, maybe add additional 'id' argument - this would work similar
+ hash in Python objects. 'id' would be calculated based on arguments
+ passed or on anything else (even could be constant). This would give
+ core developers (that add new extpoints) more freedom, but is this
+ necessary?
+
+ :param gui_extpoint_name: name of GUI extension point.
+ :type gui_extpoint_name: unicode
+ :param args: arguments that `PluginManager.gui_extension_point` was
+ called with for this extension point. This is used (along with
+ extension point name) to identify element to be removed.
+ :type args: tuple
+ '''
+
+ if gui_extpoint_name in self.gui_extension_points:
+ #log.debug('Removing GUI extpoint\n name: %s\n args: %s'%(gui_extpoint_name, args))
+ self.gui_extension_points[gui_extpoint_name].remove(args)
+
+
+ @log_calls('PluginManager')
+ def _add_gui_extension_point_call_to_list(self, gui_extpoint_name, *args):
+ '''
+ Adds GUI extension point call to list of calls.
+
+ This is done only if such call hasn't been added already
+ (same extension point name and same arguments).
+
+ :note: This is assumption that GUI extension points are different only
+ if they have different name or different arguments.
+
+ :param gui_extpoint_name: GUI extension point name used to identify it
+ by plugins.
+ :type gui_extpoint_name: str
+
+ :param args: parameters to be passed to extension point handlers
+ (typically and object that invokes `gui_extension_point`; however,
+ this can be practically anything)
+ :type args: tuple
+
+ '''
+ if ((gui_extpoint_name not in self.gui_extension_points)
+ or (args not in self.gui_extension_points[gui_extpoint_name])):
+ self.gui_extension_points.setdefault(gui_extpoint_name, []).append(args)
+
+ @log_calls('PluginManager')
+ def _execute_all_handlers_of_gui_extension_point(self, gui_extpoint_name, *args):
+ 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)
+
+ def _register_network_events_in_nec(self, plugin):
+ for event_class in plugin.events:
+ setattr(event_class, 'plugin', plugin)
+ if issubclass(event_class, nec.NetworkIncomingEvent):
+ gajim.nec.register_incoming_event(event_class)
+ elif issubclass(event_class, nec.NetworkOutgoingEvent):
+ gajim.nec.register_outgoing_event(event_class)
+
+ def _remove_network_events_from_nec(self, plugin):
+ for event_class in plugin.events:
+ if issubclass(event_class, nec.NetworkIncomingEvent):
+ gajim.nec.unregister_incoming_event(event_class)
+ elif issubclass(event_class, nec.NetworkOutgoingEvent):
+ gajim.nec.unregister_outgoing_event(event_class)
+
+ @log_calls('PluginManager')
+ def activate_plugin(self, plugin):
+ '''
+ :param plugin: plugin to be activated
+ :type plugin: class object of `GajimPlugin` subclass
+
+ :todo: success checks should be implemented using exceptions. Such
+ 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)
+ self._register_network_events_in_nec(plugin)
+
+ success = True
+
+ if success:
+ self.active_plugins.append(plugin)
+ plugin.activate()
+ self._set_plugin_active_in_global_config(plugin)
+ plugin.active = True
+
+ return success
+
+ def deactivate_plugin(self, plugin):
+ # remove GUI extension points handlers (provided by plug-in) from
+ # handlers list
+ for gui_extpoint_name, gui_extpoint_handlers in \
+ plugin.gui_extension_points.iteritems():
+ self.gui_extension_points_handlers[gui_extpoint_name].remove(gui_extpoint_handlers)
+
+ # detaching plug-in from handler GUI extension points (calling
+ # cleaning up method that must be provided by plug-in developer
+ # for each handled GUI extension point)
+ for gui_extpoint_name, gui_extpoint_handlers in \
+ plugin.gui_extension_points.iteritems():
+ if gui_extpoint_name in self.gui_extension_points:
+ for gui_extension_point_args in self.gui_extension_points[gui_extpoint_name]:
+ handler = gui_extpoint_handlers[1]
+ if handler:
+ handler(*gui_extension_point_args)
+
+ self._remove_events_handler_from_ged(plugin)
+ self._remove_network_events_from_nec(plugin)
+
+ # removing plug-in from active plug-ins list
+ plugin.deactivate()
+ self.active_plugins.remove(plugin)
+ self._set_plugin_active_in_global_config(plugin, False)
+ plugin.active = False
+
+ def _deactivate_all_plugins(self):
+ for plugin_object in self.active_plugins:
+ self.deactivate_plugin(plugin_object)
+
+ @log_calls('PluginManager')
+ def _add_gui_extension_points_handlers_from_plugin(self, plugin):
+ for gui_extpoint_name, gui_extpoint_handlers in \
+ plugin.gui_extension_points.iteritems():
+ self.gui_extension_points_handlers.setdefault(gui_extpoint_name, []).append(
+ gui_extpoint_handlers)
+
+ @log_calls('PluginManager')
+ def _handle_all_gui_extension_points_with_plugin(self, plugin):
+ for gui_extpoint_name, gui_extpoint_handlers in \
+ plugin.gui_extension_points.iteritems():
+ if gui_extpoint_name in self.gui_extension_points:
+ for gui_extension_point_args in self.gui_extension_points[gui_extpoint_name]:
+ handler = gui_extpoint_handlers[0]
+ if handler:
+ handler(*gui_extension_point_args)
+
+ @log_calls('PluginManager')
+ def _activate_all_plugins(self):
+ '''
+ Activates all plugins in `plugins`.
+
+ Activated plugins are appended to `active_plugins` list.
+ '''
+ #self.active_plugins = []
+ for plugin in self.plugins:
+ self.activate_plugin(plugin)
+
+ def _activate_all_plugins_from_global_config(self):
+ for plugin in self.plugins:
+ if self._plugin_is_active_in_global_config(plugin):
+ self.activate_plugin(plugin)
+
+ def _plugin_is_active_in_global_config(self, plugin):
+ return gajim.config.get_per('plugins', plugin.short_name, 'active')
+
+ def _set_plugin_active_in_global_config(self, plugin, active=True):
+ gajim.config.set_per('plugins', plugin.short_name, 'active', active)
+
+ @staticmethod
+ @log_calls('PluginManager')
+ def scan_dir_for_plugins(path):
+ '''
+ Scans given directory for plugin classes.
+
+ :param path: directory to scan for plugins
+ :type path: unicode
+
+ :return: list of found plugin classes (subclasses of `GajimPlugin`
+ :rtype: [] of class objects
+
+ :note: currently it only searches for plugin classes in '\*.py' files
+ present in given direcotory `path` (no recursion here)
+
+ :todo: add scanning packages
+ :todo: add scanning zipped modules
+ '''
+ plugins_found = []
+ if os.path.isdir(path):
+ dir_list = os.listdir(path)
+ #log.debug(dir_list)
+
+ sys.path.insert(0, path)
+ #log.debug(sys.path)
+
+ for elem_name in dir_list:
+ #log.debug('- "%s"'%(elem_name))
+ file_path = os.path.join(path, elem_name)
+ #log.debug(' "%s"'%(file_path))
+
+ module = None
+
+ if os.path.isfile(file_path) and fnmatch.fnmatch(file_path, '*.py'):
+ module_name = os.path.splitext(elem_name)[0]
+ #log.debug('Possible module detected.')
+ try:
+ module = __import__(module_name)
+ #log.debug('Module imported.')
+ except ValueError, value_error:
+ pass
+ #log.debug('Module not imported successfully. ValueError: %s'%(value_error))
+ except ImportError, import_error:
+ pass
+ #log.debug('Module not imported successfully. ImportError: %s'%(import_error))
+
+ elif os.path.isdir(file_path):
+ module_name = elem_name
+ file_path += os.path.sep
+ #log.debug('Possible package detected.')
+ try:
+ module = __import__(module_name)
+ #log.debug('Package imported.')
+ except ValueError, value_error:
+ pass
+ #log.debug('Package not imported successfully. ValueError: %s'%(value_error))
+ except ImportError, import_error:
+ pass
+ #log.debug('Package not imported successfully. ImportError: %s'%(import_error))
+
+
+ if module:
+ 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))
+
+ try:
+ if issubclass(module_attr, GajimPlugin) and \
+ not module_attr is 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))
+ plugins_found.append(module_attr)
+ except TypeError, type_error:
+ pass
+ #log.debug('module_attr: %s, error : %s'%(
+ #module_name+'.'+module_attr_name,
+ #type_error))
+
+ #log.debug(module)
+
+ return plugins_found
diff --git a/src/pycallgraph.py b/src/pycallgraph.py
new file mode 100644
index 000000000..3a5fd7ecd
--- /dev/null
+++ b/src/pycallgraph.py
@@ -0,0 +1,410 @@
+"""
+pycallgraph
+
+U{http://pycallgraph.slowchop.com/}
+
+Copyright Gerald Kaszuba 2007
+
+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; either version 2 of the License, or
+(at your option) any later version.
+
+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.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+"""
+
+__version__ = '0.4.1'
+__author__ = 'Gerald Kaszuba'
+
+import inspect
+import sys
+import os
+import re
+import tempfile
+import time
+from distutils import sysconfig
+
+# Initialise module variables.
+# TODO Move these into settings
+trace_filter = None
+time_filter = None
+
+
+def colourize_node(calls, total_time):
+ value = float(total_time * 2 + calls) / 3
+ return '%f %f %f' % (value / 2 + .5, value, 0.9)
+
+
+def colourize_edge(calls, total_time):
+ value = float(total_time * 2 + calls) / 3
+ return '%f %f %f' % (value / 2 + .5, value, 0.7)
+
+
+def reset_settings():
+ global settings
+ global graph_attributes
+ global __version__
+
+ settings = {
+ 'node_attributes': {
+ 'label': r'%(func)s\ncalls: %(hits)i\ntotal time: %(total_time)f',
+ 'color': '%(col)s',
+ },
+ 'node_colour': colourize_node,
+ 'edge_colour': colourize_edge,
+ 'dont_exclude_anything': False,
+ 'include_stdlib': True,
+ }
+
+ # TODO: Move this into settings
+ graph_attributes = {
+ 'graph': {
+ 'fontname': 'Verdana',
+ 'fontsize': 7,
+ 'fontcolor': '0 0 0.5',
+ 'label': r'Generated by Python Call Graph v%s\n' \
+ r'http://pycallgraph.slowchop.com' % __version__,
+ },
+ 'node': {
+ 'fontname': 'Verdana',
+ 'fontsize': 7,
+ 'color': '.5 0 .9',
+ 'style': 'filled',
+ 'shape': 'rect',
+ },
+ }
+
+
+def reset_trace():
+ """Resets all collected statistics. This is run automatically by
+ start_trace(reset=True) and when the module is loaded.
+ """
+ global call_dict
+ global call_stack
+ global func_count
+ global func_count_max
+ global func_time
+ global func_time_max
+ global call_stack_timer
+
+ call_dict = {}
+
+ # current call stack
+ call_stack = ['__main__']
+
+ # counters for each function
+ func_count = {}
+ func_count_max = 0
+
+ # accumative time per function
+ func_time = {}
+ func_time_max = 0
+
+ # keeps track of the start time of each call on the stack
+ call_stack_timer = []
+
+
+class PyCallGraphException(Exception):
+ """Exception used for pycallgraph"""
+ pass
+
+
+class GlobbingFilter(object):
+ """Filter module names using a set of globs.
+
+ Objects are matched against the exclude list first, then the include list.
+ Anything that passes through without matching either, is excluded.
+ """
+
+ def __init__(self, include=None, exclude=None, max_depth=None,
+ min_depth=None):
+ if include is None and exclude is None:
+ include = ['*']
+ exclude = []
+ elif include is None:
+ include = ['*']
+ elif exclude is None:
+ exclude = []
+ self.include = include
+ self.exclude = exclude
+ self.max_depth = max_depth or 9999
+ self.min_depth = min_depth or 0
+
+ def __call__(self, stack, module_name=None, class_name=None,
+ func_name=None, full_name=None):
+ from fnmatch import fnmatch
+ if len(stack) > self.max_depth:
+ return False
+ if len(stack) < self.min_depth:
+ return False
+ for pattern in self.exclude:
+ if fnmatch(full_name, pattern):
+ return False
+ for pattern in self.include:
+ if fnmatch(full_name, pattern):
+ return True
+ return False
+
+
+def is_module_stdlib(file_name):
+ """Returns True if the file_name is in the lib directory."""
+ # TODO: Move these calls away from this function so it doesn't have to run
+ # every time.
+ lib_path = sysconfig.get_python_lib()
+ path = os.path.split(lib_path)
+ if path[1] == 'site-packages':
+ lib_path = path[0]
+ return file_name.lower().startswith(lib_path.lower())
+
+
+def start_trace(reset=True, filter_func=None, time_filter_func=None):
+ """Begins a trace. Setting reset to True will reset all previously recorded
+ trace data. filter_func needs to point to a callable function that accepts
+ the parameters (call_stack, module_name, class_name, func_name, full_name).
+ Every call will be passed into this function and it is up to the function
+ to decide if it should be included or not. Returning False means the call
+ will be filtered out and not included in the call graph.
+ """
+ global trace_filter
+ global time_filter
+ if reset:
+ reset_trace()
+
+ if filter_func:
+ trace_filter = filter_func
+ else:
+ trace_filter = GlobbingFilter(exclude=['pycallgraph.*'])
+
+ if time_filter_func:
+ time_filter = time_filter_func
+ else:
+ time_filter = GlobbingFilter()
+
+ sys.settrace(tracer)
+
+
+def stop_trace():
+ """Stops the currently running trace, if any."""
+ sys.settrace(None)
+
+
+def tracer(frame, event, arg):
+ """This is an internal function that is called every time a call is made
+ during a trace. It keeps track of relationships between calls.
+ """
+ global func_count_max
+ global func_count
+ global trace_filter
+ global time_filter
+ global call_stack
+ global func_time
+ global func_time_max
+
+ if event == 'call':
+ keep = True
+ code = frame.f_code
+
+ # Stores all the parts of a human readable name of the current call.
+ full_name_list = []
+
+ # Work out the module name
+ module = inspect.getmodule(code)
+ if module:
+ module_name = module.__name__
+ module_path = module.__file__
+ if not settings['include_stdlib'] \
+ and is_module_stdlib(module_path):
+ keep = False
+ if module_name == '__main__':
+ module_name = ''
+ else:
+ module_name = ''
+ if module_name:
+ full_name_list.append(module_name)
+
+ # Work out the class name.
+ try:
+ class_name = frame.f_locals['self'].__class__.__name__
+ full_name_list.append(class_name)
+ except (KeyError, AttributeError):
+ class_name = ''
+
+ # Work out the current function or method
+ func_name = code.co_name
+ if func_name == '?':
+ func_name = '__main__'
+ full_name_list.append(func_name)
+
+ # Create a readable representation of the current call
+ full_name = '.'.join(full_name_list)
+
+ # Load the trace filter, if any. 'keep' determines if we should ignore
+ # this call
+ if keep and trace_filter:
+ keep = trace_filter(call_stack, module_name, class_name,
+ func_name, full_name)
+
+ # Store the call information
+ if keep:
+
+ fr = call_stack[-1]
+ if fr not in call_dict:
+ call_dict[fr] = {}
+ if full_name not in call_dict[fr]:
+ call_dict[fr][full_name] = 0
+ call_dict[fr][full_name] += 1
+
+ if full_name not in func_count:
+ func_count[full_name] = 0
+ func_count[full_name] += 1
+ if func_count[full_name] > func_count_max:
+ func_count_max = func_count[full_name]
+
+ call_stack.append(full_name)
+ call_stack_timer.append(time.time())
+
+ else:
+ call_stack.append('')
+ call_stack_timer.append(None)
+
+ if event == 'return':
+ if call_stack:
+ full_name = call_stack.pop(-1)
+ t = call_stack_timer.pop(-1)
+ if t and time_filter(stack=call_stack, full_name=full_name):
+ if full_name not in func_time:
+ func_time[full_name] = 0
+ call_time = (time.time() - t)
+ func_time[full_name] += call_time
+ if func_time[full_name] > func_time_max:
+ func_time_max = func_time[full_name]
+
+ return tracer
+
+
+def get_dot(stop=True):
+ """Returns a string containing a DOT file. Setting stop to True will cause
+ the trace to stop.
+ """
+ global func_time_max
+
+ def frac_calculation(func, count):
+ global func_count_max
+ global func_time
+ global func_time_max
+ calls_frac = float(count) / func_count_max
+ try:
+ total_time = func_time[func]
+ except KeyError:
+ total_time = 0
+ if func_time_max:
+ total_time_frac = float(total_time) / func_time_max
+ else:
+ total_time_frac = 0
+ return calls_frac, total_time_frac, total_time
+
+ if stop:
+ stop_trace()
+ ret = ['digraph G {', ]
+ for comp, comp_attr in graph_attributes.items():
+ ret.append('%s [' % comp)
+ for attr, val in comp_attr.items():
+ ret.append('%(attr)s = "%(val)s",' % locals())
+ ret.append('];')
+ for func, hits in func_count.items():
+ calls_frac, total_time_frac, total_time = frac_calculation(func, hits)
+ col = settings['node_colour'](calls_frac, total_time_frac)
+ attribs = ['%s="%s"' % a for a in settings['node_attributes'].items()]
+ node_str = '"%s" [%s];' % (func, ','.join(attribs))
+ ret.append(node_str % locals())
+ for fr_key, fr_val in call_dict.items():
+ if fr_key == '':
+ continue
+ for to_key, to_val in fr_val.items():
+ calls_frac, total_time_frac, totla_time = \
+ frac_calculation(to_key, to_val)
+ col = settings['edge_colour'](calls_frac, total_time_frac)
+ edge = '[ color = "%s" ]' % col
+ ret.append('"%s"->"%s" %s' % (fr_key, to_key, edge))
+ ret.append('}')
+ ret = '\n'.join(ret)
+ return ret
+
+
+def save_dot(filename):
+ """Generates a DOT file and writes it into filename."""
+ open(filename, 'w').write(get_dot())
+
+
+def make_graph(filename, format=None, tool=None, stop=None):
+ """This has been changed to make_dot_graph."""
+ raise PyCallGraphException( \
+ 'make_graph is depricated. Please use make_dot_graph')
+
+
+def make_dot_graph(filename, format='png', tool='dot', stop=True):
+ """Creates a graph using a Graphviz tool that supports the dot language. It
+ will output into a file specified by filename with the format specified.
+ Setting stop to True will stop the current trace.
+ """
+ if stop:
+ stop_trace()
+
+ # create a temporary file to be used for the dot data
+ fd, tempname = tempfile.mkstemp()
+ f = os.fdopen(fd, 'w')
+ f.write(get_dot())
+ f.close()
+
+ # normalize filename
+ regex_user_expand = re.compile('\A~')
+ if regex_user_expand.match(filename):
+ filename = os.path.expanduser(filename)
+ else:
+ filename = os.path.expandvars(filename) # expand, just in case
+
+ cmd = '%(tool)s -T%(format)s -o%(filename)s %(tempname)s' % locals()
+ try:
+ ret = os.system(cmd)
+ if ret:
+ raise PyCallGraphException( \
+ 'The command "%(cmd)s" failed with error ' \
+ 'code %(ret)i.' % locals())
+ finally:
+ os.unlink(tempname)
+
+
+def simple_memoize(callable_object):
+ """Simple memoization for functions without keyword arguments.
+
+ This is useful for mapping code objects to module in this context.
+ inspect.getmodule() requires a number of system calls, which may slow down
+ the tracing considerably. Caching the mapping from code objects (there is
+ *one* code object for each function, regardless of how many simultaneous
+ activations records there are).
+
+ In this context we can ignore keyword arguments, but a generic memoizer
+ ought to take care of that as well.
+ """
+
+ cache = dict()
+ def wrapper(*rest):
+ if rest not in cache:
+ cache[rest] = callable_object(*rest)
+ return cache[rest]
+
+ return wrapper
+
+
+settings = {}
+graph_attributes = {}
+reset_settings()
+reset_trace()
+inspect.getmodule = simple_memoize(inspect.getmodule)
diff --git a/src/pylint.rc b/src/pylint.rc
new file mode 100644
index 000000000..7816d3659
--- /dev/null
+++ b/src/pylint.rc
@@ -0,0 +1,310 @@
+# lint Python modules using external checkers.
+#
+# This is the main checker controling the other ones and the reports
+# generation. It is itself both a raw checker and an astng checker in order
+# to:
+# * handle message activation / deactivation at the module level
+# * handle some basic but necessary stats'data (number of classes, methods...)
+#
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Profiled execution.
+profile=no
+
+# Add to the black list. It should be a base name, not a
+# path. You may set this option multiple times.
+ignore=CVS
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Set the cache size for astng objects.
+cache-size=500
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable only checker(s) with the given id(s). This option conflicts with the
+# disable-checker option
+#enable-checker=
+
+# Enable all checker(s) except those with the given id(s). This option
+# conflicts with the enable-checker option
+#disable-checker=
+
+# Enable all messages in the listed categories.
+#enable-msg-cat=
+
+# Disable all messages in the listed categories.
+#disable-msg-cat=
+
+# Enable the message(s) with the given id(s).
+enable-msg=R0801
+
+# Disable the message(s) with the given id(s).
+disable-msg=W0312
+# disable-msg=
+
+
+[REPORTS]
+
+# set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=text
+
+# Include message's id in output
+include-ids=yes
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells wether to display a full report or only the messages
+reports=yes
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note).You have access to the variables errors warning, statement which
+# respectivly contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (R0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (R0004).
+comment=no
+
+# Enable the report(s) with the given id(s).
+#enable-report=
+
+# Disable the report(s) with the given id(s).
+#disable-report=
+
+
+# try to find bugs in the code using type inference
+#
+[TYPECHECK]
+
+# Tells wether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamicaly set).
+ignored-classes=SQLObject
+
+# When zope mode is activated, consider the acquired-members option to ignore
+# access to some undefined attributes.
+zope=no
+
+# List of members which are usually get through zope's acquisition mecanism and
+# so shouldn't trigger E0201 when accessed (need zope=yes to be considered).
+acquired-members=REQUEST,acl_users,aq_parent
+
+
+# checks for
+# * unused variables / imports
+# * undefined variables
+# * redefinition of variable from builtins or from an outer scope
+# * use of variable before assigment
+#
+[VARIABLES]
+
+# Tells wether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching names used for dummy variables (i.e. not used).
+dummy-variables-rgx=_|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+
+# checks for :
+# * doc strings
+# * modules / classes / functions / methods / arguments / variables name
+# * number of arguments, local variables, branchs, returns and statements in
+# functions, methods
+# * required module attributes
+# * dangerous default values as arguments
+# * redefinition of function / method / class
+# * uses of the global statement
+#
+[BASIC]
+
+# Required attributes for module, separated by a comma
+required-attributes=
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+no-docstring-rgx=__.*__
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))$
+
+# Regular expression which should only match correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression which should only match correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter,apply,input
+
+
+# checks for sign of poor/misdesign:
+# * number of methods, attributes, local variables...
+# * size, complexity of functions, methods
+#
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+max-branchs=12
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+
+# checks for
+# * external modules dependencies
+# * relative / wildcard imports
+# * cyclic imports
+# * uses of deprecated modules
+#
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report R0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report R0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report R0402 must
+# not be disabled)
+int-import-graph=
+
+
+# checks for :
+# * methods without self as first argument
+# * overridden methods signature
+# * access only to existant members via self
+# * attributes not defined in the __init__ method
+# * supported interfaces implementation
+# * unreachable code
+#
+[CLASSES]
+
+# List of interface methods to ignore, separated by a comma. This is used for
+# instance to not check methods defines in Zope's Interface base class.
+ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+
+# checks for similarities and duplicated code. This computation may be
+# memory / CPU intensive, so you should disable it if you experiments some
+# problems.
+#
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+
+# checks for:
+# * warning notes in the code like FIXME, XXX
+# * PEP 263: source code with non ascii character but no encoding declaration
+#
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+# checks for :
+# * unauthorized constructions
+# * strict indentation
+# * line length
+# * use of <> instead of !=
+#
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
diff --git a/src/remote_control.py b/src/remote_control.py
index 95411a653..3cb29fa68 100644
--- a/src/remote_control.py
+++ b/src/remote_control.py
@@ -36,6 +36,7 @@ from common import gajim
from common import helpers
from time import time
from dialogs import AddNewContactWindow, NewChatDialog, JoinGroupchatWindow
+from common import ged
from common import dbus_support
if dbus_support.supported:
@@ -103,6 +104,31 @@ class Remote:
bus_name = dbus.service.BusName(SERVICE, bus=session_bus)
self.signal_object = SignalObject(bus_name)
+
+ gajim.ged.register_event_handler('last-result-received', ged.POSTGUI,
+ self.on_last_status_time)
+ gajim.ged.register_event_handler('version-result-received', ged.POSTGUI,
+ self.on_os_info)
+ gajim.ged.register_event_handler('time-result-received', ged.POSTGUI,
+ self.on_time)
+ gajim.ged.register_event_handler('gmail-nofify', ged.POSTGUI,
+ self.on_gmail_notify)
+
+ def on_last_status_time(self, obj):
+ self.raise_signal('LastStatusTime', (obj.conn.name, [
+ obj.jid, obj.resource, obj.seconds, obj.status]))
+
+ def on_os_info(self, obj):
+ self.raise_signal('OsInfo', (obj.conn.name, [obj.jid, obj.resource,
+ obj.client_info, obj.os_info]))
+
+ def on_time(self, obj):
+ self.raise_signal('EntityTime', (obj.conn.name, [obj.jid, obj.resource,
+ obj.time_info]))
+
+ def on_gmail_notify(self, obj):
+ self.raise_signal('NewGmail', (obj.conn.name, [obj.jid, obj.newmsgs,
+ obj.gmail_messages_list]))
def raise_signal(self, signal, arg):
if self.signal_object:
diff --git a/src/roster_window.py b/src/roster_window.py
index 5c7725a67..b4848ec70 100644
--- a/src/roster_window.py
+++ b/src/roster_window.py
@@ -53,6 +53,8 @@ import tooltips
import message_control
import adhoc_commands
import features_window
+import plugins
+import plugins.gui
from common import gajim
from common import helpers
@@ -3509,6 +3511,12 @@ class RosterWindow:
gajim.interface.instances['preferences'] = config.PreferencesWindow(
)
+ def on_plugins_menuitem_activate(self, widget):
+ if gajim.interface.instances.has_key('plugins'):
+ gajim.interface.instances['plugins'].window.present()
+ else:
+ gajim.interface.instances['plugins'] = plugins.gui.PluginsWindow()
+
def on_publish_tune_toggled(self, widget, account):
active = widget.get_active()
gajim.config.set_per('accounts', account, 'publish_tune', active)
diff --git a/src/session.py b/src/session.py
index 2d2b017af..2396f9883 100644
--- a/src/session.py
+++ b/src/session.py
@@ -272,6 +272,11 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession):
[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]))
+
def roster_message(self, jid, msg, tim, encrypted=False, msg_type='',
subject=None, resource='', msg_id=None, user_nick='',
advanced_notif_num=None, xhtml=None, form_node=None, displaymarking=None):
diff --git a/src/vcard.py b/src/vcard.py
index fa2f96936..d33770c3e 100644
--- a/src/vcard.py
+++ b/src/vcard.py
@@ -42,6 +42,7 @@ import gtkgui_helpers
from common import helpers
from common import gajim
+from common import ged
from common.i18n import Q_
def get_avatar_pixbuf_encoded_mime(photo):
@@ -125,6 +126,13 @@ class VcardWindow:
self.update_progressbar_timeout_id = gobject.timeout_add(100,
self.update_progressbar)
+ gajim.ged.register_event_handler('version-result-received', ged.GUI1,
+ self.set_os_info)
+ gajim.ged.register_event_handler('last-result-received', ged.GUI2,
+ self.set_last_status_time)
+ gajim.ged.register_event_handler('time-result-received', ged.GUI1,
+ self.set_entity_time)
+
self.fill_jabber_page()
annotations = gajim.connections[self.account].annotations
if self.contact.jid in annotations:
@@ -150,6 +158,12 @@ class VcardWindow:
if annotation != connection.annotations.get(self.contact.jid, ''):
connection.annotations[self.contact.jid] = annotation
connection.store_annotations()
+ gajim.ged.remove_event_handler('version-result-received', ged.GUI1,
+ self.set_os_info)
+ gajim.ged.remove_event_handler('last-result-received', ged.GUI2,
+ self.set_last_status_time)
+ gajim.ged.remove_event_handler('time-result-received', ged.GUI1,
+ self.set_entity_time)
def on_vcard_information_window_key_press_event(self, widget, event):
if event.keyval == gtk.keysyms.Escape:
@@ -226,10 +240,10 @@ class VcardWindow:
self.progressbar.hide()
self.update_progressbar_timeout_id = None
- def set_last_status_time(self):
+ def set_last_status_time(self, obj):
self.fill_status_label()
- def set_os_info(self, resource, client_info, os_info):
+ def set_os_info(self, obj):
if self.xml.get_object('information_notebook').get_n_pages() < 5:
return
i = 0
@@ -237,9 +251,9 @@ class VcardWindow:
os = ''
while i in self.os_info:
if not self.os_info[i]['resource'] or \
- self.os_info[i]['resource'] == resource:
- self.os_info[i]['client'] = client_info
- self.os_info[i]['os'] = os_info
+ self.os_info[i]['resource'] == obj.resource:
+ self.os_info[i]['client'] = obj.client_info
+ self.os_info[i]['os'] = obj.os_info
if i > 0:
client += '\n'
os += '\n'
@@ -256,15 +270,15 @@ class VcardWindow:
self.os_info_arrived = True
self.test_remove_progressbar()
- def set_entity_time(self, resource, time_info):
+ def set_entity_time(self, obj):
if self.xml.get_object('information_notebook').get_n_pages() < 5:
return
i = 0
time_s = ''
while i in self.time_info:
if not self.time_info[i]['resource'] or \
- self.time_info[i]['resource'] == resource:
- self.time_info[i]['time'] = time_info
+ self.time_info[i]['resource'] == obj.resource:
+ self.time_info[i]['time'] = obj.time_info
if i > 0:
time_s += '\n'
time_s += self.time_info[i]['time']
diff --git a/test/test_pluginmanager.py b/test/test_pluginmanager.py
new file mode 100644
index 000000000..c6dafac13
--- /dev/null
+++ b/test/test_pluginmanager.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+# -*- 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 .
+##
+
+'''
+Testing PluginManager class.
+
+:author: Mateusz Biliński
+:since: 05/30/2008
+:copyright: Copyright (2008) Mateusz Biliński
+:license: GPL
+'''
+
+import sys
+import os
+import unittest
+
+gajim_root = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..')
+sys.path.append(gajim_root + '/src')
+
+# a temporary version of ~/.gajim for testing
+configdir = gajim_root + '/test/tmp'
+
+import time
+
+# define _ for i18n
+import __builtin__
+__builtin__._ = lambda x: x
+
+# wipe config directory
+import os
+if os.path.isdir(configdir):
+ import shutil
+ shutil.rmtree(configdir)
+
+os.mkdir(configdir)
+
+import common.configpaths
+common.configpaths.gajimpaths.init(configdir)
+common.configpaths.gajimpaths.init_profile()
+
+# for some reason common.gajim needs to be imported before xmpppy?
+from common import gajim
+from common import xmpp
+
+gajim.DATA_DIR = gajim_root + '/data'
+
+from common.stanza_session import StanzaSession
+
+# name to use for the test account
+account_name = 'test'
+
+from plugins import PluginManager
+
+class PluginManagerTestCase(unittest.TestCase):
+ def setUp(self):
+ self.pluginmanager = PluginManager()
+
+ def tearDown(self):
+ pass
+
+ def test_01_Singleton(self):
+ """ 1. Checking whether PluginManger class is singleton. """
+ self.pluginmanager.test_arg = 1
+ secondPluginManager = PluginManager()
+
+ self.failUnlessEqual(id(secondPluginManager), id(self.pluginmanager),
+ 'Different IDs in references to PluginManager objects (not a singleton)')
+ self.failUnlessEqual(secondPluginManager.test_arg, 1,
+ 'References point to different PluginManager objects (not a singleton')
+
+def suite():
+ suite = unittest.TestLoader().loadTestsFromTestCase(PluginManagerTestCase)
+ return suite
+
+if __name__=='__main__':
+ runner = unittest.TextTestRunner()
+ test_suite = suite()
+ runner.run(test_suite)