Inbound channel photo & description

This commit is contained in:
Andrew Ferrazzutti 2022-04-13 13:02:55 -04:00
parent d843fcf5d2
commit a12efc92c4
8 changed files with 125 additions and 25 deletions

View File

@ -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)

View File

@ -3,3 +3,4 @@ from mautrix.util.async_db import UpgradeTable
upgrade_table = UpgradeTable()
from . import v01_initial_revision
from . import v02_channel_meta

View File

@ -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 <https://www.gnu.org/licenses/>.
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")

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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
})
}