2020-09-24 09:57:54 +10:00
|
|
|
# Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
|
2019-12-19 22:46:16 +11:00
|
|
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
|
|
|
2020-07-17 06:09:14 +10:00
|
|
|
import asyncio
|
2019-11-09 01:20:38 +11:00
|
|
|
import json
|
|
|
|
import logging as log
|
|
|
|
from dataclasses import dataclass, field
|
2020-07-10 09:53:25 +10:00
|
|
|
from datetime import datetime, timedelta
|
2020-07-08 00:33:10 +10:00
|
|
|
from html import escape
|
2020-07-21 12:58:02 +10:00
|
|
|
from pathlib import Path
|
2020-11-01 16:23:33 +11:00
|
|
|
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
|
2019-11-27 21:30:42 +11:00
|
|
|
from urllib.parse import quote
|
2019-11-09 01:20:38 +11:00
|
|
|
|
|
|
|
import nio
|
|
|
|
|
2019-12-19 00:33:22 +11:00
|
|
|
from .html_markdown import HTML_PROCESSOR
|
2020-07-21 12:58:02 +10:00
|
|
|
from .media_cache import Media
|
2020-11-03 21:36:31 +11:00
|
|
|
from .models.items import PushRule, TypeSpecifier
|
2020-07-19 08:33:57 +10:00
|
|
|
from .presence import Presence
|
2020-07-09 07:26:52 +10:00
|
|
|
from .pyotherside_events import DevicesUpdated
|
2020-05-31 13:39:07 +10:00
|
|
|
from .utils import classes_defined_in, plain2html
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2020-02-12 07:22:05 +11:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
from .matrix_client import MatrixClient
|
|
|
|
|
2019-11-09 01:20:38 +11:00
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class NioCallbacks:
|
2020-05-31 13:39:07 +10:00
|
|
|
"""Register callbacks for nio's request responses and events.
|
2019-12-19 05:24:55 +11:00
|
|
|
|
2020-05-31 13:39:07 +10:00
|
|
|
For every class defined in the `nio.responses` and `nio.events` modules,
|
|
|
|
this class can have a method named
|
|
|
|
`on<ClassName>` (e.g. `onRoomMessageText`) that will
|
|
|
|
automatically be registered in the `client`'s callbacks.
|
2019-12-19 05:24:55 +11:00
|
|
|
|
|
|
|
For room event content strings, the `%1` and `%2` placeholders
|
2020-04-03 04:54:06 +11:00
|
|
|
refer to the event's sender and who this event targets (`state_key`) or
|
|
|
|
the redactor of this event.
|
|
|
|
These are processed from QML, to allow for future translations of
|
|
|
|
the strings.
|
2019-12-19 05:24:55 +11:00
|
|
|
"""
|
|
|
|
|
2020-02-12 07:22:05 +11:00
|
|
|
client: "MatrixClient" = field()
|
2019-11-09 01:20:38 +11:00
|
|
|
|
|
|
|
def __post_init__(self) -> None:
|
2019-12-19 05:24:55 +11:00
|
|
|
"""Register our methods as callbacks."""
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2020-05-18 03:57:09 +10:00
|
|
|
self.models = self.client.models
|
|
|
|
|
2020-05-31 13:39:07 +10:00
|
|
|
for name, response_class in classes_defined_in(nio.responses).items():
|
|
|
|
method = getattr(self, f"on{name}", None)
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2020-05-31 13:39:07 +10:00
|
|
|
if method:
|
|
|
|
self.client.add_response_callback(method, response_class)
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2020-10-31 05:15:17 +11:00
|
|
|
for name, ev_class in classes_defined_in(nio.events).items():
|
2020-05-31 13:39:07 +10:00
|
|
|
method = getattr(self, f"on{name}", None)
|
|
|
|
|
|
|
|
if not method:
|
|
|
|
continue
|
|
|
|
|
2020-10-31 05:15:17 +11:00
|
|
|
if issubclass(ev_class, nio.EphemeralEvent):
|
|
|
|
self.client.add_ephemeral_callback(method, ev_class)
|
|
|
|
elif issubclass(ev_class, nio.ToDeviceEvent):
|
|
|
|
self.client.add_to_device_callback(method, ev_class)
|
|
|
|
elif issubclass(ev_class, nio.AccountDataEvent):
|
|
|
|
self.client.add_global_account_data_callback(method, ev_class)
|
|
|
|
self.client.add_room_account_data_callback(method, ev_class)
|
|
|
|
elif issubclass(ev_class, nio.PresenceEvent):
|
|
|
|
self.client.add_presence_callback(method, ev_class)
|
2020-05-31 13:39:07 +10:00
|
|
|
else:
|
2020-10-31 05:15:17 +11:00
|
|
|
self.client.add_event_callback(method, ev_class)
|
2019-11-09 01:20:38 +11:00
|
|
|
|
|
|
|
|
2020-05-18 03:57:09 +10:00
|
|
|
@property
|
|
|
|
def user_id(self) -> str:
|
|
|
|
return self.client.user_id
|
|
|
|
|
|
|
|
|
2019-12-19 05:24:55 +11:00
|
|
|
# Response callbacks
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2019-12-19 05:24:55 +11:00
|
|
|
async def onSyncResponse(self, resp: nio.SyncResponse) -> None:
|
2020-06-01 23:25:09 +10:00
|
|
|
for room_id in resp.rooms.invite:
|
|
|
|
await self.client.register_nio_room(self.client.all_rooms[room_id])
|
2020-04-09 19:52:33 +10:00
|
|
|
|
2019-11-09 01:20:38 +11:00
|
|
|
for room_id, info in resp.rooms.join.items():
|
2020-06-01 23:25:09 +10:00
|
|
|
await self.client.register_nio_room(self.client.rooms[room_id])
|
2020-04-09 19:52:33 +10:00
|
|
|
|
2019-12-19 05:24:55 +11:00
|
|
|
if room_id not in self.client.past_tokens:
|
|
|
|
self.client.past_tokens[room_id] = info.timeline.prev_batch
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2020-07-14 11:13:20 +10:00
|
|
|
for ev in info.state:
|
|
|
|
if isinstance(ev, nio.PowerLevelsEvent):
|
|
|
|
stored = self.client.power_level_events.get(room_id)
|
|
|
|
time = ev.server_timestamp
|
|
|
|
|
|
|
|
if not stored or time > stored.server_timestamp:
|
|
|
|
self.client.power_level_events[room_id] = ev
|
|
|
|
|
2019-11-09 01:20:38 +11:00
|
|
|
# TODO: way of knowing if a nio.MatrixRoom is left
|
|
|
|
for room_id, info in resp.rooms.leave.items():
|
|
|
|
# TODO: handle in nio, these are rooms that were left before
|
|
|
|
# starting the client.
|
2019-12-19 05:24:55 +11:00
|
|
|
if room_id not in self.client.all_rooms:
|
2019-11-09 01:20:38 +11:00
|
|
|
continue
|
|
|
|
|
|
|
|
# TODO: handle left events in nio async client
|
|
|
|
for ev in info.timeline.events:
|
|
|
|
if isinstance(ev, nio.RoomMemberEvent):
|
2019-12-19 05:24:55 +11:00
|
|
|
await self.onRoomMemberEvent(
|
|
|
|
self.client.all_rooms[room_id], ev,
|
|
|
|
)
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2019-12-19 05:24:55 +11:00
|
|
|
await self.client.register_nio_room(
|
|
|
|
self.client.all_rooms[room_id], left=True,
|
|
|
|
)
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2020-08-21 19:29:42 +10:00
|
|
|
account = self.models["accounts"][self.user_id]
|
|
|
|
account.connecting = False
|
|
|
|
|
2019-12-19 05:24:55 +11:00
|
|
|
if not self.client.first_sync_done.is_set():
|
|
|
|
self.client.first_sync_done.set()
|
|
|
|
self.client.first_sync_date = datetime.now()
|
|
|
|
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2020-06-28 01:11:14 +10:00
|
|
|
async def onKeysQueryResponse(self, resp: nio.KeysQueryResponse) -> None:
|
|
|
|
refresh_rooms = {}
|
2020-07-10 02:27:47 +10:00
|
|
|
clients = self.client.backend.clients
|
2020-06-28 01:11:14 +10:00
|
|
|
|
|
|
|
for user_id in resp.changed:
|
|
|
|
for room in self.client.rooms.values():
|
|
|
|
if user_id in room.users:
|
|
|
|
refresh_rooms[room.room_id] = room
|
|
|
|
|
2020-07-10 02:27:47 +10:00
|
|
|
if user_id != self.user_id and user_id in clients:
|
|
|
|
await self.client.auto_verify_account(clients[user_id])
|
|
|
|
|
2020-06-28 01:11:14 +10:00
|
|
|
for room_id, room in refresh_rooms.items():
|
|
|
|
room_item = self.models[self.user_id, "rooms"].get(room_id)
|
|
|
|
|
|
|
|
if room_item:
|
|
|
|
room_item.unverified_devices = \
|
|
|
|
self.client.room_contains_unverified(room_id)
|
|
|
|
else:
|
|
|
|
await self.client.register_nio_room(room)
|
|
|
|
|
2020-07-09 07:26:52 +10:00
|
|
|
DevicesUpdated(self.user_id)
|
|
|
|
|
2020-06-28 01:11:14 +10:00
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
# Room events, invite events and misc events callbacks
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomMessageText(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomMessageText,
|
|
|
|
) -> None:
|
2019-12-19 00:33:22 +11:00
|
|
|
co = HTML_PROCESSOR.filter(
|
2019-11-09 01:20:38 +11:00
|
|
|
ev.formatted_body
|
|
|
|
if ev.format == "org.matrix.custom.html" else
|
2020-05-31 13:39:07 +10:00
|
|
|
plain2html(ev.body),
|
2019-11-09 01:20:38 +11:00
|
|
|
)
|
2020-03-23 14:55:48 +11:00
|
|
|
|
2020-03-24 06:39:14 +11:00
|
|
|
mention_list = HTML_PROCESSOR.mentions_in_html(co)
|
|
|
|
|
|
|
|
await self.client.register_nio_event(
|
|
|
|
room, ev, content=co, mentions=mention_list,
|
|
|
|
)
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2020-03-24 04:43:30 +11:00
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomMessageNotice(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomMessageNotice,
|
|
|
|
) -> None:
|
2019-11-30 22:10:48 +11:00
|
|
|
await self.onRoomMessageText(room, ev)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomMessageEmote(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomMessageEmote,
|
|
|
|
) -> None:
|
2019-11-30 22:10:48 +11:00
|
|
|
await self.onRoomMessageText(room, ev)
|
2019-11-09 01:20:38 +11:00
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomMessageUnknown(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomMessageUnknown,
|
|
|
|
) -> None:
|
2020-07-08 00:33:10 +10:00
|
|
|
co = f"%1 sent an unsupported <b>{escape(ev.msgtype)}</b> message"
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.client.register_nio_event(room, ev, content=co)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomMessageMedia(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomMessageMedia,
|
|
|
|
) -> None:
|
2019-11-09 01:20:38 +11:00
|
|
|
info = ev.source["content"].get("info", {})
|
|
|
|
media_crypt_dict = ev.source["content"].get("file", {})
|
|
|
|
thumb_info = info.get("thumbnail_info", {})
|
|
|
|
thumb_crypt_dict = info.get("thumbnail_file", {})
|
|
|
|
|
2020-07-21 12:58:02 +10:00
|
|
|
try:
|
|
|
|
media_local_path: Union[Path, str] = await Media(
|
2020-08-24 06:57:53 +10:00
|
|
|
cache = self.client.backend.media_cache,
|
|
|
|
client_user_id = self.user_id,
|
|
|
|
mxc = ev.url,
|
|
|
|
title = ev.body,
|
2021-01-21 06:50:04 +11:00
|
|
|
room_id = room.room_id,
|
|
|
|
filesize = info.get("size") or 0,
|
2020-08-24 06:57:53 +10:00
|
|
|
crypt_dict = media_crypt_dict,
|
2020-07-21 12:58:02 +10:00
|
|
|
).get_local()
|
|
|
|
except FileNotFoundError:
|
|
|
|
media_local_path = ""
|
|
|
|
|
|
|
|
item = await self.client.register_nio_event(
|
2019-11-09 01:20:38 +11:00
|
|
|
room,
|
|
|
|
ev,
|
|
|
|
content = "",
|
|
|
|
inline_content = ev.body,
|
|
|
|
|
|
|
|
media_url = ev.url,
|
2020-07-21 13:09:28 +10:00
|
|
|
media_http_url = await self.client.mxc_to_http(ev.url),
|
2019-11-09 01:20:38 +11:00
|
|
|
media_title = ev.body,
|
|
|
|
media_width = info.get("w") or 0,
|
|
|
|
media_height = info.get("h") or 0,
|
|
|
|
media_duration = info.get("duration") or 0,
|
|
|
|
media_size = info.get("size") or 0,
|
2020-05-21 15:05:31 +10:00
|
|
|
media_mime = info.get("mimetype") or "",
|
2019-11-09 01:20:38 +11:00
|
|
|
media_crypt_dict = media_crypt_dict,
|
2020-07-21 12:58:02 +10:00
|
|
|
media_local_path = media_local_path,
|
2019-11-09 01:20:38 +11:00
|
|
|
|
|
|
|
thumbnail_url =
|
|
|
|
info.get("thumbnail_url") or thumb_crypt_dict.get("url") or "",
|
|
|
|
|
|
|
|
thumbnail_width = thumb_info.get("w") or 0,
|
|
|
|
thumbnail_height = thumb_info.get("h") or 0,
|
2020-03-10 03:06:58 +11:00
|
|
|
thumbnail_mime = thumb_info.get("mimetype") or "",
|
2019-11-09 01:20:38 +11:00
|
|
|
thumbnail_crypt_dict = thumb_crypt_dict,
|
|
|
|
)
|
|
|
|
|
2020-07-21 12:58:02 +10:00
|
|
|
self.client.backend.mxc_events[ev.url].append(item)
|
|
|
|
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomEncryptedMedia(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomEncryptedMedia,
|
|
|
|
) -> None:
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.onRoomMessageMedia(room, ev)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRedactionEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RedactionEvent,
|
|
|
|
) -> None:
|
2020-05-18 03:57:09 +10:00
|
|
|
model = self.models[self.user_id, room.room_id, "events"]
|
2020-03-30 09:06:13 +11:00
|
|
|
event = None
|
|
|
|
|
2020-04-02 06:15:49 +11:00
|
|
|
for existing in model._sorted_data:
|
|
|
|
if existing.event_id == ev.redacts:
|
|
|
|
event = existing
|
2020-03-30 09:06:13 +11:00
|
|
|
break
|
|
|
|
|
2020-04-03 11:51:53 +11:00
|
|
|
if not (
|
|
|
|
event and
|
2020-07-14 13:42:25 +10:00
|
|
|
(event.event_type is not nio.RedactedEvent or event.is_local_echo)
|
2020-04-03 11:51:53 +11:00
|
|
|
):
|
2020-04-09 19:52:33 +10:00
|
|
|
await self.client.register_nio_room(room)
|
2020-04-02 04:33:19 +11:00
|
|
|
return
|
|
|
|
|
|
|
|
event.source.source["content"] = {}
|
|
|
|
event.source.source["unsigned"] = {
|
|
|
|
"redacted_by": ev.event_id,
|
|
|
|
"redacted_because": ev.source,
|
|
|
|
}
|
|
|
|
|
|
|
|
await self.onRedactedEvent(
|
|
|
|
room,
|
|
|
|
nio.RedactedEvent.from_dict(event.source.source),
|
|
|
|
event_id = event.id,
|
|
|
|
)
|
2020-03-30 09:06:13 +11:00
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRedactedEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RedactedEvent, event_id: str = "",
|
|
|
|
) -> None:
|
2020-05-20 17:42:40 +10:00
|
|
|
redacter_name, _, must_fetch_redacter = \
|
|
|
|
await self.client.get_member_profile(room.room_id, ev.redacter) \
|
|
|
|
if ev.redacter else ("", "", False)
|
|
|
|
|
2020-03-30 09:06:13 +11:00
|
|
|
await self.client.register_nio_event(
|
2020-04-02 04:33:19 +11:00
|
|
|
room,
|
|
|
|
ev,
|
2020-04-03 20:50:02 +11:00
|
|
|
event_id = event_id,
|
|
|
|
reason = ev.reason or "",
|
|
|
|
|
|
|
|
content = await self.client.get_redacted_event_content(
|
2020-04-03 21:50:24 +11:00
|
|
|
type(ev), ev.redacter, ev.sender, ev.reason,
|
2020-04-03 20:50:02 +11:00
|
|
|
),
|
|
|
|
|
2020-09-05 04:47:47 +10:00
|
|
|
mentions = [],
|
|
|
|
type_specifier = TypeSpecifier.Unset,
|
|
|
|
media_url = "",
|
|
|
|
media_http_url = "",
|
|
|
|
media_title = "",
|
|
|
|
media_local_path = "",
|
|
|
|
thumbnail_url = "",
|
2020-05-20 17:42:40 +10:00
|
|
|
redacter_id = ev.redacter or "",
|
|
|
|
redacter_name = redacter_name,
|
|
|
|
override_fetch_profile = True,
|
2020-03-29 04:24:05 +11:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomCreateEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomCreateEvent,
|
|
|
|
) -> None:
|
2019-12-07 09:23:25 +11:00
|
|
|
co = "%1 allowed users on other matrix servers to join this room" \
|
2019-11-09 01:20:38 +11:00
|
|
|
if ev.federate else \
|
2019-12-07 09:23:25 +11:00
|
|
|
"%1 blocked users on other matrix servers from joining this room"
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.client.register_nio_event(room, ev, content=co)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomGuestAccessEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomGuestAccessEvent,
|
|
|
|
) -> None:
|
2020-06-26 00:11:42 +10:00
|
|
|
allowed = "allowed" if ev.guest_access == "can_join" else "forbad"
|
2019-12-07 09:23:25 +11:00
|
|
|
co = f"%1 {allowed} guests to join the room"
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.client.register_nio_event(room, ev, content=co)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomJoinRulesEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomJoinRulesEvent,
|
|
|
|
) -> None:
|
2019-11-09 01:20:38 +11:00
|
|
|
access = "public" if ev.join_rule == "public" else "invite-only"
|
2019-12-07 09:23:25 +11:00
|
|
|
co = f"%1 made the room {access}"
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.client.register_nio_event(room, ev, content=co)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomHistoryVisibilityEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomHistoryVisibilityEvent,
|
|
|
|
) -> None:
|
2019-11-09 01:20:38 +11:00
|
|
|
if ev.history_visibility == "shared":
|
|
|
|
to = "all room members"
|
|
|
|
elif ev.history_visibility == "world_readable":
|
|
|
|
to = "any member or outsider"
|
|
|
|
elif ev.history_visibility == "joined":
|
|
|
|
to = "all room members, since the time they joined"
|
|
|
|
elif ev.history_visibility == "invited":
|
|
|
|
to = "all room members, since the time they were invited"
|
|
|
|
else:
|
|
|
|
to = "???"
|
|
|
|
log.warning("Invalid visibility - %s",
|
|
|
|
json.dumps(vars(ev), indent=4))
|
|
|
|
|
2019-12-07 09:23:25 +11:00
|
|
|
co = f"%1 made future room history visible to {to}"
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.client.register_nio_event(room, ev, content=co)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onPowerLevelsEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.PowerLevelsEvent,
|
|
|
|
) -> None:
|
2020-07-14 12:55:15 +10:00
|
|
|
levels = ev.power_levels
|
2020-07-14 11:13:20 +10:00
|
|
|
stored = self.client.power_level_events.get(room.room_id)
|
|
|
|
|
|
|
|
if not stored or ev.server_timestamp > stored.server_timestamp:
|
|
|
|
self.client.power_level_events[room.room_id] = ev
|
2020-07-13 08:48:34 +10:00
|
|
|
|
2020-07-13 09:06:35 +10:00
|
|
|
try:
|
2020-07-14 12:55:15 +10:00
|
|
|
previous = ev.source["unsigned"]["prev_content"]
|
2020-07-13 09:06:35 +10:00
|
|
|
except KeyError:
|
2020-07-14 12:55:15 +10:00
|
|
|
previous = {}
|
2020-07-13 09:06:35 +10:00
|
|
|
|
2020-07-14 12:55:15 +10:00
|
|
|
users_previous = previous.get("users", {})
|
|
|
|
events_previous = previous.get("events", {})
|
|
|
|
|
2020-07-17 02:40:47 +10:00
|
|
|
changes: List[Tuple[str, int, int]] = []
|
|
|
|
event_changes: List[Tuple[str, int, int]] = []
|
|
|
|
user_changes: List[Tuple[str, int, int]] = []
|
2020-07-17 02:12:37 +10:00
|
|
|
|
2020-07-14 12:55:15 +10:00
|
|
|
def lvl(level: int) -> str:
|
|
|
|
return (
|
2020-11-16 05:57:00 +11:00
|
|
|
f"Admin ({level})" if level == 100 else
|
2020-07-14 12:55:15 +10:00
|
|
|
f"Moderator ({level})" if level >= 50 else
|
2020-11-16 05:57:00 +11:00
|
|
|
f"User ({level})" if level >= 0 else
|
2020-09-05 09:55:14 +10:00
|
|
|
f"Muted ({level})"
|
2020-07-14 12:55:15 +10:00
|
|
|
)
|
|
|
|
|
2020-07-17 02:12:37 +10:00
|
|
|
def format_defaults_dict(
|
|
|
|
levels: Dict[str, Union[int, dict]],
|
|
|
|
previous: Dict[str, Union[int, dict]],
|
|
|
|
prefix: str = "",
|
|
|
|
) -> None:
|
2020-07-14 12:55:15 +10:00
|
|
|
|
2020-09-05 09:51:27 +10:00
|
|
|
default_0 = ("users_default", "events_default", "invite")
|
2020-07-14 12:55:15 +10:00
|
|
|
|
2020-09-05 09:51:27 +10:00
|
|
|
for name in set({**levels, **previous}):
|
|
|
|
if not prefix and name in ("users", "events"):
|
2020-07-17 02:12:37 +10:00
|
|
|
continue
|
2020-07-14 12:55:15 +10:00
|
|
|
|
2020-09-05 09:51:27 +10:00
|
|
|
old_level = previous.get(
|
|
|
|
name, 0 if not prefix and name in default_0 else 50,
|
|
|
|
)
|
|
|
|
level = levels.get(
|
2020-07-17 02:12:37 +10:00
|
|
|
name, 0 if not prefix and name in default_0 else 50,
|
|
|
|
)
|
|
|
|
|
2020-09-05 09:51:27 +10:00
|
|
|
if isinstance(level, dict):
|
|
|
|
if not isinstance(old_level, dict):
|
|
|
|
old_level = {}
|
2020-07-17 02:12:37 +10:00
|
|
|
|
2020-09-05 09:51:27 +10:00
|
|
|
format_defaults_dict(level, old_level, f"{prefix}{name}.")
|
|
|
|
continue
|
|
|
|
|
|
|
|
if not isinstance(old_level, int):
|
|
|
|
old_level = 50
|
|
|
|
|
|
|
|
if old_level != level or not previous:
|
|
|
|
changes.append((f"{prefix}{name}", old_level, level))
|
2020-07-17 02:12:37 +10:00
|
|
|
|
|
|
|
format_defaults_dict(ev.source["content"], previous)
|
2020-07-14 12:55:15 +10:00
|
|
|
|
|
|
|
# Minimum level to send event changes
|
|
|
|
|
2020-09-05 09:51:27 +10:00
|
|
|
for ev_type in set({**levels.events, **events_previous}):
|
|
|
|
old_level = events_previous.get(
|
2020-07-14 12:55:15 +10:00
|
|
|
ev_type,
|
|
|
|
|
|
|
|
levels.defaults.state_default
|
|
|
|
if ev_type.startswith("m.room.") else
|
|
|
|
levels.defaults.events_default,
|
|
|
|
)
|
2020-09-05 09:51:27 +10:00
|
|
|
level = levels.events.get(
|
|
|
|
ev_type,
|
2020-07-14 12:55:15 +10:00
|
|
|
|
2020-09-05 09:51:27 +10:00
|
|
|
levels.defaults.state_default
|
|
|
|
if ev_type.startswith("m.room.") else
|
|
|
|
levels.defaults.events_default,
|
|
|
|
)
|
|
|
|
|
|
|
|
if old_level != level or not previous:
|
|
|
|
event_changes.append((ev_type, old_level, level))
|
2020-07-14 12:55:15 +10:00
|
|
|
|
|
|
|
# User level changes
|
|
|
|
|
2020-09-05 09:51:27 +10:00
|
|
|
for user_id in set({**levels.users, **users_previous}):
|
|
|
|
old_level = \
|
|
|
|
users_previous.get(user_id, levels.defaults.users_default)
|
|
|
|
|
|
|
|
level = levels.users.get(user_id, levels.defaults.users_default)
|
2020-07-14 12:55:15 +10:00
|
|
|
|
2020-09-05 09:51:27 +10:00
|
|
|
if old_level != level or not previous:
|
|
|
|
user_changes.append((user_id, old_level, level))
|
|
|
|
|
|
|
|
if user_id in room.users:
|
|
|
|
await self.client.add_member(room, user_id)
|
2020-07-14 12:55:15 +10:00
|
|
|
|
|
|
|
# Gather and format changes
|
|
|
|
|
|
|
|
if changes or event_changes or user_changes:
|
2020-07-17 02:12:37 +10:00
|
|
|
changes.sort(key=lambda c: (c[2], c[0]))
|
2020-07-17 02:40:47 +10:00
|
|
|
event_changes.sort(key=lambda c: (c[2], c[0]))
|
|
|
|
user_changes.sort(key=lambda c: (c[2], c[0]))
|
|
|
|
|
|
|
|
all_changes = changes + event_changes + user_changes
|
|
|
|
|
|
|
|
if len(all_changes) == 1:
|
|
|
|
co = HTML_PROCESSOR.from_markdown(
|
|
|
|
"%%1 changed the level for **%s**: %s → %s " % (
|
|
|
|
all_changes[0][0],
|
|
|
|
lvl(all_changes[0][1]).lower(),
|
|
|
|
lvl(all_changes[0][2]).lower(),
|
|
|
|
),
|
2020-07-17 02:47:28 +10:00
|
|
|
inline = True,
|
2020-07-17 02:40:47 +10:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
co = HTML_PROCESSOR.from_markdown("\n".join([
|
|
|
|
"%1 changed the room's permissions",
|
|
|
|
"",
|
|
|
|
"Change | Previous | Current ",
|
|
|
|
"--- | --- | ---",
|
|
|
|
*[
|
|
|
|
f"{name} | {lvl(old)} | {lvl(now)}"
|
|
|
|
for name, old, now in all_changes
|
|
|
|
],
|
|
|
|
]))
|
2020-07-14 12:55:15 +10:00
|
|
|
else:
|
|
|
|
co = "%1 didn't change the room's permissions"
|
|
|
|
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.client.register_nio_event(room, ev, content=co)
|
|
|
|
|
|
|
|
|
|
|
|
async def process_room_member_event(
|
2019-12-19 05:24:55 +11:00
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomMemberEvent,
|
2019-11-09 01:20:38 +11:00
|
|
|
) -> Optional[Tuple[TypeSpecifier, str]]:
|
2019-12-19 05:24:55 +11:00
|
|
|
"""Return a `TypeSpecifier` and string describing a member event.
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2019-12-19 05:24:55 +11:00
|
|
|
Matrix member events can represent many actions:
|
|
|
|
a user joined the room, a user banned another, a user changed their
|
|
|
|
display name, etc.
|
|
|
|
"""
|
2019-11-09 01:20:38 +11:00
|
|
|
if ev.prev_content == ev.content:
|
|
|
|
return None
|
|
|
|
|
|
|
|
prev = ev.prev_content
|
|
|
|
now = ev.content
|
|
|
|
membership = ev.membership
|
|
|
|
prev_membership = ev.prev_membership
|
|
|
|
ev_date = datetime.fromtimestamp(ev.server_timestamp / 1000)
|
|
|
|
|
2019-12-03 07:29:29 +11:00
|
|
|
member_change = TypeSpecifier.MembershipChange
|
2019-11-09 01:20:38 +11:00
|
|
|
|
|
|
|
# Membership changes
|
|
|
|
if not prev or membership != prev_membership:
|
2020-10-08 11:12:32 +11:00
|
|
|
if not self.client.backend.settings.Chat.show_membership_events:
|
2020-03-23 03:04:43 +11:00
|
|
|
return None
|
|
|
|
|
2020-07-08 00:33:10 +10:00
|
|
|
reason = escape(
|
|
|
|
f", reason: {now['reason']}" if now.get("reason") else "",
|
|
|
|
)
|
2019-11-09 01:20:38 +11:00
|
|
|
|
|
|
|
if membership == "join":
|
|
|
|
return (
|
|
|
|
member_change,
|
2019-12-07 09:23:25 +11:00
|
|
|
"%1 accepted their invitation"
|
2019-11-09 01:20:38 +11:00
|
|
|
if prev and prev_membership == "invite" else
|
2019-12-07 09:23:25 +11:00
|
|
|
"%1 joined the room",
|
2019-11-09 01:20:38 +11:00
|
|
|
)
|
|
|
|
|
|
|
|
if membership == "invite":
|
2019-12-07 09:23:25 +11:00
|
|
|
return (member_change, "%1 invited %2 to the room")
|
2019-11-09 01:20:38 +11:00
|
|
|
|
|
|
|
if membership == "leave":
|
|
|
|
if ev.state_key == ev.sender:
|
|
|
|
return (
|
|
|
|
member_change,
|
2019-12-07 09:23:25 +11:00
|
|
|
f"%1 declined their invitation{reason}"
|
2019-11-09 01:20:38 +11:00
|
|
|
if prev and prev_membership == "invite" else
|
2019-12-07 09:23:25 +11:00
|
|
|
f"%1 left the room{reason}",
|
2019-11-09 01:20:38 +11:00
|
|
|
)
|
|
|
|
|
|
|
|
return (
|
|
|
|
member_change,
|
|
|
|
|
2019-12-07 09:23:25 +11:00
|
|
|
f"%1 withdrew %2's invitation{reason}"
|
2019-11-09 01:20:38 +11:00
|
|
|
if prev and prev_membership == "invite" else
|
|
|
|
|
2019-12-07 09:23:25 +11:00
|
|
|
f"%1 unbanned %2 from the room{reason}"
|
2019-11-09 01:20:38 +11:00
|
|
|
if prev and prev_membership == "ban" else
|
|
|
|
|
2020-04-20 01:12:35 +10:00
|
|
|
f"%1 kicked %2 out from the room{reason}",
|
2019-11-09 01:20:38 +11:00
|
|
|
)
|
|
|
|
|
|
|
|
if membership == "ban":
|
2019-12-07 09:23:25 +11:00
|
|
|
return (member_change, f"%1 banned %2 from the room{reason}")
|
2019-11-09 01:20:38 +11:00
|
|
|
|
|
|
|
# Profile changes
|
|
|
|
changed = []
|
|
|
|
|
2020-03-24 18:56:31 +11:00
|
|
|
if prev and now.get("avatar_url") != prev.get("avatar_url"):
|
2019-11-09 01:20:38 +11:00
|
|
|
changed.append("profile picture") # TODO: <img>s
|
|
|
|
|
2020-03-24 18:56:31 +11:00
|
|
|
if prev and now.get("displayname") != prev.get("displayname"):
|
2019-11-09 01:20:38 +11:00
|
|
|
changed.append('display name from "{}" to "{}"'.format(
|
2020-07-08 00:33:10 +10:00
|
|
|
escape(prev.get("displayname") or ev.state_key),
|
|
|
|
escape(now.get("displayname") or ev.state_key),
|
2019-11-09 01:20:38 +11:00
|
|
|
))
|
|
|
|
|
|
|
|
if changed:
|
|
|
|
# Update our account profile if the event is newer than last update
|
2020-05-18 03:57:09 +10:00
|
|
|
if ev.state_key == self.user_id:
|
|
|
|
account = self.models["accounts"][self.user_id]
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2019-12-03 07:29:29 +11:00
|
|
|
if account.profile_updated < ev_date:
|
2020-07-02 02:00:50 +10:00
|
|
|
account.set_fields(
|
|
|
|
profile_updated = ev_date,
|
|
|
|
display_name = now.get("displayname") or "",
|
|
|
|
avatar_url = now.get("avatar_url") or "",
|
|
|
|
)
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2020-10-08 11:12:32 +11:00
|
|
|
if not self.client.backend.settings.Chat.show_profile_changes:
|
2020-03-23 03:04:43 +11:00
|
|
|
return None
|
2019-11-09 01:20:38 +11:00
|
|
|
|
|
|
|
return (
|
2019-12-03 07:29:29 +11:00
|
|
|
TypeSpecifier.ProfileChange,
|
2019-12-07 09:23:25 +11:00
|
|
|
"%1 changed their {}".format(" and ".join(changed)),
|
2019-11-09 01:20:38 +11:00
|
|
|
)
|
|
|
|
|
2020-07-27 16:43:11 +10:00
|
|
|
# log.warning("Unknown member ev.: %s", json.dumps(vars(ev), indent=4))
|
2019-11-09 01:20:38 +11:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomMemberEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomMemberEvent,
|
|
|
|
) -> None:
|
2020-05-20 23:05:36 +10:00
|
|
|
# The event can be a past event, don't trust it to update the model
|
|
|
|
# room's current state.
|
|
|
|
if ev.state_key in room.users:
|
2020-05-18 05:29:23 +10:00
|
|
|
await self.client.add_member(room, user_id=ev.state_key)
|
2020-05-20 23:05:36 +10:00
|
|
|
else:
|
2020-05-18 05:29:23 +10:00
|
|
|
await self.client.remove_member(room, user_id=ev.state_key)
|
|
|
|
|
2019-11-09 01:20:38 +11:00
|
|
|
type_and_content = await self.process_room_member_event(room, ev)
|
|
|
|
|
2019-12-13 01:03:39 +11:00
|
|
|
if type_and_content is not None:
|
2019-11-09 01:20:38 +11:00
|
|
|
type_specifier, content = type_and_content
|
2019-12-15 04:50:21 +11:00
|
|
|
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.client.register_nio_event(
|
|
|
|
room, ev, content=content, type_specifier=type_specifier,
|
|
|
|
)
|
2019-12-13 01:03:39 +11:00
|
|
|
else:
|
|
|
|
# Normally, register_nio_event() will call register_nio_room().
|
|
|
|
# but in this case we don't have any event we want to register.
|
|
|
|
await self.client.register_nio_room(room)
|
2019-11-09 01:20:38 +11:00
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomAliasEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomAliasEvent,
|
|
|
|
) -> None:
|
2019-11-27 21:18:06 +11:00
|
|
|
if ev.canonical_alias:
|
2019-11-27 21:30:42 +11:00
|
|
|
url = f"https://matrix.to/#/{quote(ev.canonical_alias)}"
|
2020-07-08 00:33:10 +10:00
|
|
|
link = f"<a href='{url}'>{escape(ev.canonical_alias)}</a>"
|
2019-12-07 09:23:25 +11:00
|
|
|
co = f"%1 set the room's main address to {link}"
|
2019-11-27 21:18:06 +11:00
|
|
|
else:
|
2019-12-07 09:23:25 +11:00
|
|
|
co = "%1 removed the room's main address"
|
2019-11-27 21:18:06 +11:00
|
|
|
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.client.register_nio_event(room, ev, content=co)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomNameEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomNameEvent,
|
|
|
|
) -> None:
|
2019-11-27 21:18:06 +11:00
|
|
|
if ev.name:
|
2020-07-08 00:33:10 +10:00
|
|
|
co = f"%1 changed the room's name to \"{escape(ev.name)}\""
|
2019-11-27 21:18:06 +11:00
|
|
|
else:
|
2019-12-07 09:23:25 +11:00
|
|
|
co = "%1 removed the room's name"
|
2019-11-27 21:18:06 +11:00
|
|
|
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.client.register_nio_event(room, ev, content=co)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomAvatarEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomAvatarEvent,
|
|
|
|
) -> None:
|
2019-11-27 21:18:06 +11:00
|
|
|
if ev.avatar_url:
|
2019-12-07 09:23:25 +11:00
|
|
|
co = "%1 changed the room's picture"
|
2019-11-27 21:18:06 +11:00
|
|
|
else:
|
2019-12-07 09:23:25 +11:00
|
|
|
co = "%1 removed the room's picture"
|
2019-11-27 21:18:06 +11:00
|
|
|
|
2020-07-21 13:09:28 +10:00
|
|
|
http = await self.client.mxc_to_http(ev.avatar_url)
|
|
|
|
|
2019-11-27 21:18:06 +11:00
|
|
|
await self.client.register_nio_event(
|
2020-07-21 13:09:28 +10:00
|
|
|
room, ev, content=co, media_url=ev.avatar_url, media_http_url=http,
|
2019-11-27 21:18:06 +11:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomTopicEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomTopicEvent,
|
|
|
|
) -> None:
|
2019-11-27 21:18:06 +11:00
|
|
|
if ev.topic:
|
2020-08-21 15:17:29 +10:00
|
|
|
topic = HTML_PROCESSOR.filter(plain2html(ev.topic), inline=True)
|
|
|
|
co = f"%1 changed the room's topic to \"{topic}\""
|
2019-11-27 21:18:06 +11:00
|
|
|
else:
|
2019-12-07 09:23:25 +11:00
|
|
|
co = "%1 removed the room's topic"
|
2019-11-27 21:18:06 +11:00
|
|
|
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.client.register_nio_event(room, ev, content=co)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onRoomEncryptionEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.RoomEncryptionEvent,
|
|
|
|
) -> None:
|
2019-12-07 09:23:25 +11:00
|
|
|
co = "%1 turned on encryption for this room"
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.client.register_nio_event(room, ev, content=co)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onMegolmEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.MegolmEvent,
|
|
|
|
) -> None:
|
2019-12-07 09:23:25 +11:00
|
|
|
co = "%1 sent an undecryptable message"
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.client.register_nio_event(room, ev, content=co)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onBadEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.BadEvent,
|
|
|
|
) -> None:
|
2020-07-08 00:33:10 +10:00
|
|
|
co = f"%1 sent a malformed <b>{escape(ev.type)}</b> event"
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.client.register_nio_event(room, ev, content=co)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onUnknownBadEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.UnknownBadEvent,
|
|
|
|
) -> None:
|
2019-12-09 03:46:25 +11:00
|
|
|
co = "%1 sent a malformed event lacking a minimal structure"
|
2019-12-05 09:20:30 +11:00
|
|
|
await self.client.register_nio_event(room, ev, content=co)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onUnknownEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.UnknownEvent,
|
|
|
|
) -> None:
|
2020-10-08 11:12:32 +11:00
|
|
|
if not self.client.backend.settings.Chat.show_unknown_events:
|
2020-05-22 04:37:48 +10:00
|
|
|
await self.client.register_nio_room(room)
|
2020-03-24 07:58:31 +11:00
|
|
|
return
|
|
|
|
|
2020-07-08 00:33:10 +10:00
|
|
|
co = f"%1 sent an unsupported <b>{escape(ev.type)}</b> event"
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.client.register_nio_event(room, ev, content=co)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onUnknownEncryptedEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.UnknownEncryptedEvent,
|
|
|
|
) -> None:
|
2019-12-09 03:46:25 +11:00
|
|
|
co = (
|
2020-07-08 00:33:10 +10:00
|
|
|
f"%1 sent an <b>{escape(ev.type)}</b> event encrypted with "
|
|
|
|
f"unsupported <b>{escape(ev.algorithm)}</b> algorithm"
|
2019-12-09 03:46:25 +11:00
|
|
|
)
|
2019-12-05 09:20:30 +11:00
|
|
|
await self.client.register_nio_event(room, ev, content=co)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
async def onInviteEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.InviteEvent,
|
|
|
|
) -> None:
|
2019-11-09 01:20:38 +11:00
|
|
|
await self.client.register_nio_room(room)
|
|
|
|
|
|
|
|
|
2020-05-31 14:11:56 +10:00
|
|
|
# Ephemeral event callbacks
|
|
|
|
|
|
|
|
async def onTypingNoticeEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.TypingNoticeEvent,
|
|
|
|
) -> None:
|
2019-11-09 01:20:38 +11:00
|
|
|
# Prevent recent past typing notices from being shown for a split
|
|
|
|
# second on client startup:
|
|
|
|
if not self.client.first_sync_done.is_set():
|
|
|
|
return
|
|
|
|
|
2019-12-03 07:29:29 +11:00
|
|
|
await self.client.register_nio_room(room)
|
|
|
|
|
|
|
|
room_id = room.room_id
|
2019-11-09 01:20:38 +11:00
|
|
|
|
2020-05-18 03:57:09 +10:00
|
|
|
room_item = self.models[self.user_id, "rooms"][room_id]
|
2019-11-09 01:20:38 +11:00
|
|
|
|
|
|
|
room_item.typing_members = sorted(
|
2020-04-04 22:30:58 +11:00
|
|
|
room.user_name(user_id) or user_id for user_id in ev.users
|
2019-11-09 01:20:38 +11:00
|
|
|
if user_id not in self.client.backend.clients
|
|
|
|
)
|
2020-05-31 14:38:48 +10:00
|
|
|
|
|
|
|
|
|
|
|
async def onReceiptEvent(
|
|
|
|
self, room: nio.MatrixRoom, ev: nio.ReceiptEvent,
|
|
|
|
) -> None:
|
2020-09-06 06:47:34 +10:00
|
|
|
member_model = self.models[self.user_id, room.room_id, "members"]
|
|
|
|
event_model = self.models[self.user_id, room.room_id, "events"]
|
|
|
|
unassigned_mems = self.client.unassigned_member_last_read_event
|
|
|
|
unassigned_evs = self.client.unassigned_event_last_read_by
|
2020-09-15 01:33:16 +10:00
|
|
|
recount_markers = []
|
2020-05-31 14:38:48 +10:00
|
|
|
|
|
|
|
for receipt in ev.receipts:
|
2020-09-06 06:47:34 +10:00
|
|
|
if receipt.user_id in self.client.backend.clients:
|
|
|
|
continue
|
|
|
|
|
2020-05-31 14:38:48 +10:00
|
|
|
if receipt.receipt_type != "m.read":
|
|
|
|
continue
|
|
|
|
|
2020-09-06 06:47:34 +10:00
|
|
|
echo_id = self.client.event_to_echo_ids.get(receipt.event_id)
|
|
|
|
read_event = event_model.get(echo_id or receipt.event_id)
|
|
|
|
timestamp = receipt.timestamp
|
2020-05-31 14:38:48 +10:00
|
|
|
|
2020-09-06 06:47:34 +10:00
|
|
|
if read_event:
|
2020-09-15 01:33:16 +10:00
|
|
|
recount_markers.append(read_event)
|
2020-09-06 06:47:34 +10:00
|
|
|
read_event.last_read_by[receipt.user_id] = timestamp
|
|
|
|
read_event.notify_change("last_read_by")
|
|
|
|
else:
|
|
|
|
# We haven't received the read event from the server yet
|
|
|
|
unassigned_evs[receipt.event_id][receipt.user_id] = timestamp
|
|
|
|
|
|
|
|
if receipt.user_id not in member_model:
|
|
|
|
# We haven't loaded the member yet (lazy loading), or they left
|
|
|
|
unassigned_mems[room.room_id, receipt.user_id] = \
|
|
|
|
echo_id or receipt.event_id
|
|
|
|
continue
|
|
|
|
|
|
|
|
member = member_model[receipt.user_id]
|
|
|
|
previous_read_event = event_model.get(member.last_read_event)
|
|
|
|
|
|
|
|
if previous_read_event:
|
|
|
|
# Remove the read marker from the previous last read event
|
2020-09-15 01:33:16 +10:00
|
|
|
recount_markers.append(previous_read_event)
|
2020-09-06 06:47:34 +10:00
|
|
|
previous_read_event.last_read_by.pop(receipt.user_id, None)
|
|
|
|
previous_read_event.notify_change("last_read_by")
|
|
|
|
|
|
|
|
member.last_read_event = echo_id or receipt.event_id
|
2020-06-02 11:00:22 +10:00
|
|
|
|
2020-09-15 01:33:16 +10:00
|
|
|
for ev in recount_markers:
|
|
|
|
ev.read_by_count = len(ev.last_read_by)
|
|
|
|
|
2020-06-02 11:00:22 +10:00
|
|
|
|
2020-10-31 05:15:17 +11:00
|
|
|
# Account data callbacks
|
|
|
|
|
|
|
|
async def onPushRulesEvent(self, ev: nio.PushRulesEvent) -> None:
|
|
|
|
model = self.models[self.user_id, "pushrules"]
|
|
|
|
|
2020-11-03 21:36:31 +11:00
|
|
|
kinds: Dict[nio.PushRuleKind, List[nio.PushRule]] = {
|
|
|
|
kind: getattr(ev.global_rules, kind.value) for kind in nio.PushRuleKind
|
2020-10-31 05:15:17 +11:00
|
|
|
}
|
|
|
|
|
2020-11-01 16:23:33 +11:00
|
|
|
# Remove from model rules that are now deleted.
|
|
|
|
# MUST be done first to avoid having rules sharing the same kind+order.
|
|
|
|
|
|
|
|
new_keys: Set[Tuple[str, str]] = set()
|
|
|
|
|
|
|
|
for kind, rules in kinds.items():
|
|
|
|
for rule in rules:
|
|
|
|
new_keys.add((kind.value, rule.id))
|
|
|
|
|
|
|
|
with model.batch_remove():
|
|
|
|
for key in tuple(model):
|
|
|
|
if key not in new_keys:
|
|
|
|
del model[key]
|
|
|
|
|
|
|
|
# Then, add new rules/modify changed existing ones
|
|
|
|
|
2020-10-31 05:15:17 +11:00
|
|
|
for kind, rules in kinds.items():
|
|
|
|
for order, rule in enumerate(rules):
|
|
|
|
tweaks = {
|
|
|
|
action.tweak: action.value for action in rule.actions
|
|
|
|
if isinstance(action, nio.PushSetTweak)
|
|
|
|
}
|
|
|
|
|
|
|
|
# Note: The `dont_notify` action does nothing.
|
|
|
|
# As of now (sept 2020), `coalesce` is just a `notify` synonym.
|
|
|
|
notify = any(
|
|
|
|
isinstance(action, (nio.PushNotify, nio.PushCoalesce))
|
|
|
|
for action in rule.actions
|
|
|
|
)
|
|
|
|
|
|
|
|
high = tweaks.get("highlight", False) is not False
|
|
|
|
bubble = tweaks.get("bubble", notify) is not False
|
2020-11-01 18:15:00 +11:00
|
|
|
sound = str(tweaks.get("sound") or "")
|
2020-10-31 05:15:17 +11:00
|
|
|
hint = tweaks.get("urgency_hint", high) is not False
|
|
|
|
|
2020-11-01 16:10:49 +11:00
|
|
|
model[kind.value, rule.id] = PushRule(
|
|
|
|
id = (kind.value, rule.id),
|
2020-10-31 05:15:17 +11:00
|
|
|
kind = kind,
|
2020-11-01 16:10:49 +11:00
|
|
|
rule_id = rule.id,
|
2020-10-31 05:15:17 +11:00
|
|
|
order = order,
|
|
|
|
default = rule.default,
|
|
|
|
enabled = rule.enabled,
|
|
|
|
pattern = rule.pattern,
|
|
|
|
notify = notify,
|
|
|
|
highlight = high,
|
|
|
|
bubble = bubble,
|
|
|
|
sound = sound,
|
|
|
|
urgency_hint = hint,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2020-06-02 11:00:22 +10:00
|
|
|
# Presence event callbacks
|
|
|
|
|
|
|
|
async def onPresenceEvent(self, ev: nio.PresenceEvent) -> None:
|
2020-07-03 00:44:54 +10:00
|
|
|
presence = self.client.backend.presences.get(ev.user_id, Presence())
|
|
|
|
|
2020-07-11 00:59:26 +10:00
|
|
|
presence.currently_active = ev.currently_active or False
|
2020-07-03 00:44:54 +10:00
|
|
|
presence.status_msg = ev.status_msg or ""
|
2020-07-10 09:53:25 +10:00
|
|
|
presence.last_active_at = (
|
|
|
|
datetime.now() - timedelta(milliseconds=ev.last_active_ago)
|
|
|
|
) if ev.last_active_ago else datetime.fromtimestamp(0)
|
2020-07-11 00:59:26 +10:00
|
|
|
|
2020-11-16 05:57:00 +11:00
|
|
|
presence.presence = \
|
|
|
|
Presence.State(ev.presence) if ev.presence else \
|
|
|
|
Presence.State.offline
|
2020-07-03 00:44:54 +10:00
|
|
|
|
2020-07-03 10:25:24 +10:00
|
|
|
# Add all existing members related to this presence
|
2020-07-03 00:44:54 +10:00
|
|
|
for room_id in self.models[self.user_id, "rooms"]:
|
|
|
|
member = self.models[self.user_id, room_id, "members"].get(
|
|
|
|
ev.user_id,
|
|
|
|
)
|
|
|
|
|
|
|
|
if member:
|
2020-07-11 00:59:26 +10:00
|
|
|
presence.members[room_id] = member
|
2020-07-03 00:44:54 +10:00
|
|
|
|
2020-07-03 10:25:24 +10:00
|
|
|
# Update members and accounts
|
2020-07-03 00:44:54 +10:00
|
|
|
presence.update_members()
|
2020-07-03 07:28:41 +10:00
|
|
|
|
2020-07-03 10:25:24 +10:00
|
|
|
# Check if presence event is ours
|
2020-07-10 06:06:14 +10:00
|
|
|
if (
|
|
|
|
ev.user_id in self.models["accounts"] and
|
2020-07-17 06:09:14 +10:00
|
|
|
self.models["accounts"][ev.user_id].presence !=
|
|
|
|
Presence.State.offline and
|
2020-07-10 09:53:25 +10:00
|
|
|
not (
|
|
|
|
presence.presence == Presence.State.offline and
|
|
|
|
self.models["accounts"][ev.user_id].presence !=
|
|
|
|
Presence.State.echo_invisible
|
|
|
|
)
|
2020-07-10 06:06:14 +10:00
|
|
|
):
|
|
|
|
account = self.models["accounts"][ev.user_id]
|
|
|
|
|
2020-07-17 06:09:14 +10:00
|
|
|
# Set status_msg if none is set on the server and we have one
|
|
|
|
if (
|
2020-11-16 05:57:00 +11:00
|
|
|
not presence.status_msg and
|
|
|
|
account.status_msg and
|
|
|
|
ev.user_id in self.client.backend.clients and
|
2020-07-17 21:59:43 +10:00
|
|
|
account.presence != Presence.State.echo_invisible and
|
|
|
|
presence.presence == Presence.State.offline
|
2020-07-17 06:09:14 +10:00
|
|
|
):
|
|
|
|
asyncio.ensure_future(
|
|
|
|
self.client.backend.clients[ev.user_id].set_presence(
|
|
|
|
presence.presence.value,
|
|
|
|
account.status_msg,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
2020-07-11 00:59:26 +10:00
|
|
|
# Do not fight back presence from other clients
|
2020-07-10 09:53:25 +10:00
|
|
|
self.client.backend.clients[ev.user_id]._presence = ev.presence
|
|
|
|
|
2020-07-03 10:25:24 +10:00
|
|
|
# Servers that send presence events support presence
|
2020-07-10 06:06:14 +10:00
|
|
|
account.presence_support = True
|
2020-07-03 10:25:24 +10:00
|
|
|
|
|
|
|
# Save the presence for the next resume
|
2020-07-11 14:51:53 +10:00
|
|
|
if account.save_presence:
|
2020-07-17 06:09:14 +10:00
|
|
|
status_msg = presence.status_msg
|
|
|
|
state = presence.presence
|
|
|
|
|
|
|
|
if account.presence == Presence.State.echo_invisible:
|
|
|
|
status_msg = account.status_msg
|
|
|
|
state = Presence.State.invisible
|
|
|
|
|
2020-10-05 18:06:07 +11:00
|
|
|
await self.client.backend.saved_accounts.set(
|
2020-07-17 06:09:14 +10:00
|
|
|
user_id = ev.user_id,
|
|
|
|
status_msg = status_msg,
|
|
|
|
presence = state.value,
|
2020-07-11 14:51:53 +10:00
|
|
|
)
|
2020-07-10 06:06:14 +10:00
|
|
|
|
|
|
|
presence.update_account()
|
2020-07-03 07:28:41 +10:00
|
|
|
|
2020-07-03 00:44:54 +10:00
|
|
|
self.client.backend.presences[ev.user_id] = presence
|