Merge branch 'olm-command'

This commit is contained in:
Damir Jelić 2019-01-23 16:39:19 +01:00
commit d56cf219cf
24 changed files with 6677 additions and 3580 deletions

2
.gitignore vendored
View file

@ -3,3 +3,5 @@
.hypothesis/
.mypy_cache/
.pytest_cache/
.ropeproject
.coverage

View file

@ -130,7 +130,8 @@ disable=print-statement,
bad-whitespace,
too-few-public-methods,
too-many-lines,
missing-docstring
missing-docstring,
bad-continuation,
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option

View file

@ -1,4 +1,4 @@
.PHONY: install install-lib install-dir uninstall phony test
.PHONY: install install-lib install-dir uninstall phony test typecheck
WEECHAT_HOME ?= $(HOME)/.weechat
PREFIX ?= $(WEECHAT_HOME)
@ -26,3 +26,6 @@ $(DESTDIR)$(PREFIX)/python/matrix/%.py: matrix/%.py phony
test:
python3 -m pytest
python2 -m pytest
typecheck:
mypy -p matrix --ignore-missing-imports --warn-redundant-casts

245
contrib/matrix_upload Executable file
View file

@ -0,0 +1,245 @@
#!/usr/bin/python3 -u
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
#
# 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.
import os
import json
import magic
import requests
import argparse
from urllib.parse import urlparse
import urllib3
from nio import Api, UploadResponse, UploadError
from json.decoder import JSONDecodeError
urllib3.disable_warnings()
mime = magic.Magic(mime=True)
class Upload(object):
def __init__(self, file, chunksize=1 << 13):
self.file = file
self.filename = os.path.basename(file)
self.chunksize = chunksize
self.totalsize = os.path.getsize(file)
self.mimetype = mime.from_file(file)
self.readsofar = 0
def send_progress(self):
message = {
"type": "progress",
"data": self.readsofar
}
to_stdout(message)
def __iter__(self):
with open(self.filename, 'rb') as file:
while True:
data = file.read(self.chunksize)
if not data:
break
self.readsofar += len(data)
self.send_progress()
yield data
def __len__(self):
return self.totalsize
class IterableToFileAdapter(object):
def __init__(self, iterable):
self.iterator = iter(iterable)
self.length = len(iterable)
def read(self, size=-1):
return next(self.iterator, b'')
def __len__(self):
return self.length
def to_stdout(message):
print(json.dumps(message), flush=True)
def error(e):
message = {
"type": "status",
"status": "error",
"message": str(e)
}
to_stdout(message)
os.sys.exit()
def upload_process(args):
file_path = os.path.expanduser(args.file)
try:
upload = Upload(file_path, 10)
except (FileNotFoundError, OSError, IOError) as e:
error(e)
try:
url = urlparse(args.homeserver)
except ValueError as e:
error(e)
upload_url = ("https://{}".format(args.homeserver)
if not url.scheme else args.homeserver)
_, api_path, _ = Api.upload(args.access_token, upload.filename)
upload_url += api_path
headers = {
"Content-type": upload.mimetype,
}
proxies = {}
if args.proxy_address:
user = args.proxy_user or ""
if args.proxy_password:
user += ":{}".format(args.proxy_password)
if user:
user += "@"
proxies = {
"https": "{}://{}{}:{}/".format(
args.proxy_type,
user,
args.proxy_address,
args.proxy_port
)
}
message = {
"type": "status",
"status": "started",
"total": upload.totalsize,
"mimetype": upload.mimetype,
"file_name": upload.filename,
}
to_stdout(message)
session = requests.Session()
session.trust_env = False
try:
r = session.post(
url=upload_url,
auth=None,
headers=headers,
data=IterableToFileAdapter(upload),
verify=(not args.insecure),
proxies=proxies
)
except (requests.exceptions.RequestException, OSError) as e:
error(e)
try:
json_response = json.loads(r.content)
except JSONDecodeError:
error(r.content)
response = UploadResponse.from_dict(json_response)
if isinstance(response, UploadError):
error(str(response))
message = {
"type": "status",
"status": "done",
"url": response.content_uri
}
to_stdout(message)
return 0
def main():
parser = argparse.ArgumentParser(
description="Download and decrypt matrix attachments"
)
parser.add_argument("file", help="the file that will be uploaded")
parser.add_argument(
"homeserver",
type=str,
help="the address of the homeserver"
)
parser.add_argument(
"access_token",
type=str,
help="the access token to use for the upload"
)
parser.add_argument(
"--encrypt",
action="store_const",
const=True,
default=False,
help="encrypt the file before uploading it"
)
parser.add_argument(
"--insecure",
action="store_const",
const=True,
default=False,
help="disable SSL certificate verification"
)
parser.add_argument(
"--proxy-type",
choices=[
"http",
"socks4",
"socks5"
],
default="http",
help="type of the proxy that will be used to establish a connection"
)
parser.add_argument(
"--proxy-address",
type=str,
help="address of the proxy that will be used to establish a connection"
)
parser.add_argument(
"--proxy-port",
type=int,
default=8080,
help="port of the proxy that will be used to establish a connection"
)
parser.add_argument(
"--proxy-user",
type=str,
help="user that will be used for authentication on the proxy"
)
parser.add_argument(
"--proxy-password",
type=str,
help="password that will be used for authentication on the proxy"
)
args = parser.parse_args()
upload_process(args)
if __name__ == "__main__":
main()

380
main.py
View file

@ -17,69 +17,70 @@
from __future__ import unicode_literals
import os
import socket
import ssl
import time
import pprint
import OpenSSL.crypto as crypto
import textwrap
from itertools import chain
# pylint: disable=redefined-builtin
from builtins import str
from future.utils import bytes_to_native_str as n
from itertools import chain
# pylint: disable=unused-import
from typing import (List, Set, Dict, Tuple, Text, Optional, AnyStr, Deque, Any)
from typing import Any, AnyStr, Deque, Dict, List, Optional, Set, Text, Tuple
from matrix.colors import Formatted
from matrix.utf import utf8_decode
from matrix.http import HttpResponse
from matrix.api import MatrixSendMessage
import logbook
import OpenSSL.crypto as crypto
from future.utils import bytes_to_native_str as n
from logbook import Logger, StreamHandler
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_page_up, matrix_command_cb,
matrix_command_join_cb, matrix_command_part_cb,
matrix_command_invite_cb, matrix_command_topic_cb,
matrix_command_pgup_cb, matrix_redact_command_cb,
matrix_command_buf_clear_cb, matrix_me_command_cb,
matrix_command_kick_cb)
from matrix.commands import (hook_commands, 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)
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)
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, MAX_EVENTS
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,
matrix_partial_sync_cb)
from matrix.utf import utf8_decode
from matrix.utils import server_buffer_prnt, server_buffer_set_title
from matrix.server import (
MatrixServer,
create_default_server,
send_cb,
matrix_timer_cb,
matrix_config_server_read_cb,
matrix_config_server_write_cb,
matrix_config_server_change_cb,
)
from matrix.bar_items import (init_bar_items, matrix_bar_item_name,
matrix_bar_item_plugin, matrix_bar_item_lag,
matrix_bar_item_buffer_modes)
from matrix.completion import (
init_completion, matrix_command_completion_cb,
matrix_server_command_completion_cb, matrix_debug_completion_cb,
matrix_message_completion_cb, matrix_server_completion_cb)
from matrix.utils import (key_from_value, server_buffer_prnt, prnt_debug,
server_buffer_set_title)
from matrix.plugin_options import (DebugType, RedactType)
from matrix.config import (matrix_config_init, matrix_config_read,
matrix_config_free, matrix_config_change_cb,
matrix_config_reload_cb)
import matrix.globals
from matrix.globals import W, SERVERS
from matrix.uploads import UploadsBuffer, upload_cb
# yapf: disable
WEECHAT_SCRIPT_NAME = "matrix" # type: str
WEECHAT_SCRIPT_NAME = SCRIPT_NAME
WEECHAT_SCRIPT_DESCRIPTION = "matrix chat plugin" # type: str
WEECHAT_SCRIPT_AUTHOR = "Damir Jelić <poljar@termina.org.uk>" # type: str
WEECHAT_SCRIPT_VERSION = "0.1" # type: str
@ -87,6 +88,9 @@ 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))
@ -102,7 +106,8 @@ def print_certificate_info(buff, sock, cert):
key_info = ("key info: {key_type} key {bits} bits, signed using "
"{algo}").format(
key_type=key_type, bits=key_size, algo=signature_algorithm)
key_type=key_type, bits=key_size,
algo=n(signature_algorithm))
validity_info = (" Begins on: {before}\n"
" Expires on: {after}").format(
@ -120,8 +125,8 @@ def print_certificate_info(buff, sock, cert):
issuer = "issuer: {issuer}".format(issuer=issuer)
fingerprints = (" SHA1: {}\n"
" SHA256: {}").format(sha1_fingerprint,
sha256_fingerprint)
" SHA256: {}").format(n(sha1_fingerprint),
n(sha256_fingerprint))
wrapper = textwrap.TextWrapper(
initial_indent=" - ", subsequent_indent=" ")
@ -143,13 +148,6 @@ def print_certificate_info(buff, sock, cert):
W.prnt(buff, message)
@utf8_decode
def matrix_event_timer_cb(server_name, remaining_calls):
server = SERVERS[server_name]
server.handle_events()
return W.WEECHAT_RC_OK
def wrap_socket(server, file_descriptor):
# type: (MatrixServer, int) -> None
sock = None # type: socket.socket
@ -157,6 +155,10 @@ def wrap_socket(server, file_descriptor):
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
@ -178,7 +180,7 @@ def wrap_socket(server, file_descriptor):
ssl_socket = server.ssl_context.wrap_socket(
sock, do_handshake_on_connect=False,
server_hostname=server.address) # type: ssl.SSLSocket
server_hostname=server.config.address) # type: ssl.SSLSocket
server.socket = ssl_socket
@ -236,8 +238,11 @@ def try_ssl_handshake(server):
return False
except ssl.SSLError as error:
str_error = error.reason if error.reason else "Unknown error"
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(
@ -247,7 +252,7 @@ def try_ssl_handshake(server):
server_buffer_prnt(
server, ("{prefix}matrix: disconnecting from server..."
).format(prefix=W.prefix("network")))
).format(prefix=W.prefix("network")))
server.disconnect()
return False
@ -275,7 +280,7 @@ def receive_cb(server_name, file_descriptor):
server_buffer_prnt(
server, ("{prefix}matrix: disconnecting from server..."
).format(prefix=W.prefix("network")))
).format(prefix=W.prefix("network")))
server.disconnect()
@ -288,54 +293,60 @@ def receive_cb(server_name, file_descriptor):
prefix=W.prefix("network")))
server_buffer_prnt(
server, ("{prefix}matrix: disconnecting from server..."
).format(prefix=W.prefix("network")))
).format(prefix=W.prefix("network")))
server.disconnect()
break
received = len(data) # type: int
parsed_bytes = server.http_parser.execute(data, received)
try:
server.client.receive(data)
except (RemoteTransportError, RemoteProtocolError) as e:
server.error(str(e))
server.disconnect()
break
assert parsed_bytes == received
response = server.client.next_response(MAX_EVENTS)
if server.http_parser.is_partial_body():
server.http_buffer.append(server.http_parser.recv_body())
# Check if we need to send some data back
data_to_send = server.client.data_to_send()
if server.http_parser.is_message_complete():
status = server.http_parser.get_status_code()
headers = server.http_parser.get_headers()
body = b"".join(server.http_buffer)
if data_to_send:
server.send(data_to_send)
message = server.receive_queue.popleft()
message.response = HttpResponse(status, headers, body)
receive_time = time.time()
server.lag = (receive_time - message.send_time) * 1000
server.lag_done = True
W.bar_item_update("lag")
message.receive_time = receive_time
prnt_debug(DebugType.MESSAGING, server,
("{prefix}Received message of type {t} and "
"status {s}").format(
prefix=W.prefix("error"),
t=message.__class__.__name__,
s=status))
# Message done, reset the parser state.
server.reset_parser()
server.handle_response(message)
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)
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()
if negotiated_protocol is None:
negotiated_protocol = server.socket.selected_npn_protocol()
if negotiated_protocol == "http/1.1":
server.transport_type = TransportType.HTTP
elif negotiated_protocol == "h2":
server.transport_type = TransportType.HTTP2
data = server.client.connect(server.transport_type)
server.send(data)
server.login()
@ -356,71 +367,41 @@ def connect_cb(data, status, gnutls_rc, sock, error, ip_address):
return W.WEECHAT_RC_OK
elif status_value == W.WEECHAT_HOOK_CONNECT_ADDRESS_NOT_FOUND:
W.prnt(
server.server_buffer,
'{address} not found'.format(address=ip_address))
server.error('{address} not found'.format(address=ip_address))
elif status_value == W.WEECHAT_HOOK_CONNECT_IP_ADDRESS_NOT_FOUND:
W.prnt(server.server_buffer, 'IP address not found')
server.error('IP address not found')
elif status_value == W.WEECHAT_HOOK_CONNECT_CONNECTION_REFUSED:
W.prnt(server.server_buffer, 'Connection refused')
server.error('Connection refused')
elif status_value == W.WEECHAT_HOOK_CONNECT_PROXY_ERROR:
W.prnt(server.server_buffer,
'Proxy fails to establish connection to server')
server.error('Proxy fails to establish connection to server')
elif status_value == W.WEECHAT_HOOK_CONNECT_LOCAL_HOSTNAME_ERROR:
W.prnt(server.server_buffer, 'Unable to set local hostname')
server.error('Unable to set local hostname')
elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_INIT_ERROR:
W.prnt(server.server_buffer, 'TLS init error')
server.error('TLS init error')
elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_HANDSHAKE_ERROR:
W.prnt(server.server_buffer, 'TLS Handshake failed')
server.error('TLS Handshake failed')
elif status_value == W.WEECHAT_HOOK_CONNECT_MEMORY_ERROR:
W.prnt(server.server_buffer, 'Not enough memory')
server.error('Not enough memory')
elif status_value == W.WEECHAT_HOOK_CONNECT_TIMEOUT:
W.prnt(server.server_buffer, 'Timeout')
server.error('Timeout')
elif status_value == W.WEECHAT_HOOK_CONNECT_SOCKET_ERROR:
W.prnt(server.server_buffer, 'Unable to create socket')
server.error('Unable to create socket')
else:
W.prnt(
server.server_buffer,
'Unexpected error: {status}'.format(status=status_value))
server.error('Unexpected error: {status}'.format(status=status_value))
server.disconnect(reconnect=True)
return W.WEECHAT_RC_OK
@utf8_decode
def room_input_cb(server_name, buffer, input_data):
server = SERVERS[server_name]
if not server.connected:
message = "{prefix}matrix: you are not connected to the server".format(
prefix=W.prefix("error"))
W.prnt(buffer, message)
return W.WEECHAT_RC_ERROR
room_id = key_from_value(server.buffers, buffer)
room = server.rooms[room_id]
if room.encrypted:
return W.WEECHAT_RC_OK
formatted_data = Formatted.from_input_line(input_data)
message = MatrixSendMessage(
server.client, room_id=room_id, formatted_message=formatted_data)
server.send_or_queue(message)
return W.WEECHAT_RC_OK
@utf8_decode
def room_close_cb(data, buffer):
W.prnt("",
@ -430,17 +411,123 @@ def room_close_cb(data, buffer):
@utf8_decode
def matrix_unload_cb():
matrix_config_free(matrix.globals.CONFIG)
W.prnt("", "unloading")
for server in SERVERS.values():
server.config.free()
G.CONFIG.free()
# for server in SERVERS.values():
# server.store_olm()
return W.WEECHAT_RC_OK
def autoconnect(servers):
for server in servers.values():
if server.autoconnect:
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
if room_buffer.should_send_read_marker:
event_id = room_buffer.last_event_id
# A buffer may not have any events, in that case no event id is
# here returned
if event_id:
server.room_send_read_marker(
room_buffer.room.room_id, event_id)
room_buffer.last_read_event = event_id
if room_buffer.members_fetched:
return W.WEECHAT_RC_OK
room_id = room_buffer.room.room_id
server.get_joined_members(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
if __name__ == "__main__":
if W.register(WEECHAT_SCRIPT_NAME, WEECHAT_SCRIPT_AUTHOR,
WEECHAT_SCRIPT_VERSION, WEECHAT_SCRIPT_LICENSE,
@ -451,17 +538,22 @@ if __name__ == "__main__":
"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.
matrix.globals.CONFIG = W.config_new("matrix",
"matrix_config_reload_cb", "")
matrix_config_init(matrix.globals.CONFIG)
matrix_config_read(matrix.globals.CONFIG)
G.CONFIG = MatrixConfig()
G.CONFIG.read()
hook_commands()
init_bar_items()
init_completion()
W.hook_signal("buffer_switch", "buffer_switch_cb", "")
W.hook_signal("input_text_changed", "typing_notification_cb", "")
if not SERVERS:
create_default_server(matrix.globals.CONFIG)
create_default_server(G.CONFIG)
autoconnect(SERVERS)

View file

@ -1,33 +1,106 @@
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,
},
'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,
},
}
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
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"
}
escape_codes = []
reset_code = "0"
def make_fg_color(color):
return "38;5;{}".format(color)
def make_fg_color(color_code):
return "38;5;{}".format(color_code)
def make_bg_color(color):
return "48;5;{}".format(color)
def make_bg_color(color_code):
return "48;5;{}".format(color_code)
attributes = {
"bold": "1",
@ -70,21 +143,21 @@ def color(color_name):
stripped_color = fg_color.lstrip("*_|/!")
if stripped_color in weechat_base_colors:
if stripped_color in WEECHAT_BASE_COLORS:
escape_codes.append(
make_fg_color(weechat_base_colors[stripped_color]))
make_fg_color(WEECHAT_BASE_COLORS[stripped_color]))
elif stripped_color.isdigit():
num_color = int(stripped_color)
if num_color >= 0 and num_color < 256:
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]))
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 num_color >= 0 and num_color < 256:
if 0 <= num_color < 256:
escape_codes.append(make_bg_color(bg_color))
escape_string = "\033[{}{}m".format(reset_code, ";".join(escape_codes))
@ -92,7 +165,7 @@ def color(color_name):
return escape_string
def prefix(prefix):
def prefix(prefix_string):
prefix_to_symbol = {
"error": "=!=",
"network": "--",
@ -101,27 +174,75 @@ def prefix(prefix):
"quit": "<--"
}
if prefix in prefix_to_symbol:
if prefix_string in prefix_to_symbol:
return prefix_to_symbol[prefix]
return ""
def prnt(_, string):
print(string)
def prnt(_, message):
print(message)
def config_search_section(*args, **kwargs):
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(*args, **kwargs):
def config_new_option(*_, **__):
pass
def mkdir_home(*args, **kwargs):
def mkdir_home(*_, **__):
return True
def info_get(info, *args):
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 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

View file

@ -1,512 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
#
# 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 builtins import str
import time
import json
from enum import Enum, unique
from functools import partial
try:
from json.decoder import JSONDecodeError
except ImportError:
JSONDecodeError = ValueError
try:
from urllib import quote, urlencode
from urlparse import urlparse
except ImportError:
from urllib.parse import quote, urlencode, urlparse
from matrix.http import RequestType, HttpRequest
import matrix.events as MatrixEvents
MATRIX_API_PATH = "/_matrix/client/r0" # type: str
class MatrixClient:
def __init__(
self,
host, # type: str
access_token="", # type: str
user_agent="" # type: str
):
# type: (...) -> None
self.host = host
self.user_agent = user_agent
self.access_token = access_token
self.txn_id = 0 # type: int
def _get_txn_id(self):
txn_id = self.txn_id
self.txn_id = self.txn_id + 1
return txn_id
def login(self, user, password, device_name="", device_id=""):
# type (str, str, str) -> HttpRequest
path = ("{api}/login").format(api=MATRIX_API_PATH)
post_data = {
"type": "m.login.password",
"user": user,
"password": password
}
if device_id:
post_data["device_id"] = device_id
if device_name:
post_data["initial_device_display_name"] = device_name
return HttpRequest(RequestType.POST, self.host, path, post_data)
def sync(self, next_batch="", sync_filter=None):
# type: (str, Dict[Any, Any]) -> HttpRequest
assert self.access_token
query_parameters = {"access_token": self.access_token}
if sync_filter:
query_parameters["filter"] = json.dumps(
sync_filter, separators=(",", ":"))
if next_batch:
query_parameters["since"] = next_batch
path = ("{api}/sync?{query_params}").format(
api=MATRIX_API_PATH, query_params=urlencode(query_parameters))
return HttpRequest(RequestType.GET, self.host, path)
def room_send_message(self,
room_id,
message_type,
content,
formatted_content=None):
# type: (str, str, str) -> HttpRequest
query_parameters = {"access_token": self.access_token}
body = {"msgtype": message_type, "body": content}
if formatted_content:
body["format"] = "org.matrix.custom.html"
body["formatted_body"] = formatted_content
path = ("{api}/rooms/{room}/send/m.room.message/"
"{tx_id}?{query_parameters}").format(
api=MATRIX_API_PATH,
room=quote(room_id),
tx_id=quote(str(self._get_txn_id())),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.PUT, self.host, path, body)
def room_topic(self, room_id, topic):
# type: (str, str) -> HttpRequest
query_parameters = {"access_token": self.access_token}
content = {"topic": topic}
path = ("{api}/rooms/{room}/state/m.room.topic?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
room=quote(room_id),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.PUT, self.host, path, content)
def room_redact(self, room_id, event_id, reason=None):
# type: (str, str, str) -> HttpRequest
query_parameters = {"access_token": self.access_token}
content = {}
if reason:
content["reason"] = reason
path = ("{api}/rooms/{room}/redact/{event_id}/{tx_id}?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
room=quote(room_id),
event_id=quote(event_id),
tx_id=quote(str(self._get_txn_id())),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.PUT, self.host, path, content)
def room_get_messages(self,
room_id,
start_token,
end_token="",
limit=10,
direction='b'):
query_parameters = {
"access_token": self.access_token,
"from": start_token,
"dir": direction,
"limit": str(limit)
}
if end_token:
query_parameters["to"] = end_token
path = ("{api}/rooms/{room}/messages?{query_parameters}").format(
api=MATRIX_API_PATH,
room=quote(room_id),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.GET, self.host, path)
def room_join(self, room_id):
query_parameters = {"access_token": self.access_token}
path = ("{api}/join/{room_id}?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
room_id=quote(room_id),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.POST, self.host, path)
def room_leave(self, room_id):
query_parameters = {"access_token": self.access_token}
path = ("{api}/rooms/{room_id}/leave?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
room_id=quote(room_id),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.POST, self.host, path)
def room_invite(self, room_id, user_id):
query_parameters = {"access_token": self.access_token}
content = {"user_id": user_id}
path = ("{api}/rooms/{room_id}/invite?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
room_id=quote(room_id),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.POST, self.host, path, content)
def room_kick(self, room_id, user_id, reason=None):
query_parameters = {"access_token": self.access_token}
content = {"user_id": user_id}
if reason:
content["reason"] = reason
path = ("{api}/rooms/{room_id}/kick?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
room_id=quote(room_id),
query_parameters=urlencode(query_parameters))
h = HttpRequest(RequestType.POST, self.host, path, content)
return h
def mxc_to_http(self, mxc):
# type: (str) -> str
url = urlparse(mxc)
if url.scheme != "mxc":
return None
if not url.netloc or not url.path:
return None
http_url = ("https://{host}/_matrix/media/r0/download/"
"{server_name}{mediaId}").format(
host=self.host,
server_name=url.netloc,
mediaId=url.path)
return http_url
class MatrixMessage():
def __init__(
self,
request_func, # type: Callable[[...], HttpRequest]
func_args,
):
# type: (...) -> None
# yapf: disable
self.request = None # type: HttpRequest
self.response = None # type: HttpResponse
self.decoded_response = None # type: Dict[Any, Any]
self.creation_time = time.time() # type: float
self.send_time = None # type: float
self.receive_time = None # type: float
self.event = None
self.request = request_func(**func_args)
# yapf: enable
def decode_body(self, server):
try:
self.decoded_response = json.loads(
self.response.body, encoding='utf-8')
return (True, None)
except Exception as error:
return (False, error)
def _decode(self, server, object_hook):
try:
parsed_dict = json.loads(
self.response.body,
encoding='utf-8',
)
self.event = object_hook(parsed_dict)
return (True, None)
except JSONDecodeError as error:
return (False, error)
class MatrixLoginMessage(MatrixMessage):
def __init__(self, client, user, password, device_name, device_id=None):
data = {"user": user, "password": password, "device_name": device_name}
if device_id:
data["device_id"] = device_id
MatrixMessage.__init__(self, client.login, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixLoginEvent.from_dict, server)
return self._decode(server, object_hook)
class MatrixSyncMessage(MatrixMessage):
def __init__(self, client, next_batch=None, limit=None):
data = {}
if next_batch:
data["next_batch"] = next_batch
if limit:
data["sync_filter"] = {"room": {"timeline": {"limit": limit}}}
MatrixMessage.__init__(self, client.sync, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixSyncEvent.from_dict, server)
return self._decode(server, object_hook)
class MatrixSendMessage(MatrixMessage):
def __init__(self,
client,
room_id,
formatted_message,
message_type="m.text"):
self.room_id = room_id
self.formatted_message = formatted_message
data = {
"room_id": self.room_id,
"message_type": message_type,
"content": self.formatted_message.to_plain()
}
if self.formatted_message.is_formatted():
data["formatted_content"] = self.formatted_message.to_html()
MatrixMessage.__init__(self, client.room_send_message, data)
def decode_body(self, server):
object_hook = partial(
MatrixEvents.MatrixSendEvent.from_dict,
server,
self.room_id,
self.formatted_message,
)
return self._decode(server, object_hook)
class MatrixEmoteMessage(MatrixSendMessage):
def __init__(self, client, room_id, formatted_message):
MatrixSendMessage.__init__(self, client, room_id, formatted_message,
"m.emote")
def decode_body(self, server):
object_hook = partial(
MatrixEvents.MatrixEmoteEvent.from_dict,
server,
self.room_id,
self.formatted_message,
)
return self._decode(server, object_hook)
class MatrixTopicMessage(MatrixMessage):
def __init__(self, client, room_id, topic):
self.room_id = room_id
self.topic = topic
data = {"room_id": self.room_id, "topic": self.topic}
MatrixMessage.__init__(self, client.room_topic, data)
def decode_body(self, server):
object_hook = partial(
MatrixEvents.MatrixTopicEvent.from_dict,
server,
self.room_id,
self.topic,
)
return self._decode(server, object_hook)
class MatrixRedactMessage(MatrixMessage):
def __init__(self, client, room_id, event_id, reason=None):
self.room_id = room_id
self.event_id = event_id
self.reason = reason
data = {"room_id": self.room_id, "event_id": self.event_id}
if reason:
data["reason"] = reason
MatrixMessage.__init__(self, client.room_redact, data)
def decode_body(self, server):
object_hook = partial(
MatrixEvents.MatrixRedactEvent.from_dict,
server,
self.room_id,
self.reason,
)
return self._decode(server, object_hook)
class MatrixBacklogMessage(MatrixMessage):
def __init__(self, client, room_id, token, limit):
self.room_id = room_id
data = {
"room_id": self.room_id,
"start_token": token,
"direction": "b",
"limit": limit
}
MatrixMessage.__init__(self, client.room_get_messages, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixBacklogEvent.from_dict, server,
self.room_id)
return self._decode(server, object_hook)
class MatrixJoinMessage(MatrixMessage):
def __init__(self, client, room_id):
self.room_id = room_id
data = {"room_id": self.room_id}
MatrixMessage.__init__(self, client.room_join, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixJoinEvent.from_dict, server,
self.room_id)
return self._decode(server, object_hook)
class MatrixPartMessage(MatrixMessage):
def __init__(self, client, room_id):
self.room_id = room_id
data = {"room_id": self.room_id}
MatrixMessage.__init__(self, client.room_leave, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixPartEvent.from_dict, server,
self.room_id)
return self._decode(server, object_hook)
class MatrixInviteMessage(MatrixMessage):
def __init__(self, client, room_id, user_id):
self.room_id = room_id
self.user_id = user_id
data = {"room_id": self.room_id, "user_id": self.user_id}
MatrixMessage.__init__(self, client.room_invite, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixInviteEvent.from_dict, server,
self.room_id, self.user_id)
return self._decode(server, object_hook)
class MatrixKickMessage(MatrixMessage):
def __init__(self, client, room_id, user_id, reason=None):
self.room_id = room_id
self.user_id = user_id
self.reason = reason
data = {"room_id": self.room_id,
"user_id": self.user_id,
"reason": reason}
MatrixMessage.__init__(self, client.room_kick, data)
def decode_body(self, server):
object_hook = partial(
MatrixEvents.MatrixKickEvent.from_dict,
server,
self.room_id,
self.user_id,
self.reason)
return self._decode(server, object_hook)

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
# Copyright © 2018, 2019 Damir Jelić <poljar@termina.org.uk>
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
@ -15,24 +15,22 @@
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
from __future__ import unicode_literals
from builtins import str
from matrix.utf import utf8_decode
from matrix.utils import key_from_value
from matrix.globals import W, SERVERS
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):
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)
name=server.name,
)
return ""
@ -42,25 +40,31 @@ 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")
color = (
"status_name_ssl"
if server.ssl_context.check_hostname
else "status_name"
)
room_id = key_from_value(server.buffers, buffer)
room = server.rooms[room_id]
room_buffer = server.find_room_from_ptr(buffer)
room = room_buffer.room
return "{color}{name}".format(
color=W.color(color),
name=room.display_name(server.user_id))
color=W.color(color), name=room.display_name
)
elif buffer == server.server_buffer:
color = ("status_name_ssl"
if server.ssl_context.check_hostname else "status_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=server.name,
)
return ""
@ -69,9 +73,8 @@ def matrix_bar_item_name(data, item, window, buffer, extra_info):
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 >= 500:
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")
@ -80,7 +83,8 @@ def matrix_bar_item_lag(data, item, window, buffer, extra_info):
lag_string = "Lag: {color}{lag}{ncolor}".format(
lag=lag.format((server.lag / 1000)),
color=color,
ncolor=W.color("reset"))
ncolor=W.color("reset"),
)
return lag_string
return ""
@ -92,23 +96,94 @@ 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_id = key_from_value(server.buffers, buffer)
room = server.rooms[room_id]
room_buffer = server.find_room_from_ptr(buffer)
room = room_buffer.room
modes = []
if room.encrypted:
modes.append("🔐")
modes.append(G.CONFIG.look.encrypted_room_sign)
if room.backlog_pending:
modes.append("")
if (server.client
and server.client.room_contains_unverified(room.room_id)):
modes.append(G.CONFIG.look.encryption_warning_sign)
if not server.connected:
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
for server in SERVERS.values():
if buffer in server.buffers.values():
room_buffer = server.find_room_from_ptr(buffer)
room = room_buffer.room
return str(room.member_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(<item>) 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", "")

1688
matrix/buffer.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,8 @@
# Copyright © 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
# Copyright © 2016 Avi Halachmi <avihpit@yahoo.com>
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
# Copyright © 2018, 2019 Damir Jelić <poljar@termina.org.uk>
# Copyright © 2018, 2019 Denis Kasak <dkasak@termina.org.uk>
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
@ -18,39 +19,63 @@
from __future__ import unicode_literals
import html
import re
import textwrap
# pylint: disable=redefined-builtin
from builtins import str
from collections import namedtuple
from matrix.globals import W
from matrix.utils import string_strikethrough
from typing import List
import textwrap
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
import html
from html.entities import name2codepoint
FormattedString = namedtuple('FormattedString', ['text', 'attributes'])
class FormattedString:
__slots__ = ("text", "attributes")
quote_wrapper = textwrap.TextWrapper(
initial_indent="> ", subsequent_indent="> ")
def __init__(self, text, attributes):
self.attributes = DEFAULT_ATTRIBUTES.copy()
self.attributes.update(attributes)
self.text = text
class Formatted():
class Formatted(object):
def __init__(self, substrings):
# type: (List[FormattedString]) -> None
self.substrings = substrings
@property
def textwrapper(self):
quote_pair = color_pair(G.CONFIG.color.quote_fg,
G.CONFIG.color.quote_bg)
return textwrap.TextWrapper(
width=67,
initial_indent="{}> ".format(W.color(quote_pair)),
subsequent_indent="{}> ".format(W.color(quote_pair)),
)
def is_formatted(self):
# type: (Formatted) -> bool
for string in self.substrings:
if string.attributes != DEFAULT_ATRIBUTES:
if string.attributes != DEFAULT_ATTRIBUTES:
return True
return False
@ -64,35 +89,52 @@ class Formatted():
"""
text = "" # type: str
substrings = [] # type: List[FormattedString]
attributes = DEFAULT_ATRIBUTES.copy()
attributes = DEFAULT_ATTRIBUTES.copy()
i = 0
while i < len(line):
# Bold
if line[i] == "\x02":
if line[i] == "\x02" and not attributes["code"]:
if text:
substrings.append(FormattedString(text, attributes.copy()))
text = ""
attributes["bold"] = not attributes["bold"]
i = i + 1
# Markdown bold
elif line[i] == "*":
if attributes["italic"] and not line[i-1].isspace():
# Markdown inline code
elif line[i] == "`":
if text:
# strip leading and trailing spaces and compress consecutive
# spaces in inline code blocks
if attributes["code"]:
text = text.strip()
text = re.sub(r"\s+", " ", text)
substrings.append(
FormattedString(text, attributes.copy())
)
text = ""
attributes["code"] = not attributes["code"]
i = i + 1
# Markdown emphasis
elif line[i] == "*" and not attributes["code"]:
if attributes["italic"] and not line[i - 1].isspace():
if text:
substrings.append(FormattedString(
text, attributes.copy()))
substrings.append(
FormattedString(text, attributes.copy())
)
text = ""
attributes["italic"] = not attributes["italic"]
i = i + 1
continue
elif attributes["italic"] and line[i-1].isspace():
elif attributes["italic"] and line[i - 1].isspace():
text = text + line[i]
i = i + 1
continue
elif i+1 < len(line) and line[i+1].isspace():
elif i + 1 < len(line) and line[i + 1].isspace():
text = text + line[i]
i = i + 1
continue
@ -109,7 +151,7 @@ class Formatted():
i = i + 1
# Color
elif line[i] == "\x03":
elif line[i] == "\x03" and not attributes["code"]:
if text:
substrings.append(FormattedString(text, attributes.copy()))
text = ""
@ -147,15 +189,16 @@ class Formatted():
else:
attributes["bgcolor"] = None
# Reset
elif line[i] == "\x0F":
elif line[i] == "\x0F" and not attributes["code"]:
if text:
substrings.append(FormattedString(text, attributes.copy()))
text = ""
# Reset all the attributes
attributes = DEFAULT_ATRIBUTES.copy()
attributes = DEFAULT_ATTRIBUTES.copy()
i = i + 1
# Italic
elif line[i] == "\x1D":
elif line[i] == "\x1D" and not attributes["code"]:
if text:
substrings.append(FormattedString(text, attributes.copy()))
text = ""
@ -163,7 +206,7 @@ class Formatted():
i = i + 1
# Underline
elif line[i] == "\x1F":
elif line[i] == "\x1F" and not attributes["code"]:
if text:
substrings.append(FormattedString(text, attributes.copy()))
text = ""
@ -175,7 +218,7 @@ class Formatted():
text = text + line[i]
i = i + 1
substrings.append(FormattedString(text, attributes))
substrings.append(FormattedString(text, DEFAULT_ATTRIBUTES.copy()))
return cls(substrings)
@classmethod
@ -190,36 +233,60 @@ class Formatted():
def add_attribute(string, name, value):
if name == "bold" and value:
return "{bold_on}{text}{bold_off}".format(
bold_on="<strong>", text=string, bold_off="</strong>")
elif name == "italic" and value:
bold_on="<strong>", text=string, bold_off="</strong>"
)
if name == "italic" and value:
return "{italic_on}{text}{italic_off}".format(
italic_on="<em>", text=string, italic_off="</em>")
elif name == "underline" and value:
italic_on="<em>", text=string, italic_off="</em>"
)
if name == "underline" and value:
return "{underline_on}{text}{underline_off}".format(
underline_on="<u>", text=string, underline_off="</u>")
elif name == "strikethrough" and value:
underline_on="<u>", text=string, underline_off="</u>"
)
if name == "strikethrough" and value:
return "{strike_on}{text}{strike_off}".format(
strike_on="<del>", text=string, strike_off="</del>")
elif name == "quote" and value:
strike_on="<del>", text=string, strike_off="</del>"
)
if name == "quote" and value:
return "{quote_on}{text}{quote_off}".format(
quote_on="<blockquote>",
text=string,
quote_off="</blockquote>")
elif name == "fgcolor" and value:
quote_off="</blockquote>",
)
if name == "code" and value:
return "{code_on}{text}{code_off}".format(
code_on="<code>", text=string, code_off="</code>"
)
if name == "fgcolor" and value:
return "{color_on}{text}{color_off}".format(
color_on="<font color={color}>".format(
color=color_weechat_to_html(value)),
color=color_weechat_to_html(value)
),
text=string,
color_off="</font>")
color_off="</font>",
)
return string
def format_string(formatted_string):
text = formatted_string.text
attributes = formatted_string.attributes
attributes = formatted_string.attributes.copy()
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")
else:
for key, value in attributes.items():
text = add_attribute(text, key, value)
for key, value in attributes.items():
text = add_attribute(text, key, value)
return text
html_string = map(format_string, self.substrings)
@ -228,7 +295,7 @@ class Formatted():
# TODO do we want at least some formatting using unicode
# (strikethrough, quotes)?
def to_plain(self):
# type: (List[FormattedString]) -> str
# type: () -> str
def strip_atribute(string, _, __):
return string
@ -245,44 +312,91 @@ class Formatted():
def to_weechat(self):
# TODO BG COLOR
def add_attribute(string, name, value):
if name == "bold" and value:
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" and value:
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" and value:
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" and value:
underline_off=W.color("-underline"),
)
elif name == "strikethrough":
return string_strikethrough(string)
elif name == "quote":
return self.textwrapper.fill(
W.string_remove_color(string.replace("\n", ""), "")
)
elif name == "code":
code_color_pair = color_pair(
G.CONFIG.color.untagged_code_fg,
G.CONFIG.color.untagged_code_bg
)
elif name == "quote" and value:
return quote_wrapper.fill(string.replace("\n", ""))
margin = G.CONFIG.look.code_block_margin
elif name == "fgcolor" and value:
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" and value:
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"))
return string
color_off=W.color("resetcolor"),
)
else:
return string
def format_string(formatted_string):
text = formatted_string.text
@ -291,28 +405,68 @@ class Formatted():
# 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.pop('strikethrough')
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():
text = add_attribute(text, key, value)
return text
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)
return "".join(weechat_strings).rstrip("\n")
# 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()
# TODO this should be a typed dict.
DEFAULT_ATRIBUTES = {
DEFAULT_ATTRIBUTES = {
"bold": False,
"italic": False,
"underline": False,
"strikethrough": False,
"preformatted": False,
"quote": False,
"code": None,
"fgcolor": None,
"bgcolor": None
"bgcolor": None,
}
@ -323,7 +477,7 @@ class MatrixHtmlParser(HTMLParser):
HTMLParser.__init__(self)
self.text = "" # type: str
self.substrings = [] # type: List[FormattedString]
self.attributes = DEFAULT_ATRIBUTES.copy()
self.attributes = DEFAULT_ATTRIBUTES.copy()
def unescape(self, text):
"""Shim to unescape HTML in both Python 2 and 3.
@ -357,11 +511,33 @@ class MatrixHtmlParser(HTMLParser):
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_ATRIBUTES.copy())
self.add_substring(self.text, DEFAULT_ATTRIBUTES.copy())
self.text = ""
elif tag == "font":
for key, value in attrs:
@ -387,8 +563,18 @@ class MatrixHtmlParser(HTMLParser):
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())
@ -515,7 +701,7 @@ def color_line_to_weechat(color_string):
"96": "250",
"97": "254",
"98": "231",
"99": "default"
"99": "default",
}
assert color_string in line_colors
@ -523,20 +709,20 @@ def color_line_to_weechat(color_string):
return line_colors[color_string]
# The functions colour_dist_sq(), colour_to_6cube(), and colour_find_rgb
# 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 colour_dist_sq(R, G, B, r, g, b):
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 colour_to_6cube(v):
def color_to_6cube(v):
# pylint: disable=invalid-name
# type: (int) -> int
if v < 48:
@ -546,15 +732,15 @@ def colour_to_6cube(v):
return (v - 35) // 40
def colour_find_rgb(r, g, b):
def color_find_rgb(r, g, b):
# type: (int, int, int) -> int
"""Convert an RGB triplet to the xterm(1) 256 colour palette.
"""Convert an RGB triplet to the xterm(1) 256 color palette.
xterm provides a 6x6x6 colour cube (16 - 231) and 24 greys (232 - 255).
We map our RGB colour to the closest in the cube, also work out the
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 colours (they
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).
@ -563,16 +749,16 @@ def colour_find_rgb(r, g, b):
q2c = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
# Map RGB to 6x6x6 cube.
qr = colour_to_6cube(r)
qg = colour_to_6cube(g)
qb = colour_to_6cube(b)
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 colour exactly, return early.
if (cr == r and cg == g and cb == b):
# 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).
@ -585,10 +771,10 @@ def colour_find_rgb(r, g, b):
grey = 8 + (10 * grey_idx)
# Is grey or 6x6x6 colour closest?
d = colour_dist_sq(cr, cg, cb, r, g, b)
# Is grey or 6x6x6 color closest?
d = color_dist_sq(cr, cg, cb, r, g, b)
if colour_dist_sq(grey, grey, grey, r, g, b) < d:
if color_dist_sq(grey, grey, grey, r, g, b) < d:
idx = 232 + grey_idx
else:
idx = 16 + (36 * qr) + (6 * qg) + qb
@ -622,12 +808,12 @@ def color_html_to_weechat(color):
try:
rgb_color = webcolors.html5_parse_legacy_color(color)
except ValueError:
return None
return ""
if rgb_color in weechat_basic_colors:
return weechat_basic_colors[rgb_color]
return str(colour_find_rgb(*rgb_color))
return str(color_find_rgb(*rgb_color))
def color_weechat_to_html(color):
@ -913,5 +1099,50 @@ def color_weechat_to_html(color):
# yapf: enable
if color in weechat_basic_colors:
return hex_colors[weechat_basic_colors[color]]
else:
return hex_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)

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
# Copyright © 2018, 2019 Damir Jelić <poljar@termina.org.uk>
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
@ -16,29 +16,33 @@
from __future__ import unicode_literals
from matrix.globals import SERVERS, W
from matrix.utf import utf8_decode
from matrix.globals import W, SERVERS, OPTIONS
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)
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):
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']
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)
W.hook_completion_list_add(
completion, command, 0, W.WEECHAT_LIST_POS_SORT
)
if len(args) == 1:
complete_commands()
@ -47,11 +51,11 @@ def matrix_server_command_completion_cb(data, completion_item, buffer,
if args[1] not in commands:
complete_commands()
else:
if args[1] == 'delete' or args[1] == 'listfull':
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[1] == "delete" or args[1] == "listfull":
if args[2] not in SERVERS:
add_servers_to_completion(completion)
@ -67,69 +71,255 @@ def matrix_server_completion_cb(data, completion_item, buffer, completion):
@utf8_decode
def matrix_command_completion_cb(data, completion_item, buffer, completion):
for command in [
"connect", "disconnect", "reconnect", "server", "help", "debug"
"connect",
"disconnect",
"reconnect",
"server",
"help",
"debug",
]:
W.hook_completion_list_add(completion, command, 0,
W.WEECHAT_LIST_POS_SORT)
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)
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):
own_lines = W.hdata_pointer(W.hdata_get('buffer'), buffer, 'own_lines')
own_lines = W.hdata_pointer(W.hdata_get("buffer"), buffer, "own_lines")
if own_lines:
line = W.hdata_pointer(W.hdata_get('lines'), own_lines, 'last_line')
line = W.hdata_pointer(W.hdata_get("lines"), own_lines, "last_line")
line_number = 1
while line:
line_data = W.hdata_pointer(W.hdata_get('line'), line, 'data')
line_data = W.hdata_pointer(W.hdata_get("line"), line, "data")
if line_data:
message = W.hdata_string(
W.hdata_get('line_data'), line_data, 'message')
W.hdata_get("line_data"), line_data, "message"
)
tags = tags_from_line_data(line_data)
# Only add non redacted user messages to the completion
if (message and 'matrix_message' in tags and
'matrix_redacted' not in tags):
if (
message
and "matrix_message" in tags
and "matrix_redacted" not in tags
):
if len(message) > OPTIONS.redaction_comp_len + 2:
message = (message[:OPTIONS.redaction_comp_len] + '..')
if len(message) > REDACTION_COMP_LEN + 2:
message = message[:REDACTION_COMP_LEN] + ".."
item = ("{number}:\"{message}\"").format(
number=line_number, message=message)
item = ('{number}:"{message}"').format(
number=line_number, message=message
)
W.hook_completion_list_add(completion, item, 0,
W.WEECHAT_LIST_POS_END)
W.hook_completion_list_add(
completion, item, 0, W.WEECHAT_LIST_POS_END
)
line_number += 1
line = W.hdata_move(W.hdata_get('line'), line, -1)
line = W.hdata_move(W.hdata_get("line"), line, -1)
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
def init_completion():
W.hook_completion("matrix_server_commands", "Matrix server completion",
"matrix_server_command_completion_cb", "")
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_servers",
"Matrix server completion",
"matrix_server_completion_cb",
"",
)
W.hook_completion("matrix_commands", "Matrix command completion",
"matrix_command_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_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(
"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",
"",
)

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
# Copyright © 2018, 2019 Damir Jelić <poljar@termina.org.uk>
# Copyright © 2018, 2019 Denis Kasak <dkasak@termina.org.uk>
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
@ -14,15 +15,112 @@
# 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
"""weechat-matrix Configuration module.
from matrix.plugin_options import (Option, RedactType, ServerBufferType)
This module contains abstractions on top of weechats configuration files and
the main script configuration class.
import matrix.globals
from matrix.globals import W, OPTIONS, SERVERS
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 Enum, unique
import logbook
import nio
from matrix.globals import SCRIPT_NAME, SERVERS, W
from matrix.utf import utf8_decode
from matrix.utils import key_from_value, server_buffer_merge
from matrix.commands import hook_page_up
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
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
@ -30,105 +128,648 @@ 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 matrix_config_change_cb(data, option):
option_name = key_from_value(OPTIONS.options, option)
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"""
if option_name == "redactions":
OPTIONS.redaction_type = RedactType(W.config_integer(option))
for server in SERVERS.values():
server.buffer_merge()
return 1
elif option_name == "server_buffer":
OPTIONS.look_server_buf = ServerBufferType(W.config_integer(option))
for server in SERVERS.values():
if server.server_buffer:
server_buffer_merge(server.server_buffer)
elif option_name == "max_initial_sync_events":
OPTIONS.sync_limit = W.config_integer(option)
@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
elif option_name == "max_backlog_sync_events":
OPTIONS.backlog_limit = W.config_integer(option)
elif option_name == "fetch_backlog_on_pgup":
OPTIONS.enable_backlog = W.config_boolean(option)
@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
if OPTIONS.enable_backlog:
if not OPTIONS.page_up_hook:
hook_page_up(matrix.globals.CONFIG)
else:
if OPTIONS.page_up_hook:
W.unhook(OPTIONS.page_up_hook)
OPTIONS.page_up_hook = None
@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 matrix_config_init(config_file):
look_options = [
Option("redactions", "integer", "strikethrough|notice|delete", 0, 0,
"strikethrough",
("Only notice redactions, strike through or delete "
"redacted messages")),
Option("server_buffer", "integer",
"merge_with_core|merge_without_core|independent", 0, 0,
"merge_with_core", "Merge server buffers")
]
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
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"))
]
def add_global_options(section, options):
for option in options:
OPTIONS.options[option.name] = W.config_new_option(
config_file, section, option.name, option.type,
option.description, option.string_values, option.min,
option.max, option.value, option.value, 0, "", "",
"matrix_config_change_cb", "", "", "")
section = W.config_new_section(config_file, "color", 0, 0, "", "", "", "",
"", "", "", "", "", "")
# TODO color options
section = W.config_new_section(config_file, "look", 0, 0, "", "", "", "",
"", "", "", "", "", "")
add_global_options(section, look_options)
section = W.config_new_section(config_file, "network", 0, 0, "", "", "", "",
"", "", "", "", "", "")
add_global_options(section, network_options)
W.config_new_section(
config_file, "server", 0, 0, "matrix_config_server_read_cb", "",
"matrix_config_server_write_cb", "", "", "", "", "", "", "")
return config_file
return logbook.ERROR
def matrix_config_read(config):
# type: (str) -> bool
return_code = W.config_read(config)
if return_code == W.WEECHAT_CONFIG_READ_OK:
return True
elif return_code == W.WEECHAT_CONFIG_READ_MEMORY_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 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
elif return_code == W.WEECHAT_CONFIG_READ_FILE_NOT_FOUND:
return True
return False
def matrix_config_free(config):
for section in ["network", "look", "color", "server"]:
section_pointer = W.config_search_section(config, section)
W.config_section_free_options(section_pointer)
W.config_section_free(section_pointer)
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 = {}
W.config_free(config)
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
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(
"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"),
),
]
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(
"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"),
),
]
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",
),
]
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 free(self):
section_ptr = W.config_search_section(self._ptr, "server")
W.config_section_free(section_ptr)
super().free()

View file

@ -1,453 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
#
# 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 builtins import str
from collections import deque
from functools import partial
from operator import itemgetter
from matrix.globals import W
from matrix.utils import (tags_for_message, sanitize_id, sanitize_token,
sanitize_text, tags_from_line_data)
from matrix.rooms import (matrix_create_room_buffer, RoomInfo, RoomMessageText,
RoomMessageEvent, RoomRedactedMessageEvent,
RoomMessageEmote)
class MatrixEvent():
def __init__(self, server):
self.server = server
def execute(self):
pass
class MatrixErrorEvent(MatrixEvent):
def __init__(self, server, error_message, fatal=False):
self.error_message = error_message
self.fatal = fatal
MatrixEvent.__init__(self, server)
def execute(self):
message = ("{prefix}matrix: {error}").format(
prefix=W.prefix("error"), error=self.error_message)
W.prnt(self.server.server_buffer, message)
if self.fatal:
self.server.disconnect(reconnect=False)
@classmethod
def from_dict(cls, server, error_prefix, fatal, parsed_dict):
try:
message = "{prefix}: {error}".format(
prefix=error_prefix, error=sanitize_text(parsed_dict["error"]))
return cls(server, message, fatal=fatal)
except KeyError:
return cls(
server, ("{prefix}: Invalid JSON response "
"from server.").format(prefix=error_prefix),
fatal=fatal)
class MatrixLoginEvent(MatrixEvent):
def __init__(self, server, user_id, device_id, access_token):
self.user_id = user_id
self.access_token = access_token
self.device_id = device_id
MatrixEvent.__init__(self, server)
def execute(self):
self.server.access_token = self.access_token
self.server.user_id = self.user_id
self.server.client.access_token = self.access_token
self.server.device_id = self.device_id
self.server.save_device_id()
message = "{prefix}matrix: Logged in as {user}".format(
prefix=W.prefix("network"), user=self.user_id)
W.prnt(self.server.server_buffer, message)
self.server.sync()
@classmethod
def from_dict(cls, server, parsed_dict):
try:
return cls(server, sanitize_id(parsed_dict["user_id"]),
sanitize_id(parsed_dict["device_id"]),
sanitize_token(parsed_dict["access_token"]))
except (KeyError, TypeError, ValueError):
return MatrixErrorEvent.from_dict(server, "Error logging in", True,
parsed_dict)
class MatrixSendEvent(MatrixEvent):
def __init__(self, server, room_id, message):
self.room_id = room_id
self.message = message
MatrixEvent.__init__(self, server)
def execute(self):
tags = [
"matrix_message", "notify_none", "no_highlight", "self_msg", "log1"
]
buff = self.server.buffers[self.room_id]
room = self.server.rooms[self.room_id]
self.message.execute(self.server, room, buff, tags)
@classmethod
def from_dict(cls, server, room_id, message, parsed_dict):
try:
event_id = sanitize_id(parsed_dict["event_id"])
sender = server.user_id
age = 0
plain_message = message.to_plain()
formatted_message = message
message = RoomMessageText(event_id, sender, age, plain_message,
formatted_message)
return cls(server, room_id, message)
except (KeyError, TypeError, ValueError):
return MatrixErrorEvent.from_dict(server, "Error sending message",
False, parsed_dict)
class MatrixEmoteEvent(MatrixSendEvent):
@classmethod
def from_dict(cls, server, room_id, message, parsed_dict):
try:
event_id = sanitize_id(parsed_dict["event_id"])
sender = server.user_id
age = 0
plain_message = message.to_plain()
formatted_message = message
message = RoomMessageEmote(event_id, sender, age, plain_message,
formatted_message)
return cls(server, room_id, message)
except (KeyError, TypeError, ValueError):
return MatrixErrorEvent.from_dict(server, "Error sending message",
False, parsed_dict)
class MatrixTopicEvent(MatrixEvent):
def __init__(self, server, room_id, event_id, topic):
self.room_id = room_id
self.topic = topic
self.event_id = event_id
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, room_id, topic, parsed_dict):
try:
return cls(server, room_id, sanitize_id(parsed_dict["event_id"]),
topic)
except (KeyError, TypeError, ValueError):
return MatrixErrorEvent.from_dict(server, "Error setting topic",
False, parsed_dict)
class MatrixRedactEvent(MatrixEvent):
def __init__(self, server, room_id, event_id, reason):
self.room_id = room_id
self.topic = reason
self.event_id = event_id
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, room_id, reason, parsed_dict):
try:
return cls(server, room_id, sanitize_id(parsed_dict["event_id"]),
reason)
except (KeyError, TypeError, ValueError):
return MatrixErrorEvent.from_dict(server, "Error redacting message",
False, parsed_dict)
class MatrixJoinEvent(MatrixEvent):
def __init__(self, server, room, room_id):
self.room = room
self.room_id = room_id
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, room_id, parsed_dict):
try:
return cls(
server,
room_id,
sanitize_id(parsed_dict["room_id"]),
)
except (KeyError, TypeError, ValueError):
return MatrixErrorEvent.from_dict(server, "Error joining room",
False, parsed_dict)
class MatrixPartEvent(MatrixEvent):
def __init__(self, server, room_id):
self.room_id = room_id
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, room_id, parsed_dict):
try:
if parsed_dict == {}:
return cls(server, room_id)
raise KeyError
except KeyError:
return MatrixErrorEvent.from_dict(server, "Error leaving room",
False, parsed_dict)
class MatrixInviteEvent(MatrixEvent):
def __init__(self, server, room_id, user_id):
self.room_id = room_id
self.user_id = user_id
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, room_id, user_id, parsed_dict):
try:
if parsed_dict == {}:
return cls(server, room_id, user_id)
raise KeyError
except KeyError:
return MatrixErrorEvent.from_dict(server, "Error inviting user",
False, parsed_dict)
class MatrixKickEvent(MatrixEvent):
def __init__(self, server, room_id, user_id, reason):
self.room_id = room_id
self.user_id = user_id
self.reason = reason
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, room_id, user_id, reason, parsed_dict):
try:
if parsed_dict == {}:
return cls(server, room_id, user_id, reason)
raise KeyError
except KeyError:
return MatrixErrorEvent.from_dict(server, "Error kicking user",
False, parsed_dict)
class MatrixBacklogEvent(MatrixEvent):
def __init__(self, server, room_id, end_token, events):
self.room_id = room_id
self.end_token = end_token
self.events = events
MatrixEvent.__init__(self, server)
@staticmethod
def _room_event_from_dict(room_id, event_dict):
if room_id != event_dict["room_id"]:
raise ValueError
if "redacted_by" in event_dict["unsigned"]:
return RoomRedactedMessageEvent.from_dict(event_dict)
return RoomMessageEvent.from_dict(event_dict)
@staticmethod
def buffer_sort_messages(buff):
lines = []
own_lines = W.hdata_pointer(W.hdata_get('buffer'), buff, 'own_lines')
if own_lines:
hdata_line = W.hdata_get('line')
hdata_line_data = W.hdata_get('line_data')
line = W.hdata_pointer(
W.hdata_get('lines'), own_lines, 'first_line')
while line:
data = W.hdata_pointer(hdata_line, line, 'data')
line_data = {}
if data:
date = W.hdata_time(hdata_line_data, data, 'date')
print_date = W.hdata_time(hdata_line_data, data,
'date_printed')
tags = tags_from_line_data(data)
prefix = W.hdata_string(hdata_line_data, data, 'prefix')
message = W.hdata_string(hdata_line_data, data, 'message')
highlight = W.hdata_char(hdata_line_data, data, "highlight")
line_data = {
'date': date,
'date_printed': print_date,
'tags_array': ','.join(tags),
'prefix': prefix,
'message': message,
'highlight': highlight
}
lines.append(line_data)
line = W.hdata_move(hdata_line, line, 1)
sorted_lines = sorted(lines, key=itemgetter('date'))
lines = []
# We need to convert the dates to a string for hdata_update(), this
# will reverse the list at the same time
while sorted_lines:
line = sorted_lines.pop()
new_line = {k: str(v) for k, v in line.items()}
lines.append(new_line)
MatrixBacklogEvent.update_buffer_lines(lines, own_lines)
@staticmethod
def update_buffer_lines(new_lines, own_lines):
hdata_line = W.hdata_get('line')
hdata_line_data = W.hdata_get('line_data')
line = W.hdata_pointer(W.hdata_get('lines'), own_lines, 'first_line')
while line:
data = W.hdata_pointer(hdata_line, line, 'data')
if data:
W.hdata_update(hdata_line_data, data, new_lines.pop())
line = W.hdata_move(hdata_line, line, 1)
@classmethod
def from_dict(cls, server, room_id, parsed_dict):
try:
end_token = sanitize_id(parsed_dict["end"])
if not parsed_dict["chunk"]:
return cls(server, room_id, end_token, [])
event_func = partial(MatrixBacklogEvent._room_event_from_dict,
room_id)
message_events = list(
filter(lambda event: event["type"] == "m.room.message",
parsed_dict["chunk"]))
events = [event_func(m) for m in message_events]
return cls(server, room_id, end_token, events)
except (KeyError, ValueError, TypeError):
return MatrixErrorEvent.from_dict(server, "Error fetching backlog",
False, parsed_dict)
def execute(self):
room = self.server.rooms[self.room_id]
buff = self.server.buffers[self.room_id]
tags = tags_for_message("backlog")
for event in self.events:
event.execute(self.server, room, buff, list(tags))
room.prev_batch = self.end_token
MatrixBacklogEvent.buffer_sort_messages(buff)
room.backlog_pending = False
W.bar_item_update("buffer_modes")
class MatrixSyncEvent(MatrixEvent):
def __init__(self, server, next_batch, room_infos, invited_infos):
self.next_batch = next_batch
self.joined_room_infos = room_infos
self.invited_room_infos = invited_infos
MatrixEvent.__init__(self, server)
@staticmethod
def _infos_from_dict(parsed_dict):
join_infos = []
invite_infos = []
for room_id, room_dict in parsed_dict['join'].items():
if not room_id:
continue
join_infos.append(RoomInfo.from_dict(room_id, room_dict))
return (join_infos, invite_infos)
@classmethod
def from_dict(cls, server, parsed_dict):
try:
next_batch = sanitize_id(parsed_dict["next_batch"])
room_info_dict = parsed_dict["rooms"]
join_infos, invite_infos = MatrixSyncEvent._infos_from_dict(
room_info_dict)
return cls(server, next_batch, join_infos, invite_infos)
except (KeyError, ValueError, TypeError):
return MatrixErrorEvent.from_dict(server, "Error syncing", False,
parsed_dict)
def _queue_joined_info(self):
server = self.server
while self.joined_room_infos:
info = self.joined_room_infos.pop()
if info.room_id not in server.buffers:
matrix_create_room_buffer(server, info.room_id)
room = server.rooms[info.room_id]
if not room.prev_batch:
room.prev_batch = info.prev_batch
server.event_queue.append(info)
def execute(self):
server = self.server
# we got the same batch again, nothing to do
if self.next_batch == server.next_batch:
server.sync()
return
self._queue_joined_info()
server.next_batch = self.next_batch
server.handle_events()

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
# Copyright © 2018, 2019 Damir Jelić <poljar@termina.org.uk>
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
@ -17,17 +17,32 @@
from __future__ import unicode_literals
import sys
from typing import 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
from matrix.utf import WeechatWrapper
from matrix.plugin_options import PluginOptions
try:
import weechat
W = weechat if sys.hexversion >= 0x3000000 else WeechatWrapper(weechat)
except ImportError:
import matrix._weechat as weechat
import matrix._weechat as weechat # type: ignore
W = weechat
OPTIONS = PluginOptions() # type: PluginOptions
SERVERS = dict() # type: Dict[str, MatrixServer]
CONFIG = None # type: weechat.config
CONFIG = None # type: Optional[MatrixConfig]
ENCRYPTION = True # type: bool
SCRIPT_NAME = "matrix" # type: str
MAX_EVENTS = 100
TYPING_NOTICE_TIMEOUT = 4000 # 4 seconds typing notice lifetime
LOGGER = Logger("weechat-matrix")
UPLOADS = OrderedDict() # type: Dict[str, Upload]

View file

@ -1,93 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
#
# 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 builtins import str
import json
from enum import Enum, unique
@unique
class RequestType(Enum):
GET = 0
POST = 1
PUT = 2
class HttpResponse:
def __init__(self, status, headers, body):
self.status = status # type: int
self.headers = headers # type: Dict[str, str]
self.body = body # type: bytes
# yapf: disable
class HttpRequest:
def __init__(
self,
request_type, # type: RequestType
host, # type: str
location, # type: str
data=None, # type: Dict[str, Any]
user_agent='weechat-matrix/{version}'.format(
version="0.1") # type: str
):
# type: (...) -> None
user_agent = 'User-Agent: {agent}'.format(agent=user_agent)
host_header = 'Host: {host}'.format(host=host)
keepalive = "Connection: keep-alive"
request_list = [] # type: List[str]
accept_header = 'Accept: */*' # type: str
end_separator = '\r\n' # type: str
payload = "" # type: str
# yapf: enable
if request_type == RequestType.GET:
get = 'GET {location} HTTP/1.1'.format(location=location)
request_list = [
get, host_header, user_agent, keepalive, accept_header,
end_separator
]
elif (request_type == RequestType.POST or
request_type == RequestType.PUT):
json_data = json.dumps(data, separators=(',', ':'))
if request_type == RequestType.POST:
method = "POST"
else:
method = "PUT"
request_line = '{method} {location} HTTP/1.1'.format(
method=method, location=location)
type_header = 'Content-Type: application/x-www-form-urlencoded'
length_header = 'Content-Length: {length}'.format(
length=len(json_data))
request_list = [
request_line, host_header, user_agent, keepalive,
accept_header, length_header, type_header, end_separator
]
payload = json_data
request = '\r\n'.join(request_list)
self.request = request
self.payload = payload

View file

@ -1,62 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
#
# 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 collections import namedtuple
from enum import Enum, unique
@unique
class RedactType(Enum):
STRIKETHROUGH = 0
NOTICE = 1
DELETE = 2
@unique
class ServerBufferType(Enum):
MERGE_CORE = 0
MERGE = 1
INDEPENDENT = 2
@unique
class DebugType(Enum):
MESSAGING = 0
NETWORK = 1
TIMING = 2
Option = namedtuple(
'Option',
['name', 'type', 'string_values', 'min', 'max', 'value', 'description'])
class PluginOptions:
def __init__(self):
self.redaction_type = RedactType.STRIKETHROUGH # type: RedactType
self.look_server_buf = ServerBufferType.MERGE_CORE # type: ServerBufferType
self.sync_limit = 30 # type: int
self.backlog_limit = 10 # type: int
self.enable_backlog = True # type: bool
self.page_up_hook = None # type: weechat.hook
self.redaction_comp_len = 50 # type: int
self.options = dict() # type: Dict[str, weechat.config_option]
self.debug = [] # type: List[DebugType]

View file

@ -1,922 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
#
# 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 builtins import str
from pprint import pformat
from collections import namedtuple, deque
from datetime import datetime
from matrix.globals import W, OPTIONS
from matrix.plugin_options import RedactType
from matrix.colors import Formatted
from matrix.utils import (
strip_matrix_server, color_for_tags, server_ts_to_weechat,
sender_to_nick_and_color, add_event_tags, sanitize_id, sanitize_ts,
sanitize_string, sanitize_text, shorten_sender, add_user_to_nicklist,
get_prefix_for_level, sanitize_power_level, string_strikethrough,
line_pointer_and_tags_from_event, sender_to_prefix_and_color)
PowerLevel = namedtuple('PowerLevel', ['user', 'level'])
class MatrixRoom:
def __init__(self, room_id):
# type: (str) -> None
# yapf: disable
self.room_id = room_id # type: str
self.canonical_alias = None # type: str
self.name = None # type: str
self.topic = "" # type: str
self.topic_author = "" # type: str
self.topic_date = None # type: datetime.datetime
self.prev_batch = "" # type: str
self.users = dict() # type: Dict[str, MatrixUser]
self.encrypted = False # type: bool
self.backlog_pending = False # type: bool
# yapf: enable
def display_name(self, own_user_id):
"""
Calculate display name for a room.
Prefer returning the room name if it exists, falling back to
a group-style name if not.
Mostly follows:
https://matrix.org/docs/spec/client_server/r0.3.0.html#id268
An exception is that we prepend '#' before the room name to make it
visually distinct from private messages and unnamed groups of users
("direct chats") in weechat's buffer list.
"""
if self.is_named():
return self.named_room_name()
else:
return self.group_name(own_user_id)
def named_room_name(self):
"""
Returns the name of the room, if it's a named room. Otherwise return
None.
"""
if self.name:
return "#" + self.name
elif self.canonical_alias:
return self.canonical_alias
else:
return None
def group_name(self, own_user_id):
"""
Returns the group-style name of the room, i.e. a name based on the room
members.
"""
# Sort user display names, excluding our own user and using the
# mxid as the sorting key.
#
# TODO: Hook the user display name disambiguation algorithm here.
# Currently, we use the user display names as is, which may not be
# unique.
users = [user.name for mxid, user
in sorted(self.users.items(), key=lambda t: t[0])
if mxid != own_user_id]
num_users = len(users)
if num_users == 1:
return users[0]
elif num_users == 2:
return " and ".join(users)
elif num_users >= 3:
return "{first_user} and {num} others".format(
first_user=users[0],
num=num_users-1)
else:
return "Empty room?"
def machine_name(self):
"""
Calculate an unambiguous, unique machine name for a room.
Either use the more human-friendly canonical alias, if it exists, or
the internal room ID if not.
"""
if self.canonical_alias:
return self.canonical_alias
else:
return self.room_id
def is_named(self):
"""
Is this a named room?
A named room is a room with either the name or a canonical alias set.
"""
return self.canonical_alias or self.name
def is_group(self):
"""
Is this an ad hoc group of users?
A group is an unnamed room with no canonical alias.
"""
return not self.is_named()
class MatrixUser:
def __init__(self, name, display_name):
# yapf: disable
self.name = name # type: str
self.display_name = display_name # type: str
self.power_level = 0 # type: int
self.nick_color = "" # type: str
self.prefix = "" # type: str
# yapf: enable
def matrix_create_room_buffer(server, room_id):
# type: (MatrixServer, str) -> None
buf = W.buffer_new(room_id, "room_input_cb", server.name, "room_close_cb",
server.name)
W.buffer_set(buf, "localvar_set_type", 'channel')
W.buffer_set(buf, "type", 'formatted')
W.buffer_set(buf, "localvar_set_channel", room_id)
W.buffer_set(buf, "localvar_set_nick", server.user)
W.buffer_set(buf, "localvar_set_server", server.name)
short_name = strip_matrix_server(room_id)
W.buffer_set(buf, "short_name", short_name)
W.nicklist_add_group(buf, '', "000|o", "weechat.color.nicklist_group", 1)
W.nicklist_add_group(buf, '', "001|h", "weechat.color.nicklist_group", 1)
W.nicklist_add_group(buf, '', "002|v", "weechat.color.nicklist_group", 1)
W.nicklist_add_group(buf, '', "999|...", "weechat.color.nicklist_group", 1)
W.buffer_set(buf, "nicklist", "1")
W.buffer_set(buf, "nicklist_display_groups", "0")
# TODO make this configurable
W.buffer_set(buf, "highlight_tags_restrict", "matrix_message")
server.buffers[room_id] = buf
server.rooms[room_id] = MatrixRoom(room_id)
class RoomInfo():
def __init__(self, room_id, prev_batch, events):
# type: (str, str, List[Any], List[Any]) -> None
self.room_id = room_id
self.prev_batch = prev_batch
self.events = deque(events)
@staticmethod
def _message_from_event(event):
# The transaction id will only be present for events that are send out
# from this client, since we print out our own messages as soon as we
# get a receive confirmation from the server we don't care about our
# own messages in a sync event. More info under:
# https://github.com/matrix-org/matrix-doc/blob/master/api/client-server/definitions/event.yaml#L53
if "transaction_id" in event["unsigned"]:
return None
if "redacted_by" in event["unsigned"]:
return RoomRedactedMessageEvent.from_dict(event)
return RoomMessageEvent.from_dict(event)
@staticmethod
def _membership_from_dict(event_dict):
if (event_dict["content"]["membership"] not in [
"invite", "join", "knock", "leave", "ban"
]):
raise ValueError
if event_dict["content"]["membership"] == "join":
event = RoomMemberJoin.from_dict(event_dict)
try:
message = RoomMembershipMessage(
event.event_id, event.sender, event.timestamp,
"has joined", "join")
return event, message
except AttributeError:
return event, None
elif event_dict["content"]["membership"] == "leave":
event = RoomMemberLeave.from_dict(event_dict)
try:
msg = ("has left" if event.sender == event.leaving_user else
"has been kicked")
message = RoomMembershipMessage(
event.event_id, event.leaving_user, event.timestamp, msg, "quit")
return event, message
except AttributeError:
return event, None
return None, None
@staticmethod
def parse_event(event_dict):
# type: (Dict[Any, Any]) -> (RoomEvent, RoomEvent)
state_event = None
message_event = None
if event_dict["type"] == "m.room.message":
message_event = RoomInfo._message_from_event(event_dict)
elif event_dict["type"] == "m.room.member":
state_event, message_event = (
RoomInfo._membership_from_dict(event_dict))
elif event_dict["type"] == "m.room.power_levels":
state_event = RoomPowerLevels.from_dict(event_dict)
elif event_dict["type"] == "m.room.topic":
state_event = RoomTopicEvent.from_dict(event_dict)
message_event = RoomTopiceMessage(
state_event.event_id,
state_event.sender,
state_event.timestamp,
state_event.topic)
elif event_dict["type"] == "m.room.redaction":
message_event = RoomRedactionEvent.from_dict(event_dict)
elif event_dict["type"] == "m.room.name":
state_event = RoomNameEvent.from_dict(event_dict)
elif event_dict["type"] == "m.room.canonical_alias":
state_event = RoomAliasEvent.from_dict(event_dict)
elif event_dict["type"] == "m.room.encryption":
state_event = RoomEncryptionEvent.from_dict(event_dict)
return state_event, message_event
@staticmethod
def _parse_events(parsed_dict, messages=True, state=True):
state_events = []
message_events = []
if not messages and not state:
return []
try:
for event in parsed_dict:
m_event, s_event = RoomInfo.parse_event(event)
state_events.append(m_event)
message_events.append(s_event)
except (ValueError, TypeError, KeyError) as error:
message = ("{prefix}matrix: Error parsing "
"room event of type {type}: {error}").format(
prefix=W.prefix("error"),
type=event["type"],
error=pformat(error))
W.prnt("", message)
raise
events = []
if state:
events = events + state_events
if messages:
events = events + message_events
return events
@classmethod
def from_dict(cls, room_id, parsed_dict):
prev_batch = sanitize_id(parsed_dict['timeline']['prev_batch'])
state_dict = parsed_dict['state']['events']
timeline_dict = parsed_dict['timeline']['events']
state_events = RoomInfo._parse_events(state_dict, messages=False)
timeline_events = RoomInfo._parse_events(timeline_dict)
events = state_events + timeline_events
return cls(room_id, prev_batch, list(filter(None, events)))
class RoomEvent():
def __init__(self, event_id, sender, timestamp):
self.event_id = event_id
self.sender = sender
self.timestamp = timestamp
class RoomRedactedMessageEvent(RoomEvent):
def __init__(self, event_id, sender, timestamp, censor, reason=None):
self.censor = censor
self.reason = reason
RoomEvent.__init__(self, event_id, sender, timestamp)
@classmethod
def from_dict(cls, event):
event_id = sanitize_id(event["event_id"])
sender = sanitize_id(event["sender"])
timestamp = sanitize_ts(event["origin_server_ts"])
censor = sanitize_id(event['unsigned']['redacted_because']['sender'])
reason = None
if 'reason' in event['unsigned']['redacted_because']['content']:
reason = sanitize_text(
event['unsigned']['redacted_because']['content']['reason'])
return cls(event_id, sender, timestamp, censor, reason)
def execute(self, server, room, buff, tags):
nick, color_name = sender_to_nick_and_color(room, self.sender)
color = color_for_tags(color_name)
date = server_ts_to_weechat(self.timestamp)
event_tags = add_event_tags(self.event_id, nick, color, tags)
reason = (", reason: \"{reason}\"".format(reason=self.reason)
if self.reason else "")
censor, _ = sender_to_nick_and_color(room, self.censor)
msg = ("{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)
event_tags.append("matrix_redacted")
tags_string = ",".join(event_tags)
data = "{author}\t{msg}".format(author=nick, msg=msg)
W.prnt_date_tags(buff, date, tags_string, data)
class RoomMessageEvent(RoomEvent):
@classmethod
def from_dict(cls, event):
if event['content']['msgtype'] == 'm.text':
return RoomMessageText.from_dict(event)
elif event['content']['msgtype'] == 'm.image':
return RoomMessageMedia.from_dict(event)
elif event['content']['msgtype'] == 'm.audio':
return RoomMessageMedia.from_dict(event)
elif event['content']['msgtype'] == 'm.file':
return RoomMessageMedia.from_dict(event)
elif event['content']['msgtype'] == 'm.video':
return RoomMessageMedia.from_dict(event)
elif event['content']['msgtype'] == 'm.emote':
return RoomMessageEmote.from_dict(event)
elif event['content']['msgtype'] == 'm.notice':
return RoomMessageNotice.from_dict(event)
return RoomMessageUnknown.from_dict(event)
def _print_message(self, message, room, buff, tags):
nick, color_name = sender_to_nick_and_color(room, self.sender)
color = color_for_tags(color_name)
event_tags = add_event_tags(self.event_id, nick, color, tags)
tags_string = ",".join(event_tags)
prefix, prefix_color = sender_to_prefix_and_color(room, self.sender)
prefix_string = ("" if not prefix else "{}{}{}".format(
W.color(prefix_color), prefix, W.color("reset")))
data = "{prefix}{color}{author}{ncolor}\t{msg}".format(
prefix=prefix_string,
color=W.color(color_name),
author=nick,
ncolor=W.color("reset"),
msg=message)
date = server_ts_to_weechat(self.timestamp)
W.prnt_date_tags(buff, date, tags_string, data)
class RoomMessageSimple(RoomMessageEvent):
def __init__(self, event_id, sender, timestamp, message, message_type):
self.message = message
self.message_type = message_type
RoomEvent.__init__(self, event_id, sender, timestamp)
@classmethod
def from_dict(cls, event):
event_id = sanitize_id(event["event_id"])
sender = sanitize_id(event["sender"])
timestamp = sanitize_ts(event["origin_server_ts"])
message = sanitize_text(event["content"]["body"])
message_type = sanitize_text(event["content"]["msgtype"])
return cls(event_id, sender, timestamp, message, message_type)
class RoomMessageUnknown(RoomMessageSimple):
def execute(self, server, room, buff, tags):
msg = ("Unknown message of type {t}, body: {body}").format(
t=self.message_type, body=self.message)
self._print_message(msg, room, buff, tags)
class RoomMessageText(RoomMessageEvent):
def __init__(self, event_id, sender, timestamp, message, formatted_message=None):
self.message = message
self.formatted_message = formatted_message
RoomEvent.__init__(self, event_id, sender, timestamp)
@classmethod
def from_dict(cls, event):
event_id = sanitize_id(event["event_id"])
sender = sanitize_id(event["sender"])
timestamp = sanitize_ts(event["origin_server_ts"])
msg = ""
formatted_msg = None
msg = sanitize_text(event['content']['body'])
if ('format' in event['content'] and
'formatted_body' in event['content']):
if event['content']['format'] == "org.matrix.custom.html":
formatted_msg = Formatted.from_html(
sanitize_text(event['content']['formatted_body']))
return cls(event_id, sender, timestamp, msg, formatted_msg)
def execute(self, server, room, buff, tags):
msg = (self.formatted_message.to_weechat()
if self.formatted_message else self.message)
self._print_message(msg, room, buff, tags)
class RoomMessageEmote(RoomMessageSimple):
def execute(self, server, room, buff, tags):
nick, color_name = sender_to_nick_and_color(room, self.sender)
color = color_for_tags(color_name)
event_tags = add_event_tags(self.event_id, nick, color, tags)
event_tags.append("matrix_action")
tags_string = ",".join(event_tags)
data = "{prefix}{nick_color}{author}{ncolor} {msg}".format(
prefix=W.prefix("action"),
nick_color=W.color(color_name),
author=nick,
ncolor=W.color("reset"),
msg=self.message)
date = server_ts_to_weechat(self.timestamp)
W.prnt_date_tags(buff, date, tags_string, data)
class RoomMessageNotice(RoomMessageText):
def execute(self, server, room, buff, tags):
msg = "{color}{message}{ncolor}".format(
color=W.color("irc.color.notice"),
message=self.message,
ncolor=W.color("reset"))
self._print_message(msg, room, buff, tags)
class RoomMessageMedia(RoomMessageEvent):
def __init__(self, event_id, sender, timestamp, url, description):
self.url = url
self.description = description
RoomEvent.__init__(self, event_id, sender, timestamp)
@classmethod
def from_dict(cls, event):
event_id = sanitize_id(event["event_id"])
sender = sanitize_id(event["sender"])
timestamp = sanitize_ts(event["origin_server_ts"])
mxc_url = sanitize_text(event['content']['url'])
description = sanitize_text(event["content"]["body"])
return cls(event_id, sender, timestamp, mxc_url, description)
def execute(self, server, room, buff, tags):
http_url = server.client.mxc_to_http(self.url)
url = http_url if http_url else self.url
description = (" ({})".format(self.description)
if self.description else "")
msg = "{url}{desc}".format(url=url, desc=description)
self._print_message(msg, room, buff, tags)
class RoomMembershipMessage(RoomEvent):
def __init__(self, event_id, sender, timestamp, message, prefix):
self.message = message
self.prefix = prefix
RoomEvent.__init__(self, event_id, sender, timestamp)
def execute(self, server, room, buff, tags):
nick, color_name = sender_to_nick_and_color(room, self.sender)
event_tags = add_event_tags(self.event_id, nick, None, [])
# TODO this should be configurable
action_color = "red" if self.prefix == "quit" else "green"
data = ("{prefix}{color}{author}{ncolor} "
"{del_color}({host_color}{user_id}{del_color})"
"{action_color} {message} "
"{channel_color}{room}{ncolor}").format(
prefix=W.prefix(self.prefix),
color=W.color(color_name),
author=nick,
ncolor=W.color("reset"),
del_color=W.color("chat_delimiters"),
host_color=W.color("chat_host"),
user_id=self.sender,
action_color=W.color(action_color),
message=self.message,
channel_color=W.color("chat_channel"),
room="" if room.is_group() else room.named_room_name())
date = server_ts_to_weechat(self.timestamp)
tags_string = ",".join(event_tags)
W.prnt_date_tags(buff, date, tags_string, data)
class RoomMemberJoin(RoomEvent):
def __init__(self, event_id, sender, timestamp, display_name):
self.display_name = display_name
RoomEvent.__init__(self, event_id, sender, timestamp)
@classmethod
def from_dict(cls, event_dict):
event_id = sanitize_id(event_dict["event_id"])
sender = sanitize_id(event_dict["sender"])
timestamp = sanitize_ts(event_dict["origin_server_ts"])
display_name = None
if event_dict["content"]:
if "display_name" in event_dict["content"]:
display_name = sanitize_text(
event_dict["content"]["displayname"])
return cls(event_id, sender, timestamp, display_name)
def execute(self, server, room, buff, tags):
short_name = shorten_sender(self.sender)
if self.sender in room.users:
user = room.users[self.sender]
if self.display_name:
user.display_name = self.display_name
else:
user = MatrixUser(short_name, self.display_name)
if not user.nick_color:
if self.sender == server.user_id:
highlight_words = [self.sender, user.name]
if self.display_name:
highlight_words.append(self.display_name)
user.nick_color = "weechat.color.chat_nick_self"
W.buffer_set(buff, "highlight_words", ",".join(highlight_words))
else:
user.nick_color = W.info_get("nick_color_name", user.name)
room.users[self.sender] = user
nick_pointer = W.nicklist_search_nick(buff, "", self.sender)
if not nick_pointer:
add_user_to_nicklist(buff, self.sender, user)
# calculate room display name and set it as the buffer list name
room_name = room.display_name(server.user_id)
W.buffer_set(buff, "short_name", room_name)
class RoomMemberLeave(RoomEvent):
def __init__(self, event_id, sender, leaving_user, timestamp):
self.leaving_user = leaving_user
RoomEvent.__init__(self, event_id, sender, timestamp)
@classmethod
def from_dict(cls, event_dict):
event_id = sanitize_id(event_dict["event_id"])
sender = sanitize_id(event_dict["sender"])
leaving_user = sanitize_id(event_dict["state_key"])
timestamp = sanitize_ts(event_dict["origin_server_ts"])
return cls(event_id, sender, leaving_user, timestamp)
def execute(self, server, room, buff, tags):
if self.leaving_user in room.users:
nick_pointer = W.nicklist_search_nick(buff, "", self.leaving_user)
if nick_pointer:
W.nicklist_remove_nick(buff, nick_pointer)
del room.users[self.leaving_user]
# calculate room display name and set it as the buffer list name
room_name = room.display_name(server.user_id)
W.buffer_set(buff, "short_name", room_name)
class RoomPowerLevels(RoomEvent):
def __init__(self, event_id, sender, timestamp, power_levels):
self.power_levels = power_levels
RoomEvent.__init__(self, event_id, sender, timestamp)
@classmethod
def from_dict(cls, event_dict):
event_id = sanitize_id(event_dict["event_id"])
sender = sanitize_id(event_dict["sender"])
timestamp = sanitize_ts(event_dict["origin_server_ts"])
power_levels = []
for user, level in event_dict["content"]["users"].items():
power_levels.append(
PowerLevel(sanitize_id(user), sanitize_power_level(level)))
return cls(event_id, sender, timestamp, power_levels)
def _set_power_level(self, room, buff, power_level):
user_id = power_level.user
level = power_level.level
if user_id not in room.users:
return
user = room.users[user_id]
user.power_level = level
user.prefix = get_prefix_for_level(level)
nick_pointer = W.nicklist_search_nick(buff, "", user_id)
if nick_pointer:
W.nicklist_remove_nick(buff, nick_pointer)
add_user_to_nicklist(buff, user_id, user)
def execute(self, server, room, buff, tags):
for level in self.power_levels:
self._set_power_level(room, buff, level)
class RoomTopiceMessage(RoomEvent):
def __init__(self, event_id, sender, timestamp, topic):
self.topic = topic
RoomEvent.__init__(self, event_id, sender, timestamp)
def execute(self, server, room, buff, tags):
topic = self.topic
nick, color_name = sender_to_nick_and_color(room, self.sender)
author = ("{nick_color}{user}{ncolor}").format(
nick_color=W.color(color_name), user=nick, ncolor=W.color("reset"))
# TODO print old topic if configured so
if room.is_named():
message = ("{prefix}{nick} has changed "
"the topic for {chan_color}{room}{ncolor} "
"to \"{topic}\"").format(
prefix=W.prefix("network"),
nick=author,
chan_color=W.color("chat_channel"),
ncolor=W.color("reset"),
room=room.named_room_name(),
topic=topic)
else:
message = ('{prefix}{nick} has changed the topic to '
'"{topic}"').format(
prefix=W.prefix("network"),
nick=author,
topic=topic)
tags = ["matrix_topic", "log3", "matrix_id_{}".format(self.event_id)]
date = server_ts_to_weechat(self.timestamp)
W.prnt_date_tags(buff, date, ",".join(tags), message)
class RoomTopicEvent(RoomEvent):
def __init__(self, event_id, sender, timestamp, topic):
self.topic = topic
RoomEvent.__init__(self, event_id, sender, timestamp)
@classmethod
def from_dict(cls, event_dict):
event_id = sanitize_id(event_dict["event_id"])
sender = sanitize_id(event_dict["sender"])
timestamp = sanitize_ts(event_dict["origin_server_ts"])
topic = sanitize_text(event_dict["content"]["topic"])
return cls(event_id, sender, timestamp, topic)
def execute(self, server, room, buff, tags):
topic = self.topic
W.buffer_set(buff, "title", topic)
room.topic = topic
room.topic_author = self.sender
room.topic_date = datetime.fromtimestamp(
server_ts_to_weechat(self.timestamp))
class RoomRedactionEvent(RoomEvent):
def __init__(self, event_id, sender, timestamp, redaction_id, reason=None):
self.redaction_id = redaction_id
self.reason = reason
RoomEvent.__init__(self, event_id, sender, timestamp)
@classmethod
def from_dict(cls, event_dict):
event_id = sanitize_id(event_dict["event_id"])
sender = sanitize_id(event_dict["sender"])
timestamp = sanitize_ts(event_dict["origin_server_ts"])
redaction_id = sanitize_id(event_dict["redacts"])
reason = (sanitize_text(event_dict["content"]["reason"])
if "reason" in event_dict["content"] else None)
return cls(event_id, sender, timestamp, redaction_id, reason)
@staticmethod
def already_redacted(tags):
if "matrix_redacted" in tags:
return True
return False
def _redact_line(self, data_pointer, tags, room, buff):
hdata_line_data = W.hdata_get('line_data')
message = W.hdata_string(hdata_line_data, data_pointer, 'message')
censor, _ = sender_to_nick_and_color(room, self.sender)
reason = ("" if not self.reason else
", reason: \"{reason}\"".format(reason=self.reason))
redaction_msg = ("{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)
new_message = ""
if OPTIONS.redaction_type == RedactType.STRIKETHROUGH:
plaintext_msg = W.string_remove_color(message, '')
new_message = string_strikethrough(plaintext_msg)
elif OPTIONS.redaction_type == RedactType.NOTICE:
new_message = message
elif OPTIONS.redaction_type == RedactType.DELETE:
pass
message = " ".join(s for s in [new_message, redaction_msg] if s)
tags.append("matrix_redacted")
new_data = {'tags_array': ','.join(tags), 'message': message}
W.hdata_update(hdata_line_data, data_pointer, new_data)
def execute(self, server, room, buff, tags):
data_pointer, tags = line_pointer_and_tags_from_event(
buff, self.redaction_id)
if not data_pointer:
return
if RoomRedactionEvent.already_redacted(tags):
return
self._redact_line(data_pointer, tags, room, buff)
class RoomNameEvent(RoomEvent):
def __init__(self, event_id, sender, timestamp, name):
self.name = name
RoomEvent.__init__(self, event_id, sender, timestamp)
@classmethod
def from_dict(cls, event_dict):
event_id = sanitize_id(event_dict["event_id"])
sender = sanitize_id(event_dict["sender"])
timestamp = sanitize_ts(event_dict["origin_server_ts"])
name = sanitize_string(event_dict['content']['name'])
return cls(event_id, sender, timestamp, name)
def execute(self, server, room, buff, tags):
if not self.name:
return
room.name = self.name
W.buffer_set(buff, "name", self.name)
W.buffer_set(buff, "localvar_set_channel", self.name)
# calculate room display name and set it as the buffer list name
room_name = room.display_name(server.user_id)
W.buffer_set(buff, "short_name", room_name)
class RoomAliasEvent(RoomEvent):
def __init__(self, event_id, sender, timestamp, canonical_alias):
self.canonical_alias = canonical_alias
RoomEvent.__init__(self, event_id, sender, timestamp)
@classmethod
def from_dict(cls, event_dict):
event_id = sanitize_id(event_dict["event_id"])
sender = sanitize_id(event_dict["sender"])
timestamp = sanitize_ts(event_dict["origin_server_ts"])
canonical_alias = sanitize_id(event_dict["content"]["alias"])
return cls(event_id, sender, timestamp, canonical_alias)
def execute(self, server, room, buff, tags):
if not self.canonical_alias:
return
# TODO: What should we do with this?
# W.buffer_set(buff, "name", self.name)
# W.buffer_set(buff, "localvar_set_channel", self.name)
# calculate room display name and set it as the buffer list name
room.canonical_alias = self.canonical_alias
room_name = room.display_name(server.user_id)
W.buffer_set(buff, "short_name", room_name)
class RoomEncryptionEvent(RoomEvent):
@classmethod
def from_dict(cls, event_dict):
event_id = sanitize_id(event_dict["event_id"])
sender = sanitize_id(event_dict["sender"])
timestamp = sanitize_ts(event_dict["origin_server_ts"])
return cls(event_id, sender, timestamp)
def execute(self, server, room, buff, tags):
room.encrypted = True
message = ("{prefix}This room is encrypted, encryption is "
"currently unsuported. Message sending is disabled for "
"this room.").format(prefix=W.prefix("error"))
W.prnt(buff, message)

File diff suppressed because it is too large Load diff

340
matrix/uploads.py Normal file
View file

@ -0,0 +1,340 @@
# -*- coding: utf-8 -*-
# Copyright © 2018, 2019 Damir Jelić <poljar@termina.org.uk>
#
# 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 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 matrix import globals as G
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)
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
@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"]
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

View file

@ -28,9 +28,13 @@ import sys
# pylint: disable=redefined-builtin
from builtins import bytes, str
from collections import Mapping, Iterable
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:
@ -39,13 +43,11 @@ from functools import wraps
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
@ -69,7 +71,8 @@ class WeechatWrapper(object):
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)
buffer, date, tags, message
)
def utf8_decode(function):
@ -92,7 +95,7 @@ def utf8_decode(function):
def decode_from_utf8(data):
if isinstance(data, bytes):
return data.decode('utf-8')
return data.decode("utf-8")
if isinstance(data, str):
return data
elif isinstance(data, Mapping):
@ -104,7 +107,7 @@ def decode_from_utf8(data):
def encode_to_utf8(data):
if isinstance(data, str):
return data.encode('utf-8')
return data.encode("utf-8")
if isinstance(data, bytes):
return data
elif isinstance(data, Mapping):

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
# Copyright © 2018, 2019 Damir Jelić <poljar@termina.org.uk>
# Copyright © 2018, 2019 Denis Kasak <dkasak@termina.org.uk>
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
@ -14,15 +15,15 @@
# 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 builtins import str
from __future__ import unicode_literals, division
import time
import math
from typing import Any, Dict, List
from matrix.globals import W, SERVERS, OPTIONS
from .globals import W
from matrix.plugin_options import ServerBufferType
if False:
from .server import MatrixServer
def key_from_value(dictionary, value):
@ -30,11 +31,6 @@ def key_from_value(dictionary, value):
return list(dictionary.keys())[list(dictionary.values()).index(value)]
def prnt_debug(debug_type, server, message):
if debug_type in OPTIONS.debug:
W.prnt(server.server_buffer, message)
def server_buffer_prnt(server, string):
# type: (MatrixServer, str) -> None
assert server.server_buffer
@ -44,13 +40,15 @@ def server_buffer_prnt(server, string):
def tags_from_line_data(line_data):
# type: (weechat.hdata) -> List[str]
# type: (str) -> List[str]
tags_count = W.hdata_get_var_array_size(
W.hdata_get('line_data'), line_data, 'tags_array')
W.hdata_get("line_data"), line_data, "tags_array"
)
tags = [
W.hdata_string(
W.hdata_get('line_data'), line_data, '%d|tags_array' % i)
W.hdata_get("line_data"), line_data, "%d|tags_array" % i
)
for i in range(tags_count)
]
@ -59,38 +57,21 @@ def tags_from_line_data(line_data):
def create_server_buffer(server):
# type: (MatrixServer) -> None
server.server_buffer = W.buffer_new(server.name, "server_buffer_cb",
server.name, "", "")
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, "localvar_set_type", 'server')
W.buffer_set(server.server_buffer, "localvar_set_nick", server.user)
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(server.server_buffer)
def server_buffer_merge(buffer):
if OPTIONS.look_server_buf == ServerBufferType.MERGE_CORE:
num = W.buffer_get_integer(W.buffer_search_main(), "number")
W.buffer_unmerge(buffer, num + 1)
W.buffer_merge(buffer, W.buffer_search_main())
elif OPTIONS.look_server_buf == 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(buffer, num + 1)
if buffer is not first:
W.buffer_merge(buffer, first)
else:
num = W.buffer_get_integer(W.buffer_search_main(), "number")
W.buffer_unmerge(buffer, num + 1)
server.buffer_merge()
def server_buffer_set_title(server):
@ -101,18 +82,12 @@ def server_buffer_set_title(server):
ip_string = ""
title = ("Matrix: {address}:{port}{ip}").format(
address=server.address, port=server.port, ip=ip_string)
address=server.config.address, port=server.config.port, ip=ip_string
)
W.buffer_set(server.server_buffer, "title", title)
def color_for_tags(color):
if color == "weechat.color.chat_nick_self":
option = W.config_get(color)
return W.config_string(option)
return color
def server_ts_to_weechat(timestamp):
# type: (float) -> int
date = int(timestamp / 1000)
@ -129,224 +104,65 @@ def shorten_sender(sender):
return strip_matrix_server(sender)[1:]
def sender_to_prefix_and_color(room, sender):
if sender in room.users:
user = room.users[sender]
prefix = user.prefix
prefix_color = get_prefix_color(prefix)
return prefix, prefix_color
return None, None
def sender_to_nick_and_color(room, sender):
nick = sender
nick_color_name = "default"
if sender in room.users:
user = room.users[sender]
nick = (user.display_name if user.display_name else user.name)
nick_color_name = user.nick_color
else:
nick = sender
nick_color_name = W.info_get("nick_color_name", nick)
return (nick, nick_color_name)
def tags_for_message(message_type):
default_tags = {
"message": ["matrix_message", "notify_message", "log1"],
"backlog":
["matrix_message", "notify_message", "no_log", "no_highlight"]
}
return default_tags[message_type]
def add_event_tags(event_id, nick, color=None, tags=[]):
tags.append("nick_{nick}".format(nick=nick))
if color:
tags.append("prefix_nick_{color}".format(color=color_for_tags(color)))
tags.append("matrix_id_{event_id}".format(event_id=event_id))
return tags
def sanitize_token(string):
# type: (str) -> str
string = sanitize_string(string)
if len(string) > 512:
raise ValueError
if string == "":
raise ValueError
return string
def sanitize_string(string):
# type: (str) -> str
if not isinstance(string, str):
raise TypeError
# string keys can have empty string values sometimes (e.g. room names that
# got deleted)
if string == "":
return None
remap = {
ord('\b'): None,
ord('\f'): None,
ord('\n'): None,
ord('\r'): None,
ord('\t'): None,
ord('\0'): None
}
return string.translate(remap)
def sanitize_id(string):
# type: (str) -> str
string = sanitize_string(string)
if len(string) > 128:
raise ValueError
if string == "":
raise ValueError
return string
def sanitize_int(number, minimum=None, maximum=None):
# type: (int, int, int) -> int
if not isinstance(number, int):
raise TypeError
if math.isnan(number):
raise ValueError
if math.isinf(number):
raise ValueError
if minimum:
if number < minimum:
raise ValueError
if maximum:
if number > maximum:
raise ValueError
return number
def sanitize_ts(timestamp):
# type: (int) -> int
return sanitize_int(timestamp, 0)
def sanitize_power_level(level):
# type: (int) -> int
return sanitize_int(level, 0, 100)
def sanitize_text(string):
# type: (str) -> str
if not isinstance(string, str):
raise TypeError
# yapf: disable
remap = {
ord('\b'): None,
ord('\f'): None,
ord('\r'): None,
ord('\0'): None
}
# yapf: enable
return string.translate(remap)
def add_user_to_nicklist(buf, user_id, user):
group_name = "999|..."
if user.power_level >= 100:
group_name = "000|o"
elif user.power_level >= 50:
group_name = "001|h"
elif user.power_level > 0:
group_name = "002|v"
group = W.nicklist_search_group(buf, "", group_name)
prefix = user.prefix if user.prefix else " "
# TODO make it configurable so we can use a display name or user_id here
W.nicklist_add_nick(buf, group, user_id, user.nick_color, prefix,
get_prefix_color(user.prefix), 1)
def get_prefix_for_level(level):
# type: (int) -> str
if level >= 100:
return "&"
elif level >= 50:
return "@"
elif level > 0:
return "+"
return ""
# TODO make this configurable
def get_prefix_color(prefix):
# type: (str) -> str
if prefix == "&":
return "lightgreen"
elif prefix == "@":
return "lightgreen"
elif prefix == "+":
return "yellow"
return ""
def string_strikethrough(string):
return "".join(["{}\u0336".format(c) for c in string])
def line_pointer_and_tags_from_event(buff, event_id):
# type: (str, str) -> str
own_lines = W.hdata_pointer(W.hdata_get('buffer'), buff, 'own_lines')
def string_color_and_reset(string, color):
"""Color string with color, then reset all attributes."""
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:
tags = tags_from_line_data(data_pointer)
message_id = event_id_from_tags(tags)
if event_id == message_id:
return data_pointer, tags
line_pointer = W.hdata_move(hdata_line, line_pointer, -1)
return None, []
lines = string.split('\n')
lines = ("{}{}{}".format(W.color(color), line, W.color("reset"))
for line in lines)
return "\n".join(lines)
def event_id_from_tags(tags):
# type: (List[str]) -> str
for tag in tags:
if tag.startswith("matrix_id"):
return tag[10:]
def string_color(string, color):
"""Color string with color, then reset the color attribute."""
return ""
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)

16
tests/buffer_test.py Normal file
View file

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from matrix.buffer import WeechatChannelBuffer
class TestClass(object):
def test_buffer(self):
b = WeechatChannelBuffer("test_buffer_name", "example.org", "alice")
assert b
def test_buffer_print(self):
b = WeechatChannelBuffer("test_buffer_name", "example.org", "alice")
b.message("alice", "hello world", 0, 0)
assert b

View file

@ -7,8 +7,11 @@ from collections import OrderedDict
from hypothesis import given
from hypothesis.strategies import sampled_from
from matrix.colors import (Formatted, FormattedString,
from matrix.colors import (G, Formatted, FormattedString,
color_html_to_weechat, color_weechat_to_html)
from matrix._weechat import MockConfig
G.CONFIG = MockConfig()
html_prism = ("<font color=maroon>T</font><font color=red>e</font><font "
"color=olive>s</font><font color=yellow>t</font>")
@ -42,3 +45,14 @@ def test_handle_strikethrough_first():
assert f1.to_weechat() == valid_result
assert f2.to_weechat() == valid_result
def test_normalize_spaces_in_inline_code():
"""Normalize spaces in inline code blocks.
Strips leading and trailing spaces and compress consecutive infix spaces.
"""
valid_result = '\x1b[0m* a *\x1b[00m'
formatted = Formatted.from_input_line('` * a * `')
assert formatted.to_weechat() == valid_result