weechat-matrix/main.py

377 lines
11 KiB
Python
Raw Normal View History

2017-12-30 14:03:03 +01:00
# -*- coding: utf-8 -*-
# Weechat Matrix Protocol Script
# 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.
2017-12-30 14:03:03 +01:00
from __future__ import unicode_literals
import socket
import ssl
import time
2018-01-19 12:43:12 +01:00
import pprint
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
# pylint: disable=redefined-builtin
from builtins import str
2018-01-06 17:12:54 +01:00
# pylint: disable=unused-import
from typing import (List, Set, Dict, Tuple, Text, Optional, AnyStr, Deque, Any)
2017-12-30 14:03:03 +01:00
from matrix import colors
from matrix.utf import utf8_decode
2018-01-26 14:38:46 +01:00
from matrix.http import HttpResponse
2018-01-29 13:07:49 +01:00
from matrix.api import MatrixMessage, MessageType, matrix_login
from matrix.socket import disconnect, send_or_queue, send, connect
from matrix.messages import handle_http_response
# 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,
2018-01-29 14:01:58 +01:00
matrix_command_buf_clear_cb
)
from matrix.server import (
MatrixServer,
2018-01-30 11:46:29 +01:00
create_default_server,
matrix_server_reconnect,
matrix_timer_cb,
matrix_config_server_read_cb,
matrix_config_server_write_cb,
matrix_config_server_change_cb,
)
2018-01-29 13:07:49 +01:00
from matrix.bar_items import (
init_bar_items,
matrix_bar_item_name,
2018-01-30 12:58:16 +01:00
matrix_bar_item_plugin,
matrix_bar_item_lag
2018-01-29 13:07:49 +01:00
)
2018-01-29 14:01:58 +01:00
from matrix.completion import (
init_completion,
matrix_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,
2018-01-29 13:07:49 +01:00
tags_from_line_data,
server_buffer_set_title
)
2018-01-29 17:47:47 +01:00
from matrix.plugin_options import (
2018-01-26 14:38:46 +01:00
DebugType,
RedactType,
2018-01-29 17:47:47 +01:00
ServerBufferType,
2018-01-26 14:38:46 +01:00
)
2018-01-29 17:47:47 +01:00
from matrix.config import (
matrix_config_init,
matrix_config_read,
matrix_config_free,
matrix_config_change_cb,
matrix_config_reload_cb
)
2018-01-29 17:47:47 +01:00
from matrix.globals import W, OPTIONS, CONFIG, SERVERS
2017-12-30 14:03:03 +01:00
2018-01-27 16:21:10 +01:00
WEECHAT_SCRIPT_NAME = "matrix" # type: str
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
WEECHAT_SCRIPT_LICENSE = "ISC" # type: str
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
def wrap_socket(server, file_descriptor):
# type: (MatrixServer, int) -> socket.socket
2018-01-06 17:12:54 +01:00
sock = None # type: socket.socket
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
temp_socket = socket.fromfd(
file_descriptor,
socket.AF_INET,
socket.SOCK_STREAM
)
2018-01-03 12:14:24 +01:00
# 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
2018-01-23 13:30:42 +01:00
# pylint: disable=protected-access,unidiomatic-typecheck
if type(temp_socket) == socket._socket.socket:
2018-01-07 15:46:18 +01:00
# pylint: disable=no-member
2018-01-06 17:12:54 +01:00
sock = socket._socketobject(_sock=temp_socket)
2017-12-30 14:03:03 +01:00
else:
2018-01-06 17:12:54 +01:00
sock = temp_socket
2017-12-30 14:03:03 +01:00
try:
2018-01-06 17:12:54 +01:00
ssl_socket = server.ssl_context.wrap_socket(
sock,
2018-01-03 12:14:24 +01:00
server_hostname=server.address) # type: ssl.SSLSocket
2018-01-06 17:12:54 +01:00
2018-01-03 12:14:24 +01:00
return ssl_socket
# TODO add finer grained error messages with the subclass exceptions
2018-01-06 17:12:54 +01:00
except ssl.SSLError as error:
server_buffer_prnt(server, str(error))
2018-01-03 12:14:24 +01:00
return None
2017-12-30 14:03:03 +01:00
@utf8_decode
2018-01-06 17:12:54 +01:00
def receive_cb(server_name, file_descriptor):
server = SERVERS[server_name]
2017-12-30 14:03:03 +01:00
while True:
try:
data = server.socket.recv(4096)
2017-12-30 14:03:03 +01:00
except ssl.SSLWantReadError:
break
2018-01-06 17:12:54 +01:00
except socket.error as error:
disconnect(server)
2017-12-30 14:03:03 +01:00
# Queue the failed message for resending
if server.receive_queue:
message = server.receive_queue.popleft()
server.send_queue.appendleft(message)
2017-12-30 14:03:03 +01:00
2018-01-25 12:33:50 +01:00
server_buffer_prnt(server, pprint.pformat(error))
2018-01-06 17:12:54 +01:00
return W.WEECHAT_RC_OK
2017-12-30 14:03:03 +01:00
if not data:
server_buffer_prnt(server, "No data while reading")
# Queue the failed message for resending
if server.receive_queue:
message = server.receive_queue.popleft()
server.send_queue.appendleft(message)
disconnect(server)
2017-12-30 14:03:03 +01:00
break
2018-01-06 17:12:54 +01:00
received = len(data) # type: int
parsed_bytes = server.http_parser.execute(data, received)
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
assert parsed_bytes == received
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
if server.http_parser.is_partial_body():
server.http_buffer.append(server.http_parser.recv_body())
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
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)
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
message = server.receive_queue.popleft()
2017-12-30 14:03:03 +01:00
message.response = HttpResponse(status, headers, body)
receive_time = time.time()
2018-01-30 12:58:16 +01:00
server.lag = (receive_time - message.send_time) * 1000
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.type,
s=status))
2017-12-30 14:03:03 +01:00
# Message done, reset the parser state.
2018-01-26 14:38:46 +01:00
server.reset_parser()
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
handle_http_response(server, message)
2017-12-30 14:03:03 +01:00
break
return W.WEECHAT_RC_OK
@utf8_decode
def connect_cb(data, status, gnutls_rc, sock, error, ip_address):
2018-01-07 16:04:17 +01:00
# pylint: disable=too-many-arguments,too-many-branches
2018-01-09 12:42:06 +01:00
status_value = int(status) # type: int
2018-01-03 12:14:24 +01:00
server = SERVERS[data]
2017-12-30 14:03:03 +01:00
if status_value == W.WEECHAT_HOOK_CONNECT_OK:
2018-01-06 17:12:54 +01:00
file_descriptor = int(sock) # type: int
sock = wrap_socket(server, file_descriptor)
if sock:
server.socket = sock
hook = W.hook_fd(
server.socket.fileno(),
1, 0, 0,
"receive_cb",
server.name
)
2017-12-30 14:03:03 +01:00
2018-01-27 16:21:10 +01:00
server.fd_hook = hook
server.connected = True
server.connecting = False
2018-01-07 15:46:18 +01:00
server.reconnect_count = 0
2018-01-11 16:21:14 +01:00
server.numeric_address = ip_address
2018-01-03 12:14:24 +01:00
2018-01-11 16:21:14 +01:00
server_buffer_set_title(server)
server_buffer_prnt(server, "Connected")
2018-01-03 12:14:24 +01:00
if not server.access_token:
matrix_login(server)
2017-12-30 14:03:03 +01:00
else:
2018-01-30 11:46:29 +01:00
matrix_server_reconnect(server)
return W.WEECHAT_RC_OK
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_ADDRESS_NOT_FOUND:
W.prnt(
server.server_buffer,
'{address} not found'.format(address=ip_address)
)
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_IP_ADDRESS_NOT_FOUND:
W.prnt(server.server_buffer, 'IP address not found')
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_CONNECTION_REFUSED:
W.prnt(server.server_buffer, 'Connection refused')
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_PROXY_ERROR:
W.prnt(
server.server_buffer,
'Proxy fails to establish connection to server'
)
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_LOCAL_HOSTNAME_ERROR:
W.prnt(server.server_buffer, 'Unable to set local hostname')
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_INIT_ERROR:
W.prnt(server.server_buffer, 'TLS init error')
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_HANDSHAKE_ERROR:
W.prnt(server.server_buffer, 'TLS Handshake failed')
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_MEMORY_ERROR:
W.prnt(server.server_buffer, 'Not enough memory')
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_TIMEOUT:
W.prnt(server.server_buffer, 'Timeout')
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_SOCKET_ERROR:
W.prnt(server.server_buffer, 'Unable to create socket')
2017-12-30 14:03:03 +01:00
else:
W.prnt(
server.server_buffer,
'Unexpected error: {status}'.format(status=status_value)
)
2017-12-30 14:03:03 +01:00
2018-01-30 11:46:29 +01:00
matrix_server_reconnect(server)
2017-12-30 14:03:03 +01:00
return W.WEECHAT_RC_OK
@utf8_decode
def reconnect_cb(server_name, remaining):
server = SERVERS[server_name]
connect(server)
2017-12-30 14:03:03 +01:00
return W.WEECHAT_RC_OK
@utf8_decode
def room_input_cb(server_name, buffer, input_data):
server = SERVERS[server_name]
2018-01-07 15:46:18 +01:00
if not server.connected:
message = "{prefix}you are not connected to the server".format(
prefix=W.prefix("error"))
W.prnt(buffer, message)
return W.WEECHAT_RC_ERROR
2018-01-10 17:09:02 +01:00
room_id = key_from_value(server.buffers, buffer)
room = server.rooms[room_id]
if room.encrypted:
return W.WEECHAT_RC_OK
formatted_data = colors.parse_input_line(input_data)
body = {
"msgtype": "m.text",
"body": colors.formatted_to_plain(formatted_data)
}
if colors.formatted(formatted_data):
body["format"] = "org.matrix.custom.html"
body["formatted_body"] = colors.formatted_to_html(formatted_data)
2018-01-23 13:44:20 +01:00
extra_data = {
"author": server.user,
"message": colors.formatted_to_weechat(W, formatted_data),
"room_id": room_id
}
2018-01-29 17:47:47 +01:00
message = MatrixMessage(server, OPTIONS, MessageType.SEND,
data=body, room_id=room_id,
extra_data=extra_data)
2018-01-10 17:09:02 +01:00
send_or_queue(server, message)
2017-12-30 14:03:03 +01:00
return W.WEECHAT_RC_OK
@utf8_decode
2018-01-03 12:14:24 +01:00
def room_close_cb(data, buffer):
2017-12-30 14:03:03 +01:00
W.prnt("", "Buffer '%s' will be closed!" %
W.buffer_get_string(buffer, "name"))
return W.WEECHAT_RC_OK
2018-01-03 12:14:24 +01:00
@utf8_decode
def matrix_unload_cb():
2018-01-29 17:47:47 +01:00
matrix_config_free(CONFIG)
2018-01-03 12:14:24 +01:00
return W.WEECHAT_RC_OK
2018-01-06 17:12:54 +01:00
def autoconnect(servers):
for server in servers.values():
if server.autoconnect:
connect(server)
2017-12-30 14:03:03 +01:00
if __name__ == "__main__":
if W.register(WEECHAT_SCRIPT_NAME,
WEECHAT_SCRIPT_AUTHOR,
WEECHAT_SCRIPT_VERSION,
WEECHAT_SCRIPT_LICENSE,
WEECHAT_SCRIPT_DESCRIPTION,
'matrix_unload_cb',
2017-12-30 14:03:03 +01:00
''):
2018-01-03 12:14:24 +01:00
# TODO if this fails we should abort and unload the script.
2018-01-29 17:47:47 +01:00
CONFIG = matrix_config_init()
matrix_config_read(CONFIG)
2018-01-03 12:14:24 +01:00
hook_commands()
2018-01-29 13:07:49 +01:00
init_bar_items()
2018-01-29 14:01:58 +01:00
init_completion()
2018-01-06 17:12:54 +01:00
if not SERVERS:
create_default_server(CONFIG)
2018-01-06 17:12:54 +01:00
autoconnect(SERVERS)