Verified Commit 1c1ab3cb authored by mathieui's avatar mathieui

Merge branch 'master' of git.poez.io:poezio into slix

Conflicts:
	src/bookmark.py
	src/config.py
	src/connection.py
	src/core/commands.py
	src/core/core.py
	src/core/handlers.py
	src/windows/info_bar.py
	src/windows/muc.py
	src/windows/roster_win.py
	src/windows/text_win.py
	src/xhtml.py
parents cedc5a6e ea2b703b
language: python
python:
- "3.4"
install:
- pip install -r requirements.txt
- python setup.py build_ext --inplace
script: make test
......@@ -32,6 +32,10 @@ uninstall:
doc:
make -C doc/ html
test:
py.test -v test/
pot:
xgettext src/*.py --from-code=utf-8 --keyword=_ -o locale/poezio.pot
......@@ -45,4 +49,4 @@ release:
tar cJf poezio-$(version).tar.xz poezio-$(version) && \
tar czf poezio-$(version).tar.gz poezio-$(version)
.PHONY : doc
.PHONY : doc test
......@@ -379,6 +379,14 @@ ack_message_receipts = true
# Ask for message delivery receipts (XEP-0184)
request_message_receipts = true
# Extract base64 images received in XHTML-IM messages
# if true.
extract_inline_images = true
# The directory where the images will be saved; if unset,
# defaults to $XDG_CACHE_HOME/poezio/images.
tmp_image_dir =
# Receive the tune notifications or not (in order to display informations
# in the roster).
# If this is set to false, then the display_tune_notifications
......
......@@ -316,6 +316,14 @@ to understand what is :ref:`carbons <carbons-details>` or
If this is set to ``false``, you will no longer be subscribed to tune events,
and the :term:`display_tune_notifications` option will be ignored.
group_corrections
**Default value:** ``true``
Enable a message to “correct” (replace) another message in the display if the
sender intended it as such. See :ref:`Message Correction <correct-feature>` for
more information.
use_bookmark_method
**Default value:** ``[empty]``
......@@ -851,6 +859,25 @@ Other
The lang some automated entities will use when replying to you.
extract_inline_images
**Default value:** ``true``
Some clients send inline images in base64 inside some messages, which results in
an useless wall of text. If this option is ``true``, then that base64 text will
be replaced with a :file:`file://` link to the image file extracted in
:term:`tmp_image_dir` or :file:`$XDG_CACHE_HOME/poezio/images` by default, which
is usually :file:`~/.cache/poezio/images`
tmp_image_dir
**Default value:** ``[empty]``
The directory where poezio will save the images received, if
:term:`extract_inline_images` is set to true. If unset, poezio
will default to :file:`$XDG_CACHE_HOME/poezio/images` which is
usually :file:`~/.cache/poezio/images`.
muc_history_length
**Default value:** ``50``
......
......@@ -80,6 +80,8 @@ Poezio depends on two libraries:
- DNSPython_ (the python3 version, often called dnspython3)
- SleekXMPP_
Additionally, it needs *python3-setuptools* to install an executable file.
If you do not want to install those libraries, you can skip directly to
the :ref:`installation part <poezio-install-label>`
......@@ -139,7 +141,8 @@ If you have git installed, it will download and update locally the
libraries for you. (and if you don’t have git installed, install it)
If you really want to install it, run as root (or sudo in ubuntu or whatever):
If you really want to install it, first install the *python3-setuptools* package
in your distribution, then run as root (or sudo in ubuntu or whatever):
.. code-block:: bash
......
.. _correct-feature:
Message Correction
==================
......
......@@ -210,7 +210,7 @@ def hl(tab):
conv_jid = safeJID(tab.name)
if 'private' in config.get('beep_on', 'highlight private').split():
if not config.get_by_tabname('disable_beep', False, conv_jid.bare, False):
if not config.get_by_tabname('disable_beep', conv_jid.bare, default=False):
curses.beep()
class PoezioContext(Context):
......@@ -430,11 +430,11 @@ class Plugin(BasePlugin):
jid = safeJID(jid).full
if not jid in self.contexts:
flags = POLICY_FLAGS.copy()
policy = self.config.get_by_tabname('encryption_policy', 'ondemand', jid).lower()
logging_policy = self.config.get_by_tabname('log', 'false', jid).lower()
allow_v2 = self.config.get_by_tabname('allow_v2', 'true', jid).lower()
policy = self.config.get_by_tabname('encryption_policy', jid, default='ondemand').lower()
logging_policy = self.config.get_by_tabname('log', jid, default='false').lower()
allow_v2 = self.config.get_by_tabname('allow_v2', jid, default='true').lower()
flags['ALLOW_V2'] = (allow_v2 != 'false')
allow_v1 = self.config.get_by_tabname('allow_v1', 'false', jid).lower()
allow_v1 = self.config.get_by_tabname('allow_v1', jid, default='false').lower()
flags['ALLOW_V1'] = (allow_v1 == 'true')
self.contexts[jid] = PoezioContext(self.account, jid, self.core.xmpp, self.core)
self.contexts[jid].log = 1 if logging_policy != 'false' else 0
......@@ -544,7 +544,7 @@ class Plugin(BasePlugin):
nick_color = get_theme().COLOR_REMOTE_USER
body = txt.decode()
if self.config.get_by_tabname('decode_xhtml', True, msg['from'].bare):
if self.config.get_by_tabname('decode_xhtml', msg['from'].bare, default=True):
try:
body = xhtml.xhtml_to_poezio_colors(body, force=True)
except:
......
......@@ -7,15 +7,14 @@ import os
import stat
import pyinotify
SCREEN_DIR = '/var/run/screen/S-%s' % (os.getlogin(),)
class Plugin(BasePlugin):
def init(self):
screen_dir = '/var/run/screen/S-%s' % (os.getlogin(),)
self.timed_event = None
sock_path = None
self.thread = None
for f in os.listdir(SCREEN_DIR):
path = os.path.join(SCREEN_DIR, f)
for f in os.listdir(screen_dir):
path = os.path.join(screen_dir, f)
if screen_attached(path):
sock_path = path
self.attached = True
......
-e git://github.com/afflux/pure-python-otr.git#egg=potr
git+git://github.com/afflux/pure-python-otr.git#egg=potr
sleekxmpp==1.2
dnspython3==1.11.1
sphinx==1.2.1
setuptools
argparse
pyinotify
python-mpd2
#!/usr/bin/python3
from poezio import main
main()
#!/usr/bin/env python3
try:
from setuptools import setup, Extension
except ImportError:
print('Setuptools was not found.\n'
'This script will use distutils instead, which will NOT'
' be able to install a `poezio` executable.\nIf you are '
'using it to build a package or install poezio, please '
'install setuptools.\n\nYou will also see a few warnings.\n')
from distutils.core import setup, Extension
import os
......@@ -53,11 +59,12 @@ setup(name="poezio",
'poezio_plugins', 'poezio_plugins.gpg', 'poezio_themes'],
package_dir = {'poezio': 'src', 'poezio_plugins': 'plugins', 'poezio_themes': 'data/themes'},
package_data = {'poezio': ['default_config.cfg']},
scripts = ['scripts/poezio', 'scripts/poezio_gpg_export'],
scripts = ['scripts/poezio_gpg_export'],
entry_points={ 'console_scripts': [ 'poezio = poezio:main' ] },
data_files = [('share/man/man1/', ['data/poezio.1'])],
install_requires = ['sleekxmpp==1.2.4',
'dnspython3>=1.11.1'],
install_requires = ['sleekxmpp>=1.2.4',
'dnspython3>=1.10.0'],
extras_require = {'OTR plugin': 'python-potr>=1.0',
'Screen autoaway plugin': 'pyinotify==0.9.4'}
)
......
......@@ -25,7 +25,7 @@ def xml_iter(xml, tag=''):
else:
return xml.getiterator(tag)
preferred = config.get('use_bookmarks_method', 'pep').lower()
preferred = config.get('use_bookmarks_method').lower()
if preferred not in ('pep', 'privatexml'):
preferred = 'privatexml'
not_preferred = 'privatexml' if preferred == 'pep' else 'pep'
......@@ -159,8 +159,8 @@ def save(xmpp, core=None):
core.information('Could not save bookmarks.', 'Error')
elif core:
core.information('Bookmarks saved', 'Info')
if config.get('use_remote_bookmarks', True):
preferred = config.get('use_bookmarks_method', 'privatexml')
if config.get('use_remote_bookmarks'):
preferred = config.get('use_bookmarks_method')
cb = functools.partial(_cb, core)
save_remote(xmpp, cb, method=preferred)
......@@ -201,7 +201,7 @@ def get_remote(xmpp, callback):
"""Add the remotely stored bookmarks to the list."""
if xmpp.anon:
return
method = config.get('use_bookmarks_method', '')
method = config.get('use_bookmarks_method')
if not method:
available_methods = {}
def _save_and_call_callback():
......@@ -232,7 +232,7 @@ def save_bookmarks_method(available_methods):
def get_local():
"""Add the locally stored bookmarks to the list."""
rooms = config.get('rooms', '')
rooms = config.get('rooms')
if not rooms:
return
rooms = rooms.split(':')
......@@ -244,7 +244,7 @@ def get_local():
nick = jid.resource
else:
nick = None
passwd = config.get_by_tabname('password', '', jid.bare, fallback=False) or None
passwd = config.get_by_tabname('password', jid.bare, fallback=False) or None
b = Bookmark(jid.bare, autojoin=True, nick=nick, password=passwd, method='local')
if not get_by_jid(b.jid):
bookmarks.append(b)
......@@ -188,17 +188,6 @@ def datetime_tuple(timestamp):
:param str timestamp: The string containing the formatted date.
:return: The date.
:rtype: :py:class:`datetime.datetime`
>>> time.timezone = 0; time.altzone = 0
>>> datetime_tuple('20130226T06:23:12')
datetime.datetime(2013, 2, 26, 6, 23, 12)
>>> datetime_tuple('2013-02-26T06:23:12+02:00')
datetime.datetime(2013, 2, 26, 4, 23, 12)
>>> time.timezone = -3600; time.altzone = -3600
>>> datetime_tuple('20130226T07:23:12')
datetime.datetime(2013, 2, 26, 8, 23, 12)
>>> datetime_tuple('2013-02-26T07:23:12+02:00')
datetime.datetime(2013, 2, 26, 6, 23, 12)
"""
timestamp = timestamp.replace('-', '', 2).replace(':', '')
date = timestamp[:15]
......@@ -227,15 +216,10 @@ def datetime_tuple(timestamp):
def get_utc_time(local_time=None):
"""
Get the current time in UTC
Get the current UTC time
:param datetime local_time: The current local time
:return: The current UTC time
>>> delta = timedelta(seconds=-3600)
>>> d = datetime.now()
>>> time.timezone = -3600; time.altzone = -3600
>>> get_utc_time(local_time=d) == d + delta
True
"""
if local_time is None:
local_time = datetime.now()
......@@ -258,12 +242,6 @@ def get_utc_time(local_time=None):
def get_local_time(utc_time):
"""
Get the local time from an UTC time
>>> delta = timedelta(seconds=-3600)
>>> d = datetime.now()
>>> time.timezone = -3600; time.altzone = -3600
>>> get_local_time(d) == d - delta
True
"""
if OLD_PYTHON:
isdst = time.localtime(int(utc_time.strftime("%s"))).tm_isdst
......@@ -315,16 +293,6 @@ def shell_split(st):
>>> shell_split('"sdf 1" "toto 2"')
['sdf 1', 'toto 2']
>>> shell_split('toto "titi"')
['toto', 'titi']
>>> shell_split('toto ""')
['toto', '']
>>> shell_split('to"to titi "a" b')
['to"to', 'titi', 'a', 'b']
>>> shell_split('"toto titi" toto ""')
['toto titi', 'toto', '']
>>> shell_split('toto "titi')
['toto', 'titi']
"""
sh = shlex.shlex(st)
ret = []
......@@ -358,18 +326,8 @@ def find_argument(pos, text, quoted=True):
def find_argument_quoted(pos, text):
"""
>>> find_argument_quoted(4, 'toto titi tata')
3
>>> find_argument_quoted(4, '"toto titi" tata')
0
>>> find_argument_quoted(8, '"toto" "titi tata"')
1
>>> find_argument_quoted(8, '"toto" "titi tata')
1
>>> find_argument_quoted(3, '"toto" "titi tata')
0
>>> find_argument_quoted(18, '"toto" "titi tata" ')
2
Get the number of the argument at position pos in
a string with possibly quoted text.
"""
sh = shlex.shlex(text)
count = -1
......@@ -384,16 +342,8 @@ def find_argument_quoted(pos, text):
def find_argument_unquoted(pos, text):
"""
>>> find_argument_unquoted(2, 'toto titi tata')
0
>>> find_argument_unquoted(3, 'toto titi tata')
0
>>> find_argument_unquoted(6, 'toto titi tata')
1
>>> find_argument_unquoted(4, 'toto titi tata')
3
>>> find_argument_unquoted(25, 'toto titi tata')
3
Get the number of the argument at position pos in
a string without interpreting quotes.
"""
ret = text.split()
search = 0
......@@ -531,7 +481,3 @@ def safeJID(*args, **kwargs):
except InvalidJID:
return JID('')
if __name__ == "__main__":
import doctest
doctest.testmod()
......@@ -22,16 +22,129 @@ from os import environ, makedirs, path, remove
from shutil import copy2
from args import parse_args
DEFAULT_CONFIG = {
'Poezio': {
'ack_message_receipts': True,
'add_space_after_completion': True,
'after_completion': ',',
'alternative_nickname': '',
'auto_reconnect': False,
'autorejoin_delay': '5',
'autorejoin': False,
'beep_on': 'highlight private invite',
'ca_cert_path': '',
'certificate': '',
'ciphers': 'HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK:!SRP:!3DES:!aNULL',
'connection_check_interval': 60,
'connection_timeout_delay': 10,
'create_gaps': False,
'custom_host': '',
'custom_port': '',
'default_nick': '',
'display_activity_notifications': False,
'display_gaming_notifications': False,
'display_mood_notifications': False,
'display_tune_notifications': False,
'display_user_color_in_join_part': True,
'enable_carbons': False,
'enable_user_activity': True,
'enable_user_gaming': True,
'enable_user_mood': True,
'enable_user_nick': True,
'enable_user_tune': True,
'enable_vertical_tab_list': False,
'enable_xhtml_im': True,
'exec_remote': False,
'extract_inline_images': True,
'filter_info_messages': '',
'force_encryption': True,
'group_corrections': True,
'hide_exit_join': -1,
'hide_status_change': 120,
'hide_user_list': False,
'highlight_on': '',
'ignore_certificate': False,
'ignore_private': False,
'information_buffer_popup_on': 'error roster warning help info',
'jid': '',
'lang': 'en',
'lazy_resize': True,
'load_log': 10,
'log_dir': '',
'logfile': 'logs',
'log_errors': True,
'max_lines_in_memory': 2048,
'max_messages_in_memory': 2048,
'max_nick_length': 25,
'muc_history_length': 50,
'notify_messages': True,
'open_all_bookmarks': False,
'password': '',
'plugins_autoload': '',
'plugins_conf_dir': '',
'plugins_dir': '',
'popup_time': 4,
'private_auto_response': '',
'remote_fifo_path': './',
'request_message_receipts': True,
'resource': '',
'rooms': '',
'roster_group_sort': 'name',
'roster_show_offline': False,
'roster_sort': 'jid:show',
'save_status': True,
'send_chat_states': True,
'send_initial_presence': True,
'send_os_info': True,
'send_poezio_info': True,
'send_time': True,
'separate_history': False,
'server': 'anon.jeproteste.info',
'show_composing_tabs': 'direct',
'show_inactive_tabs': True,
'show_muc_jid': True,
'show_roster_jids': True,
'show_roster_subscriptions': '',
'show_s2s_errors': True,
'show_tab_names': False,
'show_tab_numbers': True,
'show_timestamps': True,
'show_useless_separator': False,
'status': '',
'status_message': '',
'theme': 'default',
'themes_dir': '',
'tmp_image_dir': '',
'use_bookmarks_method': '',
'use_log': False,
'use_remote_bookmarks': True,
'user_list_sort': 'desc',
'use_tab_nicks': True,
'vertical_tab_list_size': 20,
'vertical_tab_list_sort': 'desc',
'whitespace_interval': 300,
'words': ''
},
'bindings': {
'M-i': '^I'
},
'var': {
'folded_roster_groups': '',
'info_win_height': 2
}
}
class Config(RawConfigParser):
"""
load/save the config to a file
"""
def __init__(self, file_name):
def __init__(self, file_name, default=None):
RawConfigParser.__init__(self, None)
# make the options case sensitive
self.optionxform = str
self.file_name = file_name
self.read_file()
self.default = default
def read_file(self):
try:
......@@ -43,13 +156,19 @@ class Config(RawConfigParser):
if not self.has_section(section):
self.add_section(section)
def get(self, option, default, section=DEFSECTION):
def get(self, option, default=None, section=DEFSECTION):
"""
get a value from the config but return
a default value if it is not found
The type of default defines the type
returned
"""
if default is None:
if self.default:
default = self.default.get(section, {}).get(option)
else:
default = ''
try:
if type(default) == int:
res = self.getint(option, section)
......@@ -61,18 +180,21 @@ class Config(RawConfigParser):
res = self.getstr(option, section)
except (NoOptionError, NoSectionError, ValueError, AttributeError):
return default
if res is None:
return default
return res
def get_by_tabname(self, option, default, tabname, fallback=True,
fallback_server=True):
def get_by_tabname(self, option, tabname,
fallback=True, fallback_server=True, default=''):
"""
Try to get the value for the option. First we look in
a section named `tabname`, if the option is not present
in the section, we search for the global option if fallback is
True. And we return `default` as a fallback as a last resort.
"""
if self.default and (not default) and fallback:
default = self.default.get(DEFSECTION, {}).get(option, '')
if tabname in self.sections():
if option in self.options(tabname):
# We go the tab-specific option
......@@ -360,7 +482,6 @@ def file_ok(filepath):
def check_create_config_dir():
"""
create the configuration directory if it doesn't exist
and copy the default config in it
"""
CONFIG_HOME = environ.get("XDG_CONFIG_HOME")
if not CONFIG_HOME:
......@@ -373,6 +494,23 @@ def check_create_config_dir():
pass
return CONFIG_PATH
def check_create_cache_dir():
"""
create the cache directory if it doesn't exist
also create the subdirectories
"""
global CACHE_DIR
CACHE_HOME = environ.get("XDG_CACHE_HOME")
if not CACHE_HOME:
CACHE_HOME = path.join(environ.get('HOME'), '.cache')
CACHE_DIR = path.join(CACHE_HOME, 'poezio')
try:
makedirs(CACHE_DIR)
makedirs(path.join(CACHE_DIR, 'images'))
except OSError:
pass
def run_cmdline_args(CONFIG_PATH):
"Parse the command line arguments"
global options
......@@ -394,7 +532,7 @@ def create_global_config():
"Create the global config object, or crash"
try:
global config
config = Config(options.filename)
config = Config(options.filename, DEFAULT_CONFIG)
except:
import traceback
sys.stderr.write('Poezio was unable to read or'
......@@ -405,7 +543,7 @@ def create_global_config():
def check_create_log_dir():
"Create the poezio logging directory if it doesn’t exist"
global LOG_DIR
LOG_DIR = config.get('log_dir', '')
LOG_DIR = config.get('log_dir')
if not LOG_DIR:
......@@ -425,7 +563,7 @@ def check_create_log_dir():
def setup_logging():
"Change the logging config according to the cmdline options and config"
if config.get('log_errors', True):
if config.get('log_errors'):
LOGGING_CONFIG['root']['handlers'].append('error')
LOGGING_CONFIG['handlers']['error'] = {
'level': 'ERROR',
......@@ -494,3 +632,6 @@ safeJID = None
# the global log dir
LOG_DIR = ''
# the global cache dir
CACHE_DIR = ''
......@@ -29,28 +29,28 @@ class Connection(slixmpp.ClientXMPP):
"""
__init = False
def __init__(self):
resource = config.get('resource', '')
if config.get('jid', ''):
resource = config.get('resource')
if config.get('jid'):
# Field used to know if we are anonymous or not.
# many features will be handled differently
# depending on this setting
self.anon = False
jid = '%s' % config.get('jid', '')
jid = '%s' % config.get('jid')
if resource:
jid = '%s/%s'% (jid, resource)
password = config.get('password', '') or getpass.getpass()
password = config.get('password') or getpass.getpass()
else: # anonymous auth
self.anon = True
jid = config.get('server', 'anon.jeproteste.info')
jid = config.get('server')
if resource:
jid = '%s/%s' % (jid, resource)
password = None
jid = safeJID(jid)
# TODO: use the system language
slixmpp.ClientXMPP.__init__(self, jid, password,
lang=config.get('lang', 'en'))
lang=config.get('lang'))
force_encryption = config.get('force_encryption', True)
force_encryption = config.get('force_encryption')
if force_encryption:
self['feature_mechanisms'].unencrypted_plain = False
self['feature_mechanisms'].unencrypted_digest = False
......@@ -58,6 +58,7 @@ class Connection(slixmpp.ClientXMPP):
self['feature_mechanisms'<