diff options
Diffstat (limited to 'src/core.py')
-rw-r--r-- | src/core.py | 1398 |
1 files changed, 1398 insertions, 0 deletions
diff --git a/src/core.py b/src/core.py new file mode 100644 index 00000000..23bc3ed4 --- /dev/null +++ b/src/core.py @@ -0,0 +1,1398 @@ +# Copyright 2010 Le Coz Florent <louiz@louiz.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 gettext import (bindtextdomain, textdomain, bind_textdomain_codeset, + gettext as _) +from os.path import isfile + +from time import sleep + +import os +import re +import sys +import shlex +import curses +import threading +import webbrowser + +from datetime import datetime + +import common +import theme + +import multiuserchat as muc +from connection import connection +from handler import Handler +from config import config +from tab import MucTab, InfoTab, PrivateTab, RosterInfoTab, ConversationTab +from user import User +from room import Room +from roster import Roster, RosterGroup, roster +from contact import Contact, Resource +from message import Message +from text_buffer import TextBuffer +from keyboard import read_char +from common import jid_get_domain, is_jid + +# 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'), + } + +SHOW_NAME = { + 'dnd': _('busy'), + 'away': _('away'), + 'xa': _('not available'), + 'chat': _('chatty'), + '': _('available') + } + +resize_lock = threading.Lock() + +class Core(object): + """ + User interface using ncurses + """ + def __init__(self, xmpp): + self.running = True + self.stdscr = curses.initscr() + self.init_curses(self.stdscr) + self.xmpp = xmpp + default_tab = InfoTab(self.stdscr, self, "Info") if self.xmpp.anon\ + else RosterInfoTab(self.stdscr, self) + default_tab.on_gain_focus() + self.tabs = [default_tab] + # self.roster = Roster() + # a unique buffer used to store global informations + # that are displayed in almost all tabs, in an + # information window. + self.information_buffer = TextBuffer() + self.information_win_size = 2 # Todo, get this from config + self.ignores = {} + self.resize_timer = None + self.previous_tab_nb = 0 + + self.commands = { + 'help': (self.command_help, '\_o< KOIN KOIN KOIN'), + '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")), + '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.")), + '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.")), + '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 [status] 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)")), + 'unquery': (self.command_unquery, _("Usage: /unquery\nClose the private conversation window")), + '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")), + '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.")), + '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')), + '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 "/"')), + 'whois': (self.command_whois, _('Usage: /whois <nickname>\nWhois: Request many informations about the user.')), + 'theme': (self.command_theme, _('Usage: /theme\nTheme: Reload the theme defined in the config file.')), + 'recolor': (self.command_recolor, _('Usage: /recolor\nRecolor: Re-assign a color to all participants of the current room, based on the last time they talked. Use this if the participants currently talking have too many identical colors.')), + } + + self.key_func = { + "KEY_PPAGE": self.scroll_page_up, + "KEY_NPAGE": self.scroll_page_down, + "KEY_F(5)": self.rotate_rooms_left, + "^P": self.rotate_rooms_left, + "KEY_F(6)": self.rotate_rooms_right, + "KEY_F(7)": self.shrink_information_win, + "KEY_F(8)": self.grow_information_win, + "^N": self.rotate_rooms_right, + "KEY_RESIZE": self.call_for_resize, + 'M-e': self.go_to_important_room, + 'M-r': self.go_to_roster, + 'M-z': self.go_to_previous_tab, + 'M-v': self.move_separator, + } + + # 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.xmpp.add_event_handler("got_online" , self.on_got_online) + self.xmpp.add_event_handler("got_offline" , self.on_got_offline) + self.xmpp.add_event_handler("roster_update", self.on_roster_update) + self.xmpp.add_event_handler("changed_status", self.on_presence) + + # self.__debug_fill_roster() + + def grow_information_win(self): + """ + """ + if self.information_win_size == 14: + return + self.information_win_size += 1 + for tab in self.tabs: + tab.on_info_win_size_changed(self.stdscr) + self.refresh_window() + + def shrink_information_win(self): + """ + """ + if self.information_win_size == 0: + return + self.information_win_size -= 1 + for tab in self.tabs: + tab.on_info_win_size_changed(self.stdscr) + self.refresh_window() + + def on_got_offline(self, presence): + jid = presence['from'] + contact = roster.get_contact_by_jid(jid.bare) + if not contact: + return + resource = contact.get_resource_by_fulljid(jid.full) + assert resource + self.information('%s is offline' % (resource.get_jid()), "Roster") + contact.remove_resource(resource) + if isinstance(self.current_tab(), RosterInfoTab): + self.refresh_window() + + def on_got_online(self, presence): + jid = presence['from'] + contact = roster.get_contact_by_jid(jid.bare) + if not contact: + # Todo, handle presence comming from contacts not in roster + return + resource = contact.get_resource_by_fulljid(jid.full) + assert not resource + resource = Resource(jid.full) + status = presence['type'] + priority = presence.getPriority() or 0 + resource.set_presence(status) + resource.set_priority(priority) + contact.add_resource(resource) + self.information("%s is online (%s)" % (resource.get_jid().full, status), "Roster") + + 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.boundjid.full) + + if not self.xmpp.anon: + # request the roster + self.xmpp.getRoster() + # send initial presence + self.xmpp.makePresence(pfrom=self.xmpp.boundjid.bare).send() + 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, False) + muc.join_groupchat(self.xmpp, roomname, nick) + # if not self.xmpp.anon: + # 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 + msg = None + affiliation = presence['muc']['affiliation'] + show = presence['show'] + status = presence['status'] + role = presence['muc']['role'] + jid = presence['muc']['jid'] + typ = presence['type'] + if not room.joined: # user in the room BEFORE us. + # ignore redondant presence message, see bug #1509 + if from_nick not in [user.nick for user in room.users]: + new_user = User(from_nick, affiliation, show, status, role) + room.users.append(new_user) + if from_nick == room.own_nick: + room.joined = True + new_user.color = theme.COLOR_OWN_NICK + self.add_message_to_text_buffer(room, _("Your nickname is %s") % (from_nick)) + if '170' in status_codes: + self.add_message_to_text_buffer(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: + self.on_user_join(room, from_nick, affiliation, show, status, role, jid) + # nick change + elif change_nick: + self.on_user_nick_change(room, presence, user, from_nick, from_room) + # kick + elif kick: + self.on_user_kicked(room, presence, user, from_nick) + # user quit + elif typ == 'unavailable': + self.on_user_leave_groupchat(room, user, jid, status, from_nick, from_room) + # status change + else: + self.on_user_change_status(room, user, from_nick, from_room, affiliation, role, show, status) + self.refresh_window() + self.doupdate() + + def on_user_join(self, room, from_nick, affiliation, show, status, role, jid): + """ + When a new user joins a groupchat + """ + room.users.append(User(from_nick, affiliation, + show, status, role)) + hide_exit_join = config.get('hide_exit_join', -1) + if hide_exit_join != 0: + if not jid.full: + self.add_message_to_text_buffer(room, _('%(spec)s "[%(nick)s]" joined the room') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_JOIN.replace('"', '\\"')}, colorized=True) + else: + self.add_message_to_text_buffer(room, _('%(spec)s "[%(nick)s]" "(%(jid)s)" joined the room') % {'spec':theme.CHAR_JOIN.replace('"', '\\"'), 'nick':from_nick.replace('"', '\\"'), 'jid':jid.full}, colorized=True) + + def on_user_nick_change(self, room, presence, user, from_nick, from_room): + new_nick = presence.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}item').attrib['nick'] + if user.nick == room.own_nick: + room.own_nick = new_nick + # also change our nick in all private discussion of this room + for _tab in self.tabs: + if isinstance(_tab, PrivateTab) and _tab.get_name().split('/', 1)[0] == room.name: + _tab.get_room().own_nick = new_nick + user.change_nick(new_nick) + self.add_message_to_text_buffer(room, _('"[%(old)s]" is now known as "[%(new)s]"') % {'old':from_nick.replace('"', '\\"'), 'new':new_nick.replace('"', '\\"')}, colorized=True) + # 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_text_buffer(private_room, _('"[%(old_nick)s]" is now known as "[%(new_nick)s]"') % {'old_nick':from_nick.replace('"', '\\"'), 'new_nick':new_nick.replace('"', '\\"')}, colorized=True) + new_jid = private_room.name.split('/', 1)[0]+'/'+new_nick + private_room.name = new_jid + + def on_user_kicked(self, room, presence, user, from_nick): + """ + When someone is kicked + """ + room.users.remove(user) + by = presence.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}item/{http://jabber.org/protocol/muc#user}actor') + reason = presence.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}item/{http://jabber.org/protocol/muc#user}reason') + by = by.attrib['jid'] if by is not None else None + reason = reason.text if reason else '' + if from_nick == room.own_nick: # we are kicked + room.disconnect() + if by: + kick_msg = _('%(spec)s [You] have been kicked by "[%(by)s]"') % {'spec': theme.CHAR_KICK.replace('"', '\\"'), 'by':by} + else: + kick_msg = _('%(spec)s [You] have been kicked.') % {'spec':theme.CHAR_KICK.replace('"', '\\"')} + # 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.replace('"', '\\"'), 'nick':from_nick.replace('"', '\\"'), 'by':by.replace('"', '\\"')} + else: + kick_msg = _('%(spec)s "[%(nick)s]" has been kicked') % {'spec':theme.CHAR_KICK, 'nick':from_nick.replace('"', '\\"')} + if reason: + kick_msg += _(' Reason: %(reason)s') % {'reason': reason} + self.add_message_to_text_buffer(room, kick_msg, colorized=True) + + def on_user_leave_groupchat(self, room, user, jid, status, from_nick, from_room): + """ + When an user leaves a groupchat + """ + room.users.remove(user) + if room.own_nick == user.nick: + # We are now out of the room. Happens with some buggy (? not sure) servers + room.disconnect() + 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.replace('"', '\\"'), 'spec':theme.CHAR_QUIT.replace('"', '\\"')} + else: + leave_msg = _('%(spec)s "[%(nick)s]" "(%(jid)s)" has left the room') % {'spec':theme.CHAR_QUIT.replace('"', '\\"'), 'nick':from_nick.replace('"', '\\"'), 'jid':jid.full.replace('"', '\\"')} + if status: + leave_msg += ' (%s)' % status + self.add_message_to_text_buffer(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_text_buffer(private_room, _('%(spec)s "[%(nick)s]" has left the room') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_QUIT.replace('"', '\\"')}, colorized=True) + else: + self.add_message_to_text_buffer(private_room, _('%(spec)s "[%(nick)s]" has left the room "(%(status)s)"') % {'nick':from_nick.replace('"', '\\"'), 'spec':theme.CHAR_QUIT, 'status': status.replace('"', '\\"')}, colorized=True) + + def on_user_change_status(self, room, user, from_nick, from_room, affiliation, role, show, status): + """ + When an user changes her status + """ + # build the message + display_message = False # flag to know if something significant enough + # to be displayed has changed + msg = _('"%s" changed: ')% from_nick.replace('"', '\\"') + if affiliation != user.affiliation: + msg += _('affiliation: %s, ') % affiliation + display_message = True + if role != user.role: + msg += _('role: %s, ') % role + display_message = True + if show != user.show and show in list(SHOW_NAME.keys()): + msg += _('show: %s, ') % SHOW_NAME[show] + display_message = True + if status and status != user.status: + msg += _('status: %s, ') % status + display_message = True + if not display_message: + return + msg = msg[:-2] # 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_text_buffer(room, msg, colorized=True) + private_room = self.get_room_by_name('%s/%s' % (from_room, from_nick)) + if private_room: # display the message in private + self.add_message_to_text_buffer(private_room, msg, colorized=True) + # finally, effectively change the user status + user.update(affiliation, show, status, role) + + 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 tab in self.tabs: + if isinstance(tab, MucTab) and tab.get_name() == jid_from.bare: # check all the MUC we are in + if message['type'] == 'error': + return self.room_error(message, tab.get_room().name) + else: + 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.resource + room_from = jid.bare + 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, False) + if not room: + return + body = message['body'] + self.add_message_to_text_buffer(room, body, None, nick_from) + self.refresh_window() + self.doupdate() + + def focus_tab_named(self, tab_name): + for tab in self.tabs: + if tab.get_name() == tab_name: + self.command_win('%s' % (tab.nb,)) + + def on_normal_message(self, message): + """ + When receiving "normal" messages (from someone in our roster) + """ + jid = message['from'].bare + room = self.get_conversation_by_jid(jid) + body = message['body'] + if not body: + return + if not room: + room = self.open_conversation_window(jid, False) + if not room: + return + self.add_message_to_text_buffer(room, body, None, jid) + self.refresh_window() + return + + def on_presence(self, presence): + """ + """ + jid = presence['from'] + contact = roster.get_contact_by_jid(jid.bare) + if not contact: + return + resource = contact.get_resource_by_fulljid(jid.full) + if not resource: + return + status = presence['type'] + priority = presence.getPriority() or 0 + resource.set_presence(status) + resource.set_priority(priority) + if isinstance(self.current_tab(), RosterInfoTab): + self.refresh_window() + + def __debug_fill_roster(self): + for i in range(10): + jid = 'contact%s@fion%s.org'%(i,i) + contact = Contact(jid) + contact.set_ask('wat') + contact.set_subscription('both') + roster.add_contact(contact, jid) + contact.set_name('%s %s fion'%(i,i)) + roster.edit_groups_of_contact(contact, ['hello']) + for i in range(10): + jid = 'test%s@bernard%s.org'%(i,i) + contact = Contact(jid) + contact.set_ask('wat') + contact.set_subscription('both') + roster.add_contact(contact, jid) + contact.set_name('%s test'%(i)) + roster.edit_groups_of_contact(contact, ['hello']) + for i in range(10): + jid = 'pouet@top%s.org'%(i) + contact = Contact(jid) + contact.set_ask('wat') + contact.set_subscription('both') + roster.add_contact(contact, jid) + contact.set_name('%s oula'%(i)) + roster.edit_groups_of_contact(contact, ['hello']) + if isinstance(self.current_tab(), RosterInfoTab): + self.refresh_window() + + def on_roster_update(self, iq): + """ + A subscription changed, or we received a roster item + after a roster request, etc + """ + for item in iq.findall('{jabber:iq:roster}query/{jabber:iq:roster}item'): + jid = item.attrib['jid'] + contact = roster.get_contact_by_jid(jid) + if not contact: + contact = Contact(jid) + roster.add_contact(contact, jid) + if 'ask' in item.attrib: + contact.set_ask(item.attrib['ask']) + else: + contact.set_ask(None) + if 'name' in item.attrib: + contact.set_name(item.attrib['name']) + else: + contact.set_name(None) + if item.attrib['subscription']: + contact.set_subscription(item.attrib['subscription']) + groups = item.findall('{jabber:iq:roster}group') + roster.edit_groups_of_contact(contact, [group.text for group in groups]) + if isinstance(self.current_tab(), RosterInfoTab): + self.refresh_window() + + def call_for_resize(self): + """ + Starts a very short timer. If no other terminal resize + occured in this delay then poezio is REALLY resize. + This is to avoid multiple unnecessary software resizes (this + can be heavy on resource on slow computers or networks) + """ + # with resize_lock: + # if self.resize_timer: + # # a recent terminal resize occured. + # # Cancel the programmed software resize + # self.resize_timer.cancel() + # # add the new timer + # self.resize_timer = threading.Timer(0.15, self.resize_window) + # self.resize_timer.start() + self.resize_window() + + def resize_window(self): + """ + Resize the whole screen + """ + with resize_lock: + # self.resize_timer = None + for tab in self.tabs: + tab.resize(self.stdscr) + self.refresh_window() + + def main_loop(self): + """ + main loop waiting for the user to press a key + """ + self.refresh_window() + while self.running: + self.doupdate() + char=read_char(self.stdscr) + # search for keyboard shortcut + if char in list(self.key_func.keys()): + self.key_func[char]() + else: + self.do_command(char) + + def current_tab(self): + """ + returns the current room, the one we are viewing + """ + return self.tabs[0] + + def get_conversation_by_jid(self, jid): + """ + Return the room of the ConversationTab with the given jid + """ + for tab in self.tabs: + if isinstance(tab, ConversationTab): + if tab.get_name() == jid: + return tab.get_room() + return None + + def get_room_by_name(self, name): + """ + returns the room that has this name + """ + for tab in self.tabs: + if (isinstance(tab, MucTab) or + isinstance(tab, PrivateTab)) and tab.get_name() == name: + return tab.get_room() + return None + + def init_curses(self, stdscr): + """ + ncurses initialization + """ + curses.curs_set(1) + curses.noecho() + # curses.raw() + theme.init_colors() + stdscr.keypad(True) + + def reset_curses(self): + """ + Reset terminal capabilities to what they were before ncurses + init + """ + curses.echo() + curses.nocbreak() + curses.endwin() + + def refresh_window(self): + """ + Refresh everything + """ + self.current_tab().set_color_state(theme.COLOR_TAB_CURRENT) + self.current_tab().refresh(self.tabs, self.information_buffer, roster) + self.doupdate() + + def open_new_room(self, room, nick, focus=True): + """ + Open a new MucTab containing a muc Room, using the specified nick + """ + r = Room(room, nick) + new_tab = MucTab(self.stdscr, self, r) + if self.current_tab().nb == 0: + self.tabs.append(new_tab) + else: + for ta in self.tabs: + if ta.nb == 0: + self.tabs.insert(self.tabs.index(ta), new_tab) + break + if focus: + self.command_win("%s" % new_tab.nb) + self.refresh_window() + + def go_to_roster(self): + self.command_win('0') + + def go_to_previous_tab(self): + self.command_win('%s' % (self.previous_tab_nb,)) + + 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 tab in self.tabs: + if tab.get_color_state() == theme.COLOR_TAB_PRIVATE: + self.command_win('%s' % tab.nb) + return + for tab in self.tabs: + if tab.get_color_state() == theme.COLOR_TAB_HIGHLIGHT: + self.command_win('%s' % tab.nb) + return + for tab in self.tabs: + if tab.get_color_state() == theme.COLOR_TAB_NEW_MESSAGE: + self.command_win('%s' % tab.nb) + return + + def rotate_rooms_right(self, args=None): + """ + rotate the rooms list to the right + """ + self.current_tab().on_lose_focus() + self.tabs.append(self.tabs.pop(0)) + self.current_tab().on_gain_focus() + self.refresh_window() + + def rotate_rooms_left(self, args=None): + """ + rotate the rooms list to the right + """ + self.current_tab().on_lose_focus() + self.tabs.insert(0, self.tabs.pop()) + self.current_tab().on_gain_focus() + self.refresh_window() + + def scroll_page_down(self, args=None): + self.current_tab().on_scroll_down() + self.refresh_window() + + def scroll_page_up(self, args=None): + self.current_tab().on_scroll_up() + self.refresh_window() + + def room_error(self, error, room_name): + """ + Display the error on the room window + """ + room = self.get_room_by_name(room_name) + msg = error['error']['type'] + condition = error['error']['condition'] + code = error['error']['code'] + body = error['error']['text'] + if not body: + if code in list(ERROR_AND_STATUS_CODES.keys()): + body = ERROR_AND_STATUS_CODES[code] + else: + body = condition or _('Unknown error') + if code: + msg = _('Error: %(code)s - %(msg)s: %(body)s') % {'msg':msg, 'body':body, 'code':code} + self.add_message_to_text_buffer(room, msg) + else: + msg = _('Error: %(msg)s: %(body)s') % {'msg':msg, 'body':body} + self.add_message_to_text_buffer(room, msg) + if code == '401': + msg = _('To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)') + self.add_message_to_text_buffer(room, msg) + if code == '409': + if config.get('alternative_nickname', '') != '': + self.command_join('%s/%s'% (room.name, room.own_nick+config.get('alternative_nickname', ''))) + else: + self.add_message_to_text_buffer(room, _('You can join the room with an other nick, by typing "/join /other_nick"')) + self.refresh_window() + + def open_conversation_window(self, room_name, focus=True): + """ + open a new conversation tab and focus it if needed + """ + r = Room(room_name, self.xmpp.boundjid.full) + new_tab = ConversationTab(self.stdscr, self, r) + # insert it in the rooms + if self.current_tab().nb == 0: + self.tabs.append(new_tab) + else: + for ta in self.tabs: + if ta.nb == 0: + self.tabs.insert(self.tabs.index(ta), new_tab) + break + if focus: # focus the room if needed + self.command_win('%s' % (new_tab.nb)) + # self.window.new_room(r) + self.refresh_window() + return r + + def open_private_window(self, room_name, user_nick, focus=True): + complete_jid = room_name+'/'+user_nick + for tab in self.tabs: # if the room exists, focus it and return + if isinstance(tab, PrivateTab): + if tab.get_name() == complete_jid: + self.command_win('%s' % tab.nb) + return + # create the new tab + room = self.get_room_by_name(room_name) + if not room: + return None + own_nick = room.own_nick + r = Room(complete_jid, own_nick) # PrivateRoom here + new_tab = PrivateTab(self.stdscr, self, r) + # insert it in the tabs + if self.current_tab().nb == 0: + self.tabs.append(new_tab) + else: + for ta in self.tabs: + if ta.nb == 0: + self.tabs.insert(self.tabs.index(ta), new_tab) + break + if focus: # focus the room if needed + self.command_win('%s' % (new_tab.nb)) + # self.window.new_room(r) + self.refresh_window() + return r + + def on_groupchat_message(self, message): + """ + Triggered whenever a message is received from a multi-user chat room. + """ + delay_tag = message.find('{urn:xmpp:delay}delay') + if delay_tag is not None: + delayed = True + date = common.datetime_tuple(delay_tag.attrib['stamp']) + else: + # We support the OLD and deprecated XEP: http://xmpp.org/extensions/xep-0091.html + # But it sucks, please, Jabber servers, don't do this :( + delay_tag = message.find('{jabber:x:delay}x') + if delay_tag is not None: + delayed = True + date = common.datetime_tuple(delay_tag.attrib['stamp']) + else: + delayed = False + date = None + nick_from = message['mucnick'] + room_from = message.getMucroom() + if message['type'] == 'error': # Check if it's an error + return self.room_error(message, from_room) + if nick_from == room_from: + nick_from = None + room = self.get_room_by_name(room_from) + if (room_from in self.ignores) and (nick_from in self.ignores[room_from]): + return + room = self.get_room_by_name(room_from) + if not room: + self.information(_("message received for a non-existing room: %s") % (room_from)) + return + body = message['body'] + subject = message['subject'] + if subject: + if nick_from: + self.add_message_to_text_buffer(room, _("%(nick)s changed the subject to: %(subject)s") % {'nick':nick_from, 'subject':subject}, time=date) + else: + self.add_message_to_text_buffer(room, _("The subject is: %(subject)s") % {'subject':subject}, time=date) + room.topic = subject.replace('\n', '|') + elif body: + date = date if delayed == True else None + self.add_message_to_text_buffer(room, body, date, nick_from) + self.refresh_window() + self.doupdate() + + def add_message_to_text_buffer(self, room, txt, time=None, nickname=None, colorized=False): + """ + Add the message to the room if possible, else, add it to the Info window + (in the Info tab of the info window in the RosterTab) + """ + if not room: + self.information('Error, trying to add a message in no room: %s' % txt) + else: + room.add_message(txt, time, nickname, colorized) + self.refresh_window() + + def command_help(self, arg): + """ + /help <command_name> + """ + args = arg.split() + if len(args) == 0: + msg = _('Available commands are: ') + for command in list(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 list(self.commands.keys()): + msg = self.commands[args[0]][1] + else: + msg = _('Unknown command: %s') % args[0] + self.information(msg) + + def command_whois(self, arg): + """ + /whois <nickname> + """ + # TODO + return + # check shlex here + try: + args = shlex.split(arg) + except ValueError as error: + return self.information(str(error), _("Error")) + room = self.current_room() + if len(args) != 1: + self.add_message_to_text_buffer(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) + + def command_theme(self, arg): + """ + """ + theme.reload_theme() + self.resize_window() + + def command_recolor(self, arg): + """ + Re-assign color to the participants of the room + """ + tab = self.current_tab() + if not isinstance(tab, MucTab): + return + room = tab.get_room() + i = 0 + compare_users = lambda x: x.last_talked + users = list(room.users) + # search our own user, to remove it from the room + for user in users: + if user.nick == room.own_nick: + users.remove(user) + nb_color = len(theme.LIST_COLOR_NICKNAMES) + for user in sorted(users, key=compare_users, reverse=True): + user.color = theme.LIST_COLOR_NICKNAMES[i % nb_color] + i+= 1 + self.refresh_window() + + def command_win(self, arg): + """ + /win <number> + """ + args = arg.split() + if len(args) != 1: + self.command_help('win') + return + try: + nb = int(args[0]) + except ValueError: + self.command_help('win') + return + if self.current_tab().nb == nb: + return + self.previous_tab_nb = self.current_tab().nb + self.current_tab().on_lose_focus() + start = self.current_tab() + self.tabs.append(self.tabs.pop(0)) + while self.current_tab().nb != nb: + self.tabs.append(self.tabs.pop(0)) + if self.current_tab() == start: + self.current_tab().set_color_state(theme.COLOR_TAB_CURRENT) + self.refresh_window() + return + self.current_tab().on_gain_focus() + self.refresh_window() + + def command_kick(self, arg): + """ + /kick <nick> [reason] + """ + try: + args = shlex.split(arg) + except ValueError as error: + return self.information(str(error), _("Error")) + if len(args) < 1: + self.command_help('kick') + return + nick = args[0] + if len(args) >= 2: + reason = ' '.join(args[1:]) + else: + reason = '' + if not isinstance(self.current_tab(), MucTab) or not self.current_tab().get_room().joined: + return + roomname = self.current_tab().get_name() + res = muc.eject_user(self.xmpp, roomname, nick, reason) + if res['type'] == 'error': + self.room_error(res, roomname) + + def command_join(self, arg): + """ + /join [room][/nick] [password] + """ + args = arg.split() + password = None + if len(args) == 0: + t = self.current_tab() + if not isinstance(t, MucTab) and not isinstance(t, PrivateTab): + return + room = t.get_name() + nick = t.get_room().own_nick + else: + info = args[0].split('/') + if len(info) == 1: + default = os.environ.get('USER') if os.environ.get('USER') else 'poezio' + nick = config.get('default_nick', '') + if nick == '': + nick = default + else: + nick = info[1] + if info[0] == '': # happens with /join /nickname, which is OK + t = self.current_tab() + if not isinstance(t, MucTab): + return + room = t.get_name() + if nick == '': + nick = t.get_room().own_nick + else: + room = info[0] + 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 isinstance(self.current_tab(), MucTab) and\ + is_jid(self.current_tab().get_name()): + room += '@%s' % jid_get_domain(self.current_tab().get_name()) + else: # no server could be found, print a message and return + self.information(_("You didn't specify a server for the room you want to join"), 'Error') + return + r = self.get_room_by_name(room) + if len(args) == 2: # a password is provided + password = args[1] + if r and r.joined: # if we are already in the room + self.focus_tab_named(r.name) + return + room = room.lower() + if r and not r.joined: + muc.join_groupchat(self.xmpp, room, nick, password) + if not r: # if the room window exists, we don't recreate it. + self.open_new_room(room, nick) + muc.join_groupchat(self.xmpp, room, nick, password) + else: + r.own_nick = nick + r.users = [] + + def command_bookmark(self, arg): + """ + /bookmark [room][/nick] + """ + args = arg.split() + nick = None + if not isinstance(self.current_tab(), MucTab): + return + if len(args) == 0: + room = self.current_tab().get_room() + roomname = self.current_tab().get_name() + if room.joined: + nick = room.own_nick + else: + info = args[0].split('/') + if len(info) == 2: + nick = info[1] + roomname = info[0] + if roomname == '': + roomname = self.current_tab().get_name() + if nick: + res = roomname+'/'+nick + else: + res = roomname + 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) + if bookmarked: + bookmarks = bookmarked+':'+res + else: + bookmarks = res + config.set_and_save('rooms', bookmarks) + self.information(_('Your bookmarks are now: %s') % bookmarks) + + def command_set(self, arg): + """ + /set <option> [value] + """ + args = arg.split() + if len(args) != 2 and len(args) != 1: + self.command_help('set') + return + option = args[0] + if len(args) == 2: + value = args[1] + else: + value = '' + config.set_and_save(option, value) + msg = "%s=%s" % (option, value) + self.information(msg) + + def command_show(self, arg): + """ + /show <status> [msg] + """ + args = arg.split() + 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 list(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 tab in self.tabs: + if isinstance(tab, MucTab) and tab.get_room().joined: + muc.change_show(self.xmpp, tab.get_room().name, tab.get_room().own_nick, show, msg) + + def command_ignore(self, arg): + """ + /ignore <nick> + """ + try: + args = shlex.split(arg) + except ValueError as error: + return self.information(str(error), _("Error")) + if len(args) != 1: + self.command_help('ignore') + return + if not isinstance(self.current_tab(), MucTab): + return + roomname = self.current_tab().get_name() + nick = args[0] + if roomname not in self.ignores: + self.ignores[roomname] = set() # no need for any order + if nick not in self.ignores[roomname]: + self.ignores[roomname].add(nick) + self.information(_("%s is now ignored") % nick, 'info') + else: + self.information(_("%s is alread ignored") % nick, 'info') + + def command_unignore(self, arg): + """ + /unignore <nick> + """ + try: + args = shlex.split(arg) + except ValueError as error: + return self.information(str(error), _("Error")) + if len(args) != 1: + self.command_help('unignore') + return + if not isinstance(self.current_tab(), MucTab): + return + roomname = self.current_tab().get_name() + nick = args[0] + if roomname not in self.ignores or (nick not in self.ignores[roomname]): + self.information(_("%s was not ignored") % nick, info) + return + self.ignores[roomname].remove(nick) + if not self.ignores[roomname]: + del self.ignores[roomname] + self.information(_("%s is now unignored") % nick, 'info') + + def command_away(self, arg): + """ + /away [msg] + """ + self.command_show("away "+arg) + + def command_busy(self, arg): + """ + /busy [msg] + """ + self.command_show("busy "+arg) + + def command_avail(self, arg): + """ + /avail [msg] + """ + self.command_show("available "+arg) + + def command_part(self, arg): + """ + /part [msg] + """ + args = arg.split() + reason = None + if not isinstance(self.current_tab(), MucTab) and\ + not isinstance(self.current_tab(), PrivateTab): + return + room = self.current_tab().get_room() + if len(args): + msg = ' '.join(args) + else: + msg = None + if isinstance(self.current_tab(), MucTab) and\ + self.current_tab().get_room().joined: + muc.leave_groupchat(self.xmpp, room.name, room.own_nick, arg) + self.tabs.remove(self.current_tab()) + self.refresh_window() + + def command_unquery(self, arg): + """ + /unquery + """ + tab = self.current_tab() + if isinstance(tab, PrivateTab): + self.tabs.remove(tab) + self.refresh_window() + + def command_query(self, arg): + """ + /query <nick> [message] + """ + try: + args = shlex.split(arg) + except ValueError as error: + return self.information(str(error), _("Error")) + if len(args) < 1 or not isinstance(self.current_tab(), MucTab): + return + nick = args[0] + room = self.current_tab().get_room() + r = None + for user in room.users: + if user.nick == nick: + r = self.open_private_window(room.name, user.nick) + if r and len(args) > 1: + msg = arg[len(nick)+1:] + muc.send_private_message(self.xmpp, r.name, msg) + self.add_message_to_text_buffer(r, msg, None, r.own_nick) + + def command_topic(self, arg): + """ + /topic [new topic] + """ + if not isinstance(self.current_tab(), MucTab): + return + room = self.current_tab().get_room() + if not arg.strip(): + self.add_message_to_text_buffer(room, _("The subject of the room is: %s") % room.topic) + return + subject = arg + muc.change_subject(self.xmpp, room.name, subject) + + def command_link(self, arg): + """ + /link <option> <nb> + Opens the link in a browser, or join the room, or add the JID, or + copy it in the clipboard + """ + if not isinstance(self.current_tab(), MucTab) and\ + not isinstance(self.current_tab(), PrivateTab): + return + args = arg.split() + if len(args) > 2: + # INFO + # self.add_message_to_text_buffer(self.current_room(), + # _("Link: This command takes at most 2 arguments")) + return + # set the default parameters + option = "open" + nb = 0 + # check the provided parameters + if len(args) >= 1: + try: # check if the argument is the number + nb = int(args[0]) + except ValueError: # nope, it's the option + option = args[0] + if len(args) == 2: + try: + nb = int(args[0]) + except ValueError: + # INFO + # self.add_message_to_text_buffer(self.current_room(), + # _("Link: 2nd parameter should be a number")) + return + # find the nb-th link in the current buffer + i = 0 + link = None + for msg in self.current_tab().get_room().messages[:-200:-1]: + if not msg: + continue + matches = re.findall('"((ftp|http|https|gopher|mailto|news|nntp|telnet|wais|file|prospero|aim|webcal):(([A-Za-z0-9$_.+!*(),;/?:@&~=-])|%[A-Fa-f0-9]{2}){2,}(#([a-zA-Z0-9][a-zA-Z0-9$_.+!*(),;/?:@&~=%-]*))?([A-Za-z0-9$_+!*();/?:~-]))"', msg.txt) + for m in matches: + if i == nb: + url = m[0] + self.link_open(url) + return + + def url_open(self, url): + """ + Use webbrowser to open the provided link + """ + webbrowser.open(url) + + def move_separator(self): + """ + Move the new-messages separator at the bottom on the current + text. + """ + try: + room = self.current_tab().get_room() + except: + return + room.remove_line_separator() + room.add_line_separator() + self.refresh_window() + + def command_nick(self, arg): + """ + /nick <nickname> + """ + try: + args = shlex.split(arg) + except ValueError as error: + return self.information(str(error), _("Error")) + if not isinstance(self.current_tab(), MucTab): + return + if len(args) != 1: + return + nick = args[0] + room = self.current_tab().get_room() + if not room.joined or room.name == "Info": + return + muc.change_nick(self.xmpp, room.name, nick) + + def information(self, msg, typ=''): + """ + Displays an informational message in the "Info" room window + """ + self.information_buffer.add_message(msg, nickname=typ) + self.refresh_window() + + def command_quit(self, arg): + """ + /quit + """ + if len(arg.strip()) != 0: + msg = arg + else: + msg = None + for tab in self.tabs: + if isinstance(tab, MucTab): + muc.leave_groupchat(self.xmpp, tab.get_room().name, tab.get_room().own_nick, msg) + self.xmpp.disconnect() + self.running = False + self.reset_curses() + + def do_command(self, key): + if not key: + return + res = self.current_tab().on_input(key) + if not res: + return + if key in ('^J', '\n') and isinstance(res, str): + self.execute(res) + else : + # we did "enter" with an empty input in the roster + self.on_roster_enter_key(res) + + def on_roster_enter_key(self, roster_row): + """ + when enter is pressed on the roster window + """ + if isinstance(roster_row, Contact): + # roster_row.toggle_folded() + if not self.get_conversation_by_jid(roster_row.get_bare_jid()): + self.open_conversation_window(roster_row.get_bare_jid()) + else: + self.focus_tab_named(roster_row.get_bare_jid()) + if isinstance(roster_row, Resource): + if not self.get_conversation_by_jid(roster_row.get_jid().full): + self.open_conversation_window(roster_row.get_jid().full) + else: + self.focus_tab_named(roster_row.get_jid().full) + self.refresh_window() + + def execute(self,line): + """ + Execute the /command or just send the line on the current room + """ + if line == "": + return + if line.startswith('//'): + self.command_say(line[1:]) + elif line.startswith('/') and not line.startswith('/me '): + command = line.strip()[:].split()[0][1:] + arg = line[2+len(command):] # jump the '/' and the ' ' + # example. on "/link 0 open", command = "link" and arg = "0 open" + if command in list(self.commands.keys()): + func = self.commands[command][0] + func(arg) + return + else: + self.information(_("unknown command (%s)") % (command), _('Error')) + else: + self.command_say(line) + + def command_say(self, line): + if isinstance(self.current_tab(), PrivateTab): + muc.send_private_message(self.xmpp, self.current_tab().get_name(), line) + elif isinstance(self.current_tab(), ConversationTab): # todo, special case + muc.send_private_message(self.xmpp, self.current_tab().get_name(), line) + if isinstance(self.current_tab(), PrivateTab) or\ + isinstance(self.current_tab(), ConversationTab): + self.add_message_to_text_buffer(self.current_tab().get_room(), line, None, self.current_tab().get_room().own_nick) + elif isinstance(self.current_tab(), MucTab): + muc.send_groupchat_message(self.xmpp, self.current_tab().get_name(), line) + self.doupdate() + + def doupdate(self): + self.current_tab().just_before_refresh() + curses.doupdate() + +# # global core object +core = Core(connection) + |