diff --git a/data/gui/groupchat_control.ui b/data/gui/groupchat_control.ui index 86f9ee82b6b3ccd90fb06c328650ddf26c6729b4..05c089f8569ef05ef277153ab8a1716307bef254 100644 --- a/data/gui/groupchat_control.ui +++ b/data/gui/groupchat_control.ui @@ -19,7 +19,7 @@ <object class="GtkHBox" id="hbox3024"> <property name="visible">True</property> <child> - <object class="GtkImage" id="gc_banner_status_image"> + <object class="GtkImage" id="banner_status_image"> <property name="visible">True</property> <property name="stock">gtk-missing-image</property> </object> diff --git a/data/gui/plugins_window.ui b/data/gui/plugins_window.ui new file mode 100644 index 0000000000000000000000000000000000000000..4560c201308bdafda982c5b926ac6033a9b73eaf --- /dev/null +++ b/data/gui/plugins_window.ui @@ -0,0 +1,644 @@ +<?xml version="1.0"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkWindow" id="plugins_window"> + <property name="width_request">650</property> + <property name="height_request">500</property> + <property name="border_width">6</property> + <property name="title" translatable="yes">Plugins</property> + <property name="default_width">650</property> + <property name="default_height">500</property> + <signal name="destroy" handler="on_plugins_window_destroy"/> + <child> + <object class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkNotebook" id="plugins_notebook"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkHPaned" id="hpaned1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="position">250</property> + <property name="position_set">True</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="border_width">6</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkTreeView" id="installed_plugins_treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + </child> + </object> + <packing> + <property name="resize">False</property> + <property name="shrink">True</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox2"> + <property name="visible">True</property> + <property name="border_width">6</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkHBox" id="hbox4"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="plugin_name_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">&lt;empty&gt;</property> + <property name="use_markup">True</property> + <property name="wrap">True</property> + <property name="selectable">True</property> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="label" translatable="yes">Version:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="plugin_version_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes"><empty></property> + <property name="selectable">True</property> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox2"> + <property name="visible">True</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label4"> + <property name="visible">True</property> + <property name="yalign">0</property> + <property name="label" translatable="yes">Authors:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="plugin_authors_label"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + <property name="label" translatable="yes"><empty></property> + <property name="wrap_mode">word-char</property> + <property name="selectable">True</property> + <property name="ellipsize">end</property> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox3"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="label" translatable="yes">Homepage:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLinkButton" id="plugin_homepage_linkbutton"> + <property name="label" translatable="yes">homepage url</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="relief">none</property> + <property name="focus_on_click">False</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox3"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkHBox" id="hbox5"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="label7"> + <property name="visible">True</property> + <property name="label" translatable="yes">Descrition:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkTextView" id="plugin_description_textview"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="pixels_above_lines">6</property> + <property name="editable">False</property> + <property name="wrap_mode">word</property> + <property name="left_margin">6</property> + <property name="right_margin">6</property> + <property name="indent">1</property> + <property name="buffer">textbuffer1</property> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox2"> + <property name="visible">True</property> + <property name="spacing">5</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="uninstall_plugin_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_uninstall_plugin_button_clicked"/> + <child> + <object class="GtkHBox" id="hbox11"> + <property name="visible">True</property> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="stock">gtk-cancel</property> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="uninstall_plugin_button_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Uninstall</property> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="configure_plugin_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_configure_plugin_button_clicked"/> + <child> + <object class="GtkHBox" id="hbox12"> + <property name="visible">True</property> + <child> + <object class="GtkImage" id="image2"> + <property name="visible">True</property> + <property name="stock">gtk-preferences</property> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="configure_plugin_button_label"> + <property name="visible">True</property> + <property name="label" translatable="yes">Configure</property> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">5</property> + </packing> + </child> + </object> + <packing> + <property name="resize">True</property> + <property name="shrink">True</property> + </packing> + </child> + </object> + </child> + <child type="tab"> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="label" translatable="yes">Installed</property> + </object> + <packing> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <object class="GtkHPaned" id="hpaned2"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkTreeView" id="treeview2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + </child> + </object> + <packing> + <property name="resize">False</property> + <property name="shrink">True</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox4"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkHBox" id="hbox6"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="plugin_name_label1"> + <property name="visible">True</property> + <property name="label" translatable="yes"><empty></property> + <property name="selectable">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox7"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="label" translatable="yes">Version:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="plugin_version_label1"> + <property name="visible">True</property> + <property name="label" translatable="yes"><empty></property> + <property name="selectable">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox8"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="label" translatable="yes">Authors:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="plugin_authors_label1"> + <property name="visible">True</property> + <property name="label" translatable="yes"><empty></property> + <property name="selectable">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox9"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="label9"> + <property name="visible">True</property> + <property name="label" translatable="yes">Homepage:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLinkButton" id="plugin_homepage_linkbutton1"> + <property name="label" translatable="yes">button</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="relief">none</property> + <property name="focus_on_click">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox5"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkHBox" id="hbox10"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="label10"> + <property name="visible">True</property> + <property name="label" translatable="yes">Descrition:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment4"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkTextView" id="plugin_description_textview1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox3"> + <property name="visible">True</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="uninstall_plugin_button1"> + <property name="label" translatable="yes">button</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="configure_plugin_button1"> + <property name="label" translatable="yes">button</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">5</property> + </packing> + </child> + </object> + <packing> + <property name="resize">True</property> + <property name="shrink">True</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">Available</property> + </object> + <packing> + <property name="position">1</property> + <property name="tab_fill">False</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child type="tab"> + <placeholder/> + </child> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox1"> + <property name="visible">True</property> + <property name="spacing">15</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="close_button"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_close_button_clicked"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkTextBuffer" id="textbuffer1"> + <property name="text" translatable="yes">Plug-in decription should be displayed here. This text will be erased during PluginsWindow initialization.</property> + </object> +</interface> diff --git a/data/gui/roster_window.ui b/data/gui/roster_window.ui index d8e1428ec2ba264b5b92a6e6e362e4b1b82cb3fa..b48b2ad20d07f0c044a457eef146721d3b10cc24 100644 --- a/data/gui/roster_window.ui +++ b/data/gui/roster_window.ui @@ -54,8 +54,8 @@ <child> <object class="GtkImageMenuItem" id="join_gc_menuitem"> <property name="label" translatable="yes">Join _Group Chat...</property> - <property name="use_underline">True</property> <property name="visible">True</property> + <property name="use_underline">True</property> <property name="image">image3</property> <property name="use_stock">False</property> <property name="accel_group">accelgroup1</property> @@ -69,8 +69,8 @@ <child> <object class="GtkImageMenuItem" id="add_new_contact_menuitem"> <property name="label" translatable="yes">Add _Contact...</property> - <property name="use_underline">True</property> <property name="visible">True</property> + <property name="use_underline">True</property> <property name="image">image4</property> <property name="use_stock">False</property> <property name="accel_group">accelgroup1</property> @@ -159,6 +159,16 @@ <signal name="activate" handler="on_preferences_menuitem_activate"/> </object> </child> + <child> + <object class="GtkImageMenuItem" id="plugins_menuitem"> + <property name="label" translatable="yes">P_lugins</property> + <property name="visible">True</property> + <property name="use_underline">True</property> + <property name="image">image13</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_plugins_menuitem_activate"/> + </object> + </child> </object> </child> </object> @@ -437,4 +447,9 @@ <property name="stock">gtk-properties</property> <property name="icon-size">1</property> </object> + <object class="GtkImage" id="image13"> + <property name="visible">True</property> + <property name="stock">gtk-disconnect</property> + <property name="icon-size">1</property> + </object> </interface> diff --git a/plugins/acronyms_expander.py b/plugins/acronyms_expander.py new file mode 100644 index 0000000000000000000000000000000000000000..12ad677b0a47b588967d703bd386706af4789c3c --- /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 <http://www.gnu.org/licenses/>. +## + +''' +Acronyms expander plugin. + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 9th June 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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 <mateusz@bilinski.it>'] + 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 0000000000000000000000000000000000000000..a328f68ee0ab591b645c7c2c3c600930175c29da --- /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 0000000000000000000000000000000000000000..1994c1c9d0164814f1ee3af611a3149c847279ae --- /dev/null +++ b/plugins/banner_tweaks/config_dialog.ui @@ -0,0 +1,75 @@ +<?xml version="1.0"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkWindow" id="window1"> + <child> + <object class="GtkVBox" id="banner_tweaks_config_vbox"> + <property name="visible">True</property> + <property name="border_width">9</property> + <property name="orientation">vertical</property> + <property name="spacing">4</property> + <child> + <object class="GtkCheckButton" id="show_banner_image_checkbutton"> + <property name="label" translatable="yes">Display status icon</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">If checked, status icon will be displayed in chat window banner.</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_show_banner_image_checkbutton_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="show_banner_online_msg_checkbutton"> + <property name="label" translatable="yes">Display status message of contact</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">If checked, status message of contact will be displayed in chat window banner.</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_show_banner_online_msg_checkbutton_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="show_banner_resource_checkbutton"> + <property name="label" translatable="yes">Display resource name of contact</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">If checked, resource name of contact will be displayed in chat window banner.</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_show_banner_resource_checkbutton_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="banner_small_fonts_checkbutton"> + <property name="label" translatable="yes">Use small fonts for contact name and resource name</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">If checked, smaller font will be used to display resource name and contact name in chat window banner.</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_banner_small_fonts_checkbutton_toggled"/> + </object> + <packing> + <property name="expand">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/plugins/banner_tweaks/plugin.py b/plugins/banner_tweaks/plugin.py new file mode 100644 index 0000000000000000000000000000000000000000..121606aa492fc513ccb6e2035c31376d84a4fced --- /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 <http://www.gnu.org/licenses/>. +## + +''' +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 <mateusz@bilinski.it> +:since: 30 July 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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 <mateusz@bilinski.it>'] + 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 = '<span %s>%s</span><span %s>%s %s</span>' % \ + (font_attrs, name, font_attrs_small, acct_info, chatstate) + else: + # weight="heavy" size="x-large" + label_text = '<span %s>%s</span><span %s>%s</span>' % \ + (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 0000000000000000000000000000000000000000..3851c6bb99141b3499ce9eee9b2b4bc4f16ac13e --- /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 0000000000000000000000000000000000000000..c34e0ad0cf69d755b8d5070fafcce3ea7e29a3eb --- /dev/null +++ b/plugins/dbus_plugin/plugin.py @@ -0,0 +1,738 @@ +# -*- coding: utf-8 -*- + +## Copyright (C) 2005-2006 Yann Leboulanger <asterix@lagaule.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> +## Copyright (C) 2005-2006 Dimitur Kirov <dkirov@gmail.com> +## Copyright (C) 2005-2006 Andrew Sayman <lorien420@myrealbox.com> +## Copyright (C) 2007 Lukas Petrovicky <lukas@petrovicky.net> +## Copyright (C) 2007 Julien Pivotto <roidelapluie@gmail.com> +## Copyright (C) 2007 Travis Shirk <travis@pobox.com> +## Copyright (C) 2008 Mateusz Biliński <mateusz@bilinski.it> +## +## 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 <http://www.gnu.org/licenses/>. +## +''' +D-BUS Support plugin. + +Based on src/remote_control.py + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 8th August 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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 <mateusz@bilinski.it>'] + 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 0000000000000000000000000000000000000000..de174c1b9c00410b5e98145471dfe17977de31c3 --- /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 0000000000000000000000000000000000000000..3e816ae3ac83477fa833f87c76dc61e01a25d9b8 --- /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 <http://www.gnu.org/licenses/>. +## +''' +Events Dump plugin. + +Dumps info about selected events to console. + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 10th August 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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 <mateusz@bilinski.it>'] + 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 0000000000000000000000000000000000000000..dc2c3bc369bad7a218fc4674d4ba8ae0765889ff --- /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 0000000000000000000000000000000000000000..a20664060d2a7c93c354fe81b9cae5ca30dfac25 --- /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 <http://www.gnu.org/licenses/>. +## +''' +Google Translation plugin. + +Translates (currently only incoming) messages using Google Translate. + +:note: consider this as proof-of-concept +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 25th August 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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 <mateusz@bilinski.it>'] + 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<text>[^"]*)"}, 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 0000000000000000000000000000000000000000..67c8c614aa62e20a5bc25a72e1eb0af1cba3d76e --- /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 0000000000000000000000000000000000000000..f06bfe1155cce882a64968f8c735348dc647f47b --- /dev/null +++ b/plugins/length_notifier/config_dialog.ui @@ -0,0 +1,152 @@ +<?xml version="1.0"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkWindow" id="window1"> + <child> + <object class="GtkTable" id="length_notifier_config_table"> + <property name="visible">True</property> + <property name="border_width">9</property> + <property name="n_rows">3</property> + <property name="n_columns">2</property> + <property name="column_spacing">7</property> + <property name="row_spacing">5</property> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="tooltip_text" translatable="yes">Message length at which notification is invoked.</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Message length:</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="tooltip_text" translatable="yes">Background color of text entry field in chat window when notification is invoked.</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Notification color:</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="tooltip_text" translatable="yes">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. </property> + <property name="xalign">0</property> + <property name="label" translatable="yes">JabberIDs to include:</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="jids_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">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. </property> + <signal name="editing_done" handler="on_jids_entry_editing_done"/> + <signal name="changed" handler="on_jids_entry_changed"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <child> + <object class="GtkColorButton" id="notification_colorbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Background color of text entry field in chat window when notification is invoked.</property> + <property name="xalign">0</property> + <property name="title" translatable="yes">Pick a color for notification</property> + <property name="color">#000000000000</property> + <signal name="color_set" handler="on_notification_colorbutton_color_set"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox2"> + <property name="visible">True</property> + <child> + <object class="GtkSpinButton" id="message_length_spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="tooltip_text" translatable="yes">Message length at which notification is invoked.</property> + <property name="width_chars">6</property> + <property name="snap_to_ticks">True</property> + <property name="numeric">True</property> + <signal name="value_changed" handler="on_message_length_spinbutton_value_changed"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment2"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/plugins/length_notifier/length_notifier.py b/plugins/length_notifier/length_notifier.py new file mode 100644 index 0000000000000000000000000000000000000000..cde1322085def2227df1efbe70f710ba7b64410b --- /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 <http://www.gnu.org/licenses/>. +## + +''' +Message length notifier plugin. + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 1st June 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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 <mateusz@bilinski.it>'] + 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 0000000000000000000000000000000000000000..523d43e14c6f4e5c7936867dff691e66254af0a8 --- /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 0000000000000000000000000000000000000000..ff40dd56f3248b7c60606fadf14158148256f404 --- /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 <http://www.gnu.org/licenses/>. +## +''' +New Events Example plugin. + +Demonstrates how to use Network Events Controller to generate new events +based on existing one. + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 15th August 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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 <mateusz@bilinski.it>'] + 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 0000000000000000000000000000000000000000..48754d57c644d5f12af6137fb5dfd17c58a3956a --- /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 0000000000000000000000000000000000000000..a75fd8c6c7f8a8e54bac77fad43e6769b9d3466a --- /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 <http://www.gnu.org/licenses/>. +## + +''' +Roster buttons plug-in. + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 14th June 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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 <mateusz@bilinski.it>'] + 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 0000000000000000000000000000000000000000..b91b0d2a63094cd5b149c89481dbd86bf7e40482 --- /dev/null +++ b/plugins/roster_buttons/roster_buttons.ui @@ -0,0 +1,70 @@ +<?xml version="1.0"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkWindow" id="window1"> + <child> + <object class="GtkHButtonBox" id="roster_buttons_buttonbox"> + <property name="visible">True</property> + <property name="homogeneous">True</property> + <property name="layout_style">spread</property> + <child> + <object class="GtkButton" id="roster_button_1"> + <property name="label" translatable="yes">1</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_roster_button_1_clicked"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="roster_button_2"> + <property name="label" translatable="yes">2</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_roster_button_2_clicked"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="roster_button_3"> + <property name="label" translatable="yes">3</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_roster_button_3_clicked"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="roster_button_4"> + <property name="label" translatable="yes">4</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_roster_button_4_clicked"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/plugins/snarl_notifications/PySnarl.py b/plugins/snarl_notifications/PySnarl.py new file mode 100755 index 0000000000000000000000000000000000000000..c3c657e56f2ff25b13ca4a15ab7804c304065fb8 --- /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 0000000000000000000000000000000000000000..b504dfd50a3b87c8e8e24d1baf7bd702df610599 --- /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 0000000000000000000000000000000000000000..3e21acb778d4a68199ec3b98bcff673770367f95 --- /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 <http://www.gnu.org/licenses/>. +## +''' +Events notifications using Snarl + +Fancy events notifications under Windows using Snarl infrastructure. + +:note: plugin is at proof-of-concept state. + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 15th August 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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 <mateusz@bilinski.it>'] + 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/chat_control.py b/src/chat_control.py index 6a89dfcd6bb51670b468d7f6cc2347e04525792c..8221b898d1cc601e8c349df9256959dd4bfc0f97 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) + def set_speller(self): # now set the one the user selected per_type = 'contacts' @@ -444,6 +450,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 @@ -1585,6 +1597,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 @@ -2455,7 +2471,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 33368ebe1823178802c6992b8667594a38164086..00a50e6fe8f973e7be8644ead17b08a712bb590b 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 7f82a72589b39919360485e80f64f5824e467d17..3473d6f7e1bb43aacc3db220a834bba8e95e4e51 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 465e766768ddce4e54b8bd7511315486217b6d1e..74027994d46c1f1f3bbe0bfed7aacc3132b4ac88 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 96855013188bb5aa1ac1c0060b7c73dcc7525ceb..f60aadc16d3b2ee225d8d062623f4d1eba0ac98d 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)) @@ -1210,25 +1203,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 +1236,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,54 +1263,15 @@ 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): """ @@ -1427,6 +1322,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 +1681,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 +2350,213 @@ 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: + who = self.conn.groupchat_jids[self.id_] + del self.conn.groupchat_jids[self.id_] + else: + who = helpers.get_full_jid_from_iq(self.iq_obj) + self.jid, self.resource = gajim.get_room_and_nick_from_fjid(who) + + 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 = '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.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 \ No newline at end of file diff --git a/src/common/gajim.py b/src/common/gajim.py index a44029de5016bc66fe60c73eb00f27e7eabf2232..cfd4419afce99309340207d0a8cd7d6a7c790704 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 92c54a2684d8f7d9c0002f55ed73a6342746e467..485176fa24081fe7b81244f0dcd3d191096b144f 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 0000000000000000000000000000000000000000..ec41b05e3699c0adff4894e2f6f64268f69a0a1a --- /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 <http://www.gnu.org/licenses/>. +## + +''' +Network Events Controller. + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 10th August 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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 '<NetworkEvent object> Attributes: %s'%(pformat(self.__dict__)) + + def __repr__(self): + return '<NetworkEvent object> 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/gajim-remote-plugin.py b/src/gajim-remote-plugin.py new file mode 100755 index 0000000000000000000000000000000000000000..e9acaf8f12eac425598f924c89da8ac1cfbf7436 --- /dev/null +++ b/src/gajim-remote-plugin.py @@ -0,0 +1,548 @@ +#!/usr/bin/env python +## +## Copyright (C) 2005-2006 Yann Leboulanger <asterix@lagaule.org> +## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com> +## Copyright (C) 2005 Dimitur Kirov <dkirov@gmail.com> +## +## 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 <http://www.gnu.org/licenses/>. +## + +# 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 0a0468f4463c2bdf12051b5a6267e99b419deafc..ea4a064b20b765a9177b69cae7fddf56a4b9db73 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 75a85c5414bc73681486a300f50bd08a08cc7ce8..9296667f3f1ecd84a8282f0008f378d4ff86896a 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 1fb6c304315a762f59b8f3bae03a954198fc50f3..92d5c081549a865871115e7ca397c4d7a3b029ee 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) - - 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) - - 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)) + 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 + + 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, 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)) @@ -2122,9 +2087,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 +2102,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], @@ -2186,7 +2146,10 @@ 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], } def register_core_handlers(self): @@ -2197,7 +2160,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 +3209,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 +3243,6 @@ class Interface: pass gobject.timeout_add_seconds(5, remote_init) - def __init__(self): gajim.interface = self gajim.thread_interface = ThreadInterface @@ -3398,6 +3364,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 d09cb446be1efa1328d01f1505244ef69153bb92..700ac94856fee40a8d1d262133899ec1e3f5451d 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/plugins/__init__.py b/src/plugins/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0d5d18fda2885d02b94e8a64b898491f791f3320 --- /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 <http://www.gnu.org/licenses/>. +## + +''' +Main file of plugins package. + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 05/30/2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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 0000000000000000000000000000000000000000..ab0281e1f6aa4e6768d686a34522a9b96f14b3e5 --- /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 <http://www.gnu.org/licenses/>. +## + +''' +GUI classes related to plug-in management. + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 6th June 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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 0000000000000000000000000000000000000000..6b62fa3ec9dc4eb7b48c490d0ce7a1b503cbcdd0 --- /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 <http://www.gnu.org/licenses/>. +## + +''' +Helper code related to plug-ins management system. + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 30th May 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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() <entered>'%{ + 'funcname': self.full_func_name}) + result = f(*args, **kwargs) + log.debug('%(funcname)s() <left>'%{ + '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 0000000000000000000000000000000000000000..19ca1ce5e5ef5c0428107ca9acd1c38eda41f319 --- /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 <http://www.gnu.org/licenses/>. +## + +''' +Base class for implementing plugin. + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 1st June 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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 0000000000000000000000000000000000000000..a0708c89cbf8b592c832e8b6b20080d30341767d --- /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 <http://www.gnu.org/licenses/>. +## + +''' +Plug-in management related classes. + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 30th May 2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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 0000000000000000000000000000000000000000..3a5fd7ecda47ad92df8419bdb571fd752452b9d5 --- /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 0000000000000000000000000000000000000000..7816d365973391af91e9328a054280db871e66e8 --- /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 <file or directory> 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 95411a653493038f20db41ff95e88538f3206874..3cb29fa689d34012e6c573e61dd2e6af6192ce06 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 4fd62bc809670fe46c2bd280cd99ed330399559b..95452ca0784ab4ef30b47ad86590c05faf384882 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 851ef3a7eb840f2425c0d450d56413597ceb3042..9d0834ce6ecc3c8e51285e797ccf502fb7fa0976 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 fa2f96936ded3db26e0a7ad61ca1b6c975b7365b..d33770c3e67d5d8b2a5531e7cec35360c9a4c5de 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 0000000000000000000000000000000000000000..c6dafac13540161a1230ad94cbdfe818023b364a --- /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 <http://www.gnu.org/licenses/>. +## + +''' +Testing PluginManager class. + +:author: Mateusz Biliński <mateusz@bilinski.it> +:since: 05/30/2008 +:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> +: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)