1764 lines
57 KiB
Python
1764 lines
57 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
|
||
|
# Weechat Matrix Protocol Script
|
||
|
# Copyright © 2018, 2019 Damir Jelić <poljar@termina.org.uk>
|
||
|
#
|
||
|
# Permission to use, copy, modify, and/or distribute this software for
|
||
|
# any purpose with or without fee is hereby granted, provided that the
|
||
|
# above copyright notice and this permission notice appear in all copies.
|
||
|
#
|
||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||
|
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||
|
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||
|
# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||
|
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||
|
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||
|
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||
|
|
||
|
from __future__ import unicode_literals
|
||
|
|
||
|
import time
|
||
|
import attr
|
||
|
import pprint
|
||
|
from builtins import super
|
||
|
from functools import partial
|
||
|
from collections import deque
|
||
|
from typing import Dict, List, NamedTuple, Optional, Set
|
||
|
from uuid import UUID
|
||
|
|
||
|
from nio import (
|
||
|
Api,
|
||
|
PowerLevelsEvent,
|
||
|
RedactedEvent,
|
||
|
RedactionEvent,
|
||
|
RoomAliasEvent,
|
||
|
RoomEncryptionEvent,
|
||
|
RoomMemberEvent,
|
||
|
RoomMessage,
|
||
|
RoomMessageEmote,
|
||
|
RoomMessageMedia,
|
||
|
RoomEncryptedMedia,
|
||
|
RoomMessageNotice,
|
||
|
RoomMessageText,
|
||
|
RoomMessageUnknown,
|
||
|
RoomNameEvent,
|
||
|
RoomTopicEvent,
|
||
|
MegolmEvent,
|
||
|
Event,
|
||
|
OlmTrustError,
|
||
|
UnknownEvent,
|
||
|
FullyReadEvent,
|
||
|
BadEvent,
|
||
|
UnknownBadEvent,
|
||
|
)
|
||
|
|
||
|
from . import globals as G
|
||
|
from .colors import Formatted
|
||
|
from .config import RedactType
|
||
|
from .globals import SCRIPT_NAME, SERVERS, W, TYPING_NOTICE_TIMEOUT
|
||
|
from .utf import utf8_decode
|
||
|
from .utils import (
|
||
|
server_ts_to_weechat,
|
||
|
shorten_sender,
|
||
|
string_strikethrough,
|
||
|
color_pair,
|
||
|
)
|
||
|
|
||
|
|
||
|
@attr.s
|
||
|
class OwnMessages(object):
|
||
|
sender = attr.ib(type=str)
|
||
|
age = attr.ib(type=int)
|
||
|
event_id = attr.ib(type=str)
|
||
|
uuid = attr.ib(type=str)
|
||
|
room_id = attr.ib(type=str)
|
||
|
formatted_message = attr.ib(type=Formatted)
|
||
|
|
||
|
|
||
|
class OwnMessage(OwnMessages):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class OwnAction(OwnMessage):
|
||
|
pass
|
||
|
|
||
|
|
||
|
@utf8_decode
|
||
|
def room_buffer_input_cb(server_name, buffer, input_data):
|
||
|
server = SERVERS[server_name]
|
||
|
room_buffer = server.find_room_from_ptr(buffer)
|
||
|
|
||
|
if not room_buffer:
|
||
|
# TODO log error
|
||
|
return W.WEECHAT_RC_ERROR
|
||
|
|
||
|
if not server.connected:
|
||
|
room_buffer.error("You are not connected to the server")
|
||
|
return W.WEECHAT_RC_ERROR
|
||
|
|
||
|
data = W.string_input_for_buffer(input_data)
|
||
|
|
||
|
if not data:
|
||
|
data = input_data
|
||
|
|
||
|
formatted_data = Formatted.from_input_line(data)
|
||
|
|
||
|
try:
|
||
|
server.room_send_message(room_buffer, formatted_data, "m.text")
|
||
|
room_buffer.last_message = None
|
||
|
except OlmTrustError as e:
|
||
|
if (G.CONFIG.network.resending_ignores_devices
|
||
|
and room_buffer.last_message):
|
||
|
room_buffer.error("Ignoring unverified devices.")
|
||
|
|
||
|
if (room_buffer.last_message.to_weechat() ==
|
||
|
formatted_data.to_weechat()):
|
||
|
server.room_send_message(room_buffer, formatted_data, "m.text",
|
||
|
ignore_unverified_devices=True)
|
||
|
room_buffer.last_message = None
|
||
|
else:
|
||
|
# If the item is a normal user message store it in the
|
||
|
# buffer to enable the send-anyways functionality.
|
||
|
room_buffer.error("Untrusted devices found in room: {}".format(e))
|
||
|
room_buffer.last_message = formatted_data
|
||
|
|
||
|
return W.WEECHAT_RC_OK
|
||
|
|
||
|
|
||
|
@utf8_decode
|
||
|
def room_buffer_close_cb(server_name, buffer):
|
||
|
server = SERVERS[server_name]
|
||
|
room_buffer = server.find_room_from_ptr(buffer)
|
||
|
|
||
|
if room_buffer:
|
||
|
room_id = room_buffer.room.room_id
|
||
|
server.buffers.pop(room_id, None)
|
||
|
server.room_buffers.pop(room_id, None)
|
||
|
|
||
|
return W.WEECHAT_RC_OK
|
||
|
|
||
|
|
||
|
class WeechatUser(object):
|
||
|
def __init__(self, nick, host=None, prefix="", join_time=None):
|
||
|
# type: (str, str, str, int) -> None
|
||
|
self.nick = nick
|
||
|
self.host = host
|
||
|
self.prefix = prefix
|
||
|
self.color = W.info_get("nick_color_name", nick)
|
||
|
self.join_time = join_time or time.time()
|
||
|
self.speaking_time = None # type: Optional[int]
|
||
|
|
||
|
def update_speaking_time(self, new_time=None):
|
||
|
self.speaking_time = new_time or time.time()
|
||
|
|
||
|
@property
|
||
|
def joined_recently(self):
|
||
|
# TODO make the delay configurable
|
||
|
delay = 30
|
||
|
limit = time.time() - (delay * 60)
|
||
|
return self.join_time < limit
|
||
|
|
||
|
@property
|
||
|
def spoken_recently(self):
|
||
|
if not self.speaking_time:
|
||
|
return False
|
||
|
|
||
|
# TODO make the delay configurable
|
||
|
delay = 5
|
||
|
limit = time.time() - (delay * 60)
|
||
|
return self.speaking_time < limit
|
||
|
|
||
|
|
||
|
class RoomUser(WeechatUser):
|
||
|
def __init__(self, nick, user_id=None, power_level=0, join_time=None):
|
||
|
# type: (str, str, int, int) -> None
|
||
|
prefix = self._get_prefix(power_level)
|
||
|
super().__init__(nick, user_id, prefix, join_time)
|
||
|
|
||
|
@property
|
||
|
def power_level(self):
|
||
|
# This shouldn't be used since it's a lossy function. It's only here
|
||
|
# for the setter
|
||
|
if self.prefix == "&":
|
||
|
return 100
|
||
|
if self.prefix == "@":
|
||
|
return 50
|
||
|
if self.prefix == "+":
|
||
|
return 10
|
||
|
return 0
|
||
|
|
||
|
@power_level.setter
|
||
|
def power_level(self, level):
|
||
|
self.prefix = self._get_prefix(level)
|
||
|
|
||
|
@staticmethod
|
||
|
def _get_prefix(power_level):
|
||
|
# type: (int) -> str
|
||
|
if power_level >= 100:
|
||
|
return "&"
|
||
|
if power_level >= 50:
|
||
|
return "@"
|
||
|
if power_level > 0:
|
||
|
return "+"
|
||
|
return ""
|
||
|
|
||
|
|
||
|
class WeechatChannelBuffer(object):
|
||
|
tags = {
|
||
|
"message": [SCRIPT_NAME + "_message", "notify_message", "log1"],
|
||
|
"message_private": [
|
||
|
SCRIPT_NAME + "_message",
|
||
|
"notify_private",
|
||
|
"log1"
|
||
|
],
|
||
|
"self_message": [
|
||
|
SCRIPT_NAME + "_message",
|
||
|
"notify_none",
|
||
|
"no_highlight",
|
||
|
"self_msg",
|
||
|
"log1",
|
||
|
],
|
||
|
"action": [
|
||
|
SCRIPT_NAME + "_message",
|
||
|
SCRIPT_NAME + "_action",
|
||
|
"notify_message",
|
||
|
"log1",
|
||
|
],
|
||
|
"action_private": [
|
||
|
SCRIPT_NAME + "_message",
|
||
|
SCRIPT_NAME + "_action",
|
||
|
"notify_private",
|
||
|
"log1",
|
||
|
],
|
||
|
"notice": [SCRIPT_NAME + "_notice", "notify_message", "log1"],
|
||
|
"old_message": [
|
||
|
SCRIPT_NAME + "_message",
|
||
|
"notify_message",
|
||
|
"no_log",
|
||
|
"no_highlight",
|
||
|
],
|
||
|
"join": [SCRIPT_NAME + "_join", "log4"],
|
||
|
"part": [SCRIPT_NAME + "_leave", "log4"],
|
||
|
"kick": [SCRIPT_NAME + "_kick", "log4"],
|
||
|
"invite": [SCRIPT_NAME + "_invite", "log4"],
|
||
|
"topic": [SCRIPT_NAME + "_topic", "log3"],
|
||
|
}
|
||
|
|
||
|
membership_messages = {
|
||
|
"join": "has joined",
|
||
|
"part": "has left",
|
||
|
"kick": "has been kicked from",
|
||
|
"invite": "has been invited to",
|
||
|
}
|
||
|
|
||
|
class Line(object):
|
||
|
def __init__(self, pointer):
|
||
|
self._ptr = pointer
|
||
|
|
||
|
@property
|
||
|
def _hdata(self):
|
||
|
return W.hdata_get("line_data")
|
||
|
|
||
|
@property
|
||
|
def prefix(self):
|
||
|
return W.hdata_string(self._hdata, self._ptr, "prefix")
|
||
|
|
||
|
@prefix.setter
|
||
|
def prefix(self, new_prefix):
|
||
|
new_data = {"prefix": new_prefix}
|
||
|
W.hdata_update(self._hdata, self._ptr, new_data)
|
||
|
|
||
|
@property
|
||
|
def message(self):
|
||
|
return W.hdata_string(self._hdata, self._ptr, "message")
|
||
|
|
||
|
@message.setter
|
||
|
def message(self, new_message):
|
||
|
# type: (str) -> None
|
||
|
new_data = {"message": new_message}
|
||
|
W.hdata_update(self._hdata, self._ptr, new_data)
|
||
|
|
||
|
@property
|
||
|
def tags(self):
|
||
|
tags_count = W.hdata_get_var_array_size(
|
||
|
self._hdata, self._ptr, "tags_array"
|
||
|
)
|
||
|
|
||
|
tags = [
|
||
|
W.hdata_string(self._hdata, self._ptr, "%d|tags_array" % i)
|
||
|
for i in range(tags_count)
|
||
|
]
|
||
|
return tags
|
||
|
|
||
|
@tags.setter
|
||
|
def tags(self, new_tags):
|
||
|
# type: (List[str]) -> None
|
||
|
new_data = {"tags_array": ",".join(new_tags)}
|
||
|
W.hdata_update(self._hdata, self._ptr, new_data)
|
||
|
|
||
|
@property
|
||
|
def date(self):
|
||
|
# type: () -> int
|
||
|
return W.hdata_time(self._hdata, self._ptr, "date")
|
||
|
|
||
|
@date.setter
|
||
|
def date(self, new_date):
|
||
|
# type: (int) -> None
|
||
|
new_data = {"date": str(new_date)}
|
||
|
W.hdata_update(self._hdata, self._ptr, new_data)
|
||
|
|
||
|
@property
|
||
|
def date_printed(self):
|
||
|
# type: () -> int
|
||
|
return W.hdata_time(self._hdata, self._ptr, "date_printed")
|
||
|
|
||
|
@date_printed.setter
|
||
|
def date_printed(self, new_date):
|
||
|
# type: (int) -> None
|
||
|
new_data = {"date_printed": str(new_date)}
|
||
|
W.hdata_update(self._hdata, self._ptr, new_data)
|
||
|
|
||
|
@property
|
||
|
def highlight(self):
|
||
|
# type: () -> bool
|
||
|
return bool(W.hdata_char(self._hdata, self._ptr, "highlight"))
|
||
|
|
||
|
def update(
|
||
|
self,
|
||
|
date=None,
|
||
|
date_printed=None,
|
||
|
tags=None,
|
||
|
prefix=None,
|
||
|
message=None,
|
||
|
highlight=None,
|
||
|
):
|
||
|
new_data = {}
|
||
|
|
||
|
if date is not None:
|
||
|
new_data["date"] = str(date)
|
||
|
if date_printed is not None:
|
||
|
new_data["date_printed"] = str(date_printed)
|
||
|
if tags is not None:
|
||
|
new_data["tags_array"] = ",".join(tags)
|
||
|
if prefix is not None:
|
||
|
new_data["prefix"] = prefix
|
||
|
if message is not None:
|
||
|
new_data["message"] = message
|
||
|
if highlight is not None:
|
||
|
new_data["highlight"] = highlight
|
||
|
|
||
|
if new_data:
|
||
|
W.hdata_update(self._hdata, self._ptr, new_data)
|
||
|
|
||
|
def __init__(self, name, server_name, user):
|
||
|
# type: (str, str, str) -> None
|
||
|
self._ptr = W.buffer_new(
|
||
|
name,
|
||
|
"room_buffer_input_cb",
|
||
|
server_name,
|
||
|
"room_buffer_close_cb",
|
||
|
server_name,
|
||
|
)
|
||
|
|
||
|
self.name = ""
|
||
|
self.users = {} # type: Dict[str, WeechatUser]
|
||
|
self.smart_filtered_nicks = set() # type: Set[str]
|
||
|
|
||
|
self.topic_author = ""
|
||
|
self.topic_date = None
|
||
|
|
||
|
W.buffer_set(self._ptr, "localvar_set_type", "private")
|
||
|
W.buffer_set(self._ptr, "type", "formatted")
|
||
|
|
||
|
W.buffer_set(self._ptr, "localvar_set_channel", name)
|
||
|
|
||
|
W.buffer_set(self._ptr, "localvar_set_nick", user)
|
||
|
|
||
|
W.buffer_set(self._ptr, "localvar_set_server", server_name)
|
||
|
|
||
|
W.nicklist_add_group(
|
||
|
self._ptr, "", "000|o", "weechat.color.nicklist_group", 1
|
||
|
)
|
||
|
W.nicklist_add_group(
|
||
|
self._ptr, "", "001|h", "weechat.color.nicklist_group", 1
|
||
|
)
|
||
|
W.nicklist_add_group(
|
||
|
self._ptr, "", "002|v", "weechat.color.nicklist_group", 1
|
||
|
)
|
||
|
W.nicklist_add_group(
|
||
|
self._ptr, "", "999|...", "weechat.color.nicklist_group", 1
|
||
|
)
|
||
|
|
||
|
W.buffer_set(self._ptr, "nicklist", "1")
|
||
|
W.buffer_set(self._ptr, "nicklist_display_groups", "0")
|
||
|
|
||
|
W.buffer_set(self._ptr, "highlight_words", user)
|
||
|
|
||
|
# TODO make this configurable
|
||
|
W.buffer_set(
|
||
|
self._ptr, "highlight_tags_restrict", SCRIPT_NAME + "_message"
|
||
|
)
|
||
|
|
||
|
@property
|
||
|
def _hdata(self):
|
||
|
return W.hdata_get("buffer")
|
||
|
|
||
|
def add_smart_filtered_nick(self, nick):
|
||
|
self.smart_filtered_nicks.add(nick)
|
||
|
|
||
|
def remove_smart_filtered_nick(self, nick):
|
||
|
self.smart_filtered_nicks.discard(nick)
|
||
|
|
||
|
def unmask_smart_filtered_nick(self, nick):
|
||
|
if nick not in self.smart_filtered_nicks:
|
||
|
return
|
||
|
|
||
|
for line in self.lines:
|
||
|
filtered = False
|
||
|
join = False
|
||
|
tags = line.tags
|
||
|
|
||
|
if "nick_{}".format(nick) not in tags:
|
||
|
continue
|
||
|
|
||
|
if SCRIPT_NAME + "_smart_filter" in tags:
|
||
|
filtered = True
|
||
|
elif SCRIPT_NAME + "_join" in tags:
|
||
|
join = True
|
||
|
|
||
|
if filtered:
|
||
|
tags.remove(SCRIPT_NAME + "_smart_filter")
|
||
|
line.tags = tags
|
||
|
|
||
|
if join:
|
||
|
break
|
||
|
|
||
|
self.remove_smart_filtered_nick(nick)
|
||
|
|
||
|
@property
|
||
|
def input(self):
|
||
|
# type: () -> str
|
||
|
"""Get the bar item input text of the buffer."""
|
||
|
return W.buffer_get_string(self._ptr, "input")
|
||
|
|
||
|
@property
|
||
|
def lines(self):
|
||
|
own_lines = W.hdata_pointer(self._hdata, self._ptr, "own_lines")
|
||
|
|
||
|
if own_lines:
|
||
|
hdata_line = W.hdata_get("line")
|
||
|
|
||
|
line_pointer = W.hdata_pointer(
|
||
|
W.hdata_get("lines"), own_lines, "last_line"
|
||
|
)
|
||
|
|
||
|
while line_pointer:
|
||
|
data_pointer = W.hdata_pointer(
|
||
|
hdata_line, line_pointer, "data"
|
||
|
)
|
||
|
|
||
|
if data_pointer:
|
||
|
yield WeechatChannelBuffer.Line(data_pointer)
|
||
|
|
||
|
line_pointer = W.hdata_move(hdata_line, line_pointer, -1)
|
||
|
|
||
|
def _print(self, string):
|
||
|
# type: (str) -> None
|
||
|
""" Print a string to the room buffer """
|
||
|
W.prnt(self._ptr, string)
|
||
|
|
||
|
def print_date_tags(self, data, date=None, tags=None):
|
||
|
# type: (str, Optional[int], Optional[List[str]]) -> None
|
||
|
date = date or int(time.time())
|
||
|
tags = tags or []
|
||
|
|
||
|
tags_string = ",".join(tags)
|
||
|
W.prnt_date_tags(self._ptr, date, tags_string, data)
|
||
|
|
||
|
def error(self, string):
|
||
|
# type: (str) -> None
|
||
|
""" Print an error to the room buffer """
|
||
|
message = "{prefix}{script}: {message}".format(
|
||
|
prefix=W.prefix("error"), script=SCRIPT_NAME, message=string
|
||
|
)
|
||
|
|
||
|
self._print(message)
|
||
|
|
||
|
@staticmethod
|
||
|
def _color_for_tags(color):
|
||
|
# type: (str) -> str
|
||
|
if color == "weechat.color.chat_nick_self":
|
||
|
option = W.config_get(color)
|
||
|
return W.config_string(option)
|
||
|
|
||
|
return color
|
||
|
|
||
|
def _message_tags(self, user, message_type):
|
||
|
# type: (WeechatUser, str) -> List[str]
|
||
|
tags = list(self.tags[message_type])
|
||
|
|
||
|
tags.append("nick_{nick}".format(nick=user.nick))
|
||
|
|
||
|
color = self._color_for_tags(user.color)
|
||
|
|
||
|
if message_type not in ("action", "notice"):
|
||
|
tags.append("prefix_nick_{color}".format(color=color))
|
||
|
|
||
|
return tags
|
||
|
|
||
|
def _get_user(self, nick):
|
||
|
# type: (str) -> WeechatUser
|
||
|
if nick in self.users:
|
||
|
return self.users[nick]
|
||
|
|
||
|
# A message from a non joined user
|
||
|
return WeechatUser(nick)
|
||
|
|
||
|
def _print_message(self, user, message, date, tags, extra_prefix=""):
|
||
|
prefix_string = (
|
||
|
extra_prefix
|
||
|
if not user.prefix
|
||
|
else "{}{}{}{}".format(
|
||
|
extra_prefix,
|
||
|
W.color(self._get_prefix_color(user.prefix)),
|
||
|
user.prefix,
|
||
|
W.color("reset"),
|
||
|
)
|
||
|
)
|
||
|
|
||
|
data = "{prefix}{color}{author}{ncolor}\t{msg}".format(
|
||
|
prefix=prefix_string,
|
||
|
color=W.color(user.color),
|
||
|
author=user.nick,
|
||
|
ncolor=W.color("reset"),
|
||
|
msg=message,
|
||
|
)
|
||
|
|
||
|
self.print_date_tags(data, date, tags)
|
||
|
|
||
|
def message(self, nick, message, date, extra_tags=None, extra_prefix=""):
|
||
|
# type: (str, str, int, List[str], str) -> None
|
||
|
user = self._get_user(nick)
|
||
|
tags_type = "message_private" if self.type == "private" else "message"
|
||
|
tags = self._message_tags(user, tags_type) + (extra_tags or [])
|
||
|
self._print_message(user, message, date, tags, extra_prefix)
|
||
|
|
||
|
user.update_speaking_time(date)
|
||
|
self.unmask_smart_filtered_nick(nick)
|
||
|
|
||
|
def notice(self, nick, message, date, extra_tags=None, extra_prefix=""):
|
||
|
# type: (str, str, int, Optional[List[str]], str) -> None
|
||
|
user = self._get_user(nick)
|
||
|
user_prefix = (
|
||
|
""
|
||
|
if not user.prefix
|
||
|
else "{}{}{}".format(
|
||
|
W.color(self._get_prefix_color(user.prefix)),
|
||
|
user.prefix,
|
||
|
W.color("reset"),
|
||
|
)
|
||
|
)
|
||
|
|
||
|
user_string = "{}{}{}{}".format(
|
||
|
user_prefix, W.color(user.color), user.nick, W.color("reset")
|
||
|
)
|
||
|
|
||
|
data = (
|
||
|
"{extra_prefix}{prefix}{color}Notice"
|
||
|
"{del_color}({ncolor}{user}{del_color}){ncolor}"
|
||
|
": {message}"
|
||
|
).format(
|
||
|
extra_prefix=extra_prefix,
|
||
|
prefix=W.prefix("network"),
|
||
|
color=W.color("irc.color.notice"),
|
||
|
del_color=W.color("chat_delimiters"),
|
||
|
ncolor=W.color("reset"),
|
||
|
user=user_string,
|
||
|
message=message,
|
||
|
)
|
||
|
|
||
|
tags = self._message_tags(user, "notice") + (extra_tags or [])
|
||
|
self.print_date_tags(data, date, tags)
|
||
|
|
||
|
user.update_speaking_time(date)
|
||
|
self.unmask_smart_filtered_nick(nick)
|
||
|
|
||
|
def _format_action(self, user, message):
|
||
|
nick_prefix = (
|
||
|
""
|
||
|
if not user.prefix
|
||
|
else "{}{}{}".format(
|
||
|
W.color(self._get_prefix_color(user.prefix)),
|
||
|
user.prefix,
|
||
|
W.color("reset"),
|
||
|
)
|
||
|
)
|
||
|
|
||
|
data = (
|
||
|
"{nick_prefix}{nick_color}{author}"
|
||
|
"{ncolor} {msg}").format(
|
||
|
nick_prefix=nick_prefix,
|
||
|
nick_color=W.color(user.color),
|
||
|
author=user.nick,
|
||
|
ncolor=W.color("reset"),
|
||
|
msg=message,
|
||
|
)
|
||
|
return data
|
||
|
|
||
|
def _print_action(self, user, message, date, tags, extra_prefix=""):
|
||
|
data = self._format_action(user, message)
|
||
|
data = "{extra_prefix}{prefix}{data}".format(
|
||
|
extra_prefix=extra_prefix,
|
||
|
prefix=W.prefix("action"),
|
||
|
data=data)
|
||
|
|
||
|
self.print_date_tags(data, date, tags)
|
||
|
|
||
|
def action(self, nick, message, date, extra_tags=None, extra_prefix=""):
|
||
|
# type: (str, str, int, Optional[List[str]], str) -> None
|
||
|
user = self._get_user(nick)
|
||
|
tags_type = "action_private" if self.type == "private" else "action"
|
||
|
tags = self._message_tags(user, tags_type) + (extra_tags or [])
|
||
|
self._print_action(user, message, date, tags, extra_prefix)
|
||
|
|
||
|
user.update_speaking_time(date)
|
||
|
self.unmask_smart_filtered_nick(nick)
|
||
|
|
||
|
@staticmethod
|
||
|
def _get_nicklist_group(user):
|
||
|
# type: (WeechatUser) -> str
|
||
|
group_name = "999|..."
|
||
|
|
||
|
if user.prefix == "&":
|
||
|
group_name = "000|o"
|
||
|
elif user.prefix == "@":
|
||
|
group_name = "001|h"
|
||
|
elif user.prefix > "+":
|
||
|
group_name = "002|v"
|
||
|
|
||
|
return group_name
|
||
|
|
||
|
@staticmethod
|
||
|
def _get_prefix_color(prefix):
|
||
|
# type: (str) -> str
|
||
|
# TODO make this configurable
|
||
|
color = ""
|
||
|
|
||
|
if prefix == "&":
|
||
|
color = "lightgreen"
|
||
|
elif prefix == "@":
|
||
|
color = "lightgreen"
|
||
|
elif prefix == "+":
|
||
|
color = "yellow"
|
||
|
|
||
|
return color
|
||
|
|
||
|
def _add_user_to_nicklist(self, user):
|
||
|
# type: (WeechatUser) -> None
|
||
|
nick_pointer = W.nicklist_search_nick(self._ptr, "", user.nick)
|
||
|
|
||
|
if not nick_pointer:
|
||
|
group = W.nicklist_search_group(
|
||
|
self._ptr, "", self._get_nicklist_group(user)
|
||
|
)
|
||
|
prefix = user.prefix if user.prefix else " "
|
||
|
W.nicklist_add_nick(
|
||
|
self._ptr,
|
||
|
group,
|
||
|
user.nick,
|
||
|
user.color,
|
||
|
prefix,
|
||
|
self._get_prefix_color(user.prefix),
|
||
|
1,
|
||
|
)
|
||
|
|
||
|
def _membership_message(self, user, message_type):
|
||
|
# type: (WeechatUser, str) -> str
|
||
|
action_color = "green" if message_type in ("join", "invite") else "red"
|
||
|
prefix = "join" if message_type in ("join", "invite") else "quit"
|
||
|
|
||
|
membership_message = self.membership_messages[message_type]
|
||
|
|
||
|
message = (
|
||
|
"{prefix}{color}{author}{ncolor} "
|
||
|
"{del_color}({host_color}{host}{del_color})"
|
||
|
"{action_color} {message} "
|
||
|
"{channel_color}{room}{ncolor}"
|
||
|
).format(
|
||
|
prefix=W.prefix(prefix),
|
||
|
color=W.color(user.color),
|
||
|
author=user.nick,
|
||
|
ncolor=W.color("reset"),
|
||
|
del_color=W.color("chat_delimiters"),
|
||
|
host_color=W.color("chat_host"),
|
||
|
host=user.host,
|
||
|
action_color=W.color(action_color),
|
||
|
message=membership_message,
|
||
|
channel_color=W.color("chat_channel"),
|
||
|
room=self.short_name,
|
||
|
)
|
||
|
|
||
|
return message
|
||
|
|
||
|
def join(self, user, date, message=True, extra_tags=None):
|
||
|
# type: (WeechatUser, int, Optional[bool], Optional[List[str]]) -> None
|
||
|
self._add_user_to_nicklist(user)
|
||
|
self.users[user.nick] = user
|
||
|
|
||
|
if len(self.users) > 2:
|
||
|
W.buffer_set(self._ptr, "localvar_set_type", "channel")
|
||
|
|
||
|
if message:
|
||
|
tags = self._message_tags(user, "join")
|
||
|
msg = self._membership_message(user, "join")
|
||
|
|
||
|
# TODO add a option to disable smart filters
|
||
|
tags.append(SCRIPT_NAME + "_smart_filter")
|
||
|
|
||
|
self.print_date_tags(msg, date, tags)
|
||
|
self.add_smart_filtered_nick(user.nick)
|
||
|
|
||
|
def invite(self, nick, date, extra_tags=None):
|
||
|
# type: (str, int, Optional[List[str]]) -> None
|
||
|
user = self._get_user(nick)
|
||
|
tags = self._message_tags(user, "invite")
|
||
|
message = self._membership_message(user, "invite")
|
||
|
self.print_date_tags(message, date, tags + (extra_tags or []))
|
||
|
|
||
|
def remove_user_from_nicklist(self, user):
|
||
|
# type: (WeechatUser) -> None
|
||
|
nick_pointer = W.nicklist_search_nick(self._ptr, "", user.nick)
|
||
|
|
||
|
if nick_pointer:
|
||
|
W.nicklist_remove_nick(self._ptr, nick_pointer)
|
||
|
|
||
|
def _leave(self, nick, date, message, leave_type, extra_tags=None):
|
||
|
# type: (str, int, bool, str, List[str]) -> None
|
||
|
user = self._get_user(nick)
|
||
|
self.remove_user_from_nicklist(user)
|
||
|
|
||
|
if len(self.users) <= 2:
|
||
|
W.buffer_set(self._ptr, "localvar_set_type", "private")
|
||
|
|
||
|
if message:
|
||
|
tags = self._message_tags(user, leave_type)
|
||
|
|
||
|
# TODO make this configurable
|
||
|
if not user.spoken_recently:
|
||
|
tags.append(SCRIPT_NAME + "_smart_filter")
|
||
|
|
||
|
msg = self._membership_message(user, leave_type)
|
||
|
self.print_date_tags(msg, date, tags + (extra_tags or []))
|
||
|
self.remove_smart_filtered_nick(user.nick)
|
||
|
|
||
|
if user.nick in self.users:
|
||
|
del self.users[user.nick]
|
||
|
|
||
|
def part(self, nick, date, message=True, extra_tags=None):
|
||
|
# type: (str, int, bool, Optional[List[str]]) -> None
|
||
|
self._leave(nick, date, message, "part", extra_tags)
|
||
|
|
||
|
def kick(self, nick, date, message=True, extra_tags=None):
|
||
|
# type: (str, int, bool, Optional[List[str]]) -> None
|
||
|
self._leave(nick, date, message, "kick", extra_tags)
|
||
|
|
||
|
def _print_topic(self, nick, topic, date):
|
||
|
user = self._get_user(nick)
|
||
|
tags = self._message_tags(user, "topic")
|
||
|
|
||
|
data = (
|
||
|
"{prefix}{nick} has changed "
|
||
|
"the topic for {chan_color}{room}{ncolor} "
|
||
|
'to "{topic}"'
|
||
|
).format(
|
||
|
prefix=W.prefix("network"),
|
||
|
nick=user.nick,
|
||
|
chan_color=W.color("chat_channel"),
|
||
|
ncolor=W.color("reset"),
|
||
|
room=self.short_name,
|
||
|
topic=topic,
|
||
|
)
|
||
|
|
||
|
self.print_date_tags(data, date, tags)
|
||
|
user.update_speaking_time(date)
|
||
|
self.unmask_smart_filtered_nick(nick)
|
||
|
|
||
|
@property
|
||
|
def topic(self):
|
||
|
return W.buffer_get_string(self._ptr, "title")
|
||
|
|
||
|
@topic.setter
|
||
|
def topic(self, topic):
|
||
|
W.buffer_set(self._ptr, "title", topic)
|
||
|
|
||
|
def change_topic(self, nick, topic, date, message=True):
|
||
|
if message:
|
||
|
self._print_topic(nick, topic, date)
|
||
|
|
||
|
self.topic = topic
|
||
|
self.topic_author = nick
|
||
|
self.topic_date = date
|
||
|
|
||
|
def self_message(self, nick, message, date, tags=None):
|
||
|
user = self._get_user(nick)
|
||
|
tags = self._message_tags(user, "self_message") + (tags or [])
|
||
|
self._print_message(user, message, date, tags)
|
||
|
|
||
|
def self_action(self, nick, message, date, tags=None):
|
||
|
user = self._get_user(nick)
|
||
|
tags = self._message_tags(user, "self_message") + (tags or [])
|
||
|
tags.append(SCRIPT_NAME + "_action")
|
||
|
self._print_action(user, message, date, tags)
|
||
|
|
||
|
@property
|
||
|
def type(self):
|
||
|
return W.buffer_get_string(self._ptr, "localvar_type")
|
||
|
|
||
|
@property
|
||
|
def short_name(self):
|
||
|
return W.buffer_get_string(self._ptr, "short_name")
|
||
|
|
||
|
@short_name.setter
|
||
|
def short_name(self, name):
|
||
|
W.buffer_set(self._ptr, "short_name", name)
|
||
|
|
||
|
@property
|
||
|
def name(self):
|
||
|
return W.buffer_get_string(self._ptr, "name")
|
||
|
|
||
|
@name.setter
|
||
|
def name(self, name):
|
||
|
W.buffer_set(self._ptr, "name", name)
|
||
|
|
||
|
@property
|
||
|
def number(self):
|
||
|
"""Get the buffer number, starts at 1."""
|
||
|
return int(W.buffer_get_integer(self._ptr, "number"))
|
||
|
|
||
|
def find_lines(self, predicate, max_lines=None):
|
||
|
lines = []
|
||
|
count = 0
|
||
|
for line in self.lines:
|
||
|
if predicate(line):
|
||
|
lines.append(line)
|
||
|
count += 1
|
||
|
if max_lines is not None and count == max_lines:
|
||
|
return lines
|
||
|
|
||
|
return lines
|
||
|
|
||
|
|
||
|
class RoomBuffer(object):
|
||
|
def __init__(self, room, server_name, homeserver, prev_batch):
|
||
|
self.room = room
|
||
|
self.homeserver = homeserver
|
||
|
self._backlog_pending = False
|
||
|
self.prev_batch = prev_batch
|
||
|
self.joined = True
|
||
|
self.leave_event_id = None # type: Optional[str]
|
||
|
self.members_fetched = False
|
||
|
self.unhandled_users = [] # type: List[str]
|
||
|
self.inactive_users = []
|
||
|
|
||
|
self.sent_messages_queue = dict() # type: Dict[UUID, OwnMessage]
|
||
|
self.printed_before_ack_queue = list() # type: List[UUID]
|
||
|
self.undecrypted_events = deque(maxlen=5000)
|
||
|
|
||
|
self.typing_notice_time = None
|
||
|
self._typing = False
|
||
|
self.typing_enabled = True
|
||
|
|
||
|
self.last_read_event = None
|
||
|
self._read_markers_enabled = True
|
||
|
self.server_name = server_name
|
||
|
|
||
|
self.last_message = None
|
||
|
|
||
|
buffer_name = "{}.{}".format(server_name, room.room_id)
|
||
|
|
||
|
# This dict remembers the connection from a user_id to the name we
|
||
|
# displayed in the buffer
|
||
|
self.displayed_nicks = {}
|
||
|
user = shorten_sender(self.room.own_user_id)
|
||
|
|
||
|
self.weechat_buffer = WeechatChannelBuffer(
|
||
|
buffer_name, server_name, user
|
||
|
)
|
||
|
|
||
|
W.buffer_set(
|
||
|
self.weechat_buffer._ptr,
|
||
|
"localvar_set_domain",
|
||
|
self.homeserver.hostname
|
||
|
)
|
||
|
|
||
|
W.buffer_set(
|
||
|
self.weechat_buffer._ptr,
|
||
|
"localvar_set_room_id",
|
||
|
room.room_id
|
||
|
)
|
||
|
|
||
|
@property
|
||
|
def backlog_pending(self):
|
||
|
return self._backlog_pending
|
||
|
|
||
|
@backlog_pending.setter
|
||
|
def backlog_pending(self, value):
|
||
|
self._backlog_pending = value
|
||
|
W.bar_item_update("buffer_modes")
|
||
|
W.bar_item_update("matrix_modes")
|
||
|
|
||
|
@property
|
||
|
def warning_prefix(self):
|
||
|
return "⚠️ "
|
||
|
|
||
|
@property
|
||
|
def typing(self):
|
||
|
# type: () -> bool
|
||
|
"""Return our typing status."""
|
||
|
return self._typing
|
||
|
|
||
|
@typing.setter
|
||
|
def typing(self, value):
|
||
|
self._typing = value
|
||
|
if value:
|
||
|
self.typing_notice_time = time.time()
|
||
|
else:
|
||
|
self.typing_notice_time = None
|
||
|
|
||
|
@property
|
||
|
def typing_notice_expired(self):
|
||
|
# type: () -> bool
|
||
|
"""Check if the typing notice has expired.
|
||
|
|
||
|
Returns true if a new typing notice should be sent.
|
||
|
"""
|
||
|
if not self.typing_notice_time:
|
||
|
return True
|
||
|
|
||
|
now = time.time()
|
||
|
if (now - self.typing_notice_time) > (TYPING_NOTICE_TIMEOUT / 1000):
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
@property
|
||
|
def should_send_read_marker(self):
|
||
|
# type () -> bool
|
||
|
"""Check if we need to send out a read receipt."""
|
||
|
if not self.read_markers_enabled:
|
||
|
return False
|
||
|
|
||
|
if not self.last_read_event:
|
||
|
return True
|
||
|
|
||
|
if self.last_read_event == self.last_event_id:
|
||
|
return False
|
||
|
|
||
|
return True
|
||
|
|
||
|
@property
|
||
|
def last_event_id(self):
|
||
|
# type () -> str
|
||
|
"""Get the event id of the last shown matrix event."""
|
||
|
for line in self.weechat_buffer.lines:
|
||
|
for tag in line.tags:
|
||
|
if tag.startswith("matrix_id"):
|
||
|
event_id = tag[10:]
|
||
|
return event_id
|
||
|
|
||
|
return ""
|
||
|
|
||
|
@property
|
||
|
def read_markers_enabled(self):
|
||
|
# type: () -> bool
|
||
|
"""Check if read receipts are enabled for this room."""
|
||
|
return bool(int(W.string_eval_expression(
|
||
|
G.CONFIG.network.read_markers_conditions,
|
||
|
{},
|
||
|
{"markers_enabled": str(int(self._read_markers_enabled))},
|
||
|
{"type": "condition"}
|
||
|
)))
|
||
|
|
||
|
@read_markers_enabled.setter
|
||
|
def read_markers_enabled(self, value):
|
||
|
self._read_markers_enabled = value
|
||
|
|
||
|
def find_nick(self, user_id):
|
||
|
# type: (str) -> str
|
||
|
"""Find a suitable nick from a user_id."""
|
||
|
if user_id in self.displayed_nicks:
|
||
|
return self.displayed_nicks[user_id]
|
||
|
|
||
|
return user_id
|
||
|
|
||
|
def add_user(self, user_id, date, is_state, force_add=False):
|
||
|
# User is already added don't add him again.
|
||
|
if user_id in self.displayed_nicks:
|
||
|
return
|
||
|
|
||
|
try:
|
||
|
user = self.room.users[user_id]
|
||
|
except KeyError:
|
||
|
# No user found, he must have left already in an event that is
|
||
|
# yet to come, so do nothing
|
||
|
return
|
||
|
|
||
|
# Adding users to the nicklist is a O(1) + search time
|
||
|
# operation (the nicks are added to a linked list sorted).
|
||
|
# The search time is O(N * min(a,b)) where N is the number
|
||
|
# of nicks already added and a/b are the length of
|
||
|
# the strings that are compared at every itteration.
|
||
|
# Because the search time get's increasingly longer we're
|
||
|
# going to stop adding inactive users, they will be lazily added if
|
||
|
# they become active.
|
||
|
if is_state and not force_add and user.power_level <= 0:
|
||
|
if (len(self.displayed_nicks) >=
|
||
|
G.CONFIG.network.max_nicklist_users):
|
||
|
self.inactive_users.append(user_id)
|
||
|
return
|
||
|
|
||
|
try:
|
||
|
self.inactive_users.remove(user_id)
|
||
|
except ValueError:
|
||
|
pass
|
||
|
|
||
|
short_name = shorten_sender(user.user_id)
|
||
|
|
||
|
# TODO handle this special case for discord bridge users and
|
||
|
# freenode bridge users better
|
||
|
if (user.user_id.startswith("@_discord_") or
|
||
|
user.user_id.startswith("@_slack_") or
|
||
|
user.user_id.startswith("@whatsapp_") or
|
||
|
user.user_id.startswith("@facebook_") or
|
||
|
user.user_id.startswith("@telegram_") or
|
||
|
user.user_id.startswith("@_xmpp_")):
|
||
|
if user.display_name:
|
||
|
short_name = user.display_name[0:50]
|
||
|
elif user.user_id.startswith("@twilio_"):
|
||
|
short_name = shorten_sender(user.user_id[7:])
|
||
|
elif user.user_id.startswith("@freenode_"):
|
||
|
short_name = shorten_sender(user.user_id[9:])
|
||
|
elif user.user_id.startswith("@_ircnet_"):
|
||
|
short_name = shorten_sender(user.user_id[8:])
|
||
|
elif user.user_id.startswith("@gitter_"):
|
||
|
short_name = shorten_sender(user.user_id[7:])
|
||
|
|
||
|
# TODO make this configurable
|
||
|
if not short_name or short_name in self.displayed_nicks.values():
|
||
|
# Use the full user id, but don't include the @
|
||
|
nick = user_id[1:]
|
||
|
else:
|
||
|
nick = short_name
|
||
|
|
||
|
buffer_user = RoomUser(nick, user_id, user.power_level, date)
|
||
|
self.displayed_nicks[user_id] = nick
|
||
|
|
||
|
if self.room.own_user_id == user_id:
|
||
|
buffer_user.color = "weechat.color.chat_nick_self"
|
||
|
user.nick_color = "weechat.color.chat_nick_self"
|
||
|
|
||
|
self.weechat_buffer.join(buffer_user, date, not is_state)
|
||
|
|
||
|
def handle_membership_events(self, event, is_state):
|
||
|
date = server_ts_to_weechat(event.server_timestamp)
|
||
|
|
||
|
if event.content["membership"] == "join":
|
||
|
if (event.state_key not in self.displayed_nicks
|
||
|
and event.state_key not in self.inactive_users):
|
||
|
if len(self.room.users) > 100:
|
||
|
self.unhandled_users.append(event.state_key)
|
||
|
return
|
||
|
|
||
|
self.add_user(event.state_key, date, is_state)
|
||
|
else:
|
||
|
# TODO print out profile changes
|
||
|
return
|
||
|
|
||
|
elif event.content["membership"] == "leave":
|
||
|
if event.state_key in self.unhandled_users:
|
||
|
self.unhandled_users.remove(event.state_key)
|
||
|
return
|
||
|
|
||
|
nick = self.find_nick(event.state_key)
|
||
|
if event.sender == event.state_key:
|
||
|
self.weechat_buffer.part(nick, date, not is_state)
|
||
|
else:
|
||
|
self.weechat_buffer.kick(nick, date, not is_state)
|
||
|
|
||
|
if event.state_key in self.displayed_nicks:
|
||
|
del self.displayed_nicks[event.state_key]
|
||
|
|
||
|
# We left the room, remember the event id of our leave, if we
|
||
|
# rejoin we get events that came before this event as well as
|
||
|
# after our leave, this way we know where to continue
|
||
|
if event.state_key == self.room.own_user_id:
|
||
|
self.leave_event_id = event.event_id
|
||
|
|
||
|
elif event.content["membership"] == "invite":
|
||
|
if is_state:
|
||
|
return
|
||
|
|
||
|
self.weechat_buffer.invite(event.state_key, date)
|
||
|
return
|
||
|
|
||
|
self.update_buffer_name()
|
||
|
|
||
|
def update_buffer_name(self):
|
||
|
room_name = self.room.display_name
|
||
|
self.weechat_buffer.short_name = room_name
|
||
|
|
||
|
if G.CONFIG.human_buffer_names:
|
||
|
buffer_name = "{}.{}".format(self.server_name, room_name)
|
||
|
self.weechat_buffer.name = buffer_name
|
||
|
|
||
|
def _redact_line(self, event):
|
||
|
def predicate(event_id, line):
|
||
|
def already_redacted(tags):
|
||
|
if SCRIPT_NAME + "_redacted" in tags:
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
event_tag = SCRIPT_NAME + "_id_{}".format(event_id)
|
||
|
tags = line.tags
|
||
|
|
||
|
if event_tag in tags and not already_redacted(tags):
|
||
|
return True
|
||
|
|
||
|
return False
|
||
|
|
||
|
def redact_string(message):
|
||
|
new_message = ""
|
||
|
|
||
|
if G.CONFIG.look.redactions == RedactType.STRIKETHROUGH:
|
||
|
plaintext_msg = W.string_remove_color(message, "")
|
||
|
new_message = string_strikethrough(plaintext_msg)
|
||
|
elif G.CONFIG.look.redactions == RedactType.NOTICE:
|
||
|
new_message = message
|
||
|
elif G.CONFIG.look.redactions == RedactType.DELETE:
|
||
|
pass
|
||
|
|
||
|
return new_message
|
||
|
|
||
|
lines = self.weechat_buffer.find_lines(
|
||
|
partial(predicate, event.redacts)
|
||
|
)
|
||
|
|
||
|
# No line to redact, return early
|
||
|
if not lines:
|
||
|
return
|
||
|
|
||
|
censor = self.find_nick(event.sender)
|
||
|
|
||
|
reason = (
|
||
|
""
|
||
|
if not event.reason
|
||
|
else ', reason: "{reason}"'.format(reason=event.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,
|
||
|
)
|
||
|
|
||
|
line = lines[0]
|
||
|
message = line.message
|
||
|
tags = line.tags
|
||
|
|
||
|
new_message = redact_string(message)
|
||
|
message = " ".join(s for s in [new_message, redaction_msg] if s)
|
||
|
tags.append("matrix_redacted")
|
||
|
|
||
|
line.message = message
|
||
|
line.tags = tags
|
||
|
|
||
|
for line in lines[1:]:
|
||
|
message = line.message
|
||
|
tags = line.tags
|
||
|
|
||
|
new_message = redact_string(message)
|
||
|
|
||
|
if not new_message:
|
||
|
new_message = redaction_msg
|
||
|
elif G.CONFIG.look.redactions == RedactType.NOTICE:
|
||
|
new_message += " {}".format(redaction_msg)
|
||
|
|
||
|
tags.append("matrix_redacted")
|
||
|
|
||
|
line.message = new_message
|
||
|
line.tags = tags
|
||
|
|
||
|
def _handle_redacted_message(self, event):
|
||
|
nick = self.find_nick(event.sender)
|
||
|
date = server_ts_to_weechat(event.server_timestamp)
|
||
|
tags = self.get_event_tags(event)
|
||
|
tags.append(SCRIPT_NAME + "_redacted")
|
||
|
|
||
|
reason = (
|
||
|
', reason: "{reason}"'.format(reason=event.reason)
|
||
|
if event.reason
|
||
|
else ""
|
||
|
)
|
||
|
|
||
|
censor = self.find_nick(event.redacter)
|
||
|
|
||
|
data = (
|
||
|
"{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,
|
||
|
)
|
||
|
|
||
|
self.weechat_buffer.message(nick, data, date, tags)
|
||
|
|
||
|
def _handle_topic(self, event, is_state):
|
||
|
nick = self.find_nick(event.sender)
|
||
|
|
||
|
self.weechat_buffer.change_topic(
|
||
|
nick,
|
||
|
event.topic,
|
||
|
server_ts_to_weechat(event.server_timestamp),
|
||
|
not is_state,
|
||
|
)
|
||
|
|
||
|
@staticmethod
|
||
|
def get_event_tags(event):
|
||
|
# type: (Event) -> List[str]
|
||
|
tags = [SCRIPT_NAME + "_id_{}".format(event.event_id)]
|
||
|
if event.sender_key:
|
||
|
tags.append(SCRIPT_NAME + "_senderkey_{}".format(event.sender_key))
|
||
|
if event.session_id:
|
||
|
tags.append(SCRIPT_NAME + "_session_id_{}".format(
|
||
|
event.session_id
|
||
|
))
|
||
|
|
||
|
return tags
|
||
|
|
||
|
def _handle_power_level(self, _):
|
||
|
for user_id in self.room.power_levels.users:
|
||
|
if user_id in self.displayed_nicks:
|
||
|
nick = self.find_nick(user_id)
|
||
|
|
||
|
user = self.weechat_buffer.users[nick]
|
||
|
user.power_level = self.room.power_levels.get_user_level(
|
||
|
user_id
|
||
|
)
|
||
|
|
||
|
# There is no way to change the group of a user without
|
||
|
# removing him from the nicklist
|
||
|
self.weechat_buffer.remove_user_from_nicklist(user)
|
||
|
self.weechat_buffer._add_user_to_nicklist(user)
|
||
|
|
||
|
def handle_state_event(self, event):
|
||
|
if isinstance(event, RoomMemberEvent):
|
||
|
self.handle_membership_events(event, True)
|
||
|
elif isinstance(event, RoomTopicEvent):
|
||
|
self._handle_topic(event, True)
|
||
|
elif isinstance(event, PowerLevelsEvent):
|
||
|
self._handle_power_level(event)
|
||
|
elif isinstance(event, (RoomNameEvent, RoomAliasEvent)):
|
||
|
self.update_buffer_name()
|
||
|
elif isinstance(event, RoomEncryptionEvent):
|
||
|
pass
|
||
|
|
||
|
def handle_own_message_in_timeline(self, event):
|
||
|
"""Check if our own message is already printed if not print it.
|
||
|
This function is called for messages that contain a transaction id
|
||
|
indicating that they were sent out using our own client. If we sent out
|
||
|
a message but never got a valid server response (e.g. due to
|
||
|
disconnects) this function prints them out using data from the next
|
||
|
sync response"""
|
||
|
uuid = UUID(event.transaction_id)
|
||
|
message = self.sent_messages_queue.pop(uuid, None)
|
||
|
|
||
|
# We already got a response to the room_send_message() API call and
|
||
|
# handled the message, no need to print it out again
|
||
|
if not message:
|
||
|
return
|
||
|
|
||
|
message.event_id = event.event_id
|
||
|
if uuid in self.printed_before_ack_queue:
|
||
|
self.replace_printed_line_by_uuid(
|
||
|
event.transaction_id,
|
||
|
message
|
||
|
)
|
||
|
self.printed_before_ack_queue.remove(uuid)
|
||
|
return
|
||
|
|
||
|
if isinstance(message, OwnAction):
|
||
|
self.self_action(message)
|
||
|
elif isinstance(message, OwnMessage):
|
||
|
self.self_message(message)
|
||
|
return
|
||
|
|
||
|
def handle_timeline_event(self, event):
|
||
|
# TODO this should be done for every messagetype that gets printed in
|
||
|
# the buffer
|
||
|
if isinstance(event, (RoomMessage, MegolmEvent)):
|
||
|
if (event.sender not in self.displayed_nicks and
|
||
|
event.sender in self.room.users):
|
||
|
|
||
|
try:
|
||
|
self.unhandled_users.remove(event.sender)
|
||
|
except ValueError:
|
||
|
pass
|
||
|
|
||
|
self.add_user(event.sender, 0, True, True)
|
||
|
|
||
|
if event.transaction_id:
|
||
|
self.handle_own_message_in_timeline(event)
|
||
|
return
|
||
|
|
||
|
if isinstance(event, RoomMemberEvent):
|
||
|
self.handle_membership_events(event, False)
|
||
|
|
||
|
elif isinstance(event, (RoomNameEvent, RoomAliasEvent)):
|
||
|
self.update_buffer_name()
|
||
|
|
||
|
elif isinstance(event, RoomTopicEvent):
|
||
|
self._handle_topic(event, False)
|
||
|
|
||
|
# Emotes are a subclass of RoomMessageText, so put them before the text
|
||
|
# ones
|
||
|
elif isinstance(event, RoomMessageEmote):
|
||
|
nick = self.find_nick(event.sender)
|
||
|
date = server_ts_to_weechat(event.server_timestamp)
|
||
|
|
||
|
extra_prefix = (self.warning_prefix if event.decrypted
|
||
|
and not event.verified else "")
|
||
|
|
||
|
self.weechat_buffer.action(
|
||
|
nick, event.body, date, self.get_event_tags(event),
|
||
|
extra_prefix
|
||
|
)
|
||
|
|
||
|
elif isinstance(event, RoomMessageText):
|
||
|
nick = self.find_nick(event.sender)
|
||
|
formatted = None
|
||
|
|
||
|
if event.formatted_body:
|
||
|
formatted = Formatted.from_html(event.formatted_body)
|
||
|
|
||
|
data = formatted.to_weechat() if formatted else event.body
|
||
|
|
||
|
extra_prefix = (self.warning_prefix if event.decrypted
|
||
|
and not event.verified else "")
|
||
|
|
||
|
date = server_ts_to_weechat(event.server_timestamp)
|
||
|
self.weechat_buffer.message(
|
||
|
nick, data, date, self.get_event_tags(event), extra_prefix
|
||
|
)
|
||
|
|
||
|
elif isinstance(event, RoomMessageNotice):
|
||
|
nick = self.find_nick(event.sender)
|
||
|
date = server_ts_to_weechat(event.server_timestamp)
|
||
|
extra_prefix = (self.warning_prefix if event.decrypted
|
||
|
and not event.verified else "")
|
||
|
|
||
|
self.weechat_buffer.notice(
|
||
|
nick, event.body, date, self.get_event_tags(event),
|
||
|
extra_prefix
|
||
|
)
|
||
|
|
||
|
elif isinstance(event, RoomMessageMedia):
|
||
|
nick = self.find_nick(event.sender)
|
||
|
date = server_ts_to_weechat(event.server_timestamp)
|
||
|
http_url = Api.mxc_to_http(event.url, self.homeserver.geturl())
|
||
|
url = http_url if http_url else event.url
|
||
|
|
||
|
description = "/{}".format(event.body) if event.body else ""
|
||
|
data = "{url}{desc}".format(url=url, desc=description)
|
||
|
|
||
|
extra_prefix = (self.warning_prefix if event.decrypted
|
||
|
and not event.verified else "")
|
||
|
|
||
|
self.weechat_buffer.message(
|
||
|
nick, data, date, self.get_event_tags(event), extra_prefix
|
||
|
)
|
||
|
|
||
|
elif isinstance(event, RoomEncryptedMedia):
|
||
|
nick = self.find_nick(event.sender)
|
||
|
date = server_ts_to_weechat(event.server_timestamp)
|
||
|
http_url = Api.encrypted_mxc_to_plumb(
|
||
|
event.url,
|
||
|
event.key["k"],
|
||
|
event.hashes["sha256"],
|
||
|
event.iv,
|
||
|
self.homeserver.geturl()
|
||
|
)
|
||
|
url = http_url if http_url else event.url
|
||
|
|
||
|
description = "{}".format(event.body) if event.body else "file"
|
||
|
data = ("{del_color}<{ncolor}{desc}{del_color}>{ncolor} "
|
||
|
"{del_color}[{ncolor}{url}{del_color}]{ncolor}").format(
|
||
|
del_color=W.color("chat_delimiters"),
|
||
|
ncolor=W.color("reset"),
|
||
|
desc=description, url=url)
|
||
|
|
||
|
extra_prefix = (self.warning_prefix if event.decrypted
|
||
|
and not event.verified else "")
|
||
|
|
||
|
self.weechat_buffer.message(
|
||
|
nick, data, date, self.get_event_tags(event), extra_prefix
|
||
|
)
|
||
|
|
||
|
elif isinstance(event, RoomMessageUnknown):
|
||
|
nick = self.find_nick(event.sender)
|
||
|
date = server_ts_to_weechat(event.server_timestamp)
|
||
|
data = ("Unknown message of type {t}").format(t=event.type)
|
||
|
extra_prefix = (self.warning_prefix if event.decrypted
|
||
|
and not event.verified else "")
|
||
|
|
||
|
self.weechat_buffer.message(
|
||
|
nick, data, date, self.get_event_tags(event), extra_prefix
|
||
|
)
|
||
|
|
||
|
elif isinstance(event, RedactionEvent):
|
||
|
self._redact_line(event)
|
||
|
|
||
|
elif isinstance(event, RedactedEvent):
|
||
|
self._handle_redacted_message(event)
|
||
|
|
||
|
elif isinstance(event, RoomEncryptionEvent):
|
||
|
message = (
|
||
|
"This room is encrypted, encryption is "
|
||
|
"currently unsuported. Message sending is disabled for "
|
||
|
"this room."
|
||
|
)
|
||
|
self.weechat_buffer.error(message)
|
||
|
|
||
|
elif isinstance(event, PowerLevelsEvent):
|
||
|
self._handle_power_level(event)
|
||
|
|
||
|
elif isinstance(event, MegolmEvent):
|
||
|
nick = self.find_nick(event.sender)
|
||
|
date = server_ts_to_weechat(event.server_timestamp)
|
||
|
|
||
|
data = ("{del_color}<{log_color}Unable to decrypt: "
|
||
|
"The sender's device has not sent us "
|
||
|
"the keys for this message{del_color}>{ncolor}").format(
|
||
|
del_color=W.color("chat_delimiters"),
|
||
|
log_color=W.color("logger.color.backlog_line"),
|
||
|
ncolor=W.color("reset"))
|
||
|
session_id_tag = SCRIPT_NAME + "_sessionid_" + event.session_id
|
||
|
self.weechat_buffer.message(
|
||
|
nick,
|
||
|
data,
|
||
|
date,
|
||
|
self.get_event_tags(event) + [session_id_tag]
|
||
|
)
|
||
|
|
||
|
self.undecrypted_events.append(event)
|
||
|
|
||
|
elif isinstance(event, UnknownEvent):
|
||
|
pass
|
||
|
|
||
|
elif isinstance(event, BadEvent):
|
||
|
nick = self.find_nick(event.sender)
|
||
|
date = server_ts_to_weechat(event.server_timestamp)
|
||
|
data = ("Bad event received, event type: {t}").format(t=event.type)
|
||
|
extra_prefix = self.warning_prefix
|
||
|
|
||
|
self.weechat_buffer.message(
|
||
|
nick, data, date, self.get_event_tags(event), extra_prefix
|
||
|
)
|
||
|
|
||
|
elif isinstance(event, UnknownBadEvent):
|
||
|
self.error("Unkwnown bad event: {}".format(
|
||
|
pprint.pformat(event.event_dict)
|
||
|
))
|
||
|
|
||
|
else:
|
||
|
W.prnt(
|
||
|
"", "Unhandled event of type {}.".format(type(event).__name__)
|
||
|
)
|
||
|
|
||
|
def self_message(self, message):
|
||
|
# type: (OwnMessage) -> None
|
||
|
nick = self.find_nick(self.room.own_user_id)
|
||
|
data = message.formatted_message.to_weechat()
|
||
|
if message.event_id:
|
||
|
tags = [SCRIPT_NAME + "_id_{}".format(message.event_id)]
|
||
|
else:
|
||
|
tags = [SCRIPT_NAME + "_uuid_{}".format(message.uuid)]
|
||
|
date = message.age
|
||
|
|
||
|
self.weechat_buffer.self_message(nick, data, date, tags)
|
||
|
|
||
|
def self_action(self, message):
|
||
|
# type: (OwnMessage) -> None
|
||
|
nick = self.find_nick(self.room.own_user_id)
|
||
|
date = message.age
|
||
|
if message.event_id:
|
||
|
tags = [SCRIPT_NAME + "_id_{}".format(message.event_id)]
|
||
|
else:
|
||
|
tags = [SCRIPT_NAME + "_uuid_{}".format(message.uuid)]
|
||
|
|
||
|
self.weechat_buffer.self_action(
|
||
|
nick, message.formatted_message.to_weechat(), date, tags
|
||
|
)
|
||
|
|
||
|
@staticmethod
|
||
|
def _find_by_uuid_predicate(uuid, line):
|
||
|
uuid_tag = SCRIPT_NAME + "_uuid_{}".format(uuid)
|
||
|
tags = line.tags
|
||
|
|
||
|
if uuid_tag in tags:
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
def mark_message_as_unsent(self, uuid, _):
|
||
|
"""Append to already printed lines that are greyed out an error
|
||
|
message"""
|
||
|
lines = self.weechat_buffer.find_lines(
|
||
|
partial(self._find_by_uuid_predicate, uuid)
|
||
|
)
|
||
|
last_line = lines[-1]
|
||
|
|
||
|
message = last_line.message
|
||
|
message += (" {del_color}<{ncolor}{error_color}Error sending "
|
||
|
"message{del_color}>{ncolor}").format(
|
||
|
del_color=W.color("chat_delimiters"),
|
||
|
ncolor=W.color("reset"),
|
||
|
error_color=W.color(color_pair(
|
||
|
G.CONFIG.color.error_message_fg,
|
||
|
G.CONFIG.color.error_message_bg)))
|
||
|
|
||
|
last_line.message = message
|
||
|
|
||
|
def replace_printed_line_by_uuid(self, uuid, new_message):
|
||
|
"""Replace already printed lines that are greyed out with real ones."""
|
||
|
if isinstance(new_message, OwnAction):
|
||
|
displayed_nick = self.displayed_nicks[self.room.own_user_id]
|
||
|
user = self.weechat_buffer._get_user(displayed_nick)
|
||
|
data = self.weechat_buffer._format_action(
|
||
|
user,
|
||
|
new_message.formatted_message.to_weechat()
|
||
|
)
|
||
|
new_lines = data.split("\n")
|
||
|
else:
|
||
|
new_lines = new_message.formatted_message.to_weechat().split("\n")
|
||
|
|
||
|
line_count = len(new_lines)
|
||
|
|
||
|
lines = self.weechat_buffer.find_lines(
|
||
|
partial(self._find_by_uuid_predicate, uuid), line_count
|
||
|
)
|
||
|
|
||
|
for i, line in enumerate(lines):
|
||
|
line.message = new_lines[i]
|
||
|
tags = line.tags
|
||
|
|
||
|
new_tags = [
|
||
|
tag for tag in tags
|
||
|
if not tag.startswith(SCRIPT_NAME + "_uuid_")
|
||
|
]
|
||
|
new_tags.append(SCRIPT_NAME + "_id_" + new_message.event_id)
|
||
|
line.tags = new_tags
|
||
|
|
||
|
def replace_undecrypted_line(self, event):
|
||
|
"""Find a undecrypted message in the buffer and replace it with the now
|
||
|
decrypted event."""
|
||
|
# TODO different messages need different formatting
|
||
|
# To implement this, refactor out the different formatting code
|
||
|
# snippets to a Formatter class and reuse them here.
|
||
|
if not isinstance(event, RoomMessageText):
|
||
|
return
|
||
|
|
||
|
def predicate(event_id, line):
|
||
|
event_tag = SCRIPT_NAME + "_id_{}".format(event_id)
|
||
|
if event_tag in line.tags:
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
lines = self.weechat_buffer.find_lines(
|
||
|
partial(predicate, event.event_id)
|
||
|
)
|
||
|
|
||
|
if not lines:
|
||
|
return
|
||
|
|
||
|
formatted = None
|
||
|
if event.formatted_body:
|
||
|
formatted = Formatted.from_html(event.formatted_body)
|
||
|
|
||
|
data = formatted.to_weechat() if formatted else event.body
|
||
|
# TODO this isn't right if the data has multiple lines, that is
|
||
|
# everything is printed on a signle line and newlines are shown as a
|
||
|
# space.
|
||
|
# Weechat should support deleting lines and printing new ones at an
|
||
|
# arbitrary position.
|
||
|
# To implement this without weechat support either only handle single
|
||
|
# line messages or edit the first line in place, print new ones at the
|
||
|
# bottom and sort the buffer lines.
|
||
|
lines[0].message = data
|
||
|
|
||
|
def old_redacted(self, event):
|
||
|
tags = [
|
||
|
SCRIPT_NAME + "_message",
|
||
|
"notify_message",
|
||
|
"no_log",
|
||
|
"no_highlight",
|
||
|
]
|
||
|
reason = (
|
||
|
', reason: "{reason}"'.format(reason=event.reason)
|
||
|
if event.reason
|
||
|
else ""
|
||
|
)
|
||
|
|
||
|
censor = self.find_nick(event.redacter)
|
||
|
|
||
|
data = (
|
||
|
"{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,
|
||
|
)
|
||
|
|
||
|
tags += self.get_event_tags(event)
|
||
|
nick = self.find_nick(event.sender)
|
||
|
user = self.weechat_buffer._get_user(nick)
|
||
|
date = server_ts_to_weechat(event.server_timestamp)
|
||
|
self.weechat_buffer._print_message(user, data, date, tags)
|
||
|
|
||
|
def old_message(self, event):
|
||
|
tags = [
|
||
|
SCRIPT_NAME + "_message",
|
||
|
"notify_message",
|
||
|
"no_log",
|
||
|
"no_highlight",
|
||
|
]
|
||
|
tags += self.get_event_tags(event)
|
||
|
nick = self.find_nick(event.sender)
|
||
|
|
||
|
formatted = None
|
||
|
|
||
|
if event.formatted_body:
|
||
|
formatted = Formatted.from_html(event.formatted_body)
|
||
|
|
||
|
data = formatted.to_weechat() if formatted else event.body
|
||
|
user = self.weechat_buffer._get_user(nick)
|
||
|
date = server_ts_to_weechat(event.server_timestamp)
|
||
|
self.weechat_buffer._print_message(user, data, date, tags)
|
||
|
|
||
|
def sort_messages(self):
|
||
|
class LineCopy(object):
|
||
|
def __init__(
|
||
|
self, date, date_printed, tags, prefix, message, highlight
|
||
|
):
|
||
|
self.date = date
|
||
|
self.date_printed = date_printed
|
||
|
self.tags = tags
|
||
|
self.prefix = prefix
|
||
|
self.message = message
|
||
|
self.highlight = highlight
|
||
|
|
||
|
@classmethod
|
||
|
def from_line(cls, line):
|
||
|
return cls(
|
||
|
line.date,
|
||
|
line.date_printed,
|
||
|
line.tags,
|
||
|
line.prefix,
|
||
|
line.message,
|
||
|
line.highlight,
|
||
|
)
|
||
|
|
||
|
lines = [
|
||
|
LineCopy.from_line(line) for line in self.weechat_buffer.lines
|
||
|
]
|
||
|
sorted_lines = sorted(lines, key=lambda line: line.date, reverse=True)
|
||
|
|
||
|
for line_number, line in enumerate(self.weechat_buffer.lines):
|
||
|
new = sorted_lines[line_number]
|
||
|
line.update(
|
||
|
new.date, new.date_printed, new.tags, new.prefix, new.message
|
||
|
)
|
||
|
|
||
|
def handle_backlog(self, response):
|
||
|
self.prev_batch = response.end
|
||
|
|
||
|
for event in response.chunk:
|
||
|
if isinstance(event, RoomMessageText):
|
||
|
self.old_message(event)
|
||
|
elif isinstance(event, RedactedEvent):
|
||
|
self.old_redacted(event)
|
||
|
|
||
|
self.sort_messages()
|
||
|
|
||
|
self.backlog_pending = False
|
||
|
|
||
|
def handle_joined_room(self, info):
|
||
|
for event in info.state:
|
||
|
self.handle_state_event(event)
|
||
|
|
||
|
timeline_events = None
|
||
|
|
||
|
# This is a rejoin, skip already handled events
|
||
|
if not self.joined:
|
||
|
leave_index = None
|
||
|
|
||
|
for i, event in enumerate(info.timeline.events):
|
||
|
if event.event_id == self.leave_event_id:
|
||
|
leave_index = i
|
||
|
break
|
||
|
|
||
|
if leave_index:
|
||
|
timeline_events = info.timeline.events[leave_index + 1:]
|
||
|
# Handle our leave as a state event since we're not in the
|
||
|
# nicklist anymore but we're already printed out our leave
|
||
|
self.handle_state_event(info.timeline.events[leave_index])
|
||
|
else:
|
||
|
timeline_events = info.timeline.events
|
||
|
|
||
|
# mark that we are now joined
|
||
|
self.joined = True
|
||
|
|
||
|
else:
|
||
|
timeline_events = info.timeline.events
|
||
|
|
||
|
for event in timeline_events:
|
||
|
self.handle_timeline_event(event)
|
||
|
|
||
|
for event in info.account_data:
|
||
|
if isinstance(event, FullyReadEvent):
|
||
|
if event.event_id == self.last_event_id:
|
||
|
current_buffer = W.buffer_search("", "")
|
||
|
|
||
|
if self.weechat_buffer._ptr == current_buffer:
|
||
|
continue
|
||
|
|
||
|
W.buffer_set(self.weechat_buffer._ptr, "unread", "")
|
||
|
W.buffer_set(self.weechat_buffer._ptr, "hotlist", "-1")
|
||
|
|
||
|
# We didn't handle all joined users, the room display name might still
|
||
|
# be outdated because of that, update it now.
|
||
|
if self.unhandled_users:
|
||
|
self.update_buffer_name()
|
||
|
|
||
|
def handle_left_room(self, info):
|
||
|
self.joined = False
|
||
|
|
||
|
for event in info.state:
|
||
|
self.handle_state_event(event)
|
||
|
|
||
|
for event in info.timeline.events:
|
||
|
self.handle_timeline_event(event)
|
||
|
|
||
|
def error(self, string):
|
||
|
# type: (str) -> None
|
||
|
self.weechat_buffer.error(string)
|