From 66507b23ce74002a67f713ea12c00b917ab314e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?poljar=20=28Damir=20Jeli=C4=87=29?= Date: Fri, 6 Jul 2018 14:56:06 +0200 Subject: [PATCH] buffer: Add a matrix specific buffer class. --- matrix/buffer.py | 398 +++++++++++++++++++++++++++++++++++++++++++++-- matrix/rooms.py | 131 ++-------------- matrix/server.py | 127 ++------------- 3 files changed, 415 insertions(+), 241 deletions(-) diff --git a/matrix/buffer.py b/matrix/buffer.py index c659718..0d39fe1 100644 --- a/matrix/buffer.py +++ b/matrix/buffer.py @@ -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) diff --git a/matrix/rooms.py b/matrix/rooms.py index 0c2c29c..b5e6556 100644 --- a/matrix/rooms.py +++ b/matrix/rooms.py @@ -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): diff --git a/matrix/server.py b/matrix/server.py index f5278c8..db7ed5e 100644 --- a/matrix/server.py +++ b/matrix/server.py @@ -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)