# -*- coding: utf-8 -*- # Weechat Matrix Protocol Script # Copyright © 2018 Damir Jelić # # Permission to use, copy, modify, and/or distribute this software for # any purpose with or without fee is hereby granted, provided that the # above copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY # SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. from __future__ import unicode_literals import socket import ssl import time import pprint # pylint: disable=redefined-builtin from builtins import str # pylint: disable=unused-import from typing import (List, Set, Dict, Tuple, Text, Optional, AnyStr, Deque, Any) from matrix import colors from matrix.utf import utf8_decode from matrix.http import HttpResponse 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, matrix_command_buf_clear_cb ) from matrix.server import ( MatrixServer, create_default_server, matrix_server_reconnect, 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 ) 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, tags_from_line_data, server_buffer_set_title ) from matrix.plugin_options import ( DebugType, RedactType, ServerBufferType, ) from matrix.config import ( matrix_config_init, matrix_config_read, matrix_config_free, matrix_config_change_cb, matrix_config_reload_cb ) from matrix.globals import W, OPTIONS, CONFIG, SERVERS WEECHAT_SCRIPT_NAME = "matrix" # type: str WEECHAT_SCRIPT_DESCRIPTION = "matrix chat plugin" # type: str WEECHAT_SCRIPT_AUTHOR = "Damir Jelić " # type: str WEECHAT_SCRIPT_VERSION = "0.1" # type: str WEECHAT_SCRIPT_LICENSE = "ISC" # type: str def wrap_socket(server, file_descriptor): # type: (MatrixServer, int) -> socket.socket sock = None # type: socket.socket temp_socket = socket.fromfd( file_descriptor, socket.AF_INET, socket.SOCK_STREAM ) # For python 2.7 wrap_socket() doesn't work with sockets created from an # file descriptor because fromfd() doesn't return a wrapped socket, the bug # was fixed for python 3, more info: https://bugs.python.org/issue13942 # pylint: disable=protected-access,unidiomatic-typecheck if type(temp_socket) == socket._socket.socket: # pylint: disable=no-member sock = socket._socketobject(_sock=temp_socket) else: sock = temp_socket try: start = time.time() message = "{prefix}matrix: Doing SSL handshake...".format( prefix=W.prefix("network")) W.prnt(server.server_buffer, message) # TODO this blocks currently ssl_socket = server.ssl_context.wrap_socket( sock, server_hostname=server.address) # type: ssl.SSLSocket cipher = ssl_socket.cipher() cipher_message = ("{prefix}matrix: Connected using {tls}, and " "{bit} bit {cipher} cipher suite.").format( prefix=W.prefix("network"), tls=cipher[1], bit=cipher[2], cipher=cipher[0]) W.prnt(server.server_buffer, cipher_message) # TODO print out the certificates # cert = ssl_socket.getpeercert() # W.prnt(server.server_buffer, pprint.pformat(cert)) server.lag = (time.time() - start) * 1000 W.bar_item_update("lag") return ssl_socket # TODO add finer grained error messages with the subclass exceptions except ssl.SSLError as error: server_buffer_prnt(server, str(error)) return None @utf8_decode def receive_cb(server_name, file_descriptor): server = SERVERS[server_name] while True: try: data = server.socket.recv(4096) except ssl.SSLWantReadError: break except socket.error as error: disconnect(server) # Queue the failed message for resending if server.receive_queue: message = server.receive_queue.popleft() server.send_queue.appendleft(message) server_buffer_prnt(server, pprint.pformat(error)) return W.WEECHAT_RC_OK 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) break received = len(data) # type: int parsed_bytes = server.http_parser.execute(data, received) assert parsed_bytes == received if server.http_parser.is_partial_body(): server.http_buffer.append(server.http_parser.recv_body()) 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) message = server.receive_queue.popleft() message.response = HttpResponse(status, headers, body) receive_time = time.time() 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)) # Message done, reset the parser state. server.reset_parser() handle_http_response(server, message) break return W.WEECHAT_RC_OK @utf8_decode def connect_cb(data, status, gnutls_rc, sock, error, ip_address): # pylint: disable=too-many-arguments,too-many-branches status_value = int(status) # type: int server = SERVERS[data] if status_value == W.WEECHAT_HOOK_CONNECT_OK: file_descriptor = int(sock) # type: int 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 ) if not server.timer_hook: server.timer_hook = W.hook_timer( 1 * 1000, 0, 0, "matrix_timer_cb", server.name ) server.fd_hook = hook server.connected = True server.connecting = False server.reconnect_count = 0 server.numeric_address = ip_address server_buffer_set_title(server) if not server.access_token: matrix_login(server) else: matrix_server_reconnect(server) 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) ) elif status_value == W.WEECHAT_HOOK_CONNECT_IP_ADDRESS_NOT_FOUND: W.prnt(server.server_buffer, 'IP address not found') elif status_value == W.WEECHAT_HOOK_CONNECT_CONNECTION_REFUSED: W.prnt(server.server_buffer, 'Connection refused') elif status_value == W.WEECHAT_HOOK_CONNECT_PROXY_ERROR: W.prnt( server.server_buffer, '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') elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_INIT_ERROR: W.prnt(server.server_buffer, 'TLS init error') elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_HANDSHAKE_ERROR: W.prnt(server.server_buffer, 'TLS Handshake failed') elif status_value == W.WEECHAT_HOOK_CONNECT_MEMORY_ERROR: W.prnt(server.server_buffer, 'Not enough memory') elif status_value == W.WEECHAT_HOOK_CONNECT_TIMEOUT: W.prnt(server.server_buffer, 'Timeout') elif status_value == W.WEECHAT_HOOK_CONNECT_SOCKET_ERROR: W.prnt(server.server_buffer, 'Unable to create socket') else: W.prnt( server.server_buffer, 'Unexpected error: {status}'.format(status=status_value) ) matrix_server_reconnect(server) return W.WEECHAT_RC_OK @utf8_decode def reconnect_cb(server_name, remaining): server = SERVERS[server_name] connect(server) 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}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 = 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) extra_data = { "author": server.user, "message": colors.formatted_to_weechat(W, formatted_data), "room_id": room_id } message = MatrixMessage(server, OPTIONS, MessageType.SEND, data=body, room_id=room_id, extra_data=extra_data) send_or_queue(server, message) return W.WEECHAT_RC_OK @utf8_decode def room_close_cb(data, buffer): W.prnt("", "Buffer '%s' will be closed!" % W.buffer_get_string(buffer, "name")) return W.WEECHAT_RC_OK @utf8_decode def matrix_unload_cb(): matrix_config_free(CONFIG) return W.WEECHAT_RC_OK def autoconnect(servers): for server in servers.values(): if server.autoconnect: connect(server) if __name__ == "__main__": if W.register(WEECHAT_SCRIPT_NAME, WEECHAT_SCRIPT_AUTHOR, WEECHAT_SCRIPT_VERSION, WEECHAT_SCRIPT_LICENSE, WEECHAT_SCRIPT_DESCRIPTION, 'matrix_unload_cb', ''): # TODO if this fails we should abort and unload the script. CONFIG = matrix_config_init() matrix_config_read(CONFIG) hook_commands() init_bar_items() init_completion() if not SERVERS: create_default_server(CONFIG) autoconnect(SERVERS)