summaryrefslogtreecommitdiff
path: root/poezio/core
diff options
context:
space:
mode:
authorEmmanuel Gil Peyrot <linkmauve@linkmauve.fr>2016-03-31 18:54:41 +0100
committerEmmanuel Gil Peyrot <linkmauve@linkmauve.fr>2016-06-11 20:49:43 +0100
commit332a5c2553db41de777473a1e1be9cd1522c9496 (patch)
tree3ee06a59f147ccc4009b35cccfbe2461bcd18310 /poezio/core
parentcf44cf7cdec9fdb35caa372563d57e7045dc29dd (diff)
downloadpoezio-332a5c2553db41de777473a1e1be9cd1522c9496.tar.gz
poezio-332a5c2553db41de777473a1e1be9cd1522c9496.tar.bz2
poezio-332a5c2553db41de777473a1e1be9cd1522c9496.tar.xz
poezio-332a5c2553db41de777473a1e1be9cd1522c9496.zip
Move the src directory to poezio, for better cython compatibility.
Diffstat (limited to 'poezio/core')
-rw-r--r--poezio/core/__init__.py8
-rw-r--r--poezio/core/commands.py999
-rw-r--r--poezio/core/completions.py387
-rw-r--r--poezio/core/core.py2102
-rw-r--r--poezio/core/handlers.py1354
-rw-r--r--poezio/core/structs.py49
6 files changed, 4899 insertions, 0 deletions
diff --git a/poezio/core/__init__.py b/poezio/core/__init__.py
new file mode 100644
index 00000000..6a82e2bb
--- /dev/null
+++ b/poezio/core/__init__.py
@@ -0,0 +1,8 @@
+"""
+Core class, splitted into smaller chunks
+"""
+
+from . core import Core
+from . structs import Command, Status, possible_show, DEPRECATED_ERRORS, \
+ ERROR_AND_STATUS_CODES
+
diff --git a/poezio/core/commands.py b/poezio/core/commands.py
new file mode 100644
index 00000000..a0a636c1
--- /dev/null
+++ b/poezio/core/commands.py
@@ -0,0 +1,999 @@
+"""
+Global commands which are to be linked to the Core class
+"""
+
+import logging
+
+log = logging.getLogger(__name__)
+
+import os
+from datetime import datetime
+from xml.etree import cElementTree as ET
+
+from slixmpp.xmlstream.stanzabase import StanzaBase
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+
+import common
+import fixes
+import pep
+import tabs
+from bookmarks import Bookmark
+from common import safeJID
+from config import config, DEFAULT_CONFIG, options as config_opts
+import multiuserchat as muc
+from plugin import PluginConfig
+from roster import roster
+from theming import dump_tuple, get_theme
+from decorators import command_args_parser
+
+from . structs import Command, possible_show
+
+
+@command_args_parser.quoted(0, 1)
+def command_help(self, args):
+ """
+ /help [command_name]
+ """
+ if not args:
+ color = dump_tuple(get_theme().COLOR_HELP_COMMANDS)
+ acc = []
+ buff = ['Global commands:']
+ for command in self.commands:
+ if isinstance(self.commands[command], Command):
+ acc.append(' \x19%s}%s\x19o - %s' % (
+ color,
+ command,
+ self.commands[command].short))
+ else:
+ acc.append(' \x19%s}%s\x19o' % (color, command))
+ acc = sorted(acc)
+ buff.extend(acc)
+ acc = []
+ buff.append('Tab-specific commands:')
+ commands = self.current_tab().commands
+ for command in commands:
+ if isinstance(commands[command], Command):
+ acc.append(' \x19%s}%s\x19o - %s' % (
+ color,
+ command,
+ commands[command].short))
+ else:
+ acc.append(' \x19%s}%s\x19o' % (color, command))
+ acc = sorted(acc)
+ buff.extend(acc)
+
+ msg = '\n'.join(buff)
+ msg += "\nType /help <command_name> to know what each command does"
+ else:
+ command = args[0].lstrip('/').strip()
+
+ if command in self.current_tab().commands:
+ tup = self.current_tab().commands[command]
+ elif command in self.commands:
+ tup = self.commands[command]
+ else:
+ self.information('Unknown command: %s' % command, 'Error')
+ return
+ if isinstance(tup, Command):
+ msg = 'Usage: /%s %s\n' % (command, tup.usage)
+ msg += tup.desc
+ else:
+ msg = tup[1]
+ self.information(msg, 'Help')
+
+@command_args_parser.quoted(1)
+def command_runkey(self, args):
+ """
+ /runkey <key>
+ """
+ def replace_line_breaks(key):
+ "replace ^J with \n"
+ if key == '^J':
+ return '\n'
+ return key
+ if args is None:
+ return self.command_help('runkey')
+ char = args[0]
+ func = self.key_func.get(char, None)
+ if func:
+ func()
+ else:
+ res = self.do_command(replace_line_breaks(char), False)
+ if res:
+ self.refresh_window()
+
+@command_args_parser.quoted(1, 1, [None])
+def command_status(self, args):
+ """
+ /status <status> [msg]
+ """
+ if args is None:
+ return self.command_help('status')
+
+ if not args[0] in possible_show.keys():
+ return self.command_help('status')
+
+ show = possible_show[args[0]]
+ msg = args[1]
+
+ pres = self.xmpp.make_presence()
+ if msg:
+ pres['status'] = msg
+ pres['type'] = show
+ self.events.trigger('send_normal_presence', pres)
+ pres.send()
+ current = self.current_tab()
+ is_muctab = isinstance(current, tabs.MucTab)
+ if is_muctab and current.joined and show in ('away', 'xa'):
+ current.send_chat_state('inactive')
+ for tab in self.tabs:
+ if isinstance(tab, tabs.MucTab) and tab.joined:
+ muc.change_show(self.xmpp, tab.name, tab.own_nick, show, msg)
+ if hasattr(tab, 'directed_presence'):
+ del tab.directed_presence
+ self.set_status(show, msg)
+ if is_muctab and current.joined and show not in ('away', 'xa'):
+ current.send_chat_state('active')
+
+@command_args_parser.quoted(1, 2, [None, None])
+def command_presence(self, args):
+ """
+ /presence <JID> [type] [status]
+ """
+ if args is None:
+ return self.command_help('presence')
+
+ jid, type, status = args[0], args[1], args[2]
+ if jid == '.' and isinstance(self.current_tab(), tabs.ChatTab):
+ jid = self.current_tab().name
+ if type == 'available':
+ type = None
+ try:
+ pres = self.xmpp.make_presence(pto=jid, ptype=type, pstatus=status)
+ self.events.trigger('send_normal_presence', pres)
+ pres.send()
+ except:
+ self.information('Could not send directed presence', 'Error')
+ log.debug('Could not send directed presence to %s', jid, exc_info=True)
+ return
+ tab = self.get_tab_by_name(jid)
+ if tab:
+ if type in ('xa', 'away'):
+ tab.directed_presence = False
+ chatstate = 'inactive'
+ else:
+ tab.directed_presence = True
+ chatstate = 'active'
+ if tab == self.current_tab():
+ tab.send_chat_state(chatstate, True)
+ if isinstance(tab, tabs.MucTab):
+ for private in tab.privates:
+ private.directed_presence = tab.directed_presence
+ if self.current_tab() in tab.privates:
+ self.current_tab().send_chat_state(chatstate, True)
+
+@command_args_parser.quoted(1)
+def command_theme(self, args=None):
+ """/theme <theme name>"""
+ if args is None:
+ return self.command_help('theme')
+ self.command_set('theme %s' % (args[0],))
+
+@command_args_parser.quoted(1)
+def command_win(self, args):
+ """
+ /win <number>
+ """
+ if args is None:
+ return self.command_help('win')
+
+ nb = args[0]
+ try:
+ nb = int(nb)
+ except ValueError:
+ pass
+ if self.current_tab_nb == nb:
+ return
+ self.previous_tab_nb = self.current_tab_nb
+ old_tab = self.current_tab()
+ if isinstance(nb, int):
+ if 0 <= nb < len(self.tabs):
+ if not self.tabs[nb]:
+ return
+ self.current_tab_nb = nb
+ else:
+ matchs = []
+ for tab in self.tabs:
+ for name in tab.matching_names():
+ if nb.lower() in name[1].lower():
+ matchs.append((name[0], tab))
+ self.current_tab_nb = tab.nb
+ if not matchs:
+ return
+ tab = min(matchs, key=lambda m: m[0])[1]
+ self.current_tab_nb = tab.nb
+ old_tab.on_lose_focus()
+ self.current_tab().on_gain_focus()
+ self.refresh_window()
+
+@command_args_parser.quoted(2)
+def command_move_tab(self, args):
+ """
+ /move_tab old_pos new_pos
+ """
+ if args is None:
+ return self.command_help('move_tab')
+
+ current_tab = self.current_tab()
+ if args[0] == '.':
+ args[0] = current_tab.nb
+ if args[1] == '.':
+ args[1] = current_tab.nb
+
+ def get_nb_from_value(value):
+ "parse the cmdline to guess the tab the users wants"
+ ref = None
+ try:
+ ref = int(value)
+ except ValueError:
+ old_tab = None
+ for tab in self.tabs:
+ if not old_tab and value == tab.name:
+ old_tab = tab
+ if not old_tab:
+ self.information("Tab %s does not exist" % args[0], "Error")
+ return None
+ ref = old_tab.nb
+ return ref
+ old = get_nb_from_value(args[0])
+ new = get_nb_from_value(args[1])
+ if new is None or old is None:
+ return self.information('Unable to move the tab.', 'Info')
+ result = self.insert_tab(old, new)
+ if not result:
+ self.information('Unable to move the tab.', 'Info')
+ else:
+ self.current_tab_nb = self.tabs.index(current_tab)
+ self.refresh_window()
+
+@command_args_parser.quoted(0, 1)
+def command_list(self, args):
+ """
+ /list [server]
+ Opens a MucListTab containing the list of the room in the specified server
+ """
+ if args is None:
+ return self.command_help('list')
+ elif args:
+ jid = safeJID(args[0])
+ else:
+ if not isinstance(self.current_tab(), tabs.MucTab):
+ return self.information('Please provide a server', 'Error')
+ jid = safeJID(self.current_tab().name).server
+ list_tab = tabs.MucListTab(jid)
+ self.add_tab(list_tab, True)
+ cb = list_tab.on_muc_list_item_received
+ self.xmpp.plugin['xep_0030'].get_items(jid=jid,
+ callback=cb)
+
+@command_args_parser.quoted(1)
+def command_version(self, args):
+ """
+ /version <jid>
+ """
+ def callback(res):
+ "Callback for /version"
+ if not res:
+ return self.information('Could not get the software'
+ ' version from %s' % jid,
+ 'Warning')
+ version = '%s is running %s version %s on %s' % (
+ jid,
+ res.get('name') or 'an unknown software',
+ res.get('version') or 'unknown',
+ res.get('os') or 'an unknown platform')
+ self.information(version, 'Info')
+
+ if args is None:
+ return self.command_help('version')
+
+ jid = safeJID(args[0])
+ if jid.resource or jid not in roster:
+ fixes.get_version(self.xmpp, jid, callback=callback)
+ elif jid in roster:
+ for resource in roster[jid].resources:
+ fixes.get_version(self.xmpp, resource.jid, callback=callback)
+ else:
+ fixes.get_version(self.xmpp, jid, callback=callback)
+
+@command_args_parser.quoted(0, 2)
+def command_join(self, args):
+ """
+ /join [room][/nick] [password]
+ """
+ password = None
+ if len(args) == 0:
+ tab = self.current_tab()
+ if not isinstance(tab, (tabs.MucTab, tabs.PrivateTab)):
+ return
+ room = safeJID(tab.name).bare
+ nick = tab.own_nick
+ else:
+ if args[0].startswith('@'): # we try to join a server directly
+ server_root = True
+ info = safeJID(args[0][1:])
+ else:
+ info = safeJID(args[0])
+ server_root = False
+ if info == '' and len(args[0]) > 1 and args[0][0] == '/':
+ nick = args[0][1:]
+ elif info.resource == '':
+ nick = self.own_nick
+ else:
+ nick = info.resource
+ if info.bare == '': # happens with /join /nickname, which is OK
+ tab = self.current_tab()
+ if not isinstance(tab, tabs.MucTab):
+ return
+ room = tab.name
+ if nick == '':
+ nick = tab.own_nick
+ else:
+ room = info.bare
+ # 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 room.find('@') == -1 and not server_root:
+ if isinstance(self.current_tab(), tabs.MucTab) and\
+ self.current_tab().name.find('@') != -1:
+ domain = safeJID(self.current_tab().name).domain
+ room += '@%s' % domain
+ else:
+ room = args[0]
+ room = room.lower()
+ if room in self.pending_invites:
+ del self.pending_invites[room]
+ tab = self.get_tab_by_name(room, tabs.MucTab)
+ if tab is not None:
+ self.focus_tab_named(tab.name)
+ if tab.own_nick == nick and tab.joined:
+ self.information('/join: Nothing to do.', 'Info')
+ else:
+ tab.command_part('')
+ tab.own_nick = nick
+ tab.join()
+
+ return
+
+ if room.startswith('@'):
+ room = room[1:]
+ if len(args) == 2: # a password is provided
+ password = args[1]
+ if password is None: # try to use a saved password
+ password = config.get_by_tabname('password', room, fallback=False)
+ if tab is not None:
+ if password:
+ tab.password = password
+ tab.join()
+ else:
+ tab = self.open_new_room(room, nick, password=password)
+ tab.join()
+
+ if tab.joined:
+ self.enable_private_tabs(room)
+ tab.state = "normal"
+ if tab == self.current_tab():
+ tab.refresh()
+ self.doupdate()
+
+@command_args_parser.quoted(0, 2)
+def command_bookmark_local(self, args):
+ """
+ /bookmark_local [room][/nick] [password]
+ """
+ if not args and not isinstance(self.current_tab(), tabs.MucTab):
+ return
+ password = args[1] if len(args) > 1 else None
+ jid = args[0] if args else None
+
+ _add_bookmark(self, jid, True, password, 'local')
+
+@command_args_parser.quoted(0, 3)
+def command_bookmark(self, args):
+ """
+ /bookmark [room][/nick] [autojoin] [password]
+ """
+ if not args and not isinstance(self.current_tab(), tabs.MucTab):
+ return
+ jid = args[0] if args else ''
+ password = args[2] if len(args) > 2 else None
+
+ if not config.get('use_remote_bookmarks'):
+ return _add_bookmark(self, jid, True, password, 'local')
+
+ if len(args) > 1:
+ autojoin = False if args[1].lower() != 'true' else True
+ else:
+ autojoin = True
+
+ _add_bookmark(self, jid, autojoin, password, 'remote')
+
+def _add_bookmark(self, jid, autojoin, password, method):
+ nick = None
+ if not jid:
+ tab = self.current_tab()
+ roomname = tab.name
+ if tab.joined and tab.own_nick != self.own_nick:
+ nick = tab.own_nick
+ if password is None and tab.password is not None:
+ password = tab.password
+ elif jid == '*':
+ return _add_wildcard_bookmarks(self, method)
+ else:
+ info = safeJID(jid)
+ roomname, nick = info.bare, info.resource
+ if roomname == '':
+ if not isinstance(self.current_tab(), tabs.MucTab):
+ return
+ roomname = self.current_tab().name
+ bookmark = self.bookmarks[roomname]
+ if bookmark is None:
+ bookmark = Bookmark(roomname)
+ self.bookmarks.append(bookmark)
+ bookmark.method = method
+ bookmark.autojoin = autojoin
+ if nick:
+ bookmark.nick = nick
+ if password:
+ bookmark.password = password
+ def callback(iq):
+ if iq["type"] != "error":
+ self.information('Bookmark added.', 'Info')
+ else:
+ self.information("Could not add the bookmarks.", "Info")
+ self.bookmarks.save_local()
+ self.bookmarks.save_remote(self.xmpp, callback)
+
+def _add_wildcard_bookmarks(self, method):
+ new_bookmarks = []
+ for tab in self.get_tabs(tabs.MucTab):
+ bookmark = self.bookmarks[tab.name]
+ if not bookmark:
+ bookmark = Bookmark(tab.name, autojoin=True,
+ method=method)
+ new_bookmarks.append(bookmark)
+ else:
+ bookmark.method = method
+ new_bookmarks.append(bookmark)
+ self.bookmarks.remove(bookmark)
+ new_bookmarks.extend(self.bookmarks.bookmarks)
+ self.bookmarks.set(new_bookmarks)
+ def _cb(iq):
+ if iq["type"] != "error":
+ self.information("Bookmarks saved.", "Info")
+ else:
+ self.information("Could not save the remote bookmarks.", "Info")
+ self.bookmarks.save_local()
+ self.bookmarks.save_remote(self.xmpp, _cb)
+
+@command_args_parser.ignored
+def command_bookmarks(self):
+ """/bookmarks"""
+ tab = self.get_tab_by_name('Bookmarks', tabs.BookmarksTab)
+ old_tab = self.current_tab()
+ if tab:
+ self.current_tab_nb = tab.nb
+ else:
+ tab = tabs.BookmarksTab(self.bookmarks)
+ self.tabs.append(tab)
+ self.current_tab_nb = tab.nb
+ old_tab.on_lose_focus()
+ tab.on_gain_focus()
+ self.refresh_window()
+
+@command_args_parser.quoted(0, 1)
+def command_remove_bookmark(self, args):
+ """/remove_bookmark [jid]"""
+
+ def cb(success):
+ if success:
+ self.information('Bookmark deleted', 'Info')
+ else:
+ self.information('Error while deleting the bookmark', 'Error')
+
+ if not args:
+ tab = self.current_tab()
+ if isinstance(tab, tabs.MucTab) and self.bookmarks[tab.name]:
+ self.bookmarks.remove(tab.name)
+ self.bookmarks.save(self.xmpp, callback=cb)
+ else:
+ self.information('No bookmark to remove', 'Info')
+ else:
+ if self.bookmarks[args[0]]:
+ self.bookmarks.remove(args[0])
+ self.bookmarks.save(self.xmpp, callback=cb)
+ else:
+ self.information('No bookmark to remove', 'Info')
+
+@command_args_parser.quoted(0, 3)
+def command_set(self, args):
+ """
+ /set [module|][section] <option> [value]
+ """
+ if args is None or len(args) == 0:
+ config_dict = config.to_dict()
+ lines = []
+ theme = get_theme()
+ for section_name, section in config_dict.items():
+ lines.append('\x19%(section_col)s}[%(section)s]\x19o' %
+ {
+ 'section': section_name,
+ 'section_col': dump_tuple(theme.COLOR_INFORMATION_TEXT),
+ })
+ for option_name, option_value in section.items():
+ lines.append('%s\x19%s}=\x19o%s' % (option_name,
+ dump_tuple(theme.COLOR_REVISIONS_MESSAGE),
+ option_value))
+ info = ('Current options:\n%s' % '\n'.join(lines), 'Info')
+ elif len(args) == 1:
+ option = args[0]
+ value = config.get(option)
+ if value is None and '=' in option:
+ args = option.split('=', 1)
+ info = ('%s=%s' % (option, value), 'Info')
+ if len(args) == 2:
+ if '|' in args[0]:
+ plugin_name, section = args[0].split('|')[:2]
+ if not section:
+ section = plugin_name
+ option = args[1]
+ if not plugin_name in self.plugin_manager.plugins:
+ file_name = self.plugin_manager.plugins_conf_dir
+ file_name = os.path.join(file_name, plugin_name + '.cfg')
+ plugin_config = PluginConfig(file_name, plugin_name)
+ else:
+ plugin_config = self.plugin_manager.plugins[plugin_name].config
+ value = plugin_config.get(option, default='', section=section)
+ info = ('%s=%s' % (option, value), 'Info')
+ else:
+ possible_section = args[0]
+ if config.has_section(possible_section):
+ section = possible_section
+ option = args[1]
+ value = config.get(option, section=section)
+ info = ('%s=%s' % (option, value), 'Info')
+ else:
+ option = args[0]
+ value = args[1]
+ info = config.set_and_save(option, value)
+ self.trigger_configuration_change(option, value)
+ elif len(args) == 3:
+ if '|' in args[0]:
+ plugin_name, section = args[0].split('|')[:2]
+ if not section:
+ section = plugin_name
+ option = args[1]
+ value = args[2]
+ if not plugin_name in self.plugin_manager.plugins:
+ file_name = self.plugin_manager.plugins_conf_dir
+ file_name = os.path.join(file_name, plugin_name + '.cfg')
+ plugin_config = PluginConfig(file_name, plugin_name)
+ else:
+ plugin_config = self.plugin_manager.plugins[plugin_name].config
+ info = plugin_config.set_and_save(option, value, section)
+ else:
+ if args[0] == '.':
+ name = safeJID(self.current_tab().name).bare
+ if not name:
+ self.information('Invalid tab to use the "." argument.',
+ 'Error')
+ return
+ section = name
+ else:
+ section = args[0]
+ option = args[1]
+ value = args[2]
+ info = config.set_and_save(option, value, section)
+ self.trigger_configuration_change(option, value)
+ elif len(args) > 3:
+ return self.command_help('set')
+ self.information(*info)
+
+@command_args_parser.quoted(1, 2)
+def command_set_default(self, args):
+ """
+ /set_default [section] <option>
+ """
+ if len(args) == 1:
+ option = args[0]
+ section = 'Poezio'
+ elif len(args) == 2:
+ section = args[0]
+ option = args[1]
+ else:
+ return self.command_help('set_default')
+
+ default_config = DEFAULT_CONFIG.get(section, tuple())
+ if option not in default_config:
+ info = ("Option %s has no default value" % (option), "Error")
+ return self.information(*info)
+ self.command_set('%s %s %s' % (section, option, default_config[option]))
+
+@command_args_parser.quoted(1)
+def command_toggle(self, args):
+ """
+ /toggle <option>
+ shortcut for /set <option> toggle
+ """
+ if args is None:
+ return self.command_help('toggle')
+
+ if args[0]:
+ self.command_set('%s toggle' % args[0])
+
+@command_args_parser.quoted(1, 1)
+def command_server_cycle(self, args):
+ """
+ Do a /cycle on each room of the given server.
+ If none, do it on the current tab
+ """
+ tab = self.current_tab()
+ message = ""
+ if args:
+ domain = args[0]
+ if len(args) == 2:
+ message = args[1]
+ else:
+ if isinstance(tab, tabs.MucTab):
+ domain = safeJID(tab.name).domain
+ else:
+ return self.information("No server specified", "Error")
+ for tab in self.get_tabs(tabs.MucTab):
+ if tab.name.endswith(domain):
+ if tab.joined:
+ muc.leave_groupchat(tab.core.xmpp,
+ tab.name,
+ tab.own_nick,
+ message)
+ tab.joined = False
+ if tab.name == domain:
+ self.command_join('"@%s/%s"' %(tab.name, tab.own_nick))
+ else:
+ self.command_join('"%s/%s"' %(tab.name, tab.own_nick))
+
+@command_args_parser.quoted(1)
+def command_last_activity(self, args):
+ """
+ /last_activity <jid>
+ """
+ def callback(iq):
+ "Callback for the last activity"
+ if iq['type'] != 'result':
+ if iq['error']['type'] == 'auth':
+ self.information('You are not allowed to see the '
+ 'activity of this contact.',
+ 'Error')
+ else:
+ self.information('Error retrieving the activity', 'Error')
+ return
+ seconds = iq['last_activity']['seconds']
+ status = iq['last_activity']['status']
+ from_ = iq['from']
+ if not safeJID(from_).user:
+ msg = 'The uptime of %s is %s.' % (
+ from_,
+ common.parse_secs_to_str(seconds))
+ else:
+ msg = 'The last activity of %s was %s ago%s' % (
+ from_,
+ common.parse_secs_to_str(seconds),
+ (' and his/her last status was %s' % status) if status else '')
+ self.information(msg, 'Info')
+
+ if args is None:
+ return self.command_help('last_activity')
+ jid = safeJID(args[0])
+ self.xmpp.plugin['xep_0012'].get_last_activity(jid,
+ callback=callback)
+
+@command_args_parser.quoted(0, 2)
+def command_mood(self, args):
+ """
+ /mood [<mood> [text]]
+ """
+ if not args:
+ return self.xmpp.plugin['xep_0107'].stop()
+
+ mood = args[0]
+ if mood not in pep.MOODS:
+ return self.information('%s is not a correct value for a mood.'
+ % mood,
+ 'Error')
+ if len(args) == 2:
+ text = args[1]
+ else:
+ text = None
+ self.xmpp.plugin['xep_0107'].publish_mood(mood, text,
+ callback=dumb_callback)
+
+@command_args_parser.quoted(0, 3)
+def command_activity(self, args):
+ """
+ /activity [<general> [specific] [text]]
+ """
+ length = len(args)
+ if not length:
+ return self.xmpp.plugin['xep_0108'].stop()
+
+ general = args[0]
+ if general not in pep.ACTIVITIES:
+ return self.information('%s is not a correct value for an activity'
+ % general,
+ 'Error')
+ specific = None
+ text = None
+ if length == 2:
+ if args[1] in pep.ACTIVITIES[general]:
+ specific = args[1]
+ else:
+ text = args[1]
+ elif length == 3:
+ specific = args[1]
+ text = args[2]
+ if specific and specific not in pep.ACTIVITIES[general]:
+ return self.information('%s is not a correct value '
+ 'for an activity' % specific,
+ 'Error')
+ self.xmpp.plugin['xep_0108'].publish_activity(general, specific, text,
+ callback=dumb_callback)
+
+@command_args_parser.quoted(0, 2)
+def command_gaming(self, args):
+ """
+ /gaming [<game name> [server address]]
+ """
+ if not args:
+ return self.xmpp.plugin['xep_0196'].stop()
+
+ name = args[0]
+ if len(args) > 1:
+ address = args[1]
+ else:
+ address = None
+ return self.xmpp.plugin['xep_0196'].publish_gaming(name=name,
+ server_address=address,
+ callback=dumb_callback)
+
+@command_args_parser.quoted(2, 1, [None])
+def command_invite(self, args):
+ """/invite <to> <room> [reason]"""
+
+ if args is None:
+ return self.command_help('invite')
+
+ reason = args[2]
+ to = safeJID(args[0])
+ room = safeJID(args[1]).bare
+ self.invite(to.full, room, reason=reason)
+ self.information('Invited %s to %s' % (to.bare, room), 'Info')
+
+@command_args_parser.quoted(1, 1, [''])
+def command_decline(self, args):
+ """/decline <room@server.tld> [reason]"""
+ if args is None:
+ return self.command_help('decline')
+ jid = safeJID(args[0])
+ if jid.bare not in self.pending_invites:
+ return
+ reason = args[1]
+ del self.pending_invites[jid.bare]
+ self.xmpp.plugin['xep_0045'].decline_invite(jid.bare,
+ self.pending_invites[jid.bare],
+ reason)
+
+### Commands without a completion in this class ###
+
+@command_args_parser.ignored
+def command_invitations(self):
+ """/invitations"""
+ build = ""
+ for invite in self.pending_invites:
+ build += "%s by %s" % (invite,
+ safeJID(self.pending_invites[invite]).bare)
+ if self.pending_invites:
+ build = "You are invited to the following rooms:\n" + build
+ else:
+ build = "You do not have any pending invitations."
+ self.information(build, 'Info')
+
+@command_args_parser.quoted(0, 1, [None])
+def command_quit(self, args):
+ """
+ /quit [message]
+ """
+ if not self.xmpp.is_connected():
+ self.exit()
+ return
+
+ msg = args[0]
+ if config.get('enable_user_mood'):
+ self.xmpp.plugin['xep_0107'].stop()
+ if config.get('enable_user_activity'):
+ self.xmpp.plugin['xep_0108'].stop()
+ if config.get('enable_user_gaming'):
+ self.xmpp.plugin['xep_0196'].stop()
+ self.save_config()
+ self.plugin_manager.disable_plugins()
+ self.disconnect(msg)
+ self.xmpp.add_event_handler("disconnected", self.exit, disposable=True)
+
+@command_args_parser.quoted(0, 1, [''])
+def command_destroy_room(self, args):
+ """
+ /destroy_room [JID]
+ """
+ room = safeJID(args[0]).bare
+ if room:
+ muc.destroy_room(self.xmpp, room)
+ elif isinstance(self.current_tab(), tabs.MucTab) and not args[0]:
+ muc.destroy_room(self.xmpp, self.current_tab().general_jid)
+ else:
+ self.information('Invalid JID: "%s"' % args[0], 'Error')
+
+@command_args_parser.quoted(1, 1, [''])
+def command_bind(self, args):
+ """
+ Bind a key.
+ """
+ if args is None:
+ return self.command_help('bind')
+
+ if not config.silent_set(args[0], args[1], section='bindings'):
+ self.information('Unable to write in the config file', 'Error')
+
+ if args[1]:
+ self.information('%s is now bound to %s' % (args[0], args[1]), 'Info')
+ else:
+ self.information('%s is now unbound' % args[0], 'Info')
+
+@command_args_parser.raw
+def command_rawxml(self, args):
+ """
+ /rawxml <xml stanza>
+ """
+
+ if not args:
+ return
+
+ stanza = args
+ try:
+ stanza = StanzaBase(self.xmpp, xml=ET.fromstring(stanza))
+ if stanza.xml.tag == 'iq' and stanza.xml.attrib.get('type') in ('get', 'set'):
+ iq_id = stanza.xml.attrib.get('id')
+ if not iq_id:
+ iq_id = self.xmpp.new_id()
+ stanza['id'] = iq_id
+
+ def iqfunc(iq):
+ "handler for an iq reply"
+ self.information('%s' % iq, 'Iq')
+ self.xmpp.remove_handler('Iq %s' % iq_id)
+
+ self.xmpp.register_handler(
+ Callback('Iq %s' % iq_id,
+ StanzaPath('iq@id=%s' % iq_id),
+ iqfunc
+ )
+ )
+ log.debug('handler')
+ log.debug('%s %s', stanza.xml.tag, stanza.xml.attrib)
+
+ stanza.send()
+ except:
+ self.information('Could not send custom stanza', 'Error')
+ log.debug('/rawxml: Could not send custom stanza (%s)',
+ repr(stanza),
+ exc_info=True)
+
+
+@command_args_parser.quoted(1, 256)
+def command_load(self, args):
+ """
+ /load <plugin> [<otherplugin> …]
+ # TODO: being able to load more than 256 plugins at once, hihi.
+ """
+ for plugin in args:
+ self.plugin_manager.load(plugin)
+
+@command_args_parser.quoted(1, 256)
+def command_unload(self, args):
+ """
+ /unload <plugin> [<otherplugin> …]
+ """
+ for plugin in args:
+ self.plugin_manager.unload(plugin)
+
+@command_args_parser.ignored
+def command_plugins(self):
+ """
+ /plugins
+ """
+ self.information("Plugins currently in use: %s" %
+ repr(list(self.plugin_manager.plugins.keys())),
+ 'Info')
+
+@command_args_parser.quoted(1, 1)
+def command_message(self, args):
+ """
+ /message <jid> [message]
+ """
+ if args is None:
+ return self.command_help('message')
+ jid = safeJID(args[0])
+ if not jid.user and not jid.domain and not jid.resource:
+ return self.information('Invalid JID.', 'Error')
+ tab = self.get_conversation_by_jid(jid.full, False, fallback_barejid=False)
+ muc = self.get_tab_by_name(jid.bare, typ=tabs.MucTab)
+ if not tab and not muc:
+ tab = self.open_conversation_window(jid.full, focus=True)
+ elif muc:
+ tab = self.get_tab_by_name(jid.full, typ=tabs.PrivateTab)
+ if tab:
+ self.focus_tab_named(tab.name)
+ else:
+ tab = self.open_private_window(jid.bare, jid.resource)
+ else:
+ self.focus_tab_named(tab.name)
+ if len(args) == 2:
+ tab.command_say(args[1])
+
+@command_args_parser.ignored
+def command_xml_tab(self):
+ """/xml_tab"""
+ xml_tab = self.focus_tab_named('XMLTab', tabs.XMLTab)
+ if not xml_tab:
+ tab = tabs.XMLTab()
+ self.add_tab(tab, True)
+ self.xml_tab = tab
+
+@command_args_parser.quoted(1)
+def command_adhoc(self, args):
+ if not args:
+ return self.command_help('ad-hoc')
+ jid = safeJID(args[0])
+ list_tab = tabs.AdhocCommandsListTab(jid)
+ self.add_tab(list_tab, True)
+ cb = list_tab.on_list_received
+ self.xmpp.plugin['xep_0050'].get_commands(jid=jid, local=False,
+ callback=cb)
+
+@command_args_parser.ignored
+def command_self(self):
+ """
+ /self
+ """
+ status = self.get_status()
+ show, message = status.show, status.message
+ nick = self.own_nick
+ jid = self.xmpp.boundjid.full
+ info = ('Your JID is %s\nYour current status is "%s" (%s)'
+ '\nYour default nickname is %s\nYou are running poezio %s' % (
+ jid,
+ message if message else '',
+ show if show else 'available',
+ nick,
+ config_opts.version))
+ self.information(info, 'Info')
+
+
+@command_args_parser.ignored
+def command_reload(self):
+ """
+ /reload
+ """
+ self.reload_config()
+
+def dumb_callback(*args, **kwargs):
+ "mock callback"
+
diff --git a/poezio/core/completions.py b/poezio/core/completions.py
new file mode 100644
index 00000000..9fd44f1b
--- /dev/null
+++ b/poezio/core/completions.py
@@ -0,0 +1,387 @@
+"""
+Completions for the global commands
+"""
+import logging
+
+log = logging.getLogger(__name__)
+
+import os
+from functools import reduce
+
+import common
+import pep
+import tabs
+from common import safeJID
+from config import config
+from roster import roster
+
+from . structs import possible_show
+
+
+def completion_help(self, the_input):
+ """Completion for /help."""
+ commands = sorted(self.commands.keys()) + sorted(self.current_tab().commands.keys())
+ return the_input.new_completion(commands, 1, quotify=False)
+
+
+def completion_status(self, the_input):
+ """
+ Completion of /status
+ """
+ if the_input.get_argument_position() == 1:
+ return the_input.new_completion([status for status in possible_show], 1, ' ', quotify=False)
+
+
+def completion_presence(self, the_input):
+ """
+ Completion of /presence
+ """
+ arg = the_input.get_argument_position()
+ if arg == 1:
+ return the_input.auto_completion([jid for jid in roster.jids()], '', quotify=True)
+ elif arg == 2:
+ return the_input.auto_completion([status for status in possible_show], '', quotify=True)
+
+
+def completion_theme(self, the_input):
+ """ Completion for /theme"""
+ themes_dir = config.get('themes_dir')
+ themes_dir = themes_dir or\
+ os.path.join(os.environ.get('XDG_DATA_HOME') or\
+ os.path.join(os.environ.get('HOME'), '.local', 'share'),
+ 'poezio', 'themes')
+ themes_dir = os.path.expanduser(themes_dir)
+ try:
+ names = os.listdir(themes_dir)
+ except OSError as e:
+ log.error('Completion for /theme failed', exc_info=True)
+ return
+ theme_files = [name[:-3] for name in names if name.endswith('.py') and name != '__init__.py']
+ if not 'default' in theme_files:
+ theme_files.append('default')
+ return the_input.new_completion(theme_files, 1, '', quotify=False)
+
+
+def completion_win(self, the_input):
+ """Completion for /win"""
+ l = []
+ for tab in self.tabs:
+ l.extend(tab.matching_names())
+ l = [i[1] for i in l]
+ return the_input.new_completion(l, 1, '', quotify=False)
+
+
+def completion_join(self, the_input):
+ """
+ Completion for /join
+
+ Try to complete the MUC JID:
+ if only a resource is provided, complete with the default nick
+ if only a server is provided, complete with the rooms from the
+ disco#items of that server
+ if only a nodepart is provided, complete with the servers of the
+ current joined rooms
+ """
+ n = the_input.get_argument_position(quoted=True)
+ args = common.shell_split(the_input.text)
+ if n != 1:
+ # we are not on the 1st argument of the command line
+ return False
+ if len(args) == 1:
+ args.append('')
+ jid = safeJID(args[1])
+ if args[1].endswith('@') and not jid.user and not jid.server:
+ jid.user = args[1][:-1]
+
+ relevant_rooms = []
+ relevant_rooms.extend(sorted(self.pending_invites.keys()))
+ bookmarks = {str(elem.jid): False for elem in self.bookmarks}
+ for tab in self.get_tabs(tabs.MucTab):
+ name = tab.name
+ if name in bookmarks and not tab.joined:
+ bookmarks[name] = True
+ relevant_rooms.extend(sorted(room[0] for room in bookmarks.items() if room[1]))
+
+ if the_input.last_completion:
+ return the_input.new_completion([], 1, quotify=True)
+
+ if jid.user:
+ # we are writing the server: complete the server
+ serv_list = []
+ for tab in self.get_tabs(tabs.MucTab):
+ if tab.joined:
+ serv_list.append('%s@%s'% (jid.user, safeJID(tab.name).host))
+ serv_list.extend(relevant_rooms)
+ return the_input.new_completion(serv_list, 1, quotify=True)
+ elif args[1].startswith('/'):
+ # we completing only a resource
+ return the_input.new_completion(['/%s' % self.own_nick], 1, quotify=True)
+ else:
+ return the_input.new_completion(relevant_rooms, 1, quotify=True)
+
+
+def completion_version(self, the_input):
+ """Completion for /version"""
+ comp = reduce(lambda x, y: x + [i.jid for i in y], (roster[jid].resources for jid in roster.jids() if len(roster[jid])), [])
+ return the_input.new_completion(sorted(comp), 1, quotify=False)
+
+
+def completion_list(self, the_input):
+ """Completion for /list"""
+ muc_serv_list = []
+ for tab in self.get_tabs(tabs.MucTab): # TODO, also from an history
+ if tab.name not in muc_serv_list:
+ muc_serv_list.append(safeJID(tab.name).server)
+ if muc_serv_list:
+ return the_input.new_completion(muc_serv_list, 1, quotify=False)
+
+
+def completion_move_tab(self, the_input):
+ """Completion for /move_tab"""
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
+ nodes = [tab.name for tab in self.tabs if tab]
+ nodes.remove('Roster')
+ return the_input.new_completion(nodes, 1, ' ', quotify=True)
+
+
+def completion_runkey(self, the_input):
+ """
+ Completion for /runkey
+ """
+ list_ = []
+ list_.extend(self.key_func.keys())
+ list_.extend(self.current_tab().key_func.keys())
+ return the_input.new_completion(list_, 1, quotify=False)
+
+
+def completion_bookmark(self, the_input):
+ """Completion for /bookmark"""
+ args = common.shell_split(the_input.text)
+ n = the_input.get_argument_position(quoted=True)
+
+ if n == 2:
+ return the_input.new_completion(['true', 'false'], 2, quotify=True)
+ if n >= 3:
+ return
+
+ if len(args) == 1:
+ args.append('')
+ jid = safeJID(args[1])
+
+ if jid.server and (jid.resource or jid.full.endswith('/')):
+ tab = self.get_tab_by_name(jid.bare, tabs.MucTab)
+ nicks = [tab.own_nick] if tab else []
+ default = os.environ.get('USER') if os.environ.get('USER') else 'poezio'
+ nick = config.get('default_nick')
+ if not nick:
+ if not default in nicks:
+ nicks.append(default)
+ else:
+ if not nick in nicks:
+ nicks.append(nick)
+ jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks]
+ return the_input.new_completion(jids_list, 1, quotify=True)
+ muc_list = [tab.name for tab in self.get_tabs(tabs.MucTab)]
+ muc_list.sort()
+ muc_list.append('*')
+ return the_input.new_completion(muc_list, 1, quotify=True)
+
+
+def completion_remove_bookmark(self, the_input):
+ """Completion for /remove_bookmark"""
+ return the_input.new_completion([bm.jid for bm in self.bookmarks], 1, quotify=False)
+
+
+def completion_decline(self, the_input):
+ """Completion for /decline"""
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
+ return the_input.auto_completion(sorted(self.pending_invites.keys()), 1, '', quotify=True)
+
+
+def completion_bind(self, the_input):
+ n = the_input.get_argument_position()
+ if n == 1:
+ args = [key for key in self.key_func if not key.startswith('_')]
+ elif n == 2:
+ args = [key for key in self.key_func]
+ else:
+ return
+
+ return the_input.new_completion(args, n, '', quotify=False)
+
+
+def completion_message(self, the_input):
+ """Completion for /message"""
+ n = the_input.get_argument_position(quoted=True)
+ if n >= 2:
+ return
+ l = []
+ for jid in roster.jids():
+ if len(roster[jid]):
+ l.append(jid)
+ for resource in roster[jid].resources:
+ l.append(resource.jid)
+ return the_input.new_completion(l, 1, '', quotify=True)
+
+
+def completion_invite(self, the_input):
+ """Completion for /invite"""
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
+ comp = reduce(lambda x, y: x + [i.jid for i in y], (roster[jid].resources for jid in roster.jids() if len(roster[jid])), [])
+ comp = sorted(comp)
+ bares = sorted(roster[contact].bare_jid for contact in roster.jids() if len(roster[contact]))
+ off = sorted(jid for jid in roster.jids() if jid not in bares)
+ comp = comp + bares + off
+ return the_input.new_completion(comp, n, quotify=True)
+ elif n == 2:
+ rooms = []
+ for tab in self.get_tabs(tabs.MucTab):
+ if tab.joined:
+ rooms.append(tab.name)
+ rooms.sort()
+ return the_input.new_completion(rooms, n, '', quotify=True)
+
+
+def completion_activity(self, the_input):
+ """Completion for /activity"""
+ n = the_input.get_argument_position(quoted=True)
+ args = common.shell_split(the_input.text)
+ if n == 1:
+ return the_input.new_completion(sorted(pep.ACTIVITIES.keys()), n, quotify=True)
+ elif n == 2:
+ if args[1] in pep.ACTIVITIES:
+ l = list(pep.ACTIVITIES[args[1]])
+ l.remove('category')
+ l.sort()
+ return the_input.new_completion(l, n, quotify=True)
+
+
+def completion_mood(self, the_input):
+ """Completion for /mood"""
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
+ return the_input.new_completion(sorted(pep.MOODS.keys()), 1, quotify=True)
+
+
+def completion_last_activity(self, the_input):
+ """
+ Completion for /last_activity <jid>
+ """
+ n = the_input.get_argument_position(quoted=False)
+ if n >= 2:
+ return
+ comp = reduce(lambda x, y: x + [i.jid for i in y], (roster[jid].resources for jid in roster.jids() if len(roster[jid])), [])
+ return the_input.new_completion(sorted(comp), 1, '', quotify=False)
+
+
+def completion_server_cycle(self, the_input):
+ """Completion for /server_cycle"""
+ serv_list = set()
+ for tab in self.get_tabs(tabs.MucTab):
+ serv = safeJID(tab.name).server
+ serv_list.add(serv)
+ return the_input.new_completion(sorted(serv_list), 1, ' ')
+
+
+def completion_set(self, the_input):
+ """Completion for /set"""
+ args = common.shell_split(the_input.text)
+ n = the_input.get_argument_position(quoted=True)
+ if n >= len(args):
+ args.append('')
+ if n == 1:
+ if '|' in args[1]:
+ plugin_name, section = args[1].split('|')[:2]
+ if not plugin_name in self.plugin_manager.plugins:
+ return the_input.new_completion([], n, quotify=True)
+ plugin = self.plugin_manager.plugins[plugin_name]
+ end_list = ['%s|%s' % (plugin_name, section) for section in plugin.config.sections()]
+ else:
+ end_list = set(config.options('Poezio'))
+ end_list.update(config.default.get('Poezio', {}))
+ end_list = list(end_list)
+ end_list.sort()
+ elif n == 2:
+ if '|' in args[1]:
+ plugin_name, section = args[1].split('|')[:2]
+ if not plugin_name in self.plugin_manager.plugins:
+ return the_input.new_completion([''], n, quotify=True)
+ plugin = self.plugin_manager.plugins[plugin_name]
+ end_list = set(plugin.config.options(section or plugin_name))
+ if plugin.config.default:
+ end_list.update(plugin.config.default.get(section or plugin_name, {}))
+ end_list = list(end_list)
+ end_list.sort()
+ elif not config.has_option('Poezio', args[1]):
+ if config.has_section(args[1]):
+ end_list = config.options(args[1])
+ end_list.append('')
+ else:
+ end_list = []
+ else:
+ end_list = [str(config.get(args[1], '')), '']
+ elif n == 3:
+ if '|' in args[1]:
+ plugin_name, section = args[1].split('|')[:2]
+ if not plugin_name in self.plugin_manager.plugins:
+ return the_input.new_completion([''], n, quotify=True)
+ plugin = self.plugin_manager.plugins[plugin_name]
+ end_list = [str(plugin.config.get(args[2], '', section or plugin_name)), '']
+ else:
+ if not config.has_section(args[1]):
+ end_list = ['']
+ else:
+ end_list = [str(config.get(args[2], '', args[1])), '']
+ else:
+ return
+ return the_input.new_completion(end_list, n, quotify=True)
+
+
+def completion_set_default(self, the_input):
+ """ Completion for /set_default
+ """
+ args = common.shell_split(the_input.text)
+ n = the_input.get_argument_position(quoted=True)
+ if n >= len(args):
+ args.append('')
+ if n == 1 or (n == 2 and config.has_section(args[1])):
+ return self.completion_set(the_input)
+ return []
+
+
+def completion_toggle(self, the_input):
+ "Completion for /toggle"
+ return the_input.new_completion(config.options('Poezio'), 1, quotify=False)
+
+
+def completion_bookmark_local(self, the_input):
+ """Completion for /bookmark_local"""
+ n = the_input.get_argument_position(quoted=True)
+ args = common.shell_split(the_input.text)
+
+ if n >= 2:
+ return
+ if len(args) == 1:
+ args.append('')
+ jid = safeJID(args[1])
+
+ if jid.server and (jid.resource or jid.full.endswith('/')):
+ tab = self.get_tab_by_name(jid.bare, tabs.MucTab)
+ nicks = [tab.own_nick] if tab else []
+ default = os.environ.get('USER') if os.environ.get('USER') else 'poezio'
+ nick = config.get('default_nick')
+ if not nick:
+ if not default in nicks:
+ nicks.append(default)
+ else:
+ if not nick in nicks:
+ nicks.append(nick)
+ jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks]
+ return the_input.new_completion(jids_list, 1, quotify=True)
+ muc_list = [tab.name for tab in self.get_tabs(tabs.MucTab)]
+ muc_list.append('*')
+ return the_input.new_completion(muc_list, 1, quotify=True)
+
diff --git a/poezio/core/core.py b/poezio/core/core.py
new file mode 100644
index 00000000..f32099f1
--- /dev/null
+++ b/poezio/core/core.py
@@ -0,0 +1,2102 @@
+"""
+Module defining the Core class, which is the central orchestrator
+of poezio and contains the main loop, the list of tabs, sets the state
+of everything; it also contains global commands, completions and event
+handlers but those are defined in submodules in order to avoir cluttering
+this file.
+"""
+import logging
+
+log = logging.getLogger(__name__)
+
+import asyncio
+import shutil
+import curses
+import os
+import pipes
+import sys
+import time
+
+from slixmpp.xmlstream.handler import Callback
+
+import connection
+import decorators
+import events
+import singleton
+import tabs
+import theming
+import timed_events
+import windows
+
+from bookmarks import BookmarkList
+from common import safeJID
+from config import config, firstrun
+from contact import Contact, Resource
+from daemon import Executor
+from fifo import Fifo
+from logger import logger
+from plugin_manager import PluginManager
+from roster import roster
+from size_manager import SizeManager
+from text_buffer import TextBuffer
+from theming import get_theme
+import keyboard
+
+from . import completions
+from . import commands
+from . import handlers
+from . structs import possible_show, DEPRECATED_ERRORS, \
+ ERROR_AND_STATUS_CODES, Command, Status
+
+
+class Core(object):
+ """
+ “Main” class of poezion
+ """
+
+ def __init__(self):
+ # All uncaught exception are given to this callback, instead
+ # of being displayed on the screen and exiting the program.
+ sys.excepthook = self.on_exception
+ self.connection_time = time.time()
+ self.stdscr = None
+ status = config.get('status')
+ status = possible_show.get(status, None)
+ self.status = Status(show=status,
+ message=config.get('status_message'))
+ self.running = True
+ self.xmpp = singleton.Singleton(connection.Connection)
+ self.xmpp.core = self
+ self.keyboard = keyboard.Keyboard()
+ roster.set_node(self.xmpp.client_roster)
+ decorators.refresh_wrapper.core = self
+ self.bookmarks = BookmarkList()
+ self.debug = False
+ self.remote_fifo = None
+ # 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 = config.get('info_win_height', section='var')
+ self.information_win = windows.TextWin(300)
+ self.information_buffer.add_window(self.information_win)
+ self.left_tab_win = None
+
+ self.tab_win = windows.GlobalInfoBar()
+ # Whether the XML tab is opened
+ self.xml_tab = None
+ self.xml_buffer = TextBuffer()
+
+ self.tabs = []
+ self._current_tab_nb = 0
+ self.previous_tab_nb = 0
+
+ own_nick = config.get('default_nick')
+ own_nick = own_nick or self.xmpp.boundjid.user
+ own_nick = own_nick or os.environ.get('USER')
+ own_nick = own_nick or 'poezio'
+ self.own_nick = own_nick
+
+ self.plugins_autoloaded = False
+ self.plugin_manager = PluginManager(self)
+ self.events = events.EventHandler()
+
+ self.size = SizeManager(self, windows.Win)
+
+ # Set to True whenever we consider that we have been disconnected
+ # from the server because of a legitimate reason (bad credentials,
+ # or explicit disconnect from the user for example), in that case we
+ # should not try to auto-reconnect, even if auto_reconnect is true
+ # in the user config.
+ self.legitimate_disconnect = False
+
+ # global commands, available from all tabs
+ # a command is tuple of the form:
+ # (the function executing the command. Takes a string as argument,
+ # a string representing the help message,
+ # a completion function, taking a Input as argument. Can be None)
+ # The completion function should return True if a completion was
+ # made ; False otherwise
+ self.commands = {}
+ self.register_initial_commands()
+
+ # We are invisible
+ if not config.get('send_initial_presence'):
+ del self.commands['status']
+ del self.commands['show']
+
+ # A list of integers. For example if the user presses Alt+j, 2, 1,
+ # we will insert 2, then 1 in that list, and we will finally build
+ # the number 21 and use it with command_win, before clearing the
+ # list.
+ self.room_number_jump = []
+ self.key_func = KeyDict()
+ # Key bindings associated with handlers
+ # and pseudo-keys used to map actions below.
+ key_func = {
+ "KEY_PPAGE": self.scroll_page_up,
+ "KEY_NPAGE": self.scroll_page_down,
+ "^B": self.scroll_line_up,
+ "^F": self.scroll_line_down,
+ "^X": self.scroll_half_down,
+ "^S": self.scroll_half_up,
+ "KEY_F(5)": self.rotate_rooms_left,
+ "^P": self.rotate_rooms_left,
+ "M-[-D": self.rotate_rooms_left,
+ 'kLFT3': self.rotate_rooms_left,
+ "KEY_F(6)": self.rotate_rooms_right,
+ "^N": self.rotate_rooms_right,
+ "M-[-C": self.rotate_rooms_right,
+ 'kRIT3': self.rotate_rooms_right,
+ "KEY_F(4)": self.toggle_left_pane,
+ "KEY_F(7)": self.shrink_information_win,
+ "KEY_F(8)": self.grow_information_win,
+ "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,
+ '^L': self.full_screen_redraw,
+ 'M-j': self.go_to_room_number,
+ 'M-D': self.scroll_info_up,
+ 'M-C': self.scroll_info_down,
+ 'M-k': self.escape_next_key,
+ ######## actions mappings ##########
+ '_bookmark': self.command_bookmark,
+ '_bookmark_local': self.command_bookmark_local,
+ '_close_tab': self.close_tab,
+ '_disconnect': self.disconnect,
+ '_quit': self.command_quit,
+ '_redraw_screen': self.full_screen_redraw,
+ '_reload_theme': self.command_theme,
+ '_remove_bookmark': self.command_remove_bookmark,
+ '_room_left': self.rotate_rooms_left,
+ '_room_right': self.rotate_rooms_right,
+ '_show_roster': self.go_to_roster,
+ '_scroll_down': self.scroll_page_down,
+ '_scroll_up': self.scroll_page_up,
+ '_scroll_info_up': self.scroll_info_up,
+ '_scroll_info_down': self.scroll_info_down,
+ '_server_cycle': self.command_server_cycle,
+ '_show_bookmarks': self.command_bookmarks,
+ '_show_important_room': self.go_to_important_room,
+ '_show_invitations': self.command_invitations,
+ '_show_plugins': self.command_plugins,
+ '_show_xmltab': self.command_xml_tab,
+ '_toggle_pane': self.toggle_left_pane,
+ ###### status actions ######
+ '_available': lambda: self.command_status('available'),
+ '_away': lambda: self.command_status('away'),
+ '_chat': lambda: self.command_status('chat'),
+ '_dnd': lambda: self.command_status('dnd'),
+ '_xa': lambda: self.command_status('xa'),
+ ##### Custom actions ########
+ '_exc_': self.try_execute,
+ }
+ self.key_func.update(key_func)
+
+ # Add handlers
+ self.xmpp.add_event_handler('connecting', self.on_connecting)
+ self.xmpp.add_event_handler('connected', self.on_connected)
+ self.xmpp.add_event_handler('connection_failed', self.on_failed_connection)
+ self.xmpp.add_event_handler('disconnected', self.on_disconnected)
+ self.xmpp.add_event_handler('stream_error', self.on_stream_error)
+ self.xmpp.add_event_handler('failed_all_auth', self.on_failed_all_auth)
+ self.xmpp.add_event_handler('no_auth', self.on_no_auth)
+ self.xmpp.add_event_handler("session_start", self.on_session_start)
+ self.xmpp.add_event_handler("session_start",
+ self.on_session_start_features)
+ 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("groupchat_invite",
+ self.on_groupchat_invitation)
+ self.xmpp.add_event_handler("groupchat_direct_invite",
+ self.on_groupchat_direct_invitation)
+ self.xmpp.add_event_handler("groupchat_decline",
+ self.on_groupchat_decline)
+ self.xmpp.add_event_handler("groupchat_config_status",
+ self.on_status_codes)
+ self.xmpp.add_event_handler("groupchat_subject",
+ self.on_groupchat_subject)
+ self.xmpp.add_event_handler("message", self.on_message)
+ self.xmpp.add_event_handler("message_error", self.on_error_message)
+ self.xmpp.add_event_handler("receipt_received", self.on_receipt)
+ 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.xmpp.add_event_handler("presence_error", self.on_presence_error)
+ self.xmpp.add_event_handler("roster_subscription_request",
+ self.on_subscription_request)
+ self.xmpp.add_event_handler("roster_subscription_authorized",
+ self.on_subscription_authorized)
+ self.xmpp.add_event_handler("roster_subscription_remove",
+ self.on_subscription_remove)
+ self.xmpp.add_event_handler("roster_subscription_removed",
+ self.on_subscription_removed)
+ self.xmpp.add_event_handler("message_xform", self.on_data_form)
+ self.xmpp.add_event_handler("chatstate_active",
+ self.on_chatstate_active)
+ self.xmpp.add_event_handler("chatstate_composing",
+ self.on_chatstate_composing)
+ self.xmpp.add_event_handler("chatstate_paused",
+ self.on_chatstate_paused)
+ self.xmpp.add_event_handler("chatstate_gone",
+ self.on_chatstate_gone)
+ self.xmpp.add_event_handler("chatstate_inactive",
+ self.on_chatstate_inactive)
+ self.xmpp.add_event_handler("attention", self.on_attention)
+ self.xmpp.add_event_handler("ssl_cert", self.validate_ssl)
+ self.xmpp.add_event_handler("ssl_invalid_chain", self.ssl_invalid_chain)
+ self.xmpp.add_event_handler('carbon_received', self.on_carbon_received)
+ self.xmpp.add_event_handler('carbon_sent', self.on_carbon_sent)
+
+ self.all_stanzas = Callback('custom matcher',
+ connection.MatchAll(None),
+ self.incoming_stanza)
+ self.xmpp.register_handler(self.all_stanzas)
+ if config.get('enable_user_tune'):
+ self.xmpp.add_event_handler("user_tune_publish",
+ self.on_tune_event)
+ if config.get('enable_user_nick'):
+ self.xmpp.add_event_handler("user_nick_publish",
+ self.on_nick_received)
+ if config.get('enable_user_mood'):
+ self.xmpp.add_event_handler("user_mood_publish",
+ self.on_mood_event)
+ if config.get('enable_user_activity'):
+ self.xmpp.add_event_handler("user_activity_publish",
+ self.on_activity_event)
+ if config.get('enable_user_gaming'):
+ self.xmpp.add_event_handler("user_gaming_publish",
+ self.on_gaming_event)
+
+ self.initial_joins = []
+
+ self.connected_events = {}
+
+ self.pending_invites = {}
+
+ # a dict of the form {'config_option': [list, of, callbacks]}
+ # Whenever a configuration option is changed (using /set or by
+ # reloading a new config using a signal), all the associated
+ # callbacks are triggered.
+ # Use Core.add_configuration_handler("option", callback) to add a
+ # handler
+ # Note that the callback will be called when it’s changed in the
+ # global section, OR in a special section.
+ # As a special case, handlers can be associated with the empty
+ # string option (""), they will be called for every option change
+ # The callback takes two argument: the config option, and the new
+ # value
+ self.configuration_change_handlers = {"": []}
+ self.add_configuration_handler("create_gaps",
+ self.on_gaps_config_change)
+ self.add_configuration_handler("request_message_receipts",
+ self.on_request_receipts_config_change)
+ self.add_configuration_handler("ack_message_receipts",
+ self.on_ack_receipts_config_change)
+ self.add_configuration_handler("plugins_dir",
+ self.on_plugins_dir_config_change)
+ self.add_configuration_handler("plugins_conf_dir",
+ self.on_plugins_conf_dir_config_change)
+ self.add_configuration_handler("connection_timeout_delay",
+ self.xmpp.set_keepalive_values)
+ self.add_configuration_handler("connection_check_interval",
+ self.xmpp.set_keepalive_values)
+ self.add_configuration_handler("themes_dir",
+ theming.update_themes_dir)
+ self.add_configuration_handler("theme",
+ self.on_theme_config_change)
+ self.add_configuration_handler("use_bookmarks_method",
+ self.on_bookmarks_method_config_change)
+ self.add_configuration_handler("password",
+ self.on_password_change)
+ self.add_configuration_handler("enable_vertical_tab_list",
+ self.on_vertical_tab_list_config_change)
+ self.add_configuration_handler("vertical_tab_list_size",
+ self.on_vertical_tab_list_config_change)
+ self.add_configuration_handler("deterministic_nick_colors",
+ self.on_nick_determinism_changed)
+ self.add_configuration_handler("enable_carbons",
+ self.on_carbons_switch)
+ self.add_configuration_handler("hide_user_list",
+ self.on_hide_user_list_change)
+
+ self.add_configuration_handler("", self.on_any_config_change)
+
+ def on_any_config_change(self, option, value):
+ """
+ Update the roster, in case a roster option changed.
+ """
+ roster.modified()
+
+ def add_configuration_handler(self, option, callback):
+ """
+ Add a callback, associated with the given option. It will be called
+ each time the configuration option is changed using /set or by
+ reloading the configuration with a signal
+ """
+ if option not in self.configuration_change_handlers:
+ self.configuration_change_handlers[option] = []
+ self.configuration_change_handlers[option].append(callback)
+
+ def trigger_configuration_change(self, option, value):
+ """
+ Triggers all the handlers associated with the given configuration
+ option
+ """
+ # First call the callbacks associated with any configuration change
+ for callback in self.configuration_change_handlers[""]:
+ callback(option, value)
+ # and then the callbacks associated with this specific option, if
+ # any
+ if option not in self.configuration_change_handlers:
+ return
+ for callback in self.configuration_change_handlers[option]:
+ callback(option, value)
+
+ def on_hide_user_list_change(self, option, value):
+ """
+ Called when the hide_user_list option changes
+ """
+ self.call_for_resize()
+
+ def on_bookmarks_method_config_change(self, option, value):
+ """
+ Called when the use_bookmarks_method option changes
+ """
+ if value not in ('pep', 'privatexml'):
+ return
+ self.bookmarks.preferred = value
+ self.bookmarks.save(self.xmpp, core=self)
+
+ def on_gaps_config_change(self, option, value):
+ """
+ Called when the option create_gaps is changed.
+ Remove all gaptabs if switching from gaps to nogaps.
+ """
+ if value.lower() == "false":
+ self.tabs = list(tab for tab in self.tabs if tab)
+
+ def on_request_receipts_config_change(self, option, value):
+ """
+ Called when the request_message_receipts option changes
+ """
+ self.xmpp.plugin['xep_0184'].auto_request = config.get(option,
+ default=True)
+
+ def on_ack_receipts_config_change(self, option, value):
+ """
+ Called when the ack_message_receipts option changes
+ """
+ self.xmpp.plugin['xep_0184'].auto_ack = config.get(option, default=True)
+
+ def on_plugins_dir_config_change(self, option, value):
+ """
+ Called when the plugins_dir option is changed
+ """
+ path = os.path.expanduser(value)
+ self.plugin_manager.on_plugins_dir_change(path)
+
+ def on_vertical_tab_list_config_change(self, option, value):
+ """
+ Called when the enable_vertical_tab_list option is changed
+ """
+ self.call_for_resize()
+
+ def on_plugins_conf_dir_config_change(self, option, value):
+ """
+ Called when the plugins_conf_dir option is changed
+ """
+ path = os.path.expanduser(value)
+ self.plugin_manager.on_plugins_conf_dir_change(path)
+
+ def on_theme_config_change(self, option, value):
+ """
+ Called when the theme option is changed
+ """
+ error_msg = theming.reload_theme()
+ if error_msg:
+ self.information(error_msg, 'Warning')
+ self.refresh_window()
+
+ def on_password_change(self, option, value):
+ """
+ Set the new password in the slixmpp.ClientXMPP object
+ """
+ self.xmpp.password = value
+
+
+ def on_nick_determinism_changed(self, option, value):
+ """If we change the value to true, we call /recolor on all the MucTabs, to
+ make the current nick colors reflect their deterministic value.
+ """
+ if value.lower() == "true":
+ for tab in self.get_tabs(tabs.MucTab):
+ tab.command_recolor('')
+
+ def on_carbons_switch(self, option, value):
+ """Whenever the user enables or disables carbons using /set, we should
+ inform the server immediately, this way we do not require a restart
+ for the change to take effect
+ """
+ if value:
+ self.xmpp.plugin['xep_0280'].enable()
+ else:
+ self.xmpp.plugin['xep_0280'].disable()
+
+ def reload_config(self):
+ # reload all log files
+ log.debug("Reloading the log files…")
+ logger.reload_all()
+ log.debug("Log files reloaded.")
+ # reload the theme
+ log.debug("Reloading the theme…")
+ theming.reload_theme()
+ log.debug("Theme reloaded.")
+ # reload the config from the disk
+ log.debug("Reloading the config…")
+ # Copy the old config in a dict
+ old_config = config.to_dict()
+ config.read_file()
+ # Compare old and current config, to trigger the callbacks of all
+ # modified options
+ for section in config.sections():
+ old_section = old_config.get(section, {})
+ for option in config.options(section):
+ old_value = old_section.get(option)
+ new_value = config.get(option, default="", section=section)
+ if new_value != old_value:
+ self.trigger_configuration_change(option, new_value)
+ log.debug("Config reloaded.")
+ # in case some roster options have changed
+ roster.modified()
+
+ def sigusr_handler(self, num, stack):
+ """
+ Handle SIGUSR1 (10)
+ When caught, reload all the possible files.
+ """
+ log.debug("SIGUSR1 caught, reloading the files…")
+ self.reload_config()
+
+ def exit_from_signal(self, *args, **kwargs):
+ """
+ Quit when receiving SIGHUP or SIGTERM or SIGPIPE
+
+ do not save the config because it is not a normal exit
+ (and only roster UI things are not yet saved)
+ """
+ sig = args[0]
+ signals = {
+ 1: 'SIGHUP',
+ 13: 'SIGPIPE',
+ 15: 'SIGTERM',
+ }
+
+ log.error("%s received. Exiting…", signals[sig])
+ if config.get('enable_user_mood'):
+ self.xmpp.plugin['xep_0107'].stop()
+ if config.get('enable_user_activity'):
+ self.xmpp.plugin['xep_0108'].stop()
+ if config.get('enable_user_gaming'):
+ self.xmpp.plugin['xep_0196'].stop()
+ self.plugin_manager.disable_plugins()
+ self.disconnect('%s received' % signals.get(sig))
+ self.xmpp.add_event_handler("disconnected", self.exit, disposable=True)
+
+ def autoload_plugins(self):
+ """
+ Load the plugins on startup.
+ """
+ plugins = config.get('plugins_autoload')
+ if ':' in plugins:
+ for plugin in plugins.split(':'):
+ self.plugin_manager.load(plugin)
+ else:
+ for plugin in plugins.split():
+ self.plugin_manager.load(plugin)
+ self.plugins_autoloaded = True
+
+ def start(self):
+ """
+ Init curses, create the first tab, etc
+ """
+ self.stdscr = curses.initscr()
+ self.init_curses(self.stdscr)
+ self.call_for_resize()
+ default_tab = tabs.RosterInfoTab()
+ default_tab.on_gain_focus()
+ self.tabs.append(default_tab)
+ self.information('Welcome to poezio!', 'Info')
+ if firstrun:
+ self.information(
+ 'It seems that it is the first time you start poezio.\n'
+ 'The online help is here http://doc.poez.io/\n'
+ 'No room is joined by default, but you can join poezio’s'
+ ' chatroom (with /join poezio@muc.poez.io), where you can'
+ ' ask for help or tell us how great it is.',
+ 'Help')
+ self.refresh_window()
+ self.xmpp.plugin['xep_0012'].begin_idle(jid=self.xmpp.boundjid)
+
+ def exit(self, event=None):
+ log.debug("exit(%s)" % (event,))
+ asyncio.get_event_loop().stop()
+
+ def on_exception(self, typ, value, trace):
+ """
+ When an exception is raised, just reset curses and call
+ the original exception handler (will nicely print the traceback)
+ """
+ try:
+ self.reset_curses()
+ except:
+ pass
+ sys.__excepthook__(typ, value, trace)
+
+ def sigwinch_handler(self):
+ """A work-around for ncurses resize stuff, which sucks. Normally, ncurses
+ catches SIGWINCH itself. In its signal handler, it updates the
+ windows structures (for example the size, etc) and it
+ ungetch(KEY_RESIZE). That way, the next time we call getch() we know
+ that a resize occured and we can act on it. BUT poezio doesn’t call
+ getch() until it knows it will return something. The problem is we
+ can’t know that, because stdin is not affected by this KEY_RESIZE
+ value (it is only inserted in a ncurses internal fifo that we can’t
+ access).
+
+ The (ugly) solution is to handle SIGWINCH ourself, trigger the
+ change of the internal windows sizes stored in ncurses module, using
+ sizes that we get using shutil, ungetch the KEY_RESIZE value and
+ then call getch to handle the resize on poezio’s side properly.
+ """
+ size = shutil.get_terminal_size()
+ curses.resizeterm(size.lines, size.columns)
+ curses.ungetch(curses.KEY_RESIZE)
+ self.on_input_readable()
+
+ def on_input_readable(self):
+ """
+ main loop waiting for the user to press a key
+ """
+ def replace_line_breaks(key):
+ "replace ^J with \n"
+ if key == '^J':
+ return '\n'
+ return key
+ def separate_chars_from_bindings(char_list):
+ """
+ returns a list of lists. For example if you give
+ ['a', 'b', 'KEY_BACKSPACE', 'n', 'u'], this function returns
+ [['a', 'b'], ['KEY_BACKSPACE'], ['n', 'u']]
+
+ This way, in case of lag (for example), we handle the typed text
+ by “batch” as much as possible (instead of one char at a time,
+ which implies a refresh after each char, which is very slow),
+ but we still handle the special chars (backspaces, arrows,
+ ctrl+x ou alt+x, etc) one by one, which avoids the issue of
+ printing them OR ignoring them in that case. This should
+ resolve the “my ^W are ignored when I lag ;(”.
+ """
+ res = []
+ current = []
+ for char in char_list:
+ assert char
+ # Transform that stupid char into what we actually meant
+ if char == '\x1f':
+ char = '^/'
+ if len(char) == 1:
+ current.append(char)
+ else:
+ # special case for the ^I key, it’s considered as \t
+ # only when pasting some text, otherwise that’s the ^I
+ # (or M-i) key, which stands for completion by default.
+ if char == '^I' and len(char_list) != 1:
+ current.append('\t')
+ continue
+ if current:
+ res.append(current)
+ current = []
+ res.append([char])
+ if current:
+ res.append(current)
+ return res
+
+ log.debug("Input is readable.")
+ big_char_list = [replace_key_with_bound(key)\
+ for key in self.read_keyboard()]
+ log.debug("Got from keyboard: %s", (big_char_list,))
+
+ # whether to refresh after ALL keys have been handled
+ for char_list in separate_chars_from_bindings(big_char_list):
+ # Special case for M-x where x is a number
+ if len(char_list) == 1:
+ char = char_list[0]
+ if char.startswith('M-') and len(char) == 3:
+ try:
+ nb = int(char[2])
+ except ValueError:
+ pass
+ else:
+ if self.current_tab().nb == nb and config.get('go_to_previous_tab_on_alt_number'):
+ self.go_to_previous_tab()
+ else:
+ self.command_win('%d' % nb)
+ # search for keyboard shortcut
+ func = self.key_func.get(char, None)
+ if func:
+ func()
+ else:
+ self.do_command(replace_line_breaks(char), False)
+ else:
+ self.do_command(''.join(char_list), True)
+ if self.status.show not in ('xa', 'away'):
+ self.xmpp.plugin['xep_0319'].idle()
+ self.doupdate()
+
+ def save_config(self):
+ """
+ Save config in the file just before exit
+ """
+ ok = roster.save_to_config_file()
+ ok = ok and config.silent_set('info_win_height',
+ self.information_win_size,
+ 'var')
+ if not ok:
+ self.information('Unable to save runtime preferences'
+ ' in the config file',
+ 'Error')
+
+ def on_roster_enter_key(self, roster_row):
+ """
+ when enter is pressed on the roster window
+ """
+ if isinstance(roster_row, Contact):
+ if not self.get_conversation_by_jid(roster_row.bare_jid, False):
+ self.open_conversation_window(roster_row.bare_jid)
+ else:
+ self.focus_tab_named(roster_row.bare_jid)
+ if isinstance(roster_row, Resource):
+ if not self.get_conversation_by_jid(roster_row.jid,
+ False,
+ fallback_barejid=False):
+ self.open_conversation_window(roster_row.jid)
+ else:
+ self.focus_tab_named(roster_row.jid)
+ self.refresh_window()
+
+ def get_conversation_messages(self):
+ """
+ Returns a list of all the messages in the current chat.
+ If the current tab is not a ChatTab, returns None.
+
+ Messages are namedtuples of the form
+ ('txt nick_color time str_time nickname user')
+ """
+ if not isinstance(self.current_tab(), tabs.ChatTab):
+ return None
+ return self.current_tab().get_conversation_messages()
+
+ def insert_input_text(self, text):
+ """
+ Insert the given text into the current input
+ """
+ self.do_command(text, True)
+
+
+##################### Anything related to command execution ###################
+
+ def execute(self, line):
+ """
+ Execute the /command or just send the line on the current room
+ """
+ if line == "":
+ return
+ if line.startswith('/'):
+ 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 self.commands:
+ func = self.commands[command][0]
+ func(arg)
+ return
+ else:
+ self.information("Unknown command (%s)" % (command),
+ 'Error')
+
+ def exec_command(self, command):
+ """
+ Execute an external command on the local or a remote machine,
+ depending on the conf. For example, to open a link in a browser, do
+ exec_command(["firefox", "http://poezio.eu"]), and this will call
+ the command on the correct computer.
+
+ The command argument is a list of strings, not quoted or escaped in
+ any way. The escaping is done here if needed.
+
+ The remote execution is done
+ by writing the command on a fifo. That fifo has to be on the
+ machine where poezio is running, and accessible (through sshfs for
+ example) from the local machine (where poezio is not running). A
+ very simple daemon (daemon.py) reads on that fifo, and executes any
+ command that is read in it. Since we can only write strings to that
+ fifo, each argument has to be pipes.quote()d. That way the
+ shlex.split on the reading-side of the daemon will be safe.
+
+ You cannot use a real command line with pipes, redirections etc, but
+ this function supports a simple case redirection to file: if the
+ before-last argument of the command is ">" or ">>", then the last
+ argument is considered to be a filename where the command stdout
+ will be written. For example you can do exec_command(["echo",
+ "coucou les amis coucou coucou", ">", "output.txt"]) and this will
+ work. If you try to do anything else, your |, [, <<, etc will be
+ interpreted as normal command arguments, not shell special tokens.
+ """
+ if config.get('exec_remote'):
+ # We just write the command in the fifo
+ fifo_path = config.get('remote_fifo_path')
+ if not self.remote_fifo:
+ try:
+ self.remote_fifo = Fifo(os.path.join(fifo_path,
+ 'poezio.fifo'),
+ 'w')
+ except (OSError, IOError) as exc:
+ log.error('Could not open the fifo for writing (%s)',
+ os.path.join(fifo_path, './', 'poezio.fifo'),
+ exc_info=True)
+ self.information('Could not open the fifo '
+ 'file for writing: %s' % exc,
+ 'Error')
+ return
+
+ args = (pipes.quote(arg.replace('\n', ' ')) for arg in command)
+ command_str = ' '.join(args) + '\n'
+ try:
+ self.remote_fifo.write(command_str)
+ except (IOError) as exc:
+ log.error('Could not write in the fifo (%s): %s',
+ os.path.join(fifo_path, './', 'poezio.fifo'),
+ repr(command),
+ exc_info=True)
+ self.information('Could not execute %s: %s' % (command, exc),
+ 'Error')
+ self.remote_fifo = None
+ else:
+ executor = Executor(command)
+ try:
+ executor.start()
+ except ValueError as exc:
+ log.error('Could not execute command (%s)',
+ repr(command),
+ exc_info=True)
+ self.information('%s' % exc, 'Error')
+
+
+ def do_command(self, key, raw):
+ """
+ Execute the action associated with a key
+
+ Or if keyboard.continuation_keys_callback is set, call it instead. See
+ the comment of this variable.
+ """
+ if not key:
+ return
+ if keyboard.continuation_keys_callback is not None:
+ # Reset the callback to None BEFORE calling it, because this
+ # callback MAY set a new callback itself, and we don’t want to
+ # erase it in that case
+ cb = keyboard.continuation_keys_callback
+ keyboard.continuation_keys_callback = None
+ cb(key)
+ else:
+ self.current_tab().on_input(key, raw)
+
+
+ def try_execute(self, line):
+ """
+ Try to execute a command in the current tab
+ """
+ line = '/' + line
+ try:
+ self.current_tab().execute_command(line)
+ except:
+ log.error('Execute failed (%s)', line, exc_info=True)
+
+
+########################## TImed Events #######################################
+
+ def remove_timed_event(self, event):
+ """Remove an existing timed event"""
+ event.handler.cancel()
+
+ def add_timed_event(self, event):
+ """Add a new timed event"""
+ event.handler = asyncio.get_event_loop().call_later(event.delay,
+ event.callback,
+ *event.args)
+
+####################### XMPP-related actions ##################################
+
+ def get_status(self):
+ """
+ Get the last status that was previously set
+ """
+ return self.status
+
+ def set_status(self, pres, msg):
+ """
+ Set our current status so we can remember
+ it and use it back when needed (for example to display it
+ or to use it when joining a new muc)
+ """
+ self.status = Status(show=pres, message=msg)
+ if config.get('save_status'):
+ ok = config.silent_set('status', pres if pres else '')
+ msg = msg.replace('\n', '|') if msg else ''
+ ok = ok and config.silent_set('status_message', msg)
+ if not ok:
+ self.information('Unable to save the status in '
+ 'the config file', 'Error')
+
+ def get_bookmark_nickname(self, room_name):
+ """
+ Returns the nickname associated with a bookmark
+ or the default nickname
+ """
+ bm = self.bookmarks[room_name]
+ if bm:
+ return bm.nick
+ return self.own_nick
+
+ def disconnect(self, msg='', reconnect=False):
+ """
+ Disconnect from remote server and correctly set the states of all
+ parts of the client (for example, set the MucTabs as not joined, etc)
+ """
+ self.legitimate_disconnect = True
+ msg = msg or ''
+ for tab in self.get_tabs(tabs.MucTab):
+ tab.command_part(msg)
+ self.xmpp.disconnect()
+ if reconnect:
+ # Add a one-time event to reconnect as soon as we are
+ # effectively disconnected
+ self.xmpp.add_event_handler('disconnected', lambda event: self.xmpp.connect(), disposable=True)
+
+ def send_message(self, msg):
+ """
+ Function to use in plugins to send a message in the current
+ conversation.
+ Returns False if the current tab is not a conversation tab
+ """
+ if not isinstance(self.current_tab(), tabs.ChatTab):
+ return False
+ self.current_tab().command_say(msg)
+ return True
+
+ def invite(self, jid, room, reason=None):
+ """
+ Checks if the sender supports XEP-0249, then send an invitation,
+ or a mediated one if it does not.
+ TODO: allow passwords
+ """
+ def callback(iq):
+ if not iq:
+ return
+ if 'jabber:x:conference' in iq['disco_info'].get_features():
+ self.xmpp.plugin['xep_0249'].send_invitation(
+ jid,
+ room,
+ reason=reason)
+ else: # fallback
+ self.xmpp.plugin['xep_0045'].invite(room, jid,
+ reason=reason or '')
+
+ self.xmpp.plugin['xep_0030'].get_info(jid=jid, timeout=5,
+ callback=callback)
+
+ def get_error_message(self, stanza, deprecated=False):
+ """
+ Takes a stanza of the form <message type='error'><error/></message>
+ and return a well formed string containing the error informations
+ """
+ sender = stanza.attrib['from']
+ msg = stanza['error']['type']
+ condition = stanza['error']['condition']
+ code = stanza['error']['code']
+ body = stanza['error']['text']
+ if not body:
+ if deprecated:
+ if code in DEPRECATED_ERRORS:
+ body = DEPRECATED_ERRORS[code]
+ else:
+ body = condition or 'Unknown error'
+ else:
+ if code in ERROR_AND_STATUS_CODES:
+ body = ERROR_AND_STATUS_CODES[code]
+ else:
+ body = condition or 'Unknown error'
+ if code:
+ message = '%(from)s: %(code)s - %(msg)s: %(body)s' % {
+ 'from': sender, 'msg': msg, 'body': body, 'code': code}
+ else:
+ message = '%(from)s: %(msg)s: %(body)s' % {
+ 'from': sender, 'msg': msg, 'body': body}
+ return message
+
+
+####################### Tab logic-related things ##############################
+
+ ### Tab getters ###
+
+ def get_tabs(self, cls=tabs.Tab):
+ "Get all the tabs of a type"
+ return filter(lambda tab: isinstance(tab, cls), self.tabs)
+
+ def current_tab(self):
+ """
+ returns the current room, the one we are viewing
+ """
+ self.current_tab_nb = self.current_tab_nb
+ return self.tabs[self.current_tab_nb]
+
+ def get_conversation_by_jid(self, jid, create=True, fallback_barejid=True):
+ """
+ From a JID, get the tab containing the conversation with it.
+ If none already exist, and create is "True", we create it
+ and return it. Otherwise, we return None.
+
+ If fallback_barejid is True, then this method will seek other
+ tabs with the same barejid, instead of searching only by fulljid.
+ """
+ jid = safeJID(jid)
+ # We first check if we have a static conversation opened
+ # with this precise resource
+ conversation = self.get_tab_by_name(jid.full,
+ tabs.StaticConversationTab)
+ if jid.bare == jid.full and not conversation:
+ conversation = self.get_tab_by_name(jid.full,
+ tabs.DynamicConversationTab)
+
+ if not conversation and fallback_barejid:
+ # If not, we search for a conversation with the bare jid
+ conversation = self.get_tab_by_name(jid.bare,
+ tabs.DynamicConversationTab)
+ if not conversation:
+ if create:
+ # We create a dynamic conversation with the bare Jid if
+ # nothing was found (and we lock it to the resource
+ # later)
+ conversation = self.open_conversation_window(jid.bare,
+ False)
+ else:
+ conversation = None
+ return conversation
+
+ def get_tab_by_name(self, name, typ=None):
+ """
+ Get the tab with the given name.
+ If typ is provided, return a tab of this type only
+ """
+ for tab in self.tabs:
+ if tab.name == name:
+ if (typ and isinstance(tab, typ)) or\
+ not typ:
+ return tab
+ return None
+
+ def get_tab_by_number(self, number):
+ if 0 <= number < len(self.tabs):
+ return self.tabs[number]
+ return None
+
+ def add_tab(self, new_tab, focus=False):
+ """
+ Appends the new_tab in the tab list and
+ focus it if focus==True
+ """
+ self.tabs.append(new_tab)
+ if focus:
+ self.command_win("%s" % new_tab.nb)
+
+ def insert_tab_nogaps(self, old_pos, new_pos):
+ """
+ Move tabs without creating gaps
+ old_pos: old position of the tab
+ new_pos: desired position of the tab
+ """
+ tab = self.tabs[old_pos]
+ if new_pos < old_pos:
+ self.tabs.pop(old_pos)
+ self.tabs.insert(new_pos, tab)
+ elif new_pos > old_pos:
+ self.tabs.insert(new_pos, tab)
+ self.tabs.remove(tab)
+ else:
+ return False
+ return True
+
+ def insert_tab_gaps(self, old_pos, new_pos):
+ """
+ Move tabs and create gaps in the eventual remaining space
+ old_pos: old position of the tab
+ new_pos: desired position of the tab
+ """
+ tab = self.tabs[old_pos]
+ target = None if new_pos >= len(self.tabs) else self.tabs[new_pos]
+ if not target:
+ if new_pos < len(self.tabs):
+ old_tab = self.tabs[old_pos]
+ self.tabs[new_pos], self.tabs[old_pos] = old_tab, tabs.GapTab()
+ else:
+ self.tabs.append(self.tabs[old_pos])
+ self.tabs[old_pos] = tabs.GapTab()
+ else:
+ if new_pos > old_pos:
+ self.tabs.insert(new_pos, tab)
+ self.tabs[old_pos] = tabs.GapTab()
+ elif new_pos < old_pos:
+ self.tabs[old_pos] = tabs.GapTab()
+ self.tabs.insert(new_pos, tab)
+ else:
+ return False
+ i = self.tabs.index(tab)
+ done = False
+ # Remove the first Gap on the right in the list
+ # in order to prevent global shifts when there is empty space
+ while not done:
+ i += 1
+ if i >= len(self.tabs):
+ done = True
+ elif not self.tabs[i]:
+ self.tabs.pop(i)
+ done = True
+ # Remove the trailing gaps
+ i = len(self.tabs) - 1
+ while isinstance(self.tabs[i], tabs.GapTab):
+ self.tabs.pop()
+ i -= 1
+ return True
+
+ def insert_tab(self, old_pos, new_pos=99999):
+ """
+ Insert a tab at a position, changing the number of the following tabs
+ returns False if it could not move the tab, True otherwise
+ """
+ if old_pos <= 0 or old_pos >= len(self.tabs):
+ return False
+ elif new_pos <= 0:
+ return False
+ elif new_pos == old_pos:
+ return False
+ elif not self.tabs[old_pos]:
+ return False
+ if config.get('create_gaps'):
+ return self.insert_tab_gaps(old_pos, new_pos)
+ return self.insert_tab_nogaps(old_pos, new_pos)
+
+ ### Move actions (e.g. go to next room) ###
+
+ def rotate_rooms_right(self, args=None):
+ """
+ rotate the rooms list to the right
+ """
+ self.current_tab().on_lose_focus()
+ self.current_tab_nb += 1
+ while not self.tabs[self.current_tab_nb]:
+ self.current_tab_nb += 1
+ 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.current_tab_nb -= 1
+ while not self.tabs[self.current_tab_nb]:
+ self.current_tab_nb -= 1
+ self.current_tab().on_gain_focus()
+ self.refresh_window()
+
+ def go_to_room_number(self):
+ """
+ Read 2 more chars and go to the tab
+ with the given number
+ """
+ def read_next_digit(digit):
+ try:
+ nb = int(digit)
+ except ValueError:
+ # If it is not a number, we do nothing. If it was the first
+ # one, we do not wait for a second one by re-setting the
+ # callback
+ self.room_number_jump.clear()
+ else:
+ self.room_number_jump.append(digit)
+ if len(self.room_number_jump) == 2:
+ arg = "".join(self.room_number_jump)
+ self.room_number_jump.clear()
+ self.command_win(arg)
+ else:
+ # We need to read more digits
+ keyboard.continuation_keys_callback = read_next_digit
+ keyboard.continuation_keys_callback = read_next_digit
+
+ def go_to_roster(self):
+ "Select the roster as the current tab"
+ self.command_win('0')
+
+ def go_to_previous_tab(self):
+ "Go to the previous tab"
+ self.command_win('%s' % (self.previous_tab_nb,))
+
+ def go_to_important_room(self):
+ """
+ Go to the next room with activity, in the order defined in the
+ dict tabs.STATE_PRIORITY
+ """
+ # shortcut
+ priority = tabs.STATE_PRIORITY
+ tab_refs = {}
+ # put all the active tabs in a dict of lists by state
+ for tab in self.tabs:
+ if not tab:
+ continue
+ if tab.state not in tab_refs:
+ tab_refs[tab.state] = [tab]
+ else:
+ tab_refs[tab.state].append(tab)
+ # sort the state by priority and remove those with negative priority
+ states = sorted(tab_refs.keys(),
+ key=(lambda x: priority.get(x, 0)),
+ reverse=True)
+ states = [state for state in states if priority.get(state, -1) >= 0]
+
+ for state in states:
+ for tab in tab_refs[state]:
+ if (tab.nb < self.current_tab_nb and
+ tab_refs[state][-1].nb > self.current_tab_nb):
+ continue
+ self.command_win('%s' % tab.nb)
+ return
+ return
+
+ def focus_tab_named(self, tab_name, type_=None):
+ """Returns True if it found a tab to focus on"""
+ for tab in self.tabs:
+ if tab.name == tab_name:
+ if (type_ and (isinstance(tab, type_))) or not type_:
+ self.command_win('%s' % (tab.nb,))
+ return True
+ return False
+
+ @property
+ def current_tab_nb(self):
+ """Wrapper for the current tab number"""
+ return self._current_tab_nb
+
+ @current_tab_nb.setter
+ def current_tab_nb(self, value):
+ """
+ Prevents the tab number from going over the total number of opened
+ tabs, or under 0
+ """
+ old = self._current_tab_nb
+ if value >= len(self.tabs):
+ self._current_tab_nb = 0
+ elif value < 0:
+ self._current_tab_nb = len(self.tabs) - 1
+ else:
+ self._current_tab_nb = value
+ if old != self._current_tab_nb and self.tabs[self._current_tab_nb]:
+ self.events.trigger('tab_change', old, self._current_tab_nb)
+
+ ### Opening actions ###
+
+ def open_conversation_window(self, jid, focus=True):
+ """
+ Open a new conversation tab and focus it if needed. If a resource is
+ provided, we open a StaticConversationTab, else a
+ DynamicConversationTab
+ """
+ if safeJID(jid).resource:
+ new_tab = tabs.StaticConversationTab(jid)
+ else:
+ new_tab = tabs.DynamicConversationTab(jid)
+ if not focus:
+ new_tab.state = "private"
+ self.add_tab(new_tab, focus)
+ self.refresh_window()
+ return new_tab
+
+ def open_private_window(self, room_name, user_nick, focus=True):
+ """
+ Open a Private conversation in a MUC and focus if needed.
+ """
+ complete_jid = room_name+'/'+user_nick
+ # if the room exists, focus it and return
+ for tab in self.get_tabs(tabs.PrivateTab):
+ if tab.name == complete_jid:
+ self.command_win('%s' % tab.nb)
+ return tab
+ # create the new tab
+ tab = self.get_tab_by_name(room_name, tabs.MucTab)
+ if not tab:
+ return None
+ new_tab = tabs.PrivateTab(complete_jid, tab.own_nick)
+ if hasattr(tab, 'directed_presence'):
+ new_tab.directed_presence = tab.directed_presence
+ if not focus:
+ new_tab.state = "private"
+ # insert it in the tabs
+ self.add_tab(new_tab, focus)
+ self.refresh_window()
+ tab.privates.append(new_tab)
+ return new_tab
+
+ def open_new_room(self, room, nick, *, password=None, focus=True):
+ """
+ Open a new tab.MucTab containing a muc Room, using the specified nick
+ """
+ new_tab = tabs.MucTab(room, nick, password=password)
+ self.add_tab(new_tab, focus)
+ self.refresh_window()
+ return new_tab
+
+ def open_new_form(self, form, on_cancel, on_send, **kwargs):
+ """
+ Open a new tab containing the form
+ The callback are called with the completed form as parameter in
+ addition with kwargs
+ """
+ form_tab = tabs.DataFormsTab(form, on_cancel, on_send, kwargs)
+ self.add_tab(form_tab, True)
+
+ ### Modifying actions ###
+
+ def rename_private_tabs(self, room_name, old_nick, new_nick):
+ """
+ Call this method when someone changes his/her nick in a MUC,
+ this updates the name of all the opened private conversations
+ with him/her
+ """
+ tab = self.get_tab_by_name('%s/%s' % (room_name, old_nick),
+ tabs.PrivateTab)
+ if tab:
+ tab.rename_user(old_nick, new_nick)
+
+ def on_user_left_private_conversation(self, room_name, nick, status_message):
+ """
+ The user left the MUC: add a message in the associated
+ private conversation
+ """
+ tab = self.get_tab_by_name('%s/%s' % (room_name, nick), tabs.PrivateTab)
+ if tab:
+ tab.user_left(status_message, nick)
+
+ def on_user_rejoined_private_conversation(self, room_name, nick):
+ """
+ The user joined a MUC: add a message in the associated
+ private conversation
+ """
+ tab = self.get_tab_by_name('%s/%s' % (room_name, nick), tabs.PrivateTab)
+ if tab:
+ tab.user_rejoined(nick)
+
+ def disable_private_tabs(self, room_name, reason=None):
+ """
+ Disable private tabs when leaving a room
+ """
+ if reason is None:
+ reason = '\x195}You left the chatroom\x193}'
+ for tab in self.get_tabs(tabs.PrivateTab):
+ if tab.name.startswith(room_name):
+ tab.deactivate(reason=reason)
+
+ def enable_private_tabs(self, room_name, reason=None):
+ """
+ Enable private tabs when joining a room
+ """
+ if reason is None:
+ reason = '\x195}You joined the chatroom\x193}'
+ for tab in self.get_tabs(tabs.PrivateTab):
+ if tab.name.startswith(room_name):
+ tab.activate(reason=reason)
+
+ def on_user_changed_status_in_private(self, jid, msg):
+ tab = self.get_tab_by_name(jid, tabs.ChatTab)
+ if tab: # display the message in private
+ tab.add_message(msg, typ=2)
+
+ def close_tab(self, tab=None):
+ """
+ Close the given tab. If None, close the current one
+ """
+ was_current = tab is None
+ tab = tab or self.current_tab()
+ if isinstance(tab, tabs.RosterInfoTab):
+ return # The tab 0 should NEVER be closed
+ del tab.key_func # Remove self references
+ del tab.commands # and make the object collectable
+ tab.on_close()
+ nb = tab.nb
+ if was_current:
+ if self.previous_tab_nb != nb:
+ self.current_tab_nb = self.previous_tab_nb
+ self.previous_tab_nb = 0
+ if config.get('create_gaps'):
+ if nb >= len(self.tabs) - 1:
+ self.tabs.remove(tab)
+ nb -= 1
+ while not self.tabs[nb]: # remove the trailing gaps
+ self.tabs.pop()
+ nb -= 1
+ else:
+ self.tabs[nb] = tabs.GapTab()
+ else:
+ self.tabs.remove(tab)
+ if tab and tab.name in logger.fds:
+ logger.fds[tab.name].close()
+ log.debug("Log file for %s closed.", tab.name)
+ del logger.fds[tab.name]
+ if self.current_tab_nb >= len(self.tabs):
+ self.current_tab_nb = len(self.tabs) - 1
+ while not self.tabs[self.current_tab_nb]:
+ self.current_tab_nb -= 1
+ if was_current:
+ self.current_tab().on_gain_focus()
+ self.refresh_window()
+ import gc
+ gc.collect()
+ log.debug('___ Referrers of closing tab:\n%s\n______',
+ gc.get_referrers(tab))
+ del tab
+
+ def add_information_message_to_conversation_tab(self, jid, msg):
+ """
+ Search for a ConversationTab with the given jid (full or bare),
+ if yes, add the given message to it
+ """
+ tab = self.get_tab_by_name(jid, tabs.ConversationTab)
+ if tab:
+ tab.add_message(msg, typ=2)
+ if self.current_tab() is tab:
+ self.refresh_window()
+
+
+####################### Curses and ui-related stuff ###########################
+
+ def doupdate(self):
+ "Do a curses update"
+ if not self.running:
+ return
+ curses.doupdate()
+
+ def information(self, msg, typ=''):
+ """
+ Displays an informational message in the "Info" buffer
+ """
+ filter_messages = config.get('filter_info_messages').split(':')
+ for words in filter_messages:
+ if words and words in msg:
+ log.debug('Did not show the message:\n\t%s> %s', typ, msg)
+ return False
+ colors = get_theme().INFO_COLORS
+ color = colors.get(typ.lower(), colors.get('default', None))
+ nb_lines = self.information_buffer.add_message(msg,
+ nickname=typ,
+ nick_color=color)
+ popup_on = config.get('information_buffer_popup_on').split()
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
+ elif typ != '' and typ.lower() in popup_on:
+ popup_time = config.get('popup_time') + (nb_lines - 1) * 2
+ self.pop_information_win_up(nb_lines, popup_time)
+ else:
+ if self.information_win_size != 0:
+ self.information_win.refresh()
+ self.current_tab().input.refresh()
+ return True
+
+ def init_curses(self, stdscr):
+ """
+ ncurses initialization
+ """
+ curses.curs_set(1)
+ curses.noecho()
+ curses.nonl()
+ curses.raw()
+ stdscr.idlok(1)
+ stdscr.keypad(1)
+ curses.start_color()
+ curses.use_default_colors()
+ theming.reload_theme()
+ curses.ungetch(" ") # H4X: without this, the screen is
+ stdscr.getkey() # erased on the first "getkey()"
+
+ def reset_curses(self):
+ """
+ Reset terminal capabilities to what they were before ncurses
+ init
+ """
+ curses.echo()
+ curses.nocbreak()
+ curses.curs_set(1)
+ curses.endwin()
+
+ @property
+ def informations(self):
+ return self.information_buffer
+
+ def refresh_window(self):
+ """
+ Refresh everything
+ """
+ nocursor = curses.curs_set(0)
+ self.current_tab().state = 'current'
+ self.current_tab().refresh()
+ self.doupdate()
+ curses.curs_set(nocursor)
+
+ def refresh_tab_win(self):
+ """
+ Refresh the window containing the tab list
+ """
+ self.current_tab().refresh_tab_win()
+ self.refresh_input()
+ self.doupdate()
+
+ def refresh_input(self):
+ """
+ Refresh the input if it exists
+ """
+ if self.current_tab().input:
+ self.current_tab().input.refresh()
+ self.doupdate()
+
+ def scroll_page_down(self, args=None):
+ """
+ Scroll a page down, if possible.
+ Returns True on success, None on failure.
+ """
+ if self.current_tab().on_scroll_down():
+ self.refresh_window()
+ return True
+
+ def scroll_page_up(self, args=None):
+ """
+ Scroll a page up, if possible.
+ Returns True on success, None on failure.
+ """
+ if self.current_tab().on_scroll_up():
+ self.refresh_window()
+ return True
+
+ def scroll_line_up(self, args=None):
+ """
+ Scroll a line up, if possible.
+ Returns True on success, None on failure.
+ """
+ if self.current_tab().on_line_up():
+ self.refresh_window()
+ return True
+
+ def scroll_line_down(self, args=None):
+ """
+ Scroll a line down, if possible.
+ Returns True on success, None on failure.
+ """
+ if self.current_tab().on_line_down():
+ self.refresh_window()
+ return True
+
+ def scroll_half_up(self, args=None):
+ """
+ Scroll half a screen down, if possible.
+ Returns True on success, None on failure.
+ """
+ if self.current_tab().on_half_scroll_up():
+ self.refresh_window()
+ return True
+
+ def scroll_half_down(self, args=None):
+ """
+ Scroll half a screen down, if possible.
+ Returns True on success, None on failure.
+ """
+ if self.current_tab().on_half_scroll_down():
+ self.refresh_window()
+ return True
+
+ def grow_information_win(self, nb=1):
+ """
+ Expand the information win a number of lines
+ """
+ if self.information_win_size >= self.current_tab().height -5 or \
+ self.information_win_size+nb >= self.current_tab().height-4 or\
+ self.size.core_degrade_y:
+ return 0
+ self.information_win_size += nb
+ self.resize_global_information_win()
+ for tab in self.tabs:
+ tab.on_info_win_size_changed()
+ self.refresh_window()
+ return nb
+
+ def shrink_information_win(self, nb=1):
+ """
+ Reduce the size of the information win
+ """
+ if self.information_win_size == 0 or self.size.core_degrade_y:
+ return
+ self.information_win_size -= nb
+ if self.information_win_size < 0:
+ self.information_win_size = 0
+ self.resize_global_information_win()
+ for tab in self.tabs:
+ tab.on_info_win_size_changed()
+ self.refresh_window()
+
+ def scroll_info_up(self):
+ """
+ Scroll the information buffer up
+ """
+ self.information_win.scroll_up(self.information_win.height)
+ if not isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.information_win.refresh()
+ else:
+ info = self.current_tab().information_win
+ info.scroll_up(info.height)
+ self.refresh_window()
+
+ def scroll_info_down(self):
+ """
+ Scroll the information buffer down
+ """
+ self.information_win.scroll_down(self.information_win.height)
+ if not isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.information_win.refresh()
+ else:
+ info = self.current_tab().information_win
+ info.scroll_down(info.height)
+ self.refresh_window()
+
+ def pop_information_win_up(self, size, time):
+ """
+ Temporarly increase the size of the information win of size lines
+ during time seconds.
+ After that delay, the size will decrease from size lines.
+ """
+ if time <= 0 or size <= 0:
+ return
+ result = self.grow_information_win(size)
+ timed_event = timed_events.DelayedEvent(time,
+ self.shrink_information_win,
+ result)
+ self.add_timed_event(timed_event)
+ self.refresh_window()
+
+ def toggle_left_pane(self):
+ """
+ Enable/disable the left panel.
+ """
+ enabled = config.get('enable_vertical_tab_list')
+ if not config.silent_set('enable_vertical_tab_list', str(not enabled)):
+ self.information('Unable to write in the config file', 'Error')
+ self.call_for_resize()
+
+ def resize_global_information_win(self):
+ """
+ Resize the global_information_win only once at each resize.
+ """
+ if self.information_win_size > tabs.Tab.height - 6:
+ self.information_win_size = tabs.Tab.height - 6
+ if tabs.Tab.height < 6:
+ self.information_win_size = 0
+ height = (tabs.Tab.height - 1 - self.information_win_size
+ - tabs.Tab.tab_win_height())
+ self.information_win.resize(self.information_win_size,
+ tabs.Tab.width,
+ height,
+ 0)
+
+ def resize_global_info_bar(self):
+ """
+ Resize the GlobalInfoBar only once at each resize
+ """
+ height, width = self.stdscr.getmaxyx()
+ if config.get('enable_vertical_tab_list'):
+
+ if self.size.core_degrade_x:
+ return
+ try:
+ height, _ = self.stdscr.getmaxyx()
+ truncated_win = self.stdscr.subwin(height,
+ config.get('vertical_tab_list_size'),
+ 0, 0)
+ except:
+ log.error('Curses error on infobar resize', exc_info=True)
+ return
+ self.left_tab_win = windows.VerticalGlobalInfoBar(truncated_win)
+ elif not self.size.core_degrade_y:
+ self.tab_win.resize(1, tabs.Tab.width,
+ tabs.Tab.height - 2, 0)
+ self.left_tab_win = None
+
+ def add_message_to_text_buffer(self, buff, txt,
+ time=None, nickname=None, history=None):
+ """
+ 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 buff:
+ self.information('Trying to add a message in no room: %s' % txt, 'Error')
+ else:
+ buff.add_message(txt, time, nickname, history=history)
+
+ def full_screen_redraw(self):
+ """
+ Completely erase and redraw the screen
+ """
+ self.stdscr.clear()
+ self.refresh_window()
+
+ def call_for_resize(self):
+ """
+ Called when we want to resize the screen
+ """
+ # If we have the tabs list on the left, we just give a truncated
+ # window to each Tab class, so they draw themself in the portion of
+ # the screen that they can occupy, and we draw the tab list on the
+ # remaining space, on the left
+ height, width = self.stdscr.getmaxyx()
+ if (config.get('enable_vertical_tab_list') and
+ not self.size.core_degrade_x):
+ try:
+ scr = self.stdscr.subwin(0,
+ config.get('vertical_tab_list_size'))
+ except:
+ log.error('Curses error on resize', exc_info=True)
+ return
+ else:
+ scr = self.stdscr
+ tabs.Tab.resize(scr)
+ self.resize_global_info_bar()
+ self.resize_global_information_win()
+ for tab in self.tabs:
+ if config.get('lazy_resize'):
+ tab.need_resize = True
+ else:
+ tab.resize()
+ if self.tabs:
+ self.full_screen_redraw()
+
+ def read_keyboard(self):
+ """
+ Get the next keyboard key pressed and returns it. It blocks until
+ something can be read on stdin, this function must be called only if
+ there is something to read. No timeout ever occurs.
+ """
+ return self.keyboard.get_user_input(self.stdscr)
+
+ def escape_next_key(self):
+ """
+ Tell the Keyboard object that the next key pressed by the user
+ should be escaped. See Keyboard.get_user_input
+ """
+ self.keyboard.escape_next_key()
+
+####################### Commands and completions ##############################
+
+ def register_command(self, name, func, **kwargs):
+ """
+ Add a command
+ """
+ desc = kwargs.get('desc', '')
+ shortdesc = kwargs.get('shortdesc', '')
+ completion = kwargs.get('completion')
+ usage = kwargs.get('usage', '')
+ if name in self.commands:
+ return
+ if not desc and shortdesc:
+ desc = shortdesc
+ self.commands[name] = Command(func, desc, completion, shortdesc, usage)
+
+ def register_initial_commands(self):
+ """
+ Register the commands when poezio starts
+ """
+ self.register_command('help', self.command_help,
+ usage='[command]',
+ shortdesc='\\_o< KOIN KOIN KOIN',
+ completion=self.completion_help)
+ self.register_command('join', self.command_join,
+ usage="[room_name][@server][/nick] [password]",
+ desc="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",
+ shortdesc='Join a room',
+ completion=self.completion_join)
+ self.register_command('exit', self.command_quit,
+ desc='Just disconnect from the server and exit poezio.',
+ shortdesc='Exit poezio.')
+ self.register_command('quit', self.command_quit,
+ desc='Just disconnect from the server and exit poezio.',
+ shortdesc='Exit poezio.')
+ self.register_command('next', self.rotate_rooms_right,
+ shortdesc='Go to the next room.')
+ self.register_command('prev', self.rotate_rooms_left,
+ shortdesc='Go to the previous room.')
+ self.register_command('win', self.command_win,
+ usage='<number or name>',
+ shortdesc='Go to the specified room',
+ completion=self.completion_win)
+ self.commands['w'] = self.commands['win']
+ self.register_command('move_tab', self.command_move_tab,
+ usage='<source> <destination>',
+ desc="Insert the <source> tab at the position of "
+ "<destination>. This will make the following tabs shift in"
+ " some cases (refer to the documentation). A tab can be "
+ "designated by its number or by the beginning of its "
+ "address. You can use \".\" as a shortcut for the current "
+ "tab.",
+ shortdesc='Move a tab.',
+ completion=self.completion_move_tab)
+ self.register_command('destroy_room', self.command_destroy_room,
+ usage='[room JID]',
+ desc='Try to destroy the room [room JID], or the current'
+ ' tab if it is a multi-user chat and [room JID] is '
+ 'not given.',
+ shortdesc='Destroy a room.',
+ completion=None)
+ self.register_command('show', self.command_status,
+ usage='<availability> [status message]',
+ desc="Sets your availability and (optionally) your status "
+ "message. The <availability> argument is one of \"available"
+ ", chat, away, afk, dnd, busy, xa\" and the optional "
+ "[status message] argument will be your status message.",
+ shortdesc='Change your availability.',
+ completion=self.completion_status)
+ self.commands['status'] = self.commands['show']
+ self.register_command('bookmark_local', self.command_bookmark_local,
+ usage="[roomname][/nick] [password]",
+ desc="Bookmark Local: Bookmark locally the specified room "
+ "(you will then auto-join it on each poezio start). This"
+ " commands uses almost the same syntaxe as /join. Type "
+ "/help join for syntax 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)",
+ shortdesc='Bookmark a room locally.',
+ completion=self.completion_bookmark_local)
+ self.register_command('bookmark', self.command_bookmark,
+ usage="[roomname][/nick] [autojoin] [password]",
+ desc="Bookmark: Bookmark online the specified room (you "
+ "will then auto-join it on each poezio start if autojoin"
+ " is specified and is 'true'). This commands uses almost"
+ " the same syntax as /join. Type /help join for syntax "
+ "examples. Note that when typing \"/bookmark\" alone, the"
+ " room will be bookmarked with the nickname you\'re "
+ "currently using in this room (instead of default_nick).",
+ shortdesc="Bookmark a room online.",
+ completion=self.completion_bookmark)
+ self.register_command('set', self.command_set,
+ usage="[plugin|][section] <option> [value]",
+ desc="Set the value of an 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 options in specific "
+ "sections with `/set bindings M-i ^i` or in specific plugin"
+ " with `/set mpd_client| host 127.0.0.1`. `toggle` can be "
+ "used as a special value to toggle a boolean option.",
+ shortdesc="Set the value of an option",
+ completion=self.completion_set)
+ self.register_command('set_default', self.command_set_default,
+ usage="[section] <option>",
+ desc="Set the default value of an option. For example, "
+ "`/set_default resource` will reset the resource "
+ "option. You can also reset options in specific "
+ "sections by doing `/set_default section option`.",
+ shortdesc="Set the default value of an option",
+ completion=self.completion_set_default)
+ self.register_command('toggle', self.command_toggle,
+ usage='<option>',
+ desc='Shortcut for /set <option> toggle',
+ shortdesc='Toggle an option',
+ completion=self.completion_toggle)
+ self.register_command('theme', self.command_theme,
+ usage='[theme name]',
+ desc="Reload the theme defined in the config file. If theme"
+ "_name is provided, set that theme before reloading it.",
+ shortdesc='Load a theme',
+ completion=self.completion_theme)
+ self.register_command('list', self.command_list,
+ usage='[server]',
+ desc="Get the list of public chatrooms"
+ " on the specified server.",
+ shortdesc='List the rooms.',
+ completion=self.completion_list)
+ self.register_command('message', self.command_message,
+ usage='<jid> [optional message]',
+ desc="Open a conversation with the specified JID (even if it"
+ " is not in our roster), and send a message to it, if the "
+ "message is specified.",
+ shortdesc='Send a message',
+ completion=self.completion_message)
+ self.register_command('version', self.command_version,
+ usage='<jid>',
+ desc="Get the software version of the given JID (usually its"
+ " XMPP client and Operating System).",
+ shortdesc='Get the software version of a JID.',
+ completion=self.completion_version)
+ self.register_command('server_cycle', self.command_server_cycle,
+ usage='[domain] [message]',
+ desc='Disconnect and reconnect in all the rooms in domain.',
+ shortdesc='Cycle a range of rooms',
+ completion=self.completion_server_cycle)
+ self.register_command('bind', self.command_bind,
+ usage='<key> <equ>',
+ desc="Bind a key to another key or to a “command”. For "
+ "example \"/bind ^H KEY_UP\" makes Control + h do the"
+ " same same as the Up key.",
+ completion=self.completion_bind,
+ shortdesc='Bind a key to another key.')
+ self.register_command('load', self.command_load,
+ usage='<plugin> [<otherplugin> …]',
+ shortdesc='Load the specified plugin(s)',
+ completion=self.plugin_manager.completion_load)
+ self.register_command('unload', self.command_unload,
+ usage='<plugin> [<otherplugin> …]',
+ shortdesc='Unload the specified plugin(s)',
+ completion=self.plugin_manager.completion_unload)
+ self.register_command('plugins', self.command_plugins,
+ shortdesc='Show the plugins in use.')
+ self.register_command('presence', self.command_presence,
+ usage='<JID> [type] [status]',
+ desc="Send a directed presence to <JID> and using"
+ " [type] and [status] if provided.",
+ shortdesc='Send a directed presence.',
+ completion=self.completion_presence)
+ self.register_command('rawxml', self.command_rawxml,
+ usage='<xml>',
+ shortdesc='Send a custom xml stanza.')
+ self.register_command('invite', self.command_invite,
+ usage='<jid> <room> [reason]',
+ desc='Invite jid in room with reason.',
+ shortdesc='Invite someone in a room.',
+ completion=self.completion_invite)
+ self.register_command('invitations', self.command_invitations,
+ shortdesc='Show the pending invitations.')
+ self.register_command('bookmarks', self.command_bookmarks,
+ shortdesc='Show the current bookmarks.')
+ self.register_command('remove_bookmark', self.command_remove_bookmark,
+ usage='[jid]',
+ desc="Remove the specified bookmark, or the "
+ "bookmark on the current tab, if any.",
+ shortdesc='Remove a bookmark',
+ completion=self.completion_remove_bookmark)
+ self.register_command('xml_tab', self.command_xml_tab,
+ shortdesc='Open an XML tab.')
+ self.register_command('runkey', self.command_runkey,
+ usage='<key>',
+ shortdesc='Execute the action defined for <key>.',
+ completion=self.completion_runkey)
+ self.register_command('self', self.command_self,
+ shortdesc='Remind you of who you are.')
+ self.register_command('last_activity', self.command_last_activity,
+ usage='<jid>',
+ desc='Informs you of the last activity of a JID.',
+ shortdesc='Get the activity of someone.',
+ completion=self.completion_last_activity)
+ self.register_command('ad-hoc', self.command_adhoc,
+ usage='<jid>',
+ shortdesc='List available ad-hoc commands on the given jid')
+ self.register_command('reload', self.command_reload,
+ shortdesc='Reload the config. You can achieve the same by '
+ 'sending SIGUSR1 to poezio.')
+
+ if config.get('enable_user_activity'):
+ self.register_command('activity', self.command_activity,
+ usage='[<general> [specific] [text]]',
+ desc='Send your current activity to your contacts '
+ '(use the completion). Nothing means '
+ '"stop broadcasting an activity".',
+ shortdesc='Send your activity.',
+ completion=self.completion_activity)
+ if config.get('enable_user_mood'):
+ self.register_command('mood', self.command_mood,
+ usage='[<mood> [text]]',
+ desc='Send your current mood to your contacts '
+ '(use the completion). Nothing means '
+ '"stop broadcasting a mood".',
+ shortdesc='Send your mood.',
+ completion=self.completion_mood)
+ if config.get('enable_user_gaming'):
+ self.register_command('gaming', self.command_gaming,
+ usage='[<game name> [server address]]',
+ desc='Send your current gaming activity to '
+ 'your contacts. Nothing means "stop '
+ 'broadcasting a gaming activity".',
+ shortdesc='Send your gaming activity.',
+ completion=None)
+
+####################### XMPP Event Handlers ##################################
+ on_session_start_features = handlers.on_session_start_features
+ on_carbon_received = handlers.on_carbon_received
+ on_carbon_sent = handlers.on_carbon_sent
+ on_groupchat_invitation = handlers.on_groupchat_invitation
+ on_groupchat_direct_invitation = handlers.on_groupchat_direct_invitation
+ on_groupchat_decline = handlers.on_groupchat_decline
+ on_message = handlers.on_message
+ on_error_message = handlers.on_error_message
+ on_normal_message = handlers.on_normal_message
+ on_nick_received = handlers.on_nick_received
+ on_gaming_event = handlers.on_gaming_event
+ on_mood_event = handlers.on_mood_event
+ on_activity_event = handlers.on_activity_event
+ on_tune_event = handlers.on_tune_event
+ on_groupchat_message = handlers.on_groupchat_message
+ on_muc_own_nickchange = handlers.on_muc_own_nickchange
+ on_groupchat_private_message = handlers.on_groupchat_private_message
+ on_chatstate_active = handlers.on_chatstate_active
+ on_chatstate_inactive = handlers.on_chatstate_inactive
+ on_chatstate_composing = handlers.on_chatstate_composing
+ on_chatstate_paused = handlers.on_chatstate_paused
+ on_chatstate_gone = handlers.on_chatstate_gone
+ on_chatstate = handlers.on_chatstate
+ on_chatstate_normal_conversation = handlers.on_chatstate_normal_conversation
+ on_chatstate_private_conversation = \
+ handlers.on_chatstate_private_conversation
+ on_chatstate_groupchat_conversation = \
+ handlers.on_chatstate_groupchat_conversation
+ on_roster_update = handlers.on_roster_update
+ on_subscription_request = handlers.on_subscription_request
+ on_subscription_authorized = handlers.on_subscription_authorized
+ on_subscription_remove = handlers.on_subscription_remove
+ on_subscription_removed = handlers.on_subscription_removed
+ on_presence = handlers.on_presence
+ on_presence_error = handlers.on_presence_error
+ on_got_offline = handlers.on_got_offline
+ on_got_online = handlers.on_got_online
+ on_groupchat_presence = handlers.on_groupchat_presence
+ on_failed_connection = handlers.on_failed_connection
+ on_disconnected = handlers.on_disconnected
+ on_stream_error = handlers.on_stream_error
+ on_failed_all_auth = handlers.on_failed_all_auth
+ on_no_auth = handlers.on_no_auth
+ on_connected = handlers.on_connected
+ on_connecting = handlers.on_connecting
+ on_session_start = handlers.on_session_start
+ on_status_codes = handlers.on_status_codes
+ on_groupchat_subject = handlers.on_groupchat_subject
+ on_data_form = handlers.on_data_form
+ on_receipt = handlers.on_receipt
+ on_attention = handlers.on_attention
+ room_error = handlers.room_error
+ check_bookmark_storage = handlers.check_bookmark_storage
+ outgoing_stanza = handlers.outgoing_stanza
+ incoming_stanza = handlers.incoming_stanza
+ validate_ssl = handlers.validate_ssl
+ ssl_invalid_chain = handlers.ssl_invalid_chain
+ on_next_adhoc_step = handlers.on_next_adhoc_step
+ on_adhoc_error = handlers.on_adhoc_error
+ cancel_adhoc_command = handlers.cancel_adhoc_command
+ validate_adhoc_step = handlers.validate_adhoc_step
+ terminate_adhoc_command = handlers.terminate_adhoc_command
+ command_help = commands.command_help
+ command_runkey = commands.command_runkey
+ command_status = commands.command_status
+ command_presence = commands.command_presence
+ command_theme = commands.command_theme
+ command_win = commands.command_win
+ command_move_tab = commands.command_move_tab
+ command_list = commands.command_list
+ command_version = commands.command_version
+ command_join = commands.command_join
+ command_bookmark_local = commands.command_bookmark_local
+ command_bookmark = commands.command_bookmark
+ command_bookmarks = commands.command_bookmarks
+ command_destroy_room = commands.command_destroy_room
+ command_remove_bookmark = commands.command_remove_bookmark
+ command_set = commands.command_set
+ command_set_default = commands.command_set_default
+ command_toggle = commands.command_toggle
+ command_server_cycle = commands.command_server_cycle
+ command_last_activity = commands.command_last_activity
+ command_mood = commands.command_mood
+ command_activity = commands.command_activity
+ command_gaming = commands.command_gaming
+ command_invite = commands.command_invite
+ command_decline = commands.command_decline
+ command_invitations = commands.command_invitations
+ command_quit = commands.command_quit
+ command_bind = commands.command_bind
+ command_rawxml = commands.command_rawxml
+ command_load = commands.command_load
+ command_unload = commands.command_unload
+ command_plugins = commands.command_plugins
+ command_message = commands.command_message
+ command_xml_tab = commands.command_xml_tab
+ command_adhoc = commands.command_adhoc
+ command_self = commands.command_self
+ command_reload = commands.command_reload
+ completion_help = completions.completion_help
+ completion_status = completions.completion_status
+ completion_presence = completions.completion_presence
+ completion_theme = completions.completion_theme
+ completion_win = completions.completion_win
+ completion_join = completions.completion_join
+ completion_version = completions.completion_version
+ completion_list = completions.completion_list
+ completion_move_tab = completions.completion_move_tab
+ completion_runkey = completions.completion_runkey
+ completion_bookmark = completions.completion_bookmark
+ completion_remove_bookmark = completions.completion_remove_bookmark
+ completion_decline = completions.completion_decline
+ completion_bind = completions.completion_bind
+ completion_message = completions.completion_message
+ completion_invite = completions.completion_invite
+ completion_activity = completions.completion_activity
+ completion_mood = completions.completion_mood
+ completion_last_activity = completions.completion_last_activity
+ completion_server_cycle = completions.completion_server_cycle
+ completion_set = completions.completion_set
+ completion_set_default = completions.completion_set_default
+ completion_toggle = completions.completion_toggle
+ completion_bookmark_local = completions.completion_bookmark_local
+
+
+
+class KeyDict(dict):
+ """
+ A dict, with a wrapper for get() that will return a custom value
+ if the key starts with _exc_
+ """
+ def get(self, k, d=None):
+ if isinstance(k, str) and k.startswith('_exc_') and len(k) > 5:
+ return lambda: dict.get(self, '_exc_')(k[5:])
+ return dict.get(self, k, d)
+
+def replace_key_with_bound(key):
+ """
+ Replace an inputted key with the one defined as its replacement
+ in the config
+ """
+ bind = config.get(key, default=key, section='bindings')
+ if not bind:
+ bind = key
+ return bind
+
+
diff --git a/poezio/core/handlers.py b/poezio/core/handlers.py
new file mode 100644
index 00000000..8cc08179
--- /dev/null
+++ b/poezio/core/handlers.py
@@ -0,0 +1,1354 @@
+"""
+XMPP-related handlers for the Core class
+"""
+
+import logging
+log = logging.getLogger(__name__)
+
+import asyncio
+import curses
+import functools
+import ssl
+import sys
+import time
+from datetime import datetime
+from hashlib import sha1, sha512
+from os import path
+
+from slixmpp import InvalidJID
+from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase
+from xml.etree import ElementTree as ET
+
+import common
+import fixes
+import pep
+import tabs
+import windows
+import xhtml
+import multiuserchat as muc
+from common import safeJID
+from config import config, CACHE_DIR
+from contact import Resource
+from logger import logger
+from roster import roster
+from text_buffer import CorrectionError, AckError
+from theming import dump_tuple, get_theme
+
+from . commands import dumb_callback
+
+try:
+ from pygments import highlight
+ from pygments.lexers import get_lexer_by_name
+ from pygments.formatters import HtmlFormatter
+ LEXER = get_lexer_by_name('xml')
+ FORMATTER = HtmlFormatter(noclasses=True)
+ PYGMENTS = True
+except ImportError:
+ PYGMENTS = False
+
+def _join_initial_rooms(self, bookmarks):
+ """Join all rooms given in the iterator `bookmarks`"""
+ for bm in bookmarks:
+ if not (bm.autojoin or config.get('open_all_bookmarks')):
+ continue
+ tab = self.get_tab_by_name(bm.jid, tabs.MucTab)
+ nick = bm.nick if bm.nick else self.own_nick
+ if not tab:
+ self.open_new_room(bm.jid, nick, focus=False,
+ password=bm.password)
+ self.initial_joins.append(bm.jid)
+ # do not join rooms that do not have autojoin
+ # but display them anyway
+ if bm.autojoin:
+ muc.join_groupchat(self, bm.jid, nick,
+ passwd=bm.password,
+ status=self.status.message,
+ show=self.status.show)
+
+def check_bookmark_storage(self, features):
+ private = 'jabber:iq:private' in features
+ pep_ = 'http://jabber.org/protocol/pubsub#publish' in features
+ self.bookmarks.available_storage['private'] = private
+ self.bookmarks.available_storage['pep'] = pep_
+ def _join_remote_only(iq):
+ if iq['type'] == 'error':
+ type_ = iq['error']['type']
+ condition = iq['error']['condition']
+ if not (type_ == 'cancel' and condition == 'item-not-found'):
+ self.information('Unable to fetch the remote'
+ ' bookmarks; %s: %s' % (type_, condition),
+ 'Error')
+ return
+ remote_bookmarks = self.bookmarks.remote()
+ _join_initial_rooms(self, remote_bookmarks)
+ if not self.xmpp.anon and config.get('use_remote_bookmarks'):
+ self.bookmarks.get_remote(self.xmpp, self.information, _join_remote_only)
+
+def on_session_start_features(self, _):
+ """
+ Enable carbons & blocking on session start if wanted and possible
+ """
+ def callback(iq):
+ if not iq:
+ return
+ features = iq['disco_info']['features']
+ rostertab = self.get_tab_by_name('Roster', tabs.RosterInfoTab)
+ rostertab.check_blocking(features)
+ rostertab.check_saslexternal(features)
+ if (config.get('enable_carbons') and
+ 'urn:xmpp:carbons:2' in features):
+ self.xmpp.plugin['xep_0280'].enable()
+ self.check_bookmark_storage(features)
+
+ self.xmpp.plugin['xep_0030'].get_info(jid=self.xmpp.boundjid.domain,
+ callback=callback)
+
+def on_carbon_received(self, message):
+ """
+ Carbon <received/> received
+ """
+ def ignore_message(recv):
+ log.debug('%s has category conference, ignoring carbon',
+ recv['from'].server)
+ def receive_message(recv):
+ recv['to'] = self.xmpp.boundjid.full
+ if recv['receipt']:
+ return self.on_receipt(recv)
+ self.on_normal_message(recv)
+
+ recv = message['carbon_received']
+ if (recv['from'].bare not in roster or
+ roster[recv['from'].bare].subscription == 'none'):
+ fixes.has_identity(self.xmpp, recv['from'].server,
+ identity='conference',
+ on_true=functools.partial(ignore_message, recv),
+ on_false=functools.partial(receive_message, recv))
+ return
+ else:
+ receive_message(recv)
+
+def on_carbon_sent(self, message):
+ """
+ Carbon <sent/> received
+ """
+ def ignore_message(sent):
+ log.debug('%s has category conference, ignoring carbon',
+ sent['to'].server)
+ def send_message(sent):
+ sent['from'] = self.xmpp.boundjid.full
+ self.on_normal_message(sent)
+
+ sent = message['carbon_sent']
+ if (sent['to'].bare not in roster or
+ roster[sent['to'].bare].subscription == 'none'):
+ fixes.has_identity(self.xmpp, sent['to'].server,
+ identity='conference',
+ on_true=functools.partial(ignore_message, sent),
+ on_false=functools.partial(send_message, sent))
+ else:
+ send_message(sent)
+
+### Invites ###
+
+def on_groupchat_invitation(self, message):
+ """
+ Mediated invitation received
+ """
+ jid = message['from']
+ if jid.bare in self.pending_invites:
+ return
+ # there are 2 'x' tags in the messages, making message['x'] useless
+ invite = StanzaBase(self.xmpp, xml=message.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite'))
+ inviter = invite['from']
+ reason = invite['reason']
+ password = invite['password']
+ msg = "You are invited to the room %s by %s" % (jid.full, inviter.full)
+ if reason:
+ msg += "because: %s" % reason
+ if password:
+ msg += ". The password is \"%s\"." % password
+ self.information(msg, 'Info')
+ if 'invite' in config.get('beep_on').split():
+ curses.beep()
+ logger.log_roster_change(inviter.full, 'invited you to %s' % jid.full)
+ self.pending_invites[jid.bare] = inviter.full
+
+def on_groupchat_decline(self, decline):
+ "Mediated invitation declined; skip for now"
+ pass
+
+def on_groupchat_direct_invitation(self, message):
+ """
+ Direct invitation received
+ """
+ room = safeJID(message['groupchat_invite']['jid'])
+ if room.bare in self.pending_invites:
+ return
+
+ inviter = message['from']
+ reason = message['groupchat_invite']['reason']
+ password = message['groupchat_invite']['password']
+ continue_ = message['groupchat_invite']['continue']
+ msg = "You are invited to the room %s by %s" % (room, inviter.full)
+
+ if password:
+ msg += ' (password: "%s")' % password
+ if continue_:
+ msg += '\nto continue the discussion'
+ if reason:
+ msg += "\nreason: %s" % reason
+
+ self.information(msg, 'Info')
+ if 'invite' in config.get('beep_on').split():
+ curses.beep()
+
+ self.pending_invites[room.bare] = inviter.full
+ logger.log_roster_change(inviter.full, 'invited you to %s' % room.bare)
+
+### "classic" messages ###
+
+def on_message(self, message):
+ """
+ When receiving private message from a muc OR a normal message
+ (from one of our contacts)
+ """
+ if message.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite') != None:
+ return
+ if message['type'] == 'groupchat':
+ return
+ # Differentiate both type of messages, and call the appropriate handler.
+ jid_from = message['from']
+ for tab in self.get_tabs(tabs.MucTab):
+ if tab.name == jid_from.bare:
+ if message['type'] == 'chat':
+ return self.on_groupchat_private_message(message)
+ return self.on_normal_message(message)
+
+def on_error_message(self, message):
+ """
+ When receiving any message with type="error"
+ """
+ jid_from = message['from']
+ for tab in self.get_tabs(tabs.MucTab):
+ if tab.name == jid_from.bare:
+ if message['type'] == 'error':
+ return self.room_error(message, jid_from.bare)
+ else:
+ return self.on_groupchat_private_message(message)
+ tab = self.get_conversation_by_jid(message['from'], create=False)
+ error_msg = self.get_error_message(message, deprecated=True)
+ if not tab:
+ return self.information(error_msg, 'Error')
+ error = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_CHAR_NACK),
+ error_msg)
+ if not tab.nack_message('\n' + error, message['id'], message['to']):
+ tab.add_message(error, typ=0)
+ self.refresh_window()
+
+
+def on_normal_message(self, message):
+ """
+ When receiving "normal" messages (not a private message from a
+ muc participant)
+ """
+ if message['type'] == 'error':
+ return
+ elif message['type'] == 'headline' and message['body']:
+ return self.information('%s says: %s' % (message['from'], message['body']), 'Headline')
+
+ use_xhtml = config.get('enable_xhtml_im')
+ tmp_dir = config.get('tmp_image_dir') or path.join(CACHE_DIR, 'images')
+ extract_images = config.get('extract_inline_images')
+ body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml,
+ tmp_dir=tmp_dir,
+ extract_images=extract_images)
+ if not body:
+ return
+
+ remote_nick = ''
+ # normal message, we are the recipient
+ if message['to'].bare == self.xmpp.boundjid.bare:
+ conv_jid = message['from']
+ jid = conv_jid
+ color = get_theme().COLOR_REMOTE_USER
+ # check for a name
+ if conv_jid.bare in roster:
+ remote_nick = roster[conv_jid.bare].name
+ # check for a received nick
+ if not remote_nick and config.get('enable_user_nick'):
+ if message.xml.find('{http://jabber.org/protocol/nick}nick') is not None:
+ remote_nick = message['nick']['nick']
+ if not remote_nick:
+ remote_nick = conv_jid.user
+ if not remote_nick:
+ remote_nick = conv_jid.full
+ own = False
+ # we wrote the message (happens with carbons)
+ elif message['from'].bare == self.xmpp.boundjid.bare:
+ conv_jid = message['to']
+ jid = self.xmpp.boundjid
+ color = get_theme().COLOR_OWN_NICK
+ remote_nick = self.own_nick
+ own = True
+ # we are not part of that message, drop it
+ else:
+ return
+
+ conversation = self.get_conversation_by_jid(conv_jid, create=True)
+ if isinstance(conversation, tabs.DynamicConversationTab) and conv_jid.resource:
+ conversation.lock(conv_jid.resource)
+
+ if not own and not conversation.nick:
+ conversation.nick = remote_nick
+ elif not own: # keep a fixed nick during the whole conversation
+ remote_nick = conversation.nick
+
+ self.events.trigger('conversation_msg', message, conversation)
+ if not message['body']:
+ return
+ body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml,
+ tmp_dir=tmp_dir,
+ extract_images=extract_images)
+ delayed, date = common.find_delayed_tag(message)
+
+ def try_modify():
+ replaced_id = message['replace']['id']
+ if replaced_id and config.get_by_tabname('group_corrections',
+ conv_jid.bare):
+ try:
+ conversation.modify_message(body, replaced_id, message['id'], jid=jid,
+ nickname=remote_nick)
+ return True
+ except CorrectionError:
+ log.debug('Unable to correct a message', exc_info=True)
+ return False
+
+ if not try_modify():
+ conversation.add_message(body, date,
+ nickname=remote_nick,
+ nick_color=color,
+ history=delayed,
+ identifier=message['id'],
+ jid=jid,
+ typ=1)
+
+ if conversation.remote_wants_chatstates is None and not delayed:
+ if message['chat_state']:
+ conversation.remote_wants_chatstates = True
+ else:
+ conversation.remote_wants_chatstates = False
+ if not own and 'private' in config.get('beep_on').split():
+ if not config.get_by_tabname('disable_beep', conv_jid.bare):
+ curses.beep()
+ if self.current_tab() is not conversation:
+ if not own:
+ conversation.state = 'private'
+ self.refresh_tab_win()
+ else:
+ conversation.set_state('normal')
+ self.refresh_tab_win()
+ else:
+ self.refresh_window()
+
+def on_nick_received(self, message):
+ """
+ Called when a pep notification for an user nickname
+ is received
+ """
+ contact = roster[message['from'].bare]
+ if not contact:
+ return
+ item = message['pubsub_event']['items']['item']
+ if item.xml.find('{http://jabber.org/protocol/nick}nick'):
+ contact.name = item['nick']['nick']
+ else:
+ contact.name = ''
+
+def on_gaming_event(self, message):
+ """
+ Called when a pep notification for user gaming
+ is received
+ """
+ contact = roster[message['from'].bare]
+ if not contact:
+ return
+ item = message['pubsub_event']['items']['item']
+ old_gaming = contact.gaming
+ if item.xml.find('{urn:xmpp:gaming:0}gaming'):
+ item = item['gaming']
+ # only name and server_address are used for now
+ contact.gaming = {
+ 'character_name': item['character_name'],
+ 'character_profile': item['character_profile'],
+ 'name': item['name'],
+ 'level': item['level'],
+ 'uri': item['uri'],
+ 'server_name': item['server_name'],
+ 'server_address': item['server_address'],
+ }
+ else:
+ contact.gaming = {}
+
+ if contact.gaming:
+ logger.log_roster_change(contact.bare_jid, 'is playing %s' % (common.format_gaming_string(contact.gaming)))
+
+ if old_gaming != contact.gaming and config.get_by_tabname('display_gaming_notifications', contact.bare_jid):
+ if contact.gaming:
+ self.information('%s is playing %s' % (contact.bare_jid, common.format_gaming_string(contact.gaming)), 'Gaming')
+ else:
+ self.information(contact.bare_jid + ' stopped playing.', 'Gaming')
+
+def on_mood_event(self, message):
+ """
+ Called when a pep notification for an user mood
+ is received.
+ """
+ contact = roster[message['from'].bare]
+ if not contact:
+ return
+ roster.modified()
+ item = message['pubsub_event']['items']['item']
+ old_mood = contact.mood
+ if item.xml.find('{http://jabber.org/protocol/mood}mood'):
+ mood = item['mood']['value']
+ if mood:
+ mood = pep.MOODS.get(mood, mood)
+ text = item['mood']['text']
+ if text:
+ mood = '%s (%s)' % (mood, text)
+ contact.mood = mood
+ else:
+ contact.mood = ''
+ else:
+ contact.mood = ''
+
+ if contact.mood:
+ logger.log_roster_change(contact.bare_jid, 'has now the mood: %s' % contact.mood)
+
+ if old_mood != contact.mood and config.get_by_tabname('display_mood_notifications', contact.bare_jid):
+ if contact.mood:
+ self.information('Mood from '+ contact.bare_jid + ': ' + contact.mood, 'Mood')
+ else:
+ self.information(contact.bare_jid + ' stopped having his/her mood.', 'Mood')
+
+def on_activity_event(self, message):
+ """
+ Called when a pep notification for an user activity
+ is received.
+ """
+ contact = roster[message['from'].bare]
+ if not contact:
+ return
+ roster.modified()
+ item = message['pubsub_event']['items']['item']
+ old_activity = contact.activity
+ if item.xml.find('{http://jabber.org/protocol/activity}activity'):
+ try:
+ activity = item['activity']['value']
+ except ValueError:
+ return
+ if activity[0]:
+ general = pep.ACTIVITIES.get(activity[0])
+ s = general['category']
+ if activity[1]:
+ s = s + '/' + general.get(activity[1], 'other')
+ text = item['activity']['text']
+ if text:
+ s = '%s (%s)' % (s, text)
+ contact.activity = s
+ else:
+ contact.activity = ''
+ else:
+ contact.activity = ''
+
+ if contact.activity:
+ logger.log_roster_change(contact.bare_jid, 'has now the activity %s' % contact.activity)
+
+ if old_activity != contact.activity and config.get_by_tabname('display_activity_notifications', contact.bare_jid):
+ if contact.activity:
+ self.information('Activity from '+ contact.bare_jid + ': ' + contact.activity, 'Activity')
+ else:
+ self.information(contact.bare_jid + ' stopped doing his/her activity.', 'Activity')
+
+def on_tune_event(self, message):
+ """
+ Called when a pep notification for an user tune
+ is received
+ """
+ contact = roster[message['from'].bare]
+ if not contact:
+ return
+ roster.modified()
+ item = message['pubsub_event']['items']['item']
+ old_tune = contact.tune
+ if item.xml.find('{http://jabber.org/protocol/tune}tune'):
+ item = item['tune']
+ contact.tune = {
+ 'artist': item['artist'],
+ 'length': item['length'],
+ 'rating': item['rating'],
+ 'source': item['source'],
+ 'title': item['title'],
+ 'track': item['track'],
+ 'uri': item['uri']
+ }
+ else:
+ contact.tune = {}
+
+ if contact.tune:
+ logger.log_roster_change(message['from'].bare, 'is now listening to %s' % common.format_tune_string(contact.tune))
+
+ if old_tune != contact.tune and config.get_by_tabname('display_tune_notifications', contact.bare_jid):
+ if contact.tune:
+ self.information(
+ 'Tune from '+ message['from'].bare + ': ' + common.format_tune_string(contact.tune),
+ 'Tune')
+ else:
+ self.information(contact.bare_jid + ' stopped listening to music.', 'Tune')
+
+def on_groupchat_message(self, message):
+ """
+ Triggered whenever a message is received from a multi-user chat room.
+ """
+ if message['subject']:
+ return
+ room_from = message['from'].bare
+
+ if message['type'] == 'error': # Check if it's an error
+ return self.room_error(message, room_from)
+
+ tab = self.get_tab_by_name(room_from, tabs.MucTab)
+ if not tab:
+ self.information("message received for a non-existing room: %s" % (room_from))
+ muc.leave_groupchat(self.xmpp, room_from, self.own_nick, msg='')
+ return
+
+ nick_from = message['mucnick']
+ user = tab.get_user_by_name(nick_from)
+ if user and user in tab.ignores:
+ return
+
+ self.events.trigger('muc_msg', message, tab)
+ use_xhtml = config.get('enable_xhtml_im')
+ tmp_dir = config.get('tmp_image_dir') or path.join(CACHE_DIR, 'images')
+ extract_images = config.get('extract_inline_images')
+ body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml,
+ tmp_dir=tmp_dir,
+ extract_images=extract_images)
+ if not body:
+ return
+
+ old_state = tab.state
+ delayed, date = common.find_delayed_tag(message)
+ replaced_id = message['replace']['id']
+ replaced = False
+ if replaced_id is not '' and config.get_by_tabname('group_corrections',
+ message['from'].bare):
+ try:
+ delayed_date = date or datetime.now()
+ if tab.modify_message(body, replaced_id, message['id'],
+ time=delayed_date,
+ nickname=nick_from, user=user):
+ self.events.trigger('highlight', message, tab)
+ replaced = True
+ except CorrectionError:
+ log.debug('Unable to correct a message', exc_info=True)
+ if not replaced and tab.add_message(body, date, nick_from, history=delayed, identifier=message['id'], jid=message['from'], typ=1):
+ self.events.trigger('highlight', message, tab)
+
+ if message['from'].resource == tab.own_nick:
+ tab.last_sent_message = message
+
+ if tab is self.current_tab():
+ tab.text_win.refresh()
+ tab.info_header.refresh(tab, tab.text_win)
+ tab.input.refresh()
+ self.doupdate()
+ elif tab.state != old_state:
+ self.refresh_tab_win()
+ current = self.current_tab()
+ if hasattr(current, 'input') and current.input:
+ current.input.refresh()
+ self.doupdate()
+
+ if 'message' in config.get('beep_on').split():
+ if (not config.get_by_tabname('disable_beep', room_from)
+ and self.own_nick != message['from'].resource):
+ curses.beep()
+
+def on_muc_own_nickchange(self, muc):
+ "We changed our nick in a MUC"
+ for tab in self.get_tabs(tabs.PrivateTab):
+ if tab.parent_muc == muc:
+ tab.own_nick = muc.own_nick
+
+def on_groupchat_private_message(self, message):
+ """
+ We received a Private Message (from someone in a Muc)
+ """
+ jid = message['from']
+ nick_from = jid.resource
+ if not nick_from:
+ return self.on_groupchat_message(message)
+
+ room_from = jid.bare
+ use_xhtml = config.get('enable_xhtml_im')
+ tmp_dir = config.get('tmp_image_dir') or path.join(CACHE_DIR, 'images')
+ extract_images = config.get('extract_inline_images')
+ body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml,
+ tmp_dir=tmp_dir,
+ extract_images=extract_images)
+ tab = self.get_tab_by_name(jid.full, tabs.PrivateTab) # get the tab with the private conversation
+ ignore = config.get_by_tabname('ignore_private', room_from)
+ if not tab: # It's the first message we receive: create the tab
+ if body and not ignore:
+ tab = self.open_private_window(room_from, nick_from, False)
+ if ignore:
+ self.events.trigger('ignored_private', message, tab)
+ msg = config.get_by_tabname('private_auto_response', room_from)
+ if msg and body:
+ self.xmpp.send_message(mto=jid.full, mbody=msg, mtype='chat')
+ return
+ self.events.trigger('private_msg', message, tab)
+ body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml,
+ tmp_dir=tmp_dir,
+ extract_images=extract_images)
+ if not body or not tab:
+ return
+ replaced_id = message['replace']['id']
+ replaced = False
+ user = tab.parent_muc.get_user_by_name(nick_from)
+ if replaced_id is not '' and config.get_by_tabname('group_corrections',
+ room_from):
+ try:
+ tab.modify_message(body, replaced_id, message['id'], user=user, jid=message['from'],
+ nickname=nick_from)
+ replaced = True
+ except CorrectionError:
+ log.debug('Unable to correct a message', exc_info=True)
+ if not replaced:
+ tab.add_message(body, time=None, nickname=nick_from,
+ forced_user=user,
+ identifier=message['id'],
+ jid=message['from'],
+ typ=1)
+
+ if tab.remote_wants_chatstates is None:
+ if message['chat_state']:
+ tab.remote_wants_chatstates = True
+ else:
+ tab.remote_wants_chatstates = False
+ if 'private' in config.get('beep_on').split():
+ if not config.get_by_tabname('disable_beep', jid.full):
+ curses.beep()
+ if tab is self.current_tab():
+ self.refresh_window()
+ else:
+ tab.state = 'private'
+ self.refresh_tab_win()
+
+### Chatstates ###
+
+def on_chatstate_active(self, message):
+ self.on_chatstate(message, "active")
+
+def on_chatstate_inactive(self, message):
+ self.on_chatstate(message, "inactive")
+
+def on_chatstate_composing(self, message):
+ self.on_chatstate(message, "composing")
+
+def on_chatstate_paused(self, message):
+ self.on_chatstate(message, "paused")
+
+def on_chatstate_gone(self, message):
+ self.on_chatstate(message, "gone")
+
+def on_chatstate(self, message, state):
+ if message['type'] == 'chat':
+ if not self.on_chatstate_normal_conversation(message, state):
+ tab = self.get_tab_by_name(message['from'].full, tabs.PrivateTab)
+ if not tab:
+ return
+ self.on_chatstate_private_conversation(message, state)
+ elif message['type'] == 'groupchat':
+ self.on_chatstate_groupchat_conversation(message, state)
+
+def on_chatstate_normal_conversation(self, message, state):
+ tab = self.get_conversation_by_jid(message['from'], False)
+ if not tab:
+ return False
+ tab.remote_wants_chatstates = True
+ self.events.trigger('normal_chatstate', message, tab)
+ tab.chatstate = state
+ if state == 'gone' and isinstance(tab, tabs.DynamicConversationTab):
+ tab.unlock()
+ if tab == self.current_tab():
+ tab.refresh_info_header()
+ self.doupdate()
+ else:
+ _composing_tab_state(tab, state)
+ self.refresh_tab_win()
+ return True
+
+def on_chatstate_private_conversation(self, message, state):
+ """
+ Chatstate received in a private conversation from a MUC
+ """
+ tab = self.get_tab_by_name(message['from'].full, tabs.PrivateTab)
+ if not tab:
+ return
+ tab.remote_wants_chatstates = True
+ self.events.trigger('private_chatstate', message, tab)
+ tab.chatstate = state
+ if tab == self.current_tab():
+ tab.refresh_info_header()
+ self.doupdate()
+ else:
+ _composing_tab_state(tab, state)
+ self.refresh_tab_win()
+ return True
+
+def on_chatstate_groupchat_conversation(self, message, state):
+ """
+ Chatstate received in a MUC
+ """
+ nick = message['mucnick']
+ room_from = message.get_mucroom()
+ tab = self.get_tab_by_name(room_from, tabs.MucTab)
+ if tab and tab.get_user_by_name(nick):
+ self.events.trigger('muc_chatstate', message, tab)
+ tab.get_user_by_name(nick).chatstate = state
+ if tab == self.current_tab():
+ if not self.size.tab_degrade_x:
+ tab.user_win.refresh(tab.users)
+ tab.input.refresh()
+ self.doupdate()
+ else:
+ _composing_tab_state(tab, state)
+ self.refresh_tab_win()
+
+### subscription-related handlers ###
+
+def on_roster_update(self, iq):
+ """
+ The roster was received.
+ """
+ for item in iq['roster']:
+ try:
+ jid = item['jid']
+ except InvalidJID:
+ jid = item._get_attr('jid', '')
+ log.error('Invalid JID: "%s"', jid, exc_info=True)
+ else:
+ if item['subscription'] == 'remove':
+ del roster[jid]
+ else:
+ roster.update_contact_groups(jid)
+ roster.update_size()
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
+
+def on_subscription_request(self, presence):
+ """subscribe received"""
+ jid = presence['from'].bare
+ contact = roster[jid]
+ if contact and contact.subscription in ('from', 'both'):
+ return
+ elif contact and contact.subscription == 'to':
+ self.xmpp.sendPresence(pto=jid, ptype='subscribed')
+ self.xmpp.sendPresence(pto=jid)
+ else:
+ if not contact:
+ contact = roster.get_and_set(jid)
+ roster.update_contact_groups(contact)
+ contact.pending_in = True
+ self.information('%s wants to subscribe to your presence, use '
+ '/accept <jid> or /deny <jid> in the roster '
+ 'tab to accept or reject the query.' % jid,
+ 'Roster')
+ self.get_tab_by_number(0).state = 'highlight'
+ roster.modified()
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
+
+def on_subscription_authorized(self, presence):
+ """subscribed received"""
+ jid = presence['from'].bare
+ contact = roster[jid]
+ if contact.subscription not in ('both', 'from'):
+ self.information('%s accepted your contact proposal' % jid, 'Roster')
+ if contact.pending_out:
+ contact.pending_out = False
+
+ roster.modified()
+
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
+
+def on_subscription_remove(self, presence):
+ """unsubscribe received"""
+ jid = presence['from'].bare
+ contact = roster[jid]
+ if not contact:
+ return
+ roster.modified()
+ self.information('%s does not want to receive your status anymore.' % jid, 'Roster')
+ self.get_tab_by_number(0).state = 'highlight'
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
+
+def on_subscription_removed(self, presence):
+ """unsubscribed received"""
+ jid = presence['from'].bare
+ contact = roster[jid]
+ if not contact:
+ return
+ roster.modified()
+ if contact.pending_out:
+ self.information('%s rejected your contact proposal' % jid, 'Roster')
+ contact.pending_out = False
+ else:
+ self.information('%s does not want you to receive his/her/its status anymore.'%jid, 'Roster')
+ self.get_tab_by_number(0).state = 'highlight'
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
+
+### Presence-related handlers ###
+
+def on_presence(self, presence):
+ if presence.match('presence/muc') or presence.xml.find('{http://jabber.org/protocol/muc#user}x'):
+ return
+ jid = presence['from']
+ contact = roster[jid.bare]
+ tab = self.get_conversation_by_jid(jid, create=False)
+ if isinstance(tab, tabs.DynamicConversationTab):
+ if tab.get_dest_jid() != jid.full:
+ tab.unlock(from_=jid.full)
+ elif presence['type'] == 'unavailable':
+ tab.unlock()
+ if contact is None:
+ return
+ roster.modified()
+ contact.error = None
+ self.events.trigger('normal_presence', presence, contact[jid.full])
+ tab = self.get_conversation_by_jid(jid, create=False)
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
+ elif self.current_tab() == tab:
+ tab.refresh()
+ self.doupdate()
+
+def on_presence_error(self, presence):
+ jid = presence['from']
+ contact = roster[jid.bare]
+ if not contact:
+ return
+ roster.modified()
+ contact.error = presence['error']['type'] + ': ' + presence['error']['condition']
+ # reset chat states status on presence error
+ tab = self.get_tab_by_name(jid.full, tabs.ConversationTab)
+ if tab:
+ tab.remote_wants_chatstates = None
+
+def on_got_offline(self, presence):
+ """
+ A JID got offline
+ """
+ if presence.match('presence/muc') or presence.xml.find('{http://jabber.org/protocol/muc#user}x'):
+ return
+ jid = presence['from']
+ if not logger.log_roster_change(jid.bare, 'got offline'):
+ self.information('Unable to write in the log file', 'Error')
+ # If a resource got offline, display the message in the conversation with this
+ # precise resource.
+ contact = roster[jid.bare]
+ name = jid.bare
+ if contact:
+ roster.connected -= 1
+ if contact.name:
+ name = contact.name
+ if jid.resource:
+ self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x191}offline' % name)
+ self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x191}offline' % name)
+ self.information('\x193}%s \x195}is \x191}offline' % name, 'Roster')
+ roster.modified()
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
+
+def on_got_online(self, presence):
+ """
+ A JID got online
+ """
+ if presence.match('presence/muc') or presence.xml.find('{http://jabber.org/protocol/muc#user}x'):
+ return
+ jid = presence['from']
+ contact = roster[jid.bare]
+ if contact is None:
+ # Todo, handle presence coming from contacts not in roster
+ return
+ roster.connected += 1
+ roster.modified()
+ if not logger.log_roster_change(jid.bare, 'got online'):
+ self.information('Unable to write in the log file', 'Error')
+ resource = Resource(jid.full, {
+ 'priority': presence.get_priority() or 0,
+ 'status': presence['status'],
+ 'show': presence['show'],
+ })
+ self.events.trigger('normal_presence', presence, resource)
+ name = contact.name if contact.name else jid.bare
+ self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x194}online' % name)
+ if time.time() - self.connection_time > 10:
+ # We do not display messages if we recently logged in
+ if presence['status']:
+ self.information("\x193}%s \x195}is \x194}online\x195} (\x19o%s\x195})" % (name, presence['status']), "Roster")
+ else:
+ self.information("\x193}%s \x195}is \x194}online\x195}" % name, "Roster")
+ self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x194}online' % name)
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
+
+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_room = presence['from'].bare
+ tab = self.get_tab_by_name(from_room, tabs.MucTab)
+ if tab:
+ self.events.trigger('muc_presence', presence, tab)
+ tab.handle_presence(presence)
+
+
+### Connection-related handlers ###
+
+def on_failed_connection(self, error):
+ """
+ We cannot contact the remote server
+ """
+ self.information("Connection to remote server failed: %s" % (error,), 'Error')
+
+def on_disconnected(self, event):
+ """
+ When we are disconnected from remote server
+ """
+ roster.connected = 0
+ # Stop the ping plugin. It would try to send stanza on regular basis
+ self.xmpp.plugin['xep_0199'].disable_keepalive()
+ roster.modified()
+ for tab in self.get_tabs(tabs.MucTab):
+ tab.disconnect()
+ msg_typ = 'Error' if not self.legitimate_disconnect else 'Info'
+ self.information("Disconnected from server.", msg_typ)
+ if not self.legitimate_disconnect and config.get('auto_reconnect', True):
+ self.information("Auto-reconnecting.", 'Info')
+ self.xmpp.start()
+
+def on_stream_error(self, event):
+ """
+ When we receive a stream error
+ """
+ if event and event['text']:
+ self.information('Stream error: %s' % event['text'], 'Error')
+
+def on_failed_all_auth(self, event):
+ """
+ Authentication failed
+ """
+ self.information("Authentication failed (bad credentials?).",
+ 'Error')
+ self.legitimate_disconnect = True
+
+def on_no_auth(self, event):
+ """
+ Authentication failed (no mech)
+ """
+ self.information("Authentication failed, no login method available.",
+ 'Error')
+ self.legitimate_disconnect = True
+
+def on_connected(self, event):
+ """
+ Remote host responded, but we are not yet authenticated
+ """
+ self.information("Connected to server.", 'Info')
+
+def on_connecting(self, event):
+ """
+ Just before we try to connect to the server
+ """
+ self.legitimate_disconnect = False
+
+def on_session_start(self, event):
+ """
+ Called when we are connected and authenticated
+ """
+ self.connection_time = time.time()
+ if not self.plugins_autoloaded: # Do not reload plugins on reconnection
+ self.autoload_plugins()
+ self.information("Authentication success.", 'Info')
+ self.information("Your JID is %s" % self.xmpp.boundjid.full, 'Info')
+ if not self.xmpp.anon:
+ # request the roster
+ self.xmpp.get_roster()
+ roster.update_contact_groups(self.xmpp.boundjid.bare)
+ # send initial presence
+ if config.get('send_initial_presence'):
+ pres = self.xmpp.make_presence()
+ pres['show'] = self.status.show
+ pres['status'] = self.status.message
+ self.events.trigger('send_normal_presence', pres)
+ pres.send()
+ self.bookmarks.get_local()
+ # join all the available bookmarks. As of yet, this is just the local ones
+ _join_initial_rooms(self, self.bookmarks)
+
+ if config.get('enable_user_nick'):
+ self.xmpp.plugin['xep_0172'].publish_nick(nick=self.own_nick, callback=dumb_callback)
+ asyncio.async(self.xmpp.plugin['xep_0115'].update_caps())
+ # Start the ping's plugin regular event
+ self.xmpp.set_keepalive_values()
+
+### Other handlers ###
+
+def on_status_codes(self, message):
+ """
+ Handle groupchat messages with status codes.
+ Those are received when a room configuration change occurs.
+ """
+ room_from = message['from']
+ tab = self.get_tab_by_name(room_from, tabs.MucTab)
+ status_codes = set([s.attrib['code'] for s in message.findall('{%s}x/{%s}status' % (tabs.NS_MUC_USER, tabs.NS_MUC_USER))])
+ if '101' in status_codes:
+ self.information('Your affiliation in the room %s changed' % room_from, 'Info')
+ elif tab and status_codes:
+ show_unavailable = '102' in status_codes
+ hide_unavailable = '103' in status_codes
+ non_priv = '104' in status_codes
+ logging_on = '170' in status_codes
+ logging_off = '171' in status_codes
+ non_anon = '172' in status_codes
+ semi_anon = '173' in status_codes
+ full_anon = '174' in status_codes
+ modif = False
+ if show_unavailable or hide_unavailable or non_priv or logging_off\
+ or non_anon or semi_anon or full_anon:
+ tab.add_message('\x19%(info_col)s}Info: A configuration change not privacy-related occured.' %
+ {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ typ=2)
+ modif = True
+ if show_unavailable:
+ tab.add_message('\x19%(info_col)s}Info: The unavailable members are now shown.' %
+ {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ typ=2)
+ elif hide_unavailable:
+ tab.add_message('\x19%(info_col)s}Info: The unavailable members are now hidden.' %
+ {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ typ=2)
+ if non_anon:
+ tab.add_message('\x191}Warning:\x19%(info_col)s} The room is now not anonymous. (public JID)' %
+ {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ typ=2)
+ elif semi_anon:
+ tab.add_message('\x19%(info_col)s}Info: The room is now semi-anonymous. (moderators-only JID)' %
+ {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ typ=2)
+ elif full_anon:
+ tab.add_message('\x19%(info_col)s}Info: The room is now fully anonymous.' %
+ {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ typ=2)
+ if logging_on:
+ tab.add_message('\x191}Warning: \x19%(info_col)s}This room is publicly logged' %
+ {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ typ=2)
+ elif logging_off:
+ tab.add_message('\x19%(info_col)s}Info: This room is not logged anymore.' %
+ {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ typ=2)
+ if modif:
+ self.refresh_window()
+
+def on_groupchat_subject(self, message):
+ """
+ Triggered when the topic is changed.
+ """
+ nick_from = message['mucnick']
+ room_from = message.get_mucroom()
+ tab = self.get_tab_by_name(room_from, tabs.MucTab)
+ subject = message['subject']
+ if subject is None or not tab:
+ return
+ if subject != tab.topic:
+ # Do not display the message if the subject did not change or if we
+ # receive an empty topic when joining the room.
+ if nick_from:
+ tab.add_message("\x19%(info_col)s}%(nick)s set the subject to: %(subject)s" %
+ {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT), 'nick':nick_from, 'subject':subject},
+ time=None,
+ typ=2)
+ else:
+ tab.add_message("\x19%(info_col)s}The subject is: %(subject)s" %
+ {'subject':subject, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
+ time=None,
+ typ=2)
+ tab.topic = subject
+ tab.topic_from = nick_from
+ if self.get_tab_by_name(room_from, tabs.MucTab) is self.current_tab():
+ self.refresh_window()
+
+def on_receipt(self, message):
+ """
+ When a delivery receipt is received (XEP-0184)
+ """
+ jid = message['from']
+ msg_id = message['receipt']
+ if not msg_id:
+ return
+
+ conversation = self.get_tab_by_name(jid, tabs.ChatTab)
+ conversation = conversation or self.get_tab_by_name(jid.bare, tabs.ChatTab)
+ if not conversation:
+ return
+
+ try:
+ conversation.ack_message(msg_id, self.xmpp.boundjid)
+ except AckError:
+ log.debug('Error while receiving an ack', exc_info=True)
+
+def on_data_form(self, message):
+ """
+ When a data form is received
+ """
+ self.information('%s' % message)
+
+def on_attention(self, message):
+ """
+ Attention probe received.
+ """
+ jid_from = message['from']
+ self.information('%s requests your attention!' % jid_from, 'Info')
+ for tab in self.tabs:
+ if tab.name == jid_from:
+ tab.state = 'attention'
+ self.refresh_tab_win()
+ return
+ for tab in self.tabs:
+ if tab.name == jid_from.bare:
+ tab.state = 'attention'
+ self.refresh_tab_win()
+ return
+ self.information('%s tab not found.' % jid_from, 'Error')
+
+def room_error(self, error, room_name):
+ """
+ Display the error in the tab
+ """
+ tab = self.get_tab_by_name(room_name, tabs.MucTab)
+ if not tab:
+ return
+ error_message = self.get_error_message(error)
+ tab.add_message(error_message, highlight=True, nickname='Error',
+ nick_color=get_theme().COLOR_ERROR_MSG, typ=2)
+ code = error['error']['code']
+ if code == '401':
+ msg = 'To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)'
+ tab.add_message(msg, typ=2)
+ if code == '409':
+ if config.get('alternative_nickname') != '':
+ self.command_join('%s/%s'% (tab.name, tab.own_nick+config.get('alternative_nickname')))
+ else:
+ if not tab.joined:
+ tab.add_message('You can join the room with an other nick, by typing "/join /other_nick"', typ=2)
+ self.refresh_window()
+
+def outgoing_stanza(self, stanza):
+ """
+ We are sending a new stanza, write it in the xml buffer if needed.
+ """
+ if self.xml_tab:
+ if PYGMENTS:
+ xhtml_text = highlight('%s' % stanza, LEXER, FORMATTER)
+ poezio_colored = xhtml.xhtml_to_poezio_colors(xhtml_text, force=True).rstrip('\x19o').strip()
+ else:
+ poezio_colored = '%s' % stanza
+ self.add_message_to_text_buffer(self.xml_buffer, poezio_colored,
+ nickname=get_theme().CHAR_XML_OUT)
+ try:
+ if self.xml_tab.match_stanza(ElementBase(ET.fromstring(stanza))):
+ self.add_message_to_text_buffer(self.xml_tab.filtered_buffer, poezio_colored,
+ nickname=get_theme().CHAR_XML_OUT)
+ except:
+ log.debug('', exc_info=True)
+
+ if isinstance(self.current_tab(), tabs.XMLTab):
+ self.current_tab().refresh()
+ self.doupdate()
+
+def incoming_stanza(self, stanza):
+ """
+ We are receiving a new stanza, write it in the xml buffer if needed.
+ """
+ if self.xml_tab:
+ if PYGMENTS:
+ xhtml_text = highlight('%s' % stanza, LEXER, FORMATTER)
+ poezio_colored = xhtml.xhtml_to_poezio_colors(xhtml_text, force=True).rstrip('\x19o').strip()
+ else:
+ poezio_colored = '%s' % stanza
+ self.add_message_to_text_buffer(self.xml_buffer, poezio_colored,
+ nickname=get_theme().CHAR_XML_IN)
+ try:
+ if self.xml_tab.match_stanza(stanza):
+ self.add_message_to_text_buffer(self.xml_tab.filtered_buffer, poezio_colored,
+ nickname=get_theme().CHAR_XML_IN)
+ except:
+ log.debug('', exc_info=True)
+ if isinstance(self.current_tab(), tabs.XMLTab):
+ self.current_tab().refresh()
+ self.doupdate()
+
+def ssl_invalid_chain(self, tb):
+ self.information('The certificate sent by the server is invalid.', 'Error')
+ self.disconnect()
+
+def validate_ssl(self, pem):
+ """
+ Check the server certificate using the slixmpp ssl_cert event
+ """
+ if config.get('ignore_certificate'):
+ return
+ cert = config.get('certificate')
+ # update the cert representation when it uses the old one
+ if cert and not ':' in cert:
+ cert = ':'.join(i + j for i, j in zip(cert[::2], cert[1::2])).upper()
+ config.set_and_save('certificate', cert)
+
+ der = ssl.PEM_cert_to_DER_cert(pem)
+ sha1_digest = sha1(der).hexdigest().upper()
+ sha1_found_cert = ':'.join(i + j for i, j in zip(sha1_digest[::2], sha1_digest[1::2]))
+ sha2_digest = sha512(der).hexdigest().upper()
+ sha2_found_cert = ':'.join(i + j for i, j in zip(sha2_digest[::2], sha2_digest[1::2]))
+ if cert:
+ if sha1_found_cert == cert:
+ log.debug('Cert %s OK', sha1_found_cert)
+ log.debug('Current hash is SHA-1, moving to SHA-2 (%s)',
+ sha2_found_cert)
+ config.set_and_save('certificate', sha2_found_cert)
+ return
+ elif sha2_found_cert == cert:
+ log.debug('Cert %s OK', sha2_found_cert)
+ return
+ else:
+ saved_input = self.current_tab().input
+ log.debug('\nWARNING: CERTIFICATE CHANGED old: %s, new: %s\n', cert, sha2_found_cert)
+ self.information('New certificate found (sha-2 hash:'
+ ' %s)\nPlease validate or abort' % sha2_found_cert,
+ 'Warning')
+ def check_input():
+ self.current_tab().input = saved_input
+ if input.value:
+ self.information('Setting new certificate: old: %s, new: %s' % (cert, sha2_found_cert), 'Info')
+ log.debug('Setting certificate to %s', sha2_found_cert)
+ if not config.silent_set('certificate', sha2_found_cert):
+ self.information('Unable to write in the config file', 'Error')
+ else:
+ self.information('You refused to validate the certificate. You are now disconnected', 'Info')
+ self.disconnect()
+ new_loop.stop()
+ asyncio.set_event_loop(old_loop)
+ input = windows.YesNoInput(text="WARNING! Server certificate has changed, accept? (y/n)", callback=check_input)
+ self.current_tab().input = input
+ input.resize(1, self.current_tab().width, self.current_tab().height-1, 0)
+ input.refresh()
+ self.doupdate()
+ old_loop = asyncio.get_event_loop()
+ new_loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(new_loop)
+ new_loop.add_reader(sys.stdin, self.on_input_readable)
+ curses.beep()
+ new_loop.run_forever()
+ else:
+ log.debug('First time. Setting certificate to %s', sha2_found_cert)
+ if not config.silent_set('certificate', sha2_found_cert):
+ self.information('Unable to write in the config file', 'Error')
+
+def _composing_tab_state(tab, state):
+ """
+ Set a tab state to or from the "composing" state
+ according to the config and the current tab state
+ """
+ if isinstance(tab, tabs.MucTab):
+ values = ('true', 'muc')
+ elif isinstance(tab, tabs.PrivateTab):
+ values = ('true', 'direct', 'private')
+ elif isinstance(tab, tabs.ConversationTab):
+ values = ('true', 'direct', 'conversation')
+ else:
+ return # should not happen
+
+ show = config.get('show_composing_tabs')
+ show = show in values
+
+ if tab.state != 'composing' and state == 'composing':
+ if show:
+ if tabs.STATE_PRIORITY[tab.state] > tabs.STATE_PRIORITY[state]:
+ return
+ tab.save_state()
+ tab.state = 'composing'
+ elif tab.state == 'composing' and state != 'composing':
+ tab.restore_state()
+
+### Ad-hoc commands
+
+def on_next_adhoc_step(self, iq, adhoc_session):
+ status = iq['command']['status']
+ xform = iq.xml.find('{http://jabber.org/protocol/commands}command/{jabber:x:data}x')
+ if xform is not None:
+ form = self.xmpp.plugin['xep_0004'].buildForm(xform)
+ else:
+ form = None
+
+ if status == 'error':
+ return self.information("An error occured while executing the command")
+
+ if status == 'executing':
+ if not form:
+ self.information("Adhoc command step does not contain a data-form. Aborting the execution.", "Error")
+ return self.xmpp.plugin['xep_0050'].cancel_command(adhoc_session)
+ on_validate = self.validate_adhoc_step
+ on_cancel = self.cancel_adhoc_command
+ if status == 'completed':
+ on_validate = lambda form, session: self.close_tab()
+ on_cancel = lambda form, session: self.close_tab()
+
+ # If a form is available, use it, and add the Notes from the
+ # response to it, if any
+ if form:
+ for note in iq['command']['notes']:
+ form.add_field(type='fixed', label=note[1])
+ self.open_new_form(form, on_cancel, on_validate,
+ session=adhoc_session)
+ else: # otherwise, just display an information
+ # message
+ notes = '\n'.join([note[1] for note in iq['command']['notes']])
+ self.information("Adhoc command %s: %s" % (status, notes), "Info")
+
+def on_adhoc_error(self, iq, adhoc_session):
+ self.xmpp.plugin['xep_0050'].terminate_command(adhoc_session)
+ error_message = self.get_error_message(iq)
+ self.information("An error occured while executing the command: %s" % (error_message),
+ 'Error')
+
+def cancel_adhoc_command(self, form, session):
+ self.xmpp.plugin['xep_0050'].cancel_command(session)
+ self.close_tab()
+
+def validate_adhoc_step(self, form, session):
+ session['payload'] = form
+ self.xmpp.plugin['xep_0050'].continue_command(session)
+ self.close_tab()
+
+def terminate_adhoc_command(self, form, session):
+ self.xmpp.plugin['xep_0050'].terminate_command(session)
+ self.close_tab()
diff --git a/poezio/core/structs.py b/poezio/core/structs.py
new file mode 100644
index 00000000..4ce0ef43
--- /dev/null
+++ b/poezio/core/structs.py
@@ -0,0 +1,49 @@
+"""
+Module defining structures useful to the core class and related methods
+"""
+import collections
+
+# http://xmpp.org/extensions/xep-0045.html#errorstatus
+ERROR_AND_STATUS_CODES = {
+ '401': 'A password is required',
+ '403': 'Permission denied',
+ '404': 'The room doesn’t 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',
+ }
+
+# http://xmpp.org/extensions/xep-0086.html
+DEPRECATED_ERRORS = {
+ '302': 'Redirect',
+ '400': 'Bad request',
+ '401': 'Not authorized',
+ '402': 'Payment required',
+ '403': 'Forbidden',
+ '404': 'Not found',
+ '405': 'Not allowed',
+ '406': 'Not acceptable',
+ '407': 'Registration required',
+ '408': 'Request timeout',
+ '409': 'Conflict',
+ '500': 'Internal server error',
+ '501': 'Feature not implemented',
+ '502': 'Remote server error',
+ '503': 'Service unavailable',
+ '504': 'Remote server timeout',
+ '510': 'Disconnected',
+}
+
+possible_show = {'available':None,
+ 'chat':'chat',
+ 'away':'away',
+ 'afk':'away',
+ 'dnd':'dnd',
+ 'busy':'dnd',
+ 'xa':'xa'
+ }
+
+Status = collections.namedtuple('Status', 'show message')
+Command = collections.namedtuple('Command', 'func desc comp short usage')