├── .gitignore ├── .travis.yml ├── ChangeLog ├── README.md ├── doc ├── gen_startup_troubleshooting.sh ├── startup_troubleshooting.dot └── startup_troubleshooting.png ├── docker ├── Dockerfile ├── README ├── build └── run ├── pylint.rc ├── requirements.txt ├── run_tests.sh ├── signatures └── index.html ├── test_until_fail.sh ├── watch_tests.py ├── weechat_otr.py └── weechat_otr_test ├── __init__.py ├── is_encrypted_account.py ├── is_encrypted_context.py ├── mock_account.py ├── mock_context.py ├── mock_user.py ├── mock_weechat.py ├── mock_window.py ├── raising_account.py ├── raising_context.py ├── recording_account.py ├── recording_context.py ├── session_test_case.py ├── test_arg_parsing.py ├── test_assembler.py ├── test_buffer_closing.py ├── test_command_finish.py ├── test_command_start.py ├── test_context.py ├── test_context_get_logger_option_name.py ├── test_context_message_in_cb.py ├── test_context_msg_convert_in.py ├── test_context_msg_convert_out.py ├── test_debug_buffer.py ├── test_excepthook.py ├── test_fingerprint.py ├── test_git_info.py ├── test_html_escape_policy.py ├── test_irc_context.py ├── test_irc_html_parser.py ├── test_irc_otr_account_load_trusts.py ├── test_irc_user.py ├── test_is_a_channel.py ├── test_key_generation.py ├── test_log_level.py ├── test_message_out_cb.py ├── test_otr_session.py ├── test_otr_statusbar_cb.py ├── test_parse_irc_privmsg.py ├── test_policy.py ├── test_policy_account.py ├── test_policy_server.py ├── test_shutdown.py ├── test_smp.py ├── test_table_formatter.py ├── test_weechat_otr.py ├── test_weechat_version_ok.py └── weechat_otr_test_case.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | - "3.5" 8 | install: 9 | - "pip install pylint" 10 | - "pip install -r requirements.txt" 11 | script: ./run_tests.sh 12 | matrix: 13 | fast_finish: true 14 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 1.9.2 2 | 2018-03-26 3 | 4 | Fix weechat crash on /reload. 5 | 6 | Preparation for python 2.6 support. 7 | 8 | 1.9.1 9 | 2017-07-30 10 | 11 | Fix potr ErrorReceived printing bug. 12 | 13 | Argument parsing errors now raise exceptions. 14 | 15 | Fix require_encryption policy hint. 16 | 17 | Fix traceback caused by non-ASCII nick 18 | (https://github.com/mmb/weechat-otr/issues/133). 19 | 20 | 1.9.0 21 | 2017-04-19 22 | 23 | Fix completion for log command. 24 | 25 | Fix error in log command help (thanks @w2ak). 26 | 27 | Set buffer local vars with OTR conversation status. These can be used by 28 | remote interfaces like Glowing Bear. 29 | 30 | 1.8.0 31 | 2015-12-21 32 | 33 | IRC nicks are case insensitive. Treat all cases of a nick as 34 | equivalent. 35 | 36 | 1.7.0 37 | 2015-08-02 38 | 39 | Allow setting policies on a per server and per account basis. Peer policies 40 | override account policies which override server policies. 41 | 42 | Convert data loaded from trust files to unicode. 43 | 44 | Do not OTR whitespace tag nicks starting with *. 45 | 46 | Nicks to skip OTR tagging for are now a user-configurable regex. Set the 47 | general.no_send_tag_regex option. 48 | 49 | Add otr.general.default.key option to share a key across accounts. Set 50 | this to nick@server of the account with the key you want to use as the 51 | default key for new accounts. 52 | 53 | Do not OTR whitespace tag CTCP messages. 54 | 55 | 1.6.0 56 | 2015-02-06 57 | 58 | Fix bug with messaging yourself 59 | 60 | Print dependency versions with exception tracebacks to diagnose 61 | reported bugs more easily 62 | 63 | Fix a number of bugs that caused tracebacks when incorrect 64 | arguments were passed to commands 65 | 66 | Addition of screenshot and other improvements to the README 67 | 68 | Make the meanings of allow_v2 and require_encryption policies 69 | clearer to users 70 | 71 | Fix bug with allow_v2/require_encryption policies where a 72 | message was sent but the user was told the message was not sent 73 | 74 | Faster Travis builds due to using a container on Travis 75 | 76 | Use IRC isupport (005) messages to get channel and statusmsg 77 | prefixes instead of hardcoding them 78 | 79 | Run tests on Python 3.2 and 3.4 80 | 81 | Fix bug with treating statusmsgs as private messages instead 82 | of channel messages 83 | 84 | Check WeeChat version during startup 85 | 86 | /otr refresh is an alias for /otr start 87 | 88 | /otr end is an alias for /otr finish 89 | 90 | 1.5.0 91 | 2014-09-30 92 | 93 | Handle nicks containing unicode characters 94 | 95 | Add /otr fingerprint command for showing fingerprints 96 | 97 | Disable send_tag for ChanServ, MemoServ and NickServer 98 | 99 | End OTR sessions when their buffer is closed 100 | 101 | Colorize a lot of output that was previously not colored 102 | 103 | User configurable prefix for messages from script 104 | 105 | Handle incoming IRC ACTION in unencrypted messages 106 | 107 | Only modify actions if the context is encrypted 108 | 109 | Restore default log level after OTR conversation 110 | 111 | Make PRIVMSG parsing case insensitive 112 | 113 | Raise an exception when a PRIVMSG can't be parsed 114 | 115 | Fix parsing PRIVMSGs where the message doesn't start with : 116 | 117 | 1.4.0 118 | 2014-04-11 119 | 120 | Non-ascii character bug fixes 121 | 122 | Use WeeChat IRC message parser 123 | 124 | Make messages from the script look different from messages from a user to 125 | prevent a user impersonating the script 126 | 127 | Fix character encoding bug when writing trusts file 128 | 129 | More experimental Python 3 support 130 | 131 | Add ability to remove HTML from incoming messages with per-peer setting 132 | 133 | Script version now includes git describe output if the script is in a git 134 | tree 135 | 136 | Add outbound HTML escaping with with per-peer setting 137 | 138 | Better multiline message handling 139 | 140 | Bug fix for SMP command parsing with nick, server and secret 141 | 142 | 1.3.0 143 | 2013-12-13 144 | 145 | Character encoding bug fixes 146 | 147 | Update to latest version of python-potr 148 | 149 | Add tests 150 | 151 | Fix for channels that start with & 152 | 153 | Fix for incoming messages with newlines in them 154 | 155 | Preference for turning off hints 156 | 157 | Match OTR query string anywhere in the message 158 | 159 | Better sanitization when building PRIVMSG commands 160 | 161 | Prefix messages printed to server buffer with nick 162 | 163 | Warn when a message is not sent due to require_encryption policy 164 | 165 | Support ACTIONs (/me) in OTR messages 166 | 167 | SMP questions can now contain non-ASCII characters 168 | 169 | 1.2.0 170 | 2013-08-13 171 | 172 | Fix UTF8 encoding errors 173 | 174 | Add the possibility to revoke a contact's trust permanently 175 | 176 | Add logging control and settings during an OTR session. Logs are disabled 177 | by default during the OTR session and restored to their previous value 178 | after the OTR session has ended 179 | 180 | Add the possibility to abort a SMP process 181 | 182 | Better policy management 183 | 184 | Clearer messages and warnings to avoid user mistakes and confusion 185 | 186 | Send debug messages to a dedicated buffer for easier debugging 187 | 188 | Add a command "/otr status" to see fingerprints and the OTR status 189 | of a specific conversation 190 | 191 | Lots of bugfixes and little improvements 192 | 193 | 194 | 195 | 1.1.0 196 | 2012-08-15 197 | 198 | Remove references to OTR protocol version 1 because pure-python-otr 199 | doesn't support it. 200 | 201 | Stricter argument parsing to prevent tracebacks. 202 | 203 | Fix status bar item when it occurs in a root bar. 204 | 205 | 1.0.0 206 | 2012-05-26 207 | 208 | Initial release. 209 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WeeChat script for Off-the-Record (OTR) Messaging 2 | 3 | ![Screenshot](https://cloud.githubusercontent.com/assets/24275/5772021/262c6dd6-9cff-11e4-964f-1812a6a545c6.png) 4 | 5 | > **Please note**: This script makes every effort to securely provide OTR 6 | > Messaging in WeeChat but offers no guarantee. Please report any security 7 | > problems you find. 8 | 9 | Testing and security auditing are appreciated. 10 | 11 | [![Build Status](https://travis-ci.org/mmb/weechat-otr.svg?branch=master)](https://travis-ci.org/mmb/weechat-otr) 12 | 13 | ## Installation 14 | 15 | This script requires Weechat 0.4.2 or later and the 16 | [Pure Python OTR](https://github.com/afflux/pure-python-otr) 17 | package to be installed with one of the following methods: 18 | 19 | Python package: 20 | ```bash 21 | pip install --upgrade --user python-potr 22 | ``` 23 | 24 | If this fails, read 25 | [Requirements for building Pure Python OTR](#requirements-for-building-pure-python-otr) 26 | below. 27 | 28 | Arch: 29 | ```bash 30 | yaourt -S python2-potr 31 | ``` 32 | 33 | Debian based systems: 34 | ```bash 35 | sudo apt-get install python-potr 36 | ``` 37 | 38 | The latest release version of WeeChat OTR can be found in the 39 | [WeeChat scripts repository](https://www.weechat.org/scripts/source/otr.py.html/). 40 | To install from within WeeChat: 41 | 42 | /script install otr.py 43 | 44 | To install manually, download `weechat_otr.py` from GitHub and save it in 45 | `~/.weechat/python`. Then either symlink it into 46 | `~/.weechat/python/autoload` or `/python load weechat_otr.py` 47 | in WeeChat. 48 | 49 | [Latest unstable version from GitHub](https://raw.githubusercontent.com/mmb/weechat-otr/master/weechat_otr.py) 50 | 51 | If you are using an official release of the script, it is a good idea to 52 | [verify the signature](https://s3.amazonaws.com/weechat-otr-signatures/index.html). 53 | 54 | ### Requirements for building Pure Python OTR 55 | 56 | If python-potr fails to install, you are probably missing some packages. 57 | To install all the requirements on a Debian/Ubuntu system, run 58 | 59 | sudo apt-get install python-pip python-wheel build-essential python-dev 60 | 61 | Or on Arch run 62 | 63 | sudo pacman --needed -S python2-pip python2-wheel python2-keyring base-devel 64 | 65 | ## Buffer Local Variables 66 | 67 | The script will set the following buffer local variables: 68 | 69 | - `localvar_set_otr_encrypted` - whether the buffer is OTR encrypted (true or false) 70 | - `localvar_set_otr_authenticated` - whether the buffer is OTR authenticated (true or false) 71 | - `localvar_set_otr_logged` - whether the buffer is logged (true or false) 72 | 73 | These match what is shown in the status bar and can be used by remote interfaces 74 | via the WeeChat relay protocol or by other scripts. 75 | 76 | ## Support 77 | 78 | IRC channel: `#weechat-otr` on Freenode 79 | 80 | Create GitHub issues/pull requests for questions, comments and patches or 81 | email matthewm@boedicker.org or koolfy@koolfy.be. 82 | 83 | ## Thanks 84 | 85 | Thanks to Kjell Braden for the Pure Python OTR library and the Gajim 86 | Python plugin which was used as a reference. 87 | -------------------------------------------------------------------------------- /doc/gen_startup_troubleshooting.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | dot \ 4 | -Tpng \ 5 | startup_troubleshooting.dot \ 6 | -o startup_troubleshooting.png 7 | -------------------------------------------------------------------------------- /doc/startup_troubleshooting.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | start [shape=box, label="Start"]; 3 | start -> python; 4 | 5 | python [shape=box, label="Is WeeChat compiled with Python support?\n/python"]; 6 | python -> python_version [label="Yes"]; 7 | python -> recompile [label="No"]; 8 | 9 | python_version [shape=box, label="Which version of Python is WeeChat using?\n/debug libs"]; 10 | python_version -> potr_installed; 11 | 12 | recompile [shape=box, label="Recompile WeeChat with Python support"]; 13 | recompile -> start; 14 | 15 | potr_installed [shape=box, label="Is potr installed in that version of Python?\n$ python --version\n$ python -c 'import potr'"]; 16 | potr_installed -> script [label="Yes"]; 17 | potr_installed -> pip_python [label="No"]; 18 | 19 | script [shape=box, label="Install weechat-otr\n/script install otr.py"]; 20 | script -> help; 21 | 22 | pip_python [shape="box", label="Is pip using that version of Python?\n$ pip --version"]; 23 | pip_python -> install_potr [label="Yes"]; 24 | pip_python -> pip_version [label="No"]; 25 | 26 | install_potr [shape=box, label="$ pip install --upgrade --user python-potr"]; 27 | install_potr -> potr_installed; 28 | 29 | pip_version [shape=box, label="$ python2.7 $(which pip) install --upgrade --user python-potr"]; 30 | pip_version -> potr_installed; 31 | 32 | help [shape=box, label="/help otr"]; 33 | } 34 | -------------------------------------------------------------------------------- /doc/startup_troubleshooting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmb/weechat-otr/330d7b0632d5e05dbdd5891fd3622c7358b2b6f6/doc/startup_troubleshooting.png -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | 3 | RUN apt-get update 4 | RUN apt-get --assume-yes install build-essential 5 | RUN apt-get --assume-yes install pylint 6 | RUN apt-get --assume-yes install python-dev 7 | RUN apt-get --assume-yes install python-setuptools 8 | 9 | RUN easy_install pip 10 | RUN pip install python-potr 11 | -------------------------------------------------------------------------------- /docker/README: -------------------------------------------------------------------------------- 1 | Dockerfile and scripts for a reproducible weechat-otr development environment. 2 | -------------------------------------------------------------------------------- /docker/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker build \ 4 | --tag=weechat-otr \ 5 | . 6 | -------------------------------------------------------------------------------- /docker/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker run \ 4 | --interactive=true \ 5 | --tty=true \ 6 | --volume=$(readlink -f ..):/weechat-otr \ 7 | --workdir=/weechat-otr \ 8 | --rm=true \ 9 | weechat-otr \ 10 | /bin/bash 11 | -------------------------------------------------------------------------------- /pylint.rc: -------------------------------------------------------------------------------- 1 | [REPORTS] 2 | max-line-length=80 3 | output-format=colorized 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-potr -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | pylint --rcfile=pylint.rc weechat_otr.py 4 | pylint --rcfile=pylint.rc weechat_otr_test 5 | 6 | python -m unittest discover 7 | -------------------------------------------------------------------------------- /signatures/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | weechat-otr Release Signatures 5 | 6 | 7 | 8 | 9 |

weechat-otr Release Signatures

10 | 11 |

Import the public key

12 | 13 |
14 | curl https://s3.amazonaws.com/matthewm.boedicker.org/matthewm@boedicker.org_pgp_public_key.txt | gpg --import
15 | 
16 | 17 |

Verify the key fingerprint

18 | 19 |
20 | gpg --fingerprint matthewm@boedicker.org
21 | dig +short txt matthewm._pka.boedicker.org
22 | 
23 | 24 | Download 1.9.2.asc 25 | 26 |
27 | gpg --verify 1.9.2.asc ~/.weechat/python/otr.py
28 | 
29 | 30 | Download 1.9.1.asc 31 | 32 |
33 | gpg --verify 1.9.1.asc ~/.weechat/python/otr.py
34 | 
35 | 36 | Download 1.9.0.asc 37 | 38 |
39 | gpg --verify 1.9.0.asc ~/.weechat/python/otr.py
40 | 
41 | 42 | Download 1.8.0.asc 43 | 44 |
45 | gpg --verify 1.8.0.asc ~/.weechat/python/otr.py
46 | 
47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /test_until_fail.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | COUNT=0 4 | 5 | while python -m unittest discover; do 6 | echo $((COUNT+=1)) 7 | done 8 | -------------------------------------------------------------------------------- /watch_tests.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import time 3 | 4 | import watchdog.observers 5 | import watchdog.events 6 | 7 | class CodeModifiedEventHandler(watchdog.events.PatternMatchingEventHandler): 8 | 9 | def __init__(self, command): 10 | super(CodeModifiedEventHandler, self).__init__(['*.py']) 11 | self.command = command 12 | 13 | def on_any_event(self, event): 14 | try: 15 | print(subprocess.check_output(self.command)) 16 | except subprocess.CalledProcessError as err: 17 | print(err.output) 18 | 19 | print('-' * 80) 20 | 21 | class TestWatcher(object): 22 | 23 | def __init__(self, path, command): 24 | self.observer = watchdog.observers.Observer() 25 | handler = CodeModifiedEventHandler(command) 26 | self.observer.schedule(handler, path, recursive=True) 27 | 28 | def start(self): 29 | self.observer.start() 30 | try: 31 | while True: 32 | time.sleep(1) 33 | except KeyboardInterrupt: 34 | self.observer.stop() 35 | self.observer.join() 36 | 37 | if __name__ == '__main__': 38 | TestWatcher('.', ['python', '-m', 'unittest', 'discover']).start() 39 | -------------------------------------------------------------------------------- /weechat_otr.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """otr - WeeChat script for Off-the-Record IRC messaging 3 | 4 | DISCLAIMER: To the best of my knowledge this script securely provides OTR 5 | messaging in WeeChat, but I offer no guarantee. Please report any security 6 | holes you find. 7 | 8 | Copyright (c) 2012-2015 Matthew M. Boedicker 9 | Nils Görs 10 | Daniel "koolfy" Faucon 11 | Felix Eckhofer 12 | 13 | Report issues at https://github.com/mmb/weechat-otr 14 | 15 | This program is free software: you can redistribute it and/or modify 16 | it under the terms of the GNU General Public License as published by 17 | the Free Software Foundation, either version 3 of the License, or 18 | (at your option) any later version. 19 | 20 | This program is distributed in the hope that it will be useful, 21 | but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | GNU General Public License for more details. 24 | 25 | You should have received a copy of the GNU General Public License 26 | along with this program. If not, see . 27 | """ 28 | 29 | # pylint: disable=too-many-lines 30 | 31 | from __future__ import unicode_literals 32 | 33 | import collections 34 | import glob 35 | import io 36 | import os 37 | import platform 38 | import re 39 | import traceback 40 | import shlex 41 | import shutil 42 | import sys 43 | 44 | import potr 45 | import weechat 46 | 47 | class PythonVersion2(object): 48 | """Python 2 version of code that must differ between Python 2 and 3.""" 49 | 50 | def __init__(self): 51 | import cgi 52 | self.cgi = cgi 53 | 54 | import HTMLParser 55 | self.html_parser = HTMLParser 56 | self.html_parser_init_kwargs = {} 57 | 58 | import htmlentitydefs 59 | self.html_entities = htmlentitydefs 60 | 61 | def html_escape(self, strng): 62 | """Escape HTML characters in a string.""" 63 | return self.cgi.escape(strng) 64 | 65 | def unicode(self, *args, **kwargs): 66 | """Return the Unicode version of a string.""" 67 | return unicode(*args, **kwargs) 68 | 69 | def unichr(self, *args, **kwargs): 70 | """Return the one character string of a Unicode character number.""" 71 | return unichr(*args, **kwargs) 72 | 73 | def to_unicode(self, strng): 74 | """Convert a utf-8 encoded string to a Unicode.""" 75 | if isinstance(strng, unicode): 76 | return strng 77 | return strng.decode('utf-8', 'replace') 78 | 79 | def to_str(self, strng): 80 | """Convert a Unicode to a utf-8 encoded string.""" 81 | return strng.encode('utf-8', 'replace') 82 | 83 | class PythonVersion3(object): 84 | """Python 3 version of code that must differ between Python 2 and 3.""" 85 | 86 | def __init__(self, minor): 87 | self.minor = minor 88 | 89 | import html 90 | self.html = html 91 | 92 | import html.parser 93 | self.html_parser = html.parser 94 | if self.minor >= 4: 95 | self.html_parser_init_kwargs = {'convert_charrefs' : True} 96 | else: 97 | self.html_parser_init_kwargs = {} 98 | 99 | import html.entities 100 | self.html_entities = html.entities 101 | 102 | def html_escape(self, strng): 103 | """Escape HTML characters in a string.""" 104 | return self.html.escape(strng, quote=False) 105 | 106 | def unicode(self, *args, **kwargs): 107 | """Return the Unicode version of a string.""" 108 | return str(*args, **kwargs) 109 | 110 | def unichr(self, *args, **kwargs): 111 | """Return the one character string of a Unicode character number.""" 112 | return chr(*args, **kwargs) 113 | 114 | def to_unicode(self, strng): 115 | """Convert a utf-8 encoded string to unicode.""" 116 | if isinstance(strng, bytes): 117 | return strng.decode('utf-8', 'replace') 118 | return strng 119 | 120 | def to_str(self, strng): 121 | """Convert a Unicode to a utf-8 encoded string.""" 122 | return strng 123 | 124 | # We cannot use version_info.major as this is only supported on python >= 2.7 125 | if sys.version_info[0] >= 3: 126 | PYVER = PythonVersion3(sys.version_info.minor) 127 | else: 128 | PYVER = PythonVersion2() 129 | 130 | SCRIPT_NAME = 'otr' 131 | SCRIPT_DESC = 'Off-the-Record messaging for IRC' 132 | SCRIPT_HELP = """{description} 133 | 134 | Quick start: 135 | 136 | Add an OTR item to the status bar by adding '[otr]' to the config setting 137 | weechat.bar.status.items. This will show you whether your current conversation 138 | is encrypted, authenticated and logged. /set otr.* for OTR status bar 139 | customization options. 140 | 141 | Start a private conversation with a friend who has OTR: /query yourpeer hi 142 | 143 | In the private chat buffer: /otr start 144 | 145 | If you have not authenticated your peer yet, follow the instructions for 146 | authentication. 147 | 148 | You can, at any time, see the current OTR session status and fingerprints with: 149 | /otr status 150 | 151 | View OTR policies for your peer: /otr policy 152 | 153 | View default OTR policies: /otr policy default 154 | 155 | Start/Stop log recording for the current OTR session: /otr log [start|stop] 156 | This will be reverted back to the previous log setting at the end of the session. 157 | 158 | To refresh the OTR session: /otr refresh 159 | 160 | To end your private conversation: /otr finish 161 | 162 | This script supports only OTR protocol version 2. 163 | """.format(description=SCRIPT_DESC) 164 | 165 | SCRIPT_AUTHOR = 'Matthew M. Boedicker' 166 | SCRIPT_LICENCE = 'GPL3' 167 | SCRIPT_VERSION = '1.9.2' 168 | 169 | OTR_DIR_NAME = 'otr' 170 | 171 | OTR_QUERY_RE = re.compile(r'\?OTR(\?|\??v[a-z\d]*\?)') 172 | 173 | POLICIES = { 174 | 'allow_v2' : 'allow OTR protocol version 2, effectively enable OTR ' 175 | 'since v2 is the only supported version', 176 | 'require_encryption' : 'refuse to send unencrypted messages when OTR is ' 177 | 'enabled', 178 | 'log' : 'enable logging of OTR conversations', 179 | 'send_tag' : 'advertise your OTR capability using the whitespace tag', 180 | 'html_escape' : 'escape HTML special characters in outbound messages', 181 | 'html_filter' : 'filter HTML in incoming messages', 182 | } 183 | 184 | READ_ONLY_POLICIES = { 185 | 'allow_v1' : False, 186 | } 187 | 188 | ACTION_PREFIX = '/me ' 189 | IRC_ACTION_RE = re.compile('^\x01ACTION (?P.*)\x01$') 190 | PLAIN_ACTION_RE = re.compile('^'+ACTION_PREFIX+'(?P.*)$') 191 | 192 | IRC_SANITIZE_TABLE = dict((ord(char), None) for char in '\n\r\x00') 193 | 194 | # Patch potr.proto.TaggedPlaintext to not end plaintext tags in a space. 195 | # 196 | # When POTR adds OTR tags to plaintext it puts them at the end of the message. 197 | # The tags end in a space which gets stripped off by WeeChat because it 198 | # strips trailing spaces from commands. This causes OTR initiation to fail so 199 | # the following code adds an extra tab at the end of the plaintext tags if 200 | # they end in a space. 201 | # 202 | # The patched version also skips OTR tagging for CTCP messages because it 203 | # breaks the CTCP format. 204 | def patched__bytes__(self): 205 | """Patched potr.proto.TaggedPlainText.__bytes__.""" 206 | # Do not tag CTCP messages. 207 | if self.msg.startswith(b'\x01') and \ 208 | self.msg.endswith(b'\x01'): 209 | return self.msg 210 | 211 | data = self.msg + potr.proto.MESSAGE_TAG_BASE 212 | for v in self.versions: 213 | data += potr.proto.MESSAGE_TAGS[v] 214 | if data.endswith(b' '): 215 | data += b'\t' 216 | return data 217 | 218 | potr.proto.TaggedPlaintext.__bytes__ = patched__bytes__ 219 | 220 | def command(buf, command_str): 221 | """Wrap weechat.command() with utf-8 encode.""" 222 | debug(command_str) 223 | weechat.command(buf, PYVER.to_str(command_str)) 224 | 225 | def privmsg(server, nick, message): 226 | """Send a private message to a nick.""" 227 | for line in message.splitlines(): 228 | command('', '/quote -server {server} PRIVMSG {nick} :{line}'.format( 229 | server=irc_sanitize(server), 230 | nick=irc_sanitize(nick), 231 | line=irc_sanitize(line))) 232 | 233 | def build_privmsg_in(fromm, target, msg): 234 | """Build inbound IRC PRIVMSG command.""" 235 | return ':{user} PRIVMSG {target} :{msg}'.format( 236 | user=irc_sanitize(fromm), 237 | target=irc_sanitize(target), 238 | msg=irc_sanitize(msg)) 239 | 240 | def build_privmsgs_in(fromm, target, msg, prefix=''): 241 | """Build an inbound IRC PRIVMSG command for each line in msg. 242 | If prefix is supplied, prefix each line of msg with it.""" 243 | cmd = [] 244 | for line in msg.splitlines(): 245 | cmd.append(build_privmsg_in(fromm, target, prefix+line)) 246 | return '\r\n'.join(cmd) 247 | 248 | def build_privmsg_out(target, msg): 249 | """Build outbound IRC PRIVMSG command(s).""" 250 | cmd = [] 251 | for line in msg.splitlines(): 252 | cmd.append('PRIVMSG {target} :{line}'.format( 253 | target=irc_sanitize(target), 254 | line=irc_sanitize(line))) 255 | return '\r\n'.join(cmd) 256 | 257 | def irc_sanitize(msg): 258 | """Remove NUL, CR and LF characters from msg. 259 | The (utf-8 encoded version of a) string returned from this function 260 | should be safe to use as an argument in an irc command.""" 261 | return PYVER.unicode(msg).translate(IRC_SANITIZE_TABLE) 262 | 263 | def prnt(buf, message): 264 | """Wrap weechat.prnt() with utf-8 encode.""" 265 | weechat.prnt(buf, PYVER.to_str(message)) 266 | 267 | def print_buffer(buf, message, level='info'): 268 | """Print message to buf with prefix, 269 | using color according to level.""" 270 | prnt(buf, '{prefix}\t{msg}'.format( 271 | prefix=get_prefix(), 272 | msg=colorize(message, 'buffer.{0}'.format(level)))) 273 | 274 | def get_prefix(): 275 | """Returns configured message prefix.""" 276 | return weechat.string_eval_expression( 277 | config_string('look.prefix'), 278 | {}, {}, {}) 279 | 280 | def debug(msg): 281 | """Send a debug message to the OTR debug buffer.""" 282 | debug_option = config_get_prefixed('general.debug') 283 | 284 | if not weechat.config_boolean(debug_option): 285 | return 286 | 287 | debug_buffer = weechat.buffer_search('python', 'OTR Debug') 288 | if not debug_buffer: 289 | debug_buffer = weechat.buffer_new('OTR Debug', '', '', '', '') 290 | weechat.buffer_set(debug_buffer, 'title', 'OTR Debug') 291 | weechat.buffer_set(debug_buffer, 'localvar_set_no_log', '1') 292 | 293 | prnt(debug_buffer, ('{script} debug\t{text}'.format( 294 | script=SCRIPT_NAME, text=PYVER.unicode(msg)))) 295 | 296 | def current_user(server_name): 297 | """Get the nick and server of the current user on a server.""" 298 | return irc_user(info_get('irc_nick', server_name), server_name) 299 | 300 | def irc_user(nick, server): 301 | """Build an IRC user string from a nick and server.""" 302 | return '{nick}@{server}'.format( 303 | nick=nick.lower(), 304 | server=server) 305 | 306 | def isupport_value(server, feature): 307 | """Get the value of an IRC server feature.""" 308 | args = '{server},{feature}'.format(server=server, feature=feature) 309 | return info_get('irc_server_isupport_value', args) 310 | 311 | def is_a_channel(channel, server): 312 | """Return true if a string has an IRC channel prefix.""" 313 | prefixes = \ 314 | tuple(isupport_value(server, 'CHANTYPES')) + \ 315 | tuple(isupport_value(server, 'STATUSMSG')) 316 | 317 | # If the server returns nothing for CHANTYPES and STATUSMSG use 318 | # default prefixes. 319 | if not prefixes: 320 | prefixes = ('#', '&', '+', '!', '@') 321 | 322 | return channel.startswith(prefixes) 323 | 324 | class PrivmsgParseException(Exception): 325 | """Exception class for PRIVMSG parsing exceptions.""" 326 | pass 327 | 328 | def parse_irc_privmsg(message, server): 329 | """Parse an IRC PRIVMSG command and return a dictionary. 330 | 331 | Either the to_channel key or the to_nick key will be set depending on 332 | whether the message is to a nick or a channel. The other will be None. 333 | 334 | Example input: 335 | 336 | :nick!user@host PRIVMSG #weechat :message here 337 | 338 | Output: 339 | 340 | {'from': 'nick!user@host', 341 | 'from_nick': 'nick', 342 | 'to': '#weechat', 343 | 'to_channel': '#weechat', 344 | 'to_nick': None, 345 | 'text': 'message here'} 346 | """ 347 | 348 | weechat_result = weechat.info_get_hashtable( 349 | 'irc_message_parse', dict(message=message)) 350 | 351 | if weechat_result['command'].upper() == 'PRIVMSG': 352 | target, text = PYVER.to_unicode( 353 | weechat_result['arguments']).split(' ', 1) 354 | if text.startswith(':'): 355 | text = text[1:] 356 | 357 | result = { 358 | 'from': PYVER.to_unicode(weechat_result['host']), 359 | 'to' : target, 360 | 'text': text, 361 | } 362 | 363 | if weechat_result['host']: 364 | result['from_nick'] = PYVER.to_unicode(weechat_result['nick']) 365 | else: 366 | result['from_nick'] = '' 367 | 368 | if is_a_channel(target, server): 369 | result['to_channel'] = target 370 | result['to_nick'] = None 371 | else: 372 | result['to_channel'] = None 373 | result['to_nick'] = target 374 | 375 | return result 376 | else: 377 | raise PrivmsgParseException(message) 378 | 379 | def has_otr_end(msg): 380 | """Return True if the message is the end of an OTR message.""" 381 | return msg.endswith('.') or msg.endswith(',') 382 | 383 | def first_instance(objs, klass): 384 | """Return the first object in the list that is an instance of a class.""" 385 | for obj in objs: 386 | if isinstance(obj, klass): 387 | return obj 388 | 389 | def config_prefix(option): 390 | """Add the config prefix to an option and return the full option name.""" 391 | return '{script}.{option}'.format( 392 | script=SCRIPT_NAME, 393 | option=option) 394 | 395 | def config_color(option): 396 | """Get the color of a color config option.""" 397 | return weechat.color(weechat.config_color(config_get_prefixed( 398 | 'color.{0}'.format(option)))) 399 | 400 | def config_string(option): 401 | """Get the string value of a config option with utf-8 decode.""" 402 | return PYVER.to_unicode(weechat.config_string( 403 | config_get_prefixed(option))) 404 | 405 | def config_get(option): 406 | """Get the value of a WeeChat config option.""" 407 | return weechat.config_get(PYVER.to_str(option)) 408 | 409 | def config_get_prefixed(option): 410 | """Get the value of a script prefixed WeeChat config option.""" 411 | return config_get(config_prefix(option)) 412 | 413 | def buffer_get_string(buf, prop): 414 | """Wrap weechat.buffer_get_string() with utf-8 encode/decode.""" 415 | if buf is not None: 416 | encoded_buf = PYVER.to_str(buf) 417 | else: 418 | encoded_buf = None 419 | 420 | return PYVER.to_unicode(weechat.buffer_get_string( 421 | encoded_buf, PYVER.to_str(prop))) 422 | 423 | def buffer_is_private(buf): 424 | """Return True if a buffer is private.""" 425 | return buffer_get_string(buf, 'localvar_type') == 'private' 426 | 427 | def info_get(info_name, arguments): 428 | """Wrap weechat.info_get() with utf-8 encode/decode.""" 429 | return PYVER.to_unicode(weechat.info_get( 430 | PYVER.to_str(info_name), PYVER.to_str(arguments))) 431 | 432 | def msg_irc_from_plain(msg): 433 | """Transform a plain-text message to irc format. 434 | This will replace lines that start with /me with the respective 435 | irc command.""" 436 | return PLAIN_ACTION_RE.sub('\x01ACTION \g\x01', msg) 437 | 438 | def msg_plain_from_irc(msg): 439 | """Transform an irc message to plain-text. 440 | Any ACTION found will be rewritten as /me .""" 441 | return IRC_ACTION_RE.sub(ACTION_PREFIX + r'\g', msg) 442 | 443 | def default_peer_args(args, buf): 444 | """Get the nick and server of a remote peer from command arguments or 445 | a buffer. 446 | 447 | args is the [nick, server] slice of arguments from a command. 448 | If these are present, return them. If args is empty and the buffer buf 449 | is private, return the remote nick and server of buf.""" 450 | result = None, None 451 | 452 | if len(args) == 2: 453 | result = tuple(args) 454 | else: 455 | if buffer_is_private(buf): 456 | result = ( 457 | buffer_get_string(buf, 'localvar_channel'), 458 | buffer_get_string(buf, 'localvar_server')) 459 | 460 | return result 461 | 462 | def format_default_policies(): 463 | """Return current default policies formatted as a string for the user.""" 464 | buf = io.StringIO() 465 | 466 | buf.write('Current default OTR policies:\n') 467 | 468 | for policy, desc in sorted(POLICIES.items()): 469 | buf.write(' {policy} ({desc}) : {value}\n'.format( 470 | policy=policy, 471 | desc=desc, 472 | value=config_string('policy.default.{0}'.format(policy)))) 473 | 474 | buf.write('Change default policies with: /otr policy default NAME on|off') 475 | 476 | return buf.getvalue() 477 | 478 | def to_bytes(strng): 479 | """Convert a python str or unicode to bytes.""" 480 | return strng.encode('utf-8', 'replace') 481 | 482 | def colorize(msg, color): 483 | """Colorize each line of msg using color.""" 484 | result = [] 485 | colorstr = config_color(color) 486 | 487 | for line in msg.splitlines(): 488 | result.append('{color}{msg}'.format( 489 | color=colorstr, 490 | msg=line)) 491 | 492 | return '\r\n'.join(result) 493 | 494 | def accounts(): 495 | """Return a list of all IrcOtrAccounts sorted by name.""" 496 | result = [] 497 | for key_path in glob.iglob(os.path.join(OTR_DIR, '*.key3')): 498 | key_name, _ = os.path.splitext(os.path.basename(key_path)) 499 | result.append(ACCOUNTS[key_name]) 500 | 501 | return sorted(result, key=lambda account: account.name) 502 | 503 | def show_account_fingerprints(): 504 | """Print all account names and their fingerprints to the core buffer.""" 505 | table_formatter = TableFormatter() 506 | for account in accounts(): 507 | table_formatter.add_row([ 508 | account.name, 509 | str(account.getPrivkey())]) 510 | print_buffer('', table_formatter.format()) 511 | 512 | def show_peer_fingerprints(grep=None): 513 | """Print peer names and their fingerprints to the core buffer. 514 | 515 | If grep is passed in, show all peer names containing that substring.""" 516 | trust_descs = { 517 | '' : 'unverified', 518 | 'smp' : 'SMP verified', 519 | 'verified' : 'verified', 520 | } 521 | 522 | table_formatter = TableFormatter() 523 | for account in accounts(): 524 | for peer, peer_data in sorted(account.trusts.items()): 525 | for fingerprint, trust in sorted(peer_data.items()): 526 | if grep is None or grep in peer: 527 | table_formatter.add_row([ 528 | peer, 529 | account.name, 530 | potr.human_hash(fingerprint), 531 | trust_descs[trust], 532 | ]) 533 | print_buffer('', table_formatter.format()) 534 | 535 | def private_key_file_path(account_name): 536 | """Return the private key file path for an account.""" 537 | return os.path.join(OTR_DIR, '{0}.key3'.format(account_name)) 538 | 539 | def read_private_key(key_file_path): 540 | """Return the private key in a private key file.""" 541 | debug(('read private key', key_file_path)) 542 | 543 | with open(key_file_path, 'rb') as key_file: 544 | return potr.crypt.PK.parsePrivateKey(key_file.read())[0] 545 | 546 | def get_context(account_name, context_name): 547 | """Return a context from an account.""" 548 | return ACCOUNTS[account_name].getContext(context_name) 549 | 550 | def get_server_context(server, peer_nick): 551 | """Return the context for the current server user and peer.""" 552 | return get_context(current_user(server), irc_user(peer_nick, server)) 553 | 554 | class AccountDict(collections.defaultdict): 555 | """Dictionary that adds missing keys as IrcOtrAccount instances.""" 556 | 557 | def __missing__(self, key): 558 | debug(('add account', key)) 559 | self[key] = IrcOtrAccount(key) 560 | 561 | return self[key] 562 | 563 | class Assembler(object): 564 | """Reassemble fragmented OTR messages. 565 | 566 | This does not deal with OTR fragmentation, which is handled by potr, but 567 | fragmentation of received OTR messages that are too large for IRC. 568 | """ 569 | def __init__(self): 570 | self.clear() 571 | 572 | def add(self, data): 573 | """Add data to the buffer.""" 574 | self.value += data 575 | 576 | def clear(self): 577 | """Empty the buffer.""" 578 | self.value = '' 579 | 580 | def is_done(self): 581 | """Return True if the buffer is a complete message.""" 582 | return self.is_query() or \ 583 | not to_bytes(self.value).startswith(potr.proto.OTRTAG) or \ 584 | has_otr_end(self.value) 585 | 586 | def get(self): 587 | """Return the current value of the buffer and empty it.""" 588 | result = self.value 589 | self.clear() 590 | 591 | return result 592 | 593 | def is_query(self): 594 | """Return true if the buffer is an OTR query.""" 595 | return OTR_QUERY_RE.search(self.value) 596 | 597 | class IrcContext(potr.context.Context): 598 | """Context class for OTR over IRC.""" 599 | 600 | def __init__(self, account, peername): 601 | super(IrcContext, self).__init__(account, peername) 602 | 603 | self.peer_nick, self.peer_server = peername.split('@', 1) 604 | self.in_assembler = Assembler() 605 | self.in_otr_message = False 606 | self.in_smp = False 607 | self.smp_question = False 608 | 609 | def policy_config_option(self, policy): 610 | """Get the option name of a policy option for this context.""" 611 | return config_prefix('.'.join([ 612 | 'policy', self.peer_server, self.user.nick, self.peer_nick, 613 | policy.lower()])) 614 | 615 | def getPolicy(self, key): 616 | """Get the value of a policy option for this context.""" 617 | key_lower = key.lower() 618 | 619 | if key_lower in READ_ONLY_POLICIES: 620 | result = READ_ONLY_POLICIES[key_lower] 621 | elif key_lower == 'send_tag' and self.no_send_tag(): 622 | result = False 623 | else: 624 | option = config_get(self.policy_config_option(key)) 625 | 626 | if option == '': 627 | option = config_get(self.user.policy_config_option(key)) 628 | 629 | if option == '': 630 | option = config_get_prefixed('.'.join( 631 | ['policy', self.peer_server, key_lower])) 632 | 633 | if option == '': 634 | option = config_get_prefixed( 635 | 'policy.default.{0}'.format(key_lower)) 636 | 637 | result = bool(weechat.config_boolean(option)) 638 | 639 | debug(('getPolicy', key, result)) 640 | 641 | return result 642 | 643 | def inject(self, msg, appdata=None): 644 | """Send a message to the remote peer.""" 645 | if isinstance(msg, potr.proto.OTRMessage): 646 | msg = PYVER.unicode(msg) 647 | else: 648 | msg = PYVER.to_unicode(msg) 649 | 650 | debug(('inject', msg, 'len {0}'.format(len(msg)), appdata)) 651 | 652 | privmsg(self.peer_server, self.peer_nick, msg) 653 | 654 | def setState(self, newstate): 655 | """Handle state transition.""" 656 | debug(('state', self.state, newstate)) 657 | 658 | if self.is_encrypted(): 659 | if newstate == potr.context.STATE_ENCRYPTED: 660 | self.print_buffer( 661 | 'Private conversation has been refreshed.', 'success') 662 | elif newstate == potr.context.STATE_FINISHED: 663 | self.print_buffer( 664 | '{peer} has ended the private conversation. You should do ' 665 | 'the same:\n/otr finish'.format(peer=self.peer_nick)) 666 | elif newstate == potr.context.STATE_ENCRYPTED: 667 | # unencrypted => encrypted 668 | trust = self.getCurrentTrust() 669 | 670 | # Disable logging before any proof of OTR activity is generated. 671 | # This is necessary when the session is started automatically, and 672 | # not by /otr start. 673 | if not self.getPolicy('log'): 674 | self.previous_log_level = self.disable_logging() 675 | else: 676 | self.previous_log_level = self.get_log_level() 677 | if self.is_logged(): 678 | self.hint( 679 | 'You have enabled the recording to disk of OTR ' 680 | 'conversations. By doing this you are potentially ' 681 | 'putting yourself and your correspondent in danger. ' 682 | 'Please consider disabling this policy with ' 683 | '"/otr policy default log off". To disable logging ' 684 | 'for this OTR session, use "/otr log stop"') 685 | 686 | if trust is None: 687 | fpr = str(self.getCurrentKey()) 688 | self.print_buffer('New fingerprint: {0}'.format(fpr), 'warning') 689 | self.setCurrentTrust('') 690 | 691 | if bool(trust): 692 | self.print_buffer( 693 | 'Authenticated secured OTR conversation started.', 694 | 'success') 695 | else: 696 | self.print_buffer( 697 | 'Unauthenticated secured OTR conversation started.', 698 | 'warning') 699 | self.hint(self.verify_instructions()) 700 | 701 | if self.state != potr.context.STATE_PLAINTEXT and \ 702 | newstate == potr.context.STATE_PLAINTEXT: 703 | self.print_buffer('Private conversation ended.') 704 | 705 | # If we altered the logging value, restore it. 706 | if self.previous_log_level is not None: 707 | self.restore_logging(self.previous_log_level) 708 | 709 | super(IrcContext, self).setState(newstate) 710 | 711 | def maxMessageSize(self, appdata=None): 712 | """Return the max message size for this context.""" 713 | # remove 'PRIVMSG :' from max message size 714 | result = self.user.maxMessageSize - 10 - len(self.peer_nick) 715 | debug('max message size {0}'.format(result)) 716 | 717 | return result 718 | 719 | def buffer(self): 720 | """Get the buffer for this context.""" 721 | return info_get( 722 | 'irc_buffer', '{server},{nick}'.format( 723 | server=self.peer_server, 724 | nick=self.peer_nick 725 | )) 726 | 727 | def print_buffer(self, msg, level='info'): 728 | """Print a message to the buffer for this context. 729 | level is used to colorize the message.""" 730 | buf = self.buffer() 731 | 732 | # add [nick] prefix if we have only a server buffer for the query 733 | if self.peer_nick and not buffer_is_private(buf): 734 | msg = '[{nick}] {msg}'.format( 735 | nick=self.peer_nick, 736 | msg=msg) 737 | 738 | print_buffer(buf, msg, level) 739 | 740 | def hint(self, msg): 741 | """Print a message to the buffer but only when hints are enabled.""" 742 | hints_option = config_get_prefixed('general.hints') 743 | 744 | if weechat.config_boolean(hints_option): 745 | self.print_buffer(msg, 'hint') 746 | 747 | def smp_finish(self, message=False, level='info'): 748 | """Reset SMP state and send a message to the user.""" 749 | self.in_smp = False 750 | self.smp_question = False 751 | 752 | self.user.saveTrusts() 753 | if message: 754 | self.print_buffer(message, level) 755 | 756 | def handle_tlvs(self, tlvs): 757 | """Handle SMP states.""" 758 | if tlvs: 759 | smp1q = first_instance(tlvs, potr.proto.SMP1QTLV) 760 | smp3 = first_instance(tlvs, potr.proto.SMP3TLV) 761 | smp4 = first_instance(tlvs, potr.proto.SMP4TLV) 762 | 763 | if first_instance(tlvs, potr.proto.SMPABORTTLV): 764 | debug('SMP aborted by peer') 765 | self.smp_finish('SMP aborted by peer.', 'warning') 766 | elif self.in_smp and not self.smpIsValid(): 767 | debug('SMP aborted') 768 | self.smp_finish('SMP aborted.', 'error') 769 | elif first_instance(tlvs, potr.proto.SMP1TLV): 770 | debug('SMP1') 771 | self.in_smp = True 772 | 773 | self.print_buffer( 774 | """Peer has requested SMP verification. 775 | Respond with: /otr smp respond """) 776 | elif smp1q: 777 | debug(('SMP1Q', smp1q.msg)) 778 | self.in_smp = True 779 | self.smp_question = True 780 | 781 | self.print_buffer( 782 | """Peer has requested SMP verification: {msg} 783 | Respond with: /otr smp respond """.format( 784 | msg=PYVER.to_unicode(smp1q.msg))) 785 | elif first_instance(tlvs, potr.proto.SMP2TLV): 786 | if not self.in_smp: 787 | debug('Received unexpected SMP2') 788 | self.smp_finish() 789 | else: 790 | debug('SMP2') 791 | self.print_buffer('SMP progressing.') 792 | elif smp3 or smp4: 793 | if smp3: 794 | debug('SMP3') 795 | elif smp4: 796 | debug('SMP4') 797 | 798 | if self.smpIsSuccess(): 799 | 800 | if self.smp_question: 801 | self.smp_finish( 802 | 'SMP verification succeeded.', 'success') 803 | if not self.is_verified: 804 | self.print_buffer( 805 | 'You may want to authenticate your peer by ' 806 | 'asking your own question:\n' 807 | "/otr smp ask <'question'> 'secret'") 808 | else: 809 | self.smp_finish( 810 | 'SMP verification succeeded.', 'success') 811 | 812 | else: 813 | self.smp_finish('SMP verification failed.', 'error') 814 | 815 | def verify_instructions(self): 816 | """Generate verification instructions for user.""" 817 | return """You can verify that this contact is who they claim to be in 818 | one of the following ways: 819 | 820 | 1) Verify each other's fingerprints using a secure channel: 821 | Your fingerprint : {your_fp} 822 | {peer}'s fingerprint : {peer_fp} 823 | then use the command: /otr trust {peer_nick} {peer_server} 824 | 825 | 2) SMP pre-shared secret that you both know: 826 | /otr smp ask {peer_nick} {peer_server} 'secret' 827 | 828 | 3) SMP pre-shared secret that you both know with a question: 829 | /otr smp ask {peer_nick} {peer_server} <'question'> 'secret' 830 | 831 | Note: You can safely omit specifying the peer and server when 832 | executing these commands from the appropriate conversation 833 | buffer 834 | """.format( 835 | your_fp=self.user.getPrivkey(), 836 | peer=self.peer, 837 | peer_nick=self.peer_nick, 838 | peer_server=self.peer_server, 839 | peer_fp=potr.human_hash(self.crypto.theirPubkey.cfingerprint())) 840 | 841 | def is_encrypted(self): 842 | """Return True if the conversation with this context's peer is 843 | currently encrypted.""" 844 | return self.state == potr.context.STATE_ENCRYPTED 845 | 846 | def is_verified(self): 847 | """Return True if this context's peer is verified.""" 848 | return bool(self.getCurrentTrust()) 849 | 850 | def format_policies(self): 851 | """Return current policies for this context formatted as a string for 852 | the user.""" 853 | buf = io.StringIO() 854 | 855 | buf.write('Current OTR policies for {peer}:\n'.format( 856 | peer=self.peer)) 857 | 858 | for policy, desc in sorted(POLICIES.items()): 859 | buf.write(' {policy} ({desc}) : {value}\n'.format( 860 | policy=policy, 861 | desc=desc, 862 | value='on' if self.getPolicy(policy) else 'off')) 863 | 864 | buf.write('Change policies with: /otr policy NAME on|off') 865 | 866 | return buf.getvalue() 867 | 868 | def is_logged(self): 869 | """Return True if conversations with this context's peer are currently 870 | being logged to disk.""" 871 | infolist = weechat.infolist_get('logger_buffer', '', '') 872 | 873 | buf = self.buffer() 874 | 875 | result = False 876 | 877 | while weechat.infolist_next(infolist): 878 | if weechat.infolist_pointer(infolist, 'buffer') == buf: 879 | result = bool(weechat.infolist_integer(infolist, 'log_enabled')) 880 | break 881 | 882 | weechat.infolist_free(infolist) 883 | 884 | return result 885 | 886 | def get_log_level(self): 887 | """Return the current logging level for this context's peer 888 | or -1 if the buffer uses the default log level of weechat.""" 889 | infolist = weechat.infolist_get('logger_buffer', '', '') 890 | 891 | buf = self.buffer() 892 | 893 | if not config_get(self.get_logger_option_name()): 894 | result = -1 895 | else: 896 | result = 0 897 | 898 | while weechat.infolist_next(infolist): 899 | if weechat.infolist_pointer(infolist, 'buffer') == buf: 900 | result = weechat.infolist_integer(infolist, 'log_level') 901 | break 902 | 903 | weechat.infolist_free(infolist) 904 | 905 | return result 906 | 907 | def get_logger_option_name(self): 908 | """Returns the logger config option for this context's buffer.""" 909 | buf = self.buffer() 910 | name = buffer_get_string(buf, 'name') 911 | plugin = buffer_get_string(buf, 'plugin') 912 | 913 | return 'logger.level.{plugin}.{name}'.format( 914 | plugin=plugin, name=name) 915 | 916 | def disable_logging(self): 917 | """Return the previous logger level and set the buffer logger level 918 | to 0. If it was already 0, return None.""" 919 | # If previous_log_level has not been previously set, return the level 920 | # we detect now. 921 | if not hasattr(self, 'previous_log_level'): 922 | previous_log_level = self.get_log_level() 923 | 924 | if self.is_logged(): 925 | weechat.command(self.buffer(), '/mute logger disable') 926 | self.print_buffer( 927 | 'Logs have been temporarily disabled for the session. ' 928 | 'They will be restored upon finishing the OTR session.') 929 | 930 | return previous_log_level 931 | 932 | # If previous_log_level was already set, it means we already altered it 933 | # and that we just detected an already modified logging level. 934 | # Return the pre-existing value so it doesn't get lost, and we can 935 | # restore it later. 936 | else: 937 | return self.previous_log_level 938 | 939 | def restore_logging(self, previous_log_level): 940 | """Restore the log level of the buffer.""" 941 | buf = self.buffer() 942 | 943 | if (previous_log_level >= 0) and (previous_log_level < 10): 944 | self.print_buffer( 945 | 'Restoring buffer logging value to: {0}'.format( 946 | previous_log_level), 'warning') 947 | weechat.command(buf, '/mute logger set {0}'.format( 948 | previous_log_level)) 949 | 950 | if previous_log_level == -1: 951 | logger_option_name = self.get_logger_option_name() 952 | self.print_buffer( 953 | 'Restoring buffer logging value to default', 'warning') 954 | weechat.command(buf, '/mute unset {0}'.format( 955 | logger_option_name)) 956 | 957 | del self.previous_log_level 958 | 959 | def msg_convert_in(self, msg): 960 | """Transform incoming OTR message to IRC format. 961 | This includes stripping html, converting plain-text ACTIONs 962 | and character encoding conversion. 963 | Only character encoding is changed if context is unencrypted.""" 964 | msg = PYVER.to_unicode(msg) 965 | 966 | if not self.is_encrypted(): 967 | return msg 968 | 969 | if self.getPolicy('html_filter'): 970 | try: 971 | msg = IrcHTMLParser.parse(msg) 972 | except PYVER.html_parser.HTMLParseError: 973 | pass 974 | 975 | return msg_irc_from_plain(msg) 976 | 977 | def msg_convert_out(self, msg): 978 | """Convert an outgoing IRC message to be sent over OTR. 979 | This includes escaping html, converting ACTIONs to plain-text 980 | and character encoding conversion 981 | Only character encoding is changed if context is unencrypted.""" 982 | if self.is_encrypted(): 983 | msg = msg_plain_from_irc(msg) 984 | 985 | if self.getPolicy('html_escape'): 986 | msg = PYVER.html_escape(msg) 987 | 988 | # potr expects bytes to be returned 989 | return to_bytes(msg) 990 | 991 | def no_send_tag(self): 992 | """Skip OTR whitespace tagging to bots and services. 993 | 994 | Any nicks matching the otr.general.no_send_tag_regex config setting 995 | will not be tagged. 996 | """ 997 | no_send_tag_regex = config_string('general.no_send_tag_regex') 998 | debug(('no_send_tag', no_send_tag_regex, self.peer_nick)) 999 | if no_send_tag_regex: 1000 | return re.match(no_send_tag_regex, self.peer_nick, re.IGNORECASE) 1001 | 1002 | def __repr__(self): 1003 | return PYVER.to_str(( 1004 | '<{0} {1:x} peer_nick={c.peer_nick} peer_server={c.peer_server}>' 1005 | ).format(self.__class__.__name__, id(self), c=self)) 1006 | 1007 | class IrcOtrAccount(potr.context.Account): 1008 | """Account class for OTR over IRC.""" 1009 | 1010 | contextclass = IrcContext 1011 | 1012 | PROTOCOL = 'irc' 1013 | MAX_MSG_SIZE = 415 1014 | 1015 | def __init__(self, name): 1016 | super(IrcOtrAccount, self).__init__( 1017 | name, IrcOtrAccount.PROTOCOL, IrcOtrAccount.MAX_MSG_SIZE) 1018 | 1019 | self.nick, self.server = self.name.split('@', 1) 1020 | 1021 | # IRC messages cannot have newlines, OTR query and "no plugin" text 1022 | # need to be one message 1023 | self.defaultQuery = self.defaultQuery.replace("\n", ' ') 1024 | 1025 | self.key_file_path = private_key_file_path(name) 1026 | self.fpr_file_path = os.path.join(OTR_DIR, '{0}.fpr'.format(name)) 1027 | 1028 | self.load_trusts() 1029 | 1030 | def load_trusts(self): 1031 | """Load trust data from the fingerprint file.""" 1032 | if os.path.exists(self.fpr_file_path): 1033 | with open(self.fpr_file_path) as fpr_file: 1034 | for line in fpr_file: 1035 | debug(('load trust check', line)) 1036 | 1037 | context, account, protocol, fpr, trust = \ 1038 | PYVER.to_unicode(line[:-1]).split('\t') 1039 | 1040 | if account == self.name and \ 1041 | protocol == IrcOtrAccount.PROTOCOL: 1042 | debug(('set trust', context, fpr, trust)) 1043 | self.setTrust(context, fpr, trust) 1044 | 1045 | def loadPrivkey(self): 1046 | """Load key file. 1047 | 1048 | If no key file exists, load the default key. If there is no default 1049 | key, a new key will be generated automatically by potr.""" 1050 | debug(('load private key', self.key_file_path)) 1051 | 1052 | if os.path.exists(self.key_file_path): 1053 | return read_private_key(self.key_file_path) 1054 | else: 1055 | default_key = config_string('general.defaultkey') 1056 | if default_key: 1057 | default_key_path = private_key_file_path(default_key) 1058 | 1059 | if os.path.exists(default_key_path): 1060 | shutil.copyfile(default_key_path, self.key_file_path) 1061 | return read_private_key(self.key_file_path) 1062 | 1063 | def savePrivkey(self): 1064 | """Save key file.""" 1065 | debug(('save private key', self.key_file_path)) 1066 | 1067 | with open(self.key_file_path, 'wb') as key_file: 1068 | key_file.write(self.getPrivkey().serializePrivateKey()) 1069 | 1070 | def saveTrusts(self): 1071 | """Save trusts.""" 1072 | with open(self.fpr_file_path, 'w') as fpr_file: 1073 | for uid, trusts in self.trusts.items(): 1074 | for fpr, trust in trusts.items(): 1075 | debug(('trust write', uid, self.name, 1076 | IrcOtrAccount.PROTOCOL, fpr, trust)) 1077 | fpr_file.write(PYVER.to_str('\t'.join( 1078 | (uid, self.name, IrcOtrAccount.PROTOCOL, fpr, trust)))) 1079 | fpr_file.write('\n') 1080 | 1081 | def end_all_private(self): 1082 | """End all currently encrypted conversations.""" 1083 | for context in self.ctxs.values(): 1084 | if context.is_encrypted(): 1085 | context.disconnect() 1086 | 1087 | def policy_config_option(self, policy): 1088 | """Get the option name of a policy option for this account.""" 1089 | return config_prefix('.'.join([ 1090 | 'policy', self.server, self.nick, policy.lower()])) 1091 | 1092 | class IrcHTMLParser(PYVER.html_parser.HTMLParser): 1093 | """A simple HTML parser that throws away anything but newlines and links""" 1094 | 1095 | @staticmethod 1096 | def parse(data): 1097 | """Create a temporary IrcHTMLParser and parse a single string""" 1098 | parser = IrcHTMLParser(**PYVER.html_parser_init_kwargs) 1099 | parser.feed(data) 1100 | parser.close() 1101 | return parser.result 1102 | 1103 | def reset(self): 1104 | """Forget all state, called from __init__""" 1105 | PYVER.html_parser.HTMLParser.reset(self) 1106 | self.result = '' 1107 | self.linktarget = '' 1108 | self.linkstart = 0 1109 | 1110 | def handle_starttag(self, tag, attrs): 1111 | """Called when a start tag is encountered""" 1112 | if tag == 'br': 1113 | self.result += '\n' 1114 | elif tag == 'a': 1115 | attrs = dict(attrs) 1116 | if 'href' in attrs: 1117 | self.result += '[' 1118 | self.linktarget = attrs['href'] 1119 | self.linkstart = len(self.result) 1120 | 1121 | def handle_endtag(self, tag): 1122 | """Called when an end tag is encountered""" 1123 | if tag == 'a': 1124 | if self.linktarget: 1125 | if self.result[self.linkstart:] == self.linktarget: 1126 | self.result += ']' 1127 | else: 1128 | self.result += ']({0})'.format(self.linktarget) 1129 | self.linktarget = '' 1130 | 1131 | def handle_data(self, data): 1132 | """Called for character data (i.e. text)""" 1133 | self.result += data 1134 | 1135 | def handle_entityref(self, name): 1136 | """Called for entity references, such as &""" 1137 | try: 1138 | self.result += PYVER.unichr( 1139 | PYVER.html_entities.name2codepoint[name]) 1140 | except KeyError: 1141 | self.result += '&{0};'.format(name) 1142 | 1143 | def handle_charref(self, name): 1144 | """Called for character references, such as '""" 1145 | try: 1146 | if name.startswith('x'): 1147 | self.result += PYVER.unichr(int(name[1:], 16)) 1148 | else: 1149 | self.result += PYVER.unichr(int(name)) 1150 | except ValueError: 1151 | self.result += '&#{0};'.format(name) 1152 | 1153 | class TableFormatter(object): 1154 | """Format lists of string into aligned tables.""" 1155 | 1156 | def __init__(self): 1157 | self.rows = [] 1158 | self.max_widths = None 1159 | 1160 | def add_row(self, row): 1161 | """Add a row to the table.""" 1162 | self.rows.append(row) 1163 | row_widths = [len(s) for s in row] 1164 | if self.max_widths is None: 1165 | self.max_widths = row_widths 1166 | else: 1167 | self.max_widths = list(map(max, self.max_widths, row_widths)) 1168 | 1169 | def format(self): 1170 | """Return the formatted table as a string.""" 1171 | return '\n'.join([self.format_row(row) for row in self.rows]) 1172 | 1173 | def format_row(self, row): 1174 | """Format a single row as a string.""" 1175 | return ' |'.join( 1176 | [s.ljust(self.max_widths[i]) for i, s in enumerate(row)]) 1177 | 1178 | def message_in_cb(data, modifier, modifier_data, string): 1179 | """Incoming message callback""" 1180 | debug(('message_in_cb', data, modifier, modifier_data, string)) 1181 | 1182 | parsed = parse_irc_privmsg( 1183 | PYVER.to_unicode(string), PYVER.to_unicode(modifier_data)) 1184 | debug(('parsed message', parsed)) 1185 | 1186 | # skip processing messages to public channels 1187 | if parsed['to_channel']: 1188 | return string 1189 | 1190 | server = PYVER.to_unicode(modifier_data) 1191 | 1192 | context = get_server_context(server, parsed['from_nick']) 1193 | 1194 | context.in_assembler.add(parsed['text']) 1195 | 1196 | result = '' 1197 | 1198 | if context.in_assembler.is_done(): 1199 | try: 1200 | msg, tlvs = context.receiveMessage( 1201 | # potr expects bytes 1202 | to_bytes(context.in_assembler.get())) 1203 | 1204 | debug(('receive', msg, tlvs)) 1205 | 1206 | if msg: 1207 | result = PYVER.to_str(build_privmsgs_in( 1208 | parsed['from'], parsed['to'], 1209 | context.msg_convert_in(msg))) 1210 | 1211 | context.handle_tlvs(tlvs) 1212 | except potr.context.ErrorReceived as err: 1213 | context.print_buffer('Received OTR error: {0}'.format( 1214 | PYVER.to_unicode(err.args[0])), 'error') 1215 | except potr.context.NotEncryptedError: 1216 | context.print_buffer( 1217 | 'Received encrypted data but no private session established.', 1218 | 'warning') 1219 | except potr.context.NotOTRMessage: 1220 | result = string 1221 | except potr.context.UnencryptedMessage as err: 1222 | result = PYVER.to_str(build_privmsgs_in( 1223 | parsed['from'], parsed['to'], PYVER.to_unicode( 1224 | msg_plain_from_irc(err.args[0])), 1225 | 'Unencrypted message received: ')) 1226 | 1227 | weechat.bar_item_update(SCRIPT_NAME) 1228 | 1229 | return result 1230 | 1231 | def message_out_cb(data, modifier, modifier_data, string): 1232 | """Outgoing message callback.""" 1233 | result = '' 1234 | 1235 | # If any exception is raised in this function, WeeChat will not send the 1236 | # outgoing message, which could be something that the user intended to be 1237 | # encrypted. This paranoid exception handling ensures that the system 1238 | # fails closed and not open. 1239 | try: 1240 | debug(('message_out_cb', data, modifier, modifier_data, string)) 1241 | 1242 | parsed = parse_irc_privmsg( 1243 | PYVER.to_unicode(string), PYVER.to_unicode(modifier_data)) 1244 | debug(('parsed message', parsed)) 1245 | 1246 | # skip processing messages to public channels 1247 | if parsed['to_channel']: 1248 | return string 1249 | 1250 | server = PYVER.to_unicode(modifier_data) 1251 | 1252 | context = get_server_context(server, parsed['to_nick']) 1253 | is_query = OTR_QUERY_RE.search(parsed['text']) 1254 | 1255 | parsed_text_bytes = to_bytes(parsed['text']) 1256 | 1257 | is_otr_message = \ 1258 | parsed_text_bytes[:len(potr.proto.OTRTAG)] == potr.proto.OTRTAG 1259 | 1260 | if is_otr_message and not is_query: 1261 | if not has_otr_end(parsed['text']): 1262 | debug('in OTR message') 1263 | context.in_otr_message = True 1264 | else: 1265 | debug('complete OTR message') 1266 | result = string 1267 | elif context.in_otr_message: 1268 | if has_otr_end(parsed['text']): 1269 | context.in_otr_message = False 1270 | debug('in OTR message end') 1271 | result = string 1272 | else: 1273 | debug(('context send message', parsed['text'], parsed['to_nick'], 1274 | server)) 1275 | 1276 | if context.policyOtrEnabled() and \ 1277 | not context.is_encrypted() and \ 1278 | not is_query and \ 1279 | context.getPolicy('require_encryption'): 1280 | context.print_buffer( 1281 | 'Your message will not be sent, because policy requires an ' 1282 | 'encrypted connection.', 'error') 1283 | context.hint( 1284 | 'Wait for the OTR connection or change the policy to allow ' 1285 | 'clear-text messages:\n' 1286 | '/otr policy require_encryption off') 1287 | 1288 | try: 1289 | ret = context.sendMessage( 1290 | potr.context.FRAGMENT_SEND_ALL, 1291 | context.msg_convert_out(parsed['text'])) 1292 | 1293 | if ret: 1294 | debug(('sendMessage returned', ret)) 1295 | result = PYVER.to_str( 1296 | build_privmsg_out( 1297 | parsed['to_nick'], PYVER.to_unicode(ret) 1298 | )) 1299 | 1300 | except potr.context.NotEncryptedError as err: 1301 | if err.args[0] == potr.context.EXC_FINISHED: 1302 | context.print_buffer( 1303 | 'Your message was not sent. End your private ' 1304 | 'conversation:\n/otr finish', 1305 | 'error') 1306 | else: 1307 | raise 1308 | 1309 | weechat.bar_item_update(SCRIPT_NAME) 1310 | # pylint: disable=bare-except 1311 | except: 1312 | try: 1313 | print_buffer('', traceback.format_exc(), 'error') 1314 | print_buffer('', 'Versions: {versions}'.format( 1315 | versions=dependency_versions()), 'error') 1316 | context.print_buffer( 1317 | 'Failed to send message. See core buffer for traceback.', 1318 | 'error') 1319 | # pylint: disable=bare-except 1320 | except: 1321 | pass 1322 | 1323 | return result 1324 | 1325 | def shutdown(): 1326 | """Script unload callback.""" 1327 | debug('shutdown') 1328 | 1329 | weechat.config_write(CONFIG_FILE) 1330 | 1331 | for account in ACCOUNTS.values(): 1332 | account.end_all_private() 1333 | 1334 | free_all_config() 1335 | 1336 | weechat.bar_item_remove(OTR_STATUSBAR) 1337 | 1338 | return weechat.WEECHAT_RC_OK 1339 | 1340 | def command_cb(data, buf, args): 1341 | """Parse and dispatch WeeChat OTR commands.""" 1342 | result = weechat.WEECHAT_RC_ERROR 1343 | 1344 | arg_parts = [PYVER.to_unicode(arg) for arg in shlex.split(args)] 1345 | 1346 | if len(arg_parts) in (1, 3) and arg_parts[0] in ('start', 'refresh'): 1347 | nick, server = default_peer_args(arg_parts[1:3], buf) 1348 | 1349 | if nick is not None and server is not None: 1350 | context = get_server_context(server, nick) 1351 | # We need to wall disable_logging() here so that no OTR-related 1352 | # buffer messages get logged at any point. disable_logging() will 1353 | # be called again when effectively switching to encrypted, but 1354 | # the previous_log_level we set here will be preserved for later 1355 | # restoring. 1356 | if not context.getPolicy('log'): 1357 | context.previous_log_level = context.disable_logging() 1358 | else: 1359 | context.previous_log_level = context.get_log_level() 1360 | 1361 | context.hint( 1362 | 'Sending OTR query... Please await confirmation of the OTR ' 1363 | 'session being started before sending a message.') 1364 | if not context.getPolicy('send_tag'): 1365 | context.hint( 1366 | 'To try OTR on all conversations with {peer}: /otr ' 1367 | 'policy send_tag on'.format(peer=context.peer)) 1368 | 1369 | privmsg(server, nick, '?OTR?') 1370 | 1371 | result = weechat.WEECHAT_RC_OK 1372 | elif len(arg_parts) in (1, 3) and arg_parts[0] in ('finish', 'end'): 1373 | nick, server = default_peer_args(arg_parts[1:3], buf) 1374 | 1375 | if nick is not None and server is not None: 1376 | context = get_server_context(server, nick) 1377 | context.disconnect() 1378 | 1379 | result = weechat.WEECHAT_RC_OK 1380 | 1381 | elif len(arg_parts) in (1, 3) and arg_parts[0] == 'status': 1382 | nick, server = default_peer_args(arg_parts[1:3], buf) 1383 | 1384 | if nick is not None and server is not None: 1385 | context = get_server_context(server, nick) 1386 | if context.is_encrypted(): 1387 | context.print_buffer( 1388 | 'This conversation is encrypted.', 'success') 1389 | context.print_buffer("Your fingerprint is: {0}".format( 1390 | context.user.getPrivkey())) 1391 | context.print_buffer("Your peer's fingerprint is: {0}".format( 1392 | potr.human_hash(context.crypto.theirPubkey.cfingerprint()))) 1393 | if context.is_verified(): 1394 | context.print_buffer( 1395 | "The peer's identity has been verified.", 1396 | 'success') 1397 | else: 1398 | context.print_buffer( 1399 | "You have not verified the peer's identity yet.", 1400 | 'warning') 1401 | else: 1402 | context.print_buffer( 1403 | "This current conversation is not encrypted.", 1404 | 'warning') 1405 | 1406 | result = weechat.WEECHAT_RC_OK 1407 | 1408 | elif len(arg_parts) in range(2, 7) and arg_parts[0] == 'smp': 1409 | action = arg_parts[1] 1410 | 1411 | if action == 'respond': 1412 | # Check if nickname and server are specified 1413 | if len(arg_parts) == 3: 1414 | nick, server = default_peer_args([], buf) 1415 | secret = arg_parts[2] 1416 | elif len(arg_parts) == 5: 1417 | nick, server = default_peer_args(arg_parts[2:4], buf) 1418 | secret = arg_parts[4] 1419 | else: 1420 | return weechat.WEECHAT_RC_ERROR 1421 | 1422 | if secret: 1423 | secret = PYVER.to_str(secret) 1424 | 1425 | context = get_server_context(server, nick) 1426 | context.smpGotSecret(secret) 1427 | 1428 | result = weechat.WEECHAT_RC_OK 1429 | 1430 | elif action == 'ask': 1431 | question = None 1432 | secret = None 1433 | 1434 | # Nickname and server are not specified 1435 | # Check whether it's a simple challenge or a question/answer request 1436 | if len(arg_parts) == 3: 1437 | nick, server = default_peer_args([], buf) 1438 | secret = arg_parts[2] 1439 | elif len(arg_parts) == 4: 1440 | nick, server = default_peer_args([], buf) 1441 | secret = arg_parts[3] 1442 | question = arg_parts[2] 1443 | 1444 | # Nickname and server are specified 1445 | # Check whether it's a simple challenge or a question/answer request 1446 | elif len(arg_parts) == 5: 1447 | nick, server = default_peer_args(arg_parts[2:4], buf) 1448 | secret = arg_parts[4] 1449 | elif len(arg_parts) == 6: 1450 | nick, server = default_peer_args(arg_parts[2:4], buf) 1451 | secret = arg_parts[5] 1452 | question = arg_parts[4] 1453 | else: 1454 | return weechat.WEECHAT_RC_ERROR 1455 | 1456 | context = get_server_context(server, nick) 1457 | 1458 | if secret: 1459 | secret = PYVER.to_str(secret) 1460 | if question: 1461 | question = PYVER.to_str(question) 1462 | 1463 | try: 1464 | context.smpInit(secret, question) 1465 | except potr.context.NotEncryptedError: 1466 | context.print_buffer( 1467 | 'There is currently no encrypted session with {0}.'.format( 1468 | context.peer), 'error') 1469 | else: 1470 | if question: 1471 | context.print_buffer('SMP challenge sent...') 1472 | else: 1473 | context.print_buffer('SMP question sent...') 1474 | context.in_smp = True 1475 | result = weechat.WEECHAT_RC_OK 1476 | 1477 | elif action == 'abort': 1478 | # Nickname and server are not specified 1479 | if len(arg_parts) == 2: 1480 | nick, server = default_peer_args([], buf) 1481 | # Nickname and server are specified 1482 | elif len(arg_parts) == 4: 1483 | nick, server = default_peer_args(arg_parts[2:4], buf) 1484 | else: 1485 | return weechat.WEECHAT_RC_ERROR 1486 | 1487 | context = get_server_context(server, nick) 1488 | 1489 | if context.in_smp: 1490 | try: 1491 | context.smpAbort() 1492 | except potr.context.NotEncryptedError: 1493 | context.print_buffer( 1494 | 'There is currently no encrypted session with {0}.' 1495 | .format(context.peer), 'error') 1496 | else: 1497 | debug('SMP aborted') 1498 | context.smp_finish('SMP aborted.') 1499 | result = weechat.WEECHAT_RC_OK 1500 | 1501 | elif len(arg_parts) in (1, 3) and arg_parts[0] == 'trust': 1502 | nick, server = default_peer_args(arg_parts[1:3], buf) 1503 | 1504 | if nick is not None and server is not None: 1505 | context = get_server_context(server, nick) 1506 | 1507 | if context.crypto.theirPubkey is not None: 1508 | context.setCurrentTrust('verified') 1509 | context.print_buffer('{peer} is now authenticated.'.format( 1510 | peer=context.peer)) 1511 | 1512 | weechat.bar_item_update(SCRIPT_NAME) 1513 | else: 1514 | context.print_buffer( 1515 | 'No fingerprint for {peer}. Start an OTR conversation ' 1516 | 'first: /otr start'.format(peer=context.peer), 1517 | 'error') 1518 | 1519 | result = weechat.WEECHAT_RC_OK 1520 | elif len(arg_parts) in (1, 3) and arg_parts[0] == 'distrust': 1521 | nick, server = default_peer_args(arg_parts[1:3], buf) 1522 | 1523 | if nick is not None and server is not None: 1524 | context = get_server_context(server, nick) 1525 | 1526 | if context.crypto.theirPubkey is not None: 1527 | context.setCurrentTrust('') 1528 | context.print_buffer( 1529 | '{peer} is now de-authenticated.'.format( 1530 | peer=context.peer)) 1531 | 1532 | weechat.bar_item_update(SCRIPT_NAME) 1533 | else: 1534 | context.print_buffer( 1535 | 'No fingerprint for {peer}. Start an OTR conversation ' 1536 | 'first: /otr start'.format(peer=context.peer), 'error') 1537 | 1538 | result = weechat.WEECHAT_RC_OK 1539 | 1540 | elif len(arg_parts) in (1, 2) and arg_parts[0] == 'log': 1541 | nick, server = default_peer_args([], buf) 1542 | if len(arg_parts) == 1: 1543 | if nick is not None and server is not None: 1544 | context = get_server_context(server, nick) 1545 | 1546 | if context.is_encrypted(): 1547 | if context.is_logged(): 1548 | context.print_buffer( 1549 | 'This conversation is currently being logged.', 1550 | 'warning') 1551 | result = weechat.WEECHAT_RC_OK 1552 | 1553 | else: 1554 | context.print_buffer( 1555 | 'This conversation is currently NOT being logged.') 1556 | result = weechat.WEECHAT_RC_OK 1557 | else: 1558 | context.print_buffer( 1559 | 'OTR LOG: Not in an OTR session', 'error') 1560 | result = weechat.WEECHAT_RC_OK 1561 | 1562 | else: 1563 | print_buffer('', 'OTR LOG: Not in an OTR session', 'error') 1564 | result = weechat.WEECHAT_RC_OK 1565 | 1566 | if len(arg_parts) == 2: 1567 | if nick is not None and server is not None: 1568 | context = get_server_context(server, nick) 1569 | 1570 | if arg_parts[1] == 'start' and \ 1571 | not context.is_logged() and \ 1572 | context.is_encrypted(): 1573 | if context.previous_log_level is None: 1574 | context.previous_log_level = context.get_log_level() 1575 | context.print_buffer( 1576 | 'From this point on, this conversation will be ' 1577 | 'logged. Please keep in mind that by doing so you ' 1578 | 'are potentially putting yourself and your ' 1579 | 'interlocutor at risk. You can disable this by doing ' 1580 | '/otr log stop', 1581 | 'warning') 1582 | weechat.command(buf, '/mute logger set 9') 1583 | result = weechat.WEECHAT_RC_OK 1584 | 1585 | elif arg_parts[1] == 'stop' and \ 1586 | context.is_logged() and \ 1587 | context.is_encrypted(): 1588 | if context.previous_log_level is None: 1589 | context.previous_log_level = context.get_log_level() 1590 | weechat.command(buf, '/mute logger set 0') 1591 | context.print_buffer( 1592 | 'From this point on, this conversation will NOT be ' 1593 | 'logged ANYMORE.') 1594 | result = weechat.WEECHAT_RC_OK 1595 | 1596 | elif not context.is_encrypted(): 1597 | context.print_buffer( 1598 | 'OTR LOG: Not in an OTR session', 'error') 1599 | result = weechat.WEECHAT_RC_OK 1600 | 1601 | else: 1602 | # Don't need to do anything. 1603 | result = weechat.WEECHAT_RC_OK 1604 | 1605 | else: 1606 | print_buffer('', 'OTR LOG: Not in an OTR session', 'error') 1607 | 1608 | elif len(arg_parts) in (1, 2, 3, 4) and arg_parts[0] == 'policy': 1609 | if len(arg_parts) == 1: 1610 | nick, server = default_peer_args([], buf) 1611 | 1612 | if nick is not None and server is not None: 1613 | context = get_server_context(server, nick) 1614 | 1615 | context.print_buffer(context.format_policies()) 1616 | else: 1617 | prnt('', format_default_policies()) 1618 | 1619 | result = weechat.WEECHAT_RC_OK 1620 | 1621 | elif len(arg_parts) == 2 and arg_parts[1].lower() == 'default': 1622 | nick, server = default_peer_args([], buf) 1623 | 1624 | if nick is not None and server is not None: 1625 | context = get_server_context(server, nick) 1626 | 1627 | context.print_buffer(format_default_policies()) 1628 | else: 1629 | prnt('', format_default_policies()) 1630 | 1631 | result = weechat.WEECHAT_RC_OK 1632 | 1633 | elif len(arg_parts) == 3 and arg_parts[1].lower() in POLICIES: 1634 | nick, server = default_peer_args([], buf) 1635 | 1636 | if nick is not None and server is not None: 1637 | context = get_server_context(server, nick) 1638 | 1639 | policy_var = context.policy_config_option(arg_parts[1].lower()) 1640 | 1641 | command('', '/set {policy} {value}'.format( 1642 | policy=policy_var, 1643 | value=arg_parts[2])) 1644 | 1645 | context.print_buffer(context.format_policies()) 1646 | 1647 | result = weechat.WEECHAT_RC_OK 1648 | 1649 | elif len(arg_parts) == 4 and \ 1650 | arg_parts[1].lower() == 'default' and \ 1651 | arg_parts[2].lower() in POLICIES: 1652 | nick, server = default_peer_args([], buf) 1653 | 1654 | policy_var = "otr.policy.default." + arg_parts[2].lower() 1655 | 1656 | command('', '/set {policy} {value}'.format( 1657 | policy=policy_var, 1658 | value=arg_parts[3])) 1659 | 1660 | if nick is not None and server is not None: 1661 | context = get_server_context(server, nick) 1662 | 1663 | context.print_buffer(format_default_policies()) 1664 | else: 1665 | prnt('', format_default_policies()) 1666 | 1667 | result = weechat.WEECHAT_RC_OK 1668 | elif len(arg_parts) in (1, 2) and arg_parts[0] == 'fingerprint': 1669 | if len(arg_parts) == 1: 1670 | show_account_fingerprints() 1671 | result = weechat.WEECHAT_RC_OK 1672 | elif len(arg_parts) == 2: 1673 | if arg_parts[1] == 'all': 1674 | show_peer_fingerprints() 1675 | else: 1676 | show_peer_fingerprints(grep=arg_parts[1]) 1677 | result = weechat.WEECHAT_RC_OK 1678 | 1679 | return result 1680 | 1681 | def otr_statusbar_cb(data, item, window): 1682 | """Update the statusbar.""" 1683 | if window: 1684 | buf = weechat.window_get_pointer(window, 'buffer') 1685 | else: 1686 | # If the bar item is in a root bar that is not in a window, window 1687 | # will be empty. 1688 | buf = weechat.current_buffer() 1689 | 1690 | if not buffer_is_private(buf): 1691 | return '' 1692 | 1693 | local_user = irc_user( 1694 | buffer_get_string(buf, 'localvar_nick'), 1695 | buffer_get_string(buf, 'localvar_server')) 1696 | 1697 | remote_user = irc_user( 1698 | buffer_get_string(buf, 'localvar_channel'), 1699 | buffer_get_string(buf, 'localvar_server')) 1700 | 1701 | context = get_context(local_user, remote_user) 1702 | 1703 | encrypted_str = config_string('look.bar.state.encrypted') 1704 | unencrypted_str = config_string('look.bar.state.unencrypted') 1705 | authenticated_str = config_string('look.bar.state.authenticated') 1706 | unauthenticated_str = config_string('look.bar.state.unauthenticated') 1707 | logged_str = config_string('look.bar.state.logged') 1708 | notlogged_str = config_string('look.bar.state.notlogged') 1709 | 1710 | bar_parts = [] 1711 | 1712 | if context.is_encrypted(): 1713 | if encrypted_str: 1714 | bar_parts.append(''.join([ 1715 | config_color('status.encrypted'), 1716 | encrypted_str, 1717 | config_color('status.default')])) 1718 | 1719 | if context.is_verified(): 1720 | if authenticated_str: 1721 | bar_parts.append(''.join([ 1722 | config_color('status.authenticated'), 1723 | authenticated_str, 1724 | config_color('status.default')])) 1725 | elif unauthenticated_str: 1726 | bar_parts.append(''.join([ 1727 | config_color('status.unauthenticated'), 1728 | unauthenticated_str, 1729 | config_color('status.default')])) 1730 | 1731 | if context.is_logged(): 1732 | if logged_str: 1733 | bar_parts.append(''.join([ 1734 | config_color('status.logged'), 1735 | logged_str, 1736 | config_color('status.default')])) 1737 | elif notlogged_str: 1738 | bar_parts.append(''.join([ 1739 | config_color('status.notlogged'), 1740 | notlogged_str, 1741 | config_color('status.default')])) 1742 | 1743 | elif unencrypted_str: 1744 | bar_parts.append(''.join([ 1745 | config_color('status.unencrypted'), 1746 | unencrypted_str, 1747 | config_color('status.default')])) 1748 | 1749 | result = config_string('look.bar.state.separator').join(bar_parts) 1750 | 1751 | if result: 1752 | result = '{color}{prefix}{result}'.format( 1753 | color=config_color('status.default'), 1754 | prefix=config_string('look.bar.prefix'), 1755 | result=result) 1756 | 1757 | if context.is_encrypted(): 1758 | weechat.buffer_set(buf, 'localvar_set_otr_encrypted', 'true') 1759 | else: 1760 | weechat.buffer_set(buf, 'localvar_set_otr_encrypted', 'false') 1761 | 1762 | if context.is_verified(): 1763 | weechat.buffer_set(buf, 'localvar_set_otr_authenticated', 'true') 1764 | else: 1765 | weechat.buffer_set(buf, 'localvar_set_otr_authenticated', 'false') 1766 | 1767 | if context.is_logged(): 1768 | weechat.buffer_set(buf, 'localvar_set_otr_logged', 'true') 1769 | else: 1770 | weechat.buffer_set(buf, 'localvar_set_otr_logged', 'false') 1771 | 1772 | return result 1773 | 1774 | def bar_config_update_cb(data, option): 1775 | """Callback for updating the status bar when its config changes.""" 1776 | weechat.bar_item_update(SCRIPT_NAME) 1777 | 1778 | return weechat.WEECHAT_RC_OK 1779 | 1780 | def policy_completion_cb(data, completion_item, buf, completion): 1781 | """Callback for policy tab completion.""" 1782 | for policy in POLICIES: 1783 | weechat.hook_completion_list_add( 1784 | completion, policy, 0, weechat.WEECHAT_LIST_POS_SORT) 1785 | 1786 | return weechat.WEECHAT_RC_OK 1787 | 1788 | def policy_create_option_cb(data, config_file, section, name, value): 1789 | """Callback for creating a new policy option when the user sets one 1790 | that doesn't exist.""" 1791 | weechat.config_new_option( 1792 | config_file, section, name, 'boolean', '', '', 0, 0, value, value, 0, 1793 | '', '', '', '', '', '') 1794 | 1795 | return weechat.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED 1796 | 1797 | def logger_level_update_cb(data, option, value): 1798 | """Callback called when any logger level changes.""" 1799 | weechat.bar_item_update(SCRIPT_NAME) 1800 | 1801 | return weechat.WEECHAT_RC_OK 1802 | 1803 | def buffer_switch_cb(data, signal, signal_data): 1804 | """Callback for buffer switched. 1805 | 1806 | Used for updating the status bar item when it is in a root bar. 1807 | """ 1808 | weechat.bar_item_update(SCRIPT_NAME) 1809 | 1810 | return weechat.WEECHAT_RC_OK 1811 | 1812 | def buffer_closing_cb(data, signal, signal_data): 1813 | """Callback for buffer closed. 1814 | 1815 | It closes the OTR session when the buffer is about to be closed. 1816 | """ 1817 | result = weechat.WEECHAT_RC_ERROR 1818 | nick, server = default_peer_args([], signal_data) 1819 | 1820 | if nick is not None and server is not None: 1821 | context = get_server_context(server, nick) 1822 | context.disconnect() 1823 | 1824 | result = weechat.WEECHAT_RC_OK 1825 | return result 1826 | 1827 | def init_config(): 1828 | """Set up configuration options and load config file.""" 1829 | global CONFIG_FILE 1830 | CONFIG_FILE = weechat.config_new(SCRIPT_NAME, '', '') 1831 | 1832 | global CONFIG_SECTIONS 1833 | CONFIG_SECTIONS = {} 1834 | 1835 | CONFIG_SECTIONS['general'] = weechat.config_new_section( 1836 | CONFIG_FILE, 'general', 0, 0, '', '', '', '', '', '', '', '', '', '') 1837 | 1838 | for option, typ, desc, default in [ 1839 | ('debug', 1840 | 'boolean', 1841 | 'OTR script debugging', 1842 | 'off'), 1843 | ('hints', 1844 | 'boolean', 1845 | 'Give helpful hints how to use this script and how to stay ' 1846 | 'secure while using OTR (recommended)', 1847 | 'on'), 1848 | ('defaultkey', 1849 | 'string', 1850 | 'default private key to use for new accounts (nick@server)', 1851 | ''), 1852 | ('no_send_tag_regex', 1853 | 'string', 1854 | 'do not OTR whitespace tag messages to nicks matching this regex ' 1855 | '(case insensitive)', 1856 | '^(alis|chanfix|global|.+serv|\*.+)$'), 1857 | ]: 1858 | weechat.config_new_option( 1859 | CONFIG_FILE, CONFIG_SECTIONS['general'], option, typ, desc, '', 0, 1860 | 0, default, default, 0, '', '', '', '', '', '') 1861 | 1862 | CONFIG_SECTIONS['color'] = weechat.config_new_section( 1863 | CONFIG_FILE, 'color', 0, 0, '', '', '', '', '', '', '', '', '', '') 1864 | 1865 | for option, desc, default, update_cb in [ 1866 | ('status.default', 1867 | 'status bar default color', 1868 | 'default', 1869 | 'bar_config_update_cb'), 1870 | ('status.encrypted', 1871 | 'status bar encrypted indicator color', 1872 | 'green', 1873 | 'bar_config_update_cb'), 1874 | ('status.unencrypted', 1875 | 'status bar unencrypted indicator color', 1876 | 'lightred', 1877 | 'bar_config_update_cb'), 1878 | ('status.authenticated', 1879 | 'status bar authenticated indicator color', 1880 | 'green', 1881 | 'bar_config_update_cb'), 1882 | ('status.unauthenticated', 1883 | 'status bar unauthenticated indicator color', 1884 | 'lightred', 1885 | 'bar_config_update_cb'), 1886 | ('status.logged', 1887 | 'status bar logged indicator color', 1888 | 'lightred', 1889 | 'bar_config_update_cb'), 1890 | ('status.notlogged', 1891 | 'status bar not logged indicator color', 1892 | 'green', 1893 | 'bar_config_update_cb'), 1894 | ('buffer.hint', 1895 | 'text color for hints', 1896 | 'lightblue', 1897 | ''), 1898 | ('buffer.info', 1899 | 'text color for informational messages', 1900 | 'default', 1901 | ''), 1902 | ('buffer.success', 1903 | 'text color for success messages', 1904 | 'lightgreen', 1905 | ''), 1906 | ('buffer.warning', 1907 | 'text color for warnings', 1908 | 'yellow', 1909 | ''), 1910 | ('buffer.error', 1911 | 'text color for errors', 1912 | 'lightred', 1913 | ''), 1914 | ]: 1915 | weechat.config_new_option( 1916 | CONFIG_FILE, CONFIG_SECTIONS['color'], option, 'color', desc, '', 0, 1917 | 0, default, default, 0, '', '', update_cb, '', '', '') 1918 | 1919 | CONFIG_SECTIONS['look'] = weechat.config_new_section( 1920 | CONFIG_FILE, 'look', 0, 0, '', '', '', '', '', '', '', '', '', '') 1921 | 1922 | for option, desc, default, update_cb in [ 1923 | ('bar.prefix', 1924 | 'prefix for OTR status bar item', 1925 | 'OTR:', 1926 | 'bar_config_update_cb'), 1927 | ('bar.state.encrypted', 1928 | 'shown in status bar when conversation is encrypted', 1929 | 'SEC', 1930 | 'bar_config_update_cb'), 1931 | ('bar.state.unencrypted', 1932 | 'shown in status bar when conversation is not encrypted', 1933 | '!SEC', 1934 | 'bar_config_update_cb'), 1935 | ('bar.state.authenticated', 1936 | 'shown in status bar when peer is authenticated', 1937 | 'AUTH', 1938 | 'bar_config_update_cb'), 1939 | ('bar.state.unauthenticated', 1940 | 'shown in status bar when peer is not authenticated', 1941 | '!AUTH', 1942 | 'bar_config_update_cb'), 1943 | ('bar.state.logged', 1944 | 'shown in status bar when peer conversation is being logged to ' 1945 | 'disk', 1946 | 'LOG', 1947 | 'bar_config_update_cb'), 1948 | ('bar.state.notlogged', 1949 | 'shown in status bar when peer conversation is not being logged ' 1950 | 'to disk', 1951 | '!LOG', 1952 | 'bar_config_update_cb'), 1953 | ('bar.state.separator', 1954 | 'separator for states in the status bar', 1955 | ',', 1956 | 'bar_config_update_cb'), 1957 | ('prefix', 1958 | 'prefix used for messages from otr (note: content is evaluated, ' 1959 | 'see /help eval)', 1960 | '${color:default}:! ${color:brown}otr${color:default} !:', 1961 | ''), 1962 | ]: 1963 | weechat.config_new_option( 1964 | CONFIG_FILE, CONFIG_SECTIONS['look'], option, 'string', desc, '', 1965 | 0, 0, default, default, 0, '', '', update_cb, '', '', '') 1966 | 1967 | CONFIG_SECTIONS['policy'] = weechat.config_new_section( 1968 | CONFIG_FILE, 'policy', 1, 1, '', '', '', '', '', '', 1969 | 'policy_create_option_cb', '', '', '') 1970 | 1971 | for option, desc, default in [ 1972 | ('default.allow_v2', 1973 | 'default allow OTR v2 policy', 1974 | 'on'), 1975 | ('default.require_encryption', 1976 | 'default require encryption policy', 1977 | 'off'), 1978 | ('default.log', 1979 | 'default enable logging to disk', 1980 | 'off'), 1981 | ('default.send_tag', 1982 | 'default send tag policy', 1983 | 'off'), 1984 | ('default.html_escape', 1985 | 'default HTML escape policy', 1986 | 'off'), 1987 | ('default.html_filter', 1988 | 'default HTML filter policy', 1989 | 'on'), 1990 | ]: 1991 | weechat.config_new_option( 1992 | CONFIG_FILE, CONFIG_SECTIONS['policy'], option, 'boolean', desc, '', 1993 | 0, 0, default, default, 0, '', '', '', '', '', '') 1994 | 1995 | weechat.config_read(CONFIG_FILE) 1996 | 1997 | def free_all_config(): 1998 | """Free all config options, sections and config file.""" 1999 | for section in CONFIG_SECTIONS.values(): 2000 | weechat.config_section_free_options(section) 2001 | weechat.config_section_free(section) 2002 | 2003 | weechat.config_free(CONFIG_FILE) 2004 | 2005 | def create_dir(): 2006 | """Create the OTR subdirectory in the WeeChat config directory if it does 2007 | not exist.""" 2008 | if not os.path.exists(OTR_DIR): 2009 | weechat.mkdir_home(OTR_DIR_NAME, 0o700) 2010 | 2011 | def git_info(): 2012 | """If this script is part of a git repository return the repo state.""" 2013 | result = None 2014 | script_dir = os.path.dirname(os.path.realpath(__file__)) 2015 | git_dir = os.path.join(script_dir, '.git') 2016 | if os.path.isdir(git_dir): 2017 | import subprocess 2018 | try: 2019 | # We can't use check_output here without breaking compatibility 2020 | # for Python 2.6, but we ignore the return value anyway, so Popen 2021 | # is only slightly more complicated: 2022 | process = subprocess.Popen([ 2023 | 'git', 2024 | '--git-dir', git_dir, 2025 | '--work-tree', script_dir, 2026 | 'describe', '--dirty', '--always', 2027 | ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 2028 | output = process.communicate()[0] 2029 | if output: 2030 | result = PYVER.to_unicode(output).lstrip('v').rstrip() 2031 | except OSError: 2032 | pass 2033 | 2034 | return result 2035 | 2036 | def weechat_version_ok(): 2037 | """Check if the WeeChat version is compatible with this script. 2038 | 2039 | If WeeChat version < 0.4.2 log an error to the core buffer and return 2040 | False. Otherwise return True. 2041 | """ 2042 | weechat_version = weechat.info_get('version_number', '') or 0 2043 | if int(weechat_version) < 0x00040200: 2044 | error_message = ( 2045 | '{script_name} requires WeeChat version >= 0.4.2. The current ' 2046 | 'version is {current_version}.').format( 2047 | script_name=SCRIPT_NAME, 2048 | current_version=weechat.info_get('version', '')) 2049 | prnt('', error_message) 2050 | return False 2051 | return True 2052 | 2053 | SCRIPT_VERSION = git_info() or SCRIPT_VERSION 2054 | 2055 | def dependency_versions(): 2056 | """Return a string containing the versions of all dependencies.""" 2057 | return ('weechat-otr {script_version}, ' 2058 | 'potr {potr_major}.{potr_minor}.{potr_patch}-{potr_sub}, ' 2059 | 'Python {python_version}, ' 2060 | 'WeeChat {weechat_version}' 2061 | ).format( 2062 | script_version=SCRIPT_VERSION, 2063 | potr_major=potr.VERSION[0], 2064 | potr_minor=potr.VERSION[1], 2065 | potr_patch=potr.VERSION[2], 2066 | potr_sub=potr.VERSION[3], 2067 | python_version=platform.python_version(), 2068 | weechat_version=weechat.info_get('version', '')) 2069 | 2070 | def excepthook(typ, value, tracebak): 2071 | """Add dependency versions to tracebacks.""" 2072 | sys.stderr.write('Versions: ') 2073 | sys.stderr.write(dependency_versions()) 2074 | sys.stderr.write('\n') 2075 | 2076 | sys.__excepthook__(typ, value, tracebak) 2077 | 2078 | sys.excepthook = excepthook 2079 | 2080 | if weechat.register( 2081 | SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENCE, SCRIPT_DESC, 2082 | 'shutdown', ''): 2083 | if weechat_version_ok(): 2084 | init_config() 2085 | 2086 | OTR_DIR = os.path.join(info_get('weechat_dir', ''), OTR_DIR_NAME) 2087 | create_dir() 2088 | 2089 | ACCOUNTS = AccountDict() 2090 | 2091 | weechat.hook_modifier('irc_in_privmsg', 'message_in_cb', '') 2092 | weechat.hook_modifier('irc_out_privmsg', 'message_out_cb', '') 2093 | 2094 | weechat.hook_command( 2095 | SCRIPT_NAME, SCRIPT_HELP, 2096 | 'start [NICK SERVER] || ' 2097 | 'refresh [NICK SERVER] || ' 2098 | 'finish [NICK SERVER] || ' 2099 | 'end [NICK SERVER] || ' 2100 | 'status [NICK SERVER] || ' 2101 | 'smp ask [NICK SERVER] [QUESTION] SECRET || ' 2102 | 'smp respond [NICK SERVER] SECRET || ' 2103 | 'smp abort [NICK SERVER] || ' 2104 | 'trust [NICK SERVER] || ' 2105 | 'distrust [NICK SERVER] || ' 2106 | 'log [start|stop] || ' 2107 | 'policy [POLICY on|off] || ' 2108 | 'fingerprint [SEARCH|all]', 2109 | '', 2110 | 'start %(nick) %(irc_servers) %-||' 2111 | 'refresh %(nick) %(irc_servers) %-||' 2112 | 'finish %(nick) %(irc_servers) %-||' 2113 | 'end %(nick) %(irc_servers) %-||' 2114 | 'status %(nick) %(irc_servers) %-||' 2115 | 'smp ask|respond %(nick) %(irc_servers) %-||' 2116 | 'smp abort %(nick) %(irc_servers) %-||' 2117 | 'trust %(nick) %(irc_servers) %-||' 2118 | 'distrust %(nick) %(irc_servers) %-||' 2119 | 'log start|stop %-||' 2120 | 'policy %(otr_policy) on|off %-||' 2121 | 'fingerprint all %-||', 2122 | 'command_cb', 2123 | '') 2124 | 2125 | weechat.hook_completion( 2126 | 'otr_policy', 'OTR policies', 'policy_completion_cb', '') 2127 | 2128 | weechat.hook_config('logger.level.irc.*', 'logger_level_update_cb', '') 2129 | 2130 | weechat.hook_signal('buffer_switch', 'buffer_switch_cb', '') 2131 | weechat.hook_signal('buffer_closing', 'buffer_closing_cb', '') 2132 | 2133 | OTR_STATUSBAR = weechat.bar_item_new( 2134 | SCRIPT_NAME, 'otr_statusbar_cb', '') 2135 | weechat.bar_item_update(SCRIPT_NAME) 2136 | -------------------------------------------------------------------------------- /weechat_otr_test/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """unittest tests for weechat-otr.""" 3 | -------------------------------------------------------------------------------- /weechat_otr_test/is_encrypted_account.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | 5 | import weechat_otr_test.is_encrypted_context 6 | 7 | import weechat_otr 8 | 9 | class IsEncryptedAccount(weechat_otr.IrcOtrAccount): 10 | contextclass = weechat_otr_test.is_encrypted_context.IsEncryptedContext 11 | -------------------------------------------------------------------------------- /weechat_otr_test/is_encrypted_context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=abstract-method 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | import weechat_otr 7 | 8 | class IsEncryptedContext(weechat_otr.IrcContext): 9 | 10 | def __init__(self, account, peername): 11 | super(IsEncryptedContext, self).__init__(account, peername) 12 | self.is_encrypted_fake = False 13 | 14 | def is_encrypted(self): 15 | return self.is_encrypted_fake 16 | -------------------------------------------------------------------------------- /weechat_otr_test/mock_account.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | 5 | class MockAccount(object): 6 | 7 | def __init__(self): 8 | self.contexts = {} 9 | self.end_all_privates = 0 10 | 11 | def add_context(self, peer, context): 12 | self.contexts[peer] = context 13 | 14 | def getContext(self, peer): 15 | return self.contexts[peer] 16 | 17 | def end_all_private(self): 18 | self.end_all_privates += 1 19 | -------------------------------------------------------------------------------- /weechat_otr_test/mock_context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-instance-attributes 5 | 6 | class MockContext(object): 7 | 8 | def __init__(self): 9 | self.smp_init = None 10 | self.smp_got_secret = None 11 | self.in_smp = False 12 | self.smp_finishes = [] 13 | self.disconnects = 0 14 | self.encrypted = False 15 | self.verified = False 16 | self.logged = False 17 | 18 | def smpAbort(self): 19 | pass 20 | 21 | def smpInit(self, *args): 22 | self.smp_init = args 23 | 24 | def smpGotSecret(self, *args): 25 | self.smp_got_secret = args 26 | 27 | def smp_finish(self, *args): 28 | self.smp_finishes.append(args) 29 | 30 | def print_buffer(self, *args): 31 | pass 32 | 33 | def disconnect(self): 34 | self.disconnects += 1 35 | 36 | def is_encrypted(self): 37 | return self.encrypted 38 | 39 | def is_verified(self): 40 | return self.verified 41 | 42 | def is_logged(self): 43 | return self.logged 44 | -------------------------------------------------------------------------------- /weechat_otr_test/mock_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | # pylint: disable=too-few-public-methods 4 | 5 | class MockUser(object): 6 | 7 | def __init__(self, nick): 8 | self.nick = nick 9 | 10 | def policy_config_option(self, _): 11 | # pylint: disable=no-self-use 12 | return '' 13 | -------------------------------------------------------------------------------- /weechat_otr_test/mock_weechat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | # pylint: disable=no-method-argument 4 | # pylint: disable=no-self-use 5 | # pylint: disable=too-many-public-methods 6 | # pylint: disable=unused-argument 7 | # pylint: disable=too-many-instance-attributes 8 | # pylint: disable=invalid-name 9 | # pylint: disable=too-many-arguments 10 | 11 | from __future__ import unicode_literals 12 | 13 | import collections 14 | import copy 15 | import io 16 | import os 17 | import shutil 18 | import tarfile 19 | import types 20 | 21 | class MockWeechat(types.ModuleType): 22 | 23 | WEECHAT_RC_ERROR = None 24 | WEECHAT_RC_OK = None 25 | 26 | def __init__(self, weechat_dir): 27 | self.weechat_dir = weechat_dir 28 | 29 | self.config_options = {} 30 | self.script_name = None 31 | self.printed = {} 32 | self.saved_state = None 33 | self.weechat_dir_tar = None 34 | self.infos = { 35 | ('',) : { 36 | 'weechat_dir': self.weechat_dir, 37 | 'version_number': 0x01000100, 38 | }, 39 | ('server',) : { 40 | 'irc_nick': 'nick', 41 | }, 42 | ('server,nick',) : { 43 | 'irc_buffer' : 'server_nick_buffer', 44 | }, 45 | ('server,nick2',) : { 46 | 'irc_buffer' : 'server_nick2_buffer', 47 | }, 48 | ('server,no_window_nick',) : { 49 | 'irc_buffer' : 'non_private_buffer', 50 | }, 51 | ('server,STATUSMSG',) : { 52 | 'irc_server_isupport_value' : '@+', 53 | }, 54 | ('server,CHANTYPES',) : { 55 | 'irc_server_isupport_value' : '#', 56 | }, 57 | } 58 | self.buffers = { 59 | None : { 60 | 'localvar_type' : 'private', 61 | 'localvar_channel' : 'nick', 62 | 'localvar_server' : 'server', 63 | 'localvar_nick' : 'me', 64 | }, 65 | 'server_nick_buffer' : { 66 | 'localvar_type' : 'private', 67 | 'localvar_channel' : 'nick', 68 | 'localvar_server' : 'server', 69 | 'name' : 'server_nick_buffer_name', 70 | 'plugin' : 'irc', 71 | }, 72 | 'server_nick2_buffer': { 73 | 'localvar_type' : 'private', 74 | 'localvar_channel' : 'nick2', 75 | 'localvar_server' : 'server', 76 | 'name' : 'server_nick2_buffer_name', 77 | 'plugin' : 'irc', 78 | }, 79 | 'non_private_buffer' : { 80 | 'localvar_type' : 'non_private', 81 | 'name' : 'non_private_buffer_name', 82 | 'plugin' : 'irc', 83 | } 84 | } 85 | self.config_written = [] 86 | self.bar_items_removed = [] 87 | self.config_section_free_options_calls = [] 88 | self.config_section_free_calls = [] 89 | self.config_free_calls = [] 90 | self.buffer_new_calls = [] 91 | self.buffer_sets = {} 92 | self.buffer_search_calls = [] 93 | self.buffer_search_returns = [] 94 | self.info_hashtables = {} 95 | self.info_get_hashtable_raise = None 96 | self.infolists = {} 97 | self.config_integer_defaults = {} 98 | self.commands = [] 99 | self.hook_signals = [] 100 | self.info_gets = [] 101 | self.window_get_pointers = {} 102 | 103 | def save(self): 104 | self.snapshot_weechat_dir() 105 | self.saved_state = copy.deepcopy(self.__dict__) 106 | 107 | def restore(self): 108 | prev_state = copy.deepcopy(self.saved_state) 109 | self.__dict__.clear() 110 | self.__dict__.update(prev_state) 111 | 112 | self.restore_weechat_dir() 113 | 114 | def snapshot_weechat_dir(self): 115 | tar_io = io.BytesIO() 116 | # Note that we cannot use "with tarfile.open" due to lack of support 117 | # for context manager in Python 2.6 118 | tar = tarfile.open(fileobj=tar_io, mode='w') 119 | tar.add(self.weechat_dir, '.') 120 | tar.close() 121 | self.weechat_dir_tar = tar_io.getvalue() 122 | 123 | def restore_weechat_dir(self): 124 | shutil.rmtree(self.weechat_dir) 125 | tar_io = io.BytesIO(self.weechat_dir_tar) 126 | # Note that we cannot use "with tarfile.open" due to lack of support 127 | # for context manager in Python 2.6 128 | tar = tarfile.open(fileobj=tar_io) 129 | tar.extractall(self.weechat_dir) 130 | tar.close() 131 | 132 | def bar_item_new(*args): 133 | return 'bar item' 134 | 135 | def bar_item_update(*args): 136 | pass 137 | 138 | def buffer_get_string(self, buf, string): 139 | return self.buffers[buf].get(string) 140 | 141 | def buffer_search(self, plugin, name): 142 | self.buffer_search_calls.append((plugin, name)) 143 | return self.buffer_search_returns.pop(0) 144 | 145 | def command(self, *args): 146 | self.commands.append(args) 147 | 148 | def config_boolean(self, val): 149 | if val == 'on': 150 | return 1 151 | return 0 152 | 153 | def config_get(self, key): 154 | return self.config_options.get(key, '') 155 | 156 | def config_new(*args): 157 | return 'config file' 158 | 159 | def config_new_option(self, config_file, section, name, *args): 160 | parts = [self.script_name] 161 | if section is not None: 162 | parts.append(section) 163 | parts.append(name) 164 | default = args[5] 165 | full_option_name = '.'.join(parts) 166 | 167 | self.config_options[full_option_name] = default 168 | 169 | def config_new_section(self, config_file, name, *args): 170 | return name 171 | 172 | def config_read(*args): 173 | pass 174 | 175 | def config_string(self, key): 176 | return key 177 | 178 | def current_buffer(*args): 179 | pass 180 | 181 | def hook_command(*args): 182 | pass 183 | 184 | def hook_completion(*args): 185 | pass 186 | 187 | def hook_config(*args): 188 | pass 189 | 190 | def hook_modifier(*args): 191 | pass 192 | 193 | def hook_signal(self, *args): 194 | self.hook_signals.append(args) 195 | 196 | def info_get(self, name, *args): 197 | self.info_gets.append((name, args)) 198 | 199 | return self.infos[args].get(name) 200 | 201 | def info_get_hashtable(self, name, args): 202 | # pylint: disable=too-many-branches 203 | # pylint: disable=too-many-statements 204 | # pylint: disable=too-many-nested-blocks 205 | if self.info_get_hashtable_raise: 206 | # pylint: disable=raising-bad-type 207 | raise self.info_get_hashtable_raise 208 | if name in self.info_hashtables: 209 | return self.info_hashtables[name].pop() 210 | if name == 'irc_message_parse': 211 | # Python translation of WeeChat's message parsing code. 212 | # 213 | # https://github.com/weechat/weechat/blob/bd06f0f60f8c3f5ab883df9c 214 | # b876fe29715055b3/src/plugins/irc/irc-message.c#L43-210 215 | result = { 216 | 'arguments' : '', 217 | 'channel' : '', 218 | 'host' : '', 219 | 'nick' : '' 220 | } 221 | 222 | nick_set = False 223 | 224 | ptr_message = args['message'] 225 | 226 | if ptr_message[0] == ':': 227 | pos3 = ptr_message.find('@') 228 | pos2 = ptr_message.find('!') 229 | pos = ptr_message.find(' ') 230 | if pos2 == -1 or (pos != -1 and pos2 > pos): 231 | pos2 = pos3 232 | if pos2 != -1 and (pos == -1 or pos > pos2): 233 | result['nick'] = ptr_message[1:pos2] 234 | nick_set = True 235 | elif pos != -1: 236 | result['nick'] = ptr_message[1:pos] 237 | nick_set = True 238 | if pos != -1: 239 | result['host'] = ptr_message[1:pos] 240 | ptr_message = ptr_message[pos:].lstrip() 241 | else: 242 | result['host'] = ptr_message[1:] 243 | ptr_message = '' 244 | 245 | if ptr_message: 246 | pos = ptr_message.find(' ') 247 | if pos != -1: 248 | result['command'] = ptr_message[:pos] 249 | pos += 1 250 | while ptr_message[pos] == ' ': 251 | pos += 1 252 | result['arguments'] = ptr_message[pos:] 253 | if ptr_message[pos] != ':': 254 | if ptr_message[pos] in ('#', '&', '+', '!'): 255 | pos2 = ptr_message[pos:].find(' ') 256 | if pos2 != -1: 257 | result['channel'] = ptr_message[pos:][:pos2] 258 | else: 259 | result['channel'] = ptr_message[pos:] 260 | else: 261 | pos2 = ptr_message[pos:].find(' ') 262 | if not nick_set: 263 | if pos2 != -1: 264 | result['nick'] = ptr_message[pos:][:pos2] 265 | else: 266 | result['nick'] = ptr_message[pos:] 267 | if pos2 != -1: 268 | pos3 = pos2 269 | pos2 += 1 270 | while ptr_message[pos2] == ' ': 271 | pos2 += 1 272 | if ptr_message[pos2] in ('#', '&', '+', '!'): 273 | pos4 = ptr_message[pos2:].find(' ') 274 | if pos4 != -1: 275 | result['channel'] = \ 276 | ptr_message[pos2:][:pos4] 277 | else: 278 | result['channel'] = ptr_message[pos2:] 279 | elif not result['channel']: 280 | result['channel'] = ptr_message[pos:][:pos3] 281 | else: 282 | result['command'] = ptr_message 283 | 284 | return result 285 | 286 | def infolist_free(*args): 287 | pass 288 | 289 | def infolist_get(self, *args): 290 | return collections.deque(self.infolists.get(args, []) + [False]) 291 | 292 | def infolist_next(self, infolist): 293 | # The current item in the infolist is the last item. 294 | infolist.rotate(-1) 295 | return infolist[-1] 296 | 297 | def infolist_integer(self, infolist, key): 298 | return infolist[-1]['integer'][key] 299 | 300 | def infolist_pointer(self, infolist, key): 301 | return infolist[-1]['pointer'][key] 302 | 303 | def mkdir_home(self, name, mode): 304 | os.mkdir(os.path.join(self.weechat_dir, name), mode) 305 | 306 | def string_eval_expression(self, expr, pointers, extra_vars, options): 307 | return 'eval({0})'.format(expr) 308 | 309 | def config_color(self, key): 310 | return key 311 | 312 | def color(self, name): 313 | return '(color {0})'.format(name) 314 | 315 | def prnt(self, buf, message): 316 | self.printed.setdefault(buf, []).append(message) 317 | 318 | def register(self, script_name, *args): 319 | self.script_name = script_name 320 | 321 | return True 322 | 323 | def set_server_current_nick(self, server, nick): 324 | self.infos[(server, )]['irc_nick'] = nick 325 | 326 | def config_integer_default(self, key): 327 | return self.config_integer_defaults.get(key) 328 | 329 | def config_write(self, *args): 330 | self.config_written.append(args) 331 | 332 | def config_section_free_options(self, *args): 333 | self.config_section_free_options_calls.append(args) 334 | 335 | def config_section_free(self, *args): 336 | self.config_section_free_calls.append(args) 337 | 338 | def config_free(self, *args): 339 | self.config_free_calls.append(args) 340 | 341 | def bar_item_remove(self, *args): 342 | self.bar_items_removed.append(args) 343 | 344 | def buffer_new(self, *args): 345 | self.buffer_new_calls.append(args) 346 | 347 | return args[0] 348 | 349 | def buffer_set(self, name, key, value): 350 | self.buffer_sets.setdefault(name, {}).update({key: value}) 351 | 352 | def window_get_pointer(self, *args): 353 | return self.window_get_pointers[args] 354 | 355 | def add_channel_prefix(self, prefix): 356 | self.infos[('server,CHANTYPES',)]['irc_server_isupport_value'] += \ 357 | prefix 358 | 359 | def server_no_chantypes(self): 360 | self.infos[('server,CHANTYPES',)]['irc_server_isupport_value'] = '' 361 | self.infos[('server,STATUSMSG',)]['irc_server_isupport_value'] = '' 362 | -------------------------------------------------------------------------------- /weechat_otr_test/mock_window.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | # pylint: disable=too-few-public-methods 4 | 5 | class MockWindow(object): 6 | 7 | pass 8 | -------------------------------------------------------------------------------- /weechat_otr_test/raising_account.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | 5 | import weechat_otr_test.raising_context 6 | 7 | import weechat_otr 8 | 9 | class RaisingAccount(weechat_otr.IrcOtrAccount): 10 | 11 | contextclass = weechat_otr_test.raising_context.RaisingContext 12 | -------------------------------------------------------------------------------- /weechat_otr_test/raising_context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=abstract-method 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | import potr 7 | 8 | import weechat_otr 9 | 10 | class RaisingContext(weechat_otr.IrcContext): 11 | 12 | def __init__(self, account, peername): 13 | super(RaisingContext, self).__init__(account, peername) 14 | self.unencrypted = [] 15 | 16 | def receiveMessage(self, messageData, appdata=None): 17 | if self.unencrypted: 18 | raise potr.context.UnencryptedMessage(*self.unencrypted) 19 | super(RaisingContext, self).receiveMessage(messageData, appdata) 20 | -------------------------------------------------------------------------------- /weechat_otr_test/recording_account.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=abstract-method 3 | # pylint: disable=invalid-name 4 | # pylint: disable=missing-docstring 5 | 6 | import weechat_otr_test.recording_context 7 | 8 | import weechat_otr 9 | 10 | class RecordingAccount(weechat_otr.IrcOtrAccount): 11 | contextclass = weechat_otr_test.recording_context.RecordingContext 12 | -------------------------------------------------------------------------------- /weechat_otr_test/recording_context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=abstract-method 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | import weechat_otr 7 | 8 | class RecordingContext(weechat_otr.IrcContext): 9 | 10 | def __init__(self, account, peername): 11 | super(RecordingContext, self).__init__(account, peername) 12 | 13 | self.injected = [] 14 | 15 | def inject(self, msg, appdata=None): 16 | self.injected.insert(0, msg) 17 | 18 | super(RecordingContext, self).inject(msg, appdata) 19 | -------------------------------------------------------------------------------- /weechat_otr_test/session_test_case.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | # pylint: disable=too-many-public-methods 4 | 5 | from __future__ import unicode_literals 6 | 7 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 8 | 9 | import weechat_otr 10 | 11 | class SessionTestCase(WeechatOtrTestCase): 12 | 13 | def send_all(self, from_nick, to_nick, messages): 14 | # pylint: disable=no-self-use 15 | while messages: 16 | weechat_otr.message_in_cb( 17 | None, None, 'server', 18 | ':{from_nick}!user@host PRIVMSG {to_nick} :{message}'.format( 19 | from_nick=from_nick, 20 | to_nick=to_nick, 21 | message=messages.pop().decode('utf-8', 'replace'))) 22 | -------------------------------------------------------------------------------- /weechat_otr_test/test_arg_parsing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-few-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | 10 | import potr 11 | 12 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 13 | 14 | import weechat_otr 15 | 16 | def create_arg_test_method(command, num_args): 17 | def f(_): 18 | sys.modules['weechat'].infos.update({ 19 | ('arg2',) : {'irc_nick' : ''}, 20 | ('arg2,arg1',) : {'irc_buffer' : 'arg2_arg1_buffer'}, 21 | }) 22 | sys.modules['weechat'].buffers.update({ 23 | 'arg2_arg1_buffer' : { 24 | 'localvar_type' : 'private', 25 | 'name' : 'arg2_arg1_buffer_name', 26 | 'plugin' : 'irc', 27 | }, 28 | }) 29 | 30 | args = ["arg{i}".format(i=i) for i in range(1, num_args + 1)] 31 | arg_str = ' '.join(args) 32 | try: 33 | weechat_otr.command_cb( 34 | None, None, 35 | "{command} {args}".format(command=command, args=arg_str)) 36 | except potr.context.NotEncryptedError: 37 | pass 38 | 39 | return f 40 | 41 | def add_arg_test_methods(): 42 | commands = [ 43 | 'distrust', 44 | 'end', 45 | 'fingerprint', 46 | 'fingerprinti all', 47 | 'finish', 48 | 'log start', 49 | 'log stop', 50 | 'log', 51 | 'policy allow_v2', 52 | 'policy default allow_v2', 53 | 'policy default', 54 | 'policy', 55 | 'refresh', 56 | 'smp abort', 57 | 'smp ask', 58 | 'smp respond', 59 | 'smp', 60 | 'start', 61 | 'status', 62 | 'trust', 63 | ] 64 | 65 | for command, num_args in [(c, i) for c in commands for i in range(8)]: 66 | method = create_arg_test_method(command, num_args) 67 | command_method = command.replace(' ', '_') 68 | setattr(ArgParsingTestCase, 'test_{command}_{num_args}_args'.format( 69 | command=command_method, num_args=num_args), method) 70 | 71 | class ArgParsingTestCase(WeechatOtrTestCase): 72 | """Fuzz test all commands with 0 - 7 arguments. 73 | 74 | Find inputs that don't parse correctly and cause tracebacks. 75 | """ 76 | pass 77 | 78 | add_arg_test_methods() 79 | -------------------------------------------------------------------------------- /weechat_otr_test/test_assembler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | # pylint: disable=too-many-public-methods 4 | 5 | from __future__ import unicode_literals 6 | 7 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 8 | 9 | import weechat_otr 10 | 11 | class AssemblerTestCase(WeechatOtrTestCase): 12 | 13 | def after_setup(self): 14 | # pylint: disable=attribute-defined-outside-init 15 | self.assembler = weechat_otr.Assembler() 16 | 17 | def test_is_query_start(self): 18 | self.assembler.add('?OTRv2? encryption?') 19 | 20 | self.assertTrue(self.assembler.is_query()) 21 | 22 | def test_is_query_start_non_ascii(self): 23 | self.assembler.add('?OTRv2? verschlüsselung?') 24 | 25 | self.assertTrue(self.assembler.is_query()) 26 | 27 | def test_is_query_middle(self): 28 | self.assembler.add('ATT: ?OTRv2?someone requested encryption!') 29 | 30 | self.assertTrue(self.assembler.is_query()) 31 | 32 | def test_is_query_middle_non_ascii(self): 33 | self.assembler.add('ATT: ?OTRv2?someone requested verschlüsselung!') 34 | 35 | self.assertTrue(self.assembler.is_query()) 36 | 37 | def test_is_query_end(self): 38 | self.assembler.add('encryption? ?OTRv2?') 39 | 40 | self.assertTrue(self.assembler.is_query()) 41 | 42 | def test_is_query_end_non_ascii(self): 43 | self.assembler.add('verschlüsselung? ?OTRv2?') 44 | 45 | self.assertTrue(self.assembler.is_query()) 46 | 47 | def test_add_get(self): 48 | self.assembler.add('part 1') 49 | self.assembler.add('part 2') 50 | self.assertEqual(self.assembler.get(), 'part 1part 2') 51 | self.assertEqual(self.assembler.get(), '') 52 | 53 | def test_add_get_non_ascii(self): 54 | self.assembler.add('stück 1') 55 | self.assembler.add('stück 2') 56 | self.assertEqual(self.assembler.get(), 'stück 1stück 2') 57 | self.assertEqual(self.assembler.get(), '') 58 | 59 | def test_is_done_non_otr(self): 60 | self.assembler.add('part 1') 61 | self.assertTrue(self.assembler.is_done()) 62 | -------------------------------------------------------------------------------- /weechat_otr_test/test_buffer_closing.py: -------------------------------------------------------------------------------- 1 | 2 | # -*- coding: utf-8 -*- 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | # pylint: disable=invalid-name 6 | 7 | from __future__ import unicode_literals 8 | 9 | import sys 10 | 11 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 12 | import weechat_otr_test.mock_account 13 | import weechat_otr_test.mock_context 14 | 15 | import weechat_otr 16 | 17 | class BufferClosedTestCase(WeechatOtrTestCase): 18 | 19 | def test_callback_registered(self): 20 | self.assertIn( 21 | ('buffer_closing', 'buffer_closing_cb', ''), 22 | sys.modules['weechat'].hook_signals) 23 | 24 | def test_session_ended(self): 25 | context = weechat_otr_test.mock_context.MockContext() 26 | account = weechat_otr_test.mock_account.MockAccount() 27 | account.add_context('nick2@server', context) 28 | weechat_otr.ACCOUNTS['nick@server'] = account 29 | 30 | weechat_otr.buffer_closing_cb(None, None, 'server_nick2_buffer') 31 | 32 | self.assertEqual(context.disconnects, 1) 33 | -------------------------------------------------------------------------------- /weechat_otr_test/test_command_finish.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 9 | import weechat_otr_test.mock_account 10 | import weechat_otr_test.mock_context 11 | 12 | import weechat_otr 13 | 14 | class CommandFinishTestCase(WeechatOtrTestCase): 15 | 16 | def after_setup(self): 17 | self.context = weechat_otr_test.mock_context.MockContext() 18 | account = weechat_otr_test.mock_account.MockAccount() 19 | 20 | account.add_context('nick2@server', self.context) 21 | weechat_otr.ACCOUNTS['nick@server'] = account 22 | 23 | def test_finish_buffer(self): 24 | weechat_otr.command_cb(None, 'server_nick2_buffer', 'finish') 25 | 26 | self.assertEqual(self.context.disconnects, 1) 27 | 28 | def test_finish_args(self): 29 | weechat_otr.command_cb(None, None, 'finish nick2 server') 30 | 31 | self.assertEqual(self.context.disconnects, 1) 32 | 33 | # /otr end is an alias for /otr finish 34 | 35 | def test_end_buffer(self): 36 | weechat_otr.command_cb(None, 'server_nick2_buffer', 'end') 37 | 38 | self.assertEqual(self.context.disconnects, 1) 39 | 40 | def test_end_args(self): 41 | weechat_otr.command_cb(None, None, 'end nick2 server') 42 | 43 | self.assertEqual(self.context.disconnects, 1) 44 | -------------------------------------------------------------------------------- /weechat_otr_test/test_command_start.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | 10 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 11 | 12 | import weechat_otr 13 | 14 | class CommandStartTestCase(WeechatOtrTestCase): 15 | 16 | def test_start_buffer(self): 17 | weechat_otr.command_cb(None, 'server_nick2_buffer', 'start') 18 | 19 | self.assertIn( 20 | (u'', '/quote -server server PRIVMSG nick2 :?OTR?'), 21 | sys.modules['weechat'].commands) 22 | 23 | def test_start_args(self): 24 | weechat_otr.command_cb(None, None, 'start nick2 server') 25 | 26 | self.assertIn( 27 | (u'', '/quote -server server PRIVMSG nick2 :?OTR?'), 28 | sys.modules['weechat'].commands) 29 | 30 | # /otr refresh is an alias for /otr start 31 | 32 | def test_refresh_buffer(self): 33 | weechat_otr.command_cb(None, 'server_nick2_buffer', 'refresh') 34 | 35 | self.assertIn( 36 | (u'', '/quote -server server PRIVMSG nick2 :?OTR?'), 37 | sys.modules['weechat'].commands) 38 | 39 | def test_refresh_args(self): 40 | weechat_otr.command_cb(None, None, 'refresh nick2 server') 41 | 42 | self.assertIn( 43 | (u'', '/quote -server server PRIVMSG nick2 :?OTR?'), 44 | sys.modules['weechat'].commands) 45 | -------------------------------------------------------------------------------- /weechat_otr_test/test_context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | # pylint: disable=too-many-public-methods 4 | 5 | from __future__ import unicode_literals 6 | 7 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 8 | 9 | import weechat_otr_test.mock_account 10 | 11 | import weechat_otr 12 | 13 | class ContextTestCase(WeechatOtrTestCase): 14 | 15 | def test_print_buffer(self): 16 | account = weechat_otr_test.mock_account.MockAccount() 17 | context = weechat_otr.IrcContext(account, 'nick@server') 18 | context.print_buffer('a message from the script') 19 | 20 | self.assertPrinted( 21 | 'server_nick_buffer', 22 | 'eval(${color:default}:! ${color:brown}otr${color:default} !:)\t' 23 | '(color default)a message from the script') 24 | 25 | def test_print_buffer_non_ascii(self): 26 | account = weechat_otr_test.mock_account.MockAccount() 27 | context = weechat_otr.IrcContext(account, 'nick@server') 28 | context.print_buffer('gefährte') 29 | 30 | self.assertPrinted( 31 | 'server_nick_buffer', 32 | weechat_otr.PYVER.to_str( 33 | 'eval(${color:default}:! ${color:brown}otr${color:default} ' 34 | '!:)\t(color default)gefährte')) 35 | -------------------------------------------------------------------------------- /weechat_otr_test/test_context_get_logger_option_name.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | # pylint: disable=too-many-public-methods 4 | 5 | from __future__ import unicode_literals 6 | 7 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 8 | 9 | import weechat_otr_test.mock_account 10 | 11 | import weechat_otr 12 | 13 | class ContextGetLoggerOptionNameTestCase(WeechatOtrTestCase): 14 | 15 | def test_get_log_level(self): 16 | account = weechat_otr_test.mock_account.MockAccount() 17 | context = weechat_otr.IrcContext(account, 'nick@server') 18 | 19 | self.assertEqual( 20 | context.get_logger_option_name(), 21 | 'logger.level.irc.server_nick_buffer_name') 22 | -------------------------------------------------------------------------------- /weechat_otr_test/test_context_message_in_cb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 9 | import weechat_otr_test.raising_account 10 | 11 | import weechat_otr 12 | 13 | class ContextMessageInCbTestCase(WeechatOtrTestCase): 14 | 15 | def test_action_encrypted(self): 16 | account = \ 17 | weechat_otr_test.raising_account.RaisingAccount('nick@server') 18 | weechat_otr.ACCOUNTS['nick@server'] = account 19 | context = account.getContext('friend@server') 20 | context.unencrypted = ['\x01ACTION lols\x01'] 21 | 22 | result = weechat_otr.message_in_cb( 23 | None, None, 'server', ':friend!user@host PRIVMSG nick :test') 24 | 25 | self.assertEqual( 26 | result, 27 | ':friend!user@host PRIVMSG nick :Unencrypted message received: ' 28 | '/me lols') 29 | -------------------------------------------------------------------------------- /weechat_otr_test/test_context_msg_convert_in.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | 10 | import potr 11 | 12 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 13 | 14 | from weechat_otr_test import mock_user 15 | 16 | import weechat_otr 17 | 18 | class ContextMessageConvertInTestCase(WeechatOtrTestCase): 19 | 20 | def after_setup(self): 21 | self.context = weechat_otr.IrcContext('account', 'nick@server') 22 | self.context.user = mock_user.MockUser('user') 23 | 24 | def test_unicode(self): 25 | result = self.context.msg_convert_in(weechat_otr.PYVER.to_str('hi')) 26 | self.assertEqual(result, 'hi') 27 | 28 | def test_html_filter_on_encrypted(self): 29 | sys.modules['weechat'].config_options[ 30 | 'otr.policy.server.user.nick.html_filter'] = 'on' 31 | self.context.state = potr.context.STATE_ENCRYPTED 32 | 33 | result = self.context.msg_convert_in( 34 | weechat_otr.PYVER.to_str('lol
<>')) 35 | self.assertEqual(result, 'lol\n<>') 36 | 37 | def test_html_filter_on_unencrypted(self): 38 | sys.modules['weechat'].config_options[ 39 | 'otr.policy.server.user.nick.html_filter'] = 'on' 40 | self.context.state = potr.context.STATE_PLAINTEXT 41 | 42 | result = self.context.msg_convert_in( 43 | weechat_otr.PYVER.to_str('lol
<>')) 44 | self.assertEqual(result, 'lol
<>') 45 | 46 | def test_html_filter_off_encrypted(self): 47 | sys.modules['weechat'].config_options[ 48 | 'otr.policy.server.user.nick.html_filter'] = 'off' 49 | self.context.state = potr.context.STATE_ENCRYPTED 50 | 51 | result = self.context.msg_convert_in( 52 | weechat_otr.PYVER.to_str('lol
<>')) 53 | self.assertEqual(result, 'lol
<>') 54 | 55 | def test_html_filter_off_unencrypted(self): 56 | sys.modules['weechat'].config_options[ 57 | 'otr.policy.server.user.nick.html_filter'] = 'off' 58 | self.context.state = potr.context.STATE_PLAINTEXT 59 | 60 | result = self.context.msg_convert_in( 61 | weechat_otr.PYVER.to_str('lol
<>')) 62 | self.assertEqual(result, 'lol
<>') 63 | 64 | def test_action_encrypted(self): 65 | self.context.state = potr.context.STATE_ENCRYPTED 66 | 67 | result = self.context.msg_convert_in( 68 | weechat_otr.PYVER.to_str('/me lols')) 69 | self.assertEqual(result, '\x01ACTION lols\x01') 70 | 71 | def test_action_unencrypted(self): 72 | self.context.state = potr.context.STATE_PLAINTEXT 73 | 74 | result = self.context.msg_convert_in( 75 | weechat_otr.PYVER.to_str('/me lols')) 76 | self.assertEqual(result, '/me lols') 77 | -------------------------------------------------------------------------------- /weechat_otr_test/test_context_msg_convert_out.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | 10 | import potr 11 | 12 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 13 | 14 | from weechat_otr_test import mock_user 15 | 16 | import weechat_otr 17 | 18 | class ContextMessageConvertOutTestCase(WeechatOtrTestCase): 19 | 20 | def after_setup(self): 21 | self.context = weechat_otr.IrcContext('account', 'nick@server') 22 | self.context.user = mock_user.MockUser('user') 23 | 24 | def test_action_encrypted(self): 25 | self.context.state = potr.context.STATE_ENCRYPTED 26 | 27 | result = self.context.msg_convert_out('\x01ACTION lols\x01') 28 | self.assertEqual(result, b'/me lols') 29 | 30 | def test_html_escape_on_encrypted(self): 31 | sys.modules['weechat'].config_options[ 32 | 'otr.policy.server.user.nick.html_escape'] = 'on' 33 | self.context.state = potr.context.STATE_ENCRYPTED 34 | 35 | result = self.context.msg_convert_out( 36 | weechat_otr.PYVER.to_str('< > &')) 37 | self.assertEqual(result, b'< > &') 38 | 39 | def test_html_escape_on_unencrypted(self): 40 | sys.modules['weechat'].config_options[ 41 | 'otr.policy.server.user.nick.html_escape'] = 'on' 42 | self.context.state = potr.context.STATE_PLAINTEXT 43 | 44 | result = self.context.msg_convert_out( 45 | weechat_otr.PYVER.to_str('< > &')) 46 | self.assertEqual(result, b'< > &') 47 | 48 | def test_html_escape_off_encrypted(self): 49 | sys.modules['weechat'].config_options[ 50 | 'otr.policy.server.user.nick.html_escape'] = 'off' 51 | self.context.state = potr.context.STATE_ENCRYPTED 52 | 53 | result = self.context.msg_convert_out( 54 | weechat_otr.PYVER.to_str('< > &')) 55 | self.assertEqual(result, b'< > &') 56 | 57 | def test_html_escape_off_unencrypted(self): 58 | sys.modules['weechat'].config_options[ 59 | 'otr.policy.server.user.nick.html_escape'] = 'off' 60 | self.context.state = potr.context.STATE_PLAINTEXT 61 | 62 | result = self.context.msg_convert_out( 63 | weechat_otr.PYVER.to_str('< > &')) 64 | self.assertEqual(result, b'< > &') 65 | -------------------------------------------------------------------------------- /weechat_otr_test/test_debug_buffer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | # pylint: disable=too-many-public-methods 4 | # pylint: disable=invalid-name 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | 10 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 11 | 12 | import weechat_otr 13 | 14 | class ShutdownTestCase(WeechatOtrTestCase): 15 | 16 | def test_debug_option_off(self): 17 | sys.modules['weechat'].config_options['otr.general.debug'] = 'off' 18 | weechat_otr.debug('test') 19 | 20 | self.assertNotPrinted('OTR Debug', 'otr debug\ttest') 21 | 22 | def test_debug_buffer_on(self): 23 | sys.modules['weechat'].config_options['otr.general.debug'] = 'on' 24 | sys.modules['weechat'].buffer_search_returns = ['OTR Debug'] 25 | weechat_otr.debug('test') 26 | 27 | self.assertPrinted('OTR Debug', 'otr debug\ttest') 28 | 29 | def test_debug_buffer_non_ascii(self): 30 | sys.modules['weechat'].config_options['otr.general.debug'] = 'on' 31 | sys.modules['weechat'].buffer_search_returns = ['OTR Debug'] 32 | weechat_otr.debug('gefährte') 33 | 34 | self.assertPrinted( 35 | 'OTR Debug', weechat_otr.PYVER.to_str('otr debug\tgefährte')) 36 | 37 | def test_creates_buffer(self): 38 | sys.modules['weechat'].config_options['otr.general.debug'] = 'on' 39 | sys.modules['weechat'].buffer_search_returns = [None] 40 | weechat_otr.debug('test') 41 | 42 | self.assertEqual(sys.modules['weechat'].buffer_new_calls, [ 43 | ('OTR Debug', '', '', '', '')]) 44 | self.assertEqual(sys.modules['weechat'].buffer_sets, { 45 | 'OTR Debug':{ 46 | 'title' : 'OTR Debug', 47 | 'localvar_set_no_log': '1', 48 | }}) 49 | 50 | def test_caches_buffer(self): 51 | sys.modules['weechat'].config_options['otr.general.debug'] = 'on' 52 | sys.modules['weechat'].buffer_search_returns = [None, 'OTR Debug'] 53 | weechat_otr.debug('test1') 54 | weechat_otr.debug('test2') 55 | 56 | self.assertEqual(1, len(sys.modules['weechat'].buffer_new_calls)) 57 | -------------------------------------------------------------------------------- /weechat_otr_test/test_excepthook.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import io 9 | import platform 10 | import sys 11 | 12 | import potr 13 | 14 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 15 | 16 | import weechat_otr 17 | 18 | class TestExcepthook(WeechatOtrTestCase): 19 | 20 | def test_excepthook(self): 21 | sys.modules['weechat'].infos[('',)]['version'] = '9.8.7' 22 | 23 | new_stderr = io.StringIO() 24 | orig_stderr = sys.stderr 25 | sys.stderr = new_stderr 26 | weechat_otr.excepthook(Exception, 'error', []) 27 | sys.stderr = orig_stderr 28 | 29 | version_str = ( 30 | 'Versions: weechat-otr {script_version}, ' 31 | 'potr {potr_major}.{potr_minor}.{potr_patch}-{potr_sub}, ' 32 | 'Python {python_version}, ' 33 | 'WeeChat 9.8.7\n' 34 | ).format( 35 | script_version=weechat_otr.SCRIPT_VERSION, 36 | potr_major=potr.VERSION[0], 37 | potr_minor=potr.VERSION[1], 38 | potr_patch=potr.VERSION[2], 39 | potr_sub=potr.VERSION[3], 40 | python_version=platform.python_version()) 41 | 42 | self.assertIn(version_str, new_stderr.getvalue()) 43 | 44 | def test_excepthook_hooked(self): 45 | self.assertEqual(sys.excepthook, weechat_otr.excepthook) 46 | -------------------------------------------------------------------------------- /weechat_otr_test/test_fingerprint.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import os.path 9 | import sys 10 | 11 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 12 | 13 | import weechat_otr 14 | 15 | class FingerprintTestCase(WeechatOtrTestCase): 16 | 17 | def test_fingerprint(self): 18 | account1 = weechat_otr.ACCOUNTS['nick@server'] 19 | account1.getPrivkey() 20 | 21 | account2 = weechat_otr.ACCOUNTS['nick2@server2'] 22 | account2.getPrivkey() 23 | 24 | weechat_otr.command_cb(None, None, 'fingerprint') 25 | 26 | self.assertPrinted('', ( 27 | 'eval(${{color:default}}:! ${{color:brown}}otr${{color:default}}' 28 | ' !:)\t' 29 | '(color default)nick2@server2 |{fp2}\r\n' 30 | '(color default)nick@server |{fp1}').format( 31 | fp1=account1.getPrivkey(), 32 | fp2=account2.getPrivkey())) 33 | 34 | def test_fingerprint_pattern(self): 35 | fpr_path1 = os.path.join( 36 | sys.modules['weechat'].weechat_dir, 37 | 'otr', 38 | 'nick@server.fpr') 39 | with open(fpr_path1, 'w') as f: 40 | for fields in [ 41 | ['notamachxxx@server', 'nick@server', 'irc', 'fp123', ''], 42 | ['matchxxxxxx@server', 'nick@server', 'irc', 'fp123', ''], 43 | ['beforematch@server', 'nick@server', 'irc', 'fp123', ''], 44 | ['before@servermatch', 'nick@server', 'irc', 'fp123', ''], 45 | ]: 46 | f.write("\t".join(fields)) 47 | f.write("\n") 48 | 49 | account1 = weechat_otr.ACCOUNTS['nick@server'] 50 | account1.getPrivkey() 51 | 52 | weechat_otr.command_cb(None, None, 'fingerprint match') 53 | 54 | self.assertNoPrintedContains('', 'notamachxxx@server') 55 | 56 | self.assertPrinted('', ( 57 | 'eval(${color:default}:! ${color:brown}otr${color:default}' 58 | ' !:)\t' 59 | '(color default)before@servermatch |nick@server |F P 1 2 3 |' 60 | 'unverified\r\n' 61 | '(color default)beforematch@server |nick@server |F P 1 2 3 |' 62 | 'unverified\r\n' 63 | '(color default)matchxxxxxx@server |nick@server |F P 1 2 3 |' 64 | 'unverified')) 65 | 66 | def test_fingerprint_all(self): 67 | fpr_path1 = os.path.join( 68 | sys.modules['weechat'].weechat_dir, 69 | 'otr', 70 | 'nick@server.fpr') 71 | with open(fpr_path1, 'w') as f: 72 | for fields in [ 73 | ['peer1@server', 'nick@server', 'irc', 'fp111', ''], 74 | ['peer2@server', 'nick@server', 'irc', 'fp222', 'smp'], 75 | ['peer3@server', 'nick@server', 'irc', 'fp333', 'verified'], 76 | ]: 77 | f.write("\t".join(fields)) 78 | f.write("\n") 79 | 80 | account1 = weechat_otr.ACCOUNTS['nick@server'] 81 | account1.getPrivkey() 82 | 83 | fpr_path2 = os.path.join( 84 | sys.modules['weechat'].weechat_dir, 85 | 'otr', 86 | 'nick2@server2.fpr') 87 | with open(fpr_path2, 'w') as f: 88 | for fields in [ 89 | ['peer4@server2', 'nick2@server2', 'irc', 'fp444', 90 | 'verified'], 91 | ]: 92 | f.write("\t".join(fields)) 93 | f.write("\n") 94 | 95 | account2 = weechat_otr.ACCOUNTS['nick2@server2'] 96 | account2.getPrivkey() 97 | 98 | weechat_otr.command_cb(None, None, 'fingerprint all') 99 | 100 | self.assertPrinted('', ( 101 | 'eval(${color:default}:! ${color:brown}otr${color:default}' 102 | ' !:)\t' 103 | '(color default)peer4@server2 |nick2@server2 |F P 4 4 4 |' 104 | 'verified \r\n' 105 | '(color default)peer1@server |nick@server |F P 1 1 1 |' 106 | 'unverified \r\n' 107 | '(color default)peer2@server |nick@server |F P 2 2 2 |' 108 | 'SMP verified\r\n' 109 | '(color default)peer3@server |nick@server |F P 3 3 3 |' 110 | 'verified ')) 111 | -------------------------------------------------------------------------------- /weechat_otr_test/test_git_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import os 9 | import shutil 10 | import subprocess 11 | import tempfile 12 | 13 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 14 | 15 | import weechat_otr 16 | 17 | class TestGitInfo(WeechatOtrTestCase): 18 | 19 | def after_setup(self): 20 | self.temp_dir = tempfile.mkdtemp() 21 | self.test_file = os.path.join(self.temp_dir, 'test') 22 | open(self.test_file, 'w').close() 23 | 24 | self.prev_file = weechat_otr.__file__ 25 | weechat_otr.__file__ = self.test_file 26 | 27 | def after_teardown(self): 28 | shutil.rmtree(self.temp_dir) 29 | weechat_otr.__file__ = self.prev_file 30 | 31 | def test_git_info(self): 32 | try: 33 | process = subprocess.Popen(['git', 'init', '--quiet', self.temp_dir]) 34 | except OSError: 35 | self.skipTest('install git to run this test') 36 | process.communicate() 37 | 38 | git_dir = os.path.join(self.temp_dir, '.git') 39 | process = subprocess.Popen([ 40 | 'git', 41 | '--git-dir', git_dir, 42 | '--work-tree', self.temp_dir, 43 | 'add', '.']) 44 | process.communicate() 45 | 46 | process = subprocess.Popen([ 47 | 'git', 48 | '--git-dir', git_dir, 49 | '--work-tree', self.temp_dir, 50 | 'config', 51 | 'user.email', 'testy@test.com']) 52 | process.communicate() 53 | 54 | process = subprocess.Popen([ 55 | 'git', 56 | '--git-dir', git_dir, 57 | '--work-tree', self.temp_dir, 58 | 'config', 59 | 'user.name', 'test']) 60 | process.communicate() 61 | 62 | process = subprocess.Popen([ 63 | 'git', 64 | '--git-dir', git_dir, 65 | '--work-tree', self.temp_dir, 66 | 'commit', 67 | '--quiet', 68 | '-m', 69 | 'test']) 70 | process.communicate() 71 | 72 | self.assertRegex(weechat_otr.git_info(), '[0-9a-f]+') 73 | 74 | def test_git_info_failure(self): 75 | self.assertEqual(weechat_otr.git_info(), None) 76 | -------------------------------------------------------------------------------- /weechat_otr_test/test_html_escape_policy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | 10 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 11 | 12 | import weechat_otr_test.is_encrypted_account 13 | import weechat_otr 14 | 15 | class HtmlEscapePolicyTestCase(WeechatOtrTestCase): 16 | 17 | def after_setup(self): 18 | self.account = \ 19 | weechat_otr_test.is_encrypted_account.IsEncryptedAccount( 20 | 'nick@server') 21 | weechat_otr.ACCOUNTS['nick@server'] = self.account 22 | 23 | def test_default_html_escape_policy(self): 24 | result = weechat_otr.message_out_cb( 25 | None, None, 'server', ':nick!user@host PRIVMSG friend :< > " \' &') 26 | self.assertEqual(result, 'PRIVMSG friend :< > " \' &') 27 | 28 | def test_html_escape_policy_on_encrypted(self): 29 | sys.modules['weechat'].config_options[ 30 | 'otr.policy.server.nick.friend.html_escape'] = 'on' 31 | self.account.getContext('friend@server').is_encrypted_fake = True 32 | 33 | result = weechat_otr.message_out_cb( 34 | None, None, 'server', ':nick!user@host PRIVMSG friend :< > " \' &') 35 | self.assertEqual(result, 'PRIVMSG friend :< > " \' &') 36 | 37 | def test_html_escape_policy_on_unencrypted(self): 38 | sys.modules['weechat'].config_options[ 39 | 'otr.policy.server.nick.friend.html_escape'] = 'on' 40 | self.account.getContext('friend@server').is_encrypted_fake = False 41 | 42 | result = weechat_otr.message_out_cb( 43 | None, None, 'server', ':nick!user@host PRIVMSG friend :< > " \' &') 44 | self.assertEqual(result, 'PRIVMSG friend :< > " \' &') 45 | 46 | def test_html_escape_policy_off_encrypted(self): 47 | sys.modules['weechat'].config_options[ 48 | 'otr.policy.server.nick.friend.html_escape'] = 'off' 49 | self.account.getContext('friend@server').is_encrypted_fake = True 50 | 51 | result = weechat_otr.message_out_cb( 52 | None, None, 'server', ':nick!user@host PRIVMSG friend :< > " \' &') 53 | self.assertEqual(result, 'PRIVMSG friend :< > " \' &') 54 | 55 | def test_html_escape_policy_off_unencrypted(self): 56 | sys.modules['weechat'].config_options[ 57 | 'otr.policy.server.nick.friend.html_escape'] = 'off' 58 | self.account.getContext('friend@server').is_encrypted_fake = False 59 | 60 | result = weechat_otr.message_out_cb( 61 | None, None, 'server', ':nick!user@host PRIVMSG friend :< > " \' &') 62 | self.assertEqual(result, 'PRIVMSG friend :< > " \' &') 63 | 64 | def test_html_escape_policy_non_ascii(self): 65 | sys.modules['weechat'].config_options[weechat_otr.PYVER.to_str( 66 | 'otr.policy.server.nick.gefährte.html_escape')] = 'on' 67 | self.account.getContext('gefährte@server').is_encrypted_fake = True 68 | 69 | result = weechat_otr.message_out_cb( 70 | None, None, 'server', 71 | ':nick!user@host PRIVMSG gefährte :< > " \' &') 72 | self.assertEqual(result, weechat_otr.PYVER.to_str( 73 | 'PRIVMSG gefährte :< > " \' &')) 74 | -------------------------------------------------------------------------------- /weechat_otr_test/test_irc_context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | # pylint: disable=too-many-public-methods 4 | 5 | from __future__ import unicode_literals 6 | 7 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 8 | 9 | import weechat_otr 10 | 11 | class IrcContextTestCase(WeechatOtrTestCase): 12 | 13 | def test_repr(self): 14 | self.assertRegex( 15 | weechat_otr.IrcContext(None, 'nick@server').__repr__(), 16 | r'') 17 | 18 | def test_repr_non_ascii(self): 19 | self.assertRegex( 20 | weechat_otr.IrcContext(None, 'gefährte@gefährteserver').__repr__(), 21 | weechat_otr.PYVER.to_str( 22 | r'')) 24 | -------------------------------------------------------------------------------- /weechat_otr_test/test_irc_html_parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | # pylint: disable=too-many-public-methods 4 | 5 | from __future__ import unicode_literals 6 | 7 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 8 | 9 | import weechat_otr 10 | 11 | class IrcHTMLParserTestCase(WeechatOtrTestCase): 12 | 13 | def test_tag_a(self): 14 | self.check_parse_result( 15 | 'this is a link', 16 | 'this is a [link](http://weechat.org)' 17 | ) 18 | 19 | def test_tag_a_non_ascii(self): 20 | self.check_parse_result( 21 | 'this is a verknüpfung', 22 | 'this is a [verknüpfung](http://weechat.org)' 23 | ) 24 | 25 | def test_tag_a_same(self): 26 | self.check_parse_result( 27 | 'http://weechat.org', 28 | '[http://weechat.org]' 29 | ) 30 | 31 | def test_tag_br(self): 32 | self.check_parse_result( 33 | 'foo
bar
baz', 34 | 'foo\nbar\nbaz' 35 | ) 36 | 37 | def test_tag_unknown(self): 38 | self.check_parse_result( 39 | 'none of ' \ 40 | 'this ' \ 41 | 'matters', 42 | 'none of this matters' 43 | ) 44 | 45 | def test_entity_named(self): 46 | self.check_parse_result( 47 | 'tom & jerry', 48 | 'tom & jerry' 49 | ) 50 | 51 | def test_entity_numeric(self): 52 | self.check_parse_result( 53 | '<html>', 54 | '' 55 | ) 56 | 57 | def check_parse_result(self, html, result): 58 | self.assertEqual(weechat_otr.IrcHTMLParser.parse(html), result) 59 | -------------------------------------------------------------------------------- /weechat_otr_test/test_irc_otr_account_load_trusts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import os.path 9 | import sys 10 | 11 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 12 | 13 | import weechat_otr 14 | 15 | class IrcOtrAccountLoadTrustsTestCase(WeechatOtrTestCase): 16 | 17 | def test_trust_non_ascii(self): 18 | fpr_path = os.path.join( 19 | sys.modules['weechat'].weechat_dir, 20 | 'otr', 21 | 'nick@server.fpr') 22 | with open(fpr_path, 'w') as f: 23 | f.write(weechat_otr.PYVER.to_str( 24 | "gefährte@server\tnick@server\tirc\tfp123\tverified\n")) 25 | 26 | account1 = weechat_otr.ACCOUNTS['nick@server'] 27 | self.assertEqual( 28 | 'verified', account1.getTrust('gefährte@server', 'fp123')) 29 | -------------------------------------------------------------------------------- /weechat_otr_test/test_irc_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | 4 | from __future__ import unicode_literals 5 | 6 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 7 | 8 | import weechat_otr 9 | 10 | class IrcUserTestCase(WeechatOtrTestCase): 11 | 12 | def test_irc_user(self): 13 | self.assertEqual(weechat_otr.irc_user('nick', 'server'), 'nick@server') 14 | 15 | def test_irc_user_case_insensitive(self): 16 | self.assertEqual(weechat_otr.irc_user('NiCk', 'server'), 'nick@server') 17 | -------------------------------------------------------------------------------- /weechat_otr_test/test_is_a_channel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | 10 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 11 | 12 | import weechat_otr 13 | 14 | class IsAChannelTestCase(WeechatOtrTestCase): 15 | 16 | def test_hash(self): 17 | self.assertTrue(weechat_otr.is_a_channel('#channel', 'server')) 18 | 19 | def test_ampersand(self): 20 | sys.modules['weechat'].add_channel_prefix('&') 21 | self.assertTrue(weechat_otr.is_a_channel('&channel', 'server')) 22 | 23 | def test_plus(self): 24 | sys.modules['weechat'].add_channel_prefix('+') 25 | self.assertTrue(weechat_otr.is_a_channel('+channel', 'server')) 26 | 27 | def test_bang(self): 28 | sys.modules['weechat'].add_channel_prefix('!') 29 | self.assertTrue(weechat_otr.is_a_channel('!channel', 'server')) 30 | 31 | def test_at(self): 32 | self.assertTrue(weechat_otr.is_a_channel('@#channel', 'server')) 33 | 34 | def test_not_a_channel(self): 35 | self.assertFalse(weechat_otr.is_a_channel('nick', 'server')) 36 | 37 | def test_hash_server_doesnt_isupport(self): 38 | sys.modules['weechat'].server_no_chantypes() 39 | self.assertTrue(weechat_otr.is_a_channel('#channel', 'server')) 40 | 41 | def test_ampersand_server_doesnt_isupport(self): 42 | sys.modules['weechat'].server_no_chantypes() 43 | self.assertTrue(weechat_otr.is_a_channel('&channel', 'server')) 44 | 45 | def test_plus_server_doesnt_isupport(self): 46 | sys.modules['weechat'].server_no_chantypes() 47 | self.assertTrue(weechat_otr.is_a_channel('+channel', 'server')) 48 | 49 | def test_bang_server_doesnt_isupport(self): 50 | sys.modules['weechat'].server_no_chantypes() 51 | self.assertTrue(weechat_otr.is_a_channel('!channel', 'server')) 52 | 53 | def test_at_server_doesnt_isupport(self): 54 | sys.modules['weechat'].server_no_chantypes() 55 | self.assertTrue(weechat_otr.is_a_channel('@#channel', 'server')) 56 | 57 | def test_not_a_channel_server_doesnt_isupport(self): 58 | sys.modules['weechat'].server_no_chantypes() 59 | self.assertFalse(weechat_otr.is_a_channel('nick', 'server')) 60 | -------------------------------------------------------------------------------- /weechat_otr_test/test_key_generation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import os 9 | import sys 10 | 11 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 12 | 13 | import weechat_otr 14 | 15 | class KeyGenerationTestCase(WeechatOtrTestCase): 16 | 17 | def test_creates_key_file(self): 18 | sys.modules['weechat'].set_server_current_nick('server', 'noob') 19 | 20 | weechat_otr.message_in_cb( 21 | None, None, 'server', ':nick!user@host PRIVMSG noob :?OTRv2?') 22 | 23 | key_path = os.path.join( 24 | sys.modules['weechat'].weechat_dir, 25 | 'otr', 26 | 'noob@server.key3') 27 | 28 | self.assertGreater(os.path.getsize(key_path), 0) 29 | 30 | def test_creates_key_file_non_ascii(self): 31 | sys.modules['weechat'].set_server_current_nick('server', 'gefährte') 32 | 33 | weechat_otr.message_in_cb( 34 | None, None, 'server', ':nick!user@host PRIVMSG gefährte :?OTRv2?') 35 | 36 | key_path = os.path.join( 37 | sys.modules['weechat'].weechat_dir, 38 | 'otr', 39 | 'gefährte@server.key3') 40 | 41 | self.assertGreater(os.path.getsize(key_path), 0) 42 | 43 | def test_creates_key_file_from_default_key(self): 44 | sys.modules['weechat'].set_server_current_nick('server', 'noob') 45 | 46 | weechat_otr.message_in_cb( 47 | None, None, 'server', ':nick!user@host PRIVMSG noob :?OTRv2?') 48 | 49 | sys.modules['weechat'].config_options[ 50 | 'otr.general.defaultkey'] = 'noob@server' 51 | 52 | sys.modules['weechat'].set_server_current_nick('server', 'noob2') 53 | 54 | weechat_otr.message_in_cb( 55 | None, None, 'server', ':nick!user@host PRIVMSG noob2 :?OTRv2?') 56 | 57 | noob_key_path = os.path.join( 58 | sys.modules['weechat'].weechat_dir, 59 | 'otr', 60 | 'noob@server.key3') 61 | 62 | noob2_key_path = os.path.join( 63 | sys.modules['weechat'].weechat_dir, 64 | 'otr', 65 | 'noob2@server.key3') 66 | 67 | with open(noob_key_path, 'rb') as noob_key: 68 | with open(noob2_key_path, 'rb') as noob2_key: 69 | self.assertEqual(noob_key.read(), noob2_key.read()) 70 | 71 | def test_preserves_existing_keys_if_default_key(self): 72 | sys.modules['weechat'].set_server_current_nick('server', 'noob2') 73 | 74 | weechat_otr.message_in_cb( 75 | None, None, 'server', ':nick!user@host PRIVMSG noob2 :?OTRv2?') 76 | 77 | account = weechat_otr.ACCOUNTS['noob2@server'] 78 | orig_noob2_priv_key = account.getPrivkey() 79 | 80 | sys.modules['weechat'].set_server_current_nick('server', 'noob') 81 | 82 | weechat_otr.message_in_cb( 83 | None, None, 'server', ':nick!user@host PRIVMSG noob :?OTRv2?') 84 | 85 | sys.modules['weechat'].config_options[ 86 | 'otr.general.defaultkey'] = 'noob@server' 87 | 88 | account.privkey = None 89 | 90 | sys.modules['weechat'].set_server_current_nick('server', 'noob2') 91 | 92 | weechat_otr.message_in_cb( 93 | None, None, 'server', ':nick!user@host PRIVMSG noob2 :?OTRv2?') 94 | 95 | self.assertEqual(account.privkey, orig_noob2_priv_key) 96 | 97 | def test_reads_key_file(self): 98 | sys.modules['weechat'].set_server_current_nick('server', 'noob') 99 | 100 | weechat_otr.message_in_cb( 101 | None, None, 'server', ':nick!user@host PRIVMSG noob :?OTRv2?') 102 | 103 | account = weechat_otr.ACCOUNTS['noob@server'] 104 | priv_key = account.getPrivkey() 105 | account.privkey = None 106 | 107 | self.assertEqual(priv_key, account.getPrivkey()) 108 | -------------------------------------------------------------------------------- /weechat_otr_test/test_log_level.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | 10 | from weechat_otr_test.session_test_case import SessionTestCase 11 | 12 | import weechat_otr_test.recording_account 13 | import weechat_otr 14 | 15 | class OtrSessionTestCase(SessionTestCase): 16 | 17 | def test_log_level_return_to_default(self): 18 | sys.modules['weechat'].set_server_current_nick('server', 'nick') 19 | 20 | account1 = weechat_otr_test.recording_account.RecordingAccount( 21 | 'nick@server') 22 | weechat_otr.ACCOUNTS['nick@server'] = account1 23 | 24 | account2 = weechat_otr_test.recording_account.RecordingAccount( 25 | 'nick2@server') 26 | weechat_otr.ACCOUNTS['nick2@server'] = account2 27 | 28 | context1 = account2.getContext('nick@server') 29 | context2 = account1.getContext('nick2@server') 30 | 31 | sys.modules['weechat'].infolists = { 32 | ('logger_buffer', '', '') : [ 33 | { 34 | 'pointer' : {'buffer' : context1.buffer()}, 35 | 'integer' : { 36 | 'log_enabled' : 1, 37 | 'log_level' : 0, 38 | }, 39 | } 40 | ], 41 | } 42 | 43 | weechat_otr.message_in_cb( 44 | None, None, 'server', ':nick2!user@host PRIVMSG nick :?OTRv2?') 45 | 46 | sys.modules['weechat'].set_server_current_nick('server', 'nick2') 47 | self.send_all('nick', 'nick2', context2.injected) 48 | 49 | sys.modules['weechat'].set_server_current_nick('server', 'nick') 50 | self.send_all('nick2', 'nick', context1.injected) 51 | 52 | sys.modules['weechat'].set_server_current_nick('server', 'nick2') 53 | self.send_all('nick', 'nick2', context2.injected) 54 | 55 | sys.modules['weechat'].set_server_current_nick('server', 'nick') 56 | self.send_all('nick2', 'nick', context1.injected) 57 | 58 | context1.disconnect() 59 | 60 | self.assertIn( 61 | ('server_nick_buffer', 62 | '/mute unset logger.level.irc.server_nick_buffer_name'), 63 | sys.modules['weechat'].commands) 64 | 65 | def test_log_level_return_to_previous(self): 66 | sys.modules['weechat'].config_options[ 67 | 'logger.level.irc.server_nick_buffer_name'] = 2 68 | 69 | sys.modules['weechat'].set_server_current_nick('server', 'nick') 70 | 71 | account1 = weechat_otr_test.recording_account.RecordingAccount( 72 | 'nick@server') 73 | weechat_otr.ACCOUNTS['nick@server'] = account1 74 | 75 | account2 = weechat_otr_test.recording_account.RecordingAccount( 76 | 'nick2@server') 77 | weechat_otr.ACCOUNTS['nick2@server'] = account2 78 | 79 | context1 = account2.getContext('nick@server') 80 | context2 = account1.getContext('nick2@server') 81 | 82 | sys.modules['weechat'].infolists = { 83 | ('logger_buffer', '', '') : [ 84 | { 85 | 'pointer' : {'buffer' : context1.buffer()}, 86 | 'integer' : { 87 | 'log_enabled' : 1, 88 | 'log_level' : 2, 89 | }, 90 | } 91 | ], 92 | } 93 | 94 | weechat_otr.message_in_cb( 95 | None, None, 'server', ':nick2!user@host PRIVMSG nick :?OTRv2?') 96 | 97 | sys.modules['weechat'].set_server_current_nick('server', 'nick2') 98 | self.send_all('nick', 'nick2', context2.injected) 99 | 100 | sys.modules['weechat'].set_server_current_nick('server', 'nick') 101 | self.send_all('nick2', 'nick', context1.injected) 102 | 103 | sys.modules['weechat'].set_server_current_nick('server', 'nick2') 104 | self.send_all('nick', 'nick2', context2.injected) 105 | 106 | sys.modules['weechat'].set_server_current_nick('server', 'nick') 107 | self.send_all('nick2', 'nick', context1.injected) 108 | 109 | context1.disconnect() 110 | 111 | self.assertIn( 112 | ('server_nick_buffer', '/mute logger set 2'), 113 | sys.modules['weechat'].commands) 114 | -------------------------------------------------------------------------------- /weechat_otr_test/test_message_out_cb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import platform 9 | import sys 10 | 11 | import potr 12 | 13 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 14 | 15 | import weechat_otr 16 | 17 | class MessageOutCbTestCase(WeechatOtrTestCase): 18 | 19 | def test_message_out_cb(self): 20 | result = weechat_otr.message_out_cb( 21 | None, None, 'server', ':nick!user@host PRIVMSG friend :hello') 22 | self.assertEqual(result, 'PRIVMSG friend :hello') 23 | 24 | def test_message_out_cb_send_tag_non_ascii(self): 25 | sys.modules['weechat'].config_options[ 26 | 'otr.policy.server.nick.friend.send_tag'] = 'on' 27 | 28 | result = weechat_otr.message_out_cb( 29 | None, None, 'server', 30 | ":nick!user@host PRIVMSG friend :\xc3") 31 | self.assertEqual( 32 | weechat_otr.PYVER.to_unicode(result), 33 | "PRIVMSG friend :\xc3 \t \t\t\t\t \t \t \t \t\t \t \t") 34 | 35 | def test_message_out_cb_send_tag_alis(self): 36 | self.assertNickIsNotTagged('Alis') 37 | 38 | def test_message_out_cb_send_tag_botserv(self): 39 | self.assertNickIsNotTagged('BotServ') 40 | 41 | def test_message_out_cb_send_tag_chanfix(self): 42 | self.assertNickIsNotTagged('ChanFix') 43 | 44 | def test_message_out_cb_send_tag_chanserv(self): 45 | self.assertNickIsNotTagged('ChanServ') 46 | 47 | def test_message_out_cb_send_tag_gameserv(self): 48 | self.assertNickIsNotTagged('GameServ') 49 | 50 | def test_message_out_cb_send_tag_global(self): 51 | self.assertNickIsNotTagged('Global') 52 | 53 | def test_message_out_cb_send_tag_groupserv(self): 54 | self.assertNickIsNotTagged('GroupServ') 55 | 56 | def test_message_out_cb_send_tag_helpserv(self): 57 | self.assertNickIsNotTagged('HelpServ') 58 | 59 | def test_message_out_cb_send_tag_hostserv(self): 60 | self.assertNickIsNotTagged('HostServ') 61 | 62 | def test_message_out_cb_send_tag_infoserv(self): 63 | self.assertNickIsNotTagged('InfoServ') 64 | 65 | def test_message_out_cb_send_tag_memoserv(self): 66 | self.assertNickIsNotTagged('MemoServ') 67 | 68 | def test_message_out_cb_send_tag_nickserv(self): 69 | self.assertNickIsNotTagged('NickServ') 70 | 71 | def test_message_out_cb_send_tag_operserv(self): 72 | self.assertNickIsNotTagged('OperServ') 73 | 74 | def test_message_out_cb_send_tag_rpgserv(self): 75 | self.assertNickIsNotTagged('RpgServ') 76 | 77 | def test_message_out_cb_send_tag_statserv(self): 78 | self.assertNickIsNotTagged('StatServ') 79 | 80 | def test_message_out_cb_send_tag_saslserv(self): 81 | self.assertNickIsNotTagged('SaslServ') 82 | 83 | def test_message_out_cb_send_tag_star(self): 84 | self.assertNickIsNotTagged('*man') 85 | 86 | def test_message_out_cb_send_tag_ctcp(self): 87 | sys.modules['weechat'].config_options[ 88 | 'otr.policy.default.send_tag'] = 'on' 89 | 90 | result = weechat_otr.message_out_cb( 91 | None, None, 'server', 92 | ':nick!user@host PRIVMSG friend :\x01CTCP VERSION\x01') 93 | self.assertEqual( 94 | weechat_otr.PYVER.to_unicode(result), 95 | 'PRIVMSG friend :\x01CTCP VERSION\x01') 96 | 97 | def test_message_out_cb_no_send_tag_regex_non_ascii(self): 98 | sys.modules['weechat'].config_options[ 99 | 'otr.general.no_send_tag_regex'] = \ 100 | weechat_otr.PYVER.to_str('^gefährte$') 101 | 102 | self.assertNickIsNotTagged('gefährte') 103 | 104 | def test_message_out_cb_empty_no_send_tag_regex(self): 105 | sys.modules['weechat'].config_options[ 106 | 'otr.policy.default.send_tag'] = 'on' 107 | sys.modules['weechat'].config_options[ 108 | 'otr.general.no_send_tag_regex'] = weechat_otr.PYVER.to_str('') 109 | 110 | result = weechat_otr.message_out_cb( 111 | None, None, 'server', 112 | weechat_otr.PYVER.to_str(":nick!user@host PRIVMSG friend :hi")) 113 | self.assertEqual( 114 | weechat_otr.PYVER.to_unicode(result), 115 | "PRIVMSG friend :hi \t \t\t\t\t \t \t \t \t\t \t \t") 116 | 117 | def test_message_out_cb_nick_with_at(self): 118 | result = weechat_otr.message_out_cb( 119 | None, None, 'server', ':nick!user@host PRIVMSG @#chan :hello') 120 | self.assertEqual(result, ':nick!user@host PRIVMSG @#chan :hello') 121 | 122 | def test_require_encryption(self): 123 | sys.modules['weechat'].config_options.update({ 124 | 'otr.policy.default.require_encryption' : 'on' 125 | }) 126 | 127 | weechat_otr.message_out_cb( 128 | None, None, 'server', ':nick!user@host PRIVMSG nick2 :hello') 129 | 130 | self.assertPrinted( 131 | 'server_nick2_buffer', 132 | 'eval(${color:default}:! ${color:brown}otr${color:default} !:)\t' 133 | '(color lightred)Your message will not be sent, because policy ' 134 | 'requires an encrypted connection.') 135 | self.assertPrinted( 136 | 'server_nick2_buffer', 137 | 'eval(${color:default}:! ${color:brown}otr${color:default} !:)\t' 138 | '(color lightblue)Wait for the OTR connection or change the ' 139 | 'policy to allow clear-text messages:\r\n(color ' 140 | 'lightblue)/otr policy require_encryption off') 141 | 142 | def test_otr_disabled_require_encryption(self): 143 | sys.modules['weechat'].config_options.update({ 144 | 'otr.policy.default.allow_v2' : 'off', 145 | 'otr.policy.default.require_encryption' : 'on' 146 | }) 147 | 148 | weechat_otr.message_out_cb( 149 | None, None, 'server', ':nick!user@host PRIVMSG nick2 :hello') 150 | 151 | self.assertNotPrinted( 152 | 'server_nick2_buffer', 153 | 'eval(${color:default}:! ${color:brown}otr${color:default} !:)\t' 154 | '(color lightred)Your message will not be sent, because policy ' 155 | 'requires an encrypted connection.') 156 | self.assertNotPrinted( 157 | 'server_nick2_buffer', 158 | 'eval(${color:default}:! ${color:brown}otr${color:default} !:)\t' 159 | '(color lightblue)Wait for the OTR connection or change the ' 160 | 'policy to allow clear-text messages:\r\n(color ' 161 | 'lightblue)/policy set require_encryption off') 162 | 163 | def test_exception_raised_returns_empty_string(self): 164 | sys.modules['weechat'].info_get_hashtable_raise = Exception('test') 165 | 166 | result = weechat_otr.message_out_cb( 167 | None, None, 'server', ':nick!user@host PRIVMSG nick2 :hello') 168 | self.assertEqual(result, '') 169 | 170 | def test_exception_raised_prints_traceback(self): 171 | sys.modules['weechat'].info_get_hashtable_raise = Exception('test') 172 | 173 | weechat_otr.message_out_cb( 174 | None, None, 'server', ':nick!user@host PRIVMSG nick2 :hello') 175 | self.assertPrintedContains('', 'Exception: test') 176 | 177 | def test_exception_raised_prints_versions(self): 178 | sys.modules['weechat'].info_get_hashtable_raise = Exception('test') 179 | sys.modules['weechat'].infos[('',)]['version'] = '9.8.7' 180 | 181 | version_str = ( 182 | 'Versions: weechat-otr {script_version}, ' 183 | 'potr {potr_major}.{potr_minor}.{potr_patch}-{potr_sub}, ' 184 | 'Python {python_version}, ' 185 | 'WeeChat 9.8.7' 186 | ).format( 187 | script_version=weechat_otr.SCRIPT_VERSION, 188 | potr_major=potr.VERSION[0], 189 | potr_minor=potr.VERSION[1], 190 | potr_patch=potr.VERSION[2], 191 | potr_sub=potr.VERSION[3], 192 | python_version=platform.python_version()) 193 | 194 | weechat_otr.message_out_cb( 195 | None, None, 'server', ':nick!user@host PRIVMSG nick2 :hello') 196 | self.assertPrintedContains('', version_str) 197 | 198 | def assertNickIsNotTagged(self, nick): 199 | sys.modules['weechat'].config_options[ 200 | 'otr.policy.default.send_tag'] = 'on' 201 | 202 | result = weechat_otr.message_out_cb( 203 | None, None, 'server', weechat_otr.PYVER.to_str( 204 | ':nick!user@host PRIVMSG {nick} :send friend hi'.format( 205 | nick=nick))) 206 | self.assertEqual( 207 | weechat_otr.PYVER.to_unicode(result), 208 | 'PRIVMSG {nick} :send friend hi'.format(nick=nick)) 209 | -------------------------------------------------------------------------------- /weechat_otr_test/test_otr_session.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | # pylint: disable=too-many-public-methods 4 | 5 | from __future__ import unicode_literals 6 | 7 | import sys 8 | 9 | from weechat_otr_test.session_test_case import SessionTestCase 10 | 11 | import weechat_otr_test.recording_account 12 | import weechat_otr 13 | 14 | class OtrSessionTestCase(SessionTestCase): 15 | 16 | def test_otr_session(self): 17 | sys.modules['weechat'].set_server_current_nick('server', 'nick') 18 | 19 | account1 = weechat_otr_test.recording_account.RecordingAccount( 20 | 'nick@server') 21 | weechat_otr.ACCOUNTS['nick@server'] = account1 22 | 23 | account2 = weechat_otr_test.recording_account.RecordingAccount( 24 | 'nick2@server') 25 | weechat_otr.ACCOUNTS['nick2@server'] = account2 26 | 27 | context1 = account2.getContext('nick@server') 28 | context2 = account1.getContext('nick2@server') 29 | 30 | weechat_otr.message_in_cb( 31 | None, None, 'server', ':nick2!user@host PRIVMSG nick :?OTRv2?') 32 | 33 | sys.modules['weechat'].set_server_current_nick('server', 'nick2') 34 | self.send_all('nick', 'nick2', context2.injected) 35 | 36 | sys.modules['weechat'].set_server_current_nick('server', 'nick') 37 | self.send_all('nick2', 'nick', context1.injected) 38 | 39 | sys.modules['weechat'].set_server_current_nick('server', 'nick2') 40 | self.send_all('nick', 'nick2', context2.injected) 41 | 42 | sys.modules['weechat'].set_server_current_nick('server', 'nick') 43 | self.send_all('nick2', 'nick', context1.injected) 44 | 45 | self.assertTrue(context1.is_encrypted()) 46 | self.assertTrue(context2.is_encrypted()) 47 | 48 | weechat_otr.message_out_cb( 49 | None, None, 'server', ':nick!user@host PRIVMSG nick2 :hi') 50 | 51 | sys.modules['weechat'].set_server_current_nick('server', 'nick2') 52 | 53 | result = weechat_otr.message_in_cb( 54 | None, None, 'server', 55 | ':nick!user@host PRIVMSG nick2 :%s' % context2.injected.pop()) 56 | 57 | self.assertEqual(result, ':nick!user@host PRIVMSG nick2 :hi') 58 | 59 | def test_otr_session_non_ascii(self): 60 | sys.modules['weechat'].set_server_current_nick('server', 'gefährte') 61 | sys.modules['weechat'].infos.update({ 62 | (weechat_otr.PYVER.to_str('server,gefährte'),) : 63 | {'irc_buffer' : 'server_gefährte_buffer'}, 64 | (weechat_otr.PYVER.to_str('server,gefährte2'),) : 65 | {'irc_buffer' : 'server_gefährte2_buffer'}, 66 | }) 67 | sys.modules['weechat'].buffers.update({ 68 | weechat_otr.PYVER.to_str('server_gefährte_buffer') : { 69 | 'localvar_type' : 'private', 70 | 'localvar_channel' : 'gefährte', 71 | 'localvar_server' : 'server', 72 | 'name' : 'server_gefährte_buffer_name', 73 | 'plugin' : 'irc', 74 | }, 75 | weechat_otr.PYVER.to_str('server_gefährte2_buffer') : { 76 | 'localvar_type' : 'private', 77 | 'localvar_channel' : 'gefährte2', 78 | 'localvar_server' : 'server', 79 | 'name' : 'server_gefährte2_buffer_name', 80 | 'plugin' : 'irc', 81 | }, 82 | }) 83 | 84 | account1 = weechat_otr_test.recording_account.RecordingAccount( 85 | 'gefährte@server') 86 | weechat_otr.ACCOUNTS['gefährte@server'] = account1 87 | 88 | account2 = weechat_otr_test.recording_account.RecordingAccount( 89 | 'gefährte2@server') 90 | weechat_otr.ACCOUNTS['gefährte2@server'] = account2 91 | 92 | context1 = account2.getContext('gefährte@server') 93 | context2 = account1.getContext('gefährte2@server') 94 | 95 | weechat_otr.message_in_cb( 96 | None, None, 'server', 97 | ':gefährte2!user@host PRIVMSG gefährte :?OTRv2?') 98 | 99 | sys.modules['weechat'].set_server_current_nick('server', 'gefährte2') 100 | self.send_all('gefährte', 'gefährte2', context2.injected) 101 | 102 | sys.modules['weechat'].set_server_current_nick('server', 'gefährte') 103 | self.send_all('gefährte2', 'gefährte', context1.injected) 104 | 105 | sys.modules['weechat'].set_server_current_nick('server', 'gefährte2') 106 | self.send_all('gefährte', 'gefährte2', context2.injected) 107 | 108 | sys.modules['weechat'].set_server_current_nick('server', 'gefährte') 109 | self.send_all('gefährte2', 'gefährte', context1.injected) 110 | 111 | self.assertTrue(context1.is_encrypted()) 112 | self.assertTrue(context2.is_encrypted()) 113 | 114 | weechat_otr.message_out_cb( 115 | None, None, 'server', ':gefährte!user@host PRIVMSG gefährte2 :hi') 116 | 117 | sys.modules['weechat'].set_server_current_nick('server', 'gefährte2') 118 | 119 | result = weechat_otr.message_in_cb( 120 | None, None, 'server', 121 | ':gefährte!user@host PRIVMSG gefährte2 :%s' % \ 122 | context2.injected.pop()) 123 | 124 | self.assertEqual(result, weechat_otr.PYVER.to_str( 125 | ':gefährte!user@host PRIVMSG gefährte2 :hi')) 126 | 127 | def test_otr_change_my_nick_case(self): 128 | sys.modules['weechat'].set_server_current_nick('server', 'nick') 129 | 130 | account1 = weechat_otr_test.recording_account.RecordingAccount( 131 | 'nick@server') 132 | weechat_otr.ACCOUNTS['nick@server'] = account1 133 | 134 | account2 = weechat_otr_test.recording_account.RecordingAccount( 135 | 'nick2@server') 136 | weechat_otr.ACCOUNTS['nick2@server'] = account2 137 | 138 | context1 = account2.getContext('nick@server') 139 | context2 = account1.getContext('nick2@server') 140 | 141 | weechat_otr.message_in_cb( 142 | None, None, 'server', ':nick2!user@host PRIVMSG nick :?OTRv2?') 143 | 144 | sys.modules['weechat'].set_server_current_nick('server', 'nick2') 145 | self.send_all('nick', 'nick2', context2.injected) 146 | 147 | sys.modules['weechat'].set_server_current_nick('server', 'nick') 148 | self.send_all('nick2', 'nick', context1.injected) 149 | 150 | sys.modules['weechat'].set_server_current_nick('server', 'nick2') 151 | self.send_all('nick', 'nick2', context2.injected) 152 | 153 | sys.modules['weechat'].set_server_current_nick('server', 'nick') 154 | self.send_all('nick2', 'nick', context1.injected) 155 | 156 | self.assertTrue(context1.is_encrypted()) 157 | self.assertTrue(context2.is_encrypted()) 158 | 159 | sys.modules['weechat'].set_server_current_nick('server', 'NiCk') 160 | weechat_otr.message_out_cb( 161 | None, None, 'server', ':NiCk!user@host PRIVMSG nick2 :hi') 162 | 163 | sys.modules['weechat'].set_server_current_nick('server', 'nick2') 164 | 165 | result = weechat_otr.message_in_cb( 166 | None, None, 'server', 167 | ':NiCk!user@host PRIVMSG nick2 :%s' % context2.injected.pop()) 168 | 169 | self.assertEqual(result, ':NiCk!user@host PRIVMSG nick2 :hi') 170 | 171 | def test_otr_change_peer_nick_case(self): 172 | sys.modules['weechat'].set_server_current_nick('server', 'nick') 173 | 174 | account1 = weechat_otr_test.recording_account.RecordingAccount( 175 | 'nick@server') 176 | weechat_otr.ACCOUNTS['nick@server'] = account1 177 | 178 | account2 = weechat_otr_test.recording_account.RecordingAccount( 179 | 'nick2@server') 180 | weechat_otr.ACCOUNTS['nick2@server'] = account2 181 | 182 | context1 = account2.getContext('nick@server') 183 | context2 = account1.getContext('nick2@server') 184 | 185 | weechat_otr.message_in_cb( 186 | None, None, 'server', ':nick2!user@host PRIVMSG nick :?OTRv2?') 187 | 188 | sys.modules['weechat'].set_server_current_nick('server', 'nick2') 189 | self.send_all('nick', 'nick2', context2.injected) 190 | 191 | sys.modules['weechat'].set_server_current_nick('server', 'nick') 192 | self.send_all('nick2', 'nick', context1.injected) 193 | 194 | sys.modules['weechat'].set_server_current_nick('server', 'nick2') 195 | self.send_all('nick', 'nick2', context2.injected) 196 | 197 | sys.modules['weechat'].set_server_current_nick('server', 'nick') 198 | self.send_all('nick2', 'nick', context1.injected) 199 | 200 | self.assertTrue(context1.is_encrypted()) 201 | self.assertTrue(context2.is_encrypted()) 202 | 203 | weechat_otr.message_out_cb( 204 | None, None, 'server', ':nick!user@host PRIVMSG NiCk2 :hi') 205 | 206 | sys.modules['weechat'].set_server_current_nick('server', 'nick2') 207 | 208 | result = weechat_otr.message_in_cb( 209 | None, None, 'server', 210 | ':nick!user@host PRIVMSG nick2 :%s' % context2.injected.pop()) 211 | 212 | self.assertEqual(result, ':nick!user@host PRIVMSG nick2 :hi') 213 | -------------------------------------------------------------------------------- /weechat_otr_test/test_otr_statusbar_cb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | 10 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 11 | 12 | import weechat_otr_test.mock_window 13 | 14 | import weechat_otr 15 | 16 | class OtrStatusbarCbTestCase(WeechatOtrTestCase): 17 | 18 | def test_no_window_unencrypted(self): 19 | self.assertEqual( 20 | weechat_otr.otr_statusbar_cb(None, None, None), 21 | '(color default)OTR:(color lightred)!SEC(color default)') 22 | 23 | def test_window_unencrypted(self): 24 | window = weechat_otr_test.mock_window.MockWindow() 25 | sys.modules['weechat'].window_get_pointers[(window, 'buffer')] = None 26 | 27 | self.assertEqual( 28 | weechat_otr.otr_statusbar_cb(None, None, window), 29 | '(color default)OTR:(color lightred)!SEC(color default)') 30 | 31 | def test_buffer_not_private(self): 32 | window = weechat_otr_test.mock_window.MockWindow() 33 | sys.modules['weechat'].window_get_pointers[(window, 'buffer')] = \ 34 | 'non_private_buffer' 35 | 36 | self.assertEqual(weechat_otr.otr_statusbar_cb(None, None, window), '') 37 | 38 | def test_encrypted_authenticated_logged(self): 39 | context = self.setup_context('me@server', 'nick@server') 40 | context.encrypted = True 41 | context.verified = True 42 | context.logged = True 43 | 44 | self.assertEqual( 45 | weechat_otr.otr_statusbar_cb(None, None, None), 46 | '(color default)OTR:(color green)SEC(color default),(color green)' 47 | 'AUTH(color default),(color lightred)LOG(color default)') 48 | 49 | def test_encrypted_authenticated_not_logged(self): 50 | context = self.setup_context('me@server', 'nick@server') 51 | context.encrypted = True 52 | context.verified = True 53 | context.logged = False 54 | 55 | self.assertEqual( 56 | weechat_otr.otr_statusbar_cb(None, None, None), 57 | '(color default)OTR:(color green)SEC(color default),(color green)' 58 | 'AUTH(color default),(color green)!LOG(color default)') 59 | 60 | self.assertEqual(sys.modules['weechat'].buffer_sets, { 61 | None: { 62 | 'localvar_set_otr_encrypted': 'true', 63 | 'localvar_set_otr_authenticated': 'true', 64 | 'localvar_set_otr_logged': 'false', 65 | }}) 66 | 67 | def test_encrypted_unauthenticated_logged(self): 68 | context = self.setup_context('me@server', 'nick@server') 69 | context.encrypted = True 70 | context.verified = False 71 | context.logged = True 72 | 73 | self.assertEqual( 74 | weechat_otr.otr_statusbar_cb(None, None, None), 75 | '(color default)OTR:(color green)SEC(color default),' 76 | '(color lightred)!AUTH(color default),(color lightred)LOG' 77 | '(color default)') 78 | 79 | self.assertEqual(sys.modules['weechat'].buffer_sets, { 80 | None: { 81 | 'localvar_set_otr_encrypted': 'true', 82 | 'localvar_set_otr_authenticated': 'false', 83 | 'localvar_set_otr_logged': 'true', 84 | }}) 85 | 86 | def test_encrypted_unauthenticated_not_logged(self): 87 | context = self.setup_context('me@server', 'nick@server') 88 | context.encrypted = True 89 | context.verified = False 90 | context.logged = False 91 | 92 | self.assertEqual( 93 | weechat_otr.otr_statusbar_cb(None, None, None), 94 | '(color default)OTR:(color green)SEC(color default),' 95 | '(color lightred)!AUTH(color default),(color green)!LOG' 96 | '(color default)') 97 | 98 | self.assertEqual(sys.modules['weechat'].buffer_sets, { 99 | None: { 100 | 'localvar_set_otr_encrypted': 'true', 101 | 'localvar_set_otr_authenticated': 'false', 102 | 'localvar_set_otr_logged': 'false', 103 | }}) 104 | 105 | def test_unencrypted_unauthenticated_logged(self): 106 | context = self.setup_context('me@server', 'nick@server') 107 | context.encrypted = False 108 | context.verified = False 109 | context.logged = True 110 | 111 | self.assertEqual( 112 | weechat_otr.otr_statusbar_cb(None, None, None), 113 | '(color default)OTR:(color lightred)!SEC(color default)') 114 | 115 | self.assertEqual(sys.modules['weechat'].buffer_sets, { 116 | None: { 117 | 'localvar_set_otr_encrypted': 'false', 118 | 'localvar_set_otr_authenticated': 'false', 119 | 'localvar_set_otr_logged': 'true', 120 | }}) 121 | 122 | def test_unencrypted_unauthenticated_not_logged(self): 123 | context = self.setup_context('me@server', 'nick@server') 124 | context.encrypted = False 125 | context.verified = False 126 | context.logged = False 127 | 128 | self.assertEqual( 129 | weechat_otr.otr_statusbar_cb(None, None, None), 130 | '(color default)OTR:(color lightred)!SEC(color default)') 131 | 132 | self.assertEqual(sys.modules['weechat'].buffer_sets, { 133 | None: { 134 | 'localvar_set_otr_encrypted': 'false', 135 | 'localvar_set_otr_authenticated': 'false', 136 | 'localvar_set_otr_logged': 'false', 137 | }}) 138 | -------------------------------------------------------------------------------- /weechat_otr_test/test_parse_irc_privmsg.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | 10 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 11 | 12 | import weechat_otr 13 | 14 | class ParseIrcPrivmsgTestCase(WeechatOtrTestCase): 15 | 16 | def test_parse_irc_privmsg_nick(self): 17 | result = weechat_otr.parse_irc_privmsg( 18 | ':nick!user@host PRIVMSG nick2 :the message', 'server') 19 | 20 | self.assertEqual(result, { 21 | 'from': 'nick!user@host', 22 | 'from_nick': 'nick', 23 | 'to': 'nick2', 24 | 'to_channel': None, 25 | 'to_nick': 'nick2', 26 | 'text': 'the message' 27 | }) 28 | 29 | def test_parse_irc_privmsg_channel_ampersand(self): 30 | sys.modules['weechat'].infos[ 31 | ('server,CHANTYPES',)]['irc_server_isupport_value'] += '&' 32 | 33 | result = weechat_otr.parse_irc_privmsg( 34 | ':nick!user@host PRIVMSG &channel :test', 'server') 35 | self.assertEqual(result['to_channel'], '&channel') 36 | 37 | def test_parse_irc_privmsg_non_ascii(self): 38 | sys.modules['weechat'].info_hashtables = { 39 | 'irc_message_parse' : [{ 40 | 'arguments': weechat_otr.PYVER.to_str('nick2\xc3 :\xc3'), 41 | 'channel': weechat_otr.PYVER.to_str('nick2\xc3'), 42 | 'command': weechat_otr.PYVER.to_str('PRIVMSG'), 43 | 'host': weechat_otr.PYVER.to_str('nick\xc3!user@host'), 44 | 'nick': weechat_otr.PYVER.to_str('nick\xc3'), 45 | }] 46 | } 47 | 48 | result = weechat_otr.parse_irc_privmsg( 49 | ':nick\xc3!user@host PRIVMSG nick2\xc3 :\xc3', 'server') 50 | 51 | self.assertEqual(result, { 52 | 'from': 'nick\xc3!user@host', 53 | 'from_nick': 'nick\xc3', 54 | 'to': 'nick2\xc3', 55 | 'to_channel': None, 56 | 'to_nick': 'nick2\xc3', 57 | 'text': '\xc3' 58 | }) 59 | 60 | def test_parse_irc_privmsg_nick_no_colon(self): 61 | result = weechat_otr.parse_irc_privmsg( 62 | ':nick!user@host PRIVMSG nick2 test', 'server') 63 | 64 | self.assertEqual(result, { 65 | 'from': 'nick!user@host', 66 | 'from_nick': 'nick', 67 | 'to': 'nick2', 68 | 'to_channel': None, 69 | 'to_nick': 'nick2', 70 | 'text': 'test' 71 | }) 72 | 73 | def test_parse_irc_privmsg_nick_no_colon_spaces(self): 74 | result = weechat_otr.parse_irc_privmsg( 75 | ':nick!user@host PRIVMSG nick2 the message', 'server') 76 | 77 | self.assertEqual(result, { 78 | 'from': 'nick!user@host', 79 | 'from_nick': 'nick', 80 | 'to': 'nick2', 81 | 'to_channel': None, 82 | 'to_nick': 'nick2', 83 | 'text': 'the message' 84 | }) 85 | 86 | def test_parse_irc_privmsg_not_privmsg(self): 87 | with self.assertRaises(weechat_otr.PrivmsgParseException): 88 | weechat_otr.parse_irc_privmsg( 89 | ':nick!user@host OTHERCOMMAND nick2 the message', 'server') 90 | 91 | def test_parse_irc_privmsg_command_case_insensitive(self): 92 | result = weechat_otr.parse_irc_privmsg( 93 | ':nick!user@host privmsg nick2 :the message', 'server') 94 | 95 | self.assertEqual(result, { 96 | 'from': 'nick!user@host', 97 | 'from_nick': 'nick', 98 | 'to': 'nick2', 99 | 'to_channel': None, 100 | 'to_nick': 'nick2', 101 | 'text': 'the message' 102 | }) 103 | 104 | def test_parse_irc_privmsg_no_from(self): 105 | result = weechat_otr.parse_irc_privmsg( 106 | 'PRIVMSG nickserv :identify secret', 'server') 107 | 108 | self.assertEqual(result, { 109 | 'from': '', 110 | 'from_nick': '', 111 | 'to': 'nickserv', 112 | 'to_channel': None, 113 | 'to_nick': 'nickserv', 114 | 'text': 'identify secret' 115 | }) 116 | 117 | def test_parse_irc_privmsg_to_self(self): 118 | result = weechat_otr.parse_irc_privmsg( 119 | ':nick!user@host PRIVMSG nick :the message', 'server') 120 | 121 | self.assertEqual(result, { 122 | 'from': 'nick!user@host', 123 | 'from_nick': 'nick', 124 | 'to': 'nick', 125 | 'to_channel': None, 126 | 'to_nick': 'nick', 127 | 'text': 'the message' 128 | }) 129 | -------------------------------------------------------------------------------- /weechat_otr_test/test_policy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 9 | 10 | import weechat_otr 11 | 12 | class TestPolicy(WeechatOtrTestCase): 13 | 14 | def test_policy_default_server_nick_buffer(self): 15 | weechat_otr.command_cb(None, None, 'policy default') 16 | 17 | self.assertPrinted('server_nick_buffer', ( 18 | 'eval(${color:default}:! ${color:brown}otr${color:default} !:)\t' 19 | '(color default)' 20 | 'Current default OTR policies:\r\n' 21 | '(color default)' 22 | ' allow_v2 (allow OTR protocol version 2, effectively enable ' 23 | 'OTR since v2 is the only supported version) : on\r\n' 24 | '(color default)' 25 | ' html_escape (escape HTML special characters in outbound ' 26 | 'messages) : off\r\n' 27 | '(color default)' 28 | ' html_filter (filter HTML in incoming messages) : on\r\n' 29 | '(color default)' 30 | ' log (enable logging of OTR conversations) : off\r\n' 31 | '(color default)' 32 | ' require_encryption (refuse to send unencrypted messages when ' 33 | 'OTR is enabled) : off\r\n' 34 | '(color default)' 35 | ' send_tag (advertise your OTR capability using the whitespace ' 36 | 'tag) : off\r\n' 37 | '(color default)' 38 | 'Change default policies with: /otr policy default NAME on|off')) 39 | 40 | def test_policy_default_no_server_nick_buffer(self): 41 | weechat_otr.command_cb(None, 'non_private_buffer', 'policy default') 42 | 43 | self.assertPrinted('', ( 44 | 'Current default OTR policies:\n' 45 | ' allow_v2 (allow OTR protocol version 2, effectively enable ' 46 | 'OTR since v2 is the only supported version) : on\n' 47 | ' html_escape (escape HTML special characters in outbound ' 48 | 'messages) : off\n' 49 | ' html_filter (filter HTML in incoming messages) : on\n' 50 | ' log (enable logging of OTR conversations) : off\n' 51 | ' require_encryption (refuse to send unencrypted messages when ' 52 | 'OTR is enabled) : off\n' 53 | ' send_tag (advertise your OTR capability using the whitespace ' 54 | 'tag) : off\n' 55 | 'Change default policies with: /otr policy default NAME on|off')) 56 | -------------------------------------------------------------------------------- /weechat_otr_test/test_policy_account.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | 10 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 11 | 12 | import weechat_otr 13 | 14 | class TestPolicyAccount(WeechatOtrTestCase): 15 | 16 | def test_policy_account(self): 17 | account = weechat_otr.ACCOUNTS['nick@server'] 18 | 19 | self.assertEqual( 20 | account.getContext('friend@server').getPolicy('html_escape'), 21 | False) 22 | sys.modules['weechat'].config_options[ 23 | 'otr.policy.server.nick.html_escape'] = 'on' 24 | self.assertEqual( 25 | account.getContext('friend@server').getPolicy('html_escape'), 26 | True) 27 | 28 | def test_policy_account_non_ascii(self): 29 | account = weechat_otr.ACCOUNTS['gefährtenick@server'] 30 | 31 | self.assertEqual( 32 | account.getContext('gefährte@server').getPolicy('html_escape'), 33 | False) 34 | sys.modules['weechat'].config_options[weechat_otr.PYVER.to_str( 35 | 'otr.policy.server.gefährtenick.html_escape')] = 'on' 36 | self.assertEqual( 37 | account.getContext('gefährte@server').getPolicy('html_escape'), 38 | True) 39 | 40 | def test_policy_account_peer_override(self): 41 | account = weechat_otr.ACCOUNTS['nick@server'] 42 | 43 | self.assertEqual( 44 | account.getContext('friend@server').getPolicy('html_escape'), 45 | False) 46 | sys.modules['weechat'].config_options[ 47 | 'otr.policy.server.nick.html_escape'] = 'on' 48 | sys.modules['weechat'].config_options[ 49 | 'otr.policy.server.nick.friend.html_escape'] = 'off' 50 | self.assertEqual( 51 | account.getContext('friend@server').getPolicy('html_escape'), 52 | False) 53 | 54 | def test_policy_account_peer_override_non_ascii(self): 55 | account = weechat_otr.ACCOUNTS['gefährtenick@server'] 56 | 57 | self.assertEqual( 58 | account.getContext('gefährte@server').getPolicy('html_escape'), 59 | False) 60 | sys.modules['weechat'].config_options[weechat_otr.PYVER.to_str( 61 | 'otr.policy.server.gefährtenick.html_escape')] = 'on' 62 | sys.modules['weechat'].config_options[weechat_otr.PYVER.to_str( 63 | 'otr.policy.server.gefährtenick.gefährtenick.html_escape')] = 'off' 64 | self.assertEqual( 65 | account.getContext('gefährte@server').getPolicy('html_escape'), 66 | True) 67 | -------------------------------------------------------------------------------- /weechat_otr_test/test_policy_server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | 10 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 11 | 12 | import weechat_otr 13 | 14 | class TestPolicyServer(WeechatOtrTestCase): 15 | 16 | def test_policy_server(self): 17 | account = weechat_otr.ACCOUNTS['nick@server'] 18 | 19 | self.assertEqual( 20 | account.getContext('friend@server').getPolicy('html_escape'), 21 | False) 22 | sys.modules['weechat'].config_options[ 23 | 'otr.policy.server.html_escape'] = 'on' 24 | self.assertEqual( 25 | account.getContext('friend@server').getPolicy('html_escape'), 26 | True) 27 | 28 | def test_policy_server_account_override(self): 29 | account = weechat_otr.ACCOUNTS['nick@server'] 30 | 31 | self.assertEqual( 32 | account.getContext('friend@server').getPolicy('html_escape'), 33 | False) 34 | sys.modules['weechat'].config_options[ 35 | 'otr.policy.server.html_escape'] = 'on' 36 | sys.modules['weechat'].config_options[ 37 | 'otr.policy.server.nick.html_escape'] = 'off' 38 | self.assertEqual( 39 | account.getContext('friend@server').getPolicy('html_escape'), 40 | False) 41 | 42 | def test_policy_server_peer_override(self): 43 | account = weechat_otr.ACCOUNTS['nick@server'] 44 | 45 | self.assertEqual( 46 | account.getContext('friend@server').getPolicy('html_escape'), 47 | False) 48 | sys.modules['weechat'].config_options[ 49 | 'otr.policy.server.html_escape'] = 'on' 50 | sys.modules['weechat'].config_options[ 51 | 'otr.policy.server.nick.friend.html_escape'] = 'off' 52 | self.assertEqual( 53 | account.getContext('friend@server').getPolicy('html_escape'), 54 | False) 55 | -------------------------------------------------------------------------------- /weechat_otr_test/test_shutdown.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | # pylint: disable=too-many-public-methods 4 | # pylint: disable=invalid-name 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | 10 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 11 | import weechat_otr_test.mock_account 12 | 13 | import weechat_otr 14 | 15 | class ShutdownTestCase(WeechatOtrTestCase): 16 | 17 | def test_writes_config(self): 18 | weechat_otr.shutdown() 19 | self.assertEqual( 20 | [('config file', )], sys.modules['weechat'].config_written) 21 | 22 | def test_ends_all_private(self): 23 | account1 = weechat_otr_test.mock_account.MockAccount() 24 | account2 = weechat_otr_test.mock_account.MockAccount() 25 | 26 | weechat_otr.ACCOUNTS['account1'] = account1 27 | weechat_otr.ACCOUNTS['account2'] = account2 28 | 29 | weechat_otr.shutdown() 30 | self.assertEqual(account1.end_all_privates, 1) 31 | self.assertEqual(account2.end_all_privates, 1) 32 | 33 | def test_frees_config_section_options(self): 34 | weechat_otr.shutdown() 35 | 36 | sections = sorted([ 37 | ('color', ), 38 | ('policy', ), 39 | ('look', ), 40 | ('general', ) 41 | ]) 42 | 43 | self.assertEqual( 44 | sections, 45 | sorted(sys.modules['weechat'].config_section_free_options_calls)) 46 | 47 | def test_frees_config_sections(self): 48 | weechat_otr.shutdown() 49 | 50 | sections = sorted([ 51 | ('color', ), 52 | ('policy', ), 53 | ('look', ), 54 | ('general', ) 55 | ]) 56 | 57 | self.assertEqual( 58 | sections, sorted(sys.modules['weechat'].config_section_free_calls)) 59 | 60 | def test_frees_config_file(self): 61 | weechat_otr.shutdown() 62 | 63 | self.assertEqual( 64 | [('config file', )], sys.modules['weechat'].config_free_calls) 65 | 66 | def test_removes_bar_item(self): 67 | weechat_otr.shutdown() 68 | self.assertEqual( 69 | [('bar item', )], sys.modules['weechat'].bar_items_removed) 70 | 71 | def test_returns_ok(self): 72 | self.assertEqual( 73 | weechat_otr.shutdown(), sys.modules['weechat'].WEECHAT_RC_OK) 74 | -------------------------------------------------------------------------------- /weechat_otr_test/test_smp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 9 | 10 | import weechat_otr 11 | 12 | class SmpTestCase(WeechatOtrTestCase): 13 | 14 | def test_smp_ask_nick_server_question_secret(self): 15 | context = self.setup_context('nick@server', 'nick2@server') 16 | 17 | weechat_otr.command_cb( 18 | None, None, 'smp ask nick2 server question secret') 19 | 20 | self.assertEqual(('secret', 'question'), context.smp_init) 21 | 22 | def test_smp_ask_nick_server_secret(self): 23 | context = self.setup_context('nick@server', 'nick2@server') 24 | 25 | weechat_otr.command_cb( 26 | None, None, 'smp ask nick2 server secret') 27 | 28 | self.assertEqual(('secret', None), context.smp_init) 29 | 30 | def test_smp_ask_nick_server_secret_non_ascii(self): 31 | context = self.setup_context('nick@server', 'nick2@server') 32 | 33 | weechat_otr.command_cb( 34 | None, None, 35 | weechat_otr.PYVER.to_str('smp ask nick2 server motörhead')) 36 | 37 | self.assertEqual( 38 | (weechat_otr.PYVER.to_str('motörhead'), None), context.smp_init) 39 | 40 | def test_smp_ask_question_secret(self): 41 | context = self.setup_context('nick@server', 'nick2@server') 42 | 43 | weechat_otr.command_cb( 44 | None, 'server_nick2_buffer', 'smp ask question secret') 45 | 46 | self.assertEqual(('secret', 'question'), context.smp_init) 47 | 48 | def test_smp_ask_secret(self): 49 | context = self.setup_context('nick@server', 'nick2@server') 50 | 51 | weechat_otr.command_cb(None, 'server_nick2_buffer', 'smp ask secret') 52 | 53 | self.assertEqual(('secret', None), context.smp_init) 54 | 55 | def test_smp_ask_nick_server_question_secret_multiple_words(self): 56 | context = self.setup_context('nick@server', 'nick2@server') 57 | 58 | weechat_otr.command_cb( 59 | None, None, "smp ask nick2 server 'what is the secret?' " 60 | "'eastmost penninsula is the secret'") 61 | 62 | self.assertEqual( 63 | ('eastmost penninsula is the secret', 'what is the secret?'), 64 | context.smp_init) 65 | 66 | def test_smp_respond_secret(self): 67 | context = self.setup_context('nick@server', 'nick2@server') 68 | 69 | weechat_otr.command_cb( 70 | None, 'server_nick2_buffer', 'smp respond secret') 71 | 72 | self.assertEqual(('secret', ), context.smp_got_secret) 73 | 74 | def test_smp_respond_nick_server_secret(self): 75 | context = self.setup_context('nick@server', 'nick2@server') 76 | 77 | weechat_otr.command_cb( 78 | None, 'server_nick2_buffer', 'smp respond nick2 server secret') 79 | 80 | self.assertEqual(('secret', ), context.smp_got_secret) 81 | 82 | def test_smp_respond_secret_non_ascii(self): 83 | context = self.setup_context('nick@server', 'nick2@server') 84 | 85 | weechat_otr.command_cb( 86 | None, 'server_nick2_buffer', 87 | weechat_otr.PYVER.to_str('smp respond deathtöngue')) 88 | 89 | self.assertEqual( 90 | (weechat_otr.PYVER.to_str('deathtöngue'), ), 91 | context.smp_got_secret) 92 | 93 | def test_smp_abort(self): 94 | context = self.setup_context('nick@server', 'nick2@server') 95 | context.in_smp = True 96 | 97 | weechat_otr.command_cb(None, 'server_nick2_buffer', 'smp abort') 98 | 99 | self.assertEqual([('SMP aborted.',)], context.smp_finishes) 100 | -------------------------------------------------------------------------------- /weechat_otr_test/test_table_formatter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 9 | 10 | import weechat_otr 11 | 12 | 13 | class TableFormatterTestCase(WeechatOtrTestCase): 14 | 15 | def test_format(self): 16 | table_formatter = weechat_otr.TableFormatter() 17 | table_formatter.add_row(['a', 'b', 'c']) 18 | table_formatter.add_row(['a', 'bb', 'c']) 19 | table_formatter.add_row(['a', 'b', 'ccc']) 20 | 21 | self.assertEqual( 22 | table_formatter.format(), 23 | # pylint: disable=trailing-whitespace 24 | """a |b |c 25 | a |bb |c 26 | a |b |ccc""") 27 | -------------------------------------------------------------------------------- /weechat_otr_test/test_weechat_otr.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | 10 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 11 | 12 | import weechat_otr 13 | 14 | class WeechatOtrGeneralTestCase(WeechatOtrTestCase): 15 | 16 | def test_build_privmsg_in_without_newline(self): 17 | result = weechat_otr.build_privmsg_in('f', 't', 'line1') 18 | self.assertEqual(result, ':f PRIVMSG t :line1') 19 | 20 | def test_build_privmsg_in_with_newline(self): 21 | result = weechat_otr.build_privmsg_in('f', 't', 'line1\nline2') 22 | self.assertEqual(result, ':f PRIVMSG t :line1line2') 23 | 24 | def test_build_privmsgs_in_without_newline(self): 25 | fromm = 'f' 26 | to = 't' 27 | line = 'line1' 28 | result = weechat_otr.build_privmsgs_in(fromm, to, line) 29 | self.assertEqual( 30 | result, weechat_otr.build_privmsg_in(fromm, to, line)) 31 | 32 | def test_build_privmsgs_in_without_newline_prefix(self): 33 | fromm = 'f' 34 | to = 't' 35 | line = 'line1' 36 | prefix = 'Some prefix: ' 37 | result = weechat_otr.build_privmsgs_in(fromm, to, line, prefix) 38 | self.assertEqual( 39 | result, 40 | weechat_otr.build_privmsg_in( 41 | fromm, to, '{prefix}{line}'.format(prefix=prefix, line=line))) 42 | 43 | def test_build_privmsgs_in_with_newline(self): 44 | fromm = 'f' 45 | to = 't' 46 | result = weechat_otr.build_privmsgs_in(fromm, to, 'line1\nline2') 47 | self.assertEqual(result, '{msg1}\r\n{msg2}'.format( 48 | msg1=weechat_otr.build_privmsg_in(fromm, to, 'line1'), 49 | msg2=weechat_otr.build_privmsg_in(fromm, to, 'line2'))) 50 | 51 | def test_build_privmsgs_in_with_newline_prefix(self): 52 | fromm = 'f' 53 | to = 't' 54 | prefix = 'Some prefix: ' 55 | result = weechat_otr.build_privmsgs_in( 56 | fromm, to, 'line1\nline2', prefix) 57 | self.assertEqual(result, '{msg1}\r\n{msg2}'.format( 58 | msg1=weechat_otr.build_privmsg_in( 59 | fromm, to, '{0}line1'.format(prefix)), 60 | msg2=weechat_otr.build_privmsg_in( 61 | fromm, to, '{0}line2'.format(prefix)))) 62 | 63 | def test_build_privmsg_out_without_newline(self): 64 | result = weechat_otr.build_privmsg_out('t', 'line1') 65 | self.assertEqual(result, 'PRIVMSG t :line1') 66 | 67 | def test_build_privmsg_out_with_newline(self): 68 | result = weechat_otr.build_privmsg_out('t', 'line1\nline2') 69 | self.assertEqual(result, 'PRIVMSG t :line1\r\nPRIVMSG t :line2') 70 | 71 | def test_msg_irc_from_plain_action(self): 72 | result = weechat_otr.msg_irc_from_plain('/me does something') 73 | self.assertEqual(result, '\x01ACTION does something\x01') 74 | 75 | def test_msg_irc_from_plain_no_action(self): 76 | msg_no_action = 'just a message' 77 | self.assertEqual( 78 | weechat_otr.msg_irc_from_plain(msg_no_action), 79 | msg_no_action) 80 | 81 | def test_msg_irc_from_plain_action_invariant(self): 82 | msg_action = '\x01ACTION does something\x01' 83 | self.assertEqual( 84 | msg_action, 85 | weechat_otr.msg_irc_from_plain( 86 | weechat_otr.msg_plain_from_irc(msg_action))) 87 | 88 | def test_msg_plain_from_irc_action(self): 89 | result = weechat_otr.msg_plain_from_irc('\x01ACTION does something\x01') 90 | self.assertEqual(result, '/me does something') 91 | 92 | def test_msg_plain_from_irc_no_action(self): 93 | msg_no_action = 'just a message' 94 | self.assertEqual( 95 | weechat_otr.msg_plain_from_irc(msg_no_action), 96 | msg_no_action) 97 | 98 | def test_msg_plain_from_irc_action_invariant(self): 99 | msg_action = '/me does something' 100 | self.assertEqual( 101 | msg_action, 102 | weechat_otr.msg_plain_from_irc( 103 | weechat_otr.msg_irc_from_plain(msg_action))) 104 | 105 | def test_command_cb_start_send_tag_off(self): 106 | weechat_otr.command_cb(None, None, 'start') 107 | 108 | self.assertPrinted('server_nick_buffer', ( 109 | 'eval(${color:default}:! ${color:brown}otr${color:default} !:)\t' 110 | '(color lightblue)' 111 | 'Sending OTR query... Please await confirmation of the OTR ' 112 | 'session being started before sending a message.')) 113 | 114 | self.assertPrinted('server_nick_buffer', ( 115 | 'eval(${color:default}:! ${color:brown}otr${color:default} !:)\t' 116 | '(color lightblue)' 117 | 'To try OTR on all conversations with nick@server: /otr ' 118 | 'policy send_tag on')) 119 | 120 | def test_command_cb_start_send_tag_off_no_hints(self): 121 | sys.modules['weechat'].config_options[ 122 | 'otr.general.hints'] = 'off' 123 | weechat_otr.command_cb(None, None, 'start') 124 | 125 | self.assertNotPrinted('server_nick_buffer', ( 126 | 'eval(${color:default}:! ${color:brown}otr${color:default} !:)\t' 127 | '(color lightblue)' 128 | 'To try OTR on all conversations with nick@server: /otr ' 129 | 'policy send_tag on')) 130 | 131 | def test_command_cb_start_send_tag_off_with_hints(self): 132 | sys.modules['weechat'].config_options['otr.general.hints'] = 'on' 133 | weechat_otr.command_cb(None, None, 'start') 134 | 135 | self.assertPrinted('server_nick_buffer', ( 136 | 'eval(${color:default}:! ${color:brown}otr${color:default} !:)\t' 137 | '(color lightblue)' 138 | 'To try OTR on all conversations with nick@server: /otr ' 139 | 'policy send_tag on')) 140 | 141 | def test_command_cb_start_send_tag_on(self): 142 | sys.modules['weechat'].config_options[ 143 | 'otr.policy.server.nick.nick.send_tag'] = 'on' 144 | weechat_otr.command_cb(None, None, 'start') 145 | 146 | self.assertPrinted('server_nick_buffer', ( 147 | 'eval(${color:default}:! ${color:brown}otr${color:default} !:)\t' 148 | '(color lightblue)' 149 | 'Sending OTR query... Please await confirmation of the OTR ' 150 | 'session being started before sending a message.')) 151 | 152 | def test_irc_sanitize(self): 153 | result = weechat_otr.irc_sanitize( 154 | 'this\r\x00 is \r\n\rnot an i\n\x00rc command') 155 | self.assertEqual(result, 'this is not an irc command') 156 | 157 | def test_print_buffer_not_private(self): 158 | weechat_otr.command_cb(None, None, 'start no_window_nick server') 159 | self.assertPrinted('non_private_buffer', ( 160 | 'eval(${color:default}:! ${color:brown}otr${color:default} !:)\t' 161 | '(color lightblue)' 162 | '[no_window_nick] Sending OTR query... Please await confirmation ' 163 | 'of the OTR session being started before sending a message.')) 164 | -------------------------------------------------------------------------------- /weechat_otr_test/test_weechat_version_ok.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=invalid-name 3 | # pylint: disable=missing-docstring 4 | # pylint: disable=too-many-public-methods 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | 10 | from weechat_otr_test.weechat_otr_test_case import WeechatOtrTestCase 11 | 12 | import weechat_otr 13 | 14 | class WeechatVersionOkTestCase(WeechatOtrTestCase): 15 | 16 | def test_empty_version(self): 17 | sys.modules['weechat'].infos[('',)]['version_number'] = '' 18 | sys.modules['weechat'].infos[('',)]['version'] = '' 19 | self.assertFalse(weechat_otr.weechat_version_ok()) 20 | self.assertPrinted( 21 | '', 22 | 'otr requires WeeChat version >= 0.4.2. The current version is .') 23 | 24 | def test_too_low(self): 25 | sys.modules['weechat'].infos[('',)]['version_number'] = 0x00030800 26 | sys.modules['weechat'].infos[('',)]['version'] = '0.4.1' 27 | self.assertFalse(weechat_otr.weechat_version_ok()) 28 | self.assertPrinted( 29 | '', 30 | 'otr requires WeeChat version >= 0.4.2. The current version is ' 31 | '0.4.1.') 32 | 33 | def test_ok(self): 34 | sys.modules['weechat'].infos[('',)]['version_number'] = 0x01000100 35 | sys.modules['weechat'].infos[('',)]['version'] = '1.0.1' 36 | self.assertTrue(weechat_otr.weechat_version_ok()) 37 | self.assertNotPrinted( 38 | '', 39 | 'otr requires WeeChat version >= 0.4.2. The current version is ' 40 | '1.0.1.') 41 | 42 | def test_version_checked_during_setup(self): 43 | self.assertIn( 44 | ('version_number', ('',)), sys.modules['weechat'].info_gets) 45 | -------------------------------------------------------------------------------- /weechat_otr_test/weechat_otr_test_case.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | # pylint: disable=too-many-public-methods 4 | # pylint: disable=invalid-name 5 | 6 | from __future__ import unicode_literals 7 | 8 | import sys 9 | import tempfile 10 | import unittest 11 | 12 | import weechat_otr_test.mock_weechat 13 | sys.modules['weechat'] = weechat_otr_test.mock_weechat.MockWeechat( 14 | tempfile.mkdtemp()) 15 | 16 | # pylint: disable=wrong-import-position 17 | import weechat_otr 18 | 19 | class WeechatOtrTestCase(unittest.TestCase): 20 | 21 | def setUp(self): 22 | sys.modules['weechat'].save() 23 | weechat_otr.ACCOUNTS.clear() 24 | weechat_otr.otr_debug_buffer = None 25 | self.after_setup() 26 | 27 | def tearDown(self): 28 | sys.modules['weechat'].restore() 29 | self.after_teardown() 30 | 31 | def after_setup(self): 32 | pass 33 | 34 | def after_teardown(self): 35 | pass 36 | 37 | def assertPrinted(self, buf, text): 38 | self.assertIn(text, sys.modules['weechat'].printed[buf]) 39 | 40 | def assertPrintedContains(self, buf, regex): 41 | self.assertRegex( 42 | ''.join(sys.modules['weechat'].printed.get(buf, [])), 43 | regex) 44 | 45 | def assertNotPrinted(self, buf, text): 46 | self.assertNotIn(text, sys.modules['weechat'].printed.get(buf, [])) 47 | 48 | def assertNoPrintedContains(self, buf, text): 49 | for line in sys.modules['weechat'].printed.get(buf, []): 50 | self.assertNotIn(text, line) 51 | 52 | def assertRegex(self, s, r): 53 | # To get rid of deprecation warning in python 3. 54 | try: 55 | # Python 3. 56 | super(WeechatOtrTestCase, self).assertRegex(s, r) 57 | except AttributeError: 58 | # Python 2. 59 | self.assertRegexpMatches(s, r) 60 | 61 | def setup_context(self, account_name, context_name): 62 | # pylint: disable=no-self-use 63 | context = weechat_otr_test.mock_context.MockContext() 64 | account = weechat_otr_test.mock_account.MockAccount() 65 | account.add_context(context_name, context) 66 | weechat_otr.ACCOUNTS[account_name] = account 67 | 68 | return context 69 | --------------------------------------------------------------------------------