2018-01-26 14:38:46 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
2018-01-26 18:22:06 +01:00
|
|
|
# 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.
|
|
|
|
|
2018-01-26 14:38:46 +01:00
|
|
|
from __future__ import unicode_literals
|
2018-02-01 13:20:52 +01:00
|
|
|
from builtins import str, bytes
|
2018-01-26 14:38:46 +01:00
|
|
|
|
2018-02-27 19:22:38 +01:00
|
|
|
import os
|
2018-01-26 14:38:46 +01:00
|
|
|
import ssl
|
2018-02-01 13:20:52 +01:00
|
|
|
import socket
|
|
|
|
import time
|
2018-02-25 17:59:08 +01:00
|
|
|
import datetime
|
|
|
|
import pprint
|
2018-01-26 14:38:46 +01:00
|
|
|
|
2018-05-16 11:21:59 +02:00
|
|
|
from collections import deque, defaultdict
|
2018-01-26 14:38:46 +01:00
|
|
|
from http_parser.pyparser import HttpParser
|
|
|
|
|
2018-01-30 11:46:29 +01:00
|
|
|
from matrix.plugin_options import Option, DebugType
|
2018-02-21 17:00:11 +01:00
|
|
|
from matrix.utils import (key_from_value, prnt_debug, server_buffer_prnt,
|
2018-03-19 14:51:07 +01:00
|
|
|
create_server_buffer, tags_for_message)
|
2018-01-29 18:11:45 +01:00
|
|
|
from matrix.utf import utf8_decode
|
2018-02-12 14:53:19 +01:00
|
|
|
from matrix.globals import W, SERVERS, OPTIONS
|
2018-02-25 17:59:08 +01:00
|
|
|
import matrix.api as API
|
2018-03-22 21:02:31 +01:00
|
|
|
from matrix.api import (
|
|
|
|
MatrixClient,
|
|
|
|
MatrixSyncMessage,
|
|
|
|
MatrixLoginMessage,
|
2018-04-12 14:19:32 +02:00
|
|
|
MatrixKeyUploadMessage,
|
2018-05-11 13:03:42 +02:00
|
|
|
MatrixKeyQueryMessage,
|
|
|
|
MatrixToDeviceMessage,
|
2018-05-16 11:21:59 +02:00
|
|
|
MatrixEncryptedMessage,
|
|
|
|
MatrixKeyClaimMessage
|
2018-03-22 21:02:31 +01:00
|
|
|
)
|
2018-01-26 14:38:46 +01:00
|
|
|
|
2018-05-17 14:34:34 +02:00
|
|
|
from matrix.encryption import (
|
|
|
|
Olm,
|
|
|
|
EncryptionError,
|
|
|
|
OlmTrustError,
|
|
|
|
encrypt_enabled
|
|
|
|
)
|
2018-03-05 19:34:51 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
FileNotFoundError
|
|
|
|
except NameError:
|
|
|
|
FileNotFoundError = IOError
|
|
|
|
|
2018-01-26 14:38:46 +01:00
|
|
|
|
|
|
|
class MatrixServer:
|
|
|
|
# pylint: disable=too-many-instance-attributes
|
2018-01-29 18:11:45 +01:00
|
|
|
def __init__(self, name, config_file):
|
2018-02-12 10:56:28 +01:00
|
|
|
# type: (str, weechat.config) -> None
|
2018-02-21 17:00:11 +01:00
|
|
|
# yapf: disable
|
2018-01-27 16:21:10 +01:00
|
|
|
self.name = name # type: str
|
|
|
|
self.user_id = ""
|
|
|
|
self.address = "" # type: str
|
|
|
|
self.port = 8448 # type: int
|
|
|
|
self.options = dict() # type: Dict[str, weechat.config]
|
|
|
|
self.device_name = "Weechat Matrix" # type: str
|
2018-02-27 19:22:38 +01:00
|
|
|
self.device_id = "" # type: str
|
2018-05-16 11:21:59 +02:00
|
|
|
|
2018-03-05 19:34:51 +01:00
|
|
|
self.olm = None # type: Olm
|
2018-05-16 11:21:59 +02:00
|
|
|
self.encryption_queue = defaultdict(deque)
|
2018-01-27 16:21:10 +01:00
|
|
|
|
|
|
|
self.user = "" # type: str
|
|
|
|
self.password = "" # type: str
|
|
|
|
|
|
|
|
self.rooms = dict() # type: Dict[str, MatrixRoom]
|
|
|
|
self.buffers = dict() # type: Dict[str, weechat.buffer]
|
|
|
|
self.server_buffer = None # type: weechat.buffer
|
|
|
|
self.fd_hook = None # type: weechat.hook
|
2018-01-30 19:29:03 +01:00
|
|
|
self.ssl_hook = None # type: weechat.hook
|
2018-01-27 16:21:10 +01:00
|
|
|
self.timer_hook = None # type: weechat.hook
|
|
|
|
self.numeric_address = "" # type: str
|
|
|
|
|
|
|
|
self.autoconnect = False # type: bool
|
|
|
|
self.connected = False # type: bool
|
|
|
|
self.connecting = False # type: bool
|
2018-03-03 23:47:44 +01:00
|
|
|
self.proxy = None # type: str
|
2018-02-01 13:20:52 +01:00
|
|
|
self.reconnect_delay = 0 # type: int
|
|
|
|
self.reconnect_time = None # type: float
|
2018-01-27 16:21:10 +01:00
|
|
|
self.socket = None # type: ssl.SSLSocket
|
|
|
|
self.ssl_context = ssl.create_default_context() # type: ssl.SSLContext
|
|
|
|
|
2018-02-02 15:03:52 +01:00
|
|
|
self.client = None
|
2018-01-27 16:21:10 +01:00
|
|
|
self.access_token = None # type: str
|
|
|
|
self.next_batch = None # type: str
|
|
|
|
self.transaction_id = 0 # type: int
|
2018-01-30 12:58:16 +01:00
|
|
|
self.lag = 0 # type: int
|
2018-02-08 10:58:33 +01:00
|
|
|
self.lag_done = False # type: bool
|
2018-01-26 14:38:46 +01:00
|
|
|
|
2018-01-31 13:55:55 +01:00
|
|
|
self.send_fd_hook = None # type: weechat.hook
|
|
|
|
self.send_buffer = b"" # type: bytes
|
|
|
|
self.current_message = None # type: MatrixMessage
|
2018-04-12 14:19:32 +02:00
|
|
|
self.device_check_timestamp = None
|
2018-01-31 13:55:55 +01:00
|
|
|
|
2018-01-26 14:38:46 +01:00
|
|
|
self.http_parser = HttpParser() # type: HttpParser
|
|
|
|
self.http_buffer = [] # type: List[bytes]
|
|
|
|
|
|
|
|
# Queue of messages we need to send off.
|
2018-01-27 16:21:10 +01:00
|
|
|
self.send_queue = deque() # type: Deque[MatrixMessage]
|
|
|
|
|
2018-01-26 14:38:46 +01:00
|
|
|
# Queue of messages we send off and are waiting a response for
|
|
|
|
self.receive_queue = deque() # type: Deque[MatrixMessage]
|
2018-01-27 16:21:10 +01:00
|
|
|
|
2018-03-19 14:51:07 +01:00
|
|
|
self.event_queue_timer = None
|
|
|
|
self.event_queue = deque() # type: Deque[RoomInfo]
|
2018-01-26 14:38:46 +01:00
|
|
|
|
2018-01-29 18:11:45 +01:00
|
|
|
self._create_options(config_file)
|
2018-02-27 19:22:38 +01:00
|
|
|
self._create_session_dir()
|
2018-02-21 17:00:11 +01:00
|
|
|
# yapf: enable
|
2018-01-26 14:38:46 +01:00
|
|
|
|
2018-02-27 19:22:38 +01:00
|
|
|
def _create_session_dir(self):
|
|
|
|
path = os.path.join("matrix", self.name)
|
|
|
|
if not W.mkdir_home(path, 0o700):
|
|
|
|
message = ("{prefix}matrix: Error creating server session "
|
|
|
|
"directory").format(prefix=W.prefix("error"))
|
|
|
|
W.prnt("", message)
|
|
|
|
|
2018-03-04 17:19:10 +01:00
|
|
|
def get_session_path(self):
|
2018-02-27 19:22:38 +01:00
|
|
|
home_dir = W.info_get('weechat_dir', '')
|
|
|
|
return os.path.join(home_dir, "matrix", self.name)
|
|
|
|
|
2018-03-04 17:19:10 +01:00
|
|
|
def _load_device_id(self):
|
|
|
|
file_name = "{}{}".format(self.user, ".device_id")
|
|
|
|
path = os.path.join(self.get_session_path(), file_name)
|
2018-02-27 19:22:38 +01:00
|
|
|
|
|
|
|
if not os.path.isfile(path):
|
|
|
|
return
|
|
|
|
|
|
|
|
with open(path, 'r') as f:
|
|
|
|
device_id = f.readline().rstrip()
|
|
|
|
if device_id:
|
|
|
|
self.device_id = device_id
|
|
|
|
|
|
|
|
def save_device_id(self):
|
2018-03-04 17:19:10 +01:00
|
|
|
file_name = "{}{}".format(self.user, ".device_id")
|
|
|
|
path = os.path.join(self.get_session_path(), file_name)
|
2018-02-27 19:22:38 +01:00
|
|
|
|
|
|
|
with open(path, 'w') as f:
|
|
|
|
f.write(self.device_id)
|
|
|
|
|
2018-03-05 19:34:51 +01:00
|
|
|
def _load_olm(self):
|
|
|
|
try:
|
2018-04-07 11:30:36 +02:00
|
|
|
self.olm = Olm.from_session_dir(
|
|
|
|
self.user,
|
|
|
|
self.device_id,
|
|
|
|
self.get_session_path()
|
|
|
|
)
|
2018-04-11 14:00:37 +02:00
|
|
|
message = ("{prefix}matrix: Loaded Olm account for {user} (device:"
|
|
|
|
"{device})").format(prefix=W.prefix("network"),
|
|
|
|
user=self.user,
|
|
|
|
device=self.device_id)
|
|
|
|
W.prnt("", message)
|
|
|
|
|
2018-03-05 19:34:51 +01:00
|
|
|
except FileNotFoundError:
|
2018-03-22 11:32:54 +01:00
|
|
|
pass
|
2018-03-05 19:34:51 +01:00
|
|
|
except EncryptionError as error:
|
|
|
|
message = ("{prefix}matrix: Error loading Olm"
|
|
|
|
"account: {error}.").format(
|
|
|
|
prefix=W.prefix("error"), error=error)
|
|
|
|
W.prnt("", message)
|
|
|
|
|
2018-03-24 20:14:46 +01:00
|
|
|
@encrypt_enabled
|
2018-03-22 11:32:54 +01:00
|
|
|
def create_olm(self):
|
|
|
|
message = ("{prefix}matrix: Creating new Olm identity for "
|
|
|
|
"{self_color}{user}{ncolor}"
|
|
|
|
" on {server_color}{server}{ncolor} for device "
|
|
|
|
"{device}.").format(
|
|
|
|
prefix=W.prefix("network"),
|
|
|
|
self_color=W.color("chat_nick_self"),
|
|
|
|
ncolor=W.color("reset"),
|
|
|
|
user=self.user_id,
|
|
|
|
server_color=W.color("chat_server"),
|
|
|
|
server=self.name,
|
|
|
|
device=self.device_id)
|
|
|
|
W.prnt(self.server_buffer, message)
|
2018-04-07 11:30:36 +02:00
|
|
|
self.olm = Olm(self.user, self.device_id, self.get_session_path())
|
2018-03-22 12:07:22 +01:00
|
|
|
|
2018-03-24 20:14:46 +01:00
|
|
|
@encrypt_enabled
|
2018-03-22 12:07:22 +01:00
|
|
|
def store_olm(self):
|
2018-05-06 15:04:19 +02:00
|
|
|
if self.olm:
|
|
|
|
self.olm.to_session_dir()
|
2018-03-22 11:32:54 +01:00
|
|
|
|
2018-01-29 18:11:45 +01:00
|
|
|
def _create_options(self, config_file):
|
2018-01-26 14:38:46 +01:00
|
|
|
options = [
|
2018-02-21 17:00:11 +01:00
|
|
|
Option('autoconnect', 'boolean', '', 0, 0, 'off',
|
|
|
|
("automatically connect to the matrix server when weechat "
|
|
|
|
"is starting")),
|
|
|
|
Option('address', 'string', '', 0, 0, '',
|
|
|
|
"Hostname or IP address for the server"),
|
|
|
|
Option('port', 'integer', '', 0, 65535, '8448',
|
|
|
|
"Port for the server"),
|
2018-03-03 23:47:44 +01:00
|
|
|
Option('proxy', 'string', '', 0, 0, '',
|
|
|
|
("Name of weechat proxy to use (see /help proxy)")),
|
2018-02-21 17:00:11 +01:00
|
|
|
Option('ssl_verify', 'boolean', '', 0, 0, 'on',
|
|
|
|
("Check that the SSL connection is fully trusted")),
|
|
|
|
Option('username', 'string', '', 0, 0, '',
|
|
|
|
"Username to use on server"),
|
2018-01-26 14:38:46 +01:00
|
|
|
Option(
|
|
|
|
'password', 'string', '', 0, 0, '',
|
2018-01-28 11:44:57 +01:00
|
|
|
("Password for server (note: content is evaluated, see /help "
|
2018-02-21 17:00:11 +01:00
|
|
|
"eval)")),
|
|
|
|
Option('device_name', 'string', '', 0, 0, 'Weechat Matrix',
|
|
|
|
"Device name to use while logging in to the matrix server"),
|
2018-01-26 14:38:46 +01:00
|
|
|
]
|
|
|
|
|
2018-01-29 18:11:45 +01:00
|
|
|
section = W.config_search_section(config_file, 'server')
|
2018-01-26 14:38:46 +01:00
|
|
|
|
|
|
|
for option in options:
|
|
|
|
option_name = "{server}.{option}".format(
|
|
|
|
server=self.name, option=option.name)
|
|
|
|
|
2018-01-29 18:11:45 +01:00
|
|
|
self.options[option.name] = W.config_new_option(
|
2018-02-21 17:00:11 +01:00
|
|
|
config_file, section, option_name, option.type,
|
|
|
|
option.description, option.string_values, option.min,
|
|
|
|
option.max, option.value, option.value, 0, "", "",
|
|
|
|
"matrix_config_server_change_cb", self.name, "", "")
|
2018-01-26 14:38:46 +01:00
|
|
|
|
|
|
|
def reset_parser(self):
|
|
|
|
self.http_parser = HttpParser()
|
|
|
|
self.http_buffer = []
|
2018-01-26 14:48:34 +01:00
|
|
|
|
2018-02-02 15:03:52 +01:00
|
|
|
def _change_client(self):
|
|
|
|
host = ':'.join([self.address, str(self.port)])
|
|
|
|
user_agent = 'weechat-matrix/{version}'.format(version="0.1")
|
|
|
|
self.client = MatrixClient(host, user_agent=user_agent)
|
|
|
|
|
2018-01-29 18:11:45 +01:00
|
|
|
def update_option(self, option, option_name):
|
2018-01-26 14:48:34 +01:00
|
|
|
if option_name == "address":
|
|
|
|
value = W.config_string(option)
|
|
|
|
self.address = value
|
2018-02-02 15:03:52 +01:00
|
|
|
self._change_client()
|
2018-01-26 14:48:34 +01:00
|
|
|
elif option_name == "autoconnect":
|
|
|
|
value = W.config_boolean(option)
|
|
|
|
self.autoconnect = value
|
|
|
|
elif option_name == "port":
|
|
|
|
value = W.config_integer(option)
|
|
|
|
self.port = value
|
2018-02-02 15:03:52 +01:00
|
|
|
self._change_client()
|
2018-03-03 23:47:44 +01:00
|
|
|
elif option_name == "proxy":
|
|
|
|
value = W.config_string(option)
|
|
|
|
self.proxy = value
|
2018-01-26 14:48:34 +01:00
|
|
|
elif option_name == "ssl_verify":
|
|
|
|
value = W.config_boolean(option)
|
|
|
|
if value:
|
|
|
|
self.ssl_context.verify_mode = ssl.CERT_REQUIRED
|
2018-01-27 14:40:01 +01:00
|
|
|
self.ssl_context.check_hostname = True
|
2018-01-26 14:48:34 +01:00
|
|
|
else:
|
|
|
|
self.ssl_context.check_hostname = False
|
|
|
|
self.ssl_context.verify_mode = ssl.CERT_NONE
|
|
|
|
elif option_name == "username":
|
|
|
|
value = W.config_string(option)
|
|
|
|
self.user = value
|
|
|
|
self.access_token = ""
|
2018-03-04 17:19:10 +01:00
|
|
|
|
|
|
|
self._load_device_id()
|
|
|
|
|
2018-03-05 19:34:51 +01:00
|
|
|
if self.device_id:
|
|
|
|
self._load_olm()
|
|
|
|
|
2018-01-26 14:48:34 +01:00
|
|
|
elif option_name == "password":
|
|
|
|
value = W.config_string(option)
|
2018-01-28 11:44:57 +01:00
|
|
|
self.password = W.string_eval_expression(value, {}, {}, {})
|
2018-01-26 14:48:34 +01:00
|
|
|
elif option_name == "device_name":
|
|
|
|
value = W.config_string(option)
|
|
|
|
self.device_name = value
|
|
|
|
else:
|
|
|
|
pass
|
2018-01-29 18:11:45 +01:00
|
|
|
|
2018-02-02 10:13:56 +01:00
|
|
|
def send_or_queue(self, message):
|
|
|
|
# type: (MatrixServer, MatrixMessage) -> None
|
|
|
|
if not self.send(message):
|
|
|
|
prnt_debug(DebugType.MESSAGING, self,
|
|
|
|
("{prefix} Failed sending message of type {t}. "
|
|
|
|
"Adding to queue").format(
|
2018-02-25 18:04:49 +01:00
|
|
|
prefix=W.prefix("error"),
|
|
|
|
t=message.__class__.__name__))
|
2018-02-02 10:13:56 +01:00
|
|
|
self.send_queue.append(message)
|
|
|
|
|
|
|
|
def try_send(self, message):
|
|
|
|
# type: (MatrixServer, bytes) -> bool
|
|
|
|
|
|
|
|
sock = self.socket
|
|
|
|
total_sent = 0
|
|
|
|
message_length = len(message)
|
|
|
|
|
|
|
|
while total_sent < message_length:
|
|
|
|
try:
|
|
|
|
sent = sock.send(message[total_sent:])
|
|
|
|
|
|
|
|
except ssl.SSLWantWriteError:
|
2018-02-21 17:00:11 +01:00
|
|
|
hook = W.hook_fd(sock.fileno(), 0, 1, 0, "send_cb", self.name)
|
2018-02-02 10:13:56 +01:00
|
|
|
self.send_fd_hook = hook
|
|
|
|
self.send_buffer = message[total_sent:]
|
|
|
|
return True
|
|
|
|
|
|
|
|
except socket.error as error:
|
2018-02-02 10:36:54 +01:00
|
|
|
self._abort_send()
|
2018-02-02 10:13:56 +01:00
|
|
|
|
|
|
|
errno = "error" + str(error.errno) + " " if error.errno else ""
|
|
|
|
strerr = error.strerror if error.strerror else "Unknown reason"
|
|
|
|
strerr = errno + strerr
|
|
|
|
|
2018-02-12 10:56:28 +01:00
|
|
|
error_message = ("{prefix}Error while writing to "
|
|
|
|
"socket: {error}").format(
|
2018-02-21 17:00:11 +01:00
|
|
|
prefix=W.prefix("network"), error=strerr)
|
2018-02-02 10:13:56 +01:00
|
|
|
|
2018-02-12 10:56:28 +01:00
|
|
|
server_buffer_prnt(self, error_message)
|
2018-02-02 10:13:56 +01:00
|
|
|
server_buffer_prnt(
|
2018-02-21 17:00:11 +01:00
|
|
|
self, ("{prefix}matrix: disconnecting from server..."
|
|
|
|
).format(prefix=W.prefix("network")))
|
2018-02-02 10:13:56 +01:00
|
|
|
|
2018-02-02 10:29:59 +01:00
|
|
|
self.disconnect()
|
2018-02-02 10:13:56 +01:00
|
|
|
return False
|
|
|
|
|
|
|
|
if sent == 0:
|
2018-02-02 10:36:54 +01:00
|
|
|
self._abort_send()
|
2018-02-02 10:13:56 +01:00
|
|
|
|
|
|
|
server_buffer_prnt(
|
|
|
|
self,
|
|
|
|
"{prefix}matrix: Error while writing to socket".format(
|
2018-02-07 12:42:33 +01:00
|
|
|
prefix=W.prefix("network")))
|
2018-02-02 10:13:56 +01:00
|
|
|
server_buffer_prnt(
|
2018-02-21 17:00:11 +01:00
|
|
|
self, ("{prefix}matrix: disconnecting from server..."
|
|
|
|
).format(prefix=W.prefix("network")))
|
2018-02-02 10:29:59 +01:00
|
|
|
self.disconnect()
|
2018-02-02 10:13:56 +01:00
|
|
|
return False
|
|
|
|
|
|
|
|
total_sent = total_sent + sent
|
|
|
|
|
2018-02-02 10:36:54 +01:00
|
|
|
self._finalize_send()
|
2018-02-02 10:13:56 +01:00
|
|
|
return True
|
|
|
|
|
2018-02-02 10:36:54 +01:00
|
|
|
def _abort_send(self):
|
2018-02-02 10:13:56 +01:00
|
|
|
self.send_queue.appendleft(self.current_message)
|
|
|
|
self.current_message = None
|
|
|
|
self.send_buffer = ""
|
|
|
|
|
2018-02-02 10:36:54 +01:00
|
|
|
def _finalize_send(self):
|
2018-02-02 10:13:56 +01:00
|
|
|
# type: (MatrixServer) -> None
|
|
|
|
self.current_message.send_time = time.time()
|
|
|
|
self.receive_queue.append(self.current_message)
|
|
|
|
|
2018-02-12 10:56:28 +01:00
|
|
|
self.send_buffer = b""
|
2018-02-02 10:13:56 +01:00
|
|
|
self.current_message = None
|
|
|
|
|
|
|
|
def send(self, message):
|
|
|
|
# type: (MatrixServer, MatrixMessage) -> bool
|
|
|
|
if self.current_message:
|
|
|
|
return False
|
|
|
|
|
|
|
|
self.current_message = message
|
|
|
|
|
|
|
|
request = message.request.request
|
|
|
|
payload = message.request.payload
|
|
|
|
|
|
|
|
bytes_message = bytes(request, 'utf-8') + bytes(payload, 'utf-8')
|
|
|
|
|
|
|
|
self.try_send(bytes_message)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
2018-02-02 10:29:59 +01:00
|
|
|
def reconnect(self):
|
2018-02-21 17:00:11 +01:00
|
|
|
message = ("{prefix}matrix: reconnecting to server..."
|
|
|
|
).format(prefix=W.prefix("network"))
|
2018-02-02 10:29:59 +01:00
|
|
|
|
|
|
|
server_buffer_prnt(self, message)
|
|
|
|
|
|
|
|
self.reconnect_time = None
|
|
|
|
|
|
|
|
if not self.connect():
|
|
|
|
self.schedule_reconnect()
|
|
|
|
|
|
|
|
def schedule_reconnect(self):
|
|
|
|
# type: (MatrixServer) -> None
|
|
|
|
self.connecting = True
|
|
|
|
self.reconnect_time = time.time()
|
|
|
|
|
|
|
|
if self.reconnect_delay:
|
|
|
|
self.reconnect_delay = self.reconnect_delay * 2
|
|
|
|
else:
|
|
|
|
self.reconnect_delay = 10
|
|
|
|
|
|
|
|
message = ("{prefix}matrix: reconnecting to server in {t} "
|
|
|
|
"seconds").format(
|
2018-02-21 17:00:11 +01:00
|
|
|
prefix=W.prefix("network"), t=self.reconnect_delay)
|
2018-02-02 10:29:59 +01:00
|
|
|
|
|
|
|
server_buffer_prnt(self, message)
|
|
|
|
|
2018-02-02 10:37:06 +01:00
|
|
|
def _close_socket(self):
|
2018-02-10 15:36:31 +01:00
|
|
|
# type: () -> None
|
2018-02-02 10:37:06 +01:00
|
|
|
if self.socket:
|
2018-02-03 12:51:13 +01:00
|
|
|
try:
|
|
|
|
self.socket.shutdown(socket.SHUT_RDWR)
|
|
|
|
self.socket.close()
|
2018-02-10 15:36:31 +01:00
|
|
|
except socket.error:
|
2018-02-03 12:51:13 +01:00
|
|
|
pass
|
2018-02-02 10:37:06 +01:00
|
|
|
|
2018-02-02 10:29:59 +01:00
|
|
|
def disconnect(self, reconnect=True):
|
2018-02-12 10:56:28 +01:00
|
|
|
# type: (bool) -> None
|
2018-02-02 10:29:59 +01:00
|
|
|
if self.fd_hook:
|
|
|
|
W.unhook(self.fd_hook)
|
|
|
|
|
2018-02-02 10:37:06 +01:00
|
|
|
self._close_socket()
|
2018-02-02 10:29:59 +01:00
|
|
|
|
|
|
|
self.fd_hook = None
|
|
|
|
self.socket = None
|
|
|
|
self.connected = False
|
|
|
|
self.access_token = ""
|
2018-02-15 15:28:01 +01:00
|
|
|
self.send_queue.clear()
|
2018-02-02 10:29:59 +01:00
|
|
|
self.receive_queue.clear()
|
|
|
|
|
2018-02-20 10:12:14 +01:00
|
|
|
self.send_buffer = b""
|
|
|
|
self.current_message = None
|
|
|
|
self.reset_parser()
|
|
|
|
|
2018-02-13 10:14:43 +01:00
|
|
|
self.lag = 0
|
|
|
|
W.bar_item_update("lag")
|
2018-02-02 10:29:59 +01:00
|
|
|
self.reconnect_delay = 0
|
|
|
|
self.reconnect_time = None
|
|
|
|
|
|
|
|
if self.server_buffer:
|
2018-02-21 17:00:11 +01:00
|
|
|
message = ("{prefix}matrix: disconnected from server"
|
|
|
|
).format(prefix=W.prefix("network"))
|
2018-02-02 10:29:59 +01:00
|
|
|
server_buffer_prnt(self, message)
|
|
|
|
|
|
|
|
if reconnect:
|
|
|
|
self.schedule_reconnect()
|
|
|
|
|
|
|
|
def connect(self):
|
|
|
|
# type: (MatrixServer) -> int
|
|
|
|
if not self.address or not self.port:
|
|
|
|
message = "{prefix}Server address or port not set".format(
|
|
|
|
prefix=W.prefix("error"))
|
|
|
|
W.prnt("", message)
|
|
|
|
return False
|
|
|
|
|
|
|
|
if not self.user or not self.password:
|
|
|
|
message = "{prefix}User or password not set".format(
|
|
|
|
prefix=W.prefix("error"))
|
|
|
|
W.prnt("", message)
|
|
|
|
return False
|
|
|
|
|
|
|
|
if self.connected:
|
|
|
|
return True
|
|
|
|
|
|
|
|
if not self.server_buffer:
|
|
|
|
create_server_buffer(self)
|
|
|
|
|
|
|
|
if not self.timer_hook:
|
2018-02-21 17:00:11 +01:00
|
|
|
self.timer_hook = W.hook_timer(1 * 1000, 0, 0, "matrix_timer_cb",
|
|
|
|
self.name)
|
2018-02-02 10:29:59 +01:00
|
|
|
|
|
|
|
ssl_message = " (SSL)" if self.ssl_context.check_hostname else ""
|
|
|
|
|
|
|
|
message = ("{prefix}matrix: Connecting to "
|
|
|
|
"{server}:{port}{ssl}...").format(
|
2018-02-07 12:42:33 +01:00
|
|
|
prefix=W.prefix("network"),
|
|
|
|
server=self.address,
|
|
|
|
port=self.port,
|
|
|
|
ssl=ssl_message)
|
2018-02-02 10:29:59 +01:00
|
|
|
|
|
|
|
W.prnt(self.server_buffer, message)
|
|
|
|
|
2018-03-03 23:47:44 +01:00
|
|
|
W.hook_connect(self.proxy if self.proxy else "",
|
|
|
|
self.address, self.port,
|
|
|
|
1, 0, "", "connect_cb",
|
2018-02-21 17:00:11 +01:00
|
|
|
self.name)
|
2018-02-02 10:29:59 +01:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
2018-02-12 14:53:19 +01:00
|
|
|
def sync(self):
|
2018-03-06 13:53:34 +01:00
|
|
|
limit = None if self.next_batch else OPTIONS.sync_limit
|
|
|
|
message = MatrixSyncMessage(self.client, self.next_batch, limit)
|
2018-02-12 14:53:19 +01:00
|
|
|
self.send_queue.append(message)
|
|
|
|
|
2018-05-16 11:21:59 +02:00
|
|
|
def send_room_message(
|
|
|
|
self,
|
|
|
|
room_id,
|
|
|
|
formatted_data,
|
|
|
|
already_claimed=False
|
|
|
|
):
|
2018-05-09 14:00:32 +02:00
|
|
|
# type: (str, Formatted) -> None
|
|
|
|
room = self.rooms[room_id]
|
|
|
|
|
|
|
|
if not room.encrypted:
|
|
|
|
return
|
|
|
|
|
2018-05-11 13:03:42 +02:00
|
|
|
# TODO don't send messages unless all the devices are verified
|
2018-05-09 14:00:32 +02:00
|
|
|
missing = self.olm.get_missing_sessions(room.users.keys())
|
|
|
|
|
2018-05-16 11:21:59 +02:00
|
|
|
if missing and not already_claimed:
|
2018-05-09 14:00:32 +02:00
|
|
|
W.prnt("", "{prefix}matrix: Olm session missing for room, can't"
|
|
|
|
" encrypt message.")
|
|
|
|
W.prnt("", pprint.pformat(missing))
|
2018-05-16 11:21:59 +02:00
|
|
|
self.encryption_queue[room_id].append(formatted_data)
|
|
|
|
message = MatrixKeyClaimMessage(self.client, room_id, missing)
|
|
|
|
self.send_or_queue(message)
|
2018-05-09 14:00:32 +02:00
|
|
|
return
|
|
|
|
|
2018-05-11 13:03:42 +02:00
|
|
|
body = {"msgtype": "m.text", "body": formatted_data.to_plain()}
|
|
|
|
|
|
|
|
if formatted_data.is_formatted():
|
|
|
|
body["format"] = "org.matrix.custom.html"
|
|
|
|
body["formatted_body"] = formatted_data.to_html()
|
|
|
|
|
|
|
|
plaintext_dict = {
|
|
|
|
"type": "m.room.message",
|
|
|
|
"content": body
|
|
|
|
}
|
|
|
|
|
|
|
|
W.prnt("", "matrix: Encrypting message")
|
|
|
|
|
2018-05-17 14:34:34 +02:00
|
|
|
try:
|
|
|
|
payload_dict, to_device_dict = self.olm.group_encrypt(
|
|
|
|
room_id,
|
|
|
|
plaintext_dict,
|
|
|
|
self.user_id,
|
|
|
|
room.users.keys()
|
|
|
|
)
|
2018-05-11 13:03:42 +02:00
|
|
|
|
2018-05-17 14:34:34 +02:00
|
|
|
if to_device_dict:
|
|
|
|
W.prnt("", "matrix: Megolm session missing for room.")
|
|
|
|
message = MatrixToDeviceMessage(self.client, to_device_dict)
|
|
|
|
self.send_queue.append(message)
|
2018-05-11 13:03:42 +02:00
|
|
|
|
2018-05-17 14:34:34 +02:00
|
|
|
message = MatrixEncryptedMessage(
|
|
|
|
self.client,
|
|
|
|
room_id,
|
|
|
|
formatted_data,
|
|
|
|
payload_dict
|
|
|
|
)
|
2018-05-11 13:03:42 +02:00
|
|
|
|
2018-05-17 14:34:34 +02:00
|
|
|
self.send_queue.append(message)
|
|
|
|
|
|
|
|
except OlmTrustError:
|
|
|
|
m = ("{prefix}matrix: Untrusted devices found in room, "
|
|
|
|
"verification is needed before sending a message").format(
|
|
|
|
prefix=W.prefix("error"))
|
|
|
|
W.prnt(self.server_buffer, m)
|
|
|
|
return
|
2018-05-09 14:00:32 +02:00
|
|
|
|
2018-03-24 20:14:46 +01:00
|
|
|
@encrypt_enabled
|
2018-03-22 21:02:31 +01:00
|
|
|
def upload_keys(self, device_keys=False, one_time_keys=False):
|
|
|
|
keys = self.olm.account.identity_keys() if device_keys else None
|
|
|
|
|
2018-06-04 22:34:18 +02:00
|
|
|
one_time_keys = (self.olm.account.one_time_keys["curve25519"] if
|
2018-03-23 18:43:56 +01:00
|
|
|
one_time_keys else None)
|
|
|
|
|
2018-03-22 21:02:31 +01:00
|
|
|
message = MatrixKeyUploadMessage(self.client, self.user_id,
|
2018-03-23 18:43:56 +01:00
|
|
|
self.device_id, self.olm,
|
|
|
|
keys, one_time_keys)
|
2018-03-22 21:02:31 +01:00
|
|
|
self.send_queue.append(message)
|
|
|
|
|
2018-03-23 18:43:56 +01:00
|
|
|
@encrypt_enabled
|
|
|
|
def check_one_time_keys(self, key_count):
|
2018-06-04 22:34:18 +02:00
|
|
|
max_keys = self.olm.account.max_one_time_keys
|
2018-03-23 18:43:56 +01:00
|
|
|
|
|
|
|
key_count = (max_keys / 2) - key_count
|
|
|
|
|
|
|
|
if key_count <= 0:
|
|
|
|
return
|
|
|
|
|
|
|
|
self.olm.account.generate_one_time_keys(key_count)
|
|
|
|
self.upload_keys(device_keys=False, one_time_keys=True)
|
|
|
|
|
2018-04-12 14:19:32 +02:00
|
|
|
@encrypt_enabled
|
|
|
|
def query_keys(self):
|
|
|
|
users = []
|
|
|
|
|
|
|
|
for room in self.rooms.values():
|
|
|
|
if not room.encrypted:
|
|
|
|
continue
|
|
|
|
users += list(room.users)
|
|
|
|
|
|
|
|
if not users:
|
|
|
|
return
|
|
|
|
|
|
|
|
message = MatrixKeyQueryMessage(self.client, users)
|
|
|
|
self.send_queue.append(message)
|
|
|
|
|
2018-02-12 14:53:19 +01:00
|
|
|
def login(self):
|
|
|
|
# type: (MatrixServer) -> None
|
2018-02-21 17:00:11 +01:00
|
|
|
message = MatrixLoginMessage(self.client, self.user, self.password,
|
2018-02-27 19:22:38 +01:00
|
|
|
self.device_name, self.device_id)
|
2018-02-12 14:53:19 +01:00
|
|
|
self.send_or_queue(message)
|
|
|
|
|
2018-02-27 19:55:30 +01:00
|
|
|
msg = "{prefix}matrix: Logging in...".format(prefix=W.prefix("network"))
|
|
|
|
|
|
|
|
W.prnt(self.server_buffer, msg)
|
|
|
|
|
2018-02-25 17:59:08 +01:00
|
|
|
def _print_message_error(self, message):
|
|
|
|
server_buffer_prnt(self,
|
|
|
|
("{prefix}Unhandled {status_code} error, please "
|
|
|
|
"inform the developers about this.").format(
|
|
|
|
prefix=W.prefix("error"),
|
|
|
|
status_code=message.response.status))
|
|
|
|
|
|
|
|
server_buffer_prnt(self, pprint.pformat(message.__class__.__name__))
|
|
|
|
server_buffer_prnt(self, pprint.pformat(message.request.payload))
|
|
|
|
server_buffer_prnt(self, pprint.pformat(message.response.body))
|
|
|
|
|
2018-03-19 14:51:07 +01:00
|
|
|
def _loop_events(self, info, n):
|
|
|
|
|
|
|
|
for i in range(n+1):
|
|
|
|
try:
|
|
|
|
event = info.events.popleft()
|
|
|
|
except IndexError:
|
|
|
|
return i
|
|
|
|
|
|
|
|
room = self.rooms[info.room_id]
|
|
|
|
buf = self.buffers[info.room_id]
|
|
|
|
|
|
|
|
tags = tags_for_message("message")
|
|
|
|
event.execute(self, room, buf, tags)
|
|
|
|
|
|
|
|
self.event_queue.appendleft(info)
|
|
|
|
return i
|
|
|
|
|
|
|
|
def handle_events(self):
|
|
|
|
n = 25
|
|
|
|
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
info = self.event_queue.popleft()
|
|
|
|
except IndexError:
|
|
|
|
if self.event_queue_timer:
|
|
|
|
W.unhook(self.event_queue_timer)
|
|
|
|
self.event_queue_timer = None
|
|
|
|
|
|
|
|
self.sync()
|
|
|
|
return
|
|
|
|
|
|
|
|
ret = self._loop_events(info, n)
|
|
|
|
|
|
|
|
if ret < n:
|
|
|
|
n = n - ret
|
|
|
|
else:
|
|
|
|
self.event_queue.appendleft(info)
|
|
|
|
|
|
|
|
if not self.event_queue_timer:
|
|
|
|
hook = W.hook_timer(1 * 100, 0, 0, "matrix_event_timer_cb",
|
|
|
|
self.name)
|
|
|
|
self.event_queue_timer = hook
|
|
|
|
|
|
|
|
return
|
|
|
|
|
2018-02-25 17:59:08 +01:00
|
|
|
def handle_response(self, message):
|
|
|
|
# type: (MatrixMessage) -> None
|
|
|
|
|
|
|
|
assert message.response
|
|
|
|
|
|
|
|
if ('content-type' in message.response.headers and
|
|
|
|
message.response.headers['content-type'] == 'application/json'):
|
|
|
|
ret, error = message.decode_body(self)
|
|
|
|
|
|
|
|
if not ret:
|
|
|
|
message = ("{prefix}matrix: Error decoding json response from "
|
|
|
|
"server: {error}").format(
|
|
|
|
prefix=W.prefix("error"), error=error)
|
|
|
|
W.prnt(self.server_buffer, message)
|
|
|
|
return
|
|
|
|
|
|
|
|
event = message.event
|
|
|
|
event.execute()
|
|
|
|
else:
|
|
|
|
status_code = message.response.status
|
|
|
|
if status_code == 504:
|
|
|
|
if isinstance(message, API.MatrixSyncMessage):
|
|
|
|
self.sync()
|
|
|
|
else:
|
|
|
|
self._print_message_error(message)
|
|
|
|
else:
|
|
|
|
self._print_message_error(message)
|
|
|
|
|
|
|
|
creation_date = datetime.datetime.fromtimestamp(message.creation_time)
|
|
|
|
done_time = time.time()
|
|
|
|
info_message = (
|
|
|
|
"Message of type {t} created at {c}."
|
|
|
|
"\nMessage lifetime information:"
|
|
|
|
"\n Send delay: {s} ms"
|
|
|
|
"\n Receive delay: {r} ms"
|
|
|
|
"\n Handling time: {h} ms"
|
|
|
|
"\n Total time: {total} ms").format(
|
2018-02-25 18:04:49 +01:00
|
|
|
t=message.__class__.__name__,
|
2018-02-25 17:59:08 +01:00
|
|
|
c=creation_date,
|
|
|
|
s=(message.send_time - message.creation_time) * 1000,
|
|
|
|
r=(message.receive_time - message.send_time) * 1000,
|
|
|
|
h=(done_time - message.receive_time) * 1000,
|
|
|
|
total=(done_time - message.creation_time) * 1000,
|
|
|
|
)
|
|
|
|
prnt_debug(DebugType.TIMING, self, info_message)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
2018-01-29 18:11:45 +01:00
|
|
|
|
|
|
|
@utf8_decode
|
2018-02-21 17:00:11 +01:00
|
|
|
def matrix_config_server_read_cb(data, config_file, section, option_name,
|
|
|
|
value):
|
2018-01-29 18:11:45 +01:00
|
|
|
|
|
|
|
return_code = W.WEECHAT_CONFIG_OPTION_SET_ERROR
|
|
|
|
|
|
|
|
if option_name:
|
|
|
|
server_name, option = option_name.rsplit('.', 1)
|
|
|
|
server = None
|
|
|
|
|
|
|
|
if server_name in SERVERS:
|
|
|
|
server = SERVERS[server_name]
|
|
|
|
else:
|
|
|
|
server = MatrixServer(server_name, config_file)
|
|
|
|
SERVERS[server.name] = server
|
|
|
|
|
|
|
|
# Ignore invalid options
|
|
|
|
if option in server.options:
|
|
|
|
return_code = W.config_option_set(server.options[option], value, 1)
|
|
|
|
|
|
|
|
# TODO print out error message in case of erroneous return_code
|
|
|
|
|
|
|
|
return return_code
|
|
|
|
|
|
|
|
|
|
|
|
@utf8_decode
|
|
|
|
def matrix_config_server_write_cb(data, config_file, section_name):
|
|
|
|
if not W.config_write_line(config_file, section_name, ""):
|
|
|
|
return W.WECHAT_CONFIG_WRITE_ERROR
|
|
|
|
|
|
|
|
for server in SERVERS.values():
|
|
|
|
for option in server.options.values():
|
|
|
|
if not W.config_write_option(config_file, option):
|
|
|
|
return W.WECHAT_CONFIG_WRITE_ERROR
|
|
|
|
|
|
|
|
return W.WEECHAT_CONFIG_WRITE_OK
|
|
|
|
|
|
|
|
|
|
|
|
@utf8_decode
|
|
|
|
def matrix_config_server_change_cb(server_name, option):
|
|
|
|
# type: (str, weechat.config_option) -> int
|
|
|
|
server = SERVERS[server_name]
|
|
|
|
option_name = None
|
|
|
|
|
|
|
|
# The function config_option_get_string() is used to get differing
|
|
|
|
# properties from a config option, sadly it's only available in the plugin
|
|
|
|
# API of weechat.
|
|
|
|
option_name = key_from_value(server.options, option)
|
|
|
|
server.update_option(option, option_name)
|
|
|
|
|
|
|
|
return 1
|
2018-01-30 11:46:29 +01:00
|
|
|
|
|
|
|
|
|
|
|
@utf8_decode
|
|
|
|
def matrix_timer_cb(server_name, remaining_calls):
|
|
|
|
server = SERVERS[server_name]
|
|
|
|
|
2018-02-01 13:20:52 +01:00
|
|
|
current_time = time.time()
|
|
|
|
|
2018-02-21 17:00:11 +01:00
|
|
|
if ((not server.connected) and server.reconnect_time and
|
2018-02-01 13:20:52 +01:00
|
|
|
current_time >= (server.reconnect_time + server.reconnect_delay)):
|
2018-02-02 10:29:59 +01:00
|
|
|
server.reconnect()
|
2018-02-13 10:15:05 +01:00
|
|
|
return W.WEECHAT_RC_OK
|
2018-02-01 13:20:52 +01:00
|
|
|
|
2018-01-30 11:46:29 +01:00
|
|
|
if not server.connected:
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
2018-02-08 10:58:33 +01:00
|
|
|
# check lag, disconnect if it's too big
|
|
|
|
if server.receive_queue:
|
|
|
|
message = server.receive_queue.popleft()
|
|
|
|
server.lag = (current_time - message.send_time) * 1000
|
|
|
|
server.receive_queue.appendleft(message)
|
|
|
|
server.lag_done = False
|
|
|
|
W.bar_item_update("lag")
|
|
|
|
|
|
|
|
# TODO print out message, make timeout configurable
|
|
|
|
if server.lag > 300000:
|
|
|
|
server.disconnect()
|
2018-02-13 10:15:05 +01:00
|
|
|
return W.WEECHAT_RC_OK
|
2018-02-08 10:58:33 +01:00
|
|
|
|
2018-01-30 11:46:29 +01:00
|
|
|
while server.send_queue:
|
|
|
|
message = server.send_queue.popleft()
|
2018-02-21 17:00:11 +01:00
|
|
|
prnt_debug(
|
|
|
|
DebugType.MESSAGING,
|
|
|
|
server, ("Timer hook found message of type {t} in queue. Sending "
|
2018-02-25 18:04:49 +01:00
|
|
|
"out.".format(t=message.__class__.__name__)))
|
2018-01-30 11:46:29 +01:00
|
|
|
|
2018-02-02 10:13:56 +01:00
|
|
|
if not server.send(message):
|
2018-01-30 11:46:29 +01:00
|
|
|
# We got an error while sending the last message return the message
|
|
|
|
# to the queue and exit the loop
|
|
|
|
server.send_queue.appendleft(message)
|
|
|
|
break
|
|
|
|
|
2018-04-12 14:19:32 +02:00
|
|
|
if not server.next_batch:
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
|
|
# check for new devices by users in encrypted rooms periodically
|
|
|
|
if (not server.device_check_timestamp or
|
|
|
|
current_time - server.device_check_timestamp > 600):
|
|
|
|
|
|
|
|
W.prnt(server.server_buffer,
|
|
|
|
"{prefix}matrix: Querying user devices.".format(
|
|
|
|
prefix=W.prefix("networ")))
|
|
|
|
|
|
|
|
server.query_keys()
|
|
|
|
server.device_check_timestamp = current_time
|
|
|
|
|
2018-01-30 11:46:29 +01:00
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
|
|
|
|
|
|
def create_default_server(config_file):
|
2018-02-05 12:38:45 +01:00
|
|
|
server = MatrixServer('matrix.org', config_file)
|
2018-01-30 11:46:29 +01:00
|
|
|
SERVERS[server.name] = server
|
|
|
|
|
|
|
|
W.config_option_set(server.options["address"], "matrix.org", 1)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2018-02-01 13:20:52 +01:00
|
|
|
@utf8_decode
|
|
|
|
def send_cb(server_name, file_descriptor):
|
|
|
|
# type: (str, int) -> int
|
|
|
|
|
|
|
|
server = SERVERS[server_name]
|
|
|
|
|
|
|
|
if server.send_fd_hook:
|
|
|
|
W.unhook(server.send_fd_hook)
|
|
|
|
server.send_fd_hook = None
|
|
|
|
|
|
|
|
if server.send_buffer:
|
2018-02-02 10:13:56 +01:00
|
|
|
server.try_send(server, server.send_buffer)
|
2018-02-01 13:20:52 +01:00
|
|
|
|
|
|
|
return W.WEECHAT_RC_OK
|