diff --git a/matrix_puppeteer_line/db/portal.py b/matrix_puppeteer_line/db/portal.py index fe64fa3..8a35130 100644 --- a/matrix_puppeteer_line/db/portal.py +++ b/matrix_puppeteer_line/db/portal.py @@ -31,23 +31,24 @@ class Portal: other_user: str mxid: Optional[RoomID] name: Optional[str] + icon_url: Optional[str] encrypted: bool async def insert(self) -> None: - q = ("INSERT INTO portal (chat_id, other_user, mxid, name, encrypted) " - "VALUES ($1, $2, $3, $4, $5)") + q = ("INSERT INTO portal (chat_id, other_user, mxid, name, icon_url, encrypted) " + "VALUES ($1, $2, $3, $4, $5, $6)") await self.db.execute(q, self.chat_id, self.other_user, self.mxid, self.name, - self.encrypted) + self.icon_url, self.encrypted) async def update(self) -> None: - q = ("UPDATE portal SET other_user=$2, mxid=$3, name=$4, encrypted=$5 " + q = ("UPDATE portal SET other_user=$2, mxid=$3, name=$4, icon_url=$5, encrypted=$6 " "WHERE chat_id=$1") await self.db.execute(q, self.chat_id, self.other_user, - self.mxid, self.name, self.encrypted) + self.mxid, self.name, self.icon_url, self.encrypted) @classmethod async def get_by_mxid(cls, mxid: RoomID) -> Optional['Portal']: - q = ("SELECT chat_id, other_user, mxid, name, encrypted " + q = ("SELECT chat_id, other_user, mxid, name, icon_url, encrypted " "FROM portal WHERE mxid=$1") row = await cls.db.fetchrow(q, mxid) if not row: @@ -56,7 +57,7 @@ class Portal: @classmethod async def get_by_chat_id(cls, chat_id: int) -> Optional['Portal']: - q = ("SELECT chat_id, other_user, mxid, name, encrypted " + q = ("SELECT chat_id, other_user, mxid, name, icon_url, encrypted " "FROM portal WHERE chat_id=$1") row = await cls.db.fetchrow(q, chat_id) if not row: @@ -65,12 +66,12 @@ class Portal: @classmethod async def find_private_chats(cls) -> List['Portal']: - rows = await cls.db.fetch("SELECT chat_id, other_user, mxid, name, encrypted " + rows = await cls.db.fetch("SELECT chat_id, other_user, mxid, name, icon_url, encrypted " "FROM portal WHERE other_user IS NOT NULL") return [cls(**row) for row in rows] @classmethod async def all_with_room(cls) -> List['Portal']: - rows = await cls.db.fetch("SELECT chat_id, other_user, mxid, name, encrypted " + rows = await cls.db.fetch("SELECT chat_id, other_user, mxid, name, icon_url, encrypted " "FROM portal WHERE mxid IS NOT NULL") return [cls(**row) for row in rows] diff --git a/matrix_puppeteer_line/db/puppet.py b/matrix_puppeteer_line/db/puppet.py index b27d59c..94258d2 100644 --- a/matrix_puppeteer_line/db/puppet.py +++ b/matrix_puppeteer_line/db/puppet.py @@ -28,20 +28,20 @@ class Puppet: mid: str name: Optional[str] - # TODO avatar: Optional[str] + avatar_url: Optional[str] is_registered: bool async def insert(self) -> None: - q = "INSERT INTO puppet (mid, name, is_registered) VALUES ($1, $2, $3)" - await self.db.execute(q, self.mid, self.name, self.is_registered) + q = "INSERT INTO puppet (mid, name, avatar_url, is_registered) VALUES ($1, $2, $3, $4)" + await self.db.execute(q, self.mid, self.name, self.avatar_url, self.is_registered) async def update(self) -> None: - q = "UPDATE puppet SET name=$2, is_registered=$3 WHERE mid=$1" - await self.db.execute(q, self.mid, self.name, self.is_registered) + q = "UPDATE puppet SET name=$2, avatar_url=$3, is_registered=$4 WHERE mid=$1" + await self.db.execute(q, self.mid, self.name, self.avatar_url, self.is_registered) @classmethod async def get_by_mid(cls, mid: str) -> Optional['Puppet']: - row = await cls.db.fetchrow("SELECT mid, name, is_registered FROM puppet WHERE mid=$1", + row = await cls.db.fetchrow("SELECT mid, name, avatar_url, is_registered FROM puppet WHERE mid=$1", mid) if not row: return None diff --git a/matrix_puppeteer_line/db/upgrade.py b/matrix_puppeteer_line/db/upgrade.py index b692364..80884f2 100644 --- a/matrix_puppeteer_line/db/upgrade.py +++ b/matrix_puppeteer_line/db/upgrade.py @@ -46,3 +46,13 @@ async def upgrade_v1(conn: Connection) -> None: UNIQUE (mxid, mx_room) )""") + + +@upgrade_table.register(description="Avatars and icons") +async def upgrade_avatars(conn: Connection) -> None: + for (table, column) in [('puppet', 'avatar_url'), ('portal', 'icon_url')]: + column_exists = await conn.fetchval( + "SELECT EXISTS(SELECT FROM information_schema.columns " + f"WHERE table_name='{table}' AND column_name='{column}')") + if not column_exists: + await conn.execute(f'ALTER TABLE "{table}" ADD COLUMN {column} TEXT') \ No newline at end of file diff --git a/matrix_puppeteer_line/portal.py b/matrix_puppeteer_line/portal.py index 59f8822..4f1c220 100644 --- a/matrix_puppeteer_line/portal.py +++ b/matrix_puppeteer_line/portal.py @@ -62,9 +62,9 @@ class Portal(DBPortal, BasePortal): _last_participant_update: Set[str] def __init__(self, chat_id: int, other_user: Optional[str] = None, - mxid: Optional[RoomID] = None, name: Optional[str] = None, encrypted: bool = False - ) -> None: - super().__init__(chat_id, other_user, mxid, name, encrypted) + mxid: Optional[RoomID] = None, name: Optional[str] = None, icon_url: Optional[str] = None, + encrypted: bool = False) -> None: + super().__init__(chat_id, other_user, mxid, name, icon_url, encrypted) self._create_room_lock = asyncio.Lock() self.log = self.log.getChild(str(chat_id)) @@ -240,6 +240,7 @@ class Portal(DBPortal, BasePortal): # TODO Consider setting no room name for non-group chats. # But then the LINE bot itself may appear in the title... changed = await self._update_name(f"{conv.name} (LINE)") + changed = await self._update_icon(conv.iconURL) or changed if changed: await self.update_bridge_info() await self.update() @@ -255,6 +256,15 @@ class Portal(DBPortal, BasePortal): return True return False + async def _update_icon(self, icon_url: Optional[str]) -> bool: + if self.icon_url != icon_url: + self.icon_url = icon_url + if icon_url: + # TODO set icon from bytes + pass + return True + return False + async def _update_participants(self, participants: List[Participant]) -> None: if not self.mxid: return @@ -472,6 +482,8 @@ class Portal(DBPortal, BasePortal): await DBMessage.delete_all(self.mxid) self.by_mxid.pop(self.mxid, None) self.mxid = None + self.name = None + self.icon_url = None self.encrypted = False await self.update() diff --git a/matrix_puppeteer_line/puppet.py b/matrix_puppeteer_line/puppet.py index d9f43fc..e684177 100644 --- a/matrix_puppeteer_line/puppet.py +++ b/matrix_puppeteer_line/puppet.py @@ -38,8 +38,9 @@ class Puppet(DBPuppet, BasePuppet): default_mxid: UserID - def __init__(self, mid: str, name: Optional[str] = None, is_registered: bool = False) -> None: - super().__init__(mid=mid, name=name, is_registered=is_registered) + def __init__(self, mid: str, name: Optional[str] = None, avatar_url: Optional[str] = None, + is_registered: bool = False) -> None: + super().__init__(mid=mid, name=name, avatar_url=avatar_url, is_registered=is_registered) self.log = self.log.getChild(mid) self.default_mxid = self.get_mxid_from_id(mid) @@ -63,7 +64,7 @@ class Puppet(DBPuppet, BasePuppet): async def update_info(self, info: Participant) -> None: update = False update = await self._update_name(info.name) or update - # TODO Update avatar + update = await self._update_avatar(info.avatarURL) or update if update: await self.update() @@ -75,6 +76,15 @@ class Puppet(DBPuppet, BasePuppet): return True return False + async def _update_avatar(self, avatar_url: Optional[str]) -> bool: + if avatar_url != self.avatar_url: + self.avatar_url = avatar_url + if avatar_url: + # TODO set the avatar from bytes + pass + return True + return False + def _add_to_cache(self) -> None: self.by_mid[self.mid] = self diff --git a/matrix_puppeteer_line/rpc/types.py b/matrix_puppeteer_line/rpc/types.py index b10e41d..05533cf 100644 --- a/matrix_puppeteer_line/rpc/types.py +++ b/matrix_puppeteer_line/rpc/types.py @@ -28,6 +28,7 @@ class RPCError(Exception): class ChatListInfo(SerializableAttrs['ChatListInfo']): id: int name: str + iconURL: Optional[str] lastMsg: str lastMsgDate: str @@ -35,7 +36,7 @@ class ChatListInfo(SerializableAttrs['ChatListInfo']): @dataclass class Participant(SerializableAttrs['Participant']): id: str - # TODO avatar: str + avatarURL: Optional[str] name: str diff --git a/puppet/src/contentscript.js b/puppet/src/contentscript.js index 7eba87c..1fc0fce 100644 --- a/puppet/src/contentscript.js +++ b/puppet/src/contentscript.js @@ -174,9 +174,6 @@ class MautrixController { // Room members are always friends (right?), // so search the friend list for the sender's name // and get their ID from there. - // TODO For rooms, allow creating Matrix puppets in case - // a message is sent by someone who since left the - // room and never had a puppet made for them yet. sender.id = this.getUserIdFromFriendsList(sender.name) // Group members aren't necessarily friends, // but the participant list includes their ID. @@ -185,7 +182,7 @@ class MautrixController { const participantsList = document.querySelector(participantsListSelector) sender.id = participantsList.querySelector(`img[alt='${senderName}'`).parentElement.parentElement.getAttribute("data-mid") } - // TODO Avatar + sender.avatarURL = this.getParticipantListItemAvatarURL(element) } else { // TODO Get own ID and store it somewhere appropriate. // Unable to get own ID from a room chat... @@ -199,7 +196,7 @@ class MautrixController { await window.__mautrixShowParticipantsList() const participantsList = document.querySelector(participantsListSelector) sender.name = this.getParticipantListItemName(participantsList.children[0]) - // TODO avatar + sender.avatarURL = this.getParticipantListItemAvatarURL(participantsList.children[0]) sender.id = this.ownID } @@ -306,7 +303,7 @@ class MautrixController { * @typedef Participant * @type object * @property {string} id - The member ID for the participant - * TODO @property {string} avatar - The URL of the participant's avatar + * @property {string} avatarURL - The URL of the participant's avatar * @property {string} name - The contact list name of the participant */ @@ -314,6 +311,15 @@ class MautrixController { return element.querySelector(".mdRGT13Ttl").innerText } + getParticipantListItemAvatarURL(element) { + const img = element.querySelector(".mdRGT13Img img[src]") + if (img && img.getAttribute("data-picture-path") != "" && img.src.startsWith("blob:")) { + return img.src + } else { + return "" + } + } + getParticipantListItemId(element) { // TODO Cache own ID return element.getAttribute("data-mid") @@ -334,7 +340,7 @@ class MautrixController { // One idea is to add real ID as suffix if we're in a group, and // put in the puppet DB table somehow. id: this.ownID, - // TODO avatar: child.querySelector("img").src, + avatarURL: this.getParticipantListItemAvatarURL(element.children[0]), name: this.getParticipantListItemName(element.children[0]), } @@ -343,7 +349,7 @@ class MautrixController { const id = this.getParticipantListItemId(child) || this.getUserIdFromFriendsList(name) return { id: id, // NOTE Don't want non-own user's ID to ever be null. - // TODO avatar: child.querySelector("img").src, + avatarURL: this.getParticipantListItemAvatarURL(child), name: name, } })) @@ -354,7 +360,7 @@ class MautrixController { * @type object * @property {number} id - The ID of the chat. * @property {string} name - The name of the chat. - * TODO @property {string} icon - The icon of the chat. + * @property {string} iconURL - The URL of the chat icon. * @property {string} lastMsg - The most recent message in the chat. * May be prefixed by sender name. * @property {string} lastMsgDate - An imprecise date for the most recent message @@ -369,6 +375,15 @@ class MautrixController { return element.querySelector(".mdCMN04Ttl").innerText } + getChatListItemIconURL(element) { + const img = element.querySelector(".mdCMN04Img > :not(.mdCMN04ImgInner) > img[src]") + if (img && img.getAttribute("data-picture-path") != "" && img.src.startsWith("blob:")) { + return img.src + } else { + return "" + } + } + getChatListItemLastMsg(element) { return element.querySelector(".mdCMN04Desc").innerText } @@ -388,7 +403,7 @@ class MautrixController { return !element.classList.contains("chatList") ? null : { id: knownId || this.getChatListItemId(element), name: this.getChatListItemName(element), - // TODO icon, but only for groups + iconURL: this.getChatListItemIconURL(element), lastMsg: this.getChatListItemLastMsg(element), lastMsgDate: this.getChatListItemLastMsgDate(element), } diff --git a/puppet/src/puppet.js b/puppet/src/puppet.js index d67c716..f86ff91 100644 --- a/puppet/src/puppet.js +++ b/puppet/src/puppet.js @@ -496,7 +496,7 @@ export default class MessagesPuppeteer { //await chatDetailArea.$(".MdTxtDesc02") || // 1:1 chat with custom title - get participant's real name participants = [{ id: id, - // TODO avatar, or leave null since this is a 1:1 chat + avatarURL: chatListInfo.iconURL, name: chatListInfo.name, }] }