Merge branch 'olm-command'
This commit is contained in:
commit
d56cf219cf
24 changed files with 6677 additions and 3580 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -3,3 +3,5 @@
|
|||
.hypothesis/
|
||||
.mypy_cache/
|
||||
.pytest_cache/
|
||||
.ropeproject
|
||||
.coverage
|
||||
|
|
|
@ -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
|
||||
|
|
5
Makefile
5
Makefile
|
@ -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
245
contrib/matrix_upload
Executable 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
380
main.py
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
512
matrix/api.py
512
matrix/api.py
|
@ -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)
|
|
@ -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
1688
matrix/buffer.py
Normal file
File diff suppressed because it is too large
Load diff
421
matrix/colors.py
421
matrix/colors.py
|
@ -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)
|
||||
|
|
1705
matrix/commands.py
1705
matrix/commands.py
File diff suppressed because it is too large
Load diff
|
@ -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",
|
||||
"",
|
||||
)
|
||||
|
|
823
matrix/config.py
823
matrix/config.py
|
@ -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()
|
||||
|
|
453
matrix/events.py
453
matrix/events.py
|
@ -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()
|
|
@ -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]
|
||||
|
|
|
@ -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
|
|
@ -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]
|
922
matrix/rooms.py
922
matrix/rooms.py
|
@ -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)
|
1593
matrix/server.py
1593
matrix/server.py
File diff suppressed because it is too large
Load diff
340
matrix/uploads.py
Normal file
340
matrix/uploads.py
Normal 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
|
|
@ -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):
|
||||
|
|
340
matrix/utils.py
340
matrix/utils.py
|
@ -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
16
tests/buffer_test.py
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue