merge default branch to jingle
This commit is contained in:
commit
6b99db8980
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#!/bin/sh
|
||||
cd "$(dirname $0)/src"
|
||||
exec python -OOt gajim.py $@
|
||||
exec python -Ot gajim.py $@
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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.
|
||||
"""
|
|
@ -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..."
|
|
@ -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
|
|
@ -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)
|
|
@ -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]
|
|
@ -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 ],
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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'])
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
113
src/gajim.py
113
src/gajim.py
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue