merge default branch to jingle

This commit is contained in:
Thibaut GIRKA 2009-09-19 16:35:47 +02:00
commit 6b99db8980
25 changed files with 1662 additions and 619 deletions

View File

@ -1,5 +1,5 @@
AC_INIT([Gajim - A Jabber Instant Messager],
[0.12.5.1-dev],[http://trac.gajim.org/],[gajim])
[0.12.5.2-dev],[http://trac.gajim.org/],[gajim])
AC_PREREQ([2.59])
AC_CONFIG_HEADER(config.h)

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
<!--*- mode: xml -*-->
<?xml version="1.0"?>
<glade-interface>
<!-- interface-requires gtk+ 2.14 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkWindow" id="service_discovery_window">
<property name="border_width">6</property>
<property name="role">Service Discovery</property>
<property name="default_width">450</property>
<property name="default_width">550</property>
<property name="default_height">420</property>
<signal name="destroy" handler="on_service_discovery_window_destroy"/>
<child>
@ -28,6 +28,9 @@
Agent JID - node</property>
<property name="use_markup">True</property>
</widget>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkImage" id="banner_agent_icon">
@ -47,6 +50,7 @@ Agent JID - node</property>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
@ -55,22 +59,12 @@ Agent JID - node</property>
<property name="n_rows">3</property>
<property name="n_columns">3</property>
<property name="column_spacing">6</property>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<widget class="GtkComboBoxEntry" id="address_comboboxentry">
<property name="visible">True</property>
<property name="items" translatable="yes"></property>
<signal name="changed" handler="on_address_comboboxentry_changed"/>
<signal name="key_press_event" handler="on_address_comboboxentry_key_press_event"/>
<child internal-child="entry">
<widget class="GtkEntry" id="comboboxentry-entry1">
</widget>
</child>
</widget>
<packing>
<property name="left_attach">1</property>
@ -86,7 +80,7 @@ Agent JID - node</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="has_default">True</property>
<property name="response_id">0</property>
<property name="receives_default">False</property>
<signal name="clicked" handler="on_go_button_clicked"/>
<child>
<widget class="GtkAlignment" id="alignment93">
@ -105,6 +99,7 @@ Agent JID - node</property>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
@ -145,6 +140,12 @@ Agent JID - node</property>
<property name="y_options"></property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</widget>
<packing>
<property name="expand">False</property>
@ -155,9 +156,9 @@ Agent JID - node</property>
<widget class="GtkScrolledWindow" id="services_scrollwin">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_ETCHED_IN</property>
<property name="hscrollbar_policy">automatic</property>
<property name="vscrollbar_policy">automatic</property>
<property name="shadow_type">etched-in</property>
<child>
<widget class="GtkTreeView" id="services_treeview">
<property name="visible">True</property>
@ -184,6 +185,7 @@ Agent JID - node</property>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
@ -200,19 +202,20 @@ Agent JID - node</property>
<property name="spacing">6</property>
<child>
<widget class="GtkButton" id="close_button">
<property name="label">gtk-close</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_focus">True</property>
<property name="can_default">True</property>
<property name="label">gtk-close</property>
<property name="receives_default">False</property>
<property name="use_stock">True</property>
<property name="response_id">0</property>
<signal name="clicked" handler="on_close_button_clicked"/>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">GTK_PACK_END</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
</widget>

View File

@ -1,3 +1,3 @@
#!/bin/sh
cd "$(dirname $0)/src"
exec python -OOt gajim.py $@
exec python -Ot gajim.py $@

View File

@ -41,12 +41,17 @@ gajimsrc3dir = $(pkgdatadir)/src/common/zeroconf
gajimsrc3_PYTHON = \
$(srcdir)/common/zeroconf/*.py
gajimsrc4dir = $(pkgdatadir)/src/commands
gajimsrc4_PYTHON = \
$(srcdir)/commands/*.py
DISTCLEANFILES =
EXTRA_DIST = $(gajimsrc_PYTHON) \
$(gajimsrc1_PYTHON) \
$(gajimsrc2_PYTHON) \
$(gajimsrc3_PYTHON) \
$(gajimsrc4_PYTHON) \
eggtrayicon.c \
trayiconmodule.c \
eggtrayicon.h \

View File

@ -52,6 +52,8 @@ 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
from commands.implementation import CommonCommands, ChatCommands
try:
import gtkspell
HAS_GTK_SPELL = True
@ -75,11 +77,15 @@ if gajim.config.get('use_speller') and HAS_GTK_SPELL:
spell.set_language(langs[lang])
except OSError:
del langs[lang]
if spell:
spell.detach()
del tv
################################################################################
class ChatControlBase(MessageControl):
class ChatControlBase(MessageControl, CommonCommands):
'''A base class containing a banner, ConversationTextview, MessageTextView
'''
def make_href(self, match):
url_color = gajim.config.get('urlmsgcolor')
return '<a href="%s"><span color="%s">%s</span></a>' % (match.group(),
@ -146,7 +152,54 @@ class ChatControlBase(MessageControl):
event_keymod):
# Derived should implement this rather than connecting to the event
# itself.
pass
event = gtk.gdk.Event(gtk.gdk.KEY_PRESS)
event.keyval = event_keyval
event.state = event_keymod
event.time = 0
buffer = widget.get_buffer()
start, end = buffer.get_bounds()
if event.keyval -- gtk.keysyms.Tab:
position = buffer.get_insert()
end = buffer.get_iter_at_mark(position)
text = buffer.get_text(start, end, False)
text = text.decode('utf8')
splitted = text.split()
if (text.startswith(self.COMMAND_PREFIX) and not
text.startswith(self.COMMAND_PREFIX * 2) and len(splitted) == 1):
text = splitted[0]
bare = text.lstrip(self.COMMAND_PREFIX)
if len(text) == 1:
self.command_hits = []
for command in self.list_commands():
for name in command.names:
self.command_hits.append(name)
else:
if (self.last_key_tabs and self.command_hits and
self.command_hits[0].startswith(bare)):
self.command_hits.append(self.command_hits.pop(0))
else:
self.command_hits = []
for command in self.list_commands():
for name in command.names:
if name.startswith(bare):
self.command_hits.append(name)
if self.command_hits:
buffer.delete(start, end)
buffer.insert_at_cursor(self.COMMAND_PREFIX + self.command_hits[0] + ' ')
self.last_key_tabs = True
return True
self.last_key_tabs = False
def status_url_clicked(self, widget, url):
helpers.launch_browser_mailer('url', url)
@ -303,6 +356,9 @@ class ChatControlBase(MessageControl):
self.smooth = True
self.msg_textview.grab_focus()
self.command_hits = []
self.last_key_tabs = False
def set_speller(self):
# now set the one the user selected
per_type = 'contacts'
@ -604,45 +660,27 @@ class ChatControlBase(MessageControl):
self.drag_entered_conv = True
self.conv_textview.tv.set_editable(True)
def _process_command(self, message):
if not message or message[0] != '/':
return False
message = message[1:]
message_array = message.split(' ', 1)
command = message_array.pop(0).lower()
if message_array == ['']:
message_array = []
if command == 'clear' and not len(message_array):
self.conv_textview.clear() # clear conversation
self.clear(self.msg_textview) # clear message textview too
return True
elif message == 'compact' and not len(message_array):
self.chat_buttons_set_visible(not self.hide_chat_buttons)
self.clear(self.msg_textview)
return True
return False
def send_message(self, message, keyID='', type_='chat', chatstate=None,
msg_id=None, composing_xep=None, resource=None, process_command=True,
xhtml=None, callback=None, callback_args=[]):
msg_id=None, composing_xep=None, resource=None,
xhtml=None, callback=None, callback_args=[], process_commands=True):
'''Send the given message to the active tab. Doesn't return None if error
'''
if not message or message == '\n':
return None
if not process_command or not self._process_command(message):
MessageControl.send_message(self, message, keyID, type_=type_,
chatstate=chatstate, msg_id=msg_id, composing_xep=composing_xep,
resource=resource, user_nick=self.user_nick, xhtml=xhtml,
callback=callback, callback_args=callback_args)
if process_commands and self.process_as_command(message):
return
# Record message history
self.save_sent_message(message)
MessageControl.send_message(self, message, keyID, type_=type_,
chatstate=chatstate, msg_id=msg_id, composing_xep=composing_xep,
resource=resource, user_nick=self.user_nick, xhtml=xhtml,
callback=callback, callback_args=callback_args)
# Be sure to send user nickname only once according to JEP-0172
self.user_nick = None
# Record message history
self.save_sent_message(message)
# Be sure to send user nickname only once according to JEP-0172
self.user_nick = None
# Clear msg input
message_buffer = self.msg_textview.get_buffer()
@ -1126,7 +1164,7 @@ class ChatControlBase(MessageControl):
# FIXME: Set sensitivity for toolbar
################################################################################
class ChatControl(ChatControlBase):
class ChatControl(ChatControlBase, ChatCommands):
'''A control for standard 1-1 chat'''
(
AUDIO_STATE_NOT_AVAILABLE,
@ -1139,7 +1177,8 @@ class ChatControl(ChatControlBase):
TYPE_ID = message_control.TYPE_CHAT
old_msg_kind = None # last kind of the printed message
CHAT_CMDS = ['clear', 'compact', 'help', 'me', 'ping', 'say']
DISPATCHED_BY = ChatCommands
def __init__(self, parent_win, contact, acct, session, resource = None):
ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
@ -1490,6 +1529,19 @@ class ChatControl(ChatControlBase):
self._audio_image.set_from_stock(gtk.STOCK_DIALOG_WARNING, 1)
self.update_toolbar()
def change_resource(self, resource):
old_full_jid = self.get_full_jid()
self.resource = resource
new_full_jid = self.get_full_jid()
# update gajim.last_message_time
if old_full_jid in gajim.last_message_time[self.account]:
gajim.last_message_time[self.account][new_full_jid] = \
gajim.last_message_time[self.account][old_full_jid]
# update events
gajim.events.change_jid(self.account, old_full_jid, new_full_jid)
# update MessageWindow._controls
self.parent_win.change_jid(self.account, old_full_jid, new_full_jid)
def set_audio_state(self, state, sid=None, reason=None):
if state in ('connecting', 'connected', 'stop'):
str = _('Audio state : %s') % state
@ -1813,83 +1865,12 @@ class ChatControl(ChatControlBase):
elif self.session and self.session.enable_encryption:
dialogs.ESessionInfoWindow(self.session)
def _process_command(self, message):
if message[0] != '/':
return False
# Handle common commands
if ChatControlBase._process_command(self, message):
return True
message = message[1:]
message_array = message.split(' ', 1)
command = message_array.pop(0).lower()
if message_array == ['']:
message_array = []
if command == 'me':
if len(message_array):
return False # /me is not really a command
else:
self.get_command_help(command)
return True # do not send "/me" as message
if command == 'help':
if len(message_array):
subcommand = message_array.pop(0)
self.get_command_help(subcommand)
else:
self.get_command_help(command)
self.clear(self.msg_textview)
return True
elif command == 'ping':
if not len(message_array):
if self.account == gajim.ZEROCONF_ACC_NAME:
self.print_conversation(
_('Command not supported for zeroconf account.'), 'info')
else:
gajim.connections[self.account].sendPing(self.contact)
else:
self.get_command_help(command)
self.clear(self.msg_textview)
return True
return False
def get_command_help(self, command):
if command == 'help':
self.print_conversation(_('Commands: %s') % ChatControl.CHAT_CMDS,
'info')
elif command == 'clear':
self.print_conversation(_('Usage: /%s, clears the text window.') % \
command, 'info')
elif command == 'compact':
self.print_conversation(_('Usage: /%s, hide the chat buttons.') % \
command, 'info')
elif command == 'me':
self.print_conversation(_('Usage: /%(command)s <action>, sends action '
'to the current group chat. Use third person. (e.g. /%(command)s '
'explodes.)'
) % {'command': command}, 'info')
elif command == 'ping':
self.print_conversation(_('Usage: /%s, sends a ping to the contact') %\
command, 'info')
elif command == 'say':
self.print_conversation(_('Usage: /%s, send the message to the contact') %\
command, 'info')
else:
self.print_conversation(_('No help info for /%s') % command, 'info')
def send_message(self, message, keyID='', chatstate=None, xhtml=None):
def send_message(self, message, keyID='', chatstate=None, xhtml=None,
process_commands=True):
'''Send a message to contact'''
if message in ('', None, '\n') or self._process_command(message):
if message in ('', None, '\n'):
return None
# Do we need to process command for the message ?
process_command = True
if message.startswith('/say'):
message = message[5:]
process_command = False
# refresh timers
self.reset_kbd_mouse_timeout_vars()
@ -1948,8 +1929,9 @@ class ChatControl(ChatControlBase):
ChatControlBase.send_message(self, message, keyID, type_='chat',
chatstate=chatstate_to_send, composing_xep=composing_xep,
process_command=process_command, xhtml=xhtml, callback=_on_sent,
callback_args=[contact, message, encrypted, xhtml])
xhtml=xhtml, callback=_on_sent,
callback_args=[contact, message, encrypted, xhtml],
process_commands=process_commands)
def check_for_possible_paused_chatstate(self, arg):
''' did we move mouse of that window or write something in message
@ -2428,6 +2410,10 @@ class ChatControl(ChatControlBase):
self.handlers[i].disconnect(i)
del self.handlers[i]
self.conv_textview.del_handlers()
if gajim.config.get('use_speller') and HAS_GTK_SPELL:
spell_obj = gtkspell.get_from_text_view(self.msg_textview)
if spell_obj:
spell_obj.detach()
self.msg_textview.destroy()
def minimizable(self):

20
src/commands/__init__.py Normal file
View File

@ -0,0 +1,20 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
"""
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.
"""

88
src/commands/custom.py Normal file
View File

@ -0,0 +1,88 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
"""
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..."

764
src/commands/framework.py Normal file
View File

@ -0,0 +1,764 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
"""
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'(?<!\n)\n(?!\n)', re.MULTILINE)
ARG_USAGE_PATTERN = 'Usage: %s %s'
def __init__(self, handler, usage, source, raw, extra, overlap, empty, expand_short):
self.handler = handler
self.usage = usage
self.source = source
self.raw = raw
self.extra = extra
self.overlap = overlap
self.empty = empty
self.expand_short = expand_short
def __call__(self, *args, **kwargs):
try:
return self.handler(*args, **kwargs)
except CommandError, exception:
# Re-raise an excepttion with a proper command attribute set,
# unless it is already set by the one who raised an exception.
if not exception.command and not exception.name:
raise CommandError(exception.message, self)
# Do not forget to re-raise an exception just like it was if at
# least either, command or name attribute is set properly.
raise
# This one is a little bit too wide, but as Python does not have
# anything more constrained - there is no other choice. Take a look here
# if command complains about invalid arguments while they are ok.
except TypeError:
raise CommandError("Command received invalid arguments", self)
def __repr__(self):
return "<Command %s>" % ', '.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<name>\w+)_')
SAFE_NAME_SUBS_PATTERN = '_%s_'
# Quite complex piece of regular expression logic.
ARG_PATTERN = re.compile(r'(\'|")?(?P<body>(?(1).+?|\S+))(?(1)\1)')
OPT_PATTERN = re.compile(r'(?<!\w)--?(?P<key>[\w-]+)(?:(?:=|\s)(\'|")?(?P<value>(?(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:
<arg1, arg2> <<extra>> [-(-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))]
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
text = text[len(self.COMMAND_PREFIX):]
text = text.strip()
parts = text.split(' ', 1)
name, arguments = parts if len(parts) > 1 else (parts[0], None)
flag = self.looks_like_command(text, name, arguments)
if flag is not None:
return flag
self.execute_command(name, arguments)
return True
def execute_command(self, 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

View File

@ -0,0 +1,262 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
"""
Provides an actual implementation of the standard commands.
"""
import dialogs
from common import gajim
from common import helpers
from common.exceptions import GajimGeneralException
from framework import command, CommandError
from middleware import ChatMiddleware
class CommonCommands(ChatMiddleware):
"""
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.
"""
@command
def clear(self):
"""
Clear the text window
"""
self.conv_textview.clear()
@command
def compact(self):
"""
Hide the chat buttons
"""
self.chat_buttons_set_visible(not self.hide_chat_buttons)
@command(overlap=True)
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)
doc = _(command.extract_doc())
usage = command.extract_arg_usage()
if doc:
return (doc + '\n\n' + usage) if command.usage else doc
else:
return usage
elif all:
for command in self.list_commands():
names = ', '.join(command.names)
description = command.extract_description()
self.echo("%s - %s" % (names, description))
else:
self.echo(self._help_(self, 'help'))
@command(raw=True)
def say(self, message):
"""
Send a message to the contact
"""
self.send(message)
@command(raw=True)
def me(self, action):
"""
Send action (in the third person) to the current chat
"""
self.send("/me %s" % action)
class ChatCommands(CommonCommands):
"""
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.
"""
DISPATCH = True
INHERIT = True
@command
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):
"""
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.
"""
DISPATCH = True
INHERIT = True
class GroupChatCommands(CommonCommands):
"""
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.
"""
DISPATCH = True
INHERIT = True
@command(raw=True)
def nick(self, new_nick):
"""
Change your nickname in a group chat
"""
try:
new_nick = helpers.parse_resource(new_nick)
except Exception:
raise CommandError(_("Invalid nickname"))
self.connection.join_gc(new_nick, self.room_jid, None, change_nick=True)
self.new_nick = new_nick
@command('query', raw=True)
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)
else:
raise CommandError(_("Nickname not found"))
@command('msg', raw=True)
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)
else:
raise CommandError(_("Nickname not found"))
@command(raw=True, empty=True)
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)
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)
def join(self, jid, nick):
"""
Join a group chat given by a jid, optionally using given nickname
"""
if not nick:
nick = self.nick
if '@' not in jid:
jid = jid + '@' + gajim.get_server_from_jid(self.room_jid)
try:
gajim.interface.instances[self.account]['join_gc'].window.present()
except KeyError:
try:
dialogs.JoinGroupchatWindow(account=None, room_jid=jid, nick=nick)
except GajimGeneralException:
pass
@command('part', 'close', raw=True, empty=True)
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
If given nickname is not found it will be treated as a jid.
"""
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)
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
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)
# First we do alpha-numeric sort and then role-based one.
nicks.sort()
nicks.sort(key=lambda nick: get_contact(nick).role)
if verbose:
for nick in nicks:
contact = get_contact(nick)
role = helpers.get_uf_role(contact.role)
affiliation = helpers.get_uf_affiliation(contact.affiliation)
self.echo("%s - %s - %s" % (nick, role, affiliation))
else:
return ', '.join(nicks)
@command(raw=True)
def block(self, who):
"""
Forbid an occupant to send you public or private messages
"""
self.on_block(None, who)
@command(raw=True)
def unblock(self, who):
"""
Allow an occupant to send you public or privates messages
"""
self.on_unblock(None, who)

105
src/commands/middleware.py Normal file
View File

@ -0,0 +1,105 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
"""
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.
"""
from common import gajim
from types import StringTypes
from framework import CommandProcessor, CommandError
from traceback import print_exc
class ChatMiddleware(CommandProcessor):
"""
Provides basic scaffolding for the convenient interaction with ChatControl.
Also provides some few basic utilities for the same purpose.
"""
def process_as_command(self, text):
try:
return super(ChatMiddleware, self).process_as_command(text)
except CommandError, exception:
self.echo("%s: %s" %(exception.name, exception.message), 'error')
return True
except Exception:
self.echo("An error occured while trying to execute the command", 'error')
print_exc()
return True
finally:
self.add_history(text)
self.clear_input()
def looks_like_command(self, text, 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)
return True
def command_preprocessor(self, name, command, arguments, args, kwargs):
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))
return True
def command_postprocessor(self, name, command, arguments, args, kwargs, value):
if value and isinstance(value, StringTypes):
self.echo(value)
def echo(self, text, kind='info'):
"""
Print given text to the user.
"""
self.print_conversation(str(text), kind)
def send(self, text):
"""
Send a message to the contact.
"""
self.send_message(text, process_commands=False)
def set_input(self, text):
"""
Set given text into the input.
"""
message_buffer = self.msg_textview.get_buffer()
message_buffer.set_text(text)
def clear_input(self):
"""
Clear input.
"""
self.set_input(str())
def add_history(self, text):
"""
Add given text to the input history, so user can scroll through it
using ctrl + up/down arrow keys.
"""
self.save_sent_message(text)
@property
def connection(self):
"""
Get the current connection object.
"""
return gajim.connections[self.account]

View File

@ -67,7 +67,6 @@ class Config:
__options = {
# name: [ type, default_value, help_string ]
'verbose': [ opt_bool, False, '', True ],
'alwaysauth': [ opt_bool, False ],
'autopopup': [ opt_bool, False ],
'notify_on_signin': [ opt_bool, True ],
'notify_on_signout': [ opt_bool, False ],
@ -249,6 +248,8 @@ class Config:
'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 ],
'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.')],
'confirm_custom_status': [ opt_str, '', _('Should we show the confirm custom status dialog or not? Empty string means we never show the dialog.')],
'enable_negative_priority': [ opt_bool, False, _('If True, you will be able to set a negative priority to your account in account modification window. BE CAREFUL, when you are logged in with a negative priority, you will NOT receive any message from your server.')],
'use_gnomekeyring': [opt_bool, True, _('If True, Gajim will use Gnome Keyring (if available) to store account passwords.')],
'use_kwalletcli': [opt_bool, True, _('If True, Gajim will use KDE Wallet (if kwalletcli is available) to store account passwords.')],
@ -285,6 +286,7 @@ class Config:
'autoconnect_as': [ opt_str, 'online', _('Status used to autoconnect as. Can be online, chat, away, xa, dnd, invisible. NOTE: this option is used only if restore_last_status is disabled'), True ],
'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],
'proxy': [ opt_str, '', '', True ],
'keyid': [ opt_str, '', '', True ],

View File

@ -219,7 +219,7 @@ class Connection(ConnectionHandlers):
# We are doing disconnect at so many places, better use one function in all
def disconnect(self, on_purpose=False):
gajim.interface.roster.music_track_changed(None, None, self.name)
gajim.interface.music_track_changed(None, None, self.name)
self.on_purpose = on_purpose
self.connected = 0
self.time_to_reconnect = None
@ -1199,7 +1199,7 @@ class Connection(ConnectionHandlers):
msgenc = ''
if session:
fjid = str(session.jid)
fjid = session.get_to()
if keyID and self.USE_GPG:
xhtml = None
@ -1953,8 +1953,15 @@ class Connection(ConnectionHandlers):
hostname = gajim.config.get_per('accounts', self.name, 'hostname')
iq = common.xmpp.Iq(typ = 'set', to = hostname)
iq.setTag(common.xmpp.NS_REGISTER + ' query').setTag('remove')
con.send(iq)
on_remove_success(True)
def _on_answer(result):
if result.getType() == 'result':
on_remove_success(True)
return
self.dispatch('ERROR', (_('Unregister failed'),
_('Unregistration with server %(server)s failed: %(error)s') \
% {'server': hostname, 'error': result.getErrorMsg()}))
on_remove_success(False)
con.SendAndCallForResponse(iq, _on_answer)
return
on_remove_success(False)
if self.connected == 0:

View File

@ -900,8 +900,8 @@ class ConnectionDisco:
track = listener.get_playing_track()
if gajim.config.get_per('accounts', self.name,
'publish_tune'):
gajim.interface.roster.music_track_changed(listener,
track, self.name)
gajim.interface.music_track_changed(listener, track,
self.name)
break
if features.__contains__(common.xmpp.NS_VCARD):
self.vcard_supported = True
@ -2355,13 +2355,14 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
if ptype == 'subscribe':
log.debug('subscribe request from %s' % who)
if gajim.config.get('alwaysauth') or who.find("@") <= 0 or \
jid_stripped in self.jids_for_auto_auth or transport_auto_auth:
if gajim.config.get_per('accounts', self.name, 'autoauth') or \
who.find('@') <= 0 or jid_stripped in self.jids_for_auto_auth or \
transport_auto_auth:
if self.connection:
p = common.xmpp.Presence(who, 'subscribed')
p = self.add_sha(p)
self.connection.send(p)
if who.find("@") <= 0 or transport_auto_auth:
if who.find('@') <= 0 or transport_auto_auth:
self.dispatch('NOTIFY', (jid_stripped, 'offline', 'offline',
resource, prio, keyID, timestamp, None))
if transport_auto_auth:
@ -2617,9 +2618,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
if sign_msg and not signed:
signed = self.get_signed_presence(msg)
if signed is None:
self.dispatch('ERROR', (_('OpenPGP passphrase was not given'),
#%s is the account name here
_('You will be connected to %s without OpenPGP.') % self.name))
self.dispatch('BAD_PASSPHRASE', ())
self.USE_GPG = False
signed = ''
self.connected = gajim.SHOW_LIST.index(show)

View File

@ -27,7 +27,7 @@ docdir = '../'
datadir = '../'
localedir = '../po'
version = '0.12.5.1-dev'
version = '0.12.5.2-dev'
import sys, os.path
for base in ('.', 'common'):

View File

@ -114,6 +114,12 @@ def latex_to_image(str_):
result = None
exitcode = 0
try:
bg_str, fg_str = gajim.interface.get_bg_fg_colors()
except:
# interface may not be available when we test latext at startup
bg_str, fg_str = 'rgb 1.0 1.0 1.0', 'rgb 0.0 0.0 0.0'
# filter latex code with bad commands
if check_blacklist(str_):
# we triggered the blacklist, immediately return None
@ -131,7 +137,7 @@ def latex_to_image(str_):
if exitcode == 0:
# convert dvi to png
latex_png_dpi = gajim.config.get('latex_png_dpi')
exitcode = try_run(['dvipng', '-bg', 'rgb 1.0 1.0 1.0', '-T',
exitcode = try_run(['dvipng', '-bg', bg_str, '-fg', fg_str, '-T',
'tight', '-D', latex_png_dpi, tmpfile + '.dvi', '-o',
tmpfile + '.png'])

View File

@ -202,6 +202,8 @@ class OptionsParser:
self.update_config_to_01231()
if old < [0, 12, 5, 1] and new >= [0, 12, 5, 1]:
self.update_config_to_01251()
if old < [0, 12, 5, 2] and new >= [0, 12, 5, 2]:
self.update_config_to_01252()
gajim.logger.init_vars()
gajim.config.set('version', new_version)
@ -727,4 +729,11 @@ class OptionsParser:
con.close()
gajim.config.set('version', '0.12.5.1')
def update_config_to_01252(self):
if 'alwaysauth' in self.old_values:
val = self.old_values['alwaysauth']
for account in gajim.config.get_per('accounts'):
gajim.config.set_per('accounts', account, 'autoauth', val)
gajim.config.set('version', '0.12.5.2')
# vim: se ts=3:

View File

@ -54,6 +54,7 @@ class StanzaSession(object):
self.conn = conn
self.jid = jid
self.type = type_
self.resource = None
if thread_id:
self.received_thread_id = True
@ -75,6 +76,12 @@ class StanzaSession(object):
def is_loggable(self):
return self.loggable and gajim.config.should_log(self.conn.name, self.jid)
def get_to(self):
to = str(self.jid)
if self.resource:
to += '/' + self.resource
return to
def remove_events(self, types):
'''
Remove events associated with this session from the queue.
@ -107,7 +114,7 @@ class StanzaSession(object):
if self.thread_id:
msg.NT.thread = self.thread_id
msg.setAttr('to', self.jid)
msg.setAttr('to', self.get_to())
self.conn.send_stanza(msg)
if isinstance(msg, xmpp.Message):

View File

@ -1077,6 +1077,7 @@ class ManageProxiesWindow:
self.proxytype_combobox = self.xml.get_widget('proxytype_combobox')
self.init_list()
self.block_signal = False
self.xml.signal_autoconnect(self)
self.window.show_all()
# hide the BOSH fields by default
@ -1134,6 +1135,7 @@ class ManageProxiesWindow:
iter_ = model.append()
model.set(iter_, 0, 'proxy' + unicode(i))
gajim.config.add_per('proxies', 'proxy' + unicode(i))
self.proxies_treeview.set_cursor(model.get_path(iter_))
def on_remove_proxy_button_clicked(self, widget):
(model, iter_) = self.proxies_treeview.get_selection().get_selected()
@ -1143,11 +1145,16 @@ class ManageProxiesWindow:
model.remove(iter_)
gajim.config.del_per('proxies', proxy)
self.xml.get_widget('remove_proxy_button').set_sensitive(False)
self.block_signal = True
self.on_proxies_treeview_cursor_changed(self.proxies_treeview)
self.block_signal = False
def on_close_button_clicked(self, widget):
self.window.destroy()
def on_useauth_checkbutton_toggled(self, widget):
if self.block_signal:
return
act = widget.get_active()
proxy = self.proxyname_entry.get_text().decode('utf-8')
gajim.config.set_per('proxies', proxy, 'useauth', act)
@ -1155,6 +1162,8 @@ class ManageProxiesWindow:
self.xml.get_widget('proxypass_entry').set_sensitive(act)
def on_boshuseproxy_checkbutton_toggled(self, widget):
if self.block_signal:
return
act = widget.get_active()
proxy = self.proxyname_entry.get_text().decode('utf-8')
gajim.config.set_per('proxies', proxy, 'bosh_useproxy', act)
@ -1164,11 +1173,6 @@ class ManageProxiesWindow:
def on_proxies_treeview_cursor_changed(self, widget):
#FIXME: check if off proxy settings are correct (see
# http://trac.gajim.org/changeset/1921#file2 line 1221
(model, iter_) = widget.get_selection().get_selected()
if not iter_:
return
proxy = model[iter_][0]
self.xml.get_widget('proxyname_entry').set_text(proxy)
proxyhost_entry = self.xml.get_widget('proxyhost_entry')
proxyport_entry = self.xml.get_widget('proxyport_entry')
proxyuser_entry = self.xml.get_widget('proxyuser_entry')
@ -1176,6 +1180,7 @@ class ManageProxiesWindow:
boshuri_entry = self.xml.get_widget('boshuri_entry')
useauth_checkbutton = self.xml.get_widget('useauth_checkbutton')
boshuseproxy_checkbutton = self.xml.get_widget('boshuseproxy_checkbutton')
self.block_signal = True
proxyhost_entry.set_text('')
proxyport_entry.set_text('')
proxyuser_entry.set_text('')
@ -1188,6 +1193,17 @@ class ManageProxiesWindow:
#useauth_checkbutton.set_active(False)
#self.on_useauth_checkbutton_toggled(useauth_checkbutton)
(model, iter_) = widget.get_selection().get_selected()
if not iter_:
self.xml.get_widget('proxyname_entry').set_text('')
self.xml.get_widget('proxytype_combobox').set_sensitive(False)
self.xml.get_widget('proxy_table').set_sensitive(False)
self.block_signal = False
return
proxy = model[iter_][0]
self.xml.get_widget('proxyname_entry').set_text(proxy)
if proxy == _('None'): # special proxy None
self.show_bosh_fields(False)
self.proxyname_entry.set_editable(False)
@ -1219,12 +1235,15 @@ class ManageProxiesWindow:
gajim.config.get_per('proxies', proxy, 'bosh_useproxy'))
useauth_checkbutton.set_active(
gajim.config.get_per('proxies', proxy, 'useauth'))
self.block_signal = False
def on_proxies_treeview_key_press_event(self, widget, event):
if event.keyval == gtk.keysyms.Delete:
self.on_remove_proxy_button_clicked(widget)
def on_proxyname_entry_changed(self, widget):
if self.block_signal:
return
(model, iter_) = self.proxies_treeview.get_selection().get_selected()
if not iter_:
return
@ -1243,6 +1262,8 @@ class ManageProxiesWindow:
model.set_value(iter_, 0, new_name)
def on_proxytype_combobox_changed(self, widget):
if self.block_signal:
return
types = ['http', 'socks5', 'bosh']
type_ = self.proxytype_combobox.get_active()
self.show_bosh_fields(types[type_]=='bosh')
@ -1250,26 +1271,36 @@ class ManageProxiesWindow:
gajim.config.set_per('proxies', proxy, 'type', types[type_])
def on_proxyhost_entry_changed(self, widget):
if self.block_signal:
return
value = widget.get_text().decode('utf-8')
proxy = self.proxyname_entry.get_text().decode('utf-8')
gajim.config.set_per('proxies', proxy, 'host', value)
def on_proxyport_entry_changed(self, widget):
if self.block_signal:
return
value = widget.get_text().decode('utf-8')
proxy = self.proxyname_entry.get_text().decode('utf-8')
gajim.config.set_per('proxies', proxy, 'port', value)
def on_proxyuser_entry_changed(self, widget):
if self.block_signal:
return
value = widget.get_text().decode('utf-8')
proxy = self.proxyname_entry.get_text().decode('utf-8')
gajim.config.set_per('proxies', proxy, 'user', value)
def on_boshuri_entry_changed(self, widget):
if self.block_signal:
return
value = widget.get_text().decode('utf-8')
proxy = self.proxyname_entry.get_text().decode('utf-8')
gajim.config.set_per('proxies', proxy, 'bosh_uri', value)
def on_proxypass_entry_changed(self, widget):
if self.block_signal:
return
value = widget.get_text().decode('utf-8')
proxy = self.proxyname_entry.get_text().decode('utf-8')
gajim.config.set_per('proxies', proxy, 'pass', value)

View File

@ -1698,7 +1698,8 @@ class ChangeNickDialog(InputDialogCheck):
if len(self.room_queue) == 0:
self.cancel_handler = None
self.dialog.destroy()
del gajim.interface.instances['change_nick_dialog']
if 'change_nick_dialog' in gajim.interface.instances:
del gajim.interface.instances['change_nick_dialog']
return
self.account, self.room_jid, self.prompt = self.room_queue.pop(0)
self.setup_dialog()

View File

@ -1137,11 +1137,11 @@ class ToplevelAgentBrowser(AgentBrowser):
# Icon Renderer
renderer = gtk.CellRendererPixbuf()
renderer.set_property('xpad', 6)
col.pack_start(renderer, expand = False)
col.pack_start(renderer, expand=False)
col.set_cell_data_func(renderer, self._pixbuf_renderer_data_func)
# Text Renderer
renderer = gtk.CellRendererText()
col.pack_start(renderer, expand = True)
col.pack_start(renderer, expand=True)
col.set_cell_data_func(renderer, self._text_renderer_data_func)
renderer.set_property('foreground', 'dark gray')
# Save this so we can go along with theme changes
@ -1487,7 +1487,7 @@ class ToplevelAgentBrowser(AgentBrowser):
if not cat:
cat = self._create_category(*cat_args)
self.model.append(cat, (jid, node, pix, descr, 1))
self._expand_all()
gobject.idle_add(self._expand_all)
# Grab info on the service
self.cache.get_info(jid, node, self._agent_info, force=force)
self._update_progressbar()

View File

@ -156,6 +156,7 @@ except exceptions.DatabaseMalformed:
else:
from common import dbus_support
if dbus_support.supported:
from music_track_listener import MusicTrackListener
import dbus
if os.name == 'posix': # dl module is Unix Only
@ -229,6 +230,15 @@ from chat_control import ChatControlBase
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
@ -243,6 +253,7 @@ from common import helpers
from common import optparser
from common import dataforms
from common import passwords
from common import pep
gajimpaths = common.configpaths.gajimpaths
@ -1507,10 +1518,15 @@ class Interface:
if use_gpg_agent:
sectext = _('You configured Gajim to use GPG agent, but there is no '
'GPG agent running or it returned a wrong passphrase.\n')
sectext += _('You are currently connected without your OpenPGP key.')
sectext += _('You are currently connected without your OpenPGP key.')
dialogs.WarningDialog(_('Your passphrase is incorrect'), sectext)
else:
path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'warning.png')
notify.popup('warning', account, account, 'warning', path,
_('OpenGPG Passphrase Incorrect'),
_('You are currently connected without your OpenPGP key.'))
keyID = gajim.config.get_per('accounts', account, 'keyid')
self.forget_gpg_passphrase(keyID)
dialogs.WarningDialog(_('Your passphrase is incorrect'), sectext)
def handle_event_gpg_password_required(self, account, array):
#('GPG_PASSWORD_REQUIRED', account, (callback,))
@ -3048,6 +3064,93 @@ class Interface:
### Other Methods
################################################################################
def _change_awn_icon_status(self, status):
if not dbus_support.supported:
# do nothing if user doesn't have D-Bus bindings
return
try:
bus = dbus.SessionBus()
if not 'com.google.code.Awn' in bus.list_names():
# Awn is not installed
return
except Exception:
return
iconset = gajim.config.get('iconset')
prefix = os.path.join(helpers.get_iconset_path(iconset), '32x32')
if status in ('chat', 'away', 'xa', 'dnd', 'invisible', 'offline'):
status = status + '.png'
elif status == 'online':
prefix = os.path.join(gajim.DATA_DIR, 'pixmaps')
status = 'gajim.png'
path = os.path.join(prefix, status)
try:
obj = bus.get_object('com.google.code.Awn', '/com/google/code/Awn')
awn = dbus.Interface(obj, 'com.google.code.Awn')
awn.SetTaskIconByName('Gajim', os.path.abspath(path))
except Exception:
pass
def enable_music_listener(self):
if not self.music_track_changed_signal:
listener = MusicTrackListener.get()
self.music_track_changed_signal = listener.connect(
'music-track-changed', self.music_track_changed)
track = listener.get_playing_track()
self.music_track_changed(listener, track)
def disable_music_listener(self):
listener = MusicTrackListener.get()
listener.disconnect(self.music_track_changed_signal)
self.music_track_changed_signal = None
def music_track_changed(self, unused_listener, music_track_info, account=''):
if account == '':
accounts = gajim.connections.keys()
else:
accounts = [account]
if music_track_info is None:
artist = ''
title = ''
source = ''
elif hasattr(music_track_info, 'paused') and music_track_info.paused == 0:
artist = ''
title = ''
source = ''
else:
artist = music_track_info.artist
title = music_track_info.title
source = music_track_info.album
for acct in accounts:
if acct not in gajim.connections:
continue
if not gajim.account_is_connected(acct):
continue
if not gajim.connections[acct].pep_supported:
continue
if gajim.connections[acct].music_track_info == music_track_info:
continue
pep.user_send_tune(acct, artist, title, source)
gajim.connections[acct].music_track_info = music_track_info
def get_bg_fg_colors(self):
def gdkcolor_to_rgb (gdkcolor):
return [c / 65535. for c in (gdkcolor.red, gdkcolor.green,
gdkcolor.blue)]
def format_rgb (r, g, b):
return ' '.join([str(c) for c in ('rgb', r, g, b)])
def format_gdkcolor (gdkcolor):
return format_rgb (*gdkcolor_to_rgb (gdkcolor))
# get style colors and create string for dvipng
dummy = gtk.Invisible()
dummy.ensure_style()
style = dummy.get_style()
bg_str = format_gdkcolor(style.base[gtk.STATE_NORMAL])
fg_str = format_gdkcolor(style.text[gtk.STATE_NORMAL])
return (bg_str, fg_str)
def read_sleepy(self):
'''Check idle status and change that status if needed'''
if not self.sleeper.poll():
@ -3603,6 +3706,12 @@ class Interface:
except Exception:
pass
gobject.timeout_add_seconds(5, remote_init)
self.music_track_changed_signal = None
for account in gajim.connections:
if gajim.config.get_per('accounts', account, 'publish_tune') and \
dbus_support.supported:
self.enable_music_listener()
break
if __name__ == '__main__':
def sigint_cb(num, stack):

View File

@ -47,6 +47,8 @@ from chat_control import ChatControl
from chat_control import ChatControlBase
from common.exceptions import GajimGeneralException
from commands.implementation import PrivateChatCommands, GroupChatCommands
import logging
log = logging.getLogger('gajim.groupchat_control')
@ -116,9 +118,11 @@ 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):
class PrivateChatControl(ChatControl, PrivateChatCommands):
TYPE_ID = message_control.TYPE_PM
DISPATCHED_BY = PrivateChatCommands
def __init__(self, parent_win, gc_contact, contact, account, session):
room_jid = contact.jid.split('/')[0]
room_ctrl = gajim.interface.msg_win_mgr.get_gc_control(room_jid, account)
@ -132,7 +136,7 @@ class PrivateChatControl(ChatControl):
ChatControl.__init__(self, parent_win, contact, account, session)
self.TYPE_ID = 'pm'
def send_message(self, message, xhtml=None):
def send_message(self, message, xhtml=None, process_commands=True):
'''call this function to send our message'''
if not message:
return
@ -158,7 +162,8 @@ class PrivateChatControl(ChatControl):
'left.') % {'room': room, 'nick': nick})
return
ChatControl.send_message(self, message, xhtml=xhtml)
ChatControl.send_message(self, message, xhtml=xhtml,
process_commands=process_commands)
def update_ui(self):
if self.contact.show == 'offline':
@ -180,12 +185,10 @@ class PrivateChatControl(ChatControl):
self.session.negotiate_e2e(False)
class GroupchatControl(ChatControlBase):
class GroupchatControl(ChatControlBase, GroupChatCommands):
TYPE_ID = message_control.TYPE_GC
# alphanum sorted
MUC_CMDS = ['ban', 'block', 'chat', 'query', 'clear', 'close', 'compact',
'help', 'invite', 'join', 'kick', 'leave', 'me', 'msg', 'nick',
'part', 'names', 'say', 'topic', 'unblock']
DISPATCHED_BY = GroupChatCommands
def __init__(self, parent_win, contact, acct, is_continued=False):
ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
@ -281,7 +284,6 @@ class GroupchatControl(ChatControlBase):
self.attention_list = []
self.room_creation = int(time.time()) # Use int to reduce mem usage
self.nick_hits = []
self.cmd_hits = []
self.last_key_tabs = False
self.subject = ''
@ -1510,267 +1512,14 @@ class GroupchatControl(ChatControlBase):
if model.iter_n_children(parent_iter) == 0:
model.remove(parent_iter)
def _process_command(self, message):
if message[0] != '/':
return False
# Handle common commands
if ChatControlBase._process_command(self, message):
return True
message = message[1:]
message_array = message.split(' ', 1)
command = message_array.pop(0).lower()
if message_array == ['']:
message_array = []
if command == 'me':
return False # This is not really a command
if command == 'nick':
# example: /nick foo
if len(message_array) and message_array[0] != self.nick:
nick = message_array[0]
try:
nick = helpers.parse_resource(nick)
except Exception:
# Invalid Nickname
dialogs.ErrorDialog(_('Invalid nickname'),
_('The nickname has not allowed characters.'))
return True
gajim.connections[self.account].join_gc(nick, self.room_jid, None,
change_nick=True)
self.new_nick = nick
self.clear(self.msg_textview)
else:
self.get_command_help(command)
return True
elif command == 'query' or command == 'chat':
# Open a chat window to the specified nick
# example: /query foo
if len(message_array):
nick0 = message_array.pop(0)
if nick0[-1] == ' ':
nick1 = nick0[:-1]
else:
nick1 = nick0
nicks = gajim.contacts.get_nick_list(self.account, self.room_jid)
for nick in (nick0, nick1):
if nick in nicks:
self.on_send_pm(nick=nick)
self.clear(self.msg_textview)
return True
self.print_conversation(_('Nickname not found: %s') % \
nick0, 'info')
else:
self.get_command_help(command)
return True
elif command == 'msg':
# Send a message to a nick. Also opens a private message window.
# example: /msg foo Hey, what's up?
if len(message_array):
message_array = message_array[0].split()
nick = message_array.pop(0)
room_nicks = gajim.contacts.get_nick_list(self.account,
self.room_jid)
if nick in room_nicks:
privmsg = ' '.join(message_array)
self.on_send_pm(nick=nick, msg=privmsg)
self.clear(self.msg_textview)
else:
self.print_conversation(_('Nickname not found: %s') % nick,
'info')
else:
self.get_command_help(command)
return True
elif command == 'topic':
# display or change the room topic
# example: /topic : print topic
# /topic foo : change topic to foo
if len(message_array):
new_topic = message_array.pop(0)
gajim.connections[self.account].send_gc_subject(self.room_jid,
new_topic)
elif self.subject is not '':
self.print_conversation(self.subject, 'info')
else:
self.print_conversation(_('This group chat has no subject'), 'info')
self.clear(self.msg_textview)
return True
elif command == 'invite':
# invite a user to a room for a reason
# example: /invite user@example.com reason
if len(message_array):
message_array = message_array[0].split()
invitee = message_array.pop(0)
reason = ' '.join(message_array)
gajim.connections[self.account].send_invite(self.room_jid, invitee,
reason)
s = _('Invited %(contact_jid)s to %(room_jid)s.') % {
'contact_jid': invitee,
'room_jid': self.room_jid}
self.print_conversation(s, 'info')
self.clear(self.msg_textview)
else:
self.get_command_help(command)
return True
elif command == 'join':
# example: /join room@conference.example.com/nick
if len(message_array):
room_jid = message_array[0]
if room_jid.find('@') < 0:
room_jid = room_jid + '@' + gajim.get_server_from_jid(
self.room_jid)
else:
room_jid = '@' + gajim.get_server_from_jid(self.room_jid)
if room_jid.find('/') >= 0:
room_jid, nick = room_jid.split('/', 1)
else:
nick = ''
# join_gc window is needed in order to provide for password entry.
if 'join_gc' in gajim.interface.instances[self.account]:
gajim.interface.instances[self.account]['join_gc'].\
window.present()
else:
try:
dialogs.JoinGroupchatWindow(account=None, room_jid=room_jid,
nick=nick)
except GajimGeneralException:
pass
self.clear(self.msg_textview)
return True
elif command == 'leave' or command == 'part' or command == 'close':
# Leave the room and close the tab or window
reason = 'offline'
if len(message_array):
reason = message_array.pop(0)
self.parent_win.remove_tab(self, self.parent_win.CLOSE_COMMAND, reason)
self.clear(self.msg_textview)
return True
elif command == 'ban':
if len(message_array):
room_nicks = gajim.contacts.get_nick_list(self.account,
self.room_jid)
nb_match = 0
nick_ban = ''
for nick in room_nicks:
if message_array[0].startswith(nick):
nb_match += 1
nick_ban = nick
test_reason = message_array[0][len(nick) + 1:]
if len(test_reason) == 0:
reason = 'None'
else:
reason = test_reason
banned_jid = None
if nb_match == 1:
gc_contact = gajim.contacts.get_gc_contact(self.account,
self.room_jid, nick_ban)
banned_jid = gc_contact.jid
elif nb_match > 1:
self.print_conversation(_('There is an ambiguity: %d nicks '
'match.\n Please use graphical interface ') % nb_match,
'info')
self.clear(self.msg_textview)
elif message_array[0].split()[0].find('@') > 0:
message_splited = message_array[0].split(' ', 1)
banned_jid = message_splited[0]
if len(message_splited) == 2:
reason = message_splited[1]
else:
reason = 'None'
if banned_jid:
gajim.connections[self.account].gc_set_affiliation(self.room_jid,
banned_jid, 'outcast', reason)
self.clear(self.msg_textview)
else:
self.print_conversation(_('Nickname not found'), 'info')
else:
self.get_command_help(command)
return True
elif command == 'kick':
if len(message_array):
nick_kick = ''
room_nicks = gajim.contacts.get_nick_list(self.account,
self.room_jid)
nb_match = 0
for nick in room_nicks:
if message_array[0].startswith(nick):
nb_match += 1
nick_kick = nick
test_reason = message_array[0][len(nick) + 1:]
if len(test_reason) == 0:
reason = 'None'
else:
reason = test_reason
if nb_match == 1:
gajim.connections[self.account].gc_set_role(self.room_jid,
nick_kick, 'none', reason)
self.clear(self.msg_textview)
elif nb_match > 1:
self.print_conversation(_('There is an ambiguity: %d nicks '
'match.\n Please use graphical interface') % nb_match ,
'info' )
self.clear(self.msg_textview)
else:
# We can't do the difference between nick and reason
# So we don't say the nick
self.print_conversation(_('Nickname not found') , 'info')
else:
self.get_command_help(command)
return True
elif command == 'names':
# print the list of participants
nicklist=''
i=0
for contact in self.iter_contact_rows():
nicklist += '[ %-12.12s ] ' % (contact[C_NICK].decode('utf-8'))
i=i+1
if i == 3:
i=0
self.print_conversation(nicklist, 'info')
nicklist=''
if nicklist:
self.print_conversation(nicklist, 'info')
self.clear(self.msg_textview)
return True
elif command == 'help':
if len(message_array):
subcommand = message_array.pop(0)
self.get_command_help(subcommand)
else:
self.get_command_help(command)
self.clear(self.msg_textview)
return True
elif command == 'say':
gajim.connections[self.account].send_gc_message(self.room_jid,
message[4:])
self.clear(self.msg_textview)
return True
elif command == 'block':
if len(message_array) == 0:
self.get_command_help(command)
return True
nick = message_array[0].strip()
self.on_block(None, nick)
self.clear(self.msg_textview)
return True
elif command == 'unblock':
if len(message_array) == 0:
self.get_command_help(command)
return True
nick = message_array[0].strip()
self.on_unblock(None, nick)
self.clear(self.msg_textview)
return True
return False
def send_message(self, message, xhtml=None):
def send_message(self, message, xhtml=None, process_commands=True):
'''call this function to send our message'''
if not message:
return
if process_commands and self.process_as_command(message):
return
message = helpers.remove_invalid_xml_chars(message)
if not message:
@ -1778,79 +1527,12 @@ class GroupchatControl(ChatControlBase):
if message != '' or message != '\n':
self.save_sent_message(message)
if not self._process_command(message):
# Send the message
gajim.connections[self.account].send_gc_message(self.room_jid,
message, xhtml=xhtml)
self.msg_textview.get_buffer().set_text('')
self.msg_textview.grab_focus()
def get_command_help(self, command):
if command == 'help':
self.print_conversation(_('Commands: %s') % GroupchatControl.MUC_CMDS,
'info')
elif command == 'ban':
s = _('Usage: /%s <nickname|JID> [reason], bans the JID from the group'
' chat. The nickname of an occupant may be substituted, but not if '
'it contains "@". If the JID is currently in the group chat, '
'he/she/it will also be kicked.') % command
self.print_conversation(s, 'info')
elif command == 'chat' or command == 'query':
self.print_conversation(_('Usage: /%s <nickname>, opens a private chat'
' window with the specified occupant.') % command, 'info')
elif command == 'clear':
self.print_conversation(
_('Usage: /%s, clears the text window.') % command, 'info')
elif command == 'close' or command == 'leave' or command == 'part':
self.print_conversation(_('Usage: /%s [reason], closes the current '
'window or tab, displaying reason if specified.') % command, 'info')
elif command == 'compact':
self.print_conversation(_('Usage: /%s, hide the chat buttons.') % \
command, 'info')
elif command == 'invite':
self.print_conversation(_('Usage: /%s <JID> [reason], invites JID to '
'the current group chat, optionally providing a reason.') % command,
'info')
elif command == 'join':
self.print_conversation(_('Usage: /%s <room>@<server>[/nickname], '
'offers to join room@server optionally using specified nickname.') \
% command, 'info')
elif command == 'kick':
self.print_conversation(_('Usage: /%s <nickname> [reason], removes '
'the occupant specified by nickname from the group chat and '
'optionally displays a reason.') % command, 'info')
elif command == 'me':
self.print_conversation(_('Usage: /%(command)s <action>, sends action '
'to the current group chat. Use third person. (e.g. /%(command)s '
'explodes.)') % {'command': command}, 'info')
elif command == 'msg':
s = _('Usage: /%s <nickname> [message], opens a private message window'
' and sends message to the occupant specified by nickname.') % \
command
self.print_conversation(s, 'info')
elif command == 'nick':
s = _('Usage: /%s <nickname>, changes your nickname in current group '
'chat.') % command
self.print_conversation(s, 'info')
elif command == 'names':
s = _('Usage: /%s , display the names of group chat occupants.')\
% command
self.print_conversation(s, 'info')
elif command == 'topic':
self.print_conversation(_('Usage: /%s [topic], displays or updates the'
' current group chat topic.') % command, 'info')
elif command == 'say':
self.print_conversation(_('Usage: /%s <message>, sends a message '
'without looking for other commands.') % command, 'info')
elif command == 'block':
self.print_conversation(_('Usage: /%s <nickname>, prevent <nickname> '
'to send you messages or private messages.') % command, 'info')
elif command == 'unblock':
self.print_conversation(_('Usage: /%s <nickname>, allow <nickname> '
'to send you messages and private messages.') % command, 'info')
else:
self.print_conversation(_('No help info for /%s') % command, 'info')
# Send the message
gajim.connections[self.account].send_gc_message(self.room_jid,
message, xhtml=xhtml)
self.msg_textview.get_buffer().set_text('')
self.msg_textview.grab_focus()
def get_role(self, nick):
gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid,
@ -2100,41 +1782,13 @@ class GroupchatControl(ChatControlBase):
'utf-8')
splitted_text = text.split()
# topic completion
splitted_text2 = text.split(None, 1)
if text.startswith('/topic '):
if len(splitted_text2) == 2 and \
self.subject.startswith(splitted_text2[1]) and\
len(self.subject) > len(splitted_text2[1]):
message_buffer.insert_at_cursor(
self.subject[len(splitted_text2[1]):])
return True
elif len(splitted_text2) == 1 and text.startswith('/topic '):
message_buffer.delete(start_iter, end_iter)
message_buffer.insert_at_cursor('/topic '+self.subject)
return True
# command completion
if text.startswith('/') and len(splitted_text) == 1:
text = splitted_text[0]
if len(text) == 1: # user wants to cycle all commands
self.cmd_hits = GroupchatControl.MUC_CMDS
else:
# cycle possible commands depending on what the user typed
if self.last_key_tabs and len(self.cmd_hits) and \
self.cmd_hits[0].startswith(text.lstrip('/')):
self.cmd_hits.append(self.cmd_hits[0])
self.cmd_hits.pop(0)
else: # find possible commands
self.cmd_hits = []
for cmd in GroupchatControl.MUC_CMDS:
if cmd.startswith(text.lstrip('/')):
self.cmd_hits.append(cmd)
if len(self.cmd_hits):
message_buffer.delete(start_iter, end_iter)
message_buffer.insert_at_cursor('/' + self.cmd_hits[0] + ' ')
self.last_key_tabs = True
return True
# HACK: Not the best soltution.
if (text.startswith(self.COMMAND_PREFIX) and not
text.startswith(self.COMMAND_PREFIX * 2) and len(splitted_text) == 1):
return super(GroupchatControl,
self).handle_message_textview_mykey_press(widget, event_keyval,
event_keymod)
# nick completion
# check if tab is pressed with empty message

View File

@ -158,6 +158,15 @@ class MessageWindow(object):
if self.account == old_name:
self.account = new_name
def change_jid(self, account, old_jid, new_jid):
''' call then when the full jid of a contral change'''
if account not in self._controls:
return
if old_jid not in self._controls[account]:
return
self._controls[account][new_jid] = self._controls[account][old_jid]
del self._controls[account][old_jid]
def get_num_controls(self):
return sum(len(d) for d in self._controls.values())

View File

@ -62,7 +62,6 @@ from message_window import MessageWindowMgr
from common import dbus_support
if dbus_support.supported:
from music_track_listener import MusicTrackListener
import dbus
from common.xmpp.protocol import NS_COMMANDS, NS_FILE, NS_MUC
@ -1736,64 +1735,6 @@ class RosterWindow:
if chat_control:
chat_control.contact = contact1
def _change_awn_icon_status(self, status):
if not dbus_support.supported:
# do nothing if user doesn't have D-Bus bindings
return
try:
bus = dbus.SessionBus()
if not 'com.google.code.Awn' in bus.list_names():
# Awn is not installed
return
except Exception:
return
iconset = gajim.config.get('iconset')
prefix = os.path.join(helpers.get_iconset_path(iconset), '32x32')
if status in ('chat', 'away', 'xa', 'dnd', 'invisible', 'offline'):
status = status + '.png'
elif status == 'online':
prefix = os.path.join(gajim.DATA_DIR, 'pixmaps')
status = 'gajim.png'
path = os.path.join(prefix, status)
try:
obj = bus.get_object('com.google.code.Awn', '/com/google/code/Awn')
awn = dbus.Interface(obj, 'com.google.code.Awn')
awn.SetTaskIconByName('Gajim', os.path.abspath(path))
except Exception:
pass
def music_track_changed(self, unused_listener, music_track_info,
account=''):
if account == '':
accounts = gajim.connections.keys()
if music_track_info is None:
artist = ''
title = ''
source = ''
elif hasattr(music_track_info, 'paused') and music_track_info.paused == 0:
artist = ''
title = ''
source = ''
else:
artist = music_track_info.artist
title = music_track_info.title
source = music_track_info.album
if account == '':
for account in accounts:
if not gajim.account_is_connected(account):
continue
if not gajim.connections[account].pep_supported:
continue
if gajim.connections[account].music_track_info == music_track_info:
continue
pep.user_send_tune(account, artist, title, source)
gajim.connections[account].music_track_info = music_track_info
elif account in gajim.connections and \
gajim.connections[account].pep_supported:
if gajim.connections[account].music_track_info != music_track_info:
pep.user_send_tune(account, artist, title, source)
gajim.connections[account].music_track_info = music_track_info
def connected_rooms(self, account):
if account in gajim.gc_connected[account].values():
return True
@ -2189,7 +2130,7 @@ class RosterWindow:
liststore.prepend([status_combobox_text,
gajim.interface.jabber_state_images['16'][show], show, False])
self.status_combobox.set_active(0)
self._change_awn_icon_status(show)
gajim.interface._change_awn_icon_status(show)
self.combobox_callback_active = True
if gajim.interface.systray_enabled:
gajim.interface.systray.change_status(show)
@ -2634,7 +2575,24 @@ class RosterWindow:
connection.set_default_list('block')
connection.get_privacy_list('block')
self.get_status_message('offline', on_continue, show_pep=False)
def _block_it(is_checked=None):
if is_checked is not None: # dialog has been shown
if is_checked: # user does not want to be asked again
gajim.config.set('confirm_block', 'no')
else:
gajim.config.set('confirm_block', 'yes')
self.get_status_message('offline', on_continue, show_pep=False)
confirm_block = gajim.config.get('confirm_block')
if confirm_block == 'no':
_block_it()
return
pritext = _('You are about to block a contact. Are you sure you want'
' to continue?')
sectext = _('This contact will see you offline and you will not receive '
'messages he will send you.')
dlg = dialogs.ConfirmationDialogCheck(pritext, sectext,
_('Do _not ask me again'), on_response_ok=_block_it)
def on_unblock(self, widget, list_, group=None):
''' When clicked on the 'unblock' button in context menu. '''
@ -2886,8 +2844,15 @@ class RosterWindow:
ctrl.got_disconnected()
self.remove_groupchat(jid, account)
def on_reconnect(self, widget, jid, account):
'''When disconnect menuitem is activated: disconect from room'''
if jid in gajim.interface.minimized_controls[account]:
ctrl = gajim.interface.minimized_controls[account][jid]
gajim.interface.join_gc_room(account, jid, ctrl.nick,
gajim.gc_passwords.get(jid, ''))
def on_send_single_message_menuitem_activate(self, widget, account,
contact = None):
contact=None):
if contact is None:
dialogs.SingleMessageWindow(account, action='send')
elif isinstance(contact, list):
@ -2937,7 +2902,7 @@ class RosterWindow:
break
def on_invite_to_room(self, widget, list_, room_jid, room_account,
resource = None):
resource=None):
''' resource parameter MUST NOT be used if more than one contact in
list '''
for e in list_:
@ -3243,8 +3208,26 @@ class RosterWindow:
jid += '/' + contact.resource
self.send_status(account, show, message, to=jid)
self.get_status_message(show, on_response, show_pep=False,
always_ask=True)
def send_it(is_checked=None):
if is_checked is not None: # dialog has been shown
if is_checked: # user does not want to be asked again
gajim.config.set('confirm_custom_status', 'no')
else:
gajim.config.set('confirm_custom_status', 'yes')
self.get_status_message(show, on_response, show_pep=False,
always_ask=True)
confirm_custom_status = gajim.config.get('confirm_custom_status')
if confirm_custom_status == 'no':
send_it()
return
pritext = _('You are about to send a custom status. Are you sure you want'
' to continue?')
sectext = _('This contact will temporarily see you as %(status)s, '
'but only until you change your status. Then he will see your global '
'status.') % {'status': show}
dlg = dialogs.ConfirmationDialogCheck(pritext, sectext,
_('Do _not ask me again'), on_response_ok=send_it)
def on_status_combobox_changed(self, widget):
'''When we change our status via the combobox'''
@ -3354,21 +3337,14 @@ class RosterWindow:
act = widget.get_active()
gajim.config.set_per('accounts', account, 'publish_tune', act)
if act:
listener = MusicTrackListener.get()
if not self.music_track_changed_signal:
self.music_track_changed_signal = listener.connect(
'music-track-changed', self.music_track_changed)
track = listener.get_playing_track()
self.music_track_changed(listener, track)
gajim.interface.enable_music_listener()
else:
# disable it only if no other account use it
for acct in gajim.connections:
if gajim.config.get_per('accounts', acct, 'publish_tune'):
break
else:
listener = MusicTrackListener.get()
listener.disconnect(self.music_track_changed_signal)
self.music_track_changed_signal = None
gajim.interface.disable_music_listener()
if gajim.connections[account].pep_supported:
# As many implementations don't support retracting items, we send a
@ -5919,6 +5895,13 @@ class RosterWindow:
jid, account)
menu.append(maximize_menuitem)
if not gajim.gc_connected[account].get(jid, False):
connect_menuitem = gtk.ImageMenuItem(_('_Reconnect'))
connect_icon = gtk.image_new_from_stock(gtk.STOCK_CONNECT, \
gtk.ICON_SIZE_MENU)
connect_menuitem.set_image(connect_icon)
connect_menuitem.connect('activate', self.on_reconnect, jid, account)
menu.append(connect_menuitem)
disconnect_menuitem = gtk.ImageMenuItem(_('_Disconnect'))
disconnect_icon = gtk.image_new_from_stock(gtk.STOCK_DISCONNECT, \
gtk.ICON_SIZE_MENU)
@ -6190,7 +6173,6 @@ class RosterWindow:
self.xml = gtkgui_helpers.get_glade('roster_window.glade')
self.window = self.xml.get_widget('roster_window')
self.hpaned = self.xml.get_widget('roster_hpaned')
self.music_track_changed_signal = None
gajim.interface.msg_win_mgr = MessageWindowMgr(self.window, self.hpaned)
gajim.interface.msg_win_mgr.connect('window-delete',
self.on_message_window_delete)
@ -6411,16 +6393,6 @@ class RosterWindow:
self._toggeling_row = False
self.setup_and_draw_roster()
for account in gajim.connections:
if gajim.config.get_per('accounts', account, 'publish_tune') and \
dbus_support.supported:
listener = MusicTrackListener.get()
self.music_track_changed_signal = listener.connect(
'music-track-changed', self.music_track_changed)
track = listener.get_playing_track()
self.music_track_changed(listener, track)
break
if gajim.config.get('show_roster_on_startup'):
self.window.show_all()
else:

View File

@ -86,6 +86,10 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession):
'''dispatch a received <message> 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)
if self.control and self.control.resource:
self.control.change_resource(self.resource)
if not msg_type or msg_type not in ('chat', 'groupchat', 'error'):
msg_type = 'normal'