gui.py 47.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
# -*- coding:utf-8 -*-
#
# Copyright 2010 Le Coz Florent <louizatakk@fedoraproject.org>
#
# 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/>.

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

23 24
from time import sleep

25
import sys
26
import os
27
import re
28
import curses
29 30
import webbrowser

31
from datetime import datetime
32 33

import common
34
import theme
35

36
import multiuserchat as muc
37
from handler import Handler
38
from config import config
39
from window import Window
40 41
from user import User
from room import Room
42
from message import Message
43
from keyboard import read_char
44
from common import is_jid_the_same, jid_get_domain, jid_get_resource, is_jid
45

46 47 48 49 50 51 52 53 54 55 56 57
# http://xmpp.org/extensions/xep-0045.html#errorstatus
ERROR_AND_STATUS_CODES = {
    '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',
    }

58 59
def doupdate():
    curses.doupdate()
60

61 62
class Gui(object):
    """
63
    User interface using ncurses
64
    """
65 66 67 68 69
    def __init__(self, xmpp):
        self.stdscr = curses.initscr()
        self.init_curses(self.stdscr)
        self.xmpp = xmpp
        self.window = Window(self.stdscr)
70
        self.rooms = [Room('Info', '', self.window)]
71
        self.ignores = {}
72

73
        self.commands = {
74
            'help': (self.command_help, u'\_o< KOIN KOIN KOIN'),
75
            '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")),
76 77 78 79 80 81 82 83
            '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.")),
84 85
            '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.")),
86 87 88 89 90 91 92
            'part': (self.command_part, _("Usage: /part [message]\n Part: disconnect from a room. You can specify an optional message.")),
            '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 [message] argument will be your status message")),
            '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)")),
93
            'unquery': (self.command_unquery, _("Usage: /unquery\nClose the private conversation window")),
94 95 96
            '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")),
97
            '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.")),
98
            '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')),
99 100
            '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 "/"')),
101
            'whois': (self.command_whois, _('Usage: /whois <nickname>\nWhois: Request many informations about the user.')),
102
            'theme': (self.command_theme, _('Usage: /theme\nTheme: Reload the theme defined in the config file.')),
103 104
            }

105 106 107 108 109 110 111
        self.key_func = {
            "KEY_LEFT": self.window.input.key_left,
            "KEY_RIGHT": self.window.input.key_right,
            "KEY_UP": self.window.input.key_up,
            "KEY_END": self.window.input.key_end,
            "KEY_HOME": self.window.input.key_home,
            "KEY_DOWN": self.window.input.key_down,
112 113
            "KEY_PPAGE": self.scroll_page_up,
            "KEY_NPAGE": self.scroll_page_down,
114
            "KEY_DC": self.window.input.key_dc,
115
            "KEY_F(5)": self.rotate_rooms_left,
116
            "^P": self.rotate_rooms_left,
117
            "KEY_F(6)": self.rotate_rooms_right,
118
            "^N": self.rotate_rooms_right,
119 120
            "\t": self.completion,
            "^I": self.completion,
121
            "KEY_BTAB": self.last_words_completion,
122
            "KEY_RESIZE": self.resize_window,
123
            "KEY_BACKSPACE": self.window.input.key_backspace,
124
            '^?': self.window.input.key_backspace,
125 126 127 128 129 130 131 132 133 134 135 136
            '^J': self.execute,
            '\n': self.execute,
            '^D': self.window.input.key_dc,
            '^W': self.window.input.delete_word,
            '^K': self.window.input.delete_end_of_line,
            '^U': self.window.input.delete_begining_of_line,
            '^Y': self.window.input.paste_clipboard,
            '^A': self.window.input.key_home,
            '^E': self.window.input.key_end,
            'M-f': self.window.input.jump_word_right,
            '^X': self.go_to_important_room,
            'M-b': self.window.input.jump_word_left
137 138
            }

139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
        # 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)
        # self.handler = Handler()
        # self.handler.connect('on-connected', self.on_connected)
        # self.handler.connect('join-room', self.join_room)
        # self.handler.connect('room-presence', self.room_presence)
        # self.handler.connect('room-message', self.room_message)
        # self.handler.connect('private-message', self.private_message)
        # self.handler.connect('error-message', self.room_error)
        # self.handler.connect('error', self.information)

    def on_connected(self, event):
        """
        Called when we are connected and authenticated
        """
        self.information(_("Welcome on Poezio \o/!"))
        self.information(_("Your JID is %s") % self.xmpp.fulljid)

        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
            self.open_new_room(roomname, nick)
            muc.join_groupchat(self.xmpp, roomname, nick)
        # 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
	room = self.get_room_by_name(from_room)
        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)
	if not room:
            return
        else:
            msg = None
            affiliation = presence['muc']['affiliation']
            show = presence['muc']['type']
            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)
                    if from_nick.encode('utf-8') == room.own_nick:
                        room.joined = True
                        new_user.color = theme.COLOR_OWN_NICK
                        self.add_message_to_room(room, _("Your nickname is %s") % (from_nick))
                        if '170' in status_codes:
                            self.add_message_to_room(room, 'Warning: this room is publicly logged')
            else:
                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:
                    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:
                            self.add_message_to_room(room, _("%(spec)s [%(nick)s] joined the room") % {'nick':from_nick, 'spec':theme.CHAR_JOIN}, colorized=True)
                        else:
                            self.add_message_to_room(room, _("%(spec)s [%(nick)s] (%(jid)s) joined the room") % {'spec':theme.CHAR_JOIN, 'nick':from_nick, 'jid':jid.full}, colorized=True)
                # nick change
                elif change_nick:
                    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
                        for _room in self.rooms:
                            if _room.jid is not None and is_jid_the_same(_room.jid, room.name):
                                _room.own_nick = new_nick
                    user.change_nick(new_nick)
                    self.add_message_to_room(room, _('[%(old)s] is now known as [%(new)s]') % {'old':from_nick, 'new':new_nick}, colorized=True)
                    # rename the private tabs if needed
                    private_room = self.get_room_by_name('%s/%s' % (from_room, from_nick))
                    if private_room:
                        self.add_message_to_room(private_room, _('[%(old_nick)s] is now known as [%(new_nick)s]') % {'old_nick':from_nick, 'new_nick':new_nick}, colorized=True)
                        new_jid = private_room.name.split('/')[0]+'/'+new_nick
                        private_room.jid = private_room.name = new_jid

                # kick
                elif kick:
                    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')
                    by = by.attrib['jid'] if by else ''
                    reason = reason.text# if reason else ''
                    if from_nick == room.own_nick: # we are kicked
                        room.disconnect()
                        if by:
                            kick_msg = _("%(spec) [You] have been kicked by [%(by)s].") % {'spec': theme.CHAR_KICK, 'by':by}
                        else:
                            kick_msg = _("%(spec)s [You] have been kicked.") % {'spec':theme.CHAR_KICK}
                        # try to auto-rejoin
                        if config.get('autorejoin', 'false') == 'true':
                            muc.join_groupchat(self.xmpp, room.name, room.own_nick)
                    else:
                        if by:
                            kick_msg = _("%(spec)s [%(nick)s] has been kicked by %(by)s.") % {'spec':theme.CHAR_KICK, 'nick':from_nick, 'by':by}
                        else:
                            kick_msg = _("%(spec)s [%(nick)s] has been kicked") % {'spec':theme.CHAR_KICK, 'nick':from_nick}
                    if reason:
                        kick_msg += _(' Reason: %(reason)s') % {'reason': reason}
                    self.add_message_to_room(room, kick_msg, colorized=True)

                # user quit
                elif typ == 'unavailable':
                    room.users.remove(user)
                    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:
                            leave_msg = _('%(spec)s [%(nick)s] has left the room') % {'nick':from_nick, 'spec':theme.CHAR_QUIT}
                        else:
                            leave_msg = _('%(spec)s [%(nick)s] (%(jid)s) has left the room') % {'spec':theme.CHAR_QUIT, 'nick':from_nick, 'jid':jid.full}
                        if status:
                            leave_msg += ' (%s)' % status
                        self.add_message_to_room(room, leave_msg, colorized=True)
                    private_room = self.get_room_by_name('%s/%s' % (from_room, from_nick))
                    if private_room:
                        if not status:
                            self.add_message_to_room(private_room, _('%(spec)s [%(nick)s] has left the room') % {'nick':from_nick, 'spec':theme.CHAR_QUIT}, colorized=True)
                        else:
                            self.add_message_to_room(private_room, _('%(spec)s [%(nick)s] has left the room (%(status)s)') % {'nick':from_nick, 'spec':theme.CHAR_QUIT, 'status': status}, colorized=True)
                # status change
                else:
                    # build the message
                    msg = _('%s changed his/her status: ')% from_nick
                    if affiliation != user.affiliation:
                        msg += _('affiliation: %s,') % affiliation
                    if role != user.role:
                        msg += _('role: %s,') % role
                    if show != user.show:
                        msg += _('show: %s,') % show
                    if status != user.status:
                        msg += _('status: %s,') % status
                    msg = msg[:-1] # remove the last ","
                    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
                        self.add_message_to_room(room, msg)
                    private_room = self.get_room_by_name(from_room)
                    if private_room: # display the message in private
                        self.add_message_to_room(private_room, msg)
                    # finally, effectively change the user status
                    user.update(affiliation, show, status, role)
            if room == self.current_room():
                self.window.user_win.refresh(room.users)
        self.window.input.refresh()
        doupdate()

    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']
        for room in self.rooms:
            if room.jid is None and room.name == jid_from.bare: # check all the MUC we are in
                return self.on_groupchat_private_message(message)
        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']
        nick_from = jid.user
        room_from = jid.server
        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
            room = self.open_private_window(room_from, nick_from.encode('utf-8'), False)
            if not room:
                return
        body = message['body']
        self.add_message_to_room(room, body, None, nick_from)
        self.window.input.refresh()
        doupdate()

    def on_normal_message(self, message):
        """
        When receiving "normal" messages (from someone in our roster)
        """
        return
363

364 365 366 367 368 369 370
    def resize_window(self):
        """
        Resize the whole screen
        """
        self.window.resize(self.stdscr)
        self.window.refresh(self.rooms)

371
    def main_loop(self):
372 373 374
        """
        main loop waiting for the user to press a key
        """
375
        self.refresh_window()
376
        while True:
377
            doupdate()
378
            char=read_char(self.stdscr)
379
            try: # if this is not a valide utf-8 char, discard it
380
                char.decode('utf-8')
381 382
            except UnicodeDecodeError:
                continue
383 384 385
            # search for keyboard shortcut
            if char in self.key_func.keys():
                self.key_func[char]()
386
            else:
387 388
                if len(char.decode('utf-8')) > 1:
                    continue    # ignore non-handled keyboard shortcuts
389
                self.window.do_command(char)
390

391
    def current_room(self):
392 393 394
        """
        returns the current room, the one we are viewing
        """
395
        return self.rooms[0]
396 397

    def get_room_by_name(self, name):
398 399 400 401
        """
        returns the room that has this name
        """
        for room in self.rooms:
402 403 404 405 406 407
            try:
                if room.name.decode('utf-8') == name:
                    return room
            except UnicodeEncodeError:
                if room.name == name:
                    return room
408
        return None
409

410
    def init_curses(self, stdscr):
411 412 413
        """
        ncurses initialization
        """
414
        theme.init_colors()
415
        curses.noecho()
416
        curses.curs_set(0)
417
        stdscr.keypad(True)
418

419
    def reset_curses(self):
420 421 422 423
        """
        Reset terminal capabilities to what they were before ncurses
        init
        """
424
        # TODO remove me?
425
        curses.echo()
426
        curses.nocbreak()
427
        curses.endwin()
428

429 430 431 432
    def refresh_window(self):
        """
        Refresh everything
        """
433
        self.current_room().set_color_state(theme.COLOR_TAB_CURRENT)
434 435
        self.window.refresh(self.rooms)

436
    def open_new_room(self, room, nick, focus=True):
437
        """
438
        Open a new Tab containing a Muc room, using the specified nick
439
        """
440
        r = Room(room, nick, self.window)
441
        self.current_room().set_color_state(theme.COLOR_TAB_NORMAL)
442 443 444 445 446 447 448
        if self.current_room().nb == 0:
            self.rooms.append(r)
        else:
            for ro in self.rooms:
                if ro.nb == 0:
                    self.rooms.insert(self.rooms.index(ro), r)
                    break
449 450
        if focus:
            self.command_win("%s" % r.nb)
451
        self.refresh_window()
452

453
    def completion(self):
454 455 456
        """
        Called when Tab is pressed, complete the nickname in the input
        """
457 458
        def compare_users(a, b):
            """
459
            Used to sort users by their last_talked
460
            """
461
            if not a.last_talked and b.last_talked:
462
                return 0
463 464 465 466 467
            elif not b.last_talked and a.last_talked:
                return 1
            if a.last_talked <  b.last_talked:
                return 1
            else:
468
                return -1
469
        self.window.input.auto_completion([user.nick for user in sorted(self.current_room().users, compare_users)])
470

471 472 473 474 475 476 477
    def last_words_completion(self):
        """
        Complete the input with words recently said
        """
        # build the list of the recent words
        char_we_dont_want = [',', '(', ')', '.']
        words = list()
478
        for msg in self.current_room().messages[:-9:-1]:
479 480
            if not msg:
                continue
481 482 483 484
            for word in msg.txt.split():
                for char in char_we_dont_want: # remove the chars we don't want
                    word = word.replace(char, '')
                if len(word) > 5:
485
                    words.append(word.encode('utf-8'))
486 487
        self.window.input.auto_completion(words)

488 489 490 491 492 493 494 495
    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
        """
        for room in self.rooms:
496
            if room.color_state == theme.COLOR_TAB_PRIVATE:
497
                self.command_win('%s' % room.nb)
498 499
                return
        for room in self.rooms:
500
            if room.color_state == theme.COLOR_TAB_HIGHLIGHT:
501
                self.command_win('%s' % room.nb)
502 503
                return
        for room in self.rooms:
504
            if room.color_state == theme.COLOR_TAB_NEW_MESSAGE:
505 506
                self.command_win('%s' % room.nb)
                return
507

508
    def rotate_rooms_right(self, args=None):
509 510 511
        """
        rotate the rooms list to the right
        """
512
        self.current_room().set_color_state(theme.COLOR_TAB_NORMAL)
513
        self.current_room().remove_line_separator()
514
        self.rooms.append(self.rooms.pop(0))
515
        self.current_room().set_color_state(theme.COLOR_TAB_CURRENT)
516
        self.refresh_window()
517

518
    def rotate_rooms_left(self, args=None):
519 520 521
        """
        rotate the rooms list to the right
        """
522
        self.current_room().set_color_state(theme.COLOR_TAB_NORMAL)
523
        self.current_room().remove_line_separator()
524
        self.rooms.insert(0, self.rooms.pop())
525
        self.current_room().set_color_state(theme.COLOR_TAB_CURRENT)
526
        self.refresh_window()
527

528
    def scroll_page_down(self, args=None):
529
        self.current_room().scroll_down(self.window.text_win.height-1)
530
        self.refresh_window()
531 532

    def scroll_page_up(self, args=None):
533
        self.current_room().scroll_up(self.window.text_win.height-1)
534
        self.refresh_window()
535

536
    def room_error(self, error, room_name):
537 538 539
        """
        Display the error on the room window
        """
540
        room = self.get_room_by_name(room_name)
541 542
        if not room:
            room = self.get_room_by_name('Info')
543 544 545 546 547
        msg = error['error']['type']
        condition = error['error']['condition']
        code = error['error']['code']
        body = error['error']['text']
        if not body:
548 549 550
            if code in ERROR_AND_STATUS_CODES.keys():
                body = ERROR_AND_STATUS_CODES[code]
            else:
551 552 553 554 555 556 557
                body = condition or _('Unknown error')
        if code:
            self.add_message_to_room(room, _('Error: %(code)s - %(msg)s: %(body)s' %
                                             {'msg':msg, 'body':body, 'code':code}))
        else:
            self.add_message_to_room(room, _('Error: %(msg)s: %(body)s' %
                                             {'msg':msg, 'body':body}))
558
        if code == '401':
559 560
            self.add_message_to_room(room, _('To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)'))
        if code == '409':
561 562 563
            if config.get('alternative_nickname', '') != '':
                self.command_join('%s/%s'% (room.name, room.own_nick+config.get('alternative_nickname', '')))
            else:
564
                self.add_message_to_room(room, _('You can join the room with an other nick, by typing "/join /other_nick"'))
565
        self.refresh_window()
566

567
    def open_private_window(self, room_name, user_nick, focus=True):
568
        complete_jid = room_name.decode('utf-8')+'/'+user_nick
569 570 571
        for room in self.rooms: # if the room exists, focus it and return
            if room.jid:
                if room.jid == complete_jid:
572
                    self.command_win('%s' % room.nb)
573 574
                    return
        # create the new tab
575
        room = self.get_room_by_name(room_name.decode('utf-8'))
576 577 578
        if not room:
            return None
        own_nick = room.own_nick
579 580 581 582 583 584 585 586 587 588
        r = Room(complete_jid, own_nick, self.window, complete_jid)
        # insert it in the rooms
        if self.current_room().nb == 0:
            self.rooms.append(r)
        else:
            for ro in self.rooms:
                if ro.nb == 0:
                    self.rooms.insert(self.rooms.index(ro), r)
                    break
        if focus:               # focus the room if needed
589
            self.command_win('%s' % (r.nb))
590
        # self.window.new_room(r)
591
        self.refresh_window()
592 593
        return r

594
    def on_groupchat_message(self, message):
595
        """
596
        Triggered whenever a message is received from a multi-user chat room.
597
        """
598 599 600
        # FIXME: not receiving subjects? :/
        delay_tag = message.find('{urn:xmpp:delay}delay')
        if delay_tag is not None:
601
            delayed = True
602
            date = common.datetime_tuple(delay_tag.attrib['stamp'])
603
        else:
604 605
            # We support the OLD and deprecated XEP: http://xmpp.org/extensions/xep-0091.html
            # But it sucks, please, Jabber servers, don't do this :(
606 607
            delay_tag = message.find('{jabber:x:delay}x')
            if delay_tag is not None:
608
                delayed = True
609
                date = common.datetime_tuple(delay_tag.attrib['stamp'])
610 611
            else:
                delayed = False
612 613 614 615
                date = None
        nick_from = message['from'].resource
        room_from = message.getMucroom()
	room = self.get_room_by_name(room_from)
616 617
        if (self.ignores.has_key(room_from)) and (nick_from in self.ignores[room_from]):
            return
618
        room = self.get_room_by_name(room_from)
619 620
        if not room:
            self.information(_("message received for a non-existing room: %s") % (room_from))
621
            return
622 623
        body = message['body']#stanza.getBody()
        subject = message['subject']#stanza.getSubject()
624
        if subject:
625
            if nick_from:
626
                self.add_message_to_room(room, _("%(nick)s changed the subject to: %(subject)s") % {'nick':nick_from, 'subject':subject}, time=date)
627
            else:
628
                self.add_message_to_room(room, _("The subject is: %(subject)s") % {'subject':subject}, time=date)
629
            room.topic = subject.encode('utf-8').replace('\n', '|')
630 631
            if room == self.current_room():
                self.window.topic_win.refresh(room.topic)
632
        elif body:
633
            if body.startswith('/me '):
634
                self.add_message_to_room(room, "* "+nick_from + ' ' + body[4:], date)
635
            else:
636 637
                date = date if delayed == True else None
                self.add_message_to_room(room, body, date, nick_from)
638
        self.refresh_window()
639
        doupdate()
640

641

642
    def add_message_to_room(self, room, txt, time=None, nickname=None, colorized=False):
643
        """
644 645
        Add the message to the room and refresh the associated component
        of the interface
646
        """
647 648
        if room != self.current_room():
            room.add_line_separator()
649
        room.add_message(txt, time, nickname, colorized)
650
        if room == self.current_room():
651 652
            self.window.text_win.refresh(room)
        else:
653
            self.window.info_win.refresh(self.rooms, self.current_room())
654
        self.window.input.refresh()
655 656

    def execute(self):
657 658 659
        """
        Execute the /command or just send the line on the current room
        """
660 661
        line = self.window.input.get_text()
        self.window.input.clear_text()
662
        self.window.input.refresh()
663 664
        if line == "":
            return
665
        if line.startswith('/') and not line.startswith('/me '):
666
            command = line.strip()[:].split()[0][1:]
667
            arg = line[2+len(command):] # jump the '/' and the ' '
668
            # example. on "/link 0 open", command = "link" and arg = "0 open"
669
            if command in self.commands.keys():
670
                func = self.commands[command][0]
671
                func(arg)
672
                return
673
            else:
674
                self.add_message_to_room(self.current_room(), _("Error: unknown command (%s)") % (command))
675
        elif self.current_room().name != 'Info':
676
            if self.current_room().jid is not None:
677
                muc.send_private_message(self.xmpp, self.current_room().name, line)
678
                self.add_message_to_room(self.current_room(), line.decode('utf-8'), None, self.current_room().own_nick.decode('utf-8'))
679
            else:
680
                muc.send_groupchat_message(self.xmpp, self.current_room().name, line)
681
        self.window.input.refresh()
682
        doupdate()
683

684
    def command_help(self, arg):
685 686 687
        """
        /help <command_name>
        """
688
        args = arg.split()
689 690
        room = self.current_room()
        if len(args) == 0:
691
            msg = _('Available commands are: ')
692 693
            for command in self.commands.keys():
                msg += "%s " % command
694
            msg += _("\nType /help <command_name> to know what each command does")
695 696 697 698
        if len(args) == 1:
            if args[0] in self.commands.keys():
                msg = self.commands[args[0]][1]
            else:
699
                msg = _('Unknown command: %s') % args[0]
700
        self.add_message_to_room(room, msg)
701

702 703 704 705
    def command_whois(self, arg):
        """
        /whois <nickname>
        """
706 707
        # TODO
        return
708 709 710 711 712 713 714 715 716 717 718
        args = arg.split()
        room = self.current_room()
        if len(args) != 1:
            self.add_message_to_room(room, _('whois command takes exactly one argument'))
            return
        # check if current room is a MUC
        if room.jid or room.name == 'Info':
            return
        nickname = args[0]
        self.muc.request_vcard(room.name, nickname)

719 720 721 722
    def command_theme(self, arg):
        """
        """
        theme.reload_theme()
723
        self.resize_window()
724

725
    def command_win(self, arg):
726 727 728
        """
        /win <number>
        """
729
        args = arg.split()
730
        if len(args) != 1:
731
            self.command_help('win')
732 733 734 735
            return
        try:
            nb = int(args[0])
        except ValueError:
736
            self.command_help('win')
737 738 739
            return
        if self.current_room().nb == nb:
            return
740
        self.current_room().set_color_state(theme.COLOR_TAB_NORMAL)
741
        self.current_room().remove_line_separator()
742 743 744 745 746
        start = self.current_room()
        self.rooms.append(self.rooms.pop(0))
        while self.current_room().nb != nb:
            self.rooms.append(self.rooms.pop(0))
            if self.current_room() == start:
747
                self.current_room().set_color_state(theme.COLOR_TAB_CURRENT)
748
                self.refresh_window()
749
                return
750
        self.current_room().set_color_state(theme.COLOR_TAB_CURRENT)
751
        self.refresh_window()
752

753
    def command_kick(self, arg):
754 755 756
        """
        /kick <nick> [reason]
        """
757
        args = arg.split()
758
        if len(args) < 1:
759
            self.command_help('kick')
760 761 762 763 764 765 766 767 768
            return
        nick = args[0]
        if len(args) >= 2:
            reason = ' '.join(args[1:])
        else:
            reason = ''
        if self.current_room().name == 'Info' or not self.current_room().joined:
            return
        roomname = self.current_room().name
769 770 771
        res = muc.eject_user(self.xmpp, roomname, nick, reason)
        if res['type'] == 'error':
            self.room_error(res, roomname)
772

773 774 775 776 777 778 779
    def command_say(self, arg):
        """
        /say <message>
        """
        line = arg
        if self.current_room().name != 'Info':
            if self.current_room().jid is not None:
780
                muc.send_private_message(self.xmpp, self.current_room().name, line)
781 782
                self.add_message_to_room(self.current_room(), line.decode('utf-8'), None, self.current_room().own_nick)
            else:
783
                muc.send_groupchat_message(self.xmpp, self.current_room().name, line)
784 785 786
        self.window.input.refresh()
        doupdate()

787
    def command_join(self, arg):
788 789 790
        """
        /join [room][/nick] [password]
        """
791
        args = arg.split()
792
        password = None
793 794 795 796 797 798
        if len(args) == 0:
            r = self.current_room()
            if r.name == 'Info':
                return
            room = r.name
            nick = r.own_nick
799
        else:
800 801
            info = args[0].split('/')
            if len(info) == 1:
802 803 804 805
                default = os.environ.get('USER') if os.environ.get('USER') else 'poezio'
                nick = config.get('default_nick', '')
                if nick == '':
                    nick = default
806 807
            else:
                nick = info[1]
808
            if info[0] == '':   # happens with /join /nickname, which is OK
809 810 811 812
                r = self.current_room()
                if r.name == 'Info':
                    return
                room = r.name
813 814
                if nick == '':
                    nick = r.own_nick
815 816
            else:
                room = info[0]
817 818 819 820
            if not is_jid(room): # no server is provided, like "/join hello"
                # use the server of the current room if available
                # check if the current room's name has a server
                if is_jid(self.current_room().name):
821
                    room += '@%s' % jid_get_domain(self.current_room().name)
822
                else:           # no server could be found, print a message and return
823
                    self.add_message_to_room(self.current_room(), _("You didn't specify a server for the room you want to join"))
824
                    return
825
            r = self.get_room_by_name(room)
826 827
        if len(args) == 2:       # a password is provided
            password = args[1]
828
        if r and r.joined:       # if we are already in the room
829
            self.command_win('%s' % (r.nb))
830
            return
831 832
        room = room.lower()
        self.xmpp.plugin['xep_0045'].joinMUC(room, nick, password)
833
        if not r:   # if the room window exists, we don't recreate it.
834
            self.open_new_room(room, nick)
835
        else:
836
            r.own_nick = nick
837
            r.users = []
838

839
    def command_bookmark(self, arg):
840 841 842
        """
        /bookmark [room][/nick]
        """
843
        args = arg.split()
844 845 846 847 848 849 850 851 852 853 854 855 856
        nick = None
        if len(args) == 0:
            room = self.current_room()
            if room.name == 'Info':
                return
            roomname = room.name
            if room.joined:
                nick = room.own_nick
        else:
            info = args[0].split('/')
            if len(info) == 2:
                nick = info[1]
            roomname = info[0]
857 858
            if roomname == '':
                roomname = self.current_room().name
859 860 861 862
        if nick:
            res = roomname+'/'+nick
        else:
            res = roomname
863 864 865 866 867 868 869 870 871
        bookmarked = config.get('rooms', '')
        # check if the room is already bookmarked.
        # if yes, replace it (i.e., update the associated nick)
        bookmarked = bookmarked.split(':')
        for room in bookmarked:
            if room.split('/')[0] == roomname:
                bookmarked.remove(room)
                break
        bookmarked = ':'.join(bookmarked)
872 873 874
        bookmarks = bookmarked+':'+res
        config.set_and_save('rooms', bookmarks)
        self.add_message_to_room(self.current_room(), _('Your bookmarks are now: %s') % bookmarks)
875

876
    def command_set(self, arg):
877 878 879
        """
        /set <option> [value]
        """
880
        args = arg.split()
881
        if len(args) != 2 and len(args) != 1:
882
            self.command_help('set')
883 884
            return
        option = args[0]
885 886 887 888
        if len(args) == 2:
            value = args[1]
        else:
            value = ''
889
        config.set_and_save(option, value)
890 891
        msg = "%s=%s" % (option, value)
        room = self.current_room()
892
        self.add_message_to_room(room, msg)
893

894
    def command_show(self, arg):
895 896 897
        """
        /show <status> [msg]
        """
898
        args = arg.split()
899 900 901 902
        possible_show = {'avail':None,
                         'available':None,
                         'ok':None,
                         'here':None,
903 904 905 906 907 908 909 910 911 912
                         'chat':'chat',
                         'away':'away',
                         'afk':'away',
                         'dnd':'dnd',
                         'busy':'dnd',
                         'xa':'xa'
                         }
        if len(args) < 1:
            return
        if not args[0] in possible_show.keys():