gui.py 21.5 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):
62
        self.name = name
63 64 65 66 67
        self.own_nick = nick
        self.joined = False     # false until self presence is received
        self.users = []
        self.lines = []         # (time, nick, msg) or (time, info)
        self.topic = ''
68

69 70 71 72
    def disconnect(self):
        self.joined = False
        self.users = []

73
    def add_message(self, nick, msg):
74 75 76
        if not msg:
            logger.info('msg is None..., %s' % (nick))
            return
77 78
        self.lines.append((datetime.now(), nick.encode('utf-8'),
                           msg.encode('utf-8')))
79

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

89 90
    def get_user_by_name(self, nick):
        for user in self.users:
91
            if user.nick == nick.encode('utf-8'):
92 93 94
                return user
        return None

95
    def on_presence(self, stanza, nick):
96 97
        """
        """
98 99 100 101
        affiliation = stanza.getAffiliation()
        show = stanza.getShow()
        status = stanza.getStatus()
        role = stanza.getRole()
102
        if not self.joined:     # user in the room BEFORE us.
103 104 105 106 107
            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')))
108
        change_nick = stanza.getStatusCode() == '303'
109
        kick = stanza.getStatusCode() == '307'
110 111 112 113
        user = self.get_user_by_name(nick)
        # New user
        if not user:
            self.users.append(User(nick, affiliation, show, status, role))
114
            return self.add_info(_('%(nick)s joined the room %(roomname)s') % {'nick':nick, 'roomname': self.name})
115 116 117 118 119
        # nick change
        if change_nick:
            if user.nick == self.own_nick:
                self.own_nick = stanza.getNick().encode('utf-8')
            user.change_nick(stanza.getNick())
120
            return self.add_info(_('%(old_nick)s is now known as %(new_nick)s') % {'old_nick':nick, 'new_nick':stanza.getNick()})
121 122 123 124 125 126 127 128 129 130 131
        # 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:
132
                    return self.add_info(_('You have been kicked by %(by)s. Reason: %(reason)s') % {'by':by, 'reason':reason})
133
                else:
134
                    return self.add_info(_('You have been kicked. Reason: %s') % (reason))
135 136
            else:
                if by:
137
                    return self.add_info(_('%(nick)s has been kicked by %(by)s. Reason: %(reason)s') % {'nick':nick, 'by':by, 'reason':reason})
138
                else:
139
                    return self.add_info(_('%(nick)s has been kicked. Reason: %(reason)s') % {'nick':nick, 'reason':reason})
140 141 142
        # user quit
        if status == 'offline' or role == 'none':
            self.users.remove(user)
143
            return self.add_info(_('%s has left the room') % (nick))
144 145
        # status change
        user.update(affiliation, show, status, role)
146
        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})
147

148

149 150 151 152
class Gui(object):
    """
    Graphical user interface using ncurses
    """
153 154 155 156 157 158
    def __init__(self, stdscr=None, muc=None):

        self.init_curses(stdscr)
        self.stdscr = stdscr
        self.rooms = [Room('Info', '')]         # current_room is self.rooms[0]
        self.window = Window(stdscr)
159
        self.window.text_win.new_win('Info')
160 161 162
        self.window.refresh(self.rooms[0])

        self.muc = muc
163

164
        self.commands = {
165 166 167 168 169 170 171
            '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.')),
            'next': (self.rotate_rooms_left, _('Usage: /next\nNext: Go to the next room.')),
            'prev': (self.rotate_rooms_right, _('Usage: /prev\nPrev: Go to the previous room.')),
            'part': (self.command_part, _('Usage: /part [message]\nPart: disconnect from a room. You can specify an optional message.')),
172
            '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')),
173 174 175 176 177
            '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)')),
178
            '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`')),
179 180
            '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.')),
181
            'nick': (self.command_nick, _('Usage: /nick <nickname>\nNick: Change your nickname in the current room'))
182 183
            }

184 185 186 187 188 189 190
        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,
191
            "KEY_DC": self.window.input.key_dc,
192 193 194 195
            "KEY_F(5)": self.rotate_rooms_left,
            "KEY_F(6)": self.rotate_rooms_right,
            "kLFT5": self.rotate_rooms_left,
            "kRIT5": self.rotate_rooms_right,
196 197 198
            "KEY_BACKSPACE": self.window.input.key_backspace
            }

199 200 201 202 203
        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)
204
        self.handler.connect('room-iq', self.room_iq)
205

206 207 208
    def main_loop(self, stdscr):
        while 1:
            curses.doupdate()
209 210 211 212 213
            try:
                key = stdscr.getkey()
            except:
                self.window.resize(stdscr)
                self.window.refresh(self.current_room())
214
                continue
215
            if str(key) in self.key_func.keys():
216
                self.key_func[key]()
217 218 219
            elif str(key) == 'KEY_RESIZE':
                self.window.resize(stdscr)
                self.window.refresh(self.current_room())
220 221
            elif len(key) >= 4:
                continue
222 223
            elif ord(key) == 10:
                self.execute()
224
            elif ord(key) == 8 or ord(key) == 127:
225
                self.window.input.key_backspace()
226 227
            elif ord(key) < 32:
                continue
228
            else:
229 230 231 232 233
                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
234
                elif ord(key) > 190 and ord(key) < 225:
235 236 237 238 239 240
                    key = key+stdscr.getkey()
                elif ord(key) == 226:
                    key = key+stdscr.getkey()
                    key = key+stdscr.getkey()
                self.window.do_command(key)

241
    def current_room(self):
242
        return self.rooms[0]
243 244 245 246 247 248 249

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

250 251 252
    def init_curses(self, stdscr):
        curses.start_color()
        curses.noecho()
253
        stdscr.keypad(True)
254 255
        curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
        curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_BLACK)
256 257 258
        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
259 260 261 262
        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)
263

264 265
    def reset_curses(self):
	curses.echo()
266
        curses.endwin()
267

268
    def on_connected(self, jid):
269 270
        self.information(_("Welcome on Poezio \o/!"))
        self.information(_("Your JID is %s") % jid)
271 272

    def join_room(self, room, nick):
273
        self.window.text_win.new_win(room)
274
        self.rooms.insert(0, Room(room, nick))
275
        self.window.refresh(self.current_room())
276

277
    def rotate_rooms_left(self, args=None):
278
        self.rooms.append(self.rooms.pop(0))
279
        self.window.refresh(self.current_room())
280

281
    def rotate_rooms_right(self, args=None):
282
        self.rooms.insert(0, self.rooms.pop())
283
        self.window.refresh(self.current_room())
284 285

    def room_message(self, stanza):
286 287
        if len(sys.argv) > 1:
            self.information(str(stanza))
288 289 290 291
        if stanza.getType() != 'groupchat':
            return  # ignore all messages not comming from a MUC
        room_from = stanza.getFrom().getStripped()
        nick_from = stanza.getFrom().getResource()
292 293 294
        if not nick_from:
            nick_from = ''
	room = self.get_room_by_name(room_from)
295
	if not room:
296
	    self.information(_("message received for a non-existing room: %s") % (room_from))
297
            return
298 299 300
        body = stanza.getBody()
        if not body:
            body = stanza.getSubject()
301
            info = room.add_info(_("%(nick)s changed the subject to: %(subject)s") % {'nick':nick_from, 'subject':stanza.getSubject()})
302 303 304 305
            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)
306
                self.window.text_win.refresh(room.name)
307
            curses.doupdate()
308 309
        else:
            room.add_message(nick_from, body)
310
            self.window.text_win.add_line(room, (datetime.now(), nick_from.encode('utf-8'), body.encode('utf-8')))
311
        if room == self.current_room():
312
            self.window.text_win.refresh(room.name)
313
            self.window.input.refresh()
314
        curses.doupdate()
315 316

    def room_presence(self, stanza):
317 318
        if len(sys.argv) > 1:
            self.information(str(stanza))
319 320
        from_nick = stanza.getFrom().getResource()
        from_room = stanza.getFrom().getStripped()
321
	room = self.get_room_by_name(from_room)
322
	if not room:
323
	    self.information(_("presence received for a non-existing room: %s") % (from_room))
324
        if stanza.getType() == 'error':
325
            msg = _("Error: %s") % stanza.getError()
326 327
        else:
            msg = room.on_presence(stanza, from_nick)
328
        if room == self.current_room():
329
            self.window.text_win.add_line(room, (datetime.now(), msg))
330 331
            self.window.text_win.refresh(room.name)
            self.window.user_win.refresh(room.users)
332
            self.window.text_win.refresh()
333
            curses.doupdate()
334

335 336 337 338
    def room_iq(self, iq):
        if len(sys.argv) > 1:
            self.information(str(iq))

339
    def execute(self):
340 341
        line = self.window.input.get_text()
        self.window.input.clear_text()
342
        self.window.input.refresh()
343
        curses.doupdate()
344 345
        if line == "":
            return
346
        if line.startswith('/'):
347 348 349
            command = line.strip()[:].split()[0][1:]
            args = line.strip()[:].split()[1:]
            if command in self.commands.keys():
350
                func = self.commands[command][0]
351
                func(args)
352
                return
353 354 355
        if self.current_room().name != 'Info':
            self.muc.send_message(self.current_room().name, line)
	self.window.input.refresh()
356

357 358 359
    def command_help(self, args):
        room = self.current_room()
        if len(args) == 0:
360
            msg = _('Available commands are:')
361 362
            for command in self.commands.keys():
                msg += "%s " % command
363
            msg += _("\nType /help <command_name> to know what each command does")
364 365 366 367
        if len(args) == 1:
            if args[0] in self.commands.keys():
                msg = self.commands[args[0]][1]
            else:
368
                msg = _('Unknown command: %s') % args[0]
369 370
        room.add_info(msg)
        self.window.text_win.add_line(room, (datetime.now(), msg))
371 372 373
        self.window.text_win.refresh(room.name)
        self.window.input.refresh()

374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
    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)

    # def command_ban(self, args):
    #     if len(args) < 1:
    #         self.command_help(['ban'])
    #         return
    #     nick = args[0]
    #     if len(args) >= 2:
    #         reason = ' '.join(args[1:])
    #     else:
    #         reason = None
    #     if self.current_room().name == 'Info' or not self.current_room().joined:
    #         return
    #     roomname = self.current_room().name
    #     self.muc.eject_user(roomname, 'ban', nick, reason)

402
    def command_join(self, args):
403 404 405 406 407 408
        if len(args) == 0:
            r = self.current_room()
            if r.name == 'Info':
                return
            room = r.name
            nick = r.own_nick
409
        else:
410 411 412 413 414
            info = args[0].split('/')
            if len(info) == 1:
                nick = config.get('default_nick', 'Poezio')
            else:
                nick = info[1]
415
            if info[0] == '':   # happens with /join /nickname, which is OK
416 417 418 419 420 421 422
                r = self.current_room()
                if r.name == 'Info':
                    return
                room = r.name
            else:
                room = info[0]
            r = self.get_room_by_name(room)
423
        if r and r.joined:                   # if we are already in the room
424
            self.information(_("already in room [%s]") % room)
425
            return
426
        self.muc.join_room(room, nick)
427
        if not r: # if the room window exists, we don't recreate it.
428
            self.join_room(room, nick)
429

430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
    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
449
        config.set_and_save('rooms', bookmarked+':'+res)
450

451 452 453 454 455 456
    def command_set(self, args):
        if len(args) != 2:
            self.command_help(['set'])
            return
        option = args[0]
        value = args[1]
457
        config.set_and_save(option, value)
458 459 460 461 462 463 464
        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()

465
    def command_show(self, args):
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
        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)
502

503 504 505 506 507 508 509 510 511
    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
512 513
        if room.joined:
            self.muc.quit_room(room.name, room.own_nick, msg)
514 515 516
        self.rooms.remove(self.current_room())
        self.window.refresh(self.current_room())

517 518 519 520 521 522 523 524 525
    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)

526 527 528
    def information(self, msg):
        room = self.get_room_by_name("Info")
        info = room.add_info(msg)
529 530
        if self.current_room() == room:
            self.window.text_win.add_line(room, (datetime.now(), info))
531 532
            self.window.text_win.refresh(room.name)
            curses.doupdate()
533

534
    def command_quit(self, args):
535
	self.reset_curses()
536
        sys.exit()