diff --git a/configure.ac b/configure.ac index cc04ee34b..a6aadddfc 100644 --- a/configure.ac +++ b/configure.ac @@ -1,5 +1,5 @@ AC_INIT([Gajim - A Jabber Instant Messager], - [0.12.5.2-dev],[http://trac.gajim.org/],[gajim]) + [0.12.5.6-dev],[http://trac.gajim.org/],[gajim]) AC_PREREQ([2.59]) AC_CONFIG_HEADER(config.h) diff --git a/data/glade/accounts_window.glade b/data/glade/accounts_window.glade index 55daa9acc..bb5f3cb9a 100644 --- a/data/glade/accounts_window.glade +++ b/data/glade/accounts_window.glade @@ -1,14 +1,14 @@ - + + + - - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 12 Accounts 800 - + True @@ -30,9 +30,9 @@ True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - automatic - automatic - in + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + GTK_SHADOW_IN True @@ -43,18 +43,16 @@ - - 0 - - gtk-add True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + gtk-add True + 0 @@ -64,12 +62,13 @@ - gtk-remove True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + gtk-remove True + 0 @@ -83,6 +82,7 @@ True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 0 @@ -96,7 +96,6 @@ False - 0 @@ -166,566 +165,535 @@ - + True - True - + True - 6 - 5 - 3 - 6 - 6 + True + _Enable + True + 0 + True + + + + False + False + + + + + True + True - + True - 0 - _Jabber ID: - True - jid_entry1 - - - GTK_FILL - - - - - - True - 0 - _Password: - True - password_entry1 - - - 1 - 2 - GTK_FILL - - - - - - True - False - True - False - True - - - - 1 - 2 - 1 - 2 - GTK_EXPAND | GTK_SHRINK | GTK_FILL - - - - - - Save pass_word - True - True - False - If checked, Gajim will remember the password for this account - True - False - True - - - - 2 - 3 - 1 - 2 - GTK_FILL - - - - - - True - 0 - Resour_ce: - True - resource_entry1 - - - 2 - 3 - GTK_FILL - - - - - - True - 0 - Priori_ty: - True - priority_spinbutton1 - - - 3 - 4 - GTK_FILL - - - - - - A_djust to status - True - True - False - Priority will change automatically according to your status. - True - True - - - - 1 - 2 - 3 - 4 - GTK_FILL - - - - - - True - True - Priority is used in Jabber to determine who gets the events from the jabber server when two or more clients are connected using the same account; The client with the highest priority gets the events - 5 0 127 1 5 0 - 1 - True - - - - 2 - 3 - 3 - 4 - GTK_FILL - - - - - - True - True + 6 + 5 + 3 + 6 + 6 - + True - 11 - True - - - Synchronise contacts - True - True - False - Click to request authorization to all contacts of another account - True - - - - False - 0 - - - - - Chan_ge Password - True - True - False - Click to change account's password - True - - - - False - 1 - - - - - - - True - Administration operations + True + Anonymous authentication + 0 + True + - label_item + 2 + 3 + GTK_FILL + + + + + + True + True + Resource is sent to the Jabber server in order to separate the same JID in two or more parts depending on the number of the clients connected in the same server with the same account. So you might be connected in the same account with resource 'Home' and 'Work' at the same time. The resource which has the highest priority will get the events. (see below) + Gajim + + + + 1 + 3 + 2 + 3 + GTK_EXPAND | GTK_SHRINK | GTK_FILL + + + + + + True + True + True + + + + 1 + 2 + + + + + + True + True + + + True + 11 + True + + + True + True + Click to request authorization to all contacts of another account + Synchronise contacts + True + 0 + + + + False + + + + + True + True + Click to change account's password + Chan_ge Password + True + 0 + + + + False + 1 + + + + + + + True + Administration operations + + + label_item + + + + + 3 + 4 + 5 + GTK_FILL + GTK_FILL + + + + + True + True + Priority is used in Jabber to determine who gets the events from the jabber server when two or more clients are connected using the same account; The client with the highest priority gets the events + 5 0 127 1 5 0 + 1 + True + + + + 2 + 3 + 3 + 4 + GTK_FILL + + + + + + True + True + Priority will change automatically according to your status. + A_djust to status + True + 0 + True + + + + 1 + 2 + 3 + 4 + GTK_FILL + + + + + + True + 0 + Priori_ty: + True + priority_spinbutton1 + + + 3 + 4 + GTK_FILL + + + + + + True + 0 + Resour_ce: + True + resource_entry1 + + + 2 + 3 + GTK_FILL + + + + + + True + True + If checked, Gajim will remember the password for this account + Save pass_word + True + False + 0 + True + + + + 2 + 3 + 1 + 2 + GTK_FILL + + + + + + True + False + True + False + True + + + + 1 + 2 + 1 + 2 + GTK_EXPAND | GTK_SHRINK | GTK_FILL + + + + + + True + 0 + _Password: + True + password_entry1 + + + 1 + 2 + GTK_FILL + + + + + + True + 0 + _Jabber ID: + True + jid_entry1 + + + GTK_FILL + + + + + + + + True + Account + + + tab + False + + + + + True + 6 + 6 + + + True + True + If checked, Gajim, when launched, will automatically connect to jabber using this account + C_onnect on Gajim startup + True + 0 + True + + + + False + False + + + + + True + True + Auto-reconnect when connection is lost + True + 0 + True + + + + False + False + 1 + + + + + True + True + Save conversation _logs for all contacts + True + 0 + True + True + + + + False + False + 2 + + + + + True + True + If checked, any change to the global status (handled by the combobox at the bottom of the roster window) will change the status of this account accordingly + Synch_ronize account status with global status + True + 0 + True + + + + False + False + 3 + + + + + True + True + If checked, Gajim will also broadcast some more IPs except from just your IP, so file transfer has higher chances of working. + Use file transfer proxies + True + 0 + True + + + + False + False + 4 - 3 - 4 - 5 - GTK_FILL - GTK_FILL - - - - - True - True - True - - - - 1 - 2 - - - - - - True - True - Resource is sent to the Jabber server in order to separate the same JID in two or more parts depending on the number of the clients connected in the same server with the same account. So you might be connected in the same account with resource 'Home' and 'Work' at the same time. The resource which has the highest priority will get the events. (see below) - Gajim - - - - 1 - 3 - 2 - 3 - GTK_EXPAND | GTK_SHRINK | GTK_FILL - - - - - - Anonymous authentication - True - True - False - True - - - - 2 - 3 - GTK_FILL - - - - - - - - True - Account - - - False - tab - - - - - True - 6 - 6 - - - C_onnect on Gajim startup - True - True - False - If checked, Gajim, when launched, will automatically connect to jabber using this account - True - True - - - - False - False - 0 - - - - - Auto-reconnect when connection is lost - True - True - False - True - True - - - - False - False 1 - - Save conversation _logs for all contacts + True - True - False - True - True - True - + General + True - False - False - 2 + tab + 1 + False - - Synch_ronize account status with global status + True - True - False - If checked, any change to the global status (handled by the combobox at the bottom of the roster window) will change the status of this account accordingly - True - True - - - - False - False - 3 - - - - - Use file transfer proxies - True - True - False - If checked, Gajim will also broadcast some more IPs except from just your IP, so file transfer has higher chances of working. - True - True - - - - False - False - 4 - - - - - 1 - - - - - True - General - True - - - 1 - False - tab - - - - - True - 6 - 12 - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 0 - none + 6 + 12 - + True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 12 + 0 + GTK_SHADOW_NONE - + True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 6 - 6 + 12 - - _use HTTP__PROXY environment variable + True - True - False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - True - - - - False - 0 - - - - - True + 6 6 - - True - None - - - - 0 - - - - - _Manage... + True True - False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + _use HTTP__PROXY environment variable True - + 0 + True + + + + False + + + + + True + 6 + + + True + None + + + + + + True + True + _Manage... + True + 0 + + + + False + False + 1 + + False - False 1 - - False - 1 - + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + <b>Proxy</b> + True + + + label_item + + - + True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - <b>Proxy</b> - True - - - label_item - - - - - 0 - - - - - True - 0 - none - - - True - 12 + 0 + GTK_SHADOW_NONE - + True - 6 - 6 + 12 - - _Warn before using an insecure connection + True - True - False - Check this so Gajim will ask you before sending your password over an insecure connection. - True - True - - - - False - False - 0 - - - - - Send _keep-alive packets - True - True - False - If checked, Gajim will send keep-alive packets to prevent connection timeout which results in disconnection - True - True - True - - - - False - False - 1 - - - - - Use cust_om hostname/port - True - True - False - True - True - - - - False - False - 2 - - - - - True - False + 6 6 - + True - _Hostname: + True + Check this so Gajim will ask you before sending your password over an insecure connection. + _Warn before using an insecure connection True + 0 + True + False False - 0 - + True True - + If checked, Gajim will send keep-alive packets to prevent connection timeout which results in disconnection + Send _keep-alive packets + True + 0 + True + True + + False + False 1 - + True - _Port: + True + Use cust_om hostname/port True + 0 + True + False @@ -734,217 +702,257 @@ - + True - True - 6 - 5222 - + False + 6 + + + True + _Hostname: + True + + + False + False + + + + + True + True + + + + 1 + + + + + True + _Port: + True + + + False + False + 2 + + + + + True + True + 6 + 5222 + + + + False + 3 + + - False 3 - - 3 - - - - - - True - <b>Miscellaneous</b> - True + + + True + <b>Miscellaneous</b> + True + + + label_item + + - label_item + False + 1 - False - 1 + 2 - - - 2 - - - - - True - Connection - - - 2 - False - tab - - - - - True - 5 - 6 - + True - 0 - none + Connection + + + tab + 2 + False + + + + + True + 5 + 6 - + True - 12 + 0 + GTK_SHADOW_NONE - + True - 6 - 6 + 12 - + True + 6 6 - + True + 6 + + + True + True + No key selected + True + + + False + False + + + + + True + True + True + + + 1 + + + + + True + True + Choose _Key... + True + 0 + + + + False + False + 2 + + + + + False + + + + + True + False True - No key selected - True + If checked, Gajim will get the password from a GPG agent like seahorse + Use G_PG Agent + True + 0 + True + False False - 0 - - - - - True - True - True - - 1 - - - Choose _Key... - True - True - False - True - - - - False - False - 2 - - - - False - 0 - - - - - Use G_PG Agent - True - False - True - False - If checked, Gajim will get the password from a GPG agent like seahorse - True - True - - - - False - False - 1 - - - - - - True - <b>OpenPGP</b> - True + + + True + <b>OpenPGP</b> + True + + + label_item + + - label_item + False + + + + + True + 0 + GTK_SHADOW_NONE + + + True + 6 + 12 + + + True + True + Information about you, as stored in the server + _Edit Personal Information... + True + 0 + + + + + + + + True + <b>Personal Information</b> + True + + + label_item + + + + + False + 1 - False - 0 + 3 - + True - 0 - none - - - True - 6 - 12 - - - _Edit Personal Information... - True - True - False - Information about you, as stored in the server - True - - - - - - - - True - <b>Personal Information</b> - True - - - label_item - - + Personal Information - False - 1 + tab + 3 + False - 3 - - - - - True - Personal Information - - - 3 - False - tab + 1 @@ -962,20 +970,20 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 1 - _Enable True True - False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + _Enable True + 0 True False - 0 @@ -989,28 +997,27 @@ 6 - Co_nnect on Gajim startup True True - False If checked, Gajim, when launched, will automatically connect to jabber using this account + Co_nnect on Gajim startup True + 0 True False False - 0 - Save conversation _logs for all contacts True True - False + Save conversation _logs for all contacts True + 0 True @@ -1022,12 +1029,12 @@ - Synchroni_ze account status with global status True True - False If checked, any change to the global status (handled by the combobox at the bottom of the roster window) will change the status of this account accordingly + Synchroni_ze account status with global status True + 0 True @@ -1042,20 +1049,19 @@ True - Use cust_om port: True True - False If the default port that is used for incoming messages is unfitting for your setup you can select another one here. You might consider to change possible firewall settings. + Use cust_om port: True + 0 True False False - 0 @@ -1087,8 +1093,8 @@ You might consider to change possible firewall settings. True - False tab + False @@ -1113,7 +1119,6 @@ You might consider to change possible firewall settings. False False - 0 @@ -1128,7 +1133,6 @@ You might consider to change possible firewall settings. False False - 0 @@ -1141,11 +1145,11 @@ You might consider to change possible firewall settings. - Choose _Key... True True - False + Choose _Key... True + 0 @@ -1161,12 +1165,12 @@ You might consider to change possible firewall settings. - Use G_PG Agent True True - False If checked, Gajim will get the password from a GPG agent like seahorse + Use G_PG Agent True + 0 True @@ -1312,9 +1316,9 @@ You might consider to change possible firewall settings. Personal Information + tab 1 False - tab @@ -1340,18 +1344,15 @@ You might consider to change possible firewall settings. - - 0 - - Mer_ge accounts True True - False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Mer_ge accounts True + 0 True @@ -1364,21 +1365,21 @@ You might consider to change possible firewall settings. True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 6 - end + GTK_BUTTONBOX_END - gtk-close True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + gtk-close True + 0 False False - 0 diff --git a/data/glade/join_groupchat_window.glade b/data/glade/join_groupchat_window.glade index 6e08f2826..2a3d63c34 100644 --- a/data/glade/join_groupchat_window.glade +++ b/data/glade/join_groupchat_window.glade @@ -1,12 +1,12 @@ - + + + - - 6 Join Group Chat - + True @@ -14,7 +14,7 @@ True - 6 + 7 2 12 6 @@ -132,17 +132,18 @@ - Join this room automatically when I connect True + False True - False + Join this room automatically when I connect True + 0 True 2 - 5 - 6 + 6 + 7 GTK_FILL @@ -170,30 +171,43 @@ GTK_FILL + + + True + True + Bookmark this room + 0 + True + + + + 2 + 5 + 6 + GTK_FILL + + + - - 0 - True 12 - end + GTK_BUTTONBOX_END - gtk-cancel True True True - False + gtk-cancel True + 0 False False - 0 @@ -202,7 +216,7 @@ True True True - False + 0 @@ -221,7 +235,6 @@ False False - 0 diff --git a/data/glade/privacy_list_window.glade b/data/glade/privacy_list_window.glade index d2d527c76..78704da07 100644 --- a/data/glade/privacy_list_window.glade +++ b/data/glade/privacy_list_window.glade @@ -165,6 +165,7 @@ True + False 5 @@ -422,6 +423,19 @@ to 3 + + + True + True + All (including subscription) + 0 + True + + + + 4 + + 2 diff --git a/launch.sh b/launch.sh index c7c361f3d..bcb968553 100755 --- a/launch.sh +++ b/launch.sh @@ -1,3 +1,3 @@ #!/bin/sh cd "$(dirname $0)/src" -exec python -Ot gajim.py $@ +exec python -OOt gajim.py $@ diff --git a/src/Makefile.am b/src/Makefile.am index 17a1d1c9c..24e7e2234 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -41,9 +41,13 @@ gajimsrc3dir = $(pkgdatadir)/src/common/zeroconf gajimsrc3_PYTHON = \ $(srcdir)/common/zeroconf/*.py -gajimsrc4dir = $(pkgdatadir)/src/commands +gajimsrc4dir = $(pkgdatadir)/src/command_system gajimsrc4_PYTHON = \ - $(srcdir)/commands/*.py + $(srcdir)/command_system/*.py + +gajimsrc5dir = $(pkgdatadir)/src/command_system/implementation +gajimsrc5_PYTHON = \ + $(srcdir)/command_system/implementation/*.py DISTCLEANFILES = @@ -52,6 +56,7 @@ EXTRA_DIST = $(gajimsrc_PYTHON) \ $(gajimsrc2_PYTHON) \ $(gajimsrc3_PYTHON) \ $(gajimsrc4_PYTHON) \ + $(gajimsrc5_PYTHON) \ eggtrayicon.c \ trayiconmodule.c \ eggtrayicon.h \ diff --git a/src/chat_control.py b/src/chat_control.py index e66ff82ba..31a7704c1 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -52,7 +52,13 @@ from common.xmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION from common.xmpp.protocol import NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_JINGLE_ICE_UDP -from commands.implementation import CommonCommands, ChatCommands +from command_system.implementation.middleware import ChatCommandProcessor +from command_system.implementation.middleware import CommandTools +from command_system.implementation.hosts import ChatCommands + +# Here we load the module with the standard commands, so they are being detected +# and dispatched. +import command_system.implementation.standard try: import gtkspell @@ -82,7 +88,7 @@ if gajim.config.get('use_speller') and HAS_GTK_SPELL: del tv ################################################################################ -class ChatControlBase(MessageControl, CommonCommands): +class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): '''A base class containing a banner, ConversationTextview, MessageTextView ''' @@ -705,7 +711,8 @@ class ChatControlBase(MessageControl, CommonCommands): def print_conversation_line(self, text, kind, name, tim, other_tags_for_name=[], other_tags_for_time=[], other_tags_for_text=[], - count_as_new=True, subject=None, old_kind=None, xhtml=None, simple=False, xep0184_id = None): + count_as_new=True, subject=None, old_kind=None, xhtml=None, simple=False, + xep0184_id=None, graphics=True): '''prints 'chat' type messages''' jid = self.contact.jid full_jid = self.get_full_jid() @@ -715,7 +722,7 @@ class ChatControlBase(MessageControl, CommonCommands): end = True textview.print_conversation_line(text, jid, kind, name, tim, other_tags_for_name, other_tags_for_time, other_tags_for_text, - subject, old_kind, xhtml, simple=simple) + subject, old_kind, xhtml, simple=simple, graphics=graphics) if xep0184_id is not None: textview.show_xep0184_warning(xep0184_id) @@ -1164,7 +1171,7 @@ class ChatControlBase(MessageControl, CommonCommands): # FIXME: Set sensitivity for toolbar ################################################################################ -class ChatControl(ChatControlBase, ChatCommands): +class ChatControl(ChatControlBase): '''A control for standard 1-1 chat''' ( JINGLE_STATE_NOT_AVAILABLE, @@ -1178,7 +1185,9 @@ class ChatControl(ChatControlBase, ChatCommands): TYPE_ID = message_control.TYPE_CHAT old_msg_kind = None # last kind of the printed message - DISPATCHED_BY = ChatCommands + # Set a command host to bound to. Every command given through a chat will be + # processed with this command host. + COMMAND_HOST = ChatCommands def __init__(self, parent_win, contact, acct, session, resource = None): ChatControlBase.__init__(self, self.TYPE_ID, parent_win, @@ -1303,14 +1312,12 @@ class ChatControl(ChatControlBase, ChatCommands): self.handlers[id_] = widget if not session: - session = gajim.connections[self.account]. \ - find_controlless_session(self.contact.jid) - if session: - # Don't use previous session if we want to a specific resource - # and it's not the same - r = gajim.get_room_and_nick_from_fjid(str(session.jid))[1] - if resource and resource != r: - session = None + # Don't use previous session if we want to a specific resource + # and it's not the same + if not resource: + resource = contact.resource + session = gajim.connections[self.account].find_controlless_session( + self.contact.jid, resource) if session: session.control = self @@ -2315,7 +2322,8 @@ class ChatControl(ChatControlBase, ChatCommands): # XXX: Once we have fallback to disco, remove notexistant check if not gajim.HAVE_PYCRYPTO or \ not gajim.capscache.is_supported(contact, NS_ESESSION) or \ - gajim.capscache.is_supported(contact, 'notexistant'): + gajim.capscache.is_supported(contact, 'notexistant') or \ + not gajim.config.get_per('accounts', self.account, 'enable_esessions'): toggle_e2e_menuitem.set_sensitive(False) else: toggle_e2e_menuitem.set_active(e2e_is_active) diff --git a/src/command_system/__init__.py b/src/command_system/__init__.py new file mode 100644 index 000000000..c0a48f863 --- /dev/null +++ b/src/command_system/__init__.py @@ -0,0 +1,20 @@ +# Copyright (C) 2009 red-agent +# +# 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 3 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, see . + +""" +The command system providing scalable, clean and convenient architecture in +combination with declarative way of defining commands and a fair amount of +automatization for routine processes. +""" diff --git a/src/command_system/dispatching.py b/src/command_system/dispatching.py new file mode 100644 index 000000000..2bfd76b96 --- /dev/null +++ b/src/command_system/dispatching.py @@ -0,0 +1,89 @@ +# Copyright (C) 2009 red-agent +# +# 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 3 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, see . + +""" +The backbone of the command system. Provides automatic dispatching which does +not require explicit registering commands or containers and remains active even +after everything is done, so new commands can be added during the runtime. +""" + +from types import NoneType + +class Dispatcher(type): + + containers = {} + commands = {} + + @classmethod + def register_host(klass, host): + klass.containers[host] = [] + + @classmethod + def register_container(klass, container): + for host in container.HOSTS: + klass.containers[host].append(container) + + @classmethod + def register_commands(klass, container): + klass.commands[container] = {} + for command in klass.traverse_commands(container): + for name in command.names: + klass.commands[container][name] = command + + @classmethod + def get_command(klass, host, name): + for container in klass.containers[host]: + command = klass.commands[container].get(name) + if command: + return command + + @classmethod + def list_commands(klass, host): + for container in klass.containers[host]: + commands = klass.commands[container] + for name, command in commands.iteritems(): + yield name, command + + @classmethod + def traverse_commands(klass, container): + for name in dir(container): + attribute = getattr(container, name) + if klass.is_command(attribute): + yield attribute + + @staticmethod + def is_root(ns): + meta = ns.get('__metaclass__', NoneType) + return issubclass(meta, Dispatcher) + + @staticmethod + def is_command(attribute): + name = attribute.__class__.__name__ + return name == 'Command' + +class HostDispatcher(Dispatcher): + + def __init__(klass, name, bases, ns): + if not Dispatcher.is_root(ns): + HostDispatcher.register_host(klass) + super(HostDispatcher, klass).__init__(name, bases, ns) + +class ContainerDispatcher(Dispatcher): + + def __init__(klass, name, bases, ns): + if not Dispatcher.is_root(ns): + ContainerDispatcher.register_container(klass) + ContainerDispatcher.register_commands(klass) + super(ContainerDispatcher, klass).__init__(name, bases, ns) diff --git a/src/command_system/errors.py b/src/command_system/errors.py new file mode 100644 index 000000000..992e83ccf --- /dev/null +++ b/src/command_system/errors.py @@ -0,0 +1,41 @@ +# Copyright (C) 2009 red-agent +# +# 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 3 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, see . + +class BaseError(Exception): + """ + Common base for errors which relate to a specific command. Encapsulates + everything needed to identify a command, by either its object or name. + """ + + def __init__(self, message, command=None, name=None): + self.command = command + self.name = name + + if command and not name: + self.name = command.first_name + + super(BaseError, self).__init__(message) + +class DefinitionError(BaseError): + """ + Used to indicate errors occured on command definition. + """ + pass + +class CommandError(BaseError): + """ + Used to indicate errors occured during command execution. + """ + pass diff --git a/src/command_system/framework.py b/src/command_system/framework.py new file mode 100644 index 000000000..f46f701c5 --- /dev/null +++ b/src/command_system/framework.py @@ -0,0 +1,337 @@ +# Copyright (C) 2009 red-agent +# +# 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 3 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, see . + +""" +Provides a tiny framework with simple, yet powerful and extensible architecture +to implement commands in a streight and flexible, declarative way. +""" + +import re +from types import FunctionType +from inspect import getargspec + +from dispatching import Dispatcher, HostDispatcher, ContainerDispatcher +from mapping import parse_arguments, adapt_arguments +from errors import DefinitionError, CommandError + +class CommandHost(object): + """ + Command host is a hub between numerous command processors and command + containers. Aimed to participate in a dispatching process in order to + provide clean and transparent architecture. + """ + __metaclass__ = HostDispatcher + +class CommandContainer(object): + """ + Command container is an entity which holds defined commands, allowing them + to be dispatched and proccessed correctly. Each command container may be + bound to a one or more command hosts. + + Bounding is controlled by the HOSTS variable, which must be defined in the + body of the command container. This variable should contain a list of hosts + to bound to, as a tuple or list. + """ + __metaclass__ = ContainerDispatcher + +class CommandProcessor(object): + """ + Command processor is an immediate command emitter. It does not participate + in the dispatching process directly, but must define a host to bound to. + + Bounding is controlled by the COMMAND_HOST variable, which must be defined + in the body of the command processor. This variable should be set to a + specific command host. + """ + + # This defines a command prefix (or an initializer), which should preceede a + # a text in order it to be processed as a command. + COMMAND_PREFIX = '/' + + def process_as_command(self, text): + """ + Try to process text as a command. Returns True if it has been processed + as a command and False otherwise. + """ + if not text.startswith(self.COMMAND_PREFIX): + return False + + body = text[len(self.COMMAND_PREFIX):] + body = body.strip() + + parts = body.split(None, 1) + name, arguments = parts if len(parts) > 1 else (parts[0], None) + + flag = self.looks_like_command(text, body, name, arguments) + if flag is not None: + return flag + + self.execute_command(name, arguments) + + return True + + def execute_command(self, name, arguments): + command = self.get_command(name) + + args, opts = parse_arguments(arguments) if arguments else ([], []) + args, kwargs = adapt_arguments(command, arguments, args, opts) + + if self.command_preprocessor(command, name, arguments, args, kwargs): + return + value = command(self, *args, **kwargs) + self.command_postprocessor(command, name, arguments, args, kwargs, value) + + def command_preprocessor(self, command, name, arguments, args, kwargs): + """ + Redefine this method in the subclass to execute custom code before + command gets executed. + + If returns True then command execution will be interrupted and command + will not be executed. + """ + pass + + def command_postprocessor(self, command, name, arguments, args, kwargs, value): + """ + Redefine this method in the subclass to execute custom code after + command gets executed. + """ + pass + + def looks_like_command(self, text, body, name, arguments): + """ + This hook is being called before any processing, but after it was + determined that text looks like a command. + + If returns value other then None - then further processing will be + interrupted and that value will be used to return from + process_as_command. + """ + pass + + def get_command(self, name): + command = Dispatcher.get_command(self.COMMAND_HOST, name) + if not command: + raise CommandError("Command does not exist", name=name) + return command + + def list_commands(self): + commands = Dispatcher.list_commands(self.COMMAND_HOST) + commands = dict(commands) + return sorted(set(commands.itervalues())) + +class Command(object): + + # These two regular expression patterns control how command documentation + # will be formatted to be transformed to a normal, readable state. + DOC_STRIP_PATTERN = re.compile(r'(?:^[ \t]+|\A\n)', re.MULTILINE) + DOC_FORMAT_PATTERN = re.compile(r'(?" % ', '.join(self.names) + + def __cmp__(self, other): + return cmp(self.first_name, other.first_name) + + @property + def first_name(self): + return self.names[0] + + @property + def native_name(self): + return self.handler.__name__ + + def extract_documentation(self): + """ + Extract handler's documentation which is a doc-string and transform it + to a usable format. + + Transformation is done based on the DOC_STRIP_PATTERN and + DOC_FORMAT_PATTERN regular expression patterns. + """ + documentation = self.handler.__doc__ or None + + if not documentation: + return + + documentation = re.sub(self.DOC_STRIP_PATTERN, str(), documentation) + documentation = re.sub(self.DOC_FORMAT_PATTERN, ' ', documentation) + + return documentation + + def extract_description(self): + """ + Extract handler's description (which is a first line of the + documentation). Try to keep them simple yet meaningful. + """ + documentation = self.extract_documentation() + return documentation.split('\n', 1)[0] if documentation else None + + def extract_specification(self): + """ + Extract handler's arguments specification, as it was defined preserving + their order. + """ + names, var_args, var_kwargs, defaults = getargspec(self.handler) + + # Behavior of this code need to be checked. Might yield incorrect + # results on some rare occasions. + spec_args = names[:-len(defaults) if defaults else len(names)] + spec_kwargs = list(zip(names[-len(defaults):], defaults)) if defaults else {} + + # Removing self from arguments specification. Command handler should + # receive the processors as a first argument, which should be self by + # the canonical means. + if spec_args.pop(0) != 'self': + raise DefinitionError("First argument must be self", self) + + return spec_args, spec_kwargs, var_args, var_kwargs + +def command(*names, **properties): + """ + A decorator for defining commands in a declarative way. Provides facilities + for setting command's names and properties. + + Names should contain a set of names (aliases) by which the command can be + reached. If no names are given - the the native name (the one extracted from + the command handler) will be used. + + If include_native=True is given (default) and names is non-empty - then the + native name of the command will be prepended in addition to the given names. + + If usage=True is given (default) - then command help will be appended with + autogenerated usage info, based of the command handler arguments + introspection. + + If source=True is given - then the first argument of the command will + receive the source arguments, as a raw, unprocessed string. The further + mapping of arguments and options will not be affected. + + If raw=True is given - then command considered to be raw and should define + positional arguments only. If it defines only one positional argument - this + argument will receive all the raw and unprocessed arguments. If the command + defines more then one positional argument - then all the arguments except + the last one will be processed normally; the last argument will get what is + left after the processing as raw and unprocessed string. + + If empty=True is given - this will allow to call a raw command without + arguments. + + If extra=True is given - then all the extra arguments passed to a command + will be collected into a sequence and given to the last positional argument. + + If overlap=True is given - then all the extra arguments will be mapped as if + they were values for the keyword arguments. + + If expand_short=True is given (default) - then short, one-letter options + will be expanded to a verbose ones, based of the comparison of the first + letter. If more then one option with the same first letter is given - then + only first one will be used in the expansion. + """ + names = list(names) + + include_native = properties.get('include_native', True) + + usage = properties.get('usage', True) + source = properties.get('source', False) + raw = properties.get('raw', False) + empty = properties.get('empty', False) + extra = properties.get('extra', False) + overlap = properties.get('overlap', False) + expand_short = properties.get('expand_short', True) + + if empty and not raw: + raise DefinitionError("Empty option can be used only with raw commands") + + if extra and overlap: + raise DefinitionError("Extra and overlap options can not be used together") + + properties = { + 'usage': usage, + 'source': source, + 'raw': raw, + 'extra': extra, + 'overlap': overlap, + 'empty': empty, + 'expand_short': expand_short + } + + def decorator(handler): + """ + Decorator which receives handler as a first argument and then wraps it + in the command which then returns back. + """ + command = Command(handler, *names, **properties) + + # Extract and inject a native name if either no other names are + # specified or include_native property is enabled, while making sure it + # is going to be the first one in the list. + if not names or include_native: + names.insert(0, command.native_name) + command.names = tuple(names) + + return command + + # Workaround if we are getting called without parameters. Keep in mind that + # in that case - first item in the names will be the handler. + if names and isinstance(names[0], FunctionType): + return decorator(names.pop(0)) + + return decorator + +def documentation(text): + """ + This decorator is used to bind a documentation (a help) to a command. + + Though this can be done easily by using doc-strings in a declarative and + Pythonic way - some of Gajim's developers are against it because of the + scaffolding needed to support the tranlation of such documentation. + """ + def decorator(target): + if isinstance(target, Command): + target.handler.__doc__ = text + else: + target.__doc__ = text + return target + + return decorator diff --git a/src/commands/__init__.py b/src/command_system/implementation/__init__.py similarity index 79% rename from src/commands/__init__.py rename to src/command_system/implementation/__init__.py index 48f99c416..66d097f42 100644 --- a/src/commands/__init__.py +++ b/src/command_system/implementation/__init__.py @@ -14,7 +14,6 @@ # along with this program. If not, see . """ -The command system providing scalable and convenient architecture in combination -with declarative way of defining commands and a fair amount of automatization -for routine processes. +The implementation and auxilary systems which implement the standard Gajim +commands and also provide an infrastructure for adding custom commands. """ diff --git a/src/command_system/implementation/custom.py b/src/command_system/implementation/custom.py new file mode 100644 index 000000000..4f54670da --- /dev/null +++ b/src/command_system/implementation/custom.py @@ -0,0 +1,86 @@ +# Copyright (C) 2009 red-agent +# +# 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 3 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, see . + +""" +The module contains examples of how to create your own commands, by creating a +new command container and definding a set of commands. + +Keep in mind that this module is not being loaded, so the code will not be +executed and commands defined here will not be detected. +""" + +from ..framework import CommandContainer, command, documentation +from hosts import ChatCommands, PrivateChatCommands, GroupChatCommands + +class CustomCommonCommands(CommandContainer): + """ + This command container bounds to all three available in the default + implementation command hosts. This means that commands defined in this + container will be available to all - chat, private chat and a group chat. + """ + + HOSTS = (ChatCommands, PrivateChatCommands, GroupChatCommands) + + @command + def dance(self): + """ + First line of the doc string is called a description and will be + programmatically extracted and formatted. + + After that you can give more help, like explanation of the options. This + one will be programatically extracted and formatted too. + + After all the documentation - there will be autogenerated (based on the + method signature) usage information appended. You can turn it off + though, if you want. + """ + return "I can't dance, you stupid fuck, I'm just a command system! A cool one, though..." + +class CustomChatCommands(CommandContainer): + """ + This command container bounds only to the ChatCommands command host. + Therefore command defined here will be available only to a chat. + """ + + HOSTS = (ChatCommands,) + + @documentation(_("The same as using a doc-string, except it supports translation")) + @command + def sing(self): + return "Are you phreaking kidding me? Buy yourself a damn stereo..." + +class CustomPrivateChatCommands(CommandContainer): + """ + This command container bounds only to the PrivateChatCommands command host. + Therefore command defined here will be available only to a private chat. + """ + + HOSTS = (PrivateChatCommands,) + + @command + def make_coffee(self): + return "What do I look like, you ass? A coffee machine!?" + +class CustomGroupChatCommands(CommandContainer): + """ + This command container bounds only to the GroupChatCommands command host. + Therefore command defined here will be available only to a group chat. + """ + + HOSTS = (GroupChatCommands,) + + @command + def fetch(self): + return "You should really buy yourself a dog and start torturing it instead of me..." diff --git a/src/command_system/implementation/hosts.py b/src/command_system/implementation/hosts.py new file mode 100644 index 000000000..b38bb1a35 --- /dev/null +++ b/src/command_system/implementation/hosts.py @@ -0,0 +1,42 @@ +# Copyright (C) 2009 red-agent +# +# 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 3 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, see . + +""" +The module defines a set of command hosts, which are bound to a different +command processors, which are the source of commands. +""" + +from ..framework import CommandHost + +class ChatCommands(CommandHost): + """ + This command host is bound to the command processor which processes commands + from a chat. + """ + pass + +class PrivateChatCommands(CommandHost): + """ + This command host is bound to the command processor which processes commands + from a private chat. + """ + pass + +class GroupChatCommands(CommandHost): + """ + This command host is bound to the command processor which processes commands + from a group chat. + """ + pass diff --git a/src/commands/middleware.py b/src/command_system/implementation/middleware.py similarity index 61% rename from src/commands/middleware.py rename to src/command_system/implementation/middleware.py index 7c139afd9..9ef4bea29 100644 --- a/src/commands/middleware.py +++ b/src/command_system/implementation/middleware.py @@ -16,53 +16,70 @@ """ Provides a glue to tie command system framework and the actual code where it would be dropped in. Defines a little bit of scaffolding to support interaction -between the two and a few utility methods so you don't need to dig up the host -code to write basic commands. +between the two and a few utility methods so you don't need to dig up the code +itself code to write basic commands. """ -from common import gajim from types import StringTypes -from framework import CommandProcessor, CommandError from traceback import print_exc -class ChatMiddleware(CommandProcessor): +from common import gajim + +from ..framework import CommandProcessor +from ..errors import CommandError + +class ChatCommandProcessor(CommandProcessor): """ - Provides basic scaffolding for the convenient interaction with ChatControl. - Also provides some few basic utilities for the same purpose. + A basic scaffolding to provide convenient interaction between the command + system and chat controls. """ - def execute_command(self, text, name, arguments): + def process_as_command(self, text): + flag = super(ChatCommandProcessor, self).process_as_command(text) + if flag: + self.add_history(text) + self.clear_input() + return flag + + def execute_command(self, name, arguments): try: - super(ChatMiddleware, self).execute_command(text, name, arguments) - except CommandError, exception: - self.echo("%s: %s" %(exception.name, exception.message), 'error') + super(ChatCommandProcessor, self).execute_command(name, arguments) + except CommandError, error: + self.echo("%s: %s" %(error.name, error.message), 'error') except Exception: self.echo("An error occured while trying to execute the command", 'error') print_exc() - finally: - self.add_history(text) - self.clear_input() - def looks_like_command(self, text, name, arguments): + def looks_like_command(self, text, body, name, arguments): # Command escape stuff ggoes here. If text was prepended by the command # prefix twice, like //not_a_command (if prefix is set to /) then it # will be escaped, that is sent just as a regular message with one (only # one) prefix removed, so message will be /not_a_command. - if name.startswith(self.COMMAND_PREFIX): - self._say_(self, text) + if body.startswith(self.COMMAND_PREFIX): + self.send(body) return True - def command_preprocessor(self, name, command, arguments, args, kwargs): + def command_preprocessor(self, command, name, arguments, args, kwargs): + # If command argument contain h or help option - forward it to the /help + # command. Dont forget to pass self, as all commands are unbound. And + # also don't forget to print output. if 'h' in kwargs or 'help' in kwargs: - # Forwarding to the /help command. Dont forget to pass self, as - # all commands are unbound. And also don't forget to print output. - self.echo(self._help_(self, name)) + help = self.get_command('help') + self.echo(help(self, name)) return True - def command_postprocessor(self, name, command, arguments, args, kwargs, value): + def command_postprocessor(self, command, name, arguments, args, kwargs, value): + # If command returns a string - print it to a user. A convenient and + # sufficient in most simple cases shortcut to a using echo. if value and isinstance(value, StringTypes): self.echo(value) +class CommandTools: + """ + Contains a set of basic tools and shortcuts you can use in your commands to + performe some simple operations. + """ + def echo(self, text, kind='info'): """ Print given text to the user. @@ -79,8 +96,8 @@ class ChatMiddleware(CommandProcessor): """ Set given text into the input. """ - message_buffer = self.msg_textview.get_buffer() - message_buffer.set_text(text) + buffer = self.msg_textview.get_buffer() + buffer.set_text(text) def clear_input(self): """ @@ -90,8 +107,8 @@ class ChatMiddleware(CommandProcessor): def add_history(self, text): """ - Add given text to the input history, so user can scroll through it - using ctrl + up/down arrow keys. + Add given text to the input history, so user can scroll through it using + ctrl + up/down arrow keys. """ self.save_sent_message(text) diff --git a/src/commands/implementation.py b/src/command_system/implementation/standard.py similarity index 59% rename from src/commands/implementation.py rename to src/command_system/implementation/standard.py index 35e6b3bdf..1823caf97 100644 --- a/src/commands/implementation.py +++ b/src/command_system/implementation/standard.py @@ -14,7 +14,7 @@ # along with this program. If not, see . """ -Provides an actual implementation of the standard commands. +Provides an actual implementation for the standard commands. """ import dialogs @@ -22,46 +22,47 @@ from common import gajim from common import helpers from common.exceptions import GajimGeneralException -from framework import command, CommandError -from middleware import ChatMiddleware +from ..framework import CommandContainer, command, documentation +from ..mapping import generate_usage -class CommonCommands(ChatMiddleware): +from hosts import ChatCommands, PrivateChatCommands, GroupChatCommands + +class StandardCommonCommands(CommandContainer): """ - Here defined commands will be common to all, chat, private chat and group - chat. Keep in mind that self is set to an instance of either ChatControl, - PrivateChatControl or GroupchatControl when command is being called. + This command container contains standard commands which are common to all - + chat, private chat, group chat. """ + HOSTS = (ChatCommands, PrivateChatCommands, GroupChatCommands) + @command + @documentation(_("Clear the text window")) def clear(self): - """ - Clear the text window - """ self.conv_textview.clear() @command + @documentation(_("Hide the chat buttons")) def compact(self): - """ - Hide the chat buttons - """ - self.chat_buttons_set_visible(not self.hide_chat_buttons) + new_status = not self.hide_chat_buttons + self.chat_buttons_set_visible(new_status) @command(overlap=True) + @documentation(_("Show help on a given command or a list of available commands if -(-a)ll is given")) def help(self, command=None, all=False): - """ - Show help on a given command or a list of available commands if -(-a)ll is - given - """ if command: - command = self.retrieve_command(command) + command = self.get_command(command) - doc = _(command.extract_doc()) - usage = command.extract_arg_usage() + documentation = _(command.extract_documentation()) + usage = generate_usage(command) - if doc: - return (doc + '\n\n' + usage) if command.usage else doc - else: - return usage + text = [] + + if documentation: + text.append(documentation) + if command.usage: + text.append(usage) + + return '\n\n'.join(text) elif all: for command in self.list_commands(): names = ', '.join(command.names) @@ -69,66 +70,52 @@ class CommonCommands(ChatMiddleware): self.echo("%s - %s" % (names, description)) else: - self.echo(self._help_(self, 'help')) + help = self.get_command('help') + self.echo(help(self, 'help')) @command(raw=True) + @documentation(_("Send a message to the contact")) def say(self, message): - """ - Send a message to the contact - """ self.send(message) @command(raw=True) + @documentation(_("Send action (in the third person) to the current chat")) def me(self, action): - """ - Send action (in the third person) to the current chat - """ self.send("/me %s" % action) -class ChatCommands(CommonCommands): +class StandardChatCommands(CommandContainer): """ - Here defined commands will be unique to a chat. Use it as a hoster to provide - commands which should be unique to a chat. Keep in mind that self is set to - an instance of ChatControl when command is being called. + This command container contains standard command which are unique to a chat. """ - DISPATCH = True - INHERIT = True + HOSTS = (ChatCommands,) @command + @documentation(_("Send a ping to the contact")) def ping(self): - """ - Send a ping to the contact - """ if self.account == gajim.ZEROCONF_ACC_NAME: raise CommandError(_('Command is not supported for zeroconf accounts')) gajim.connections[self.account].sendPing(self.contact) -class PrivateChatCommands(CommonCommands): +class StandardPrivateChatCommands(CommandContainer): """ - Here defined commands will be unique to a private chat. Use it as a hoster to - provide commands which should be unique to a private chat. Keep in mind that - self is set to an instance of PrivateChatControl when command is being called. + This command container contains standard command which are unique to a + private chat. """ - DISPATCH = True - INHERIT = True + HOSTS = (PrivateChatCommands,) -class GroupChatCommands(CommonCommands): +class StandardGroupchatCommands(CommandContainer): """ - Here defined commands will be unique to a group chat. Use it as a hoster to - provide commands which should be unique to a group chat. Keep in mind that - self is set to an instance of GroupchatControl when command is being called. + This command container contains standard command which are unique to a group + chat. """ - DISPATCH = True - INHERIT = True + HOSTS = (GroupChatCommands,) @command(raw=True) + @documentation(_("Change your nickname in a group chat")) def nick(self, new_nick): - """ - Change your nickname in a group chat - """ try: new_nick = helpers.parse_resource(new_nick) except Exception: @@ -137,10 +124,8 @@ class GroupChatCommands(CommonCommands): self.new_nick = new_nick @command('query', raw=True) + @documentation(_("Open a private chat window with a specified occupant")) def chat(self, nick): - """ - Open a private chat window with a specified occupant - """ nicks = gajim.contacts.get_nick_list(self.account, self.room_jid) if nick in nicks: self.on_send_pm(nick=nick) @@ -148,11 +133,8 @@ class GroupChatCommands(CommonCommands): raise CommandError(_("Nickname not found")) @command('msg', raw=True) + @documentation(_("Open a private chat window with a specified occupant and send him a message")) def message(self, nick, a_message): - """ - Open a private chat window with a specified occupant and send him a - message - """ nicks = gajim.contacts.get_nick_list(self.account, self.room_jid) if nick in nicks: self.on_send_pm(nick=nick, msg=a_message) @@ -160,28 +142,22 @@ class GroupChatCommands(CommonCommands): raise CommandError(_("Nickname not found")) @command(raw=True, empty=True) + @documentation(_("Display or change a group chat topic")) def topic(self, new_topic): - """ - Display or change a group chat topic - """ if new_topic: self.connection.send_gc_subject(self.room_jid, new_topic) else: return self.subject @command(raw=True, empty=True) + @documentation(_("Invite a user to a room for a reason")) def invite(self, jid, reason): - """ - Invite a user to a room for a reason - """ self.connection.send_invite(self.room_jid, jid, reason) return _("Invited %s to %s") % (jid, self.room_jid) @command(raw=True, empty=True) + @documentation(_("Join a group chat given by a jid, optionally using given nickname")) def join(self, jid, nick): - """ - Join a group chat given by a jid, optionally using given nickname - """ if not nick: nick = self.nick @@ -192,43 +168,37 @@ class GroupChatCommands(CommonCommands): gajim.interface.instances[self.account]['join_gc'].window.present() except KeyError: try: - dialogs.JoinGroupchatWindow(account=self.account, room_jid=jid, nick=nick) + dialogs.JoinGroupchatWindow(account=None, room_jid=jid, nick=nick) except GajimGeneralException: pass @command('part', 'close', raw=True, empty=True) + @documentation(_("Leave the groupchat, optionally giving a reason, and close tab or window")) def leave(self, reason): - """ - Leave the groupchat, optionally giving a reason, and close tab or window - """ self.parent_win.remove_tab(self, self.parent_win.CLOSE_COMMAND, reason) @command(raw=True, empty=True) - def ban(self, who, reason): - """ - Ban user by a nick or a jid from a groupchat + @documentation(_(""" + Ban user by a nick or a jid from a groupchat - If given nickname is not found it will be treated as a jid. - """ + If given nickname is not found it will be treated as a jid. + """)) + def ban(self, who, reason): if who in gajim.contacts.get_nick_list(self.account, self.room_jid): contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, who) who = contact.jid self.connection.gc_set_affiliation(self.room_jid, who, 'outcast', reason or str()) @command(raw=True, empty=True) + @documentation(_("Kick user by a nick from a groupchat")) def kick(self, who, reason): - """ - Kick user by a nick from a groupchat - """ if not who in gajim.contacts.get_nick_list(self.account, self.room_jid): raise CommandError(_("Nickname not found")) self.connection.gc_set_role(self.room_jid, who, 'none', reason or str()) @command + @documentation(_("Display names of all group chat occupants")) def names(self, verbose=False): - """ - Display names of all group chat occupants - """ get_contact = lambda nick: gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) nicks = gajim.contacts.get_nick_list(self.account, self.room_jid) @@ -247,16 +217,12 @@ class GroupChatCommands(CommonCommands): else: return ', '.join(nicks) - @command(raw=True) + @command('ignore', raw=True) + @documentation(_("Forbid an occupant to send you public or private messages")) def block(self, who): - """ - Forbid an occupant to send you public or private messages - """ self.on_block(None, who) - @command(raw=True) + @command('unignore', raw=True) + @documentation(_("Allow an occupant to send you public or private messages")) def unblock(self, who): - """ - Allow an occupant to send you public or privates messages - """ self.on_unblock(None, who) diff --git a/src/command_system/mapping.py b/src/command_system/mapping.py new file mode 100644 index 000000000..707866a20 --- /dev/null +++ b/src/command_system/mapping.py @@ -0,0 +1,360 @@ +# Copyright (C) 2009 red-agent +# +# 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 3 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, see . + +""" +The module contains routines to parse command arguments and map them to the +command handler's positonal and keyword arguments. + +Mapping is done in two stages: 1) parse arguments into positional arguments and +options; 2) adapt them to the specific command handler according to the command +properties. +""" + +import re +from types import BooleanType, UnicodeType +from types import TupleType, ListType +from operator import itemgetter + +from errors import DefinitionError, CommandError + +# Quite complex piece of regular expression logic to parse options and +# arguments. Might need some tweaking along the way. +ARG_PATTERN = re.compile(r'(\'|")?(?P(?(1).+?|\S+))(?(1)\1)') +OPT_PATTERN = re.compile(r'(?[\w-]+)(?:(?:=|\s)(\'|")?(?P(?(2)[^-]+?|[^-\s]+))(?(2)\2))?') + +# Option keys needs to be encoded to a specific encoding as Python does not +# allow to expand dictionary with raw unicode strings as keys from a **kwargs. +KEY_ENCODING = 'UTF-8' + +# Defines how complete representation of command usage (generated based on +# command handler argument specification) will be rendered. +USAGE_PATTERN = 'Usage: %s %s' + +def parse_arguments(arguments): + """ + Simple yet effective and sufficient in most cases parser which parses + command arguments and returns them as two lists. + + First list represents positional arguments as (argument, position), and + second representing options as (key, value, position) tuples, where position + is a (start, end) span tuple of where it was found in the string. + + Options may be given in --long or -short format. As --option=value or + --option value or -option value. Keys without values will get None as value. + + Arguments and option values that contain spaces may be given as 'one two + three' or "one two three"; that is between single or double quotes. + """ + args, opts = [], [] + + def intersects_opts((given_start, given_end)): + """ + Check if given span intersects with any of options. + """ + for key, value, (start, end) in opts: + if given_start >= start and given_end <= end: + return True + return False + + def intersects_args((given_start, given_end)): + """ + Check if given span intersects with any of arguments. + """ + for arg, (start, end) in args: + if given_start >= start and given_end <= end: + return True + return False + + for match in re.finditer(OPT_PATTERN, arguments): + if match: + key = match.group('key') + value = match.group('value') or None + position = match.span() + opts.append((key, value, position)) + + for match in re.finditer(ARG_PATTERN, arguments): + if match: + body = match.group('body') + position = match.span() + args.append((body, position)) + + # Primitive but sufficiently effective way of disposing of conflicted + # sectors. Remove any arguments that intersect with options. + for arg, position in args[:]: + if intersects_opts(position): + args.remove((arg, position)) + + # Primitive but sufficiently effective way of disposing of conflicted + # sectors. Remove any options that intersect with arguments. + for key, value, position in opts[:]: + if intersects_args(position): + opts.remove((key, value, position)) + + return args, opts + +def adapt_arguments(command, arguments, args, opts): + """ + Adapt args and opts got from the parser to a specific handler by means of + arguments specified on command definition. That is transform them to *args + and **kwargs suitable for passing to a command handler. + + Dashes (-) in the option names will be converted to underscores. So you can + map --one-more-option to a one_more_option=None. + + If the initial value of a keyword argument is a boolean (False in most + cases) - then this option will be treated as a switch, that is an option + which does not take an argument. If a switch is followed by an argument - + then this argument will be treated just like a normal positional argument. + + If the initial value of a keyword argument is a sequence, that is a tuple or + list - then a value of this option will be considered correct only if it is + present in the sequence. + """ + spec_args, spec_kwargs, var_args, var_kwargs = command.extract_specification() + norm_kwargs = dict(spec_kwargs) + + # Quite complex piece of neck-breaking logic to extract raw arguments if + # there is more, then one positional argument specified by the command. In + # case if it's just one argument which is the collector - this is fairly + # easy. But when it's more then one argument - the neck-breaking logic of + # how to retrieve residual arguments as a raw, all in one piece string, + # kicks in. + if command.raw: + if arguments: + spec_fix = 1 if command.source else 0 + spec_len = len(spec_args) - spec_fix + arguments_end = len(arguments) - 1 + + # If there are any optional arguments given they should be either an + # unquoted postional argument or part of the raw argument. So we + # find all optional arguments that can possibly be unquoted argument + # and append them as is to the args. + for key, value, (start, end) in opts[:spec_len]: + if value: + end -= len(value) + 1 + args.append((arguments[start:end], (start, end))) + args.append((value, (end, end + len(value) + 1))) + else: + args.append((arguments[start:end], (start, end))) + + # We need in-place sort here because after manipulations with + # options order of arguments might be wrong and we just can't have + # more complex logic to not let that happen. + args.sort(key=itemgetter(1)) + + if spec_len > 1: + try: + stopper, (start, end) = args[spec_len - 2] + except IndexError: + raise CommandError("Missing arguments", command) + + # The essential point of the whole play. After boundaries are + # being determined (supposingly correct) we separate raw part + # from the rest of arguments, which should be normally + # processed. + raw = arguments[end:] + raw = raw.strip() or None + + if not raw and not command.empty: + raise CommandError("Missing arguments", command) + + # Discard residual arguments and all of the options as raw + # command does not support options and if an option is given it + # is rather a part of a raw argument. + args = args[:spec_len - 1] + opts = [] + + args.append((raw, (end, arguments_end))) + else: + # Substitue all of the arguments with only one, which contain + # raw and unprocessed arguments as a string. And discard all the + # options, as raw command does not support them. + args = [(arguments, (0, arguments_end))] + opts = [] + else: + if command.empty: + args.append((None, (0, 0))) + else: + raise CommandError("Missing arguments", command) + + # The first stage of transforming options we have got to a format that can + # be used to associate them with declared keyword arguments. Substituting + # dashes (-) in their names with underscores (_). + for index, (key, value, position) in enumerate(opts): + if '-' in key: + opts[index] = (key.replace('-', '_'), value, position) + + # The second stage of transforming options to an associatable state. + # Expanding short, one-letter options to a verbose ones, if corresponding + # optin has been given. + if command.expand_short: + expanded = [] + for spec_key, spec_value in norm_kwargs.iteritems(): + letter = spec_key[0] if len(spec_key) > 1 else None + if letter and letter not in expanded: + for index, (key, value, position) in enumerate(opts): + if key == letter: + expanded.append(letter) + opts[index] = (spec_key, value, position) + break + + # Detect switches and set their values accordingly. If any of them carries a + # value - append it to args. + for index, (key, value, position) in enumerate(opts): + if isinstance(norm_kwargs.get(key), BooleanType): + opts[index] = (key, True, position) + if value: + args.append((value, position)) + + # Sorting arguments and options (just to be sure) in regarding to their + # positions in the string. + args.sort(key=itemgetter(1)) + opts.sort(key=itemgetter(2)) + + # Stripping down position information supplied with arguments and options as + # it won't be needed again. + args = map(lambda (arg, position): arg, args) + opts = map(lambda (key, value, position): (key, value), opts) + + # If command has extra option enabled - collect all extra arguments and pass + # them to a last positional argument command defines as a list. + if command.extra: + if not var_args: + spec_fix = 1 if not command.source else 2 + spec_len = len(spec_args) - spec_fix + extra = args[spec_len:] + args = args[:spec_len] + args.append(extra) + else: + raise DefinitionError("Can not have both, extra and *args") + + # Detect if positional arguments overlap keyword arguments. If so and this + # is allowed by command options - then map them directly to their options, + # so they can get propert further processings. + spec_fix = 1 if command.source else 0 + spec_len = len(spec_args) - spec_fix + if len(args) > spec_len: + if command.overlap: + overlapped = args[spec_len:] + args = args[:spec_len] + for arg, (spec_key, spec_value) in zip(overlapped, spec_kwargs): + opts.append((spec_key, arg)) + else: + raise CommandError("Excessive arguments", command) + + # Detect every switch and ensure it will not receive any arguments. + # Normally this does not happen unless overlapping is enabled. + for key, value in opts: + initial = norm_kwargs.get(key) + if isinstance(initial, BooleanType): + if not isinstance(value, BooleanType): + raise CommandError("%s: Switch can not take an argument" % key, command) + + # Detect every sequence constraint and ensure that if corresponding options + # are given - they contain proper values, within the constraint range. + for key, value in opts: + initial = norm_kwargs.get(key) + if isinstance(initial, (TupleType, ListType)): + if value not in initial: + raise CommandError("%s: Invalid argument" % key, command) + + # If argument to an option constrained by a sequence was not given - then + # it's value should be set to None. + for spec_key, spec_value in spec_kwargs: + if isinstance(spec_value, (TupleType, ListType)): + for key, value in opts: + if spec_key == key: + break + else: + opts.append((spec_key, None)) + + # We need to encode every keyword argument to a simple string, not the + # unicode one, because ** expansion does not support it. + for index, (key, value) in enumerate(opts): + if isinstance(key, UnicodeType): + opts[index] = (key.encode(KEY_ENCODING), value) + + # Inject the source arguments as a string as a first argument, if command + # has enabled the corresponding option. + if command.source: + args.insert(0, arguments) + + # Return *args and **kwargs in the form suitable for passing to a command + # handler and being expanded. + return tuple(args), dict(opts) + +def generate_usage(command, complete=True): + """ + Extract handler's arguments specification and wrap them in a human-readable + format usage information. If complete is given - then USAGE_PATTERN will be + used to render the specification completly. + """ + spec_args, spec_kwargs, var_args, var_kwargs = command.extract_specification() + + # Remove some special positional arguments from the specifiaction, but store + # their names so they can be used for usage info generation. + sp_source = spec_args.pop(0) if command.source else None + sp_extra = spec_args.pop() if command.extra else None + + kwargs = [] + letters = [] + + for key, value in spec_kwargs: + letter = key[0] + key = key.replace('_', '-') + + if isinstance(value, BooleanType): + value = str() + elif isinstance(value, (TupleType, ListType)): + value = '={%s}' % ', '.join(value) + else: + value = '=%s' % value + + if letter not in letters: + kwargs.append('-(-%s)%s%s' % (letter, key[1:], value)) + letters.append(letter) + else: + kwargs.append('--%s%s' % (key, value)) + + usage = str() + args = str() + + if command.raw: + spec_len = len(spec_args) - 1 + if spec_len: + args += ('<%s>' % ', '.join(spec_args[:spec_len])) + ' ' + args += ('(|%s|)' if command.empty else '|%s|') % spec_args[-1] + else: + if spec_args: + args += '<%s>' % ', '.join(spec_args) + if var_args or sp_extra: + args += (' ' if spec_args else str()) + '<<%s>>' % (var_args or sp_extra) + + usage += args + + if kwargs or var_kwargs: + if kwargs: + usage += (' ' if args else str()) + '[%s]' % ', '.join(kwargs) + if var_kwargs: + usage += (' ' if args else str()) + '[[%s]]' % var_kwargs + + # Native name will be the first one if it is included. Otherwise, names will + # be in the order they were specified. + if len(command.names) > 1: + names = '%s (%s)' % (command.first_name, ', '.join(command.names[1:])) + else: + names = command.first_name + + return USAGE_PATTERN % (names, usage) if complete else usage diff --git a/src/commands/custom.py b/src/commands/custom.py deleted file mode 100644 index 44a8ab073..000000000 --- a/src/commands/custom.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (C) 2009 red-agent -# -# 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 3 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, see . - -""" -This module contains examples of how to create your own commands by creating an -adhoc command processor. Each adhoc command processor should be hosted by one or -more which dispatch the real deal and droppped in to where it belongs. -""" - -from framework import command -from implementation import ChatCommands, PrivateChatCommands, GroupChatCommands - -class CustomCommonCommands(ChatCommands, PrivateChatCommands, GroupChatCommands): - """ - This adhoc processor will be hosted by a multiple processors which dispatch - commands from all, chat, private chat and group chat. So commands defined - here will be available to all of them. - """ - - DISPATCH = True - HOSTED_BY = ChatCommands, PrivateChatCommands, GroupChatCommands - - @command - def dance(self): - """ - First line of the doc string is called a description and will be - programmatically extracted. - - After that you can give more help, like explanation of the options. This - one will be programatically extracted and formatted too. After this one - there will be autogenerated (based on the method signature) usage - information appended. You can turn it off though, if you want. - """ - return "I can't dance, you stupid fuck, I'm just a command system! A cool one, though..." - -class CustomChatCommands(ChatCommands): - """ - This adhoc processor will be hosted by a ChatCommands processor which - dispatches commands from a chat. So commands defined here will be available - only to a chat. - """ - - DISPATCH = True - HOSTED_BY = ChatCommands - - @command - def sing(self): - return "Are you phreaking kidding me? Buy yourself a damn stereo..." - -class CustomPrivateChatCommands(PrivateChatCommands): - """ - This adhoc processor will be hosted by a PrivateChatCommands processor which - dispatches commands from a private chat. So commands defined here will be - available only to a private chat. - """ - - DISPATCH = True - HOSTED_BY = PrivateChatCommands - - @command - def make_coffee(self): - return "What do I look like, you ass? A coffee machine!?" - -class CustomGroupChatCommands(GroupChatCommands): - """ - This adhoc processor will be hosted by a GroupChatCommands processor which - dispatches commands from a group chat. So commands defined here will be - available only to a group chat. - """ - - DISPATCH = True - HOSTED_BY = GroupChatCommands - - @command - def fetch(self): - return "You should really buy yourself a dog and start torturing it instead of me..." diff --git a/src/commands/framework.py b/src/commands/framework.py deleted file mode 100644 index f8168f25c..000000000 --- a/src/commands/framework.py +++ /dev/null @@ -1,765 +0,0 @@ -# Copyright (C) 2009 red-agent -# -# 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 3 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, see . - -""" -Provides a tiny framework with simple, yet powerful and extensible architecture -to implement commands in a streight and flexible, declarative way. -""" - -import re -from types import FunctionType, UnicodeType, TupleType, ListType, BooleanType -from inspect import getargspec -from operator import itemgetter - -class InternalError(Exception): - pass - -class CommandError(Exception): - def __init__(self, message=None, command=None, name=None): - self.command = command - self.name = name - - if command: - self.name = command.first_name - - if message: - super(CommandError, self).__init__(message) - else: - super(CommandError, self).__init__() - -class Command(object): - - DOC_STRIP_PATTERN = re.compile(r'(?:^[ \t]+|\A\n)', re.MULTILINE) - DOC_FORMAT_PATTERN = re.compile(r'(?" % ', '.join(self.names) - - def __cmp__(self, other): - """ - Comparison is implemented based on a first name. - """ - return cmp(self.first_name, other.first_name) - - @property - def first_name(self): - return self.names[0] - - @property - def native_name(self): - return self.handler.__name__ - - def extract_doc(self): - """ - Extract handler's doc-string and transform it to a usable format. - """ - doc = self.handler.__doc__ or None - - if not doc: - return - - doc = re.sub(self.DOC_STRIP_PATTERN, str(), doc) - doc = re.sub(self.DOC_FORMAT_PATTERN, ' ', doc) - - return doc - - def extract_description(self): - """ - Extract handler's description (which is a first line of the doc). Try to - keep them simple yet meaningful. - """ - doc = self.extract_doc() - return doc.split('\n', 1)[0] if doc else None - - def extract_arg_spec(self): - names, var_args, var_kwargs, defaults = getargspec(self.handler) - - # Behavior of this code need to be checked. Might yield incorrect - # results on some rare occasions. - spec_args = names[:-len(defaults) if defaults else len(names)] - spec_kwargs = list(zip(names[-len(defaults):], defaults)) if defaults else {} - - # Removing self from arguments specification. Command handler should - # normally be an instance method. - if spec_args.pop(0) != 'self': - raise InternalError("First argument must be self") - - return spec_args, spec_kwargs, var_args, var_kwargs - - def extract_arg_usage(self, complete=True): - """ - Extract handler's arguments specification and wrap them in a - human-readable format. If complete is given - then ARG_USAGE_PATTERN - will be used to render it completly. - """ - spec_args, spec_kwargs, var_args, var_kwargs = self.extract_arg_spec() - - # Remove some special positional arguments from the specifiaction, but - # store their names so they can be used for usage info generation. - sp_source = spec_args.pop(0) if self.source else None - sp_extra = spec_args.pop() if self.extra else None - - kwargs = [] - letters = [] - - for key, value in spec_kwargs: - letter = key[0] - key = key.replace('_', '-') - - if isinstance(value, BooleanType): - value = str() - elif isinstance(value, (TupleType, ListType)): - value = '={%s}' % ', '.join(value) - else: - value = '=%s' % value - - if letter not in letters: - kwargs.append('-(-%s)%s%s' % (letter, key[1:], value)) - letters.append(letter) - else: - kwargs.append('--%s%s' % (key, value)) - - usage = str() - args = str() - - if self.raw: - spec_len = len(spec_args) - 1 - if spec_len: - args += ('<%s>' % ', '.join(spec_args[:spec_len])) + ' ' - args += ('(|%s|)' if self.empty else '|%s|') % spec_args[-1] - else: - if spec_args: - args += '<%s>' % ', '.join(spec_args) - if var_args or sp_extra: - args += (' ' if spec_args else str()) + '<<%s>>' % (var_args or sp_extra) - - usage += args - - if kwargs or var_kwargs: - if kwargs: - usage += (' ' if args else str()) + '[%s]' % ', '.join(kwargs) - if var_kwargs: - usage += (' ' if args else str()) + '[[%s]]' % var_kwargs - - # Native name will be the first one if it is included. Otherwise, names - # will be in the order they were specified. - if len(self.names) > 1: - names = '%s (%s)' % (self.first_name, ', '.join(self.names[1:])) - else: - names = self.first_name - - return usage if not complete else self.ARG_USAGE_PATTERN % (names, usage) - -class Dispatcher(type): - table = {} - hosted = {} - - def __init__(cls, name, bases, dct): - dispatchable = Dispatcher.check_if_dispatchable(bases, dct) - hostable = Dispatcher.check_if_hostable(bases, dct) - - cls.check_if_conformed(dispatchable, hostable) - - if Dispatcher.is_suitable(cls, dct): - Dispatcher.register_processor(cls) - - # Sanitize names even if processor is not suitable for registering, - # because it might be inherited by an another processor. - Dispatcher.sanitize_names(cls) - - super(Dispatcher, cls).__init__(name, bases, dct) - - @classmethod - def is_suitable(cls, proc, dct): - is_not_root = dct.get('__metaclass__') is not cls - to_be_dispatched = bool(dct.get('DISPATCH')) - return is_not_root and to_be_dispatched - - @classmethod - def check_if_dispatchable(cls, bases, dct): - dispatcher = dct.get('DISPATCHED_BY') - if not dispatcher: - return False - if dispatcher not in bases: - raise InternalError("Should be dispatched by the same processor it inherits from") - return True - - @classmethod - def check_if_hostable(cls, bases, dct): - hosters = dct.get('HOSTED_BY') - if not hosters: - return False - if not isinstance(hosters, (TupleType, ListType)): - hosters = (hosters,) - for hoster in hosters: - if hoster not in bases: - raise InternalError("Should be hosted by the same processors it inherits from") - return True - - @classmethod - def check_if_conformed(cls, dispatchable, hostable): - if dispatchable and hostable: - raise InternalError("Processor can not be dispatchable and hostable at the same time") - - @classmethod - def register_processor(cls, proc): - cls.table[proc] = {} - inherit = proc.__dict__.get('INHERIT') - - if 'HOSTED_BY' in proc.__dict__: - cls.register_adhocs(proc) - - commands = cls.traverse_commands(proc, inherit) - cls.register_commands(proc, commands) - - @classmethod - def sanitize_names(cls, proc): - inherit = proc.__dict__.get('INHERIT') - commands = cls.traverse_commands(proc, inherit) - for key, command in commands: - if not proc.SAFE_NAME_SCAN_PATTERN.match(key): - setattr(proc, proc.SAFE_NAME_SUBS_PATTERN % key, command) - try: - delattr(proc, key) - except AttributeError: - pass - - @classmethod - def traverse_commands(cls, proc, inherit=True): - keys = dir(proc) if inherit else proc.__dict__.iterkeys() - for key in keys: - value = getattr(proc, key) - if isinstance(value, Command): - yield key, value - - @classmethod - def register_commands(cls, proc, commands): - for key, command in commands: - for name in command.names: - name = proc.prepare_name(name) - if name not in cls.table[proc]: - cls.table[proc][name] = command - else: - raise InternalError("Command with name %s already exists" % name) - @classmethod - def register_adhocs(cls, proc): - hosters = proc.HOSTED_BY - if not isinstance(hosters, (TupleType, ListType)): - hosters = (hosters,) - for hoster in hosters: - if hoster in cls.hosted: - cls.hosted[hoster].append(proc) - else: - cls.hosted[hoster] = [proc] - - @classmethod - def retrieve_command(cls, proc, name): - command = cls.table[proc.DISPATCHED_BY].get(name) - if command: - return command - if proc.DISPATCHED_BY in cls.hosted: - for adhoc in cls.hosted[proc.DISPATCHED_BY]: - command = cls.table[adhoc].get(name) - if command: - return command - - @classmethod - def list_commands(cls, proc): - commands = dict(cls.traverse_commands(proc.DISPATCHED_BY)) - if proc.DISPATCHED_BY in cls.hosted: - for adhoc in cls.hosted[proc.DISPATCHED_BY]: - inherit = adhoc.__dict__.get('INHERIT') - commands.update(dict(cls.traverse_commands(adhoc, inherit))) - return commands.values() - -class CommandProcessor(object): - """ - A base class for a drop-in command processor which you can drop (make your - class to inherit from it) in any of your classes to support commands. In - order to get it done you need to make your own processor, inheriter from - CommandProcessor and then drop it in. Don't forget about few important steps - described below. - - Every command in the processor (normally) will gain full access through self - to an object you are adding commands to. - - Your subclass, which will contain commands should define in its body - DISPATCH = True in order to be included in the dispatching table. - - Every class you will drop the processor in should define DISPATCHED_BY set - to the same processor you are inheriting from. - - Names of the commands after preparation stuff id done will be sanitized - (based on SAFE_NAME_SCAN_PATTERN and SAFE_NAME_SUBS_PATTERN) in order not to - interfere with the methods defined in a class you will drop a processor in. - - If you want to create an adhoc processor (then one that parasites on the - other one (the host), so it does not have to be included directly into - whatever includes the host) you need to inherit you processor from the host - and set HOSTED_BY to that host. - - INHERIT controls whether commands inherited from base classes (which could - include other processors) will be registered or not. This is disabled - by-default because it leads to unpredictable consequences when used in adhoc - processors which inherit from more then one processor or has such processors - in its inheritance tree. In that case - encapsulation is being broken and - some (all) commands are shared between non-related processors. - """ - __metaclass__ = Dispatcher - - SAFE_NAME_SCAN_PATTERN = re.compile(r'_(?P\w+)_') - SAFE_NAME_SUBS_PATTERN = '_%s_' - - # Quite complex piece of regular expression logic. - ARG_PATTERN = re.compile(r'(\'|")?(?P(?(1).+?|\S+))(?(1)\1)') - OPT_PATTERN = re.compile(r'(?[\w-]+)(?:(?:=|\s)(\'|")?(?P(?(2)[^-]+?|[^-\s]+))(?(2)\2))?') - - COMMAND_PREFIX = '/' - CASE_SENSITIVE_COMMANDS = False - - ARG_ENCODING = 'utf8' - - def __getattr__(self, name): - """ - This allows to reach and directly (internally) call commands which are - defined in (other) adhoc processors. - """ - command_name = self.SAFE_NAME_SCAN_PATTERN.match(name) - if command_name: - command = self.retrieve_command(command_name.group('name')) - if command: - return command - raise AttributeError(name) - - @classmethod - def prepare_name(cls, name): - return name if cls.CASE_SENSITIVE_COMMANDS else name.lower() - - @classmethod - def retrieve_command(cls, name): - name = cls.prepare_name(name) - command = Dispatcher.retrieve_command(cls, name) - if not command: - raise CommandError("Command does not exist", name=name) - return command - - @classmethod - def list_commands(cls): - commands = Dispatcher.list_commands(cls) - return sorted(set(commands)) - - @classmethod - def parse_command_arguments(cls, arguments): - """ - Simple yet effective and sufficient in most cases parser which parses - command arguments and returns them as two lists. First represents - positional arguments as (argument, position), and second representing - options as (key, value, position) tuples, where position is a (start, - end) span tuple of where it was found in the string. - - The format of the input arguments should be: - <> [-(-o)ption=value1, -(-a)nother=value2] [[extra_options]] - - Options may be given in --long or -short format. As --option=value or - --option value or -option value. Keys without values will get True as - value. Arguments and option values that contain spaces may be given as - 'one two three' or "one two three"; that is between single or double - quotes. - """ - args, opts = [], [] - - def intersects_opts((given_start, given_end)): - """ - Check if something intersects with boundaries of any parsed option. - """ - for key, value, (start, end) in opts: - if given_start >= start and given_end <= end: - return True - return False - - def intersects_args((given_start, given_end)): - """ - Check if something intersects with boundaries of any parsed argument. - """ - for arg, (start, end) in args: - if given_start >= start and given_end <= end: - return True - return False - - for match in re.finditer(cls.OPT_PATTERN, arguments): - if match: - key = match.group('key') - value = match.group('value') or None - position = match.span() - opts.append((key, value, position)) - - for match in re.finditer(cls.ARG_PATTERN, arguments): - if match and not intersects_opts(match.span()): - body = match.group('body') - position = match.span() - args.append((body, position)) - - # In rare occasions quoted options are being captured, while they should - # not be. This fixes the problem by finding options which intersect with - # arguments and removing them. - for key, value, position in opts[:]: - if intersects_args(position): - opts.remove((key, value, position)) - - return args, opts - - @classmethod - def adapt_command_arguments(cls, command, arguments, args, opts): - """ - Adapts args and opts got from the parser to a specific handler by means - of arguments specified on command definition. That is transforms them to - *args and **kwargs suitable for passing to a command handler. - - Extra arguments which are not considered extra (or optional) - will be - passed as if they were value for keywords, in the order keywords are - defined and printed in usage. - - Dashes (-) in the option names will be converted to underscores. So you - can map --one-more-option to a one_more_option=None. - - If initial value of a keyword argument is a boolean (False in most - cases) then this option will be treated as a switch, that is an option - which does not take an argument. Argument preceded by a switch will be - treated just like a normal positional argument. - - If keyword argument's initial value is a sequence (tuple or a string) - then possible values of the option will be restricted to one of the - values given by the sequence. - """ - spec_args, spec_kwargs, var_args, var_kwargs = command.extract_arg_spec() - norm_kwargs = dict(spec_kwargs) - - # Quite complex piece of neck-breaking logic to extract raw arguments if - # there is more, then one positional argument specified by the command. - # In case if it's just one argument which is the collector this is - # fairly easy. But when it's more then one argument - the neck-breaking - # logic of how to retrieve residual arguments as a raw, all in one piece - # string, kicks on. - if command.raw: - if spec_kwargs or var_args or var_kwargs: - raise InternalError("Raw commands should define only positional arguments") - - if arguments: - spec_fix = 1 if command.source else 0 - spec_len = len(spec_args) - spec_fix - arguments_end = len(arguments) - 1 - - # If there are any optional arguments given they should be - # either an unquoted postional argument or part of the raw - # argument. So we find all optional arguments that can possibly - # be unquoted argument and append them as is to the args. - for key, value, (start, end) in opts[:spec_len]: - if value: - end -= len(value) + 1 - args.append((arguments[start:end], (start, end))) - args.append((value, (end, end + len(value) + 1))) - else: - args.append((arguments[start:end], (start, end))) - - # We need in-place sort here because after manipulations with - # options order of arguments might be wrong and we just can't - # have more complex logic to not let that happen. - args.sort(key=itemgetter(1)) - - if spec_len > 1: - try: - stopper, (start, end) = args[spec_len - 2] - except IndexError: - raise CommandError("Missing arguments", command) - - raw = arguments[end:] - raw = raw.strip() or None - - if not raw and not command.empty: - raise CommandError("Missing arguments", command) - - # Discard residual arguments and all of the options as raw - # command does not support options and if an option is given - # it is rather a part of a raw argument. - args = args[:spec_len - 1] - opts = [] - - args.append((raw, (end, arguments_end))) - elif spec_len == 1: - args = [(arguments, (0, arguments_end))] - opts = [] - else: - raise InternalError("Raw command must define a collector") - else: - if command.empty: - args.append((None, (0, 0))) - else: - raise CommandError("Missing arguments", command) - - # The first stage of transforming options we have got to a format that - # can be used to associate them with declared keyword arguments. - # Substituting dashes (-) in their names with underscores (_). - for index, (key, value, position) in enumerate(opts): - if '-' in key: - opts[index] = (key.replace('-', '_'), value, position) - - # The second stage of transforming options to an associatable state. - # Expanding short, one-letter options to a verbose ones, if - # corresponding optin has been given. - if command.expand_short: - expanded = [] - for spec_key, spec_value in norm_kwargs.iteritems(): - letter = spec_key[0] if len(spec_key) > 1 else None - if letter and letter not in expanded: - for index, (key, value, position) in enumerate(opts): - if key == letter: - expanded.append(letter) - opts[index] = (spec_key, value, position) - break - - # Detect switches and set their values accordingly. If any of them - # carries a value - append it to args. - for index, (key, value, position) in enumerate(opts): - if isinstance(norm_kwargs.get(key), BooleanType): - opts[index] = (key, True, position) - if value: - args.append((value, position)) - - # Sorting arguments and options (just to be sure) in regarding to their - # positions in the string. - args.sort(key=itemgetter(1)) - opts.sort(key=itemgetter(2)) - - # Stripping down position information supplied with arguments and options as it - # won't be needed again. - args = map(lambda (arg, position): arg, args) - opts = map(lambda (key, value, position): (key, value), opts) - - # If command has extra option enabled - collect all extra arguments and - # pass them to a last positional argument command defines as a list. - if command.extra: - if not var_args: - spec_fix = 1 if not command.source else 2 - spec_len = len(spec_args) - spec_fix - extra = args[spec_len:] - args = args[:spec_len] - args.append(extra) - else: - raise InternalError("Can not have both, extra and *args") - - # Detect if positional arguments overlap keyword arguments. If so and - # this is allowed by command options - then map them directly to their - # options, so they can get propert further processings. - spec_fix = 1 if command.source else 0 - spec_len = len(spec_args) - spec_fix - if len(args) > spec_len: - if command.overlap: - overlapped = args[spec_len:] - args = args[:spec_len] - for arg, (spec_key, spec_value) in zip(overlapped, spec_kwargs): - opts.append((spec_key, arg)) - else: - raise CommandError("Excessive arguments", command) - - # Detect every contraint sequences and ensure that if corresponding - # options are given - they contain proper values, within constraint - # range. - for key, value in opts: - initial = norm_kwargs.get(key) - if isinstance(initial, (TupleType, ListType)) and value not in initial: - raise CommandError("Wrong argument", command) - - # Detect every switch and ensure it will not receive any arguments. - # Normally this does not happen unless overlapping is enabled. - for key, value in opts: - initial = norm_kwargs.get(key) - if isinstance(initial, BooleanType) and not isinstance(value, BooleanType): - raise CommandError("Switches do not take arguments", command) - - # We need to encode every keyword argument to a simple string, not the - # unicode one, because ** expansion does not support it. - for index, (key, value) in enumerate(opts): - if isinstance(key, UnicodeType): - opts[index] = (key.encode(cls.ARG_ENCODING), value) - - # Inject the source arguments as a string as a first argument, if - # command has enabled the corresponding option. - if command.source: - args.insert(0, arguments) - - # Return *args and **kwargs in the form suitable for passing to a - # command handlers and being expanded. - return tuple(args), dict(opts) - - def process_as_command(self, text): - """ - Try to process text as a command. Returns True if it is a command and - False if it is not. - """ - if not text.startswith(self.COMMAND_PREFIX): - return False - - body = text[len(self.COMMAND_PREFIX):] - body = body.strip() - - parts = body.split(' ', 1) - name, arguments = parts if len(parts) > 1 else (parts[0], None) - - flag = self.looks_like_command(body, name, arguments) - if flag is not None: - return flag - - self.execute_command(text, name, arguments) - - return True - - def execute_command(self, text, name, arguments): - command = self.retrieve_command(name) - - args, opts = self.parse_command_arguments(arguments) if arguments else ([], []) - args, kwargs = self.adapt_command_arguments(command, arguments, args, opts) - - if self.command_preprocessor(name, command, arguments, args, kwargs): - return - value = command(self, *args, **kwargs) - self.command_postprocessor(name, command, arguments, args, kwargs, value) - - def command_preprocessor(self, name, command, arguments, args, kwargs): - """ - Redefine this method in the subclass to execute custom code before - command gets executed. If returns True then command execution will be - interrupted and command will not be executed. - """ - pass - - def command_postprocessor(self, name, command, arguments, args, kwargs, output): - """ - Redefine this method in the subclass to execute custom code after - command gets executed. - """ - pass - - def looks_like_command(self, text, name, arguments): - """ - This hook is being called before any processing, but after it was - determined that text looks like a command. If returns non None value - - then further processing will be interrupted and that value will be - used to return from process_as_command. - """ - pass - -def command(*names, **kwargs): - """ - A decorator which provides a declarative way of defining commands. - - You can specify a set of names by which you can call the command. If names - is empty - then the name of the command will be set to native one (extracted - from the handler name). - - If include_native=True argument is given and names is non-empty - then - native name will be added as well. - - If usage=True is given - then handler's doc will be appended with an - auto-generated usage info. - - If source=True is given - then the first positional argument of the command - handler will receive a string with a raw and unprocessed source arguments. - - If raw=True is given - then command should define only one argument to - which all raw and unprocessed source arguments will be given. - - If empty=True is given - then when raw=True is set and command receives no - arguments - an exception will be raised. - - If extra=True is given - then last positional argument will receive every - extra positional argument that will be given to a command. This is an - analogue to specifing *args, but the latter one should be used in simplest - cases only because of some Python limitations on this - arguments can't be - mapped correctly when there are keyword arguments present. - - If overlap=True is given - then if extra=False and there is extra arguments - given to the command - they will be mapped as if they were values for the - keyword arguments, in the order they are defined. - - If expand_short=True is given - then if command receives one-letter - options (like -v or -f) they will be expanded to a verbose ones (like - --verbose or --file) if the latter are defined as a command optional - arguments. Expansion is made on a first-letter comparison basis. If more - then one long option with the same first letter defined - only first one - will be used in expansion. - """ - names = list(names) - include_native = kwargs.get('include_native', True) - - usage = kwargs.get('usage', True) - source = kwargs.get('source', False) - raw = kwargs.get('raw', False) - extra = kwargs.get('extra', False) - overlap = kwargs.get('overlap', False) - empty = kwargs.get('empty', False) - expand_short = kwargs.get('expand_short', True) - - if extra and overlap: - raise InternalError("Extra and overlap options can not be used together") - - def decorator(handler): - command = Command(handler, usage, source, raw, extra, overlap, empty, expand_short) - - # Extract and inject native name while making sure it is going to be the - # first one in the list. - if not names or include_native: - names.insert(0, command.native_name) - command.names = tuple(names) - - return command - - # Workaround if we are getting called without parameters. Keep in mind that - # in that case - first item in the names will be the handler. - if len(names) == 1 and isinstance(names[0], FunctionType): - return decorator(names.pop()) - - return decorator diff --git a/src/common/config.py b/src/common/config.py index f2277f93e..7e8342778 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -90,17 +90,23 @@ class Config: 'mood_iconset': [ opt_str, DEFAULT_MOOD_ICONSET, '', True ], 'activity_iconset': [ opt_str, DEFAULT_ACTIVITY_ICONSET, '', True ], 'use_transports_iconsets': [ opt_bool, True, '', True ], - 'inmsgcolor': [ opt_color, '#a34526', '', True ], - 'outmsgcolor': [ opt_color, '#164e6f', '', True ], - 'statusmsgcolor': [ opt_color, '#1eaa1e', '', True ], + 'inmsgcolor': [ opt_color, '#a40000', _('Incoming nickname color.'), True ], + 'outmsgcolor': [ opt_color, '#3465a4', _('Outgoing nickname color.'), True ], + 'inmsgtxtcolor': [ opt_color, '', _('Incoming text color.'), True ], + 'outmsgtxtcolor': [ opt_color, '#555753', _('Outgoing text color.'), True ], + 'statusmsgcolor': [ opt_color, '#4e9a06', _('Status message text color.'), True ], 'markedmsgcolor': [ opt_color, '#ff8080', '', True ], - 'urlmsgcolor': [ opt_color, '#0000ff', '', True ], + 'urlmsgcolor': [ opt_color, '#204a87', '', True ], + 'inmsgfont': [ opt_str, '', _('Incoming nickname font.'), True ], + 'outmsgfont': [ opt_str, '', _('Outgoing nickname font.'), True ], + 'inmsgtxtfont': [ opt_str, '', _('Incoming text font.'), True ], + 'outmsgtxtfont': [ opt_str, '', _('Outgoing text font.'), True ], + 'statusmsgfont': [ opt_str, '', _('Status message text font.'), True ], 'collapsed_rows': [ opt_str, '', _('List (space separated) of rows (accounts and groups) that are collapsed.'), True ], 'roster_theme': [ opt_str, _('default'), '', True ], 'mergeaccounts': [ opt_bool, False, '', True ], 'sort_by_show_in_roster': [ opt_bool, True, '', True ], 'sort_by_show_in_muc': [ opt_bool, False, '', True ], - 'enable_zeroconf': [opt_bool, False, _('Enable link-local/zeroconf messaging')], 'use_speller': [ opt_bool, False, ], 'ignore_incoming_xhtml': [ opt_bool, False, ], 'speller_language': [ opt_str, '', _('Language used by speller')], @@ -226,7 +232,7 @@ class Config: 'log_contact_status_changes': [opt_bool, False], 'just_connected_bg_color': [opt_str, '#adc3c6', _('Background color of contacts when they just signed in.')], 'just_disconnected_bg_color': [opt_str, '#ab6161', _('Background color of contacts when they just signed out.')], - 'restored_messages_color': [opt_str, 'grey'], + 'restored_messages_color': [opt_color, '#555753'], 'restored_messages_small': [opt_bool, True, _('If True, restored messages will use a smaller font than the default one.')], 'hide_avatar_of_transport': [opt_bool, False, _('Don\'t show avatar for the transport itself.')], 'roster_window_skip_taskbar': [opt_bool, False, _('Don\'t show roster in the system taskbar.')], @@ -245,7 +251,7 @@ class Config: 'chat_merge_consecutive_nickname': [opt_bool, False, _('In a chat, show the nickname at the beginning of a line only when it\'s not the same person talking than in previous message.')], 'chat_merge_consecutive_nickname_indent': [opt_str, ' ', _('Indentation when using merge consecutive nickname.')], 'use_smooth_scrolling': [opt_bool, True, _('Smooth scroll message in conversation window')], - 'gc_nicknames_colors': [ opt_str, '#a34526:#c000ff:#0012ff:#388a99:#045723:#7c7c7c:#ff8a00:#94452d:#244b5a:#32645a', _('List of colors, separated by ":", that will be used to color nicknames in group chats.'), True ], + 'gc_nicknames_colors': [ opt_str, '#4e9a06:#f57900:#ce5c00:#3465a4:#204a87:#75507b:#5c3566:#c17d11:#8f5902:#ef2929:#cc0000:#a40000', _('List of colors, separated by ":", that will be used to color nicknames in group chats.'), True ], 'ctrl_tab_go_to_next_composing': [opt_bool, True, _('Ctrl-Tab go to next composing tab when none is unread.')], 'confirm_metacontacts': [ opt_str, '', _('Should we show the confirm metacontacts creation dialog or not? Empty string means we never show the dialog.')], 'confirm_block': [ opt_str, '', _('Should we show the confirm block contact dialog or not? Empty string means we never show the dialog.')], @@ -264,6 +270,7 @@ class Config: 'latex_png_dpi': [opt_str, '108',_('Change the value to change the size of latex formulas displayed. The higher is larger.') ], 'uri_schemes': [opt_str, 'aaa aaas acap cap cid crid data dav dict dns fax file ftp go gopher h323 http https icap im imap info ipp iris iris.beep iris.xpc iris.xpcs iris.lwz ldap mid modem msrp msrps mtqp mupdate news nfs nntp opaquelocktoken pop pres rtsp service shttp sip sips snmp soap.beep soap.beeps tag tel telnet tftp thismessage tip tv urn vemmi xmlrpc.beep xmlrpc.beeps z39.50r z39.50s about cvs daap ed2k feed fish git iax2 irc ircs ldaps magnet mms rsync ssh svn sftp smb webcal', _('Valid uri schemes. Only schemes in this list will be accepted as "real" uri. (mailto and xmpp are handled separately)'), True], 'ask_offline_status_on_connection': [ opt_bool, False, _('Ask offline status message to all offline contacts when connection to an accoutn is established. WARNING: This causes a lot of requests to be sent!') ], + 'shell_like_completion': [ opt_bool, False, _('If True, completion in groupchats will be like a shell auto-completion')], } __options_per_key = { @@ -287,7 +294,7 @@ class Config: 'restore_last_status': [ opt_bool, False, _('If enabled, restore the last status that was used.') ], 'autoreconnect': [ opt_bool, True ], 'autoauth': [ opt_bool, False, _('If True, Contacts requesting authorization will be automatically accepted.')], - 'active': [ opt_bool, True], + 'active': [ opt_bool, True, _('If False, this account will be disabled and will not appear in roster window.'), True], 'proxy': [ opt_str, '', '', True ], 'keyid': [ opt_str, '', '', True ], 'gpg_sign_presence': [ opt_bool, True, _('If disabled, don\'t sign presences with GPG key, even if GPG is configured.') ], diff --git a/src/common/connection.py b/src/common/connection.py index 6485cd825..39a0cc5ca 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -39,6 +39,7 @@ import operator import time import locale +import hmac try: randomsource = random.SystemRandom() @@ -190,6 +191,7 @@ class Connection(ConnectionHandlers): self.vcard_supported = False self.private_storage_supported = True self.streamError = '' + self.secret_hmac = str(random.random())[2:] # END __init__ def put_event(self, ev): @@ -1604,6 +1606,8 @@ class Connection(ConnectionHandlers): self.connection.send(iq) def _request_bookmarks_xml(self): + if not self.connection: + return iq = common.xmpp.Iq(typ='get') iq2 = iq.addChild(name='query', namespace=common.xmpp.NS_PRIVATE) iq2.addChild(name='storage', namespace='storage:bookmarks') @@ -1754,27 +1758,6 @@ class Connection(ConnectionHandlers): if show == 'invisible': # Never join a room when invisible return - p = common.xmpp.Presence(to = '%s/%s' % (room_jid, nick), - show = show, status = self.status) - if gajim.config.get('send_sha_in_gc_presence'): - p = self.add_sha(p) - self.add_lang(p) - if not change_nick: - t = p.setTag(common.xmpp.NS_MUC + ' x') - last_date = gajim.logger.get_last_date_that_has_logs(room_jid, - self.name, is_room=True) - if last_date is None: - last_date = time.time() - gajim.config.get( - 'muc_restore_timeout') * 60 - else: - last_time = min(last_date, time.time() - gajim.config.get( - 'muc_restore_timeout') * 60) - last_date = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(last_date)) - t.setTag('history', {'maxstanzas': gajim.config.get( - 'muc_restore_lines'), 'since': last_date}) - if password: - t.setTagData('password', password) - self.connection.send(p) # last date/time in history to avoid duplicate if room_jid not in self.last_history_time: @@ -1787,12 +1770,37 @@ class Connection(ConnectionHandlers): if last_log is None: # Not in special table, get it from messages DB last_log = gajim.logger.get_last_date_that_has_logs(room_jid, - is_room = True) + is_room=True) # Create self.last_history_time[room_jid] even if not logging, # could be used in connection_handlers if last_log is None: last_log = 0 - self.last_history_time[room_jid]= last_log + self.last_history_time[room_jid] = last_log + + p = common.xmpp.Presence(to='%s/%s' % (room_jid, nick), + show=show, status=self.status) + h = hmac.new(self.secret_hmac, room_jid).hexdigest()[:6] + id_ = self.connection.getAnID() + id_ = 'gajim_muc_' + id_ + '_' + h + p.setID(id_) + if gajim.config.get('send_sha_in_gc_presence'): + p = self.add_sha(p) + self.add_lang(p) + if not change_nick: + t = p.setTag(common.xmpp.NS_MUC + ' x') + last_date = self.last_history_time[room_jid] + if last_date == 0: + last_date = time.time() - gajim.config.get( + 'muc_restore_timeout') * 60 + else: + last_time = min(last_date, time.time() - gajim.config.get( + 'muc_restore_timeout') * 60) + last_date = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(last_date)) + t.setTag('history', {'maxstanzas': gajim.config.get( + 'muc_restore_lines'), 'since': last_date}) + if password: + t.setTagData('password', password) + self.connection.send(p) def send_gc_message(self, jid, msg, xhtml = None): if not self.connection: @@ -1841,6 +1849,10 @@ class Connection(ConnectionHandlers): xmpp_show = helpers.get_xmpp_show(show) p = common.xmpp.Presence(to = '%s/%s' % (jid, nick), typ = ptype, show = xmpp_show, status = status) + h = hmac.new(self.secret_hmac, jid).hexdigest()[:6] + id_ = self.connection.getAnID() + id_ = 'gajim_muc_' + id_ + '_' + h + p.setID(id_) if gajim.config.get('send_sha_in_gc_presence') and show != 'offline': p = self.add_sha(p, ptype != 'unavailable') self.add_lang(p) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index b03915809..d1f0d0c17 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -34,6 +34,7 @@ import socket import sys import operator import hashlib +import hmac from time import (altzone, daylight, gmtime, localtime, mktime, strftime, time as time_time, timezone, tzname) @@ -359,7 +360,7 @@ class ConnectionBytestream: file_props['hash'] = hash_id return - def _connect_error(self, to, _id, sid, code = 404): + def _connect_error(self, to, _id, sid, code=404): ''' cb, when there is an error establishing BS connection, or when connection is rejected''' if not self.connection or self.connected < 2: @@ -879,7 +880,9 @@ class ConnectionDisco: is_muc = True identities.append(attr) elif i.getName() == 'feature': - features.append(i.getAttr('var')) + var = i.getAttr('var') + if var: + features.append(var) elif i.getName() == 'x' and i.getNamespace() == common.xmpp.NS_DATA: data.append(common.xmpp.DataForm(node=i)) jid = helpers.get_full_jid_from_iq(iq_obj) @@ -1414,12 +1417,11 @@ sent a message to.''' if chat_sessions: # return the session that we last sent a message in - return sorted(chat_sessions, - key=operator.attrgetter("last_send"))[-1] + return sorted(chat_sessions, key=operator.attrgetter("last_send"))[-1] else: return None - def find_controlless_session(self, jid): + def find_controlless_session(self, jid, resource=None): '''find an active session that doesn't have a control attached''' try: @@ -1431,6 +1433,9 @@ sent a message to.''' orphaned = [s for s in chat_sessions if not s.control] + if resource: + orphaned = [s for s in orphaned if s.resource == resource] + return orphaned[0] except (KeyError, IndexError): return None @@ -1845,7 +1850,6 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, self.dispatch('GMAIL_NOTIFY', (jid, newmsgs, gmail_messages_list)) raise common.xmpp.NodeProcessed - def _rosterItemExchangeCB(self, con, msg): ''' XEP-0144 Roster Item Echange ''' exchange_items_list = {} @@ -1870,8 +1874,9 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, exchange_items_list[jid] = [] exchange_items_list[jid].append(name) exchange_items_list[jid].append(groups) - self.dispatch('ROSTERX', (action, exchange_items_list, jid_from)) - + if exchange_items_list: + self.dispatch('ROSTERX', (action, exchange_items_list, jid_from)) + raise common.xmpp.NodeProcessed def _messageCB(self, con, msg): '''Called when we receive a message''' @@ -2013,7 +2018,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if msg.getTag('request', namespace=common.xmpp.NS_RECEIPTS) \ and gajim.config.get_per('accounts', self.name, 'answer_receipts') and ((contact and contact.sub \ - not in (u'to', u'none')) or gc_contact): + not in (u'to', u'none')) or gc_contact) and mtype != 'error': receipt = common.xmpp.Message(to=frm, typ='chat') receipt.setID(msg.getID()) receipt.setTag('received', @@ -2191,7 +2196,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, ptype = prs.getType() if ptype == 'available': ptype = None - rfc_types = ('unavailable', 'error', 'subscribe', 'subscribed', 'unsubscribe', 'unsubscribed') + rfc_types = ('unavailable', 'error', 'subscribe', 'subscribed', + 'unsubscribe', 'unsubscribed') if ptype and not ptype in rfc_types: ptype = None log.debug('PresenceCB: %s' % ptype) @@ -2211,6 +2217,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, return jid_stripped, resource = gajim.get_room_and_nick_from_fjid(who) timestamp = None + id_ = prs.getID() is_gc = False # is it a GC presence ? sigTag = None ns_muc_user_x = None @@ -2250,6 +2257,13 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if self.connection.getRoster().getItem(agent): # to be sure it's a transport contact transport_auto_auth = True + if not is_gc and id_ and id_.startswith('gajim_muc_') and \ + ptype == 'error': + # Error presences may not include sent stanza, so we don't detect it's + # a muc preence. So detect it by ID + h = hmac.new(self.secret_hmac, jid_stripped).hexdigest()[:6] + if id_.split('_')[-1] == h: + is_gc = True status = prs.getStatus() or '' show = prs.getShow() if not show in gajim.SHOW_LIST: @@ -2273,32 +2287,55 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if is_gc: if ptype == 'error': - errmsg = prs.getError() + errcon = prs.getError() + errmsg = prs.getErrorMsg() errcode = prs.getErrorCode() room_jid, nick = gajim.get_room_and_nick_from_fjid(who) - if errcode == '502': # Internal Timeout: + + gc_control = gajim.interface.msg_win_mgr.get_gc_control(room_jid, + self.name) + + # If gc_control is missing - it may be minimized. Try to get it from + # there. If it's not there - then it's missing anyway and will + # remain set to None. + if gc_control is None: + minimized = gajim.interface.minimized_controls[self.name] + gc_control = minimized.get(room_jid) + + if errcode == '502': + # Internal Timeout: self.dispatch('NOTIFY', (jid_stripped, 'error', errmsg, resource, prio, keyID, timestamp, None)) - elif errcode == '401': # password required to join + elif (errcode == '503'): + # maximum user number reached + self.dispatch('ERROR', (_('Unable to join group chat'), + _('Maximum number of users for %s has been reached') % \ + room_jid)) + elif (errcode == '401') or (errcon == 'not-authorized'): + # password required to join self.dispatch('GC_PASSWORD_REQUIRED', (room_jid, nick)) - elif errcode == '403': # we are banned + elif (errcode == '403') or (errcon == 'forbidden'): + # we are banned self.dispatch('ERROR', (_('Unable to join group chat'), _('You are banned from group chat %s.') % room_jid)) - elif errcode == '404': # group chat does not exist - self.dispatch('ERROR', (_('Unable to join group chat'), - _('Group chat %s does not exist.') % room_jid)) - elif errcode == '405': + elif (errcode == '404') or (errcon == 'item-not-found'): + if gc_control is None or gc_control.autorejoin is None: + # group chat does not exist + self.dispatch('ERROR', (_('Unable to join group chat'), + _('Group chat %s does not exist.') % room_jid)) + elif (errcode == '405') or (errcon == 'not-allowed'): self.dispatch('ERROR', (_('Unable to join group chat'), _('Group chat creation is restricted.'))) - elif errcode == '406': + elif (errcode == '406') or (errcon == 'not-acceptable'): self.dispatch('ERROR', (_('Unable to join group chat'), _('Your registered nickname must be used in group chat %s.') \ % room_jid)) - elif errcode == '407': + elif (errcode == '407') or (errcon == 'registration-required'): self.dispatch('ERROR', (_('Unable to join group chat'), _('You are not in the members list in groupchat %s.') % \ room_jid)) - elif errcode == '409': # nick conflict + elif (errcode == '409') or (errcon == 'conflict'): + # nick conflict room_jid = gajim.get_room_from_fjid(who) self.dispatch('ASK_NEW_NICK', (room_jid,)) else: # print in the window the error @@ -2428,19 +2465,22 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, self.dispatch('NOTIFY', (jid_stripped, 'error', errmsg, resource, prio, keyID, timestamp, None)) - if ptype == 'unavailable' and jid_stripped in self.sessions: - # automatically terminate sessions that they haven't sent a thread ID - # in, only if other part support thread ID - for sess in self.sessions[jid_stripped].values(): - if not sess.received_thread_id: - contact = gajim.contacts.get_contact(self.name, jid_stripped) + if ptype == 'unavailable': + for jid in [jid_stripped, who]: + if jid not in self.sessions: + continue + # automatically terminate sessions that they haven't sent a thread + # ID in, only if other part support thread ID + for sess in self.sessions[jid].values(): + if not sess.received_thread_id: + contact = gajim.contacts.get_contact(self.name, jid) - session_supported = gajim.capscache.is_supported(contact, - common.xmpp.NS_SSN) or gajim.capscache.is_supported(contact, - common.xmpp.NS_ESESSION) - if session_supported: - sess.terminate() - del self.sessions[jid_stripped][sess.thread_id] + session_supported = gajim.capscache.is_supported(contact, + common.xmpp.NS_SSN) or gajim.capscache.is_supported( + contact, common.xmpp.NS_ESESSION) + if session_supported: + sess.terminate() + del self.sessions[jid][sess.thread_id] if avatar_sha is not None and ptype != 'error': if jid_stripped not in self.vcard_shas: diff --git a/src/common/crypto.py b/src/common/crypto.py index 17a010976..785b753bb 100644 --- a/src/common/crypto.py +++ b/src/common/crypto.py @@ -22,7 +22,7 @@ import os import math -from Crypto.Hash import SHA256 +from hashlib import sha256 as SHA256 # convert a large integer to a big-endian bitstring def encode_mpi(n): @@ -58,7 +58,7 @@ def decode_mpi(s): return 256 * decode_mpi(s[:-1]) + ord(s[-1]) def sha256(string): - sh = SHA256.new() + sh = SHA256() sh.update(string) return sh.digest() diff --git a/src/common/defs.py b/src/common/defs.py index 83ef075a3..92bcb527c 100644 --- a/src/common/defs.py +++ b/src/common/defs.py @@ -27,7 +27,7 @@ docdir = '../' datadir = '../' localedir = '../po' -version = '0.12.5.2-dev' +version = '0.12.5.6-dev' import sys, os.path for base in ('.', 'common'): diff --git a/src/common/gajim.py b/src/common/gajim.py index 0b083a1c6..0f340114a 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -183,7 +183,7 @@ else: import latex HAVE_LATEX = latex.check_for_latex_support() -HAVE_INDICATOR = False +HAVE_INDICATOR = True try: import indicate except ImportError: @@ -217,10 +217,10 @@ def get_server_from_jid(jid): pos = jid.find('@') + 1 # after @ return jid[pos:] -def get_nick_from_fjid(jid): - # fake jid is the jid for a contact in a room - # gaim@conference.jabber.no/nick/nick-continued - return jid.split('/', 1)[1] +def get_resource_from_jid(jid): + tokens = jid.split('/', 1) + if len(tokens) > 1: + return tokens[1] def get_name_and_server_from_jid(jid): name = get_nick_from_jid(jid) diff --git a/src/common/helpers.py b/src/common/helpers.py index 350eebf3c..2b495413d 100644 --- a/src/common/helpers.py +++ b/src/common/helpers.py @@ -122,7 +122,7 @@ def parse_resource(resource): '''Perform stringprep on resource and return it''' if resource: try: - from xmpp_stringprep import resourceprep + from xmpp.stringprepare import resourceprep return resourceprep.prepare(unicode(resource)) except UnicodeError: raise InvalidFormat, 'Invalid character in resource.' @@ -134,7 +134,7 @@ def prep(user, server, resource): if user: try: - from xmpp_stringprep import nodeprep + from xmpp.stringprepare import nodeprep user = nodeprep.prepare(unicode(user)) except UnicodeError: raise InvalidFormat, _('Invalid character in username.') @@ -145,14 +145,14 @@ def prep(user, server, resource): raise InvalidFormat, _('Server address required.') else: try: - from xmpp_stringprep import nameprep + from xmpp.stringprepare import nameprep server = nameprep.prepare(unicode(server)) except UnicodeError: raise InvalidFormat, _('Invalid character in hostname.') if resource: try: - from xmpp_stringprep import resourceprep + from xmpp.stringprepare import resourceprep resource = resourceprep.prepare(unicode(resource)) except UnicodeError: raise InvalidFormat, _('Invalid character in resource.') @@ -356,7 +356,7 @@ def is_in_path(command, return_abs_path=False): return False def exec_command(command): - subprocess.Popen(command, shell = True) + subprocess.Popen('%s &' % command, shell=True).wait() def build_command(executable, parameter): # we add to the parameter (can hold path with spaces) diff --git a/src/common/logger.py b/src/common/logger.py index a2179f51d..d4af560f1 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -130,8 +130,8 @@ class Logger: # if locked, wait up to 20 sec to unlock # before raise (hopefully should be enough) - self.con = sqlite.connect(LOG_DB_FILE, timeout = 20.0, - isolation_level = 'IMMEDIATE') + self.con = sqlite.connect(LOG_DB_FILE, timeout=20.0, + isolation_level='IMMEDIATE') os.chdir(back) self.cur = self.con.cursor() self.set_synchronous(False) @@ -160,7 +160,8 @@ class Logger: def get_jids_already_in_db(self): try: self.cur.execute('SELECT jid FROM jids') - rows = self.cur.fetchall() # list of tupples: [(u'aaa@bbb',), (u'cc@dd',)] + # list of tupples: [(u'aaa@bbb',), (u'cc@dd',)] + rows = self.cur.fetchall() except sqlite.DatabaseError: raise exceptions.DatabaseMalformed self.jids_already_in = [] @@ -200,7 +201,7 @@ class Logger: else: return True - def get_jid_id(self, jid, typestr = None): + def get_jid_id(self, jid, typestr=None): '''jids table has jid and jid_id logs table has log_id, jid_id, contact_name, time, kind, show, message so to ask logs we need jid_id that matches our jid in jids table @@ -360,8 +361,9 @@ class Logger: if sub == constants.SUBSCRIPTION_BOTH: return 'both' - def commit_to_db(self, values, write_unread = False): - sql = 'INSERT INTO logs (jid_id, contact_name, time, kind, show, message, subject) VALUES (?, ?, ?, ?, ?, ?, ?)' + def commit_to_db(self, values, write_unread=False): + sql = '''INSERT INTO logs (jid_id, contact_name, time, kind, show, + message, subject) VALUES (?, ?, ?, ?, ?, ?, ?)''' try: self.cur.execute(sql, values) except sqlite.DatabaseError: @@ -432,8 +434,7 @@ class Logger: all_messages.append(results[0] + (shown,)) return all_messages - def write(self, kind, jid, message = None, show = None, tim = None, - subject = None): + def write(self, kind, jid, message=None, show=None, tim=None, subject=None): '''write a row (status, gcstatus, message etc) to logs database kind can be status, gcstatus, gc_msg, (we only recv for those 3), single_msg_recv, chat_msg_recv, chat_msg_sent, single_msg_sent @@ -515,7 +516,7 @@ class Logger: return self.commit_to_db(values, write_unread) def get_last_conversation_lines(self, jid, restore_how_many_rows, - pending_how_many, timeout, account): + pending_how_many, timeout, account): '''accepts how many rows to restore and when to time them out (in minutes) (mark them as too old) and number of messages that are in queue and are already logged but pending to be viewed, @@ -560,7 +561,7 @@ class Logger: return start_of_day def get_conversation_for_date(self, jid, year, month, day, account): - '''returns contact_name, time, kind, show, message + '''returns contact_name, time, kind, show, message, subject for each row in a list of tupples, returns list with empty tupple if we found nothing to meet our demands''' try: @@ -575,7 +576,7 @@ class Logger: last_second_of_day = start_of_day + seconds_in_a_day - 1 self.cur.execute(''' - SELECT contact_name, time, kind, show, message FROM logs + SELECT contact_name, time, kind, show, message, subject FROM logs WHERE (%s) AND time BETWEEN %d AND %d ORDER BY time @@ -594,7 +595,7 @@ class Logger: # Error trying to create a new jid_id. This means there is no log return [] - if False: #query.startswith('SELECT '): # it's SQL query (FIXME) + if False: # query.startswith('SELECT '): # it's SQL query (FIXME) try: self.cur.execute(query) except sqlite.OperationalError, e: @@ -648,7 +649,7 @@ class Logger: return days_with_logs - def get_last_date_that_has_logs(self, jid, account = None, is_room = False): + def get_last_date_that_has_logs(self, jid, account=None, is_room=False): '''returns last time (in seconds since EPOCH) for which we had logs (excluding statuses)''' where_sql = '' @@ -759,10 +760,12 @@ class Logger: return answer # A longer note here: - # The database contains a blob field. Pysqlite seems to need special care for such fields. + # The database contains a blob field. Pysqlite seems to need special care for + # such fields. # When storing, we need to convert string into buffer object (1). - # When retrieving, we need to convert it back to a string to decompress it. (2) - # GzipFile needs a file-like object, StringIO emulates file for plain strings. + # When retrieving, we need to convert it back to a string to decompress it. + # (2) + # GzipFile needs a file-like object, StringIO emulates file for plain strings def iter_caps_data(self): ''' Iterate over caps cache data stored in the database. The iterator values are pairs of (node, ver, ext, identities, features): @@ -787,12 +790,13 @@ class Logger: # ..., 'FEAT', feature1, feature2, ...).join(' ')) # NOTE: if there's a need to do more gzip, put that to a function try: - data = GzipFile(fileobj=StringIO(str(data))).read().decode('utf-8').split('\0') + data = GzipFile(fileobj=StringIO(str(data))).read().decode( + 'utf-8').split('\0') except IOError: # This data is corrupted. It probably contains non-ascii chars to_be_removed.append((hash_method, hash_)) continue - i=0 + i = 0 identities = list() features = list() while i < (len(data) - 3) and data[i] != 'FEAT': @@ -811,11 +815,12 @@ class Logger: # yield the row yield hash_method, hash_, identities, features for hash_method, hash_ in to_be_removed: - sql = 'DELETE FROM caps_cache WHERE hash_method = "%s" AND hash = "%s"' % (hash_method, hash_) + sql = '''DELETE FROM caps_cache WHERE hash_method = "%s" AND + hash = "%s"''' % (hash_method, hash_) self.simple_commit(sql) def add_caps_entry(self, hash_method, hash_, identities, features): - data=[] + data = [] for identity in identities: # there is no FEAT category if identity['category'] == 'FEAT': @@ -875,8 +880,12 @@ class Logger: jid_id = self.get_jid_id(jid) except exceptions.PysqliteOperationalError, e: raise exceptions.PysqliteOperationalError(str(e)) - self.cur.execute('DELETE FROM roster_group WHERE account_jid_id=? AND jid_id=?', (account_jid_id, jid_id)) - self.cur.execute('DELETE FROM roster_entry WHERE account_jid_id=? AND jid_id=?', (account_jid_id, jid_id)) + self.cur.execute( + 'DELETE FROM roster_group WHERE account_jid_id=? AND jid_id=?', + (account_jid_id, jid_id)) + self.cur.execute( + 'DELETE FROM roster_entry WHERE account_jid_id=? AND jid_id=?', + (account_jid_id, jid_id)) self.con.commit() def add_or_update_contact(self, account_jid, jid, name, sub, ask, groups): @@ -893,7 +902,9 @@ class Logger: # Update groups information # First we delete all previous groups information - self.cur.execute('DELETE FROM roster_group WHERE account_jid_id=? AND jid_id=?', (account_jid_id, jid_id)) + self.cur.execute( + 'DELETE FROM roster_group WHERE account_jid_id=? AND jid_id=?', + (account_jid_id, jid_id)) # Then we add all new groups information for group in groups: self.cur.execute('INSERT INTO roster_group VALUES(?, ?, ?)', @@ -914,14 +925,19 @@ class Logger: account_jid_id = self.get_jid_id(account_jid) # First we fill data with roster_entry informations - self.cur.execute('SELECT j.jid, re.jid_id, re.name, re.subscription, re.ask FROM roster_entry re, jids j WHERE re.account_jid_id=? AND j.jid_id=re.jid_id', (account_jid_id,)) + self.cur.execute(''' + SELECT j.jid, re.jid_id, re.name, re.subscription, re.ask + FROM roster_entry re, jids j + WHERE re.account_jid_id=? AND j.jid_id=re.jid_id''', (account_jid_id,)) for jid, jid_id, name, subscription, ask in self.cur: data[jid] = {} if name: data[jid]['name'] = name else: data[jid]['name'] = None - data[jid]['subscription'] = self.convert_db_api_values_to_human_subscription_values(subscription) + data[jid]['subscription'] = \ + self.convert_db_api_values_to_human_subscription_values( + subscription) data[jid]['groups'] = [] data[jid]['resources'] = {} if ask: @@ -932,7 +948,10 @@ class Logger: # Then we add group for roster entries for jid in data: - self.cur.execute('SELECT group_name FROM roster_group WHERE account_jid_id=? AND jid_id=?', (account_jid_id, data[jid]['id'])) + self.cur.execute(''' + SELECT group_name FROM roster_group + WHERE account_jid_id=? AND jid_id=?''', + (account_jid_id, data[jid]['id'])) for (group_name,) in self.cur: data[jid]['groups'].append(group_name) del data[jid]['id'] diff --git a/src/common/optparser.py b/src/common/optparser.py index 5cedeeaca..05821a2e0 100644 --- a/src/common/optparser.py +++ b/src/common/optparser.py @@ -204,6 +204,14 @@ class OptionsParser: self.update_config_to_01251() if old < [0, 12, 5, 2] and new >= [0, 12, 5, 2]: self.update_config_to_01252() + if old < [0, 12, 5, 3] and new >= [0, 12, 5, 3]: + self.update_config_to_01253() + if old < [0, 12, 5, 4] and new >= [0, 12, 5, 4]: + self.update_config_to_01254() + if old < [0, 12, 5, 5] and new >= [0, 12, 5, 5]: + self.update_config_to_01255() + if old < [0, 12, 5, 6] and new >= [0, 12, 5, 6]: + self.update_config_to_01256() gajim.logger.init_vars() gajim.config.set('version', new_version) @@ -736,4 +744,53 @@ class OptionsParser: gajim.config.set_per('accounts', account, 'autoauth', val) gajim.config.set('version', '0.12.5.2') + def update_config_to_01253(self): + if 'enable_zeroconf' in self.old_values: + val = self.old_values['enable_zeroconf'] + for account in gajim.config.get_per('accounts'): + if gajim.config.get_per('accounts', account, 'is_zeroconf'): + gajim.config.set_per('accounts', account, 'active', val) + else: + gajim.config.set_per('accounts', account, 'active', True) + gajim.config.set('version', '0.12.5.3') + + def update_config_to_01254(self): + vals = {'inmsgcolor': ['#a34526', '#a40000'], + 'outmsgcolor': ['#164e6f', '#3465a4'], + 'restored_messages_color': ['grey', '#555753'], + 'statusmsgcolor': ['#1eaa1e', '#73d216'], + 'urlmsgcolor': ['#0000ff', '#204a87'], + 'gc_nicknames_colors': ['#a34526:#c000ff:#0012ff:#388a99:#045723:#7c7c7c:#ff8a00:#94452d:#244b5a:#32645a', '#4e9a06:#f57900:#ce5c00:#3465a4:#204a87:#75507b:#5c3566:#c17d11:#8f5902:#ef2929:#cc0000:#a40000']} + for c in vals: + if c not in self.old_values: + continue + val = self.old_values[c] + if val == vals[c][0]: + # We didn't change default value, so update it with new default + gajim.config.set(c, vals[c][1]) + gajim.config.set('version', '0.12.5.4') + + def update_config_to_01255(self): + vals = {'statusmsgcolor': ['#73d216', '#4e9a06'], + 'outmsgtxtcolor': ['#a2a2a2', '#555753']} + for c in vals: + if c not in self.old_values: + continue + val = self.old_values[c] + if val == vals[c][0]: + # We didn't change default value, so update it with new default + gajim.config.set(c, vals[c][1]) + gajim.config.set('version', '0.12.5.5') + + def update_config_to_01256(self): + vals = {'gc_nicknames_colors': ['#4e9a06:#f57900:#ce5c00:#3465a4:#204a87:#75507b:#5c3566:#c17d11:#8f5902:#ef2929:#cc0000:#a40000', '#f57900:#ce5c00:#204a87:#75507b:#5c3566:#c17d11:#8f5902:#ef2929:#cc0000:#a40000']} + for c in vals: + if c not in self.old_values: + continue + val = self.old_values[c] + if val == vals[c][0]: + # We didn't change default value, so update it with new default + gajim.config.set(c, vals[c][1]) + gajim.config.set('version', '0.12.5.6') + # vim: se ts=3: diff --git a/src/common/sleepy.py b/src/common/sleepy.py index d5f17edf2..507452ea9 100644 --- a/src/common/sleepy.py +++ b/src/common/sleepy.py @@ -43,6 +43,11 @@ try: lastInputInfo = LASTINPUTINFO() lastInputInfo.cbSize = ctypes.sizeof(lastInputInfo) + + # one or more of these may not be supported before XP. + OpenInputDesktop = ctypes.windll.user32.OpenInputDesktop + CloseDesktop = ctypes.windll.user32.CloseDesktop + SystemParametersInfo = ctypes.windll.user32.SystemParametersInfoW else: # unix from common import idle except Exception: @@ -65,6 +70,21 @@ class SleepyWindows: if not SUPPORTED: return False + # screen saver, in windows >= XP + saver_runing = ctypes.c_int(0) + # 0x72 is SPI_GETSCREENSAVERRUNNING + if SystemParametersInfo(0x72, 0, ctypes.byref(saver_runing), 0) and \ + saver_runing.value: + self.state = STATE_XA + return True + + desk = OpenInputDesktop(0, False, 0) + if not desk: + # Screen locked + self.state = STATE_XA + return True + CloseDesktop(desk) + idleTime = self.getIdleSec() # xa is stronger than away so check for xa first diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 51a12b488..cedf01553 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -33,12 +33,13 @@ import string import time import base64 import os +from hashlib import sha256 +from hmac import HMAC +from common import crypto if gajim.HAVE_PYCRYPTO: from Crypto.Cipher import AES - from Crypto.Hash import HMAC, SHA256 from Crypto.PublicKey import RSA - from common import crypto from common import dh import secrets @@ -78,7 +79,7 @@ class StanzaSession(object): def get_to(self): to = str(self.jid) - if self.resource: + if self.resource and not to.endswith(self.resource): to += '/' + self.resource return to @@ -214,6 +215,18 @@ class EncryptedStanzaSession(StanzaSession): # has the remote contact's identity ever been verified? self.verified_identity = False + def _get_contact(self): + c = gajim.contacts.get_contact(self.conn.name, self.jid, self.resource) + if not c: + c = gajim.contacts.get_contact(self.conn.name, self.jid) + return c + + def _is_buggy_gajim(self): + c = self._get_contact() + if gajim.capscache.is_supported(c, xmpp.NS_ROSTERX): + return False + return True + def set_kc_s(self, value): ''' keep the encrypter updated with my latest cipher key @@ -296,7 +309,7 @@ class EncryptedStanzaSession(StanzaSession): msg.getTag('c', namespace=xmpp.NS_STANZA_CRYPTO) def hmac(self, key, content): - return HMAC.new(key, content, self.hash_alg).digest() + return HMAC(key, content, self.hash_alg).digest() def generate_initiator_keys(self, k): return (self.hmac(k, 'Initiator Cipher Key'), @@ -375,7 +388,8 @@ class EncryptedStanzaSession(StanzaSession): def c7lize_mac_id(self, form): kids = form.getChildren() macable = [x for x in kids if x.getVar() not in ('mac', 'identity')] - return ''.join(xmpp.c14n.c14n(el) for el in macable) + return ''.join(xmpp.c14n.c14n(el, self._is_buggy_gajim()) for el in \ + macable) def verify_identity(self, form, dh_i, sigmai, i_o): m_o = base64.b64decode(form['mac']) @@ -408,7 +422,7 @@ class EncryptedStanzaSession(StanzaSession): keyvalue.getTagData(x))) for x in ('Modulus', 'Exponent')) eir_pubkey = RSA.construct((n,long(e))) - pubkey_o = xmpp.c14n.c14n(keyvalue) + pubkey_o = xmpp.c14n.c14n(keyvalue, self._is_buggy_gajim()) else: # FIXME DSA, etc. raise NotImplementedError() @@ -458,7 +472,8 @@ class EncryptedStanzaSession(StanzaSession): else: pubkey_s = '' - form_s2 = ''.join(xmpp.c14n.c14n(el) for el in form.getChildren()) + form_s2 = ''.join(xmpp.c14n.c14n(el, self._is_buggy_gajim()) for el in \ + form.getChildren()) old_c_s = self.c_s content = self.n_o + self.n_s + crypto.encode_mpi(dh_i) + pubkey_s + \ @@ -559,7 +574,8 @@ class EncryptedStanzaSession(StanzaSession): x.addChild(node=self.make_dhfield(modp_options, sigmai)) self.sigmai = sigmai - self.form_s = ''.join(xmpp.c14n.c14n(el) for el in x.getChildren()) + self.form_s = ''.join(xmpp.c14n.c14n(el, self._is_buggy_gajim()) for el \ + in x.getChildren()) feature.addChild(node=x) @@ -582,7 +598,7 @@ class EncryptedStanzaSession(StanzaSession): self.sas_algs = 'sas28x5' self.cipher = AES - self.hash_alg = SHA256 + self.hash_alg = sha256 self.compression = None for name in form.asDict(): @@ -688,8 +704,10 @@ class EncryptedStanzaSession(StanzaSession): b64ed = base64.b64encode(to_add[name]) x.addChild(node=xmpp.DataField(name=name, value=b64ed)) - self.form_o = ''.join(xmpp.c14n.c14n(el) for el in form.getChildren()) - self.form_s = ''.join(xmpp.c14n.c14n(el) for el in x.getChildren()) + self.form_o = ''.join(xmpp.c14n.c14n(el, self._is_buggy_gajim()) for el \ + in form.getChildren()) + self.form_s = ''.join(xmpp.c14n.c14n(el, self._is_buggy_gajim()) for el \ + in x.getChildren()) self.status = 'responded-e2e' @@ -742,7 +760,7 @@ class EncryptedStanzaSession(StanzaSession): self.encryptable_stanzas = ['message'] self.sas_algs = 'sas28x5' self.cipher = AES - self.hash_alg = SHA256 + self.hash_alg = sha256 self.compression = None self.negotiated = negotiated @@ -783,7 +801,7 @@ class EncryptedStanzaSession(StanzaSession): if not rshashes: # we've never spoken before, but we'll pretend we have - rshash_size = self.hash_alg.digest_size + rshash_size = self.hash_alg().digest_size rshashes.append(crypto.random_bytes(rshash_size)) rshashes = [base64.b64encode(rshash) for rshash in rshashes] @@ -791,7 +809,8 @@ class EncryptedStanzaSession(StanzaSession): result.addChild(node=xmpp.DataField(name='dhkeys', value=base64.b64encode(crypto.encode_mpi(e)))) - self.form_o = ''.join(xmpp.c14n.c14n(el) for el in form.getChildren()) + self.form_o = ''.join(xmpp.c14n.c14n(el, self._is_buggy_gajim()) for \ + el in form.getChildren()) # MUST securely destroy K unless it will be used later to generate the # final shared secret diff --git a/src/common/xmpp/c14n.py b/src/common/xmpp/c14n.py index 333cf7c55..bccce8155 100644 --- a/src/common/xmpp/c14n.py +++ b/src/common/xmpp/c14n.py @@ -21,7 +21,7 @@ ''' XML canonicalisation methods (for XEP-0116) ''' from simplexml import ustr -def c14n(node): +def c14n(node, is_buggy): s = "<" + node.name if node.namespace: if not node.parent or node.parent.namespace != node.namespace: @@ -29,6 +29,8 @@ def c14n(node): sorted_attrs = sorted(node.attrs.keys()) for key in sorted_attrs: + if not is_buggy and key == 'xmlns': + continue val = ustr(node.attrs[key]) # like XMLescape() but with whitespace and without > s = s + ' %s="%s"' % ( key, normalise_attr(val) ) @@ -38,7 +40,7 @@ def c14n(node): for a in node.kids: if (len(node.data)-1) >= cnt: s = s + normalise_text(node.data[cnt]) - s = s + c14n(a) + s = s + c14n(a, is_buggy) cnt=cnt+1 if (len(node.data)-1) >= cnt: s = s + normalise_text(node.data[cnt]) if not node.kids and s.endswith('>'): diff --git a/src/common/xmpp_stringprep.py b/src/common/xmpp/stringprepare.py similarity index 71% rename from src/common/xmpp_stringprep.py rename to src/common/xmpp/stringprepare.py index b2715df0c..47b1a2d1e 100644 --- a/src/common/xmpp_stringprep.py +++ b/src/common/xmpp/stringprepare.py @@ -1,5 +1,5 @@ # -*- coding:utf-8 -*- -## src/common/xmpp_stringprep.py +## src/common/xmpp/stringprepare.py ## ## Copyright (C) 2001-2005 Twisted Matrix Laboratories ## Copyright (C) 2005-2007 Yann Leboulanger @@ -21,34 +21,9 @@ ## along with Gajim. If not, see . ## -import sys, warnings - -if sys.version_info < (2,3,2): - import re - - class IDNA: - dots = re.compile(u"[\u002E\u3002\uFF0E\uFF61]") - def nameprep(self, label): - return label.lower() - - idna = IDNA() - - crippled = True - - warnings.warn("Accented and non-Western Jabber IDs will not be properly " - "case-folded with this version of Python, resulting in " - "incorrect protocol-level behavior. It is strongly " - "recommended you upgrade to Python 2.3.2 or newer if you " - "intend to use Twisted's Jabber support.") - -else: - import stringprep - import unicodedata - from encodings import idna - - crippled = False - -del sys, warnings +import stringprep +import unicodedata +from encodings import idna class ILookupTable: """ Interface for character lookup classes. """ @@ -222,44 +197,30 @@ class NamePrep: raise UnicodeError, "Invalid trailing hyphen-minus" return label -if crippled: - case_map = MappingTableFromFunction(lambda c: c.lower()) - nodeprep = Profile(mappings=[case_map], - normalize=False, - prohibiteds=[LookupTable([u' ', u'"', u'&', u"'", u'/', - u':', u'<', u'>', u'@'])], - check_unassigneds=False, - check_bidi=False) +C_11 = LookupTableFromFunction(stringprep.in_table_c11) +C_12 = LookupTableFromFunction(stringprep.in_table_c12) +C_21 = LookupTableFromFunction(stringprep.in_table_c21) +C_22 = LookupTableFromFunction(stringprep.in_table_c22) +C_3 = LookupTableFromFunction(stringprep.in_table_c3) +C_4 = LookupTableFromFunction(stringprep.in_table_c4) +C_5 = LookupTableFromFunction(stringprep.in_table_c5) +C_6 = LookupTableFromFunction(stringprep.in_table_c6) +C_7 = LookupTableFromFunction(stringprep.in_table_c7) +C_8 = LookupTableFromFunction(stringprep.in_table_c8) +C_9 = LookupTableFromFunction(stringprep.in_table_c9) - resourceprep = Profile(normalize=False, - check_unassigneds=False, - check_bidi=False) +B_1 = EmptyMappingTable(stringprep.in_table_b1) +B_2 = MappingTableFromFunction(stringprep.map_table_b2) -else: - C_11 = LookupTableFromFunction(stringprep.in_table_c11) - C_12 = LookupTableFromFunction(stringprep.in_table_c12) - C_21 = LookupTableFromFunction(stringprep.in_table_c21) - C_22 = LookupTableFromFunction(stringprep.in_table_c22) - C_3 = LookupTableFromFunction(stringprep.in_table_c3) - C_4 = LookupTableFromFunction(stringprep.in_table_c4) - C_5 = LookupTableFromFunction(stringprep.in_table_c5) - C_6 = LookupTableFromFunction(stringprep.in_table_c6) - C_7 = LookupTableFromFunction(stringprep.in_table_c7) - C_8 = LookupTableFromFunction(stringprep.in_table_c8) - C_9 = LookupTableFromFunction(stringprep.in_table_c9) +nodeprep = Profile(mappings=[B_1, B_2], + prohibiteds=[C_11, C_12, C_21, C_22, + C_3, C_4, C_5, C_6, C_7, C_8, C_9, + LookupTable([u'"', u'&', u"'", u'/', + u':', u'<', u'>', u'@'])]) - B_1 = EmptyMappingTable(stringprep.in_table_b1) - B_2 = MappingTableFromFunction(stringprep.map_table_b2) - - nodeprep = Profile(mappings=[B_1, B_2], - prohibiteds=[C_11, C_12, C_21, C_22, - C_3, C_4, C_5, C_6, C_7, C_8, C_9, - LookupTable([u'"', u'&', u"'", u'/', - u':', u'<', u'>', u'@'])]) - - resourceprep = Profile(mappings=[B_1,], - prohibiteds=[C_12, C_21, C_22, - C_3, C_4, C_5, C_6, C_7, C_8, C_9]) +resourceprep = Profile(mappings=[B_1,], + prohibiteds=[C_12, C_21, C_22, + C_3, C_4, C_5, C_6, C_7, C_8, C_9]) nameprep = NamePrep() diff --git a/src/config.py b/src/config.py index 7aea772a7..d5be18655 100644 --- a/src/config.py +++ b/src/config.py @@ -1387,6 +1387,8 @@ class AccountsWindow: model.set(iter_, 0, account) def resend(self, account): + if not account in gajim.connections: + return show = gajim.SHOW_LIST[gajim.connections[account].connected] status = gajim.connections[account].status gajim.connections[account].change_status(show, status) @@ -1420,10 +1422,12 @@ class AccountsWindow: def on_no(account): if self.resend_presence: self.resend(account) - self.dialog = dialogs.YesNoDialog(_('Relogin now?'), - _('If you want all the changes to apply instantly, ' - 'you must relogin.'), on_response_yes=(on_yes, - self.current_account), on_response_no=(on_no, self.current_account)) + if self.current_account in gajim.connections: + self.dialog = dialogs.YesNoDialog(_('Relogin now?'), + _('If you want all the changes to apply instantly, ' + 'you must relogin.'), on_response_yes=(on_yes, + self.current_account), on_response_no=(on_no, + self.current_account)) elif self.resend_presence: self.resend(self.current_account) @@ -1507,12 +1511,13 @@ class AccountsWindow: self.notebook.set_current_page(1) def init_zeroconf_account(self): - enable = gajim.config.get('enable_zeroconf') and gajim.HAVE_ZEROCONF - self.xml.get_widget('enable_zeroconf_checkbutton2').set_active(enable) + active = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'active') + self.xml.get_widget('enable_zeroconf_checkbutton2').set_active(active) if not gajim.HAVE_ZEROCONF: self.xml.get_widget('enable_zeroconf_checkbutton2').set_sensitive( False) - self.xml.get_widget('zeroconf_notebook').set_sensitive(enable) + self.xml.get_widget('zeroconf_notebook').set_sensitive(active) # General tab st = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'autoconnect') @@ -1573,7 +1578,7 @@ class AccountsWindow: use_gpg_agent_checkbutton = self.xml.get_widget( 'use_gpg_agent_checkbutton' + widget_name_add) - if not keyid or not gajim.connections[account].gpg: + if not keyid: use_gpg_agent_checkbutton.set_sensitive(False) gpg_key_label.set_text(_('No key selected')) gpg_name_label.set_text('') @@ -1587,6 +1592,9 @@ class AccountsWindow: def draw_normal_jid(self): account = self.current_account self.ignore_events = True + active = gajim.config.get_per('accounts', account, 'active') + self.xml.get_widget('enable_checkbutton1').set_active(active) + self.xml.get_widget('normal_notebook1').set_sensitive(active) if gajim.config.get_per('accounts', account, 'anonymous_auth'): self.xml.get_widget('anonymous_checkbutton1').set_active(True) self.xml.get_widget('jid_label1').set_text(_('Server:')) @@ -1667,7 +1675,7 @@ class AccountsWindow: # Personal tab gpg_key_label = self.xml.get_widget('gpg_key_label1') - if gajim.connections[account].gpg: + if gajim.HAVE_GPG: self.xml.get_widget('gpg_choose_button1').set_sensitive(True) self.init_account_gpg() else: @@ -1744,9 +1752,8 @@ class AccountsWindow: def on_rename_button_clicked(self, widget): if not self.current_account: return - enable = gajim.config.get('enable_zeroconf') - if (self.current_account != gajim.ZEROCONF_ACC_NAME or enable) and \ - gajim.connections[self.current_account].connected != 0: + active = gajim.config.get_per('accounts', self.current_account, 'active') + if active and gajim.connections[self.current_account].connected != 0: dialogs.ErrorDialog( _('You are currently connected to the server'), _('To change the account name, you must be disconnected.')) @@ -1771,7 +1778,7 @@ class AccountsWindow: dialogs.ErrorDialog(_('Invalid account name'), _('Account name cannot contain spaces.')) return - if self.current_account != gajim.ZEROCONF_ACC_NAME or enable: + if active: # update variables gajim.interface.instances[new_name] = gajim.interface.instances[ old_name] @@ -2179,15 +2186,101 @@ class AccountsWindow: def on_merge_checkbutton_toggled(self, widget): self.on_checkbutton_toggled(widget, 'mergeaccounts') - if len(gajim.connections) >= 2: # Do not merge accounts if only one exists + if len(gajim.connections) >= 2: # Do not merge accounts if only one active gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') else: gajim.interface.roster.regroup = False gajim.interface.roster.setup_and_draw_roster() + def _disable_account(self, account): + gajim.interface.roster.close_all(account) + if account == gajim.ZEROCONF_ACC_NAME: + gajim.connections[account].disable_account() + del gajim.connections[account] + gajim.interface.save_config() + del gajim.interface.instances[account] + del gajim.interface.minimized_controls[account] + del gajim.nicks[account] + del gajim.block_signed_in_notifications[account] + del gajim.groups[account] + gajim.contacts.remove_account(account) + del gajim.gc_connected[account] + del gajim.automatic_rooms[account] + del gajim.to_be_removed[account] + del gajim.newly_added[account] + del gajim.sleeper_state[account] + del gajim.encrypted_chats[account] + del gajim.last_message_time[account] + del gajim.status_before_autoaway[account] + del gajim.transport_avatar[account] + del gajim.gajim_optional_features[account] + del gajim.caps_hash[account] + if len(gajim.connections) >= 2: + # Do not merge accounts if only one exists + gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') + else: + gajim.interface.roster.regroup = False + gajim.interface.roster.setup_and_draw_roster() + gajim.interface.roster.set_actions_menu_needs_rebuild() + + def _enable_account(self, account): + if account == gajim.ZEROCONF_ACC_NAME: + gajim.connections[account] = connection_zeroconf.ConnectionZeroconf( + account) + if gajim.connections[account].gpg: + self.xml.get_widget('gpg_choose_button2').set_sensitive(True) + else: + gajim.connections[account] = common.connection.Connection(account) + if gajim.connections[account].gpg: + self.xml.get_widget('gpg_choose_button1').set_sensitive(True) + self.init_account_gpg() + # update variables + gajim.interface.instances[account] = {'infos': {}, + 'disco': {}, 'gc_config': {}, 'search': {}, 'online_dialog': {}} + gajim.interface.minimized_controls[account] = {} + gajim.connections[account].connected = 0 + gajim.groups[account] = {} + gajim.contacts.add_account(account) + gajim.gc_connected[account] = {} + gajim.automatic_rooms[account] = {} + gajim.newly_added[account] = [] + gajim.to_be_removed[account] = [] + if account == gajim.ZEROCONF_ACC_NAME: + gajim.nicks[account] = gajim.ZEROCONF_ACC_NAME + else: + gajim.nicks[account] = gajim.config.get_per('accounts', account, + 'name') + gajim.block_signed_in_notifications[account] = True + gajim.sleeper_state[account] = 'off' + gajim.encrypted_chats[account] = [] + gajim.last_message_time[account] = {} + gajim.status_before_autoaway[account] = '' + gajim.transport_avatar[account] = {} + gajim.gajim_optional_features[account] = [] + gajim.caps_hash[account] = '' + # refresh roster + if len(gajim.connections) >= 2: + # Do not merge accounts if only one exists + gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') + else: + gajim.interface.roster.regroup = False + gajim.interface.roster.setup_and_draw_roster() + gajim.interface.roster.set_actions_menu_needs_rebuild() + gajim.interface.save_config() + def on_enable_zeroconf_checkbutton2_toggled(self, widget): # don't do anything if there is an account with the local name but is a # normal account + if self.ignore_events: + return + if gajim.account_is_connected(self.current_account): + self.ignore_events = True + self.xml.get_widget('enable_zeroconf_checkbutton2').set_active(True) + self.ignore_events = False + dialogs.ErrorDialog( + _('You are currently connected to the server'), + _('To disable the account, you must be disconnected.')) + return if gajim.ZEROCONF_ACC_NAME in gajim.connections and not \ gajim.connections[gajim.ZEROCONF_ACC_NAME].is_zeroconf: gajim.connections[gajim.ZEROCONF_ACC_NAME].dispatch('ERROR', @@ -2196,77 +2289,42 @@ class AccountsWindow: '.'))) return - if gajim.config.get('enable_zeroconf') and not widget.get_active(): + if gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'active') \ + and not widget.get_active(): self.xml.get_widget('zeroconf_notebook').set_sensitive(False) # disable - gajim.interface.roster.close_all(gajim.ZEROCONF_ACC_NAME) - gajim.connections[gajim.ZEROCONF_ACC_NAME].disable_account() - del gajim.connections[gajim.ZEROCONF_ACC_NAME] - gajim.interface.save_config() - del gajim.interface.instances[gajim.ZEROCONF_ACC_NAME] - del gajim.interface.minimized_controls[gajim.ZEROCONF_ACC_NAME] - del gajim.nicks[gajim.ZEROCONF_ACC_NAME] - del gajim.block_signed_in_notifications[gajim.ZEROCONF_ACC_NAME] - del gajim.groups[gajim.ZEROCONF_ACC_NAME] - gajim.contacts.remove_account(gajim.ZEROCONF_ACC_NAME) - del gajim.gc_connected[gajim.ZEROCONF_ACC_NAME] - del gajim.automatic_rooms[gajim.ZEROCONF_ACC_NAME] - del gajim.to_be_removed[gajim.ZEROCONF_ACC_NAME] - del gajim.newly_added[gajim.ZEROCONF_ACC_NAME] - del gajim.sleeper_state[gajim.ZEROCONF_ACC_NAME] - del gajim.encrypted_chats[gajim.ZEROCONF_ACC_NAME] - del gajim.last_message_time[gajim.ZEROCONF_ACC_NAME] - del gajim.status_before_autoaway[gajim.ZEROCONF_ACC_NAME] - del gajim.transport_avatar[gajim.ZEROCONF_ACC_NAME] - del gajim.gajim_optional_features[gajim.ZEROCONF_ACC_NAME] - del gajim.caps_hash[gajim.ZEROCONF_ACC_NAME] - if len(gajim.connections) >= 2: - # Do not merge accounts if only one exists - gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') - else: - gajim.interface.roster.regroup = False - gajim.interface.roster.setup_and_draw_roster() - gajim.interface.roster.set_actions_menu_needs_rebuild() + self._disable_account(gajim.ZEROCONF_ACC_NAME) - elif not gajim.config.get('enable_zeroconf') and widget.get_active(): + elif not gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'active') and widget.get_active(): self.xml.get_widget('zeroconf_notebook').set_sensitive(True) # enable (will create new account if not present) - gajim.connections[gajim.ZEROCONF_ACC_NAME] = connection_zeroconf.\ - ConnectionZeroconf(gajim.ZEROCONF_ACC_NAME) - if gajim.connections[gajim.ZEROCONF_ACC_NAME].gpg: - self.xml.get_widget('gpg_choose_button2').set_sensitive(True) - self.init_account_gpg() - # update variables - gajim.interface.instances[gajim.ZEROCONF_ACC_NAME] = {'infos': {}, - 'disco': {}, 'gc_config': {}, 'search': {}, 'online_dialog': {}} - gajim.interface.minimized_controls[gajim.ZEROCONF_ACC_NAME] = {} - gajim.connections[gajim.ZEROCONF_ACC_NAME].connected = 0 - gajim.groups[gajim.ZEROCONF_ACC_NAME] = {} - gajim.contacts.add_account(gajim.ZEROCONF_ACC_NAME) - gajim.gc_connected[gajim.ZEROCONF_ACC_NAME] = {} - gajim.automatic_rooms[gajim.ZEROCONF_ACC_NAME] = {} - gajim.newly_added[gajim.ZEROCONF_ACC_NAME] = [] - gajim.to_be_removed[gajim.ZEROCONF_ACC_NAME] = [] - gajim.nicks[gajim.ZEROCONF_ACC_NAME] = gajim.ZEROCONF_ACC_NAME - gajim.block_signed_in_notifications[gajim.ZEROCONF_ACC_NAME] = True - gajim.sleeper_state[gajim.ZEROCONF_ACC_NAME] = 'off' - gajim.encrypted_chats[gajim.ZEROCONF_ACC_NAME] = [] - gajim.last_message_time[gajim.ZEROCONF_ACC_NAME] = {} - gajim.status_before_autoaway[gajim.ZEROCONF_ACC_NAME] = '' - gajim.transport_avatar[gajim.ZEROCONF_ACC_NAME] = {} - gajim.gajim_optional_features[gajim.ZEROCONF_ACC_NAME] = [] - gajim.caps_hash[gajim.ZEROCONF_ACC_NAME] = '' - # refresh roster - if len(gajim.connections) >= 2: - # Do not merge accounts if only one exists - gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') - else: - gajim.interface.roster.regroup = False - gajim.interface.roster.setup_and_draw_roster() - gajim.interface.roster.set_actions_menu_needs_rebuild() - gajim.interface.save_config() + self._enable_account(gajim.ZEROCONF_ACC_NAME) - self.on_checkbutton_toggled(widget, 'enable_zeroconf') + self.on_checkbutton_toggled(widget, 'active', + account=gajim.ZEROCONF_ACC_NAME) + + def on_enable_checkbutton1_toggled(self, widget): + if self.ignore_events: + return + if gajim.account_is_connected(self.current_account): + self.ignore_events = True + self.xml.get_widget('enable_checkbutton1').set_active(True) + self.ignore_events = False + dialogs.ErrorDialog( + _('You are currently connected to the server'), + _('To disable the account, you must be disconnected.')) + return + # add/remove account in roster and all variables + if widget.get_active(): + # enable + self._enable_account(self.current_account) + else: + # disable + self._disable_account(self.current_account) + self.on_checkbutton_toggled(widget, 'active', + account=self.current_account, change_sensitivity_widgets=[ + self.xml.get_widget('normal_notebook1')]) def on_custom_port_checkbutton2_toggled(self, widget): self.xml.get_widget('custom_port_entry2').set_sensitive( diff --git a/src/conversation_textview.py b/src/conversation_textview.py index ff74f606b..3af295cbb 100644 --- a/src/conversation_textview.py +++ b/src/conversation_textview.py @@ -230,13 +230,35 @@ class ConversationTextview(gobject.GObject): self.tagIn = buffer_.create_tag('incoming') color = gajim.config.get('inmsgcolor') + font = pango.FontDescription(gajim.config.get('inmsgfont')) self.tagIn.set_property('foreground', color) + self.tagIn.set_property('font-desc', font) + self.tagOut = buffer_.create_tag('outgoing') color = gajim.config.get('outmsgcolor') + font = pango.FontDescription(gajim.config.get('outmsgfont')) self.tagOut.set_property('foreground', color) + self.tagOut.set_property('font-desc', font) + self.tagStatus = buffer_.create_tag('status') color = gajim.config.get('statusmsgcolor') + font = pango.FontDescription(gajim.config.get('satusmsgfont')) self.tagStatus.set_property('foreground', color) + self.tagStatus.set_property('font-desc', font) + + self.tagInText = buffer_.create_tag('incomingtxt') + color = gajim.config.get('inmsgtxtcolor') + font = pango.FontDescription(gajim.config.get('inmsgtxtfont')) + if color: + self.tagInText.set_property('foreground', color) + self.tagInText.set_property('font-desc', font) + + self.tagOutText = buffer_.create_tag('outgoingtxt') + color = gajim.config.get('outmsgtxtcolor') + if color: + font = pango.FontDescription(gajim.config.get('outmsgtxtfont')) + self.tagOutText.set_property('foreground', color) + self.tagOutText.set_property('font-desc', font) colors = gajim.config.get('gc_nicknames_colors') colors = colors.split(':') @@ -945,7 +967,7 @@ class ConversationTextview(gobject.GObject): helpers.launch_browser_mailer(kind, href) - def detect_and_print_special_text(self, otext, other_tags): + def detect_and_print_special_text(self, otext, other_tags, graphics=True): '''detects special text (emots & links & formatting) prints normal text before any special text it founts, then print special text (that happens many times until @@ -959,7 +981,7 @@ class ConversationTextview(gobject.GObject): # detect_and_print_special_text() is also used by # HtmlHandler.handle_specials() and there tags is gtk.TextTag objects, # not strings - if len(other_tags) > 0 and isinstance(other_tags[0], gtk.TextTag): + if other_tags and isinstance(other_tags[0], gtk.TextTag): insert_tags_func = buffer_.insert_with_tags index = 0 @@ -970,7 +992,8 @@ class ConversationTextview(gobject.GObject): specials_limit = 100 # basic: links + mail + formatting is always checked (we like that) - if gajim.config.get('emoticons_theme'): # search for emoticons & urls + if gajim.config.get('emoticons_theme') and graphics: + # search for emoticons & urls iterator = gajim.interface.emot_and_basic_re.finditer(otext) else: # search for just urls + mail + formatting iterator = gajim.interface.basic_pattern_re.finditer(otext) @@ -985,7 +1008,7 @@ class ConversationTextview(gobject.GObject): index = end # update index # now print it - self.print_special_text(special_text, other_tags) + self.print_special_text(special_text, other_tags, graphics=graphics) specials_limit -= 1 if specials_limit <= 0: break @@ -996,7 +1019,7 @@ class ConversationTextview(gobject.GObject): return buffer_.get_end_iter() - def print_special_text(self, special_text, other_tags): + def print_special_text(self, special_text, other_tags, graphics=True): '''is called by detect_and_print_special_text and prints special text (emots, links, formatting)''' tags = [] @@ -1014,7 +1037,7 @@ class ConversationTextview(gobject.GObject): possible_emot_ascii_caps = special_text.upper() # emoticons keys are CAPS if gajim.config.get('emoticons_theme') and \ - possible_emot_ascii_caps in gajim.interface.emoticons.keys(): + possible_emot_ascii_caps in gajim.interface.emoticons.keys() and graphics: # it's an emoticon emot_ascii = possible_emot_ascii_caps end_iter = buffer_.get_end_iter() @@ -1090,7 +1113,7 @@ class ConversationTextview(gobject.GObject): if not show_ascii_formatting_chars: special_text = special_text[1:-1] # remove _ _ elif gajim.HAVE_LATEX and special_text.startswith('$$') and \ - special_text.endswith('$$'): + special_text.endswith('$$') and graphics: try: imagepath = latex.latex_to_image(special_text[2:-2]) except LatexError, e: @@ -1118,9 +1141,13 @@ class ConversationTextview(gobject.GObject): # It's nothing special if use_other_tags: end_iter = buffer_.get_end_iter() - buffer_.insert_with_tags_by_name(end_iter, special_text, *other_tags) + insert_tags_func = buffer_.insert_with_tags_by_name + if other_tags and isinstance(other_tags[0], gtk.TextTag): + insert_tags_func = buffer_.insert_with_tags - if len(tags) > 0: + insert_tags_func(end_iter, special_text, *other_tags) + + if tags: end_iter = buffer_.get_end_iter() all_tags = tags[:] if use_other_tags: @@ -1134,7 +1161,7 @@ class ConversationTextview(gobject.GObject): def print_conversation_line(self, text, jid, kind, name, tim, other_tags_for_name=[], other_tags_for_time=[], other_tags_for_text=[], - subject=None, old_kind=None, xhtml=None, simple=False): + subject=None, old_kind=None, xhtml=None, simple=False, graphics=True): '''prints 'chat' type messages''' buffer_ = self.tv.get_buffer() buffer_.begin_user_action() @@ -1214,8 +1241,12 @@ class ConversationTextview(gobject.GObject): 'chat_merge_consecutive_nickname_indent')) else: self.print_name(name, kind, other_tags_for_name) + if kind == 'incoming': + text_tags.append('incomingtxt') + elif kind == 'outgoing': + text_tags.append('outgoingtxt') self.print_subject(subject) - self.print_real_text(text, text_tags, name, xhtml) + self.print_real_text(text, text_tags, name, xhtml, graphics=graphics) # scroll to the end of the textview if at_the_end or kind == 'outgoing': @@ -1284,7 +1315,8 @@ class ConversationTextview(gobject.GObject): buffer_.insert(end_iter, subject) self.print_empty_line() - def print_real_text(self, text, text_tags=[], name=None, xhtml=None): + def print_real_text(self, text, text_tags=[], name=None, xhtml=None, + graphics=True): '''this adds normal and special text. call this to add text''' if xhtml: try: @@ -1293,14 +1325,14 @@ class ConversationTextview(gobject.GObject): self.tv.display_html(xhtml.encode('utf-8'), self) return except Exception, e: - gajim.log.debug(str('Error processing xhtml') + str(e)) - gajim.log.debug(str('with |' + xhtml + '|')) + gajim.log.debug('Error processing xhtml' + str(e)) + gajim.log.debug('with |' + xhtml + '|') # /me is replaced by name if name is given if name and (text.startswith('/me ') or text.startswith('/me\n')): text = '* ' + name + text[3:] text_tags.append('italic') # detect urls formatting and if the user has it on emoticons - self.detect_and_print_special_text(text, text_tags) + self.detect_and_print_special_text(text, text_tags, graphics=graphics) # vim: se ts=3: diff --git a/src/dialogs.py b/src/dialogs.py index a93583aab..3702c374b 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -554,8 +554,7 @@ class ChangeStatusMessageDialog: message_textview = self.xml.get_widget('message_textview') self.message_buffer = message_textview.get_buffer() - self.message_buffer.connect('changed', - self.toggle_sensitiviy_of_save_as_preset) + self.message_buffer.connect('changed', self.on_message_buffer_changed) if not msg: msg = '' msg = helpers.from_one_line(msg) @@ -713,7 +712,11 @@ class ChangeStatusMessageDialog: # Stop the event return True - def toggle_sensitiviy_of_save_as_preset(self, widget): + def on_message_buffer_changed(self, widget): + self.countdown_enabled = False + self.toggle_sensitiviy_of_save_as_preset() + + def toggle_sensitiviy_of_save_as_preset(self): btn = self.xml.get_widget('save_as_preset_button') if self.message_buffer.get_char_count() == 0: btn.set_sensitive(False) @@ -1976,7 +1979,7 @@ class JoinGroupchatWindow: self.xml.get_widget('join_button').set_sensitive(False) if account and not gajim.connections[account].private_storage_supported: - self.xml.get_widget('auto_join_checkbutton').set_sensitive(False) + self.xml.get_widget('bookmark_checkbutton').set_sensitive(False) self.window.show_all() @@ -2016,6 +2019,13 @@ class JoinGroupchatWindow: '''When Cancel button is clicked''' self.window.destroy() + def on_bookmark_checkbutton_toggled(self, widget): + auto_join_checkbutton = self.xml.get_widget('auto_join_checkbutton') + if widget.get_active(): + auto_join_checkbutton.set_sensitive(True) + else: + auto_join_checkbutton.set_sensitive(False) + def on_join_button_clicked(self, widget): '''When Join button is clicked''' if not self.account: @@ -2059,10 +2069,14 @@ class JoinGroupchatWindow: gajim.config.set('recently_groupchat', ' '.join(self.recently_groupchat)) - if self.xml.get_widget('auto_join_checkbutton').get_active(): + if self.xml.get_widget('bookmark_checkbutton').get_active(): + if self.xml.get_widget('auto_join_checkbutton').get_active(): + autojoin = '1' + else: + autojoin = '0' # Add as bookmark, with autojoin and not minimized name = gajim.get_nick_from_jid(room_jid) - gajim.interface.add_gc_bookmark(self.account, name, room_jid, '1', \ + gajim.interface.add_gc_bookmark(self.account, name, room_jid, autojoin, '0', password, nickname) if self.automatic: @@ -2816,6 +2830,8 @@ class RosterItemExchangeWindow: self.message_body = message_body self.jid_from = jid_from + show_dialog = False + # Connect to glade self.xml = gtkgui_helpers.get_glade('roster_item_exchange_window.glade') self.window = self.xml.get_widget('roster_item_exchange_window') @@ -2884,6 +2900,7 @@ class RosterItemExchangeWindow: else: groups = groups + group + ', ' if not is_in_roster: + show_dialog = True iter = model.append() model.set(iter, 0, True, 1, jid, 2, name, 3, groups) @@ -2916,6 +2933,7 @@ class RosterItemExchangeWindow: else: groups = groups + group + ', ' if not is_right and is_in_roster: + show_dialog = True iter = model.append() model.set(iter, 0, True, 1, jid, 2, name, 3, groups) @@ -2941,6 +2959,7 @@ class RosterItemExchangeWindow: else: groups = groups + group + ', ' if is_in_roster: + show_dialog = True iter = model.append() model.set(iter, 0, True, 1, jid, 2, name, 3, groups) @@ -2949,9 +2968,9 @@ class RosterItemExchangeWindow: get_children()[0].get_children()[1] accept_button_label.set_label(_('Delete')) - self.window.show_all() - - self.xml.signal_autoconnect(self) + if show_dialog: + self.window.show_all() + self.xml.signal_autoconnect(self) def toggled_callback(self, cell, path): model = self.items_list_treeview.get_model() @@ -3051,20 +3070,20 @@ class PrivacyListWindow: # Add Widgets for widget_to_add in ('title_hbox', 'privacy_lists_title_label', - 'list_of_rules_label', 'add_edit_rule_label', 'delete_open_buttons_hbox', - 'privacy_list_active_checkbutton', 'privacy_list_default_checkbutton', - 'list_of_rules_combobox', 'delete_open_buttons_hbox', - 'delete_rule_button', 'open_rule_button', 'edit_allow_radiobutton', - 'edit_deny_radiobutton', 'edit_type_jabberid_radiobutton', - 'edit_type_jabberid_entry', 'edit_type_group_radiobutton', - 'edit_type_group_combobox', 'edit_type_subscription_radiobutton', - 'edit_type_subscription_combobox', 'edit_type_select_all_radiobutton', - 'edit_queries_send_checkbutton', 'edit_send_messages_checkbutton', - 'edit_view_status_checkbutton', 'edit_order_spinbutton', - 'new_rule_button', 'save_rule_button', 'privacy_list_refresh_button', - 'privacy_list_close_button', 'edit_send_status_checkbutton', - 'add_edit_vbox', 'privacy_list_active_checkbutton', - 'privacy_list_default_checkbutton'): + 'list_of_rules_label', 'add_edit_rule_label', 'delete_open_buttons_hbox', + 'privacy_list_active_checkbutton', 'privacy_list_default_checkbutton', + 'list_of_rules_combobox', 'delete_open_buttons_hbox', + 'delete_rule_button', 'open_rule_button', 'edit_allow_radiobutton', + 'edit_deny_radiobutton', 'edit_type_jabberid_radiobutton', + 'edit_type_jabberid_entry', 'edit_type_group_radiobutton', + 'edit_type_group_combobox', 'edit_type_subscription_radiobutton', + 'edit_type_subscription_combobox', 'edit_type_select_all_radiobutton', + 'edit_queries_send_checkbutton', 'edit_send_messages_checkbutton', + 'edit_view_status_checkbutton', 'edit_all_checkbutton', + 'edit_order_spinbutton', 'new_rule_button', 'save_rule_button', + 'privacy_list_refresh_button', 'privacy_list_close_button', + 'edit_send_status_checkbutton', 'add_edit_vbox', + 'privacy_list_active_checkbutton', 'privacy_list_default_checkbutton'): self.__dict__[widget_to_add] = self.xml.get_widget(widget_to_add) self.privacy_lists_title_label.set_label( @@ -3129,12 +3148,12 @@ class PrivacyListWindow: for rule in rules: if 'type' in rule: text_item = _('Order: %(order)s, action: %(action)s, type: %(type)s' - ', value: %(value)s') % {'order': rule['order'], - 'action': rule['action'], 'type': rule['type'], - 'value': rule['value']} + ', value: %(value)s') % {'order': rule['order'], + 'action': rule['action'], 'type': rule['type'], + 'value': rule['value']} else: text_item = _('Order: %(order)s, action: %(action)s') % \ - {'order': rule['order'], 'action': rule['action']} + {'order': rule['order'], 'action': rule['action']} self.global_rules[text_item] = rule self.list_of_rules_combobox.append_text(text_item) if len(rules) == 0: @@ -3215,14 +3234,17 @@ class PrivacyListWindow: self.edit_queries_send_checkbutton.set_active(False) self.edit_view_status_checkbutton.set_active(False) self.edit_send_status_checkbutton.set_active(False) - for child in rule_info['child']: - if child == 'presence-out': + self.edit_all_checkbutton.set_active(False) + if not rule_info['child']: + self.edit_all_checkbutton.set_active(True) + else: + if 'presence-out' in rule_info['child']: self.edit_send_status_checkbutton.set_active(True) - elif child == 'presence-in': + if 'presence-in' in rule_info['child']: self.edit_view_status_checkbutton.set_active(True) - elif child == 'iq': + if 'iq' in rule_info['child']: self.edit_queries_send_checkbutton.set_active(True) - elif child == 'message': + if 'message' in rule_info['child']: self.edit_send_messages_checkbutton.set_active(True) if rule_info['action'] == 'allow': @@ -3231,6 +3253,26 @@ class PrivacyListWindow: self.edit_deny_radiobutton.set_active(True) self.add_edit_vbox.show() + def on_edit_all_checkbutton_toggled(self, widget): + if widget.get_active(): + self.edit_send_messages_checkbutton.set_active(True) + self.edit_queries_send_checkbutton.set_active(True) + self.edit_view_status_checkbutton.set_active(True) + self.edit_send_status_checkbutton.set_active(True) + self.edit_send_messages_checkbutton.set_sensitive(False) + self.edit_queries_send_checkbutton.set_sensitive(False) + self.edit_view_status_checkbutton.set_sensitive(False) + self.edit_send_status_checkbutton.set_sensitive(False) + else: + self.edit_send_messages_checkbutton.set_active(False) + self.edit_queries_send_checkbutton.set_active(False) + self.edit_view_status_checkbutton.set_active(False) + self.edit_send_status_checkbutton.set_active(False) + self.edit_send_messages_checkbutton.set_sensitive(True) + self.edit_queries_send_checkbutton.set_sensitive(True) + self.edit_view_status_checkbutton.set_sensitive(True) + self.edit_send_status_checkbutton.set_sensitive(True) + def on_privacy_list_active_checkbutton_toggled(self, widget): if widget.get_active(): gajim.connections[self.account].set_active_list( @@ -3258,6 +3300,7 @@ class PrivacyListWindow: self.edit_queries_send_checkbutton.set_active(False) self.edit_view_status_checkbutton.set_active(False) self.edit_send_status_checkbutton.set_active(False) + self.edit_all_checkbutton.set_active(False) self.edit_order_spinbutton.set_value(1) self.edit_type_group_combobox.set_active(0) self.edit_type_subscription_combobox.set_active(0) @@ -3284,14 +3327,15 @@ class PrivacyListWindow: else: edit_deny = 'deny' child = [] - if self.edit_send_messages_checkbutton.get_active(): - child.append('message') - if self.edit_queries_send_checkbutton.get_active(): - child.append('iq') - if self.edit_send_status_checkbutton.get_active(): - child.append('presence-out') - if self.edit_view_status_checkbutton.get_active(): - child.append('presence-in') + if not self.edit_all_checkbutton.get_active(): + if self.edit_send_messages_checkbutton.get_active(): + child.append('message') + if self.edit_queries_send_checkbutton.get_active(): + child.append('iq') + if self.edit_send_status_checkbutton.get_active(): + child.append('presence-out') + if self.edit_view_status_checkbutton.get_active(): + child.append('presence-in') if edit_type != '': return {'order': edit_order, 'action': edit_deny, 'type': edit_type, 'value': edit_value, 'child': child} diff --git a/src/gajim.py b/src/gajim.py index 304c446cc..75d14c612 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -159,13 +159,22 @@ else: from music_track_listener import MusicTrackListener import dbus - if os.name == 'posix': # dl module is Unix Only - try: # rename the process name to gajim - import dl - libc = dl.open('/lib/libc.so.6') - libc.call('prctl', 15, 'gajim\0', 0, 0, 0) - except Exception: - pass + from ctypes import CDLL + from ctypes.util import find_library + import platform + + sysname = platform.system() + if sysname in ('Linux', 'FreeBSD', 'OpenBSD', 'NetBSD'): + libc = CDLL(find_library('c')) + + # The constant defined in which is used to set the name of + # the process. + PR_SET_NAME = 15 + + if sysname == 'Linux': + libc.prctl(PR_SET_NAME, 'gajim') + elif sysname in ('FreeBSD', 'OpenBSD', 'NetBSD'): + libc.setproctitle('gajim') if gtk.pygtk_version < (2, 12, 0): pritext = _('Gajim needs PyGTK 2.12 or above') @@ -231,14 +240,6 @@ from chat_control import ChatControl from groupchat_control import GroupchatControl from groupchat_control import PrivateChatControl -# Here custom adhoc processors should be loaded. At this point there is -# everything they need to function properly. The next line loads custom exmple -# adhoc processors. Technically, they could be loaded earlier as host processors -# themself does not depend on the chat controls, but that should not be done -# uless there is a really good reason for that.. -# -# from commands import custom - from atom_window import AtomWindow from session import ChatControlSession @@ -746,9 +747,9 @@ class Interface: lcontact.append(contact1) elif contact1.show in statuss: old_show = statuss.index(contact1.show) - # FIXME: What am I? if (resources != [''] and (len(lcontact) != 1 or \ lcontact[0].show != 'offline')) and jid.find('@') > 0: + # Another resource of an existing contact connected old_show = 0 contact1 = gajim.contacts.copy_contact(contact1) lcontact.append(contact1) @@ -883,6 +884,7 @@ class Interface: ctrl = self.msg_win_mgr.get_control(jid, account) if ctrl: + ctrl.no_autonegotiation = False ctrl.set_session(None) ctrl.contact = highest @@ -892,6 +894,11 @@ class Interface: jids = full_jid_with_resource.split('/', 1) jid = jids[0] + if array[1] == '503': + # If we get server-not-found error, stop sending chatstates + for contact in gajim.contacts.get_contacts(account, jid): + contact.composing_xep = False + session = None if len(array) > 5: session = array[5] @@ -3045,7 +3052,6 @@ class Interface: def on_open_chat_window(self, widget, contact, account, resource=None, session=None): - # Get the window containing the chat fjid = contact.jid @@ -3586,11 +3592,13 @@ class Interface: gajim.proxy65_manager = proxy65_manager.Proxy65Manager(gajim.idlequeue) gajim.default_session_type = ChatControlSession self.register_handlers() - if gajim.config.get('enable_zeroconf') and gajim.HAVE_ZEROCONF: + if gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'active') \ + and gajim.HAVE_ZEROCONF: gajim.connections[gajim.ZEROCONF_ACC_NAME] = \ connection_zeroconf.ConnectionZeroconf(gajim.ZEROCONF_ACC_NAME) for account in gajim.config.get_per('accounts'): - if not gajim.config.get_per('accounts', account, 'is_zeroconf'): + if not gajim.config.get_per('accounts', account, 'is_zeroconf') and \ + gajim.config.get_per('accounts', account, 'active'): gajim.connections[account] = common.connection.Connection(account) # gtk hooks diff --git a/src/groupchat_control.py b/src/groupchat_control.py index daaa32597..4b7962135 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -47,7 +47,8 @@ from chat_control import ChatControl from chat_control import ChatControlBase from common.exceptions import GajimGeneralException -from commands.implementation import PrivateChatCommands, GroupChatCommands +from command_system.implementation.hosts import PrivateChatCommands +from command_system.implementation.hosts import GroupChatCommands import logging log = logging.getLogger('gajim.groupchat_control') @@ -118,10 +119,12 @@ def tree_cell_data_func(column, renderer, model, iter_, tv=None): renderer.set_property('font', gtkgui_helpers.get_theme_font_for_option(theme, 'groupfont')) -class PrivateChatControl(ChatControl, PrivateChatCommands): +class PrivateChatControl(ChatControl): TYPE_ID = message_control.TYPE_PM - DISPATCHED_BY = PrivateChatCommands + # Set a command host to bound to. Every command given through a private chat + # will be processed with this command host. + COMMAND_HOST = PrivateChatCommands def __init__(self, parent_win, gc_contact, contact, account, session): room_jid = contact.jid.split('/')[0] @@ -185,10 +188,12 @@ class PrivateChatControl(ChatControl, PrivateChatCommands): self.session.negotiate_e2e(False) -class GroupchatControl(ChatControlBase, GroupChatCommands): +class GroupchatControl(ChatControlBase): TYPE_ID = message_control.TYPE_GC - DISPATCHED_BY = GroupChatCommands + # Set a command host to bound to. Every command given through a group chat + # will be processed with this command host. + COMMAND_HOST = GroupChatCommands def __init__(self, parent_win, contact, acct, is_continued=False): ChatControlBase.__init__(self, self.TYPE_ID, parent_win, @@ -842,7 +847,8 @@ class GroupchatControl(ChatControlBase, GroupChatCommands): small_attr, small_attr + ['restored_message'], small_attr + ['restored_message'], count_as_new=False, xhtml=xhtml) - def print_conversation(self, text, contact='', tim=None, xhtml=None): + def print_conversation(self, text, contact='', tim=None, xhtml=None, + graphics=True): '''Print a line in the conversation: if contact is set: it's a message from someone or an info message (contact = 'info' in such a case) @@ -905,7 +911,8 @@ class GroupchatControl(ChatControlBase, GroupChatCommands): self.check_and_possibly_add_focus_out_line() ChatControlBase.print_conversation_line(self, text, kind, contact, tim, - other_tags_for_name, [], other_tags_for_text, xhtml=xhtml) + other_tags_for_name, [], other_tags_for_text, xhtml=xhtml, + graphics=graphics) def get_nb_unread(self): type_events = ['printed_marked_gc_msg'] @@ -1203,7 +1210,7 @@ class GroupchatControl(ChatControlBase, GroupChatCommands): 'nick': nick, 'who': actor, 'reason': reason } - self.print_conversation(s, 'info', tim=tim) + self.print_conversation(s, 'info', tim=tim, graphics=False) if nick == self.nick and not gajim.config.get( 'muc_autorejoin_on_kick'): self.autorejoin = False @@ -1217,7 +1224,7 @@ class GroupchatControl(ChatControlBase, GroupChatCommands): 'nick': nick, 'who': actor, 'reason': reason } - self.print_conversation(s, 'info', tim=tim) + self.print_conversation(s, 'info', tim=tim, graphics=False) if nick == self.nick: self.autorejoin = False elif '303' in statusCode: # Someone changed his or her nick @@ -1277,23 +1284,23 @@ class GroupchatControl(ChatControlBase, GroupChatCommands): # remove 'TEST' os.remove(files[old_file]) os.rename(old_file, files[old_file]) - self.print_conversation(s, 'info', tim) + self.print_conversation(s, 'info', tim=tim, graphics=False) elif '321' in statusCode: s = _('%(nick)s has been removed from the room (%(reason)s)') % { 'nick': nick, 'reason': _('affiliation changed') } - self.print_conversation(s, 'info', tim=tim) + self.print_conversation(s, 'info', tim=tim, graphics=False) elif '322' in statusCode: s = _('%(nick)s has been removed from the room (%(reason)s)') % { 'nick': nick, 'reason': _('room configuration changed to members-only') } - self.print_conversation(s, 'info', tim=tim) + self.print_conversation(s, 'info', tim=tim, graphics=False) elif '332' in statusCode: s = _('%(nick)s has been removed from the room (%(reason)s)') % { 'nick': nick, 'reason': _('system shutdown') } - self.print_conversation(s, 'info', tim=tim) + self.print_conversation(s, 'info', tim=tim, graphics=False) elif 'destroyed' in statusCode: # Room has been destroyed - self.print_conversation(reason, 'info', tim) + self.print_conversation(reason, 'info', tim, graphics=False) if len(gajim.events.get_events(self.account, jid=fake_jid, types=['pm'])) == 0: @@ -1319,7 +1326,7 @@ class GroupchatControl(ChatControlBase, GroupChatCommands): # Server changed our nick self.nick = nick s = _('You are now known as %s') % nick - self.print_conversation(s, 'info', tim=tim) + self.print_conversation(s, 'info', tim=tim, graphics=False) iter_ = self.add_contact_to_roster(nick, show, role, affiliation, status, jid) newly_created = True @@ -1376,7 +1383,7 @@ class GroupchatControl(ChatControlBase, GroupChatCommands): 'affiliation': affiliation} if reason: st += ' (%s)' % reason - self.print_conversation(st, tim=tim) + self.print_conversation(st, tim=tim, graphics=False) right_changed = True actual_role = self.get_role(nick) if role != actual_role: @@ -1394,7 +1401,7 @@ class GroupchatControl(ChatControlBase, GroupChatCommands): 'nick': nick_jid, 'role': role} if reason: st += ' (%s)' % reason - self.print_conversation(st, tim=tim) + self.print_conversation(st, tim=tim, graphics=False) right_changed = True else: if gc_c.show == show and gc_c.status == status and \ @@ -1431,7 +1438,7 @@ class GroupchatControl(ChatControlBase, GroupChatCommands): if st: if status: st += ' (' + status + ')' - self.print_conversation(st, tim=tim) + self.print_conversation(st, tim=tim, graphics=False) def add_contact_to_roster(self, nick, show, role, affiliation, status, jid=''): @@ -1849,7 +1856,23 @@ class GroupchatControl(ChatControlBase, GroupChatCommands): start_iter.backward_chars(len(begin)) message_buffer.delete(start_iter, end_iter) - message_buffer.insert_at_cursor(self.nick_hits[0] + add) + completion = self.nick_hits[0] + # get a shell-like completion + # if there's more than one nick for this completion, complete only + # the part that all these nicks have in common + if gajim.config.get('shell_like_completion') and \ + len(self.nick_hits) > 1: + end = False + cur = '' + while not end: + cur = self.nick_hits[0][:len(cur)+1] + for nick in self.nick_hits: + if cur.lower() not in nick.lower(): + end = True + cur = cur[:-1] + completion = cur + add = "" # if nick is not complete, don't but any comma or so + message_buffer.insert_at_cursor(completion + add) self.last_key_tabs = True return True self.last_key_tabs = False diff --git a/src/history_window.py b/src/history_window.py index bd20605ae..2ad639fb7 100644 --- a/src/history_window.py +++ b/src/history_window.py @@ -372,9 +372,10 @@ class HistoryWindow: for line in lines: # line[0] is contact_name, line[1] is time of message # line[2] is kind, line[3] is show, line[4] is message - self._add_new_line(line[0], line[1], line[2], line[3], line[4]) + self._add_new_line(line[0], line[1], line[2], line[3], line[4], + line[5]) - def _add_new_line(self, contact_name, tim, kind, show, message): + def _add_new_line(self, contact_name, tim, kind, show, message, subject): '''add a new line in textbuffer''' if not message and kind not in (constants.KIND_STATUS, constants.KIND_GCSTATUS): @@ -408,6 +409,7 @@ class HistoryWindow: constants.KIND_CHAT_MSG_RECV): contact_name = self.completion_dict[self.jid][C_INFO_NAME] tag_name = 'incoming' + tag_msg = 'incomingtxt' elif kind in (constants.KIND_SINGLE_MSG_SENT, constants.KIND_CHAT_MSG_SENT): if self.account: @@ -418,6 +420,7 @@ class HistoryWindow: account = gajim.contacts.get_accounts()[0] contact_name = gajim.nicks[account] tag_name = 'outgoing' + tag_msg = 'outgoingtxt' elif kind == constants.KIND_GCSTATUS: # message here (if not None) is status message if message: @@ -457,7 +460,9 @@ class HistoryWindow: format = before_str + contact_name + after_str + ' ' buf.insert_with_tags_by_name(end_iter, format, tag_name) - message = message + '\n' + if subject: + message = _('Subject: %s\n') % subject + message + message += '\n' if tag_msg: self.history_textview.print_real_text(message, [tag_msg], name=contact_name) diff --git a/src/htmltextview.py b/src/htmltextview.py index 89082855a..1a25548a6 100644 --- a/src/htmltextview.py +++ b/src/htmltextview.py @@ -951,12 +951,15 @@ if __name__ == '__main__': ' World\n' '\n') htmlview.print_real_text(None, xhtml='
') + htmlview.print_real_text(None, xhtml='''

a:bGoogle


''') htmlview.print_real_text(None, xhtml=''' +

OMG, I'm green with envy!

+ ''') htmlview.print_real_text(None, xhtml='
') htmlview.print_real_text(None, xhtml=''' diff --git a/src/message_control.py b/src/message_control.py index 5c58e3730..8792a2896 100644 --- a/src/message_control.py +++ b/src/message_control.py @@ -182,7 +182,12 @@ class MessageControl: conn = gajim.connections[self.account] if not self.session: - sess = conn.find_controlless_session(jid) + if not resource: + if self.resource: + resource = self.resource + else: + resource = self.contact.resource + sess = conn.find_controlless_session(jid, resource=resource) if self.resource: jid += '/' + self.resource diff --git a/src/notify.py b/src/notify.py index 507794340..3d9eb6051 100644 --- a/src/notify.py +++ b/src/notify.py @@ -341,7 +341,7 @@ def popup(event_type, jid, account, msg_type='', path_to_image=None, if gajim.HAVE_INDICATOR and event_type in (_('New Message'), _('New Single Message'), _('New Private Message')): - indicator = indicate.IndicatorMessage() + indicator = indicate.Indicator() indicator.set_property('subtype', 'im') indicator.set_property('sender', jid) indicator.set_property('body', text) diff --git a/src/roster_window.py b/src/roster_window.py index c5eb1474e..c9201e4e7 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -849,7 +849,7 @@ class RosterWindow: self.remove_contact(jid, account, force=True, backend=True) return True - def rename_group(self, old_name, new_name): + def rename_group(self, old_name, new_name, account): """ rename a roster group """ @@ -2396,7 +2396,7 @@ class RosterWindow: else: # user is running svn helpers.exec_command('%s history_manager.py' % sys.executable) else: # Unix user - helpers.exec_command('%s history_manager.py &' % sys.executable) + helpers.exec_command('%s history_manager.py' % sys.executable) def on_info(self, widget, contact, account): '''Call vcard_information_window class to display contact's information''' @@ -2756,7 +2756,7 @@ class RosterWindow: win.show_title() elif row_type == 'group': # in C_JID column, we hold the group name (which is not escaped) - self.rename_group(old_text, new_text) + self.rename_group(old_text, new_text, account) def on_canceled(): if 'rename' in gajim.interface.instances: @@ -3510,6 +3510,28 @@ class RosterWindow: not gajim.config.get('quit_on_roster_x_button'): self.tooltip.hide_tooltip() self.window.hide() + elif event.state & gtk.gdk.CONTROL_MASK and event.keyval == gtk.keysyms.i: + treeselection = self.tree.get_selection() + model, list_of_paths = treeselection.get_selected_rows() + for path in list_of_paths: + type_ = model[path][C_TYPE] + if type_ in ('contact', 'agent'): + jid = model[path][C_JID].decode('utf-8') + account = model[path][C_ACCOUNT].decode('utf-8') + contact = gajim.contacts.get_first_contact_from_jid(account, jid) + self.on_info(widget, contact, account) + elif event.state & gtk.gdk.CONTROL_MASK and event.keyval == gtk.keysyms.h: + treeselection = self.tree.get_selection() + model, list_of_paths = treeselection.get_selected_rows() + if len(list_of_paths) != 1: + return + path = list_of_paths[0] + type_ = model[path][C_TYPE] + if type_ in ('contact', 'agent'): + jid = model[path][C_JID].decode('utf-8') + account = model[path][C_ACCOUNT].decode('utf-8') + contact = gajim.contacts.get_first_contact_from_jid(account, jid) + self.on_history(widget, contact, account) def on_roster_window_popup_menu(self, widget): event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) diff --git a/src/session.py b/src/session.py index 8f5617106..dec454c18 100644 --- a/src/session.py +++ b/src/session.py @@ -86,8 +86,9 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): '''dispatch a received stanza''' msg_type = msg.getType() subject = msg.getSubject() - if self.jid != full_jid_with_resource: - self.resource = gajim.get_nick_from_fjid(full_jid_with_resource) + resource = gajim.get_resource_from_jid(full_jid_with_resource) + if self.resource != resource: + self.resource = resource if self.control and self.control.resource: self.control.change_resource(self.resource) diff --git a/test/test_xmpp_transports_nb.py b/test/test_xmpp_transports_nb.py index 1b3073fa7..edc2f3ebb 100644 --- a/test/test_xmpp_transports_nb.py +++ b/test/test_xmpp_transports_nb.py @@ -37,7 +37,6 @@ class TestModuleLevelFunctions(unittest.TestCase): bosh_dict = {'bosh_content': u'text/xml; charset=utf-8', 'bosh_hold': 2, 'bosh_http_pipelining': False, - 'bosh_port': 5280, 'bosh_uri': u'http://gajim.org:5280/http-bind', 'bosh_useproxy': False, 'bosh_wait': 30, @@ -173,17 +172,16 @@ class TestNonBlockingTCP(AbstractTransportTest): def test_connect_disconnect_plain(self): ''' Establish plain connection ''' self.client.do_connect(establish_tls=False) - self.assert_(self.client.socket.state == 'CONNECTED') + self.assertEquals(self.client.socket.state, 'CONNECTED') self.client.do_disconnect() - self.assert_(self.client.socket.state == 'DISCONNECTED') + self.assertEquals(self.client.socket.state, 'DISCONNECTED') - # FIXME: testcase not working... - #def test_connect_disconnect_ssl(self): - # ''' Establish SSL (not TLS) connection ''' - # self.client.do_connect(establish_tls=True) - # self.assert_(self.client.socket.state == 'CONNECTED') - # self.client.do_disconnect() - # self.assert_(self.client.socket.state == 'DISCONNECTED') +# def test_connect_disconnect_ssl(self): +# ''' Establish SSL (not TLS) connection ''' +# self.client.do_connect(establish_tls=True) +# self.assertEquals(self.client.socket.state, 'CONNECTED') +# self.client.do_disconnect() +# self.assertEquals(self.client.socket.state, 'DISCONNECTED') def test_do_receive(self): ''' Test _do_receive method by overwriting socket.recv ''' @@ -259,12 +257,11 @@ class TestNonBlockingHTTP(AbstractTransportTest): ''' Test class for NonBlockingHTTP transport''' bosh_http_dict = { - 'http_uri': 'http://httpcm.jabber.org/webclient', - 'http_port': 1010, + 'http_uri': 'http://gajim.org:5280/http-bind', 'http_version': 'HTTP/1.1', 'http_persistent': True, 'add_proxy_headers': False - } + } def _get_transport(self, http_dict, proxy_dict=None): return transports_nb.NonBlockingHTTP( @@ -308,12 +305,22 @@ class TestNonBlockingHTTP(AbstractTransportTest): transport._on_receive(message) self.assertTrue(self.have_received_expected(), msg='Failed: In one go') - # try to receive in chunks - chunk1, chunk2, chunk3 = message[:20], message[20:73], message[73:] - nextmessage_chunk = "\r\n\r\nHTTP/1.1 200 OK\r\nContent-Type: text/x" - chunks = (chunk1, chunk2, chunk3, nextmessage_chunk) - - #TODO: BOSH implementatio ndoesn't support that for the moment +# FIXME: Not yet implemented. +# def test_receive_http_message_in_chunks(self): +# ''' Let _on_receive handle some chunked http messages ''' +# transport = self._get_transport(self.bosh_http_dict) +# +# header = ("HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=utf-8\r\n" + +# "Content-Length: 88\r\n\r\n") +# payload = "Please don't fail!" +# body = "%s" \ +# % payload +# message = "%s%s" % (header, body) +# +# chunk1, chunk2, chunk3 = message[:20], message[20:73], message[73:] +# nextmessage_chunk = "\r\n\r\nHTTP/1.1 200 OK\r\nContent-Type: text/x" +# chunks = (chunk1, chunk2, chunk3, nextmessage_chunk) +# # transport.onreceive(self.expect_receive(body, msg='Failed: In chunks')) # for chunk in chunks: # transport._on_receive(chunk)