diff --git a/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.blacklisted_devices b/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.blacklisted_devices deleted file mode 100644 index e69de29..0000000 diff --git a/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.db b/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.db deleted file mode 100644 index 1b82fe5..0000000 Binary files a/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.db and /dev/null differ diff --git a/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.ignored_devices b/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.ignored_devices deleted file mode 100644 index 7b2eba9..0000000 --- a/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.ignored_devices +++ /dev/null @@ -1,2 +0,0 @@ -@wymiller:matrix.wyattjmiller.com TXKANGNDNI matrix-ed25519 KGLDDeAaSEFFC4Q9Zuu2hnJIq1r2GwJNY90y4QCANkk -@wymiller:matrix.wyattjmiller.com JPNMWZPYIT matrix-ed25519 c11Z1Db3MD5KfmDAIjOYwL+vi6ORIH/iIxf+TBi6HUA diff --git a/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.trusted_devices b/weechat/matrix/wyattjmiller/@wymiller:matrix.wyattjmiller.com_OPEWNFFIWC.trusted_devices deleted file mode 100644 index e69de29..0000000 diff --git a/weechat/matrix/wyattjmiller/wymiller.device_id b/weechat/matrix/wyattjmiller/wymiller.device_id deleted file mode 100644 index 6c7ef33..0000000 --- a/weechat/matrix/wyattjmiller/wymiller.device_id +++ /dev/null @@ -1 +0,0 @@ -OPEWNFFIWC \ No newline at end of file diff --git a/weechat/python/autoload/_weechat.py b/weechat/python/autoload/_weechat.py deleted file mode 120000 index 2dbe8e5..0000000 --- a/weechat/python/autoload/_weechat.py +++ /dev/null @@ -1 +0,0 @@ -/home/wyatt/.weechat/python/matrix/_weechat.py \ No newline at end of file diff --git a/weechat/python/matrix.py b/weechat/python/matrix.py deleted file mode 100644 index ba2eb85..0000000 --- a/weechat/python/matrix.py +++ /dev/null @@ -1,710 +0,0 @@ -# -*- coding: utf-8 -*- - -# Weechat Matrix Protocol Script -# Copyright © 2018 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals - -import os - -# See if there is a `venv` directory next to our script, and use that if -# present. This first resolves symlinks, so this also works when we are -# loaded through a symlink (e.g. from autoload). -# See https://virtualenv.pypa.io/en/latest/userguide/#using-virtualenv-without-bin-python -# This does not support pyvenv or the python3 venv module, which do not -# create an activate_this.py: https://stackoverflow.com/questions/27462582 -activate_this = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'venv', 'bin', 'activate_this.py') -if os.path.exists(activate_this): - exec(open(activate_this).read(), {'__file__': activate_this}) - -import socket -import ssl -import textwrap -# pylint: disable=redefined-builtin -from builtins import str -from itertools import chain -# pylint: disable=unused-import -from typing import Any, AnyStr, Deque, Dict, List, Optional, Set, Text, Tuple - -import logbook -import json -import OpenSSL.crypto as crypto -from future.utils import bytes_to_native_str as n -from logbook import Logger, StreamHandler - -try: - from json.decoder import JSONDecodeError -except ImportError: - JSONDecodeError = ValueError # type: ignore - - -from nio import RemoteProtocolError, RemoteTransportError, TransportType - -from matrix import globals as G -from matrix.bar_items import ( - init_bar_items, - matrix_bar_item_buffer_modes, - matrix_bar_item_lag, - matrix_bar_item_name, - matrix_bar_item_plugin, - matrix_bar_nicklist_count, - matrix_bar_typing_notices_cb -) -from matrix.buffer import room_buffer_close_cb, room_buffer_input_cb -# Weechat searches for the registered callbacks in the scope of the main script -# file, import the callbacks here so weechat can find them. -from matrix.commands import (hook_commands, hook_key_bindings, hook_page_up, - matrix_command_buf_clear_cb, matrix_command_cb, - matrix_command_pgup_cb, matrix_invite_command_cb, - matrix_join_command_cb, matrix_kick_command_cb, - matrix_me_command_cb, matrix_part_command_cb, - matrix_redact_command_cb, matrix_topic_command_cb, - matrix_olm_command_cb, matrix_devices_command_cb, - matrix_room_command_cb, matrix_uploads_command_cb, - matrix_upload_command_cb, matrix_send_anyways_cb, - matrix_reply_command_cb, - matrix_cursor_reply_signal_cb) -from matrix.completion import (init_completion, matrix_command_completion_cb, - matrix_debug_completion_cb, - matrix_message_completion_cb, - matrix_olm_device_completion_cb, - matrix_olm_user_completion_cb, - matrix_server_command_completion_cb, - matrix_server_completion_cb, - matrix_user_completion_cb, - matrix_own_devices_completion_cb, - matrix_room_completion_cb) -from matrix.config import (MatrixConfig, config_log_category_cb, - config_log_level_cb, config_server_buffer_cb, - matrix_config_reload_cb, config_pgup_cb) -from matrix.globals import SCRIPT_NAME, SERVERS, W -from matrix.server import (MatrixServer, create_default_server, - matrix_config_server_change_cb, - matrix_config_server_read_cb, - matrix_config_server_write_cb, matrix_timer_cb, - send_cb, matrix_load_users_cb) -from matrix.utf import utf8_decode -from matrix.utils import server_buffer_prnt, server_buffer_set_title - -from matrix.uploads import UploadsBuffer, upload_cb - -try: - from urllib.parse import urlunparse -except ImportError: - from urlparse import urlunparse - -# yapf: disable -WEECHAT_SCRIPT_NAME = SCRIPT_NAME -WEECHAT_SCRIPT_DESCRIPTION = "matrix chat plugin" # type: str -WEECHAT_SCRIPT_AUTHOR = "Damir Jelić " # type: str -WEECHAT_SCRIPT_VERSION = "0.2.0" # type: str -WEECHAT_SCRIPT_LICENSE = "ISC" # type: str -# yapf: enable - - -logger = Logger("matrix-cli") - - -def print_certificate_info(buff, sock, cert): - cert_pem = ssl.DER_cert_to_PEM_cert(sock.getpeercert(True)) - - x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem) - - public_key = x509.get_pubkey() - - key_type = ("RSA" if public_key.type() == crypto.TYPE_RSA else "DSA") - key_size = str(public_key.bits()) - sha256_fingerprint = x509.digest(n(b"SHA256")) - sha1_fingerprint = x509.digest(n(b"SHA1")) - signature_algorithm = x509.get_signature_algorithm() - - key_info = ("key info: {key_type} key {bits} bits, signed using " - "{algo}").format( - key_type=key_type, bits=key_size, - algo=n(signature_algorithm)) - - validity_info = (" Begins on: {before}\n" - " Expires on: {after}").format( - before=cert["notBefore"], after=cert["notAfter"]) - - rdns = chain(*cert["subject"]) - subject = ", ".join(["{}={}".format(name, value) for name, value in rdns]) - - rdns = chain(*cert["issuer"]) - issuer = ", ".join(["{}={}".format(name, value) for name, value in rdns]) - - subject = "subject: {sub}, serial number {serial}".format( - sub=subject, serial=cert["serialNumber"]) - - issuer = "issuer: {issuer}".format(issuer=issuer) - - fingerprints = (" SHA1: {}\n" - " SHA256: {}").format(n(sha1_fingerprint), - n(sha256_fingerprint)) - - wrapper = textwrap.TextWrapper( - initial_indent=" - ", subsequent_indent=" ") - - message = ("{prefix}matrix: received certificate\n" - " - certificate info:\n" - "{subject}\n" - "{issuer}\n" - "{key_info}\n" - " - period of validity:\n{validity_info}\n" - " - fingerprints:\n{fingerprints}").format( - prefix=W.prefix("network"), - subject=wrapper.fill(subject), - issuer=wrapper.fill(issuer), - key_info=wrapper.fill(key_info), - validity_info=validity_info, - fingerprints=fingerprints) - - W.prnt(buff, message) - - -def wrap_socket(server, file_descriptor): - # type: (MatrixServer, int) -> None - sock = None # type: socket.socket - - temp_socket = socket.fromfd(file_descriptor, socket.AF_INET, - socket.SOCK_STREAM) - - # fromfd() duplicates the file descriptor, we can close the one we got from - # weechat now since we use the one from our socket when calling hook_fd() - os.close(file_descriptor) - - # For python 2.7 wrap_socket() doesn't work with sockets created from an - # file descriptor because fromfd() doesn't return a wrapped socket, the bug - # was fixed for python 3, more info: https://bugs.python.org/issue13942 - # pylint: disable=protected-access,unidiomatic-typecheck - if type(temp_socket) == socket._socket.socket: - # pylint: disable=no-member - sock = socket._socketobject(_sock=temp_socket) - else: - sock = temp_socket - - # fromfd() duplicates the file descriptor but doesn't retain it's blocking - # non-blocking attribute, so mark the socket as non-blocking even though - # weechat already did that for us - sock.setblocking(False) - - message = "{prefix}matrix: Doing SSL handshake...".format( - prefix=W.prefix("network")) - W.prnt(server.server_buffer, message) - - ssl_socket = server.ssl_context.wrap_socket( - sock, do_handshake_on_connect=False, - server_hostname=server.address) # type: ssl.SSLSocket - - server.socket = ssl_socket - - try_ssl_handshake(server) - - -@utf8_decode -def ssl_fd_cb(server_name, file_descriptor): - server = SERVERS[server_name] - - if server.ssl_hook: - W.unhook(server.ssl_hook) - server.ssl_hook = None - - try_ssl_handshake(server) - - return W.WEECHAT_RC_OK - - -def try_ssl_handshake(server): - sock = server.socket - - while True: - try: - sock.do_handshake() - - cipher = sock.cipher() - cipher_message = ("{prefix}matrix: Connected using {tls}, and " - "{bit} bit {cipher} cipher suite.").format( - prefix=W.prefix("network"), - tls=cipher[1], - bit=cipher[2], - cipher=cipher[0]) - W.prnt(server.server_buffer, cipher_message) - - cert = sock.getpeercert() - if cert: - print_certificate_info(server.server_buffer, sock, cert) - - finalize_connection(server) - - return True - - except ssl.SSLWantReadError: - hook = W.hook_fd(server.socket.fileno(), 1, 0, 0, "ssl_fd_cb", - server.name) - server.ssl_hook = hook - - return False - - except ssl.SSLWantWriteError: - hook = W.hook_fd(server.socket.fileno(), 0, 1, 0, "ssl_fd_cb", - server.name) - server.ssl_hook = hook - - return False - - except (ssl.SSLError, ssl.CertificateError, socket.error) as error: - try: - str_error = error.reason if error.reason else "Unknown error" - except AttributeError: - str_error = str(error) - - message = ("{prefix}Error while doing SSL handshake" - ": {error}").format( - prefix=W.prefix("network"), error=str_error) - - server_buffer_prnt(server, message) - - server_buffer_prnt( - server, ("{prefix}matrix: disconnecting from server..." - ).format(prefix=W.prefix("network"))) - - server.disconnect() - return False - - -@utf8_decode -def receive_cb(server_name, file_descriptor): - server = SERVERS[server_name] - - while True: - try: - data = server.socket.recv(4096) - except ssl.SSLWantReadError: - break - except socket.error as error: - errno = "error" + str(error.errno) + " " if error.errno else "" - str_error = error.strerror if error.strerror else "Unknown error" - str_error = errno + str_error - - message = ("{prefix}Error while reading from " - "socket: {error}").format( - prefix=W.prefix("network"), error=str_error) - - server_buffer_prnt(server, message) - - server_buffer_prnt( - server, ("{prefix}matrix: disconnecting from server..." - ).format(prefix=W.prefix("network"))) - - server.disconnect() - - return W.WEECHAT_RC_OK - - if not data: - server_buffer_prnt( - server, - "{prefix}matrix: Error while reading from socket".format( - prefix=W.prefix("network"))) - server_buffer_prnt( - server, ("{prefix}matrix: disconnecting from server..." - ).format(prefix=W.prefix("network"))) - - server.disconnect() - break - - try: - server.client.receive(data) - except (RemoteTransportError, RemoteProtocolError) as e: - server.error(str(e)) - server.disconnect() - break - - response = server.client.next_response() - - # Check if we need to send some data back - data_to_send = server.client.data_to_send() - - if data_to_send: - server.send(data_to_send) - - if response: - server.handle_response(response) - break - - return W.WEECHAT_RC_OK - - -def finalize_connection(server): - hook = W.hook_fd( - server.socket.fileno(), - 1, - 0, - 0, - "receive_cb", - server.name - ) - - server.fd_hook = hook - server.connected = True - server.connecting = False - server.reconnect_delay = 0 - - negotiated_protocol = (server.socket.selected_alpn_protocol() or - server.socket.selected_npn_protocol()) - - if negotiated_protocol == "h2": - server.transport_type = TransportType.HTTP2 - else: - server.transport_type = TransportType.HTTP - - data = server.client.connect(server.transport_type) - server.send(data) - - server.login_info() - - -@utf8_decode -def sso_login_cb(server_name, command, return_code, out, err): - try: - server = SERVERS[server_name] - except KeyError: - message = ( - "{}{}: SSO callback ran, but no server for it was found.").format( - W.prefix("error"), SCRIPT_NAME) - W.prnt("", message) - - if return_code == W.WEECHAT_HOOK_PROCESS_ERROR: - server.error("Error while running the matrix_sso_helper. Please " - "make sure that the helper script is executable and can " - "be found in your PATH.") - server.sso_hook = None - server.disconnect() - return W.WEECHAT_RC_OK - - # The child process exited mark the hook as done. - if return_code == 0: - server.sso_hook = None - - if err != "": - W.prnt("", "stderr: %s" % err) - - if out == "": - return W.WEECHAT_RC_OK - - try: - ret = json.loads(out) - msgtype = ret.get("type") - - if msgtype == "redirectUrl": - redirect_url = "http://{}:{}".format(ret["host"], ret["port"]) - - login_url = ( - "{}/_matrix/client/r0/login/sso/redirect?redirectUrl={}" - ).format(server.homeserver.geturl(), redirect_url) - - server.info_highlight( - "The server requested a single sign-on, please open " - "this URL in your browser. Note that the " - "browser needs to run on the same host as Weechat.") - server.info_highlight(login_url) - - message = { - "server": server.name, - "url": login_url - } - W.hook_hsignal_send("matrix_sso_login", message) - - elif msgtype == "token": - token = ret["loginToken"] - server.login(token=token) - - elif msgtype == "error": - server.error("Error in the SSO helper {}".format(ret["message"])) - - else: - server.error("Unknown SSO login message received from child " - "process.") - - except JSONDecodeError: - server.error( - "Error decoding SSO login message from child process: {}".format( - out - )) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def connect_cb(data, status, gnutls_rc, sock, error, ip_address): - # pylint: disable=too-many-arguments,too-many-branches - status_value = int(status) # type: int - server = SERVERS[data] - - if status_value == W.WEECHAT_HOOK_CONNECT_OK: - file_descriptor = int(sock) # type: int - server.numeric_address = ip_address - server_buffer_set_title(server) - - wrap_socket(server, file_descriptor) - - return W.WEECHAT_RC_OK - - elif status_value == W.WEECHAT_HOOK_CONNECT_ADDRESS_NOT_FOUND: - server.error('{address} not found'.format(address=ip_address)) - - elif status_value == W.WEECHAT_HOOK_CONNECT_IP_ADDRESS_NOT_FOUND: - server.error('IP address not found') - - elif status_value == W.WEECHAT_HOOK_CONNECT_CONNECTION_REFUSED: - server.error('Connection refused') - - elif status_value == W.WEECHAT_HOOK_CONNECT_PROXY_ERROR: - server.error('Proxy fails to establish connection to server') - - elif status_value == W.WEECHAT_HOOK_CONNECT_LOCAL_HOSTNAME_ERROR: - server.error('Unable to set local hostname') - - elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_INIT_ERROR: - server.error('TLS init error') - - elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_HANDSHAKE_ERROR: - server.error('TLS Handshake failed') - - elif status_value == W.WEECHAT_HOOK_CONNECT_MEMORY_ERROR: - server.error('Not enough memory') - - elif status_value == W.WEECHAT_HOOK_CONNECT_TIMEOUT: - server.error('Timeout') - - elif status_value == W.WEECHAT_HOOK_CONNECT_SOCKET_ERROR: - server.error('Unable to create socket') - else: - server.error('Unexpected error: {status}'.format(status=status_value)) - - server.disconnect(reconnect=True) - return W.WEECHAT_RC_OK - - -@utf8_decode -def room_close_cb(data, buffer): - W.prnt("", - "Buffer '%s' will be closed!" % W.buffer_get_string(buffer, "name")) - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_unload_cb(): - for server in SERVERS.values(): - server.config.free() - - G.CONFIG.free() - - return W.WEECHAT_RC_OK - - -def autoconnect(servers): - for server in servers.values(): - if server.config.autoconnect: - server.connect() - - -def debug_buffer_close_cb(data, buffer): - G.CONFIG.debug_buffer = "" - return W.WEECHAT_RC_OK - - -def server_buffer_cb(server_name, buffer, input_data): - message = ("{}{}: this buffer is not a room buffer!").format( - W.prefix("error"), SCRIPT_NAME) - W.prnt(buffer, message) - return W.WEECHAT_RC_OK - - -class WeechatHandler(StreamHandler): - def __init__(self, level=logbook.NOTSET, format_string=None, filter=None, - bubble=False): - StreamHandler.__init__( - self, - object(), - level, - format_string, - None, - filter, - bubble - ) - - def write(self, item): - buf = "" - - if G.CONFIG.network.debug_buffer: - if not G.CONFIG.debug_buffer: - G.CONFIG.debug_buffer = W.buffer_new( - "Matrix Debug", "", "", "debug_buffer_close_cb", "") - - buf = G.CONFIG.debug_buffer - - W.prnt(buf, item) - - -def buffer_switch_cb(_, _signal, buffer_ptr): - """Do some buffer operations when we switch buffers. - - This function is called every time we switch a buffer. The pointer of - the new buffer is given to us by weechat. - - If it is one of our room buffers we check if the members for the room - aren't fetched and fetch them now if they aren't. - - Read receipts are send out from here as well. - """ - for server in SERVERS.values(): - if buffer_ptr == server.server_buffer: - return W.WEECHAT_RC_OK - - if buffer_ptr not in server.buffers.values(): - continue - - room_buffer = server.find_room_from_ptr(buffer_ptr) - if not room_buffer: - continue - - last_event_id = room_buffer.last_event_id - - if room_buffer.should_send_read_marker: - # A buffer may not have any events, in that case no event id is - # here returned - if last_event_id: - server.room_send_read_marker( - room_buffer.room.room_id, last_event_id) - room_buffer.last_read_event = last_event_id - - if not room_buffer.members_fetched: - room_id = room_buffer.room.room_id - server.get_joined_members(room_id) - - # The buffer is empty and we are seeing it for the first time. - # Let us fetch some messages from the room history so it doesn't feel so - # empty. - if room_buffer.first_view and room_buffer.weechat_buffer.num_lines < 10: - # TODO we may want to fetch 10 - num_lines messages here for - # consistency reasons. - server.room_get_messages(room_buffer.room.room_id) - - break - - return W.WEECHAT_RC_OK - - -def typing_notification_cb(data, signal, buffer_ptr): - """Send out typing notifications if the user is typing. - - This function is called every time the input text is changed. - It checks if we are on a buffer we own, and if we are sends out a typing - notification if the room is configured to send them out. - """ - for server in SERVERS.values(): - room_buffer = server.find_room_from_ptr(buffer_ptr) - if room_buffer: - server.room_send_typing_notice(room_buffer) - return W.WEECHAT_RC_OK - - if buffer_ptr == server.server_buffer: - return W.WEECHAT_RC_OK - - return W.WEECHAT_RC_OK - - -def buffer_command_cb(data, _, command): - """Override the buffer command to allow switching buffers by short name.""" - command = command[7:].strip() - - buffer_subcommands = ["list", "add", "clear", "move", "swap", "cycle", - "merge", "unmerge", "hide", "unhide", "renumber", - "close", "notify", "localvar", "set", "get"] - - if not command: - return W.WEECHAT_RC_OK - - command_words = command.split() - - if len(command_words) >= 1: - if command_words[0] in buffer_subcommands: - return W.WEECHAT_RC_OK - - elif command_words[0].startswith("*"): - return W.WEECHAT_RC_OK - - try: - int(command_words[0]) - return W.WEECHAT_RC_OK - except ValueError: - pass - - room_buffers = [] - - for server in SERVERS.values(): - room_buffers.extend(server.room_buffers.values()) - - sorted_buffers = sorted( - room_buffers, - key=lambda b: b.weechat_buffer.number - ) - - for room_buffer in sorted_buffers: - buffer = room_buffer.weechat_buffer - - if command in buffer.short_name: - displayed = W.current_buffer() == buffer._ptr - - if displayed: - continue - - W.buffer_set(buffer._ptr, 'display', '1') - return W.WEECHAT_RC_OK_EAT - - return W.WEECHAT_RC_OK - - -if __name__ == "__main__": - if W.register(WEECHAT_SCRIPT_NAME, WEECHAT_SCRIPT_AUTHOR, - WEECHAT_SCRIPT_VERSION, WEECHAT_SCRIPT_LICENSE, - WEECHAT_SCRIPT_DESCRIPTION, 'matrix_unload_cb', ''): - - if not W.mkdir_home("matrix", 0o700): - message = ("{prefix}matrix: Error creating session " - "directory").format(prefix=W.prefix("error")) - W.prnt("", message) - - handler = WeechatHandler() - handler.format_string = "{record.channel}: {record.message}" - handler.push_application() - - # TODO if this fails we should abort and unload the script. - G.CONFIG = MatrixConfig() - G.CONFIG.read() - - hook_commands() - hook_key_bindings() - init_bar_items() - init_completion() - - W.hook_command_run("/buffer", "buffer_command_cb", "") - W.hook_signal("buffer_switch", "buffer_switch_cb", "") - W.hook_signal("input_text_changed", "typing_notification_cb", "") - - if not SERVERS: - create_default_server(G.CONFIG) - - autoconnect(SERVERS) diff --git a/weechat/python/matrix/__init__.py b/weechat/python/matrix/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/weechat/python/matrix/__pycache__/__init__.cpython-39.pyc b/weechat/python/matrix/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index ac03e49..0000000 Binary files a/weechat/python/matrix/__pycache__/__init__.cpython-39.pyc and /dev/null differ diff --git a/weechat/python/matrix/__pycache__/bar_items.cpython-39.pyc b/weechat/python/matrix/__pycache__/bar_items.cpython-39.pyc deleted file mode 100644 index b8c8608..0000000 Binary files a/weechat/python/matrix/__pycache__/bar_items.cpython-39.pyc and /dev/null differ diff --git a/weechat/python/matrix/__pycache__/buffer.cpython-39.pyc b/weechat/python/matrix/__pycache__/buffer.cpython-39.pyc deleted file mode 100644 index 202b3f9..0000000 Binary files a/weechat/python/matrix/__pycache__/buffer.cpython-39.pyc and /dev/null differ diff --git a/weechat/python/matrix/__pycache__/colors.cpython-39.pyc b/weechat/python/matrix/__pycache__/colors.cpython-39.pyc deleted file mode 100644 index 294fc64..0000000 Binary files a/weechat/python/matrix/__pycache__/colors.cpython-39.pyc and /dev/null differ diff --git a/weechat/python/matrix/__pycache__/commands.cpython-39.pyc b/weechat/python/matrix/__pycache__/commands.cpython-39.pyc deleted file mode 100644 index 4a9534e..0000000 Binary files a/weechat/python/matrix/__pycache__/commands.cpython-39.pyc and /dev/null differ diff --git a/weechat/python/matrix/__pycache__/completion.cpython-39.pyc b/weechat/python/matrix/__pycache__/completion.cpython-39.pyc deleted file mode 100644 index cae2c27..0000000 Binary files a/weechat/python/matrix/__pycache__/completion.cpython-39.pyc and /dev/null differ diff --git a/weechat/python/matrix/__pycache__/config.cpython-39.pyc b/weechat/python/matrix/__pycache__/config.cpython-39.pyc deleted file mode 100644 index a00ad5e..0000000 Binary files a/weechat/python/matrix/__pycache__/config.cpython-39.pyc and /dev/null differ diff --git a/weechat/python/matrix/__pycache__/globals.cpython-39.pyc b/weechat/python/matrix/__pycache__/globals.cpython-39.pyc deleted file mode 100644 index dcac796..0000000 Binary files a/weechat/python/matrix/__pycache__/globals.cpython-39.pyc and /dev/null differ diff --git a/weechat/python/matrix/__pycache__/message_renderer.cpython-39.pyc b/weechat/python/matrix/__pycache__/message_renderer.cpython-39.pyc deleted file mode 100644 index 93eed0b..0000000 Binary files a/weechat/python/matrix/__pycache__/message_renderer.cpython-39.pyc and /dev/null differ diff --git a/weechat/python/matrix/__pycache__/server.cpython-39.pyc b/weechat/python/matrix/__pycache__/server.cpython-39.pyc deleted file mode 100644 index 0ffeb92..0000000 Binary files a/weechat/python/matrix/__pycache__/server.cpython-39.pyc and /dev/null differ diff --git a/weechat/python/matrix/__pycache__/uploads.cpython-39.pyc b/weechat/python/matrix/__pycache__/uploads.cpython-39.pyc deleted file mode 100644 index 0065fd9..0000000 Binary files a/weechat/python/matrix/__pycache__/uploads.cpython-39.pyc and /dev/null differ diff --git a/weechat/python/matrix/__pycache__/utf.cpython-39.pyc b/weechat/python/matrix/__pycache__/utf.cpython-39.pyc deleted file mode 100644 index 992a4dc..0000000 Binary files a/weechat/python/matrix/__pycache__/utf.cpython-39.pyc and /dev/null differ diff --git a/weechat/python/matrix/__pycache__/utils.cpython-39.pyc b/weechat/python/matrix/__pycache__/utils.cpython-39.pyc deleted file mode 100644 index fc140cc..0000000 Binary files a/weechat/python/matrix/__pycache__/utils.cpython-39.pyc and /dev/null differ diff --git a/weechat/python/matrix/_weechat.py b/weechat/python/matrix/_weechat.py deleted file mode 100644 index 054c63d..0000000 --- a/weechat/python/matrix/_weechat.py +++ /dev/null @@ -1,260 +0,0 @@ -import datetime -import random -import string - -WEECHAT_BASE_COLORS = { - "black": "0", - "red": "1", - "green": "2", - "brown": "3", - "blue": "4", - "magenta": "5", - "cyan": "6", - "default": "7", - "gray": "8", - "lightred": "9", - "lightgreen": "10", - "yellow": "11", - "lightblue": "12", - "lightmagenta": "13", - "lightcyan": "14", - "white": "15" -} - - -class MockObject(object): - pass - -class MockConfig(object): - config_template = { - 'debug_buffer': None, - 'debug_category': None, - '_ptr': None, - 'read': None, - 'free': None, - 'page_up_hook': None, - 'color': { - 'error_message_bg': "", - 'error_message_fg': "", - 'quote_bg': "", - 'quote_fg': "", - 'unconfirmed_message_bg': "", - 'unconfirmed_message_fg': "", - 'untagged_code_bg': "", - 'untagged_code_fg': "", - }, - 'upload_buffer': { - 'display': None, - 'move_line_down': None, - 'move_line_up': None, - 'render': None, - }, - 'look': { - 'bar_item_typing_notice_prefix': None, - 'busy_sign': None, - 'code_block_margin': None, - 'code_blocks': None, - 'disconnect_sign': None, - 'encrypted_room_sign': None, - 'encryption_warning_sign': None, - 'max_typing_notice_item_length': None, - 'pygments_style': None, - 'redactions': None, - 'server_buffer': None, - 'new_channel_position': None, - 'markdown_input': True, - }, - 'network': { - 'debug_buffer': None, - 'debug_category': None, - 'debug_level': None, - 'fetch_backlog_on_pgup': None, - 'lag_min_show': None, - 'lag_reconnect': None, - 'lazy_load_room_users': None, - 'max_initial_sync_events': None, - 'max_nicklist_users': None, - 'print_unconfirmed_messages': None, - 'read_markers_conditions': None, - 'typing_notice_conditions': None, - 'autoreconnect_delay_growing': None, - 'autoreconnect_delay_max': None, - }, - } - - def __init__(self): - for category, options in MockConfig.config_template.items(): - if options: - category_object = MockObject() - for option, value in options.items(): - setattr(category_object, option, value) - else: - category_object = options - - setattr(self, category, category_object) - - -def color(color_name): - # type: (str) -> str - # yapf: disable - escape_codes = [] - reset_code = "0" - - def make_fg_color(color_code): - return "38;5;{}".format(color_code) - - def make_bg_color(color_code): - return "48;5;{}".format(color_code) - - attributes = { - "bold": "1", - "-bold": "21", - "reverse": "27", - "-reverse": "21", - "italic": "3", - "-italic": "23", - "underline": "4", - "-underline": "24", - "reset": "0", - "resetcolor": "39" - } - - short_attributes = { - "*": "1", - "!": "27", - "/": "3", - "_": "4" - } - - colors = color_name.split(",", 2) - - fg_color = colors.pop(0) - - bg_color = colors.pop(0) if colors else "" - - if fg_color in attributes: - escape_codes.append(attributes[fg_color]) - else: - chars = list(fg_color) - - for char in chars: - if char in short_attributes: - escape_codes.append(short_attributes[char]) - elif char == "|": - reset_code = "" - else: - break - - stripped_color = fg_color.lstrip("*_|/!") - - if stripped_color in WEECHAT_BASE_COLORS: - escape_codes.append( - make_fg_color(WEECHAT_BASE_COLORS[stripped_color])) - - elif stripped_color.isdigit(): - num_color = int(stripped_color) - if 0 <= num_color < 256: - escape_codes.append(make_fg_color(stripped_color)) - - if bg_color in WEECHAT_BASE_COLORS: - escape_codes.append(make_bg_color(WEECHAT_BASE_COLORS[bg_color])) - else: - if bg_color.isdigit(): - num_color = int(bg_color) - if 0 <= num_color < 256: - escape_codes.append(make_bg_color(bg_color)) - - escape_string = "\033[{}{}m".format(reset_code, ";".join(escape_codes)) - - return escape_string - - -def prefix(prefix_string): - prefix_to_symbol = { - "error": "=!=", - "network": "--", - "action": "*", - "join": "-->", - "quit": "<--" - } - - if prefix_string in prefix_to_symbol: - return prefix_to_symbol[prefix] - - return "" - - -def prnt(_, message): - print(message) - - -def prnt_date_tags(_, date, tags_string, data): - message = "{} {} [{}]".format( - datetime.datetime.fromtimestamp(date), - data, - tags_string - ) - print(message) - - -def config_search_section(*_, **__): - pass - - -def config_new_option(*_, **__): - pass - - -def mkdir_home(*_, **__): - return True - - -def info_get(info, *_): - if info == "nick_color_name": - return random.choice(list(WEECHAT_BASE_COLORS.keys())) - - return "" - - -def buffer_new(*_, **__): - return "".join( - random.choice(string.ascii_uppercase + string.digits) for _ in range(8) - ) - - -def buffer_set(*_, **__): - return - - -def buffer_get_string(_ptr, property): - if property == "localvar_type": - return "channel" - return "" - - -def buffer_get_integer(_ptr, property): - return 0 - - -def current_buffer(): - return 1 - - -def nicklist_add_group(*_, **__): - return - - -def nicklist_add_nick(*_, **__): - return - - -def nicklist_remove_nick(*_, **__): - return - - -def nicklist_search_nick(*args, **kwargs): - return buffer_new(args, kwargs) - - -def string_remove_color(message, _): - return message diff --git a/weechat/python/matrix/bar_items.py b/weechat/python/matrix/bar_items.py deleted file mode 100644 index 23e6fc9..0000000 --- a/weechat/python/matrix/bar_items.py +++ /dev/null @@ -1,202 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals - -from . import globals as G -from .globals import SERVERS, W -from .utf import utf8_decode - - -@utf8_decode -def matrix_bar_item_plugin(data, item, window, buffer, extra_info): - # pylint: disable=unused-argument - for server in SERVERS.values(): - if buffer in server.buffers.values() or buffer == server.server_buffer: - return "matrix{color}/{color_fg}{name}".format( - color=W.color("bar_delim"), - color_fg=W.color("bar_fg"), - name=server.name, - ) - - ptr_plugin = W.buffer_get_pointer(buffer, "plugin") - name = W.plugin_get_name(ptr_plugin) - - return name - - -@utf8_decode -def matrix_bar_item_name(data, item, window, buffer, extra_info): - # pylint: disable=unused-argument - for server in SERVERS.values(): - if buffer in server.buffers.values(): - color = ( - "status_name_ssl" - if server.ssl_context.check_hostname - else "status_name" - ) - - room_buffer = server.find_room_from_ptr(buffer) - room = room_buffer.room - - return "{color}{name}".format( - color=W.color(color), name=room.display_name - ) - - if buffer == server.server_buffer: - color = ( - "status_name_ssl" - if server.ssl_context.check_hostname - else "status_name" - ) - - return "{color}server{del_color}[{color}{name}{del_color}]".format( - color=W.color(color), - del_color=W.color("bar_delim"), - name=server.name, - ) - - name = W.buffer_get_string(buffer, "name") - - return "{}{}".format(W.color("status_name"), name) - - -@utf8_decode -def matrix_bar_item_lag(data, item, window, buffer, extra_info): - # pylint: disable=unused-argument - for server in SERVERS.values(): - if buffer in server.buffers.values() or buffer == server.server_buffer: - if server.lag >= G.CONFIG.network.lag_min_show: - color = W.color("irc.color.item_lag_counting") - if server.lag_done: - color = W.color("irc.color.item_lag_finished") - - lag = "{0:.3f}" if round(server.lag) < 1000 else "{0:.0f}" - lag_string = "Lag: {color}{lag}{ncolor}".format( - lag=lag.format((server.lag / 1000)), - color=color, - ncolor=W.color("reset"), - ) - return lag_string - return "" - - return "" - - -@utf8_decode -def matrix_bar_item_buffer_modes(data, item, window, buffer, extra_info): - # pylint: disable=unused-argument - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - room = room_buffer.room - modes = [] - - if room.encrypted: - modes.append(G.CONFIG.look.encrypted_room_sign) - - if (server.client - and server.client.room_contains_unverified(room.room_id)): - modes.append(G.CONFIG.look.encryption_warning_sign) - - if not server.connected or not server.client.logged_in: - modes.append(G.CONFIG.look.disconnect_sign) - - if room_buffer.backlog_pending or server.busy: - modes.append(G.CONFIG.look.busy_sign) - - return "".join(modes) - - return "" - - -@utf8_decode -def matrix_bar_nicklist_count(data, item, window, buffer, extra_info): - # pylint: disable=unused-argument - color = W.color("status_nicklist_count") - - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - room = room_buffer.room - return "{}{}".format(color, room.member_count) - - nicklist_enabled = bool(W.buffer_get_integer(buffer, "nicklist")) - - if nicklist_enabled: - nick_count = W.buffer_get_integer(buffer, "nicklist_visible_count") - return "{}{}".format(color, nick_count) - - return "" - - -@utf8_decode -def matrix_bar_typing_notices_cb(data, item, window, buffer, extra_info): - """Update a status bar item showing users currently typing. - This function is called by weechat every time a buffer is switched or - W.bar_item_update() is explicitly called. The bar item shows - currently typing users for the current buffer.""" - # pylint: disable=unused-argument - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - room = room_buffer.room - - if room.typing_users: - nicks = [] - - for user_id in room.typing_users: - if user_id == room.own_user_id: - continue - - nick = room_buffer.displayed_nicks.get(user_id, user_id) - nicks.append(nick) - - if not nicks: - return "" - - msg = "{}{}".format( - G.CONFIG.look.bar_item_typing_notice_prefix, - ", ".join(sorted(nicks)) - ) - - max_len = G.CONFIG.look.max_typing_notice_item_length - if len(msg) > max_len: - msg[:max_len - 3] + "..." - - return msg - - return "" - - return "" - - -def init_bar_items(): - W.bar_item_new("(extra)buffer_plugin", "matrix_bar_item_plugin", "") - W.bar_item_new("(extra)buffer_name", "matrix_bar_item_name", "") - W.bar_item_new("(extra)lag", "matrix_bar_item_lag", "") - W.bar_item_new( - "(extra)buffer_nicklist_count", - "matrix_bar_nicklist_count", - "" - ) - W.bar_item_new( - "(extra)matrix_typing_notice", - "matrix_bar_typing_notices_cb", - "" - ) - W.bar_item_new("(extra)buffer_modes", "matrix_bar_item_buffer_modes", "") - W.bar_item_new("(extra)matrix_modes", "matrix_bar_item_buffer_modes", "") diff --git a/weechat/python/matrix/buffer.py b/weechat/python/matrix/buffer.py deleted file mode 100644 index 7cc305e..0000000 --- a/weechat/python/matrix/buffer.py +++ /dev/null @@ -1,1810 +0,0 @@ -# -*- coding: utf-8 -*- - -# Weechat Matrix Protocol Script -# Copyright © 2018, 2019 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals - -import time -import attr -import pprint -from builtins import super -from functools import partial -from collections import deque -from typing import Dict, List, NamedTuple, Optional, Set -from uuid import UUID - -from nio import ( - Api, - PowerLevelsEvent, - RedactedEvent, - RedactionEvent, - RoomAliasEvent, - RoomEncryptionEvent, - RoomMemberEvent, - RoomMessage, - RoomMessageEmote, - RoomMessageMedia, - RoomEncryptedMedia, - RoomMessageNotice, - RoomMessageText, - RoomMessageUnknown, - RoomNameEvent, - RoomTopicEvent, - MegolmEvent, - Event, - OlmTrustError, - UnknownEvent, - FullyReadEvent, - BadEvent, - UnknownBadEvent, -) - -from . import globals as G -from .colors import Formatted -from .config import RedactType, NewChannelPosition -from .globals import SCRIPT_NAME, SERVERS, W, TYPING_NOTICE_TIMEOUT -from .utf import utf8_decode -from .message_renderer import Render -from .utils import ( - server_ts_to_weechat, - shorten_sender, - string_strikethrough, - color_pair, -) - - -@attr.s -class OwnMessages(object): - sender = attr.ib(type=str) - age = attr.ib(type=int) - event_id = attr.ib(type=str) - uuid = attr.ib(type=str) - room_id = attr.ib(type=str) - formatted_message = attr.ib(type=Formatted) - - -class OwnMessage(OwnMessages): - pass - - -class OwnAction(OwnMessage): - pass - - -@utf8_decode -def room_buffer_input_cb(server_name, buffer, input_data): - server = SERVERS[server_name] - room_buffer = server.find_room_from_ptr(buffer) - - if not room_buffer: - # TODO log error - return W.WEECHAT_RC_ERROR - - if not server.connected: - room_buffer.error("You are not connected to the server") - return W.WEECHAT_RC_ERROR - - if not server.client.logged_in: - room_buffer.error("You are not logged in.") - return W.WEECHAT_RC_ERROR - - data = W.string_input_for_buffer(input_data) - - if not data: - data = input_data - - formatted_data = Formatted.from_input_line(data) - - try: - server.room_send_message(room_buffer, formatted_data, "m.text") - room_buffer.last_message = None - except OlmTrustError as e: - if (G.CONFIG.network.resending_ignores_devices - and room_buffer.last_message): - room_buffer.error("Ignoring unverified devices.") - - if (room_buffer.last_message.to_weechat() == - formatted_data.to_weechat()): - server.room_send_message(room_buffer, formatted_data, "m.text", - ignore_unverified_devices=True) - room_buffer.last_message = None - else: - # If the item is a normal user message store it in the - # buffer to enable the send-anyways functionality. - room_buffer.error("Untrusted devices found in room: {}".format(e)) - room_buffer.last_message = formatted_data - - return W.WEECHAT_RC_OK - - -@utf8_decode -def room_buffer_close_cb(server_name, buffer): - server = SERVERS[server_name] - room_buffer = server.find_room_from_ptr(buffer) - - if room_buffer: - room_id = room_buffer.room.room_id - server.buffers.pop(room_id, None) - server.room_buffers.pop(room_id, None) - - return W.WEECHAT_RC_OK - - -class WeechatUser(object): - def __init__(self, nick, host=None, prefix="", join_time=None): - # type: (str, str, str, int) -> None - self.nick = nick - self.host = host - self.prefix = prefix - self.color = W.info_get("nick_color_name", nick) - self.join_time = join_time or time.time() - self.speaking_time = None # type: Optional[int] - - def update_speaking_time(self, new_time=None): - self.speaking_time = new_time or time.time() - - @property - def joined_recently(self): - # TODO make the delay configurable - delay = 30 - limit = time.time() - (delay * 60) - return self.join_time < limit - - @property - def spoken_recently(self): - if not self.speaking_time: - return False - - # TODO make the delay configurable - delay = 5 - limit = time.time() - (delay * 60) - return self.speaking_time < limit - - -class RoomUser(WeechatUser): - def __init__(self, nick, user_id=None, power_level=0, join_time=None): - # type: (str, str, int, int) -> None - prefix = self._get_prefix(power_level) - super().__init__(nick, user_id, prefix, join_time) - - @property - def power_level(self): - # This shouldn't be used since it's a lossy function. It's only here - # for the setter - if self.prefix == "&": - return 100 - if self.prefix == "@": - return 50 - if self.prefix == "+": - return 10 - return 0 - - @power_level.setter - def power_level(self, level): - self.prefix = self._get_prefix(level) - - @staticmethod - def _get_prefix(power_level): - # type: (int) -> str - if power_level >= 100: - return "&" - if power_level >= 50: - return "@" - if power_level > 0: - return "+" - return "" - - -class WeechatChannelBuffer(object): - tags = { - "message": [SCRIPT_NAME + "_message", "notify_message", "log1"], - "message_private": [ - SCRIPT_NAME + "_message", - "notify_private", - "log1" - ], - "self_message": [ - SCRIPT_NAME + "_message", - "notify_none", - "no_highlight", - "self_msg", - "log1", - ], - "action": [ - SCRIPT_NAME + "_message", - SCRIPT_NAME + "_action", - "notify_message", - "log1", - ], - "action_private": [ - SCRIPT_NAME + "_message", - SCRIPT_NAME + "_action", - "notify_private", - "log1", - ], - "notice": [SCRIPT_NAME + "_notice", "notify_message", "log1"], - "old_message": [ - SCRIPT_NAME + "_message", - "notify_message", - "no_log", - "no_highlight", - ], - "join": [SCRIPT_NAME + "_join", "log4"], - "part": [SCRIPT_NAME + "_leave", "log4"], - "kick": [SCRIPT_NAME + "_kick", "log4"], - "invite": [SCRIPT_NAME + "_invite", "log4"], - "topic": [SCRIPT_NAME + "_topic", "log3"], - } - - membership_messages = { - "join": "has joined", - "part": "has left", - "kick": "has been kicked from", - "invite": "has been invited to", - } - - class Line(object): - def __init__(self, pointer): - self._ptr = pointer - - @property - def _hdata(self): - return W.hdata_get("line_data") - - @property - def prefix(self): - return W.hdata_string(self._hdata, self._ptr, "prefix") - - @prefix.setter - def prefix(self, new_prefix): - new_data = {"prefix": new_prefix} - W.hdata_update(self._hdata, self._ptr, new_data) - - @property - def message(self): - return W.hdata_string(self._hdata, self._ptr, "message") - - @message.setter - def message(self, new_message): - # type: (str) -> None - new_data = {"message": new_message} - W.hdata_update(self._hdata, self._ptr, new_data) - - @property - def tags(self): - tags_count = W.hdata_get_var_array_size( - self._hdata, self._ptr, "tags_array" - ) - - tags = [ - W.hdata_string(self._hdata, self._ptr, "%d|tags_array" % i) - for i in range(tags_count) - ] - return tags - - @tags.setter - def tags(self, new_tags): - # type: (List[str]) -> None - new_data = {"tags_array": ",".join(new_tags)} - W.hdata_update(self._hdata, self._ptr, new_data) - - @property - def date(self): - # type: () -> int - return W.hdata_time(self._hdata, self._ptr, "date") - - @date.setter - def date(self, new_date): - # type: (int) -> None - new_data = {"date": str(new_date)} - W.hdata_update(self._hdata, self._ptr, new_data) - - @property - def date_printed(self): - # type: () -> int - return W.hdata_time(self._hdata, self._ptr, "date_printed") - - @date_printed.setter - def date_printed(self, new_date): - # type: (int) -> None - new_data = {"date_printed": str(new_date)} - W.hdata_update(self._hdata, self._ptr, new_data) - - @property - def highlight(self): - # type: () -> bool - return bool(W.hdata_char(self._hdata, self._ptr, "highlight")) - - def update( - self, - date=None, - date_printed=None, - tags=None, - prefix=None, - message=None, - highlight=None, - ): - new_data = {} - - if date is not None: - new_data["date"] = str(date) - if date_printed is not None: - new_data["date_printed"] = str(date_printed) - if tags is not None: - new_data["tags_array"] = ",".join(tags) - if prefix is not None: - new_data["prefix"] = prefix - if message is not None: - new_data["message"] = message - if highlight is not None: - new_data["highlight"] = highlight - - if new_data: - W.hdata_update(self._hdata, self._ptr, new_data) - - def __init__(self, name, server_name, user): - # type: (str, str, str) -> None - - # Previous buffer num before create - cur_num = W.buffer_get_integer(W.current_buffer(), "number") - self._ptr = W.buffer_new( - name, - "room_buffer_input_cb", - server_name, - "room_buffer_close_cb", - server_name, - ) - - new_channel_position = G.CONFIG.look.new_channel_position - if new_channel_position == NewChannelPosition.NONE: - pass - elif new_channel_position == NewChannelPosition.NEXT: - self.number = cur_num + 1 - elif new_channel_position == NewChannelPosition.NEAR_SERVER: - server = G.SERVERS[server_name] - last_similar_buffer_num = max( - (room.weechat_buffer.number for room - in server.room_buffers.values()), - default=W.buffer_get_integer(server.server_buffer, "number") - ) - self.number = last_similar_buffer_num + 1 - - self.name = "" - self.users = {} # type: Dict[str, WeechatUser] - self.smart_filtered_nicks = set() # type: Set[str] - - self.topic_author = "" - self.topic_date = None - - W.buffer_set(self._ptr, "localvar_set_type", "private") - W.buffer_set(self._ptr, "type", "formatted") - - W.buffer_set(self._ptr, "localvar_set_channel", name) - - W.buffer_set(self._ptr, "localvar_set_nick", user) - - W.buffer_set(self._ptr, "localvar_set_server", server_name) - - W.nicklist_add_group( - self._ptr, "", "000|o", "weechat.color.nicklist_group", 1 - ) - W.nicklist_add_group( - self._ptr, "", "001|h", "weechat.color.nicklist_group", 1 - ) - W.nicklist_add_group( - self._ptr, "", "002|v", "weechat.color.nicklist_group", 1 - ) - W.nicklist_add_group( - self._ptr, "", "999|...", "weechat.color.nicklist_group", 1 - ) - - W.buffer_set(self._ptr, "nicklist", "1") - W.buffer_set(self._ptr, "nicklist_display_groups", "0") - - W.buffer_set(self._ptr, "highlight_words", user) - - # TODO make this configurable - W.buffer_set( - self._ptr, "highlight_tags_restrict", SCRIPT_NAME + "_message" - ) - - @property - def _hdata(self): - return W.hdata_get("buffer") - - def add_smart_filtered_nick(self, nick): - self.smart_filtered_nicks.add(nick) - - def remove_smart_filtered_nick(self, nick): - self.smart_filtered_nicks.discard(nick) - - def unmask_smart_filtered_nick(self, nick): - if nick not in self.smart_filtered_nicks: - return - - for line in self.lines: - filtered = False - join = False - tags = line.tags - - if "nick_{}".format(nick) not in tags: - continue - - if SCRIPT_NAME + "_smart_filter" in tags: - filtered = True - elif SCRIPT_NAME + "_join" in tags: - join = True - - if filtered: - tags.remove(SCRIPT_NAME + "_smart_filter") - line.tags = tags - - if join: - break - - self.remove_smart_filtered_nick(nick) - - @property - def input(self): - # type: () -> str - """Get the bar item input text of the buffer.""" - return W.buffer_get_string(self._ptr, "input") - - @property - def num_lines(self): - own_lines = W.hdata_pointer(self._hdata, self._ptr, "own_lines") - return W.hdata_integer(W.hdata_get("lines"), own_lines, "lines_count") - - @property - def lines(self): - own_lines = W.hdata_pointer(self._hdata, self._ptr, "own_lines") - - if own_lines: - hdata_line = W.hdata_get("line") - - line_pointer = W.hdata_pointer( - W.hdata_get("lines"), own_lines, "last_line" - ) - - while line_pointer: - data_pointer = W.hdata_pointer( - hdata_line, line_pointer, "data" - ) - - if data_pointer: - yield WeechatChannelBuffer.Line(data_pointer) - - line_pointer = W.hdata_move(hdata_line, line_pointer, -1) - - def _print(self, string): - # type: (str) -> None - """ Print a string to the room buffer """ - W.prnt(self._ptr, string) - - def print_date_tags(self, data, date=None, tags=None): - # type: (str, Optional[int], Optional[List[str]]) -> None - date = date or int(time.time()) - tags = tags or [] - - tags_string = ",".join(tags) - W.prnt_date_tags(self._ptr, date, tags_string, data) - - def error(self, string): - # type: (str) -> None - """ Print an error to the room buffer """ - message = "{prefix}{script}: {message}".format( - prefix=W.prefix("error"), script=SCRIPT_NAME, message=string - ) - - self._print(message) - - def info(self, string): - message = "{prefix}{script}: {message}".format( - prefix=W.prefix("network"), script=SCRIPT_NAME, message=string - ) - self._print(message) - - @staticmethod - def _color_for_tags(color): - # type: (str) -> str - if color == "weechat.color.chat_nick_self": - option = W.config_get(color) - return W.config_string(option) - - return color - - def _message_tags(self, user, message_type): - # type: (WeechatUser, str) -> List[str] - tags = list(self.tags[message_type]) - - tags.append("nick_{nick}".format(nick=user.nick)) - - color = self._color_for_tags(user.color) - - if message_type not in ("action", "notice"): - tags.append("prefix_nick_{color}".format(color=color)) - - return tags - - def _get_user(self, nick): - # type: (str) -> WeechatUser - if nick in self.users: - return self.users[nick] - - # A message from a non joined user - return WeechatUser(nick) - - def _print_message(self, user, message, date, tags, extra_prefix=""): - prefix_string = ( - extra_prefix - if not user.prefix - else "{}{}{}{}".format( - extra_prefix, - W.color(self._get_prefix_color(user.prefix)), - user.prefix, - W.color("reset"), - ) - ) - - data = "{prefix}{color}{author}{ncolor}\t{msg}".format( - prefix=prefix_string, - color=W.color(user.color), - author=user.nick, - ncolor=W.color("reset"), - msg=message, - ) - - self.print_date_tags(data, date, tags) - - def message(self, nick, message, date, extra_tags=None, extra_prefix=""): - # type: (str, str, int, List[str], str) -> None - user = self._get_user(nick) - tags_type = "message_private" if self.type == "private" else "message" - tags = self._message_tags(user, tags_type) + (extra_tags or []) - self._print_message(user, message, date, tags, extra_prefix) - - user.update_speaking_time(date) - self.unmask_smart_filtered_nick(nick) - - def notice(self, nick, message, date, extra_tags=None, extra_prefix=""): - # type: (str, str, int, Optional[List[str]], str) -> None - user = self._get_user(nick) - user_prefix = ( - "" - if not user.prefix - else "{}{}{}".format( - W.color(self._get_prefix_color(user.prefix)), - user.prefix, - W.color("reset"), - ) - ) - - user_string = "{}{}{}{}".format( - user_prefix, W.color(user.color), user.nick, W.color("reset") - ) - - data = ( - "{extra_prefix}{prefix}{color}Notice" - "{del_color}({ncolor}{user}{del_color}){ncolor}" - ": {message}" - ).format( - extra_prefix=extra_prefix, - prefix=W.prefix("network"), - color=W.color("irc.color.notice"), - del_color=W.color("chat_delimiters"), - ncolor=W.color("reset"), - user=user_string, - message=message, - ) - - tags = self._message_tags(user, "notice") + (extra_tags or []) - self.print_date_tags(data, date, tags) - - user.update_speaking_time(date) - self.unmask_smart_filtered_nick(nick) - - def _format_action(self, user, message): - nick_prefix = ( - "" - if not user.prefix - else "{}{}{}".format( - W.color(self._get_prefix_color(user.prefix)), - user.prefix, - W.color("reset"), - ) - ) - - data = ( - "{nick_prefix}{nick_color}{author}" - "{ncolor} {msg}").format( - nick_prefix=nick_prefix, - nick_color=W.color(user.color), - author=user.nick, - ncolor=W.color("reset"), - msg=message, - ) - return data - - def _print_action(self, user, message, date, tags, extra_prefix=""): - data = self._format_action(user, message) - data = "{extra_prefix}{prefix}{data}".format( - extra_prefix=extra_prefix, - prefix=W.prefix("action"), - data=data) - - self.print_date_tags(data, date, tags) - - def action(self, nick, message, date, extra_tags=None, extra_prefix=""): - # type: (str, str, int, Optional[List[str]], str) -> None - user = self._get_user(nick) - tags_type = "action_private" if self.type == "private" else "action" - tags = self._message_tags(user, tags_type) + (extra_tags or []) - self._print_action(user, message, date, tags, extra_prefix) - - user.update_speaking_time(date) - self.unmask_smart_filtered_nick(nick) - - @staticmethod - def _get_nicklist_group(user): - # type: (WeechatUser) -> str - group_name = "999|..." - - if user.prefix == "&": - group_name = "000|o" - elif user.prefix == "@": - group_name = "001|h" - elif user.prefix == "+": - group_name = "002|v" - - return group_name - - @staticmethod - def _get_prefix_color(prefix): - # type: (str) -> str - - return G.CONFIG.color.nick_prefixes.get(prefix, "") - - def _add_user_to_nicklist(self, user): - # type: (WeechatUser) -> None - nick_pointer = W.nicklist_search_nick(self._ptr, "", user.nick) - - if not nick_pointer: - group = W.nicklist_search_group( - self._ptr, "", self._get_nicklist_group(user) - ) - prefix = user.prefix if user.prefix else " " - W.nicklist_add_nick( - self._ptr, - group, - user.nick, - user.color, - prefix, - self._get_prefix_color(user.prefix), - 1, - ) - - def _membership_message(self, user, message_type): - # type: (WeechatUser, str) -> str - action_color = "green" if message_type in ("join", "invite") else "red" - prefix = "join" if message_type in ("join", "invite") else "quit" - - membership_message = self.membership_messages[message_type] - - message = ( - "{prefix}{color}{author}{ncolor} " - "{del_color}({host_color}{host}{del_color})" - "{action_color} {message} " - "{channel_color}{room}{ncolor}" - ).format( - prefix=W.prefix(prefix), - color=W.color(user.color), - author=user.nick, - ncolor=W.color("reset"), - del_color=W.color("chat_delimiters"), - host_color=W.color("chat_host"), - host=user.host, - action_color=W.color(action_color), - message=membership_message, - channel_color=W.color("chat_channel"), - room=self.short_name, - ) - - return message - - def join(self, user, date, message=True, extra_tags=None): - # type: (WeechatUser, int, Optional[bool], Optional[List[str]]) -> None - self._add_user_to_nicklist(user) - self.users[user.nick] = user - - if len(self.users) > 2: - W.buffer_set(self._ptr, "localvar_set_type", "channel") - - if message: - tags = self._message_tags(user, "join") - msg = self._membership_message(user, "join") - - # TODO add a option to disable smart filters - tags.append(SCRIPT_NAME + "_smart_filter") - - self.print_date_tags(msg, date, tags) - self.add_smart_filtered_nick(user.nick) - - def invite(self, nick, date, extra_tags=None): - # type: (str, int, Optional[List[str]]) -> None - user = self._get_user(nick) - tags = self._message_tags(user, "invite") - message = self._membership_message(user, "invite") - self.print_date_tags(message, date, tags + (extra_tags or [])) - - def remove_user_from_nicklist(self, user): - # type: (WeechatUser) -> None - nick_pointer = W.nicklist_search_nick(self._ptr, "", user.nick) - - if nick_pointer: - W.nicklist_remove_nick(self._ptr, nick_pointer) - - def _leave(self, nick, date, message, leave_type, extra_tags=None): - # type: (str, int, bool, str, List[str]) -> None - user = self._get_user(nick) - self.remove_user_from_nicklist(user) - - if len(self.users) <= 2: - W.buffer_set(self._ptr, "localvar_set_type", "private") - - if message: - tags = self._message_tags(user, leave_type) - - # TODO make this configurable - if not user.spoken_recently: - tags.append(SCRIPT_NAME + "_smart_filter") - - msg = self._membership_message(user, leave_type) - self.print_date_tags(msg, date, tags + (extra_tags or [])) - self.remove_smart_filtered_nick(user.nick) - - if user.nick in self.users: - del self.users[user.nick] - - def part(self, nick, date, message=True, extra_tags=None): - # type: (str, int, bool, Optional[List[str]]) -> None - self._leave(nick, date, message, "part", extra_tags) - - def kick(self, nick, date, message=True, extra_tags=None): - # type: (str, int, bool, Optional[List[str]]) -> None - self._leave(nick, date, message, "kick", extra_tags) - - def _print_topic(self, nick, topic, date): - user = self._get_user(nick) - tags = self._message_tags(user, "topic") - - data = ( - "{prefix}{nick} has changed " - "the topic for {chan_color}{room}{ncolor} " - 'to "{topic}"' - ).format( - prefix=W.prefix("network"), - nick=user.nick, - chan_color=W.color("chat_channel"), - ncolor=W.color("reset"), - room=self.short_name, - topic=topic, - ) - - self.print_date_tags(data, date, tags) - user.update_speaking_time(date) - self.unmask_smart_filtered_nick(nick) - - @property - def topic(self): - return W.buffer_get_string(self._ptr, "title") - - @topic.setter - def topic(self, topic): - W.buffer_set(self._ptr, "title", topic) - - def change_topic(self, nick, topic, date, message=True): - if message: - self._print_topic(nick, topic, date) - - self.topic = topic - self.topic_author = nick - self.topic_date = date - - def self_message(self, nick, message, date, tags=None): - user = self._get_user(nick) - tags = self._message_tags(user, "self_message") + (tags or []) - self._print_message(user, message, date, tags) - - def self_action(self, nick, message, date, tags=None): - user = self._get_user(nick) - tags = self._message_tags(user, "self_message") + (tags or []) - tags.append(SCRIPT_NAME + "_action") - self._print_action(user, message, date, tags) - - @property - def type(self): - return W.buffer_get_string(self._ptr, "localvar_type") - - @property - def short_name(self): - return W.buffer_get_string(self._ptr, "short_name") - - @short_name.setter - def short_name(self, name): - W.buffer_set(self._ptr, "short_name", name) - - @property - def name(self): - return W.buffer_get_string(self._ptr, "name") - - @name.setter - def name(self, name): - W.buffer_set(self._ptr, "name", name) - - @property - def number(self): - """Get the buffer number, starts at 1.""" - return int(W.buffer_get_integer(self._ptr, "number")) - - @number.setter - def number(self, n): - W.buffer_set(self._ptr, "number", str(n)) - - def find_lines(self, predicate, max_lines=None): - lines = [] - count = 0 - for line in self.lines: - if predicate(line): - lines.append(line) - count += 1 - if max_lines is not None and count == max_lines: - return lines - - return lines - - -class RoomBuffer(object): - def __init__(self, room, server_name, homeserver, prev_batch): - self.room = room - self.homeserver = homeserver - self._backlog_pending = False - self.prev_batch = prev_batch - self.joined = True - self.leave_event_id = None # type: Optional[str] - self.members_fetched = False - self.first_view = True - self.first_backlog_request = True - self.unhandled_users = [] # type: List[str] - self.inactive_users = [] - - self.sent_messages_queue = dict() # type: Dict[UUID, OwnMessage] - self.printed_before_ack_queue = list() # type: List[UUID] - self.undecrypted_events = deque(maxlen=5000) - - self.typing_notice_time = None - self._typing = False - self.typing_enabled = True - - self.last_read_event = None - self._read_markers_enabled = True - self.server_name = server_name - - self.last_message = None - - buffer_name = "{}{}.{}".format(G.BUFFER_NAME_PREFIX, server_name, room.room_id) - - # This dict remembers the connection from a user_id to the name we - # displayed in the buffer - self.displayed_nicks = {} - user = shorten_sender(self.room.own_user_id) - - self.weechat_buffer = WeechatChannelBuffer( - buffer_name, server_name, user - ) - - W.buffer_set( - self.weechat_buffer._ptr, - "localvar_set_domain", - self.homeserver.hostname - ) - - W.buffer_set( - self.weechat_buffer._ptr, - "localvar_set_room_id", - room.room_id - ) - - if room.canonical_alias: - self.update_canonical_alias_localvar() - - @property - def backlog_pending(self): - return self._backlog_pending - - @backlog_pending.setter - def backlog_pending(self, value): - self._backlog_pending = value - W.bar_item_update("buffer_modes") - W.bar_item_update("matrix_modes") - - @property - def warning_prefix(self): - return G.CONFIG.look.encryption_warning_sign - - @property - def typing(self): - # type: () -> bool - """Return our typing status.""" - return self._typing - - @typing.setter - def typing(self, value): - self._typing = value - if value: - self.typing_notice_time = time.time() - else: - self.typing_notice_time = None - - @property - def typing_notice_expired(self): - # type: () -> bool - """Check if the typing notice has expired. - - Returns true if a new typing notice should be sent. - """ - if not self.typing_notice_time: - return True - - now = time.time() - if (now - self.typing_notice_time) > (TYPING_NOTICE_TIMEOUT / 1000): - return True - return False - - @property - def should_send_read_marker(self): - # type () -> bool - """Check if we need to send out a read receipt.""" - if not self.read_markers_enabled: - return False - - if not self.last_read_event: - return True - - if self.last_read_event == self.last_event_id: - return False - - return True - - @property - def last_event_id(self): - # type () -> str - """Get the event id of the last shown matrix event.""" - for line in self.weechat_buffer.lines: - for tag in line.tags: - if tag.startswith("matrix_id"): - event_id = tag[10:] - return event_id - - return "" - - @property - def printed_event_ids(self): - for line in self.weechat_buffer.lines: - for tag in line.tags: - if tag.startswith("matrix_id"): - event_id = tag[10:] - yield event_id - - @property - def read_markers_enabled(self): - # type: () -> bool - """Check if read receipts are enabled for this room.""" - return bool(int(W.string_eval_expression( - G.CONFIG.network.read_markers_conditions, - {}, - {"markers_enabled": str(int(self._read_markers_enabled))}, - {"type": "condition"} - ))) - - @read_markers_enabled.setter - def read_markers_enabled(self, value): - self._read_markers_enabled = value - - def find_nick(self, user_id): - # type: (str) -> str - """Find a suitable nick from a user_id.""" - if user_id in self.displayed_nicks: - return self.displayed_nicks[user_id] - - return user_id - - def add_user(self, user_id, date, is_state, force_add=False): - # User is already added don't add him again. - if user_id in self.displayed_nicks: - return - - try: - user = self.room.users[user_id] - except KeyError: - # No user found, he must have left already in an event that is - # yet to come, so do nothing - return - - # Adding users to the nicklist is a O(1) + search time - # operation (the nicks are added to a linked list sorted). - # The search time is O(N * min(a,b)) where N is the number - # of nicks already added and a/b are the length of - # the strings that are compared at every iteration. - # Because the search time get's increasingly longer we're - # going to stop adding inactive users, they will be lazily added if - # they become active. - if is_state and not force_add and user.power_level <= 0: - if (len(self.displayed_nicks) >= - G.CONFIG.network.max_nicklist_users): - self.inactive_users.append(user_id) - return - - try: - self.inactive_users.remove(user_id) - except ValueError: - pass - - short_name = shorten_sender(user.user_id) - - # TODO handle this special case for discord bridge users and - # freenode bridge users better - if (user.user_id.startswith("@_discord_") or - user.user_id.startswith("@_slack_") or - user.user_id.startswith("@_discordpuppet_") or - user.user_id.startswith("@_slackpuppet_") or - user.user_id.startswith("@whatsapp_") or - user.user_id.startswith("@facebook_") or - user.user_id.startswith("@telegram_") or - user.user_id.startswith("@_telegram_") or - user.user_id.startswith("@_xmpp_") or - user.user_id.startswith("@irc_")): - if user.display_name: - short_name = user.display_name[0:50] - elif user.user_id.startswith("@twilio_"): - short_name = shorten_sender(user.user_id[7:]) - elif user.user_id.startswith("@freenode_"): - short_name = shorten_sender(user.user_id[9:]) - elif user.user_id.startswith("@_ircnet_"): - short_name = shorten_sender(user.user_id[8:]) - elif user.user_id.startswith("@gitter_"): - short_name = shorten_sender(user.user_id[7:]) - - # TODO make this configurable - if not short_name or short_name in self.displayed_nicks.values(): - # Use the full user id, but don't include the @ - nick = user_id[1:] - else: - nick = short_name - - buffer_user = RoomUser(nick, user_id, user.power_level, date) - self.displayed_nicks[user_id] = nick - - if self.room.own_user_id == user_id: - buffer_user.color = "weechat.color.chat_nick_self" - user.nick_color = "weechat.color.chat_nick_self" - - self.weechat_buffer.join(buffer_user, date, not is_state) - - def handle_membership_events(self, event, is_state): - date = server_ts_to_weechat(event.server_timestamp) - - if event.content["membership"] == "join": - if (event.state_key not in self.displayed_nicks - and event.state_key not in self.inactive_users): - if len(self.room.users) > 100: - self.unhandled_users.append(event.state_key) - return - - self.add_user(event.state_key, date, is_state) - else: - # TODO print out profile changes - return - - elif event.content["membership"] == "leave": - if event.state_key in self.unhandled_users: - self.unhandled_users.remove(event.state_key) - return - - nick = self.find_nick(event.state_key) - if event.sender == event.state_key: - self.weechat_buffer.part(nick, date, not is_state) - else: - self.weechat_buffer.kick(nick, date, not is_state) - - if event.state_key in self.displayed_nicks: - del self.displayed_nicks[event.state_key] - - # We left the room, remember the event id of our leave, if we - # rejoin we get events that came before this event as well as - # after our leave, this way we know where to continue - if event.state_key == self.room.own_user_id: - self.leave_event_id = event.event_id - - elif event.content["membership"] == "invite": - if is_state: - return - - self.weechat_buffer.invite(event.state_key, date) - return - - self.update_buffer_name() - - def update_buffer_name(self): - if self.room.is_named: - if self.room.name and self.room.name != "#": - room_name = self.room.name - room_name = (room_name if room_name.startswith("#") - else "#" + room_name) - elif self.room.canonical_alias: - room_name = self.room.canonical_alias - self.update_canonical_alias_localvar() - elif self.room.name == "#": - room_name = "##" - else: - room_name = self.room.display_name - - if room_name is None: - # Use placeholder room name - room_name = 'Empty room (?)' - - self.weechat_buffer.short_name = room_name - - if G.CONFIG.human_buffer_names: - buffer_name = "{}.{}".format(self.server_name, room_name) - self.weechat_buffer.name = buffer_name - - def update_canonical_alias_localvar(self): - W.buffer_set( - self.weechat_buffer._ptr, - "localvar_set_canonical_alias", - self.room.canonical_alias - ) - - def _redact_line(self, event): - def predicate(event_id, line): - def already_redacted(tags): - if SCRIPT_NAME + "_redacted" in tags: - return True - return False - - event_tag = SCRIPT_NAME + "_id_{}".format(event_id) - tags = line.tags - - if event_tag in tags and not already_redacted(tags): - return True - - return False - - def redact_string(message): - new_message = "" - - if G.CONFIG.look.redactions == RedactType.STRIKETHROUGH: - plaintext_msg = W.string_remove_color(message, "") - new_message = string_strikethrough(plaintext_msg) - elif G.CONFIG.look.redactions == RedactType.NOTICE: - new_message = message - elif G.CONFIG.look.redactions == RedactType.DELETE: - pass - - return new_message - - lines = self.weechat_buffer.find_lines( - partial(predicate, event.redacts) - ) - - # No line to redact, return early - if not lines: - return - - censor = self.find_nick(event.sender) - redaction_msg = Render.redacted(censor, event.reason) - - line = lines[0] - message = line.message - tags = line.tags - - new_message = redact_string(message) - message = " ".join(s for s in [new_message, redaction_msg] if s) - tags.append("matrix_redacted") - - line.message = message - line.tags = tags - - for line in lines[1:]: - message = line.message - tags = line.tags - - new_message = redact_string(message) - - if not new_message: - new_message = redaction_msg - elif G.CONFIG.look.redactions == RedactType.NOTICE: - new_message += " {}".format(redaction_msg) - - tags.append("matrix_redacted") - - line.message = new_message - line.tags = tags - - def _handle_topic(self, event, is_state): - nick = self.find_nick(event.sender) - - self.weechat_buffer.change_topic( - nick, - event.topic, - server_ts_to_weechat(event.server_timestamp), - not is_state, - ) - - @staticmethod - def get_event_tags(event): - # type: (Event) -> List[str] - tags = [SCRIPT_NAME + "_id_{}".format(event.event_id)] - if event.sender_key: - tags.append(SCRIPT_NAME + "_senderkey_{}".format(event.sender_key)) - if event.session_id: - tags.append(SCRIPT_NAME + "_session_id_{}".format( - event.session_id - )) - - return tags - - def _handle_power_level(self, _): - for user_id in self.room.power_levels.users: - if user_id in self.displayed_nicks: - nick = self.find_nick(user_id) - - user = self.weechat_buffer.users[nick] - user.power_level = self.room.power_levels.get_user_level( - user_id - ) - - # There is no way to change the group of a user without - # removing him from the nicklist - self.weechat_buffer.remove_user_from_nicklist(user) - self.weechat_buffer._add_user_to_nicklist(user) - - def handle_state_event(self, event): - if isinstance(event, RoomMemberEvent): - self.handle_membership_events(event, True) - elif isinstance(event, RoomTopicEvent): - self._handle_topic(event, True) - elif isinstance(event, PowerLevelsEvent): - self._handle_power_level(event) - elif isinstance(event, (RoomNameEvent, RoomAliasEvent)): - self.update_buffer_name() - elif isinstance(event, RoomEncryptionEvent): - pass - - def handle_own_message_in_timeline(self, event): - """Check if our own message is already printed if not print it. - This function is called for messages that contain a transaction id - indicating that they were sent out using our own client. If we sent out - a message but never got a valid server response (e.g. due to - disconnects) this function prints them out using data from the next - sync response""" - uuid = UUID(event.transaction_id) - message = self.sent_messages_queue.pop(uuid, None) - - # We already got a response to the room_send_message() API call and - # handled the message, no need to print it out again - if not message: - return - - message.event_id = event.event_id - if uuid in self.printed_before_ack_queue: - self.replace_printed_line_by_uuid( - event.transaction_id, - message - ) - self.printed_before_ack_queue.remove(uuid) - return - - if isinstance(message, OwnAction): - self.self_action(message) - elif isinstance(message, OwnMessage): - self.self_message(message) - return - - def print_room_message(self, event, extra_tags=None): - extra_tags = extra_tags or [] - nick = self.find_nick(event.sender) - - data = Render.message(event.body, event.formatted_body) - - extra_prefix = (self.warning_prefix if event.decrypted - and not event.verified else "") - - date = server_ts_to_weechat(event.server_timestamp) - self.weechat_buffer.message( - nick, data, date, self.get_event_tags(event) + extra_tags, - extra_prefix - ) - - def print_room_emote(self, event, extra_tags=None): - extra_tags = extra_tags or [] - nick = self.find_nick(event.sender) - date = server_ts_to_weechat(event.server_timestamp) - - extra_prefix = (self.warning_prefix if event.decrypted - and not event.verified else "") - - self.weechat_buffer.action( - nick, event.body, date, self.get_event_tags(event) + extra_tags, - extra_prefix - ) - - def print_room_notice(self, event, extra_tags=None): - extra_tags = extra_tags or [] - nick = self.find_nick(event.sender) - date = server_ts_to_weechat(event.server_timestamp) - extra_prefix = (self.warning_prefix if event.decrypted - and not event.verified else "") - - self.weechat_buffer.notice( - nick, event.body, date, self.get_event_tags(event) + extra_tags, - extra_prefix - ) - - def print_room_media(self, event, extra_tags=None): - extra_tags = extra_tags or [] - nick = self.find_nick(event.sender) - date = server_ts_to_weechat(event.server_timestamp) - if isinstance(event, RoomMessageMedia): - data = Render.media(event.url, event.body, self.homeserver.geturl()) - else: - data = Render.encrypted_media( - event.url, event.body, event.key["k"], event.hashes["sha256"], - event.iv, self.homeserver.geturl() - ) - - extra_prefix = (self.warning_prefix if event.decrypted - and not event.verified else "") - - self.weechat_buffer.message( - nick, data, date, self.get_event_tags(event) + extra_tags, - extra_prefix - ) - - def print_unknown(self, event, extra_tags=None): - extra_tags = extra_tags or [] - nick = self.find_nick(event.sender) - date = server_ts_to_weechat(event.server_timestamp) - data = Render.unknown(event.type, event.content) - extra_prefix = (self.warning_prefix if event.decrypted - and not event.verified else "") - - self.weechat_buffer.message( - nick, data, date, self.get_event_tags(event) + extra_tags, - extra_prefix - ) - - def print_redacted(self, event, extra_tags=None): - extra_tags = extra_tags or [] - - nick = self.find_nick(event.sender) - date = server_ts_to_weechat(event.server_timestamp) - tags = self.get_event_tags(event) - tags.append(SCRIPT_NAME + "_redacted") - tags += extra_tags - - censor = self.find_nick(event.redacter) - data = Render.redacted(censor, event.reason) - - self.weechat_buffer.message(nick, data, date, tags) - - def print_room_encryption(self, event, extra_tags=None): - nick = self.find_nick(event.sender) - data = Render.room_encryption(nick) - # TODO this should also have tags - self.weechat_buffer.info(data) - - def print_megolm(self, event, extra_tags=None): - extra_tags = extra_tags or [] - nick = self.find_nick(event.sender) - date = server_ts_to_weechat(event.server_timestamp) - - data = Render.megolm() - - session_id_tag = SCRIPT_NAME + "_sessionid_" + event.session_id - self.weechat_buffer.message( - nick, - data, - date, - self.get_event_tags(event) + [session_id_tag] + extra_tags - ) - - self.undecrypted_events.append(event) - - def print_bad_event(self, event, extra_tags=None): - extra_tags = extra_tags or [] - nick = self.find_nick(event.sender) - date = server_ts_to_weechat(event.server_timestamp) - data = Render.bad(event) - extra_prefix = self.warning_prefix - - self.weechat_buffer.message( - nick, data, date, self.get_event_tags(event) + extra_tags, - extra_prefix - ) - - def handle_room_messages(self, event, extra_tags=None): - if isinstance(event, RoomMessageEmote): - self.print_room_emote(event, extra_tags) - - elif isinstance(event, RoomMessageText): - self.print_room_message(event, extra_tags) - - elif isinstance(event, RoomMessageNotice): - self.print_room_notice(event, extra_tags) - - elif isinstance(event, RoomMessageMedia): - self.print_room_media(event, extra_tags) - - elif isinstance(event, RoomEncryptedMedia): - self.print_room_media(event, extra_tags) - - elif isinstance(event, RoomMessageUnknown): - self.print_unknown(event, extra_tags) - - elif isinstance(event, RoomEncryptionEvent): - self.print_room_encryption(event, extra_tags) - - elif isinstance(event, MegolmEvent): - self.print_megolm(event, extra_tags) - - def force_load_member(self, event): - if (event.sender not in self.displayed_nicks and - event.sender in self.room.users): - - try: - self.unhandled_users.remove(event.sender) - except ValueError: - pass - - self.add_user(event.sender, 0, True, True) - - def handle_timeline_event(self, event, extra_tags=None): - # TODO this should be done for every messagetype that gets printed in - # the buffer - if isinstance(event, (RoomMessage, MegolmEvent)): - self.force_load_member(event) - - if event.transaction_id: - self.handle_own_message_in_timeline(event) - return - - if isinstance(event, RoomMemberEvent): - self.handle_membership_events(event, False) - - elif isinstance(event, (RoomNameEvent, RoomAliasEvent)): - self.update_buffer_name() - - elif isinstance(event, RoomTopicEvent): - self._handle_topic(event, False) - - # Emotes are a subclass of RoomMessageText, so put them before the text - # ones - elif isinstance(event, RoomMessageEmote): - self.print_room_emote(event, extra_tags) - - elif isinstance(event, RoomMessageText): - self.print_room_message(event, extra_tags) - - elif isinstance(event, RoomMessageNotice): - self.print_room_notice(event, extra_tags) - - elif isinstance(event, RoomMessageMedia): - self.print_room_media(event, extra_tags) - - elif isinstance(event, RoomEncryptedMedia): - self.print_room_media(event, extra_tags) - - elif isinstance(event, RoomMessageUnknown): - self.print_unknown(event, extra_tags) - - elif isinstance(event, RedactionEvent): - self._redact_line(event) - - elif isinstance(event, RedactedEvent): - self.print_redacted(event, extra_tags) - - elif isinstance(event, RoomEncryptionEvent): - self.print_room_encryption(event, extra_tags) - - elif isinstance(event, PowerLevelsEvent): - # TODO we should print out a message for this event - self._handle_power_level(event) - - elif isinstance(event, MegolmEvent): - self.print_megolm(event, extra_tags) - - elif isinstance(event, UnknownEvent): - pass - - elif isinstance(event, BadEvent): - self.print_bad_event(event, extra_tags) - - elif isinstance(event, UnknownBadEvent): - self.error("Unknown bad event: {}".format( - pprint.pformat(event.source) - )) - - else: - W.prnt( - "", "Unhandled event of type {}.".format(type(event).__name__) - ) - - def self_message(self, message): - # type: (OwnMessage) -> None - nick = self.find_nick(self.room.own_user_id) - data = message.formatted_message.to_weechat() - if message.event_id: - tags = [SCRIPT_NAME + "_id_{}".format(message.event_id)] - else: - tags = [SCRIPT_NAME + "_uuid_{}".format(message.uuid)] - date = message.age - - self.weechat_buffer.self_message(nick, data, date, tags) - - def self_action(self, message): - # type: (OwnMessage) -> None - nick = self.find_nick(self.room.own_user_id) - date = message.age - if message.event_id: - tags = [SCRIPT_NAME + "_id_{}".format(message.event_id)] - else: - tags = [SCRIPT_NAME + "_uuid_{}".format(message.uuid)] - - self.weechat_buffer.self_action( - nick, message.formatted_message.to_weechat(), date, tags - ) - - @staticmethod - def _find_by_uuid_predicate(uuid, line): - uuid_tag = SCRIPT_NAME + "_uuid_{}".format(uuid) - tags = line.tags - - if uuid_tag in tags: - return True - return False - - def mark_message_as_unsent(self, uuid, _): - """Append to already printed lines that are greyed out an error - message""" - lines = self.weechat_buffer.find_lines( - partial(self._find_by_uuid_predicate, uuid) - ) - last_line = lines[-1] - - message = last_line.message - message += (" {del_color}<{ncolor}{error_color}Error sending " - "message{del_color}>{ncolor}").format( - del_color=W.color("chat_delimiters"), - ncolor=W.color("reset"), - error_color=W.color(color_pair( - G.CONFIG.color.error_message_fg, - G.CONFIG.color.error_message_bg))) - - last_line.message = message - - def replace_printed_line_by_uuid(self, uuid, new_message): - """Replace already printed lines that are greyed out with real ones.""" - if isinstance(new_message, OwnAction): - displayed_nick = self.displayed_nicks[self.room.own_user_id] - user = self.weechat_buffer._get_user(displayed_nick) - data = self.weechat_buffer._format_action( - user, - new_message.formatted_message.to_weechat() - ) - new_lines = data.split("\n") - else: - new_lines = new_message.formatted_message.to_weechat().split("\n") - - line_count = len(new_lines) - - lines = self.weechat_buffer.find_lines( - partial(self._find_by_uuid_predicate, uuid), line_count - ) - - for i, line in enumerate(lines): - line.message = new_lines[i] - tags = line.tags - - new_tags = [ - tag for tag in tags - if not tag.startswith(SCRIPT_NAME + "_uuid_") - ] - new_tags.append(SCRIPT_NAME + "_id_" + new_message.event_id) - line.tags = new_tags - - def replace_undecrypted_line(self, event): - """Find an undecrypted message in the buffer and replace it with the now - decrypted event.""" - # TODO different messages need different formatting - # To implement this, refactor out the different formatting code - # snippets to a Formatter class and reuse them here. - if not isinstance(event, RoomMessageText): - return - - def predicate(event_id, line): - event_tag = SCRIPT_NAME + "_id_{}".format(event_id) - if event_tag in line.tags: - return True - return False - - lines = self.weechat_buffer.find_lines( - partial(predicate, event.event_id) - ) - - if not lines: - return - - formatted = None - if event.formatted_body: - formatted = Formatted.from_html(event.formatted_body) - - data = formatted.to_weechat() if formatted else event.body - # TODO this isn't right if the data has multiple lines, that is - # everything is printed on a single line and newlines are shown as a - # space. - # Weechat should support deleting lines and printing new ones at an - # arbitrary position. - # To implement this without weechat support either only handle single - # line messages or edit the first line in place, print new ones at the - # bottom and sort the buffer lines. - lines[0].message = data - - def old_message(self, event): - tags = list(self.weechat_buffer.tags["old_message"]) - # TODO events that change the room state (topics, membership changes, - # etc...) should be printed out as well, but for this to work without - # messing up the room state the state change will need to be separated - # from the print logic. - if isinstance(event, RoomMessage): - self.force_load_member(event) - self.handle_room_messages(event, tags) - - elif isinstance(event, MegolmEvent): - self.print_megolm(event, tags) - - elif isinstance(event, RedactedEvent): - self.print_redacted(event, tags) - - elif isinstance(event, BadEvent): - self.print_bad_event(event, tags) - - def sort_messages(self): - class LineCopy(object): - def __init__( - self, date, date_printed, tags, prefix, message, highlight - ): - self.date = date - self.date_printed = date_printed - self.tags = tags - self.prefix = prefix - self.message = message - self.highlight = highlight - - @classmethod - def from_line(cls, line): - return cls( - line.date, - line.date_printed, - line.tags, - line.prefix, - line.message, - line.highlight, - ) - - lines = [ - LineCopy.from_line(line) for line in self.weechat_buffer.lines - ] - sorted_lines = sorted(lines, key=lambda line: line.date, reverse=True) - - for line_number, line in enumerate(self.weechat_buffer.lines): - new = sorted_lines[line_number] - line.update( - new.date, new.date_printed, new.tags, new.prefix, new.message - ) - - def handle_backlog(self, response): - self.prev_batch = response.end - - for event in response.chunk: - # The first backlog request seems to have a race condition going on - # where we receive a message in a sync response, get a prev_batch, - # yet when we request older messages with the prev_batch the same - # message might appear in the room messages response. This only - # seems to happen if the message is relatively recently sent. - # Because of this we check if our first backlog request contains - # some already printed events, if so; skip printing them. - if (self.first_backlog_request - and event.event_id in self.printed_event_ids): - continue - - self.old_message(event) - - self.sort_messages() - - self.first_backlog_request = False - self.backlog_pending = False - - def handle_joined_room(self, info): - for event in info.state: - self.handle_state_event(event) - - timeline_events = None - - # This is a rejoin, skip already handled events - if not self.joined: - leave_index = None - - for i, event in enumerate(info.timeline.events): - if event.event_id == self.leave_event_id: - leave_index = i - break - - if leave_index: - timeline_events = info.timeline.events[leave_index + 1:] - # Handle our leave as a state event since we're not in the - # nicklist anymore but we're already printed out our leave - self.handle_state_event(info.timeline.events[leave_index]) - else: - timeline_events = info.timeline.events - - # mark that we are now joined - self.joined = True - - else: - timeline_events = info.timeline.events - - for event in timeline_events: - self.handle_timeline_event(event) - - for event in info.account_data: - if isinstance(event, FullyReadEvent): - if event.event_id == self.last_event_id: - current_buffer = W.buffer_search("", "") - - if self.weechat_buffer._ptr == current_buffer: - continue - - W.buffer_set(self.weechat_buffer._ptr, "unread", "") - W.buffer_set(self.weechat_buffer._ptr, "hotlist", "-1") - - # We didn't handle all joined users, the room display name might still - # be outdated because of that, update it now. - if self.unhandled_users: - self.update_buffer_name() - - def handle_left_room(self, info): - self.joined = False - - for event in info.state: - self.handle_state_event(event) - - for event in info.timeline.events: - self.handle_timeline_event(event) - - def error(self, string): - # type: (str) -> None - self.weechat_buffer.error(string) diff --git a/weechat/python/matrix/colors.py b/weechat/python/matrix/colors.py deleted file mode 100644 index c00bc0d..0000000 --- a/weechat/python/matrix/colors.py +++ /dev/null @@ -1,1285 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2008 Nicholas Marriott -# Copyright © 2016 Avi Halachmi -# Copyright © 2018, 2019 Damir Jelić -# Copyright © 2018, 2019 Denis Kasak -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals - -import html -import re -import textwrap - -# pylint: disable=redefined-builtin -from builtins import str -from collections import namedtuple -from typing import Dict, List, Optional, Union - -import webcolors -from pygments import highlight -from pygments.formatter import Formatter, get_style_by_name -from pygments.lexers import get_lexer_by_name -from pygments.util import ClassNotFound - -from . import globals as G -from .globals import W -from .utils import (string_strikethrough, - string_color_and_reset, - color_pair, - text_block, - colored_text_block) - -try: - from HTMLParser import HTMLParser -except ImportError: - from html.parser import HTMLParser - - -class FormattedString: - __slots__ = ("text", "attributes") - - def __init__(self, text, attributes): - self.attributes = DEFAULT_ATTRIBUTES.copy() - self.attributes.update(attributes) - self.text = text - - -class Formatted(object): - def __init__(self, substrings): - # type: (List[FormattedString]) -> None - self.substrings = substrings - - def textwrapper(self, width, colors): - return textwrap.TextWrapper( - width=width, - initial_indent="{}> ".format(W.color(colors)), - subsequent_indent="{}> ".format(W.color(colors)), - ) - - def is_formatted(self): - # type: (Formatted) -> bool - for string in self.substrings: - if string.attributes != DEFAULT_ATTRIBUTES: - return True - return False - - # TODO reverse video - @classmethod - def from_input_line(cls, line): - # type: (str) -> Formatted - """Parses the weechat input line and produces formatted strings that - can be later converted to HTML or to a string for weechat's print - functions - """ - text = "" # type: str - substrings = [] # type: List[FormattedString] - attributes = DEFAULT_ATTRIBUTES.copy() - - # If this is false, only IRC formatting characters will be parsed. - do_markdown = G.CONFIG.look.markdown_input - - # Disallow backticks in URLs so that code blocks are unaffected by the - # URL handling - url_regex = r"\b[a-z]+://[^\s`]+" - - # Escaped things are not markdown delimiters, so substitute them away - # when (quickly) looking for the last delimiters in the line. - # Additionally, URLs are ignored for the purposes of markdown - # delimiters. - # Note that the replacement needs to be the same length as the original - # for the indices to be correct. - escaped_masked = re.sub( - r"\\[\\*_`]|(?:" + url_regex + ")", - lambda m: "a" * len(m.group(0)), - line - ) - - def last_match_index(regex, offset_in_match): - matches = list(re.finditer(regex, escaped_masked)) - return matches[-1].span()[0] + offset_in_match if matches else -1 - - # 'needs_word': whether the wrapper must surround words, for example - # '*italic*' and not '* not-italic *'. - # 'validate': whether it can occur within the current attributes - wrappers = { - "**": { - "key": "bold", - "last_index": last_match_index(r"\S\*\*", 1), - "needs_word": True, - "validate": lambda attrs: not attrs["code"], - }, - "*": { - "key": "italic", - "last_index": last_match_index(r"\S\*($|[^*])", 1), - "needs_word": True, - "validate": lambda attrs: not attrs["code"], - }, - "_": { - "key": "italic", - "last_index": last_match_index(r"\S_", 1), - "needs_word": True, - "validate": lambda attrs: not attrs["code"], - }, - "`": { - "key": "code", - "last_index": last_match_index(r"`", 0), - "needs_word": False, - "validate": lambda attrs: True, - } - } - wrapper_init_chars = set(k[0] for k in wrappers.keys()) - wrapper_max_len = max(len(k) for k in wrappers.keys()) - - irc_toggles = { - "\x02": "bold", - "\x1D": "italic", - "\x1F": "underline", - } - - # Characters that consume a prefixed backslash - escapable_chars = wrapper_init_chars.copy() - escapable_chars.add("\\") - - # Collect URL spans - url_spans = [m.span() for m in re.finditer(url_regex, line)] - url_spans.reverse() # we'll be popping from the end - - # Whether we are currently in a URL - in_url = False - - i = 0 - while i < len(line): - # Update the 'in_url' flag. The first condition is not a while loop - # because URLs must contain '://', ensuring that we will not skip 2 - # URLs in one iteration. - if url_spans and i >= url_spans[-1][1]: - in_url = False - url_spans.pop() - if url_spans and i >= url_spans[-1][0]: - in_url = True - - # Markdown escape - if do_markdown and \ - i + 1 < len(line) and line[i] == "\\" \ - and (line[i + 1] in escapable_chars - if not attributes["code"] - else line[i + 1] == "`") \ - and not in_url: - text += line[i + 1] - i = i + 2 - - # IRC bold/italic/underline - elif line[i] in irc_toggles and not attributes["code"]: - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - key = irc_toggles[line[i]] - attributes[key] = not attributes[key] - i = i + 1 - - # IRC reset - elif line[i] == "\x0F" and not attributes["code"]: - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - # Reset all the attributes - attributes = DEFAULT_ATTRIBUTES.copy() - i = i + 1 - - # IRC color - elif line[i] == "\x03" and not attributes["code"]: - if text: - substrings.append(FormattedString(text, attributes.copy())) - text = "" - i = i + 1 - - # check if it's a valid color, add it to the attributes - if line[i].isdigit(): - color_string = line[i] - i = i + 1 - - if line[i].isdigit(): - if color_string == "0": - color_string = line[i] - else: - color_string = color_string + line[i] - i = i + 1 - - attributes["fgcolor"] = color_line_to_weechat(color_string) - else: - attributes["fgcolor"] = None - - # check if we have a background color - if line[i] == "," and line[i + 1].isdigit(): - color_string = line[i + 1] - i = i + 2 - - if line[i].isdigit(): - if color_string == "0": - color_string = line[i] - else: - color_string = color_string + line[i] - i = i + 1 - - attributes["bgcolor"] = color_line_to_weechat(color_string) - else: - attributes["bgcolor"] = None - - # Markdown wrapper (emphasis/bold/code) - elif do_markdown and line[i] in wrapper_init_chars and not in_url: - for l in range(wrapper_max_len, 0, -1): - if i + l <= len(line) and line[i : i + l] in wrappers: - descriptor = wrappers[line[i : i + l]] - - if not descriptor["validate"](attributes): - continue - - if attributes[descriptor["key"]]: - # needs_word wrappers can only be turned off if - # preceded by non-whitespace - if (i >= 1 and not line[i - 1].isspace()) \ - or not descriptor["needs_word"]: - if text: - # strip leading and trailing spaces and - # compress consecutive spaces in inline - # code blocks - if descriptor["key"] == "code": - text = re.sub(r"\s+", " ", text.strip()) - substrings.append( - FormattedString(text, attributes.copy())) - text = "" - attributes[descriptor["key"]] = False - i = i + l - else: - text = text + line[i : i + l] - i = i + l - - # Must have a chance of closing this, and needs_word - # wrappers must be followed by non-whitespace - elif descriptor["last_index"] >= i + l and \ - (not line[i + l].isspace() or \ - not descriptor["needs_word"]): - if text: - substrings.append( - FormattedString(text, attributes.copy())) - text = "" - attributes[descriptor["key"]] = True - i = i + l - - else: - text = text + line[i : i + l] - i = i + l - - break - - else: - # No wrapper matched here (NOTE: cannot happen since all - # wrapper prefixes are also wrappers, but for completeness' - # sake) - text = text + line[i] - i = i + 1 - - # Normal text - else: - text = text + line[i] - i = i + 1 - - if text: - substrings.append(FormattedString(text, attributes)) - - return cls(substrings) - - @classmethod - def from_html(cls, html): - # type: (str) -> Formatted - parser = MatrixHtmlParser() - parser.feed(html) - return cls(parser.get_substrings()) - - def to_html(self): - def add_attribute(string, name, value): - if name == "bold" and value: - return "{bold_on}{text}{bold_off}".format( - bold_on="", text=string, bold_off="" - ) - if name == "italic" and value: - return "{italic_on}{text}{italic_off}".format( - italic_on="", text=string, italic_off="" - ) - if name == "underline" and value: - return "{underline_on}{text}{underline_off}".format( - underline_on="", text=string, underline_off="" - ) - if name == "strikethrough" and value: - return "{strike_on}{text}{strike_off}".format( - strike_on="", text=string, strike_off="" - ) - if name == "quote" and value: - return "{quote_on}{text}{quote_off}".format( - quote_on="
", - text=string, - quote_off="
", - ) - if name == "code" and value: - return "{code_on}{text}{code_off}".format( - code_on="", text=string, code_off="" - ) - - return string - - def add_color(string, fgcolor, bgcolor): - fgcolor_string = "" - bgcolor_string = "" - - if fgcolor: - fgcolor_string = " data-mx-color={}".format( - color_weechat_to_html(fgcolor) - ) - - if bgcolor: - bgcolor_string = " data-mx-bg-color={}".format( - color_weechat_to_html(bgcolor) - ) - - return "{color_on}{text}{color_off}".format( - color_on="".format( - fg=fgcolor_string, - bg=bgcolor_string - ), - text=string, - color_off="", - ) - - def format_string(formatted_string): - text = formatted_string.text - attributes = formatted_string.attributes.copy() - - # Escape HTML tag characters - text = text.replace("&", "&") \ - .replace("<", "<") \ - .replace(">", ">") - - if attributes["code"]: - if attributes["preformatted"]: - # XXX: This can't really happen since there's no way of - # creating preformatted code blocks in weechat (because - # there is not multiline input), but I'm creating this - # branch as a note that it should be handled once we do - # implement them. - pass - else: - text = add_attribute(text, "code", True) - attributes.pop("code") - - if attributes["fgcolor"] or attributes["bgcolor"]: - text = add_color( - text, - attributes["fgcolor"], - attributes["bgcolor"] - ) - - if attributes["fgcolor"]: - attributes.pop("fgcolor") - - if attributes["bgcolor"]: - attributes.pop("bgcolor") - - for key, value in attributes.items(): - text = add_attribute(text, key, value) - - return text - - html_string = map(format_string, self.substrings) - return "".join(html_string) - - # TODO do we want at least some formatting using unicode - # (strikethrough, quotes)? - def to_plain(self): - # type: () -> str - def strip_atribute(string, _, __): - return string - - def format_string(formatted_string): - text = formatted_string.text - attributes = formatted_string.attributes - - for key, value in attributes.items(): - text = strip_atribute(text, key, value) - return text - - plain_string = map(format_string, self.substrings) - return "".join(plain_string) - - def to_weechat(self): - def add_attribute(string, name, value, attributes): - if not value: - return string - elif name == "bold": - return "{bold_on}{text}{bold_off}".format( - bold_on=W.color("bold"), - text=string, - bold_off=W.color("-bold"), - ) - elif name == "italic": - return "{italic_on}{text}{italic_off}".format( - italic_on=W.color("italic"), - text=string, - italic_off=W.color("-italic"), - ) - elif name == "underline": - return "{underline_on}{text}{underline_off}".format( - underline_on=W.color("underline"), - text=string, - underline_off=W.color("-underline"), - ) - elif name == "strikethrough": - return string_strikethrough(string) - elif name == "quote": - quote_pair = color_pair(G.CONFIG.color.quote_fg, - G.CONFIG.color.quote_bg) - - # Remove leading and trailing newlines; Riot sends an extra - # quoted "\n" when a user quotes a message. - string = string.strip("\n") - if len(string) == 0: - return string - - if G.CONFIG.look.quote_wrap >= 0: - wrapper = self.textwrapper(G.CONFIG.look.quote_wrap, quote_pair) - return wrapper.fill(W.string_remove_color(string, "")) - else: - # Don't wrap, just add quote markers to all lines - return "{color_on}{text}{color_off}".format( - color_on=W.color(quote_pair), - text="> " + W.string_remove_color(string.replace("\n", "\n> "), ""), - color_off=W.color("resetcolor") - ) - elif name == "code": - code_color_pair = color_pair( - G.CONFIG.color.untagged_code_fg, - G.CONFIG.color.untagged_code_bg - ) - - margin = G.CONFIG.look.code_block_margin - - if attributes["preformatted"]: - # code block - - try: - lexer = get_lexer_by_name(value) - except ClassNotFound: - if G.CONFIG.look.code_blocks: - return colored_text_block( - string, - margin=margin, - color_pair=code_color_pair) - else: - return string_color_and_reset(string, - code_color_pair) - - try: - style = get_style_by_name(G.CONFIG.look.pygments_style) - except ClassNotFound: - style = "native" - - if G.CONFIG.look.code_blocks: - code_block = text_block(string, margin=margin) - else: - code_block = string - - # highlight adds a newline to the end of the string, remove - # it from the output - highlighted_code = highlight( - code_block, - lexer, - WeechatFormatter(style=style) - ).rstrip() - - return highlighted_code - else: - return string_color_and_reset(string, code_color_pair) - elif name == "fgcolor": - return "{color_on}{text}{color_off}".format( - color_on=W.color(value), - text=string, - color_off=W.color("resetcolor"), - ) - elif name == "bgcolor": - return "{color_on}{text}{color_off}".format( - color_on=W.color("," + value), - text=string, - color_off=W.color("resetcolor"), - ) - else: - return string - - def format_string(formatted_string): - text = formatted_string.text - attributes = formatted_string.attributes - - # We need to handle strikethrough first, since doing - # a strikethrough followed by other attributes succeeds in the - # terminal, but doing it the other way around results in garbage. - if "strikethrough" in attributes: - text = add_attribute( - text, - "strikethrough", - attributes["strikethrough"], - attributes - ) - attributes.pop("strikethrough") - - def indent(text, prefix): - return prefix + text.replace("\n", "\n{}".format(prefix)) - - for key, value in attributes.items(): - if not value: - continue - - # Don't use textwrap to quote the code - if key == "quote" and attributes["code"]: - continue - - # Reflow inline code blocks - if key == "code" and not attributes["preformatted"]: - text = text.strip().replace('\n', ' ') - - text = add_attribute(text, key, value, attributes) - - # If we're quoted code add quotation marks now. - if key == "code" and attributes["quote"]: - fg = G.CONFIG.color.quote_fg - bg = G.CONFIG.color.quote_bg - text = indent( - text, - string_color_and_reset(">", color_pair(fg, bg)) + " ", - ) - - # If we're code don't remove multiple newlines blindly - if attributes["code"]: - return text - return re.sub(r"\n+", "\n", text) - - weechat_strings = map(format_string, self.substrings) - - # Remove duplicate \n elements from the list - strings = [] - for string in weechat_strings: - if len(strings) == 0 or string != "\n" or string != strings[-1]: - strings.append(string) - - return "".join(strings).strip() - - -DEFAULT_ATTRIBUTES = { - "bold": False, - "italic": False, - "underline": False, - "strikethrough": False, - "preformatted": False, - "quote": False, - "code": None, - "fgcolor": None, - "bgcolor": None, -} # type: Dict[str, Union[bool, Optional[str]]] - - -class MatrixHtmlParser(HTMLParser): - # TODO bullets - def __init__(self): - HTMLParser.__init__(self) - self.text = "" # type: str - self.substrings = [] # type: List[FormattedString] - self.attributes = DEFAULT_ATTRIBUTES.copy() - - def unescape(self, text): - """Shim to unescape HTML in both Python 2 and 3. - - The instance method was deprecated in Python 3 and html.unescape - doesn't exist in Python 2 so this is needed. - """ - try: - return html.unescape(text) - except AttributeError: - return HTMLParser.unescape(self, text) - - def add_substring(self, text, attrs): - fmt_string = FormattedString(text, attrs) - self.substrings.append(fmt_string) - - def _toggle_attribute(self, attribute): - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "" - self.attributes[attribute] = not self.attributes[attribute] - - def handle_starttag(self, tag, attrs): - if tag == "strong": - self._toggle_attribute("bold") - elif tag == "em": - self._toggle_attribute("italic") - elif tag == "u": - self._toggle_attribute("underline") - elif tag == "del": - self._toggle_attribute("strikethrough") - elif tag == "blockquote": - self._toggle_attribute("quote") - elif tag == "pre": - self._toggle_attribute("preformatted") - elif tag == "code": - lang = None - - for key, value in attrs: - if key == "class": - if value.startswith("language-"): - lang = value.split("-", 1)[1] - - lang = lang or "unknown" - - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "" - self.attributes["code"] = lang - elif tag == "p": - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "\n" - self.add_substring(self.text, DEFAULT_ATTRIBUTES.copy()) - self.text = "" - elif tag == "br": - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "\n" - self.add_substring(self.text, DEFAULT_ATTRIBUTES.copy()) - self.text = "" - elif tag == "font": - for key, value in attrs: - if key in ["data-mx-color", "color"]: - color = color_html_to_weechat(value) - - if not color: - continue - - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "" - self.attributes["fgcolor"] = color - - elif key in ["data-mx-bg-color"]: - color = color_html_to_weechat(value) - if not color: - continue - - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "" - self.attributes["bgcolor"] = color - - else: - pass - - def handle_endtag(self, tag): - if tag == "strong": - self._toggle_attribute("bold") - elif tag == "em": - self._toggle_attribute("italic") - elif tag == "u": - self._toggle_attribute("underline") - elif tag == "del": - self._toggle_attribute("strikethrough") - elif tag == "pre": - self._toggle_attribute("preformatted") - elif tag == "code": - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "" - self.attributes["code"] = None - elif tag == "blockquote": - self._toggle_attribute("quote") - self.text = "\n" - self.add_substring(self.text, DEFAULT_ATTRIBUTES.copy()) - self.text = "" - elif tag == "font": - if self.text: - self.add_substring(self.text, self.attributes.copy()) - self.text = "" - self.attributes["fgcolor"] = None - else: - pass - - def handle_data(self, data): - self.text += data - - def handle_entityref(self, name): - self.text += self.unescape("&{};".format(name)) - - def handle_charref(self, name): - self.text += self.unescape("&#{};".format(name)) - - def get_substrings(self): - if self.text: - self.add_substring(self.text, self.attributes.copy()) - - return self.substrings - - -def color_line_to_weechat(color_string): - # type: (str) -> str - line_colors = { - "0": "white", - "1": "black", - "2": "blue", - "3": "green", - "4": "lightred", - "5": "red", - "6": "magenta", - "7": "brown", - "8": "yellow", - "9": "lightgreen", - "10": "cyan", - "11": "lightcyan", - "12": "lightblue", - "13": "lightmagenta", - "14": "darkgray", - "15": "gray", - "16": "52", - "17": "94", - "18": "100", - "19": "58", - "20": "22", - "21": "29", - "22": "23", - "23": "24", - "24": "17", - "25": "54", - "26": "53", - "27": "89", - "28": "88", - "29": "130", - "30": "142", - "31": "64", - "32": "28", - "33": "35", - "34": "30", - "35": "25", - "36": "18", - "37": "91", - "38": "90", - "39": "125", - "40": "124", - "41": "166", - "42": "184", - "43": "106", - "44": "34", - "45": "49", - "46": "37", - "47": "33", - "48": "19", - "49": "129", - "50": "127", - "51": "161", - "52": "196", - "53": "208", - "54": "226", - "55": "154", - "56": "46", - "57": "86", - "58": "51", - "59": "75", - "60": "21", - "61": "171", - "62": "201", - "63": "198", - "64": "203", - "65": "215", - "66": "227", - "67": "191", - "68": "83", - "69": "122", - "70": "87", - "71": "111", - "72": "63", - "73": "177", - "74": "207", - "75": "205", - "76": "217", - "77": "223", - "78": "229", - "79": "193", - "80": "157", - "81": "158", - "82": "159", - "83": "153", - "84": "147", - "85": "183", - "86": "219", - "87": "212", - "88": "16", - "89": "233", - "90": "235", - "91": "237", - "92": "239", - "93": "241", - "94": "244", - "95": "247", - "96": "250", - "97": "254", - "98": "231", - "99": "default", - } - - assert color_string in line_colors - - return line_colors[color_string] - - -# The functions color_dist_sq(), color_to_6cube(), and color_find_rgb -# are python ports of the same named functions from the tmux -# source, they are under the copyright of Nicholas Marriott, and Avi Halachmi -# under the ISC license. -# More info: https://github.com/tmux/tmux/blob/master/colour.c - - -def color_dist_sq(R, G, B, r, g, b): - # pylint: disable=invalid-name,too-many-arguments - # type: (int, int, int, int, int, int) -> int - return (R - r) * (R - r) + (G - g) * (G - g) + (B - b) * (B - b) - - -def color_to_6cube(v): - # pylint: disable=invalid-name - # type: (int) -> int - if v < 48: - return 0 - if v < 114: - return 1 - return (v - 35) // 40 - - -def color_find_rgb(r, g, b): - # type: (int, int, int) -> int - """Convert an RGB triplet to the xterm(1) 256 color palette. - - xterm provides a 6x6x6 color cube (16 - 231) and 24 greys (232 - 255). - We map our RGB color to the closest in the cube, also work out the - closest grey, and use the nearest of the two. - - Note that the xterm has much lower resolution for darker colors (they - are not evenly spread out), so our 6 levels are not evenly spread: 0x0, - 0x5f (95), 0x87 (135), 0xaf (175), 0xd7 (215) and 0xff (255). Greys are - more evenly spread (8, 18, 28 ... 238). - """ - # pylint: disable=invalid-name - q2c = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] - - # Map RGB to 6x6x6 cube. - qr = color_to_6cube(r) - qg = color_to_6cube(g) - qb = color_to_6cube(b) - - cr = q2c[qr] - cg = q2c[qg] - cb = q2c[qb] - - # If we have hit the color exactly, return early. - if cr == r and cg == g and cb == b: - return 16 + (36 * qr) + (6 * qg) + qb - - # Work out the closest grey (average of RGB). - grey_avg = (r + g + b) // 3 - - if grey_avg > 238: - grey_idx = 23 - else: - grey_idx = (grey_avg - 3) // 10 - - grey = 8 + (10 * grey_idx) - - # Is grey or 6x6x6 color closest? - d = color_dist_sq(cr, cg, cb, r, g, b) - - if color_dist_sq(grey, grey, grey, r, g, b) < d: - idx = 232 + grey_idx - else: - idx = 16 + (36 * qr) + (6 * qg) + qb - - return idx - - -def color_html_to_weechat(color): - # type: (str) -> str - # yapf: disable - weechat_basic_colors = { - (0, 0, 0): "black", # 0 - (128, 0, 0): "red", # 1 - (0, 128, 0): "green", # 2 - (128, 128, 0): "brown", # 3 - (0, 0, 128): "blue", # 4 - (128, 0, 128): "magenta", # 5 - (0, 128, 128): "cyan", # 6 - (192, 192, 192): "default", # 7 - (128, 128, 128): "gray", # 8 - (255, 0, 0): "lightred", # 9 - (0, 255, 0): "lightgreen", # 10 - (255, 255, 0): "yellow", # 11 - (0, 0, 255): "lightblue", # 12 - (255, 0, 255): "lightmagenta", # 13 - (0, 255, 255): "lightcyan", # 14 - (255, 255, 255): "white", # 15 - } - # yapf: enable - - try: - rgb_color = webcolors.html5_parse_legacy_color(color) - except ValueError: - return "" - - if rgb_color in weechat_basic_colors: - return weechat_basic_colors[rgb_color] - - return str(color_find_rgb(*rgb_color)) - - -def color_weechat_to_html(color): - # type: (str) -> str - # yapf: disable - weechat_basic_colors = { - "black": "0", - "red": "1", - "green": "2", - "brown": "3", - "blue": "4", - "magenta": "5", - "cyan": "6", - "default": "7", - "gray": "8", - "lightred": "9", - "lightgreen": "10", - "yellow": "11", - "lightblue": "12", - "lightmagenta": "13", - "lightcyan": "14", - "white": "15", - } - hex_colors = { - "0": "#000000", - "1": "#800000", - "2": "#008000", - "3": "#808000", - "4": "#000080", - "5": "#800080", - "6": "#008080", - "7": "#c0c0c0", - "8": "#808080", - "9": "#ff0000", - "10": "#00ff00", - "11": "#ffff00", - "12": "#0000ff", - "13": "#ff00ff", - "14": "#00ffff", - "15": "#ffffff", - "16": "#000000", - "17": "#00005f", - "18": "#000087", - "19": "#0000af", - "20": "#0000d7", - "21": "#0000ff", - "22": "#005f00", - "23": "#005f5f", - "24": "#005f87", - "25": "#005faf", - "26": "#005fd7", - "27": "#005fff", - "28": "#008700", - "29": "#00875f", - "30": "#008787", - "31": "#0087af", - "32": "#0087d7", - "33": "#0087ff", - "34": "#00af00", - "35": "#00af5f", - "36": "#00af87", - "37": "#00afaf", - "38": "#00afd7", - "39": "#00afff", - "40": "#00d700", - "41": "#00d75f", - "42": "#00d787", - "43": "#00d7af", - "44": "#00d7d7", - "45": "#00d7ff", - "46": "#00ff00", - "47": "#00ff5f", - "48": "#00ff87", - "49": "#00ffaf", - "50": "#00ffd7", - "51": "#00ffff", - "52": "#5f0000", - "53": "#5f005f", - "54": "#5f0087", - "55": "#5f00af", - "56": "#5f00d7", - "57": "#5f00ff", - "58": "#5f5f00", - "59": "#5f5f5f", - "60": "#5f5f87", - "61": "#5f5faf", - "62": "#5f5fd7", - "63": "#5f5fff", - "64": "#5f8700", - "65": "#5f875f", - "66": "#5f8787", - "67": "#5f87af", - "68": "#5f87d7", - "69": "#5f87ff", - "70": "#5faf00", - "71": "#5faf5f", - "72": "#5faf87", - "73": "#5fafaf", - "74": "#5fafd7", - "75": "#5fafff", - "76": "#5fd700", - "77": "#5fd75f", - "78": "#5fd787", - "79": "#5fd7af", - "80": "#5fd7d7", - "81": "#5fd7ff", - "82": "#5fff00", - "83": "#5fff5f", - "84": "#5fff87", - "85": "#5fffaf", - "86": "#5fffd7", - "87": "#5fffff", - "88": "#870000", - "89": "#87005f", - "90": "#870087", - "91": "#8700af", - "92": "#8700d7", - "93": "#8700ff", - "94": "#875f00", - "95": "#875f5f", - "96": "#875f87", - "97": "#875faf", - "98": "#875fd7", - "99": "#875fff", - "100": "#878700", - "101": "#87875f", - "102": "#878787", - "103": "#8787af", - "104": "#8787d7", - "105": "#8787ff", - "106": "#87af00", - "107": "#87af5f", - "108": "#87af87", - "109": "#87afaf", - "110": "#87afd7", - "111": "#87afff", - "112": "#87d700", - "113": "#87d75f", - "114": "#87d787", - "115": "#87d7af", - "116": "#87d7d7", - "117": "#87d7ff", - "118": "#87ff00", - "119": "#87ff5f", - "120": "#87ff87", - "121": "#87ffaf", - "122": "#87ffd7", - "123": "#87ffff", - "124": "#af0000", - "125": "#af005f", - "126": "#af0087", - "127": "#af00af", - "128": "#af00d7", - "129": "#af00ff", - "130": "#af5f00", - "131": "#af5f5f", - "132": "#af5f87", - "133": "#af5faf", - "134": "#af5fd7", - "135": "#af5fff", - "136": "#af8700", - "137": "#af875f", - "138": "#af8787", - "139": "#af87af", - "140": "#af87d7", - "141": "#af87ff", - "142": "#afaf00", - "143": "#afaf5f", - "144": "#afaf87", - "145": "#afafaf", - "146": "#afafd7", - "147": "#afafff", - "148": "#afd700", - "149": "#afd75f", - "150": "#afd787", - "151": "#afd7af", - "152": "#afd7d7", - "153": "#afd7ff", - "154": "#afff00", - "155": "#afff5f", - "156": "#afff87", - "157": "#afffaf", - "158": "#afffd7", - "159": "#afffff", - "160": "#d70000", - "161": "#d7005f", - "162": "#d70087", - "163": "#d700af", - "164": "#d700d7", - "165": "#d700ff", - "166": "#d75f00", - "167": "#d75f5f", - "168": "#d75f87", - "169": "#d75faf", - "170": "#d75fd7", - "171": "#d75fff", - "172": "#d78700", - "173": "#d7875f", - "174": "#d78787", - "175": "#d787af", - "176": "#d787d7", - "177": "#d787ff", - "178": "#d7af00", - "179": "#d7af5f", - "180": "#d7af87", - "181": "#d7afaf", - "182": "#d7afd7", - "183": "#d7afff", - "184": "#d7d700", - "185": "#d7d75f", - "186": "#d7d787", - "187": "#d7d7af", - "188": "#d7d7d7", - "189": "#d7d7ff", - "190": "#d7ff00", - "191": "#d7ff5f", - "192": "#d7ff87", - "193": "#d7ffaf", - "194": "#d7ffd7", - "195": "#d7ffff", - "196": "#ff0000", - "197": "#ff005f", - "198": "#ff0087", - "199": "#ff00af", - "200": "#ff00d7", - "201": "#ff00ff", - "202": "#ff5f00", - "203": "#ff5f5f", - "204": "#ff5f87", - "205": "#ff5faf", - "206": "#ff5fd7", - "207": "#ff5fff", - "208": "#ff8700", - "209": "#ff875f", - "210": "#ff8787", - "211": "#ff87af", - "212": "#ff87d7", - "213": "#ff87ff", - "214": "#ffaf00", - "215": "#ffaf5f", - "216": "#ffaf87", - "217": "#ffafaf", - "218": "#ffafd7", - "219": "#ffafff", - "220": "#ffd700", - "221": "#ffd75f", - "222": "#ffd787", - "223": "#ffd7af", - "224": "#ffd7d7", - "225": "#ffd7ff", - "226": "#ffff00", - "227": "#ffff5f", - "228": "#ffff87", - "229": "#ffffaf", - "230": "#ffffd7", - "231": "#ffffff", - "232": "#080808", - "233": "#121212", - "234": "#1c1c1c", - "235": "#262626", - "236": "#303030", - "237": "#3a3a3a", - "238": "#444444", - "239": "#4e4e4e", - "240": "#585858", - "241": "#626262", - "242": "#6c6c6c", - "243": "#767676", - "244": "#808080", - "245": "#8a8a8a", - "246": "#949494", - "247": "#9e9e9e", - "248": "#a8a8a8", - "249": "#b2b2b2", - "250": "#bcbcbc", - "251": "#c6c6c6", - "252": "#d0d0d0", - "253": "#dadada", - "254": "#e4e4e4", - "255": "#eeeeee" - } - - # yapf: enable - if color in weechat_basic_colors: - return hex_colors[weechat_basic_colors[color]] - return hex_colors[color] - - -class WeechatFormatter(Formatter): - def __init__(self, **options): - Formatter.__init__(self, **options) - self.styles = {} - - for token, style in self.style: - start = end = "" - if style["color"]: - start += "{}".format( - W.color(color_html_to_weechat(str(style["color"]))) - ) - end = "{}".format(W.color("resetcolor")) + end - if style["bold"]: - start += W.color("bold") - end = W.color("-bold") + end - if style["italic"]: - start += W.color("italic") - end = W.color("-italic") + end - if style["underline"]: - start += W.color("underline") - end = W.color("-underline") + end - self.styles[token] = (start, end) - - def format(self, tokensource, outfile): - lastval = "" - lasttype = None - - for ttype, value in tokensource: - while ttype not in self.styles: - ttype = ttype.parent - - if ttype == lasttype: - lastval += value - else: - if lastval: - stylebegin, styleend = self.styles[lasttype] - outfile.write(stylebegin + lastval + styleend) - # set lastval/lasttype to current values - lastval = value - lasttype = ttype - - if lastval: - stylebegin, styleend = self.styles[lasttype] - outfile.write(stylebegin + lastval + styleend) diff --git a/weechat/python/matrix/commands.py b/weechat/python/matrix/commands.py deleted file mode 100644 index 53faf74..0000000 --- a/weechat/python/matrix/commands.py +++ /dev/null @@ -1,1969 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# Copyright © 2018, 2019 Denis Kasak -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals -import argparse -import os -import re -from builtins import str -from future.moves.itertools import zip_longest -from collections import defaultdict -from functools import partial -from nio import EncryptionError, LocalProtocolError - -from . import globals as G -from .colors import Formatted -from .globals import SERVERS, W, UPLOADS, SCRIPT_NAME -from .server import MatrixServer -from .utf import utf8_decode -from .utils import key_from_value, parse_redact_args -from .uploads import UploadsBuffer, Upload - -try: - from urllib.parse import urlparse -except ImportError: - from urlparse import urlparse # type: ignore - - -class ParseError(Exception): - pass - - -class WeechatArgParse(argparse.ArgumentParser): - def print_usage(self, file=None): - pass - - def error(self, message): - message = ( - "{prefix}Error: {message} for command {command} " - "(see /help {command})" - ).format(prefix=W.prefix("error"), message=message, command=self.prog) - W.prnt("", message) - raise ParseError - - -class WeechatCommandParser(object): - @staticmethod - def _run_parser(parser, args): - try: - parsed_args = parser.parse_args(args.split()) - return parsed_args - except ParseError: - return None - - @staticmethod - def topic(args): - parser = WeechatArgParse(prog="topic") - - parser.add_argument("-delete", action="store_true") - parser.add_argument("topic", nargs="*") - - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def kick(args): - parser = WeechatArgParse(prog="kick") - parser.add_argument("user_id") - parser.add_argument("reason", nargs="*") - - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def invite(args): - parser = WeechatArgParse(prog="invite") - parser.add_argument("user_id") - - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def join(args): - parser = WeechatArgParse(prog="join") - parser.add_argument("room_id") - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def part(args): - parser = WeechatArgParse(prog="part") - parser.add_argument("room_id", nargs="?") - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def devices(args): - parser = WeechatArgParse(prog="devices") - subparsers = parser.add_subparsers(dest="subcommand") - subparsers.add_parser("list") - - delete_parser = subparsers.add_parser("delete") - delete_parser.add_argument("device_id") - - name_parser = subparsers.add_parser("set-name") - name_parser.add_argument("device_id") - name_parser.add_argument("device_name", nargs="*") - - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def olm(args): - parser = WeechatArgParse(prog="olm") - subparsers = parser.add_subparsers(dest="subcommand") - - info_parser = subparsers.add_parser("info") - info_parser.add_argument( - "category", nargs="?", default="private", - choices=[ - "all", - "blacklisted", - "private", - "unverified", - "verified", - "ignored" - ]) - info_parser.add_argument("filter", nargs="?") - - verify_parser = subparsers.add_parser("verify") - verify_parser.add_argument("user_filter") - verify_parser.add_argument("device_filter", nargs="?") - - unverify_parser = subparsers.add_parser("unverify") - unverify_parser.add_argument("user_filter") - unverify_parser.add_argument("device_filter", nargs="?") - - blacklist_parser = subparsers.add_parser("blacklist") - blacklist_parser.add_argument("user_filter") - blacklist_parser.add_argument("device_filter", nargs="?") - - unblacklist_parser = subparsers.add_parser("unblacklist") - unblacklist_parser.add_argument("user_filter") - unblacklist_parser.add_argument("device_filter", nargs="?") - - ignore_parser = subparsers.add_parser("ignore") - ignore_parser.add_argument("user_filter") - ignore_parser.add_argument("device_filter", nargs="?") - - unignore_parser = subparsers.add_parser("unignore") - unignore_parser.add_argument("user_filter") - unignore_parser.add_argument("device_filter", nargs="?") - - export_parser = subparsers.add_parser("export") - export_parser.add_argument("file") - export_parser.add_argument("passphrase") - - import_parser = subparsers.add_parser("import") - import_parser.add_argument("file") - import_parser.add_argument("passphrase") - - sas_parser = subparsers.add_parser("verification") - sas_parser.add_argument( - "action", - choices=[ - "start", - "accept", - "confirm", - "cancel", - ]) - sas_parser.add_argument("user_id") - sas_parser.add_argument("device_id") - - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def room(args): - parser = WeechatArgParse(prog="room") - subparsers = parser.add_subparsers(dest="subcommand") - typing_notification = subparsers.add_parser("typing-notifications") - typing_notification.add_argument( - "state", - choices=["enable", "disable", "toggle"] - ) - - read_markers = subparsers.add_parser("read-markers") - read_markers.add_argument( - "state", - choices=["enable", "disable", "toggle"] - ) - - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def uploads(args): - parser = WeechatArgParse(prog="uploads") - subparsers = parser.add_subparsers(dest="subcommand") - subparsers.add_parser("list") - subparsers.add_parser("listfull") - subparsers.add_parser("up") - subparsers.add_parser("down") - - return WeechatCommandParser._run_parser(parser, args) - - @staticmethod - def upload(args): - parser = WeechatArgParse(prog="upload") - parser.add_argument("file") - return WeechatCommandParser._run_parser(parser, args) - - -def grouper(iterable, n, fillvalue=None): - "Collect data into fixed-length chunks or blocks" - # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx" - args = [iter(iterable)] * n - return zip_longest(*args, fillvalue=fillvalue) - - -def partition_key(key): - groups = grouper(key, 4, " ") - return ' '.join(''.join(g) for g in groups) - - -def hook_commands(): - W.hook_command( - # Command name and short description - "matrix", - "Matrix chat protocol command", - # Synopsis - ( - "server add [:] ||" - "server delete|list|listfull ||" - "connect ||" - "disconnect ||" - "reconnect ||" - "help " - ), - # Description - ( - " server: list, add, or remove Matrix servers\n" - " connect: connect to Matrix servers\n" - "disconnect: disconnect from one or all Matrix servers\n" - " reconnect: reconnect to server(s)\n" - " help: show detailed command help\n\n" - "Use /matrix help [command] to find out more.\n" - ), - # Completions - ( - "server %(matrix_server_commands)|%* ||" - "connect %(matrix_servers) ||" - "disconnect %(matrix_servers) ||" - "reconnect %(matrix_servers) ||" - "help %(matrix_commands)" - ), - # Function name - "matrix_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "redact", - "redact messages", - # Synopsis - ('[:""] []'), - # Description - ( - " event-id: event id of the message that will be redacted\n" - "message-part: an initial part of the message (ignored, only " - "used\n" - " as visual feedback when using completion)\n" - " reason: the redaction reason\n" - ), - # Completions - ("%(matrix_messages)"), - # Function name - "matrix_redact_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "reply-matrix", - "reply to a message", - # Synopsis - ('[:""] []'), - # Description - ( - " event-id: event id of the message that will be replied to\n" - "message-part: an initial part of the message (ignored, only " - "used\n" - " as visual feedback when using completion)\n" - " reply: the reply\n" - ), - # Completions - ("%(matrix_messages)"), - # Function name - "matrix_reply_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "topic", - "get/set the room topic", - # Synopsis - ("[|-delete]"), - # Description - (" topic: topic to set\n" "-delete: delete room topic"), - # Completions - "", - # Callback - "matrix_topic_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "me", - "send an emote message to the current room", - # Synopsis - (""), - # Description - ("message: message to send"), - # Completions - "", - # Callback - "matrix_me_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "kick", - "kick a user from the current room", - # Synopsis - (" []"), - # Description - ( - "user-id: user-id to kick\n" - " reason: reason why the user was kicked" - ), - # Completions - ("%(matrix_users)"), - # Callback - "matrix_kick_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "invite", - "invite a user to the current room", - # Synopsis - (""), - # Description - ("user-id: user-id to invite"), - # Completions - ("%(matrix_users)"), - # Callback - "matrix_invite_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "join", - "join a room", - # Synopsis - ("|"), - # Description - ( - " room-id: room-id of the room to join\n" - "room-alias: room alias of the room to join" - ), - # Completions - "", - # Callback - "matrix_join_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "part", - "leave a room", - # Synopsis - ("[]"), - # Description - (" room-name: room name of the room to leave"), - # Completions - "", - # Callback - "matrix_part_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "devices", - "list, delete or rename matrix devices", - # Synopsis - ("list ||" - "delete ||" - "set-name " - ), - # Description - ("device-id: device id of the device to delete\n" - " name: new device name to set\n"), - # Completions - ("list ||" - "delete %(matrix_own_devices) ||" - "set-name %(matrix_own_devices)"), - # Callback - "matrix_devices_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "olm", - "Matrix olm encryption configuration command", - # Synopsis - ("info all|blacklisted|ignored|private|unverified|verified ||" - "blacklist ||" - "unverify ||" - "verify ||" - "verification start|accept|cancel|confirm ||" - "ignore ||" - "unignore ||" - "export ||" - "import " - ), - # Description - (" info: show info about known devices and their keys\n" - " blacklist: blacklist a device\n" - "unblacklist: unblacklist a device\n" - " unverify: unverify a device\n" - " verify: verify a device\n" - " ignore: ignore an unverifiable but non-blacklist-worthy device\n" - " unignore: unignore a device\n" - "verification: manage interactive device verification\n" - " export: export encryption keys\n" - " import: import encryption keys\n\n" - "Examples:" - "\n /olm verify @example:example.com *" - "\n /olm info all example*" - ), - # Completions - ('info all|blacklisted|ignored|private|unverified|verified ||' - 'blacklist %(olm_user_ids) %(olm_devices) ||' - 'unblacklist %(olm_user_ids) %(olm_devices) ||' - 'unverify %(olm_user_ids) %(olm_devices) ||' - 'verify %(olm_user_ids) %(olm_devices) ||' - 'verification start|accept|cancel|confirm %(olm_user_ids) %(olm_devices) ||' - 'ignore %(olm_user_ids) %(olm_devices) ||' - 'unignore %(olm_user_ids) %(olm_devices) ||' - 'export %(filename) ||' - 'import %(filename)' - ), - # Function name - 'matrix_olm_command_cb', - '') - - W.hook_command( - # Command name and short description - "room", - "change room state", - # Synopsis - ("typing-notifications ||" - "read-markers " - ), - # Description - ("state: one of enable, disable or toggle\n"), - # Completions - ("typing-notifications enable|disable|toggle||" - "read-markers enable|disable|toggle" - ), - # Callback - "matrix_room_command_cb", - "", - ) - - # W.hook_command( - # # Command name and short description - # "uploads", - # "Open the uploads buffer or list uploads in the core buffer", - # # Synopsis - # ("list||" - # "listfull" - # ), - # # Description - # (""), - # # Completions - # ("list ||" - # "listfull"), - # # Callback - # "matrix_uploads_command_cb", - # "", - # ) - - W.hook_command( - # Command name and short description - "upload", - "Upload a file to a room", - # Synopsis - (""), - # Description - (""), - # Completions - ("%(filename)"), - # Callback - "matrix_upload_command_cb", - "", - ) - - W.hook_command( - # Command name and short description - "send-anyways", - "Send the last message in a room ignorin unverified devices.", - # Synopsis - "", - # Description - "Send the last message in a room despite there being unverified " - "devices. The unverified devices will be marked as ignored after " - "running this command.", - # Completions - "", - # Callback - "matrix_send_anyways_cb", - "", - ) - - W.hook_command_run("/buffer clear", "matrix_command_buf_clear_cb", "") - - if G.CONFIG.network.fetch_backlog_on_pgup: - hook_page_up() - - -def hook_key_bindings(): - W.hook_hsignal("matrix_cursor_reply", "matrix_cursor_reply_signal_cb", "") - - binding = "@chat(python.{}*):r".format(G.BUFFER_NAME_PREFIX) - W.key_bind("cursor", { - binding: "hsignal:matrix_cursor_reply", - }) - - -def format_device(device_id, fp_key, display_name): - fp_key = partition_key(fp_key) - message = (" - Device ID: {device_color}{device_id}{ncolor}\n" - " - Display name: {device_color}{display_name}{ncolor}\n" - " - Device key: {key_color}{fp_key}{ncolor}").format( - device_color=W.color("chat_channel"), - device_id=device_id, - ncolor=W.color("reset"), - display_name=display_name, - key_color=W.color("chat_server"), - fp_key=fp_key) - return message - - -def olm_info_command(server, args): - def print_devices( - device_store, - filter_regex, - device_category="All", - predicate=None, - ): - user_strings = [] - try: - filter_regex = re.compile(args.filter) if args.filter else None - except re.error as e: - server.error("Invalid regular expression: {}.".format(e.args[0])) - return - - for user_id in sorted(device_store.users): - device_strings = [] - for device in device_store.active_user_devices(user_id): - if filter_regex: - if (not filter_regex.search(user_id) and - not filter_regex.search(device.id)): - continue - - if predicate: - if not predicate(device): - continue - - device_strings.append(format_device( - device.id, - device.ed25519, - device.display_name - )) - - if not device_strings: - continue - - d_string = "\n".join(device_strings) - message = (" - User: {user_color}{user}{ncolor}\n").format( - user_color=W.color("chat_nick"), - user=user_id, - ncolor=W.color("reset")) - message += d_string - user_strings.append(message) - - if not user_strings: - message = ("{prefix}matrix: No matching devices " - "found.").format(prefix=W.prefix("error")) - W.prnt(server.server_buffer, message) - return - - server.info("{} devices:\n".format(device_category)) - W.prnt(server.server_buffer, "\n".join(user_strings)) - - olm = server.client.olm - - if not hasattr(args, 'category') or args.category == "private": - fp_key = partition_key(olm.account.identity_keys["ed25519"]) - message = ("Identity keys:\n" - " - User: {user_color}{user}{ncolor}\n" - " - Device ID: {device_color}{device_id}{ncolor}\n" - " - Device key: {key_color}{fp_key}{ncolor}\n" - "").format( - user_color=W.color("chat_self"), - ncolor=W.color("reset"), - user=olm.user_id, - device_color=W.color("chat_channel"), - device_id=olm.device_id, - key_color=W.color("chat_server"), - fp_key=fp_key) - server.info(message) - - elif args.category == "all": - print_devices(olm.device_store, args.filter) - - elif args.category == "verified": - print_devices( - olm.device_store, - args.filter, - "Verified", - olm.is_device_verified - ) - - elif args.category == "unverified": - def predicate(device): - return not olm.is_device_verified(device) - - print_devices( - olm.device_store, - args.filter, - "Unverified", - predicate - ) - - elif args.category == "blacklisted": - print_devices( - olm.device_store, - args.filter, - "Blacklisted", - olm.is_device_blacklisted - ) - - elif args.category == "ignored": - print_devices( - olm.device_store, - args.filter, - "Ignored", - olm.is_device_ignored - ) - - -def olm_action_command(server, args, category, error_category, prefix, action): - device_store = server.client.olm.device_store - users = [] - - if args.user_filter == "*": - users = device_store.users - else: - users = [x for x in device_store.users if args.user_filter in x] - - user_devices = { - user: device_store.active_user_devices(user) for user in users - } - - if args.device_filter and args.device_filter != "*": - filtered_user_devices = {} - for user, device_list in user_devices.items(): - filtered_devices = filter( - lambda x: args.device_filter in x.id, - device_list - ) - filtered_user_devices[user] = list(filtered_devices) - user_devices = filtered_user_devices - - changed_devices = defaultdict(list) - - for user, device_list in user_devices.items(): - for device in device_list: - if action(device): - changed_devices[user].append(device) - - if not changed_devices: - message = ("{prefix}matrix: No matching {error_category} devices " - "found.").format( - prefix=W.prefix("error"), - error_category=error_category - ) - W.prnt(server.server_buffer, message) - return - - user_strings = [] - for user_id, device_list in changed_devices.items(): - device_strings = [] - message = (" - User: {user_color}{user}{ncolor}\n").format( - user_color=W.color("chat_nick"), - user=user_id, - ncolor=W.color("reset")) - for device in device_list: - device_strings.append(format_device( - device.id, - device.ed25519, - device.display_name - )) - if not device_strings: - continue - - d_string = "\n".join(device_strings) - message += d_string - user_strings.append(message) - - W.prnt(server.server_buffer, - "{}matrix: {} key(s):\n".format(W.prefix("prefix"), category)) - W.prnt(server.server_buffer, "\n".join(user_strings)) - pass - - -def olm_verify_command(server, args): - olm_action_command( - server, - args, - "Verified", - "unverified", - "join", - server.client.verify_device - ) - - -def olm_unverify_command(server, args): - olm_action_command( - server, - args, - "Unverified", - "verified", - "quit", - server.client.unverify_device - ) - - -def olm_blacklist_command(server, args): - olm_action_command( - server, - args, - "Blacklisted", - "unblacklisted", - "join", - server.client.blacklist_device - ) - - -def olm_unblacklist_command(server, args): - olm_action_command( - server, - args, - "Unblacklisted", - "blacklisted", - "join", - server.client.unblacklist_device - ) - - -def olm_ignore_command(server, args): - olm_action_command( - server, - args, - "Ignored", - "ignored", - "join", - server.client.ignore_device - ) - - -def olm_unignore_command(server, args): - olm_action_command( - server, - args, - "Unignored", - "unignored", - "join", - server.client.unignore_device - ) - - -def olm_export_command(server, args): - file_path = os.path.expanduser(args.file) - try: - server.client.export_keys(file_path, args.passphrase) - except (OSError, IOError) as e: - server.error("Error exporting keys: {}".format(str(e))) - - server.info("Successfully exported keys") - -def olm_import_command(server, args): - file_path = os.path.expanduser(args.file) - try: - server.client.import_keys(file_path, args.passphrase) - except (OSError, IOError, EncryptionError) as e: - server.error("Error importing keys: {}".format(str(e))) - - server.info("Successfully imported keys") - - -def olm_sas_command(server, args): - try: - device_store = server.client.device_store - except LocalProtocolError: - server.error("The device store is not loaded") - return W.WEECHAT_RC_OK - - try: - device = device_store[args.user_id][args.device_id] - except KeyError: - server.error("Device {} of user {} not found".format( - args.device_id, - args.user_id - )) - return W.WEECHAT_RC_OK - - if device.deleted: - server.error("Device {} of user {} is deleted.".format( - args.device_id, - args.user_id - )) - return W.WEECHAT_RC_OK - - if args.action == "start": - server.start_verification(device) - elif args.action in ["accept", "confirm", "cancel"]: - sas = server.client.get_active_sas(args.user_id, args.device_id) - - if not sas: - server.error("No active key verification found for " - "device {} of user {}.".format( - args.device_id, - args.user_id - )) - return W.WEECHAT_RC_OK - - try: - if args.action == "accept": - server.accept_sas(sas) - elif args.action == "confirm": - server.confirm_sas(sas) - elif args.action == "cancel": - server.cancel_sas(sas) - - except LocalProtocolError as e: - server.error(str(e)) - - -@utf8_decode -def matrix_olm_command_cb(data, buffer, args): - def command(server, data, buffer, args): - parsed_args = WeechatCommandParser.olm(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - if not server.client.olm: - W.prnt(server.server_buffer, "{}matrix: Olm account isn't " - "loaded.".format(W.prefix("error"))) - return W.WEECHAT_RC_OK - - if not parsed_args.subcommand or parsed_args.subcommand == "info": - olm_info_command(server, parsed_args) - elif parsed_args.subcommand == "export": - olm_export_command(server, parsed_args) - elif parsed_args.subcommand == "import": - olm_import_command(server, parsed_args) - elif parsed_args.subcommand == "verify": - olm_verify_command(server, parsed_args) - elif parsed_args.subcommand == "unverify": - olm_unverify_command(server, parsed_args) - elif parsed_args.subcommand == "blacklist": - olm_blacklist_command(server, parsed_args) - elif parsed_args.subcommand == "unblacklist": - olm_unblacklist_command(server, parsed_args) - elif parsed_args.subcommand == "verification": - olm_sas_command(server, parsed_args) - elif parsed_args.subcommand == "ignore": - olm_ignore_command(server, parsed_args) - elif parsed_args.subcommand == "unignore": - olm_unignore_command(server, parsed_args) - else: - message = ("{prefix}matrix: Command not implemented.".format( - prefix=W.prefix("error"))) - W.prnt(server.server_buffer, message) - - W.bar_item_update("buffer_modes") - W.bar_item_update("matrix_modes") - - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer in server.buffers.values(): - return command(server, data, buffer, args) - elif buffer == server.server_buffer: - return command(server, data, buffer, args) - - W.prnt("", "{prefix}matrix: command \"olm\" must be executed on a " - "matrix buffer (server or channel)".format( - prefix=W.prefix("error") - )) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_devices_command_cb(data, buffer, args): - for server in SERVERS.values(): - if buffer in server.buffers.values() or buffer == server.server_buffer: - parsed_args = WeechatCommandParser.devices(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - if not parsed_args.subcommand or parsed_args.subcommand == "list": - server.devices() - elif parsed_args.subcommand == "delete": - server.delete_device(parsed_args.device_id) - elif parsed_args.subcommand == "set-name": - new_name = " ".join(parsed_args.device_name).strip("\"") - server.rename_device(parsed_args.device_id, new_name) - - return W.WEECHAT_RC_OK - - W.prnt("", "{prefix}matrix: command \"devices\" must be executed on a " - "matrix buffer (server or channel)".format( - prefix=W.prefix("error") - )) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_me_command_cb(data, buffer, args): - for server in SERVERS.values(): - if buffer in server.buffers.values(): - - if not server.connected: - message = ( - "{prefix}matrix: you are not connected to " "the server" - ).format(prefix=W.prefix("error")) - W.prnt(server.server_buffer, message) - return W.WEECHAT_RC_ERROR - - room_buffer = server.find_room_from_ptr(buffer) - - if not server.client.logged_in: - room_buffer.error("You are not logged in.") - return W.WEECHAT_RC_ERROR - - if not args: - return W.WEECHAT_RC_OK - - formatted_data = Formatted.from_input_line(args) - - server.room_send_message(room_buffer, formatted_data, "m.emote") - return W.WEECHAT_RC_OK - - if buffer == server.server_buffer: - message = ( - '{prefix}matrix: command "me" must be ' - "executed on a Matrix channel buffer" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return W.WEECHAT_RC_OK - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_topic_command_cb(data, buffer, args): - parsed_args = WeechatCommandParser.topic(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer == server.server_buffer: - server.error( - 'command "topic" must be ' "executed on a Matrix room buffer" - ) - return W.WEECHAT_RC_OK - - room = server.find_room_from_ptr(buffer) - if not room: - continue - - if not parsed_args.topic and not parsed_args.delete: - # TODO print the current topic - return W.WEECHAT_RC_OK - - if parsed_args.delete and parsed_args.topic: - # TODO error message - return W.WEECHAT_RC_OK - - topic = "" if parsed_args.delete else " ".join(parsed_args.topic) - content = {"topic": topic} - server.room_send_state(room, content, "m.room.topic") - - return W.WEECHAT_RC_OK - - -def matrix_fetch_old_messages(server, room_id): - room_buffer = server.find_room_from_id(room_id) - room = room_buffer.room - - if room_buffer.backlog_pending: - return - - prev_batch = room.prev_batch - - if not prev_batch: - return - - raise NotImplementedError - - -def check_server_existence(server_name, servers): - if server_name not in servers: - message = "{prefix}matrix: No such server: {server}".format( - prefix=W.prefix("error"), server=server_name - ) - W.prnt("", message) - return False - return True - - -def hook_page_up(): - G.CONFIG.page_up_hook = W.hook_command_run( - "/window page_up", "matrix_command_pgup_cb", "" - ) - - -@utf8_decode -def matrix_command_buf_clear_cb(data, buffer, command): - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - room_buffer.room.prev_batch = server.next_batch - - return W.WEECHAT_RC_OK - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_command_pgup_cb(data, buffer, command): - # TODO the highlight status of a line isn't allowed to be updated/changed - # via hdata, therefore the highlight status of a messages can't be - # reoredered this would need to be fixed in weechat - # TODO we shouldn't fetch and print out more messages than - # max_buffer_lines_number or older messages than max_buffer_lines_minutes - for server in SERVERS.values(): - if buffer in server.buffers.values(): - window = W.window_search_with_buffer(buffer) - - first_line_displayed = bool( - W.window_get_integer(window, "first_line_displayed") - ) - - room_buffer = server.find_room_from_ptr(buffer) - - if first_line_displayed or room_buffer.weechat_buffer.num_lines == 0: - room_id = key_from_value(server.buffers, buffer) - server.room_get_messages(room_id) - - return W.WEECHAT_RC_OK - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_join_command_cb(data, buffer, args): - parsed_args = WeechatCommandParser.join(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer in server.buffers.values() or buffer == server.server_buffer: - server.room_join(parsed_args.room_id) - break - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_part_command_cb(data, buffer, args): - parsed_args = WeechatCommandParser.part(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer in server.buffers.values() or buffer == server.server_buffer: - room_id = parsed_args.room_id - - if not room_id: - if buffer == server.server_buffer: - server.error( - 'command "part" must be ' - "executed on a Matrix room buffer or a room " - "name needs to be given" - ) - return W.WEECHAT_RC_OK - - room_buffer = server.find_room_from_ptr(buffer) - room_id = room_buffer.room.room_id - - server.room_leave(room_id) - break - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_invite_command_cb(data, buffer, args): - parsed_args = WeechatCommandParser.invite(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer == server.server_buffer: - server.error( - 'command "invite" must be ' "executed on a Matrix room buffer" - ) - return W.WEECHAT_RC_OK - - room = server.find_room_from_ptr(buffer) - if not room: - continue - - user_id = parsed_args.user_id - user_id = user_id if user_id.startswith("@") else "@" + user_id - - server.room_invite(room, user_id) - break - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_room_command_cb(data, buffer, args): - parsed_args = WeechatCommandParser.room(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer == server.server_buffer: - server.error( - 'command "room" must be ' "executed on a Matrix room buffer" - ) - return W.WEECHAT_RC_OK - - room = server.find_room_from_ptr(buffer) - if not room: - continue - - if not parsed_args.subcommand or parsed_args.subcommand == "list": - server.error("command no subcommand found") - return W.WEECHAT_RC_OK - - if parsed_args.subcommand == "typing-notifications": - if parsed_args.state == "enable": - room.typing_enabled = True - elif parsed_args.state == "disable": - room.typing_enabled = False - elif parsed_args.state == "toggle": - room.typing_enabled = not room.typing_enabled - break - - elif parsed_args.subcommand == "read-markers": - if parsed_args.state == "enable": - room.read_markers_enabled = True - elif parsed_args.state == "disable": - room.read_markers_enabled = False - elif parsed_args.state == "toggle": - room.read_markers_enabled = not room.read_markers_enabled - break - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_uploads_command_cb(data, buffer, args): - if not args: - if not G.CONFIG.upload_buffer: - G.CONFIG.upload_buffer = UploadsBuffer() - G.CONFIG.upload_buffer.display() - return W.WEECHAT_RC_OK - - parsed_args = WeechatCommandParser.uploads(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - if parsed_args.subcommand == "list": - pass - elif parsed_args.subcommand == "listfull": - pass - elif parsed_args.subcommand == "up": - if G.CONFIG.upload_buffer: - G.CONFIG.upload_buffer.move_line_up() - elif parsed_args.subcommand == "down": - if G.CONFIG.upload_buffer: - G.CONFIG.upload_buffer.move_line_down() - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_upload_command_cb(data, buffer, args): - parsed_args = WeechatCommandParser.upload(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer == server.server_buffer: - server.error( - 'command "upload" must be ' "executed on a Matrix room buffer" - ) - return W.WEECHAT_RC_OK - - room_buffer = server.find_room_from_ptr(buffer) - if not room_buffer: - continue - - upload = Upload( - server.name, - server.config.address, - server.client.access_token, - room_buffer.room.room_id, - parsed_args.file, - room_buffer.room.encrypted - ) - UPLOADS[upload.uuid] = upload - - if G.CONFIG.upload_buffer: - G.CONFIG.upload_buffer.render() - - break - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_kick_command_cb(data, buffer, args): - parsed_args = WeechatCommandParser.kick(args) - if not parsed_args: - return W.WEECHAT_RC_OK - - for server in SERVERS.values(): - if buffer == server.server_buffer: - server.error( - 'command "kick" must be ' "executed on a Matrix room buffer" - ) - return W.WEECHAT_RC_OK - - room = server.find_room_from_ptr(buffer) - if not room: - continue - - user_id = parsed_args.user_id - user_id = user_id if user_id.startswith("@") else "@" + user_id - reason = " ".join(parsed_args.reason) if parsed_args.reason else None - - server.room_kick(room, user_id, reason) - break - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_redact_command_cb(data, buffer, args): - def already_redacted(line): - if SCRIPT_NAME + "_redacted" in line.tags: - return True - return False - - def predicate(event_id, line): - event_tag = SCRIPT_NAME + "_id_{}".format(event_id) - tags = line.tags - - if event_tag in tags: - return True - - return False - - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - - event_id, reason = parse_redact_args(args) - - if not event_id: - message = ( - "{prefix}matrix: Invalid command " - "arguments (see /help redact)" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return W.WEECHAT_RC_ERROR - - lines = room_buffer.weechat_buffer.find_lines( - partial(predicate, event_id), max_lines=1 - ) - - if not lines: - room_buffer.error( - "No such message with event id " - "{event_id} found.".format(event_id=event_id)) - return W.WEECHAT_RC_OK - - if already_redacted(lines[0]): - room_buffer.error("Event already redacted.") - return W.WEECHAT_RC_OK - - server.room_send_redaction(room_buffer, event_id, reason) - - return W.WEECHAT_RC_OK - - if buffer == server.server_buffer: - message = ( - '{prefix}matrix: command "redact" must be ' - "executed on a Matrix channel buffer" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return W.WEECHAT_RC_OK - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_reply_command_cb(data, buffer, args): - def predicate(event_id, line): - event_tag = SCRIPT_NAME + "_id_{}".format(event_id) - tags = line.tags - - if event_tag in tags: - return True - return False - - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - - # Intentional use of `parse_redact_args` which serves the - # necessary purpose - event_id, reply = parse_redact_args(args) - - if not event_id or not reply: - message = ( - "{prefix}matrix: Invalid command " - "arguments (see /help reply)" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return W.WEECHAT_RC_ERROR - - lines = room_buffer.weechat_buffer.find_lines( - partial(predicate, event_id), max_lines=1 - ) - - if not lines: - room_buffer.error( - "No such message with event id " - "{event_id} found.".format(event_id=event_id)) - return W.WEECHAT_RC_OK - - formatted_data = Formatted.from_input_line(reply) - server.room_send_message( - room_buffer, - formatted_data, - "m.text", - in_reply_to_event_id=event_id, - ) - room_buffer.last_message = None - - return W.WEECHAT_RC_OK - - if buffer == server.server_buffer: - message = ( - '{prefix}matrix: command "reply" must be ' - "executed on a Matrix channel buffer" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return W.WEECHAT_RC_OK - - return W.WEECHAT_RC_OK - - -def matrix_command_help(args): - if not args: - message = ( - "{prefix}matrix: Too few arguments for command " - '"/matrix help" (see /matrix help help)' - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return - - for command in args: - message = "" - - if command == "connect": - message = ( - "{delimiter_color}[{ncolor}matrix{delimiter_color}] " - "{ncolor}{cmd_color}/connect{ncolor} " - " [...]" - "\n\n" - "connect to Matrix server(s)" - "\n\n" - "server-name: server to connect to" - "(internal name)" - ).format( - delimiter_color=W.color("chat_delimiters"), - cmd_color=W.color("chat_buffer"), - ncolor=W.color("reset"), - ) - - elif command == "disconnect": - message = ( - "{delimiter_color}[{ncolor}matrix{delimiter_color}] " - "{ncolor}{cmd_color}/disconnect{ncolor} " - " [...]" - "\n\n" - "disconnect from Matrix server(s)" - "\n\n" - "server-name: server to disconnect" - "(internal name)" - ).format( - delimiter_color=W.color("chat_delimiters"), - cmd_color=W.color("chat_buffer"), - ncolor=W.color("reset"), - ) - - elif command == "reconnect": - message = ( - "{delimiter_color}[{ncolor}matrix{delimiter_color}] " - "{ncolor}{cmd_color}/reconnect{ncolor} " - " [...]" - "\n\n" - "reconnect to Matrix server(s)" - "\n\n" - "server-name: server to reconnect" - "(internal name)" - ).format( - delimiter_color=W.color("chat_delimiters"), - cmd_color=W.color("chat_buffer"), - ncolor=W.color("reset"), - ) - - elif command == "server": - message = ( - "{delimiter_color}[{ncolor}matrix{delimiter_color}] " - "{ncolor}{cmd_color}/server{ncolor} " - "add [:]" - "\n " - "delete|list|listfull " - "\n\n" - "list, add, or remove Matrix servers" - "\n\n" - " list: list servers (without argument, this " - "list is displayed)\n" - " listfull: list servers with detailed info for each " - "server\n" - " add: add a new server\n" - " delete: delete a server\n" - "server-name: server to reconnect (internal name)\n" - " hostname: name or IP address of server\n" - " port: port of server (default: 443)\n" - "\n" - "Examples:" - "\n /matrix server listfull" - "\n /matrix server add matrix matrix.org:80" - "\n /matrix server delete matrix" - ).format( - delimiter_color=W.color("chat_delimiters"), - cmd_color=W.color("chat_buffer"), - ncolor=W.color("reset"), - ) - - elif command == "help": - message = ( - "{delimiter_color}[{ncolor}matrix{delimiter_color}] " - "{ncolor}{cmd_color}/help{ncolor} " - " [...]" - "\n\n" - "display help about Matrix commands" - "\n\n" - "matrix-command: a Matrix command name" - "(internal name)" - ).format( - delimiter_color=W.color("chat_delimiters"), - cmd_color=W.color("chat_buffer"), - ncolor=W.color("reset"), - ) - - else: - message = ( - '{prefix}matrix: No help available, "{command}" ' - "is not a matrix command" - ).format(prefix=W.prefix("error"), command=command) - - W.prnt("", "") - W.prnt("", message) - - return - - -def matrix_server_command_listfull(args): - def get_value_string(value, default_value): - if value == default_value: - if not value: - value = "''" - value_string = " ({value})".format(value=value) - else: - value_string = "{color}{value}{ncolor}".format( - color=W.color("chat_value"), - value=value, - ncolor=W.color("reset"), - ) - - return value_string - - for server_name in args: - if server_name not in SERVERS: - continue - - server = SERVERS[server_name] - connected = "" - - W.prnt("", "") - - if server.connected: - connected = "connected" - else: - connected = "not connected" - - message = ( - "Server: {server_color}{server}{delimiter_color}" - " [{ncolor}{connected}{delimiter_color}]" - "{ncolor}" - ).format( - server_color=W.color("chat_server"), - server=server.name, - delimiter_color=W.color("chat_delimiters"), - connected=connected, - ncolor=W.color("reset"), - ) - - W.prnt("", message) - - option = server.config._option_ptrs["autoconnect"] - default_value = W.config_string_default(option) - value = W.config_string(option) - - value_string = get_value_string(value, default_value) - message = " autoconnect. : {value}".format(value=value_string) - - W.prnt("", message) - - option = server.config._option_ptrs["address"] - default_value = W.config_string_default(option) - value = W.config_string(option) - - value_string = get_value_string(value, default_value) - message = " address. . . : {value}".format(value=value_string) - - W.prnt("", message) - - option = server.config._option_ptrs["port"] - default_value = str(W.config_integer_default(option)) - value = str(W.config_integer(option)) - - value_string = get_value_string(value, default_value) - message = " port . . . . : {value}".format(value=value_string) - - W.prnt("", message) - - option = server.config._option_ptrs["username"] - default_value = W.config_string_default(option) - value = W.config_string(option) - - value_string = get_value_string(value, default_value) - message = " username . . : {value}".format(value=value_string) - - W.prnt("", message) - - option = server.config._option_ptrs["password"] - value = W.config_string(option) - - if value: - value = "(hidden)" - - value_string = get_value_string(value, "") - message = " password . . : {value}".format(value=value_string) - - W.prnt("", message) - - -def matrix_server_command_delete(args): - for server_name in args: - if check_server_existence(server_name, SERVERS): - server = SERVERS[server_name] - - if server.connected: - message = ( - "{prefix}matrix: you can not delete server " - "{color}{server}{ncolor} because you are " - 'connected to it. Try "/matrix disconnect ' - '{color}{server}{ncolor}" before.' - ).format( - prefix=W.prefix("error"), - color=W.color("chat_server"), - ncolor=W.color("reset"), - server=server.name, - ) - W.prnt("", message) - return - - for buf in list(server.buffers.values()): - W.buffer_close(buf) - - if server.server_buffer: - W.buffer_close(server.server_buffer) - - for option in server.config._option_ptrs.values(): - W.config_option_free(option) - - if server.timer_hook: - W.unhook(server.timer_hook) - server.timer_hook = None - - message = ( - "matrix: server {color}{server}{ncolor} has been " "deleted" - ).format( - server=server.name, - color=W.color("chat_server"), - ncolor=W.color("reset"), - ) - - del SERVERS[server.name] - server = None - - W.prnt("", message) - - -def matrix_server_command_add(args): - if len(args) < 2: - message = ( - "{prefix}matrix: Too few arguments for command " - '"/matrix server add" (see /matrix help server)' - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return - if len(args) > 4: - message = ( - "{prefix}matrix: Too many arguments for command " - '"/matrix server add" (see /matrix help server)' - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return - - def remove_server(server): - for option in server.config._option_ptrs.values(): - W.config_option_free(option) - del SERVERS[server.name] - - server_name = args[0] - - if server_name in SERVERS: - message = ( - "{prefix}matrix: server {color}{server}{ncolor} " - "already exists, can't add it" - ).format( - prefix=W.prefix("error"), - color=W.color("chat_server"), - server=server_name, - ncolor=W.color("reset"), - ) - W.prnt("", message) - return - - server = MatrixServer(server_name, G.CONFIG._ptr) - SERVERS[server.name] = server - - if len(args) >= 2: - if args[1].startswith("http"): - homeserver= urlparse(args[1]) - - host = homeserver.hostname - port = str(homeserver.port) if homeserver.port else None - else: - try: - host, port = args[1].split(":", 1) - except ValueError: - host, port = args[1], None - - return_code = W.config_option_set( - server.config._option_ptrs["address"], host, 1 - ) - - if return_code == W.WEECHAT_CONFIG_OPTION_SET_ERROR: - remove_server(server) - message = ( - "{prefix}Failed to set address for server " - "{color}{server}{ncolor}, failed to add " - "server." - ).format( - prefix=W.prefix("error"), - color=W.color("chat_server"), - server=server.name, - ncolor=W.color("reset"), - ) - - W.prnt("", message) - server = None - return - - if port: - return_code = W.config_option_set( - server.config._option_ptrs["port"], port, 1 - ) - if return_code == W.WEECHAT_CONFIG_OPTION_SET_ERROR: - remove_server(server) - message = ( - "{prefix}Failed to set port for server " - "{color}{server}{ncolor}, failed to add " - "server." - ).format( - prefix=W.prefix("error"), - color=W.color("chat_server"), - server=server.name, - ncolor=W.color("reset"), - ) - - W.prnt("", message) - server = None - return - - if len(args) >= 3: - user = args[2] - return_code = W.config_option_set( - server.config._option_ptrs["username"], user, 1 - ) - - if return_code == W.WEECHAT_CONFIG_OPTION_SET_ERROR: - remove_server(server) - message = ( - "{prefix}Failed to set user for server " - "{color}{server}{ncolor}, failed to add " - "server." - ).format( - prefix=W.prefix("error"), - color=W.color("chat_server"), - server=server.name, - ncolor=W.color("reset"), - ) - - W.prnt("", message) - server = None - return - - if len(args) == 4: - password = args[3] - - return_code = W.config_option_set( - server.config._option_ptrs["password"], password, 1 - ) - if return_code == W.WEECHAT_CONFIG_OPTION_SET_ERROR: - remove_server(server) - message = ( - "{prefix}Failed to set password for server " - "{color}{server}{ncolor}, failed to add " - "server." - ).format( - prefix=W.prefix("error"), - color=W.color("chat_server"), - server=server.name, - ncolor=W.color("reset"), - ) - W.prnt("", message) - server = None - return - - message = ( - "matrix: server {color}{server}{ncolor} " "has been added" - ).format( - server=server.name, - color=W.color("chat_server"), - ncolor=W.color("reset"), - ) - W.prnt("", message) - - -def matrix_server_command(command, args): - def list_servers(_): - if SERVERS: - W.prnt("", "\nAll matrix servers:") - for server in SERVERS: - W.prnt( - "", - " {color}{server}".format( - color=W.color("chat_server"), server=server - ), - ) - - # TODO the argument for list and listfull is used as a match word to - # find/filter servers, we're currently match exactly to the whole name - if command == "list": - list_servers(args) - elif command == "listfull": - matrix_server_command_listfull(args) - elif command == "add": - matrix_server_command_add(args) - elif command == "delete": - matrix_server_command_delete(args) - else: - message = ( - "{prefix}matrix: Error: unknown matrix server command, " - '"{command}" (type /matrix help server for help)' - ).format(prefix=W.prefix("error"), command=command) - W.prnt("", message) - - -@utf8_decode -def matrix_command_cb(data, buffer, args): - def connect_server(args): - for server_name in args: - if check_server_existence(server_name, SERVERS): - server = SERVERS[server_name] - server.connect() - - def disconnect_server(args): - for server_name in args: - if check_server_existence(server_name, SERVERS): - server = SERVERS[server_name] - if server.connected or server.reconnect_time: - # W.unhook(server.timer_hook) - # server.timer_hook = None - server.access_token = "" - server.disconnect(reconnect=False) - - split_args = list(filter(bool, args.split(" "))) - - if len(split_args) < 1: - message = ( - "{prefix}matrix: Too few arguments for command " - '"/matrix" ' - "(see /help matrix)" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - return W.WEECHAT_RC_ERROR - - command, args = split_args[0], split_args[1:] - - if command == "connect": - connect_server(args) - - elif command == "disconnect": - disconnect_server(args) - - elif command == "reconnect": - disconnect_server(args) - connect_server(args) - - elif command == "server": - if len(args) >= 1: - subcommand, args = args[0], args[1:] - matrix_server_command(subcommand, args) - else: - matrix_server_command("list", "") - - elif command == "help": - matrix_command_help(args) - - else: - message = ( - "{prefix}matrix: Error: unknown matrix command, " - '"{command}" (type /help matrix for help)' - ).format(prefix=W.prefix("error"), command=command) - W.prnt("", message) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_send_anyways_cb(data, buffer, args): - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - - if not server.connected: - room_buffer.error("Server is disconnected") - break - - if not server.client.logged_in: - room_buffer.error("You are not logged in.") - return W.WEECHAT_RC_ERROR - - if not room_buffer.last_message: - room_buffer.error("No previously sent message found.") - break - - server.room_send_message( - room_buffer, - room_buffer.last_message, - "m.text", - ignore_unverified_devices=True - ) - room_buffer.last_message = None - - break - else: - message = ( - "{prefix}matrix: The 'send-anyways' command needs to be " - "run on a matrix room buffer" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - - return W.WEECHAT_RC_ERROR - - -@utf8_decode -def matrix_cursor_reply_signal_cb(data, signal, ht): - tags = ht["_chat_line_tags"].split(",") - - W.command("", "/cursor stop") - - if "matrix_message" in tags: - for tag in tags: - if tag.startswith("matrix_id_"): - matrix_id = tag[10:] - break - else: - return W.WEECHAT_RC_OK - - buffer_name = ht["_buffer_full_name"] - bufptr = W.buffer_search("==", buffer_name) - - current_input = W.buffer_get_string(bufptr, "input") - input_pos = W.buffer_get_integer(bufptr, "input_pos") - - new_prefix = "/reply-matrix {} ".format(matrix_id) - - W.buffer_set(bufptr, "input", new_prefix + current_input) - W.buffer_set(bufptr, "input_pos", str(len(new_prefix) + input_pos)) - - return W.WEECHAT_RC_OK diff --git a/weechat/python/matrix/completion.py b/weechat/python/matrix/completion.py deleted file mode 100644 index a1ac559..0000000 --- a/weechat/python/matrix/completion.py +++ /dev/null @@ -1,369 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals - -from typing import List, Optional -from matrix.globals import SERVERS, W, SCRIPT_NAME -from matrix.utf import utf8_decode -from matrix.utils import tags_from_line_data -from nio import LocalProtocolError - - -def add_servers_to_completion(completion): - for server_name in SERVERS: - W.hook_completion_list_add( - completion, server_name, 0, W.WEECHAT_LIST_POS_SORT - ) - - -@utf8_decode -def matrix_server_command_completion_cb( - data, completion_item, buffer, completion -): - buffer_input = W.buffer_get_string(buffer, "input").split() - - args = buffer_input[1:] - commands = ["add", "delete", "list", "listfull"] - - def complete_commands(): - for command in commands: - W.hook_completion_list_add( - completion, command, 0, W.WEECHAT_LIST_POS_SORT - ) - - if len(args) == 1: - complete_commands() - - elif len(args) == 2: - if args[1] not in commands: - complete_commands() - else: - if args[1] == "delete" or args[1] == "listfull": - add_servers_to_completion(completion) - - elif len(args) == 3: - if args[1] == "delete" or args[1] == "listfull": - if args[2] not in SERVERS: - add_servers_to_completion(completion) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_server_completion_cb(data, completion_item, buffer, completion): - add_servers_to_completion(completion) - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_command_completion_cb(data, completion_item, buffer, completion): - for command in [ - "connect", - "disconnect", - "reconnect", - "server", - "help", - "debug", - ]: - W.hook_completion_list_add( - completion, command, 0, W.WEECHAT_LIST_POS_SORT - ) - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_debug_completion_cb(data, completion_item, buffer, completion): - for debug_type in ["messaging", "network", "timing"]: - W.hook_completion_list_add( - completion, debug_type, 0, W.WEECHAT_LIST_POS_SORT - ) - return W.WEECHAT_RC_OK - - -# TODO this should be configurable -REDACTION_COMP_LEN = 50 - - -@utf8_decode -def matrix_message_completion_cb(data, completion_item, buffer, completion): - max_events = 500 - - def redacted_or_not_message(tags): - # type: (List[str]) -> bool - if SCRIPT_NAME + "_redacted" in tags: - return True - if SCRIPT_NAME + "_message" not in tags: - return True - - return False - - def event_id_from_tags(tags): - # type: (List[str]) -> Optional[str] - for tag in tags: - if tag.startswith("matrix_id"): - event_id = tag[10:] - return event_id - - return None - - for server in SERVERS.values(): - if buffer in server.buffers.values(): - room_buffer = server.find_room_from_ptr(buffer) - lines = room_buffer.weechat_buffer.lines - - added = 0 - - for line in lines: - tags = line.tags - if redacted_or_not_message(tags): - continue - - event_id = event_id_from_tags(tags) - - if not event_id: - continue - - # Make sure we'll be able to reliably detect the end of the - # quoted snippet - message_fmt = line.message.replace("\\", "\\\\") \ - .replace('"', '\\"') - - if len(message_fmt) > REDACTION_COMP_LEN + 2: - message_fmt = message_fmt[:REDACTION_COMP_LEN] + ".." - - item = ('{event_id}|"{message}"').format( - event_id=event_id, message=message_fmt - ) - - W.hook_completion_list_add( - completion, item, 0, W.WEECHAT_LIST_POS_END - ) - added += 1 - - if added >= max_events: - break - - return W.WEECHAT_RC_OK - - return W.WEECHAT_RC_OK - - -def server_from_buffer(buffer): - for server in SERVERS.values(): - if buffer in server.buffers.values(): - return server - if buffer == server.server_buffer: - return server - return None - - -@utf8_decode -def matrix_olm_user_completion_cb(data, completion_item, buffer, completion): - server = server_from_buffer(buffer) - - if not server: - return W.WEECHAT_RC_OK - - try: - device_store = server.client.device_store - except LocalProtocolError: - return W.WEECHAT_RC_OK - - for user in device_store.users: - W.hook_completion_list_add( - completion, user, 0, W.WEECHAT_LIST_POS_SORT - ) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_olm_device_completion_cb(data, completion_item, buffer, completion): - server = server_from_buffer(buffer) - - if not server: - return W.WEECHAT_RC_OK - - try: - device_store = server.client.device_store - except LocalProtocolError: - return W.WEECHAT_RC_OK - - args = W.hook_completion_get_string(completion, "args") - - fields = args.split() - - if len(fields) < 2: - return W.WEECHAT_RC_OK - - user = fields[-1] - - if user not in device_store.users: - return W.WEECHAT_RC_OK - - for device in device_store.active_user_devices(user): - W.hook_completion_list_add( - completion, device.id, 0, W.WEECHAT_LIST_POS_SORT - ) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_own_devices_completion_cb( - data, - completion_item, - buffer, - completion -): - server = server_from_buffer(buffer) - - if not server: - return W.WEECHAT_RC_OK - - olm = server.client.olm - - if not olm: - return W.WEECHAT_RC_OK - - W.hook_completion_list_add( - completion, olm.device_id, 0, W.WEECHAT_LIST_POS_SORT - ) - - user = olm.user_id - - if user not in olm.device_store.users: - return W.WEECHAT_RC_OK - - for device in olm.device_store.active_user_devices(user): - W.hook_completion_list_add( - completion, device.id, 0, W.WEECHAT_LIST_POS_SORT - ) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_user_completion_cb(data, completion_item, buffer, completion): - def add_user(completion, user): - W.hook_completion_list_add( - completion, user, 0, W.WEECHAT_LIST_POS_SORT - ) - - for server in SERVERS.values(): - if buffer == server.server_buffer: - return W.WEECHAT_RC_OK - - room_buffer = server.find_room_from_ptr(buffer) - - if not room_buffer: - continue - - users = room_buffer.room.users - - users = [user[1:] for user in users] - - for user in users: - add_user(completion, user) - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_room_completion_cb(data, completion_item, buffer, completion): - """Completion callback for matrix room names.""" - for server in SERVERS.values(): - for room_buffer in server.room_buffers.values(): - name = room_buffer.weechat_buffer.short_name - - W.hook_completion_list_add( - completion, name, 0, W.WEECHAT_LIST_POS_SORT - ) - - return W.WEECHAT_RC_OK - - -def init_completion(): - W.hook_completion( - "matrix_server_commands", - "Matrix server completion", - "matrix_server_command_completion_cb", - "", - ) - - W.hook_completion( - "matrix_servers", - "Matrix server completion", - "matrix_server_completion_cb", - "", - ) - - W.hook_completion( - "matrix_commands", - "Matrix command completion", - "matrix_command_completion_cb", - "", - ) - - W.hook_completion( - "matrix_messages", - "Matrix message completion", - "matrix_message_completion_cb", - "", - ) - - W.hook_completion( - "matrix_debug_types", - "Matrix debugging type completion", - "matrix_debug_completion_cb", - "", - ) - - W.hook_completion( - "olm_user_ids", - "Matrix olm user id completion", - "matrix_olm_user_completion_cb", - "", - ) - - W.hook_completion( - "olm_devices", - "Matrix olm device id completion", - "matrix_olm_device_completion_cb", - "", - ) - - W.hook_completion( - "matrix_users", - "Matrix user id completion", - "matrix_user_completion_cb", - "", - ) - - W.hook_completion( - "matrix_own_devices", - "Matrix own devices completion", - "matrix_own_devices_completion_cb", - "", - ) - - W.hook_completion( - "matrix_rooms", - "Matrix room name completion", - "matrix_room_completion_cb", - "", - ) diff --git a/weechat/python/matrix/config.py b/weechat/python/matrix/config.py deleted file mode 100644 index f4ff40a..0000000 --- a/weechat/python/matrix/config.py +++ /dev/null @@ -1,916 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# Copyright © 2018, 2019 Denis Kasak -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -"""weechat-matrix Configuration module. - -This module contains abstractions on top of weechats configuration files and -the main script configuration class. - -To add configuration options refer to MatrixConfig. -Server specific configuration options are handled in server.py -""" - -from builtins import super -from collections import namedtuple -from enum import IntEnum, Enum, unique - -import logbook - -import nio -from matrix.globals import SCRIPT_NAME, SERVERS, W -from matrix.utf import utf8_decode - -from . import globals as G - - -@unique -class RedactType(Enum): - STRIKETHROUGH = 0 - NOTICE = 1 - DELETE = 2 - - -@unique -class ServerBufferType(Enum): - MERGE_CORE = 0 - MERGE = 1 - INDEPENDENT = 2 - - -@unique -class NewChannelPosition(IntEnum): - NONE = 0 - NEXT = 1 - NEAR_SERVER = 2 - - -nio.logger_group.level = logbook.ERROR - - -class Option( - namedtuple( - "Option", - [ - "name", - "type", - "string_values", - "min", - "max", - "value", - "description", - "cast_func", - "change_callback", - ], - ) -): - """A class representing a new configuration option. - - An option object is consumed by the ConfigSection class adding - configuration options to weechat. - """ - - __slots__ = () - - def __new__( - cls, - name, - type, - string_values, - min, - max, - value, - description, - cast=None, - change_callback=None, - ): - """ - Parameters: - name (str): Name of the configuration option - type (str): Type of the configuration option, can be one of the - supported weechat types: string, boolean, integer, color - string_values: (str): A list of string values that the option can - accept seprated by | - min (int): Minimal value of the option, only used if the type of - the option is integer - max (int): Maximal value of the option, only used if the type of - the option is integer - description (str): Description of the configuration option - cast (callable): A callable function taking a single value and - returning a modified value. Useful to turn the configuration - option into an enum while reading it. - change_callback(callable): A function that will be called - by weechat every time the configuration option is changed. - """ - - return super().__new__( - cls, - name, - type, - string_values, - min, - max, - value, - description, - cast, - change_callback, - ) - - -@utf8_decode -def matrix_config_reload_cb(data, config_file): - return W.WEECHAT_RC_OK - - -def change_log_level(category, level): - """Change the log level of the underlying nio lib - - Called every time the user changes the log level or log category - configuration option.""" - - if category == "all": - nio.logger_group.level = level - elif category == "http": - nio.http.logger.level = level - elif category == "client": - nio.client.logger.level = level - elif category == "events": - nio.events.logger.level = level - elif category == "responses": - nio.responses.logger.level = level - elif category == "encryption": - nio.crypto.logger.level = level - - -@utf8_decode -def config_server_buffer_cb(data, option): - """Callback for the look.server_buffer option. - Is called when the option is changed and merges/splits the server - buffer""" - - for server in SERVERS.values(): - server.buffer_merge() - return 1 - - -@utf8_decode -def config_log_level_cb(data, option): - """Callback for the network.debug_level option.""" - change_log_level( - G.CONFIG.network.debug_category, G.CONFIG.network.debug_level - ) - return 1 - - -@utf8_decode -def config_log_category_cb(data, option): - """Callback for the network.debug_category option.""" - change_log_level(G.CONFIG.debug_category, logbook.ERROR) - G.CONFIG.debug_category = G.CONFIG.network.debug_category - change_log_level( - G.CONFIG.network.debug_category, G.CONFIG.network.debug_level - ) - return 1 - - -@utf8_decode -def config_pgup_cb(data, option): - """Callback for the network.fetch_backlog_on_pgup option. - Enables or disables the hook that is run when /window page_up is called""" - if G.CONFIG.network.fetch_backlog_on_pgup: - if not G.CONFIG.page_up_hook: - G.CONFIG.page_up_hook = W.hook_command_run( - "/window page_up", "matrix_command_pgup_cb", "" - ) - else: - if G.CONFIG.page_up_hook: - W.unhook(G.CONFIG.page_up_hook) - G.CONFIG.page_up_hook = None - - return 1 - - -def level_to_logbook(value): - if value == 0: - return logbook.ERROR - if value == 1: - return logbook.WARNING - if value == 2: - return logbook.INFO - if value == 3: - return logbook.DEBUG - - return logbook.ERROR - - -def logbook_category(value): - if value == 0: - return "all" - if value == 1: - return "http" - if value == 2: - return "client" - if value == 3: - return "events" - if value == 4: - return "responses" - if value == 5: - return "encryption" - - return "all" - - -def parse_nick_prefix_colors(value): - """Parses the nick prefix color setting string - ("admin=COLOR1;mod=COLOR2;power=COLOR3") into a prefix -> color dict.""" - - def key_to_prefix(key): - if key == "admin": - return "&" - elif key == "mod": - return "@" - elif key == "power": - return "+" - else: - return "" - - prefix_colors = { - "&": "lightgreen", - "@": "lightgreen", - "+": "yellow", - } - - for setting in value.split(";"): - # skip malformed settings - if "=" not in setting: - continue - - key, color = setting.split("=") - prefix = key_to_prefix(key) - - if prefix: - prefix_colors[prefix] = color - - return prefix_colors - - -def eval_cast(string): - """A function that passes a string to weechat which evaluates it using its - expression evaluation syntax. - Can only be used with strings, useful for passwords or options that contain - a formatted string to e.g. add colors. - More info here: - https://weechat.org/files/doc/stable/weechat_plugin_api.en.html#_string_eval_expression""" - - return W.string_eval_expression(string, {}, {}, {}) - - -class WeechatConfig(object): - """A class representing a weechat configuration file - Wraps weechats configuration creation functionality""" - - def __init__(self, sections): - """Create a new weechat configuration file, expects the global - SCRIPT_NAME to be defined and a reload callback - - Parameters: - sections (List[Tuple[str, List[Option]]]): List of config sections - that will be created for the configuration file. - """ - self._ptr = W.config_new( - SCRIPT_NAME, SCRIPT_NAME + "_config_reload_cb", "" - ) - - for section in sections: - name, options = section - section_class = ConfigSection.build(name, options) - setattr(self, name, section_class(name, self._ptr, options)) - - def free(self): - """Free all the config sections and their options as well as the - configuration file. Should be called when the script is unloaded.""" - for section in [ - getattr(self, a) - for a in dir(self) - if isinstance(getattr(self, a), ConfigSection) - ]: - section.free() - - W.config_free(self._ptr) - - def read(self): - """Read the config file""" - return_code = W.config_read(self._ptr) - if return_code == W.WEECHAT_CONFIG_READ_OK: - return True - if return_code == W.WEECHAT_CONFIG_READ_MEMORY_ERROR: - return False - if return_code == W.WEECHAT_CONFIG_READ_FILE_NOT_FOUND: - return True - return False - - -class ConfigSection(object): - """A class representing a weechat config section. - Should not be used on its own, the WeechatConfig class uses this to build - config sections.""" - @classmethod - def build(cls, name, options): - def constructor(self, name, config_ptr, options): - self._ptr = W.config_new_section( - config_ptr, name, 0, 0, "", "", "", "", "", "", "", "", "", "" - ) - self._config_ptr = config_ptr - self._option_ptrs = {} - - for option in options: - self._add_option(option) - - attributes = { - option.name: cls.option_property( - option.name, option.type, cast_func=option.cast_func - ) - for option in options - } - attributes["__init__"] = constructor - - section_class = type(name.title() + "Section", (cls,), attributes) - return section_class - - def free(self): - W.config_section_free_options(self._ptr) - W.config_section_free(self._ptr) - - def _add_option(self, option): - cb = option.change_callback.__name__ if option.change_callback else "" - option_ptr = W.config_new_option( - self._config_ptr, - self._ptr, - option.name, - option.type, - option.description, - option.string_values, - option.min, - option.max, - option.value, - option.value, - 0, - "", - "", - cb, - "", - "", - "", - ) - - self._option_ptrs[option.name] = option_ptr - - @staticmethod - def option_property(name, option_type, evaluate=False, cast_func=None): - """Create a property for this class that makes the reading of config - option values pythonic. The option will be available as a property with - the name of the option. - If a cast function was defined for the option the property will pass - the option value to the cast function and return its result.""" - - def bool_getter(self): - return bool(W.config_boolean(self._option_ptrs[name])) - - def str_getter(self): - if cast_func: - return cast_func(W.config_string(self._option_ptrs[name])) - return W.config_string(self._option_ptrs[name]) - - def str_evaluate_getter(self): - return W.string_eval_expression( - W.config_string(self._option_ptrs[name]), {}, {}, {} - ) - - def int_getter(self): - if cast_func: - return cast_func(W.config_integer(self._option_ptrs[name])) - return W.config_integer(self._option_ptrs[name]) - - if option_type in ("string", "color"): - if evaluate: - return property(str_evaluate_getter) - return property(str_getter) - if option_type == "boolean": - return property(bool_getter) - if option_type == "integer": - return property(int_getter) - - -class MatrixConfig(WeechatConfig): - """Main matrix configuration file. - This class defines all the global matrix configuration options. - New global options should be added to the constructor of this class under - the appropriate section. - - There are three main sections defined: - Look: This section is for options that change the way matrix messages - are shown or the way the buffers are shown. - Color: This section should mainly be for color options, options that - change color schemes or themes should go to the look section. - Network: This section is for options that change the way the script - behaves, e.g. the way it communicates with the server, it handles - responses or any other behavioural change that doesn't fit in the - previous sections. - - There is a special section called server defined which contains per server - configuration options. Server options aren't defined here, they need to be - added in server.py - """ - - def __init__(self): - self.debug_buffer = "" - self.upload_buffer = "" - self.debug_category = "all" - self.page_up_hook = None - self.human_buffer_names = None - - look_options = [ - Option( - "redactions", - "integer", - "strikethrough|notice|delete", - 0, - 0, - "strikethrough", - ( - "Only notice redactions, strike through or delete " - "redacted messages" - ), - RedactType, - ), - Option( - "server_buffer", - "integer", - "merge_with_core|merge_without_core|independent", - 0, - 0, - "merge_with_core", - "Merge server buffers", - ServerBufferType, - config_server_buffer_cb, - ), - Option( - "new_channel_position", - "integer", - "none|next|near_server", - min(NewChannelPosition), - max(NewChannelPosition), - "none", - "force position of new channel in list of buffers " - "(none = default position (should be last buffer), " - "next = current buffer + 1, near_server = after last " - "channel/pv of server)", - NewChannelPosition, - ), - Option( - "max_typing_notice_item_length", - "integer", - "", - 10, - 1000, - "50", - ("Limit the length of the typing notice bar item."), - ), - Option( - "bar_item_typing_notice_prefix", - "string", - "", - 0, - 0, - "Typing: ", - ("Prefix for the typing notice bar item."), - ), - Option( - "encryption_warning_sign", - "string", - "", - 0, - 0, - "⚠️ ", - ("A sign that is used to signal trust issues in encrypted " - "rooms (note: content is evaluated, see /help eval)"), - eval_cast, - ), - Option( - "busy_sign", - "string", - "", - 0, - 0, - "⏳", - ("A sign that is used to signal that the client is busy e.g. " - "when the room backlog is fetching" - " (note: content is evaluated, see /help eval)"), - eval_cast, - ), - Option( - "encrypted_room_sign", - "string", - "", - 0, - 0, - "🔐", - ("A sign that is used to show that the current room is " - "encrypted " - "(note: content is evaluated, see /help eval)"), - eval_cast, - ), - Option( - "disconnect_sign", - "string", - "", - 0, - 0, - "❌", - ("A sign that is used to show that the server is disconnected " - "(note: content is evaluated, see /help eval)"), - eval_cast, - ), - Option( - "pygments_style", - "string", - "", - 0, - 0, - "native", - "Pygments style to use for highlighting source code blocks", - ), - Option( - "code_blocks", - "boolean", - "", - 0, - 0, - "on", - ("Display preformatted code blocks as rectangular areas by " - "padding them with whitespace up to the length of the longest" - " line (with optional margin)"), - ), - Option( - "code_block_margin", - "integer", - "", - 0, - 100, - "2", - ("Number of spaces to add as a margin around around a code " - "block"), - ), - Option( - "quote_wrap", - "integer", - "", - -1, - 1000, - "67", - ("After how many characters to soft-wrap lines in a quote " - "block (reply message). Set to -1 to disable soft-wrapping."), - ), - Option( - "human_buffer_names", - "boolean", - "", - 0, - 0, - "off", - ("If turned on the buffer name will consist of the server " - "name and the room name instead of the Matrix room ID. Note, " - "this requires a change to the logger.file.mask setting " - "since conflicts can happen otherwise " - "(requires a script reload)."), - ), - Option( - "markdown_input", - "boolean", - "", - 0, - 0, - "on", - ("If turned on, markdown usage in messages will be converted " - "to actual markup (**bold**, *italic*, _italic_, `code`)."), - ), - ] - - network_options = [ - Option( - "max_initial_sync_events", - "integer", - "", - 1, - 10000, - "30", - ("How many events to fetch during the initial sync"), - ), - Option( - "max_backlog_sync_events", - "integer", - "", - 1, - 100, - "10", - ("How many events to fetch during backlog fetching"), - ), - Option( - "fetch_backlog_on_pgup", - "boolean", - "", - 0, - 0, - "on", - ("Fetch messages in the backlog on a window page up event"), - None, - config_pgup_cb, - ), - Option( - "debug_level", - "integer", - "error|warn|info|debug", - 0, - 0, - "error", - "Enable network protocol debugging.", - level_to_logbook, - config_log_level_cb, - ), - Option( - "debug_category", - "integer", - "all|http|client|events|responses|encryption", - 0, - 0, - "all", - "Debugging category", - logbook_category, - config_log_category_cb, - ), - Option( - "debug_buffer", - "boolean", - "", - 0, - 0, - "off", - ("Use a separate buffer for debug logs."), - ), - Option( - "lazy_load_room_users", - "boolean", - "", - 0, - 0, - "off", - ("If on, room users won't be loaded in the background " - "proactively, they will be loaded when the user switches to " - "the room buffer. This only affects non-encrypted rooms."), - ), - Option( - "max_nicklist_users", - "integer", - "", - 100, - 20000, - "5000", - ("Limit the number of users that are added to the nicklist. " - "Active users and users with a higher power level are always." - " Inactive users will be removed from the nicklist after a " - "day of inactivity."), - ), - Option( - "lag_reconnect", - "integer", - "", - 5, - 604800, - "90", - ("Reconnect to the server if the lag is greater than this " - "value (in seconds)"), - ), - Option( - "autoreconnect_delay_growing", - "integer", - "", - 1, - 100, - "2", - ("growing factor for autoreconnect delay to server " - "(1 = always same delay, 2 = delay*2 for each retry, etc.)"), - ), - Option( - "autoreconnect_delay_max", - "integer", - "", - 0, - 604800, - "600", - ("maximum autoreconnect delay to server " - "(in seconds, 0 = no maximum)"), - ), - Option( - "print_unconfirmed_messages", - "boolean", - "", - 0, - 0, - "on", - ("If off, messages are only printed after the server confirms " - "their receival. If on, messages are immediately printed but " - "colored differently until receival is confirmed."), - ), - Option( - "lag_min_show", - "integer", - "", - 1, - 604800, - "500", - ("minimum lag to show (in milliseconds)"), - ), - Option( - "typing_notice_conditions", - "string", - "", - 0, - 0, - "${typing_enabled}", - ("conditions to send typing notifications (note: content is " - "evaluated, see /help eval); besides the buffer and window " - "variables the typing_enabled variable is also expanded; " - "the typing_enabled variable can be manipulated with the " - "/room command, see /help room"), - ), - Option( - "read_markers_conditions", - "string", - "", - 0, - 0, - "${markers_enabled}", - ("conditions to send read markers (note: content is " - "evaluated, see /help eval); besides the buffer and window " - "variables the markers_enabled variable is also expanded; " - "the markers_enabled variable can be manipulated with the " - "/room command, see /help room"), - ), - Option( - "resending_ignores_devices", - "boolean", - "", - 0, - 0, - "on", - ("If on resending the same message to a room that contains " - "unverified devices will mark the devices as ignored and " - "continue sending the message. If off resending the message " - "will again fail and devices need to be marked as verified " - "one by one or the /send-anyways command needs to be used to " - "ignore them."), - ), - ] - - color_options = [ - Option( - "quote_fg", - "color", - "", - 0, - 0, - "lightgreen", - "Foreground color for matrix style blockquotes", - ), - Option( - "quote_bg", - "color", - "", - 0, - 0, - "default", - "Background counterpart of quote_fg", - ), - Option( - "error_message_fg", - "color", - "", - 0, - 0, - "darkgray", - ("Foreground color for error messages that appear inside a " - "room buffer (e.g. when a message errors out when sending or " - "when a message is redacted)"), - ), - Option( - "error_message_bg", - "color", - "", - 0, - 0, - "default", - "Background counterpart of error_message_fg.", - ), - Option( - "unconfirmed_message_fg", - "color", - "", - 0, - 0, - "darkgray", - ("Foreground color for messages that are printed out but the " - "server hasn't confirmed the that he received them."), - ), - Option( - "unconfirmed_message_bg", - "color", - "", - 0, - 0, - "default", - "Background counterpart of unconfirmed_message_fg." - ), - Option( - "untagged_code_fg", - "color", - "", - 0, - 0, - "blue", - ("Foreground color for code without a language specifier. " - "Also used for `inline code`."), - ), - Option( - "untagged_code_bg", - "color", - "", - 0, - 0, - "default", - "Background counterpart of untagged_code_fg", - ), - Option( - "nick_prefixes", - "string", - "", - 0, - 0, - "admin=lightgreen;mod=lightgreen;power=yellow", - ('Colors for nick prefixes indicating power level. ' - 'Format is "admin:color1;mod:color2;power:color3", ' - 'where "admin" stands for admins (power level = 100), ' - '"mod" stands for moderators (power level >= 50) and ' - '"power" for any other power user (power level > 0). ' - 'Requires restart to apply changes.'), - parse_nick_prefix_colors, - ), - ] - - sections = [ - ("network", network_options), - ("look", look_options), - ("color", color_options), - ] - - super().__init__(sections) - - # The server section is essentially a section with subsections and no - # options, handle that case independently. - W.config_new_section( - self._ptr, - "server", - 0, - 0, - "matrix_config_server_read_cb", - "", - "matrix_config_server_write_cb", - "", - "", - "", - "", - "", - "", - "", - ) - - def read(self): - super().read() - self.human_buffer_names = self.look.human_buffer_names - - def free(self): - section_ptr = W.config_search_section(self._ptr, "server") - W.config_section_free(section_ptr) - super().free() diff --git a/weechat/python/matrix/globals.py b/weechat/python/matrix/globals.py deleted file mode 100644 index c3e099e..0000000 --- a/weechat/python/matrix/globals.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals - -import sys -from typing import Any, Dict, Optional -from logbook import Logger -from collections import OrderedDict - -from .utf import WeechatWrapper - -if False: - from .server import MatrixServer - from .config import MatrixConfig - from .uploads import Upload - - -try: - import weechat - - W = weechat if sys.hexversion >= 0x3000000 else WeechatWrapper(weechat) -except ImportError: - import matrix._weechat as weechat # type: ignore - - W = weechat - -SERVERS = dict() # type: Dict[str, MatrixServer] -CONFIG = None # type: Any -ENCRYPTION = True # type: bool -SCRIPT_NAME = "matrix" # type: str -BUFFER_NAME_PREFIX = "{}.".format(SCRIPT_NAME) # type: str -TYPING_NOTICE_TIMEOUT = 4000 # 4 seconds typing notice lifetime -LOGGER = Logger("weechat-matrix") -UPLOADS = OrderedDict() # type: Dict[str, Upload] diff --git a/weechat/python/matrix/message_renderer.py b/weechat/python/matrix/message_renderer.py deleted file mode 100644 index baa1fda..0000000 --- a/weechat/python/matrix/message_renderer.py +++ /dev/null @@ -1,120 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - -"""Module for rendering matrix messages in Weechat.""" - -from __future__ import unicode_literals -from nio import Api -from .globals import W -from .colors import Formatted - - -class Render(object): - """Class collecting methods for rendering matrix messages in Weechat.""" - - @staticmethod - def _media(url, description): - return ("{del_color}<{ncolor}{desc}{del_color}>{ncolor} " - "{del_color}[{ncolor}{url}{del_color}]{ncolor}").format( - del_color=W.color("chat_delimiters"), - ncolor=W.color("reset"), - desc=description, url=url) - - @staticmethod - def media(mxc, body, homeserver=None): - """Render a mxc media URI.""" - url = Api.mxc_to_http(mxc, homeserver) - description = "{}".format(body) if body else "file" - return Render._media(url, description) - - @staticmethod - def encrypted_media(mxc, body, key, hash, iv, homeserver=None): - """Render a mxc media URI of an encrypted file.""" - http_url = Api.encrypted_mxc_to_plumb( - mxc, - key, - hash, - iv, - homeserver - ) - url = http_url if http_url else mxc - description = "{}".format(body) if body else "file" - return Render._media(url, description) - - @staticmethod - def message(body, formatted_body): - """Render a room message.""" - if formatted_body: - formatted = Formatted.from_html(formatted_body) - return formatted.to_weechat() - - return body - - @staticmethod - def redacted(censor, reason=None): - """Render a redacted event message.""" - reason = ( - ', reason: "{reason}"'.format(reason=reason) - if reason - else "" - ) - - data = ( - "{del_color}<{log_color}Message redacted by: " - "{censor}{log_color}{reason}{del_color}>{ncolor}" - ).format( - del_color=W.color("chat_delimiters"), - ncolor=W.color("reset"), - log_color=W.color("logger.color.backlog_line"), - censor=censor, - reason=reason, - ) - - return data - - @staticmethod - def room_encryption(nick): - """Render a room encryption event.""" - return "{nick} has enabled encryption in this room".format(nick=nick) - - @staticmethod - def unknown(message_type, content=None): - """Render a message of an unknown type.""" - content = ( - ': "{content}"'.format(content=content) - if content - else "" - ) - return "Unknown message of type {t}{c}".format( - t=message_type, - c=content - ) - - @staticmethod - def megolm(): - """Render an undecrypted megolm event.""" - return ("{del_color}<{log_color}Unable to decrypt: " - "The sender's device has not sent us " - "the keys for this message{del_color}>{ncolor}").format( - del_color=W.color("chat_delimiters"), - log_color=W.color("logger.color.backlog_line"), - ncolor=W.color("reset")) - - @staticmethod - def bad(event): - """Render a malformed event of a known type""" - return "Bad event received, event type: {t}".format(t=event.type) diff --git a/weechat/python/matrix/server.py b/weechat/python/matrix/server.py deleted file mode 100644 index dda861e..0000000 --- a/weechat/python/matrix/server.py +++ /dev/null @@ -1,2011 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals - -import os -import pprint -import socket -import ssl -import time -import copy -from collections import defaultdict, deque -from atomicwrites import atomic_write -from typing import ( - Any, - Deque, - Dict, - Optional, - List, - NamedTuple, - DefaultDict, - Type, - Union, -) - -from uuid import UUID - -from nio import ( - Api, - HttpClient, - ClientConfig, - LocalProtocolError, - LoginResponse, - LoginInfoResponse, - Response, - Rooms, - RoomSendResponse, - RoomSendError, - SyncResponse, - ShareGroupSessionResponse, - ShareGroupSessionError, - KeysQueryResponse, - KeysClaimResponse, - DevicesResponse, - UpdateDeviceResponse, - DeleteDevicesAuthResponse, - DeleteDevicesResponse, - TransportType, - RoomMessagesResponse, - RoomMessagesError, - EncryptionError, - GroupEncryptionError, - OlmTrustError, - ErrorResponse, - SyncError, - LoginError, - JoinedMembersResponse, - JoinedMembersError, - RoomKeyEvent, - KeyVerificationStart, - KeyVerificationCancel, - KeyVerificationKey, - KeyVerificationMac, - KeyVerificationEvent, - ToDeviceMessage, - ToDeviceResponse, - ToDeviceError -) - -from . import globals as G -from .buffer import OwnAction, OwnMessage, RoomBuffer -from .config import ConfigSection, Option, ServerBufferType -from .globals import SCRIPT_NAME, SERVERS, W, TYPING_NOTICE_TIMEOUT -from .utf import utf8_decode -from .utils import create_server_buffer, key_from_value, server_buffer_prnt -from .uploads import Upload - -from .colors import Formatted, FormattedString, DEFAULT_ATTRIBUTES - -try: - from urllib.parse import urlparse -except ImportError: - from urlparse import urlparse # type: ignore - -try: - FileNotFoundError # type: ignore -except NameError: - FileNotFoundError = IOError - - -EncryptionQueueItem = NamedTuple( - "EncryptionQueueItem", - [ - ("message_type", str), - ("message", Union[Formatted, Upload]), - ], -) - - -class ServerConfig(ConfigSection): - def __init__(self, server_name, config_ptr): - # type: (str, str) -> None - self._server_name = server_name - self._config_ptr = config_ptr - self._option_ptrs = {} # type: Dict[str, str] - - options = [ - Option( - "autoconnect", - "boolean", - "", - 0, - 0, - "off", - ( - "automatically connect to the matrix server when weechat " - "is starting" - ), - ), - Option( - "address", - "string", - "", - 0, - 0, - "", - ( - "Hostname or address of the server (note: content is " - "evaluated, see /help eval)" - ) - ), - Option( - "port", "integer", "", 0, 65535, "443", "Port for the server" - ), - Option( - "proxy", - "string", - "", - 0, - 0, - "", - ("Name of weechat proxy to use (see /help proxy) (note: " - "content is evaluated, see /help eval)"), - ), - Option( - "ssl_verify", - "boolean", - "", - 0, - 0, - "on", - ("Check that the SSL connection is fully trusted"), - ), - Option( - "username", - "string", - "", - 0, - 0, - "", - ( - "Username to use on the server (note: content is " - "evaluated, see /help eval)" - ) - ), - Option( - "password", - "string", - "", - 0, - 0, - "", - ( - "Password for the server (note: content is evaluated, see " - "/help eval)" - ), - ), - Option( - "device_name", - "string", - "", - 0, - 0, - "Weechat Matrix", - ( - "Device name to use when logging in, this " - "is only used on the firt login. Afther that the /devices " - "command can be used to change the device name. (note: " - "content is evaluated, see /help eval)" - ) - ), - Option( - "autoreconnect_delay", - "integer", - "", - 0, - 86400, - "10", - ("Delay (in seconds) before trying to reconnect to server"), - ), - Option( - "sso_helper_listening_port", - "integer", - "", - 0, - 65535, - "0", - ("The port that the SSO helpers web server should listen on"), - ), - ] - - section = W.config_search_section(config_ptr, "server") - self._ptr = section - - for option in options: - option_name = "{server}.{option}".format( - server=self._server_name, option=option.name - ) - - self._option_ptrs[option.name] = W.config_new_option( - config_ptr, - section, - option_name, - option.type, - option.description, - option.string_values, - option.min, - option.max, - option.value, - option.value, - 0, - "", - "", - "matrix_config_server_change_cb", - self._server_name, - "", - "", - ) - - autoconnect = ConfigSection.option_property("autoconnect", "boolean") - address = ConfigSection.option_property("address", "string", evaluate=True) - port = ConfigSection.option_property("port", "integer") - proxy = ConfigSection.option_property("proxy", "string", evaluate=True) - ssl_verify = ConfigSection.option_property("ssl_verify", "boolean") - username = ConfigSection.option_property("username", "string", - evaluate=True) - device_name = ConfigSection.option_property("device_name", "string", - evaluate=True) - reconnect_delay = ConfigSection.option_property("autoreconnect_delay", "integer") - password = ConfigSection.option_property( - "password", "string", evaluate=True - ) - sso_helper_listening_port = ConfigSection.option_property( - "sso_helper_listening_port", - "integer" - ) - - def free(self): - W.config_section_free_options(self._ptr) - - -class MatrixServer(object): - # pylint: disable=too-many-instance-attributes - def __init__(self, name, config_ptr): - # type: (str, str) -> None - # yapf: disable - self.name = name # type: str - self.user_id = "" - self.device_id = "" # type: str - - self.room_buffers = dict() # type: Dict[str, RoomBuffer] - self.buffers = dict() # type: Dict[str, str] - self.server_buffer = None # type: Optional[str] - self.fd_hook = None # type: Optional[str] - self.ssl_hook = None # type: Optional[str] - self.timer_hook = None # type: Optional[str] - self.numeric_address = "" # type: Optional[str] - - self._connected = False # type: bool - self.connecting = False # type: bool - self.reconnect_delay = 0 # type: int - self.reconnect_time = None # type: Optional[float] - self.sync_time = None # type: Optional[float] - self.socket = None # type: Optional[ssl.SSLSocket] - self.ssl_context = ssl.create_default_context() # type: ssl.SSLContext - self.transport_type = None # type: Optional[TransportType] - - self.sso_hook = None - - # Enable http2 negotiation on the ssl context. - self.ssl_context.set_alpn_protocols(["h2", "http/1.1"]) - - try: - self.ssl_context.set_npn_protocols(["h2", "http/1.1"]) - except NotImplementedError: - pass - - self.address = None - self.homeserver = None - self.client = None # type: Optional[HttpClient] - self.access_token = None # type: Optional[str] - self.next_batch = None # type: Optional[str] - self.transaction_id = 0 # type: int - self.lag = 0 # type: int - self.lag_done = False # type: bool - self.busy = False # type: bool - self.first_sync = True - - self.send_fd_hook = None # type: Optional[str] - self.send_buffer = b"" # type: bytes - self.device_check_timestamp = None # type: Optional[int] - - self.device_deletion_queue = dict() # type: Dict[str, str] - - self.encryption_queue = defaultdict(deque) \ - # type: DefaultDict[str, Deque[EncryptionQueueItem]] - self.backlog_queue = dict() # type: Dict[str, str] - - self.user_gc_time = time.time() # type: float - self.member_request_list = [] # type: List[str] - self.rooms_with_missing_members = [] # type: List[str] - self.lazy_load_hook = None # type: Optional[str] - - # These flags remember if we made some requests so that we don't - # make them again while we wait on a response, the flags need to be - # cleared when we disconnect. - self.keys_queried = False # type: bool - self.keys_claimed = defaultdict(bool) # type: Dict[str, bool] - self.group_session_shared = defaultdict(bool) # type: Dict[str, bool] - self.ignore_while_sharing = defaultdict(bool) # type: Dict[str, bool] - self.to_device_sent = [] # type: List[ToDeviceMessage] - - # Try to load the device id, the device id is loaded every time the - # user changes but some login flows don't use a user so try to load the - # device for a main user. - self._load_device_id("main") - self.config = ServerConfig(self.name, config_ptr) - self._create_session_dir() - # yapf: enable - - def _create_session_dir(self): - path = os.path.join("matrix", self.name) - if not W.mkdir_home(path, 0o700): - message = ( - "{prefix}matrix: Error creating server session " "directory" - ).format(prefix=W.prefix("error")) - W.prnt("", message) - - @property - def connected(self): - return self._connected - - @connected.setter - def connected(self, value): - self._connected = value - W.bar_item_update("buffer_modes") - W.bar_item_update("matrix_modes") - - def get_session_path(self): - home_dir = W.info_get("weechat_dir", "") - return os.path.join(home_dir, "matrix", self.name) - - def _load_device_id(self, user=None): - user = user or self.config.username - - file_name = "{}{}".format(user, ".device_id") - path = os.path.join(self.get_session_path(), file_name) - - if not os.path.isfile(path): - return - - with open(path, "r") as device_file: - device_id = device_file.readline().rstrip() - if device_id: - self.device_id = device_id - - def save_device_id(self): - file_name = "{}{}".format(self.config.username or "main", ".device_id") - path = os.path.join(self.get_session_path(), file_name) - - with atomic_write(path, overwrite=True) as device_file: - device_file.write(self.device_id) - - @staticmethod - def _parse_url(address, port): - if not address.startswith("http"): - address = "https://{}".format(address) - - parsed_url = urlparse(address) - - homeserver = parsed_url._replace( - netloc=parsed_url.hostname + ":{}".format(port) - ) - - return homeserver - - def _change_client(self): - homeserver = MatrixServer._parse_url( - self.config.address, - self.config.port - ) - self.address = homeserver.hostname - self.homeserver = homeserver - - config = ClientConfig(store_sync_tokens=True) - - self.client = HttpClient( - homeserver.geturl(), - self.config.username, - self.device_id, - self.get_session_path(), - config=config - ) - self.client.add_to_device_callback( - self.key_verification_cb, - KeyVerificationEvent - ) - - def key_verification_cb(self, event): - if isinstance(event, KeyVerificationStart): - self.info_highlight("{user} via {device} has started a key " - "verification process.\n" - "To accept use /olm verification " - "accept {user} {device}".format( - user=event.sender, - device=event.from_device - )) - - elif isinstance(event, KeyVerificationKey): - sas = self.client.key_verifications.get(event.transaction_id, None) - if not sas: - return - - if sas.canceled: - return - - device = sas.other_olm_device - emoji = sas.get_emoji() - - emojis = [x[0] for x in emoji] - descriptions = [x[1] for x in emoji] - - centered_width = 12 - - def center_emoji(emoji, width): - # Assume each emoji has width 2 - emoji_width = 2 - - # These are emojis that need VARIATION-SELECTOR-16 (U+FE0F) so - # that they are rendered with coloured glyphs. For these, we - # need to add an extra space after them so that they are - # rendered properly in weechat. - variation_selector_emojis = [ - '☁️', - '❤️', - '☂️', - '✏️', - '✂️', - '☎️', - '✈️' - ] - - # Hack to make weechat behave properly when one of the above is - # printed. - if emoji in variation_selector_emojis: - emoji += " " - - # This is a trick to account for the fact that emojis are wider - # than other monospace characters. - placeholder = '.' * emoji_width - - return placeholder.center(width).replace(placeholder, emoji) - - emoji_str = u"".join(center_emoji(e, centered_width) - for e in emojis) - desc = u"".join(d.center(centered_width) for d in descriptions) - short_string = u"\n".join([emoji_str, desc]) - - self.info_highlight(u"Short authentication string for " - u"{user} via {device}:\n{string}\n" - u"Confirm that the strings match with " - u"/olm verification confirm {user} " - u"{device}".format( - user=device.user_id, - device=device.id, - string=short_string - )) - - elif isinstance(event, KeyVerificationMac): - try: - sas = self.client.key_verifications[event.transaction_id] - except KeyError: - return - - device = sas.other_olm_device - - if sas.verified: - self.info_highlight("Device {} of user {} successfully " - "verified".format( - device.id, - device.user_id - )) - - elif isinstance(event, KeyVerificationCancel): - self.info_highlight("The interactive device verification with " - "user {} got canceled: {}.".format( - event.sender, - event.reason - )) - - def update_option(self, option, option_name): - if option_name == "address": - self._change_client() - elif option_name == "port": - self._change_client() - elif option_name == "ssl_verify": - value = W.config_boolean(option) - if value: - self.ssl_context.verify_mode = ssl.CERT_REQUIRED - self.ssl_context.check_hostname = True - else: - self.ssl_context.check_hostname = False - self.ssl_context.verify_mode = ssl.CERT_NONE - elif option_name == "username": - value = W.config_string(option) - self.access_token = "" - - self._load_device_id() - - if self.client: - self.client.user = value - if self.device_id: - self.client.device_id = self.device_id - else: - pass - - def send_or_queue(self, request): - # type: (bytes) -> None - self.send(request) - - def try_send(self, message): - # type: (MatrixServer, bytes) -> bool - - sock = self.socket - - if not sock: - return False - - total_sent = 0 - message_length = len(message) - - while total_sent < message_length: - try: - sent = sock.send(message[total_sent:]) - - except ssl.SSLWantWriteError: - hook = W.hook_fd(sock.fileno(), 0, 1, 0, "send_cb", self.name) - self.send_fd_hook = hook - self.send_buffer = message[total_sent:] - return True - - except socket.error as error: - self._abort_send() - - errno = "error" + str(error.errno) + " " if error.errno else "" - strerr = error.strerror if error.strerror else "Unknown reason" - strerr = errno + strerr - - error_message = ( - "{prefix}Error while writing to " "socket: {error}" - ).format(prefix=W.prefix("network"), error=strerr) - - server_buffer_prnt(self, error_message) - server_buffer_prnt( - self, - ("{prefix}matrix: disconnecting from server...").format( - prefix=W.prefix("network") - ), - ) - - self.disconnect() - return False - - if sent == 0: - self._abort_send() - - server_buffer_prnt( - self, - "{prefix}matrix: Error while writing to socket".format( - prefix=W.prefix("network") - ), - ) - server_buffer_prnt( - self, - ("{prefix}matrix: disconnecting from server...").format( - prefix=W.prefix("network") - ), - ) - self.disconnect() - return False - - total_sent = total_sent + sent - - self._finalize_send() - return True - - def _abort_send(self): - self.send_buffer = b"" - - def _finalize_send(self): - # type: (MatrixServer) -> None - self.send_buffer = b"" - - def info_highlight(self, message): - buf = "" - if self.server_buffer: - buf = self.server_buffer - - msg = "{}{}: {}".format(W.prefix("network"), SCRIPT_NAME, message) - W.prnt_date_tags(buf, 0, "notify_highlight", msg) - - def info(self, message): - buf = "" - if self.server_buffer: - buf = self.server_buffer - - msg = "{}{}: {}".format(W.prefix("network"), SCRIPT_NAME, message) - W.prnt(buf, msg) - - def error(self, message): - buf = "" - if self.server_buffer: - buf = self.server_buffer - - msg = "{}{}: {}".format(W.prefix("error"), SCRIPT_NAME, message) - W.prnt(buf, msg) - - def send(self, data): - # type: (bytes) -> bool - self.try_send(data) - - return True - - def reconnect(self): - message = ("{prefix}matrix: reconnecting to server...").format( - prefix=W.prefix("network") - ) - - server_buffer_prnt(self, message) - - self.reconnect_time = None - - if not self.connect(): - self.schedule_reconnect() - - def schedule_reconnect(self): - # type: (MatrixServer) -> None - self.connecting = True - self.reconnect_time = time.time() - - if self.reconnect_delay: - self.reconnect_delay = ( - self.reconnect_delay - * G.CONFIG.network.autoreconnect_delay_growing - ) - else: - self.reconnect_delay = self.config.reconnect_delay - - if G.CONFIG.network.autoreconnect_delay_max > 0: - self.reconnect_delay = min(self.reconnect_delay, - G.CONFIG.network.autoreconnect_delay_max) - - message = ( - "{prefix}matrix: reconnecting to server in {t} " "seconds" - ).format(prefix=W.prefix("network"), t=self.reconnect_delay) - - server_buffer_prnt(self, message) - - def _close_socket(self): - # type: () -> None - if self.socket: - try: - self.socket.shutdown(socket.SHUT_RDWR) - except socket.error: - pass - - try: - self.socket.close() - except OSError: - pass - - def disconnect(self, reconnect=True): - # type: (bool) -> None - if self.fd_hook: - W.unhook(self.fd_hook) - - self._close_socket() - - self.fd_hook = None - self.socket = None - self.connected = False - self.access_token = "" - - self.send_buffer = b"" - self.transport_type = None - self.member_request_list = [] - - if self.client: - try: - self.client.disconnect() - except LocalProtocolError: - pass - - self.lag = 0 - W.bar_item_update("lag") - self.reconnect_time = None - - # Clear our request flags. - self.keys_queried = False - self.keys_claimed = defaultdict(bool) - self.group_session_shared = defaultdict(bool) - self.ignore_while_sharing = defaultdict(bool) - self.to_device_sent = [] - - if self.server_buffer: - message = ("{prefix}matrix: disconnected from server").format( - prefix=W.prefix("network") - ) - server_buffer_prnt(self, message) - - if reconnect: - self.schedule_reconnect() - else: - self.reconnect_delay = 0 - - def connect(self): - # type: (MatrixServer) -> int - if not self.config.address or not self.config.port: - message = "{prefix}Server address or port not set".format( - prefix=W.prefix("error") - ) - W.prnt("", message) - return False - - if self.connected: - return True - - if not self.server_buffer: - create_server_buffer(self) - - if not self.timer_hook: - self.timer_hook = W.hook_timer( - 1 * 1000, 0, 0, "matrix_timer_cb", self.name - ) - - ssl_message = " (SSL)" if self.ssl_context.check_hostname else "" - - message = ( - "{prefix}matrix: Connecting to " "{server}:{port}{ssl}..." - ).format( - prefix=W.prefix("network"), - server=self.address, - port=self.config.port, - ssl=ssl_message, - ) - - W.prnt(self.server_buffer, message) - - W.hook_connect( - self.config.proxy, - self.address, - self.config.port, - 1, - 0, - "", - "connect_cb", - self.name, - ) - - return True - - def schedule_sync(self): - self.sync_time = time.time() - - def sync(self, timeout=None, sync_filter=None): - # type: (Optional[int], Optional[Dict[Any, Any]]) -> None - if not self.client: - return - - self.sync_time = None - _, request = self.client.sync(timeout, sync_filter, - full_state=self.first_sync) - - self.send_or_queue(request) - - def login_info(self): - # type: () -> None - if not self.client: - return - - if self.client.logged_in: - self.login() - return - - _, request = self.client.login_info() - self.send(request) - - """Start a local HTTP server to listen for SSO tokens.""" - def start_login_sso(self): - # type: () -> None - if self.sso_hook: - # If there is a stale SSO process hanging around kill it. We could - # let it stay around but the URL that needs to be opened by the - # user is printed out in the callback. - W.hook_set(self.sso_hook, "signal", "term") - self.sso_hook = None - - process_args = { - "buffer_flush": "1", - "arg1": "--port", - "arg2": str(self.config.sso_helper_listening_port) - } - - self.sso_hook = W.hook_process_hashtable( - "matrix_sso_helper", - process_args, - 0, - "sso_login_cb", - self.name - ) - - def login(self, token=None): - # type: (...) -> None - assert self.client is not None - if self.client.logged_in: - msg = ( - "{prefix}{script_name}: Already logged in, " "syncing..." - ).format(prefix=W.prefix("network"), script_name=SCRIPT_NAME) - W.prnt(self.server_buffer, msg) - timeout = 0 if self.transport_type == TransportType.HTTP else 30000 - limit = (G.CONFIG.network.max_initial_sync_events if self.first_sync else 500) - sync_filter = { - "room": { - "timeline": {"limit": limit}, - "state": {"lazy_load_members": True} - } - } - self.sync(timeout, sync_filter) - return - - if (not self.config.username or not self.config.password) and not token: - message = "{prefix}User or password not set".format( - prefix=W.prefix("error") - ) - W.prnt("", message) - return self.disconnect() - - if token: - _, request = self.client.login( - device_name=self.config.device_name, token=token - ) - else: - _, request = self.client.login( - password=self.config.password, device_name=self.config.device_name - ) - self.send_or_queue(request) - - msg = "{prefix}matrix: Logging in...".format( - prefix=W.prefix("network") - ) - - W.prnt(self.server_buffer, msg) - - def devices(self): - _, request = self.client.devices() - self.send_or_queue(request) - - def delete_device(self, device_id, auth=None): - uuid, request = self.client.delete_devices([device_id], auth) - self.device_deletion_queue[uuid] = device_id - self.send_or_queue(request) - return - - def rename_device(self, device_id, display_name): - content = { - "display_name": display_name - } - - _, request = self.client.update_device(device_id, content) - self.send_or_queue(request) - - def room_send_state(self, room_buffer, body, event_type): - _, request = self.client.room_put_state( - room_buffer.room.room_id, event_type, body - ) - self.send_or_queue(request) - - def room_send_redaction(self, room_buffer, event_id, reason=None): - _, request = self.client.room_redact( - room_buffer.room.room_id, event_id, reason - ) - self.send_or_queue(request) - - def room_kick(self, room_buffer, user_id, reason=None): - _, request = self.client.room_kick( - room_buffer.room.room_id, user_id, reason - ) - self.send_or_queue(request) - - def room_invite(self, room_buffer, user_id): - _, request = self.client.room_invite(room_buffer.room.room_id, user_id) - self.send_or_queue(request) - - def room_join(self, room_id): - _, request = self.client.join(room_id) - self.send_or_queue(request) - - def room_leave(self, room_id): - _, request = self.client.room_leave(room_id) - self.send_or_queue(request) - - def room_get_messages(self, room_id): - if not self.connected or not self.client.logged_in: - return False - - room_buffer = self.find_room_from_id(room_id) - - # We're already fetching old messages - if room_buffer.backlog_pending: - return False - - if not room_buffer.prev_batch: - return False - - uuid, request = self.client.room_messages( - room_id, - room_buffer.prev_batch, - limit=10) - - room_buffer.backlog_pending = True - self.backlog_queue[uuid] = room_id - self.send_or_queue(request) - - return True - - def room_send_read_marker(self, room_id, event_id): - """Send read markers for the provided room. - - Args: - room_id(str): the room for which the read markers should - be sent. - event_id(str): the event id where to set the marker - """ - if not self.connected or not self.client.logged_in: - return - - _, request = self.client.room_read_markers( - room_id, - fully_read_event=event_id, - read_event=event_id) - self.send(request) - - def room_send_typing_notice(self, room_buffer): - """Send a typing notice for the provided room. - - Args: - room_buffer(RoomBuffer): the room for which the typing notice needs - to be sent. - """ - if not self.connected or not self.client.logged_in: - return - - input = room_buffer.weechat_buffer.input - - typing_enabled = bool(int(W.string_eval_expression( - G.CONFIG.network.typing_notice_conditions, - {}, - {"typing_enabled": str(int(room_buffer.typing_enabled))}, - {"type": "condition"} - ))) - - if not typing_enabled: - return - - # Don't send a typing notice if the user is typing in a weechat command - if input.startswith("/") and not input.startswith("//"): - return - - # Don't send a typing notice if we only typed a couple of letters. - elif len(input) < 4 and not room_buffer.typing: - return - - # If we were typing already and our input bar now has no letters or - # only a couple of letters stop the typing notice. - elif len(input) < 4: - _, request = self.client.room_typing( - room_buffer.room.room_id, - typing_state=False) - room_buffer.typing = False - self.send(request) - return - - # Don't send out a typing notice if we already sent one out and it - # didn't expire yet. - if not room_buffer.typing_notice_expired: - return - - _, request = self.client.room_typing( - room_buffer.room.room_id, - typing_state=True, - timeout=TYPING_NOTICE_TIMEOUT) - - room_buffer.typing = True - self.send(request) - - def room_send_upload( - self, - upload - ): - """Send a room message containing the mxc URI of an upload.""" - try: - room_buffer = self.find_room_from_id(upload.room_id) - except (ValueError, KeyError): - return True - - assert self.client - - if room_buffer.room.encrypted: - assert upload.encrypt - - content = upload.content - - try: - uuid = self.room_send_event(upload.room_id, content) - except (EncryptionError, GroupEncryptionError): - message = EncryptionQueueItem(upload.msgtype, upload) - self.encryption_queue[upload.room_id].append(message) - return False - - attributes = DEFAULT_ATTRIBUTES.copy() - formatted = Formatted([FormattedString( - upload.render, - attributes - )]) - - own_message = OwnMessage( - self.user_id, 0, "", uuid, upload.room_id, formatted - ) - - room_buffer.sent_messages_queue[uuid] = own_message - self.print_unconfirmed_message(room_buffer, own_message) - - return True - - def share_group_session( - self, - room_id, - ignore_missing_sessions=False, - ignore_unverified_devices=False - ): - - self.ignore_while_sharing[room_id] = ignore_unverified_devices - - _, request = self.client.share_group_session( - room_id, - ignore_missing_sessions=ignore_missing_sessions, - ignore_unverified_devices=ignore_unverified_devices - ) - self.send(request) - self.group_session_shared[room_id] = True - - def room_send_event( - self, - room_id, # type: str - content, # type: Dict[str, str] - event_type="m.room.message", # type: str - ignore_unverified_devices=False, # type: bool - ): - # type: (...) -> UUID - assert self.client - - try: - uuid, request = self.client.room_send( - room_id, event_type, content - ) - self.send(request) - return uuid - except GroupEncryptionError: - try: - if not self.group_session_shared[room_id]: - self.share_group_session( - room_id, - ignore_unverified_devices=ignore_unverified_devices - ) - raise - - except EncryptionError: - if not self.keys_claimed[room_id]: - _, request = self.client.keys_claim(room_id) - self.keys_claimed[room_id] = True - self.send(request) - raise - - def room_send_message( - self, - room_buffer, # type: RoomBuffer - formatted, # type: Formatted - msgtype="m.text", # type: str - ignore_unverified_devices=False, # type: bool - in_reply_to_event_id="", # type: str - ): - # type: (...) -> bool - room = room_buffer.room - - assert self.client - - content = {"msgtype": msgtype, "body": formatted.to_plain()} - - if formatted.is_formatted() or in_reply_to_event_id: - content["format"] = "org.matrix.custom.html" - content["formatted_body"] = formatted.to_html() - if in_reply_to_event_id: - content["m.relates_to"] = { - "m.in_reply_to": {"event_id": in_reply_to_event_id} - } - - try: - uuid = self.room_send_event( - room.room_id, - content, - ignore_unverified_devices=ignore_unverified_devices - ) - except (EncryptionError, GroupEncryptionError): - message = EncryptionQueueItem(msgtype, formatted) - self.encryption_queue[room.room_id].append(message) - return False - - if msgtype == "m.emote": - message_class = OwnAction # type: Type - else: - message_class = OwnMessage - - own_message = message_class( - self.user_id, 0, "", uuid, room.room_id, formatted - ) - - room_buffer.sent_messages_queue[uuid] = own_message - self.print_unconfirmed_message(room_buffer, own_message) - - return True - - def print_unconfirmed_message(self, room_buffer, message): - """Print an outgoing message before getting a receive confirmation. - - The message is printed out greyed out and only printed out if the - client is configured to do so. The message needs to be later modified - to contain proper coloring, this is done in the - replace_printed_line_by_uuid() method of the RoomBuffer class. - - Args: - room_buffer(RoomBuffer): the buffer of the room where the message - needs to be printed out - message(OwnMessages): the message that should be printed out - """ - if G.CONFIG.network.print_unconfirmed_messages: - room_buffer.printed_before_ack_queue.append(message.uuid) - plain_message = message.formatted_message.to_weechat() - plain_message = W.string_remove_color(plain_message, "") - attributes = DEFAULT_ATTRIBUTES.copy() - attributes["fgcolor"] = G.CONFIG.color.unconfirmed_message_fg - attributes["bgcolor"] = G.CONFIG.color.unconfirmed_message_bg - new_formatted = Formatted([FormattedString( - plain_message, - attributes - )]) - - new_message = copy.copy(message) - new_message.formatted_message = new_formatted - - if isinstance(new_message, OwnAction): - room_buffer.self_action(new_message) - elif isinstance(new_message, OwnMessage): - room_buffer.self_message(new_message) - - def keys_upload(self): - _, request = self.client.keys_upload() - self.send_or_queue(request) - - def keys_query(self): - _, request = self.client.keys_query() - self.keys_queried = True - self.send_or_queue(request) - - def get_joined_members(self, room_id): - if not self.connected or not self.client.logged_in: - return - - if room_id in self.member_request_list: - return - - self.member_request_list.append(room_id) - _, request = self.client.joined_members(room_id) - self.send(request) - - def _print_message_error(self, message): - server_buffer_prnt( - self, - ( - "{prefix}Unhandled {status_code} error, please " - "inform the developers about this." - ).format( - prefix=W.prefix("error"), status_code=message.response.status - ), - ) - - server_buffer_prnt(self, pprint.pformat(message.__class__.__name__)) - server_buffer_prnt(self, pprint.pformat(message.request.payload)) - server_buffer_prnt(self, pprint.pformat(message.response.body)) - - def handle_own_messages_error(self, response): - room_buffer = self.room_buffers[response.room_id] - - if response.uuid not in room_buffer.printed_before_ack_queue: - return - - message = room_buffer.sent_messages_queue.pop(response.uuid) - room_buffer.mark_message_as_unsent(response.uuid, message) - room_buffer.printed_before_ack_queue.remove(response.uuid) - - def handle_own_messages(self, response): - def send_marker(): - if not room_buffer.read_markers_enabled: - return - - self.room_send_read_marker(response.room_id, response.event_id) - room_buffer.last_read_event = response.event_id - - room_buffer = self.room_buffers[response.room_id] - - message = room_buffer.sent_messages_queue.pop(response.uuid, None) - - # The message might have been returned in a sync response before we got - # a room send response. - if not message: - return - - message.event_id = response.event_id - # We already printed the message, just modify it to contain the proper - # colors and formatting. - if response.uuid in room_buffer.printed_before_ack_queue: - room_buffer.replace_printed_line_by_uuid(response.uuid, message) - room_buffer.printed_before_ack_queue.remove(response.uuid) - send_marker() - return - - if isinstance(message, OwnAction): - room_buffer.self_action(message) - send_marker() - return - if isinstance(message, OwnMessage): - room_buffer.self_message(message) - send_marker() - return - - raise NotImplementedError( - "Unsupported message of type {}".format(type(message)) - ) - - def handle_backlog_response(self, response): - room_id = self.backlog_queue.pop(response.uuid) - room_buffer = self.find_room_from_id(room_id) - room_buffer.first_view = False - - room_buffer.handle_backlog(response) - - def handle_devices_response(self, response): - if not response.devices: - m = "{}{}: No devices found for this account".format( - W.prefix("error"), - SCRIPT_NAME) - W.prnt(self.server_buffer, m) - - header = (W.prefix("network") + SCRIPT_NAME + ": Devices for " - "server {}{}{}:\n" - " Device ID Device Name " - "Last Seen").format( - W.color("chat_server"), - self.name, - W.color("reset") - ) - W.prnt(self.server_buffer, header) - - lines = [] - for device in response.devices: - last_seen_date = ("?" if not device.last_seen_date else - device.last_seen_date.strftime("%Y/%m/%d %H:%M")) - last_seen = "{ip} @ {date}".format( - ip=device.last_seen_ip or "?", - date=last_seen_date - ) - device_color = ("chat_self" if device.id == self.device_id else - W.info_get("nick_color_name", device.id)) - bold = W.color("bold") if device.id == self.device_id else "" - line = " {}{}{:<18}{}{:<34}{:<}".format( - bold, - W.color(device_color), - device.id, - W.color("resetcolor"), - device.display_name or "", - last_seen - ) - lines.append(line) - W.prnt(self.server_buffer, "\n".join(lines)) - - """Handle a login info response and chose one of the available flows - - This currently supports only SSO and password logins. If both are available - password takes precedence over SSO if a username and password is provided. - - """ - def _handle_login_info(self, response): - if ("m.login.sso" in response.flows - and (not self.config.username or not self.config.password)): - self.start_login_sso() - elif "m.login.password" in response.flows: - self.login() - else: - self.error("No supported login flow found") - self.disconnect() - - def _handle_login(self, response): - self.access_token = response.access_token - self.user_id = response.user_id - self.client.access_token = response.access_token - self.device_id = response.device_id - self.save_device_id() - - message = "{prefix}matrix: Logged in as {user}".format( - prefix=W.prefix("network"), user=self.user_id - ) - - W.prnt(self.server_buffer, message) - - if not self.client.olm_account_shared: - self.keys_upload() - - sync_filter = { - "room": { - "timeline": { - "limit": G.CONFIG.network.max_initial_sync_events - }, - "state": {"lazy_load_members": True} - } - } - self.sync(timeout=0, sync_filter=sync_filter) - - def _handle_room_info(self, response): - for room_id, info in response.rooms.invite.items(): - room = self.client.invited_rooms.get(room_id, None) - - if room: - if room.inviter: - inviter_msg = " by {}{}".format( - W.color("chat_nick_other"), room.inviter - ) - else: - inviter_msg = "" - - self.info_highlight( - "You have been invited to {} {}({}{}{}){}" - "{}".format( - room.display_name, - W.color("chat_delimiters"), - W.color("chat_channel"), - room_id, - W.color("chat_delimiters"), - W.color("reset"), - inviter_msg, - ) - ) - else: - self.info_highlight("You have been invited to {}.".format( - room_id - )) - - for room_id, info in response.rooms.leave.items(): - if room_id not in self.buffers: - continue - - room_buffer = self.find_room_from_id(room_id) - room_buffer.handle_left_room(info) - - for room_id, info in response.rooms.join.items(): - if room_id not in self.buffers: - self.create_room_buffer(room_id, info.timeline.prev_batch) - - room_buffer = self.find_room_from_id(room_id) - room_buffer.handle_joined_room(info) - - def add_unhandled_users(self, rooms, n): - # type: (List[RoomBuffer], int) -> bool - total_users = 0 - - while total_users <= n: - try: - room_buffer = rooms.pop() - except IndexError: - return False - - handled_users = 0 - - users = room_buffer.unhandled_users - - for user_id in users: - room_buffer.add_user(user_id, 0, True) - handled_users += 1 - total_users += 1 - - if total_users >= n: - room_buffer.unhandled_users = users[handled_users:] - rooms.append(room_buffer) - return True - - room_buffer.unhandled_users = [] - - return False - - def _hook_lazy_user_adding(self): - if not self.lazy_load_hook: - hook = W.hook_timer(1 * 1000, 0, 0, - "matrix_load_users_cb", self.name) - self.lazy_load_hook = hook - - def decrypt_printed_messages(self, key_event): - """Decrypt already printed messages and send them to the buffer""" - try: - room_buffer = self.find_room_from_id(key_event.room_id) - except KeyError: - return - - decrypted_events = [] - - for undecrypted_event in room_buffer.undecrypted_events: - if undecrypted_event.session_id != key_event.session_id: - continue - - event = self.client.decrypt_event(undecrypted_event) - if event: - decrypted_events.append((undecrypted_event, event)) - - for event_pair in decrypted_events: - undecrypted_event, event = event_pair - room_buffer.undecrypted_events.remove(undecrypted_event) - room_buffer.replace_undecrypted_line(event) - - def start_verification(self, device): - _, request = self.client.start_key_verification(device) - self.send(request) - self.info("Starting an interactive device verification with " - "{} {}".format(device.user_id, device.id)) - - def accept_sas(self, sas): - _, request = self.client.accept_key_verification(sas.transaction_id) - self.send(request) - - def cancel_sas(self, sas): - _, request = self.client.cancel_key_verification(sas.transaction_id) - self.send(request) - - def to_device(self, message): - _, request = self.client.to_device(message) - self.send(request) - - def confirm_sas(self, sas): - _, request = self.client.confirm_short_auth_string(sas.transaction_id) - self.send(request) - - device = sas.other_olm_device - - if sas.verified: - self.info("Device {} of user {} successfully verified".format( - device.id, - device.user_id - )) - else: - self.info("Waiting for {} to confirm...".format(device.user_id)) - - def _handle_sync(self, response): - # we got the same batch again, nothing to do - self.first_sync = False - - if self.next_batch == response.next_batch: - self.schedule_sync() - return - - self._handle_room_info(response) - - for event in response.to_device_events: - if isinstance(event, RoomKeyEvent): - message = { - "sender": event.sender, - "sender_key": event.sender_key, - "room_id": event.room_id, - "session_id": event.session_id, - "algorithm": event.algorithm, - "server": self.name, - } - W.hook_hsignal_send("matrix_room_key_received", message) - - # TODO try to decrypt some cached undecrypted messages with the - # new key - # self.decrypt_printed_messages(event) - - if self.client.should_upload_keys: - self.keys_upload() - - if self.client.should_query_keys and not self.keys_queried: - self.keys_query() - - for room_buffer in self.room_buffers.values(): - # It's our initial sync, we need to fetch room members, so add - # the room to the missing members queue. - # 3 reasons we fetch room members here: - # * If the lazy load room users setting is off, otherwise we will - # fetch them when we switch to the buffer - # * If the room is encrypted, encryption needs the full member - # list for it to work. - # * If we are the only member, it is unlikely really an empty - # room and since we don't want a bunch of "Empty room?" - # buffers in our buffer list we fetch members here. - if not self.next_batch: - if (not G.CONFIG.network.lazy_load_room_users - or room_buffer.room.encrypted - or room_buffer.room.member_count <= 1): - self.rooms_with_missing_members.append( - room_buffer.room.room_id - ) - if room_buffer.unhandled_users: - self._hook_lazy_user_adding() - break - - self.next_batch = response.next_batch - self.schedule_sync() - W.bar_item_update("matrix_typing_notice") - - if self.rooms_with_missing_members: - self.get_joined_members(self.rooms_with_missing_members.pop()) - - def handle_delete_device_auth(self, response): - device_id = self.device_deletion_queue.pop(response.uuid, None) - - if not device_id: - return - - for flow in response.flows: - if "m.login.password" in flow["stages"]: - session = response.session - auth = { - "type": "m.login.password", - "session": session, - "user": self.client.user_id, - "password": self.config.password - } - self.delete_device(device_id, auth) - return - - self.error("No supported auth method for device deletion found.") - - def handle_error_response(self, response): - self.error("Error: {}".format(str(response))) - - if isinstance(response, (SyncError, LoginError)): - self.disconnect() - elif isinstance(response, JoinedMembersError): - self.rooms_with_missing_members.append(response.room_id) - self.get_joined_members(self.rooms_with_missing_members.pop()) - elif isinstance(response, RoomSendError): - self.handle_own_messages_error(response) - elif isinstance(response, ShareGroupSessionError): - self.group_session_shared[response.room_id] = False - self.share_group_session( - response.room_id, - False, - self.ignore_while_sharing[response.room_id] - ) - - elif isinstance(response, ToDeviceError): - try: - self.to_device_sent.remove(response.to_device_message) - except ValueError: - pass - - def handle_response(self, response): - # type: (Response) -> None - response_lag = response.elapsed - - current_lag = 0 - - if self.client: - current_lag = self.client.lag - - if response_lag >= current_lag: - self.lag = response_lag * 1000 - self.lag_done = True - W.bar_item_update("lag") - - if isinstance(response, ErrorResponse): - self.handle_error_response(response) - if isinstance(response, RoomMessagesError): - room_buffer = self.room_buffers[response.room_id] - room_buffer.backlog_pending = False - - elif isinstance(response, ToDeviceResponse): - try: - self.to_device_sent.remove(response.to_device_message) - except ValueError: - pass - - elif isinstance(response, LoginResponse): - self._handle_login(response) - - elif isinstance(response, LoginInfoResponse): - self._handle_login_info(response) - - elif isinstance(response, SyncResponse): - self._handle_sync(response) - - elif isinstance(response, RoomSendResponse): - self.handle_own_messages(response) - - elif isinstance(response, RoomMessagesResponse): - self.handle_backlog_response(response) - - elif isinstance(response, DevicesResponse): - self.handle_devices_response(response) - - elif isinstance(response, UpdateDeviceResponse): - self.info("Device name successfully updated") - - elif isinstance(response, DeleteDevicesAuthResponse): - self.handle_delete_device_auth(response) - - elif isinstance(response, DeleteDevicesResponse): - self.info("Device successfully deleted") - - elif isinstance(response, KeysQueryResponse): - self.keys_queried = False - W.bar_item_update("buffer_modes") - W.bar_item_update("matrix_modes") - - for user_id, device_dict in response.changed.items(): - for device in device_dict.values(): - message = { - "user_id": user_id, - "device_id": device.id, - "ed25519": device.ed25519, - "curve25519": device.curve25519, - "deleted": str(device.deleted) - } - W.hook_hsignal_send("matrix_device_changed", message) - - elif isinstance(response, JoinedMembersResponse): - self.member_request_list.remove(response.room_id) - room_buffer = self.room_buffers[response.room_id] - users = [user.user_id for user in response.members] - - # Don't add the users directly use the lazy load hook. - room_buffer.unhandled_users += users - self._hook_lazy_user_adding() - room_buffer.members_fetched = True - room_buffer.update_buffer_name() - - # Fetch the users for the next room. - if self.rooms_with_missing_members: - self.get_joined_members(self.rooms_with_missing_members.pop()) - # We are done adding all the users, do a full key query now since - # the client knows all the encrypted room members. - else: - if self.client.should_query_keys and not self.keys_queried: - self.keys_query() - - elif isinstance(response, KeysClaimResponse): - self.keys_claimed[response.room_id] = False - try: - self.share_group_session( - response.room_id, - True, - self.ignore_while_sharing[response.room_id] - ) - except OlmTrustError as e: - m = ("Untrusted devices found in room: {}".format(e)) - room_buffer = self.find_room_from_id(response.room_id) - room_buffer.error(m) - - try: - item = self.encryption_queue[response.room_id][0] - if item.message_type not in ["m.file", "m.video", - "m.audio", "m.image"]: - room_buffer.last_message = item.message - except IndexError: - pass - - self.encryption_queue[response.room_id].clear() - return - - elif isinstance(response, ShareGroupSessionResponse): - room_id = response.room_id - self.group_session_shared[response.room_id] = False - ignore_unverified = self.ignore_while_sharing[response.room_id] - self.ignore_while_sharing[response.room_id] = False - - room_buffer = self.room_buffers[room_id] - - while self.encryption_queue[room_id]: - item = self.encryption_queue[room_id].popleft() - try: - if item.message_type in [ - "m.file", - "m.video", - "m.audio", - "m.image" - ]: - ret = self.room_send_upload(item.message) - else: - assert isinstance(item.message, Formatted) - ret = self.room_send_message( - room_buffer, - item.message, - item.message_type, - ignore_unverified_devices=ignore_unverified - ) - - if not ret: - self.encryption_queue[room_id].pop() - self.encryption_queue[room_id].appendleft(item) - break - - except OlmTrustError: - self.encryption_queue[room_id].clear() - - # If the item is a normal user message store it in the - # buffer to enable the send-anyways functionality. - if item.message_type not in ["m.file", "m.video", - "m.audio", "m.image"]: - room_buffer.last_message = item.message - - break - - def create_room_buffer(self, room_id, prev_batch): - room = self.client.rooms[room_id] - buf = RoomBuffer(room, self.name, self.homeserver, prev_batch) - - # We sadly don't get a correct summary on full_state from synapse so we - # can't trust it that the members are fully synced - # if room.members_synced: - # buf.members_fetched = True - - self.room_buffers[room_id] = buf - self.buffers[room_id] = buf.weechat_buffer._ptr - - def find_room_from_ptr(self, pointer): - try: - room_id = key_from_value(self.buffers, pointer) - room_buffer = self.room_buffers[room_id] - - return room_buffer - except (ValueError, KeyError): - return None - - def find_room_from_id(self, room_id): - room_buffer = self.room_buffers[room_id] - return room_buffer - - def garbage_collect_users(self): - """ Remove inactive users. - This tries to keep the number of users added to the nicklist less than - the configuration option matrix.network.max_nicklist_users. It - removes users that have not been active for a day until there are - less than max_nicklist_users or no users are left for removal. - It never removes users that have a bigger power level than the - default one. - This function is run every hour by the server timer callback""" - - now = time.time() - self.user_gc_time = now - - def day_passed(t1, t2): - return (t2 - t1) > 86400 - - for room_buffer in self.room_buffers.values(): - to_remove = max( - (len(room_buffer.displayed_nicks) - - G.CONFIG.network.max_nicklist_users), - 0 - ) - - if not to_remove: - continue - - removed = 0 - removed_user_ids = [] - - for user_id, nick in room_buffer.displayed_nicks.items(): - user = room_buffer.weechat_buffer.users[nick] - - if (not user.speaking_time or - day_passed(user.speaking_time, now)): - room_buffer.weechat_buffer.part(nick, 0, False) - removed_user_ids.append(user_id) - removed += 1 - - if removed >= to_remove: - break - - for user_id in removed_user_ids: - del room_buffer.displayed_nicks[user_id] - - def buffer_merge(self): - if not self.server_buffer: - return - - buf = self.server_buffer - - if G.CONFIG.look.server_buffer == ServerBufferType.MERGE_CORE: - num = W.buffer_get_integer(W.buffer_search_main(), "number") - W.buffer_unmerge(buf, num + 1) - W.buffer_merge(buf, W.buffer_search_main()) - elif G.CONFIG.look.server_buffer == ServerBufferType.MERGE: - if SERVERS: - first = None - for server in SERVERS.values(): - if server.server_buffer: - first = server.server_buffer - break - if first: - num = W.buffer_get_integer( - W.buffer_search_main(), "number" - ) - W.buffer_unmerge(buf, num + 1) - if buf is not first: - W.buffer_merge(buf, first) - else: - num = W.buffer_get_integer(W.buffer_search_main(), "number") - W.buffer_unmerge(buf, num + 1) - - -@utf8_decode -def matrix_config_server_read_cb( - data, config_file, section, option_name, value -): - - return_code = W.WEECHAT_CONFIG_OPTION_SET_ERROR - - if option_name: - server_name, option = option_name.rsplit(".", 1) - server = None - - if server_name in SERVERS: - server = SERVERS[server_name] - else: - server = MatrixServer(server_name, config_file) - SERVERS[server.name] = server - - # Ignore invalid options - if option in server.config._option_ptrs: - return_code = W.config_option_set( - server.config._option_ptrs[option], value, 1 - ) - - # TODO print out error message in case of erroneous return_code - - return return_code - - -@utf8_decode -def matrix_config_server_write_cb(data, config_file, section_name): - if not W.config_write_line(config_file, section_name, ""): - return W.WEECHAT_CONFIG_WRITE_ERROR - - for server in SERVERS.values(): - for option in server.config._option_ptrs.values(): - if not W.config_write_option(config_file, option): - return W.WEECHAT_CONFIG_WRITE_ERROR - - return W.WEECHAT_CONFIG_WRITE_OK - - -@utf8_decode -def matrix_config_server_change_cb(server_name, option): - # type: (str, str) -> int - server = SERVERS[server_name] - option_name = None - - # The function config_option_get_string() is used to get differing - # properties from a config option, sadly it's only available in the plugin - # API of weechat. - option_name = key_from_value(server.config._option_ptrs, option) - server.update_option(option, option_name) - - return 1 - - -@utf8_decode -def matrix_load_users_cb(server_name, remaining_calls): - server = SERVERS[server_name] - start = time.time() - - rooms = [x for x in server.room_buffers.values() if x.unhandled_users] - - while server.add_unhandled_users(rooms, 100): - current = time.time() - - if current - start >= 0.1: - return W.WEECHAT_RC_OK - - # We are done adding users, we can unhook now. - W.unhook(server.lazy_load_hook) - server.lazy_load_hook = None - - return W.WEECHAT_RC_OK - - -@utf8_decode -def matrix_timer_cb(server_name, remaining_calls): - server = SERVERS[server_name] - - current_time = time.time() - - if ( - (not server.connected) - and server.reconnect_time - and current_time >= (server.reconnect_time + server.reconnect_delay) - ): - server.reconnect() - return W.WEECHAT_RC_OK - - if not server.connected or not server.client.logged_in: - return W.WEECHAT_RC_OK - - # check lag, disconnect if it's too big - server.lag = server.client.lag * 1000 - server.lag_done = False - W.bar_item_update("lag") - - if server.lag > G.CONFIG.network.lag_reconnect * 1000: - server.disconnect() - return W.WEECHAT_RC_OK - - for i, message in enumerate(server.client.outgoing_to_device_messages): - if i >= 5: - break - - if message in server.to_device_sent: - continue - - server.to_device(message) - server.to_device_sent.append(message) - - if server.sync_time and current_time > server.sync_time: - timeout = 0 if server.transport_type == TransportType.HTTP else 30000 - sync_filter = { - "room": { - "timeline": {"limit": 500}, - "state": {"lazy_load_members": True} - } - } - server.sync(timeout, sync_filter) - - if current_time > (server.user_gc_time + 3600): - server.garbage_collect_users() - - return W.WEECHAT_RC_OK - - -def create_default_server(config_file): - server = MatrixServer("matrix_org", config_file._ptr) - SERVERS[server.name] = server - - option = W.config_get(SCRIPT_NAME + ".server." + server.name + ".address") - W.config_option_set(option, "matrix.org", 1) - - return True - - -@utf8_decode -def send_cb(server_name, file_descriptor): - # type: (str, int) -> int - - server = SERVERS[server_name] - - if server.send_fd_hook: - W.unhook(server.send_fd_hook) - server.send_fd_hook = None - - if server.send_buffer: - server.try_send(server.send_buffer) - - return W.WEECHAT_RC_OK diff --git a/weechat/python/matrix/uploads.py b/weechat/python/matrix/uploads.py deleted file mode 100644 index 0b4e1f8..0000000 --- a/weechat/python/matrix/uploads.py +++ /dev/null @@ -1,391 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -"""Module implementing upload functionality.""" - -from __future__ import unicode_literals - -import attr -import time -import json -from typing import Dict, Any -from uuid import uuid1, UUID -from enum import Enum - -try: - from json.decoder import JSONDecodeError -except ImportError: - JSONDecodeError = ValueError # type: ignore - -from .globals import SCRIPT_NAME, SERVERS, W, UPLOADS -from .utf import utf8_decode -from .message_renderer import Render -from matrix import globals as G -from nio import Api - - -class UploadState(Enum): - created = 0 - active = 1 - finished = 2 - error = 3 - aborted = 4 - - -@attr.s -class Proxy(object): - ptr = attr.ib(type=str) - - @property - def name(self): - return W.infolist_string(self.ptr, "name") - - @property - def address(self): - return W.infolist_string(self.ptr, "address") - - @property - def type(self): - return W.infolist_string(self.ptr, "type_string") - - @property - def port(self): - return str(W.infolist_integer(self.ptr, "port")) - - @property - def user(self): - return W.infolist_string(self.ptr, "username") - - @property - def password(self): - return W.infolist_string(self.ptr, "password") - - -@attr.s -class Upload(object): - """Class representing an upload to a matrix server.""" - - server_name = attr.ib(type=str) - server_address = attr.ib(type=str) - access_token = attr.ib(type=str) - room_id = attr.ib(type=str) - filepath = attr.ib(type=str) - encrypt = attr.ib(type=bool, default=False) - file_keys = attr.ib(type=Dict, default=None) - - done = 0 - total = 0 - - uuid = None - buffer = None - upload_hook = None - content_uri = None - file_name = None - mimetype = "?" - state = UploadState.created - - def __attrs_post_init__(self): - self.uuid = uuid1() - self.buffer = "" - - server = SERVERS[self.server_name] - - proxy_name = server.config.proxy - proxy = None - proxies_list = None - - if proxy_name: - proxies_list = W.infolist_get("proxy", "", proxy_name) - if proxies_list: - W.infolist_next(proxies_list) - proxy = Proxy(proxies_list) - - process_args = { - "arg1": self.filepath, - "arg2": self.server_address, - "arg3": self.access_token, - "buffer_flush": "1", - } - - arg_count = 3 - - if self.encrypt: - arg_count += 1 - process_args["arg{}".format(arg_count)] = "--encrypt" - - if not server.config.ssl_verify: - arg_count += 1 - process_args["arg{}".format(arg_count)] = "--insecure" - - if proxy: - arg_count += 1 - process_args["arg{}".format(arg_count)] = "--proxy-type" - arg_count += 1 - process_args["arg{}".format(arg_count)] = proxy.type - - arg_count += 1 - process_args["arg{}".format(arg_count)] = "--proxy-address" - arg_count += 1 - process_args["arg{}".format(arg_count)] = proxy.address - - arg_count += 1 - process_args["arg{}".format(arg_count)] = "--proxy-port" - arg_count += 1 - process_args["arg{}".format(arg_count)] = proxy.port - - if proxy.user: - arg_count += 1 - process_args["arg{}".format(arg_count)] = "--proxy-user" - arg_count += 1 - process_args["arg{}".format(arg_count)] = proxy.user - - if proxy.password: - arg_count += 1 - process_args["arg{}".format(arg_count)] = "--proxy-password" - arg_count += 1 - process_args["arg{}".format(arg_count)] = proxy.password - - self.upload_hook = W.hook_process_hashtable( - "matrix_upload", - process_args, - 0, - "upload_cb", - str(self.uuid) - ) - - if proxies_list: - W.infolist_free(proxies_list) - - def abort(self): - pass - - @property - def msgtype(self): - # type: () -> str - assert self.mimetype - return Api.mimetype_to_msgtype(self.mimetype) - - @property - def content(self): - # type: () -> Dict[Any, Any] - assert self.content_uri - - if self.encrypt: - content = { - "body": self.file_name, - "msgtype": self.msgtype, - "file": self.file_keys, - } - content["file"]["url"] = self.content_uri - content["file"]["mimetype"] = self.mimetype - - # TODO thumbnail if it's an image - - return content - - return { - "msgtype": self.msgtype, - "body": self.file_name, - "url": self.content_uri, - } - - @property - def render(self): - # type: () -> str - assert self.content_uri - - if self.encrypt: - return Render.encrypted_media( - self.content_uri, - self.file_name, - self.file_keys["key"]["k"], - self.file_keys["hashes"]["sha256"], - self.file_keys["iv"], - ) - - return Render.media(self.content_uri, self.file_name) - - -@attr.s -class UploadsBuffer(object): - """Weechat buffer showing the uploads for a server.""" - - _ptr = "" # type: str - _selected_line = 0 # type: int - uploads = UPLOADS - - def __attrs_post_init__(self): - self._ptr = W.buffer_new( - SCRIPT_NAME + ".uploads", - "", - "", - "", - "", - ) - W.buffer_set(self._ptr, "type", "free") - W.buffer_set(self._ptr, "title", "Upload list") - W.buffer_set(self._ptr, "key_bind_meta2-A", "/uploads up") - W.buffer_set(self._ptr, "key_bind_meta2-B", "/uploads down") - W.buffer_set(self._ptr, "localvar_set_type", "uploads") - - self.render() - - def move_line_up(self): - self._selected_line = max(self._selected_line - 1, 0) - self.render() - - def move_line_down(self): - self._selected_line = min( - self._selected_line + 1, - len(self.uploads) - 1 - ) - self.render() - - def display(self): - """Display the buffer.""" - W.buffer_set(self._ptr, "display", "1") - - def render(self): - """Render the new state of the upload buffer.""" - # This function is under the MIT license. - # Copyright (c) 2016 Vladimir Ignatev - def progress(count, total): - bar_len = 60 - - if total == 0: - bar = '-' * bar_len - return "[{}] {}%".format(bar, "?") - - filled_len = int(round(bar_len * count / float(total))) - percents = round(100.0 * count / float(total), 1) - bar = '=' * filled_len + '-' * (bar_len - filled_len) - - return "[{}] {}%".format(bar, percents) - - W.buffer_clear(self._ptr) - header = "{}{}{}{}{}{}{}{}".format( - W.color("green"), - "Actions (letter+enter):", - W.color("lightgreen"), - " [A] Accept", - " [C] Cancel", - " [R] Remove", - " [P] Purge finished", - " [Q] Close this buffer" - ) - W.prnt_y(self._ptr, 0, header) - - for line_number, upload in enumerate(self.uploads.values()): - line_color = "{},{}".format( - "white" if line_number == self._selected_line else "default", - "blue" if line_number == self._selected_line else "default", - ) - first_line = ("%s%s %-24s %s%s%s %s (%s.%s)" % ( - W.color(line_color), - "*** " if line_number == self._selected_line else " ", - upload.room_id, - "\"", - upload.filepath, - "\"", - upload.mimetype, - SCRIPT_NAME, - upload.server_name, - )) - W.prnt_y(self._ptr, (line_number * 2) + 2, first_line) - - status_color = "{},{}".format("green", "blue") - status = "{}{}{}".format( - W.color(status_color), - upload.state.name, - W.color(line_color) - ) - - second_line = ("{color}{prefix} {status} {progressbar} " - "{done} / {total}").format( - color=W.color(line_color), - prefix="*** " if line_number == self._selected_line else " ", - status=status, - progressbar=progress(upload.done, upload.total), - done=W.string_format_size(upload.done), - total=W.string_format_size(upload.total)) - - W.prnt_y(self._ptr, (line_number * 2) + 3, second_line) - - -def find_upload(uuid): - return UPLOADS.get(uuid, None) - - -def handle_child_message(upload, message): - if message["type"] == "progress": - upload.done = message["data"] - - elif message["type"] == "status": - if message["status"] == "started": - upload.state = UploadState.active - upload.total = message["total"] - upload.mimetype = message["mimetype"] - upload.file_name = message["file_name"] - - elif message["status"] == "done": - upload.state = UploadState.finished - upload.content_uri = message["url"] - upload.file_keys = message.get("file_keys", None) - - server = SERVERS.get(upload.server_name, None) - - if not server: - return - - server.room_send_upload(upload) - - elif message["status"] == "error": - upload.state = UploadState.error - - if G.CONFIG.upload_buffer: - G.CONFIG.upload_buffer.render() - - -@utf8_decode -def upload_cb(data, command, return_code, out, err): - upload = find_upload(UUID(data)) - - if not upload: - return W.WEECHAT_RC_OK - - if return_code == W.WEECHAT_HOOK_PROCESS_ERROR: - W.prnt("", "Error with command '%s'" % command) - return W.WEECHAT_RC_OK - - if err != "": - W.prnt("", "Error with command '%s'" % err) - upload.state = UploadState.error - - if out != "": - upload.buffer += out - messages = upload.buffer.split("\n") - upload.buffer = "" - - for m in messages: - try: - message = json.loads(m) - except (JSONDecodeError, TypeError): - upload.buffer += m - continue - - handle_child_message(upload, message) - - return W.WEECHAT_RC_OK diff --git a/weechat/python/matrix/utf.py b/weechat/python/matrix/utf.py deleted file mode 100644 index 4d71987..0000000 --- a/weechat/python/matrix/utf.py +++ /dev/null @@ -1,117 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2014-2016 Ryan Huber -# Copyright (c) 2015-2016 Tollef Fog Heen - -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: - -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from __future__ import unicode_literals - -import sys - -# pylint: disable=redefined-builtin -from builtins import bytes, str -from functools import wraps - -if sys.version_info.major == 3 and sys.version_info.minor >= 3: - from collections.abc import Iterable, Mapping -else: - from collections import Iterable, Mapping - -# These functions were written by Trygve Aaberge for wee-slack and are under a -# MIT License. -# More info can be found in the wee-slack repository under the commit: -# 5e1c7e593d70972afb9a55f29d13adaf145d0166, the repository can be found at: -# https://github.com/wee-slack/wee-slack - - -class WeechatWrapper(object): - def __init__(self, wrapped_class): - self.wrapped_class = wrapped_class - - # Helper method used to encode/decode method calls. - def wrap_for_utf8(self, method): - def hooked(*args, **kwargs): - result = method(*encode_to_utf8(args), **encode_to_utf8(kwargs)) - # Prevent wrapped_class from becoming unwrapped - if result == self.wrapped_class: - return self - return decode_from_utf8(result) - - return hooked - - # Encode and decode everything sent to/received from weechat. We use the - # unicode type internally in wee-slack, but has to send utf8 to weechat. - def __getattr__(self, attr): - orig_attr = self.wrapped_class.__getattribute__(attr) - if callable(orig_attr): - return self.wrap_for_utf8(orig_attr) - return decode_from_utf8(orig_attr) - - # Ensure all lines sent to weechat specify a prefix. For lines after the - # first, we want to disable the prefix, which is done by specifying a - # space. - def prnt_date_tags(self, buffer, date, tags, message): - message = message.replace("\n", "\n \t") - return self.wrap_for_utf8(self.wrapped_class.prnt_date_tags)( - buffer, date, tags, message - ) - - -def utf8_decode(function): - """ - Decode all arguments from byte strings to unicode strings. Use this for - functions called from outside of this script, e.g. callbacks from weechat. - """ - - @wraps(function) - def wrapper(*args, **kwargs): - - # Don't do anything if we're python 3 - if sys.hexversion >= 0x3000000: - return function(*args, **kwargs) - - return function(*decode_from_utf8(args), **decode_from_utf8(kwargs)) - - return wrapper - - -def decode_from_utf8(data): - if isinstance(data, bytes): - return data.decode("utf-8") - if isinstance(data, str): - return data - elif isinstance(data, Mapping): - return type(data)(map(decode_from_utf8, data.items())) - elif isinstance(data, Iterable): - return type(data)(map(decode_from_utf8, data)) - return data - - -def encode_to_utf8(data): - if isinstance(data, str): - return data.encode("utf-8") - if isinstance(data, bytes): - return data - elif isinstance(data, Mapping): - return type(data)(map(encode_to_utf8, data.items())) - elif isinstance(data, Iterable): - return type(data)(map(encode_to_utf8, data)) - return data diff --git a/weechat/python/matrix/utils.py b/weechat/python/matrix/utils.py deleted file mode 100644 index ce5f9d1..0000000 --- a/weechat/python/matrix/utils.py +++ /dev/null @@ -1,207 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright © 2018, 2019 Damir Jelić -# Copyright © 2018, 2019 Denis Kasak -# -# Permission to use, copy, modify, and/or distribute this software for -# any purpose with or without fee is hereby granted, provided that the -# above copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY -# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import unicode_literals, division - -import time -from typing import Any, Dict, List - -from .globals import W - -if False: - from .server import MatrixServer - - -def key_from_value(dictionary, value): - # type: (Dict[str, Any], Any) -> str - return list(dictionary.keys())[list(dictionary.values()).index(value)] - - -def server_buffer_prnt(server, string): - # type: (MatrixServer, str) -> None - assert server.server_buffer - buffer = server.server_buffer - now = int(time.time()) - W.prnt_date_tags(buffer, now, "", string) - - -def tags_from_line_data(line_data): - # type: (str) -> List[str] - tags_count = W.hdata_get_var_array_size( - W.hdata_get("line_data"), line_data, "tags_array" - ) - - tags = [ - W.hdata_string( - W.hdata_get("line_data"), line_data, "%d|tags_array" % i - ) - for i in range(tags_count) - ] - - return tags - - -def create_server_buffer(server): - # type: (MatrixServer) -> None - buffer_name = "server.{}".format(server.name) - server.server_buffer = W.buffer_new( - buffer_name, "server_buffer_cb", server.name, "", "" - ) - - server_buffer_set_title(server) - W.buffer_set(server.server_buffer, "short_name", server.name) - W.buffer_set(server.server_buffer, "localvar_set_type", "server") - W.buffer_set( - server.server_buffer, "localvar_set_nick", server.config.username - ) - W.buffer_set(server.server_buffer, "localvar_set_server", server.name) - W.buffer_set(server.server_buffer, "localvar_set_channel", server.name) - - server.buffer_merge() - - -def server_buffer_set_title(server): - # type: (MatrixServer) -> None - if server.numeric_address: - ip_string = " ({address})".format(address=server.numeric_address) - else: - ip_string = "" - - title = ("Matrix: {address}:{port}{ip}").format( - address=server.address, port=server.config.port, ip=ip_string - ) - - W.buffer_set(server.server_buffer, "title", title) - - -def server_ts_to_weechat(timestamp): - # type: (float) -> int - date = int(timestamp / 1000) - return date - - -def strip_matrix_server(string): - # type: (str) -> str - return string.rsplit(":", 1)[0] - - -def shorten_sender(sender): - # type: (str) -> str - return strip_matrix_server(sender)[1:] - - -def string_strikethrough(string): - return "".join(["{}\u0336".format(c) for c in string]) - - -def string_color_and_reset(string, color): - """Color string with color, then reset all attributes.""" - - lines = string.split('\n') - lines = ("{}{}{}".format(W.color(color), line, W.color("reset")) - for line in lines) - return "\n".join(lines) - - -def string_color(string, color): - """Color string with color, then reset the color attribute.""" - - lines = string.split('\n') - lines = ("{}{}{}".format(W.color(color), line, W.color("resetcolor")) - for line in lines) - return "\n".join(lines) - - -def color_pair(color_fg, color_bg): - """Make a color pair from a pair of colors.""" - - if color_bg: - return "{},{}".format(color_fg, color_bg) - else: - return color_fg - - -def text_block(text, margin=0): - """ - Pad block of text with whitespace to form a regular block, optionally - adding a margin. - """ - - # add vertical margin - vertical_margin = margin // 2 - text = "{}{}{}".format( - "\n" * vertical_margin, - text, - "\n" * vertical_margin - ) - - lines = text.split("\n") - longest_len = max(len(l) for l in lines) + margin - - # pad block and add horizontal margin - text = "\n".join( - "{pre}{line}{post}".format( - pre=" " * margin, - line=l, - post=" " * (longest_len - len(l))) - for l in lines) - - return text - - -def colored_text_block(text, margin=0, color_pair=""): - """ Like text_block, but also colors it.""" - return string_color_and_reset(text_block(text, margin=margin), color_pair) - -def parse_redact_args(args): - args = args.strip() - - had_example_text = False - - try: - event_id, rest = args.split("|", 1) - had_example_text = True - except ValueError: - try: - event_id, rest = args.split(" ", 1) - except ValueError: - event_id, rest = (args, "") - - if had_example_text: - rest = rest.lstrip() - reason = None # until it has been correctly determined - if rest[0] == '"': - escaped = False - for i in range(1, len(rest)): - if escaped: - escaped = False - elif rest[i] == "\\": - escaped = True - elif rest[i] == '"': - reason = rest[i+1:] - break - else: - reason = rest - - event_id = event_id.strip() - if reason: - reason = reason.strip() - # The reason might be an empty string, set it to None if so - else: - reason = None - - return event_id, reason