gui.py 22.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#!/usr/bin/python
# -*- 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/>.

20 21
from gettext import (bindtextdomain, textdomain, bind_textdomain_codeset,
                     gettext as _)
22

23 24 25 26
bindtextdomain('poezio')
textdomain('poezio')
bind_textdomain_codeset('poezio', 'utf-8')

27
import locale
28 29 30 31
locale.setlocale(locale.LC_ALL, '')
import sys

import curses
32 33
from datetime import datetime

34
from handler import Handler
35
from logging import logger
36
from random import randrange
37
from config import config
38
from window import Window
39

40
class User(object):
41
    """
42
    keep trace of an user in a Room
43
    """
44 45 46
    def __init__(self, nick, affiliation, show, status, role):
        self.update(affiliation, show, status, role)
        self.change_nick(nick)
47
        self.color = randrange(2, 10)
48

49
    def update(self, affiliation, show, status, role):
50 51
        self.affiliation = affiliation
        self.show = show
52 53
        self.status = status
        self.role = role
54

55
    def change_nick(self, nick):
56
        self.nick = nick.encode('utf-8')
57

58
class Room(object):
59 60
    """
    """
61
    def __init__(self, name, nick, number):
62
        self.name = name
63
        self.own_nick = nick
64 65
        self.color_state = 11   # color used in RoomInfo
        self.nb = number       # number used in RoomInfo
66 67 68 69
        self.joined = False     # false until self presence is received
        self.users = []
        self.lines = []         # (time, nick, msg) or (time, info)
        self.topic = ''
70

71 72 73 74
    def disconnect(self):
        self.joined = False
        self.users = []

75
    def add_message(self, nick, msg):
76 77
        self.set_color_state(12)
        # TODO check for highlight
78 79 80
        if not msg:
            logger.info('msg is None..., %s' % (nick))
            return
81 82
        self.lines.append((datetime.now(), nick.encode('utf-8'),
                           msg.encode('utf-8')))
83

84 85
    def add_info(self, info):
        """ info, like join/quit/status messages"""
86 87 88
        try:
            self.lines.append((datetime.now(), info.encode('utf-8')))
            return info.encode('utf-8')
89
        except:
90
            self.lines.append((datetime.now(), info))
91
            return info
92

93 94
    def get_user_by_name(self, nick):
        for user in self.users:
95
            if user.nick == nick.encode('utf-8'):
96 97 98
                return user
        return None

99 100 101
    def set_color_state(self, color):
        self.color_state = color

102
    def on_presence(self, stanza, nick):
103 104
        """
        """
105 106 107 108
        affiliation = stanza.getAffiliation()
        show = stanza.getShow()
        status = stanza.getStatus()
        role = stanza.getRole()
109
        if not self.joined:     # user in the room BEFORE us.
110 111 112 113 114
            self.users.append(User(nick, affiliation, show, status, role))
            if nick.encode('utf-8') == self.own_nick:
                self.joined = True
                return self.add_info(_("Your nickname is %s") % (nick))
            return self.add_info(_("%s is in the room") % (nick.encode-('utf-8')))
115
        change_nick = stanza.getStatusCode() == '303'
116
        kick = stanza.getStatusCode() == '307'
117 118 119 120
        user = self.get_user_by_name(nick)
        # New user
        if not user:
            self.users.append(User(nick, affiliation, show, status, role))
121
            return self.add_info(_('%(nick)s joined the room %(roomname)s') % {'nick':nick, 'roomname': self.name})
122 123 124 125 126
        # nick change
        if change_nick:
            if user.nick == self.own_nick:
                self.own_nick = stanza.getNick().encode('utf-8')
            user.change_nick(stanza.getNick())
127
            return self.add_info(_('%(old_nick)s is now known as %(new_nick)s') % {'old_nick':nick, 'new_nick':stanza.getNick()})
128 129 130 131 132 133 134 135 136 137 138
        # kick
        if kick:
            self.users.remove(user)
            reason = stanza.getReason().encode('utf-8') or ''
            try:
                by = stanza.getActor().encode('utf-8')
            except:
                by = None
            if nick == self.own_nick:
                self.disconnect()
                if by:
139
                    return self.add_info(_('You have been kicked by %(by)s. Reason: %(reason)s') % {'by':by, 'reason':reason})
140
                else:
141
                    return self.add_info(_('You have been kicked. Reason: %s') % (reason))
142 143
            else:
                if by:
144
                    return self.add_info(_('%(nick)s has been kicked by %(by)s. Reason: %(reason)s') % {'nick':nick, 'by':by, 'reason':reason})
145
                else:
146
                    return self.add_info(_('%(nick)s has been kicked. Reason: %(reason)s') % {'nick':nick, 'reason':reason})
147 148 149
        # user quit
        if status == 'offline' or role == 'none':
            self.users.remove(user)
150
            return self.add_info(_('%s has left the room') % (nick))
151 152
        # status change
        user.update(affiliation, show, status, role)
153
        return self.add_info(_('%(nick)s changed his/her status : %(a)s, %(b)s, %(c)s, %(d)s') % {'nick':nick, 'a':affiliation, 'b':role, 'c':show, 'd':status})
154

155

156 157 158 159
class Gui(object):
    """
    Graphical user interface using ncurses
    """
160
    def __init__(self, stdscr=None, muc=None):
161
        self.room_number = 0
162 163
        self.init_curses(stdscr)
        self.stdscr = stdscr
164
        self.rooms = [Room('Info', '', self.next_room_number())]         # current_room is self.rooms[0]
165
        self.window = Window(stdscr)
166 167 168
        self.window.new_room(self.current_room())
        self.window.refresh(self.rooms)

169 170

        self.muc = muc
171

172
        self.commands = {
173 174 175 176
            'help': (self.command_help, _('OLOL, this is SOOO recursive')),
            'join': (self.command_join, _('Usage: /join [room_name][/nick]\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). Examples:\n/join room@server.tld\n/join room@server.tld/John\n/join /me_again\n/join')),
            '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.')),
177 178
            'next': (self.rotate_rooms_right, _('Usage: /next\nNext: Go to the next room.')),
            'prev': (self.rotate_rooms_left, _('Usage: /prev\nPrev: Go to the previous room.')),
179
            'part': (self.command_part, _('Usage: /part [message]\nPart: disconnect from a room. You can specify an optional message.')),
180
            'show': (self.command_show, _(u'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')),
181 182 183 184 185
            '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 /nick. Type /help nick 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)')),
186
            '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`')),
187 188
            'kick': (self.command_kick, _('Usage: /kick <nick> [reason]\nKick: Kick the user with the specified nickname. You also can give an optional reason.')),
            # 'ban': (self.command_ban, _('Usage: /ban <nick> [reason]\nBan: Ban the user with the specified nickname. You also can give an optional reason.')),
189
            'nick': (self.command_nick, _('Usage: /nick <nickname>\nNick: Change your nickname in the current room'))
190 191
            }

192 193 194 195 196 197 198
        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,
199
            "KEY_DC": self.window.input.key_dc,
200 201 202 203
            "KEY_F(5)": self.rotate_rooms_left,
            "KEY_F(6)": self.rotate_rooms_right,
            "kLFT5": self.rotate_rooms_left,
            "kRIT5": self.rotate_rooms_right,
204
            "\t": self.auto_completion,
205 206 207
            "KEY_BACKSPACE": self.window.input.key_backspace
            }

208 209 210 211 212
        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)
213
        self.handler.connect('room-iq', self.room_iq)
214

215 216 217
    def main_loop(self, stdscr):
        while 1:
            curses.doupdate()
218 219 220 221
            try:
                key = stdscr.getkey()
            except:
                self.window.resize(stdscr)
222
                self.window.refresh(self.rooms)
223
                continue
224
            if str(key) in self.key_func.keys():
225
                self.key_func[key]()
226 227
            elif str(key) == 'KEY_RESIZE':
                self.window.resize(stdscr)
228
                self.window.refresh(self.rooms)
229 230
            elif len(key) >= 4:
                continue
231 232
            elif ord(key) == 10:
                self.execute()
233
            elif ord(key) == 8 or ord(key) == 127:
234
                self.window.input.key_backspace()
235 236
            elif ord(key) < 32:
                continue
237
            else:
238 239 240 241 242
                if ord(key) == 27 and ord(stdscr.getkey()) == 91:
                    last = ord(stdscr.getkey()) # FIXME: ugly ugly workaroung.
                    if last == 51:
                        self.window.input.key_dc()
                    continue
243
                elif ord(key) > 190 and ord(key) < 225:
244 245 246 247 248 249
                    key = key+stdscr.getkey()
                elif ord(key) == 226:
                    key = key+stdscr.getkey()
                    key = key+stdscr.getkey()
                self.window.do_command(key)

250 251 252 253 254
    def next_room_number(self):
        nb = self.room_number
        self.room_number += 1
        return nb

255
    def current_room(self):
256
        return self.rooms[0]
257 258 259 260 261 262 263

    def get_room_by_name(self, name):
	for room in self.rooms:
	    if room.name == name:
		return room
	return None

264 265 266
    def init_curses(self, stdscr):
        curses.start_color()
        curses.noecho()
267
        stdscr.keypad(True)
268 269
        curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
        curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_BLACK)
270 271 272
        curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK) # Admin
        curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK) # Participant
        curses.init_pair(5, curses.COLOR_WHITE, curses.COLOR_BLACK) # Visitor
273 274 275 276
        curses.init_pair(6, curses.COLOR_CYAN, curses.COLOR_BLACK)
        curses.init_pair(7, curses.COLOR_GREEN, curses.COLOR_BLACK)
        curses.init_pair(8, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
        curses.init_pair(9, curses.COLOR_YELLOW, curses.COLOR_BLACK)
277 278 279 280
        curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_GREEN) # current room
        curses.init_pair(11, curses.COLOR_WHITE, curses.COLOR_BLUE) # normal room
        curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_CYAN) # new message room
        curses.init_pair(13, curses.COLOR_WHITE, curses.COLOR_RED) # highlight room
281

282 283
    def reset_curses(self):
	curses.echo()
284
        curses.endwin()
285

286
    def on_connected(self, jid):
287 288
        self.information(_("Welcome on Poezio \o/!"))
        self.information(_("Your JID is %s") % jid)
289 290

    def join_room(self, room, nick):
291 292 293 294 295 296 297 298 299 300 301 302 303
        r = Room(room, nick, self.next_room_number())
        self.current_room().set_color_state(11)
        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
        while self.current_room().nb != r.nb:
            self.rooms.insert(0, self.rooms.pop())
        self.window.new_room(r)
        self.window.refresh(self.rooms)
304

305 306 307
    def auto_completion(self):
        self.window.input.auto_completion(self.current_room().users)

308 309
    def rotate_rooms_right(self, args=None):
        self.current_room().set_color_state(11)
310
        self.rooms.append(self.rooms.pop(0))
311
        self.window.refresh(self.rooms)
312

313 314
    def rotate_rooms_left(self, args=None):
        self.current_room().set_color_state(11)
315
        self.rooms.insert(0, self.rooms.pop())
316
        self.window.refresh(self.rooms)
317 318

    def room_message(self, stanza):
319 320
        if len(sys.argv) > 1:
            self.information(str(stanza))
321 322 323 324
        if stanza.getType() != 'groupchat':
            return  # ignore all messages not comming from a MUC
        room_from = stanza.getFrom().getStripped()
        nick_from = stanza.getFrom().getResource()
325 326 327
        if not nick_from:
            nick_from = ''
	room = self.get_room_by_name(room_from)
328
	if not room:
329
	    self.information(_("message received for a non-existing room: %s") % (room_from))
330
            return
331 332 333
        body = stanza.getBody()
        if not body:
            body = stanza.getSubject()
334
            info = room.add_info(_("%(nick)s changed the subject to: %(subject)s") % {'nick':nick_from, 'subject':stanza.getSubject()})
335 336 337 338
            self.window.text_win.add_line(room, (datetime.now(), info))
            room.topic = stanza.getSubject().encode('utf-8').replace('\n', '|')
            if room == self.current_room():
                self.window.topic_win.refresh(room.topic)
339
                self.window.text_win.refresh(room.name)
340
            curses.doupdate()
341 342
        else:
            room.add_message(nick_from, body)
343
            self.window.text_win.add_line(room, (datetime.now(), nick_from.encode('utf-8'), body.encode('utf-8')))
344
        if room == self.current_room():
345
            self.window.text_win.refresh(room.name)
346
            self.window.input.refresh()
347 348
        else:
            self.window.info_win.refresh(self.rooms, self.current_room())
349
        curses.doupdate()
350 351

    def room_presence(self, stanza):
352 353
        if len(sys.argv) > 1:
            self.information(str(stanza))
354 355
        from_nick = stanza.getFrom().getResource()
        from_room = stanza.getFrom().getStripped()
356
	room = self.get_room_by_name(from_room)
357
	if not room:
358
	    self.information(_("presence received for a non-existing room: %s") % (from_room))
359
        if stanza.getType() == 'error':
360
            msg = _("Error: %s") % stanza.getError()
361 362
        else:
            msg = room.on_presence(stanza, from_nick)
363
        if room == self.current_room():
364
            self.window.text_win.add_line(room, (datetime.now(), msg))
365 366
            self.window.text_win.refresh(room.name)
            self.window.user_win.refresh(room.users)
367
            self.window.text_win.refresh()
368
            curses.doupdate()
369

370 371 372 373
    def room_iq(self, iq):
        if len(sys.argv) > 1:
            self.information(str(iq))

374
    def execute(self):
375 376
        line = self.window.input.get_text()
        self.window.input.clear_text()
377
        self.window.input.refresh()
378
        curses.doupdate()
379 380
        if line == "":
            return
381
        if line.startswith('/'):
382 383 384
            command = line.strip()[:].split()[0][1:]
            args = line.strip()[:].split()[1:]
            if command in self.commands.keys():
385
                func = self.commands[command][0]
386
                func(args)
387
                return
388 389 390
        if self.current_room().name != 'Info':
            self.muc.send_message(self.current_room().name, line)
	self.window.input.refresh()
391

392 393 394
    def command_help(self, args):
        room = self.current_room()
        if len(args) == 0:
395
            msg = _('Available commands are:')
396 397
            for command in self.commands.keys():
                msg += "%s " % command
398
            msg += _("\nType /help <command_name> to know what each command does")
399 400 401 402
        if len(args) == 1:
            if args[0] in self.commands.keys():
                msg = self.commands[args[0]][1]
            else:
403
                msg = _('Unknown command: %s') % args[0]
404 405
        room.add_info(msg)
        self.window.text_win.add_line(room, (datetime.now(), msg))
406 407 408
        self.window.text_win.refresh(room.name)
        self.window.input.refresh()

409 410 411 412 413 414 415 416 417 418 419 420 421 422
    def command_kick(self, args):
        if len(args) < 1:
            self.command_help(['kick'])
            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
        self.muc.eject_user(roomname, 'kick', nick, reason)

423
    def command_join(self, args):
424 425 426 427 428 429
        if len(args) == 0:
            r = self.current_room()
            if r.name == 'Info':
                return
            room = r.name
            nick = r.own_nick
430
        else:
431 432 433 434 435
            info = args[0].split('/')
            if len(info) == 1:
                nick = config.get('default_nick', 'Poezio')
            else:
                nick = info[1]
436
            if info[0] == '':   # happens with /join /nickname, which is OK
437 438 439 440 441 442 443
                r = self.current_room()
                if r.name == 'Info':
                    return
                room = r.name
            else:
                room = info[0]
            r = self.get_room_by_name(room)
444
        if r and r.joined:                   # if we are already in the room
445
            self.information(_("already in room [%s]") % room)
446
            return
447
        self.muc.join_room(room, nick)
448
        if not r: # if the room window exists, we don't recreate it.
449
            self.join_room(room, nick)
450

451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469
    def command_bookmark(self, args):
        bookmarked = config.get('rooms', '')
        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]
        if nick:
            res = roomname+'/'+nick
        else:
            res = roomname
470
        config.set_and_save('rooms', bookmarked+':'+res)
471

472 473 474 475 476 477
    def command_set(self, args):
        if len(args) != 2:
            self.command_help(['set'])
            return
        option = args[0]
        value = args[1]
478
        config.set_and_save(option, value)
479 480 481 482 483 484 485
        msg = "%s=%s" % (option, value)
        room = self.current_room()
        room.add_info(msg)
        self.window.text_win.add_line(room, (datetime.now(), msg))
        self.window.text_win.refresh(room.name)
        self.window.input.refresh()

486
    def command_show(self, args):
487 488 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 518 519 520 521 522
        possible_show = {'avail':'None',
                         'available':'None',
                         'ok':'None',
                         'here':'None',
                         '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():
            self.command_help(['show'])
            return
        show = possible_show[args[0]]
        if len(args) > 1:
            msg = ' '.join(args[1:])
        else:
            msg = None
        for room in self.rooms:
            if room.joined:
                self.muc.change_show(room.name, room.own_nick, show, msg)

    def command_away(self, args):
        args.insert(0, 'away')
        self.command_show(args)

    def command_busy(self, args):
        args.insert(0, 'busy')
        self.command_show(args)

    def command_avail(self, args):
        args.insert(0, 'available')
        self.command_show(args)
523

524 525 526 527 528 529 530 531 532
    def command_part(self, args):
        reason = None
        room = self.current_room()
        if room.name == 'Info':
            return
        if len(args):
            msg = ' '.join(args)
        else:
            msg = None
533 534
        if room.joined:
            self.muc.quit_room(room.name, room.own_nick, msg)
535
        self.rooms.remove(self.current_room())
536
        self.window.refresh(self.rooms)
537

538 539 540 541 542 543 544 545 546
    def command_nick(self, args):
        if len(args) != 1:
            return
        nick = args[0]
        room = self.current_room()
        if not room.joined or room.name == "Info":
            return
        self.muc.change_nick(room.name, nick)

547 548 549
    def information(self, msg):
        room = self.get_room_by_name("Info")
        info = room.add_info(msg)
550 551
        if self.current_room() == room:
            self.window.text_win.add_line(room, (datetime.now(), info))
552 553
            self.window.text_win.refresh(room.name)
            curses.doupdate()
554

555
    def command_quit(self, args):
556
	self.reset_curses()
557
        sys.exit()