buffer: Add a matrix specific buffer class.

This commit is contained in:
poljar (Damir Jelić) 2018-07-06 14:56:06 +02:00
parent ae5f97ce1d
commit 66507b23ce
3 changed files with 415 additions and 241 deletions

View file

@ -18,11 +18,29 @@
from __future__ import unicode_literals
import time
from builtins import super
from functools import partial
from .globals import W, SERVERS, SCRIPT_NAME
from .globals import W, SERVERS, OPTIONS, SCRIPT_NAME
from .utf import utf8_decode
from .colors import Formatted
from builtins import super
from .utils import shorten_sender, server_ts_to_weechat, string_strikethrough
from .plugin_options import RedactType
from .rooms import (
RoomNameEvent,
RoomAliasEvent,
RoomMembershipEvent,
RoomMemberJoin,
RoomMemberLeave,
RoomMemberInvite,
RoomTopicEvent,
RoomMessageText,
RoomMessageEmote,
RoomRedactionEvent,
RoomRedactedMessageEvent
)
@utf8_decode
@ -91,6 +109,12 @@ class WeechatChannelBuffer(object):
"self_msg",
"log1"
],
"action": [
SCRIPT_NAME + "_message",
SCRIPT_NAME + "_action",
"notify_message",
"log1",
],
"old_message": [
SCRIPT_NAME + "_message",
"notify_message",
@ -126,6 +150,91 @@ class WeechatChannelBuffer(object):
"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": 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": 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, date_printed, tags, prefix, message):
new_data = {
"date": date,
"date_printed": date_printed,
"tags_array": ','.join(tags),
"prefix": prefix,
"message": message,
# "highlight": highlight
}
W.hdata_update(self._hdata, self._ptr, new_data)
def __init__(self, name, server_name, user):
# type: (str, str, str)
self._ptr = W.buffer_new(
@ -186,6 +295,8 @@ class WeechatChannelBuffer(object):
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,
@ -193,6 +304,36 @@ class WeechatChannelBuffer(object):
SCRIPT_NAME + "_message"
)
@property
def _hdata(self):
return W.hdata_get("buffer")
@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 """
@ -247,11 +388,7 @@ class WeechatChannelBuffer(object):
# A message from a non joined user
return RoomUser(nick)
def message(self, nick, message, date, tags=[]):
# type: (str, str, int, str) -> None
user = self._get_user(nick)
tags = tags or self._message_tags(user, "message")
def _print_message(self, user, message, date, tags):
prefix_string = ("" if not user.prefix else "{}{}{}".format(
W.color(self._get_prefix_color(user.prefix)),
user.prefix,
@ -267,6 +404,12 @@ class WeechatChannelBuffer(object):
self.print_date_tags(data, date, tags)
def message(self, nick, message, date, extra_tags=[]):
# type: (str, str, int, str) -> None
user = self._get_user(nick)
tags = self._message_tags(user, "message") + extra_tags
self._print_message(user, message, date, tags)
def notice(self, nick, message, date):
# type: (str, str, int) -> None
data = "{color}{message}{ncolor}".format(
@ -276,11 +419,7 @@ class WeechatChannelBuffer(object):
self.message(nick, data, date)
def action(self, nick, message, date, tags=[]):
# type: (str, str, int) -> None
user = self._get_user(nick)
tags = tags or self._message_tags(user, "action")
def _print_action(self, user, message, date, tags):
nick_prefix = ("" if not user.prefix else "{}{}{}".format(
W.color(self._get_prefix_color(user.prefix)),
user.prefix,
@ -292,12 +431,18 @@ class WeechatChannelBuffer(object):
prefix=W.prefix("action"),
nick_prefix=nick_prefix,
nick_color=W.color(user.color),
author=nick,
author=user.nick,
ncolor=W.color("reset"),
msg=message)
self.print_date_tags(data, date, tags)
def action(self, nick, message, date, extra_tags=[]):
# type: (str, str, int) -> None
user = self._get_user(nick)
tags = self._message_tags(user, "action") + extra_tags
self._print_action(user, message, date, tags)
@staticmethod
def _get_nicklist_group(user):
# type: (WeechatUser) -> str
@ -456,13 +601,13 @@ class WeechatChannelBuffer(object):
def self_message(self, nick, message, date):
user = self._get_user(nick)
tags = self._message_tags(user, "self_message")
self.message(nick, message, date, tags)
self._print_message(user, message, date, tags)
def self_action(self, nick, message, date):
user = self._get_user(nick)
tags = self._message_tags(user, "self_message")
tags.append(SCRIPT_NAME + "_action")
self.action(nick, message, date, tags)
self._print_action(user, message, date, tags)
@property
def short_name(self):
@ -471,3 +616,226 @@ class WeechatChannelBuffer(object):
@short_name.setter
def short_name(self, name):
W.buffer_set(self._ptr, "short_name", name)
def find_lines(self, predicate):
lines = []
for line in self.lines:
if predicate(line):
lines.append(line)
return lines
class RoomBuffer(object):
def __init__(self, room, server_name):
self.room = room
user = shorten_sender(self.room.own_user_id)
self.weechat_buffer = WeechatChannelBuffer(
room.room_id,
server_name,
user
)
def handle_membership_events(self, event, is_state):
def join(event, date, is_state):
user = self.room.users[event.sender]
buffer_user = RoomUser(user.name, event.sender)
# TODO remove this duplication
user.nick_color = buffer_user.color
if self.room.own_user_id == event.sender:
buffer_user.color = "weechat.color.chat_nick_self"
user.nick_color = "weechat.color.chat_nick_self"
self.weechat_buffer.join(
buffer_user,
server_ts_to_weechat(event.timestamp),
not is_state
)
date = server_ts_to_weechat(event.timestamp)
if isinstance(event, RoomMemberJoin):
if event.prev_content and "membership" in event.prev_content:
if (event.prev_content["membership"] == "leave"
or event.prev_content["membership"] == "invite"):
join(event, date, is_state)
else:
# TODO print out profile changes
return
else:
# No previous content for this user in this room, so he just
# joined.
join(event, date, is_state)
elif isinstance(event, RoomMemberLeave):
# TODO the nick can be a display name or a full sender name
nick = shorten_sender(event.sender)
if event.sender == event.leaving_user:
self.weechat_buffer.part(nick, date, not is_state)
else:
self.weechat_buffer.kick(nick, date, not is_state)
elif isinstance(event, RoomMemberInvite):
if is_state:
return
self.weechat_buffer.invite(event.invited_user, date)
return
room_name = self.room.display_name(self.room.own_user_id)
self.weechat_buffer.short_name = room_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
lines = self.weechat_buffer.find_lines(
partial(predicate, event.redaction_id)
)
# No line to redact, return early
if not lines:
return
# TODO multiple lines can contain a single matrix ID, we need to redact
# them all
line = lines[0]
# TODO the censor may not be in the room anymore
censor = self.room.users[event.sender].name
message = line.message
tags = line.tags
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)
new_message = ""
if OPTIONS.redaction_type == RedactType.STRIKETHROUGH:
plaintext_msg = W.string_remove_color(message, '')
new_message = string_strikethrough(plaintext_msg)
elif OPTIONS.redaction_type == RedactType.NOTICE:
new_message = message
elif OPTIONS.redaction_type == RedactType.DELETE:
pass
message = " ".join(s for s in [new_message, redaction_msg] if s)
tags.append("matrix_redacted")
line.message = message
line.tags = tags
def _handle_redacted_message(self, event):
# TODO user doesn't have to be in the room anymore
user = self.room.users[event.sender]
date = server_ts_to_weechat(event.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.room.users[event.censor]
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.name,
reason=reason)
self.weechat_buffer.message(user.name, data, date, tags)
def _handle_topic(self, event, is_state):
try:
user = self.room.users[event.sender]
nick = user.name
except KeyError:
nick = event.sender
self.weechat_buffer.change_topic(
nick,
event.topic,
server_ts_to_weechat(event.timestamp),
not is_state)
@staticmethod
def get_event_tags(event):
return ["matrix_id_{}".format(event.event_id)]
def handle_state_event(self, event):
if isinstance(event, RoomMembershipEvent):
self.handle_membership_events(event, True)
elif isinstance(event, RoomTopicEvent):
self._handle_topic(event, True)
def handle_timeline_event(self, event):
if isinstance(event, RoomMembershipEvent):
self.handle_membership_events(event, False)
elif isinstance(event, (RoomNameEvent, RoomAliasEvent)):
room_name = self.room.display_name(self.room.own_user_id)
self.weechat_buffer.short_name = room_name
elif isinstance(event, RoomTopicEvent):
self._handle_topic(event, False)
elif isinstance(event, RoomMessageText):
user = self.room.users[event.sender]
data = (event.formatted_message.to_weechat()
if event.formatted_message else event.message)
date = server_ts_to_weechat(event.timestamp)
self.weechat_buffer.message(
user.name,
data,
date,
self.get_event_tags(event)
)
elif isinstance(event, RoomMessageEmote):
user = self.room.users[event.sender]
date = server_ts_to_weechat(event.timestamp)
self.weechat_buffer.action(
user.name,
event.message,
date,
self.get_event_tags(event)
)
elif isinstance(event, RoomRedactionEvent):
self._redact_line(event)
elif isinstance(event, RoomRedactedMessageEvent):
self._handle_redacted_message(event)
def self_message(self, message):
user = self.room.users[self.room.own_user_id]
data = (message.formatted_message.to_weechat()
if message.formatted_message
else message.message)
date = server_ts_to_weechat(message.timestamp)
self.weechat_buffer.self_message(user.name, data, date)
def self_action(self, message):
user = self.room.users[self.room.own_user_id]
date = server_ts_to_weechat(message.timestamp)
self.weechat_buffer.self_action(user.name, message.message, date)

View file

@ -40,10 +40,11 @@ PowerLevel = namedtuple('PowerLevel', ['user', 'level'])
class MatrixRoom:
def __init__(self, room_id):
def __init__(self, room_id, own_user_id):
# type: (str) -> None
# yapf: disable
self.room_id = room_id # type: str
self.own_user_id = own_user_id
self.canonical_alias = None # type: str
self.name = None # type: str
self.topic = "" # type: str
@ -153,9 +154,23 @@ class MatrixRoom:
short_name = shorten_sender(event.sender)
user = MatrixUser(short_name, event.display_name)
self.users[event.sender] = user
return True
elif isinstance(event, RoomMemberLeave):
if event.leaving_user in self.users:
del self.users[event.leaving_user]
return True
elif isinstance(event, RoomNameEvent):
self.name = event.name
elif isinstance(event, RoomAliasEvent):
self.canonical_alias = event.canonical_alias
elif isinstance(event, RoomEncryptionEvent):
self.encrypted = True
return False
class MatrixUser:
@ -337,35 +352,6 @@ class RoomRedactedMessageEvent(RoomEvent):
return cls(event_id, sender, timestamp, censor, reason)
def execute(self, server, room, buff, tags):
nick, color_name = sender_to_nick_and_color(room, self.sender)
color = color_for_tags(color_name)
date = server_ts_to_weechat(self.timestamp)
event_tags = add_event_tags(self.event_id, nick, color, tags)
reason = (", reason: \"{reason}\"".format(reason=self.reason)
if self.reason else "")
censor, _ = sender_to_nick_and_color(room, self.censor)
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)
event_tags.append("matrix_redacted")
tags_string = ",".join(event_tags)
data = "{author}\t{msg}".format(author=nick, msg=msg)
W.prnt_date_tags(buff, date, tags_string, data)
class RoomMessageEvent(RoomEvent):
@classmethod
@ -464,12 +450,6 @@ class RoomMessageText(RoomMessageEvent):
return cls(event_id, sender, timestamp, msg, formatted_msg)
def execute(self, server, room, buff, tags):
msg = (self.formatted_message.to_weechat()
if self.formatted_message else self.message)
self._print_message(msg, room, buff, tags)
class RoomMessageEmote(RoomMessageSimple):
@ -688,60 +668,6 @@ class RoomRedactionEvent(RoomEvent):
return cls(event_id, sender, timestamp, redaction_id, reason)
@staticmethod
def already_redacted(tags):
if "matrix_redacted" in tags:
return True
return False
def _redact_line(self, data_pointer, tags, room, buff):
hdata_line_data = W.hdata_get('line_data')
message = W.hdata_string(hdata_line_data, data_pointer, 'message')
censor, _ = sender_to_nick_and_color(room, self.sender)
reason = ("" if not self.reason else
", reason: \"{reason}\"".format(reason=self.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)
new_message = ""
if OPTIONS.redaction_type == RedactType.STRIKETHROUGH:
plaintext_msg = W.string_remove_color(message, '')
new_message = string_strikethrough(plaintext_msg)
elif OPTIONS.redaction_type == RedactType.NOTICE:
new_message = message
elif OPTIONS.redaction_type == RedactType.DELETE:
pass
message = " ".join(s for s in [new_message, redaction_msg] if s)
tags.append("matrix_redacted")
new_data = {'tags_array': ','.join(tags), 'message': message}
W.hdata_update(hdata_line_data, data_pointer, new_data)
def execute(self, server, room, buff, tags):
data_pointer, tags = line_pointer_and_tags_from_event(
buff, self.redaction_id)
if not data_pointer:
return
if RoomRedactionEvent.already_redacted(tags):
return
self._redact_line(data_pointer, tags, room, buff)
class RoomNameEvent(RoomEvent):
@ -759,18 +685,6 @@ class RoomNameEvent(RoomEvent):
return cls(event_id, sender, timestamp, name)
def execute(self, server, room, buff, tags):
if not self.name:
return
room.name = self.name
W.buffer_set(buff, "name", self.name)
W.buffer_set(buff, "localvar_set_channel", self.name)
# calculate room display name and set it as the buffer list name
room_name = room.display_name(server.user_id)
W.buffer_set(buff, "short_name", room_name)
class RoomAliasEvent(RoomEvent):
@ -788,19 +702,6 @@ class RoomAliasEvent(RoomEvent):
return cls(event_id, sender, timestamp, canonical_alias)
def execute(self, server, room, buff, tags):
if not self.canonical_alias:
return
# TODO: What should we do with this?
# W.buffer_set(buff, "name", self.name)
# W.buffer_set(buff, "localvar_set_channel", self.name)
# calculate room display name and set it as the buffer list name
room.canonical_alias = self.canonical_alias
room_name = room.display_name(server.user_id)
W.buffer_set(buff, "short_name", room_name)
class RoomEncryptionEvent(RoomEvent):

View file

@ -34,7 +34,7 @@ from matrix.utils import (key_from_value, prnt_debug, server_buffer_prnt,
from matrix.utf import utf8_decode
from matrix.globals import W, SERVERS, OPTIONS
import matrix.api as API
from .buffer import WeechatChannelBuffer, RoomUser
from .buffer import RoomBuffer
from .rooms import (
MatrixRoom,
RoomMessageText,
@ -633,101 +633,6 @@ class MatrixServer:
server_buffer_prnt(self, pprint.pformat(message.request.payload))
server_buffer_prnt(self, pprint.pformat(message.response.body))
def handle_room_membership_events(
self,
room,
room_buffer,
event,
is_state_event
):
def join(event, date, room, room_buffer, is_state_event):
user = room.users[event.sender]
buffer_user = RoomUser(user.name, event.sender)
# TODO remove this duplication
user.nick_color = buffer_user.color
if self.user_id == event.sender:
buffer_user.color = "weechat.color.chat_nick_self"
user.nick_color = "weechat.color.chat_nick_self"
room_buffer.join(
buffer_user,
server_ts_to_weechat(event.timestamp),
not is_state_event
)
room.handle_event(event)
date = server_ts_to_weechat(event.timestamp)
joined = False
left = False
if isinstance(event, RoomMemberJoin):
if event.prev_content and "membership" in event.prev_content:
if (event.prev_content["membership"] == "leave"
or event.prev_content["membership"] == "invite"):
join(event, date, room, room_buffer, is_state_event)
joined = True
else:
# TODO print out profile changes
return
else:
# No previous content for this user in this room, so he just
# joined.
join(event, date, room, room_buffer, is_state_event)
joined = True
elif isinstance(event, RoomMemberLeave):
# TODO the nick can be a display name or a full sender name
nick = shorten_sender(event.sender)
if event.sender == event.leaving_user:
room_buffer.part(nick, date, not is_state_event)
else:
room_buffer.kick(nick, date, not is_state_event)
left = True
elif isinstance(event, RoomMemberInvite):
if is_state_event:
return
room_buffer.invite(event.invited_user, date)
return
# calculate room display name and set it as the buffer list name
room_name = room.display_name(self.user_id)
room_buffer.short_name = room_name
# A user has joined or left an encrypted room, we need to check for
# new devices and create a new group session
if room.encrypted and (joined or left):
self.device_check_timestamp = None
def handle_room_event(self, room, room_buffer, event, is_state_event):
if isinstance(event, RoomMembershipEvent):
self.handle_room_membership_events(
room,
room_buffer,
event,
is_state_event
)
elif isinstance(event, RoomTopicEvent):
try:
user = room.users[event.sender]
nick = user.name
except KeyError:
nick = event.sender
room_buffer.change_topic(
nick,
event.topic,
server_ts_to_weechat(event.timestamp),
not is_state_event
)
else:
tags = tags_for_message("message")
event.execute(self, room, room_buffer._ptr, tags)
def _loop_events(self, info, n):
for i in range(n+1):
@ -742,7 +647,15 @@ class MatrixServer:
return i
room, room_buffer = self.find_room_from_id(info.room_id)
self.handle_room_event(room, room_buffer, event, is_state)
# The room changed it's members, if the room is encrypted update
# the device list
if room.handle_event(event) and room.encrypted:
self.device_check_timestamp = None
if is_state:
room_buffer.handle_state_event(event)
else:
room_buffer.handle_timeline_event(event)
self.event_queue.appendleft(info)
return i
@ -777,18 +690,10 @@ class MatrixServer:
def handle_own_messages(self, room_buffer, message):
if isinstance(message, RoomMessageText):
msg = (message.formatted_message.to_weechat()
if message.formatted_message
else message.message)
date = server_ts_to_weechat(message.timestamp)
room_buffer.self_message(self.user, msg, date)
room_buffer.self_message(message)
return
elif isinstance(message, RoomMessageEmote):
date = server_ts_to_weechat(message.timestamp)
room_buffer.self_action(self.user, message.message, date)
room_buffer.self_action(message)
return
raise NotImplementedError("Unsupported message of type {}".format(
@ -850,12 +755,12 @@ class MatrixServer:
return
def create_room_buffer(self, room_id):
buf = WeechatChannelBuffer(room_id, self.name, self.user)
room = MatrixRoom(room_id, self.user_id)
buf = RoomBuffer(room, self.name)
# TODO this should turned into a propper class
self.room_buffers[room_id] = buf
self.buffers[room_id] = buf._ptr
self.rooms[room_id] = MatrixRoom(room_id)
pass
self.buffers[room_id] = buf.weechat_buffer._ptr
self.rooms[room_id] = room
def find_room_from_ptr(self, pointer):
room_id = key_from_value(self.buffers, pointer)