weechat-matrix/weechat-matrix.py

1243 lines
38 KiB
Python
Raw Normal View History

2017-12-30 14:03:03 +01:00
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import json
import socket
import ssl
import time
2018-01-06 17:12:54 +01:00
# pylint: disable=redefined-builtin
2017-12-30 14:03:03 +01:00
from builtins import bytes
2018-01-06 17:12:54 +01:00
from collections import deque, Mapping, Iterable, namedtuple
2017-12-30 14:03:03 +01:00
from enum import Enum, unique
from functools import wraps
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 http_parser.pyparser import HttpParser
2018-01-06 17:12:54 +01:00
# pylint: disable=import-error
2017-12-30 14:03:03 +01:00
import weechat
WEECHAT_SCRIPT_NAME = "matrix" # type: unicode
WEECHAT_SCRIPT_DESCRIPTION = "matrix chat plugin" # type: unicode
WEECHAT_SCRIPT_AUTHOR = "Damir Jelić <poljar@termina.org.uk>" # type: unicode
WEECHAT_SCRIPT_VERSION = "0.1" # type: unicode
2018-01-03 12:14:24 +01:00
WEECHAT_SCRIPT_LICENSE = "MIT" # type: unicode
2017-12-30 14:03:03 +01:00
MATRIX_API_PATH = "/_matrix/client/r0" # type: unicode
SERVERS = dict() # type: Dict[unicode, MatrixServer]
2018-01-06 17:12:54 +01:00
CONFIG = None # type: weechat.config
2017-12-30 14:03:03 +01:00
2018-01-07 15:46:18 +01:00
NICK_GROUP_HERE = "0|Here"
2017-12-30 14:03:03 +01:00
2018-01-07 15:46:18 +01:00
# Unicode handling
2017-12-30 14:03:03 +01:00
def encode_to_utf8(data):
if isinstance(data, unicode):
return data.encode('utf-8')
if isinstance(data, bytes):
return data
elif isinstance(data, Mapping):
return type(data)(map(encode_to_utf8, data.iteritems()))
elif isinstance(data, Iterable):
return type(data)(map(encode_to_utf8, data))
else:
return data
def decode_from_utf8(data):
if isinstance(data, bytes):
return data.decode('utf-8')
if isinstance(data, unicode):
return data
elif isinstance(data, Mapping):
return type(data)(map(decode_from_utf8, data.iteritems()))
elif isinstance(data, Iterable):
return type(data)(map(decode_from_utf8, data))
else:
return data
def utf8_decode(f):
"""
Decode all arguments from byte strings to unicode strings. Use this for
functions called from outside of this script, e.g. callbacks from weechat.
"""
@wraps(f)
def wrapper(*args, **kwargs):
return f(*decode_from_utf8(args), **decode_from_utf8(kwargs))
return wrapper
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
if result == self.wrapped_class:
return self
return decode_from_utf8(result)
return hooked
# Encode and decode everything sent to/received from weechat. We use the
# unicode type internally in wee-slack, but has to send utf8 to weechat.
def __getattr__(self, attr):
orig_attr = self.wrapped_class.__getattribute__(attr)
if callable(orig_attr):
return self.wrap_for_utf8(orig_attr)
else:
return decode_from_utf8(orig_attr)
2018-01-03 12:14:24 +01:00
# Ensure all lines sent to weechat specify a prefix. For lines after the
2017-12-30 14:03:03 +01:00
# first, we want to disable the prefix, which is done by specifying a space.
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)
@unique
class MessageType(Enum):
LOGIN = 0
SYNC = 1
POST_MSG = 2
2017-12-30 14:03:03 +01:00
@unique
class RequestType(Enum):
GET = 0
POST = 1
PUT = 2
DELETE = 3
2017-12-30 14:03:03 +01:00
class RequestBuilder:
# TODO put the user agent somewhere globally
def __init__(self, host, user_agent='weechat-matrix/0.1'):
# type: (unicode, unicode) -> None
self.host = host
self.host_header = 'Host: {host}'.format(host=host)
self.user_agent = 'User-Agent: {agent}'.format(agent=user_agent)
# TODO we need to handle PUT as well
def request(self, location, data=None):
# type: (unicode, Dict[Any, Any]) -> (HttpRequest)
request_list = [] # type: List[unicode]
accept_header = 'Accept: */*' # type: unicode
end_separator = '\r\n' # type: unicode
payload = None # type: unicode
if data:
json_data = json.dumps(data, separators=(',', ':'))
post = 'POST {location} HTTP/1.1'.format(location=location)
type_header = 'Content-Type: application/x-www-form-urlencoded'
length_header = 'Content-Length: {length}'.format(length=len(json_data))
request_list = [post, self.host_header,
self.user_agent, accept_header,
length_header, type_header, end_separator]
payload = json_data
else:
get = 'GET {location} HTTP/1.1'.format(location=location)
request_list = [get, self.host_header,
self.user_agent, accept_header, end_separator]
request = '\r\n'.join(request_list)
return HttpRequest(request, payload)
class HttpResponse:
def __init__(self, status, headers, body):
self.status = status # type: int
self.headers = headers # type: Dict[unicode, unicode]
self.body = body # type: bytes
class HttpRequest:
def __init__(self, request, payload):
# type: (unicode, unicode) -> None
self.request = request
self.payload = payload
class MatrixMessage:
def __init__(self, messageType, request, response):
# type: (MessageType, HttpRequest, HttpResponse) -> None
self.type = messageType
self.request = request
self.response = response
class Matrix:
def __init__(self):
# type: () -> None
2017-12-30 14:03:03 +01:00
self.access_token = "" # type: unicode
self.next_batch = "" # type: unicode
self.rooms = {} # type: Dict[unicode, MatrixRoom]
class MatrixRoom:
2018-01-06 17:12:54 +01:00
def __init__(self, room_id, join_rule, alias=None):
2017-12-30 14:03:03 +01:00
# type: (unicode, unicode, unicode) -> None
2018-01-06 17:12:54 +01:00
self.room_id = room_id # type: unicode
self.alias = alias # type: unicode
2017-12-30 14:03:03 +01:00
self.join_rule = join_rule # type: unicode
@utf8_decode
def server_config_change_cb(server_name, option):
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.
# TODO we already have a function to get a key from a value out of a dict
for name, server_option in server.options.items():
if server_option == option:
option_name = name
break
if not option_name:
2018-01-06 17:12:54 +01:00
# TODO print error here, can this happen?
return 0
if option_name == "address":
value = W.config_string(option)
server.address = value
elif option_name == "autoconnect":
value = W.config_boolean(option)
server.autoconnect = value
elif option_name == "port":
value = W.config_integer(option)
server.port = value
elif option_name == "username":
value = W.config_string(option)
server.user = value
elif option_name == "password":
value = W.config_string(option)
server.password = value
else:
pass
return 1
2017-12-30 14:03:03 +01:00
2018-01-03 12:14:24 +01:00
class MatrixServer:
2018-01-06 17:12:54 +01:00
# pylint: disable=too-many-instance-attributes
def __init__(self, name, config_file):
2018-01-06 17:12:54 +01:00
# type: (unicode, weechat.config) -> None
self.name = name # type: unicode
self.address = "" # type: unicode
self.port = 8448 # type: int
self.options = dict() # type: Dict[unicode, weechat.config]
2018-01-06 17:12:54 +01:00
self.user = "" # type: unicode
self.password = "" # type: unicode
2018-01-06 17:12:54 +01:00
self.buffers = dict() # type: Dict[unicode, weechat.buffer]
self.server_buffer = None # type: weechat.buffer
self.fd_hook = None # type: weechat.hook
self.timer_hook = None # type: weechat.hook
2018-01-06 17:12:54 +01:00
self.autoconnect = False # type: bool
self.connected = False # type: bool
self.connecting = False # type: bool
self.reconnect_count = 0 # type: int
self.socket = None # type: ssl.SSLSocket
self.ssl_context = ssl.create_default_context() # type: ssl.SSLContext
2018-01-06 17:12:54 +01:00
self.access_token = None # type: unicode
self.next_batch = None # type: unicode
# TODO this should be made stateless
2018-01-03 12:14:24 +01:00
host_string = ':'.join([self.address,
str(self.port)]) # type: unicode
2018-01-06 17:12:54 +01:00
self.builder = RequestBuilder(host_string) # type: RequestBuilder
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
self.http_parser = HttpParser() # type: HttpParser
self.http_buffer = [] # type: List[bytes]
2017-12-30 14:03:03 +01:00
# Queue of messages we need to send off.
2018-01-06 17:12:54 +01:00
self.send_queue = deque() # type: Deque[MatrixMessage]
2017-12-30 14:03:03 +01:00
# Queue of messages we send off and are waiting a response for
2018-01-06 17:12:54 +01:00
self.receive_queue = deque() # type: Deque[MatrixMessage]
self.message_queue = deque() # type: Deque[MatrixMessage]
2017-12-30 14:03:03 +01:00
self._create_options(config_file)
2017-12-30 14:03:03 +01:00
# FIXME Don't set insecure
self._set_insecure()
2017-12-30 14:03:03 +01:00
# TODO remove this
def _set_insecure(self):
2017-12-30 14:03:03 +01:00
self.ssl_context.check_hostname = False
self.ssl_context.verify_mode = ssl.CERT_NONE
def _create_options(self, config_file):
2018-01-06 17:12:54 +01:00
option = namedtuple(
'Option', [
'name',
'type',
'string_values',
'min',
'max',
'value',
'description'
])
options = [
2018-01-06 17:12:54 +01:00
option(
'autoconnect', 'boolean', '', 0, 0, 'off',
"Automatically connect to the matrix server when Weechat is starting"
),
2018-01-06 17:12:54 +01:00
option(
'address', 'string', '', 0, 0, '',
"Hostname or IP address for the server"
),
2018-01-06 17:12:54 +01:00
option(
'port', 'integer', '', 0, 65535, '8448',
"Port for the server"
),
2018-01-06 17:12:54 +01:00
option(
'username', 'string', '', 0, 0, '',
"Username to use on server"
),
2018-01-06 17:12:54 +01:00
option(
'password', 'string', '', 0, 0, '',
"Password for server"
),
]
section = W.config_search_section(config_file, 'server')
for option in options:
option_name = "{server}.{option}".format(
server=self.name, option=option.name)
2018-01-06 17:12:54 +01:00
self.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, "",
"", "server_config_change_cb", self.name, "", "")
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
2018-01-06 17:12:54 +01:00
# TODO explain why these type gymnastics are needed
# pylint: disable=protected-access
if isinstance(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
2017-12-30 14:03:03 +01:00
# TODO add the other relevant 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
2018-01-06 17:12:54 +01:00
def handle_http_response(server, message):
# type: (MatrixServer, MatrixMessage) -> None
2017-12-30 14:03:03 +01:00
status_code = message.response.status
# TODO handle error responses
# TODO handle try again response
if status_code == 200:
# TODO json.loads can fail
response = json.loads(message.response.body, encoding='utf-8')
2018-01-06 17:12:54 +01:00
handle_matrix_message(server, message.type, response)
2017-12-30 14:03:03 +01:00
else:
2018-01-06 17:12:54 +01:00
server_buffer_prnt(
server,
"ERROR IN HTTP RESPONSE {status_code}".format(
status_code=status_code))
server_buffer_prnt(server, message.request.request)
server_buffer_prnt(server, message.response.body)
2017-12-30 14:03:03 +01:00
return
def handle_room_info(server, room_info):
# type: (MatrixServer, Dict) -> None
2017-12-30 14:03:03 +01:00
def create_buffer(roomd_id, alias=None):
if not alias:
alias = "#{id}".format(id=room_id)
buf = W.buffer_new(
alias,
2018-01-03 12:14:24 +01:00
"room_input_cb",
server.name,
2018-01-03 12:14:24 +01:00
"room_close_cb",
server.name
2017-12-30 14:03:03 +01:00
)
# TODO set the buffer type dynamically
W.buffer_set(buf, "localvar_set_type", 'channel')
W.buffer_set(buf, "type", 'formated')
W.buffer_set(buf, "localvar_set_channel", alias)
# TODO set the nick dynamically
W.buffer_set(buf, "localvar_set_nick", 'poljar')
W.buffer_set(buf, "localvar_set_server", "matrix.org")
# TODO put this in a function
2018-01-06 17:12:54 +01:00
short_name = alias.rsplit(":", 1)[0]
2017-12-30 14:03:03 +01:00
W.buffer_set(buf, "short_name", short_name)
server.buffers[room_id] = buf
2017-12-30 14:03:03 +01:00
def handle_aliases(room_id, event):
if room_id not in server.buffers:
2017-12-30 14:03:03 +01:00
alias = event['content']['aliases'][-1]
create_buffer(room_id, alias)
def handle_members(room_id, event):
if event['membership'] == 'join':
try:
buf = server.buffers[room_id]
2017-12-30 14:03:03 +01:00
except KeyError:
event_queue.append(event)
return
W.buffer_set(buf, "nicklist", "1")
W.buffer_set(buf, "nicklist_display_groups", "0")
# create nicklists for the current channel if they don't exist
# if they do, use the existing pointer
2018-01-03 12:14:24 +01:00
# TODO move this into the buffer creation
2017-12-30 14:03:03 +01:00
here = W.nicklist_search_group(buf, '', NICK_GROUP_HERE)
nick = event['content']['displayname']
if not here:
2018-01-03 12:14:24 +01:00
here = W.nicklist_add_group(
buf,
'',
NICK_GROUP_HERE,
"weechat.color.nicklist_group",
1
)
2017-12-30 14:03:03 +01:00
W.nicklist_add_nick(buf, here, nick, "", "", "", 1)
def handle_room_state(state_events):
for event in state_events:
if event['type'] == 'm.room.aliases':
handle_aliases(room_id, event)
elif event['type'] == 'm.room.member':
handle_members(room_id, event)
elif event['type'] == 'm.room.message':
message_queue.append(event)
def handle_room_timeline(timeline_events):
for event in timeline_events:
if event['type'] == 'm.room.aliases':
handle_aliases(room_id, event)
elif event['type'] == 'm.room.member':
handle_members(room_id, event)
elif event['type'] == 'm.room.message':
message_queue.append(event)
def handle_text_message(room_id, event):
msg = event['content']['body']
# TODO put this in a function or lambda
msg_author = event['sender'].rsplit(":", 1)[0][1:]
data = "{author}\t{msg}".format(author=msg_author, msg=msg)
event_id = event['event_id']
event_id = "matrix_id_{id}".format(id=event_id)
msg_age = event['unsigned']['age']
2018-01-06 17:12:54 +01:00
now = time.time()
msg_date = int(now - (msg_age / 1000))
buf = server.buffers[room_id]
2017-12-30 14:03:03 +01:00
# TODO if this is an initial sync tag the messages as backlog
tag = "nick_{a},{event_id},irc_privmsg,notify_message".format(
2018-01-06 17:12:54 +01:00
a=msg_author, event_id=event_id)
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
W.prnt_date_tags(buf, msg_date, tag, data)
2017-12-30 14:03:03 +01:00
for room_id, room in room_info['join'].iteritems():
# TODO do we need these queues or can we just rename the buffer if and
# when we get an alias dynamically?
event_queue = deque() # type: Deque[Dict]
message_queue = deque() # type: Deque[Dict]
2017-12-30 14:03:03 +01:00
handle_room_state(room['state']['events'])
handle_room_timeline(room['timeline']['events'])
# The room doesn't have an alias, create it now using the room id
if room_id not in server.buffers:
2017-12-30 14:03:03 +01:00
create_buffer(room_id)
# TODO we don't need a separate event/message queue here
while event_queue:
event = event_queue.popleft()
if event['type'] == 'm.room.member':
handle_members(room_id, event)
else:
2018-01-06 17:12:54 +01:00
assert "Wrong event type in event queue"
2017-12-30 14:03:03 +01:00
while message_queue:
event = message_queue.popleft()
if event['type'] == 'm.room.message':
# TODO print out that there was an redacted message here
if 'redacted_by' in event['unsigned']:
continue
2017-12-30 14:03:03 +01:00
if event['content']['msgtype'] == 'm.text':
handle_text_message(room_id, event)
# TODO handle different content types here
else:
server_buffer_prnt(
server,
2018-01-06 17:12:54 +01:00
"Handling of content type {type} not implemented".format(
type=event['content']['type'])
)
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
def handle_matrix_message(server, message_type, response):
# type: (MatrixServer, MessageType, Dict[Any, Any]) -> None
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
if message_type is MessageType.LOGIN:
server.access_token = response["access_token"]
message = generate_matrix_request(server, MessageType.SYNC)
send_or_queue(server, message)
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
elif message_type is MessageType.SYNC:
next_batch = response['next_batch']
2017-12-30 14:03:03 +01:00
# we got the same batch again, nothing to do
if next_batch == server.next_batch:
2017-12-30 14:03:03 +01:00
return
2018-01-06 17:12:54 +01:00
room_info = response['rooms']
handle_room_info(server, room_info)
2017-12-30 14:03:03 +01:00
server.next_batch = next_batch
2017-12-30 14:03:03 +01:00
else:
2018-01-06 17:12:54 +01:00
server_buffer_prnt(
server,
"Handling of message type {type} not implemented".format(
type=message_type))
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
def generate_matrix_request(server, message_type, room_id=None, data=None):
# type: (MatrixServer, MessageType, unicode, Dict[Any, Any]) -> MatrixMessage
2018-01-03 12:14:24 +01:00
# TODO clean this up
2018-01-06 17:12:54 +01:00
if message_type == MessageType.LOGIN:
2017-12-30 14:03:03 +01:00
path = '/_matrix/client/r0/login'
post_data = {"type": "m.login.password",
"user": server.user,
"password": server.password}
2017-12-30 14:03:03 +01:00
request = server.builder.request(path, post_data)
2017-12-30 14:03:03 +01:00
return MatrixMessage(MessageType.LOGIN, request, None)
2018-01-06 17:12:54 +01:00
elif message_type == MessageType.SYNC:
path = '/_matrix/client/r0/sync?access_token={access_token}'.format(
access_token=server.access_token)
2017-12-30 14:03:03 +01:00
if server.next_batch:
2018-01-06 17:12:54 +01:00
path = path + '&since={next_batch}'.format(
next_batch=server.next_batch)
2017-12-30 14:03:03 +01:00
request = server.builder.request(path)
2017-12-30 14:03:03 +01:00
return MatrixMessage(MessageType.SYNC, request, None)
2018-01-06 17:12:54 +01:00
elif message_type == MessageType.POST_MSG:
path = '/_matrix/client/r0/rooms/{room}/send/m.room.message?access_token={access_token}'.format(room=room_id, access_token=server.access_token)
request = server.builder.request(path, data)
2017-12-30 14:03:03 +01:00
return MatrixMessage(MessageType.POST_MSG, request, None)
else:
2018-01-06 17:12:54 +01:00
assert "Incorrect message type"
2017-12-30 14:03:03 +01:00
return None
2018-01-03 12:14:24 +01:00
def matrix_login(server):
# type: (MatrixServer) -> None
message = generate_matrix_request(server, MessageType.LOGIN)
send_or_queue(server, message)
2017-12-30 14:03:03 +01:00
def send_or_queue(server, message):
2018-01-03 12:14:24 +01:00
# type: (MatrixServer, MatrixMessage) -> None
if not send(server, message):
2018-01-06 17:12:54 +01:00
server.send_queue.append(message)
2017-12-30 14:03:03 +01:00
def send(server, message):
# type: (MatrixServer, MatrixMessage) -> bool
2017-12-30 14:03:03 +01:00
request = message.request.request
payload = message.request.payload
try:
server.socket.sendall(bytes(request, 'utf-8'))
2017-12-30 14:03:03 +01:00
if payload:
server.socket.sendall(bytes(payload, 'utf-8'))
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
server.receive_queue.append(message)
2017-12-30 14:03:03 +01:00
return True
2018-01-06 17:12:54 +01:00
except socket.error as error:
disconnect(server)
2018-01-06 17:12:54 +01:00
server_buffer_prnt(server, str(error))
2017-12-30 14:03:03 +01:00
return False
2018-01-07 15:46:18 +01:00
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]
if not server.connected:
server_buffer_prnt(server, "NOT CONNECTED WHILE RECEIVING")
2017-12-30 14:03:03 +01:00
# can this happen?
# do reconnection
while True:
try:
data = server.socket.recv(4096)
2017-12-30 14:03:03 +01:00
# TODO add the other relevant exceptions
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
2018-01-06 17:12:54 +01:00
message = server.receive_queue.popleft()
server.send_queue.appendleft(message)
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
server_buffer_prnt(server, error)
return W.WEECHAT_RC_OK
2017-12-30 14:03:03 +01:00
if not data:
server_buffer_prnt(server, "No data while reading")
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)
# Message done, reset the parser state.
2018-01-06 17:12:54 +01:00
server.http_parser = HttpParser()
server.http_buffer = []
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
def disconnect(server):
# type: (MatrixServer) -> None
if server.fd_hook:
W.unhook(server.fd_hook)
2017-12-30 14:03:03 +01:00
server.fd_hook = None
server.socket = None
server.connected = False
2018-01-03 12:14:24 +01:00
server_buffer_prnt(server, "Disconnected")
2018-01-03 12:14:24 +01:00
def server_buffer_prnt(server, string):
# type: (MatrixServer, unicode) -> None
2018-01-06 17:12:54 +01:00
assert server.server_buffer
buffer = server.server_buffer
now = int(time.time())
W.prnt_date_tags(buffer, now, "", string)
2018-01-03 12:14:24 +01:00
def create_server_buffer(server):
# type: (MatrixServer) -> None
server.server_buffer = W.buffer_new(
server.name,
"server_buffer_cb",
server.name,
2018-01-03 12:14:24 +01:00
"",
""
)
# TODO the nick and server name should be dynamic
W.buffer_set(server.server_buffer, "localvar_set_type", 'server')
W.buffer_set(server.server_buffer, "localvar_set_nick", 'poljar')
W.buffer_set(server.server_buffer, "localvar_set_server", server.name)
W.buffer_set(server.server_buffer, "localvar_set_channel", server.name)
2018-01-03 12:14:24 +01:00
# TODO this should go into the matrix config section
if W.config_string(W.config_get('irc.look.server_buffer')) == 'merge_with_core':
W.buffer_merge(server.server_buffer, W.buffer_search_main())
2017-12-30 14:03:03 +01:00
# TODO if we're reconnecting we should retry even if there was an error on the
# socket creation
@utf8_decode
def connect_cb(data, status, gnutls_rc, sock, error, ip_address):
2018-01-06 17:12:54 +01:00
# pylint: disable=too-many-arguments
2017-12-30 14:03:03 +01:00
status_value = int(status) # type: long
2018-01-03 12:14:24 +01:00
server = SERVERS[data]
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
# pylint: disable=too-many-branches
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-07 15:46:18 +01:00
server.fd_hook = hook
server.connected = True
server.connecting = False
server.reconnect_count = 0
2018-01-03 12:14:24 +01:00
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:
reconnect(server)
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_ADDRESS_NOT_FOUND:
W.prnt("", '{address} not found'.format(address=ip_address))
elif status_value == W.WEECHAT_HOOK_CONNECT_IP_ADDRESS_NOT_FOUND:
W.prnt("", 'IP address not found')
elif status_value == W.WEECHAT_HOOK_CONNECT_CONNECTION_REFUSED:
W.prnt("", 'Connection refused')
elif status_value == W.WEECHAT_HOOK_CONNECT_PROXY_ERROR:
W.prnt("", 'Proxy fails to establish connection to server')
elif status_value == W.WEECHAT_HOOK_CONNECT_LOCAL_HOSTNAME_ERROR:
W.prnt("", 'Unable to set local hostname')
elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_INIT_ERROR:
W.prnt("", 'TLS init error')
elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_HANDSHAKE_ERROR:
W.prnt("", 'TLS Handshake failed')
elif status_value == W.WEECHAT_HOOK_CONNECT_MEMORY_ERROR:
W.prnt("", 'Not enough memory')
elif status_value == W.WEECHAT_HOOK_CONNECT_TIMEOUT:
W.prnt("", 'Timeout')
elif status_value == W.WEECHAT_HOOK_CONNECT_SOCKET_ERROR:
W.prnt("", 'Unable to create socket')
else:
W.prnt("", 'Unexpected error: {status}'.format(status=status_value))
return W.WEECHAT_RC_OK
def reconnect(server):
# type: (MatrixServer) -> None
2018-01-07 15:46:18 +01:00
# TODO this needs some more work, do we want a reconnecting flag?
server.connecting = True
2018-01-06 17:12:54 +01:00
timeout = server.reconnect_count * 5 * 1000
2017-12-30 14:03:03 +01:00
if timeout > 0:
2018-01-06 17:12:54 +01:00
server_buffer_prnt(
server,
"Reconnecting in {timeout} seconds.".format(
timeout=timeout / 1000))
W.hook_timer(timeout, 0, 1, "reconnect_cb", server.name)
2017-12-30 14:03:03 +01:00
else:
connect(server)
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
server.reconnect_count += 1
2017-12-30 14:03:03 +01:00
@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
def connect(server):
# type: (MatrixServer) -> bool
if not server.address or not server.port:
message = "{prefix}Server address or port not set".format(
prefix=W.prefix("error"))
W.prnt("", message)
return False
if not server.user or not server.password:
message = "{prefix}User or password not set".format(
prefix=W.prefix("error"))
W.prnt("", message)
return False
if server.connected:
return True
2018-01-03 12:14:24 +01:00
if not server.server_buffer:
create_server_buffer(server)
2018-01-07 15:46:18 +01:00
if not server.timer_hook:
server.timer_hook = W.hook_timer(
1 * 1000,
0,
0,
"matrix_timer_cb",
server.name
)
2018-01-03 12:14:24 +01:00
W.hook_connect("", server.address, server.port, 1, 0, "",
"connect_cb", server.name)
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
# TODO put this in a function
room_id = list(server.buffers.keys())[list(server.buffers.values()).index(buffer)]
2017-12-30 14:03:03 +01:00
body = {"msgtype": "m.text", "body": input_data}
message = generate_matrix_request(server, MessageType.POST_MSG,
data=body, room_id=room_id)
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
@utf8_decode
def matrix_timer_cb(server_name, remaining_calls):
server = SERVERS[server_name]
if not server.connected:
if not server.connecting:
server_buffer_prnt(server, "Reconnecting timeout blaaaa")
reconnect(server)
2017-12-30 14:03:03 +01:00
return W.WEECHAT_RC_OK
2018-01-06 17:12:54 +01:00
while server.send_queue:
message = server.send_queue.popleft()
2017-12-30 14:03:03 +01:00
if not send(server, message):
2017-12-30 14:03:03 +01:00
# We got an error while sending the last message return the message
# to the queue and exit the loop
2018-01-06 17:12:54 +01:00
server.send_queue.appendleft(message)
2017-12-30 14:03:03 +01:00
break
2018-01-06 17:12:54 +01:00
for message in server.message_queue:
server_buffer_prnt(
server,
"Handling message: {message}".format(message=message))
2017-12-30 14:03:03 +01:00
2018-01-03 12:14:24 +01:00
# TODO don't send this out here, if a SYNC fails for some reason (504 try
# again!) we'll hammer the server unnecessarily
if server.next_batch:
message = generate_matrix_request(server, MessageType.SYNC)
2018-01-06 17:12:54 +01:00
server.send_queue.append(message)
2017-12-30 14:03:03 +01:00
return W.WEECHAT_RC_OK
2018-01-03 12:14:24 +01:00
@utf8_decode
def matrix_config_reload_cb(data, config_file):
return W.WEECHAT_RC_OK
2018-01-06 17:12:54 +01:00
@utf8_decode
def matrix_config_server_read_cb(
2018-01-06 17:12:54 +01:00
data, config_file, section,
option_name, value
):
2018-01-06 17:12:54 +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:
2018-01-06 17:12:54 +01:00
return_code = W.config_option_set(server.options[option], value, 1)
2018-01-06 17:12:54 +01:00
# TODO print out error message in case of erroneous return_code
2018-01-06 17:12:54 +01:00
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
2018-01-03 12:14:24 +01:00
def init_matrix_config():
config_file = W.config_new("matrix", "matrix_config_reload_cb", "")
section = W.config_new_section(config_file, "color", 0, 0, "", "", "", "",
2018-01-06 17:12:54 +01:00
"", "", "", "", "", "")
2018-01-03 12:14:24 +01:00
# TODO color options
section = W.config_new_section(config_file, "look", 0, 0, "", "", "", "",
2018-01-06 17:12:54 +01:00
"", "", "", "", "", "")
2018-01-03 12:14:24 +01:00
# TODO look options
2018-01-06 17:12:54 +01:00
section = W.config_new_section(config_file, "network", 0, 0, "", "", "",
"", "", "", "", "", "", "")
2018-01-03 12:14:24 +01:00
# TODO network options
2018-01-06 17:12:54 +01:00
W.config_new_section(
config_file, "server",
0, 0,
"matrix_config_server_read_cb",
"",
"matrix_config_server_write_cb",
"", "", "", "", "", "", ""
)
2018-01-03 12:14:24 +01:00
return config_file
def read_matrix_config():
# type: () -> bool
2018-01-06 17:12:54 +01:00
return_code = W.config_read(CONFIG)
if return_code == weechat.WEECHAT_CONFIG_READ_OK:
2018-01-03 12:14:24 +01:00
return True
2018-01-06 17:12:54 +01:00
elif return_code == weechat.WEECHAT_CONFIG_READ_MEMORY_ERROR:
2018-01-03 12:14:24 +01:00
return False
2018-01-06 17:12:54 +01:00
elif return_code == weechat.WEECHAT_CONFIG_READ_FILE_NOT_FOUND:
2018-01-03 12:14:24 +01:00
return True
else:
return False
2018-01-03 12:14:24 +01:00
@utf8_decode
def matrix_unload_cb():
for section in ["network", "look", "color", "server"]:
2018-01-06 17:12:54 +01:00
section_pointer = W.config_search_section(CONFIG, section)
W.config_section_free_options(section_pointer)
W.config_section_free(section_pointer)
2018-01-03 12:14:24 +01:00
W.config_free(CONFIG)
return W.WEECHAT_RC_OK
2018-01-07 15:46:18 +01:00
def check_server_existence(server_name, servers):
if not server_name in servers:
message = "{prefix}matrix: No such server: {server} found".format(
prefix=W.prefix("error"), server=server_name)
W.prnt("", message)
return False
else:
return True
2018-01-03 12:14:24 +01:00
@utf8_decode
def matrix_server_command_cb(data, buffer, args):
2018-01-07 15:46:18 +01:00
def connect_server(args):
for server_name in args:
if check_server_existence(server_name, SERVERS):
server = SERVERS[server_name]
connect(server)
def disconnect_server(args):
for server_name in args:
if check_server_existence(server_name, SERVERS):
server = SERVERS[server_name]
W.unhook(server.timer_hook)
server.timer_hook = None
disconnect(server)
2018-01-03 12:14:24 +01:00
def list_servers():
if SERVERS:
W.prnt("", "\nAll matrix servers:")
for server in SERVERS.keys():
W.prnt("", " {color}{server}".format(
color=W.color("yellow"),
server=server
))
2018-01-06 17:12:54 +01:00
# TODO
def list_full_servers(servers):
W.prnt("", "\nCommand not implemented")
2018-01-07 15:46:18 +01:00
def delete_server(args):
for server_name in args:
if check_server_existence(server_name, SERVERS):
server = SERVERS[server_name]
if server.connected:
message = ("{prefix}matrix: you can not delete server "
"\"{server}\" because you are connected to it. "
"Try \"/matrix disconnect {server}\" "
"before.").format(prefix=W.prefix("error"),
server=server.name)
W.prnt("", message)
return
for buf in server.buffers.values():
W.buffer_close(buf)
W.buffer_close(server.server_buffer)
for option in server.options.values():
W.config_option_free(option)
message = "matrix: server {server} has been deleted".format(
server=server.name)
del SERVERS[server.name]
server = None
W.prnt("", message)
2018-01-06 17:12:54 +01:00
# TODO
2018-01-07 15:46:18 +01:00
def add_server(args):
pass
2018-01-06 17:12:54 +01:00
split_args = args.split(' ', 2)
command, args = split_args[0], split_args[1:]
2018-01-03 12:14:24 +01:00
if command == 'connect':
2018-01-06 17:12:54 +01:00
connect_server(args)
elif command == 'disconnect':
disconnect_server(args)
elif command == 'reconnect':
disconnect_server(args)
connect_server(args)
elif command == 'server':
subcommand, args = args[0], args[1:]
if subcommand == 'list':
list_servers()
2018-01-07 15:46:18 +01:00
2018-01-06 17:12:54 +01:00
if subcommand == 'listfull':
list_full_servers(args)
2018-01-07 15:46:18 +01:00
elif subcommand == 'add':
# TODO allow setting the address and port
SERVERS[args[0]] = MatrixServer(args[0], CONFIG)
2018-01-07 15:46:18 +01:00
elif subcommand == 'delete':
delete_server(args)
else:
print("Unknown subcommand")
2018-01-03 12:14:24 +01:00
else:
print("Unknown command")
return W.WEECHAT_RC_OK
def add_servers_to_completion(completion):
for server_name in SERVERS.keys():
W.hook_completion_list_add(
completion,
server_name,
0,
weechat.WEECHAT_LIST_POS_SORT
)
@utf8_decode
2018-01-06 17:12:54 +01:00
def server_command_completion_cb(data, completion_item, buffer, completion):
buffer_input = weechat.buffer_get_string(buffer, "input").split()
2018-01-06 17:12:54 +01:00
args = buffer_input[1:]
commands = ['add', 'delete', 'list', 'listfull', 'rename']
def complete_commands():
for command in commands:
W.hook_completion_list_add(
completion,
command,
0,
weechat.WEECHAT_LIST_POS_SORT
)
if len(args) == 1:
complete_commands()
elif len(args) == 2:
if args[1] not in commands:
complete_commands()
else:
2018-01-06 17:12:54 +01:00
if args[1] == 'delete' or args[1] == 'listfull':
add_servers_to_completion(completion)
elif len(args) == 3:
2018-01-06 17:12:54 +01:00
if args[1] == 'delete' or args[1] == 'listfull':
if args[2] not in SERVERS.keys():
add_servers_to_completion(completion)
return W.WEECHAT_RC_OK
def matrix_server_completion_cb(data, completion_item, buffer, completion):
add_servers_to_completion(completion)
return W.WEECHAT_RC_OK
def create_default_server(config_file):
server = MatrixServer('matrix.org', config_file)
SERVERS[server.name] = server
W.config_option_set(server.options["address"], "localhost", 1)
return True
2018-01-03 12:14:24 +01:00
2018-01-06 17:12:54 +01:00
def init_hooks():
W.hook_completion(
"matrix_server_commands", "Matrix server completion",
"server_command_completion_cb", "")
W.hook_completion(
"matrix_servers", "Matrix server completion",
"matrix_server_completion_cb", "")
W.hook_command(
# Command name and short description
'matrix', 'Matrix chat protocol command',
# Synopsis
'server add <server-name> <hostname>[:<port>] ||' +
'server delete|list|listfull <server-name> ||' +
'server rename <server-name> <new-name> ||' +
'connect <server-name> ||' +
'disconnect <server-name> ||' +
'reconnect <server-name>',
# Description
' server: list, add, remove, or rename Matrix servers' + '\n' +
' connect: connect to Matrix servers' + '\n' +
'disconnect: disconnect from one or all Matrix servers' + '\n' +
' reconnect: reconnect to server(s)' + '\n' +
'\nUse /matrix help [command] to find out more\n',
# Completions
'server %(matrix_server_commands)|%* ||' +
'connect %(matrix_servers) ||' +
'disconnect %(matrix_servers) ||' +
'reconnect %(matrix_servers)',
# Function name
'matrix_server_command_cb', '')
def autoconnect(servers):
for server in servers.values():
if server.autoconnect:
connect(server)
2017-12-30 14:03:03 +01:00
if __name__ == "__main__":
W = WeechatWrapper(weechat)
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.
CONFIG = init_matrix_config()
read_matrix_config()
2018-01-06 17:12:54 +01:00
init_hooks()
if not SERVERS:
create_default_server(CONFIG)
2018-01-06 17:12:54 +01:00
autoconnect(SERVERS)