From 0961632f01dfb9e973b6f337d243887800c63ff3 Mon Sep 17 00:00:00 2001 From: Ben Harris Date: Fri, 5 Oct 2018 14:39:01 -0400 Subject: [PATCH] upgrade scripts --- weechat/.weechat/plugins.conf | 3 + .../.weechat/python/autoload/topicdiff_alt.py | 1 + weechat/.weechat/python/grep.py | 231 +++--- weechat/.weechat/python/grep.pyc | Bin 45510 -> 0 bytes weechat/.weechat/python/otr.py | 765 ++++++++++-------- weechat/.weechat/python/topicdiff_alt.py | 70 ++ weechat/.weechat/weechat.conf | 1 + 7 files changed, 617 insertions(+), 454 deletions(-) create mode 120000 weechat/.weechat/python/autoload/topicdiff_alt.py delete mode 100644 weechat/.weechat/python/grep.pyc create mode 100644 weechat/.weechat/python/topicdiff_alt.py diff --git a/weechat/.weechat/plugins.conf b/weechat/.weechat/plugins.conf index d15da46..ff25dd8 100644 --- a/weechat/.weechat/plugins.conf +++ b/weechat/.weechat/plugins.conf @@ -69,6 +69,7 @@ python.grep.log_filter = "" python.grep.max_lines = "4000" python.grep.show_summary = "on" python.grep.size_limit = "2048" +python.grep.timeout_secs = "300" python.screen_away.away_suffix = "" python.screen_away.command_on_attach = "" python.screen_away.command_on_detach = "" @@ -78,6 +79,8 @@ python.screen_away.interval = "5" python.screen_away.message = "Detached from screen" python.screen_away.set_away = "on" python.screen_away.time_format = "since %Y-%m-%d %H:%M:%S%z" +python.topicdiff_alt.color_del = "darkgray" +python.topicdiff_alt.color_ins = "lightcyan" ruby.check_license = "off" tcl.check_license = "off" diff --git a/weechat/.weechat/python/autoload/topicdiff_alt.py b/weechat/.weechat/python/autoload/topicdiff_alt.py new file mode 120000 index 0000000..0dd2926 --- /dev/null +++ b/weechat/.weechat/python/autoload/topicdiff_alt.py @@ -0,0 +1 @@ +../topicdiff_alt.py \ No newline at end of file diff --git a/weechat/.weechat/python/grep.py b/weechat/.weechat/python/grep.py index 4fd5c64..1941531 100644 --- a/weechat/.weechat/python/grep.py +++ b/weechat/.weechat/python/grep.py @@ -53,6 +53,9 @@ # It can be used for force or disable background process, using '0' forces to always grep in # background, while using '' (empty string) will disable it. # +# * plugins.var.python.grep.timeout_secs: +# Timeout (in seconds) for background grepping. +# # * plugins.var.python.grep.default_tail_head: # Config option for define default number of lines returned when using --head or --tail options. # Can be overriden in the command with --number option. @@ -66,6 +69,24 @@ # # History: # +# 2018-04-10, Sébastien Helleu +# version 0.8.1: fix infolist_time for WeeChat >= 2.2 (WeeChat returns a long +# integer instead of a string) +# +# 2017-09-20, mickael9 +# version 0.8: +# * use weechat 1.5+ api for background processing (old method was unsafe and buggy) +# * add timeout_secs setting (was previously hardcoded to 5 mins) +# +# 2017-07-23, Sébastien Helleu +# version 0.7.8: fix modulo by zero when nick is empty string +# +# 2016-06-23, mickael9 +# version 0.7.7: fix get_home function +# +# 2015-11-26 +# version 0.7.6: fix a typo +# # 2015-01-31, Nicd- # version 0.7.5: # '~' is now expaned to the home directory in the log file path so @@ -189,7 +210,12 @@ ### from os import path -import sys, getopt, time, os, re, tempfile +import sys, getopt, time, os, re + +try: + import cPickle as pickle +except ImportError: + import pickle try: import weechat @@ -200,20 +226,21 @@ except ImportError: SCRIPT_NAME = "grep" SCRIPT_AUTHOR = "Elián Hanisch " -SCRIPT_VERSION = "0.7.5" +SCRIPT_VERSION = "0.8.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Search in buffers and logs" SCRIPT_COMMAND = "grep" ### Default Settings ### settings = { -'clear_buffer' : 'off', -'log_filter' : '', -'go_to_buffer' : 'on', -'max_lines' : '4000', -'show_summary' : 'on', -'size_limit' : '2048', -'default_tail_head' : '10', + 'clear_buffer' : 'off', + 'log_filter' : '', + 'go_to_buffer' : 'on', + 'max_lines' : '4000', + 'show_summary' : 'on', + 'size_limit' : '2048', + 'default_tail_head' : '10', + 'timeout_secs' : '300', } ### Class definitions ### @@ -372,13 +399,15 @@ def color_nick(nick): else: mode = mode_color = '' # nick color - nick_color = weechat.info_get('irc_nick_color', nick) - if not nick_color: - # probably we're in WeeChat 0.3.0 - #debug('no irc_nick_color') - color_nicks_number = config_int('weechat.look.color_nicks_number') - idx = (sum(map(ord, nick))%color_nicks_number) + 1 - nick_color = wcolor(config_string('weechat.color.chat_nick_color%02d' %idx)) + nick_color = '' + if nick: + nick_color = weechat.info_get('irc_nick_color', nick) + if not nick_color: + # probably we're in WeeChat 0.3.0 + #debug('no irc_nick_color') + color_nicks_number = config_int('weechat.look.color_nicks_number') + idx = (sum(map(ord, nick))%color_nicks_number) + 1 + nick_color = wcolor(config_string('weechat.color.chat_nick_color%02d' %idx)) return ''.join((prefix_c, prefix, mode_color, mode, nick_color, nick, suffix_c, suffix)) ### Config and value validation ### @@ -414,8 +443,9 @@ def get_config_log_filter(): def get_home(): home = weechat.config_string(weechat.config_get('logger.file.path')) + home = home.replace('%h', weechat.info_get('weechat_dir', '')) home = path.abspath(path.expanduser(home)) - return home.replace('%h', weechat.info_get('weechat_dir', '')) + return home def strip_home(s, dir=''): """Strips home dir from the begging of the log path, this makes them sorter.""" @@ -818,6 +848,10 @@ def grep_buffer(buffer, head, tail, after_context, before_context, count, regexp prefix = string_remove_color(infolist_string(infolist, 'prefix'), '') message = string_remove_color(infolist_string(infolist, 'message'), '') date = infolist_time(infolist, 'date') + # since WeeChat 2.2, infolist_time returns a long integer + # instead of a string + if not isinstance(date, str): + date = time.strftime('%F %T', time.localtime(int(date))) return '%s\t%s\t%s' %(date, prefix, message) return function get_line = make_get_line_funcion() @@ -931,104 +965,85 @@ def show_matching_lines(): elif size_limit == '': background = False + regexp = make_regexp(pattern, matchcase) + + global grep_options, log_pairs + grep_options = (head, tail, after_context, before_context, + count, regexp, hilight, exact, invert) + + log_pairs = [(strip_home(log), log) for log in search_in_files] + if not background: # run grep normally - regexp = make_regexp(pattern, matchcase) - for log in search_in_files: - log_name = strip_home(log) - matched_lines[log_name] = grep_file(log, head, tail, after_context, before_context, - count, regexp, hilight, exact, invert) + for log_name, log in log_pairs: + matched_lines[log_name] = grep_file(log, *grep_options) buffer_update() else: - # we hook a process so grepping runs in background. - #debug('on background') - global hook_file_grep, script_path, bytecode - timeout = 1000*60*5 # 5 min - - quotify = lambda s: '"%s"' %s - files_string = ', '.join(map(quotify, search_in_files)) - - global tmpFile - # we keep the file descriptor as a global var so it isn't deleted until next grep - tmpFile = tempfile.NamedTemporaryFile(prefix=SCRIPT_NAME, - dir=weechat.info_get('weechat_dir', '')) - cmd = grep_process_cmd %dict(logs=files_string, head=head, pattern=pattern, tail=tail, - hilight=hilight, after_context=after_context, before_context=before_context, - exact=exact, matchcase=matchcase, home_dir=home_dir, script_path=script_path, - count=count, invert=invert, bytecode=bytecode, filename=tmpFile.name, - python=weechat.info_get('python2_bin', '') or 'python') - - #debug(cmd) - hook_file_grep = weechat.hook_process(cmd, timeout, 'grep_file_callback', tmpFile.name) - global pattern_tmpl + global hook_file_grep, grep_stdout, grep_stderr, pattern_tmpl + grep_stdout = grep_stderr = '' + hook_file_grep = weechat.hook_process( + 'func:grep_process', + get_config_int('timeout_secs') * 1000, + 'grep_process_cb', + '' + ) if hook_file_grep: - buffer_create("Searching for '%s' in %s worth of data..." %(pattern_tmpl, - human_readable_size(size))) + buffer_create("Searching for '%s' in %s worth of data..." % ( + pattern_tmpl, + human_readable_size(size) + )) else: buffer_update() -# defined here for commodity -grep_process_cmd = """%(python)s -%(bytecode)sc ' -import sys, cPickle, os -sys.path.append("%(script_path)s") # add WeeChat script dir so we can import grep -from grep import make_regexp, grep_file, strip_home -logs = (%(logs)s, ) -try: - regexp = make_regexp("%(pattern)s", %(matchcase)s) - d = {} - for log in logs: - log_name = strip_home(log, "%(home_dir)s") - lines = grep_file(log, %(head)s, %(tail)s, %(after_context)s, %(before_context)s, - %(count)s, regexp, "%(hilight)s", %(exact)s, %(invert)s) - d[log_name] = lines - fd = open("%(filename)s", "wb") - cPickle.dump(d, fd, -1) - fd.close() -except Exception, e: - print >> sys.stderr, e' -""" + +def grep_process(*args): + result = {} + try: + global grep_options, log_pairs + for log_name, log in log_pairs: + result[log_name] = grep_file(log, *grep_options) + except Exception, e: + result = e + + return pickle.dumps(result) grep_stdout = grep_stderr = '' -def grep_file_callback(filename, command, rc, stdout, stderr): - global hook_file_grep, grep_stderr, grep_stdout - global matched_lines - #debug("rc: %s\nstderr: %s\nstdout: %s" %(rc, repr(stderr), repr(stdout))) - if stdout: - grep_stdout += stdout - if stderr: - grep_stderr += stderr - if int(rc) >= 0: - - def set_buffer_error(): - grep_buffer = buffer_create() - title = weechat.buffer_get_string(grep_buffer, 'title') - title = title + ' %serror' %color_title - weechat.buffer_set(grep_buffer, 'title', title) + +def grep_process_cb(data, command, return_code, out, err): + global grep_stdout, grep_stderr, matched_lines, hook_file_grep + + grep_stdout += out + grep_stderr += err + + def set_buffer_error(message): + error(message) + grep_buffer = buffer_create() + title = weechat.buffer_get_string(grep_buffer, 'title') + title = title + ' %serror' % color_title + weechat.buffer_set(grep_buffer, 'title', title) + + if return_code == weechat.WEECHAT_HOOK_PROCESS_ERROR: + set_buffer_error("Background grep timed out") + hook_file_grep = None + return WEECHAT_RC_OK + + elif return_code >= 0: + hook_file_grep = None + if grep_stderr: + set_buffer_error(grep_stderr) + return WEECHAT_RC_OK try: - if grep_stderr: - error(grep_stderr) - set_buffer_error() - #elif grep_stdout: - #debug(grep_stdout) - elif path.exists(filename): - import cPickle - try: - #debug(file) - fd = open(filename, 'rb') - d = cPickle.load(fd) - matched_lines.update(d) - fd.close() - except Exception, e: - error(e) - set_buffer_error() - else: - buffer_update() - global tmpFile - tmpFile = None - finally: - grep_stdout = grep_stderr = '' - hook_file_grep = None + data = pickle.loads(grep_stdout) + if isinstance(data, Exception): + raise data + matched_lines.update(data) + except Exception, e: + set_buffer_error(repr(e)) + return WEECHAT_RC_OK + else: + buffer_update() + return WEECHAT_RC_OK def get_grep_file_status(): @@ -1403,18 +1418,18 @@ def cmd_grep_parsing(args): tail = n def cmd_grep_stop(buffer, args): - global hook_file_grep, pattern, matched_lines, tmpFile + global hook_file_grep, pattern, matched_lines if hook_file_grep: if args == 'stop': weechat.unhook(hook_file_grep) hook_file_grep = None - s = 'Search for \'%s\' stopped.' %pattern + + s = 'Search for \'%s\' stopped.' % pattern say(s, buffer) grep_buffer = weechat.buffer_search('python', SCRIPT_NAME) if grep_buffer: weechat.buffer_set(grep_buffer, 'title', s) - del matched_lines - tmpFile = None + matched_lines = {} else: say(get_grep_file_status(), buffer) raise Exception @@ -1639,7 +1654,7 @@ if __name__ == '__main__' and import_ok and \ If used with 'log ' search in all logs that matches . -b --buffer: Search only in buffers, not in file logs. -c --count: Just count the number of matched lines instead of showing them. - -m --matchcase: Don't do case insensible search. + -m --matchcase: Don't do case insensitive search. -H --hilight: Colour exact matches in output buffer. -o --only-match: Print only the matching part of the line (unique matches). -v -i --invert: Print lines that don't match the regular expression. diff --git a/weechat/.weechat/python/grep.pyc b/weechat/.weechat/python/grep.pyc deleted file mode 100644 index 9f32eff15e408259a651050e17d6ef09d823d641..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45510 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 ZdMg*3%Xhzso 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 XuKJ&QeC+wnz0|^!ehW1=uls)h4gU0- diff --git a/weechat/.weechat/python/otr.py b/weechat/.weechat/python/otr.py index 404e96a..0ccfb35 100644 --- a/weechat/.weechat/python/otr.py +++ b/weechat/.weechat/python/otr.py @@ -1,29 +1,32 @@ # -*- coding: utf-8 -*- -# otr - WeeChat script for Off-the-Record IRC messaging -# -# DISCLAIMER: To the best of my knowledge this script securely provides OTR -# messaging in WeeChat, but I offer no guarantee. Please report any security -# holes you find. -# -# Copyright (c) 2012-2015 Matthew M. Boedicker -# Nils Görs -# Daniel "koolfy" Faucon -# Felix Eckhofer -# -# Report issues at https://github.com/mmb/weechat-otr -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +"""otr - WeeChat script for Off-the-Record IRC messaging -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +DISCLAIMER: To the best of my knowledge this script securely provides OTR +messaging in WeeChat, but I offer no guarantee. Please report any security +holes you find. -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +Copyright (c) 2012-2015 Matthew M. Boedicker + Nils Görs + Daniel "koolfy" Faucon + Felix Eckhofer + +Report issues at https://github.com/mmb/weechat-otr + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +# pylint: disable=too-many-lines from __future__ import unicode_literals @@ -38,6 +41,9 @@ import shlex import shutil import sys +import potr +import weechat + class PythonVersion2(object): """Python 2 version of code that must differ between Python 2 and 3.""" @@ -68,8 +74,7 @@ class PythonVersion2(object): """Convert a utf-8 encoded string to a Unicode.""" if isinstance(strng, unicode): return strng - else: - return strng.decode('utf-8', 'replace') + return strng.decode('utf-8', 'replace') def to_str(self, strng): """Convert a Unicode to a utf-8 encoded string.""" @@ -87,7 +92,7 @@ class PythonVersion3(object): import html.parser self.html_parser = html.parser if self.minor >= 4: - self.html_parser_init_kwargs = { 'convert_charrefs' : True } + self.html_parser_init_kwargs = {'convert_charrefs' : True} else: self.html_parser_init_kwargs = {} @@ -110,22 +115,18 @@ class PythonVersion3(object): """Convert a utf-8 encoded string to unicode.""" if isinstance(strng, bytes): return strng.decode('utf-8', 'replace') - else: - return strng + return strng def to_str(self, strng): """Convert a Unicode to a utf-8 encoded string.""" return strng -if sys.version_info.major >= 3: +# We cannot use version_info.major as this is only supported on python >= 2.7 +if sys.version_info[0] >= 3: PYVER = PythonVersion3(sys.version_info.minor) else: PYVER = PythonVersion2() -import weechat - -import potr - SCRIPT_NAME = 'otr' SCRIPT_DESC = 'Off-the-Record messaging for IRC' SCRIPT_HELP = """{description} @@ -163,7 +164,7 @@ This script supports only OTR protocol version 2. SCRIPT_AUTHOR = 'Matthew M. Boedicker' SCRIPT_LICENCE = 'GPL3' -SCRIPT_VERSION = '1.8.0' +SCRIPT_VERSION = '1.9.2' OTR_DIR_NAME = 'otr' @@ -171,9 +172,9 @@ OTR_QUERY_RE = re.compile(r'\?OTR(\?|\??v[a-z\d]*\?)') POLICIES = { 'allow_v2' : 'allow OTR protocol version 2, effectively enable OTR ' - 'since v2 is the only supported version', + 'since v2 is the only supported version', 'require_encryption' : 'refuse to send unencrypted messages when OTR is ' - 'enabled', + 'enabled', 'log' : 'enable logging of OTR conversations', 'send_tag' : 'advertise your OTR capability using the whitespace tag', 'html_escape' : 'escape HTML special characters in outbound messages', @@ -190,9 +191,6 @@ PLAIN_ACTION_RE = re.compile('^'+ACTION_PREFIX+'(?P.*)$') IRC_SANITIZE_TABLE = dict((ord(char), None) for char in '\n\r\x00') -global otr_debug_buffer -otr_debug_buffer = None - # Patch potr.proto.TaggedPlaintext to not end plaintext tags in a space. # # When POTR adds OTR tags to plaintext it puts them at the end of the message. @@ -204,6 +202,7 @@ otr_debug_buffer = None # The patched version also skips OTR tagging for CTCP messages because it # breaks the CTCP format. def patched__bytes__(self): + """Patched potr.proto.TaggedPlainText.__bytes__.""" # Do not tag CTCP messages. if self.msg.startswith(b'\x01') and \ self.msg.endswith(b'\x01'): @@ -270,7 +269,7 @@ def print_buffer(buf, message, level='info'): using color according to level.""" prnt(buf, '{prefix}\t{msg}'.format( prefix=get_prefix(), - msg=colorize(message, 'buffer.{}'.format(level)))) + msg=colorize(message, 'buffer.{0}'.format(level)))) def get_prefix(): """Returns configured message prefix.""" @@ -280,25 +279,19 @@ def get_prefix(): def debug(msg): """Send a debug message to the OTR debug buffer.""" - debug_option = weechat.config_get(config_prefix('general.debug')) - global otr_debug_buffer + debug_option = config_get_prefixed('general.debug') - if weechat.config_boolean(debug_option): - if not otr_debug_buffer: - otr_debug_buffer = weechat.buffer_new("OTR Debug", "", "", - "debug_buffer_close_cb", "") - weechat.buffer_set(otr_debug_buffer, 'title', 'OTR Debug') - weechat.buffer_set(otr_debug_buffer, 'localvar_set_no_log', '1') - prnt(otr_debug_buffer, ('{script} debug\t{text}'.format( - script=SCRIPT_NAME, - text=PYVER.unicode(msg) - ))) + if not weechat.config_boolean(debug_option): + return -def debug_buffer_close_cb(data, buf): - """Set the OTR debug buffer to None.""" - global otr_debug_buffer - otr_debug_buffer = None - return weechat.WEECHAT_RC_OK + debug_buffer = weechat.buffer_search('python', 'OTR Debug') + if not debug_buffer: + debug_buffer = weechat.buffer_new('OTR Debug', '', '', '', '') + weechat.buffer_set(debug_buffer, 'title', 'OTR Debug') + weechat.buffer_set(debug_buffer, 'localvar_set_no_log', '1') + + prnt(debug_buffer, ('{script} debug\t{text}'.format( + script=SCRIPT_NAME, text=PYVER.unicode(msg)))) def current_user(server_name): """Get the nick and server of the current user on a server.""" @@ -307,8 +300,8 @@ def current_user(server_name): def irc_user(nick, server): """Build an IRC user string from a nick and server.""" return '{nick}@{server}'.format( - nick=nick.lower(), - server=server) + nick=nick.lower(), + server=server) def isupport_value(server, feature): """Get the value of an IRC server feature.""" @@ -328,8 +321,8 @@ def is_a_channel(channel, server): return channel.startswith(prefixes) -# Exception class for PRIVMSG parsing exceptions. class PrivmsgParseException(Exception): + """Exception class for PRIVMSG parsing exceptions.""" pass def parse_irc_privmsg(message, server): @@ -396,18 +389,26 @@ def first_instance(objs, klass): def config_prefix(option): """Add the config prefix to an option and return the full option name.""" return '{script}.{option}'.format( - script=SCRIPT_NAME, - option=option) + script=SCRIPT_NAME, + option=option) def config_color(option): """Get the color of a color config option.""" - return weechat.color(weechat.config_color(weechat.config_get( - config_prefix('color.{}'.format(option))))) + return weechat.color(weechat.config_color(config_get_prefixed( + 'color.{0}'.format(option)))) def config_string(option): """Get the string value of a config option with utf-8 decode.""" return PYVER.to_unicode(weechat.config_string( - weechat.config_get(config_prefix(option)))) + config_get_prefixed(option))) + +def config_get(option): + """Get the value of a WeeChat config option.""" + return weechat.config_get(PYVER.to_str(option)) + +def config_get_prefixed(option): + """Get the value of a script prefixed WeeChat config option.""" + return config_get(config_prefix(option)) def buffer_get_string(buf, prop): """Wrap weechat.buffer_get_string() with utf-8 encode/decode.""" @@ -466,9 +467,9 @@ def format_default_policies(): for policy, desc in sorted(POLICIES.items()): buf.write(' {policy} ({desc}) : {value}\n'.format( - policy=policy, - desc=desc, - value=config_string('policy.default.{}'.format(policy)))) + policy=policy, + desc=desc, + value=config_string('policy.default.{0}'.format(policy)))) buf.write('Change default policies with: /otr policy default NAME on|off') @@ -504,8 +505,8 @@ def show_account_fingerprints(): table_formatter = TableFormatter() for account in accounts(): table_formatter.add_row([ - account.name, - str(account.getPrivkey())]) + account.name, + str(account.getPrivkey())]) print_buffer('', table_formatter.format()) def show_peer_fingerprints(grep=None): @@ -524,16 +525,16 @@ def show_peer_fingerprints(grep=None): for fingerprint, trust in sorted(peer_data.items()): if grep is None or grep in peer: table_formatter.add_row([ - peer, - account.name, - potr.human_hash(fingerprint), - trust_descs[trust], + peer, + account.name, + potr.human_hash(fingerprint), + trust_descs[trust], ]) print_buffer('', table_formatter.format()) def private_key_file_path(account_name): """Return the private key file path for an account.""" - return os.path.join(OTR_DIR, '{}.key3'.format(account_name)) + return os.path.join(OTR_DIR, '{0}.key3'.format(account_name)) def read_private_key(key_file_path): """Return the private key in a private key file.""" @@ -542,6 +543,14 @@ def read_private_key(key_file_path): with open(key_file_path, 'rb') as key_file: return potr.crypt.PK.parsePrivateKey(key_file.read())[0] +def get_context(account_name, context_name): + """Return a context from an account.""" + return ACCOUNTS[account_name].getContext(context_name) + +def get_server_context(server, peer_nick): + """Return the context for the current server user and peer.""" + return get_context(current_user(server), irc_user(peer_nick, server)) + class AccountDict(collections.defaultdict): """Dictionary that adds missing keys as IrcOtrAccount instances.""" @@ -600,8 +609,8 @@ class IrcContext(potr.context.Context): def policy_config_option(self, policy): """Get the option name of a policy option for this context.""" return config_prefix('.'.join([ - 'policy', self.peer_server, self.user.nick, self.peer_nick, - policy.lower()])) + 'policy', self.peer_server, self.user.nick, self.peer_nick, + policy.lower()])) def getPolicy(self, key): """Get the value of a policy option for this context.""" @@ -612,20 +621,18 @@ class IrcContext(potr.context.Context): elif key_lower == 'send_tag' and self.no_send_tag(): result = False else: - option = weechat.config_get( - PYVER.to_str(self.policy_config_option(key))) + option = config_get(self.policy_config_option(key)) if option == '': - option = weechat.config_get( - PYVER.to_str(self.user.policy_config_option(key))) + option = config_get(self.user.policy_config_option(key)) if option == '': - option = weechat.config_get(config_prefix('.'.join( - ['policy', self.peer_server, key_lower]))) + option = config_get_prefixed('.'.join( + ['policy', self.peer_server, key_lower])) if option == '': - option = weechat.config_get( - config_prefix('policy.default.{}'.format(key_lower))) + option = config_get_prefixed( + 'policy.default.{0}'.format(key_lower)) result = bool(weechat.config_boolean(option)) @@ -640,7 +647,7 @@ class IrcContext(potr.context.Context): else: msg = PYVER.to_unicode(msg) - debug(('inject', msg, 'len {}'.format(len(msg)), appdata)) + debug(('inject', msg, 'len {0}'.format(len(msg)), appdata)) privmsg(self.peer_server, self.peer_nick, msg) @@ -678,7 +685,7 @@ class IrcContext(potr.context.Context): if trust is None: fpr = str(self.getCurrentKey()) - self.print_buffer('New fingerprint: {}'.format(fpr), 'warning') + self.print_buffer('New fingerprint: {0}'.format(fpr), 'warning') self.setCurrentTrust('') if bool(trust): @@ -705,7 +712,7 @@ class IrcContext(potr.context.Context): """Return the max message size for this context.""" # remove 'PRIVMSG :' from max message size result = self.user.maxMessageSize - 10 - len(self.peer_nick) - debug('max message size {}'.format(result)) + debug('max message size {0}'.format(result)) return result @@ -725,14 +732,14 @@ class IrcContext(potr.context.Context): # add [nick] prefix if we have only a server buffer for the query if self.peer_nick and not buffer_is_private(buf): msg = '[{nick}] {msg}'.format( - nick=self.peer_nick, - msg=msg) + nick=self.peer_nick, + msg=msg) print_buffer(buf, msg, level) def hint(self, msg): """Print a message to the buffer but only when hints are enabled.""" - hints_option = weechat.config_get(config_prefix('general.hints')) + hints_option = config_get_prefixed('general.hints') if weechat.config_boolean(hints_option): self.print_buffer(msg, 'hint') @@ -774,7 +781,7 @@ Respond with: /otr smp respond """) self.print_buffer( """Peer has requested SMP verification: {msg} Respond with: /otr smp respond """.format( - msg=PYVER.to_unicode(smp1q.msg))) + msg=PYVER.to_unicode(smp1q.msg))) elif first_instance(tlvs, potr.proto.SMP2TLV): if not self.in_smp: debug('Received unexpected SMP2') @@ -791,23 +798,24 @@ Respond with: /otr smp respond """.format( if self.smpIsSuccess(): if self.smp_question: - self.smp_finish('SMP verification succeeded.', - 'success') + self.smp_finish( + 'SMP verification succeeded.', 'success') if not self.is_verified: self.print_buffer( - """You may want to authenticate your peer by asking your own question: -/otr smp ask <'question'> 'secret'""") - + 'You may want to authenticate your peer by ' + 'asking your own question:\n' + "/otr smp ask <'question'> 'secret'") else: - self.smp_finish('SMP verification succeeded.', - 'success') + self.smp_finish( + 'SMP verification succeeded.', 'success') else: self.smp_finish('SMP verification failed.', 'error') def verify_instructions(self): """Generate verification instructions for user.""" - return """You can verify that this contact is who they claim to be in one of the following ways: + return """You can verify that this contact is who they claim to be in +one of the following ways: 1) Verify each other's fingerprints using a secure channel: Your fingerprint : {your_fp} @@ -824,13 +832,11 @@ Note: You can safely omit specifying the peer and server when executing these commands from the appropriate conversation buffer """.format( - your_fp=self.user.getPrivkey(), - peer=self.peer, - peer_nick=self.peer_nick, - peer_server=self.peer_server, - peer_fp=potr.human_hash( - self.crypto.theirPubkey.cfingerprint()), - ) + your_fp=self.user.getPrivkey(), + peer=self.peer, + peer_nick=self.peer_nick, + peer_server=self.peer_server, + peer_fp=potr.human_hash(self.crypto.theirPubkey.cfingerprint())) def is_encrypted(self): """Return True if the conversation with this context's peer is @@ -851,9 +857,9 @@ Note: You can safely omit specifying the peer and server when for policy, desc in sorted(POLICIES.items()): buf.write(' {policy} ({desc}) : {value}\n'.format( - policy=policy, - desc=desc, - value='on' if self.getPolicy(policy) else 'off')) + policy=policy, + desc=desc, + value='on' if self.getPolicy(policy) else 'off')) buf.write('Change policies with: /otr policy NAME on|off') @@ -884,7 +890,7 @@ Note: You can safely omit specifying the peer and server when buf = self.buffer() - if not weechat.config_get(self.get_logger_option_name(buf)): + if not config_get(self.get_logger_option_name()): result = -1 else: result = 0 @@ -898,8 +904,9 @@ Note: You can safely omit specifying the peer and server when return result - def get_logger_option_name(self, buf): - """Returns the logger config option for the specified buffer.""" + def get_logger_option_name(self): + """Returns the logger config option for this context's buffer.""" + buf = self.buffer() name = buffer_get_string(buf, 'name') plugin = buffer_get_string(buf, 'plugin') @@ -917,7 +924,8 @@ Note: You can safely omit specifying the peer and server when if self.is_logged(): weechat.command(self.buffer(), '/mute logger disable') self.print_buffer( - 'Logs have been temporarily disabled for the session. They will be restored upon finishing the OTR session.') + 'Logs have been temporarily disabled for the session. ' + 'They will be restored upon finishing the OTR session.') return previous_log_level @@ -934,16 +942,16 @@ Note: You can safely omit specifying the peer and server when if (previous_log_level >= 0) and (previous_log_level < 10): self.print_buffer( - 'Restoring buffer logging value to: {}'.format( + 'Restoring buffer logging value to: {0}'.format( previous_log_level), 'warning') - weechat.command(buf, '/mute logger set {}'.format( + weechat.command(buf, '/mute logger set {0}'.format( previous_log_level)) if previous_log_level == -1: - logger_option_name = self.get_logger_option_name(buf) + logger_option_name = self.get_logger_option_name() self.print_buffer( 'Restoring buffer logging value to default', 'warning') - weechat.command(buf, '/mute unset {}'.format( + weechat.command(buf, '/mute unset {0}'.format( logger_option_name)) del self.previous_log_level @@ -990,11 +998,11 @@ Note: You can safely omit specifying the peer and server when debug(('no_send_tag', no_send_tag_regex, self.peer_nick)) if no_send_tag_regex: return re.match(no_send_tag_regex, self.peer_nick, re.IGNORECASE) - + def __repr__(self): - return PYVER.to_str(('<{} {:x} peer_nick={c.peer_nick} ' - 'peer_server={c.peer_server}>').format( - self.__class__.__name__, id(self), c=self)) + return PYVER.to_str(( + '<{0} {1:x} peer_nick={c.peer_nick} peer_server={c.peer_server}>' + ).format(self.__class__.__name__, id(self), c=self)) class IrcOtrAccount(potr.context.Account): """Account class for OTR over IRC.""" @@ -1015,7 +1023,7 @@ class IrcOtrAccount(potr.context.Account): self.defaultQuery = self.defaultQuery.replace("\n", ' ') self.key_file_path = private_key_file_path(name) - self.fpr_file_path = os.path.join(OTR_DIR, '{}.fpr'.format(name)) + self.fpr_file_path = os.path.join(OTR_DIR, '{0}.fpr'.format(name)) self.load_trusts() @@ -1067,8 +1075,7 @@ class IrcOtrAccount(potr.context.Account): debug(('trust write', uid, self.name, IrcOtrAccount.PROTOCOL, fpr, trust)) fpr_file.write(PYVER.to_str('\t'.join( - (uid, self.name, IrcOtrAccount.PROTOCOL, fpr, - trust)))) + (uid, self.name, IrcOtrAccount.PROTOCOL, fpr, trust)))) fpr_file.write('\n') def end_all_private(self): @@ -1080,7 +1087,7 @@ class IrcOtrAccount(potr.context.Account): def policy_config_option(self, policy): """Get the option name of a policy option for this account.""" return config_prefix('.'.join([ - 'policy', self.server, self.nick, policy.lower()])) + 'policy', self.server, self.nick, policy.lower()])) class IrcHTMLParser(PYVER.html_parser.HTMLParser): """A simple HTML parser that throws away anything but newlines and links""" @@ -1118,7 +1125,7 @@ class IrcHTMLParser(PYVER.html_parser.HTMLParser): if self.result[self.linkstart:] == self.linktarget: self.result += ']' else: - self.result += ']({})'.format(self.linktarget) + self.result += ']({0})'.format(self.linktarget) self.linktarget = '' def handle_data(self, data): @@ -1131,7 +1138,7 @@ class IrcHTMLParser(PYVER.html_parser.HTMLParser): self.result += PYVER.unichr( PYVER.html_entities.name2codepoint[name]) except KeyError: - self.result += '&{};'.format(name) + self.result += '&{0};'.format(name) def handle_charref(self, name): """Called for character references, such as '""" @@ -1141,7 +1148,7 @@ class IrcHTMLParser(PYVER.html_parser.HTMLParser): else: self.result += PYVER.unichr(int(name)) except ValueError: - self.result += '&#{};'.format(name) + self.result += '&#{0};'.format(name) class TableFormatter(object): """Format lists of string into aligned tables.""" @@ -1153,7 +1160,7 @@ class TableFormatter(object): def add_row(self, row): """Add a row to the table.""" self.rows.append(row) - row_widths = [ len(s) for s in row ] + row_widths = [len(s) for s in row] if self.max_widths is None: self.max_widths = row_widths else: @@ -1161,12 +1168,12 @@ class TableFormatter(object): def format(self): """Return the formatted table as a string.""" - return '\n'.join([ self.format_row(row) for row in self.rows ]) + return '\n'.join([self.format_row(row) for row in self.rows]) def format_row(self, row): """Format a single row as a string.""" return ' |'.join( - [ s.ljust(self.max_widths[i]) for i, s in enumerate(row) ]) + [s.ljust(self.max_widths[i]) for i, s in enumerate(row)]) def message_in_cb(data, modifier, modifier_data, string): """Incoming message callback""" @@ -1182,10 +1189,7 @@ def message_in_cb(data, modifier, modifier_data, string): server = PYVER.to_unicode(modifier_data) - from_user = irc_user(parsed['from_nick'], server) - local_user = current_user(server) - - context = ACCOUNTS[local_user].getContext(from_user) + context = get_server_context(server, parsed['from_nick']) context.in_assembler.add(parsed['text']) @@ -1206,8 +1210,8 @@ def message_in_cb(data, modifier, modifier_data, string): context.handle_tlvs(tlvs) except potr.context.ErrorReceived as err: - context.print_buffer('Received OTR error: {}'.format( - PYVER.to_unicode(err.args[0].error)), 'error') + context.print_buffer('Received OTR error: {0}'.format( + PYVER.to_unicode(err.args[0])), 'error') except potr.context.NotEncryptedError: context.print_buffer( 'Received encrypted data but no private session established.', @@ -1245,10 +1249,7 @@ def message_out_cb(data, modifier, modifier_data, string): server = PYVER.to_unicode(modifier_data) - to_user = irc_user(parsed['to_nick'], server) - local_user = current_user(server) - - context = ACCOUNTS[local_user].getContext(to_user) + context = get_server_context(server, parsed['to_nick']) is_query = OTR_QUERY_RE.search(parsed['text']) parsed_text_bytes = to_bytes(parsed['text']) @@ -1277,12 +1278,12 @@ def message_out_cb(data, modifier, modifier_data, string): not is_query and \ context.getPolicy('require_encryption'): context.print_buffer( - 'Your message will not be sent, because policy requires an ' - 'encrypted connection.', 'error') + 'Your message will not be sent, because policy requires an ' + 'encrypted connection.', 'error') context.hint( - 'Wait for the OTR connection or change the policy to allow ' - 'clear-text messages:\n' - '/policy set require_encryption off') + 'Wait for the OTR connection or change the policy to allow ' + 'clear-text messages:\n' + '/otr policy require_encryption off') try: ret = context.sendMessage( @@ -1299,12 +1300,14 @@ def message_out_cb(data, modifier, modifier_data, string): except potr.context.NotEncryptedError as err: if err.args[0] == potr.context.EXC_FINISHED: context.print_buffer( - """Your message was not sent. End your private conversation:\n/otr finish""", + 'Your message was not sent. End your private ' + 'conversation:\n/otr finish', 'error') else: raise weechat.bar_item_update(SCRIPT_NAME) + # pylint: disable=bare-except except: try: print_buffer('', traceback.format_exc(), 'error') @@ -1313,6 +1316,7 @@ def message_out_cb(data, modifier, modifier_data, string): context.print_buffer( 'Failed to send message. See core buffer for traceback.', 'error') + # pylint: disable=bare-except except: pass @@ -1337,18 +1341,13 @@ def command_cb(data, buf, args): """Parse and dispatch WeeChat OTR commands.""" result = weechat.WEECHAT_RC_ERROR - try: - arg_parts = [PYVER.to_unicode(arg) for arg in shlex.split(args)] - except: - debug("Command parsing error.") - return result + arg_parts = [PYVER.to_unicode(arg) for arg in shlex.split(args)] if len(arg_parts) in (1, 3) and arg_parts[0] in ('start', 'refresh'): nick, server = default_peer_args(arg_parts[1:3], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) # We need to wall disable_logging() here so that no OTR-related # buffer messages get logged at any point. disable_logging() will # be called again when effectively switching to encrypted, but @@ -1359,11 +1358,13 @@ def command_cb(data, buf, args): else: context.previous_log_level = context.get_log_level() - context.hint('Sending OTR query... Please await confirmation of the OTR session being started before sending a message.') + context.hint( + 'Sending OTR query... Please await confirmation of the OTR ' + 'session being started before sending a message.') if not context.getPolicy('send_tag'): context.hint( - 'To try OTR on all conversations with {peer}: /otr policy send_tag on'.format( - peer=context.peer)) + 'To try OTR on all conversations with {peer}: /otr ' + 'policy send_tag on'.format(peer=context.peer)) privmsg(server, nick, '?OTR?') @@ -1372,8 +1373,7 @@ def command_cb(data, buf, args): nick, server = default_peer_args(arg_parts[1:3], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) context.disconnect() result = weechat.WEECHAT_RC_OK @@ -1382,14 +1382,13 @@ def command_cb(data, buf, args): nick, server = default_peer_args(arg_parts[1:3], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) if context.is_encrypted(): context.print_buffer( 'This conversation is encrypted.', 'success') - context.print_buffer("Your fingerprint is: {}".format( + context.print_buffer("Your fingerprint is: {0}".format( context.user.getPrivkey())) - context.print_buffer("Your peer's fingerprint is: {}".format( + context.print_buffer("Your peer's fingerprint is: {0}".format( potr.human_hash(context.crypto.theirPubkey.cfingerprint()))) if context.is_verified(): context.print_buffer( @@ -1423,8 +1422,7 @@ def command_cb(data, buf, args): if secret: secret = PYVER.to_str(secret) - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) context.smpGotSecret(secret) result = weechat.WEECHAT_RC_OK @@ -1455,8 +1453,7 @@ def command_cb(data, buf, args): else: return weechat.WEECHAT_RC_ERROR - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) if secret: secret = PYVER.to_str(secret) @@ -1467,7 +1464,7 @@ def command_cb(data, buf, args): context.smpInit(secret, question) except potr.context.NotEncryptedError: context.print_buffer( - 'There is currently no encrypted session with {}.'.format( + 'There is currently no encrypted session with {0}.'.format( context.peer), 'error') else: if question: @@ -1487,16 +1484,15 @@ def command_cb(data, buf, args): else: return weechat.WEECHAT_RC_ERROR - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) if context.in_smp: try: context.smpAbort() except potr.context.NotEncryptedError: context.print_buffer( - 'There is currently no encrypted session with {}.'.format( - context.peer), 'error') + 'There is currently no encrypted session with {0}.' + .format(context.peer), 'error') else: debug('SMP aborted') context.smp_finish('SMP aborted.') @@ -1506,8 +1502,7 @@ def command_cb(data, buf, args): nick, server = default_peer_args(arg_parts[1:3], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) if context.crypto.theirPubkey is not None: context.setCurrentTrust('verified') @@ -1517,16 +1512,16 @@ def command_cb(data, buf, args): weechat.bar_item_update(SCRIPT_NAME) else: context.print_buffer( - 'No fingerprint for {peer}. Start an OTR conversation first: /otr start'.format( - peer=context.peer), 'error') + 'No fingerprint for {peer}. Start an OTR conversation ' + 'first: /otr start'.format(peer=context.peer), + 'error') result = weechat.WEECHAT_RC_OK elif len(arg_parts) in (1, 3) and arg_parts[0] == 'distrust': nick, server = default_peer_args(arg_parts[1:3], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) if context.crypto.theirPubkey is not None: context.setCurrentTrust('') @@ -1537,8 +1532,8 @@ def command_cb(data, buf, args): weechat.bar_item_update(SCRIPT_NAME) else: context.print_buffer( - 'No fingerprint for {peer}. Start an OTR conversation first: /otr start'.format( - peer=context.peer), 'error') + 'No fingerprint for {peer}. Start an OTR conversation ' + 'first: /otr start'.format(peer=context.peer), 'error') result = weechat.WEECHAT_RC_OK @@ -1546,8 +1541,7 @@ def command_cb(data, buf, args): nick, server = default_peer_args([], buf) if len(arg_parts) == 1: if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) if context.is_encrypted(): if context.is_logged(): @@ -1571,15 +1565,20 @@ def command_cb(data, buf, args): if len(arg_parts) == 2: if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) if arg_parts[1] == 'start' and \ not context.is_logged() and \ context.is_encrypted(): if context.previous_log_level is None: context.previous_log_level = context.get_log_level() - context.print_buffer('From this point on, this conversation will be logged. Please keep in mind that by doing so you are potentially putting yourself and your interlocutor at risk. You can disable this by doing /otr log stop', 'warning') + context.print_buffer( + 'From this point on, this conversation will be ' + 'logged. Please keep in mind that by doing so you ' + 'are potentially putting yourself and your ' + 'interlocutor at risk. You can disable this by doing ' + '/otr log stop', + 'warning') weechat.command(buf, '/mute logger set 9') result = weechat.WEECHAT_RC_OK @@ -1589,7 +1588,9 @@ def command_cb(data, buf, args): if context.previous_log_level is None: context.previous_log_level = context.get_log_level() weechat.command(buf, '/mute logger set 0') - context.print_buffer('From this point on, this conversation will NOT be logged ANYMORE.') + context.print_buffer( + 'From this point on, this conversation will NOT be ' + 'logged ANYMORE.') result = weechat.WEECHAT_RC_OK elif not context.is_encrypted(): @@ -1609,8 +1610,7 @@ def command_cb(data, buf, args): nick, server = default_peer_args([], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) context.print_buffer(context.format_policies()) else: @@ -1622,8 +1622,7 @@ def command_cb(data, buf, args): nick, server = default_peer_args([], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) context.print_buffer(format_default_policies()) else: @@ -1635,8 +1634,7 @@ def command_cb(data, buf, args): nick, server = default_peer_args([], buf) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) policy_var = context.policy_config_option(arg_parts[1].lower()) @@ -1660,8 +1658,7 @@ def command_cb(data, buf, args): value=arg_parts[3])) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) context.print_buffer(format_default_policies()) else: @@ -1690,72 +1687,87 @@ def otr_statusbar_cb(data, item, window): # will be empty. buf = weechat.current_buffer() - result = '' + if not buffer_is_private(buf): + return '' - if buffer_is_private(buf): - local_user = irc_user( - buffer_get_string(buf, 'localvar_nick'), - buffer_get_string(buf, 'localvar_server')) + local_user = irc_user( + buffer_get_string(buf, 'localvar_nick'), + buffer_get_string(buf, 'localvar_server')) - remote_user = irc_user( - buffer_get_string(buf, 'localvar_channel'), - buffer_get_string(buf, 'localvar_server')) + remote_user = irc_user( + buffer_get_string(buf, 'localvar_channel'), + buffer_get_string(buf, 'localvar_server')) - context = ACCOUNTS[local_user].getContext(remote_user) + context = get_context(local_user, remote_user) - encrypted_str = config_string('look.bar.state.encrypted') - unencrypted_str = config_string('look.bar.state.unencrypted') - authenticated_str = config_string('look.bar.state.authenticated') - unauthenticated_str = config_string('look.bar.state.unauthenticated') - logged_str = config_string('look.bar.state.logged') - notlogged_str = config_string('look.bar.state.notlogged') + encrypted_str = config_string('look.bar.state.encrypted') + unencrypted_str = config_string('look.bar.state.unencrypted') + authenticated_str = config_string('look.bar.state.authenticated') + unauthenticated_str = config_string('look.bar.state.unauthenticated') + logged_str = config_string('look.bar.state.logged') + notlogged_str = config_string('look.bar.state.notlogged') - bar_parts = [] + bar_parts = [] - if context.is_encrypted(): - if encrypted_str: - bar_parts.append(''.join([ - config_color('status.encrypted'), - encrypted_str, - config_color('status.default')])) - - if context.is_verified(): - if authenticated_str: - bar_parts.append(''.join([ - config_color('status.authenticated'), - authenticated_str, - config_color('status.default')])) - elif unauthenticated_str: - bar_parts.append(''.join([ - config_color('status.unauthenticated'), - unauthenticated_str, - config_color('status.default')])) - - if context.is_logged(): - if logged_str: - bar_parts.append(''.join([ - config_color('status.logged'), - logged_str, - config_color('status.default')])) - elif notlogged_str: - bar_parts.append(''.join([ - config_color('status.notlogged'), - notlogged_str, - config_color('status.default')])) - - elif unencrypted_str: + if context.is_encrypted(): + if encrypted_str: bar_parts.append(''.join([ - config_color('status.unencrypted'), - unencrypted_str, - config_color('status.default')])) + config_color('status.encrypted'), + encrypted_str, + config_color('status.default')])) - result = config_string('look.bar.state.separator').join(bar_parts) + if context.is_verified(): + if authenticated_str: + bar_parts.append(''.join([ + config_color('status.authenticated'), + authenticated_str, + config_color('status.default')])) + elif unauthenticated_str: + bar_parts.append(''.join([ + config_color('status.unauthenticated'), + unauthenticated_str, + config_color('status.default')])) - if result: - result = '{color}{prefix}{result}'.format( - color=config_color('status.default'), - prefix=config_string('look.bar.prefix'), - result=result) + if context.is_logged(): + if logged_str: + bar_parts.append(''.join([ + config_color('status.logged'), + logged_str, + config_color('status.default')])) + elif notlogged_str: + bar_parts.append(''.join([ + config_color('status.notlogged'), + notlogged_str, + config_color('status.default')])) + + elif unencrypted_str: + bar_parts.append(''.join([ + config_color('status.unencrypted'), + unencrypted_str, + config_color('status.default')])) + + result = config_string('look.bar.state.separator').join(bar_parts) + + if result: + result = '{color}{prefix}{result}'.format( + color=config_color('status.default'), + prefix=config_string('look.bar.prefix'), + result=result) + + if context.is_encrypted(): + weechat.buffer_set(buf, 'localvar_set_otr_encrypted', 'true') + else: + weechat.buffer_set(buf, 'localvar_set_otr_encrypted', 'false') + + if context.is_verified(): + weechat.buffer_set(buf, 'localvar_set_otr_authenticated', 'true') + else: + weechat.buffer_set(buf, 'localvar_set_otr_authenticated', 'false') + + if context.is_logged(): + weechat.buffer_set(buf, 'localvar_set_otr_logged', 'true') + else: + weechat.buffer_set(buf, 'localvar_set_otr_logged', 'false') return result @@ -1806,8 +1818,7 @@ def buffer_closing_cb(data, signal, signal_data): nick, server = default_peer_args([], signal_data) if nick is not None and server is not None: - context = ACCOUNTS[current_user(server)].getContext( - irc_user(nick, server)) + context = get_server_context(server, nick) context.disconnect() result = weechat.WEECHAT_RC_OK @@ -1816,7 +1827,7 @@ def buffer_closing_cb(data, signal, signal_data): def init_config(): """Set up configuration options and load config file.""" global CONFIG_FILE - CONFIG_FILE = weechat.config_new(SCRIPT_NAME, 'config_reload_cb', '') + CONFIG_FILE = weechat.config_new(SCRIPT_NAME, '', '') global CONFIG_SECTIONS CONFIG_SECTIONS = {} @@ -1825,14 +1836,24 @@ def init_config(): CONFIG_FILE, 'general', 0, 0, '', '', '', '', '', '', '', '', '', '') for option, typ, desc, default in [ - ('debug', 'boolean', 'OTR script debugging', 'off'), - ('hints', 'boolean', 'Give helpful hints how to use this script and how to stay secure while using OTR (recommended)', 'on'), - ('defaultkey', 'string', - 'default private key to use for new accounts (nick@server)', ''), - ('no_send_tag_regex', 'string', - 'do not OTR whitespace tag messages to nicks matching this regex ' - '(case insensitive)', - '^(alis|chanfix|global|.+serv|\*.+)$'), + ('debug', + 'boolean', + 'OTR script debugging', + 'off'), + ('hints', + 'boolean', + 'Give helpful hints how to use this script and how to stay ' + 'secure while using OTR (recommended)', + 'on'), + ('defaultkey', + 'string', + 'default private key to use for new accounts (nick@server)', + ''), + ('no_send_tag_regex', + 'string', + 'do not OTR whitespace tag messages to nicks matching this regex ' + '(case insensitive)', + '^(alis|chanfix|global|.+serv|\*.+)$'), ]: weechat.config_new_option( CONFIG_FILE, CONFIG_SECTIONS['general'], option, typ, desc, '', 0, @@ -1842,25 +1863,54 @@ def init_config(): CONFIG_FILE, 'color', 0, 0, '', '', '', '', '', '', '', '', '', '') for option, desc, default, update_cb in [ - ('status.default', 'status bar default color', 'default', - 'bar_config_update_cb'), - ('status.encrypted', 'status bar encrypted indicator color', 'green', - 'bar_config_update_cb'), - ('status.unencrypted', 'status bar unencrypted indicator color', - 'lightred', 'bar_config_update_cb'), - ('status.authenticated', 'status bar authenticated indicator color', - 'green', 'bar_config_update_cb'), - ('status.unauthenticated', 'status bar unauthenticated indicator color', - 'lightred', 'bar_config_update_cb'), - ('status.logged', 'status bar logged indicator color', 'lightred', - 'bar_config_update_cb'), - ('status.notlogged', 'status bar not logged indicator color', - 'green', 'bar_config_update_cb'), - ('buffer.hint', 'text color for hints', 'lightblue', ''), - ('buffer.info', 'text color for informational messages', 'default', ''), - ('buffer.success', 'text color for success messages', 'lightgreen', ''), - ('buffer.warning', 'text color for warnings', 'yellow', ''), - ('buffer.error', 'text color for errors', 'lightred', ''), + ('status.default', + 'status bar default color', + 'default', + 'bar_config_update_cb'), + ('status.encrypted', + 'status bar encrypted indicator color', + 'green', + 'bar_config_update_cb'), + ('status.unencrypted', + 'status bar unencrypted indicator color', + 'lightred', + 'bar_config_update_cb'), + ('status.authenticated', + 'status bar authenticated indicator color', + 'green', + 'bar_config_update_cb'), + ('status.unauthenticated', + 'status bar unauthenticated indicator color', + 'lightred', + 'bar_config_update_cb'), + ('status.logged', + 'status bar logged indicator color', + 'lightred', + 'bar_config_update_cb'), + ('status.notlogged', + 'status bar not logged indicator color', + 'green', + 'bar_config_update_cb'), + ('buffer.hint', + 'text color for hints', + 'lightblue', + ''), + ('buffer.info', + 'text color for informational messages', + 'default', + ''), + ('buffer.success', + 'text color for success messages', + 'lightgreen', + ''), + ('buffer.warning', + 'text color for warnings', + 'yellow', + ''), + ('buffer.error', + 'text color for errors', + 'lightred', + ''), ]: weechat.config_new_option( CONFIG_FILE, CONFIG_SECTIONS['color'], option, 'color', desc, '', 0, @@ -1870,32 +1920,45 @@ def init_config(): CONFIG_FILE, 'look', 0, 0, '', '', '', '', '', '', '', '', '', '') for option, desc, default, update_cb in [ - ('bar.prefix', 'prefix for OTR status bar item', 'OTR:', - 'bar_config_update_cb'), - ('bar.state.encrypted', - 'shown in status bar when conversation is encrypted', 'SEC', - 'bar_config_update_cb'), - ('bar.state.unencrypted', - 'shown in status bar when conversation is not encrypted', '!SEC', - 'bar_config_update_cb'), - ('bar.state.authenticated', - 'shown in status bar when peer is authenticated', 'AUTH', - 'bar_config_update_cb'), - ('bar.state.unauthenticated', - 'shown in status bar when peer is not authenticated', '!AUTH', - 'bar_config_update_cb'), - ('bar.state.logged', - 'shown in status bar when peer conversation is being logged to disk', - 'LOG', - 'bar_config_update_cb'), - ('bar.state.notlogged', - 'shown in status bar when peer conversation is not being logged to disk', - '!LOG', - 'bar_config_update_cb'), - ('bar.state.separator', 'separator for states in the status bar', ',', - 'bar_config_update_cb'), - ('prefix', 'prefix used for messages from otr (note: content is evaluated, see /help eval)', - '${color:default}:! ${color:brown}otr${color:default} !:', ''), + ('bar.prefix', + 'prefix for OTR status bar item', + 'OTR:', + 'bar_config_update_cb'), + ('bar.state.encrypted', + 'shown in status bar when conversation is encrypted', + 'SEC', + 'bar_config_update_cb'), + ('bar.state.unencrypted', + 'shown in status bar when conversation is not encrypted', + '!SEC', + 'bar_config_update_cb'), + ('bar.state.authenticated', + 'shown in status bar when peer is authenticated', + 'AUTH', + 'bar_config_update_cb'), + ('bar.state.unauthenticated', + 'shown in status bar when peer is not authenticated', + '!AUTH', + 'bar_config_update_cb'), + ('bar.state.logged', + 'shown in status bar when peer conversation is being logged to ' + 'disk', + 'LOG', + 'bar_config_update_cb'), + ('bar.state.notlogged', + 'shown in status bar when peer conversation is not being logged ' + 'to disk', + '!LOG', + 'bar_config_update_cb'), + ('bar.state.separator', + 'separator for states in the status bar', + ',', + 'bar_config_update_cb'), + ('prefix', + 'prefix used for messages from otr (note: content is evaluated, ' + 'see /help eval)', + '${color:default}:! ${color:brown}otr${color:default} !:', + ''), ]: weechat.config_new_option( CONFIG_FILE, CONFIG_SECTIONS['look'], option, 'string', desc, '', @@ -1906,13 +1969,24 @@ def init_config(): 'policy_create_option_cb', '', '', '') for option, desc, default in [ - ('default.allow_v2', 'default allow OTR v2 policy', 'on'), - ('default.require_encryption', 'default require encryption policy', - 'off'), - ('default.log', 'default enable logging to disk', 'off'), - ('default.send_tag', 'default send tag policy', 'off'), - ('default.html_escape', 'default HTML escape policy', 'off'), - ('default.html_filter', 'default HTML filter policy', 'on'), + ('default.allow_v2', + 'default allow OTR v2 policy', + 'on'), + ('default.require_encryption', + 'default require encryption policy', + 'off'), + ('default.log', + 'default enable logging to disk', + 'off'), + ('default.send_tag', + 'default send tag policy', + 'off'), + ('default.html_escape', + 'default HTML escape policy', + 'off'), + ('default.html_filter', + 'default HTML filter policy', + 'on'), ]: weechat.config_new_option( CONFIG_FILE, CONFIG_SECTIONS['policy'], option, 'boolean', desc, '', @@ -1920,13 +1994,6 @@ def init_config(): weechat.config_read(CONFIG_FILE) -def config_reload_cb(data, config_file): - """/reload callback to reload config from file.""" - free_all_config() - init_config() - - return weechat.WEECHAT_CONFIG_READ_OK - def free_all_config(): """Free all config options, sections and config file.""" for section in CONFIG_SECTIONS.values(): @@ -1949,13 +2016,19 @@ def git_info(): if os.path.isdir(git_dir): import subprocess try: - result = PYVER.to_unicode(subprocess.check_output([ + # We can't use check_output here without breaking compatibility + # for Python 2.6, but we ignore the return value anyway, so Popen + # is only slightly more complicated: + process = subprocess.Popen([ 'git', '--git-dir', git_dir, '--work-tree', script_dir, 'describe', '--dirty', '--always', - ])).lstrip('v').rstrip() - except (OSError, subprocess.CalledProcessError): + ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output = process.communicate()[0] + if output: + result = PYVER.to_unicode(output).lstrip('v').rstrip() + except OSError: pass return result @@ -1971,42 +2044,42 @@ def weechat_version_ok(): error_message = ( '{script_name} requires WeeChat version >= 0.4.2. The current ' 'version is {current_version}.').format( - script_name=SCRIPT_NAME, - current_version=weechat.info_get('version', '')) + script_name=SCRIPT_NAME, + current_version=weechat.info_get('version', '')) prnt('', error_message) return False - else: - return True + return True SCRIPT_VERSION = git_info() or SCRIPT_VERSION def dependency_versions(): """Return a string containing the versions of all dependencies.""" return ('weechat-otr {script_version}, ' - 'potr {potr_major}.{potr_minor}.{potr_patch}-{potr_sub}, ' - 'Python {python_version}, ' - 'WeeChat {weechat_version}' - ).format( - script_version=SCRIPT_VERSION, - potr_major=potr.VERSION[0], - potr_minor=potr.VERSION[1], - potr_patch=potr.VERSION[2], - potr_sub=potr.VERSION[3], - python_version=platform.python_version(), - weechat_version=weechat.info_get('version', '')) + 'potr {potr_major}.{potr_minor}.{potr_patch}-{potr_sub}, ' + 'Python {python_version}, ' + 'WeeChat {weechat_version}' + ).format( + script_version=SCRIPT_VERSION, + potr_major=potr.VERSION[0], + potr_minor=potr.VERSION[1], + potr_patch=potr.VERSION[2], + potr_sub=potr.VERSION[3], + python_version=platform.python_version(), + weechat_version=weechat.info_get('version', '')) -def excepthook(typ, value, traceback): +def excepthook(typ, value, tracebak): + """Add dependency versions to tracebacks.""" sys.stderr.write('Versions: ') sys.stderr.write(dependency_versions()) sys.stderr.write('\n') - sys.__excepthook__(typ, value, traceback) + sys.__excepthook__(typ, value, tracebak) sys.excepthook = excepthook if weechat.register( - SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENCE, SCRIPT_DESC, - 'shutdown', ''): + SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENCE, SCRIPT_DESC, + 'shutdown', ''): if weechat_version_ok(): init_config() @@ -2030,7 +2103,7 @@ if weechat.register( 'smp abort [NICK SERVER] || ' 'trust [NICK SERVER] || ' 'distrust [NICK SERVER] || ' - 'log [on|off] || ' + 'log [start|stop] || ' 'policy [POLICY on|off] || ' 'fingerprint [SEARCH|all]', '', @@ -2043,7 +2116,7 @@ if weechat.register( 'smp abort %(nick) %(irc_servers) %-||' 'trust %(nick) %(irc_servers) %-||' 'distrust %(nick) %(irc_servers) %-||' - 'log on|off %-||' + 'log start|stop %-||' 'policy %(otr_policy) on|off %-||' 'fingerprint all %-||', 'command_cb', diff --git a/weechat/.weechat/python/topicdiff_alt.py b/weechat/.weechat/python/topicdiff_alt.py new file mode 100644 index 0000000..c76fa22 --- /dev/null +++ b/weechat/.weechat/python/topicdiff_alt.py @@ -0,0 +1,70 @@ +import weechat +import diff_match_patch +import re + +weechat.register('topicdiff_alt', 'Juerd <#####@juerd.nl>', '1.01', 'PD', "Announce topic with changes highlighted", '', '') + +def topic(data, tags, msg): + server = tags.split(",")[0] + + match = re.search(r':(\S+)\s+TOPIC\s+(\S+)\s+:(.*)', msg) + + if not match: + return weechat.WEECHAT_RC_ERROR + + usermask, channel, newtopic = match.groups() + nick, host = usermask.split("!", 1) + + buffer = weechat.buffer_search("irc", server + "." + channel) + weechat.prnt("", server + "." + channel) + + if not buffer: + return weechat.WEECHAT_RC_ERROR + + oldtopic = weechat.buffer_get_string(buffer, "title") + if oldtopic == None: + oldtopic = "" + + dmp = diff_match_patch.diff_match_patch() + diff = dmp.diff_main(oldtopic, newtopic) + dmp.diff_cleanupEfficiency(diff) + + topic = "" + + color_reset = weechat.color("reset") + color_ins = weechat.color(weechat.config_get_plugin("color_ins")) + color_del = weechat.color(weechat.config_get_plugin("color_del")) + + for chunk in diff: + changed, text = chunk + + topic += "%s%s%s" % ( + # 0 (unchanged), 1 (added), -1 (removed) + ["", color_ins, color_del][changed], + text, + ["", color_reset, color_reset][changed] + ) + + weechat.prnt_date_tags(buffer, 0, "irc_topicdiff", + "%s%s%s%s has changed topic for %s%s%s: %s" % ( + weechat.prefix("network"), + weechat.color(weechat.info_get("irc_nick_color", nick)) \ + if weechat.config_boolean("irc.look.color_nicks_in_server_messages") \ + else weechat.color("chat_nick"), + nick, + color_reset, + weechat.color("chat_channel"), + channel, + color_reset, + topic + )) + + return weechat.WEECHAT_RC_OK + +weechat.hook_signal("*,irc_in_topic", "topic", "") + +if not weechat.config_is_set_plugin("color_ins"): + weechat.config_set_plugin("color_ins", "lightcyan") + +if not weechat.config_is_set_plugin("color_del"): + weechat.config_set_plugin("color_del", "darkgray") diff --git a/weechat/.weechat/weechat.conf b/weechat/.weechat/weechat.conf index 0feda2e..0e61008 100644 --- a/weechat/.weechat/weechat.conf +++ b/weechat/.weechat/weechat.conf @@ -360,6 +360,7 @@ irc.tilde = message irc.town = message [filter] +irc_smart = on;irc.freenode.#git;irc_smart_filter;* [key] ctrl-? = "/input delete_previous_char"