2718 lines
86 KiB
Python
2718 lines
86 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
import json
|
|
import socket
|
|
import ssl
|
|
import time
|
|
import datetime
|
|
import re
|
|
|
|
# pylint: disable=redefined-builtin
|
|
from builtins import bytes
|
|
|
|
from collections import deque, Mapping, Iterable, namedtuple
|
|
from operator import itemgetter
|
|
from enum import Enum, unique
|
|
from functools import wraps
|
|
|
|
# pylint: disable=unused-import
|
|
from typing import (List, Set, Dict, Tuple, Text, Optional, AnyStr, Deque, Any)
|
|
|
|
from http_parser.pyparser import HttpParser
|
|
|
|
# pylint: disable=import-error
|
|
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
|
|
WEECHAT_SCRIPT_LICENSE = "MIT" # type: unicode
|
|
|
|
MATRIX_API_PATH = "/_matrix/client/r0" # type: unicode
|
|
|
|
SERVERS = dict() # type: Dict[unicode, MatrixServer]
|
|
CONFIG = None # type: weechat.config
|
|
GLOBAL_OPTIONS = None # type: PluginOptions
|
|
|
|
|
|
# Unicode handling
|
|
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))
|
|
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))
|
|
return data
|
|
|
|
|
|
def utf8_decode(function):
|
|
"""
|
|
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(function)
|
|
def wrapper(*args, **kwargs):
|
|
return function(*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)
|
|
return decode_from_utf8(orig_attr)
|
|
|
|
# Ensure all lines sent to weechat specify a prefix. For lines after the
|
|
# 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
|
|
SEND = 2
|
|
STATE = 3
|
|
REDACT = 4
|
|
ROOM_MSG = 5
|
|
|
|
|
|
@unique
|
|
class RequestType(Enum):
|
|
GET = 0
|
|
POST = 1
|
|
PUT = 2
|
|
|
|
|
|
@unique
|
|
class RedactType(Enum):
|
|
STRIKETHROUGH = 0
|
|
NOTICE = 1
|
|
DELETE = 2
|
|
|
|
|
|
@unique
|
|
class ServerBufferType(Enum):
|
|
MERGE_CORE = 0
|
|
MERGE = 1
|
|
INDEPENDENT = 2
|
|
|
|
|
|
Option = namedtuple(
|
|
'Option', [
|
|
'name',
|
|
'type',
|
|
'string_values',
|
|
'min',
|
|
'max',
|
|
'value',
|
|
'description'
|
|
])
|
|
|
|
|
|
class PluginOptions:
|
|
def __init__(self):
|
|
self.redaction_type = RedactType.STRIKETHROUGH # type: RedactType
|
|
self.look_server_buf = ServerBufferType.MERGE_CORE # type: ServerBufferType
|
|
|
|
self.sync_limit = 30 # type: int
|
|
self.backlog_limit = 10 # type: int
|
|
self.enable_backlog = True # type: bool
|
|
self.page_up_hook = None # type: weechat.hook
|
|
|
|
self.redaction_comp_len = 50 # type: int
|
|
|
|
self.options = dict() # type: Dict[unicode, weechat.config_option]
|
|
|
|
|
|
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_type, # type: RequestType
|
|
host, # type: unicode
|
|
port, # type: int
|
|
location, # type: unicode
|
|
data=None, # type: Dict[unicode, Any]
|
|
user_agent='weechat-matrix/{version}'.format(
|
|
version=WEECHAT_SCRIPT_VERSION) # type: unicode
|
|
):
|
|
# type: (...) -> None
|
|
host_string = ':'.join([host, str(port)])
|
|
|
|
user_agent = 'User-Agent: {agent}'.format(agent=user_agent)
|
|
host_header = 'Host: {host}'.format(host=host_string)
|
|
request_list = [] # type: List[unicode]
|
|
accept_header = 'Accept: */*' # type: unicode
|
|
end_separator = '\r\n' # type: unicode
|
|
payload = None # type: unicode
|
|
|
|
if request_type == RequestType.GET:
|
|
get = 'GET {location} HTTP/1.1'.format(location=location)
|
|
request_list = [get, host_header,
|
|
user_agent, accept_header, end_separator]
|
|
|
|
elif (request_type == RequestType.POST or
|
|
request_type == RequestType.PUT):
|
|
|
|
json_data = json.dumps(data, separators=(',', ':'))
|
|
|
|
if request_type == RequestType.POST:
|
|
method = "POST"
|
|
else:
|
|
method = "PUT"
|
|
|
|
request_line = '{method} {location} HTTP/1.1'.format(
|
|
method=method,
|
|
location=location
|
|
)
|
|
|
|
type_header = 'Content-Type: application/x-www-form-urlencoded'
|
|
length_header = 'Content-Length: {length}'.format(
|
|
length=len(json_data)
|
|
)
|
|
|
|
request_list = [request_line, host_header,
|
|
user_agent, accept_header,
|
|
length_header, type_header, end_separator]
|
|
payload = json_data
|
|
|
|
request = '\r\n'.join(request_list)
|
|
|
|
self.request = request
|
|
self.payload = payload
|
|
|
|
|
|
def get_transaction_id(server):
|
|
# type: (MatrixServer) -> int
|
|
transaction_id = server.transaction_id
|
|
server.transaction_id += 1
|
|
return transaction_id
|
|
|
|
|
|
class MatrixMessage:
|
|
def __init__(
|
|
self,
|
|
server, # type: MatrixServer
|
|
message_type, # type: MessageType
|
|
room_id=None, # type: unicode
|
|
extra_id=None, # type: unicode
|
|
data=None, # type: Dict[unicode, Any]
|
|
extra_data=None # type: Dict[unicode, Any]
|
|
):
|
|
# type: (...) -> None
|
|
self.type = message_type # MessageType
|
|
self.request = None # HttpRequest
|
|
self.response = None # HttpResponse
|
|
self.extra_data = extra_data # Dict[unicode, Any]
|
|
|
|
if message_type == MessageType.LOGIN:
|
|
path = ("{api}/login").format(api=MATRIX_API_PATH)
|
|
self.request = HttpRequest(
|
|
RequestType.POST,
|
|
server.address,
|
|
server.port,
|
|
path,
|
|
data
|
|
)
|
|
|
|
elif message_type == MessageType.SYNC:
|
|
sync_filter = {
|
|
"room": {
|
|
"timeline": {"limit": GLOBAL_OPTIONS.sync_limit}
|
|
}
|
|
}
|
|
|
|
path = ("{api}/sync?access_token={access_token}&"
|
|
"filter={sync_filter}").format(
|
|
api=MATRIX_API_PATH,
|
|
access_token=server.access_token,
|
|
sync_filter=json.dumps(sync_filter,
|
|
separators=(',', ':')))
|
|
|
|
if server.next_batch:
|
|
path = path + '&since={next_batch}'.format(
|
|
next_batch=server.next_batch)
|
|
|
|
self.request = HttpRequest(
|
|
RequestType.GET,
|
|
server.address,
|
|
server.port,
|
|
path
|
|
)
|
|
|
|
elif message_type == MessageType.SEND:
|
|
path = ("{api}/rooms/{room}/send/m.room.message/{tx_id}?"
|
|
"access_token={access_token}").format(
|
|
api=MATRIX_API_PATH,
|
|
room=room_id,
|
|
tx_id=get_transaction_id(server),
|
|
access_token=server.access_token)
|
|
|
|
self.request = HttpRequest(
|
|
RequestType.PUT,
|
|
server.address,
|
|
server.port,
|
|
path,
|
|
data
|
|
)
|
|
|
|
elif message_type == MessageType.STATE:
|
|
path = ("{api}/rooms/{room}/state/{event_type}?"
|
|
"access_token={access_token}").format(
|
|
api=MATRIX_API_PATH,
|
|
room=room_id,
|
|
event_type=extra_id,
|
|
access_token=server.access_token)
|
|
|
|
self.request = HttpRequest(
|
|
RequestType.PUT,
|
|
server.address,
|
|
server.port,
|
|
path,
|
|
data
|
|
)
|
|
|
|
elif message_type == MessageType.REDACT:
|
|
path = ("{api}/rooms/{room}/redact/{event_id}/{tx_id}?"
|
|
"access_token={access_token}").format(
|
|
api=MATRIX_API_PATH,
|
|
room=room_id,
|
|
event_id=extra_id,
|
|
tx_id=get_transaction_id(server),
|
|
access_token=server.access_token)
|
|
|
|
self.request = HttpRequest(
|
|
RequestType.PUT,
|
|
server.address,
|
|
server.port,
|
|
path,
|
|
data
|
|
)
|
|
|
|
elif message_type == MessageType.ROOM_MSG:
|
|
path = ("{api}/rooms/{room}/messages?from={prev_batch}&"
|
|
"dir=b&limit={message_limit}&"
|
|
"access_token={access_token}").format(
|
|
api=MATRIX_API_PATH,
|
|
room=room_id,
|
|
prev_batch=extra_id,
|
|
message_limit=GLOBAL_OPTIONS.backlog_limit,
|
|
access_token=server.access_token)
|
|
self.request = HttpRequest(
|
|
RequestType.GET,
|
|
server.address,
|
|
server.port,
|
|
path,
|
|
)
|
|
|
|
|
|
class MatrixUser:
|
|
def __init__(self, name, display_name):
|
|
self.name = name # type: unicode
|
|
self.display_name = display_name # type: unicode
|
|
self.power_level = 0 # type: int
|
|
self.nick_color = "" # type: unicode
|
|
self.prefix = "" # type: unicode
|
|
|
|
|
|
class MatrixRoom:
|
|
def __init__(self, room_id):
|
|
# type: (unicode) -> None
|
|
self.room_id = room_id # type: unicode
|
|
self.alias = room_id # type: unicode
|
|
self.topic = "" # type: unicode
|
|
self.topic_author = "" # type: unicode
|
|
self.topic_date = None # type: datetime.datetime
|
|
self.prev_batch = "" # type: unicode
|
|
self.users = dict() # type: Dict[unicode, MatrixUser]
|
|
|
|
|
|
def key_from_value(dictionary, value):
|
|
# type: (Dict[unicode, Any], Any) -> unicode
|
|
return list(dictionary.keys())[list(dictionary.values()).index(value)]
|
|
|
|
|
|
@utf8_decode
|
|
def server_config_change_cb(server_name, option):
|
|
# type: (unicode, 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)
|
|
|
|
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 == "ssl_verify":
|
|
value = W.config_boolean(option)
|
|
if value:
|
|
server.ssl_context.check_hostname = True
|
|
server.ssl_context.verify_mode = ssl.CERT_REQUIRED
|
|
else:
|
|
server.ssl_context.check_hostname = False
|
|
server.ssl_context.verify_mode = ssl.CERT_NONE
|
|
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
|
|
|
|
|
|
class MatrixServer:
|
|
# pylint: disable=too-many-instance-attributes
|
|
def __init__(self, name, config_file):
|
|
# type: (unicode, weechat.config) -> None
|
|
self.name = name # type: unicode
|
|
self.user_id = ""
|
|
self.address = "" # type: unicode
|
|
self.port = 8448 # type: int
|
|
self.options = dict() # type: Dict[unicode, weechat.config]
|
|
|
|
self.user = "" # type: unicode
|
|
self.password = "" # type: unicode
|
|
|
|
self.rooms = dict() # type: Dict[unicode, MatrixRoom]
|
|
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
|
|
self.numeric_address = "" # type: unicode
|
|
|
|
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
|
|
|
|
self.access_token = None # type: unicode
|
|
self.next_batch = None # type: unicode
|
|
self.transaction_id = 0 # type: int
|
|
|
|
self.http_parser = HttpParser() # type: HttpParser
|
|
self.http_buffer = [] # type: List[bytes]
|
|
|
|
# Queue of messages we need to send off.
|
|
self.send_queue = deque() # type: Deque[MatrixMessage]
|
|
# Queue of messages we send off and are waiting a response for
|
|
self.receive_queue = deque() # type: Deque[MatrixMessage]
|
|
self.message_queue = deque() # type: Deque[MatrixMessage]
|
|
self.ignore_event_list = [] # type: List[unicode]
|
|
|
|
self._create_options(config_file)
|
|
|
|
def _create_options(self, config_file):
|
|
options = [
|
|
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"
|
|
),
|
|
Option(
|
|
'ssl_verify', 'boolean', '', 0, 0, 'on',
|
|
(
|
|
"Check that the SSL connection is fully trusted"
|
|
"is starting"
|
|
)
|
|
),
|
|
Option(
|
|
'username', 'string', '', 0, 0, '',
|
|
"Username to use on server"
|
|
),
|
|
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)
|
|
|
|
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, "", "")
|
|
|
|
|
|
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
|
|
)
|
|
|
|
# TODO explain why these type gymnastics are needed
|
|
# pylint: disable=protected-access
|
|
if isinstance(temp_socket, socket._socket.socket):
|
|
# pylint: disable=no-member
|
|
sock = socket._socketobject(_sock=temp_socket)
|
|
else:
|
|
sock = temp_socket
|
|
|
|
try:
|
|
ssl_socket = server.ssl_context.wrap_socket(
|
|
sock,
|
|
server_hostname=server.address) # type: ssl.SSLSocket
|
|
|
|
return ssl_socket
|
|
# TODO add the other relevant exceptions
|
|
except ssl.SSLError as error:
|
|
server_buffer_prnt(server, str(error))
|
|
return None
|
|
|
|
|
|
def handle_http_response(server, message):
|
|
# type: (MatrixServer, MatrixMessage) -> None
|
|
|
|
assert message.response
|
|
|
|
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')
|
|
matrix_handle_message(
|
|
server,
|
|
message.type,
|
|
response,
|
|
message.extra_data
|
|
)
|
|
else:
|
|
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.request.payload)
|
|
server_buffer_prnt(server, message.response.body)
|
|
|
|
return
|
|
|
|
|
|
def strip_matrix_server(string):
|
|
# type: (unicode) -> unicode
|
|
return string.rsplit(":", 1)[0]
|
|
|
|
|
|
def add_user_to_nicklist(buf, user):
|
|
group_name = "999|..."
|
|
|
|
if user.power_level >= 100:
|
|
group_name = "000|o"
|
|
elif user.power_level >= 50:
|
|
group_name = "001|h"
|
|
elif user.power_level > 0:
|
|
group_name = "002|v"
|
|
|
|
group = W.nicklist_search_group(buf, "", group_name)
|
|
# TODO make it configurable so we can use a display name or user_id here
|
|
W.nicklist_add_nick(
|
|
buf,
|
|
group,
|
|
user.display_name,
|
|
user.nick_color,
|
|
user.prefix,
|
|
get_prefix_color(user.prefix),
|
|
1
|
|
)
|
|
|
|
|
|
def matrix_create_room_buffer(server, room_id):
|
|
# type: (MatrixServer, unicode) -> None
|
|
buf = W.buffer_new(
|
|
room_id,
|
|
"room_input_cb",
|
|
server.name,
|
|
"room_close_cb",
|
|
server.name
|
|
)
|
|
|
|
W.buffer_set(buf, "localvar_set_type", 'channel')
|
|
W.buffer_set(buf, "type", 'formated')
|
|
|
|
W.buffer_set(buf, "localvar_set_channel", room_id)
|
|
|
|
W.buffer_set(buf, "localvar_set_nick", server.user)
|
|
|
|
W.buffer_set(buf, "localvar_set_server", server.name)
|
|
|
|
short_name = strip_matrix_server(room_id)
|
|
W.buffer_set(buf, "short_name", short_name)
|
|
|
|
W.nicklist_add_group(buf, '', "000|o", "weechat.color.nicklist_group", 1)
|
|
W.nicklist_add_group(buf, '', "001|h", "weechat.color.nicklist_group", 1)
|
|
W.nicklist_add_group(buf, '', "002|v", "weechat.color.nicklist_group", 1)
|
|
W.nicklist_add_group(buf, '', "999|...", "weechat.color.nicklist_group", 1)
|
|
|
|
W.buffer_set(buf, "nicklist", "1")
|
|
W.buffer_set(buf, "nicklist_display_groups", "0")
|
|
|
|
server.buffers[room_id] = buf
|
|
server.rooms[room_id] = MatrixRoom(room_id)
|
|
|
|
|
|
def matrix_handle_room_aliases(server, room_id, event):
|
|
# type: (MatrixServer, unicode, Dict[unicode, Any]) -> None
|
|
buf = server.buffers[room_id]
|
|
room = server.rooms[room_id]
|
|
|
|
alias = event['content']['aliases'][-1]
|
|
|
|
if not alias:
|
|
return
|
|
|
|
short_name = strip_matrix_server(alias)
|
|
|
|
room.alias = alias
|
|
W.buffer_set(buf, "name", alias)
|
|
W.buffer_set(buf, "short_name", short_name)
|
|
W.buffer_set(buf, "localvar_set_channel", alias)
|
|
|
|
|
|
def matrix_handle_room_members(server, room_id, event):
|
|
# type: (MatrixServer, unicode, Dict[unicode, Any]) -> None
|
|
buf = server.buffers[room_id]
|
|
room = server.rooms[room_id]
|
|
|
|
# TODO print out a informational message
|
|
if event['membership'] == 'join':
|
|
# TODO set the buffer type to a channel if we have more than 2 users
|
|
display_name = event['content']['displayname']
|
|
full_name = event['sender']
|
|
short_name = strip_matrix_server(full_name)[1:]
|
|
|
|
user = MatrixUser(short_name, display_name)
|
|
|
|
if full_name == server.user_id:
|
|
user.nick_color = "weechat.color.chat_nick_self"
|
|
W.buffer_set(
|
|
buf,
|
|
"highlight_words",
|
|
",".join([full_name, user.name, user.display_name]))
|
|
else:
|
|
user.nick_color = W.info_get("nick_color_name", user.name)
|
|
|
|
room.users[full_name] = user
|
|
|
|
nick_pointer = W.nicklist_search_nick(buf, "", user.display_name)
|
|
if not nick_pointer:
|
|
add_user_to_nicklist(buf, user)
|
|
else:
|
|
# TODO we can get duplicate display names
|
|
pass
|
|
|
|
elif event['membership'] == 'leave':
|
|
full_name = event['sender']
|
|
user = room.users[full_name]
|
|
nick_pointer = W.nicklist_search_nick(buf, "", user.display_name)
|
|
if nick_pointer:
|
|
W.nicklist_remove_nick(buf, nick_pointer)
|
|
|
|
del room.users[full_name]
|
|
|
|
|
|
def date_from_age(age):
|
|
# type: (float) -> int
|
|
now = time.time()
|
|
date = int(now - (age / 1000))
|
|
return date
|
|
|
|
|
|
def color_for_tags(color):
|
|
if color == "weechat.color.chat_nick_self":
|
|
option = weechat.config_get(color)
|
|
return weechat.config_string(option)
|
|
return color
|
|
|
|
|
|
def matrix_handle_room_text_message(server, room_id, event, old=False):
|
|
# type: (MatrixServer, unicode, Dict[unicode, Any], bool) -> None
|
|
tag = ""
|
|
msg_author = ""
|
|
nick_color_name = ""
|
|
|
|
room = server.rooms[room_id]
|
|
msg = event['content']['body']
|
|
|
|
if event['sender'] in room.users:
|
|
user = room.users[event['sender']]
|
|
msg_author = user.display_name
|
|
nick_color_name = user.nick_color
|
|
else:
|
|
msg_author = strip_matrix_server(event['sender'])[1:]
|
|
nick_color_name = W.info_get("nick_color_name", msg_author)
|
|
|
|
data = "{author}\t{msg}".format(author=msg_author, msg=msg)
|
|
|
|
event_id = event['event_id']
|
|
|
|
msg_date = date_from_age(event['unsigned']['age'])
|
|
|
|
# TODO if this is an initial sync tag the messages as backlog
|
|
# TODO handle self messages from other devices
|
|
if old:
|
|
tag = ("nick_{a},prefix_nick_{color},matrix_id_{event_id},"
|
|
"matrix_message,notify_message,no_log,no_highlight").format(
|
|
a=msg_author,
|
|
color=color_for_tags(nick_color_name),
|
|
event_id=event_id)
|
|
else:
|
|
tag = ("nick_{a},prefix_nick_{color},matrix_id_{event_id},"
|
|
"matrix_message,notify_message,log1").format(
|
|
a=msg_author,
|
|
color=color_for_tags(nick_color_name),
|
|
event_id=event_id)
|
|
|
|
buf = server.buffers[room_id]
|
|
W.prnt_date_tags(buf, msg_date, tag, data)
|
|
|
|
|
|
def matrix_handle_redacted_message(server, room_id, event):
|
|
# type: (MatrixServer, unicode, Dict[Any, Any]) -> None
|
|
reason = ""
|
|
room = server.rooms[room_id]
|
|
|
|
# TODO check if the message is already printed out, in that case we got the
|
|
# message a second time and a redaction event will take care of it.
|
|
censor = event['unsigned']['redacted_because']['sender']
|
|
nick_color_name = ""
|
|
|
|
if censor in room.users:
|
|
user = room.users[censor]
|
|
nick_color_name = user.nick_color
|
|
censor = ("{nick_color}{nick}{ncolor} {del_color}"
|
|
"({host_color}{full_name}{ncolor}{del_color})").format(
|
|
nick_color=W.color(nick_color_name),
|
|
nick=user.display_name,
|
|
ncolor=W.color("reset"),
|
|
del_color=W.color("chat_delimiters"),
|
|
host_color=W.color("chat_host"),
|
|
full_name=censor)
|
|
else:
|
|
censor = strip_matrix_server(censor)[1:]
|
|
nick_color_name = W.info_get("nick_color_name", censor)
|
|
censor = "{color}{censor}{ncolor}".format(
|
|
color=W.color(nick_color_name),
|
|
censor=censor,
|
|
ncolor=W.color("reset"))
|
|
|
|
if 'reason' in event['unsigned']['redacted_because']['content']:
|
|
reason = ", reason: \"{reason}\"".format(
|
|
reason=event['unsigned']['redacted_because']['content']['reason'])
|
|
|
|
msg = ("{del_color}<{log_color}Message redacted by: "
|
|
"{censor}{log_color}{reason}{del_color}>{ncolor}").format(
|
|
del_color=W.color("chat_delimiters"),
|
|
ncolor=W.color("reset"),
|
|
log_color=W.color("logger.color.backlog_line"),
|
|
censor=censor,
|
|
reason=reason)
|
|
|
|
msg_author = strip_matrix_server(event['sender'])[1:]
|
|
|
|
data = "{author}\t{msg}".format(author=msg_author, msg=msg)
|
|
|
|
event_id = event['event_id']
|
|
|
|
msg_date = date_from_age(event['unsigned']['age'])
|
|
|
|
tag = ("nick_{a},prefix_nick_{color},matrix_id_{event_id},"
|
|
"matrix_message,matrix_redacted,"
|
|
"notify_message,no_highlight").format(
|
|
a=msg_author,
|
|
color=color_for_tags(nick_color_name),
|
|
event_id=event_id)
|
|
|
|
buf = server.buffers[room_id]
|
|
W.prnt_date_tags(buf, msg_date, tag, data)
|
|
|
|
|
|
def matrix_handle_room_messages(server, room_id, event, old=False):
|
|
# type: (MatrixServer, unicode, Dict[unicode, Any], bool) -> None
|
|
if event['type'] == 'm.room.message':
|
|
if 'redacted_by' in event['unsigned']:
|
|
matrix_handle_redacted_message(server, room_id, event)
|
|
return
|
|
|
|
if event['content']['msgtype'] == 'm.text':
|
|
matrix_handle_room_text_message(server, room_id, event, old)
|
|
|
|
# TODO handle different content types here
|
|
else:
|
|
message = ("{prefix}Handling of content type "
|
|
"{type} not implemented").format(
|
|
type=event['content']['msgtype'],
|
|
prefix=W.prefix("error"))
|
|
W.prnt(server.server_buffer, message)
|
|
|
|
|
|
def event_id_from_tags(tags):
|
|
# type: (List[unicode]) -> unicode
|
|
for tag in tags:
|
|
if tag.startswith("matrix_id"):
|
|
return tag[10:]
|
|
|
|
return ""
|
|
|
|
|
|
def matrix_redact_line(data, tags, event):
|
|
reason = ""
|
|
|
|
hdata_line_data = W.hdata_get('line_data')
|
|
|
|
message = W.hdata_string(hdata_line_data, data, 'message')
|
|
censor = strip_matrix_server(event['sender'])[1:]
|
|
|
|
if 'reason' in event['content']:
|
|
reason = ", reason: \"{reason}\"".format(
|
|
reason=event['content']['reason'])
|
|
|
|
redaction_msg = ("{del_color}<{log_color}Message redacted by: "
|
|
"{censor}{log_color}{reason}{del_color}>{ncolor}").format(
|
|
del_color=W.color("chat_delimiters"),
|
|
ncolor=W.color("reset"),
|
|
log_color=W.color("logger.color.backlog_line"),
|
|
censor=censor,
|
|
reason=reason)
|
|
|
|
if GLOBAL_OPTIONS.redaction_type == RedactType.STRIKETHROUGH:
|
|
message = "".join(["{}\u0336".format(c) for c in message])
|
|
message = message + " " + redaction_msg
|
|
elif GLOBAL_OPTIONS.redaction_type == RedactType.DELETE:
|
|
message = redaction_msg
|
|
elif GLOBAL_OPTIONS.redaction_type == RedactType.NOTICE:
|
|
message = message + " " + redaction_msg
|
|
|
|
tags.append("matrix_new_redacted")
|
|
|
|
new_data = {'tags_array': tags,
|
|
'message': message}
|
|
|
|
W.hdata_update(hdata_line_data, data, new_data)
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
def matrix_handle_room_redaction(server, room_id, event):
|
|
buf = server.buffers[room_id]
|
|
event_id = event['redacts']
|
|
|
|
own_lines = W.hdata_pointer(W.hdata_get('buffer'), buf, 'own_lines')
|
|
|
|
if own_lines:
|
|
hdata_line = W.hdata_get('line')
|
|
|
|
line = W.hdata_pointer(
|
|
W.hdata_get('lines'),
|
|
own_lines,
|
|
'last_line'
|
|
)
|
|
|
|
while line:
|
|
data = W.hdata_pointer(hdata_line, line, 'data')
|
|
|
|
if data:
|
|
tags = tags_from_line_data(data)
|
|
|
|
message_id = event_id_from_tags(tags)
|
|
|
|
if event_id == message_id:
|
|
# If the message is already redacted there is nothing to do
|
|
if ("matrix_redacted" not in tags and
|
|
"matrix_new_redacted" not in tags):
|
|
matrix_redact_line(data, tags, event)
|
|
return W.WEECHAT_RC_OK
|
|
|
|
line = W.hdata_move(hdata_line, line, -1)
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
def get_prefix_for_level(level):
|
|
# type: (int) -> unicode
|
|
if level >= 100:
|
|
return "&"
|
|
elif level >= 50:
|
|
return "@"
|
|
elif level > 0:
|
|
return "+"
|
|
return ""
|
|
|
|
|
|
# TODO make this configurable
|
|
def get_prefix_color(prefix):
|
|
# type: (unicode) -> unicode
|
|
if prefix == "&":
|
|
return "lightgreen"
|
|
elif prefix == "@":
|
|
return "lightgreen"
|
|
elif prefix == "+":
|
|
return "yellow"
|
|
return ""
|
|
|
|
|
|
def matrix_handle_room_power_levels(server, room_id, event):
|
|
if not event['content']['users']:
|
|
return
|
|
|
|
buf = server.buffers[room_id]
|
|
room = server.rooms[room_id]
|
|
|
|
for full_name, level in event['content']['users'].items():
|
|
if full_name not in room.users:
|
|
continue
|
|
|
|
user = room.users[full_name]
|
|
user.power_level = level
|
|
user.prefix = get_prefix_for_level(level)
|
|
|
|
nick_pointer = W.nicklist_search_nick(buf, "", user.display_name)
|
|
W.nicklist_remove_nick(buf, nick_pointer)
|
|
add_user_to_nicklist(buf, user)
|
|
|
|
|
|
def matrix_handle_room_events(server, room_id, room_events):
|
|
# type: (MatrixServer, unicode, Dict[Any, Any]) -> None
|
|
for event in room_events:
|
|
if event['event_id'] in server.ignore_event_list:
|
|
server.ignore_event_list.remove(event['event_id'])
|
|
continue
|
|
|
|
if event['type'] == 'm.room.aliases':
|
|
matrix_handle_room_aliases(server, room_id, event)
|
|
|
|
elif event['type'] == 'm.room.member':
|
|
matrix_handle_room_members(server, room_id, event)
|
|
|
|
elif event['type'] == 'm.room.message':
|
|
matrix_handle_room_messages(server, room_id, event)
|
|
|
|
elif event['type'] == 'm.room.topic':
|
|
buf = server.buffers[room_id]
|
|
room = server.rooms[room_id]
|
|
topic = event['content']['topic']
|
|
|
|
room.topic = topic
|
|
room.topic_author = event['sender']
|
|
|
|
topic_age = event['unsigned']['age']
|
|
room.topic_date = datetime.datetime.fromtimestamp(
|
|
time.time() - (topic_age / 1000))
|
|
|
|
W.buffer_set(buf, "title", topic)
|
|
|
|
nick_color = W.info_get("nick_color_name", room.topic_author)
|
|
author = room.topic_author
|
|
|
|
if author in room.users:
|
|
user = room.users[author]
|
|
nick_color = user.nick_color
|
|
author = user.display_name
|
|
|
|
author = ("{nick_color}{user}{ncolor}").format(
|
|
nick_color=W.color(nick_color),
|
|
user=author,
|
|
ncolor=W.color("reset"))
|
|
|
|
# TODO nick and topic color
|
|
# TODO print old topic if configured so
|
|
# TODO nick display name if configured so and found
|
|
message = ("{prefix}{nick} has changed "
|
|
"the topic for {chan_color}{room}{ncolor} "
|
|
"to \"{topic}\"").format(
|
|
prefix=W.prefix("network"),
|
|
nick=author,
|
|
chan_color=W.color("chat_channel"),
|
|
ncolor=W.color("reset"),
|
|
room=strip_matrix_server(room.alias),
|
|
topic=topic)
|
|
|
|
tags = "matrix_topic,no_highlight,log3,matrix_id_{event_id}".format(
|
|
event_id=event['event_id'])
|
|
|
|
date = date_from_age(topic_age)
|
|
|
|
W.prnt_date_tags(buf, date, tags, message)
|
|
|
|
elif event['type'] == "m.room.redaction":
|
|
matrix_handle_room_redaction(server, room_id, event)
|
|
|
|
elif event["type"] == "m.room.power_levels":
|
|
matrix_handle_room_power_levels(server, room_id, event)
|
|
|
|
elif event["type"] in ["m.room.create", "m.room.join_rules",
|
|
"m.room.history_visibility",
|
|
"m.room.canonical_alias"]:
|
|
pass
|
|
|
|
else:
|
|
message = ("{prefix}Handling of room event type "
|
|
"{type} not implemented").format(
|
|
type=event['type'],
|
|
prefix=W.prefix("error"))
|
|
W.prnt(server.server_buffer, message)
|
|
|
|
|
|
def matrix_handle_room_info(server, room_info):
|
|
# type: (MatrixServer, Dict) -> None
|
|
for room_id, room in room_info['join'].iteritems():
|
|
if not room_id:
|
|
continue
|
|
|
|
if room_id not in server.buffers:
|
|
matrix_create_room_buffer(server, room_id)
|
|
|
|
if not server.rooms[room_id].prev_batch:
|
|
server.rooms[room_id].prev_batch = room['timeline']['prev_batch']
|
|
|
|
matrix_handle_room_events(server, room_id, room['state']['events'])
|
|
matrix_handle_room_events(server, room_id, room['timeline']['events'])
|
|
|
|
|
|
def matrix_sort_old_messages(server, room_id):
|
|
lines = []
|
|
buf = server.buffers[room_id]
|
|
|
|
own_lines = W.hdata_pointer(W.hdata_get('buffer'), buf, 'own_lines')
|
|
|
|
if own_lines:
|
|
hdata_line = W.hdata_get('line')
|
|
hdata_line_data = W.hdata_get('line_data')
|
|
line = W.hdata_pointer(
|
|
W.hdata_get('lines'),
|
|
own_lines,
|
|
'first_line'
|
|
)
|
|
|
|
while line:
|
|
data = W.hdata_pointer(hdata_line, line, 'data')
|
|
|
|
line_data = {}
|
|
|
|
if data:
|
|
date = W.hdata_time(hdata_line_data, data, 'date')
|
|
print_date = W.hdata_time(hdata_line_data, data,
|
|
'date_printed')
|
|
tags = tags_from_line_data(data)
|
|
prefix = W.hdata_string(hdata_line_data, data, 'prefix')
|
|
message = W.hdata_string(hdata_line_data, data,
|
|
'message')
|
|
|
|
line_data = {'date': date,
|
|
'date_printed': print_date,
|
|
'tags_array': ','.join(tags),
|
|
'prefix': prefix,
|
|
'message': message}
|
|
|
|
lines.append(line_data)
|
|
|
|
line = W.hdata_move(hdata_line, line, 1)
|
|
|
|
sorted_lines = sorted(lines, key=itemgetter('date'))
|
|
lines = []
|
|
|
|
# We need to convert the dates to a string for hdata_update(), this
|
|
# will reverse the list at the same time
|
|
while sorted_lines:
|
|
line = sorted_lines.pop()
|
|
new_line = {k: unicode(v) for k, v in line.items()}
|
|
lines.append(new_line)
|
|
|
|
matrix_update_buffer_lines(lines, own_lines)
|
|
|
|
|
|
def matrix_update_buffer_lines(new_lines, own_lines):
|
|
hdata_line = W.hdata_get('line')
|
|
hdata_line_data = W.hdata_get('line_data')
|
|
|
|
line = W.hdata_pointer(
|
|
W.hdata_get('lines'),
|
|
own_lines,
|
|
'first_line'
|
|
)
|
|
|
|
while line:
|
|
data = W.hdata_pointer(hdata_line, line, 'data')
|
|
|
|
if data:
|
|
W.hdata_update(hdata_line_data, data, new_lines.pop())
|
|
|
|
line = W.hdata_move(hdata_line, line, 1)
|
|
|
|
|
|
def matrix_handle_old_messages(server, room_id, events):
|
|
for event in events:
|
|
if event['type'] == 'm.room.message':
|
|
matrix_handle_room_messages(server, room_id, event, old=True)
|
|
# TODO do we wan't to handle topics joins/quits here?
|
|
else:
|
|
pass
|
|
|
|
matrix_sort_old_messages(server, room_id)
|
|
|
|
|
|
def matrix_handle_message(
|
|
server, # type: MatrixServer
|
|
message_type, # type: MessageType
|
|
response, # type: Dict[unicode, Any]
|
|
extra_data # type: Dict[unicode, Any]
|
|
):
|
|
# type: (...) -> None
|
|
|
|
if message_type is MessageType.LOGIN:
|
|
server.access_token = response["access_token"]
|
|
server.user_id = response["user_id"]
|
|
message = MatrixMessage(server, MessageType.SYNC)
|
|
send_or_queue(server, message)
|
|
|
|
elif message_type is MessageType.SYNC:
|
|
next_batch = response['next_batch']
|
|
|
|
# we got the same batch again, nothing to do
|
|
if next_batch == server.next_batch:
|
|
return
|
|
|
|
room_info = response['rooms']
|
|
matrix_handle_room_info(server, room_info)
|
|
|
|
server.next_batch = next_batch
|
|
|
|
elif message_type is MessageType.SEND:
|
|
author = extra_data["author"]
|
|
message = extra_data["message"]
|
|
room_id = extra_data["room_id"]
|
|
date = int(time.time())
|
|
event_id = response["event_id"]
|
|
|
|
# This message will be part of the next sync, we already printed it out
|
|
# so ignore it in the sync.
|
|
server.ignore_event_list.append(event_id)
|
|
|
|
tag = ("notify_none,no_highlight,self_msg,log1,nick_{a},"
|
|
"prefix_nick_{color},matrix_id_{event_id},"
|
|
"matrix_message").format(
|
|
a=author,
|
|
color=color_for_tags(weechat.color.chat_nick_self),
|
|
event_id=event_id)
|
|
|
|
data = "{author}\t{msg}".format(author=author, msg=message)
|
|
|
|
buf = server.buffers[room_id]
|
|
W.prnt_date_tags(buf, date, tag, data)
|
|
|
|
elif message_type == MessageType.ROOM_MSG:
|
|
# Response has no messages, that is we already got the oldest message
|
|
# in a previous request, nothing to do
|
|
if not response['chunk']:
|
|
return
|
|
|
|
room_id = response['chunk'][0]['room_id']
|
|
room = server.rooms[room_id]
|
|
|
|
matrix_handle_old_messages(server, room_id, response['chunk'])
|
|
|
|
room.prev_batch = response['end']
|
|
|
|
# Nothing to do here, we'll handle state changes and redactions in the sync
|
|
elif (message_type == MessageType.STATE or
|
|
message_type == MessageType.REDACT):
|
|
pass
|
|
|
|
else:
|
|
server_buffer_prnt(
|
|
server,
|
|
"Handling of message type {type} not implemented".format(
|
|
type=message_type))
|
|
|
|
|
|
def matrix_login(server):
|
|
# type: (MatrixServer) -> None
|
|
post_data = {"type": "m.login.password",
|
|
"user": server.user,
|
|
"password": server.password}
|
|
|
|
message = MatrixMessage(
|
|
server,
|
|
MessageType.LOGIN,
|
|
data=post_data
|
|
)
|
|
send_or_queue(server, message)
|
|
|
|
|
|
def send_or_queue(server, message):
|
|
# type: (MatrixServer, MatrixMessage) -> None
|
|
if not send(server, message):
|
|
server.send_queue.append(message)
|
|
|
|
|
|
def send(server, message):
|
|
# type: (MatrixServer, MatrixMessage) -> bool
|
|
|
|
request = message.request.request
|
|
payload = message.request.payload
|
|
|
|
try:
|
|
server.socket.sendall(bytes(request, 'utf-8'))
|
|
if payload:
|
|
server.socket.sendall(bytes(payload, 'utf-8'))
|
|
|
|
server.receive_queue.append(message)
|
|
return True
|
|
|
|
except socket.error as error:
|
|
disconnect(server)
|
|
server_buffer_prnt(server, str(error))
|
|
return False
|
|
|
|
|
|
@utf8_decode
|
|
def receive_cb(server_name, file_descriptor):
|
|
server = SERVERS[server_name]
|
|
|
|
if not server.connected:
|
|
server_buffer_prnt(server, "NOT CONNECTED WHILE RECEIVING")
|
|
# can this happen?
|
|
# do reconnection
|
|
|
|
while True:
|
|
try:
|
|
data = server.socket.recv(4096)
|
|
# TODO add the other relevant exceptions
|
|
except ssl.SSLWantReadError:
|
|
break
|
|
except socket.error as error:
|
|
disconnect(server)
|
|
|
|
# Queue the failed message for resending
|
|
message = server.receive_queue.popleft()
|
|
server.send_queue.appendleft(message)
|
|
|
|
server_buffer_prnt(server, error)
|
|
return W.WEECHAT_RC_OK
|
|
|
|
if not data:
|
|
server_buffer_prnt(server, "No data while reading")
|
|
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)
|
|
|
|
# Message done, reset the parser state.
|
|
server.http_parser = HttpParser()
|
|
server.http_buffer = []
|
|
|
|
handle_http_response(server, message)
|
|
break
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
def disconnect(server):
|
|
# type: (MatrixServer) -> None
|
|
if server.fd_hook:
|
|
W.unhook(server.fd_hook)
|
|
|
|
server.fd_hook = None
|
|
server.socket = None
|
|
server.connected = False
|
|
|
|
server_buffer_prnt(server, "Disconnected")
|
|
|
|
|
|
def server_buffer_prnt(server, string):
|
|
# type: (MatrixServer, unicode) -> None
|
|
assert server.server_buffer
|
|
buffer = server.server_buffer
|
|
now = int(time.time())
|
|
W.prnt_date_tags(buffer, now, "", string)
|
|
|
|
|
|
def server_buffer_set_title(server):
|
|
# type: (MatrixServer) -> None
|
|
if server.numeric_address:
|
|
ip_string = " ({address})".format(address=server.numeric_address)
|
|
else:
|
|
ip_string = ""
|
|
|
|
title = ("Matrix: {address}/{port}{ip}").format(
|
|
address=server.address,
|
|
port=server.port,
|
|
ip=ip_string)
|
|
|
|
W.buffer_set(server.server_buffer, "title", title)
|
|
|
|
|
|
def create_server_buffer(server):
|
|
# type: (MatrixServer) -> None
|
|
server.server_buffer = W.buffer_new(
|
|
server.name,
|
|
"server_buffer_cb",
|
|
server.name,
|
|
"",
|
|
""
|
|
)
|
|
|
|
server_buffer_set_title(server)
|
|
W.buffer_set(server.server_buffer, "localvar_set_type", 'server')
|
|
W.buffer_set(server.server_buffer, "localvar_set_nick", server.user)
|
|
W.buffer_set(server.server_buffer, "localvar_set_server", server.name)
|
|
W.buffer_set(server.server_buffer, "localvar_set_channel", server.name)
|
|
|
|
# TODO merge without core
|
|
if GLOBAL_OPTIONS.look_server_buf == ServerBufferType.MERGE_CORE:
|
|
W.buffer_merge(server.server_buffer, W.buffer_search_main())
|
|
elif GLOBAL_OPTIONS.look_server_buf == ServerBufferType.MERGE:
|
|
pass
|
|
else:
|
|
pass
|
|
|
|
|
|
# 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):
|
|
# 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
|
|
)
|
|
|
|
server.fd_hook = hook
|
|
server.connected = True
|
|
server.connecting = False
|
|
server.reconnect_count = 0
|
|
server.numeric_address = ip_address
|
|
|
|
server_buffer_set_title(server)
|
|
server_buffer_prnt(server, "Connected")
|
|
|
|
if not server.access_token:
|
|
matrix_login(server)
|
|
else:
|
|
reconnect(server)
|
|
|
|
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
|
|
server.connecting = True
|
|
timeout = server.reconnect_count * 5 * 1000
|
|
|
|
if timeout > 0:
|
|
server_buffer_prnt(
|
|
server,
|
|
"Reconnecting in {timeout} seconds.".format(
|
|
timeout=timeout / 1000))
|
|
W.hook_timer(timeout, 0, 1, "reconnect_cb", server.name)
|
|
else:
|
|
connect(server)
|
|
|
|
server.reconnect_count += 1
|
|
|
|
|
|
@utf8_decode
|
|
def reconnect_cb(server_name, remaining):
|
|
server = SERVERS[server_name]
|
|
connect(server)
|
|
|
|
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
|
|
|
|
if not server.server_buffer:
|
|
create_server_buffer(server)
|
|
|
|
if not server.timer_hook:
|
|
server.timer_hook = W.hook_timer(
|
|
1 * 1000,
|
|
0,
|
|
0,
|
|
"matrix_timer_cb",
|
|
server.name
|
|
)
|
|
|
|
W.hook_connect("", server.address, server.port, 1, 0, "",
|
|
"connect_cb", server.name)
|
|
|
|
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)
|
|
body = {"msgtype": "m.text", "body": input_data}
|
|
extra_data = {
|
|
"author": server.user,
|
|
"message": input_data,
|
|
"room_id": room_id
|
|
}
|
|
|
|
message = MatrixMessage(server, 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_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)
|
|
return W.WEECHAT_RC_OK
|
|
|
|
while server.send_queue:
|
|
message = server.send_queue.popleft()
|
|
|
|
if not send(server, message):
|
|
# 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
|
|
|
|
for message in server.message_queue:
|
|
server_buffer_prnt(
|
|
server,
|
|
"Handling message: {message}".format(message=message))
|
|
|
|
# TODO don't send this out here, if a SYNC fails for some reason (504 try
|
|
# again!) we'll hammer the server unnecessarily, send it out after a
|
|
# successful sync or after a 504 sync with a proper timeout
|
|
if server.next_batch:
|
|
message = MatrixMessage(server, MessageType.SYNC)
|
|
server.send_queue.append(message)
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
@utf8_decode
|
|
def matrix_config_reload_cb(data, config_file):
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
@utf8_decode
|
|
def matrix_config_server_read_cb(
|
|
data, config_file, section,
|
|
option_name, value
|
|
):
|
|
|
|
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_change_cb(data, option):
|
|
option_name = key_from_value(GLOBAL_OPTIONS.options, option)
|
|
|
|
if option_name == "redactions":
|
|
GLOBAL_OPTIONS.redaction_type = RedactType(W.config_integer(option))
|
|
elif option_name == "server_buffer":
|
|
GLOBAL_OPTIONS.look_server_buf = ServerBufferType(
|
|
W.config_integer(option))
|
|
elif option_name == "max_initial_sync_events":
|
|
GLOBAL_OPTIONS.sync_limit = W.config_integer(option)
|
|
elif option_name == "max_backlog_sync_events":
|
|
GLOBAL_OPTIONS.backlog_limit = W.config_integer(option)
|
|
elif option_name == "fetch_backlog_on_pgup":
|
|
GLOBAL_OPTIONS.enable_backlog = W.config_boolean(option)
|
|
|
|
if GLOBAL_OPTIONS.enable_backlog:
|
|
if not GLOBAL_OPTIONS.page_up_hook:
|
|
hook_page_up()
|
|
else:
|
|
if GLOBAL_OPTIONS.page_up_hook:
|
|
W.unhook(GLOBAL_OPTIONS.page_up_hook)
|
|
GLOBAL_OPTIONS.page_up_hook = None
|
|
|
|
return 1
|
|
|
|
|
|
def init_matrix_config():
|
|
config_file = W.config_new("matrix", "matrix_config_reload_cb", "")
|
|
|
|
look_options = [
|
|
Option(
|
|
"redactions", "integer",
|
|
"strikethrough|notice|delete", 0, 0,
|
|
"strikethrough",
|
|
(
|
|
"Only notice redactions, strike through or delete "
|
|
"redacted messages"
|
|
)
|
|
),
|
|
Option(
|
|
"server_buffer", "integer",
|
|
"merge_with_core|merge_without_core|independent",
|
|
0, 0, "merge_with_core", "Merge server buffers"
|
|
)
|
|
]
|
|
|
|
network_options = [
|
|
Option(
|
|
"max_initial_sync_events", "integer",
|
|
"", 1, 10000,
|
|
"30",
|
|
(
|
|
"How many events to fetch during the initial sync"
|
|
)
|
|
),
|
|
Option(
|
|
"max_backlog_sync_events", "integer",
|
|
"", 1, 100,
|
|
"10",
|
|
(
|
|
"How many events to fetch during backlog fetching"
|
|
)
|
|
),
|
|
Option(
|
|
"fetch_backlog_on_pgup", "boolean",
|
|
"", 0, 0,
|
|
"on",
|
|
(
|
|
"Fetch messages in the backlog on a window page up event"
|
|
)
|
|
)
|
|
]
|
|
|
|
|
|
def add_global_options(section, options):
|
|
for option in options:
|
|
GLOBAL_OPTIONS.options[option.name] = W.config_new_option(
|
|
config_file, section, option.name,
|
|
option.type, option.description, option.string_values,
|
|
option.min, option.max, option.value, option.value, 0, "",
|
|
"", "matrix_config_change_cb", "", "", "")
|
|
|
|
section = W.config_new_section(config_file, "color", 0, 0, "", "", "", "",
|
|
"", "", "", "", "", "")
|
|
|
|
# TODO color options
|
|
|
|
section = W.config_new_section(config_file, "look", 0, 0, "", "", "", "",
|
|
"", "", "", "", "", "")
|
|
|
|
add_global_options(section, look_options)
|
|
|
|
section = W.config_new_section(config_file, "network", 0, 0, "", "", "",
|
|
"", "", "", "", "", "", "")
|
|
|
|
add_global_options(section, network_options)
|
|
|
|
W.config_new_section(
|
|
config_file, "server",
|
|
0, 0,
|
|
"matrix_config_server_read_cb",
|
|
"",
|
|
"matrix_config_server_write_cb",
|
|
"", "", "", "", "", "", ""
|
|
)
|
|
|
|
return config_file
|
|
|
|
|
|
def read_matrix_config():
|
|
# type: () -> bool
|
|
return_code = W.config_read(CONFIG)
|
|
if return_code == weechat.WEECHAT_CONFIG_READ_OK:
|
|
return True
|
|
elif return_code == weechat.WEECHAT_CONFIG_READ_MEMORY_ERROR:
|
|
return False
|
|
elif return_code == weechat.WEECHAT_CONFIG_READ_FILE_NOT_FOUND:
|
|
return True
|
|
return False
|
|
|
|
|
|
@utf8_decode
|
|
def matrix_unload_cb():
|
|
for section in ["network", "look", "color", "server"]:
|
|
section_pointer = W.config_search_section(CONFIG, section)
|
|
W.config_section_free_options(section_pointer)
|
|
W.config_section_free(section_pointer)
|
|
|
|
W.config_free(CONFIG)
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
def check_server_existence(server_name, servers):
|
|
if server_name not in servers:
|
|
message = "{prefix}matrix: No such server: {server} found".format(
|
|
prefix=W.prefix("error"), server=server_name)
|
|
W.prnt("", message)
|
|
return False
|
|
return True
|
|
|
|
|
|
def matrix_command_help(args):
|
|
if not args:
|
|
message = ("{prefix}matrix: Too few arguments for command "
|
|
"\"/matrix help\" (see the help for the command: "
|
|
"/matrix help help").format(prefix=W.prefix("error"))
|
|
W.prnt("", message)
|
|
return
|
|
|
|
for command in args:
|
|
message = ""
|
|
|
|
if command == "connect":
|
|
message = ("{delimiter_color}[{ncolor}matrix{delimiter_color}] "
|
|
"{ncolor}{cmd_color}/connect{ncolor} "
|
|
"<server-name> [<server-name>...]"
|
|
"\n\n"
|
|
"connect to Matrix server(s)"
|
|
"\n\n"
|
|
"server-name: server to connect to"
|
|
"(internal name)").format(
|
|
delimiter_color=W.color("chat_delimiters"),
|
|
cmd_color=W.color("chat_buffer"),
|
|
ncolor=W.color("reset"))
|
|
|
|
elif command == "disconnect":
|
|
message = ("{delimiter_color}[{ncolor}matrix{delimiter_color}] "
|
|
"{ncolor}{cmd_color}/disconnect{ncolor} "
|
|
"<server-name> [<server-name>...]"
|
|
"\n\n"
|
|
"disconnect from Matrix server(s)"
|
|
"\n\n"
|
|
"server-name: server to disconnect"
|
|
"(internal name)").format(
|
|
delimiter_color=W.color("chat_delimiters"),
|
|
cmd_color=W.color("chat_buffer"),
|
|
ncolor=W.color("reset"))
|
|
|
|
elif command == "reconnect":
|
|
message = ("{delimiter_color}[{ncolor}matrix{delimiter_color}] "
|
|
"{ncolor}{cmd_color}/reconnect{ncolor} "
|
|
"<server-name> [<server-name>...]"
|
|
"\n\n"
|
|
"reconnect to Matrix server(s)"
|
|
"\n\n"
|
|
"server-name: server to reconnect"
|
|
"(internal name)").format(
|
|
delimiter_color=W.color("chat_delimiters"),
|
|
cmd_color=W.color("chat_buffer"),
|
|
ncolor=W.color("reset"))
|
|
|
|
elif command == "server":
|
|
message = ("{delimiter_color}[{ncolor}matrix{delimiter_color}] "
|
|
"{ncolor}{cmd_color}/server{ncolor} "
|
|
"add <server-name> <hostname>[:<port>]"
|
|
"\n "
|
|
"delete|list|listfull <server-name>"
|
|
"\n\n"
|
|
"list, add, or remove Matrix servers"
|
|
"\n\n"
|
|
" list: list servers (without argument, this "
|
|
"list is displayed)\n"
|
|
" listfull: list servers with detailed info for each "
|
|
"server\n"
|
|
" add: add a new server\n"
|
|
" delete: delete a server\n"
|
|
"server-name: server to reconnect (internal name)\n"
|
|
" hostname: name or IP address of server\n"
|
|
" port: port of server (default: 8448)\n"
|
|
"\n"
|
|
"Examples:"
|
|
"\n /server listfull"
|
|
"\n /server add matrix matrix.org:80"
|
|
"\n /server del matrix").format(
|
|
delimiter_color=W.color("chat_delimiters"),
|
|
cmd_color=W.color("chat_buffer"),
|
|
ncolor=W.color("reset"))
|
|
|
|
elif command == "help":
|
|
message = ("{delimiter_color}[{ncolor}matrix{delimiter_color}] "
|
|
"{ncolor}{cmd_color}/help{ncolor} "
|
|
"<matrix-command> [<matrix-command>...]"
|
|
"\n\n"
|
|
"display help about Matrix commands"
|
|
"\n\n"
|
|
"matrix-command: a Matrix command name"
|
|
"(internal name)").format(
|
|
delimiter_color=W.color("chat_delimiters"),
|
|
cmd_color=W.color("chat_buffer"),
|
|
ncolor=W.color("reset"))
|
|
|
|
else:
|
|
message = ("{prefix}matrix: No help available, \"{command}\" "
|
|
"is not a matrix command").format(
|
|
prefix=W.prefix("error"),
|
|
command=command)
|
|
|
|
W.prnt("", "")
|
|
W.prnt("", message)
|
|
|
|
return
|
|
|
|
|
|
def matrix_server_command_listfull(args):
|
|
def get_value_string(value, default_value):
|
|
if value == default_value:
|
|
if not value:
|
|
value = "''"
|
|
value_string = " ({value})".format(value=value)
|
|
else:
|
|
value_string = "{color}{value}{ncolor}".format(
|
|
color=W.color("chat_value"),
|
|
value=value,
|
|
ncolor=W.color("reset"))
|
|
|
|
return value_string
|
|
|
|
for server_name in args:
|
|
if server_name not in SERVERS:
|
|
continue
|
|
|
|
server = SERVERS[server_name]
|
|
connected = ""
|
|
|
|
W.prnt("", "")
|
|
|
|
if server.connected:
|
|
connected = "connected"
|
|
else:
|
|
connected = "not connected"
|
|
|
|
message = ("Server: {server_color}{server}{delimiter_color}"
|
|
" [{ncolor}{connected}{delimiter_color}]"
|
|
"{ncolor}").format(
|
|
server_color=W.color("chat_server"),
|
|
server=server.name,
|
|
delimiter_color=W.color("chat_delimiters"),
|
|
connected=connected,
|
|
ncolor=W.color("reset"))
|
|
|
|
W.prnt("", message)
|
|
|
|
option = server.options["autoconnect"]
|
|
default_value = W.config_string_default(option)
|
|
value = W.config_string(option)
|
|
|
|
value_string = get_value_string(value, default_value)
|
|
message = " autoconnect. : {value}".format(value=value_string)
|
|
|
|
W.prnt("", message)
|
|
|
|
option = server.options["address"]
|
|
default_value = W.config_string_default(option)
|
|
value = W.config_string(option)
|
|
|
|
value_string = get_value_string(value, default_value)
|
|
message = " address. . . : {value}".format(value=value_string)
|
|
|
|
W.prnt("", message)
|
|
|
|
option = server.options["port"]
|
|
default_value = str(W.config_integer_default(option))
|
|
value = str(W.config_integer(option))
|
|
|
|
value_string = get_value_string(value, default_value)
|
|
message = " port . . . . : {value}".format(value=value_string)
|
|
|
|
W.prnt("", message)
|
|
|
|
option = server.options["username"]
|
|
default_value = W.config_string_default(option)
|
|
value = W.config_string(option)
|
|
|
|
value_string = get_value_string(value, default_value)
|
|
message = " username . . : {value}".format(value=value_string)
|
|
|
|
W.prnt("", message)
|
|
|
|
option = server.options["password"]
|
|
value = W.config_string(option)
|
|
|
|
if value:
|
|
value = "(hidden)"
|
|
|
|
value_string = get_value_string(value, '')
|
|
message = " password . . : {value}".format(value=value_string)
|
|
|
|
W.prnt("", message)
|
|
|
|
|
|
def matrix_server_command_delete(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 "
|
|
"{color}{server}{ncolor} because you are "
|
|
"connected to it. Try \"/matrix disconnect "
|
|
"{color}{server}{ncolor}\" before.").format(
|
|
prefix=W.prefix("error"),
|
|
color=W.color("chat_server"),
|
|
ncolor=W.color("reset"),
|
|
server=server.name)
|
|
W.prnt("", message)
|
|
return
|
|
|
|
for buf in server.buffers.values():
|
|
W.buffer_close(buf)
|
|
|
|
if server.server_buffer:
|
|
W.buffer_close(server.server_buffer)
|
|
|
|
for option in server.options.values():
|
|
W.config_option_free(option)
|
|
|
|
message = ("matrix: server {color}{server}{ncolor} has been "
|
|
"deleted").format(
|
|
server=server.name,
|
|
color=W.color("chat_server"),
|
|
ncolor=W.color("reset"))
|
|
|
|
del SERVERS[server.name]
|
|
server = None
|
|
|
|
W.prnt("", message)
|
|
|
|
|
|
def matrix_server_command_add(args):
|
|
if len(args) < 2:
|
|
message = ("{prefix}matrix: Too few arguments for command "
|
|
"\"/matrix server add\" (see the help for the command: "
|
|
"/matrix help server").format(prefix=W.prefix("error"))
|
|
W.prnt("", message)
|
|
return
|
|
elif len(args) > 4:
|
|
message = ("{prefix}matrix: Too many arguments for command "
|
|
"\"/matrix server add\" (see the help for the command: "
|
|
"/matrix help server").format(prefix=W.prefix("error"))
|
|
W.prnt("", message)
|
|
return
|
|
|
|
def remove_server(server):
|
|
for option in server.options.values():
|
|
W.config_option_free(option)
|
|
del SERVERS[server.name]
|
|
|
|
server_name = args[0]
|
|
|
|
if server_name in SERVERS:
|
|
message = ("{prefix}matrix: server {color}{server}{ncolor} "
|
|
"already exists, can't add it").format(
|
|
prefix=W.prefix("error"),
|
|
color=W.color("chat_server"),
|
|
server=server_name,
|
|
ncolor=W.color("reset"))
|
|
W.prnt("", message)
|
|
return
|
|
|
|
server = MatrixServer(args[0], CONFIG)
|
|
SERVERS[server.name] = server
|
|
|
|
if len(args) >= 2:
|
|
try:
|
|
host, port = args[1].split(":", 1)
|
|
except ValueError:
|
|
host, port = args[1], None
|
|
|
|
return_code = W.config_option_set(
|
|
server.options["address"],
|
|
host,
|
|
1
|
|
)
|
|
|
|
if return_code == W.WEECHAT_CONFIG_OPTION_SET_ERROR:
|
|
remove_server(server)
|
|
message = ("{prefix}Failed to set address for server "
|
|
"{color}{server}{ncolor}, failed to add "
|
|
"server.").format(
|
|
prefix=W.prefix("error"),
|
|
color=W.color("chat_server"),
|
|
server=server.name,
|
|
ncolor=W.color("reset"))
|
|
|
|
W.prnt("", message)
|
|
server = None
|
|
return
|
|
|
|
if port:
|
|
return_code = W.config_option_set(
|
|
server.options["port"],
|
|
port,
|
|
1
|
|
)
|
|
if return_code == W.WEECHAT_CONFIG_OPTION_SET_ERROR:
|
|
remove_server(server)
|
|
message = ("{prefix}Failed to set port for server "
|
|
"{color}{server}{ncolor}, failed to add "
|
|
"server.").format(
|
|
prefix=W.prefix("error"),
|
|
color=W.color("chat_server"),
|
|
server=server.name,
|
|
ncolor=W.color("reset"))
|
|
|
|
W.prnt("", message)
|
|
server = None
|
|
return
|
|
|
|
if len(args) >= 3:
|
|
user = args[2]
|
|
return_code = W.config_option_set(
|
|
server.options["username"],
|
|
user,
|
|
1
|
|
)
|
|
|
|
if return_code == W.WEECHAT_CONFIG_OPTION_SET_ERROR:
|
|
remove_server(server)
|
|
message = ("{prefix}Failed to set user for server "
|
|
"{color}{server}{ncolor}, failed to add "
|
|
"server.").format(
|
|
prefix=W.prefix("error"),
|
|
color=W.color("chat_server"),
|
|
server=server.name,
|
|
ncolor=W.color("reset"))
|
|
|
|
W.prnt("", message)
|
|
server = None
|
|
return
|
|
|
|
if len(args) == 4:
|
|
password = args[3]
|
|
|
|
return_code = W.config_option_set(
|
|
server.options["password"],
|
|
password,
|
|
1
|
|
)
|
|
if return_code == W.WEECHAT_CONFIG_OPTION_SET_ERROR:
|
|
remove_server(server)
|
|
message = ("{prefix}Failed to set password for server "
|
|
"{color}{server}{ncolor}, failed to add "
|
|
"server.").format(
|
|
prefix=W.prefix("error"),
|
|
color=W.color("chat_server"),
|
|
server=server.name,
|
|
ncolor=W.color("reset"))
|
|
W.prnt("", message)
|
|
server = None
|
|
return
|
|
|
|
message = ("matrix: server {color}{server}{ncolor} "
|
|
"has been added").format(
|
|
server=server.name,
|
|
color=W.color("chat_server"),
|
|
ncolor=W.color("reset"))
|
|
W.prnt("", message)
|
|
|
|
|
|
def matrix_server_command(command, args):
|
|
def list_servers(_):
|
|
if SERVERS:
|
|
W.prnt("", "\nAll matrix servers:")
|
|
for server in SERVERS:
|
|
W.prnt("", " {color}{server}".format(
|
|
color=W.color("chat_server"),
|
|
server=server
|
|
))
|
|
|
|
# TODO the argument for list and listfull is used as a match word to
|
|
# find/filter servers, we're currently match exactly to the whole name
|
|
if command == 'list':
|
|
list_servers(args)
|
|
elif command == 'listfull':
|
|
matrix_server_command_listfull(args)
|
|
elif command == 'add':
|
|
matrix_server_command_add(args)
|
|
elif command == 'delete':
|
|
matrix_server_command_delete(args)
|
|
else:
|
|
message = ("{prefix}matrix: Error: unknown matrix server command, "
|
|
"\"{command}\" (type /matrix help server for help)").format(
|
|
prefix=W.prefix("error"),
|
|
command=command)
|
|
W.prnt("", message)
|
|
|
|
|
|
@utf8_decode
|
|
def matrix_command_cb(data, buffer, args):
|
|
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)
|
|
|
|
split_args = list(filter(bool, args.split(' ')))
|
|
|
|
if len(split_args) < 1:
|
|
message = ("{prefix}matrix: Too few arguments for command "
|
|
"\"/matrix\" (see the help for the command: "
|
|
"/help matrix").format(prefix=W.prefix("error"))
|
|
W.prnt("", message)
|
|
return W.WEECHAT_RC_ERROR
|
|
|
|
command, args = split_args[0], split_args[1:]
|
|
|
|
if command == 'connect':
|
|
connect_server(args)
|
|
|
|
elif command == 'disconnect':
|
|
disconnect_server(args)
|
|
|
|
elif command == 'reconnect':
|
|
disconnect_server(args)
|
|
connect_server(args)
|
|
|
|
elif command == 'server':
|
|
if len(args) >= 1:
|
|
subcommand, args = args[0], args[1:]
|
|
matrix_server_command(subcommand, args)
|
|
else:
|
|
matrix_server_command("list", "")
|
|
|
|
elif command == 'help':
|
|
matrix_command_help(args)
|
|
|
|
else:
|
|
message = ("{prefix}matrix: Error: unknown matrix command, "
|
|
"\"{command}\" (type /help matrix for help)").format(
|
|
prefix=W.prefix("error"),
|
|
command=command)
|
|
W.prnt("", message)
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
def add_servers_to_completion(completion):
|
|
for server_name in SERVERS:
|
|
W.hook_completion_list_add(
|
|
completion,
|
|
server_name,
|
|
0,
|
|
weechat.WEECHAT_LIST_POS_SORT
|
|
)
|
|
|
|
|
|
@utf8_decode
|
|
def server_command_completion_cb(data, completion_item, buffer, completion):
|
|
buffer_input = weechat.buffer_get_string(buffer, "input").split()
|
|
|
|
args = buffer_input[1:]
|
|
commands = ['add', 'delete', 'list', 'listfull']
|
|
|
|
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:
|
|
if args[1] == 'delete' or args[1] == 'listfull':
|
|
add_servers_to_completion(completion)
|
|
|
|
elif len(args) == 3:
|
|
if args[1] == 'delete' or args[1] == 'listfull':
|
|
if args[2] not in SERVERS:
|
|
add_servers_to_completion(completion)
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
@utf8_decode
|
|
def matrix_server_completion_cb(data, completion_item, buffer, completion):
|
|
add_servers_to_completion(completion)
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
@utf8_decode
|
|
def matrix_command_completion_cb(data, completion_item, buffer, completion):
|
|
for command in ["connect", "disconnect", "reconnect", "server", "help"]:
|
|
W.hook_completion_list_add(
|
|
completion,
|
|
command,
|
|
0,
|
|
weechat.WEECHAT_LIST_POS_SORT)
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
def create_default_server(config_file):
|
|
server = MatrixServer('matrix.org', config_file)
|
|
SERVERS[server.name] = server
|
|
|
|
# TODO set this to matrix.org
|
|
W.config_option_set(server.options["address"], "matrix.org", 1)
|
|
W.config_option_set(server.options["port"], "80", 1)
|
|
|
|
return True
|
|
|
|
|
|
@utf8_decode
|
|
def matrix_command_topic_cb(data, buffer, command):
|
|
for server in SERVERS.values():
|
|
if buffer in server.buffers.values():
|
|
topic = None
|
|
room_id = key_from_value(server.buffers, buffer)
|
|
split_command = command.split(' ', 1)
|
|
|
|
if len(split_command) == 2:
|
|
topic = split_command[1]
|
|
|
|
if not topic:
|
|
room = server.rooms[room_id]
|
|
if not room.topic:
|
|
return W.WEECHAT_RC_OK
|
|
|
|
message = ("{prefix}Topic for {color}{room}{ncolor} is "
|
|
"\"{topic}\"").format(
|
|
prefix=W.prefix("network"),
|
|
color=W.color("chat_buffer"),
|
|
ncolor=W.color("reset"),
|
|
room=room.alias,
|
|
topic=room.topic)
|
|
|
|
date = int(time.time())
|
|
topic_date = room.topic_date.strftime("%a, %d %b %Y "
|
|
"%H:%M:%S")
|
|
|
|
tags = "matrix_topic,log1"
|
|
W.prnt_date_tags(buffer, date, tags, message)
|
|
|
|
# TODO the nick should be colored
|
|
|
|
# TODO we should use the display name as well as
|
|
# the user name here
|
|
message = ("{prefix}Topic set by {author} on "
|
|
"{date}").format(
|
|
prefix=W.prefix("network"),
|
|
author=room.topic_author,
|
|
date=topic_date)
|
|
W.prnt_date_tags(buffer, date, tags, message)
|
|
|
|
return W.WEECHAT_RC_OK_EAT
|
|
|
|
body = {"topic": topic}
|
|
|
|
message = MatrixMessage(
|
|
server,
|
|
MessageType.STATE,
|
|
data=body,
|
|
room_id=room_id,
|
|
extra_id="m.room.topic"
|
|
)
|
|
send_or_queue(server, message)
|
|
|
|
return W.WEECHAT_RC_OK_EAT
|
|
|
|
elif buffer == server.server_buffer:
|
|
message = ("{prefix}matrix: command \"topic\" must be "
|
|
"executed on a Matrix channel buffer").format(
|
|
prefix=W.prefix("error"))
|
|
W.prnt(buffer, message)
|
|
return W.WEECHAT_RC_OK_EAT
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
def matrix_fetch_old_messages(server, room_id):
|
|
room = server.rooms[room_id]
|
|
prev_batch = room.prev_batch
|
|
|
|
if not prev_batch:
|
|
return
|
|
|
|
message = MatrixMessage(server, MessageType.ROOM_MSG,
|
|
room_id=room_id, extra_id=prev_batch)
|
|
|
|
send_or_queue(server, message)
|
|
|
|
return
|
|
|
|
|
|
@utf8_decode
|
|
def matrix_command_buf_clear_cb(data, buffer, command):
|
|
for server in SERVERS.values():
|
|
if buffer in server.buffers.values():
|
|
room_id = key_from_value(server.buffers, buffer)
|
|
server.rooms[room_id].prev_batch = server.next_batch
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
@utf8_decode
|
|
def matrix_command_pgup_cb(data, buffer, command):
|
|
# TODO the highlight status isn't allowed to be updated/changed via hdata,
|
|
# therefore the highlight status of a messages can't be reoredered this
|
|
# would need to be fixed in weechat
|
|
# TODO we shouldn't fetch and print out more messages than
|
|
# max_buffer_lines_number or older messages than max_buffer_lines_minutes
|
|
for server in SERVERS.values():
|
|
if buffer in server.buffers.values():
|
|
window = W.window_search_with_buffer(buffer)
|
|
|
|
first_line_displayed = bool(
|
|
W.window_get_integer(window, "first_line_displayed")
|
|
)
|
|
|
|
if first_line_displayed:
|
|
room_id = key_from_value(server.buffers, buffer)
|
|
matrix_fetch_old_messages(server, room_id)
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
def tags_from_line_data(line_data):
|
|
# type: (weechat.hdata) -> List[unicode]
|
|
tags_count = W.hdata_get_var_array_size(
|
|
W.hdata_get('line_data'),
|
|
line_data,
|
|
'tags_array')
|
|
|
|
tags = [
|
|
W.hdata_string(
|
|
W.hdata_get('line_data'),
|
|
line_data,
|
|
'%d|tags_array' % i
|
|
) for i in range(tags_count)]
|
|
|
|
return tags
|
|
|
|
|
|
def event_id_from_line(buf, target_number):
|
|
# type: (weechat.buffer, int) -> unicode
|
|
own_lines = W.hdata_pointer(W.hdata_get('buffer'), buf, 'own_lines')
|
|
if own_lines:
|
|
line = W.hdata_pointer(
|
|
W.hdata_get('lines'),
|
|
own_lines,
|
|
'last_line'
|
|
)
|
|
|
|
line_number = 1
|
|
|
|
while line:
|
|
line_data = W.hdata_pointer(
|
|
W.hdata_get('line'),
|
|
line,
|
|
'data'
|
|
)
|
|
|
|
if line_data:
|
|
tags = tags_from_line_data(line_data)
|
|
|
|
# Only count non redacted user messages
|
|
if ("matrix_message" in tags
|
|
and 'matrix_redacted' not in tags
|
|
and "matrix_new_redacted" not in tags):
|
|
|
|
if line_number == target_number:
|
|
for tag in tags:
|
|
if tag.startswith("matrix_id"):
|
|
event_id = tag[10:]
|
|
return event_id
|
|
|
|
line_number += 1
|
|
|
|
line = W.hdata_move(W.hdata_get('line'), line, -1)
|
|
|
|
return ""
|
|
|
|
|
|
@utf8_decode
|
|
def matrix_redact_command_cb(data, buffer, args):
|
|
for server in SERVERS.values():
|
|
if buffer in server.buffers.values():
|
|
body = {}
|
|
|
|
room_id = key_from_value(server.buffers, buffer)
|
|
|
|
matches = re.match(r"(\d+)(:\".*\")? ?(.*)?", args)
|
|
|
|
if not matches:
|
|
message = ("{prefix}matrix: Invalid command arguments (see "
|
|
"the help for the command /help redact)").format(
|
|
prefix=W.prefix("error"))
|
|
W.prnt("", message)
|
|
return W.WEECHAT_RC_ERROR
|
|
|
|
line_string, _, reason = matches.groups()
|
|
line = int(line_string)
|
|
|
|
if reason:
|
|
body = {"reason": reason}
|
|
|
|
event_id = event_id_from_line(buffer, line)
|
|
|
|
if not event_id:
|
|
message = ("{prefix}matrix: No such message with number "
|
|
"{number} found").format(
|
|
prefix=W.prefix("error"),
|
|
number=line)
|
|
W.prnt("", message)
|
|
return W.WEECHAT_RC_OK
|
|
|
|
message = MatrixMessage(
|
|
server,
|
|
MessageType.REDACT,
|
|
data=body,
|
|
room_id=room_id,
|
|
extra_id=event_id
|
|
)
|
|
send_or_queue(server, message)
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
elif buffer == server.server_buffer:
|
|
message = ("{prefix}matrix: command \"redact\" must be "
|
|
"executed on a Matrix channel buffer").format(
|
|
prefix=W.prefix("error"))
|
|
W.prnt("", message)
|
|
return W.WEECHAT_RC_OK
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
@utf8_decode
|
|
def matrix_message_completion_cb(data, completion_item, buffer, completion):
|
|
own_lines = W.hdata_pointer(W.hdata_get('buffer'), buffer, 'own_lines')
|
|
if own_lines:
|
|
line = W.hdata_pointer(
|
|
W.hdata_get('lines'),
|
|
own_lines,
|
|
'last_line'
|
|
)
|
|
|
|
line_number = 1
|
|
|
|
while line:
|
|
line_data = W.hdata_pointer(
|
|
W.hdata_get('line'),
|
|
line,
|
|
'data'
|
|
)
|
|
|
|
if line_data:
|
|
message = W.hdata_string(W.hdata_get('line_data'), line_data,
|
|
'message')
|
|
|
|
tags = tags_from_line_data(line_data)
|
|
|
|
# Only add non redacted user messages to the completion
|
|
if (message
|
|
and 'matrix_message' in tags
|
|
and 'matrix_redacted' not in tags):
|
|
|
|
if len(message) > GLOBAL_OPTIONS.redaction_comp_len + 2:
|
|
message = (
|
|
message[:GLOBAL_OPTIONS.redaction_comp_len]
|
|
+ '..')
|
|
|
|
item = ("{number}:\"{message}\"").format(
|
|
number=line_number,
|
|
message=message)
|
|
|
|
W.hook_completion_list_add(
|
|
completion,
|
|
item,
|
|
0,
|
|
weechat.WEECHAT_LIST_POS_END)
|
|
line_number += 1
|
|
|
|
line = W.hdata_move(W.hdata_get('line'), line, -1)
|
|
|
|
return W.WEECHAT_RC_OK
|
|
|
|
|
|
def hook_page_up():
|
|
GLOBAL_OPTIONS.page_up_hook = W.hook_command_run(
|
|
'/window page_up',
|
|
'matrix_command_pgup_cb',
|
|
''
|
|
)
|
|
|
|
|
|
@utf8_decode
|
|
def matrix_bar_item_plugin(data, item, window, buffer, extra_info):
|
|
for server in SERVERS.values():
|
|
if (buffer in server.buffers.values() or
|
|
buffer == server.server_buffer):
|
|
return "matrix{color}/{color_fg}{name}".format(
|
|
color=W.color("bar_delim"),
|
|
color_fg=W.color("bar_fg"),
|
|
name=server.name)
|
|
|
|
return ""
|
|
|
|
|
|
@utf8_decode
|
|
def matrix_bar_item_name(data, item, window, buffer, extra_info):
|
|
for server in SERVERS.values():
|
|
if buffer in server.buffers.values():
|
|
color = ("status_name_ssl"
|
|
if server.ssl_context.check_hostname else
|
|
"status_name")
|
|
|
|
room_id = key_from_value(server.buffers, buffer)
|
|
|
|
room = server.rooms[room_id]
|
|
|
|
return "{color}{name}".format(
|
|
color=W.color(color),
|
|
name=room.alias)
|
|
|
|
elif buffer in server.server_buffer:
|
|
color = ("status_name_ssl"
|
|
if server.ssl_context.check_hostname else
|
|
"status_name")
|
|
|
|
return "{color}server{del_color}[{color}{name}{del_color}]".format(
|
|
color=W.color(color),
|
|
del_color=W.color("bar_delim"),
|
|
name=server.name)
|
|
|
|
return ""
|
|
|
|
|
|
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_completion(
|
|
"matrix_commands",
|
|
"Matrix command completion",
|
|
"matrix_command_completion_cb",
|
|
""
|
|
)
|
|
|
|
W.hook_completion(
|
|
"matrix_messages",
|
|
"Matrix message completion",
|
|
"matrix_message_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> ||'
|
|
'connect <server-name> ||'
|
|
'disconnect <server-name> ||'
|
|
'reconnect <server-name> ||'
|
|
'help <matrix-command>'
|
|
),
|
|
# Description
|
|
(
|
|
' server: list, add, or remove Matrix servers\n'
|
|
' connect: connect to Matrix servers\n'
|
|
'disconnect: disconnect from one or all Matrix servers\n'
|
|
' reconnect: reconnect to server(s)\n\n'
|
|
' help: show detailed command help\n\n'
|
|
'Use /matrix help [command] to find out more\n'
|
|
),
|
|
# Completions
|
|
(
|
|
'server %(matrix_server_commands)|%* ||'
|
|
'connect %(matrix_servers) ||'
|
|
'disconnect %(matrix_servers) ||'
|
|
'reconnect %(matrix_servers) ||'
|
|
'help %(matrix_commands)'
|
|
),
|
|
# Function name
|
|
'matrix_command_cb', '')
|
|
|
|
W.hook_command(
|
|
# Command name and short description
|
|
'redact', 'redact messages',
|
|
# Synopsis
|
|
(
|
|
'<message-number>[:<"message-part">] [<reason>]'
|
|
),
|
|
# Description
|
|
(
|
|
"message-number: number of the message to redact (message numbers"
|
|
"\n start from the last recieved as "
|
|
"1 and count up)\n"
|
|
" message-part: a shortened part of the message\n"
|
|
" reason: the redaction reason\n"
|
|
),
|
|
# Completions
|
|
(
|
|
'%(matrix_messages)'
|
|
),
|
|
# Function name
|
|
'matrix_redact_command_cb', '')
|
|
|
|
W.hook_command_run('/topic', 'matrix_command_topic_cb', '')
|
|
W.hook_command_run('/buffer clear', 'matrix_command_buf_clear_cb', '')
|
|
|
|
if GLOBAL_OPTIONS.enable_backlog:
|
|
hook_page_up()
|
|
|
|
|
|
def autoconnect(servers):
|
|
for server in servers.values():
|
|
if server.autoconnect:
|
|
connect(server)
|
|
|
|
|
|
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',
|
|
''):
|
|
|
|
GLOBAL_OPTIONS = PluginOptions()
|
|
|
|
# TODO if this fails we should abort and unload the script.
|
|
CONFIG = init_matrix_config()
|
|
read_matrix_config()
|
|
|
|
init_hooks()
|
|
|
|
W.bar_item_new("(extra)buffer_plugin", "matrix_bar_item_plugin", "")
|
|
W.bar_item_new("(extra)buffer_name", "matrix_bar_item_name", "")
|
|
|
|
if not SERVERS:
|
|
create_default_server(CONFIG)
|
|
|
|
autoconnect(SERVERS)
|