gui.py 19 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#!/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/>.

from handler import Handler
import curses
from curses import textpad

24
import locale
25 26
from datetime import datetime

27 28
from logging import logger

29 30
from random import randrange

31 32
from config import config

33 34 35
locale.setlocale(locale.LC_ALL, '')
code = locale.getpreferredencoding()

36 37
import sys

38
from connection import *
39
from window import Window
40

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

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

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

59
class Room(object):
60 61
    """
    """
62
    def __init__(self, name, nick):
63
        self.name = name
64 65 66 67 68
        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 = ''
69

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

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

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

85 86
    def get_user_by_name(self, nick):
        for user in self.users:
87
            if user.nick == nick.encode('utf-8'):
88 89 90
                return user
        return None

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

146

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

        self.init_curses(stdscr)
        self.stdscr = stdscr
155
        self.stdscr.leaveok(1)
156 157
        self.rooms = [Room('Info', '')]         # current_room is self.rooms[0]
        self.window = Window(stdscr)
158
        self.window.text_win.new_win('Info')
159 160 161
        self.window.refresh(self.rooms[0])

        self.muc = muc
162

163
        self.commands = {
164
            'help': (self.command_help, 'OLOL, this is SOOO recursive'),
165
            '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'),
166 167 168 169 170
            '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 optionnal message.'),
171 172 173 174 175
            'show': (self.command_show, 'Usage: /show <availability> [status]\nShow: Change your availability and (optionnaly) 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 do "/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 do "/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 do "/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 do "/show available [message]"'),
176
            '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)'),
177
            'nick': (self.command_nick, 'Usage: /nick <nickname>\nNick: Change your nickname in the current room')
178 179
            }

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

195 196 197 198 199
        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)
200
        self.handler.connect('room-iq', self.room_iq)
201

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

236 237 238 239 240 241 242 243 244
    def current_room(self):
	return self.rooms[0]

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

245 246 247
    def init_curses(self, stdscr):
        curses.start_color()
        curses.noecho()
248
        stdscr.keypad(True)
249 250
        curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
        curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_BLACK)
251 252 253
        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
254 255 256 257
        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)
258

259 260
    def reset_curses(self):
	curses.echo()
261
        curses.endwin()
262

263
    def on_connected(self, jid):
264
        self.information("Welcome on Poezio \o/!")
265
        self.information("Your JID is %s" % jid)
266 267

    def join_room(self, room, nick):
268
        self.window.text_win.new_win(room)
269
        self.rooms.insert(0, Room(room, nick))
270
        self.window.refresh(self.current_room())
271

272
    def rotate_rooms_left(self, args=None):
273
        self.rooms.append(self.rooms.pop(0))
274
        self.window.refresh(self.current_room())
275

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

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

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

329 330 331 332
    def room_iq(self, iq):
        if len(sys.argv) > 1:
            self.information(str(iq))

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

351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
    def command_help(self, args):
        room = self.current_room()
        if len(args) == 0:
            msg = 'Available commands are:'
            for command in self.commands.keys():
                msg += "%s " % command
            msg += "\nType /help <command_name> to know what each command does"
        if len(args) == 1:
            if args[0] in self.commands.keys():
                msg = self.commands[args[0]][1]
            else:
                msg = 'Unknown command : %s' % args[0]
        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()

368
    def command_join(self, args):
369 370 371 372 373 374
        if len(args) == 0:
            r = self.current_room()
            if r.name == 'Info':
                return
            room = r.name
            nick = r.own_nick
375
        else:
376 377 378 379 380
            info = args[0].split('/')
            if len(info) == 1:
                nick = config.get('default_nick', 'Poezio')
            else:
                nick = info[1]
381
            if info[0] == '':   # happens with /join /nickname, which is OK
382 383 384 385 386 387 388
                r = self.current_room()
                if r.name == 'Info':
                    return
                room = r.name
            else:
                room = info[0]
            r = self.get_room_by_name(room)
389
        if r and r.joined:                   # if we are already in the room
390 391
            self.information("already in room [%s]" % room)
            return
392
        self.muc.join_room(room, nick)
393
        if not r: # if the room window exists, we don't recreate it.
394
            self.join_room(room, nick)
395

396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
    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
        config.setAndSave('rooms', bookmarked+':'+res)

417
    def command_show(self, args):
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
        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)
454

455 456 457 458 459 460 461 462 463
    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
464 465
        if room.joined:
            self.muc.quit_room(room.name, room.own_nick, msg)
466 467 468
        self.rooms.remove(self.current_room())
        self.window.refresh(self.current_room())

469 470 471 472 473 474 475 476 477
    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)

478 479 480
    def information(self, msg):
        room = self.get_room_by_name("Info")
        info = room.add_info(msg)
481 482
        if self.current_room() == room:
            self.window.text_win.add_line(room, (datetime.now(), info))
483 484
            self.window.text_win.refresh(room.name)
            curses.doupdate()
485

486
    def command_quit(self, args):
487
	self.reset_curses()
488
        sys.exit()