summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormathieui <mathieui@mathieui.net>2014-03-19 02:22:25 +0100
committermathieui <mathieui@mathieui.net>2014-03-19 02:22:25 +0100
commit1c621caae1204ec9a59b38a326c13bfe04be168c (patch)
treee69dc839d0fcfa39a8704e1fdbf3d7ca7df3cb26
parentbe74128cb78ebd9c8316816f04632acd12396579 (diff)
downloadpoezio-1c621caae1204ec9a59b38a326c13bfe04be168c.tar.gz
poezio-1c621caae1204ec9a59b38a326c13bfe04be168c.tar.bz2
poezio-1c621caae1204ec9a59b38a326c13bfe04be168c.tar.xz
poezio-1c621caae1204ec9a59b38a326c13bfe04be168c.zip
split the "tabs" module into separate files
- todo: write a common import file to avoid duplicating the imports
-rw-r--r--src/tabs.py4000
-rw-r--r--src/tabs/__init__.py8
-rw-r--r--src/tabs/basetabs.py666
-rw-r--r--src/tabs/conversationtab.py433
-rw-r--r--src/tabs/muclisttab.py195
-rw-r--r--src/tabs/muctab.py1214
-rw-r--r--src/tabs/privatetab.py342
-rw-r--r--src/tabs/rostertab.py991
-rw-r--r--src/tabs/xmltab.py195
9 files changed, 4044 insertions, 4000 deletions
diff --git a/src/tabs.py b/src/tabs.py
deleted file mode 100644
index c81573c6..00000000
--- a/src/tabs.py
+++ /dev/null
@@ -1,4000 +0,0 @@
-# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org>
-#
-# This file is part of Poezio.
-#
-# Poezio is free software: you can redistribute it and/or modify
-# it under the terms of the zlib license. See the COPYING file.
-
-"""
-a Tab object is a way to organize various Windows (see windows.py)
-around the screen at once.
-A tab is then composed of multiple Buffers.
-Each Tab object has different refresh() and resize() methods, defining how its
-Windows are displayed, resized, etc.
-"""
-
-MIN_WIDTH = 42
-MIN_HEIGHT = 6
-
-import logging
-log = logging.getLogger(__name__)
-
-from gettext import gettext as _
-
-import windows
-import curses
-import fixes
-import difflib
-import string
-import common
-import core
-import singleton
-import random
-import xhtml
-import weakref
-import timed_events
-import os
-import time
-
-import multiuserchat as muc
-
-from theming import get_theme, dump_tuple
-
-from common import safeJID
-from decorators import refresh_wrapper
-from sleekxmpp import JID, InvalidJID
-from sleekxmpp.xmlstream import matcher
-from sleekxmpp.xmlstream.handler import Callback
-from config import config
-from roster import RosterGroup, roster
-from contact import Contact, Resource
-from text_buffer import TextBuffer, CorrectionError
-from user import User
-from os import getenv, path
-from logger import logger
-
-from datetime import datetime, timedelta
-from xml.etree import cElementTree as ET
-
-SHOW_NAME = {
- 'dnd': _('busy'),
- 'away': _('away'),
- 'xa': _('not available'),
- 'chat': _('chatty'),
- '': _('available')
- }
-
-NS_MUC_USER = 'http://jabber.org/protocol/muc#user'
-
-STATE_COLORS = {
- 'disconnected': lambda: get_theme().COLOR_TAB_DISCONNECTED,
- 'scrolled': lambda: get_theme().COLOR_TAB_SCROLLED,
- 'joined': lambda: get_theme().COLOR_TAB_JOINED,
- 'message': lambda: get_theme().COLOR_TAB_NEW_MESSAGE,
- 'highlight': lambda: get_theme().COLOR_TAB_HIGHLIGHT,
- 'private': lambda: get_theme().COLOR_TAB_PRIVATE,
- 'normal': lambda: get_theme().COLOR_TAB_NORMAL,
- 'current': lambda: get_theme().COLOR_TAB_CURRENT,
- 'attention': lambda: get_theme().COLOR_TAB_ATTENTION,
- }
-
-VERTICAL_STATE_COLORS = {
- 'disconnected': lambda: get_theme().COLOR_VERTICAL_TAB_DISCONNECTED,
- 'scrolled': lambda: get_theme().COLOR_VERTICAL_TAB_SCROLLED,
- 'joined': lambda: get_theme().COLOR_VERTICAL_TAB_JOINED,
- 'message': lambda: get_theme().COLOR_VERTICAL_TAB_NEW_MESSAGE,
- 'highlight': lambda: get_theme().COLOR_VERTICAL_TAB_HIGHLIGHT,
- 'private': lambda: get_theme().COLOR_VERTICAL_TAB_PRIVATE,
- 'normal': lambda: get_theme().COLOR_VERTICAL_TAB_NORMAL,
- 'current': lambda: get_theme().COLOR_VERTICAL_TAB_CURRENT,
- 'attention': lambda: get_theme().COLOR_VERTICAL_TAB_ATTENTION,
- }
-
-
-STATE_PRIORITY = {
- 'normal': -1,
- 'current': -1,
- 'disconnected': 0,
- 'scrolled': 0.5,
- 'message': 1,
- 'joined': 1,
- 'highlight': 2,
- 'private': 2,
- 'attention': 3
- }
-
-class Tab(object):
- tab_core = None
-
- def __init__(self):
- self.input = None
- self._state = 'normal'
-
- self.need_resize = False
- self.need_resize = False
- self.key_func = {} # each tab should add their keys in there
- # and use them in on_input
- self.commands = {} # and their own commands
-
-
- @property
- def core(self):
- if not Tab.tab_core:
- Tab.tab_core = singleton.Singleton(core.Core)
- return Tab.tab_core
-
- @property
- def nb(self):
- for index, tab in enumerate(self.core.tabs):
- if tab == self:
- return index
- return len(self.core.tabs)
-
- @property
- def tab_win(self):
- if not Tab.tab_core:
- Tab.tab_core = singleton.Singleton(core.Core)
- return Tab.tab_core.tab_win
-
- @property
- def left_tab_win(self):
- if not Tab.tab_core:
- Tab.tab_core = singleton.Singleton(core.Core)
- return Tab.tab_core.left_tab_win
-
- @staticmethod
- def tab_win_height():
- """
- Returns 1 or 0, depending on if we are using the vertical tab list
- or not.
- """
- if config.get('enable_vertical_tab_list', 'false') == 'true':
- return 0
- return 1
-
- @property
- def info_win(self):
- return self.core.information_win
-
- @property
- def color(self):
- return STATE_COLORS[self._state]()
-
- @property
- def vertical_color(self):
- return VERTICAL_STATE_COLORS[self._state]()
-
- @property
- def state(self):
- return self._state
-
- @state.setter
- def state(self, value):
- if not value in STATE_COLORS:
- log.debug("Invalid value for tab state: %s", value)
- elif STATE_PRIORITY[value] < STATE_PRIORITY[self._state] and \
- value not in ('current', 'disconnected') and \
- not (self._state == 'scrolled' and value == 'disconnected'):
- log.debug("Did not set state because of lower priority, asked: %s, kept: %s", value, self._state)
- elif self._state == 'disconnected' and value not in ('joined', 'current'):
- log.debug('Did not set state because disconnected tabs remain visible')
- else:
- self._state = value
-
- @staticmethod
- def resize(scr):
- Tab.size = (Tab.height, Tab.width) = scr.getmaxyx()
- if Tab.height < MIN_HEIGHT or Tab.width < MIN_WIDTH:
- Tab.visible = False
- else:
- Tab.visible = True
- windows.Win._tab_win = scr
-
- def register_command(self, name, func, *, desc='', shortdesc='', completion=None, usage=''):
- """
- Add a command
- """
- if name in self.commands:
- return
- if not desc and shortdesc:
- desc = shortdesc
- self.commands[name] = core.Command(func, desc, completion, shortdesc, usage)
-
- def complete_commands(self, the_input):
- """
- Does command completion on the specified input for both global and tab-specific
- commands.
- This should be called from the completion method (on tab, for example), passing
- the input where completion is to be made.
- It can completion the command name itself or an argument of the command.
- Returns True if a completion was made, False else.
- """
- txt = the_input.get_text()
- # check if this is a command
- if txt.startswith('/') and not txt.startswith('//'):
- position = the_input.get_argument_position(quoted=False)
- if position == 0:
- words = ['/%s'% (name) for name in sorted(self.core.commands)] +\
- ['/%s' % (name) for name in sorted(self.commands)]
- the_input.new_completion(words, 0)
- # Do not try to cycle command completion if there was only
- # one possibily. The next tab will complete the argument.
- # Otherwise we would need to add a useless space before being
- # able to complete the arguments.
- hit_copy = set(the_input.hit_list)
- while not hit_copy:
- whitespace = the_input.text.find(' ')
- if whitespace == -1:
- whitespace = len(the_input.text)
- the_input.text = the_input.text[:whitespace-1] + the_input.text[whitespace:]
- the_input.new_completion(words, 0)
- hit_copy = set(the_input.hit_list)
- if len(hit_copy) == 1:
- the_input.do_command(' ')
- the_input.reset_completion()
- return True
- # check if we are in the middle of the command name
- elif len(txt.split()) > 1 or\
- (txt.endswith(' ') and not the_input.last_completion):
- command_name = txt.split()[0][1:]
- if command_name in self.commands:
- command = self.commands[command_name]
- elif command_name in self.core.commands:
- command = self.core.commands[command_name]
- else: # Unknown command, cannot complete
- return False
- if command[2] is None:
- return False # There's no completion function
- else:
- return command[2](the_input)
- return True
- return False
-
- def execute_command(self, provided_text):
- """
- Execute the command in the input and return False if
- the input didn't contain a command
- """
- txt = provided_text or self.input.key_enter()
- if txt.startswith('/') and not txt.startswith('//') and\
- not txt.startswith('/me '):
- command = txt.strip().split()[0][1:]
- arg = txt[2+len(command):] # jump the '/' and the ' '
- func = None
- if command in self.commands: # check tab-specific commands
- func = self.commands[command][0]
- elif command in self.core.commands: # check global commands
- func = self.core.commands[command][0]
- else:
- low = command.lower()
- if low in self.commands:
- func = self.commands[low][0]
- elif low in self.core.commands:
- func = self.core.commands[low][0]
- else:
- self.core.information(_("Unknown command (%s)") % (command), _('Error'))
- if command in ('correct', 'say'): # hack
- arg = xhtml.convert_simple_to_full_colors(arg)
- else:
- arg = xhtml.clean_text_simple(arg)
- if func:
- func(arg)
- return True
- else:
- return False
-
- def refresh_tab_win(self):
- if self.left_tab_win:
- self.left_tab_win.refresh()
- else:
- self.tab_win.refresh()
-
- def refresh(self):
- """
- Called on each screen refresh (when something has changed)
- """
- pass
-
- def get_name(self):
- """
- get the name of the tab
- """
- return self.__class__.__name__
-
- def get_nick(self):
- """
- Get the nick of the tab (defaults to its name)
- """
- return self.get_name()
-
- def get_text_window(self):
- """
- Returns the principal TextWin window, if there's one
- """
- return None
-
- def on_input(self, key, raw):
- """
- raw indicates if the key should activate the associated command or not.
- """
- pass
-
- def update_commands(self):
- for c in self.plugin_commands:
- if not c in self.commands:
- self.commands[c] = self.plugin_commands[c]
-
- def update_keys(self):
- for k in self.plugin_keys:
- if not k in self.key_func:
- self.key_func[k] = self.plugin_keys[k]
-
- def on_lose_focus(self):
- """
- called when this tab loses the focus.
- """
- self.state = 'normal'
-
- def on_gain_focus(self):
- """
- called when this tab gains the focus.
- """
- self.state = 'current'
-
- def on_scroll_down(self):
- """
- Defines what happens when we scroll down
- """
- pass
-
- def on_scroll_up(self):
- """
- Defines what happens when we scroll up
- """
- pass
-
- def on_line_up(self):
- """
- Defines what happens when we scroll one line up
- """
- pass
-
- def on_line_down(self):
- """
- Defines what happens when we scroll one line up
- """
- pass
-
- def on_half_scroll_down(self):
- """
- Defines what happens when we scroll half a screen down
- """
- pass
-
- def on_half_scroll_up(self):
- """
- Defines what happens when we scroll half a screen up
- """
- pass
-
- def on_info_win_size_changed(self):
- """
- Called when the window with the informations is resized
- """
- pass
-
- def on_close(self):
- """
- Called when the tab is to be closed
- """
- if self.input:
- self.input.on_delete()
-
- def matching_names(self):
- """
- Returns a list of strings that are used to name a tab with the /win
- command. For example you could switch to a tab that returns
- ['hello', 'coucou'] using /win hel, or /win coucou
- If not implemented in the tab, it just doesn’t match with anything.
- """
- return []
-
- def __del__(self):
- log.debug('------ Closing tab %s', self.__class__.__name__)
-
-class GapTab(Tab):
-
- def __bool__(self):
- return False
-
- def __len__(self):
- return 0
-
- def get_name(self):
- return ''
-
- def refresh(self):
- log.debug('WARNING: refresh() called on a gap tab, this should not happen')
-
-class ChatTab(Tab):
- """
- A tab containing a chat of any type.
- Just use this class instead of Tab if the tab needs a recent-words completion
- Also, ^M is already bound to on_enter
- And also, add the /say command
- """
- plugin_commands = {}
- plugin_keys = {}
- def __init__(self, jid=''):
- Tab.__init__(self)
- self.name = jid
- self._text_buffer = TextBuffer()
- self.remote_wants_chatstates = None # change this to True or False when
- # we know that the remote user wants chatstates, or not.
- # None means we don’t know yet, and we send only "active" chatstates
- self.chatstate = None # can be "active", "composing", "paused", "gone", "inactive"
- # We keep a weakref of the event that will set our chatstate to "paused", so that
- # we can delete it or change it if we need to
- self.timed_event_paused = None
- # if that’s None, then no paused chatstate was sent recently
- # if that’s a weakref returning None, then a paused chatstate was sent
- # since the last input
- self.remote_supports_attention = False
- # Keeps the last sent message to complete it easily in completion_correct, and to replace it.
- self.last_sent_message = None
- self.key_func['M-v'] = self.move_separator
- self.key_func['M-h'] = self.scroll_separator
- self.key_func['M-/'] = self.last_words_completion
- self.key_func['^M'] = self.on_enter
- self.register_command('say', self.command_say,
- usage=_('<message>'),
- shortdesc=_('Send the message.'))
- self.register_command('xhtml', self.command_xhtml,
- usage=_('<custom xhtml>'),
- shortdesc=_('Send custom XHTML.'))
- self.register_command('clear', self.command_clear,
- shortdesc=_('Clear the current buffer.'))
- self.register_command('correct', self.command_correct,
- desc=_('Fix the last message with whatever you want.'),
- shortdesc=_('Correct the last message.'),
- completion=self.completion_correct)
- self.chat_state = None
- self.update_commands()
- self.update_keys()
-
- # Get the logs
- log_nb = config.get('load_log', 10)
- logs = self.load_logs(log_nb)
-
- if logs:
- for message in logs:
- self._text_buffer.add_message(**message)
-
- @property
- def is_muc(self):
- return False
-
- def load_logs(self, log_nb):
- logs = logger.get_logs(safeJID(self.get_name()).bare, log_nb)
-
- def log_message(self, txt, nickname, time=None, typ=1):
- """
- Log the messages in the archives.
- """
- name = safeJID(self.name).bare
- if not logger.log_message(name, nickname, txt, date=time, typ=typ):
- self.core.information(_('Unable to write in the log file'), 'Error')
-
- def add_message(self, txt, time=None, nickname=None, forced_user=None, nick_color=None, identifier=None, jid=None, history=None, typ=1):
- self.log_message(txt, nickname, time=time, typ=typ)
- self._text_buffer.add_message(txt, time=time,
- nickname=nickname,
- nick_color=nick_color,
- history=history,
- user=forced_user,
- identifier=identifier,
- jid=jid)
-
- def modify_message(self, txt, old_id, new_id, user=None,jid=None, nickname=None):
- self.log_message(txt, nickname, typ=1)
- message = self._text_buffer.modify_message(txt, old_id, new_id, time=time, user=user, jid=jid)
- if message:
- self.text_win.modify_message(old_id, message)
- self.core.refresh_window()
- return True
- return False
-
- def last_words_completion(self):
- """
- Complete the input with words recently said
- """
- # build the list of the recent words
- char_we_dont_want = string.punctuation+' ’„“”…«»'
- words = list()
- for msg in self._text_buffer.messages[:-40:-1]:
- if not msg:
- continue
- txt = xhtml.clean_text(msg.txt)
- for char in char_we_dont_want:
- txt = txt.replace(char, ' ')
- for word in txt.split():
- if len(word) >= 4 and word not in words:
- words.append(word)
- words.extend([word for word in config.get('words', '').split(':') if word])
- self.input.auto_completion(words, ' ', quotify=False)
-
- def on_enter(self):
- txt = self.input.key_enter()
- if txt:
- if not self.execute_command(txt):
- if txt.startswith('//'):
- txt = txt[1:]
- self.command_say(xhtml.convert_simple_to_full_colors(txt))
- self.cancel_paused_delay()
-
- def command_xhtml(self, arg):
- """"
- /xhtml <custom xhtml>
- """
- message = self.generate_xhtml_message(arg)
- if message:
- message.send()
-
- def generate_xhtml_message(self, arg):
- if not arg:
- return
- try:
- body = xhtml.clean_text(xhtml.xhtml_to_poezio_colors(arg))
- # The <body /> element is the only allowable child of the <xhtm-im>
- arg = "<body xmlns='http://www.w3.org/1999/xhtml'>%s</body>" % (arg,)
- ET.fromstring(arg)
- except:
- self.core.information('Could not send custom xhtml', 'Error')
- log.error('/xhtml: Unable to send custom xhtml', exc_info=True)
- return
-
- msg = self.core.xmpp.make_message(self.get_dest_jid())
- msg['body'] = body
- msg.enable('html')
- msg['html']['body'] = arg
- return msg
-
- def get_dest_jid(self):
- return self.get_name()
-
- @refresh_wrapper.always
- def command_clear(self, args):
- """
- /clear
- """
- self._text_buffer.messages = []
- self.text_win.rebuild_everything(self._text_buffer)
-
- def send_chat_state(self, state, always_send=False):
- """
- Send an empty chatstate message
- """
- if not self.is_muc or self.joined:
- if state in ('active', 'inactive', 'gone') and self.inactive and not always_send:
- return
- if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) and \
- self.remote_wants_chatstates is not False:
- msg = self.core.xmpp.make_message(self.get_dest_jid())
- msg['type'] = self.message_type
- msg['chat_state'] = state
- self.chat_state = state
- msg.send()
-
- def send_composing_chat_state(self, empty_after):
- """
- Send the "active" or "composing" chatstate, depending
- on the the current status of the input
- """
- name = self.general_jid
- if config.get_by_tabname('send_chat_states', 'true', name, True) == 'true' and self.remote_wants_chatstates:
- needed = 'inactive' if self.inactive else 'active'
- self.cancel_paused_delay()
- if not empty_after:
- if self.chat_state != "composing":
- self.send_chat_state("composing")
- self.set_paused_delay(True)
- elif empty_after and self.chat_state != needed:
- self.send_chat_state(needed, True)
-
- def set_paused_delay(self, composing):
- """
- we create a timed event that will put us to paused
- in a few seconds
- """
- if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) != 'true':
- return
- if self.timed_event_paused:
- # check the weakref
- event = self.timed_event_paused()
- if event:
- # the event already exists: we just update
- # its date
- event.change_date(datetime.now() + timedelta(seconds=4))
- return
- new_event = timed_events.DelayedEvent(4, self.send_chat_state, 'paused')
- self.core.add_timed_event(new_event)
- self.timed_event_paused = weakref.ref(new_event)
-
- def cancel_paused_delay(self):
- """
- Remove that event from the list and set it to None.
- Called for example when the input is emptied, or when the message
- is sent
- """
- if self.timed_event_paused:
- event = self.timed_event_paused()
- if event:
- self.core.remove_timed_event(event)
- del event
- self.timed_event_paused = None
-
- def command_correct(self, line):
- """
- /correct <fixed message>
- """
- if not line:
- self.core.command_help('correct')
- return
- if not self.last_sent_message:
- self.core.information(_('There is no message to correct.'))
- return
- self.command_say(line, correct=True)
-
- def completion_correct(self, the_input):
- if self.last_sent_message and the_input.get_argument_position() == 1:
- return the_input.auto_completion([self.last_sent_message['body']], '', quotify=False)
-
- @property
- def inactive(self):
- """Whether we should send inactive or active as a chatstate"""
- return self.core.status.show in ('xa', 'away') or\
- (hasattr(self, 'directed_presence') and not self.directed_presence)
-
- def move_separator(self):
- self.text_win.remove_line_separator()
- self.text_win.add_line_separator(self._text_buffer)
- self.text_win.refresh()
- self.input.refresh()
-
- def get_conversation_messages(self):
- return self._text_buffer.messages
-
- def check_scrolled(self):
- if self.text_win.pos != 0:
- self.state = 'scrolled'
-
- def command_say(self, line, correct=False):
- pass
-
- def on_line_up(self):
- return self.text_win.scroll_up(1)
-
- def on_line_down(self):
- return self.text_win.scroll_down(1)
-
- def on_scroll_up(self):
- return self.text_win.scroll_up(self.text_win.height-1)
-
- def on_scroll_down(self):
- return self.text_win.scroll_down(self.text_win.height-1)
-
- def on_half_scroll_up(self):
- return self.text_win.scroll_up((self.text_win.height-1) // 2)
-
- def on_half_scroll_down(self):
- return self.text_win.scroll_down((self.text_win.height-1) // 2)
-
- @refresh_wrapper.always
- def scroll_separator(self):
- self.text_win.scroll_to_separator()
-
-class MucTab(ChatTab):
- """
- The tab containing a multi-user-chat room.
- It contains an userlist, an input, a topic, an information and a chat zone
- """
- message_type = 'groupchat'
- plugin_commands = {}
- plugin_keys = {}
- def __init__(self, jid, nick):
- self.joined = False
- ChatTab.__init__(self, jid)
- if self.joined == False:
- self._state = 'disconnected'
- self.own_nick = nick
- self.name = jid
- self.users = []
- self.privates = [] # private conversations
- self.topic = ''
- self.remote_wants_chatstates = True
- # We send active, composing and paused states to the MUC because
- # the chatstate may or may not be filtered by the MUC,
- # that’s not our problem.
- self.topic_win = windows.Topic()
- self.text_win = windows.TextWin()
- self._text_buffer.add_window(self.text_win)
- self.v_separator = windows.VerticalSeparator()
- self.user_win = windows.UserList()
- self.info_header = windows.MucInfoWin()
- self.input = windows.MessageInput()
- self.ignores = [] # set of Users
- # keys
- self.key_func['^I'] = self.completion
- self.key_func['M-u'] = self.scroll_user_list_down
- self.key_func['M-y'] = self.scroll_user_list_up
- self.key_func['M-n'] = self.go_to_next_hl
- self.key_func['M-p'] = self.go_to_prev_hl
- # commands
- self.register_command('ignore', self.command_ignore,
- usage=_('<nickname>'),
- desc=_('Ignore a specified nickname.'),
- shortdesc=_('Ignore someone'),
- completion=self.completion_ignore)
- self.register_command('unignore', self.command_unignore,
- usage=_('<nickname>'),
- desc=_('Remove the specified nickname from the ignore list.'),
- shortdesc=_('Unignore someone.'),
- completion=self.completion_unignore)
- self.register_command('kick', self.command_kick,
- usage=_('<nick> [reason]'),
- desc=_('Kick the user with the specified nickname. You also can give an optional reason.'),
- shortdesc=_('Kick someone.'),
- completion=self.completion_quoted)
- self.register_command('ban', self.command_ban,
- usage=_('<nick> [reason]'),
- desc=_('Ban the user with the specified nickname. You also can give an optional reason.'),
- shortdesc='Ban someone',
- completion=self.completion_quoted)
- self.register_command('role', self.command_role,
- usage=_('<nick> <role> [reason]'),
- desc=_('Set the role of an user. Roles can be: none, visitor, participant, moderator. You also can give an optional reason.'),
- shortdesc=_('Set the role of an user.'),
- completion=self.completion_role)
- self.register_command('affiliation', self.command_affiliation,
- usage=_('<nick or jid> <affiliation>'),
- desc=_('Set the affiliation of an user. Affiliations can be: outcast, none, member, admin, owner.'),
- shortdesc=_('Set the affiliation of an user.'),
- completion=self.completion_affiliation)
- self.register_command('topic', self.command_topic,
- usage=_('<subject>'),
- desc=_('Change the subject of the room.'),
- shortdesc=_('Change the subject.'),
- completion=self.completion_topic)
- self.register_command('query', self.command_query,
- usage=_('<nick> [message]'),
- desc=_('Query: Open a private conversation with <nick>. This nick has to be present in the room you\'re currently in. If you specified a message after the nickname, it will immediately be sent to this user.'),
- shortdesc=_('Query an user.'),
- completion=self.completion_quoted)
- self.register_command('part', self.command_part,
- usage=_('[message]'),
- desc=_('Disconnect from a room. You can specify an optional message.'),
- shortdesc=_('Leave the room.'))
- self.register_command('close', self.command_close,
- usage=_('[message]'),
- desc=_('Disconnect from a room and close the tab. You can specify an optional message if you are still connected.'),
- shortdesc=_('Close the tab.'))
- self.register_command('nick', self.command_nick,
- usage=_('<nickname>'),
- desc=_('Change your nickname in the current room.'),
- shortdesc=_('Change your nickname.'),
- completion=self.completion_nick)
- self.register_command('recolor', self.command_recolor,
- desc=_('Re-assign a color to all participants of the current room, based on the last time they talked. Use this if the participants currently talking have too many identical colors.'),
- shortdesc=_('Change the nicks colors.'),
- completion=self.completion_recolor)
- self.register_command('cycle', self.command_cycle,
- usage=_('[message]'),
- desc=_('Leave the current room and rejoin it immediately.'),
- shortdesc=_('Leave and re-join the room.'))
- self.register_command('info', self.command_info,
- usage=_('<nickname>'),
- desc=_('Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.'),
- shortdesc=_('Show an user\'s infos.'),
- completion=self.completion_info)
- self.register_command('configure', self.command_configure,
- desc=_('Configure the current room, through a form.'),
- shortdesc=_('Configure the room.'))
- self.register_command('version', self.command_version,
- usage=_('<jid or nick>'),
- desc=_('Get the software version of the given JID or nick in room (usually its XMPP client and Operating System).'),
- shortdesc=_('Get the software version of a jid.'),
- completion=self.completion_version)
- self.register_command('names', self.command_names,
- desc=_('Get the list of the users in the room, and the list of the people assuming the different roles.'),
- shortdesc=_('List the users.'))
- self.register_command('invite', self.command_invite,
- desc=_('Invite a contact to this room'),
- usage=_('<jid> [reason]'),
- shortdesc=_('Invite a contact to this room'),
- completion=self.completion_invite)
-
- if self.core.xmpp.boundjid.server == "gmail.com": #gmail sucks
- del self.commands["nick"]
-
- self.resize()
- self.update_commands()
- self.update_keys()
-
- @property
- def general_jid(self):
- return self.get_name()
-
- @property
- def is_muc(self):
- return True
-
- @property
- def last_connection(self):
- last_message = self._text_buffer.last_message
- if last_message:
- return last_message.time
- return None
-
- @refresh_wrapper.always
- def go_to_next_hl(self):
- """
- Go to the next HL in the room, or the last
- """
- self.text_win.next_highlight()
-
- @refresh_wrapper.always
- def go_to_prev_hl(self):
- """
- Go to the previous HL in the room, or the first
- """
- self.text_win.previous_highlight()
-
- def completion_version(self, the_input):
- """Completion for /version"""
- compare_users = lambda x: x.last_talked
- userlist = [user.nick for user in sorted(self.users, key=compare_users, reverse=True)\
- if user.nick != self.own_nick]
- return the_input.auto_completion(userlist, quotify=False)
-
- def completion_info(self, the_input):
- """Completion for /info"""
- compare_users = lambda x: x.last_talked
- userlist = [user.nick for user in sorted(self.users, key=compare_users, reverse=True)]
- return the_input.auto_completion(userlist, quotify=False)
-
- def completion_nick(self, the_input):
- """Completion for /nick"""
- nicks = [os.environ.get('USER'), config.get('default_nick', ''), self.core.get_bookmark_nickname(self.get_name())]
- nicks = [i for i in nicks if i]
- return the_input.auto_completion(nicks, '', quotify=False)
-
- def completion_recolor(self, the_input):
- if the_input.get_argument_position() == 1:
- return the_input.new_completion(['random'], 1, '', quotify=False)
- return True
-
- def completion_ignore(self, the_input):
- """Completion for /ignore"""
- userlist = [user.nick for user in self.users]
- if self.own_nick in userlist:
- userlist.remove(self.own_nick)
- userlist.sort()
- return the_input.auto_completion(userlist, quotify=False)
-
- def completion_role(self, the_input):
- """Completion for /role"""
- n = the_input.get_argument_position(quoted=True)
- if n == 1:
- userlist = [user.nick for user in self.users]
- if self.own_nick in userlist:
- userlist.remove(self.own_nick)
- return the_input.new_completion(userlist, 1, '', quotify=True)
- elif n == 2:
- possible_roles = ['none', 'visitor', 'participant', 'moderator']
- return the_input.new_completion(possible_roles, 2, '', quotify=True)
-
- def completion_affiliation(self, the_input):
- """Completion for /affiliation"""
- n = the_input.get_argument_position(quoted=True)
- if n == 1:
- userlist = [user.nick for user in self.users]
- if self.own_nick in userlist:
- userlist.remove(self.own_nick)
- jidlist = [user.jid.bare for user in self.users]
- if self.core.xmpp.boundjid.bare in jidlist:
- jidlist.remove(self.core.xmpp.boundjid.bare)
- userlist.extend(jidlist)
- return the_input.new_completion(userlist, 1, '', quotify=True)
- elif n == 2:
- possible_affiliations = ['none', 'member', 'admin', 'owner', 'outcast']
- return the_input.new_completion(possible_affiliations, 2, '', quotify=True)
-
- def command_invite(self, args):
- """/invite <jid> [reason]"""
- args = common.shell_split(args)
- if len(args) == 1:
- jid, reason = args[0], ''
- elif len(args) == 2:
- jid, reason = args
- else:
- return self.core.command_help('invite')
- self.core.command_invite('%s %s "%s"' % (jid, self.name, reason))
-
- def completion_invite(self, the_input):
- """Completion for /invite"""
- n = the_input.get_argument_position(quoted=True)
- if n == 1:
- return the_input.new_completion(roster.jids(), 1, quotify=True)
-
- def scroll_user_list_up(self):
- self.user_win.scroll_up()
- self.user_win.refresh(self.users)
- self.input.refresh()
-
- def scroll_user_list_down(self):
- self.user_win.scroll_down()
- self.user_win.refresh(self.users)
- self.input.refresh()
-
- def command_info(self, arg):
- """
- /info <nick>
- """
- if not arg:
- return self.core.command_help('info')
- user = self.get_user_by_name(arg)
- if not user:
- return self.core.information("Unknown user: %s" % arg)
- theme = get_theme()
- info = '\x19%s}%s\x19o%s: show: \x19%s}%s\x19o, affiliation: \x19%s}%s\x19o, role: \x19%s}%s\x19o%s' % (
- dump_tuple(user.color),
- arg,
- (' (\x19%s}%s\x19o)' % (dump_tuple(theme.COLOR_MUC_JID), user.jid)) if user.jid != '' else '',
- dump_tuple(theme.color_show(user.show)),
- user.show or 'Available',
- dump_tuple(theme.color_role(user.role)),
- user.affiliation or 'None',
- dump_tuple(theme.color_role(user.role)),
- user.role or 'None',
- '\n%s' % user.status if user.status else '')
- self.core.information(info, 'Info')
-
- def command_configure(self, arg):
- form = fixes.get_room_form(self.core.xmpp, self.get_name())
- if not form:
- self.core.information('Could not retrieve the configuration form', 'Error')
- return
- self.core.open_new_form(form, self.cancel_config, self.send_config)
-
- def cancel_config(self, form):
- """
- The user do not want to send his/her config, send an iq cancel
- """
- self.core.xmpp.plugin['xep_0045'].cancelConfig(self.get_name())
- self.core.close_tab()
-
- def send_config(self, form):
- """
- The user sends his/her config to the server
- """
- self.core.xmpp.plugin['xep_0045'].configureRoom(self.get_name(), form)
- self.core.close_tab()
-
- def command_cycle(self, arg):
- """/cycle [reason]"""
- if self.joined:
- muc.leave_groupchat(self.core.xmpp, self.get_name(), self.own_nick, arg)
- self.disconnect()
- self.core.disable_private_tabs(self.name)
- self.core.command_join('"/%s"' % self.own_nick)
- self.user_win.pos = 0
-
- def command_recolor(self, arg):
- """
- /recolor [random]
- Re-assign color to the participants of the room
- """
- arg = arg.strip()
- compare_users = lambda x: x.last_talked
- users = list(self.users)
- sorted_users = sorted(users, key=compare_users, reverse=True)
- # search our own user, to remove it from the list
- for user in sorted_users:
- if user.nick == self.own_nick:
- sorted_users.remove(user)
- user.color = get_theme().COLOR_OWN_NICK
- colors = list(get_theme().LIST_COLOR_NICKNAMES)
- if arg and arg == 'random':
- random.shuffle(colors)
- for i, user in enumerate(sorted_users):
- user.color = colors[i % len(colors)]
- self.text_win.rebuild_everything(self._text_buffer)
- self.user_win.refresh(self.users)
- self.text_win.refresh()
- self.input.refresh()
-
- def command_version(self, arg):
- """
- /version <jid or nick>
- """
- def callback(res):
- if not res:
- return self.core.information('Could not get the software version from %s' % (jid,), 'Warning')
- version = '%s is running %s version %s on %s' % (jid,
- res.get('name') or _('an unknown software'),
- res.get('version') or _('unknown'),
- res.get('os') or _('an unknown platform'))
- self.core.information(version, 'Info')
-
- if not arg:
- return self.core.command_help('version')
- if arg in [user.nick for user in self.users]:
- jid = safeJID(self.name).bare
- jid = safeJID(jid + '/' + arg)
- else:
- jid = safeJID(arg)
- fixes.get_version(self.core.xmpp, jid, callback=callback)
-
- def command_nick(self, arg):
- """
- /nick <nickname>
- """
- if not arg:
- return self.core.command_help('nick')
- nick = arg
- if not self.joined:
- return self.core.information('/nick only works in joined rooms', 'Info')
- current_status = self.core.get_status()
- if not safeJID(self.get_name() + '/' + nick):
- return self.core.information('Invalid nick', 'Info')
- muc.change_nick(self.core, self.name, nick, current_status.message, current_status.show)
-
- def command_part(self, arg):
- """
- /part [msg]
- """
- arg = arg.strip()
- msg = None
- if self.joined:
- self.disconnect()
- muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, arg)
- if arg:
- msg = _("\x195}You left the chatroom (\x19o%s\x195})\x193}" % arg)
- else:
- msg =_("\x195}You left the chatroom\x193}")
- self.add_message(msg, typ=2)
- if self == self.core.current_tab():
- self.refresh()
- self.core.doupdate()
- else:
- msg =_("\x195}You left the chatroom\x193}")
- self.core.disable_private_tabs(self.name, reason=msg)
-
- def command_close(self, arg):
- """
- /close [msg]
- """
- self.command_part(arg)
- self.core.close_tab()
-
- def command_query(self, arg):
- """
- /query <nick> [message]
- """
- args = common.shell_split(arg)
- if len(args) < 1:
- return
- nick = args[0]
- r = None
- for user in self.users:
- if user.nick == nick:
- r = self.core.open_private_window(self.name, user.nick)
- if r and len(args) > 1:
- msg = args[1]
- self.core.current_tab().command_say(xhtml.convert_simple_to_full_colors(msg))
- if not r:
- self.core.information(_("Cannot find user: %s" % nick), 'Error')
-
- def command_topic(self, arg):
- """
- /topic [new topic]
- """
- if not arg.strip():
- self._text_buffer.add_message(_("\x19%s}The subject of the room is: %s") %
- (dump_tuple(get_theme().COLOR_INFORMATION_TEXT), self.topic))
- self.refresh()
- return
- subject = arg
- muc.change_subject(self.core.xmpp, self.name, subject)
-
- def command_names(self, arg=None):
- """
- /names
- """
- if not self.joined:
- return
- color_visitor = dump_tuple(get_theme().COLOR_USER_VISITOR)
- color_other = dump_tuple(get_theme().COLOR_USER_NONE)
- color_moderator = dump_tuple(get_theme().COLOR_USER_MODERATOR)
- color_participant = dump_tuple(get_theme().COLOR_USER_PARTICIPANT)
- visitors, moderators, participants, others = [], [], [], []
- aff = {
- 'owner': get_theme().CHAR_AFFILIATION_OWNER,
- 'admin': get_theme().CHAR_AFFILIATION_ADMIN,
- 'member': get_theme().CHAR_AFFILIATION_MEMBER,
- 'none': get_theme().CHAR_AFFILIATION_NONE,
- }
-
- users = self.users[:]
- users.sort(key=lambda x: x.nick.lower())
- for user in users:
- color = aff.get(user.affiliation, get_theme().CHAR_AFFILIATION_NONE)
- if user.role == 'visitor':
- visitors.append((user, color))
- elif user.role == 'participant':
- participants.append((user, color))
- elif user.role == 'moderator':
- moderators.append((user, color))
- else:
- others.append((user, color))
-
- buff = ['Users: %s \n' % len(self.users)]
- for moderator in moderators:
- buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % (color_moderator,
- moderator[1], dump_tuple(moderator[0].color), moderator[0].nick))
- for participant in participants:
- buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % (color_participant,
- participant[1], dump_tuple(participant[0].color), participant[0].nick))
- for visitor in visitors:
- buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % (color_visitor,
- visitor[1], dump_tuple(visitor[0].color), visitor[0].nick))
- for other in others:
- buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % (color_other,
- other[1], dump_tuple(other[0].color), other[0].nick))
- buff.append('\n')
- message = ' '.join(buff)
-
- self._text_buffer.add_message(message)
- self.text_win.refresh()
- self.input.refresh()
-
- def completion_topic(self, the_input):
- if the_input.get_argument_position() == 1:
- return the_input.auto_completion([self.topic], '', quotify=False)
-
- def completion_quoted(self, the_input):
- """Nick completion, but with quotes"""
- if the_input.get_argument_position(quoted=True) == 1:
- compare_users = lambda x: x.last_talked
- word_list = [user.nick for user in sorted(self.users, key=compare_users, reverse=True)\
- if user.nick != self.own_nick]
- return the_input.new_completion(word_list, 1, quotify=True)
-
- def command_kick(self, arg):
- """
- /kick <nick> [reason]
- """
- args = common.shell_split(arg)
- if not args:
- self.core.command_help('kick')
- else:
- if len(args) > 1:
- msg = ' "%s"' % args[1]
- else:
- msg = ''
- self.command_role('"'+args[0]+ '" none'+msg)
-
- def command_ban(self, arg):
- """
- /ban <nick> [reason]
- """
- def callback(iq):
- if iq['type'] == 'error':
- self.core.room_error(iq, self.get_name())
- args = common.shell_split(arg)
- if not args:
- return self.core.command_help('ban')
- if len(args) > 1:
- msg = args[1]
- else:
- msg = ''
- nick = args[0]
-
- if nick in [user.nick for user in self.users]:
- res = muc.set_user_affiliation(self.core.xmpp, self.get_name(), 'outcast', nick=nick, callback=callback, reason=msg)
- else:
- res = muc.set_user_affiliation(self.core.xmpp, self.get_name(), 'outcast', jid=safeJID(nick), callback=callback, reason=msg)
- if not res:
- self.core.information('Could not ban user', 'Error')
-
- def command_role(self, arg):
- """
- /role <nick> <role> [reason]
- Changes the role of an user
- roles can be: none, visitor, participant, moderator
- """
- def callback(iq):
- if iq['type'] == 'error':
- self.core.room_error(iq, self.get_name())
- args = common.shell_split(arg)
- if len(args) < 2:
- self.core.command_help('role')
- return
- nick, role = args[0],args[1]
- if len(args) > 2:
- reason = ' '.join(args[2:])
- else:
- reason = ''
- if not self.joined or \
- not role in ('none', 'visitor', 'participant', 'moderator'):
- return
- if not safeJID(self.get_name() + '/' + nick):
- return self.core('Invalid nick', 'Info')
- muc.set_user_role(self.core.xmpp, self.get_name(), nick, reason, role, callback=callback)
-
- def command_affiliation(self, arg):
- """
- /affiliation <nick> <role>
- Changes the affiliation of an user
- affiliations can be: outcast, none, member, admin, owner
- """
- def callback(iq):
- if iq['type'] == 'error':
- self.core.room_error(iq, self.get_name())
- args = common.shell_split(arg)
- if len(args) < 2:
- self.core.command_help('affiliation')
- return
- nick, affiliation = args[0], args[1].lower()
- if not self.joined:
- return
- if affiliation not in ('outcast', 'none', 'member', 'admin', 'owner'):
- self.core.command_help('affiliation')
- return
- if nick in [user.nick for user in self.users]:
- res = muc.set_user_affiliation(self.core.xmpp, self.get_name(), affiliation, nick=nick, callback=callback)
- else:
- res = muc.set_user_affiliation(self.core.xmpp, self.get_name(), affiliation, jid=safeJID(nick), callback=callback)
- if not res:
- self.core.information('Could not set affiliation', 'Error')
-
- def command_say(self, line, correct=False):
- """
- /say <message>
- Or normal input + enter
- """
- needed = 'inactive' if self.inactive else 'active'
- msg = self.core.xmpp.make_message(self.get_name())
- msg['type'] = 'groupchat'
- msg['body'] = line
- # trigger the event BEFORE looking for colors.
- # This lets a plugin insert \x19xxx} colors, that will
- # be converted in xhtml.
- self.core.events.trigger('muc_say', msg, self)
- if not msg['body']:
- self.cancel_paused_delay()
- self.text_win.refresh()
- self.input.refresh()
- return
- if msg['body'].find('\x19') != -1:
- msg.enable('html')
- msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body'])
- msg['body'] = xhtml.clean_text(msg['body'])
- if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and self.remote_wants_chatstates is not False:
- msg['chat_state'] = needed
- if correct:
- msg['replace']['id'] = self.last_sent_message['id']
- self.cancel_paused_delay()
- self.core.events.trigger('muc_say_after', msg, self)
- if not msg['body']:
- self.cancel_paused_delay()
- self.text_win.refresh()
- self.input.refresh()
- return
- self.last_sent_message = msg
- msg.send()
- self.chat_state = needed
-
- def command_xhtml(self, arg):
- message = self.generate_xhtml_message(arg)
- if message:
- message['type'] = 'groupchat'
- message.send()
-
- def command_ignore(self, arg):
- """
- /ignore <nick>
- """
- if not arg:
- self.core.command_help('ignore')
- return
- nick = arg
- user = self.get_user_by_name(nick)
- if not user:
- self.core.information(_('%s is not in the room') % nick)
- elif user in self.ignores:
- self.core.information(_('%s is already ignored') % nick)
- else:
- self.ignores.append(user)
- self.core.information(_("%s is now ignored") % nick, 'info')
-
- def command_unignore(self, arg):
- """
- /unignore <nick>
- """
- if not arg:
- self.core.command_help('unignore')
- return
- nick = arg
- user = self.get_user_by_name(nick)
- if not user:
- self.core.information(_('%s is not in the room') % nick)
- elif user not in self.ignores:
- self.core.information(_('%s is not ignored') % nick)
- else:
- self.ignores.remove(user)
- self.core.information(_('%s is now unignored') % nick)
-
- def completion_unignore(self, the_input):
- if the_input.get_argument_position() == 1:
- return the_input.new_completion([user.nick for user in self.ignores], 1, '', quotify=False)
-
- def resize(self):
- """
- Resize the whole window. i.e. all its sub-windows
- """
- if not self.visible:
- return
- self.need_resize = False
- if config.get("hide_user_list", "false") == "true":
- text_width = self.width
- else:
- text_width = (self.width//10)*9
- self.user_win.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width-(self.width//10)*9-1, 1, (self.width//10)*9+1)
- self.topic_win.resize(1, self.width, 0, 0)
- self.v_separator.resize(self.height-2 - Tab.tab_win_height(), 1, 1, 9*(self.width//10))
- self.text_win.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), text_width, 1, 0)
- self.text_win.rebuild_everything(self._text_buffer)
- self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
- self.input.resize(1, self.width, self.height-1, 0)
-
- def refresh(self):
- if self.need_resize:
- self.resize()
- log.debug(' TAB Refresh: %s',self.__class__.__name__)
- self.topic_win.refresh(self.get_single_line_topic())
- self.text_win.refresh()
- if config.get("hide_user_list", "false") == "false":
- self.v_separator.refresh()
- self.user_win.refresh(self.users)
- self.info_header.refresh(self, self.text_win)
- self.refresh_tab_win()
- self.info_win.refresh()
- self.input.refresh()
-
- def on_input(self, key, raw):
- if not raw and key in self.key_func:
- self.key_func[key]()
- return False
- self.input.do_command(key, raw=raw)
- empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//'))
- self.send_composing_chat_state(empty_after)
- return False
-
- def completion(self):
- """
- Called when Tab is pressed, complete the nickname in the input
- """
- if self.complete_commands(self.input):
- return
-
- # If we are not completing a command or a command's argument, complete a nick
- compare_users = lambda x: x.last_talked
- word_list = [user.nick for user in sorted(self.users, key=compare_users, reverse=True)\
- if user.nick != self.own_nick]
- after = config.get('after_completion', ',')+" "
- input_pos = self.input.pos
- if ' ' not in self.input.get_text()[:input_pos] or (self.input.last_completion and\
- self.input.get_text()[:input_pos] == self.input.last_completion + after):
- add_after = after
- else:
- add_after = '' if config.get('add_space_after_completion', 'true') == 'false' else ' '
- self.input.auto_completion(word_list, add_after, quotify=False)
- empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//'))
- self.send_composing_chat_state(empty_after)
-
- def get_color_state(self):
- return self.color_state
-
- def set_color_state(self, color):
- self.set_color_state(color)
-
- def get_name(self):
- return self.name
-
- def get_nick(self):
- if config.getl('show_muc_jid', 'true') == 'false':
- return safeJID(self.name).user
- return self.name
-
- def get_text_window(self):
- return self.text_win
-
- def on_lose_focus(self):
- if self.joined:
- self.state = 'normal'
- else:
- self.state = 'disconnected'
- self.text_win.remove_line_separator()
- self.text_win.add_line_separator(self._text_buffer)
- if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and not self.input.get_text():
- self.send_chat_state('inactive')
- self.check_scrolled()
-
- def on_gain_focus(self):
- self.state = 'current'
- if self.text_win.built_lines and self.text_win.built_lines[-1] is None and config.getl('show_useless_separator', 'false') != 'true':
- self.text_win.remove_line_separator()
- curses.curs_set(1)
- if self.joined and config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and not self.input.get_text():
- self.send_chat_state('active')
-
- def on_info_win_size_changed(self):
- if self.core.information_win_size >= self.height-3:
- return
- if config.get("hide_user_list", "false") == "true":
- text_width = self.width
- else:
- text_width = (self.width//10)*9
- self.user_win.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width-(self.width//10)*9-1, 1, (self.width//10)*9+1)
- self.text_win.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), text_width, 1, 0)
- self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
-
- def handle_presence(self, presence):
- from_nick = presence['from'].resource
- from_room = presence['from'].bare
- status_codes = set([s.attrib['code'] for s in presence.findall('{%s}x/{%s}status' % (NS_MUC_USER, NS_MUC_USER))])
- # Check if it's not an error presence.
- if presence['type'] == 'error':
- return self.core.room_error(presence, from_room)
- affiliation = presence['muc']['affiliation']
- show = presence['show']
- status = presence['status']
- role = presence['muc']['role']
- jid = presence['muc']['jid']
- typ = presence['type']
- if not self.joined: # user in the room BEFORE us.
- # ignore redondant presence message, see bug #1509
- if from_nick not in [user.nick for user in self.users] and typ != "unavailable":
- new_user = User(from_nick, affiliation, show, status, role, jid)
- self.users.append(new_user)
- self.core.events.trigger('muc_join', presence, self)
- if '110' in status_codes or self.own_nick == from_nick:
- # second part of the condition is a workaround for old
- # ejabberd or every gateway in the world that just do
- # not send a 110 status code with the presence
- self.own_nick = from_nick
- self.joined = True
- if self.get_name() in self.core.initial_joins:
- self.core.initial_joins.remove(self.get_name())
- self._state = 'normal'
- elif self != self.core.current_tab():
- self._state = 'joined'
- if self.core.current_tab() == self and self.core.status.show not in ('xa', 'away'):
- self.send_chat_state('active')
- new_user.color = get_theme().COLOR_OWN_NICK
- self.add_message(_("\x19%(info_col)s}Your nickname is \x19%(nick_col)s}%(nick)s") % {
- 'nick': from_nick,
- 'nick_col': dump_tuple(new_user.color),
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
- typ=2)
- if '201' in status_codes:
- self.add_message('\x19%(info_col)s}Info: The room has been created' % {
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
- typ=2)
- if '170' in status_codes:
- self.add_message('\x191}Warning: \x19%(info_col)s}this room is publicly logged' % {
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
- typ=2)
- if '100' in status_codes:
- self.add_message('\x191}Warning: \x19%(info_col)s}This room is not anonymous.' % {
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
- typ=2)
- if self.core.current_tab() is not self:
- self.refresh_tab_win()
- self.core.current_tab().input.refresh()
- self.core.doupdate()
- self.core.enable_private_tabs(self.get_name())
- else:
- change_nick = '303' in status_codes
- kick = '307' in status_codes and typ == 'unavailable'
- ban = '301' in status_codes and typ == 'unavailable'
- shutdown = '332' in status_codes and typ == 'unavailable'
- non_member = '322' in status_codes and typ == 'unavailable'
- user = self.get_user_by_name(from_nick)
- # New user
- if not user:
- self.core.events.trigger('muc_join', presence, self)
- self.on_user_join(from_nick, affiliation, show, status, role, jid)
- # nick change
- elif change_nick:
- self.core.events.trigger('muc_nickchange', presence, self)
- self.on_user_nick_change(presence, user, from_nick, from_room)
- elif ban:
- self.core.events.trigger('muc_ban', presence, self)
- self.core.on_user_left_private_conversation(from_room, from_nick, status)
- self.on_user_banned(presence, user, from_nick)
- # kick
- elif kick:
- self.core.events.trigger('muc_kick', presence, self)
- self.core.on_user_left_private_conversation(from_room, from_nick, status)
- self.on_user_kicked(presence, user, from_nick)
- elif shutdown:
- self.core.events.trigger('muc_shutdown', presence, self)
- self.on_muc_shutdown()
- elif non_member:
- self.core.events.trigger('muc_shutdown', presence, self)
- self.on_non_member_kick()
- # user quit
- elif typ == 'unavailable':
- self.on_user_leave_groupchat(user, jid, status, from_nick, from_room)
- # status change
- else:
- self.on_user_change_status(user, from_nick, from_room, affiliation, role, show, status)
- if self.core.current_tab() is self:
- self.text_win.refresh()
- self.user_win.refresh(self.users)
- self.info_header.refresh(self, self.text_win)
- self.input.refresh()
- self.core.doupdate()
-
- def on_non_member_kicked(self):
- """We have been kicked because the MUC is members-only"""
- self.add_message('\x19%(info_col)s}%You have been kicked because you are not a member and the room is now members-only.' % {
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
- typ=2)
- self.disconnect()
-
- def on_muc_shutdown(self):
- """We have been kicked because the MUC service is shutting down"""
- self.add_message('\x19%(info_col)s}%You have been kicked because the MUC service is shutting down.' % {
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
- typ=2)
- self.disconnect()
-
- def on_user_join(self, from_nick, affiliation, show, status, role, jid):
- """
- When a new user joins the groupchat
- """
- user = User(from_nick, affiliation,
- show, status, role, jid)
- self.users.append(user)
- hide_exit_join = config.get_by_tabname('hide_exit_join', -1, self.general_jid, True)
- if hide_exit_join != 0:
- color = dump_tuple(user.color) if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
- if not jid.full:
- msg = '\x194}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} joined the room' % {
- 'nick':from_nick, 'color':color, 'spec':get_theme().CHAR_JOIN,
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- else:
- msg = '\x194}%(spec)s \x19%(color)s}%(nick)s \x19%(info_col)s}(\x19%(jid_color)s}%(jid)s\x19%(info_col)s}) joined the room' % {
- 'spec':get_theme().CHAR_JOIN, 'nick':from_nick, 'color':color, 'jid':jid.full,
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
- 'jid_color': dump_tuple(get_theme().COLOR_MUC_JID)}
- self.add_message(msg, typ=2)
- self.core.on_user_rejoined_private_conversation(self.name, from_nick)
-
- def on_user_nick_change(self, presence, user, from_nick, from_room):
- new_nick = presence.find('{%s}x/{%s}item' % (NS_MUC_USER, NS_MUC_USER)).attrib['nick']
- if user.nick == self.own_nick:
- self.own_nick = new_nick
- # also change our nick in all private discussions of this room
- self.core.on_muc_own_nickchange(self)
- user.change_nick(new_nick)
- color = dump_tuple(user.color) if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
- self.add_message('\x19%(color)s}%(old)s\x19%(info_col)s} is now known as \x19%(color)s}%(new)s' % {
- 'old':from_nick, 'new':new_nick, 'color':color,
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
- typ=2)
- # rename the private tabs if needed
- self.core.rename_private_tabs(self.name, from_nick, new_nick)
-
- def on_user_banned(self, presence, user, from_nick):
- """
- When someone is banned from a muc
- """
- self.users.remove(user)
- by = presence.find('{%s}x/{%s}item/{%s}actor' % (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER))
- reason = presence.find('{%s}x/{%s}item/{%s}reason' % (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER))
- by = by.attrib['jid'] if by is not None else None
- if from_nick == self.own_nick: # we are banned
- if by:
- kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been banned by \x194}%(by)s') % {
- 'spec': get_theme().CHAR_KICK, 'by':by,
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- else:
- kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been banned.') % {
- 'spec': get_theme().CHAR_KICK,
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- self.core.disable_private_tabs(self.name, reason=kick_msg)
- self.disconnect()
- self.refresh_tab_win()
- self.core.current_tab().input.refresh()
- self.core.doupdate()
- if config.get_by_tabname('autorejoin', 'false', self.general_jid, True) == 'true':
- delay = config.get_by_tabname('autorejoin_delay', "5", self.general_jid, True)
- delay = common.parse_str_to_secs(delay)
- if delay <= 0:
- muc.join_groupchat(self.core, self.name, self.own_nick)
- else:
- self.core.add_timed_event(timed_events.DelayedEvent(
- delay,
- muc.join_groupchat,
- self.core,
- self.name,
- self.own_nick))
-
- else:
- color = dump_tuple(user.color) if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
- if by:
- kick_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} has been banned by \x194}%(by)s') % {
- 'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'by':by,
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- else:
- kick_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} has been banned') % {
- 'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'color':color,
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- if reason is not None and reason.text:
- kick_msg += _('\x19%(info_col)s} Reason: \x196}%(reason)s\x19%(info_col)s}') % {
- 'reason': reason.text, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- self.add_message(kick_msg, typ=2)
-
- def on_user_kicked(self, presence, user, from_nick):
- """
- When someone is kicked from a muc
- """
- self.users.remove(user)
- actor_elem = presence.find('{%s}x/{%s}item/{%s}actor' % (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER))
- reason = presence.find('{%s}x/{%s}item/{%s}reason' % (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER))
- by = None
- if actor_elem is not None:
- by = actor_elem.get('nick') or actor_elem.get('jid')
- if from_nick == self.own_nick: # we are kicked
- if by:
- kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been kicked by \x193}%(by)s') % {
- 'spec': get_theme().CHAR_KICK, 'by':by,
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- else:
- kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been kicked.') % {'spec':get_theme().CHAR_KICK, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- self.core.disable_private_tabs(self.name, reason=kick_msg)
- self.disconnect()
- self.refresh_tab_win()
- self.core.current_tab().input.refresh()
- self.core.doupdate()
- # try to auto-rejoin
- if config.get_by_tabname('autorejoin', 'false', self.general_jid, True) == 'true':
- delay = config.get_by_tabname('autorejoin_delay', "5", self.general_jid, True)
- delay = common.parse_str_to_secs(delay)
- if delay <= 0:
- muc.join_groupchat(self.core, self.name, self.own_nick)
- else:
- self.core.add_timed_event(timed_events.DelayedEvent(
- delay,
- muc.join_groupchat,
- self.core,
- self.name,
- self.own_nick))
- else:
- color = dump_tuple(user.color) if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
- if by:
- kick_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} has been kicked by \x193}%(by)s') % {'spec': get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'by':by, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- else:
- kick_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} has been kicked') % {'spec': get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- if reason is not None and reason.text:
- kick_msg += _('\x19%(info_col)s} Reason: \x196}%(reason)s') % {'reason': reason.text, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- self.add_message(kick_msg, typ=2)
-
- def on_user_leave_groupchat(self, user, jid, status, from_nick, from_room):
- """
- When an user leaves a groupchat
- """
- self.users.remove(user)
- if self.own_nick == user.nick:
- # We are now out of the room. Happens with some buggy (? not sure) servers
- self.disconnect()
- self.core.disable_private_tabs(from_room)
- self.refresh_tab_win()
- self.core.current_tab().input.refresh()
- self.core.doupdate()
- hide_exit_join = config.get_by_tabname('hide_exit_join', -1, self.general_jid, True) if config.get_by_tabname('hide_exit_join', -1, self.general_jid, True) >= -1 else -1
- if hide_exit_join == -1 or user.has_talked_since(hide_exit_join):
- color = dump_tuple(user.color) if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
- if not jid.full:
- leave_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} has left the room') % {'nick':from_nick, 'color':color, 'spec':get_theme().CHAR_QUIT, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- else:
- leave_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} (\x194}%(jid)s\x19%(info_col)s}) has left the room') % {'spec':get_theme().CHAR_QUIT, 'nick':from_nick, 'color':color, 'jid':jid.full, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- if status:
- leave_msg += ' (%s)' % status
- self.add_message(leave_msg, typ=2)
- self.core.on_user_left_private_conversation(from_room, from_nick, status)
-
- def on_user_change_status(self, user, from_nick, from_room, affiliation, role, show, status):
- """
- When an user changes her status
- """
- # build the message
- display_message = False # flag to know if something significant enough
- # to be displayed has changed
- color = dump_tuple(user.color) if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
- if from_nick == self.own_nick:
- msg = _('\x193}You\x19%(info_col)s} changed: ') % {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- else:
- msg = _('\x19%(color)s}%(nick)s\x19%(info_col)s} changed: ') % {'nick': from_nick, 'color': color, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- if show not in SHOW_NAME:
- self.core.information(_("%s from room %s sent an invalid show: %s") %\
- (from_nick, from_room, show), "Warning")
- if affiliation != user.affiliation:
- msg += _('affiliation: %s, ') % affiliation
- display_message = True
- if role != user.role:
- msg += _('role: %s, ') % role
- display_message = True
- if show != user.show and show in SHOW_NAME:
- msg += _('show: %s, ') % SHOW_NAME[show]
- display_message = True
- if status != user.status:
- # if the user sets his status to nothing
- if status:
- msg += _('status: %s, ') % status
- display_message = True
- elif show in SHOW_NAME and show == user.show:
- msg += _('show: %s, ') % SHOW_NAME[show]
- display_message = True
- if not display_message:
- return
- msg = msg[:-2] # remove the last ", "
- hide_status_change = config.get_by_tabname('hide_status_change', -1, self.general_jid, True)
- if hide_status_change < -1:
- hide_status_change = -1
- if ((hide_status_change == -1 or \
- user.has_talked_since(hide_status_change) or\
- user.nick == self.own_nick)\
- and\
- (affiliation != user.affiliation or\
- role != user.role or\
- show != user.show or\
- status != user.status))\
- or\
- (affiliation != user.affiliation or\
- role != user.role):
- # display the message in the room
- self._text_buffer.add_message(msg)
- self.core.on_user_changed_status_in_private('%s/%s' % (from_room, from_nick), msg)
- # finally, effectively change the user status
- user.update(affiliation, show, status, role)
-
- def disconnect(self):
- """
- Set the state of the room as not joined, so
- we can know if we can join it, send messages to it, etc
- """
- self.users = []
- if self is not self.core.current_tab():
- self.state = 'disconnected'
- self.joined = False
-
- def get_single_line_topic(self):
- """
- Return the topic as a single-line string (for the window header)
- """
- return self.topic.replace('\n', '|')
-
- def log_message(self, txt, nickname, time=None, typ=1):
- """
- Log the messages in the archives, if it needs
- to be
- """
- if time is None and self.joined: # don't log the history messages
- if not logger.log_message(self.name, nickname, txt, typ=typ):
- self.core.information(_('Unable to write in the log file'), 'Error')
-
- def do_highlight(self, txt, time, nickname):
- """
- Set the tab color and returns the nick color
- """
- highlighted = False
- if not time and nickname and nickname != self.own_nick and self.joined:
- if self.own_nick.lower() in txt.lower():
- if self.state != 'current':
- self.state = 'highlight'
- highlighted = True
- else:
- highlight_words = config.get_by_tabname('highlight_on', '', self.general_jid, True).split(':')
- for word in highlight_words:
- if word and word.lower() in txt.lower():
- if self.state != 'current':
- self.state = 'highlight'
- highlighted = True
- break
- if highlighted:
- beep_on = config.get('beep_on', 'highlight private').split()
- if 'highlight' in beep_on and 'message' not in beep_on:
- if config.get_by_tabname('disable_beep', 'false', self.name, False).lower() != 'true':
- curses.beep()
- return highlighted
-
- def get_user_by_name(self, nick):
- """
- Gets the user associated with the given nick, or None if not found
- """
- for user in self.users:
- if user.nick == nick:
- return user
- return None
-
- def add_message(self, txt, time=None, nickname=None, **kwargs):
- """
- Note that user can be None even if nickname is not None. It happens
- when we receive an history message said by someone who is not
- in the room anymore
- Return True if the message highlighted us. False otherwise.
- """
- self.log_message(txt, nickname, time=time, typ=kwargs.get('typ', 1))
- args = {key: value for key, value in kwargs.items() if key not in ('typ', 'forced_user')}
- user = self.get_user_by_name(nickname) if nickname is not None else None
- if user:
- user.set_last_talked(datetime.now())
- args['user'] = user
- if not user and kwargs.get('forced_user'):
- args['user'] = kwargs['forced_user']
- if not time and nickname and\
- nickname != self.own_nick and\
- self.state != 'current':
- if self.state != 'highlight' and\
- config.get_by_tabname('notify_messages', 'true', self.get_name()) == 'true':
- self.state = 'message'
- if (not nickname or time) and not txt.startswith('/me '):
- txt = '\x19%(info_col)s}%(txt)s' % {'txt':txt, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- elif not kwargs.get('highlight'): # TODO
- args['highlight'] = self.do_highlight(txt, time, nickname)
- time = time or datetime.now()
- self._text_buffer.add_message(txt, time, nickname, **args)
- return args.get('highlight', False)
-
- def modify_message(self, txt, old_id, new_id, time=None, nickname=None, user=None, jid=None):
- self.log_message(txt, nickname, time=time, typ=1)
- highlight = self.do_highlight(txt, time, nickname)
- message = self._text_buffer.modify_message(txt, old_id, new_id, highlight=highlight, time=time, user=user, jid=jid)
- if message:
- self.text_win.modify_message(old_id, message)
- self.core.refresh_window()
- return highlight
- return False
-
- def matching_names(self):
- return [(1, safeJID(self.get_name()).user), (3, self.get_name())]
-
-class PrivateTab(ChatTab):
- """
- The tab containg a private conversation (someone from a MUC)
- """
- message_type = 'chat'
- plugin_commands = {}
- additional_informations = {}
- plugin_keys = {}
- def __init__(self, name, nick):
- ChatTab.__init__(self, name)
- self.own_nick = nick
- self.name = name
- self.text_win = windows.TextWin()
- self._text_buffer.add_window(self.text_win)
- self.info_header = windows.PrivateInfoWin()
- self.input = windows.MessageInput()
- self.check_attention()
- # keys
- self.key_func['^I'] = self.completion
- # commands
- self.register_command('info', self.command_info,
- desc=_('Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.'),
- shortdesc=_('Info about the user.'))
- self.register_command('unquery', self.command_unquery,
- shortdesc=_('Close the tab.'))
- self.register_command('close', self.command_unquery,
- shortdesc=_('Close the tab.'))
- self.register_command('version', self.command_version,
- desc=_('Get the software version of the current interlocutor (usually its XMPP client and Operating System).'),
- shortdesc=_('Get the software version of a jid.'))
- self.resize()
- self.parent_muc = self.core.get_tab_by_name(safeJID(name).bare, MucTab)
- self.on = True
- self.update_commands()
- self.update_keys()
-
- @property
- def general_jid(self):
- return self.get_name()
-
- @property
- def nick(self):
- return self.get_nick()
-
- @staticmethod
- def add_information_element(plugin_name, callback):
- """
- Lets a plugin add its own information to the PrivateInfoWin
- """
- PrivateTab.additional_informations[plugin_name] = callback
-
- @staticmethod
- def remove_information_element(plugin_name):
- del PrivateTab.additional_informations[plugin_name]
-
- def load_logs(self, log_nb):
- logs = logger.get_logs(safeJID(self.get_name()).full.replace('/', '\\'), log_nb)
-
- def log_message(self, txt, nickname, time=None, typ=1):
- """
- Log the messages in the archives.
- """
- if not logger.log_message(self.name, nickname, txt, date=time, typ=typ):
- self.core.information(_('Unable to write in the log file'), 'Error')
-
- def on_close(self):
- self.parent_muc.privates.remove(self)
-
- def completion(self):
- """
- Called when Tab is pressed, complete the nickname in the input
- """
- if self.complete_commands(self.input):
- return
-
- # If we are not completing a command or a command's argument, complete a nick
- compare_users = lambda x: x.last_talked
- word_list = [user.nick for user in sorted(self.parent_muc.users, key=compare_users, reverse=True)\
- if user.nick != self.own_nick]
- after = config.get('after_completion', ',')+" "
- input_pos = self.input.pos
- if ' ' not in self.input.get_text()[:input_pos] or (self.input.last_completion and\
- self.input.get_text()[:input_pos] == self.input.last_completion + after):
- add_after = after
- else:
- add_after = ''
- self.input.auto_completion(word_list, add_after, quotify=False)
- empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//'))
- self.send_composing_chat_state(empty_after)
-
- def command_say(self, line, attention=False, correct=False):
- if not self.on:
- return
- msg = self.core.xmpp.make_message(self.get_name())
- msg['type'] = 'chat'
- msg['body'] = line
- # trigger the event BEFORE looking for colors.
- # This lets a plugin insert \x19xxx} colors, that will
- # be converted in xhtml.
- self.core.events.trigger('private_say', msg, self)
- if not msg['body']:
- self.cancel_paused_delay()
- self.text_win.refresh()
- self.input.refresh()
- return
- user = self.parent_muc.get_user_by_name(self.own_nick)
- replaced = False
- if correct or msg['replace']['id']:
- msg['replace']['id'] = self.last_sent_message['id']
- if config.get_by_tabname('group_corrections', 'true', self.get_name()).lower() != 'false':
- try:
- self.modify_message(msg['body'], self.last_sent_message['id'], msg['id'],
- user=user, jid=self.core.xmpp.boundjid, nickname=self.own_nick)
- replaced = True
- except:
- log.error('Unable to correct a message', exc_info=True)
- else:
- del msg['replace']
-
- if msg['body'].find('\x19') != -1:
- msg.enable('html')
- msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body'])
- msg['body'] = xhtml.clean_text(msg['body'])
- if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and self.remote_wants_chatstates is not False:
- needed = 'inactive' if self.inactive else 'active'
- msg['chat_state'] = needed
- if attention and self.remote_supports_attention:
- msg['attention'] = True
- self.core.events.trigger('private_say_after', msg, self)
- if not msg['body']:
- self.cancel_paused_delay()
- self.text_win.refresh()
- self.input.refresh()
- return
- if not replaced:
- self.add_message(msg['body'],
- nickname=self.core.own_nick or self.own_nick,
- forced_user=user,
- nick_color=get_theme().COLOR_OWN_NICK,
- identifier=msg['id'],
- jid=self.core.xmpp.boundjid,
- typ=1)
-
- self.last_sent_message = msg
- msg.send()
- self.cancel_paused_delay()
- self.text_win.refresh()
- self.input.refresh()
-
- def command_attention(self, message=''):
- if message is not '':
- self.command_say(message, attention=True)
- else:
- msg = self.core.xmpp.make_message(self.get_name())
- msg['type'] = 'chat'
- msg['attention'] = True
- msg.send()
-
- def check_attention(self):
- self.core.xmpp.plugin['xep_0030'].get_info(jid=self.get_name(), block=False, timeout=5, callback=self.on_attention_checked)
-
- def on_attention_checked(self, iq):
- if 'urn:xmpp:attention:0' in iq['disco_info'].get_features():
- self.core.information('Attention is supported', 'Info')
- self.remote_supports_attention = True
- self.commands['attention'] = (self.command_attention, _('Usage: /attention [message]\nAttention: Require the attention of the contact. Can also send a message along with the attention.'), None)
- else:
- self.remote_supports_attention = False
-
- def command_unquery(self, arg):
- """
- /unquery
- """
- self.core.close_tab()
-
- def command_version(self, arg):
- """
- /version
- """
- def callback(res):
- if not res:
- return self.core.information('Could not get the software version from %s' % (jid,), 'Warning')
- version = '%s is running %s version %s on %s' % (jid,
- res.get('name') or _('an unknown software'),
- res.get('version') or _('unknown'),
- res.get('os') or _('an unknown platform'))
- self.core.information(version, 'Info')
- if arg:
- return self.core.command_version(arg)
- jid = safeJID(self.name)
- fixes.get_version(self.core.xmpp, jid, callback=callback)
-
- def command_info(self, arg):
- """
- /info
- """
- if arg:
- self.parent_muc.command_info(arg)
- else:
- user = safeJID(self.name).resource
- self.parent_muc.command_info(user)
-
- def resize(self):
- if self.core.information_win_size >= self.height-3 or not self.visible:
- return
- self.need_resize = False
- self.text_win.resize(self.height-2-self.core.information_win_size - Tab.tab_win_height(), self.width, 0, 0)
- self.text_win.rebuild_everything(self._text_buffer)
- self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
- self.input.resize(1, self.width, self.height-1, 0)
-
- def refresh(self):
- if self.need_resize:
- self.resize()
- log.debug(' TAB Refresh: %s',self.__class__.__name__)
- self.text_win.refresh()
- self.info_header.refresh(self.name, self.text_win, self.chatstate, PrivateTab.additional_informations)
- self.info_win.refresh()
- self.refresh_tab_win()
- self.input.refresh()
-
- def refresh_info_header(self):
- self.info_header.refresh(self.name, self.text_win, self.chatstate, PrivateTab.additional_informations)
- self.input.refresh()
-
- def get_name(self):
- return self.name
-
- def get_nick(self):
- return safeJID(self.name).resource
-
- def on_input(self, key, raw):
- if not raw and key in self.key_func:
- self.key_func[key]()
- return False
- self.input.do_command(key, raw=raw)
- if not self.on:
- return False
- empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//'))
- tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab)
- if tab and tab.joined:
- self.send_composing_chat_state(empty_after)
- return False
-
- def on_lose_focus(self):
- self.state = 'normal'
- self.text_win.remove_line_separator()
- self.text_win.add_line_separator(self._text_buffer)
- tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab)
- if tab and tab.joined and config.get_by_tabname(
- 'send_chat_states', 'true', self.general_jid, True) == 'true'\
- and not self.input.get_text() and self.on:
- self.send_chat_state('inactive')
- self.check_scrolled()
-
- def on_gain_focus(self):
- self.state = 'current'
- curses.curs_set(1)
- tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab)
- if tab and tab.joined and config.get_by_tabname(
- 'send_chat_states', 'true', self.general_jid, True) == 'true'\
- and not self.input.get_text() and self.on:
- self.send_chat_state('active')
-
- def on_info_win_size_changed(self):
- if self.core.information_win_size >= self.height-3:
- return
- self.text_win.resize(self.height-2-self.core.information_win_size - Tab.tab_win_height(), self.width, 0, 0)
- self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
-
- def get_text_window(self):
- return self.text_win
-
- def rename_user(self, old_nick, new_nick):
- """
- The user changed her nick in the corresponding muc: update the tab’s name and
- display a message.
- """
- self.add_message('\x193}%(old)s\x19%(info_col)s} is now known as \x193}%(new)s' % {'old':old_nick, 'new':new_nick, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
- new_jid = safeJID(self.name).bare+'/'+new_nick
- self.name = new_jid
-
- @refresh_wrapper.conditional
- def user_left(self, status_message, from_nick):
- """
- The user left the associated MUC
- """
- self.deactivate()
- if not status_message:
- self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x19%(info_col)s} has left the room') % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
- else:
- self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x19%(info_col)s} has left the room (%(status)s)"') % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT, 'status': status_message, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
- return self.core.current_tab() is self
-
- @refresh_wrapper.conditional
- def user_rejoined(self, nick):
- """
- The user (or at least someone with the same nick) came back in the MUC
- """
- self.activate()
- tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab)
- color = 3
- if tab and config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True):
- user = tab.get_user_by_name(nick)
- if user:
- color = dump_tuple(user.color)
- self.add_message('\x194}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} joined the room' % {'nick':nick, 'color': color, 'spec':get_theme().CHAR_JOIN, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
- return self.core.current_tab() is self
-
- def activate(self, reason=None):
- self.on = True
- if reason:
- self.add_message(txt=reason, typ=2)
-
- def deactivate(self, reason=None):
- self.on = False
- if reason:
- self.add_message(txt=reason, typ=2)
-
- def matching_names(self):
- return [(3, safeJID(self.get_name()).resource), (4, self.get_name())]
-
-class RosterInfoTab(Tab):
- """
- A tab, splitted in two, containing the roster and infos
- """
- plugin_commands = {}
- plugin_keys = {}
- def __init__(self):
- Tab.__init__(self)
- self.name = "Roster"
- self.v_separator = windows.VerticalSeparator()
- self.information_win = windows.TextWin()
- self.core.information_buffer.add_window(self.information_win)
- self.roster_win = windows.RosterWin()
- self.contact_info_win = windows.ContactInfoWin()
- self.default_help_message = windows.HelpText("Enter commands with “/”. “o”: toggle offline show")
- self.input = self.default_help_message
- self.state = 'normal'
- self.key_func['^I'] = self.completion
- self.key_func[' '] = self.on_space
- self.key_func["/"] = self.on_slash
- self.key_func["KEY_UP"] = self.move_cursor_up
- self.key_func["KEY_DOWN"] = self.move_cursor_down
- self.key_func["M-u"] = self.move_cursor_to_next_contact
- self.key_func["M-y"] = self.move_cursor_to_prev_contact
- self.key_func["M-U"] = self.move_cursor_to_next_group
- self.key_func["M-Y"] = self.move_cursor_to_prev_group
- self.key_func["M-[1;5B"] = self.move_cursor_to_next_group
- self.key_func["M-[1;5A"] = self.move_cursor_to_prev_group
- self.key_func["l"] = self.command_last_activity
- self.key_func["o"] = self.toggle_offline_show
- self.key_func["v"] = self.get_contact_version
- self.key_func["i"] = self.show_contact_info
- self.key_func["n"] = self.change_contact_name
- self.key_func["s"] = self.start_search
- self.key_func["S"] = self.start_search_slow
- self.register_command('deny', self.command_deny,
- usage=_('[jid]'),
- desc=_('Deny your presence to the provided JID (or the selected contact in your roster), who is asking you to be in his/here roster.'),
- shortdesc=_('Deny an user your presence.'),
- completion=self.completion_deny)
- self.register_command('accept', self.command_accept,
- usage=_('[jid]'),
- desc=_('Allow the provided JID (or the selected contact in your roster), to see your presence.'),
- shortdesc=_('Allow an user your presence.'),
- completion=self.completion_deny)
- self.register_command('add', self.command_add,
- usage=_('<jid>'),
- desc=_('Add the specified JID to your roster, ask him to allow you to see his presence, and allow him to see your presence.'),
- shortdesc=_('Add an user to your roster.'))
- self.register_command('name', self.command_name,
- usage=_('<jid> <name>'),
- shortdesc=_('Set the given JID\'s name.'),
- completion=self.completion_name)
- self.register_command('groupadd', self.command_groupadd,
- usage=_('<jid> <group>'),
- desc=_('Add the given JID to the given group.'),
- shortdesc=_('Add an user to a group'),
- completion=self.completion_groupadd)
- self.register_command('groupmove', self.command_groupmove,
- usage=_('<jid> <old group> <new group>'),
- desc=_('Move the given JID from the old group to the new group.'),
- shortdesc=_('Move an user to another group.'),
- completion=self.completion_groupmove)
- self.register_command('groupremove', self.command_groupremove,
- usage=_('<jid> <group>'),
- desc=_('Remove the given JID from the given group.'),
- shortdesc=_('Remove an user from a group.'),
- completion=self.completion_groupremove)
- self.register_command('remove', self.command_remove,
- usage=_('[jid]'),
- desc=_('Remove the specified JID from your roster. This wil unsubscribe you from its presence, cancel its subscription to yours, and remove the item from your roster.'),
- shortdesc=_('Remove an user from your roster.'),
- completion=self.completion_remove)
- self.register_command('reconnect', self.command_reconnect,
- desc=_('Disconnect from the remote server if you are currently connected and then connect to it again.'),
- shortdesc=_('Disconnect and reconnect to the server.'))
- self.register_command('disconnect', self.command_disconnect,
- desc=_('Disconnect from the remote server.'),
- shortdesc=_('Disconnect from the server.'))
- self.register_command('export', self.command_export,
- usage=_('[/path/to/file]'),
- desc=_('Export your contacts into /path/to/file if specified, or $HOME/poezio_contacts if not.'),
- shortdesc=_('Export your roster to a file.'),
- completion=self.completion_file)
- self.register_command('import', self.command_import,
- usage=_('[/path/to/file]'),
- desc=_('Import your contacts from /path/to/file if specified, or $HOME/poezio_contacts if not.'),
- shortdesc=_('Import your roster from a file.'),
- completion=self.completion_file)
- self.register_command('clear', self.command_clear,
- shortdesc=_('Clear the info buffer.'))
- self.register_command('last_activity', self.command_last_activity,
- usage=_('<jid>'),
- desc=_('Informs you of the last activity of a JID.'),
- shortdesc=_('Get the activity of someone.'),
- completion=self.core.completion_last_activity)
- self.register_command('password', self.command_password,
- usage='<password>',
- shortdesc=_('Change your password'))
-
- self.resize()
- self.update_commands()
- self.update_keys()
-
- def check_blocking(self, features):
- if 'urn:xmpp:blocking' in features:
- self.register_command('block', self.command_block,
- usage=_('[jid]'),
- shortdesc=_('Prevent a JID from talking to you.'),
- completion=self.completion_block)
- self.register_command('unblock', self.command_unblock,
- usage=_('[jid]'),
- shortdesc=_('Allow a JID to talk to you.'),
- completion=self.completion_unblock)
- self.register_command('list_blocks', self.command_list_blocks,
- shortdesc=_('Show the blocked contacts.'))
- self.core.xmpp.del_event_handler('session_start', self.check_blocking)
- self.core.xmpp.add_event_handler('blocked_message', self.on_blocked_message)
-
- def on_blocked_message(self, message):
- """
- When we try to send a message to a blocked contact
- """
- tab = self.core.get_conversation_by_jid(message['from'], False)
- if not tab:
- log.debug('Received message from nonexistent tab: %s', message['from'])
- message = '\x19%(info_col)s}Cannot send message to %(jid)s: contact blocked' % {
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
- 'jid': message['from'],
- }
- tab.add_message(message)
-
- def command_block(self, arg):
- """
- /block [jid]
- """
- def callback(iq):
- if iq['type'] == 'error':
- return self.core.information('Could not block the contact.', 'Error')
- elif iq['type'] == 'result':
- return self.core.information('Contact blocked.', 'Info')
-
- item = self.roster_win.selected_row
- if arg:
- jid = safeJID(arg)
- elif isinstance(item, Contact):
- jid = item.bare_jid
- elif isinstance(item, Resource):
- jid = item.jid.bare
- self.core.xmpp.plugin['xep_0191'].block(jid, block=False, callback=callback)
-
- def completion_block(self, the_input):
- """
- Completion for /block
- """
- if the_input.get_argument_position() == 1:
- jids = roster.jids()
- return the_input.new_completion(jids, 1, '', quotify=False)
-
- def command_unblock(self, arg):
- """
- /unblock [jid]
- """
- def callback(iq):
- if iq['type'] == 'error':
- return self.core.information('Could not unblock the contact.', 'Error')
- elif iq['type'] == 'result':
- return self.core.information('Contact unblocked.', 'Info')
-
- item = self.roster_win.selected_row
- if arg:
- jid = safeJID(arg)
- elif isinstance(item, Contact):
- jid = item.bare_jid
- elif isinstance(item, Resource):
- jid = item.jid.bare
- self.core.xmpp.plugin['xep_0191'].unblock(jid, block=False, callback=callback)
-
- def completion_unblock(self, the_input):
- """
- Completion for /unblock
- """
- if the_input.get_argument_position():
- try:
- iq = self.core.xmpp.plugin['xep_0191'].get_blocked(block=True)
- except Exception as e:
- iq = e.iq
- finally:
- if iq['type'] == 'error':
- return
- l = sorted(str(item) for item in iq['blocklist']['items'])
- return the_input.new_completion(l, 1, quotify=False)
-
- def command_list_blocks(self, arg=None):
- """
- /list_blocks
- """
- def callback(iq):
- if iq['type'] == 'error':
- return self.core.information('Could not retrieve the blocklist.', 'Error')
- s = 'List of blocked JIDs:\n'
- items = (str(item) for item in iq['blocklist']['items'])
- jids = '\n'.join(items)
- if jids:
- s += jids
- else:
- s = 'No blocked JIDs.'
- self.core.information(s, 'Info')
-
- self.core.xmpp.plugin['xep_0191'].get_blocked(block=False, callback=callback)
-
- def command_reconnect(self, args=None):
- """
- /reconnect
- """
- self.core.disconnect(reconnect=True)
-
- def command_disconnect(self, args=None):
- """
- /disconnect
- """
- self.core.disconnect()
-
- def command_last_activity(self, arg=None):
- """
- /activity [jid]
- """
- item = self.roster_win.selected_row
- if arg:
- jid = arg
- elif isinstance(item, Contact):
- jid = item.bare_jid
- elif isinstance(item, Resource):
- jid = item.jid.bare
- else:
- self.core.information('No JID selected.', 'Error')
- return
- self.core.command_last_activity(jid)
-
- def resize(self):
- if not self.visible:
- return
- self.need_resize = False
- roster_width = self.width//2
- info_width = self.width-roster_width-1
- self.v_separator.resize(self.height-1 - Tab.tab_win_height(), 1, 0, roster_width)
- self.information_win.resize(self.height-2-4, info_width, 0, roster_width+1, self.core.information_buffer)
- self.roster_win.resize(self.height-1 - Tab.tab_win_height(), roster_width, 0, 0)
- self.contact_info_win.resize(5 - Tab.tab_win_height(), info_width, self.height-2-4, roster_width+1)
- self.input.resize(1, self.width, self.height-1, 0)
-
- def completion(self):
- # Check if we are entering a command (with the '/' key)
- if isinstance(self.input, windows.Input) and\
- not self.input.help_message:
- self.complete_commands(self.input)
-
- def completion_file(self, the_input):
- """
- Completion for /import and /export
- """
- text = the_input.get_text()
- args = text.split()
- n = len(args)
- if n == 1:
- home = os.getenv('HOME') or '/'
- return the_input.auto_completion([home, '/tmp'], '')
- else:
- the_path = text[text.index(' ')+1:]
- try:
- names = os.listdir(the_path)
- except:
- names = []
- end_list = []
- for name in names:
- value = os.path.join(the_path, name)
- if not name.startswith('.'):
- end_list.append(value)
-
- return the_input.auto_completion(end_list, '')
-
- def command_clear(self, arg=''):
- """
- /clear
- """
- self.core.information_buffer.messages = []
- self.information_win.rebuild_everything(self.core.information_buffer)
- self.core.information_win.rebuild_everything(self.core.information_buffer)
- self.refresh()
-
- def command_password(self, arg):
- """
- /password <password>
- """
- def callback(iq):
- if iq['type'] == 'result':
- self.core.information('Password updated', 'Account')
- if config.get('password', ''):
- config.silent_set('password', arg)
- else:
- self.core.information('Unable to change the password', 'Account')
- self.core.xmpp.plugin['xep_0077'].change_password(arg, callback=callback)
-
-
-
- def command_deny(self, arg):
- """
- /deny [jid]
- Denies a JID from our roster
- """
- if not arg:
- item = self.roster_win.selected_row
- if isinstance(item, Contact):
- jid = item.bare_jid
- else:
- self.core.information('No subscription to deny')
- return
- else:
- jid = safeJID(arg).bare
- if not jid in [jid for jid in roster.jids()]:
- self.core.information('No subscription to deny')
- return
-
- contact = roster[jid]
- if contact:
- contact.unauthorize()
-
- def command_add(self, args):
- """
- Add the specified JID to the roster, and set automatically
- accept the reverse subscription
- """
- jid = safeJID(safeJID(args.strip()).bare)
- if not jid:
- self.core.information(_('No JID specified'), 'Error')
- return
- if jid in roster and roster[jid].subscription in ('to', 'both'):
- return self.core.information('Already subscribed.', 'Roster')
- roster.add(jid)
- roster.modified()
-
- def command_name(self, arg):
- """
- Set a name for the specified JID in your roster
- """
- def callback(iq):
- if not iq:
- self.core.information('The name could not be set.', 'Error')
- log.debug('Error in /name:\n%s', iq)
- args = common.shell_split(arg)
- if not args:
- return self.core.command_help('name')
- jid = safeJID(args[0]).bare
- name = args[1] if len(args) == 2 else ''
-
- contact = roster[jid]
- if contact is None:
- self.core.information(_('No such JID in roster'), 'Error')
- return
-
- groups = set(contact.groups)
- if 'none' in groups:
- groups.remove('none')
- subscription = contact.subscription
- self.core.xmpp.update_roster(jid, name=name, groups=groups, subscription=subscription,
- callback=callback, block=False)
-
- def command_groupadd(self, args):
- """
- Add the specified JID to the specified group
- """
- args = common.shell_split(args)
- if len(args) != 2:
- return
- jid = safeJID(args[0]).bare
- group = args[1]
-
- contact = roster[jid]
- if contact is None:
- self.core.information(_('No such JID in roster'), 'Error')
- return
-
- new_groups = set(contact.groups)
- if group in new_groups:
- self.core.information(_('JID already in group'), 'Error')
- return
-
- roster.modified()
- new_groups.add(group)
- try:
- new_groups.remove('none')
- except KeyError:
- pass
-
- name = contact.name
- subscription = contact.subscription
-
- def callback(iq):
- if iq:
- roster.update_contact_groups(jid)
- else:
- self.core.information('The group could not be set.', 'Error')
- log.debug('Error in groupadd:\n%s', iq)
-
- self.core.xmpp.update_roster(jid, name=name, groups=new_groups, subscription=subscription,
- callback=callback, block=False)
-
- def command_groupmove(self, arg):
- """
- Remove the specified JID from the first specified group and add it to the second one
- """
- args = common.shell_split(arg)
- if len(args) != 3:
- return self.core.command_help('groupmove')
- jid = safeJID(args[0]).bare
- group_from = args[1]
- group_to = args[2]
-
- contact = roster[jid]
- if not contact:
- self.core.information(_('No such JID in roster'), 'Error')
- return
-
- new_groups = set(contact.groups)
- if 'none' in new_groups:
- new_groups.remove('none')
-
- if group_to == 'none' or group_from == 'none':
- self.core.information(_('"none" is not a group.'), 'Error')
- return
-
- if group_from not in new_groups:
- self.core.information(_('JID not in first group'), 'Error')
- return
-
- if group_to in new_groups:
- self.core.information(_('JID already in second group'), 'Error')
- return
-
- if group_to == group_from:
- self.core.information(_('The groups are the same.'), 'Error')
- return
-
- roster.modified()
- new_groups.add(group_to)
- if 'none' in new_groups:
- new_groups.remove('none')
-
- new_groups.remove(group_from)
- name = contact.name
- subscription = contact.subscription
-
- def callback(iq):
- if iq:
- roster.update_contact_groups(contact)
- else:
- self.information('The group could not be set')
- log.debug('Error in groupmove:\n%s', iq)
-
- self.core.xmpp.update_roster(jid, name=name, groups=new_groups, subscription=subscription,
- callback=callback, block=False)
-
- def command_groupremove(self, args):
- """
- Remove the specified JID from the specified group
- """
- args = common.shell_split(args)
- if len(args) != 2:
- return
- jid = safeJID(args[0]).bare
- group = args[1]
-
- contact = roster[jid]
- if contact is None:
- self.core.information(_('No such JID in roster'), 'Error')
- return
-
- new_groups = set(contact.groups)
- try:
- new_groups.remove('none')
- except KeyError:
- pass
- if group not in new_groups:
- self.core.information(_('JID not in group'), 'Error')
- return
-
- roster.modified()
-
- new_groups.remove(group)
- name = contact.name
- subscription = contact.subscription
-
- def callback(iq):
- if iq:
- roster.update_contact_groups(jid)
- else:
- self.information('The group could not be set')
- log.debug('Error in groupremove:\n%s', iq)
-
- self.core.xmpp.update_roster(jid, name=name, groups=new_groups, subscription=subscription,
- callback=callback, block=False)
-
- def command_remove(self, args):
- """
- Remove the specified JID from the roster. i.e.: unsubscribe
- from its presence, and cancel its subscription to our.
- """
- if args.strip():
- jid = safeJID(args.strip()).bare
- else:
- item = self.roster_win.selected_row
- if isinstance(item, Contact):
- jid = item.bare_jid
- else:
- self.core.information('No roster item to remove')
- return
- roster.remove(jid)
- del roster[jid]
-
- def command_import(self, arg):
- """
- Import the contacts
- """
- args = common.shell_split(arg)
- if len(args):
- if args[0].startswith('/'):
- filepath = args[0]
- else:
- filepath = path.join(getenv('HOME'), args[0])
- else:
- filepath = path.join(getenv('HOME'), 'poezio_contacts')
- if not path.isfile(filepath):
- self.core.information('The file %s does not exist' % filepath, 'Error')
- return
- try:
- handle = open(filepath, 'r', encoding='utf-8')
- lines = handle.readlines()
- handle.close()
- except IOError:
- self.core.information('Could not open %s' % filepath, 'Error')
- log.error('Unable to correct a message', exc_info=True)
- return
- for jid in lines:
- self.command_add(jid.lstrip('\n'))
- self.core.information('Contacts imported from %s' % filepath, 'Info')
-
- def command_export(self, arg):
- """
- Export the contacts
- """
- args = common.shell_split(arg)
- if len(args):
- if args[0].startswith('/'):
- filepath = args[0]
- else:
- filepath = path.join(getenv('HOME'), args[0])
- else:
- filepath = path.join(getenv('HOME'), 'poezio_contacts')
- if path.isfile(filepath):
- self.core.information('The file already exists', 'Error')
- return
- elif not path.isdir(path.dirname(filepath)):
- self.core.information('Parent directory not found', 'Error')
- return
- if roster.export(filepath):
- self.core.information('Contacts exported to %s' % filepath, 'Info')
- else:
- self.core.information('Failed to export contacts to %s' % filepath, 'Info')
-
- def completion_remove(self, the_input):
- """
- Completion for /remove
- """
- jids = [jid for jid in roster.jids()]
- return the_input.auto_completion(jids, '', quotify=False)
-
- def completion_name(self, the_input):
- """Completion for /name"""
- n = the_input.get_argument_position()
- if n == 1:
- jids = [jid for jid in roster.jids()]
- return the_input.new_completion(jids, n, quotify=True)
- return False
-
- def completion_groupadd(self, the_input):
- n = the_input.get_argument_position()
- if n == 1:
- jids = sorted(jid for jid in roster.jids())
- return the_input.new_completion(jids, n, '', quotify=True)
- elif n == 2:
- groups = sorted(group for group in roster.groups if group != 'none')
- return the_input.new_completion(groups, n, '', quotify=True)
- return False
-
- def completion_groupmove(self, the_input):
- args = common.shell_split(the_input.text)
- n = the_input.get_argument_position()
- if n == 1:
- jids = sorted(jid for jid in roster.jids())
- return the_input.new_completion(jids, n, '', quotify=True)
- elif n == 2:
- contact = roster[args[1]]
- if not contact:
- return False
- groups = list(contact.groups)
- if 'none' in groups:
- groups.remove('none')
- return the_input.new_completion(groups, n, '', quotify=True)
- elif n == 3:
- groups = sorted(group for group in roster.groups)
- return the_input.new_completion(groups, n, '', quotify=True)
- return False
-
- def completion_groupremove(self, the_input):
- args = common.shell_split(the_input.text)
- n = the_input.get_argument_position()
- if n == 1:
- jids = sorted(jid for jid in roster.jids())
- return the_input.new_completion(jids, n, '', quotify=True)
- elif n == 2:
- contact = roster[args[1]]
- if contact is None:
- return False
- groups = sorted(contact.groups)
- try:
- groups.remove('none')
- except ValueError:
- pass
- return the_input.new_completion(groups, n, '', quotify=True)
- return False
-
- def completion_deny(self, the_input):
- """
- Complete the first argument from the list of the
- contact with ask=='subscribe'
- """
- jids = sorted(str(contact.bare_jid) for contact in roster.contacts.values()
- if contact.pending_in)
- return the_input.new_completion(jids, 1, '', quotify=False)
-
- def command_accept(self, arg):
- """
- Accept a JID from in roster. Authorize it AND subscribe to it
- """
- if not arg:
- item = self.roster_win.selected_row
- if isinstance(item, Contact):
- jid = item.bare_jid
- else:
- self.core.information('No subscription to accept')
- return
- else:
- jid = safeJID(arg).bare
- nodepart = safeJID(jid).user
- jid = safeJID(jid)
- # crappy transports putting resources inside the node part
- if '\\2f' in nodepart:
- jid.user = nodepart.split('\\2f')[0]
- contact = roster[jid]
- if contact is None:
- return
- contact.pending_in = False
- roster.modified()
- self.core.xmpp.send_presence(pto=jid, ptype='subscribed')
- self.core.xmpp.client_roster.send_last_presence()
- if contact.subscription in ('from', 'none') and not contact.pending_out:
- self.core.xmpp.send_presence(pto=jid, ptype='subscribe', pnick=self.core.own_nick)
-
- def refresh(self):
- if self.need_resize:
- self.resize()
- log.debug(' TAB Refresh: %s',self.__class__.__name__)
- self.v_separator.refresh()
- self.roster_win.refresh(roster)
- self.contact_info_win.refresh(self.roster_win.get_selected_row())
- self.information_win.refresh()
- self.refresh_tab_win()
- self.input.refresh()
-
- def get_name(self):
- return self.name
-
- def on_input(self, key, raw):
- if key == '^M':
- selected_row = self.roster_win.get_selected_row()
- res = self.input.do_command(key, raw=raw)
- if res and not isinstance(self.input, windows.Input):
- return True
- elif res:
- return False
- if key == '^M':
- self.core.on_roster_enter_key(selected_row)
- return selected_row
- elif not raw and key in self.key_func:
- return self.key_func[key]()
-
- @refresh_wrapper.conditional
- def toggle_offline_show(self):
- """
- Show or hide offline contacts
- """
- option = 'roster_show_offline'
- if config.get(option, 'false') == 'false':
- success = config.silent_set(option, 'true')
- else:
- success = config.silent_set(option, 'false')
- roster.modified()
- if not success:
- self.core.information(_('Unable to write in the config file'), 'Error')
- return True
-
- def on_slash(self):
- """
- '/' is pressed, we enter "input mode"
- """
- curses.curs_set(1)
- self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command)
- self.input.resize(1, self.width, self.height-1, 0)
- self.input.do_command("/") # we add the slash
-
- def reset_help_message(self, _=None):
- self.input = self.default_help_message
- if self.core.current_tab() is self:
- curses.curs_set(0)
- self.input.refresh()
- self.core.doupdate()
- return True
-
- def execute_slash_command(self, txt):
- if txt.startswith('/'):
- self.input.key_enter()
- self.execute_command(txt)
- return self.reset_help_message()
-
- def on_lose_focus(self):
- self.state = 'normal'
-
- def on_gain_focus(self):
- self.state = 'current'
- if isinstance(self.input, windows.HelpText):
- curses.curs_set(0)
- else:
- curses.curs_set(1)
-
- @refresh_wrapper.conditional
- def move_cursor_down(self):
- if isinstance(self.input, windows.Input) and not self.input.history_disabled:
- return
- return self.roster_win.move_cursor_down()
-
- @refresh_wrapper.conditional
- def move_cursor_up(self):
- if isinstance(self.input, windows.Input) and not self.input.history_disabled:
- return
- return self.roster_win.move_cursor_up()
-
- def move_cursor_to_prev_contact(self):
- self.roster_win.move_cursor_up()
- while not isinstance(self.roster_win.get_selected_row(), Contact):
- if not self.roster_win.move_cursor_up():
- break
- self.roster_win.refresh(roster)
-
- def move_cursor_to_next_contact(self):
- self.roster_win.move_cursor_down()
- while not isinstance(self.roster_win.get_selected_row(), Contact):
- if not self.roster_win.move_cursor_down():
- break
- self.roster_win.refresh(roster)
-
- def move_cursor_to_prev_group(self):
- self.roster_win.move_cursor_up()
- while not isinstance(self.roster_win.get_selected_row(), RosterGroup):
- if not self.roster_win.move_cursor_up():
- break
- self.roster_win.refresh(roster)
-
- def move_cursor_to_next_group(self):
- self.roster_win.move_cursor_down()
- while not isinstance(self.roster_win.get_selected_row(), RosterGroup):
- if not self.roster_win.move_cursor_down():
- break
- self.roster_win.refresh(roster)
-
- def on_scroll_down(self):
- return self.roster_win.move_cursor_down(self.height // 2)
-
- def on_scroll_up(self):
- return self.roster_win.move_cursor_up(self.height // 2)
-
- @refresh_wrapper.conditional
- def on_space(self):
- if isinstance(self.input, windows.Input):
- return
- selected_row = self.roster_win.get_selected_row()
- if isinstance(selected_row, RosterGroup):
- selected_row.toggle_folded()
- roster.modified()
- return True
- elif isinstance(selected_row, Contact):
- group = "none"
- found_group = False
- pos = self.roster_win.pos
- while not found_group and pos >= 0:
- row = self.roster_win.roster_cache[pos]
- pos -= 1
- if isinstance(row, RosterGroup):
- found_group = True
- group = row.name
- selected_row.toggle_folded(group)
- roster.modified()
- return True
- return False
-
- def get_contact_version(self):
- """
- Show the versions of the resource(s) currently selected
- """
- selected_row = self.roster_win.get_selected_row()
- if isinstance(selected_row, Contact):
- for resource in selected_row.resources:
- self.core.command_version(str(resource.jid))
- elif isinstance(selected_row, Resource):
- self.core.command_version(str(selected_row.jid))
- else:
- self.core.information('Nothing to get versions from', 'Info')
-
- def show_contact_info(self):
- """
- Show the contact info (resource number, status, presence, etc)
- when 'i' is pressed.
- """
- selected_row = self.roster_win.get_selected_row()
- if isinstance(selected_row, Contact):
- cont = selected_row
- res = selected_row.get_highest_priority_resource()
- acc = []
- acc.append('Contact: %s (%s)' % (cont.bare_jid, res.presence if res else 'unavailable'))
- if res:
- acc.append('%s connected resource%s' % (len(cont), '' if len(cont) == 1 else 's'))
- acc.append('Current status: %s' % res.status)
- if cont.tune:
- acc.append('Tune: %s' % common.format_tune_string(cont.tune))
- if cont.mood:
- acc.append('Mood: %s' % cont.mood)
- if cont.activity:
- acc.append('Activity: %s' % cont.activity)
- if cont.gaming:
- acc.append('Game: %s' % (common.format_gaming_string(cont.gaming)))
- msg = '\n'.join(acc)
- elif isinstance(selected_row, Resource):
- res = selected_row
- msg = 'Resource: %s (%s)\nCurrent status: %s\nPriority: %s' % (
- res.jid,
- res.presence,
- res.status,
- res.priority)
- elif isinstance(selected_row, RosterGroup):
- rg = selected_row
- msg = 'Group: %s [%s/%s] contacts online' % (
- rg.name,
- rg.get_nb_connected_contacts(),
- len(rg),)
- else:
- msg = None
- if msg:
- self.core.information(msg, 'Info')
-
- def change_contact_name(self):
- """
- Auto-fill a /name command when 'n' is pressed
- """
- selected_row = self.roster_win.get_selected_row()
- if isinstance(selected_row, Contact):
- jid = selected_row.bare_jid
- elif isinstance(selected_row, Resource):
- jid = safeJID(selected_row.jid).bare
- else:
- return
- self.on_slash()
- self.input.text = '/name "%s" ' % jid
- self.input.key_end()
- self.input.refresh()
-
- @refresh_wrapper.always
- def start_search(self):
- """
- Start the search. The input should appear with a short instruction
- in it.
- """
- curses.curs_set(1)
- self.input = windows.CommandInput("[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter)
- self.input.resize(1, self.width, self.height-1, 0)
- self.input.disable_history()
- roster.modified()
- self.refresh()
- return True
-
- @refresh_wrapper.always
- def start_search_slow(self):
- curses.curs_set(1)
- self.input = windows.CommandInput("[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter_slow)
- self.input.resize(1, self.width, self.height-1, 0)
- self.input.disable_history()
- return True
-
- def set_roster_filter_slow(self, txt):
- roster.contact_filter = (jid_and_name_match_slow, txt)
- roster.modified()
- self.refresh()
- return False
-
- def set_roster_filter(self, txt):
- roster.contact_filter = (jid_and_name_match, txt)
- roster.modified()
- self.refresh()
- return False
-
- @refresh_wrapper.always
- def on_search_terminate(self, txt):
- curses.curs_set(0)
- roster.contact_filter = None
- self.reset_help_message()
- roster.modified()
- return True
-
- def on_close(self):
- return
-
-class ConversationTab(ChatTab):
- """
- The tab containg a normal conversation (not from a MUC)
- Must not be instantiated, use Static or Dynamic version only.
- """
- plugin_commands = {}
- plugin_keys = {}
- additional_informations = {}
- message_type = 'chat'
- def __init__(self, jid):
- ChatTab.__init__(self, jid)
- self.nick = None
- self.nick_sent = False
- self.state = 'normal'
- self.name = jid # a conversation tab is linked to one specific full jid OR bare jid
- self.text_win = windows.TextWin()
- self._text_buffer.add_window(self.text_win)
- self.upper_bar = windows.ConversationStatusMessageWin()
- self.input = windows.MessageInput()
- self.check_attention()
- # keys
- self.key_func['^I'] = self.completion
- # commands
- self.register_command('unquery', self.command_unquery,
- shortdesc=_('Close the tab.'))
- self.register_command('close', self.command_unquery,
- shortdesc=_('Close the tab.'))
- self.register_command('version', self.command_version,
- desc=_('Get the software version of the current interlocutor (usually its XMPP client and Operating System).'),
- shortdesc=_('Get the software version of the user.'))
- self.register_command('info', self.command_info,
- shortdesc=_('Get the status of the contact.'))
- self.register_command('last_activity', self.command_last_activity,
- usage=_('[jid]'),
- desc=_('Get the last activity of the given or the current contact.'),
- shortdesc=_('Get the activity.'),
- completion=self.core.completion_last_activity)
- self.resize()
- self.update_commands()
- self.update_keys()
-
- @property
- def general_jid(self):
- return safeJID(self.get_name()).bare
-
- @staticmethod
- def add_information_element(plugin_name, callback):
- """
- Lets a plugin add its own information to the ConversationInfoWin
- """
- ConversationTab.additional_informations[plugin_name] = callback
-
- @staticmethod
- def remove_information_element(plugin_name):
- del ConversationTab.additional_informations[plugin_name]
-
- def completion(self):
- self.complete_commands(self.input)
-
- def command_say(self, line, attention=False, correct=False):
- msg = self.core.xmpp.make_message(self.get_dest_jid())
- msg['type'] = 'chat'
- msg['body'] = line
- if not self.nick_sent:
- msg['nick'] = self.core.own_nick
- self.nick_sent = True
- # trigger the event BEFORE looking for colors.
- # and before displaying the message in the window
- # This lets a plugin insert \x19xxx} colors, that will
- # be converted in xhtml.
- self.core.events.trigger('conversation_say', msg, self)
- if not msg['body']:
- self.cancel_paused_delay()
- self.text_win.refresh()
- self.input.refresh()
- return
- replaced = False
- if correct or msg['replace']['id']:
- msg['replace']['id'] = self.last_sent_message['id']
- if config.get_by_tabname('group_corrections', 'true', self.get_name()).lower() != 'false':
- try:
- self.modify_message(msg['body'], self.last_sent_message['id'], msg['id'], jid=self.core.xmpp.boundjid,
- nickname=self.core.own_nick)
- replaced = True
- except:
- log.error('Unable to correct a message', exc_info=True)
- else:
- del msg['replace']
- if msg['body'].find('\x19') != -1:
- msg.enable('html')
- msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body'])
- msg['body'] = xhtml.clean_text(msg['body'])
- if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and self.remote_wants_chatstates is not False:
- needed = 'inactive' if self.inactive else 'active'
- msg['chat_state'] = needed
- if attention and self.remote_supports_attention:
- msg['attention'] = True
- self.core.events.trigger('conversation_say_after', msg, self)
- if not msg['body']:
- self.cancel_paused_delay()
- self.text_win.refresh()
- self.input.refresh()
- return
- if not replaced:
- self.add_message(msg['body'],
- nickname=self.core.own_nick,
- nick_color=get_theme().COLOR_OWN_NICK,
- identifier=msg['id'],
- jid=self.core.xmpp.boundjid,
- typ=1)
-
- self.last_sent_message = msg
- msg.send()
- self.cancel_paused_delay()
- self.text_win.refresh()
- self.input.refresh()
-
- def command_xhtml(self, arg):
- message = self.generate_xhtml_message(arg)
- if message:
- message.send()
- self.core.add_message_to_text_buffer(self._text_buffer, body, None, self.core.own_nick)
- self.refresh()
-
- def command_last_activity(self, arg):
- """
- /activity [jid]
- """
- if arg.strip():
- return self.core.command_last_activity(arg)
-
- def callback(iq):
- if iq['type'] != 'result':
- if iq['error']['type'] == 'auth':
- self.information('You are not allowed to see the activity of this contact.', 'Error')
- else:
- self.information('Error retrieving the activity', 'Error')
- return
- seconds = iq['last_activity']['seconds']
- status = iq['last_activity']['status']
- from_ = iq['from']
- msg = '\x19%s}The last activity of %s was %s ago%s'
- if not safeJID(from_).user:
- msg = '\x19%s}The uptime of %s is %s.' % (
- dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
- from_,
- common.parse_secs_to_str(seconds))
- else:
- msg = '\x19%s}The last activity of %s was %s ago%s' % (
- dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
- from_,
- common.parse_secs_to_str(seconds),
- (' and his/her last status was %s' % status) if status else '',)
- self.add_message(msg)
- self.core.refresh_window()
-
- self.core.xmpp.plugin['xep_0012'].get_last_activity(self.general_jid, block=False, callback=callback)
-
- @refresh_wrapper.conditional
- def command_info(self, arg):
- contact = roster[self.get_dest_jid()]
- jid = safeJID(self.get_dest_jid())
- if contact:
- if jid.resource:
- resource = contact[jid.full]
- else:
- resource = contact.get_highest_priority_resource()
- else:
- resource = None
- if resource:
- status = (_('Status: %s') % resource.status) if resource.status else ''
- self._text_buffer.add_message("\x19%(info_col)s}Show: %(show)s, %(status)s\x19o" % {
- 'show': resource.show or 'available', 'status': status, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)})
- return True
- else:
- self._text_buffer.add_message("\x19%(info_col)s}No information available\x19o" % {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)})
- return True
-
-
- def command_attention(self, message=''):
- if message is not '':
- self.command_say(message, attention=True)
- else:
- msg = self.core.xmpp.make_message(self.get_dest_jid())
- msg['type'] = 'chat'
- msg['attention'] = True
- msg.send()
-
- def check_attention(self):
- self.core.xmpp.plugin['xep_0030'].get_info(jid=self.get_dest_jid(), block=False, timeout=5, callback=self.on_attention_checked)
-
- def on_attention_checked(self, iq):
- if 'urn:xmpp:attention:0' in iq['disco_info'].get_features():
- self.core.information('Attention is supported', 'Info')
- self.remote_supports_attention = True
- self.commands['attention'] = (self.command_attention, _('Usage: /attention [message]\nAttention: Require the attention of the contact. Can also send a message along with the attention.'), None)
- else:
- self.remote_supports_attention = False
-
- def command_unquery(self, arg):
- self.core.close_tab()
-
- def command_version(self, arg):
- """
- /version
- """
- def callback(res):
- if not res:
- return self.core.information('Could not get the software version from %s' % (jid,), 'Warning')
- version = '%s is running %s version %s on %s' % (jid,
- res.get('name') or _('an unknown software'),
- res.get('version') or _('unknown'),
- res.get('os') or _('an unknown platform'))
- self.core.information(version, 'Info')
- if arg:
- return self.core.command_version(arg)
- jid = safeJID(self.name)
- if not jid.resource:
- if jid in roster:
- resource = roster[jid].get_highest_priority_resource()
- jid = resource.jid if resource else jid
- fixes.get_version(self.core.xmpp, jid, callback=callback)
-
- def resize(self):
- if self.core.information_win_size >= self.height-3 or not self.visible:
- return
- self.need_resize = False
- self.text_win.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width, 1, 0)
- self.text_win.rebuild_everything(self._text_buffer)
- self.upper_bar.resize(1, self.width, 0, 0)
- self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
- self.input.resize(1, self.width, self.height-1, 0)
-
- def refresh(self):
- if self.need_resize:
- self.resize()
- log.debug(' TAB Refresh: %s',self.__class__.__name__)
- self.text_win.refresh()
- self.upper_bar.refresh(self.get_dest_jid(), roster[self.get_dest_jid()])
- self.info_header.refresh(self.get_dest_jid(), roster[self.get_dest_jid()], self.text_win, self.chatstate, ConversationTab.additional_informations)
- self.info_win.refresh()
- self.refresh_tab_win()
- self.input.refresh()
-
- def refresh_info_header(self):
- self.info_header.refresh(self.get_dest_jid(), roster[self.get_dest_jid()],
- self.text_win, self.chatstate, ConversationTab.additional_informations)
- self.input.refresh()
-
- def get_name(self):
- return self.name
-
- def get_nick(self):
- jid = safeJID(self.name)
- contact = roster[jid.bare]
- if contact:
- return contact.name or jid.user
- else:
- if self.nick:
- return self.nick
- return jid.user
-
- def on_input(self, key, raw):
- if not raw and key in self.key_func:
- self.key_func[key]()
- return False
- self.input.do_command(key, raw=raw)
- empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//'))
- self.send_composing_chat_state(empty_after)
- return False
-
- def on_lose_focus(self):
- contact = roster[self.get_dest_jid()]
- jid = safeJID(self.get_dest_jid())
- if contact:
- if jid.resource:
- resource = contact[jid.full]
- else:
- resource = contact.get_highest_priority_resource()
- else:
- resource = None
- self.state = 'normal'
- self.text_win.remove_line_separator()
- self.text_win.add_line_separator(self._text_buffer)
- if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and (not self.input.get_text() or not self.input.get_text().startswith('//')):
- if resource:
- self.send_chat_state('inactive')
- self.check_scrolled()
-
- def on_gain_focus(self):
- contact = roster[self.get_dest_jid()]
- jid = safeJID(self.get_dest_jid())
- if contact:
- if jid.resource:
- resource = contact[jid.full]
- else:
- resource = contact.get_highest_priority_resource()
- else:
- resource = None
-
- self.state = 'current'
- curses.curs_set(1)
- if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and (not self.input.get_text() or not self.input.get_text().startswith('//')):
- if resource:
- self.send_chat_state('active')
-
- def on_info_win_size_changed(self):
- if self.core.information_win_size >= self.height-3:
- return
- self.text_win.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width, 1, 0)
- self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
-
- def get_text_window(self):
- return self.text_win
-
- def on_close(self):
- Tab.on_close(self)
- if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true':
- self.send_chat_state('gone')
-
- def matching_names(self):
- res = []
- jid = safeJID(self.get_name())
- res.append((2, jid.bare))
- res.append((1, jid.user))
- contact = roster[self.get_name()]
- if contact and contact.name:
- res.append((0, contact.name))
- return res
-
-class DynamicConversationTab(ConversationTab):
- """
- A conversation tab associated with one bare JID that can be “locked” to
- a full jid, and unlocked, as described in the XEP-0296.
- Only one DynamicConversationTab can be opened for a given jid.
- """
- def __init__(self, jid, resource=None):
- self.locked_resource = None
- self.name = safeJID(jid).bare
- if resource:
- self.lock(resource)
- self.info_header = windows.DynamicConversationInfoWin()
- ConversationTab.__init__(self, jid)
- self.register_command('unlock', self.unlock_command,
- shortdesc=_('Unlock the converstation from a particular resource.'))
-
- def lock(self, resource):
- """
- Lock the tab to the resource.
- """
- assert(resource)
- self.locked_resource = resource
-
- def unlock_command(self, arg=None):
- self.unlock()
- self.refresh_info_header()
-
- def unlock(self):
- """
- Unlock the tab from a resource. It is now “associated” with the bare
- jid.
- """
- self.locked_resource = None
-
- def get_dest_jid(self):
- """
- Returns the full jid (using the locked resource), or the bare jid if
- the conversation is not locked.
- """
- if self.locked_resource:
- return "%s/%s" % (self.get_name(), self.locked_resource)
- return self.get_name()
-
- def refresh(self):
- """
- Different from the parent class only for the info_header object.
- """
- if self.need_resize:
- self.resize()
- log.debug(' TAB Refresh: %s',self.__class__.__name__)
- self.text_win.refresh()
- self.upper_bar.refresh(self.get_name(), roster[self.get_name()])
- if self.locked_resource:
- displayed_jid = "%s/%s" % (self.get_name(), self.locked_resource)
- else:
- displayed_jid = self.get_name()
- self.info_header.refresh(displayed_jid, roster[self.get_name()], self.text_win, self.chatstate, ConversationTab.additional_informations)
- self.info_win.refresh()
- self.refresh_tab_win()
- self.input.refresh()
-
- def refresh_info_header(self):
- """
- Different from the parent class only for the info_header object.
- """
- if self.locked_resource:
- displayed_jid = "%s/%s" % (self.get_name(), self.locked_resource)
- else:
- displayed_jid = self.get_name()
- self.info_header.refresh(displayed_jid, roster[self.get_name()],
- self.text_win, self.chatstate, ConversationTab.additional_informations)
- self.input.refresh()
-
-class StaticConversationTab(ConversationTab):
- """
- A conversation tab associated with one Full JID. It cannot be locked to
- an different resource or unlocked.
- """
- def __init__(self, jid):
- assert(safeJID(jid).resource)
- self.info_header = windows.ConversationInfoWin()
- ConversationTab.__init__(self, jid)
-
-class MucListTab(Tab):
- """
- A tab listing rooms from a specific server, displaying various information,
- scrollable, and letting the user join them, etc
- """
- plugin_commands = {}
- plugin_keys = {}
- def __init__(self, server):
- Tab.__init__(self)
- self.state = 'normal'
- self.name = server
- columns = ('node-part', 'name', 'users')
- self.list_header = windows.ColumnHeaderWin(columns)
- self.listview = windows.ListWin(columns)
- self.info_header = windows.MucListInfoWin(_('Chatroom list on server %s (Loading)') % self.name)
- self.default_help_message = windows.HelpText("“j”: join room.")
- self.input = self.default_help_message
- self.key_func["KEY_DOWN"] = self.move_cursor_down
- self.key_func["KEY_UP"] = self.move_cursor_up
- self.key_func['^I'] = self.completion
- self.key_func["/"] = self.on_slash
- self.key_func['j'] = self.join_selected
- self.key_func['J'] = self.join_selected_no_focus
- self.key_func['^M'] = self.join_selected
- self.key_func['KEY_LEFT'] = self.list_header.sel_column_left
- self.key_func['KEY_RIGHT'] = self.list_header.sel_column_right
- self.key_func[' '] = self.sort_by
- self.register_command('close', self.close,
- shortdesc=_('Close this tab.'))
- self.resize()
- self.update_keys()
- self.update_commands()
-
- def refresh(self):
- if self.need_resize:
- self.resize()
- log.debug(' TAB Refresh: %s',self.__class__.__name__)
- self.info_header.refresh()
- self.info_win.refresh()
- self.refresh_tab_win()
- self.list_header.refresh()
- self.listview.refresh()
- self.input.refresh()
- self.update_commands()
-
- def resize(self):
- if self.core.information_win_size >= self.height-3 or not self.visible:
- return
- self.need_resize = False
- self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
- column_size = {'node-part': int(self.width*2/8) ,
- 'name': int(self.width*5/8),
- 'users': self.width-int(self.width*2/8)-int(self.width*5/8)}
- self.list_header.resize_columns(column_size)
- self.list_header.resize(1, self.width, 0, 0)
- self.listview.resize_columns(column_size)
- self.listview.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width, 1, 0)
- self.input.resize(1, self.width, self.height-1, 0)
-
- def on_slash(self):
- """
- '/' is pressed, activate the input
- """
- curses.curs_set(1)
- self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command)
- self.input.resize(1, self.width, self.height-1, 0)
- self.input.do_command("/") # we add the slash
-
- def close(self, arg=None):
- self.input.on_delete()
- self.core.close_tab(self)
-
- def join_selected_no_focus(self):
- return
-
- def set_error(self, msg, code, body):
- """
- If there's an error (retrieving the values etc)
- """
- self._error_message = _('Error: %(code)s - %(msg)s: %(body)s') % {'msg':msg, 'body':body, 'code':code}
- self.info_header.message = self._error_message
- self.info_header.refresh()
- curses.doupdate()
-
- def on_muc_list_item_received(self, iq):
- """
- Callback called when a disco#items result is received
- Used with command_list
- """
- if iq['type'] == 'error':
- self.set_error(iq['error']['type'], iq['error']['code'], iq['error']['text'])
- return
- items = [{'node-part': safeJID(item[0]).user if safeJID(item[0]).server == self.name else safeJID(item[0]).bare,
- 'jid': item[0],
- 'name': item[2] or '' ,'users': ''} for item in iq['disco_items'].get_items()]
- self.listview.add_lines(items)
- self.info_header.message = _('Chatroom list on server %s') % self.name
- if self.core.current_tab() is self:
- self.listview.refresh()
- self.info_header.refresh()
- else:
- self.state = 'highlight'
- self.refresh_tab_win()
- curses.doupdate()
-
- def sort_by(self):
- if self.list_header.get_order():
- self.listview.sort_by_column(col_name=self.list_header.get_sel_column(),asc=False)
- self.list_header.set_order(False)
- self.list_header.refresh()
- else:
- self.listview.sort_by_column(col_name=self.list_header.get_sel_column(),asc=True)
- self.list_header.set_order(True)
- self.list_header.refresh()
- curses.doupdate()
-
- def join_selected(self):
- row = self.listview.get_selected_row()
- if not row:
- return
- self.core.command_join(row['jid'])
-
- @refresh_wrapper.always
- def reset_help_message(self, _=None):
- curses.curs_set(0)
- self.input = self.default_help_message
- self.input.resize(1, self.width, self.height-1, 0)
- return True
-
- def execute_slash_command(self, txt):
- if txt.startswith('/'):
- self.input.key_enter()
- self.execute_command(txt)
- return self.reset_help_message()
-
- def get_name(self):
- return self.name
-
- def completion(self):
- if isinstance(self.input, windows.Input):
- self.complete_commands(self.input)
-
- def on_input(self, key, raw):
- res = self.input.do_command(key, raw=raw)
- if res:
- return True
- if not raw and key in self.key_func:
- return self.key_func[key]()
-
- def on_info_win_size_changed(self):
- if self.core.information_win_size >= self.height-3:
- return
- self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
- self.listview.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width, 1, 0)
-
- def on_lose_focus(self):
- self.state = 'normal'
-
- def on_gain_focus(self):
- self.state = 'current'
- curses.curs_set(0)
-
- def on_scroll_up(self):
- return self.listview.scroll_up()
-
- def on_scroll_down(self):
- return self.listview.scroll_down()
-
- def move_cursor_up(self):
- self.listview.move_cursor_up()
- self.listview.refresh()
- self.core.doupdate()
-
- def move_cursor_down(self):
- self.listview.move_cursor_down()
- self.listview.refresh()
- self.core.doupdate()
-
- def matching_names(self):
- return [(2, self.name)]
-
-class XMLTab(Tab):
- def __init__(self):
- Tab.__init__(self)
- self.state = 'normal'
- self.text_win = windows.TextWin()
- self.core.xml_buffer.add_window(self.text_win)
- self.info_header = windows.XMLInfoWin()
- self.default_help_message = windows.HelpText("/ to enter a command")
- self.register_command('close', self.close,
- shortdesc=_("Close this tab."))
- self.register_command('clear', self.command_clear,
- shortdesc=_('Clear the current buffer.'))
- self.register_command('reset', self.command_reset,
- shortdesc=_('Reset the stanza filter.'))
- self.register_command('filter_id', self.command_filter_id,
- usage='<id>',
- desc=_('Show only the stanzas with the id <id>.'),
- shortdesc=_('Filter by id.'))
- self.register_command('filter_xpath', self.command_filter_xpath,
- usage='<xpath>',
- desc=_('Show only the stanzas matching the xpath <xpath>.'),
- shortdesc=_('Filter by XPath.'))
- self.register_command('filter_xmlmask', self.command_filter_xmlmask,
- usage=_('<xml mask>'),
- desc=_('Show only the stanzas matching the given xml mask.'),
- shortdesc=_('Filter by xml mask.'))
- self.input = self.default_help_message
- self.key_func['^T'] = self.close
- self.key_func['^I'] = self.completion
- self.key_func["KEY_DOWN"] = self.on_scroll_down
- self.key_func["KEY_UP"] = self.on_scroll_up
- self.key_func["^K"] = self.on_freeze
- self.key_func["/"] = self.on_slash
- self.resize()
- # Used to display the infobar
- self.filter_type = ''
- self.filter = ''
-
- def on_freeze(self):
- """
- Freeze the display.
- """
- self.text_win.toggle_lock()
- self.refresh()
-
- def command_filter_xmlmask(self, arg):
- """/filter_xmlmask <xml mask>"""
- try:
- handler = Callback('custom matcher', matcher.MatchXMLMask(arg),
- self.core.incoming_stanza)
- self.core.xmpp.remove_handler('custom matcher')
- self.core.xmpp.register_handler(handler)
- self.filter_type = "XML Mask Filter"
- self.filter = arg
- self.refresh()
- except:
- self.core.information('Invalid XML Mask', 'Error')
- self.command_reset('')
-
- def command_filter_id(self, arg):
- """/filter_id <id>"""
- self.core.xmpp.remove_handler('custom matcher')
- handler = Callback('custom matcher', matcher.MatcherId(arg),
- self.core.incoming_stanza)
- self.core.xmpp.register_handler(handler)
- self.filter_type = "Id Filter"
- self.filter = arg
- self.refresh()
-
- def command_filter_xpath(self, arg):
- """/filter_xpath <xpath>"""
- try:
- handler = Callback('custom matcher', matcher.MatchXPath(
- arg.replace('%n', self.core.xmpp.default_ns)),
- self.core.incoming_stanza)
- self.core.xmpp.remove_handler('custom matcher')
- self.core.xmpp.register_handler(handler)
- self.filter_type = "XPath Filter"
- self.filter = arg
- self.refresh()
- except:
- self.core.information('Invalid XML Path', 'Error')
- self.command_reset('')
-
- def command_reset(self, arg):
- """/reset"""
- self.core.xmpp.remove_handler('custom matcher')
- self.core.xmpp.register_handler(self.core.all_stanzas)
- self.filter_type = ''
- self.filter = ''
- self.refresh()
-
- def on_slash(self):
- """
- '/' is pressed, activate the input
- """
- curses.curs_set(1)
- self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command)
- self.input.resize(1, self.width, self.height-1, 0)
- self.input.do_command("/") # we add the slash
-
- def reset_help_message(self, _=None):
- if self.core.current_tab() is self:
- curses.curs_set(0)
- self.input = self.default_help_message
- self.input.refresh()
- self.core.doupdate()
- return True
-
- def on_scroll_up(self):
- return self.text_win.scroll_up(self.text_win.height-1)
-
- def on_scroll_down(self):
- return self.text_win.scroll_down(self.text_win.height-1)
-
- def command_clear(self, args):
- """
- /clear
- """
- self.core.xml_buffer.messages = []
- self.text_win.rebuild_everything(self.core.xml_buffer)
- self.refresh()
- self.core.doupdate()
-
- def execute_slash_command(self, txt):
- if txt.startswith('/'):
- self.input.key_enter()
- self.execute_command(txt)
- return self.reset_help_message()
-
- def completion(self):
- if isinstance(self.input, windows.Input):
- self.complete_commands(self.input)
-
- def on_input(self, key, raw):
- res = self.input.do_command(key, raw=raw)
- if res:
- return True
- if not raw and key in self.key_func:
- return self.key_func[key]()
-
- def close(self, arg=None):
- self.core.close_tab()
-
- def resize(self):
- if self.core.information_win_size >= self.height-3 or not self.visible:
- return
- self.need_resize = False
- min = 1 if self.left_tab_win else 2
- self.text_win.resize(self.height-self.core.information_win_size - Tab.tab_win_height() - 2, self.width, 0, 0)
- self.text_win.rebuild_everything(self.core.xml_buffer)
- self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
- self.input.resize(1, self.width, self.height-1, 0)
-
- def refresh(self):
- if self.need_resize:
- self.resize()
- log.debug(' TAB Refresh: %s',self.__class__.__name__)
- self.text_win.refresh()
- self.info_header.refresh(self.filter_type, self.filter, self.text_win)
- self.refresh_tab_win()
- self.info_win.refresh()
- self.input.refresh()
-
- def on_lose_focus(self):
- self.state = 'normal'
-
- def on_gain_focus(self):
- self.state = 'current'
- curses.curs_set(0)
-
- def on_close(self):
- self.command_clear('')
- self.core.xml_tab = False
-
- def on_info_win_size_changed(self):
- if self.core.information_win_size >= self.height-3:
- return
- self.text_win.resize(self.height-2-self.core.information_win_size - Tab.tab_win_height(), self.width, 0, 0)
- self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
-
-class SimpleTextTab(Tab):
- """
- A very simple tab, with just a text displaying some
- information or whatever.
- For example used to display tracebacks
- """
- def __init__(self, text):
- Tab.__init__(self)
- self.state = 'normal'
- self.text_win = windows.SimpleTextWin(text)
- self.default_help_message = windows.HelpText("“Ctrl+q”: close")
- self.input = self.default_help_message
- self.key_func['^T'] = self.close
- self.key_func["/"] = self.on_slash
- self.resize()
-
- def on_slash(self):
- """
- '/' is pressed, activate the input
- """
- curses.curs_set(1)
- self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command)
- self.input.resize(1, self.width, self.height-1, 0)
- self.input.do_command("/") # we add the slash
-
- def on_input(self, key, raw):
- res = self.input.do_command(key, raw=raw)
- if res:
- return True
- if not raw and key in self.key_func:
- return self.key_func[key]()
-
- def close(self):
- self.core.close_tab()
-
- def resize(self):
- if not self.visible:
- return
- self.need_resize = False
- self.text_win.resize(self.height-2, self.width, 0, 0)
- self.input.resize(1, self.width, self.height-1, 0)
-
- def refresh(self):
- if self.need_resize:
- self.resize()
- log.debug(' TAB Refresh: %s',self.__class__.__name__)
- self.text_win.refresh()
- self.refresh_tab_win()
- self.input.refresh()
-
- def on_lose_focus(self):
- self.state = 'normal'
-
- def on_gain_focus(self):
- self.state = 'current'
- curses.curs_set(0)
-
-def diffmatch(search, string):
- """
- Use difflib and a loop to check if search_pattern can
- be 'almost' found INSIDE a string.
- 'almost' being defined by difflib
- """
- if len(search) > len(string):
- return False
- l = len(search)
- ratio = 0.7
- for i in range(len(string) - l + 1):
- if difflib.SequenceMatcher(None, search, string[i:i+l]).ratio() >= ratio:
- return True
- return False
-
-def jid_and_name_match(contact, txt):
- """
- Match jid with text precisely
- """
- if not txt:
- return True
- txt = txt.lower()
- if txt in safeJID(contact.bare_jid).bare.lower():
- return True
- if txt in contact.name.lower():
- return True
- return False
-
-def jid_and_name_match_slow(contact, txt):
- """
- A function used to know if a contact in the roster should
- be shown in the roster
- """
- if not txt:
- return True # Everything matches when search is empty
- user = safeJID(contact.bare_jid).bare
- if diffmatch(txt, user):
- return True
- if contact.name and diffmatch(txt, contact.name):
- return True
- return False
diff --git a/src/tabs/__init__.py b/src/tabs/__init__.py
new file mode 100644
index 00000000..1100db09
--- /dev/null
+++ b/src/tabs/__init__.py
@@ -0,0 +1,8 @@
+from . basetabs import Tab, ChatTab, GapTab, STATE_PRIORITY
+from . rostertab import RosterInfoTab
+from . muctab import MucTab
+from . privatetab import PrivateTab
+from . conversationtab import ConversationTab, StaticConversationTab,\
+ DynamicConversationTab
+from . xmltab import XMLTab
+from . muclisttab import MucListTab
diff --git a/src/tabs/basetabs.py b/src/tabs/basetabs.py
new file mode 100644
index 00000000..86ba9e1f
--- /dev/null
+++ b/src/tabs/basetabs.py
@@ -0,0 +1,666 @@
+"""
+A Tab object is a way to organize various Windows (see windows.py)
+around the screen at once.
+A tab is then composed of multiple Buffers.
+Each Tab object has different refresh() and resize() methods, defining how its
+Windows are displayed, resized, etc.
+"""
+
+from gettext import gettext as _
+
+import logging
+log = logging.getLogger(__name__)
+
+import singleton
+import string
+import time
+import weakref
+from datetime import datetime, timedelta
+from xml.etree import cElementTree as ET
+
+import core
+import timed_events
+import windows
+import xhtml
+from common import safeJID
+from config import config
+from decorators import refresh_wrapper
+from logger import logger
+from text_buffer import TextBuffer, CorrectionError
+from theming import get_theme
+
+
+MIN_WIDTH = 42
+MIN_HEIGHT = 6
+
+STATE_COLORS = {
+ 'disconnected': lambda: get_theme().COLOR_TAB_DISCONNECTED,
+ 'scrolled': lambda: get_theme().COLOR_TAB_SCROLLED,
+ 'joined': lambda: get_theme().COLOR_TAB_JOINED,
+ 'message': lambda: get_theme().COLOR_TAB_NEW_MESSAGE,
+ 'highlight': lambda: get_theme().COLOR_TAB_HIGHLIGHT,
+ 'private': lambda: get_theme().COLOR_TAB_PRIVATE,
+ 'normal': lambda: get_theme().COLOR_TAB_NORMAL,
+ 'current': lambda: get_theme().COLOR_TAB_CURRENT,
+ 'attention': lambda: get_theme().COLOR_TAB_ATTENTION,
+ }
+
+VERTICAL_STATE_COLORS = {
+ 'disconnected': lambda: get_theme().COLOR_VERTICAL_TAB_DISCONNECTED,
+ 'scrolled': lambda: get_theme().COLOR_VERTICAL_TAB_SCROLLED,
+ 'joined': lambda: get_theme().COLOR_VERTICAL_TAB_JOINED,
+ 'message': lambda: get_theme().COLOR_VERTICAL_TAB_NEW_MESSAGE,
+ 'highlight': lambda: get_theme().COLOR_VERTICAL_TAB_HIGHLIGHT,
+ 'private': lambda: get_theme().COLOR_VERTICAL_TAB_PRIVATE,
+ 'normal': lambda: get_theme().COLOR_VERTICAL_TAB_NORMAL,
+ 'current': lambda: get_theme().COLOR_VERTICAL_TAB_CURRENT,
+ 'attention': lambda: get_theme().COLOR_VERTICAL_TAB_ATTENTION,
+ }
+
+
+STATE_PRIORITY = {
+ 'normal': -1,
+ 'current': -1,
+ 'disconnected': 0,
+ 'scrolled': 0.5,
+ 'message': 1,
+ 'joined': 1,
+ 'highlight': 2,
+ 'private': 2,
+ 'attention': 3
+ }
+
+class Tab(object):
+ tab_core = None
+
+ plugin_commands = {}
+ plugin_keys = {}
+ def __init__(self):
+ self.input = None
+ self._state = 'normal'
+
+ self.need_resize = False
+ self.need_resize = False
+ self.key_func = {} # each tab should add their keys in there
+ # and use them in on_input
+ self.commands = {} # and their own commands
+
+
+ @property
+ def core(self):
+ if not Tab.tab_core:
+ Tab.tab_core = singleton.Singleton(core.Core)
+ return Tab.tab_core
+
+ @property
+ def nb(self):
+ for index, tab in enumerate(self.core.tabs):
+ if tab == self:
+ return index
+ return len(self.core.tabs)
+
+ @property
+ def tab_win(self):
+ if not Tab.tab_core:
+ Tab.tab_core = singleton.Singleton(core.Core)
+ return Tab.tab_core.tab_win
+
+ @property
+ def left_tab_win(self):
+ if not Tab.tab_core:
+ Tab.tab_core = singleton.Singleton(core.Core)
+ return Tab.tab_core.left_tab_win
+
+ @staticmethod
+ def tab_win_height():
+ """
+ Returns 1 or 0, depending on if we are using the vertical tab list
+ or not.
+ """
+ if config.get('enable_vertical_tab_list', 'false') == 'true':
+ return 0
+ return 1
+
+ @property
+ def info_win(self):
+ return self.core.information_win
+
+ @property
+ def color(self):
+ return STATE_COLORS[self._state]()
+
+ @property
+ def vertical_color(self):
+ return VERTICAL_STATE_COLORS[self._state]()
+
+ @property
+ def state(self):
+ return self._state
+
+ @state.setter
+ def state(self, value):
+ if not value in STATE_COLORS:
+ log.debug("Invalid value for tab state: %s", value)
+ elif STATE_PRIORITY[value] < STATE_PRIORITY[self._state] and \
+ value not in ('current', 'disconnected') and \
+ not (self._state == 'scrolled' and value == 'disconnected'):
+ log.debug("Did not set state because of lower priority, asked: %s, kept: %s", value, self._state)
+ elif self._state == 'disconnected' and value not in ('joined', 'current'):
+ log.debug('Did not set state because disconnected tabs remain visible')
+ else:
+ self._state = value
+
+ @staticmethod
+ def resize(scr):
+ Tab.size = (Tab.height, Tab.width) = scr.getmaxyx()
+ if Tab.height < MIN_HEIGHT or Tab.width < MIN_WIDTH:
+ Tab.visible = False
+ else:
+ Tab.visible = True
+ windows.Win._tab_win = scr
+
+ def register_command(self, name, func, *, desc='', shortdesc='', completion=None, usage=''):
+ """
+ Add a command
+ """
+ if name in self.commands:
+ return
+ if not desc and shortdesc:
+ desc = shortdesc
+ self.commands[name] = core.Command(func, desc, completion, shortdesc, usage)
+
+ def complete_commands(self, the_input):
+ """
+ Does command completion on the specified input for both global and tab-specific
+ commands.
+ This should be called from the completion method (on tab, for example), passing
+ the input where completion is to be made.
+ It can completion the command name itself or an argument of the command.
+ Returns True if a completion was made, False else.
+ """
+ txt = the_input.get_text()
+ # check if this is a command
+ if txt.startswith('/') and not txt.startswith('//'):
+ position = the_input.get_argument_position(quoted=False)
+ if position == 0:
+ words = ['/%s'% (name) for name in sorted(self.core.commands)] +\
+ ['/%s' % (name) for name in sorted(self.commands)]
+ the_input.new_completion(words, 0)
+ # Do not try to cycle command completion if there was only
+ # one possibily. The next tab will complete the argument.
+ # Otherwise we would need to add a useless space before being
+ # able to complete the arguments.
+ hit_copy = set(the_input.hit_list)
+ while not hit_copy:
+ whitespace = the_input.text.find(' ')
+ if whitespace == -1:
+ whitespace = len(the_input.text)
+ the_input.text = the_input.text[:whitespace-1] + the_input.text[whitespace:]
+ the_input.new_completion(words, 0)
+ hit_copy = set(the_input.hit_list)
+ if len(hit_copy) == 1:
+ the_input.do_command(' ')
+ the_input.reset_completion()
+ return True
+ # check if we are in the middle of the command name
+ elif len(txt.split()) > 1 or\
+ (txt.endswith(' ') and not the_input.last_completion):
+ command_name = txt.split()[0][1:]
+ if command_name in self.commands:
+ command = self.commands[command_name]
+ elif command_name in self.core.commands:
+ command = self.core.commands[command_name]
+ else: # Unknown command, cannot complete
+ return False
+ if command[2] is None:
+ return False # There's no completion function
+ else:
+ return command[2](the_input)
+ return True
+ return False
+
+ def execute_command(self, provided_text):
+ """
+ Execute the command in the input and return False if
+ the input didn't contain a command
+ """
+ txt = provided_text or self.input.key_enter()
+ if txt.startswith('/') and not txt.startswith('//') and\
+ not txt.startswith('/me '):
+ command = txt.strip().split()[0][1:]
+ arg = txt[2+len(command):] # jump the '/' and the ' '
+ func = None
+ if command in self.commands: # check tab-specific commands
+ func = self.commands[command][0]
+ elif command in self.core.commands: # check global commands
+ func = self.core.commands[command][0]
+ else:
+ low = command.lower()
+ if low in self.commands:
+ func = self.commands[low][0]
+ elif low in self.core.commands:
+ func = self.core.commands[low][0]
+ else:
+ self.core.information(_("Unknown command (%s)") % (command), _('Error'))
+ if command in ('correct', 'say'): # hack
+ arg = xhtml.convert_simple_to_full_colors(arg)
+ else:
+ arg = xhtml.clean_text_simple(arg)
+ if func:
+ func(arg)
+ return True
+ else:
+ return False
+
+ def refresh_tab_win(self):
+ if self.left_tab_win:
+ self.left_tab_win.refresh()
+ else:
+ self.tab_win.refresh()
+
+ def refresh(self):
+ """
+ Called on each screen refresh (when something has changed)
+ """
+ pass
+
+ def get_name(self):
+ """
+ get the name of the tab
+ """
+ return self.__class__.__name__
+
+ def get_nick(self):
+ """
+ Get the nick of the tab (defaults to its name)
+ """
+ return self.get_name()
+
+ def get_text_window(self):
+ """
+ Returns the principal TextWin window, if there's one
+ """
+ return None
+
+ def on_input(self, key, raw):
+ """
+ raw indicates if the key should activate the associated command or not.
+ """
+ pass
+
+ def update_commands(self):
+ for c in self.plugin_commands:
+ if not c in self.commands:
+ self.commands[c] = self.plugin_commands[c]
+
+ def update_keys(self):
+ for k in self.plugin_keys:
+ if not k in self.key_func:
+ self.key_func[k] = self.plugin_keys[k]
+
+ def on_lose_focus(self):
+ """
+ called when this tab loses the focus.
+ """
+ self.state = 'normal'
+
+ def on_gain_focus(self):
+ """
+ called when this tab gains the focus.
+ """
+ self.state = 'current'
+
+ def on_scroll_down(self):
+ """
+ Defines what happens when we scroll down
+ """
+ pass
+
+ def on_scroll_up(self):
+ """
+ Defines what happens when we scroll up
+ """
+ pass
+
+ def on_line_up(self):
+ """
+ Defines what happens when we scroll one line up
+ """
+ pass
+
+ def on_line_down(self):
+ """
+ Defines what happens when we scroll one line up
+ """
+ pass
+
+ def on_half_scroll_down(self):
+ """
+ Defines what happens when we scroll half a screen down
+ """
+ pass
+
+ def on_half_scroll_up(self):
+ """
+ Defines what happens when we scroll half a screen up
+ """
+ pass
+
+ def on_info_win_size_changed(self):
+ """
+ Called when the window with the informations is resized
+ """
+ pass
+
+ def on_close(self):
+ """
+ Called when the tab is to be closed
+ """
+ if self.input:
+ self.input.on_delete()
+
+ def matching_names(self):
+ """
+ Returns a list of strings that are used to name a tab with the /win
+ command. For example you could switch to a tab that returns
+ ['hello', 'coucou'] using /win hel, or /win coucou
+ If not implemented in the tab, it just doesn’t match with anything.
+ """
+ return []
+
+ def __del__(self):
+ log.debug('------ Closing tab %s', self.__class__.__name__)
+
+class GapTab(Tab):
+
+ def __bool__(self):
+ return False
+
+ def __len__(self):
+ return 0
+
+ def get_name(self):
+ return ''
+
+ def refresh(self):
+ log.debug('WARNING: refresh() called on a gap tab, this should not happen')
+
+class ChatTab(Tab):
+ """
+ A tab containing a chat of any type.
+ Just use this class instead of Tab if the tab needs a recent-words completion
+ Also, ^M is already bound to on_enter
+ And also, add the /say command
+ """
+ plugin_commands = {}
+ plugin_keys = {}
+ def __init__(self, jid=''):
+ Tab.__init__(self)
+ self.name = jid
+ self.text_win = None
+ self._text_buffer = TextBuffer()
+ self.remote_wants_chatstates = None # change this to True or False when
+ # we know that the remote user wants chatstates, or not.
+ # None means we don’t know yet, and we send only "active" chatstates
+ self.chatstate = None # can be "active", "composing", "paused", "gone", "inactive"
+ # We keep a weakref of the event that will set our chatstate to "paused", so that
+ # we can delete it or change it if we need to
+ self.timed_event_paused = None
+ # if that’s None, then no paused chatstate was sent recently
+ # if that’s a weakref returning None, then a paused chatstate was sent
+ # since the last input
+ self.remote_supports_attention = False
+ # Keeps the last sent message to complete it easily in completion_correct, and to replace it.
+ self.last_sent_message = None
+ self.key_func['M-v'] = self.move_separator
+ self.key_func['M-h'] = self.scroll_separator
+ self.key_func['M-/'] = self.last_words_completion
+ self.key_func['^M'] = self.on_enter
+ self.register_command('say', self.command_say,
+ usage=_('<message>'),
+ shortdesc=_('Send the message.'))
+ self.register_command('xhtml', self.command_xhtml,
+ usage=_('<custom xhtml>'),
+ shortdesc=_('Send custom XHTML.'))
+ self.register_command('clear', self.command_clear,
+ shortdesc=_('Clear the current buffer.'))
+ self.register_command('correct', self.command_correct,
+ desc=_('Fix the last message with whatever you want.'),
+ shortdesc=_('Correct the last message.'),
+ completion=self.completion_correct)
+ self.chat_state = None
+ self.update_commands()
+ self.update_keys()
+
+ # Get the logs
+ log_nb = config.get('load_log', 10)
+ logs = self.load_logs(log_nb)
+
+ if logs:
+ for message in logs:
+ self._text_buffer.add_message(**message)
+
+ @property
+ def is_muc(self):
+ return False
+
+ def load_logs(self, log_nb):
+ logs = logger.get_logs(safeJID(self.get_name()).bare, log_nb)
+
+ def log_message(self, txt, nickname, time=None, typ=1):
+ """
+ Log the messages in the archives.
+ """
+ name = safeJID(self.name).bare
+ if not logger.log_message(name, nickname, txt, date=time, typ=typ):
+ self.core.information(_('Unable to write in the log file'), 'Error')
+
+ def add_message(self, txt, time=None, nickname=None, forced_user=None, nick_color=None, identifier=None, jid=None, history=None, typ=1):
+ self.log_message(txt, nickname, time=time, typ=typ)
+ self._text_buffer.add_message(txt, time=time,
+ nickname=nickname,
+ nick_color=nick_color,
+ history=history,
+ user=forced_user,
+ identifier=identifier,
+ jid=jid)
+
+ def modify_message(self, txt, old_id, new_id, user=None,jid=None, nickname=None):
+ self.log_message(txt, nickname, typ=1)
+ message = self._text_buffer.modify_message(txt, old_id, new_id, time=time, user=user, jid=jid)
+ if message:
+ self.text_win.modify_message(old_id, message)
+ self.core.refresh_window()
+ return True
+ return False
+
+ def last_words_completion(self):
+ """
+ Complete the input with words recently said
+ """
+ # build the list of the recent words
+ char_we_dont_want = string.punctuation+' ’„“”…«»'
+ words = list()
+ for msg in self._text_buffer.messages[:-40:-1]:
+ if not msg:
+ continue
+ txt = xhtml.clean_text(msg.txt)
+ for char in char_we_dont_want:
+ txt = txt.replace(char, ' ')
+ for word in txt.split():
+ if len(word) >= 4 and word not in words:
+ words.append(word)
+ words.extend([word for word in config.get('words', '').split(':') if word])
+ self.input.auto_completion(words, ' ', quotify=False)
+
+ def on_enter(self):
+ txt = self.input.key_enter()
+ if txt:
+ if not self.execute_command(txt):
+ if txt.startswith('//'):
+ txt = txt[1:]
+ self.command_say(xhtml.convert_simple_to_full_colors(txt))
+ self.cancel_paused_delay()
+
+ def command_xhtml(self, arg):
+ """"
+ /xhtml <custom xhtml>
+ """
+ message = self.generate_xhtml_message(arg)
+ if message:
+ message.send()
+
+ def generate_xhtml_message(self, arg):
+ if not arg:
+ return
+ try:
+ body = xhtml.clean_text(xhtml.xhtml_to_poezio_colors(arg))
+ # The <body /> element is the only allowable child of the <xhtm-im>
+ arg = "<body xmlns='http://www.w3.org/1999/xhtml'>%s</body>" % (arg,)
+ ET.fromstring(arg)
+ except:
+ self.core.information('Could not send custom xhtml', 'Error')
+ log.error('/xhtml: Unable to send custom xhtml', exc_info=True)
+ return
+
+ msg = self.core.xmpp.make_message(self.get_dest_jid())
+ msg['body'] = body
+ msg.enable('html')
+ msg['html']['body'] = arg
+ return msg
+
+ def get_dest_jid(self):
+ return self.get_name()
+
+ @refresh_wrapper.always
+ def command_clear(self, args):
+ """
+ /clear
+ """
+ self._text_buffer.messages = []
+ self.text_win.rebuild_everything(self._text_buffer)
+
+ def send_chat_state(self, state, always_send=False):
+ """
+ Send an empty chatstate message
+ """
+ if not self.is_muc or self.joined:
+ if state in ('active', 'inactive', 'gone') and self.inactive and not always_send:
+ return
+ if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) and \
+ self.remote_wants_chatstates is not False:
+ msg = self.core.xmpp.make_message(self.get_dest_jid())
+ msg['type'] = self.message_type
+ msg['chat_state'] = state
+ self.chat_state = state
+ msg.send()
+
+ def send_composing_chat_state(self, empty_after):
+ """
+ Send the "active" or "composing" chatstate, depending
+ on the the current status of the input
+ """
+ name = self.general_jid
+ if config.get_by_tabname('send_chat_states', 'true', name, True) == 'true' and self.remote_wants_chatstates:
+ needed = 'inactive' if self.inactive else 'active'
+ self.cancel_paused_delay()
+ if not empty_after:
+ if self.chat_state != "composing":
+ self.send_chat_state("composing")
+ self.set_paused_delay(True)
+ elif empty_after and self.chat_state != needed:
+ self.send_chat_state(needed, True)
+
+ def set_paused_delay(self, composing):
+ """
+ we create a timed event that will put us to paused
+ in a few seconds
+ """
+ if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) != 'true':
+ return
+ if self.timed_event_paused:
+ # check the weakref
+ event = self.timed_event_paused()
+ if event:
+ # the event already exists: we just update
+ # its date
+ event.change_date(datetime.now() + timedelta(seconds=4))
+ return
+ new_event = timed_events.DelayedEvent(4, self.send_chat_state, 'paused')
+ self.core.add_timed_event(new_event)
+ self.timed_event_paused = weakref.ref(new_event)
+
+ def cancel_paused_delay(self):
+ """
+ Remove that event from the list and set it to None.
+ Called for example when the input is emptied, or when the message
+ is sent
+ """
+ if self.timed_event_paused:
+ event = self.timed_event_paused()
+ if event:
+ self.core.remove_timed_event(event)
+ del event
+ self.timed_event_paused = None
+
+ def command_correct(self, line):
+ """
+ /correct <fixed message>
+ """
+ if not line:
+ self.core.command_help('correct')
+ return
+ if not self.last_sent_message:
+ self.core.information(_('There is no message to correct.'))
+ return
+ self.command_say(line, correct=True)
+
+ def completion_correct(self, the_input):
+ if self.last_sent_message and the_input.get_argument_position() == 1:
+ return the_input.auto_completion([self.last_sent_message['body']], '', quotify=False)
+
+ @property
+ def inactive(self):
+ """Whether we should send inactive or active as a chatstate"""
+ return self.core.status.show in ('xa', 'away') or\
+ (hasattr(self, 'directed_presence') and not self.directed_presence)
+
+ def move_separator(self):
+ self.text_win.remove_line_separator()
+ self.text_win.add_line_separator(self._text_buffer)
+ self.text_win.refresh()
+ self.input.refresh()
+
+ def get_conversation_messages(self):
+ return self._text_buffer.messages
+
+ def check_scrolled(self):
+ if self.text_win.pos != 0:
+ self.state = 'scrolled'
+
+ def command_say(self, line, correct=False):
+ pass
+
+ def on_line_up(self):
+ return self.text_win.scroll_up(1)
+
+ def on_line_down(self):
+ return self.text_win.scroll_down(1)
+
+ def on_scroll_up(self):
+ return self.text_win.scroll_up(self.text_win.height-1)
+
+ def on_scroll_down(self):
+ return self.text_win.scroll_down(self.text_win.height-1)
+
+ def on_half_scroll_up(self):
+ return self.text_win.scroll_up((self.text_win.height-1) // 2)
+
+ def on_half_scroll_down(self):
+ return self.text_win.scroll_down((self.text_win.height-1) // 2)
+
+ @refresh_wrapper.always
+ def scroll_separator(self):
+ self.text_win.scroll_to_separator()
+
+
diff --git a/src/tabs/conversationtab.py b/src/tabs/conversationtab.py
new file mode 100644
index 00000000..143c8d68
--- /dev/null
+++ b/src/tabs/conversationtab.py
@@ -0,0 +1,433 @@
+from gettext import gettext as _
+
+import logging
+log = logging.getLogger(__name__)
+
+import curses
+
+from . import ChatTab, Tab
+
+import common
+import fixes
+import windows
+import xhtml
+from common import safeJID
+from config import config
+from decorators import refresh_wrapper
+from roster import roster
+from theming import get_theme, dump_tuple
+
+class ConversationTab(ChatTab):
+ """
+ The tab containg a normal conversation (not from a MUC)
+ Must not be instantiated, use Static or Dynamic version only.
+ """
+ plugin_commands = {}
+ plugin_keys = {}
+ additional_informations = {}
+ message_type = 'chat'
+ def __init__(self, jid):
+ ChatTab.__init__(self, jid)
+ self.nick = None
+ self.nick_sent = False
+ self.state = 'normal'
+ self.name = jid # a conversation tab is linked to one specific full jid OR bare jid
+ self.text_win = windows.TextWin()
+ self._text_buffer.add_window(self.text_win)
+ self.upper_bar = windows.ConversationStatusMessageWin()
+ self.input = windows.MessageInput()
+ self.check_attention()
+ # keys
+ self.key_func['^I'] = self.completion
+ # commands
+ self.register_command('unquery', self.command_unquery,
+ shortdesc=_('Close the tab.'))
+ self.register_command('close', self.command_unquery,
+ shortdesc=_('Close the tab.'))
+ self.register_command('version', self.command_version,
+ desc=_('Get the software version of the current interlocutor (usually its XMPP client and Operating System).'),
+ shortdesc=_('Get the software version of the user.'))
+ self.register_command('info', self.command_info,
+ shortdesc=_('Get the status of the contact.'))
+ self.register_command('last_activity', self.command_last_activity,
+ usage=_('[jid]'),
+ desc=_('Get the last activity of the given or the current contact.'),
+ shortdesc=_('Get the activity.'),
+ completion=self.core.completion_last_activity)
+ self.resize()
+ self.update_commands()
+ self.update_keys()
+
+ @property
+ def general_jid(self):
+ return safeJID(self.get_name()).bare
+
+ @staticmethod
+ def add_information_element(plugin_name, callback):
+ """
+ Lets a plugin add its own information to the ConversationInfoWin
+ """
+ ConversationTab.additional_informations[plugin_name] = callback
+
+ @staticmethod
+ def remove_information_element(plugin_name):
+ del ConversationTab.additional_informations[plugin_name]
+
+ def completion(self):
+ self.complete_commands(self.input)
+
+ def command_say(self, line, attention=False, correct=False):
+ msg = self.core.xmpp.make_message(self.get_dest_jid())
+ msg['type'] = 'chat'
+ msg['body'] = line
+ if not self.nick_sent:
+ msg['nick'] = self.core.own_nick
+ self.nick_sent = True
+ # trigger the event BEFORE looking for colors.
+ # and before displaying the message in the window
+ # This lets a plugin insert \x19xxx} colors, that will
+ # be converted in xhtml.
+ self.core.events.trigger('conversation_say', msg, self)
+ if not msg['body']:
+ self.cancel_paused_delay()
+ self.text_win.refresh()
+ self.input.refresh()
+ return
+ replaced = False
+ if correct or msg['replace']['id']:
+ msg['replace']['id'] = self.last_sent_message['id']
+ if config.get_by_tabname('group_corrections', 'true', self.get_name()).lower() != 'false':
+ try:
+ self.modify_message(msg['body'], self.last_sent_message['id'], msg['id'], jid=self.core.xmpp.boundjid,
+ nickname=self.core.own_nick)
+ replaced = True
+ except:
+ log.error('Unable to correct a message', exc_info=True)
+ else:
+ del msg['replace']
+ if msg['body'].find('\x19') != -1:
+ msg.enable('html')
+ msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body'])
+ msg['body'] = xhtml.clean_text(msg['body'])
+ if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and self.remote_wants_chatstates is not False:
+ needed = 'inactive' if self.inactive else 'active'
+ msg['chat_state'] = needed
+ if attention and self.remote_supports_attention:
+ msg['attention'] = True
+ self.core.events.trigger('conversation_say_after', msg, self)
+ if not msg['body']:
+ self.cancel_paused_delay()
+ self.text_win.refresh()
+ self.input.refresh()
+ return
+ if not replaced:
+ self.add_message(msg['body'],
+ nickname=self.core.own_nick,
+ nick_color=get_theme().COLOR_OWN_NICK,
+ identifier=msg['id'],
+ jid=self.core.xmpp.boundjid,
+ typ=1)
+
+ self.last_sent_message = msg
+ msg.send()
+ self.cancel_paused_delay()
+ self.text_win.refresh()
+ self.input.refresh()
+
+ def command_xhtml(self, arg):
+ message = self.generate_xhtml_message(arg)
+ if message:
+ message.send()
+ self.core.add_message_to_text_buffer(self._text_buffer, message['body'], None, self.core.own_nick)
+ self.refresh()
+
+ def command_last_activity(self, arg):
+ """
+ /activity [jid]
+ """
+ if arg.strip():
+ return self.core.command_last_activity(arg)
+
+ def callback(iq):
+ if iq['type'] != 'result':
+ if iq['error']['type'] == 'auth':
+ self.core.information('You are not allowed to see the activity of this contact.', 'Error')
+ else:
+ self.core.information('Error retrieving the activity', 'Error')
+ return
+ seconds = iq['last_activity']['seconds']
+ status = iq['last_activity']['status']
+ from_ = iq['from']
+ msg = '\x19%s}The last activity of %s was %s ago%s'
+ if not safeJID(from_).user:
+ msg = '\x19%s}The uptime of %s is %s.' % (
+ dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
+ from_,
+ common.parse_secs_to_str(seconds))
+ else:
+ msg = '\x19%s}The last activity of %s was %s ago%s' % (
+ dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
+ from_,
+ common.parse_secs_to_str(seconds),
+ (' and his/her last status was %s' % status) if status else '',)
+ self.add_message(msg)
+ self.core.refresh_window()
+
+ self.core.xmpp.plugin['xep_0012'].get_last_activity(self.general_jid, block=False, callback=callback)
+
+ @refresh_wrapper.conditional
+ def command_info(self, arg):
+ contact = roster[self.get_dest_jid()]
+ jid = safeJID(self.get_dest_jid())
+ if contact:
+ if jid.resource:
+ resource = contact[jid.full]
+ else:
+ resource = contact.get_highest_priority_resource()
+ else:
+ resource = None
+ if resource:
+ status = (_('Status: %s') % resource.status) if resource.status else ''
+ self._text_buffer.add_message("\x19%(info_col)s}Show: %(show)s, %(status)s\x19o" % {
+ 'show': resource.show or 'available', 'status': status, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)})
+ return True
+ else:
+ self._text_buffer.add_message("\x19%(info_col)s}No information available\x19o" % {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)})
+ return True
+
+
+ def command_attention(self, message=''):
+ if message is not '':
+ self.command_say(message, attention=True)
+ else:
+ msg = self.core.xmpp.make_message(self.get_dest_jid())
+ msg['type'] = 'chat'
+ msg['attention'] = True
+ msg.send()
+
+ def check_attention(self):
+ self.core.xmpp.plugin['xep_0030'].get_info(jid=self.get_dest_jid(), block=False, timeout=5, callback=self.on_attention_checked)
+
+ def on_attention_checked(self, iq):
+ if 'urn:xmpp:attention:0' in iq['disco_info'].get_features():
+ self.core.information('Attention is supported', 'Info')
+ self.remote_supports_attention = True
+ self.commands['attention'] = (self.command_attention, _('Usage: /attention [message]\nAttention: Require the attention of the contact. Can also send a message along with the attention.'), None)
+ else:
+ self.remote_supports_attention = False
+
+ def command_unquery(self, arg):
+ self.core.close_tab()
+
+ def command_version(self, arg):
+ """
+ /version
+ """
+ def callback(res):
+ if not res:
+ return self.core.information('Could not get the software version from %s' % (jid,), 'Warning')
+ version = '%s is running %s version %s on %s' % (jid,
+ res.get('name') or _('an unknown software'),
+ res.get('version') or _('unknown'),
+ res.get('os') or _('an unknown platform'))
+ self.core.information(version, 'Info')
+ if arg:
+ return self.core.command_version(arg)
+ jid = safeJID(self.name)
+ if not jid.resource:
+ if jid in roster:
+ resource = roster[jid].get_highest_priority_resource()
+ jid = resource.jid if resource else jid
+ fixes.get_version(self.core.xmpp, jid, callback=callback)
+
+ def resize(self):
+ if self.core.information_win_size >= self.height-3 or not self.visible:
+ return
+ self.need_resize = False
+ self.text_win.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width, 1, 0)
+ self.text_win.rebuild_everything(self._text_buffer)
+ self.upper_bar.resize(1, self.width, 0, 0)
+ self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
+ self.input.resize(1, self.width, self.height-1, 0)
+
+ def refresh(self):
+ if self.need_resize:
+ self.resize()
+ log.debug(' TAB Refresh: %s',self.__class__.__name__)
+ self.text_win.refresh()
+ self.upper_bar.refresh(self.get_dest_jid(), roster[self.get_dest_jid()])
+ self.info_header.refresh(self.get_dest_jid(), roster[self.get_dest_jid()], self.text_win, self.chatstate, ConversationTab.additional_informations)
+ self.info_win.refresh()
+ self.refresh_tab_win()
+ self.input.refresh()
+
+ def refresh_info_header(self):
+ self.info_header.refresh(self.get_dest_jid(), roster[self.get_dest_jid()],
+ self.text_win, self.chatstate, ConversationTab.additional_informations)
+ self.input.refresh()
+
+ def get_name(self):
+ return self.name
+
+ def get_nick(self):
+ jid = safeJID(self.name)
+ contact = roster[jid.bare]
+ if contact:
+ return contact.name or jid.user
+ else:
+ if self.nick:
+ return self.nick
+ return jid.user
+
+ def on_input(self, key, raw):
+ if not raw and key in self.key_func:
+ self.key_func[key]()
+ return False
+ self.input.do_command(key, raw=raw)
+ empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//'))
+ self.send_composing_chat_state(empty_after)
+ return False
+
+ def on_lose_focus(self):
+ contact = roster[self.get_dest_jid()]
+ jid = safeJID(self.get_dest_jid())
+ if contact:
+ if jid.resource:
+ resource = contact[jid.full]
+ else:
+ resource = contact.get_highest_priority_resource()
+ else:
+ resource = None
+ self.state = 'normal'
+ self.text_win.remove_line_separator()
+ self.text_win.add_line_separator(self._text_buffer)
+ if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and (not self.input.get_text() or not self.input.get_text().startswith('//')):
+ if resource:
+ self.send_chat_state('inactive')
+ self.check_scrolled()
+
+ def on_gain_focus(self):
+ contact = roster[self.get_dest_jid()]
+ jid = safeJID(self.get_dest_jid())
+ if contact:
+ if jid.resource:
+ resource = contact[jid.full]
+ else:
+ resource = contact.get_highest_priority_resource()
+ else:
+ resource = None
+
+ self.state = 'current'
+ curses.curs_set(1)
+ if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and (not self.input.get_text() or not self.input.get_text().startswith('//')):
+ if resource:
+ self.send_chat_state('active')
+
+ def on_info_win_size_changed(self):
+ if self.core.information_win_size >= self.height-3:
+ return
+ self.text_win.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width, 1, 0)
+ self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
+
+ def get_text_window(self):
+ return self.text_win
+
+ def on_close(self):
+ Tab.on_close(self)
+ if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true':
+ self.send_chat_state('gone')
+
+ def matching_names(self):
+ res = []
+ jid = safeJID(self.get_name())
+ res.append((2, jid.bare))
+ res.append((1, jid.user))
+ contact = roster[self.get_name()]
+ if contact and contact.name:
+ res.append((0, contact.name))
+ return res
+
+class DynamicConversationTab(ConversationTab):
+ """
+ A conversation tab associated with one bare JID that can be “locked” to
+ a full jid, and unlocked, as described in the XEP-0296.
+ Only one DynamicConversationTab can be opened for a given jid.
+ """
+ def __init__(self, jid, resource=None):
+ self.locked_resource = None
+ self.name = safeJID(jid).bare
+ if resource:
+ self.lock(resource)
+ self.info_header = windows.DynamicConversationInfoWin()
+ ConversationTab.__init__(self, jid)
+ self.register_command('unlock', self.unlock_command,
+ shortdesc=_('Unlock the converstation from a particular resource.'))
+
+ def lock(self, resource):
+ """
+ Lock the tab to the resource.
+ """
+ assert(resource)
+ self.locked_resource = resource
+
+ def unlock_command(self, arg=None):
+ self.unlock()
+ self.refresh_info_header()
+
+ def unlock(self):
+ """
+ Unlock the tab from a resource. It is now “associated” with the bare
+ jid.
+ """
+ self.locked_resource = None
+
+ def get_dest_jid(self):
+ """
+ Returns the full jid (using the locked resource), or the bare jid if
+ the conversation is not locked.
+ """
+ if self.locked_resource:
+ return "%s/%s" % (self.get_name(), self.locked_resource)
+ return self.get_name()
+
+ def refresh(self):
+ """
+ Different from the parent class only for the info_header object.
+ """
+ if self.need_resize:
+ self.resize()
+ log.debug(' TAB Refresh: %s',self.__class__.__name__)
+ self.text_win.refresh()
+ self.upper_bar.refresh(self.get_name(), roster[self.get_name()])
+ if self.locked_resource:
+ displayed_jid = "%s/%s" % (self.get_name(), self.locked_resource)
+ else:
+ displayed_jid = self.get_name()
+ self.info_header.refresh(displayed_jid, roster[self.get_name()], self.text_win, self.chatstate, ConversationTab.additional_informations)
+ self.info_win.refresh()
+ self.refresh_tab_win()
+ self.input.refresh()
+
+ def refresh_info_header(self):
+ """
+ Different from the parent class only for the info_header object.
+ """
+ if self.locked_resource:
+ displayed_jid = "%s/%s" % (self.get_name(), self.locked_resource)
+ else:
+ displayed_jid = self.get_name()
+ self.info_header.refresh(displayed_jid, roster[self.get_name()],
+ self.text_win, self.chatstate, ConversationTab.additional_informations)
+ self.input.refresh()
+
+class StaticConversationTab(ConversationTab):
+ """
+ A conversation tab associated with one Full JID. It cannot be locked to
+ an different resource or unlocked.
+ """
+ def __init__(self, jid):
+ assert(safeJID(jid).resource)
+ self.info_header = windows.ConversationInfoWin()
+ ConversationTab.__init__(self, jid)
+
+
diff --git a/src/tabs/muclisttab.py b/src/tabs/muclisttab.py
new file mode 100644
index 00000000..0fe27307
--- /dev/null
+++ b/src/tabs/muclisttab.py
@@ -0,0 +1,195 @@
+from gettext import gettext as _
+
+import logging
+log = logging.getLogger(__name__)
+
+import curses
+
+from . import Tab
+
+import windows
+from common import safeJID
+from decorators import refresh_wrapper
+
+class MucListTab(Tab):
+ """
+ A tab listing rooms from a specific server, displaying various information,
+ scrollable, and letting the user join them, etc
+ """
+ plugin_commands = {}
+ plugin_keys = {}
+ def __init__(self, server):
+ Tab.__init__(self)
+ self.state = 'normal'
+ self.name = server
+ columns = ('node-part', 'name', 'users')
+ self.list_header = windows.ColumnHeaderWin(columns)
+ self.listview = windows.ListWin(columns)
+ self.info_header = windows.MucListInfoWin(_('Chatroom list on server %s (Loading)') % self.name)
+ self.default_help_message = windows.HelpText("“j”: join room.")
+ self.input = self.default_help_message
+ self.key_func["KEY_DOWN"] = self.move_cursor_down
+ self.key_func["KEY_UP"] = self.move_cursor_up
+ self.key_func['^I'] = self.completion
+ self.key_func["/"] = self.on_slash
+ self.key_func['j'] = self.join_selected
+ self.key_func['J'] = self.join_selected_no_focus
+ self.key_func['^M'] = self.join_selected
+ self.key_func['KEY_LEFT'] = self.list_header.sel_column_left
+ self.key_func['KEY_RIGHT'] = self.list_header.sel_column_right
+ self.key_func[' '] = self.sort_by
+ self.register_command('close', self.close,
+ shortdesc=_('Close this tab.'))
+ self.resize()
+ self.update_keys()
+ self.update_commands()
+
+ def refresh(self):
+ if self.need_resize:
+ self.resize()
+ log.debug(' TAB Refresh: %s',self.__class__.__name__)
+ self.info_header.refresh()
+ self.info_win.refresh()
+ self.refresh_tab_win()
+ self.list_header.refresh()
+ self.listview.refresh()
+ self.input.refresh()
+ self.update_commands()
+
+ def resize(self):
+ if self.core.information_win_size >= self.height-3 or not self.visible:
+ return
+ self.need_resize = False
+ self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
+ column_size = {'node-part': int(self.width*2/8) ,
+ 'name': int(self.width*5/8),
+ 'users': self.width-int(self.width*2/8)-int(self.width*5/8)}
+ self.list_header.resize_columns(column_size)
+ self.list_header.resize(1, self.width, 0, 0)
+ self.listview.resize_columns(column_size)
+ self.listview.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width, 1, 0)
+ self.input.resize(1, self.width, self.height-1, 0)
+
+ def on_slash(self):
+ """
+ '/' is pressed, activate the input
+ """
+ curses.curs_set(1)
+ self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command)
+ self.input.resize(1, self.width, self.height-1, 0)
+ self.input.do_command("/") # we add the slash
+
+ def close(self, arg=None):
+ self.input.on_delete()
+ self.core.close_tab(self)
+
+ def join_selected_no_focus(self):
+ return
+
+ def set_error(self, msg, code, body):
+ """
+ If there's an error (retrieving the values etc)
+ """
+ self._error_message = _('Error: %(code)s - %(msg)s: %(body)s') % {'msg':msg, 'body':body, 'code':code}
+ self.info_header.message = self._error_message
+ self.info_header.refresh()
+ curses.doupdate()
+
+ def on_muc_list_item_received(self, iq):
+ """
+ Callback called when a disco#items result is received
+ Used with command_list
+ """
+ if iq['type'] == 'error':
+ self.set_error(iq['error']['type'], iq['error']['code'], iq['error']['text'])
+ return
+ items = [{'node-part': safeJID(item[0]).user if safeJID(item[0]).server == self.name else safeJID(item[0]).bare,
+ 'jid': item[0],
+ 'name': item[2] or '' ,'users': ''} for item in iq['disco_items'].get_items()]
+ self.listview.add_lines(items)
+ self.info_header.message = _('Chatroom list on server %s') % self.name
+ if self.core.current_tab() is self:
+ self.listview.refresh()
+ self.info_header.refresh()
+ else:
+ self.state = 'highlight'
+ self.refresh_tab_win()
+ curses.doupdate()
+
+ def sort_by(self):
+ if self.list_header.get_order():
+ self.listview.sort_by_column(col_name=self.list_header.get_sel_column(),asc=False)
+ self.list_header.set_order(False)
+ self.list_header.refresh()
+ else:
+ self.listview.sort_by_column(col_name=self.list_header.get_sel_column(),asc=True)
+ self.list_header.set_order(True)
+ self.list_header.refresh()
+ curses.doupdate()
+
+ def join_selected(self):
+ row = self.listview.get_selected_row()
+ if not row:
+ return
+ self.core.command_join(row['jid'])
+
+ @refresh_wrapper.always
+ def reset_help_message(self, _=None):
+ curses.curs_set(0)
+ self.input = self.default_help_message
+ self.input.resize(1, self.width, self.height-1, 0)
+ return True
+
+ def execute_slash_command(self, txt):
+ if txt.startswith('/'):
+ self.input.key_enter()
+ self.execute_command(txt)
+ return self.reset_help_message()
+
+ def get_name(self):
+ return self.name
+
+ def completion(self):
+ if isinstance(self.input, windows.Input):
+ self.complete_commands(self.input)
+
+ def on_input(self, key, raw):
+ res = self.input.do_command(key, raw=raw)
+ if res:
+ return True
+ if not raw and key in self.key_func:
+ return self.key_func[key]()
+
+ def on_info_win_size_changed(self):
+ if self.core.information_win_size >= self.height-3:
+ return
+ self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
+ self.listview.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width, 1, 0)
+
+ def on_lose_focus(self):
+ self.state = 'normal'
+
+ def on_gain_focus(self):
+ self.state = 'current'
+ curses.curs_set(0)
+
+ def on_scroll_up(self):
+ return self.listview.scroll_up()
+
+ def on_scroll_down(self):
+ return self.listview.scroll_down()
+
+ def move_cursor_up(self):
+ self.listview.move_cursor_up()
+ self.listview.refresh()
+ self.core.doupdate()
+
+ def move_cursor_down(self):
+ self.listview.move_cursor_down()
+ self.listview.refresh()
+ self.core.doupdate()
+
+ def matching_names(self):
+ return [(2, self.name)]
+
+
diff --git a/src/tabs/muctab.py b/src/tabs/muctab.py
new file mode 100644
index 00000000..9bc2f88e
--- /dev/null
+++ b/src/tabs/muctab.py
@@ -0,0 +1,1214 @@
+from gettext import gettext as _
+
+import logging
+log = logging.getLogger(__name__)
+
+import curses
+import os
+import random
+from datetime import datetime, timedelta
+
+from . import ChatTab, Tab
+
+import common
+import fixes
+import multiuserchat as muc
+import timed_events
+import windows
+import xhtml
+from common import safeJID
+from config import config
+from decorators import refresh_wrapper
+from logger import logger
+from roster import roster
+from theming import get_theme, dump_tuple
+from user import User
+
+
+SHOW_NAME = {
+ 'dnd': _('busy'),
+ 'away': _('away'),
+ 'xa': _('not available'),
+ 'chat': _('chatty'),
+ '': _('available')
+ }
+
+NS_MUC_USER = 'http://jabber.org/protocol/muc#user'
+
+
+class MucTab(ChatTab):
+ """
+ The tab containing a multi-user-chat room.
+ It contains an userlist, an input, a topic, an information and a chat zone
+ """
+ message_type = 'groupchat'
+ plugin_commands = {}
+ plugin_keys = {}
+ def __init__(self, jid, nick):
+ self.joined = False
+ ChatTab.__init__(self, jid)
+ if self.joined == False:
+ self._state = 'disconnected'
+ self.own_nick = nick
+ self.name = jid
+ self.users = []
+ self.privates = [] # private conversations
+ self.topic = ''
+ self.remote_wants_chatstates = True
+ # We send active, composing and paused states to the MUC because
+ # the chatstate may or may not be filtered by the MUC,
+ # that’s not our problem.
+ self.topic_win = windows.Topic()
+ self.text_win = windows.TextWin()
+ self._text_buffer.add_window(self.text_win)
+ self.v_separator = windows.VerticalSeparator()
+ self.user_win = windows.UserList()
+ self.info_header = windows.MucInfoWin()
+ self.input = windows.MessageInput()
+ self.ignores = [] # set of Users
+ # keys
+ self.key_func['^I'] = self.completion
+ self.key_func['M-u'] = self.scroll_user_list_down
+ self.key_func['M-y'] = self.scroll_user_list_up
+ self.key_func['M-n'] = self.go_to_next_hl
+ self.key_func['M-p'] = self.go_to_prev_hl
+ # commands
+ self.register_command('ignore', self.command_ignore,
+ usage=_('<nickname>'),
+ desc=_('Ignore a specified nickname.'),
+ shortdesc=_('Ignore someone'),
+ completion=self.completion_ignore)
+ self.register_command('unignore', self.command_unignore,
+ usage=_('<nickname>'),
+ desc=_('Remove the specified nickname from the ignore list.'),
+ shortdesc=_('Unignore someone.'),
+ completion=self.completion_unignore)
+ self.register_command('kick', self.command_kick,
+ usage=_('<nick> [reason]'),
+ desc=_('Kick the user with the specified nickname. You also can give an optional reason.'),
+ shortdesc=_('Kick someone.'),
+ completion=self.completion_quoted)
+ self.register_command('ban', self.command_ban,
+ usage=_('<nick> [reason]'),
+ desc=_('Ban the user with the specified nickname. You also can give an optional reason.'),
+ shortdesc='Ban someone',
+ completion=self.completion_quoted)
+ self.register_command('role', self.command_role,
+ usage=_('<nick> <role> [reason]'),
+ desc=_('Set the role of an user. Roles can be: none, visitor, participant, moderator. You also can give an optional reason.'),
+ shortdesc=_('Set the role of an user.'),
+ completion=self.completion_role)
+ self.register_command('affiliation', self.command_affiliation,
+ usage=_('<nick or jid> <affiliation>'),
+ desc=_('Set the affiliation of an user. Affiliations can be: outcast, none, member, admin, owner.'),
+ shortdesc=_('Set the affiliation of an user.'),
+ completion=self.completion_affiliation)
+ self.register_command('topic', self.command_topic,
+ usage=_('<subject>'),
+ desc=_('Change the subject of the room.'),
+ shortdesc=_('Change the subject.'),
+ completion=self.completion_topic)
+ self.register_command('query', self.command_query,
+ usage=_('<nick> [message]'),
+ desc=_('Query: Open a private conversation with <nick>. This nick has to be present in the room you\'re currently in. If you specified a message after the nickname, it will immediately be sent to this user.'),
+ shortdesc=_('Query an user.'),
+ completion=self.completion_quoted)
+ self.register_command('part', self.command_part,
+ usage=_('[message]'),
+ desc=_('Disconnect from a room. You can specify an optional message.'),
+ shortdesc=_('Leave the room.'))
+ self.register_command('close', self.command_close,
+ usage=_('[message]'),
+ desc=_('Disconnect from a room and close the tab. You can specify an optional message if you are still connected.'),
+ shortdesc=_('Close the tab.'))
+ self.register_command('nick', self.command_nick,
+ usage=_('<nickname>'),
+ desc=_('Change your nickname in the current room.'),
+ shortdesc=_('Change your nickname.'),
+ completion=self.completion_nick)
+ self.register_command('recolor', self.command_recolor,
+ desc=_('Re-assign a color to all participants of the current room, based on the last time they talked. Use this if the participants currently talking have too many identical colors.'),
+ shortdesc=_('Change the nicks colors.'),
+ completion=self.completion_recolor)
+ self.register_command('cycle', self.command_cycle,
+ usage=_('[message]'),
+ desc=_('Leave the current room and rejoin it immediately.'),
+ shortdesc=_('Leave and re-join the room.'))
+ self.register_command('info', self.command_info,
+ usage=_('<nickname>'),
+ desc=_('Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.'),
+ shortdesc=_('Show an user\'s infos.'),
+ completion=self.completion_info)
+ self.register_command('configure', self.command_configure,
+ desc=_('Configure the current room, through a form.'),
+ shortdesc=_('Configure the room.'))
+ self.register_command('version', self.command_version,
+ usage=_('<jid or nick>'),
+ desc=_('Get the software version of the given JID or nick in room (usually its XMPP client and Operating System).'),
+ shortdesc=_('Get the software version of a jid.'),
+ completion=self.completion_version)
+ self.register_command('names', self.command_names,
+ desc=_('Get the list of the users in the room, and the list of the people assuming the different roles.'),
+ shortdesc=_('List the users.'))
+ self.register_command('invite', self.command_invite,
+ desc=_('Invite a contact to this room'),
+ usage=_('<jid> [reason]'),
+ shortdesc=_('Invite a contact to this room'),
+ completion=self.completion_invite)
+
+ if self.core.xmpp.boundjid.server == "gmail.com": #gmail sucks
+ del self.commands["nick"]
+
+ self.resize()
+ self.update_commands()
+ self.update_keys()
+
+ @property
+ def general_jid(self):
+ return self.get_name()
+
+ @property
+ def is_muc(self):
+ return True
+
+ @property
+ def last_connection(self):
+ last_message = self._text_buffer.last_message
+ if last_message:
+ return last_message.time
+ return None
+
+ @refresh_wrapper.always
+ def go_to_next_hl(self):
+ """
+ Go to the next HL in the room, or the last
+ """
+ self.text_win.next_highlight()
+
+ @refresh_wrapper.always
+ def go_to_prev_hl(self):
+ """
+ Go to the previous HL in the room, or the first
+ """
+ self.text_win.previous_highlight()
+
+ def completion_version(self, the_input):
+ """Completion for /version"""
+ compare_users = lambda x: x.last_talked
+ userlist = [user.nick for user in sorted(self.users, key=compare_users, reverse=True)\
+ if user.nick != self.own_nick]
+ return the_input.auto_completion(userlist, quotify=False)
+
+ def completion_info(self, the_input):
+ """Completion for /info"""
+ compare_users = lambda x: x.last_talked
+ userlist = [user.nick for user in sorted(self.users, key=compare_users, reverse=True)]
+ return the_input.auto_completion(userlist, quotify=False)
+
+ def completion_nick(self, the_input):
+ """Completion for /nick"""
+ nicks = [os.environ.get('USER'), config.get('default_nick', ''), self.core.get_bookmark_nickname(self.get_name())]
+ nicks = [i for i in nicks if i]
+ return the_input.auto_completion(nicks, '', quotify=False)
+
+ def completion_recolor(self, the_input):
+ if the_input.get_argument_position() == 1:
+ return the_input.new_completion(['random'], 1, '', quotify=False)
+ return True
+
+ def completion_ignore(self, the_input):
+ """Completion for /ignore"""
+ userlist = [user.nick for user in self.users]
+ if self.own_nick in userlist:
+ userlist.remove(self.own_nick)
+ userlist.sort()
+ return the_input.auto_completion(userlist, quotify=False)
+
+ def completion_role(self, the_input):
+ """Completion for /role"""
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
+ userlist = [user.nick for user in self.users]
+ if self.own_nick in userlist:
+ userlist.remove(self.own_nick)
+ return the_input.new_completion(userlist, 1, '', quotify=True)
+ elif n == 2:
+ possible_roles = ['none', 'visitor', 'participant', 'moderator']
+ return the_input.new_completion(possible_roles, 2, '', quotify=True)
+
+ def completion_affiliation(self, the_input):
+ """Completion for /affiliation"""
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
+ userlist = [user.nick for user in self.users]
+ if self.own_nick in userlist:
+ userlist.remove(self.own_nick)
+ jidlist = [user.jid.bare for user in self.users]
+ if self.core.xmpp.boundjid.bare in jidlist:
+ jidlist.remove(self.core.xmpp.boundjid.bare)
+ userlist.extend(jidlist)
+ return the_input.new_completion(userlist, 1, '', quotify=True)
+ elif n == 2:
+ possible_affiliations = ['none', 'member', 'admin', 'owner', 'outcast']
+ return the_input.new_completion(possible_affiliations, 2, '', quotify=True)
+
+ def command_invite(self, args):
+ """/invite <jid> [reason]"""
+ args = common.shell_split(args)
+ if len(args) == 1:
+ jid, reason = args[0], ''
+ elif len(args) == 2:
+ jid, reason = args
+ else:
+ return self.core.command_help('invite')
+ self.core.command_invite('%s %s "%s"' % (jid, self.name, reason))
+
+ def completion_invite(self, the_input):
+ """Completion for /invite"""
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
+ return the_input.new_completion(roster.jids(), 1, quotify=True)
+
+ def scroll_user_list_up(self):
+ self.user_win.scroll_up()
+ self.user_win.refresh(self.users)
+ self.input.refresh()
+
+ def scroll_user_list_down(self):
+ self.user_win.scroll_down()
+ self.user_win.refresh(self.users)
+ self.input.refresh()
+
+ def command_info(self, arg):
+ """
+ /info <nick>
+ """
+ if not arg:
+ return self.core.command_help('info')
+ user = self.get_user_by_name(arg)
+ if not user:
+ return self.core.information("Unknown user: %s" % arg)
+ theme = get_theme()
+ info = '\x19%s}%s\x19o%s: show: \x19%s}%s\x19o, affiliation: \x19%s}%s\x19o, role: \x19%s}%s\x19o%s' % (
+ dump_tuple(user.color),
+ arg,
+ (' (\x19%s}%s\x19o)' % (dump_tuple(theme.COLOR_MUC_JID), user.jid)) if user.jid != '' else '',
+ dump_tuple(theme.color_show(user.show)),
+ user.show or 'Available',
+ dump_tuple(theme.color_role(user.role)),
+ user.affiliation or 'None',
+ dump_tuple(theme.color_role(user.role)),
+ user.role or 'None',
+ '\n%s' % user.status if user.status else '')
+ self.core.information(info, 'Info')
+
+ def command_configure(self, arg):
+ form = fixes.get_room_form(self.core.xmpp, self.get_name())
+ if not form:
+ self.core.information('Could not retrieve the configuration form', 'Error')
+ return
+ self.core.open_new_form(form, self.cancel_config, self.send_config)
+
+ def cancel_config(self, form):
+ """
+ The user do not want to send his/her config, send an iq cancel
+ """
+ self.core.xmpp.plugin['xep_0045'].cancelConfig(self.get_name())
+ self.core.close_tab()
+
+ def send_config(self, form):
+ """
+ The user sends his/her config to the server
+ """
+ self.core.xmpp.plugin['xep_0045'].configureRoom(self.get_name(), form)
+ self.core.close_tab()
+
+ def command_cycle(self, arg):
+ """/cycle [reason]"""
+ if self.joined:
+ muc.leave_groupchat(self.core.xmpp, self.get_name(), self.own_nick, arg)
+ self.disconnect()
+ self.core.disable_private_tabs(self.name)
+ self.core.command_join('"/%s"' % self.own_nick)
+ self.user_win.pos = 0
+
+ def command_recolor(self, arg):
+ """
+ /recolor [random]
+ Re-assign color to the participants of the room
+ """
+ arg = arg.strip()
+ compare_users = lambda x: x.last_talked
+ users = list(self.users)
+ sorted_users = sorted(users, key=compare_users, reverse=True)
+ # search our own user, to remove it from the list
+ for user in sorted_users:
+ if user.nick == self.own_nick:
+ sorted_users.remove(user)
+ user.color = get_theme().COLOR_OWN_NICK
+ colors = list(get_theme().LIST_COLOR_NICKNAMES)
+ if arg and arg == 'random':
+ random.shuffle(colors)
+ for i, user in enumerate(sorted_users):
+ user.color = colors[i % len(colors)]
+ self.text_win.rebuild_everything(self._text_buffer)
+ self.user_win.refresh(self.users)
+ self.text_win.refresh()
+ self.input.refresh()
+
+ def command_version(self, arg):
+ """
+ /version <jid or nick>
+ """
+ def callback(res):
+ if not res:
+ return self.core.information('Could not get the software version from %s' % (jid,), 'Warning')
+ version = '%s is running %s version %s on %s' % (jid,
+ res.get('name') or _('an unknown software'),
+ res.get('version') or _('unknown'),
+ res.get('os') or _('an unknown platform'))
+ self.core.information(version, 'Info')
+
+ if not arg:
+ return self.core.command_help('version')
+ if arg in [user.nick for user in self.users]:
+ jid = safeJID(self.name).bare
+ jid = safeJID(jid + '/' + arg)
+ else:
+ jid = safeJID(arg)
+ fixes.get_version(self.core.xmpp, jid, callback=callback)
+
+ def command_nick(self, arg):
+ """
+ /nick <nickname>
+ """
+ if not arg:
+ return self.core.command_help('nick')
+ nick = arg
+ if not self.joined:
+ return self.core.information('/nick only works in joined rooms', 'Info')
+ current_status = self.core.get_status()
+ if not safeJID(self.get_name() + '/' + nick):
+ return self.core.information('Invalid nick', 'Info')
+ muc.change_nick(self.core, self.name, nick, current_status.message, current_status.show)
+
+ def command_part(self, arg):
+ """
+ /part [msg]
+ """
+ arg = arg.strip()
+ msg = None
+ if self.joined:
+ self.disconnect()
+ muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, arg)
+ if arg:
+ msg = _("\x195}You left the chatroom (\x19o%s\x195})\x193}" % arg)
+ else:
+ msg =_("\x195}You left the chatroom\x193}")
+ self.add_message(msg, typ=2)
+ if self == self.core.current_tab():
+ self.refresh()
+ self.core.doupdate()
+ else:
+ msg =_("\x195}You left the chatroom\x193}")
+ self.core.disable_private_tabs(self.name, reason=msg)
+
+ def command_close(self, arg):
+ """
+ /close [msg]
+ """
+ self.command_part(arg)
+ self.core.close_tab()
+
+ def command_query(self, arg):
+ """
+ /query <nick> [message]
+ """
+ args = common.shell_split(arg)
+ if len(args) < 1:
+ return
+ nick = args[0]
+ r = None
+ for user in self.users:
+ if user.nick == nick:
+ r = self.core.open_private_window(self.name, user.nick)
+ if r and len(args) > 1:
+ msg = args[1]
+ self.core.current_tab().command_say(xhtml.convert_simple_to_full_colors(msg))
+ if not r:
+ self.core.information(_("Cannot find user: %s" % nick), 'Error')
+
+ def command_topic(self, arg):
+ """
+ /topic [new topic]
+ """
+ if not arg.strip():
+ self._text_buffer.add_message(_("\x19%s}The subject of the room is: %s") %
+ (dump_tuple(get_theme().COLOR_INFORMATION_TEXT), self.topic))
+ self.refresh()
+ return
+ subject = arg
+ muc.change_subject(self.core.xmpp, self.name, subject)
+
+ def command_names(self, arg=None):
+ """
+ /names
+ """
+ if not self.joined:
+ return
+ color_visitor = dump_tuple(get_theme().COLOR_USER_VISITOR)
+ color_other = dump_tuple(get_theme().COLOR_USER_NONE)
+ color_moderator = dump_tuple(get_theme().COLOR_USER_MODERATOR)
+ color_participant = dump_tuple(get_theme().COLOR_USER_PARTICIPANT)
+ visitors, moderators, participants, others = [], [], [], []
+ aff = {
+ 'owner': get_theme().CHAR_AFFILIATION_OWNER,
+ 'admin': get_theme().CHAR_AFFILIATION_ADMIN,
+ 'member': get_theme().CHAR_AFFILIATION_MEMBER,
+ 'none': get_theme().CHAR_AFFILIATION_NONE,
+ }
+
+ users = self.users[:]
+ users.sort(key=lambda x: x.nick.lower())
+ for user in users:
+ color = aff.get(user.affiliation, get_theme().CHAR_AFFILIATION_NONE)
+ if user.role == 'visitor':
+ visitors.append((user, color))
+ elif user.role == 'participant':
+ participants.append((user, color))
+ elif user.role == 'moderator':
+ moderators.append((user, color))
+ else:
+ others.append((user, color))
+
+ buff = ['Users: %s \n' % len(self.users)]
+ for moderator in moderators:
+ buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % (color_moderator,
+ moderator[1], dump_tuple(moderator[0].color), moderator[0].nick))
+ for participant in participants:
+ buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % (color_participant,
+ participant[1], dump_tuple(participant[0].color), participant[0].nick))
+ for visitor in visitors:
+ buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % (color_visitor,
+ visitor[1], dump_tuple(visitor[0].color), visitor[0].nick))
+ for other in others:
+ buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % (color_other,
+ other[1], dump_tuple(other[0].color), other[0].nick))
+ buff.append('\n')
+ message = ' '.join(buff)
+
+ self._text_buffer.add_message(message)
+ self.text_win.refresh()
+ self.input.refresh()
+
+ def completion_topic(self, the_input):
+ if the_input.get_argument_position() == 1:
+ return the_input.auto_completion([self.topic], '', quotify=False)
+
+ def completion_quoted(self, the_input):
+ """Nick completion, but with quotes"""
+ if the_input.get_argument_position(quoted=True) == 1:
+ compare_users = lambda x: x.last_talked
+ word_list = [user.nick for user in sorted(self.users, key=compare_users, reverse=True)\
+ if user.nick != self.own_nick]
+ return the_input.new_completion(word_list, 1, quotify=True)
+
+ def command_kick(self, arg):
+ """
+ /kick <nick> [reason]
+ """
+ args = common.shell_split(arg)
+ if not args:
+ self.core.command_help('kick')
+ else:
+ if len(args) > 1:
+ msg = ' "%s"' % args[1]
+ else:
+ msg = ''
+ self.command_role('"'+args[0]+ '" none'+msg)
+
+ def command_ban(self, arg):
+ """
+ /ban <nick> [reason]
+ """
+ def callback(iq):
+ if iq['type'] == 'error':
+ self.core.room_error(iq, self.get_name())
+ args = common.shell_split(arg)
+ if not args:
+ return self.core.command_help('ban')
+ if len(args) > 1:
+ msg = args[1]
+ else:
+ msg = ''
+ nick = args[0]
+
+ if nick in [user.nick for user in self.users]:
+ res = muc.set_user_affiliation(self.core.xmpp, self.get_name(), 'outcast', nick=nick, callback=callback, reason=msg)
+ else:
+ res = muc.set_user_affiliation(self.core.xmpp, self.get_name(), 'outcast', jid=safeJID(nick), callback=callback, reason=msg)
+ if not res:
+ self.core.information('Could not ban user', 'Error')
+
+ def command_role(self, arg):
+ """
+ /role <nick> <role> [reason]
+ Changes the role of an user
+ roles can be: none, visitor, participant, moderator
+ """
+ def callback(iq):
+ if iq['type'] == 'error':
+ self.core.room_error(iq, self.get_name())
+ args = common.shell_split(arg)
+ if len(args) < 2:
+ self.core.command_help('role')
+ return
+ nick, role = args[0],args[1]
+ if len(args) > 2:
+ reason = ' '.join(args[2:])
+ else:
+ reason = ''
+ if not self.joined or \
+ not role in ('none', 'visitor', 'participant', 'moderator'):
+ return
+ if not safeJID(self.get_name() + '/' + nick):
+ return self.core('Invalid nick', 'Info')
+ muc.set_user_role(self.core.xmpp, self.get_name(), nick, reason, role, callback=callback)
+
+ def command_affiliation(self, arg):
+ """
+ /affiliation <nick> <role>
+ Changes the affiliation of an user
+ affiliations can be: outcast, none, member, admin, owner
+ """
+ def callback(iq):
+ if iq['type'] == 'error':
+ self.core.room_error(iq, self.get_name())
+ args = common.shell_split(arg)
+ if len(args) < 2:
+ self.core.command_help('affiliation')
+ return
+ nick, affiliation = args[0], args[1].lower()
+ if not self.joined:
+ return
+ if affiliation not in ('outcast', 'none', 'member', 'admin', 'owner'):
+ self.core.command_help('affiliation')
+ return
+ if nick in [user.nick for user in self.users]:
+ res = muc.set_user_affiliation(self.core.xmpp, self.get_name(), affiliation, nick=nick, callback=callback)
+ else:
+ res = muc.set_user_affiliation(self.core.xmpp, self.get_name(), affiliation, jid=safeJID(nick), callback=callback)
+ if not res:
+ self.core.information('Could not set affiliation', 'Error')
+
+ def command_say(self, line, correct=False):
+ """
+ /say <message>
+ Or normal input + enter
+ """
+ needed = 'inactive' if self.inactive else 'active'
+ msg = self.core.xmpp.make_message(self.get_name())
+ msg['type'] = 'groupchat'
+ msg['body'] = line
+ # trigger the event BEFORE looking for colors.
+ # This lets a plugin insert \x19xxx} colors, that will
+ # be converted in xhtml.
+ self.core.events.trigger('muc_say', msg, self)
+ if not msg['body']:
+ self.cancel_paused_delay()
+ self.text_win.refresh()
+ self.input.refresh()
+ return
+ if msg['body'].find('\x19') != -1:
+ msg.enable('html')
+ msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body'])
+ msg['body'] = xhtml.clean_text(msg['body'])
+ if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and self.remote_wants_chatstates is not False:
+ msg['chat_state'] = needed
+ if correct:
+ msg['replace']['id'] = self.last_sent_message['id']
+ self.cancel_paused_delay()
+ self.core.events.trigger('muc_say_after', msg, self)
+ if not msg['body']:
+ self.cancel_paused_delay()
+ self.text_win.refresh()
+ self.input.refresh()
+ return
+ self.last_sent_message = msg
+ msg.send()
+ self.chat_state = needed
+
+ def command_xhtml(self, arg):
+ message = self.generate_xhtml_message(arg)
+ if message:
+ message['type'] = 'groupchat'
+ message.send()
+
+ def command_ignore(self, arg):
+ """
+ /ignore <nick>
+ """
+ if not arg:
+ self.core.command_help('ignore')
+ return
+ nick = arg
+ user = self.get_user_by_name(nick)
+ if not user:
+ self.core.information(_('%s is not in the room') % nick)
+ elif user in self.ignores:
+ self.core.information(_('%s is already ignored') % nick)
+ else:
+ self.ignores.append(user)
+ self.core.information(_("%s is now ignored") % nick, 'info')
+
+ def command_unignore(self, arg):
+ """
+ /unignore <nick>
+ """
+ if not arg:
+ self.core.command_help('unignore')
+ return
+ nick = arg
+ user = self.get_user_by_name(nick)
+ if not user:
+ self.core.information(_('%s is not in the room') % nick)
+ elif user not in self.ignores:
+ self.core.information(_('%s is not ignored') % nick)
+ else:
+ self.ignores.remove(user)
+ self.core.information(_('%s is now unignored') % nick)
+
+ def completion_unignore(self, the_input):
+ if the_input.get_argument_position() == 1:
+ return the_input.new_completion([user.nick for user in self.ignores], 1, '', quotify=False)
+
+ def resize(self):
+ """
+ Resize the whole window. i.e. all its sub-windows
+ """
+ if not self.visible:
+ return
+ self.need_resize = False
+ if config.get("hide_user_list", "false") == "true":
+ text_width = self.width
+ else:
+ text_width = (self.width//10)*9
+ self.user_win.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width-(self.width//10)*9-1, 1, (self.width//10)*9+1)
+ self.topic_win.resize(1, self.width, 0, 0)
+ self.v_separator.resize(self.height-2 - Tab.tab_win_height(), 1, 1, 9*(self.width//10))
+ self.text_win.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), text_width, 1, 0)
+ self.text_win.rebuild_everything(self._text_buffer)
+ self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
+ self.input.resize(1, self.width, self.height-1, 0)
+
+ def refresh(self):
+ if self.need_resize:
+ self.resize()
+ log.debug(' TAB Refresh: %s',self.__class__.__name__)
+ self.topic_win.refresh(self.get_single_line_topic())
+ self.text_win.refresh()
+ if config.get("hide_user_list", "false") == "false":
+ self.v_separator.refresh()
+ self.user_win.refresh(self.users)
+ self.info_header.refresh(self, self.text_win)
+ self.refresh_tab_win()
+ self.info_win.refresh()
+ self.input.refresh()
+
+ def on_input(self, key, raw):
+ if not raw and key in self.key_func:
+ self.key_func[key]()
+ return False
+ self.input.do_command(key, raw=raw)
+ empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//'))
+ self.send_composing_chat_state(empty_after)
+ return False
+
+ def completion(self):
+ """
+ Called when Tab is pressed, complete the nickname in the input
+ """
+ if self.complete_commands(self.input):
+ return
+
+ # If we are not completing a command or a command's argument, complete a nick
+ compare_users = lambda x: x.last_talked
+ word_list = [user.nick for user in sorted(self.users, key=compare_users, reverse=True)\
+ if user.nick != self.own_nick]
+ after = config.get('after_completion', ',')+" "
+ input_pos = self.input.pos
+ if ' ' not in self.input.get_text()[:input_pos] or (self.input.last_completion and\
+ self.input.get_text()[:input_pos] == self.input.last_completion + after):
+ add_after = after
+ else:
+ add_after = '' if config.get('add_space_after_completion', 'true') == 'false' else ' '
+ self.input.auto_completion(word_list, add_after, quotify=False)
+ empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//'))
+ self.send_composing_chat_state(empty_after)
+
+ def get_name(self):
+ return self.name
+
+ def get_nick(self):
+ if config.getl('show_muc_jid', 'true') == 'false':
+ return safeJID(self.name).user
+ return self.name
+
+ def get_text_window(self):
+ return self.text_win
+
+ def on_lose_focus(self):
+ if self.joined:
+ self.state = 'normal'
+ else:
+ self.state = 'disconnected'
+ self.text_win.remove_line_separator()
+ self.text_win.add_line_separator(self._text_buffer)
+ if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and not self.input.get_text():
+ self.send_chat_state('inactive')
+ self.check_scrolled()
+
+ def on_gain_focus(self):
+ self.state = 'current'
+ if self.text_win.built_lines and self.text_win.built_lines[-1] is None and config.getl('show_useless_separator', 'false') != 'true':
+ self.text_win.remove_line_separator()
+ curses.curs_set(1)
+ if self.joined and config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and not self.input.get_text():
+ self.send_chat_state('active')
+
+ def on_info_win_size_changed(self):
+ if self.core.information_win_size >= self.height-3:
+ return
+ if config.get("hide_user_list", "false") == "true":
+ text_width = self.width
+ else:
+ text_width = (self.width//10)*9
+ self.user_win.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), self.width-(self.width//10)*9-1, 1, (self.width//10)*9+1)
+ self.text_win.resize(self.height-3-self.core.information_win_size - Tab.tab_win_height(), text_width, 1, 0)
+ self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
+
+ def handle_presence(self, presence):
+ from_nick = presence['from'].resource
+ from_room = presence['from'].bare
+ status_codes = set([s.attrib['code'] for s in presence.findall('{%s}x/{%s}status' % (NS_MUC_USER, NS_MUC_USER))])
+ # Check if it's not an error presence.
+ if presence['type'] == 'error':
+ return self.core.room_error(presence, from_room)
+ affiliation = presence['muc']['affiliation']
+ show = presence['show']
+ status = presence['status']
+ role = presence['muc']['role']
+ jid = presence['muc']['jid']
+ typ = presence['type']
+ if not self.joined: # user in the room BEFORE us.
+ # ignore redondant presence message, see bug #1509
+ if from_nick not in [user.nick for user in self.users] and typ != "unavailable":
+ new_user = User(from_nick, affiliation, show, status, role, jid)
+ self.users.append(new_user)
+ self.core.events.trigger('muc_join', presence, self)
+ if '110' in status_codes or self.own_nick == from_nick:
+ # second part of the condition is a workaround for old
+ # ejabberd or every gateway in the world that just do
+ # not send a 110 status code with the presence
+ self.own_nick = from_nick
+ self.joined = True
+ if self.get_name() in self.core.initial_joins:
+ self.core.initial_joins.remove(self.get_name())
+ self._state = 'normal'
+ elif self != self.core.current_tab():
+ self._state = 'joined'
+ if self.core.current_tab() == self and self.core.status.show not in ('xa', 'away'):
+ self.send_chat_state('active')
+ new_user.color = get_theme().COLOR_OWN_NICK
+ self.add_message(_("\x19%(info_col)s}Your nickname is \x19%(nick_col)s}%(nick)s") % {
+ 'nick': from_nick,
+ 'nick_col': dump_tuple(new_user.color),
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ typ=2)
+ if '201' in status_codes:
+ self.add_message('\x19%(info_col)s}Info: The room has been created' % {
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ typ=2)
+ if '170' in status_codes:
+ self.add_message('\x191}Warning: \x19%(info_col)s}this room is publicly logged' % {
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ typ=2)
+ if '100' in status_codes:
+ self.add_message('\x191}Warning: \x19%(info_col)s}This room is not anonymous.' % {
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ typ=2)
+ if self.core.current_tab() is not self:
+ self.refresh_tab_win()
+ self.core.current_tab().input.refresh()
+ self.core.doupdate()
+ self.core.enable_private_tabs(self.get_name())
+ else:
+ change_nick = '303' in status_codes
+ kick = '307' in status_codes and typ == 'unavailable'
+ ban = '301' in status_codes and typ == 'unavailable'
+ shutdown = '332' in status_codes and typ == 'unavailable'
+ non_member = '322' in status_codes and typ == 'unavailable'
+ user = self.get_user_by_name(from_nick)
+ # New user
+ if not user:
+ self.core.events.trigger('muc_join', presence, self)
+ self.on_user_join(from_nick, affiliation, show, status, role, jid)
+ # nick change
+ elif change_nick:
+ self.core.events.trigger('muc_nickchange', presence, self)
+ self.on_user_nick_change(presence, user, from_nick, from_room)
+ elif ban:
+ self.core.events.trigger('muc_ban', presence, self)
+ self.core.on_user_left_private_conversation(from_room, from_nick, status)
+ self.on_user_banned(presence, user, from_nick)
+ # kick
+ elif kick:
+ self.core.events.trigger('muc_kick', presence, self)
+ self.core.on_user_left_private_conversation(from_room, from_nick, status)
+ self.on_user_kicked(presence, user, from_nick)
+ elif shutdown:
+ self.core.events.trigger('muc_shutdown', presence, self)
+ self.on_muc_shutdown()
+ elif non_member:
+ self.core.events.trigger('muc_shutdown', presence, self)
+ self.on_non_member_kicked()
+ # user quit
+ elif typ == 'unavailable':
+ self.on_user_leave_groupchat(user, jid, status, from_nick, from_room)
+ # status change
+ else:
+ self.on_user_change_status(user, from_nick, from_room, affiliation, role, show, status)
+ if self.core.current_tab() is self:
+ self.text_win.refresh()
+ self.user_win.refresh(self.users)
+ self.info_header.refresh(self, self.text_win)
+ self.input.refresh()
+ self.core.doupdate()
+
+ def on_non_member_kicked(self):
+ """We have been kicked because the MUC is members-only"""
+ self.add_message('\x19%(info_col)s}%You have been kicked because you are not a member and the room is now members-only.' % {
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ typ=2)
+ self.disconnect()
+
+ def on_muc_shutdown(self):
+ """We have been kicked because the MUC service is shutting down"""
+ self.add_message('\x19%(info_col)s}%You have been kicked because the MUC service is shutting down.' % {
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ typ=2)
+ self.disconnect()
+
+ def on_user_join(self, from_nick, affiliation, show, status, role, jid):
+ """
+ When a new user joins the groupchat
+ """
+ user = User(from_nick, affiliation,
+ show, status, role, jid)
+ self.users.append(user)
+ hide_exit_join = config.get_by_tabname('hide_exit_join', -1, self.general_jid, True)
+ if hide_exit_join != 0:
+ color = dump_tuple(user.color) if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
+ if not jid.full:
+ msg = '\x194}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} joined the room' % {
+ 'nick':from_nick, 'color':color, 'spec':get_theme().CHAR_JOIN,
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ else:
+ msg = '\x194}%(spec)s \x19%(color)s}%(nick)s \x19%(info_col)s}(\x19%(jid_color)s}%(jid)s\x19%(info_col)s}) joined the room' % {
+ 'spec':get_theme().CHAR_JOIN, 'nick':from_nick, 'color':color, 'jid':jid.full,
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
+ 'jid_color': dump_tuple(get_theme().COLOR_MUC_JID)}
+ self.add_message(msg, typ=2)
+ self.core.on_user_rejoined_private_conversation(self.name, from_nick)
+
+ def on_user_nick_change(self, presence, user, from_nick, from_room):
+ new_nick = presence.find('{%s}x/{%s}item' % (NS_MUC_USER, NS_MUC_USER)).attrib['nick']
+ if user.nick == self.own_nick:
+ self.own_nick = new_nick
+ # also change our nick in all private discussions of this room
+ self.core.on_muc_own_nickchange(self)
+ user.change_nick(new_nick)
+ color = dump_tuple(user.color) if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
+ self.add_message('\x19%(color)s}%(old)s\x19%(info_col)s} is now known as \x19%(color)s}%(new)s' % {
+ 'old':from_nick, 'new':new_nick, 'color':color,
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ typ=2)
+ # rename the private tabs if needed
+ self.core.rename_private_tabs(self.name, from_nick, new_nick)
+
+ def on_user_banned(self, presence, user, from_nick):
+ """
+ When someone is banned from a muc
+ """
+ self.users.remove(user)
+ by = presence.find('{%s}x/{%s}item/{%s}actor' % (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER))
+ reason = presence.find('{%s}x/{%s}item/{%s}reason' % (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER))
+ by = by.attrib['jid'] if by is not None else None
+ if from_nick == self.own_nick: # we are banned
+ if by:
+ kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been banned by \x194}%(by)s') % {
+ 'spec': get_theme().CHAR_KICK, 'by':by,
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ else:
+ kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been banned.') % {
+ 'spec': get_theme().CHAR_KICK,
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ self.core.disable_private_tabs(self.name, reason=kick_msg)
+ self.disconnect()
+ self.refresh_tab_win()
+ self.core.current_tab().input.refresh()
+ self.core.doupdate()
+ if config.get_by_tabname('autorejoin', 'false', self.general_jid, True) == 'true':
+ delay = config.get_by_tabname('autorejoin_delay', "5", self.general_jid, True)
+ delay = common.parse_str_to_secs(delay)
+ if delay <= 0:
+ muc.join_groupchat(self.core, self.name, self.own_nick)
+ else:
+ self.core.add_timed_event(timed_events.DelayedEvent(
+ delay,
+ muc.join_groupchat,
+ self.core,
+ self.name,
+ self.own_nick))
+
+ else:
+ color = dump_tuple(user.color) if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
+ if by:
+ kick_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} has been banned by \x194}%(by)s') % {
+ 'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'by':by,
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ else:
+ kick_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} has been banned') % {
+ 'spec':get_theme().CHAR_KICK, 'nick':from_nick, 'color':color,
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ if reason is not None and reason.text:
+ kick_msg += _('\x19%(info_col)s} Reason: \x196}%(reason)s\x19%(info_col)s}') % {
+ 'reason': reason.text, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ self.add_message(kick_msg, typ=2)
+
+ def on_user_kicked(self, presence, user, from_nick):
+ """
+ When someone is kicked from a muc
+ """
+ self.users.remove(user)
+ actor_elem = presence.find('{%s}x/{%s}item/{%s}actor' % (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER))
+ reason = presence.find('{%s}x/{%s}item/{%s}reason' % (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER))
+ by = None
+ if actor_elem is not None:
+ by = actor_elem.get('nick') or actor_elem.get('jid')
+ if from_nick == self.own_nick: # we are kicked
+ if by:
+ kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been kicked by \x193}%(by)s') % {
+ 'spec': get_theme().CHAR_KICK, 'by':by,
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ else:
+ kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s} have been kicked.') % {'spec':get_theme().CHAR_KICK, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ self.core.disable_private_tabs(self.name, reason=kick_msg)
+ self.disconnect()
+ self.refresh_tab_win()
+ self.core.current_tab().input.refresh()
+ self.core.doupdate()
+ # try to auto-rejoin
+ if config.get_by_tabname('autorejoin', 'false', self.general_jid, True) == 'true':
+ delay = config.get_by_tabname('autorejoin_delay', "5", self.general_jid, True)
+ delay = common.parse_str_to_secs(delay)
+ if delay <= 0:
+ muc.join_groupchat(self.core, self.name, self.own_nick)
+ else:
+ self.core.add_timed_event(timed_events.DelayedEvent(
+ delay,
+ muc.join_groupchat,
+ self.core,
+ self.name,
+ self.own_nick))
+ else:
+ color = dump_tuple(user.color) if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
+ if by:
+ kick_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} has been kicked by \x193}%(by)s') % {'spec': get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'by':by, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ else:
+ kick_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} has been kicked') % {'spec': get_theme().CHAR_KICK, 'nick':from_nick, 'color':color, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ if reason is not None and reason.text:
+ kick_msg += _('\x19%(info_col)s} Reason: \x196}%(reason)s') % {'reason': reason.text, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ self.add_message(kick_msg, typ=2)
+
+ def on_user_leave_groupchat(self, user, jid, status, from_nick, from_room):
+ """
+ When an user leaves a groupchat
+ """
+ self.users.remove(user)
+ if self.own_nick == user.nick:
+ # We are now out of the room. Happens with some buggy (? not sure) servers
+ self.disconnect()
+ self.core.disable_private_tabs(from_room)
+ self.refresh_tab_win()
+ self.core.current_tab().input.refresh()
+ self.core.doupdate()
+ hide_exit_join = config.get_by_tabname('hide_exit_join', -1, self.general_jid, True) if config.get_by_tabname('hide_exit_join', -1, self.general_jid, True) >= -1 else -1
+ if hide_exit_join == -1 or user.has_talked_since(hide_exit_join):
+ color = dump_tuple(user.color) if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
+ if not jid.full:
+ leave_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} has left the room') % {'nick':from_nick, 'color':color, 'spec':get_theme().CHAR_QUIT, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ else:
+ leave_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} (\x194}%(jid)s\x19%(info_col)s}) has left the room') % {'spec':get_theme().CHAR_QUIT, 'nick':from_nick, 'color':color, 'jid':jid.full, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ if status:
+ leave_msg += ' (%s)' % status
+ self.add_message(leave_msg, typ=2)
+ self.core.on_user_left_private_conversation(from_room, from_nick, status)
+
+ def on_user_change_status(self, user, from_nick, from_room, affiliation, role, show, status):
+ """
+ When an user changes her status
+ """
+ # build the message
+ display_message = False # flag to know if something significant enough
+ # to be displayed has changed
+ color = dump_tuple(user.color) if config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True) == 'true' else 3
+ if from_nick == self.own_nick:
+ msg = _('\x193}You\x19%(info_col)s} changed: ') % {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ else:
+ msg = _('\x19%(color)s}%(nick)s\x19%(info_col)s} changed: ') % {'nick': from_nick, 'color': color, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ if show not in SHOW_NAME:
+ self.core.information(_("%s from room %s sent an invalid show: %s") %\
+ (from_nick, from_room, show), "Warning")
+ if affiliation != user.affiliation:
+ msg += _('affiliation: %s, ') % affiliation
+ display_message = True
+ if role != user.role:
+ msg += _('role: %s, ') % role
+ display_message = True
+ if show != user.show and show in SHOW_NAME:
+ msg += _('show: %s, ') % SHOW_NAME[show]
+ display_message = True
+ if status != user.status:
+ # if the user sets his status to nothing
+ if status:
+ msg += _('status: %s, ') % status
+ display_message = True
+ elif show in SHOW_NAME and show == user.show:
+ msg += _('show: %s, ') % SHOW_NAME[show]
+ display_message = True
+ if not display_message:
+ return
+ msg = msg[:-2] # remove the last ", "
+ hide_status_change = config.get_by_tabname('hide_status_change', -1, self.general_jid, True)
+ if hide_status_change < -1:
+ hide_status_change = -1
+ if ((hide_status_change == -1 or \
+ user.has_talked_since(hide_status_change) or\
+ user.nick == self.own_nick)\
+ and\
+ (affiliation != user.affiliation or\
+ role != user.role or\
+ show != user.show or\
+ status != user.status))\
+ or\
+ (affiliation != user.affiliation or\
+ role != user.role):
+ # display the message in the room
+ self._text_buffer.add_message(msg)
+ self.core.on_user_changed_status_in_private('%s/%s' % (from_room, from_nick), msg)
+ # finally, effectively change the user status
+ user.update(affiliation, show, status, role)
+
+ def disconnect(self):
+ """
+ Set the state of the room as not joined, so
+ we can know if we can join it, send messages to it, etc
+ """
+ self.users = []
+ if self is not self.core.current_tab():
+ self.state = 'disconnected'
+ self.joined = False
+
+ def get_single_line_topic(self):
+ """
+ Return the topic as a single-line string (for the window header)
+ """
+ return self.topic.replace('\n', '|')
+
+ def log_message(self, txt, nickname, time=None, typ=1):
+ """
+ Log the messages in the archives, if it needs
+ to be
+ """
+ if time is None and self.joined: # don't log the history messages
+ if not logger.log_message(self.name, nickname, txt, typ=typ):
+ self.core.information(_('Unable to write in the log file'), 'Error')
+
+ def do_highlight(self, txt, time, nickname):
+ """
+ Set the tab color and returns the nick color
+ """
+ highlighted = False
+ if not time and nickname and nickname != self.own_nick and self.joined:
+ if self.own_nick.lower() in txt.lower():
+ if self.state != 'current':
+ self.state = 'highlight'
+ highlighted = True
+ else:
+ highlight_words = config.get_by_tabname('highlight_on', '', self.general_jid, True).split(':')
+ for word in highlight_words:
+ if word and word.lower() in txt.lower():
+ if self.state != 'current':
+ self.state = 'highlight'
+ highlighted = True
+ break
+ if highlighted:
+ beep_on = config.get('beep_on', 'highlight private').split()
+ if 'highlight' in beep_on and 'message' not in beep_on:
+ if config.get_by_tabname('disable_beep', 'false', self.name, False).lower() != 'true':
+ curses.beep()
+ return highlighted
+
+ def get_user_by_name(self, nick):
+ """
+ Gets the user associated with the given nick, or None if not found
+ """
+ for user in self.users:
+ if user.nick == nick:
+ return user
+ return None
+
+ def add_message(self, txt, time=None, nickname=None, **kwargs):
+ """
+ Note that user can be None even if nickname is not None. It happens
+ when we receive an history message said by someone who is not
+ in the room anymore
+ Return True if the message highlighted us. False otherwise.
+ """
+ self.log_message(txt, nickname, time=time, typ=kwargs.get('typ', 1))
+ args = {key: value for key, value in kwargs.items() if key not in ('typ', 'forced_user')}
+ user = self.get_user_by_name(nickname) if nickname is not None else None
+ if user:
+ user.set_last_talked(datetime.now())
+ args['user'] = user
+ if not user and kwargs.get('forced_user'):
+ args['user'] = kwargs['forced_user']
+ if not time and nickname and\
+ nickname != self.own_nick and\
+ self.state != 'current':
+ if self.state != 'highlight' and\
+ config.get_by_tabname('notify_messages', 'true', self.get_name()) == 'true':
+ self.state = 'message'
+ if (not nickname or time) and not txt.startswith('/me '):
+ txt = '\x19%(info_col)s}%(txt)s' % {'txt':txt, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
+ elif not kwargs.get('highlight'): # TODO
+ args['highlight'] = self.do_highlight(txt, time, nickname)
+ time = time or datetime.now()
+ self._text_buffer.add_message(txt, time, nickname, **args)
+ return args.get('highlight', False)
+
+ def modify_message(self, txt, old_id, new_id, time=None, nickname=None, user=None, jid=None):
+ self.log_message(txt, nickname, time=time, typ=1)
+ highlight = self.do_highlight(txt, time, nickname)
+ message = self._text_buffer.modify_message(txt, old_id, new_id, highlight=highlight, time=time, user=user, jid=jid)
+ if message:
+ self.text_win.modify_message(old_id, message)
+ self.core.refresh_window()
+ return highlight
+ return False
+
+ def matching_names(self):
+ return [(1, safeJID(self.get_name()).user), (3, self.get_name())]
+
+
diff --git a/src/tabs/privatetab.py b/src/tabs/privatetab.py
new file mode 100644
index 00000000..574a2218
--- /dev/null
+++ b/src/tabs/privatetab.py
@@ -0,0 +1,342 @@
+from gettext import gettext as _
+
+import logging
+log = logging.getLogger(__name__)
+
+import curses
+
+from . import ChatTab, MucTab, Tab
+
+import common
+import fixes
+import windows
+import xhtml
+from common import safeJID
+from config import config
+from decorators import refresh_wrapper
+from logger import logger
+from theming import get_theme, dump_tuple
+
+class PrivateTab(ChatTab):
+ """
+ The tab containg a private conversation (someone from a MUC)
+ """
+ message_type = 'chat'
+ plugin_commands = {}
+ additional_informations = {}
+ plugin_keys = {}
+ def __init__(self, name, nick):
+ ChatTab.__init__(self, name)
+ self.own_nick = nick
+ self.name = name
+ self.text_win = windows.TextWin()
+ self._text_buffer.add_window(self.text_win)
+ self.info_header = windows.PrivateInfoWin()
+ self.input = windows.MessageInput()
+ self.check_attention()
+ # keys
+ self.key_func['^I'] = self.completion
+ # commands
+ self.register_command('info', self.command_info,
+ desc=_('Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.'),
+ shortdesc=_('Info about the user.'))
+ self.register_command('unquery', self.command_unquery,
+ shortdesc=_('Close the tab.'))
+ self.register_command('close', self.command_unquery,
+ shortdesc=_('Close the tab.'))
+ self.register_command('version', self.command_version,
+ desc=_('Get the software version of the current interlocutor (usually its XMPP client and Operating System).'),
+ shortdesc=_('Get the software version of a jid.'))
+ self.resize()
+ self.parent_muc = self.core.get_tab_by_name(safeJID(name).bare, MucTab)
+ self.on = True
+ self.update_commands()
+ self.update_keys()
+
+ @property
+ def general_jid(self):
+ return self.get_name()
+
+ @property
+ def nick(self):
+ return self.get_nick()
+
+ @staticmethod
+ def add_information_element(plugin_name, callback):
+ """
+ Lets a plugin add its own information to the PrivateInfoWin
+ """
+ PrivateTab.additional_informations[plugin_name] = callback
+
+ @staticmethod
+ def remove_information_element(plugin_name):
+ del PrivateTab.additional_informations[plugin_name]
+
+ def load_logs(self, log_nb):
+ logs = logger.get_logs(safeJID(self.get_name()).full.replace('/', '\\'), log_nb)
+
+ def log_message(self, txt, nickname, time=None, typ=1):
+ """
+ Log the messages in the archives.
+ """
+ if not logger.log_message(self.name, nickname, txt, date=time, typ=typ):
+ self.core.information(_('Unable to write in the log file'), 'Error')
+
+ def on_close(self):
+ self.parent_muc.privates.remove(self)
+
+ def completion(self):
+ """
+ Called when Tab is pressed, complete the nickname in the input
+ """
+ if self.complete_commands(self.input):
+ return
+
+ # If we are not completing a command or a command's argument, complete a nick
+ compare_users = lambda x: x.last_talked
+ word_list = [user.nick for user in sorted(self.parent_muc.users, key=compare_users, reverse=True)\
+ if user.nick != self.own_nick]
+ after = config.get('after_completion', ',')+" "
+ input_pos = self.input.pos
+ if ' ' not in self.input.get_text()[:input_pos] or (self.input.last_completion and\
+ self.input.get_text()[:input_pos] == self.input.last_completion + after):
+ add_after = after
+ else:
+ add_after = ''
+ self.input.auto_completion(word_list, add_after, quotify=False)
+ empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//'))
+ self.send_composing_chat_state(empty_after)
+
+ def command_say(self, line, attention=False, correct=False):
+ if not self.on:
+ return
+ msg = self.core.xmpp.make_message(self.get_name())
+ msg['type'] = 'chat'
+ msg['body'] = line
+ # trigger the event BEFORE looking for colors.
+ # This lets a plugin insert \x19xxx} colors, that will
+ # be converted in xhtml.
+ self.core.events.trigger('private_say', msg, self)
+ if not msg['body']:
+ self.cancel_paused_delay()
+ self.text_win.refresh()
+ self.input.refresh()
+ return
+ user = self.parent_muc.get_user_by_name(self.own_nick)
+ replaced = False
+ if correct or msg['replace']['id']:
+ msg['replace']['id'] = self.last_sent_message['id']
+ if config.get_by_tabname('group_corrections', 'true', self.get_name()).lower() != 'false':
+ try:
+ self.modify_message(msg['body'], self.last_sent_message['id'], msg['id'],
+ user=user, jid=self.core.xmpp.boundjid, nickname=self.own_nick)
+ replaced = True
+ except:
+ log.error('Unable to correct a message', exc_info=True)
+ else:
+ del msg['replace']
+
+ if msg['body'].find('\x19') != -1:
+ msg.enable('html')
+ msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body'])
+ msg['body'] = xhtml.clean_text(msg['body'])
+ if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) == 'true' and self.remote_wants_chatstates is not False:
+ needed = 'inactive' if self.inactive else 'active'
+ msg['chat_state'] = needed
+ if attention and self.remote_supports_attention:
+ msg['attention'] = True
+ self.core.events.trigger('private_say_after', msg, self)
+ if not msg['body']:
+ self.cancel_paused_delay()
+ self.text_win.refresh()
+ self.input.refresh()
+ return
+ if not replaced:
+ self.add_message(msg['body'],
+ nickname=self.core.own_nick or self.own_nick,
+ forced_user=user,
+ nick_color=get_theme().COLOR_OWN_NICK,
+ identifier=msg['id'],
+ jid=self.core.xmpp.boundjid,
+ typ=1)
+
+ self.last_sent_message = msg
+ msg.send()
+ self.cancel_paused_delay()
+ self.text_win.refresh()
+ self.input.refresh()
+
+ def command_attention(self, message=''):
+ if message is not '':
+ self.command_say(message, attention=True)
+ else:
+ msg = self.core.xmpp.make_message(self.get_name())
+ msg['type'] = 'chat'
+ msg['attention'] = True
+ msg.send()
+
+ def check_attention(self):
+ self.core.xmpp.plugin['xep_0030'].get_info(jid=self.get_name(), block=False, timeout=5, callback=self.on_attention_checked)
+
+ def on_attention_checked(self, iq):
+ if 'urn:xmpp:attention:0' in iq['disco_info'].get_features():
+ self.core.information('Attention is supported', 'Info')
+ self.remote_supports_attention = True
+ self.commands['attention'] = (self.command_attention, _('Usage: /attention [message]\nAttention: Require the attention of the contact. Can also send a message along with the attention.'), None)
+ else:
+ self.remote_supports_attention = False
+
+ def command_unquery(self, arg):
+ """
+ /unquery
+ """
+ self.core.close_tab()
+
+ def command_version(self, arg):
+ """
+ /version
+ """
+ def callback(res):
+ if not res:
+ return self.core.information('Could not get the software version from %s' % (jid,), 'Warning')
+ version = '%s is running %s version %s on %s' % (jid,
+ res.get('name') or _('an unknown software'),
+ res.get('version') or _('unknown'),
+ res.get('os') or _('an unknown platform'))
+ self.core.information(version, 'Info')
+ if arg:
+ return self.core.command_version(arg)
+ jid = safeJID(self.name)
+ fixes.get_version(self.core.xmpp, jid, callback=callback)
+
+ def command_info(self, arg):
+ """
+ /info
+ """
+ if arg:
+ self.parent_muc.command_info(arg)
+ else:
+ user = safeJID(self.name).resource
+ self.parent_muc.command_info(user)
+
+ def resize(self):
+ if self.core.information_win_size >= self.height-3 or not self.visible:
+ return
+ self.need_resize = False
+ self.text_win.resize(self.height-2-self.core.information_win_size - Tab.tab_win_height(), self.width, 0, 0)
+ self.text_win.rebuild_everything(self._text_buffer)
+ self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
+ self.input.resize(1, self.width, self.height-1, 0)
+
+ def refresh(self):
+ if self.need_resize:
+ self.resize()
+ log.debug(' TAB Refresh: %s',self.__class__.__name__)
+ self.text_win.refresh()
+ self.info_header.refresh(self.name, self.text_win, self.chatstate, PrivateTab.additional_informations)
+ self.info_win.refresh()
+ self.refresh_tab_win()
+ self.input.refresh()
+
+ def refresh_info_header(self):
+ self.info_header.refresh(self.name, self.text_win, self.chatstate, PrivateTab.additional_informations)
+ self.input.refresh()
+
+ def get_name(self):
+ return self.name
+
+ def get_nick(self):
+ return safeJID(self.name).resource
+
+ def on_input(self, key, raw):
+ if not raw and key in self.key_func:
+ self.key_func[key]()
+ return False
+ self.input.do_command(key, raw=raw)
+ if not self.on:
+ return False
+ empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//'))
+ tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab)
+ if tab and tab.joined:
+ self.send_composing_chat_state(empty_after)
+ return False
+
+ def on_lose_focus(self):
+ self.state = 'normal'
+ self.text_win.remove_line_separator()
+ self.text_win.add_line_separator(self._text_buffer)
+ tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab)
+ if tab and tab.joined and config.get_by_tabname(
+ 'send_chat_states', 'true', self.general_jid, True) == 'true'\
+ and not self.input.get_text() and self.on:
+ self.send_chat_state('inactive')
+ self.check_scrolled()
+
+ def on_gain_focus(self):
+ self.state = 'current'
+ curses.curs_set(1)
+ tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab)
+ if tab and tab.joined and config.get_by_tabname(
+ 'send_chat_states', 'true', self.general_jid, True) == 'true'\
+ and not self.input.get_text() and self.on:
+ self.send_chat_state('active')
+
+ def on_info_win_size_changed(self):
+ if self.core.information_win_size >= self.height-3:
+ return
+ self.text_win.resize(self.height-2-self.core.information_win_size - Tab.tab_win_height(), self.width, 0, 0)
+ self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
+
+ def get_text_window(self):
+ return self.text_win
+
+ def rename_user(self, old_nick, new_nick):
+ """
+ The user changed her nick in the corresponding muc: update the tab’s name and
+ display a message.
+ """
+ self.add_message('\x193}%(old)s\x19%(info_col)s} is now known as \x193}%(new)s' % {'old':old_nick, 'new':new_nick, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
+ new_jid = safeJID(self.name).bare+'/'+new_nick
+ self.name = new_jid
+
+ @refresh_wrapper.conditional
+ def user_left(self, status_message, from_nick):
+ """
+ The user left the associated MUC
+ """
+ self.deactivate()
+ if not status_message:
+ self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x19%(info_col)s} has left the room') % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
+ else:
+ self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x19%(info_col)s} has left the room (%(status)s)"') % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT, 'status': status_message, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
+ return self.core.current_tab() is self
+
+ @refresh_wrapper.conditional
+ def user_rejoined(self, nick):
+ """
+ The user (or at least someone with the same nick) came back in the MUC
+ """
+ self.activate()
+ tab = self.core.get_tab_by_name(safeJID(self.name).bare, MucTab)
+ color = 3
+ if tab and config.get_by_tabname('display_user_color_in_join_part', '', self.general_jid, True):
+ user = tab.get_user_by_name(nick)
+ if user:
+ color = dump_tuple(user.color)
+ self.add_message('\x194}%(spec)s \x19%(color)s}%(nick)s\x19%(info_col)s} joined the room' % {'nick':nick, 'color': color, 'spec':get_theme().CHAR_JOIN, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
+ return self.core.current_tab() is self
+
+ def activate(self, reason=None):
+ self.on = True
+ if reason:
+ self.add_message(txt=reason, typ=2)
+
+ def deactivate(self, reason=None):
+ self.on = False
+ if reason:
+ self.add_message(txt=reason, typ=2)
+
+ def matching_names(self):
+ return [(3, safeJID(self.get_name()).resource), (4, self.get_name())]
+
+
diff --git a/src/tabs/rostertab.py b/src/tabs/rostertab.py
new file mode 100644
index 00000000..2ab81dae
--- /dev/null
+++ b/src/tabs/rostertab.py
@@ -0,0 +1,991 @@
+from gettext import gettext as _
+
+import logging
+log = logging.getLogger(__name__)
+
+import curses
+import difflib
+import os
+from os import getenv, path
+
+from . import Tab
+
+import common
+import windows
+from common import safeJID
+from config import config
+from contact import Contact, Resource
+from decorators import refresh_wrapper
+from roster import RosterGroup, roster
+from theming import get_theme, dump_tuple
+
+class RosterInfoTab(Tab):
+ """
+ A tab, splitted in two, containing the roster and infos
+ """
+ plugin_commands = {}
+ plugin_keys = {}
+ def __init__(self):
+ Tab.__init__(self)
+ self.name = "Roster"
+ self.v_separator = windows.VerticalSeparator()
+ self.information_win = windows.TextWin()
+ self.core.information_buffer.add_window(self.information_win)
+ self.roster_win = windows.RosterWin()
+ self.contact_info_win = windows.ContactInfoWin()
+ self.default_help_message = windows.HelpText("Enter commands with “/”. “o”: toggle offline show")
+ self.input = self.default_help_message
+ self.state = 'normal'
+ self.key_func['^I'] = self.completion
+ self.key_func[' '] = self.on_space
+ self.key_func["/"] = self.on_slash
+ self.key_func["KEY_UP"] = self.move_cursor_up
+ self.key_func["KEY_DOWN"] = self.move_cursor_down
+ self.key_func["M-u"] = self.move_cursor_to_next_contact
+ self.key_func["M-y"] = self.move_cursor_to_prev_contact
+ self.key_func["M-U"] = self.move_cursor_to_next_group
+ self.key_func["M-Y"] = self.move_cursor_to_prev_group
+ self.key_func["M-[1;5B"] = self.move_cursor_to_next_group
+ self.key_func["M-[1;5A"] = self.move_cursor_to_prev_group
+ self.key_func["l"] = self.command_last_activity
+ self.key_func["o"] = self.toggle_offline_show
+ self.key_func["v"] = self.get_contact_version
+ self.key_func["i"] = self.show_contact_info
+ self.key_func["n"] = self.change_contact_name
+ self.key_func["s"] = self.start_search
+ self.key_func["S"] = self.start_search_slow
+ self.register_command('deny', self.command_deny,
+ usage=_('[jid]'),
+ desc=_('Deny your presence to the provided JID (or the selected contact in your roster), who is asking you to be in his/here roster.'),
+ shortdesc=_('Deny an user your presence.'),
+ completion=self.completion_deny)
+ self.register_command('accept', self.command_accept,
+ usage=_('[jid]'),
+ desc=_('Allow the provided JID (or the selected contact in your roster), to see your presence.'),
+ shortdesc=_('Allow an user your presence.'),
+ completion=self.completion_deny)
+ self.register_command('add', self.command_add,
+ usage=_('<jid>'),
+ desc=_('Add the specified JID to your roster, ask him to allow you to see his presence, and allow him to see your presence.'),
+ shortdesc=_('Add an user to your roster.'))
+ self.register_command('name', self.command_name,
+ usage=_('<jid> <name>'),
+ shortdesc=_('Set the given JID\'s name.'),
+ completion=self.completion_name)
+ self.register_command('groupadd', self.command_groupadd,
+ usage=_('<jid> <group>'),
+ desc=_('Add the given JID to the given group.'),
+ shortdesc=_('Add an user to a group'),
+ completion=self.completion_groupadd)
+ self.register_command('groupmove', self.command_groupmove,
+ usage=_('<jid> <old group> <new group>'),
+ desc=_('Move the given JID from the old group to the new group.'),
+ shortdesc=_('Move an user to another group.'),
+ completion=self.completion_groupmove)
+ self.register_command('groupremove', self.command_groupremove,
+ usage=_('<jid> <group>'),
+ desc=_('Remove the given JID from the given group.'),
+ shortdesc=_('Remove an user from a group.'),
+ completion=self.completion_groupremove)
+ self.register_command('remove', self.command_remove,
+ usage=_('[jid]'),
+ desc=_('Remove the specified JID from your roster. This wil unsubscribe you from its presence, cancel its subscription to yours, and remove the item from your roster.'),
+ shortdesc=_('Remove an user from your roster.'),
+ completion=self.completion_remove)
+ self.register_command('reconnect', self.command_reconnect,
+ desc=_('Disconnect from the remote server if you are currently connected and then connect to it again.'),
+ shortdesc=_('Disconnect and reconnect to the server.'))
+ self.register_command('disconnect', self.command_disconnect,
+ desc=_('Disconnect from the remote server.'),
+ shortdesc=_('Disconnect from the server.'))
+ self.register_command('export', self.command_export,
+ usage=_('[/path/to/file]'),
+ desc=_('Export your contacts into /path/to/file if specified, or $HOME/poezio_contacts if not.'),
+ shortdesc=_('Export your roster to a file.'),
+ completion=self.completion_file)
+ self.register_command('import', self.command_import,
+ usage=_('[/path/to/file]'),
+ desc=_('Import your contacts from /path/to/file if specified, or $HOME/poezio_contacts if not.'),
+ shortdesc=_('Import your roster from a file.'),
+ completion=self.completion_file)
+ self.register_command('clear', self.command_clear,
+ shortdesc=_('Clear the info buffer.'))
+ self.register_command('last_activity', self.command_last_activity,
+ usage=_('<jid>'),
+ desc=_('Informs you of the last activity of a JID.'),
+ shortdesc=_('Get the activity of someone.'),
+ completion=self.core.completion_last_activity)
+ self.register_command('password', self.command_password,
+ usage='<password>',
+ shortdesc=_('Change your password'))
+
+ self.resize()
+ self.update_commands()
+ self.update_keys()
+
+ def check_blocking(self, features):
+ if 'urn:xmpp:blocking' in features:
+ self.register_command('block', self.command_block,
+ usage=_('[jid]'),
+ shortdesc=_('Prevent a JID from talking to you.'),
+ completion=self.completion_block)
+ self.register_command('unblock', self.command_unblock,
+ usage=_('[jid]'),
+ shortdesc=_('Allow a JID to talk to you.'),
+ completion=self.completion_unblock)
+ self.register_command('list_blocks', self.command_list_blocks,
+ shortdesc=_('Show the blocked contacts.'))
+ self.core.xmpp.del_event_handler('session_start', self.check_blocking)
+ self.core.xmpp.add_event_handler('blocked_message', self.on_blocked_message)
+
+ def on_blocked_message(self, message):
+ """
+ When we try to send a message to a blocked contact
+ """
+ tab = self.core.get_conversation_by_jid(message['from'], False)
+ if not tab:
+ log.debug('Received message from nonexistent tab: %s', message['from'])
+ message = '\x19%(info_col)s}Cannot send message to %(jid)s: contact blocked' % {
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
+ 'jid': message['from'],
+ }
+ tab.add_message(message)
+
+ def command_block(self, arg):
+ """
+ /block [jid]
+ """
+ def callback(iq):
+ if iq['type'] == 'error':
+ return self.core.information('Could not block the contact.', 'Error')
+ elif iq['type'] == 'result':
+ return self.core.information('Contact blocked.', 'Info')
+
+ item = self.roster_win.selected_row
+ if arg:
+ jid = safeJID(arg)
+ elif isinstance(item, Contact):
+ jid = item.bare_jid
+ elif isinstance(item, Resource):
+ jid = item.jid.bare
+ self.core.xmpp.plugin['xep_0191'].block(jid, block=False, callback=callback)
+
+ def completion_block(self, the_input):
+ """
+ Completion for /block
+ """
+ if the_input.get_argument_position() == 1:
+ jids = roster.jids()
+ return the_input.new_completion(jids, 1, '', quotify=False)
+
+ def command_unblock(self, arg):
+ """
+ /unblock [jid]
+ """
+ def callback(iq):
+ if iq['type'] == 'error':
+ return self.core.information('Could not unblock the contact.', 'Error')
+ elif iq['type'] == 'result':
+ return self.core.information('Contact unblocked.', 'Info')
+
+ item = self.roster_win.selected_row
+ if arg:
+ jid = safeJID(arg)
+ elif isinstance(item, Contact):
+ jid = item.bare_jid
+ elif isinstance(item, Resource):
+ jid = item.jid.bare
+ self.core.xmpp.plugin['xep_0191'].unblock(jid, block=False, callback=callback)
+
+ def completion_unblock(self, the_input):
+ """
+ Completion for /unblock
+ """
+ if the_input.get_argument_position():
+ try:
+ iq = self.core.xmpp.plugin['xep_0191'].get_blocked(block=True)
+ except Exception as e:
+ iq = e.iq
+ finally:
+ if iq['type'] == 'error':
+ return
+ l = sorted(str(item) for item in iq['blocklist']['items'])
+ return the_input.new_completion(l, 1, quotify=False)
+
+ def command_list_blocks(self, arg=None):
+ """
+ /list_blocks
+ """
+ def callback(iq):
+ if iq['type'] == 'error':
+ return self.core.information('Could not retrieve the blocklist.', 'Error')
+ s = 'List of blocked JIDs:\n'
+ items = (str(item) for item in iq['blocklist']['items'])
+ jids = '\n'.join(items)
+ if jids:
+ s += jids
+ else:
+ s = 'No blocked JIDs.'
+ self.core.information(s, 'Info')
+
+ self.core.xmpp.plugin['xep_0191'].get_blocked(block=False, callback=callback)
+
+ def command_reconnect(self, args=None):
+ """
+ /reconnect
+ """
+ self.core.disconnect(reconnect=True)
+
+ def command_disconnect(self, args=None):
+ """
+ /disconnect
+ """
+ self.core.disconnect()
+
+ def command_last_activity(self, arg=None):
+ """
+ /activity [jid]
+ """
+ item = self.roster_win.selected_row
+ if arg:
+ jid = arg
+ elif isinstance(item, Contact):
+ jid = item.bare_jid
+ elif isinstance(item, Resource):
+ jid = item.jid.bare
+ else:
+ self.core.information('No JID selected.', 'Error')
+ return
+ self.core.command_last_activity(jid)
+
+ def resize(self):
+ if not self.visible:
+ return
+ self.need_resize = False
+ roster_width = self.width//2
+ info_width = self.width-roster_width-1
+ self.v_separator.resize(self.height-1 - Tab.tab_win_height(), 1, 0, roster_width)
+ self.information_win.resize(self.height-2-4, info_width, 0, roster_width+1, self.core.information_buffer)
+ self.roster_win.resize(self.height-1 - Tab.tab_win_height(), roster_width, 0, 0)
+ self.contact_info_win.resize(5 - Tab.tab_win_height(), info_width, self.height-2-4, roster_width+1)
+ self.input.resize(1, self.width, self.height-1, 0)
+
+ def completion(self):
+ # Check if we are entering a command (with the '/' key)
+ if isinstance(self.input, windows.Input) and\
+ not self.input.help_message:
+ self.complete_commands(self.input)
+
+ def completion_file(self, the_input):
+ """
+ Completion for /import and /export
+ """
+ text = the_input.get_text()
+ args = text.split()
+ n = len(args)
+ if n == 1:
+ home = os.getenv('HOME') or '/'
+ return the_input.auto_completion([home, '/tmp'], '')
+ else:
+ the_path = text[text.index(' ')+1:]
+ try:
+ names = os.listdir(the_path)
+ except:
+ names = []
+ end_list = []
+ for name in names:
+ value = os.path.join(the_path, name)
+ if not name.startswith('.'):
+ end_list.append(value)
+
+ return the_input.auto_completion(end_list, '')
+
+ def command_clear(self, arg=''):
+ """
+ /clear
+ """
+ self.core.information_buffer.messages = []
+ self.information_win.rebuild_everything(self.core.information_buffer)
+ self.core.information_win.rebuild_everything(self.core.information_buffer)
+ self.refresh()
+
+ def command_password(self, arg):
+ """
+ /password <password>
+ """
+ def callback(iq):
+ if iq['type'] == 'result':
+ self.core.information('Password updated', 'Account')
+ if config.get('password', ''):
+ config.silent_set('password', arg)
+ else:
+ self.core.information('Unable to change the password', 'Account')
+ self.core.xmpp.plugin['xep_0077'].change_password(arg, callback=callback)
+
+
+
+ def command_deny(self, arg):
+ """
+ /deny [jid]
+ Denies a JID from our roster
+ """
+ if not arg:
+ item = self.roster_win.selected_row
+ if isinstance(item, Contact):
+ jid = item.bare_jid
+ else:
+ self.core.information('No subscription to deny')
+ return
+ else:
+ jid = safeJID(arg).bare
+ if not jid in [jid for jid in roster.jids()]:
+ self.core.information('No subscription to deny')
+ return
+
+ contact = roster[jid]
+ if contact:
+ contact.unauthorize()
+
+ def command_add(self, args):
+ """
+ Add the specified JID to the roster, and set automatically
+ accept the reverse subscription
+ """
+ jid = safeJID(safeJID(args.strip()).bare)
+ if not jid:
+ self.core.information(_('No JID specified'), 'Error')
+ return
+ if jid in roster and roster[jid].subscription in ('to', 'both'):
+ return self.core.information('Already subscribed.', 'Roster')
+ roster.add(jid)
+ roster.modified()
+
+ def command_name(self, arg):
+ """
+ Set a name for the specified JID in your roster
+ """
+ def callback(iq):
+ if not iq:
+ self.core.information('The name could not be set.', 'Error')
+ log.debug('Error in /name:\n%s', iq)
+ args = common.shell_split(arg)
+ if not args:
+ return self.core.command_help('name')
+ jid = safeJID(args[0]).bare
+ name = args[1] if len(args) == 2 else ''
+
+ contact = roster[jid]
+ if contact is None:
+ self.core.information(_('No such JID in roster'), 'Error')
+ return
+
+ groups = set(contact.groups)
+ if 'none' in groups:
+ groups.remove('none')
+ subscription = contact.subscription
+ self.core.xmpp.update_roster(jid, name=name, groups=groups, subscription=subscription,
+ callback=callback, block=False)
+
+ def command_groupadd(self, args):
+ """
+ Add the specified JID to the specified group
+ """
+ args = common.shell_split(args)
+ if len(args) != 2:
+ return
+ jid = safeJID(args[0]).bare
+ group = args[1]
+
+ contact = roster[jid]
+ if contact is None:
+ self.core.information(_('No such JID in roster'), 'Error')
+ return
+
+ new_groups = set(contact.groups)
+ if group in new_groups:
+ self.core.information(_('JID already in group'), 'Error')
+ return
+
+ roster.modified()
+ new_groups.add(group)
+ try:
+ new_groups.remove('none')
+ except KeyError:
+ pass
+
+ name = contact.name
+ subscription = contact.subscription
+
+ def callback(iq):
+ if iq:
+ roster.update_contact_groups(jid)
+ else:
+ self.core.information('The group could not be set.', 'Error')
+ log.debug('Error in groupadd:\n%s', iq)
+
+ self.core.xmpp.update_roster(jid, name=name, groups=new_groups, subscription=subscription,
+ callback=callback, block=False)
+
+ def command_groupmove(self, arg):
+ """
+ Remove the specified JID from the first specified group and add it to the second one
+ """
+ args = common.shell_split(arg)
+ if len(args) != 3:
+ return self.core.command_help('groupmove')
+ jid = safeJID(args[0]).bare
+ group_from = args[1]
+ group_to = args[2]
+
+ contact = roster[jid]
+ if not contact:
+ self.core.information(_('No such JID in roster'), 'Error')
+ return
+
+ new_groups = set(contact.groups)
+ if 'none' in new_groups:
+ new_groups.remove('none')
+
+ if group_to == 'none' or group_from == 'none':
+ self.core.information(_('"none" is not a group.'), 'Error')
+ return
+
+ if group_from not in new_groups:
+ self.core.information(_('JID not in first group'), 'Error')
+ return
+
+ if group_to in new_groups:
+ self.core.information(_('JID already in second group'), 'Error')
+ return
+
+ if group_to == group_from:
+ self.core.information(_('The groups are the same.'), 'Error')
+ return
+
+ roster.modified()
+ new_groups.add(group_to)
+ if 'none' in new_groups:
+ new_groups.remove('none')
+
+ new_groups.remove(group_from)
+ name = contact.name
+ subscription = contact.subscription
+
+ def callback(iq):
+ if iq:
+ roster.update_contact_groups(contact)
+ else:
+ self.core.information('The group could not be set')
+ log.debug('Error in groupmove:\n%s', iq)
+
+ self.core.xmpp.update_roster(jid, name=name, groups=new_groups, subscription=subscription,
+ callback=callback, block=False)
+
+ def command_groupremove(self, args):
+ """
+ Remove the specified JID from the specified group
+ """
+ args = common.shell_split(args)
+ if len(args) != 2:
+ return
+ jid = safeJID(args[0]).bare
+ group = args[1]
+
+ contact = roster[jid]
+ if contact is None:
+ self.core.information(_('No such JID in roster'), 'Error')
+ return
+
+ new_groups = set(contact.groups)
+ try:
+ new_groups.remove('none')
+ except KeyError:
+ pass
+ if group not in new_groups:
+ self.core.information(_('JID not in group'), 'Error')
+ return
+
+ roster.modified()
+
+ new_groups.remove(group)
+ name = contact.name
+ subscription = contact.subscription
+
+ def callback(iq):
+ if iq:
+ roster.update_contact_groups(jid)
+ else:
+ self.core.information('The group could not be set')
+ log.debug('Error in groupremove:\n%s', iq)
+
+ self.core.xmpp.update_roster(jid, name=name, groups=new_groups, subscription=subscription,
+ callback=callback, block=False)
+
+ def command_remove(self, args):
+ """
+ Remove the specified JID from the roster. i.e.: unsubscribe
+ from its presence, and cancel its subscription to our.
+ """
+ if args.strip():
+ jid = safeJID(args.strip()).bare
+ else:
+ item = self.roster_win.selected_row
+ if isinstance(item, Contact):
+ jid = item.bare_jid
+ else:
+ self.core.information('No roster item to remove')
+ return
+ roster.remove(jid)
+ del roster[jid]
+
+ def command_import(self, arg):
+ """
+ Import the contacts
+ """
+ args = common.shell_split(arg)
+ if len(args):
+ if args[0].startswith('/'):
+ filepath = args[0]
+ else:
+ filepath = path.join(getenv('HOME'), args[0])
+ else:
+ filepath = path.join(getenv('HOME'), 'poezio_contacts')
+ if not path.isfile(filepath):
+ self.core.information('The file %s does not exist' % filepath, 'Error')
+ return
+ try:
+ handle = open(filepath, 'r', encoding='utf-8')
+ lines = handle.readlines()
+ handle.close()
+ except IOError:
+ self.core.information('Could not open %s' % filepath, 'Error')
+ log.error('Unable to correct a message', exc_info=True)
+ return
+ for jid in lines:
+ self.command_add(jid.lstrip('\n'))
+ self.core.information('Contacts imported from %s' % filepath, 'Info')
+
+ def command_export(self, arg):
+ """
+ Export the contacts
+ """
+ args = common.shell_split(arg)
+ if len(args):
+ if args[0].startswith('/'):
+ filepath = args[0]
+ else:
+ filepath = path.join(getenv('HOME'), args[0])
+ else:
+ filepath = path.join(getenv('HOME'), 'poezio_contacts')
+ if path.isfile(filepath):
+ self.core.information('The file already exists', 'Error')
+ return
+ elif not path.isdir(path.dirname(filepath)):
+ self.core.information('Parent directory not found', 'Error')
+ return
+ if roster.export(filepath):
+ self.core.information('Contacts exported to %s' % filepath, 'Info')
+ else:
+ self.core.information('Failed to export contacts to %s' % filepath, 'Info')
+
+ def completion_remove(self, the_input):
+ """
+ Completion for /remove
+ """
+ jids = [jid for jid in roster.jids()]
+ return the_input.auto_completion(jids, '', quotify=False)
+
+ def completion_name(self, the_input):
+ """Completion for /name"""
+ n = the_input.get_argument_position()
+ if n == 1:
+ jids = [jid for jid in roster.jids()]
+ return the_input.new_completion(jids, n, quotify=True)
+ return False
+
+ def completion_groupadd(self, the_input):
+ n = the_input.get_argument_position()
+ if n == 1:
+ jids = sorted(jid for jid in roster.jids())
+ return the_input.new_completion(jids, n, '', quotify=True)
+ elif n == 2:
+ groups = sorted(group for group in roster.groups if group != 'none')
+ return the_input.new_completion(groups, n, '', quotify=True)
+ return False
+
+ def completion_groupmove(self, the_input):
+ args = common.shell_split(the_input.text)
+ n = the_input.get_argument_position()
+ if n == 1:
+ jids = sorted(jid for jid in roster.jids())
+ return the_input.new_completion(jids, n, '', quotify=True)
+ elif n == 2:
+ contact = roster[args[1]]
+ if not contact:
+ return False
+ groups = list(contact.groups)
+ if 'none' in groups:
+ groups.remove('none')
+ return the_input.new_completion(groups, n, '', quotify=True)
+ elif n == 3:
+ groups = sorted(group for group in roster.groups)
+ return the_input.new_completion(groups, n, '', quotify=True)
+ return False
+
+ def completion_groupremove(self, the_input):
+ args = common.shell_split(the_input.text)
+ n = the_input.get_argument_position()
+ if n == 1:
+ jids = sorted(jid for jid in roster.jids())
+ return the_input.new_completion(jids, n, '', quotify=True)
+ elif n == 2:
+ contact = roster[args[1]]
+ if contact is None:
+ return False
+ groups = sorted(contact.groups)
+ try:
+ groups.remove('none')
+ except ValueError:
+ pass
+ return the_input.new_completion(groups, n, '', quotify=True)
+ return False
+
+ def completion_deny(self, the_input):
+ """
+ Complete the first argument from the list of the
+ contact with ask=='subscribe'
+ """
+ jids = sorted(str(contact.bare_jid) for contact in roster.contacts.values()
+ if contact.pending_in)
+ return the_input.new_completion(jids, 1, '', quotify=False)
+
+ def command_accept(self, arg):
+ """
+ Accept a JID from in roster. Authorize it AND subscribe to it
+ """
+ if not arg:
+ item = self.roster_win.selected_row
+ if isinstance(item, Contact):
+ jid = item.bare_jid
+ else:
+ self.core.information('No subscription to accept')
+ return
+ else:
+ jid = safeJID(arg).bare
+ nodepart = safeJID(jid).user
+ jid = safeJID(jid)
+ # crappy transports putting resources inside the node part
+ if '\\2f' in nodepart:
+ jid.user = nodepart.split('\\2f')[0]
+ contact = roster[jid]
+ if contact is None:
+ return
+ contact.pending_in = False
+ roster.modified()
+ self.core.xmpp.send_presence(pto=jid, ptype='subscribed')
+ self.core.xmpp.client_roster.send_last_presence()
+ if contact.subscription in ('from', 'none') and not contact.pending_out:
+ self.core.xmpp.send_presence(pto=jid, ptype='subscribe', pnick=self.core.own_nick)
+
+ def refresh(self):
+ if self.need_resize:
+ self.resize()
+ log.debug(' TAB Refresh: %s',self.__class__.__name__)
+ self.v_separator.refresh()
+ self.roster_win.refresh(roster)
+ self.contact_info_win.refresh(self.roster_win.get_selected_row())
+ self.information_win.refresh()
+ self.refresh_tab_win()
+ self.input.refresh()
+
+ def get_name(self):
+ return self.name
+
+ def on_input(self, key, raw):
+ if key == '^M':
+ selected_row = self.roster_win.get_selected_row()
+ res = self.input.do_command(key, raw=raw)
+ if res and not isinstance(self.input, windows.Input):
+ return True
+ elif res:
+ return False
+ if key == '^M':
+ self.core.on_roster_enter_key(selected_row)
+ return selected_row
+ elif not raw and key in self.key_func:
+ return self.key_func[key]()
+
+ @refresh_wrapper.conditional
+ def toggle_offline_show(self):
+ """
+ Show or hide offline contacts
+ """
+ option = 'roster_show_offline'
+ if config.get(option, 'false') == 'false':
+ success = config.silent_set(option, 'true')
+ else:
+ success = config.silent_set(option, 'false')
+ roster.modified()
+ if not success:
+ self.core.information(_('Unable to write in the config file'), 'Error')
+ return True
+
+ def on_slash(self):
+ """
+ '/' is pressed, we enter "input mode"
+ """
+ curses.curs_set(1)
+ self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command)
+ self.input.resize(1, self.width, self.height-1, 0)
+ self.input.do_command("/") # we add the slash
+
+ def reset_help_message(self, _=None):
+ self.input = self.default_help_message
+ if self.core.current_tab() is self:
+ curses.curs_set(0)
+ self.input.refresh()
+ self.core.doupdate()
+ return True
+
+ def execute_slash_command(self, txt):
+ if txt.startswith('/'):
+ self.input.key_enter()
+ self.execute_command(txt)
+ return self.reset_help_message()
+
+ def on_lose_focus(self):
+ self.state = 'normal'
+
+ def on_gain_focus(self):
+ self.state = 'current'
+ if isinstance(self.input, windows.HelpText):
+ curses.curs_set(0)
+ else:
+ curses.curs_set(1)
+
+ @refresh_wrapper.conditional
+ def move_cursor_down(self):
+ if isinstance(self.input, windows.Input) and not self.input.history_disabled:
+ return
+ return self.roster_win.move_cursor_down()
+
+ @refresh_wrapper.conditional
+ def move_cursor_up(self):
+ if isinstance(self.input, windows.Input) and not self.input.history_disabled:
+ return
+ return self.roster_win.move_cursor_up()
+
+ def move_cursor_to_prev_contact(self):
+ self.roster_win.move_cursor_up()
+ while not isinstance(self.roster_win.get_selected_row(), Contact):
+ if not self.roster_win.move_cursor_up():
+ break
+ self.roster_win.refresh(roster)
+
+ def move_cursor_to_next_contact(self):
+ self.roster_win.move_cursor_down()
+ while not isinstance(self.roster_win.get_selected_row(), Contact):
+ if not self.roster_win.move_cursor_down():
+ break
+ self.roster_win.refresh(roster)
+
+ def move_cursor_to_prev_group(self):
+ self.roster_win.move_cursor_up()
+ while not isinstance(self.roster_win.get_selected_row(), RosterGroup):
+ if not self.roster_win.move_cursor_up():
+ break
+ self.roster_win.refresh(roster)
+
+ def move_cursor_to_next_group(self):
+ self.roster_win.move_cursor_down()
+ while not isinstance(self.roster_win.get_selected_row(), RosterGroup):
+ if not self.roster_win.move_cursor_down():
+ break
+ self.roster_win.refresh(roster)
+
+ def on_scroll_down(self):
+ return self.roster_win.move_cursor_down(self.height // 2)
+
+ def on_scroll_up(self):
+ return self.roster_win.move_cursor_up(self.height // 2)
+
+ @refresh_wrapper.conditional
+ def on_space(self):
+ if isinstance(self.input, windows.Input):
+ return
+ selected_row = self.roster_win.get_selected_row()
+ if isinstance(selected_row, RosterGroup):
+ selected_row.toggle_folded()
+ roster.modified()
+ return True
+ elif isinstance(selected_row, Contact):
+ group = "none"
+ found_group = False
+ pos = self.roster_win.pos
+ while not found_group and pos >= 0:
+ row = self.roster_win.roster_cache[pos]
+ pos -= 1
+ if isinstance(row, RosterGroup):
+ found_group = True
+ group = row.name
+ selected_row.toggle_folded(group)
+ roster.modified()
+ return True
+ return False
+
+ def get_contact_version(self):
+ """
+ Show the versions of the resource(s) currently selected
+ """
+ selected_row = self.roster_win.get_selected_row()
+ if isinstance(selected_row, Contact):
+ for resource in selected_row.resources:
+ self.core.command_version(str(resource.jid))
+ elif isinstance(selected_row, Resource):
+ self.core.command_version(str(selected_row.jid))
+ else:
+ self.core.information('Nothing to get versions from', 'Info')
+
+ def show_contact_info(self):
+ """
+ Show the contact info (resource number, status, presence, etc)
+ when 'i' is pressed.
+ """
+ selected_row = self.roster_win.get_selected_row()
+ if isinstance(selected_row, Contact):
+ cont = selected_row
+ res = selected_row.get_highest_priority_resource()
+ acc = []
+ acc.append('Contact: %s (%s)' % (cont.bare_jid, res.presence if res else 'unavailable'))
+ if res:
+ acc.append('%s connected resource%s' % (len(cont), '' if len(cont) == 1 else 's'))
+ acc.append('Current status: %s' % res.status)
+ if cont.tune:
+ acc.append('Tune: %s' % common.format_tune_string(cont.tune))
+ if cont.mood:
+ acc.append('Mood: %s' % cont.mood)
+ if cont.activity:
+ acc.append('Activity: %s' % cont.activity)
+ if cont.gaming:
+ acc.append('Game: %s' % (common.format_gaming_string(cont.gaming)))
+ msg = '\n'.join(acc)
+ elif isinstance(selected_row, Resource):
+ res = selected_row
+ msg = 'Resource: %s (%s)\nCurrent status: %s\nPriority: %s' % (
+ res.jid,
+ res.presence,
+ res.status,
+ res.priority)
+ elif isinstance(selected_row, RosterGroup):
+ rg = selected_row
+ msg = 'Group: %s [%s/%s] contacts online' % (
+ rg.name,
+ rg.get_nb_connected_contacts(),
+ len(rg),)
+ else:
+ msg = None
+ if msg:
+ self.core.information(msg, 'Info')
+
+ def change_contact_name(self):
+ """
+ Auto-fill a /name command when 'n' is pressed
+ """
+ selected_row = self.roster_win.get_selected_row()
+ if isinstance(selected_row, Contact):
+ jid = selected_row.bare_jid
+ elif isinstance(selected_row, Resource):
+ jid = safeJID(selected_row.jid).bare
+ else:
+ return
+ self.on_slash()
+ self.input.text = '/name "%s" ' % jid
+ self.input.key_end()
+ self.input.refresh()
+
+ @refresh_wrapper.always
+ def start_search(self):
+ """
+ Start the search. The input should appear with a short instruction
+ in it.
+ """
+ curses.curs_set(1)
+ self.input = windows.CommandInput("[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter)
+ self.input.resize(1, self.width, self.height-1, 0)
+ self.input.disable_history()
+ roster.modified()
+ self.refresh()
+ return True
+
+ @refresh_wrapper.always
+ def start_search_slow(self):
+ curses.curs_set(1)
+ self.input = windows.CommandInput("[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter_slow)
+ self.input.resize(1, self.width, self.height-1, 0)
+ self.input.disable_history()
+ return True
+
+ def set_roster_filter_slow(self, txt):
+ roster.contact_filter = (jid_and_name_match_slow, txt)
+ roster.modified()
+ self.refresh()
+ return False
+
+ def set_roster_filter(self, txt):
+ roster.contact_filter = (jid_and_name_match, txt)
+ roster.modified()
+ self.refresh()
+ return False
+
+ @refresh_wrapper.always
+ def on_search_terminate(self, txt):
+ curses.curs_set(0)
+ roster.contact_filter = None
+ self.reset_help_message()
+ roster.modified()
+ return True
+
+ def on_close(self):
+ return
+
+def diffmatch(search, string):
+ """
+ Use difflib and a loop to check if search_pattern can
+ be 'almost' found INSIDE a string.
+ 'almost' being defined by difflib
+ """
+ if len(search) > len(string):
+ return False
+ l = len(search)
+ ratio = 0.7
+ for i in range(len(string) - l + 1):
+ if difflib.SequenceMatcher(None, search, string[i:i+l]).ratio() >= ratio:
+ return True
+ return False
+
+def jid_and_name_match(contact, txt):
+ """
+ Match jid with text precisely
+ """
+ if not txt:
+ return True
+ txt = txt.lower()
+ if txt in safeJID(contact.bare_jid).bare.lower():
+ return True
+ if txt in contact.name.lower():
+ return True
+ return False
+
+def jid_and_name_match_slow(contact, txt):
+ """
+ A function used to know if a contact in the roster should
+ be shown in the roster
+ """
+ if not txt:
+ return True # Everything matches when search is empty
+ user = safeJID(contact.bare_jid).bare
+ if diffmatch(txt, user):
+ return True
+ if contact.name and diffmatch(txt, contact.name):
+ return True
+ return False
diff --git a/src/tabs/xmltab.py b/src/tabs/xmltab.py
new file mode 100644
index 00000000..ed099405
--- /dev/null
+++ b/src/tabs/xmltab.py
@@ -0,0 +1,195 @@
+from gettext import gettext as _
+
+import logging
+log = logging.getLogger(__name__)
+
+import curses
+from sleekxmpp.xmlstream import matcher
+from sleekxmpp.xmlstream.handler import Callback
+
+from . import Tab
+
+import windows
+
+class XMLTab(Tab):
+ def __init__(self):
+ Tab.__init__(self)
+ self.state = 'normal'
+ self.text_win = windows.TextWin()
+ self.core.xml_buffer.add_window(self.text_win)
+ self.info_header = windows.XMLInfoWin()
+ self.default_help_message = windows.HelpText("/ to enter a command")
+ self.register_command('close', self.close,
+ shortdesc=_("Close this tab."))
+ self.register_command('clear', self.command_clear,
+ shortdesc=_('Clear the current buffer.'))
+ self.register_command('reset', self.command_reset,
+ shortdesc=_('Reset the stanza filter.'))
+ self.register_command('filter_id', self.command_filter_id,
+ usage='<id>',
+ desc=_('Show only the stanzas with the id <id>.'),
+ shortdesc=_('Filter by id.'))
+ self.register_command('filter_xpath', self.command_filter_xpath,
+ usage='<xpath>',
+ desc=_('Show only the stanzas matching the xpath <xpath>.'),
+ shortdesc=_('Filter by XPath.'))
+ self.register_command('filter_xmlmask', self.command_filter_xmlmask,
+ usage=_('<xml mask>'),
+ desc=_('Show only the stanzas matching the given xml mask.'),
+ shortdesc=_('Filter by xml mask.'))
+ self.input = self.default_help_message
+ self.key_func['^T'] = self.close
+ self.key_func['^I'] = self.completion
+ self.key_func["KEY_DOWN"] = self.on_scroll_down
+ self.key_func["KEY_UP"] = self.on_scroll_up
+ self.key_func["^K"] = self.on_freeze
+ self.key_func["/"] = self.on_slash
+ self.resize()
+ # Used to display the infobar
+ self.filter_type = ''
+ self.filter = ''
+
+ def on_freeze(self):
+ """
+ Freeze the display.
+ """
+ self.text_win.toggle_lock()
+ self.refresh()
+
+ def command_filter_xmlmask(self, arg):
+ """/filter_xmlmask <xml mask>"""
+ try:
+ handler = Callback('custom matcher', matcher.MatchXMLMask(arg),
+ self.core.incoming_stanza)
+ self.core.xmpp.remove_handler('custom matcher')
+ self.core.xmpp.register_handler(handler)
+ self.filter_type = "XML Mask Filter"
+ self.filter = arg
+ self.refresh()
+ except:
+ self.core.information('Invalid XML Mask', 'Error')
+ self.command_reset('')
+
+ def command_filter_id(self, arg):
+ """/filter_id <id>"""
+ self.core.xmpp.remove_handler('custom matcher')
+ handler = Callback('custom matcher', matcher.MatcherId(arg),
+ self.core.incoming_stanza)
+ self.core.xmpp.register_handler(handler)
+ self.filter_type = "Id Filter"
+ self.filter = arg
+ self.refresh()
+
+ def command_filter_xpath(self, arg):
+ """/filter_xpath <xpath>"""
+ try:
+ handler = Callback('custom matcher', matcher.MatchXPath(
+ arg.replace('%n', self.core.xmpp.default_ns)),
+ self.core.incoming_stanza)
+ self.core.xmpp.remove_handler('custom matcher')
+ self.core.xmpp.register_handler(handler)
+ self.filter_type = "XPath Filter"
+ self.filter = arg
+ self.refresh()
+ except:
+ self.core.information('Invalid XML Path', 'Error')
+ self.command_reset('')
+
+ def command_reset(self, arg):
+ """/reset"""
+ self.core.xmpp.remove_handler('custom matcher')
+ self.core.xmpp.register_handler(self.core.all_stanzas)
+ self.filter_type = ''
+ self.filter = ''
+ self.refresh()
+
+ def on_slash(self):
+ """
+ '/' is pressed, activate the input
+ """
+ curses.curs_set(1)
+ self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command)
+ self.input.resize(1, self.width, self.height-1, 0)
+ self.input.do_command("/") # we add the slash
+
+ def reset_help_message(self, _=None):
+ if self.core.current_tab() is self:
+ curses.curs_set(0)
+ self.input = self.default_help_message
+ self.input.refresh()
+ self.core.doupdate()
+ return True
+
+ def on_scroll_up(self):
+ return self.text_win.scroll_up(self.text_win.height-1)
+
+ def on_scroll_down(self):
+ return self.text_win.scroll_down(self.text_win.height-1)
+
+ def command_clear(self, args):
+ """
+ /clear
+ """
+ self.core.xml_buffer.messages = []
+ self.text_win.rebuild_everything(self.core.xml_buffer)
+ self.refresh()
+ self.core.doupdate()
+
+ def execute_slash_command(self, txt):
+ if txt.startswith('/'):
+ self.input.key_enter()
+ self.execute_command(txt)
+ return self.reset_help_message()
+
+ def completion(self):
+ if isinstance(self.input, windows.Input):
+ self.complete_commands(self.input)
+
+ def on_input(self, key, raw):
+ res = self.input.do_command(key, raw=raw)
+ if res:
+ return True
+ if not raw and key in self.key_func:
+ return self.key_func[key]()
+
+ def close(self, arg=None):
+ self.core.close_tab()
+
+ def resize(self):
+ if self.core.information_win_size >= self.height-3 or not self.visible:
+ return
+ self.need_resize = False
+ min = 1 if self.left_tab_win else 2
+ self.text_win.resize(self.height-self.core.information_win_size - Tab.tab_win_height() - 2, self.width, 0, 0)
+ self.text_win.rebuild_everything(self.core.xml_buffer)
+ self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
+ self.input.resize(1, self.width, self.height-1, 0)
+
+ def refresh(self):
+ if self.need_resize:
+ self.resize()
+ log.debug(' TAB Refresh: %s',self.__class__.__name__)
+ self.text_win.refresh()
+ self.info_header.refresh(self.filter_type, self.filter, self.text_win)
+ self.refresh_tab_win()
+ self.info_win.refresh()
+ self.input.refresh()
+
+ def on_lose_focus(self):
+ self.state = 'normal'
+
+ def on_gain_focus(self):
+ self.state = 'current'
+ curses.curs_set(0)
+
+ def on_close(self):
+ self.command_clear('')
+ self.core.xml_tab = False
+
+ def on_info_win_size_changed(self):
+ if self.core.information_win_size >= self.height-3:
+ return
+ self.text_win.resize(self.height-2-self.core.information_win_size - Tab.tab_win_height(), self.width, 0, 0)
+ self.info_header.resize(1, self.width, self.height-2-self.core.information_win_size - Tab.tab_win_height(), 0)
+
+