core.py 50.4 KB
Newer Older
1
# Copyright 2010 Le Coz Florent <louiz@louiz.org>
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
#
# This file is part of Poezio.
#
# Poezio is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# Poezio is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Poezio.  If not, see <http://www.gnu.org/licenses/>.

17 18
from gettext import (bindtextdomain, textdomain, bind_textdomain_codeset,
                     gettext as _)
19
from os.path import isfile
20

21 22
from time import sleep

23
import os
24
import re
25 26
import sys
import shlex
27
import curses
28
import threading
29 30
import webbrowser

31
from datetime import datetime
32 33

import common
34
import theme
35 36
import logging

37 38
from sleekxmpp.xmlstream.stanzabase import JID

39
log = logging.getLogger(__name__)
40

41
import multiuserchat as muc
42
from connection import connection
43
from config import config
44
from tab import MucTab, InfoTab, PrivateTab, RosterInfoTab, ConversationTab
45
from logger import logger
46 47
from user import User
from room import Room
48
from roster import Roster, RosterGroup, roster
49
from contact import Contact, Resource
50
from message import Message
51
from text_buffer import TextBuffer
52
from keyboard import read_char
53
from common import jid_get_domain, is_jid
54

55 56
# http://xmpp.org/extensions/xep-0045.html#errorstatus
ERROR_AND_STATUS_CODES = {
57 58 59 60 61 62 63 64
    '401': _('A password is required'),
    '403': _('You are banned from the room'),
    '404': _('The room does\'nt exist'),
    '405': _('Your are not allowed to create a new room'),
    '406': _('A reserved nick must be used'),
    '407': _('You are not in the member list'),
    '409': _('This nickname is already in use or has been reserved'),
    '503': _('The maximum number of users has been reached'),
65 66
    }

67 68 69 70 71 72 73
SHOW_NAME = {
    'dnd': _('busy'),
    'away': _('away'),
    'xa': _('not available'),
    'chat': _('chatty'),
    '': _('available')
    }
74 75 76

resize_lock = threading.Lock()

77
class Core(object):
78
    """
79
    User interface using ncurses
80
    """
81
    def __init__(self, xmpp):
82
        self.running = True
83 84 85
        self.stdscr = curses.initscr()
        self.init_curses(self.stdscr)
        self.xmpp = xmpp
86 87
        default_tab = InfoTab(self, "Info") if self.xmpp.anon\
            else RosterInfoTab(self)
88
        default_tab.on_gain_focus()
89
        self.tabs = [default_tab]
90 91 92 93 94
        # a unique buffer used to store global informations
        # that are displayed in almost all tabs, in an
        # information window.
        self.information_buffer = TextBuffer()
        self.information_win_size = 2 # Todo, get this from config
95
        self.resize_timer = None
96
        self.previous_tab_nb = 0
97
        self.own_nick = config.get('own_nick', self.xmpp.boundjid.bare)
98 99 100 101 102 103 104
        # global commands, available from all tabs
        # a command is tuple of the form:
        # (the function executing the command. Takes a string as argument,
        #  a string representing the help message,
        #  a completion function, taking a Input as argument. Can be None)
        #  The completion function should return True if a completion was
        #  made ; False otherwise
105
        self.commands = {
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
            'help': (self.command_help, '\_o< KOIN KOIN KOIN', None),
            'join': (self.command_join, _("Usage: /join [room_name][@server][/nick] [password]\nJoin: Join the specified room. You can specify a nickname after a slash (/). If no nickname is specified, you will use the default_nick in the configuration file. You can omit the room name: you will then join the room you\'re looking at (useful if you were kicked). You can also provide a room_name without specifying a server, the server of the room you're currently in will be used. You can also provide a password to join the room.\nExamples:\n/join room@server.tld\n/join room@server.tld/John\n/join room2\n/join /me_again\n/join\n/join room@server.tld/my_nick password\n/join / password"), self.completion_join),
            'exit': (self.command_quit, _("Usage: /exit\nExit: Just disconnect from the server and exit poezio."), None),
            'next': (self.rotate_rooms_right, _("Usage: /next\nNext: Go to the next room."), None),
            'n': (self.rotate_rooms_right, _("Usage: /n\nN: Go to the next room."), None),
            'prev': (self.rotate_rooms_left, _("Usage: /prev\nPrev: Go to the previous room."), None),
            'p': (self.rotate_rooms_left, _("Usage: /p\nP: Go to the previous room."), None),
            'win': (self.command_win, _("Usage: /win <number>\nWin: Go to the specified room."), None),
            'w': (self.command_win, _("Usage: /w <number>\nW: Go to the specified room."), None),
            'show': (self.command_show, _("Usage: /show <availability> [status]\nShow: Change your availability and (optionaly) your status. The <availability> argument is one of \"avail, available, ok, here, chat, away, afk, dnd, busy, xa\" and the optional [status] argument will be your status message"), None),
            'away': (self.command_away, _("Usage: /away [message]\nAway: Sets your availability to away and (optional) sets your status message. This is equivalent to '/show away [message]'"), None),
            'busy': (self.command_busy, _("Usage: /busy [message]\nBusy: Sets your availability to busy and (optional) sets your status message. This is equivalent to '/show busy [message]'"), None),
            'avail': (self.command_avail, _("Usage: /avail [message]\nAvail: Sets your availability to available and (optional) sets your status message. This is equivalent to '/show available [message]'"), None),
            'available': (self.command_avail, _("Usage: /available [message]\nAvailable: Sets your availability to available and (optional) sets your status message. This is equivalent to '/show available [message]'"), None),
           'bookmark': (self.command_bookmark, _("Usage: /bookmark [roomname][/nick]\nBookmark: Bookmark the specified room (you will then auto-join it on each poezio start). This commands uses the same syntaxe as /join. Type /help join for syntaxe examples. Note that when typing \"/bookmark\" on its own, the room will be bookmarked with the nickname you\'re currently using in this room (instead of default_nick)"), None),
            'set': (self.command_set, _("Usage: /set <option> [value]\nSet: Sets the value to the option in your configuration file. You can, for example, change your default nickname by doing `/set default_nick toto` or your resource with `/set resource blabla`. You can also set an empty value (nothing) by providing no [value] after <option>."), None),
            'link': (self.command_link, _("Usage: /link [option] [number]\nLink: Interact with a link in the conversation. Available options are 'open', 'copy'. Open just opens the link in the browser if it's http://, Copy just copy the link in the clipboard. An optional number can be provided, it indicates which link to interact with."), None),
            'whois': (self.command_whois, _('Usage: /whois <nickname>\nWhois: Request many informations about the user.'), None),
            'theme': (self.command_theme, _('Usage: /theme\nTheme: Reload the theme defined in the config file.'), None),
125 126
            }

127
        self.key_func = {
128 129
            "KEY_PPAGE": self.scroll_page_up,
            "KEY_NPAGE": self.scroll_page_down,
130
            "KEY_F(5)": self.rotate_rooms_left,
131
            "^P": self.rotate_rooms_left,
132
            'kLFT3': self.rotate_rooms_left,
133
            "KEY_F(6)": self.rotate_rooms_right,
134 135
            "^N": self.rotate_rooms_right,
            'kRIT3': self.rotate_rooms_right,
136 137
            "KEY_F(7)": self.shrink_information_win,
            "KEY_F(8)": self.grow_information_win,
138
            "KEY_RESIZE": self.call_for_resize,
139
            'M-e': self.go_to_important_room,
140
            'M-r': self.go_to_roster,
141 142
            'M-z': self.go_to_previous_tab,
            'M-v': self.move_separator,
143 144
            }

145 146 147 148
        # Add handlers
        self.xmpp.add_event_handler("session_start", self.on_connected)
        self.xmpp.add_event_handler("groupchat_presence", self.on_groupchat_presence)
        self.xmpp.add_event_handler("groupchat_message", self.on_groupchat_message)
149
        self.xmpp.add_event_handler("groupchat_subject", self.on_groupchat_subject)
150
        self.xmpp.add_event_handler("message", self.on_message)
151 152
        self.xmpp.add_event_handler("got_online" , self.on_got_online)
        self.xmpp.add_event_handler("got_offline" , self.on_got_offline)
153
        self.xmpp.add_event_handler("roster_update", self.on_roster_update)
154
        self.xmpp.add_event_handler("changed_status", self.on_presence)
155 156 157 158 159 160 161 162

    def grow_information_win(self):
        """
        """
        if self.information_win_size == 14:
            return
        self.information_win_size += 1
        for tab in self.tabs:
163
            tab.on_info_win_size_changed()
164 165 166 167 168 169 170 171 172
        self.refresh_window()

    def shrink_information_win(self):
        """
        """
        if self.information_win_size == 0:
            return
        self.information_win_size -= 1
        for tab in self.tabs:
173
            tab.on_info_win_size_changed()
174
        self.refresh_window()
175

176 177
    def on_got_offline(self, presence):
        jid = presence['from']
178
        contact = roster.get_contact_by_jid(jid.bare)
179 180
        if not contact:
            return
181 182 183 184 185 186
        resource = contact.get_resource_by_fulljid(jid.full)
        assert resource
        self.information('%s is offline' % (resource.get_jid()), "Roster")
        contact.remove_resource(resource)
        if isinstance(self.current_tab(), RosterInfoTab):
            self.refresh_window()
187 188 189

    def on_got_online(self, presence):
        jid = presence['from']
190
        contact = roster.get_contact_by_jid(jid.bare)
191
        if not contact:
192
            # Todo, handle presence comming from contacts not in roster
193
            return
194 195 196
        resource = contact.get_resource_by_fulljid(jid.full)
        assert not resource
        resource = Resource(jid.full)
197
        status = presence['type']
198
        status_message = presence['status']
199
        priority = presence.getPriority() or 0
200
        resource.set_status(status_message)
201 202 203 204
        resource.set_presence(status)
        resource.set_priority(priority)
        contact.add_resource(resource)
        self.information("%s is online (%s)" % (resource.get_jid().full, status), "Roster")
205

206 207 208 209 210
    def on_connected(self, event):
        """
        Called when we are connected and authenticated
        """
        self.information(_("Welcome on Poezio \o/!"))
211
        self.information(_("Your JID is %s") % self.xmpp.boundjid.full)
212

213 214 215 216
        if not self.xmpp.anon:
            # request the roster
            self.xmpp.getRoster()
            # send initial presence
217
            self.xmpp.makePresence().send()
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
        rooms = config.get('rooms', '')
        if rooms == '' or not isinstance(rooms, str):
            return
        rooms = rooms.split(':')
        for room in rooms:
            args = room.split('/')
            if args[0] == '':
                return
            roomname = args[0]
            if len(args) == 2:
                nick = args[1]
            else:
                default = os.environ.get('USER') if os.environ.get('USER') else 'poezio'
                nick = config.get('default_nick', '')
                if nick == '':
                    nick = default
234
            self.open_new_room(roomname, nick, False)
235
            muc.join_groupchat(self.xmpp, roomname, nick)
236
        # if not self.xmpp.anon:
237 238 239 240 241 242 243 244 245 246 247 248 249
        # Todo: SEND VCARD
        return
        if config.get('jid', '') == '': # Don't send the vcard if we're not anonymous
            self.vcard_sender.start()   # because the user ALREADY has one on the server

    def on_groupchat_presence(self, presence):
        """
        Triggered whenever a presence stanza is received from a user in a multi-user chat room.
        Display the presence on the room window and update the
        presence information of the concerned user
        """
        from_nick = presence['from'].resource
        from_room = presence['from'].bare
250
        room = self.get_room_by_name(from_room)
251 252 253 254 255
        code = presence.find('{jabber:client}status')
        status_codes = set([s.attrib['code'] for s in presence.findall('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}status')])
        # Check if it's not an error presence.
        if presence['type'] == 'error':
            return self.room_error(presence, from_room)
256
        if not room:
257
            return
258 259 260 261 262 263 264 265 266 267 268 269
        msg = None
        affiliation = presence['muc']['affiliation']
        show = presence['show']
        status = presence['status']
        role = presence['muc']['role']
        jid = presence['muc']['jid']
        typ = presence['type']
        if not room.joined:     # user in the room BEFORE us.
            # ignore redondant presence message, see bug #1509
            if from_nick not in [user.nick for user in room.users]:
                new_user = User(from_nick, affiliation, show, status, role)
                room.users.append(new_user)
270
                if from_nick == room.own_nick:
271 272
                    room.joined = True
                    new_user.color = theme.COLOR_OWN_NICK
273
                    self.add_message_to_text_buffer(room, _("Your nickname is %s") % (from_nick))
274
                    if '170' in status_codes:
275
                        self.add_message_to_text_buffer(room, 'Warning: this room is publicly logged')
276
        else:
277 278 279 280 281 282 283 284
            change_nick = '303' in status_codes
            kick = '307' in status_codes and typ == 'unavailable'
            user = room.get_user_by_name(from_nick)
            # New user
            if not user:
                self.on_user_join(room, from_nick, affiliation, show, status, role, jid)
            # nick change
            elif change_nick:
285
                self.on_user_nick_change(room, presence, user, from_nick, from_room)
286 287
            # kick
            elif kick:
288
                self.on_user_kicked(room, presence, user, from_nick)
289 290
            # user quit
            elif typ == 'unavailable':
291
                self.on_user_leave_groupchat(room, user, jid, status, from_nick, from_room)
292 293
            # status change
            else:
294
                self.on_user_change_status(room, user, from_nick, from_room, affiliation, role, show, status)
295
        self.refresh_window()
296
        self.doupdate()
297

298 299 300 301 302 303 304 305 306
    def on_user_join(self, room, from_nick, affiliation, show, status, role, jid):
        """
        When a new user joins a groupchat
        """
        room.users.append(User(from_nick, affiliation,
                               show, status, role))
        hide_exit_join = config.get('hide_exit_join', -1)
        if hide_exit_join != 0:
            if not jid.full:
307
                self.add_message_to_text_buffer(room, _('%(spec)s "[%(nick)s]" joined the room') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_JOIN.replace('"', '\\"')}, colorized=True)
308
            else:
309
                self.add_message_to_text_buffer(room, _('%(spec)s "[%(nick)s]" "(%(jid)s)" joined the room') % {'spec':theme.CHAR_JOIN.replace('"', '\\"'), 'nick':from_nick.replace('"', '\\"'), 'jid':jid.full}, colorized=True)
310

311 312 313 314 315
    def on_user_nick_change(self, room, presence, user, from_nick, from_room):
        new_nick = presence.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}item').attrib['nick']
        if user.nick == room.own_nick:
            room.own_nick = new_nick
            # also change our nick in all private discussion of this room
316
            for _tab in self.tabs:
317 318
                if isinstance(_tab, PrivateTab) and _tab.get_name().split('/', 1)[0] == room.name:
                    _tab.get_room().own_nick = new_nick
319
        user.change_nick(new_nick)
320
        self.add_message_to_text_buffer(room, _('"[%(old)s]" is now known as "[%(new)s]"') % {'old':from_nick.replace('"', '\\"'), 'new':new_nick.replace('"', '\\"')}, colorized=True)
321 322 323
        # rename the private tabs if needed
        private_room = self.get_room_by_name('%s/%s' % (from_room, from_nick))
        if private_room:
324
            self.add_message_to_text_buffer(private_room, _('"[%(old_nick)s]" is now known as "[%(new_nick)s]"') % {'old_nick':from_nick.replace('"', '\\"'), 'new_nick':new_nick.replace('"', '\\"')}, colorized=True)
325
            new_jid = private_room.name.split('/', 1)[0]+'/'+new_nick
326
            private_room.name = new_jid
327

328 329 330 331 332 333 334
    def on_user_kicked(self, room, presence, user, from_nick):
        """
        When someone is kicked
        """
        room.users.remove(user)
        by = presence.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}item/{http://jabber.org/protocol/muc#user}actor')
        reason = presence.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}item/{http://jabber.org/protocol/muc#user}reason')
335
        by = by.attrib['jid'] if by is not None else None
336
        reason = reason.text if reason else ''
337 338 339
        if from_nick == room.own_nick: # we are kicked
            room.disconnect()
            if by:
340
                kick_msg = _('%(spec)s [You] have been kicked by "[%(by)s]"') % {'spec': theme.CHAR_KICK.replace('"', '\\"'), 'by':by}
341
            else:
342
                kick_msg = _('%(spec)s [You] have been kicked.') % {'spec':theme.CHAR_KICK.replace('"', '\\"')}
343 344 345 346 347
            # try to auto-rejoin
            if config.get('autorejoin', 'false') == 'true':
                muc.join_groupchat(self.xmpp, room.name, room.own_nick)
        else:
            if by:
348
                kick_msg = _('%(spec)s "[%(nick)s]" has been kicked by "[%(by)s]"') % {'spec':theme.CHAR_KICK.replace('"', '\\"'), 'nick':from_nick.replace('"', '\\"'), 'by':by.replace('"', '\\"')}
349
            else:
350
                kick_msg = _('%(spec)s "[%(nick)s]" has been kicked') % {'spec':theme.CHAR_KICK, 'nick':from_nick.replace('"', '\\"')}
351 352
        if reason:
            kick_msg += _(' Reason: %(reason)s') % {'reason': reason}
353
        self.add_message_to_text_buffer(room, kick_msg, colorized=True)
354

355 356 357 358 359
    def on_user_leave_groupchat(self, room, user, jid, status, from_nick, from_room):
        """
        When an user leaves a groupchat
        """
        room.users.remove(user)
360 361 362
        if room.own_nick == user.nick:
            # We are now out of the room. Happens with some buggy (? not sure) servers
            room.disconnect()
363 364 365
        hide_exit_join = config.get('hide_exit_join', -1) if config.get('hide_exit_join', -1) >= -1 else -1
        if hide_exit_join == -1 or user.has_talked_since(hide_exit_join):
            if not jid.full:
366
                leave_msg = _('%(spec)s "[%(nick)s]" has left the room') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_QUIT.replace('"', '\\"')}
367
            else:
368
                leave_msg = _('%(spec)s "[%(nick)s]" "(%(jid)s)" has left the room') % {'spec':theme.CHAR_QUIT.replace('"', '\\"'), 'nick':from_nick.replace('"', '\\"'), 'jid':jid.full.replace('"', '\\"')}
369 370
            if status:
                leave_msg += ' (%s)' % status
371
            self.add_message_to_text_buffer(room, leave_msg, colorized=True)
372 373 374
        private_room = self.get_room_by_name('%s/%s' % (from_room, from_nick))
        if private_room:
            if not status:
375
                self.add_message_to_text_buffer(private_room, _('%(spec)s "[%(nick)s]" has left the room') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_QUIT.replace('"', '\\"')}, colorized=True)
376
            else:
377
                self.add_message_to_text_buffer(private_room, _('%(spec)s "[%(nick)s]" has left the room "(%(status)s)"') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_QUIT, 'status': status.replace('"', '\\"')}, colorized=True)
378

379 380 381 382 383
    def on_user_change_status(self, room, user, from_nick, from_room, affiliation, role, show, status):
        """
        When an user changes her status
        """
        # build the message
384 385
        display_message = False # flag to know if something significant enough
        # to be displayed has changed
386
        msg = _('"%s" changed: ')% from_nick.replace('"', '\\"')
387
        if affiliation != user.affiliation:
388
            msg += _('affiliation: %s, ') % affiliation
389
            display_message = True
390
        if role != user.role:
391
            msg += _('role: %s, ') % role
392
            display_message = True
393
        if show != user.show and show in list(SHOW_NAME.keys()):
394
            msg += _('show: %s, ') % SHOW_NAME[show]
395 396
            display_message = True
        if status and status != user.status:
397
            msg += _('status: %s, ') % status
398 399 400
            display_message = True
        if not display_message:
            return
401
        msg = msg[:-2] # remove the last ", "
402 403 404 405 406 407 408 409 410 411
        hide_status_change = config.get('hide_status_change', -1) if config.get('hide_status_change', -1) >= -1 else -1
        if (hide_status_change == -1 or \
                user.has_talked_since(hide_status_change) or\
                user.nick == room.own_nick)\
                and\
                (affiliation != user.affiliation or\
                    role != user.role or\
                    show != user.show or\
                    status != user.status):
            # display the message in the room
412
            self.add_message_to_text_buffer(room, msg, colorized=True)
413 414
        private_room = self.get_room_by_name('%s/%s' % (from_room, from_nick))
        if private_room: # display the message in private
415
            self.add_message_to_text_buffer(private_room, msg, colorized=True)
416 417 418
        # finally, effectively change the user status
        user.update(affiliation, show, status, role)

419 420 421 422 423 424
    def on_message(self, message):
        """
        When receiving private message from a muc OR a normal message
        (from one of our contacts)
        """
        if message['type'] == 'groupchat':
425
            return
426 427
        # Differentiate both type of messages, and call the appropriate handler.
        jid_from = message['from']
428
        for tab in self.tabs:
429
            if tab.get_name() == jid_from.full:
430
                if message['type'] == 'error':
431
                    return self.room_error(message, tab.get_room().name)
432 433
                else:
                    return self.on_groupchat_private_message(message)
434 435 436 437 438 439 440
        return self.on_normal_message(message)

    def on_groupchat_private_message(self, message):
        """
        We received a Private Message (from someone in a Muc)
        """
        jid = message['from']
441
        nick_from = jid.resource
442
        room_from = jid.bare
443 444
        room = self.get_room_by_name(jid.full) # get the tab with the private conversation
        if not room: # It's the first message we receive: create the tab
445
            room = self.open_private_window(room_from, nick_from, False)
446 447 448
            if not room:
                return
        body = message['body']
449 450 451
        room.add_message(body, time=None, nickname=nick_from,
                         colorized=False,
                         forced_user=self.get_room_by_name(room_from).get_user_by_name(nick_from))
452
        self.refresh_window()
453
        self.doupdate()
454

455 456 457 458 459
    def focus_tab_named(self, tab_name):
        for tab in self.tabs:
            if tab.get_name() == tab_name:
                self.command_win('%s' % (tab.nb,))

460 461 462 463
    def on_normal_message(self, message):
        """
        When receiving "normal" messages (from someone in our roster)
        """
464
        jid = message['from']
465 466 467
        body = message['body']
        if not body:
            return
468 469 470 471 472 473 474 475 476 477 478 479
        # We first check if we have a conversation opened with this precise resource
        conversation = self.get_tab_by_name(jid.full)
        if not conversation:
            # If not, we search for a conversation with the bare jid
            conversation = self.get_tab_by_name(jid.bare)
            if not conversation:
                # We create the conversation with the bare Jid if nothing was found
                conversation = self.open_conversation_window(jid.bare, False)
            # room = self.open_conversation_window(jid, False)
        self.add_message_to_text_buffer(conversation.get_room(), body, None, jid.full)
        if self.current_tab() is not conversation:
            conversation.set_color_state(theme.COLOR_TAB_PRIVATE)
480
        self.refresh_window()
481 482 483 484 485
        return

    def on_presence(self, presence):
        """
        """
486
        jid = presence['from']
487
        contact = roster.get_contact_by_jid(jid.bare)
488 489 490 491 492 493
        if not contact:
            return
        resource = contact.get_resource_by_fulljid(jid.full)
        if not resource:
            return
        status = presence['type']
494
        status_message = presence['status']
495 496 497
        priority = presence.getPriority() or 0
        resource.set_presence(status)
        resource.set_priority(priority)
498
        resource.set_status(status_message)
499 500
        if isinstance(self.current_tab(), RosterInfoTab):
            self.refresh_window()
501

502 503 504 505 506
    def on_roster_update(self, iq):
        """
        A subscription changed, or we received a roster item
        after a roster request, etc
        """
507 508
        for item in iq.findall('{jabber:iq:roster}query/{jabber:iq:roster}item'):
            jid = item.attrib['jid']
509
            contact = roster.get_contact_by_jid(jid)
510 511
            if not contact:
                contact = Contact(jid)
512
                roster.add_contact(contact, jid)
513 514 515 516
            if 'ask' in item.attrib:
                contact.set_ask(item.attrib['ask'])
            else:
                contact.set_ask(None)
517 518 519 520
            if 'name' in item.attrib:
                contact.set_name(item.attrib['name'])
            else:
                contact.set_name(None)
521 522 523
            if item.attrib['subscription']:
                contact.set_subscription(item.attrib['subscription'])
            groups = item.findall('{jabber:iq:roster}group')
524
            roster.edit_groups_of_contact(contact, [group.text for group in groups])
525 526
        if isinstance(self.current_tab(), RosterInfoTab):
            self.refresh_window()
527

528 529 530 531 532 533 534
    def call_for_resize(self):
        """
        Starts a very short timer. If no other terminal resize
        occured in this delay then poezio is REALLY resize.
        This is to avoid multiple unnecessary software resizes (this
        can be heavy on resource on slow computers or networks)
        """
535 536 537 538 539 540 541 542 543
        with resize_lock:
            if self.resize_timer:
                # a recent terminal resize occured.
                # Cancel the programmed software resize
                self.resize_timer.cancel()
            # add the new timer
            self.resize_timer = threading.Timer(0.1, self.resize_window)
            self.resize_timer.start()
        # self.resize_window()
544

545 546 547 548
    def resize_window(self):
        """
        Resize the whole screen
        """
549
        with resize_lock:
550
           # self.resize_timer = None
551
            for tab in self.tabs:
552
                tab.resize()
553
            self.refresh_window()
554

555
    def main_loop(self):
556 557 558
        """
        main loop waiting for the user to press a key
        """
559
        self.refresh_window()
560
        while self.running:
561
            self.doupdate()
562
            char=read_char(self.stdscr)
563
            # search for keyboard shortcut
564
            if char in list(self.key_func.keys()):
565
                self.key_func[char]()
566
            else:
567
                self.do_command(char)
568

569
    def current_tab(self):
570 571 572
        """
        returns the current room, the one we are viewing
        """
573
        return self.tabs[0]
574

575 576 577 578 579 580 581 582 583 584
    def get_conversation_by_jid(self, jid):
        """
        Return the room of the ConversationTab with the given jid
        """
        for tab in self.tabs:
            if isinstance(tab, ConversationTab):
                if tab.get_name() == jid:
                    return tab.get_room()
        return None

585 586 587 588 589 590 591 592 593
    def get_tab_by_name(self, name):
        """
        Get the tab with the given name.
        """
        for tab in self.tabs:
            if tab.get_name() == name:
                return tab
        return None

594
    def get_room_by_name(self, name):
595 596 597
        """
        returns the room that has this name
        """
598 599 600 601
        for tab in self.tabs:
            if (isinstance(tab, MucTab) or
                isinstance(tab, PrivateTab)) and tab.get_name() == name:
                return tab.get_room()
602
        return None
603

604
    def init_curses(self, stdscr):
605 606 607
        """
        ncurses initialization
        """
608
        curses.curs_set(1)
609
        curses.noecho()
610 611
        # curses.raw()
        theme.init_colors()
612
        stdscr.keypad(True)
613

614
    def reset_curses(self):
615 616 617 618 619
        """
        Reset terminal capabilities to what they were before ncurses
        init
        """
        curses.echo()
620
        curses.nocbreak()
621
        curses.endwin()
622

623 624 625 626
    def refresh_window(self):
        """
        Refresh everything
        """
627
        self.current_tab().set_color_state(theme.COLOR_TAB_CURRENT)
628
        self.current_tab().refresh(self.tabs, self.information_buffer, roster)
629
        self.doupdate()
630

631
    def open_new_room(self, room, nick, focus=True):
632
        """
633
        Open a new MucTab containing a muc Room, using the specified nick
634
        """
635
        r = Room(room, nick)
636
        new_tab = MucTab(self, r)
637 638
        if self.current_tab().nb == 0:
            self.tabs.append(new_tab)
639
        else:
640 641 642
            for ta in self.tabs:
                if ta.nb == 0:
                    self.tabs.insert(self.tabs.index(ta), new_tab)
643
                    break
644
        if focus:
645
            self.command_win("%s" % new_tab.nb)
646
        self.refresh_window()
647

648 649 650 651 652 653
    def go_to_roster(self):
        self.command_win('0')

    def go_to_previous_tab(self):
        self.command_win('%s' % (self.previous_tab_nb,))

654 655 656 657 658 659 660
    def go_to_important_room(self):
        """
        Go to the next room with activity, in this order:
        - A personal conversation with a new message
        - A Muc with an highlight
        - A Muc with any new message
        """
661 662 663
        for tab in self.tabs:
            if tab.get_color_state() == theme.COLOR_TAB_PRIVATE:
                self.command_win('%s' % tab.nb)
664
                return
665 666 667
        for tab in self.tabs:
            if tab.get_color_state() == theme.COLOR_TAB_HIGHLIGHT:
                self.command_win('%s' % tab.nb)
668
                return
669 670 671
        for tab in self.tabs:
            if tab.get_color_state() == theme.COLOR_TAB_NEW_MESSAGE:
                self.command_win('%s' % tab.nb)
672
                return
673

674
    def rotate_rooms_right(self, args=None):
675 676 677
        """
        rotate the rooms list to the right
        """
678 679 680
        self.current_tab().on_lose_focus()
        self.tabs.append(self.tabs.pop(0))
        self.current_tab().on_gain_focus()
681
        self.refresh_window()
682

683
    def rotate_rooms_left(self, args=None):
684 685 686
        """
        rotate the rooms list to the right
        """
687 688 689
        self.current_tab().on_lose_focus()
        self.tabs.insert(0, self.tabs.pop())
        self.current_tab().on_gain_focus()
690
        self.refresh_window()
691

692
    def scroll_page_down(self, args=None):
693
        self.current_tab().on_scroll_down()
694
        self.refresh_window()
695 696

    def scroll_page_up(self, args=None):
697
        self.current_tab().on_scroll_up()
698
        self.refresh_window()
699

700
    def room_error(self, error, room_name):
701 702 703
        """
        Display the error on the room window
        """
704 705 706 707 708 709
        room = self.get_room_by_name(room_name)
        msg = error['error']['type']
        condition = error['error']['condition']
        code = error['error']['code']
        body = error['error']['text']
        if not body:
710
            if code in list(ERROR_AND_STATUS_CODES.keys()):
711 712
                body = ERROR_AND_STATUS_CODES[code]
            else:
713 714
                body = condition or _('Unknown error')
        if code:
715 716
            msg = _('Error: %(code)s - %(msg)s: %(body)s') % {'msg':msg, 'body':body, 'code':code}
            self.add_message_to_text_buffer(room, msg)
717
        else:
718 719
            msg = _('Error: %(msg)s: %(body)s') % {'msg':msg, 'body':body}
            self.add_message_to_text_buffer(room, msg)
720
        if code == '401':
721 722
            msg = _('To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)')
            self.add_message_to_text_buffer(room, msg)
723
        if code == '409':
724 725 726
            if config.get('alternative_nickname', '') != '':
                self.command_join('%s/%s'% (room.name, room.own_nick+config.get('alternative_nickname', '')))
            else:
727
                self.add_message_to_text_buffer(room, _('You can join the room with an other nick, by typing "/join /other_nick"'))
728
        self.refresh_window()
729

730
    def open_conversation_window(self, jid, focus=True):
731 732 733
        """
        open a new conversation tab and focus it if needed
        """
734
        text_buffer = TextBuffer()
735
        new_tab = ConversationTab(self, text_buffer, jid)
736 737 738 739 740 741 742 743 744 745 746
        # insert it in the rooms
        if self.current_tab().nb == 0:
            self.tabs.append(new_tab)
        else:
            for ta in self.tabs:
                if ta.nb == 0:
                    self.tabs.insert(self.tabs.index(ta), new_tab)
                    break
        if focus:               # focus the room if needed
            self.command_win('%s' % (new_tab.nb))
        self.refresh_window()
747
        return new_tab
748

749
    def open_private_window(self, room_name, user_nick, focus=True):
750
        complete_jid = room_name+'/'+user_nick
751 752 753 754
        for tab in self.tabs: # if the room exists, focus it and return
            if isinstance(tab, PrivateTab):
                if tab.get_name() == complete_jid:
                    self.command_win('%s' % tab.nb)
755 756
                    return
        # create the new tab
757
        room = self.get_room_by_name(room_name)
758 759 760
        if not room:
            return None
        own_nick = room.own_nick
761
        r = Room(complete_jid, own_nick) # PrivateRoom here
762
        new_tab = PrivateTab(self, r)
763
        # insert it in the tabs
764 765
        if self.current_tab().nb == 0:
            self.tabs.append(new_tab)
766
        else:
767 768 769
            for ta in self.tabs:
                if ta.nb == 0:
                    self.tabs.insert(self.tabs.index(ta), new_tab)
770 771
                    break
        if focus:               # focus the room if needed
772
            self.command_win('%s' % (new_tab.nb))
773
        # self.window.new_room(r)
774
        self.refresh_window()
775 776
        return r

777 778 779 780 781 782 783 784 785 786
    def on_groupchat_subject(self, message):
        """
        triggered when the topic is changed
        """
        nick_from = message['mucnick']
        room_from = message.getMucroom()
        room = self.get_room_by_name(room_from)
        subject = message['subject']
        if not subject:
            return
787
        self.add_message_to_text_buffer(room, _("%(nick)s set the subject to: %(subject)s") % {'nick':nick_from, 'subject':subject}, time=None)
788 789 790
        room.topic = subject.replace('\n', '|')
        self.refresh_window()

791
    def on_groupchat_message(self, message):
792
        """
793
        Triggered whenever a message is received from a multi-user chat room.
794
        """
795 796
        delay_tag = message.find('{urn:xmpp:delay}delay')
        if delay_tag is not None:
797
            delayed = True
798
            date = common.datetime_tuple(delay_tag.attrib['stamp'])
799
        else:
800 801
            # We support the OLD and deprecated XEP: http://xmpp.org/extensions/xep-0091.html
            # But it sucks, please, Jabber servers, don't do this :(
802 803
            delay_tag = message.find('{jabber:x:delay}x')
            if delay_tag is not None:
804
                delayed = True
805
                date = common.datetime_tuple(delay_tag.attrib['stamp'])
806 807
            else:
                delayed = False
808
                date = None
809
        nick_from = message['mucnick']
810
        room_from = message.getMucroom()
811 812
        if message['type'] == 'error': # Check if it's an error
            return self.room_error(message, from_room)
813 814
        if