From a12efc92c41907ada74c207c0cd994286a1adfad Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Wed, 13 Apr 2022 13:02:55 -0400 Subject: [PATCH] Inbound channel photo & description --- matrix_appservice_kakaotalk/db/portal.py | 19 ++++---- .../db/upgrade/__init__.py | 1 + .../db/upgrade/v02_channel_meta.py | 24 ++++++++++ .../kt/client/client.py | 8 ++++ .../kt/client/types.py | 5 ++- matrix_appservice_kakaotalk/portal.py | 44 +++++++++++++------ matrix_appservice_kakaotalk/user.py | 18 +++++++- node/src/client.js | 31 ++++++++++++- 8 files changed, 125 insertions(+), 25 deletions(-) create mode 100644 matrix_appservice_kakaotalk/db/upgrade/v02_channel_meta.py diff --git a/matrix_appservice_kakaotalk/db/portal.py b/matrix_appservice_kakaotalk/db/portal.py index 3b5e756..7f6734c 100644 --- a/matrix_appservice_kakaotalk/db/portal.py +++ b/matrix_appservice_kakaotalk/db/portal.py @@ -38,10 +38,12 @@ class Portal: kt_type: ChannelType mxid: RoomID | None name: str | None + description: str | None photo_id: str | None avatar_url: ContentURI | None encrypted: bool name_set: bool + topic_set: bool avatar_set: bool relay_user_id: UserID | None @@ -56,7 +58,7 @@ class Portal: @classmethod async def get_by_ktid(cls, ktid: int, kt_receiver: int) -> Portal | None: q = """ - SELECT ktid, kt_receiver, kt_type, mxid, name, photo_id, avatar_url, encrypted, + SELECT ktid, kt_receiver, kt_type, mxid, name, description, photo_id, avatar_url, encrypted, name_set, avatar_set, relay_user_id FROM portal WHERE ktid=$1 AND kt_receiver=$2 """ @@ -66,7 +68,7 @@ class Portal: @classmethod async def get_by_mxid(cls, mxid: RoomID) -> Portal | None: q = """ - SELECT ktid, kt_receiver, kt_type, mxid, name, photo_id, avatar_url, encrypted, + SELECT ktid, kt_receiver, kt_type, mxid, name, description, photo_id, avatar_url, encrypted, name_set, avatar_set, relay_user_id FROM portal WHERE mxid=$1 """ @@ -76,7 +78,7 @@ class Portal: @classmethod async def get_all_by_receiver(cls, kt_receiver: int) -> list[Portal]: q = """ - SELECT ktid, kt_receiver, kt_type, mxid, name, photo_id, avatar_url, encrypted, + SELECT ktid, kt_receiver, kt_type, mxid, name, description, photo_id, avatar_url, encrypted, name_set, avatar_set, relay_user_id FROM portal WHERE kt_receiver=$1 """ @@ -86,7 +88,7 @@ class Portal: @classmethod async def all(cls) -> list[Portal]: q = """ - SELECT ktid, kt_receiver, kt_type, mxid, name, photo_id, avatar_url, encrypted, + SELECT ktid, kt_receiver, kt_type, mxid, name, description, photo_id, avatar_url, encrypted, name_set, avatar_set, relay_user_id FROM portal """ @@ -101,6 +103,7 @@ class Portal: self.kt_type, self.mxid, self.name, + self.description, self.photo_id, self.avatar_url, self.encrypted, @@ -111,9 +114,9 @@ class Portal: async def insert(self) -> None: q = """ - INSERT INTO portal (ktid, kt_receiver, kt_type, mxid, name, photo_id, avatar_url, + INSERT INTO portal (ktid, kt_receiver, kt_type, mxid, name, description, photo_id, avatar_url, encrypted, name_set, avatar_set, relay_user_id) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) """ await self.db.execute(q, *self._values) @@ -123,8 +126,8 @@ class Portal: async def save(self) -> None: q = """ - UPDATE portal SET kt_type=$3, mxid=$4, name=$5, photo_id=$6, avatar_url=$7, - encrypted=$8, name_set=$9, avatar_set=$10, relay_user_id=$11 + UPDATE portal SET kt_type=$3, mxid=$4, name=$5, description=$6, photo_id=$7, avatar_url=$8, + encrypted=$9, name_set=$10, avatar_set=$11, relay_user_id=$12 WHERE ktid=$1 AND kt_receiver=$2 """ await self.db.execute(q, *self._values) diff --git a/matrix_appservice_kakaotalk/db/upgrade/__init__.py b/matrix_appservice_kakaotalk/db/upgrade/__init__.py index 146e713..2079708 100644 --- a/matrix_appservice_kakaotalk/db/upgrade/__init__.py +++ b/matrix_appservice_kakaotalk/db/upgrade/__init__.py @@ -3,3 +3,4 @@ from mautrix.util.async_db import UpgradeTable upgrade_table = UpgradeTable() from . import v01_initial_revision +from . import v02_channel_meta diff --git a/matrix_appservice_kakaotalk/db/upgrade/v02_channel_meta.py b/matrix_appservice_kakaotalk/db/upgrade/v02_channel_meta.py new file mode 100644 index 0000000..31e9f3d --- /dev/null +++ b/matrix_appservice_kakaotalk/db/upgrade/v02_channel_meta.py @@ -0,0 +1,24 @@ +# matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge. +# Copyright (C) 2022 Tulir Asokan, Andrew Ferrazzutti +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from mautrix.util.async_db import Connection + +from . import upgrade_table + + +@upgrade_table.register(description="Support channel descriptions") +async def upgrade_v2(conn: Connection) -> None: + await conn.execute("ALTER TABLE portal ADD COLUMN description TEXT") + await conn.execute("ALTER TABLE portal ADD COLUMN topic_set BOOLEAN NOT NULL DEFAULT false") diff --git a/matrix_appservice_kakaotalk/kt/client/client.py b/matrix_appservice_kakaotalk/kt/client/client.py index 0364791..30e61a7 100644 --- a/matrix_appservice_kakaotalk/kt/client/client.py +++ b/matrix_appservice_kakaotalk/kt/client/client.py @@ -500,6 +500,13 @@ class Client: str(data["channelType"]), ) + def _on_channel_meta_change(self, data: dict[str, JSON]) -> Awaitable[None]: + return self.user.on_channel_meta_change( + PortalChannelInfo.deserialize(data["info"]), + Long.deserialize(data["channelId"]), + str(data["channelType"]), + ) + def _on_listen_disconnect(self, data: dict[str, JSON]) -> Awaitable[None]: try: @@ -532,6 +539,7 @@ class Client: self._add_event_handler("channel_kicked", self._on_channel_kicked) self._add_event_handler("user_join", self._on_user_join) self._add_event_handler("user_left", self._on_user_left) + self._add_event_handler("channel_meta_change", self._on_channel_meta_change) self._add_event_handler("disconnected", self._on_listen_disconnect) self._add_event_handler("switch_server", self._on_switch_server) self._add_event_handler("error", self._on_error) diff --git a/matrix_appservice_kakaotalk/kt/client/types.py b/matrix_appservice_kakaotalk/kt/client/types.py index e782137..6c41a09 100644 --- a/matrix_appservice_kakaotalk/kt/client/types.py +++ b/matrix_appservice_kakaotalk/kt/client/types.py @@ -69,8 +69,9 @@ setattr(UserInfoUnion, "deserialize", deserialize_user_info_union) @dataclass class PortalChannelInfo(SerializableAttrs): name: str - participants: list[UserInfoUnion] - # TODO Image + description: Optional[str] = None + photoURL: Optional[str] = None + participants: Optional[list[UserInfoUnion]] = None # May set to None to skip participant update channel_info: Optional[ChannelInfoUnion] = None # Should be set manually by caller diff --git a/matrix_appservice_kakaotalk/portal.py b/matrix_appservice_kakaotalk/portal.py index 905a96a..4e16ba5 100644 --- a/matrix_appservice_kakaotalk/portal.py +++ b/matrix_appservice_kakaotalk/portal.py @@ -146,10 +146,12 @@ class Portal(DBPortal, BasePortal): kt_type: ChannelType, mxid: RoomID | None = None, name: str | None = None, + description: str | None = None, photo_id: str | None = None, avatar_url: ContentURI | None = None, encrypted: bool = False, name_set: bool = False, + topic_set: bool = False, avatar_set: bool = False, relay_user_id: UserID | None = None, ) -> None: @@ -159,10 +161,12 @@ class Portal(DBPortal, BasePortal): kt_type, mxid, name, + description, photo_id, avatar_url, encrypted, name_set, + topic_set, avatar_set, relay_user_id, ) @@ -317,17 +321,18 @@ class Portal(DBPortal, BasePortal): changed = any( await asyncio.gather( self._update_name(info.name), - # TODO - #self._update_photo(source, info.image), + self._update_description(info.description), + self._update_photo(source, info.photoURL), ) ) - changed = await self._update_participants(source, info.participants) or changed + if info.participants is not None: + changed = await self._update_participants(source, info.participants) or changed + if self.mxid and self.is_open: + user_power_levels = await self._get_mapped_participant_power_levels(info.participants, skip_default=False) + asyncio.create_task(self.set_power_levels(user_power_levels)) if changed or force_save: await self.update_bridge_info() await self.save() - if self.mxid and self.is_open: - user_power_levels = await self._get_mapped_participant_power_levels(info.participants, skip_default=False) - await self.set_power_levels(user_power_levels) return info async def _get_mapped_participant_power_levels(self, participants: list[UserInfoUnion], skip_default: bool) -> dict[UserID, int]: @@ -434,23 +439,37 @@ class Portal(DBPortal, BasePortal): return True return False - """ - async def _update_photo(self, source: u.User, photo: graphql.Picture) -> bool: + async def _update_description(self, description: str | None) -> bool: + if self.description != description or not self.topic_set: + self.log.trace("Updating description %s -> %s", self.description, description) + self.description = description + if self.mxid and (self.encrypted or not self.is_direct): + try: + await self.main_intent.set_room_topic(self.mxid, self.description) + self.topic_set = True + except Exception: + self.log.exception("Failed to set room description") + self.topic_set = False + return True + return False + + async def _update_photo(self, source: u.User, photo_id: str | None) -> bool: if self.is_direct and not self.encrypted: return False - photo_id = self.get_photo_id(photo) + if self.photo_id is not None and photo_id is None: + self.log.warning("Portal previously had a photo_id, but new photo_id is None. Leaving it as it is") + return False if self.photo_id != photo_id or not self.avatar_set: self.photo_id = photo_id - if photo: + if photo_id: if self.photo_id != photo_id or not self.avatar_url: # Reset avatar_url first in case the upload fails self.avatar_url = None self.avatar_url = await p.Puppet.reupload_avatar( source, self.main_intent, - photo.uri, + photo_id, self.ktid, - use_graph=self.is_direct and (photo.height or 0) < 500, ) else: self.avatar_url = ContentURI("") @@ -463,7 +482,6 @@ class Portal(DBPortal, BasePortal): self.avatar_set = False return True return False - """ async def _update_photo_from_puppet(self, puppet: p.Puppet) -> bool: if self.photo_id == puppet.photo_id and self.avatar_set: diff --git a/matrix_appservice_kakaotalk/user.py b/matrix_appservice_kakaotalk/user.py index e092347..014fb43 100644 --- a/matrix_appservice_kakaotalk/user.py +++ b/matrix_appservice_kakaotalk/user.py @@ -39,7 +39,7 @@ from .db import User as DBUser from .kt.client import Client from .kt.client.errors import AuthenticationRequired, ResponseError -from .kt.client.types import SettingsStruct, FROM_PERM_MAP +from .kt.client.types import PortalChannelInfo, SettingsStruct, FROM_PERM_MAP from .kt.types.bson import Long from .kt.types.channel.channel_info import ChannelInfo, NormalChannelInfo, NormalChannelData from .kt.types.channel.channel_type import ChannelType, KnownChannelType @@ -55,6 +55,7 @@ METRIC_CONNECT_AND_SYNC = Summary("bridge_connect_and_sync", "calls to connect_a METRIC_CHAT = Summary("bridge_on_chat", "calls to on_chat") METRIC_CHAT_DELETED = Summary("bridge_on_chat_deleted", "calls to on_chat_deleted") METRIC_CHAT_READ = Summary("bridge_on_chat_read", "calls to on_chat_read") +METRIC_CHANNEL_META_CHANGE = Summary("bridge_on_channel_meta_change", "calls to on_channel_meta_change") METRIC_PROFILE_CHANGE = Summary("bridge_on_profile_changed", "calls to on_profile_changed") METRIC_PERM_CHANGE = Summary("bridge_on_perm_changed", "calls to on_perm_changed") METRIC_CHANNEL_JOIN = Summary("bridge_on_channel_join", "calls to on_channel_join") @@ -741,6 +742,21 @@ class User(DBUser, BaseUser): await portal.backfill_lock.wait(f"read receipt from {sender_id}") await portal.handle_kakaotalk_chat_read(self, puppet, chat_id) + @async_time(METRIC_CHANNEL_META_CHANGE) + async def on_channel_meta_change( + self, + info: PortalChannelInfo, + channel_id: Long, + channel_type: ChannelType, + ) -> None: + portal = await po.Portal.get_by_ktid( + channel_id, + kt_receiver=self.ktid, + kt_type=channel_type, + ) + if portal: + await portal.update_info(self, info) + @async_time(METRIC_PROFILE_CHANGE) async def on_profile_changed(self, info: OpenLinkChannelUserInfo) -> None: puppet = await pu.Puppet.get_by_ktid(info.userId) diff --git a/node/src/client.js b/node/src/client.js index 35a20e9..a98e17e 100644 --- a/node/src/client.js +++ b/node/src/client.js @@ -191,6 +191,33 @@ class UserClient { }) }) + this.#talkClient.on("meta_change", (channel, type, newMeta) => { + this.log(`Channel ${channel.channelId} metadata changed`) + }) + + this.#talkClient.on("push_packet", (method, data) => { + // TODO Find a better way to do this...but data doesn't have much. + if (method == "SYNCLINKUP") { + if (!data?.ol) return + const linkURL = data.ol?.lu + if (!linkURL) return + for (const channel of this.#talkClient.channelList.open.all()) { + if (channel.info.openLink?.linkURL == linkURL) { + this.write("channel_meta_change", { + info: { + name: data.ol?.ln, + description: data.ol?.desc || null, + photoURL: data.ol?.liu || null, + }, + channelId: channel.channelId, + channelType: channel.info.type, + }) + break + } + } + } + }) + this.#talkClient.on("disconnected", (reason) => { this.log(`Disconnected (reason=${reason})`) this.disconnect() @@ -568,8 +595,10 @@ export default class PeerClient { return makeCommandResult({ name: talkChannel.getDisplayName(), + description: talkChannel.info.openLink?.description, + // TODO Find out why linkCoverURL is blank, despite having updated the channel! + photoURL: talkChannel.info.openLink?.linkCoverURL || null, participants: Array.from(talkChannel.getAllUserInfo()), - // TODO Image }) }