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=_(''), desc=_('Ignore a specified nickname.'), shortdesc=_('Ignore someone'), completion=self.completion_ignore) self.register_command('unignore', self.command_unignore, usage=_(''), desc=_('Remove the specified nickname from the ignore list.'), shortdesc=_('Unignore someone.'), completion=self.completion_unignore) self.register_command('kick', self.command_kick, usage=_(' [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=_(' [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=_(' [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=_(' '), 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=_(''), desc=_('Change the subject of the room.'), shortdesc=_('Change the subject.'), completion=self.completion_topic) self.register_command('query', self.command_query, usage=_(' [message]'), desc=_('Query: Open a private conversation with . 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=_(''), 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=_(''), 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=_(''), 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=_(' [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 [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 """ 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 """ 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 """ 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 [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 [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 [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 [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 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 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 """ 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 """ 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())]