core.py 56.6 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 multiuserchat as muc
37
from connection import connection
38
from handler import Handler
39
from config import config
40
from tab import MucTab, InfoTab, PrivateTab, RosterInfoTab, ConversationTab
41 42
from user import User
from room import Room
43
from roster import Roster, RosterGroup, roster
44
from contact import Contact, Resource
45
from message import Message
46
from text_buffer import TextBuffer
47
from keyboard import read_char
48
from common import jid_get_domain, is_jid
49

50 51
# http://xmpp.org/extensions/xep-0045.html#errorstatus
ERROR_AND_STATUS_CODES = {
52 53 54 55 56 57 58 59
    '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'),
60 61
    }

62 63 64 65 66 67 68
SHOW_NAME = {
    'dnd': _('busy'),
    'away': _('away'),
    'xa': _('not available'),
    'chat': _('chatty'),
    '': _('available')
    }
69 70 71

resize_lock = threading.Lock()

72
class Core(object):
73
    """
74
    User interface using ncurses
75
    """
76
    def __init__(self, xmpp):
77
        self.running = True
78 79 80
        self.stdscr = curses.initscr()
        self.init_curses(self.stdscr)
        self.xmpp = xmpp
81 82
        default_tab = InfoTab(self.stdscr, self, "Info") if self.xmpp.anon\
            else RosterInfoTab(self.stdscr, self)
83
        default_tab.on_gain_focus()
84
        self.tabs = [default_tab]
85
        # self.roster = Roster()
86 87 88 89 90
        # 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
91
        self.ignores = {}
92
        self.resize_timer = None
93
        self.previous_tab_nb = 0
94

95
        self.commands = {
96
            'help': (self.command_help, '\_o< KOIN KOIN KOIN'),
97
            '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")),
98 99 100 101 102 103 104 105
            'quit': (self.command_quit, _("Usage: /quit\nQuit: Just disconnect from the server and exit poezio.")),
            'exit': (self.command_quit, _("Usage: /exit\nExit: Just disconnect from the server and exit poezio.")),
            'next': (self.rotate_rooms_right, _("Usage: /next\nNext: Go to the next room.")),
            'n': (self.rotate_rooms_right, _("Usage: /n\nN: Go to the next room.")),
            'prev': (self.rotate_rooms_left, _("Usage: /prev\nPrev: Go to the previous room.")),
            'p': (self.rotate_rooms_left, _("Usage: /p\nP: Go to the previous room.")),
            'win': (self.command_win, _("Usage: /win <number>\nWin: Go to the specified room.")),
            'w': (self.command_win, _("Usage: /w <number>\nW: Go to the specified room.")),
106 107
            'ignore': (self.command_ignore, _("Usage: /ignore <nickname> \nIgnore: Ignore a specified nickname.")),
            'unignore': (self.command_unignore, _("Usage: /unignore <nickname>\nUnignore: Remove the specified nickname from the ignore list.")),
108
            'part': (self.command_part, _("Usage: /part [message]\n Part: disconnect from a room. You can specify an optional message.")),
109
            '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")),
110 111 112 113 114
            '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]'")),
            '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]'")),
            '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]'")),
            '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]'")),
           '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)")),
115
            'unquery': (self.command_unquery, _("Usage: /unquery\nClose the private conversation window")),
116 117 118
            '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>.")),
            'kick': (self.command_kick, _("Usage: /kick <nick> [reason]\nKick: Kick the user with the specified nickname. You also can give an optional reason.")),
            'topic': (self.command_topic, _("Usage: /topic <subject> \nTopic: Change the subject of the room")),
119
            '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.")),
120
            'query': (self.command_query, _('Usage: /query <nick> [message]\nQuery: 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')),
121 122
            'nick': (self.command_nick, _("Usage: /nick <nickname>\nNick: Change your nickname in the current room")),
            'say': (self.command_say, _('Usage: /say <message>\nSay: Just send the message. Useful if you want your message to begin with a "/"')),
123
            'whois': (self.command_whois, _('Usage: /whois <nickname>\nWhois: Request many informations about the user.')),
124
            'theme': (self.command_theme, _('Usage: /theme\nTheme: Reload the theme defined in the config file.')),
125
            'recolor': (self.command_recolor, _('Usage: /recolor\nRecolor: 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.')),
126 127
            }

128
        self.key_func = {
129 130
            "KEY_PPAGE": self.scroll_page_up,
            "KEY_NPAGE": self.scroll_page_down,
131
            "KEY_F(5)": self.rotate_rooms_left,
132
            "^P": self.rotate_rooms_left,
133
            "KEY_F(6)": self.rotate_rooms_right,
134 135
            "KEY_F(7)": self.shrink_information_win,
            "KEY_F(8)": self.grow_information_win,
136
            "^N": self.rotate_rooms_right,
137
            "KEY_RESIZE": self.call_for_resize,
138
            'M-e': self.go_to_important_room,
139
            'M-r': self.go_to_roster,
140 141
            'M-z': self.go_to_previous_tab,
            'M-v': self.move_separator,
142 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)
        self.xmpp.add_event_handler("message", self.on_message)
149 150
        self.xmpp.add_event_handler("got_online" , self.on_got_online)
        self.xmpp.add_event_handler("got_offline" , self.on_got_offline)
151
        self.xmpp.add_event_handler("roster_update", self.on_roster_update)
152
        self.xmpp.add_event_handler("changed_status", self.on_presence)
153

154 155
        # self.__debug_fill_roster()

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(self.stdscr)
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(self.stdscr)
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 199 200 201 202
        priority = presence.getPriority() or 0
        resource.set_presence(status)
        resource.set_priority(priority)
        contact.add_resource(resource)
        self.information("%s is online (%s)" % (resource.get_jid().full, status), "Roster")
203

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

211 212 213 214
        if not self.xmpp.anon:
            # request the roster
            self.xmpp.getRoster()
            # send initial presence
215
            self.xmpp.makePresence(pfrom=self.xmpp.boundjid.bare).send()
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
        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
232
            self.open_new_room(roomname, nick, False)
233
            muc.join_groupchat(self.xmpp, roomname, nick)
234
        # if not self.xmpp.anon:
235 236 237 238 239 240 241 242 243 244 245 246 247
        # 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
248
        room = self.get_room_by_name(from_room)
249 250 251 252 253
        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)
254
        if not room:
255
            return
256 257 258 259 260 261 262 263 264 265 266 267
        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)
268
                if from_nick == room.own_nick:
269 270
                    room.joined = True
                    new_user.color = theme.COLOR_OWN_NICK
271
                    self.add_message_to_text_buffer(room, _("Your nickname is %s") % (from_nick))
272
                    if '170' in status_codes:
273
                        self.add_message_to_text_buffer(room, 'Warning: this room is publicly logged')
274
        else:
275 276 277 278 279 280 281 282
            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:
283
                self.on_user_nick_change(room, presence, user, from_nick, from_room)
284 285
            # kick
            elif kick:
286
                self.on_user_kicked(room, presence, user, from_nick)
287 288
            # user quit
            elif typ == 'unavailable':
289
                self.on_user_leave_groupchat(room, user, jid, status, from_nick, from_room)
290 291
            # status change
            else:
292
                self.on_user_change_status(room, user, from_nick, from_room, affiliation, role, show, status)
293
        self.refresh_window()
294
        self.doupdate()
295

296 297 298 299 300 301 302 303 304
    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:
305
                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)
306
            else:
307
                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)
308

309 310 311 312 313
    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
314
            for _tab in self.tabs:
315 316
                if isinstance(_tab, PrivateTab) and _tab.get_name().split('/', 1)[0] == room.name:
                    _tab.get_room().own_nick = new_nick
317
        user.change_nick(new_nick)
318
        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)
319 320 321
        # rename the private tabs if needed
        private_room = self.get_room_by_name('%s/%s' % (from_room, from_nick))
        if private_room:
322
            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)
323
            new_jid = private_room.name.split('/', 1)[0]+'/'+new_nick
324
            private_room.name = new_jid
325

326 327 328 329 330 331 332
    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')
333
        by = by.attrib['jid'] if by is not None else None
334
        reason = reason.text if reason else ''
335 336 337
        if from_nick == room.own_nick: # we are kicked
            room.disconnect()
            if by:
338
                kick_msg = _('%(spec)s [You] have been kicked by "[%(by)s]"') % {'spec': theme.CHAR_KICK.replace('"', '\\"'), 'by':by}
339
            else:
340
                kick_msg = _('%(spec)s [You] have been kicked.') % {'spec':theme.CHAR_KICK.replace('"', '\\"')}
341 342 343 344 345
            # try to auto-rejoin
            if config.get('autorejoin', 'false') == 'true':
                muc.join_groupchat(self.xmpp, room.name, room.own_nick)
        else:
            if by:
346
                kick_msg = _('%(spec)s "[%(nick)s]" has been kicked by "[%(by)s]"') % {'spec':theme.CHAR_KICK.replace('"', '\\"'), 'nick':from_nick.replace('"', '\\"'), 'by':by.replace('"', '\\"')}
347
            else:
348
                kick_msg = _('%(spec)s "[%(nick)s]" has been kicked') % {'spec':theme.CHAR_KICK, 'nick':from_nick.replace('"', '\\"')}
349 350
        if reason:
            kick_msg += _(' Reason: %(reason)s') % {'reason': reason}
351
        self.add_message_to_text_buffer(room, kick_msg, colorized=True)
352

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

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

417 418 419 420 421 422 423 424 425
    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':
            return None
        # Differentiate both type of messages, and call the appropriate handler.
        jid_from = message['from']
426 427
        for tab in self.tabs:
            if isinstance(tab, MucTab) and tab.get_name() == jid_from.bare: # check all the MUC we are in
428
                if message['type'] == 'error':
429
                    return self.room_error(message, tab.get_room().name)
430 431
                else:
                    return self.on_groupchat_private_message(message)
432 433 434 435 436 437 438
        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']
439
        nick_from = jid.resource
440
        room_from = jid.bare
441 442
        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
443
            room = self.open_private_window(room_from, nick_from, False)
444 445 446
            if not room:
                return
        body = message['body']
447 448
        self.add_message_to_text_buffer(room, body, None, nick_from)
        self.refresh_window()
449
        self.doupdate()
450

451 452 453 454 455
    def focus_tab_named(self, tab_name):
        for tab in self.tabs:
            if tab.get_name() == tab_name:
                self.command_win('%s' % (tab.nb,))

456 457 458 459
    def on_normal_message(self, message):
        """
        When receiving "normal" messages (from someone in our roster)
        """
460 461
        jid = message['from'].bare
        room = self.get_conversation_by_jid(jid)
462 463 464
        body = message['body']
        if not body:
            return
465 466 467 468 469 470
        if not room:
            room = self.open_conversation_window(jid, False)
            if not room:
                return
        self.add_message_to_text_buffer(room, body, None, jid)
        self.refresh_window()
471 472 473 474 475
        return

    def on_presence(self, presence):
        """
        """
476
        jid = presence['from']
477
        contact = roster.get_contact_by_jid(jid.bare)
478 479 480 481 482 483 484 485 486 487 488
        if not contact:
            return
        resource = contact.get_resource_by_fulljid(jid.full)
        if not resource:
            return
        status = presence['type']
        priority = presence.getPriority() or 0
        resource.set_presence(status)
        resource.set_priority(priority)
        if isinstance(self.current_tab(), RosterInfoTab):
            self.refresh_window()
489

490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517
    def __debug_fill_roster(self):
        for i in range(10):
            jid = 'contact%s@fion%s.org'%(i,i)
            contact = Contact(jid)
            contact.set_ask('wat')
            contact.set_subscription('both')
            roster.add_contact(contact, jid)
            contact.set_name('%s %s fion'%(i,i))
            roster.edit_groups_of_contact(contact, ['hello'])
        for i in range(10):
            jid = 'test%s@bernard%s.org'%(i,i)
            contact = Contact(jid)
            contact.set_ask('wat')
            contact.set_subscription('both')
            roster.add_contact(contact, jid)
            contact.set_name('%s test'%(i))
            roster.edit_groups_of_contact(contact, ['hello'])
        for i in range(10):
            jid = 'pouet@top%s.org'%(i)
            contact = Contact(jid)
            contact.set_ask('wat')
            contact.set_subscription('both')
            roster.add_contact(contact, jid)
            contact.set_name('%s oula'%(i))
            roster.edit_groups_of_contact(contact, ['hello'])
        if isinstance(self.current_tab(), RosterInfoTab):
            self.refresh_window()

518 519 520 521 522
    def on_roster_update(self, iq):
        """
        A subscription changed, or we received a roster item
        after a roster request, etc
        """
523 524
        for item in iq.findall('{jabber:iq:roster}query/{jabber:iq:roster}item'):
            jid = item.attrib['jid']
525
            contact = roster.get_contact_by_jid(jid)
526 527
            if not contact:
                contact = Contact(jid)
528
                roster.add_contact(contact, jid)
529 530 531 532
            if 'ask' in item.attrib:
                contact.set_ask(item.attrib['ask'])
            else:
                contact.set_ask(None)
533 534 535 536
            if 'name' in item.attrib:
                contact.set_name(item.attrib['name'])
            else:
                contact.set_name(None)
537 538 539
            if item.attrib['subscription']:
                contact.set_subscription(item.attrib['subscription'])
            groups = item.findall('{jabber:iq:roster}group')
540
            roster.edit_groups_of_contact(contact, [group.text for group in groups])
541 542
        if isinstance(self.current_tab(), RosterInfoTab):
            self.refresh_window()
543

544 545 546 547 548 549 550
    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)
        """
551 552 553 554 555 556 557 558 559
        # 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.15, self.resize_window)
            # self.resize_timer.start()
        self.resize_window()
560

561 562 563 564
    def resize_window(self):
        """
        Resize the whole screen
        """
565
        with resize_lock:
566
           # self.resize_timer = None
567 568 569
            for tab in self.tabs:
                tab.resize(self.stdscr)
            self.refresh_window()
570

571
    def main_loop(self):
572 573 574
        """
        main loop waiting for the user to press a key
        """
575
        self.refresh_window()
576
        while self.running:
577
            self.doupdate()
578
            char=read_char(self.stdscr)
579
            # search for keyboard shortcut
580
            if char in list(self.key_func.keys()):
581
                self.key_func[char]()
582
            else:
583
                self.do_command(char)
584

585
    def current_tab(self):
586 587 588
        """
        returns the current room, the one we are viewing
        """
589
        return self.tabs[0]
590

591 592 593 594 595 596 597 598 599 600
    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

601
    def get_room_by_name(self, name):
602 603 604
        """
        returns the room that has this name
        """
605 606 607 608
        for tab in self.tabs:
            if (isinstance(tab, MucTab) or
                isinstance(tab, PrivateTab)) and tab.get_name() == name:
                return tab.get_room()
609
        return None
610

611
    def init_curses(self, stdscr):
612 613 614
        """
        ncurses initialization
        """
615
        curses.curs_set(1)
616
        curses.noecho()
617 618
        # curses.raw()
        theme.init_colors()
619
        stdscr.keypad(True)
620

621
    def reset_curses(self):
622 623 624 625 626
        """
        Reset terminal capabilities to what they were before ncurses
        init
        """
        curses.echo()
627
        curses.nocbreak()
628
        curses.endwin()
629

630 631 632 633
    def refresh_window(self):
        """
        Refresh everything
        """
634
        self.current_tab().set_color_state(theme.COLOR_TAB_CURRENT)
635
        self.current_tab().refresh(self.tabs, self.information_buffer, roster)
636
        self.doupdate()
637

638
    def open_new_room(self, room, nick, focus=True):
639
        """
640
        Open a new MucTab containing a muc Room, using the specified nick
641
        """
642
        r = Room(room, nick)
643
        new_tab = MucTab(self.stdscr, self, r)
644 645
        if self.current_tab().nb == 0:
            self.tabs.append(new_tab)
646
        else:
647 648 649
            for ta in self.tabs:
                if ta.nb == 0:
                    self.tabs.insert(self.tabs.index(ta), new_tab)
650
                    break
651
        if focus:
652
            self.command_win("%s" % new_tab.nb)
653
        self.refresh_window()
654

655 656 657 658 659 660
    def go_to_roster(self):
        self.command_win('0')

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

661 662 663 664 665 666 667
    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
        """
668 669 670
        for tab in self.tabs:
            if tab.get_color_state() == theme.COLOR_TAB_PRIVATE:
                self.command_win('%s' % tab.nb)
671
                return
672 673 674
        for tab in self.tabs:
            if tab.get_color_state() == theme.COLOR_TAB_HIGHLIGHT:
                self.command_win('%s' % tab.nb)
675
                return
676 677 678
        for tab in self.tabs:
            if tab.get_color_state() == theme.COLOR_TAB_NEW_MESSAGE:
                self.command_win('%s' % tab.nb)
679
                return
680

681
    def rotate_rooms_right(self, args=None):
682 683 684
        """
        rotate the rooms list to the right
        """
685 686 687
        self.current_tab().on_lose_focus()
        self.tabs.append(self.tabs.pop(0))
        self.current_tab().on_gain_focus()
688
        self.refresh_window()
689

690
    def rotate_rooms_left(self, args=None):
691 692 693
        """
        rotate the rooms list to the right
        """
694 695 696
        self.current_tab().on_lose_focus()
        self.tabs.insert(0, self.tabs.pop())
        self.current_tab().on_gain_focus()
697
        self.refresh_window()
698

699
    def scroll_page_down(self, args=None):
700
        self.current_tab().on_scroll_down()
701
        self.refresh_window()
702 703

    def scroll_page_up(self, args=None):
704
        self.current_tab().on_scroll_up()
705
        self.refresh_window()
706

707
    def room_error(self, error, room_name):
708 709 710
        """
        Display the error on the room window
        """
711 712 713 714 715 716
        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:
717
            if code in list(ERROR_AND_STATUS_CODES.keys()):
718 719
                body = ERROR_AND_STATUS_CODES[code]
            else:
720 721
                body = condition or _('Unknown error')
        if code:
722 723
            msg = _('Error: %(code)s - %(msg)s: %(body)s') % {'msg':msg, 'body':body, 'code':code}
            self.add_message_to_text_buffer(room, msg)
724
        else:
725 726
            msg = _('Error: %(msg)s: %(body)s') % {'msg':msg, 'body':body}
            self.add_message_to_text_buffer(room, msg)
727
        if code == '401':
728 729
            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)
730
        if code == '409':
731 732 733
            if config.get('alternative_nickname', '') != '':
                self.command_join('%s/%s'% (room.name, room.own_nick+config.get('alternative_nickname', '')))
            else:
734
                self.add_message_to_text_buffer(room, _('You can join the room with an other nick, by typing "/join /other_nick"'))
735
        self.refresh_window()
736

737 738 739 740
    def open_conversation_window(self, room_name, focus=True):
        """
        open a new conversation tab and focus it if needed
        """
741
        r = Room(room_name, self.xmpp.boundjid.full)
742
        new_tab = ConversationTab(self.stdscr, self, r)
743 744 745 746 747 748 749 750 751 752 753 754 755 756
        # 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.window.new_room(r)
        self.refresh_window()
        return r

757
    def open_private_window(self, room_name, user_nick, focus=True):
758
        complete_jid = room_name+'/'+user_nick
759 760 761 762
        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)
763 764
                    return
        # create the new tab
765
        room = self.get_room_by_name(room_name)
766 767 768
        if not room:
            return None
        own_nick = room.own_nick
769
        r = Room(complete_jid, own_nick) # PrivateRoom here
770
        new_tab = PrivateTab(self.stdscr, self, r)
771
        # insert it in the tabs
772 773
        if self.current_tab().nb == 0:
            self.tabs.append(new_tab)
774
        else:
775 776 777
            for ta in self.tabs:
                if ta.nb == 0:
                    self.tabs.insert(self.tabs.index(ta), new_tab)
778 779
                    break
        if focus:               # focus the room if needed
780
            self.command_win('%s' % (new_tab.nb))
781
        # self.window.new_room(r)
782
        self.refresh_window()
783 784
        return r

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