diff --git a/epydoc.conf b/epydoc.conf new file mode 100644 index 000000000..092f7f1a5 --- /dev/null +++ b/epydoc.conf @@ -0,0 +1,28 @@ +[epydoc] + +# Information about the project. +name: Gajim +url: http://gajim.org + +verbosity: 3 +imports: yes +redundant-details: yes +docformat: restructuredtext +# top: gajim + +# The list of modules to document. Modules can be named using +# dotted names, module filenames, or package directory names. +# This option may be repeated. +modules: src/plugins/*.py + +# Write html output to the directory "apidocs" +#output: pdf +output: html +target: apidocs/ + +# Include all automatically generated graphs. These graphs are +# generated using Graphviz dot. +graph: all +dotpath: /usr/bin/dot +graph-font: Sans +graph-font-size: 10 diff --git a/plugins/length_notifier.py b/plugins/length_notifier.py new file mode 100644 index 000000000..bd0f9564f --- /dev/null +++ b/plugins/length_notifier.py @@ -0,0 +1,95 @@ +# -*- 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: 06/01/2008 +:copyright: Copyright (2008) Mateusz Biliński +:license: GPL +''' + +import sys + +import gtk + +from plugins import GajimPlugin +from plugins.helpers import log, log_calls + +class LengthNotifierPlugin(GajimPlugin): + name = 'Message Length Notifier' + short_name = 'length_notifier' + version = '0.1' + description = '''Highlights message entry field in chat window when given +length of message is exceeded.''' + authors = ['Mateusz Biliński '] + + @log_calls('LengthNotifierPlugin') + def __init__(self): + super(LengthNotifierPlugin, self).__init__() + + self.__class__.gui_extension_points = { + 'chat_control' : (self.connect_with_chat_control, + self.disconnect_from_chat_control) + } + + self.MESSAGE_WARNING_LENGTH = 140 + self.WARNING_COLOR = gtk.gdk.color_parse('#F0DB3E') + self.JIDS = [] + + @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.MESSAGE_WARNING_LENGTH: + if not d['prev_color']: + d['prev_color'] = tv.style.copy().base[gtk.STATE_NORMAL] + tv.modify_base(gtk.STATE_NORMAL, self.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 + b = tv.get_buffer() + h_id = b.connect('changed', self.textview_length_warning, chat_control) + d['h_id'] = h_id + chat_control.length_notifier_plugin_data = d + + return True + + return False + + @log_calls('LengthNotifierPlugin') + def disconnect_from_chat_control(self, chat_control): + d = chat_control.length_notifier_plugin_data + chat_control.msg_textview.get_buffer().disconnect(d['h_id']) + if d['prev_color']: + tv.modify_base(gtk.STATE_NORMAL, self.PREV_COLOR) + + @log_calls('LengthNotifierPlugin') + def jid_is_ok(self, jid): + return True \ No newline at end of file diff --git a/src/chat_control.py b/src/chat_control.py index cddf9cd35..ecc817bf6 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1139,6 +1139,8 @@ class ChatControl(ChatControlBase): self.update_ui() # restore previous conversation self.restore_conversation() + + gajim.plugin_manager.gui_extension_point('chat_control', self) def on_avatar_eventbox_enter_notify_event(self, widget, event): '''we enter the eventbox area so we under conditions add a timeout diff --git a/src/common/configpaths.py b/src/common/configpaths.py index 516d849ef..e950b3e1a 100644 --- a/src/common/configpaths.py +++ b/src/common/configpaths.py @@ -92,6 +92,10 @@ class ConfigPaths: self.add('DATA', os.path.join(u'..', windowsify(u'data'))) self.add('HOME', fse(os.path.expanduser('~'))) self.add('TMP', fse(tempfile.gettempdir())) + + # dirs for plugins + self.add('PLUGINS_BASE', os.path.join(u'..', windowsify(u'plugins'))) + self.add_from_root('PLUGINS_USER', u'plugins') try: import svn_config diff --git a/src/common/gajim.py b/src/common/gajim.py index eaa84ec4f..4cbdf609b 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -61,6 +61,7 @@ version = config.get('version') connections = {} # 'account name': 'account (connection.Connection) instance' verbose = False ipython_window = None +plugin_manager = None h = logging.StreamHandler() f = logging.Formatter('%(asctime)s %(name)s: %(message)s', '%d %b %Y %H:%M:%S') @@ -85,6 +86,8 @@ MY_CACERTS = gajimpaths['MY_CACERTS'] TMP = gajimpaths['TMP'] DATA_DIR = gajimpaths['DATA'] HOME_DIR = gajimpaths['HOME'] +PLUGINS_DIRS = [gajimpaths['PLUGINS_BASE'], + gajimpaths['PLUGINS_USER']] try: LANG = locale.getdefaultlocale()[0] # en_US, fr_FR, el_GR etc.. diff --git a/src/common/sleepy.py b/src/common/sleepy.py index bcc5f73c3..f56a7e044 100644 --- a/src/common/sleepy.py +++ b/src/common/sleepy.py @@ -35,22 +35,22 @@ STATE_AWAKE = 'awake' SUPPORTED = True try: - if os.name == 'nt': - import ctypes + if os.name == 'nt': + import ctypes - GetTickCount = ctypes.windll.kernel32.GetTickCount - GetLastInputInfo = ctypes.windll.user32.GetLastInputInfo + GetTickCount = ctypes.windll.kernel32.GetTickCount + GetLastInputInfo = ctypes.windll.user32.GetLastInputInfo - class LASTINPUTINFO(ctypes.Structure): - _fields_ = [('cbSize', ctypes.c_uint), ('dwTime', ctypes.c_uint)] + class LASTINPUTINFO(ctypes.Structure): + _fields_ = [('cbSize', ctypes.c_uint), ('dwTime', ctypes.c_uint)] - lastInputInfo = LASTINPUTINFO() - lastInputInfo.cbSize = ctypes.sizeof(lastInputInfo) + lastInputInfo = LASTINPUTINFO() + lastInputInfo.cbSize = ctypes.sizeof(lastInputInfo) - elif sys.platform == 'darwin': - import osx.idle as idle - else: # unix - import idle + elif sys.platform == 'darwin': + import osx.idle as idle + else: # unix + import idle except: gajim.log.debug('Unable to load idle module') SUPPORTED = False diff --git a/src/gajim.py b/src/gajim.py index e34e5f81a..d1d746401 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -3461,6 +3461,10 @@ class Interface: gobject.timeout_add_seconds(2, self.process_connections) gobject.timeout_add_seconds(gajim.config.get( 'check_idle_every_foo_seconds'), self.read_sleepy) + + # Creating plugin manager + import plugins + gajim.plugin_manager = plugins.PluginManager() if __name__ == '__main__': def sigint_cb(num, stack): 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/helpers.py b/src/plugins/helpers.py new file mode 100644 index 000000000..744f209ac --- /dev/null +++ b/src/plugins/helpers.py @@ -0,0 +1,113 @@ +# -*- 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: 05/30/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. + ''' + + 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 + ''' + + if classname: + self.full_func_name = classname+'.' + + 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 + @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 + 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: + log.debug('%(classname)s - returning already existing instance'%{ + 'classname' : cls.__name__}) + + return cls.instance \ No newline at end of file diff --git a/src/plugins/plugin.py b/src/plugins/plugin.py new file mode 100644 index 000000000..d3bfa62c9 --- /dev/null +++ b/src/plugins/plugin.py @@ -0,0 +1,40 @@ +# -*- 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: 06/01/2008 +:copyright: Copyright (2008) Mateusz Biliński +:license: GPL +''' + +import sys +from plugins.helpers import log, log_calls + +class GajimPlugin(object): + name = '' + short_name = '' + version = '' + description = '' + authors = [] + gui_extension_points = {} + + @log_calls('GajimPlugin') + def __init__(self): + pass \ No newline at end of file diff --git a/src/plugins/pluginmanager.py b/src/plugins/pluginmanager.py new file mode 100644 index 000000000..59c3bad11 --- /dev/null +++ b/src/plugins/pluginmanager.py @@ -0,0 +1,130 @@ +# -*- 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: 05/30/2008 +:copyright: Copyright (2008) Mateusz Biliński +:license: GPL +''' + +__all__ = ['PluginManager'] + +import os +import sys +import fnmatch + +import common.gajim as gajim + +from helpers import log, log_calls, Singleton +from plugin import GajimPlugin + +class PluginManager(object): + __metaclass__ = Singleton + + @log_calls('PluginManager') + def __init__(self): + self.plugins = [] + self.active = [] + self.gui_extension_points = {} + + for path in gajim.PLUGINS_DIRS: + self.plugins.extend(self._scan_dir_for_plugins(path)) + + log.debug('plugins: %s'%(self.plugins)) + + self._activate_all_plugins() + + log.debug('active: %s'%(self.active)) + + @log_calls('PluginManager') + def gui_extension_point(self, gui_extpoint_name, *args): + if gui_extpoint_name in self.gui_extension_points: + for handlers in self.gui_extension_points[gui_extpoint_name]: + handlers[0](*args) + + @log_calls('PluginManager') + def _activate_plugin(self, plugin): + ''' + :param plugin: Plugin to be activated. + :type plugin: class object of GajimPlugin subclass + ''' + p = plugin() + + success = True + + # :fix: 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. + for gui_extpoint_name, gui_extpoint_handlers in \ + p.gui_extension_points.iteritems(): + self.gui_extension_points.setdefault(gui_extpoint_name,[]).append( + gui_extpoint_handlers) + + if success: + self.active.append(p) + + return success + + @log_calls('PluginManager') + def _activate_all_plugins(self): + self.active = [] + for plugin in self.plugins: + self._activate_plugin(plugin) + + @log_calls('PluginManager') + def _scan_dir_for_plugins(self, path): + 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 file in fnmatch.filter(dir_list, '*.py'): + log.debug('- "%s"'%(file)) + file_path = os.path.join(path, file) + log.debug(' "%s"'%(file_path)) + if os.path.isfile(file_path): + module_name = os.path.splitext(file)[0] + module = __import__(module_name) + filter_out_bad_names = \ + lambda x: not (x.startswith('__') or + x.endswith('__')) + for module_attr_name in filter(filter_out_bad_names, + dir(module)): + 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') + plugins_found.append(module_attr) + except TypeError, e: + log.debug('module_attr: %s, error : %s'%( + module_name+'.'+module_attr_name, + e)) + + log.debug(module) + + return plugins_found \ No newline at end of file diff --git a/test/test_pluginmanager.py b/test/test_pluginmanager.py new file mode 100644 index 000000000..5c6723bcb --- /dev/null +++ b/test/test_pluginmanager.py @@ -0,0 +1,61 @@ +#!/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') + +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) \ No newline at end of file