From 555b19c2893e34030384bf95da3462dbed90f4fc Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 15 Jun 2021 02:48:29 -0400 Subject: [PATCH] Always use LINE puppet for own messages ...that are sent from another client. Also look up the profile data for the user's LINE account on sync, including at startup, so that there's always a puppet available. --- matrix_puppeteer_line/portal.py | 9 +++--- matrix_puppeteer_line/rpc/client.py | 5 ++- matrix_puppeteer_line/rpc/rpc.py | 7 +++-- matrix_puppeteer_line/user.py | 17 ++++++++++- puppet/src/client.js | 8 +++-- puppet/src/contentscript.js | 6 ++-- puppet/src/puppet.js | 47 ++++++++++++++++++++++------- 7 files changed, 72 insertions(+), 27 deletions(-) diff --git a/matrix_puppeteer_line/portal.py b/matrix_puppeteer_line/portal.py index 6e35368..8a78a43 100644 --- a/matrix_puppeteer_line/portal.py +++ b/matrix_puppeteer_line/portal.py @@ -185,9 +185,7 @@ class Portal(DBPortal, BasePortal): async def _bridge_own_message_pm(self, source: 'u.User', sender: Optional['p.Puppet'], mid: str, invite: bool = True) -> Optional[IntentAPI]: - # Use bridge bot as puppet for own user when puppet for own user is unavailable - # TODO Use own LINE puppet instead, and create it if it's not available yet - intent = sender.intent if sender else self.az.intent + intent = sender.intent if sender else (await source.get_own_puppet()).intent if self.is_direct and (sender is None or sender.mid == source.mid and not sender.is_real_user): if self.invite_own_puppet_to_pm and invite: try: @@ -529,11 +527,12 @@ class Portal(DBPortal, BasePortal): continue mid = p.Puppet.get_id_from_mxid(user_id) - if mid and mid not in current_members: + is_own_puppet = p.Puppet.is_mid_for_own_puppet(mid) + if mid and mid not in current_members and not is_own_puppet: print(mid) await self.main_intent.kick_user(self.mxid, user_id, reason="User had left this chat") - elif forbid_own_puppets and p.Puppet.is_mid_for_own_puppet(mid): + elif forbid_own_puppets and is_own_puppet: await self.main_intent.kick_user(self.mxid, user_id, reason="Kicking own puppet") diff --git a/matrix_puppeteer_line/rpc/client.py b/matrix_puppeteer_line/rpc/client.py index 3e68865..2a8600a 100644 --- a/matrix_puppeteer_line/rpc/client.py +++ b/matrix_puppeteer_line/rpc/client.py @@ -19,7 +19,7 @@ from base64 import b64decode import asyncio from .rpc import RPCClient -from .types import ChatListInfo, ChatInfo, Message, Receipt, ImageData, StartStatus +from .types import ChatListInfo, ChatInfo, ImageData, Message, Participant, Receipt, StartStatus class LoginCommand(TypedDict): @@ -41,6 +41,9 @@ class Client(RPCClient): async def resume(self) -> None: await self.request("resume") + async def get_own_profile(self) -> Participant: + return Participant.deserialize(await self.request("get_own_profile")) + async def get_chats(self) -> List[ChatListInfo]: resp = await self.request("get_chats") return [ChatListInfo.deserialize(data) for data in resp] diff --git a/matrix_puppeteer_line/rpc/rpc.py b/matrix_puppeteer_line/rpc/rpc.py index 49e088a..3400865 100644 --- a/matrix_puppeteer_line/rpc/rpc.py +++ b/matrix_puppeteer_line/rpc/rpc.py @@ -40,10 +40,11 @@ class RPCClient: _response_waiters: Dict[int, asyncio.Future] _event_handlers: Dict[str, List[EventHandler]] - def __init__(self, user_id: UserID) -> None: + def __init__(self, user_id: UserID, own_id: str) -> None: self.log = self.log.getChild(user_id) self.loop = asyncio.get_running_loop() self.user_id = user_id + self.own_id = own_id self._req_id = 0 self._min_broadcast_id = 0 self._event_handlers = {} @@ -67,7 +68,9 @@ class RPCClient: self._writer = w self.loop.create_task(self._try_read_loop()) self.loop.create_task(self._command_loop()) - await self.request("register", user_id=self.user_id) + await self.request("register", + user_id=self.user_id, + own_id = self.own_id) async def disconnect(self) -> None: self._writer.write_eof() diff --git a/matrix_puppeteer_line/user.py b/matrix_puppeteer_line/user.py index 1372ac1..2fcfc2c 100644 --- a/matrix_puppeteer_line/user.py +++ b/matrix_puppeteer_line/user.py @@ -69,6 +69,14 @@ class User(DBUser, BaseUser): self.log.debug(f"Sending bridge notice: {text}") await self.az.intent.send_notice(self.notice_room, text) + @property + def own_id(self) -> str: + # Remove characters that will conflict with mxid grammar + return f"_OWN_{self.mxid[1:].replace(':', '_ON_')}" + + async def get_own_puppet(self) -> 'pu.Puppet': + return await pu.Puppet.get_by_mid(self.own_id) + async def is_logged_in(self) -> bool: try: return self.client and (await self.client.start()).is_logged_in @@ -95,7 +103,7 @@ class User(DBUser, BaseUser): async def connect(self) -> None: self.loop.create_task(self.connect_double_puppet()) - self.client = Client(self.mxid) + self.client = Client(self.mxid, self.own_id) self.log.debug("Starting client") await self.send_bridge_notice("Starting up...") state = await self.client.start() @@ -126,6 +134,7 @@ class User(DBUser, BaseUser): self._connection_check_task.cancel() self._connection_check_task = self.loop.create_task(self._check_connection_loop()) await self.client.pause() + await self.sync_own_profile() await self.client.set_last_message_ids(await DBMessage.get_max_mids()) limit = self.config["bridge.initial_conversation_sync"] self.log.info("Syncing chats") @@ -144,6 +153,12 @@ class User(DBUser, BaseUser): await self.send_bridge_notice("Synchronization complete") await self.client.resume() + async def sync_own_profile(self) -> None: + self.log.info("Syncing own LINE profile info") + own_profile = await self.client.get_own_profile() + puppet = await self.get_own_puppet() + await puppet.update_info(own_profile, self.client) + async def stop(self) -> None: # TODO Notices for shutdown messages if self._connection_check_task: diff --git a/puppet/src/client.js b/puppet/src/client.js index 636d695..db6c9ac 100644 --- a/puppet/src/client.js +++ b/puppet/src/client.js @@ -164,7 +164,7 @@ export default class Client { let started = false if (this.puppet === null) { this.log("Opening new puppeteer for", this.userID) - this.puppet = new MessagesPuppeteer(this.userID, this) + this.puppet = new MessagesPuppeteer(this.userID, this.ownID, this) this.manager.puppets.set(this.userID, this.puppet) await this.puppet.start(!!req.debug) started = true @@ -194,11 +194,12 @@ export default class Client { handleRegister = async (req) => { this.userID = req.user_id - this.log("Registered socket", this.connID, "->", this.userID) + this.ownID = req.own_id + this.log(`Registered socket ${this.connID} -> ${this.userID}`) if (this.manager.clients.has(this.userID)) { const oldClient = this.manager.clients.get(this.userID) this.manager.clients.set(this.userID, this) - this.log("Terminating previous socket", oldClient.connID, "for", this.userID) + this.log(`Terminating previous socket ${oldClient.connID} for ${this.userID}`) await oldClient.stop("Socket replaced by new connection") } else { this.manager.clients.set(this.userID, this) @@ -258,6 +259,7 @@ export default class Client { set_last_message_ids: req => this.puppet.setLastMessageIDs(req.msg_ids), pause: () => this.puppet.stopObserving(), resume: () => this.puppet.startObserving(), + get_own_profile: () => this.puppet.getOwnProfile(), get_chats: () => this.puppet.getRecentChats(), get_chat: req => this.puppet.getChatInfo(req.chat_id), get_messages: req => this.puppet.getMessages(req.chat_id), diff --git a/puppet/src/contentscript.js b/puppet/src/contentscript.js index 226a7c4..32cfdf7 100644 --- a/puppet/src/contentscript.js +++ b/puppet/src/contentscript.js @@ -102,9 +102,7 @@ class MautrixController { } setOwnID(ownID) { - // Remove characters that will conflict with mxid grammar - const suffix = ownID.slice(1).replace(":", "_ON_") - this.ownID = `_OWN_${suffix}` + this.ownID = ownID } // TODO Commonize with Node context @@ -527,7 +525,7 @@ class MautrixController { * @typedef PathImage * @type object * @property {?string} path - The virtual path of the image (behaves like an ID). Optional. - * @property {string} src - The URL of the image. Mandatory. + * @property {string} url - The URL of the image. Mandatory. */ /** diff --git a/puppet/src/puppet.js b/puppet/src/puppet.js index 70f0c9b..974c3dd 100644 --- a/puppet/src/puppet.js +++ b/puppet/src/puppet.js @@ -36,12 +36,13 @@ export default class MessagesPuppeteer { * @param {string} id * @param {?Client} [client] */ - constructor(id, client = null) { + constructor(id, ownID, client = null) { let profilePath = path.join(MessagesPuppeteer.profileDir, id) if (!profilePath.startsWith("/")) { profilePath = path.join(process.cwd(), profilePath) } this.id = id + this.ownID = ownID this.profilePath = profilePath this.updatedChats = new Set() this.sentMessageIDs = new Set() @@ -231,7 +232,7 @@ export default class MessagesPuppeteer { this.log("Removing observers") // TODO __mautrixController is undefined when cancelling, why? - await this.page.evaluate(ownID => window.__mautrixController.setOwnID(ownID), this.id) + await this.page.evaluate(ownID => window.__mautrixController.setOwnID(ownID), this.ownID) await this.page.evaluate(() => window.__mautrixController.removeQRChangeObserver()) await this.page.evaluate(() => window.__mautrixController.removeQRAppearObserver()) await this.page.evaluate(() => window.__mautrixController.removeEmailAppearObserver()) @@ -266,15 +267,6 @@ export default class MessagesPuppeteer { } this.loginRunning = false - // Don't start observing yet, instead wait for explicit request. - // But at least view the most recent chat. - try { - const mostRecentChatID = await this.page.$eval("#_chat_list_body li", - element => window.__mautrixController.getChatListItemID(element.firstElementChild)) - await this._switchChat(mostRecentChatID) - } catch (e) { - this.log("No chats available to focus on") - } this.log("Login complete") } @@ -455,6 +447,39 @@ export default class MessagesPuppeteer { () => window.__mautrixController.removeMsgListObserver()) } + async getOwnProfile() { + return await this.taskQueue.push(() => this._getOwnProfileUnsafe()) + } + + async _getOwnProfileUnsafe() { + // NOTE Will send a read receipt if a chat was in view! + // Best to use this on startup when no chat is viewed. + let ownProfile + this.log("Opening settings view") + await this.page.click("button.mdGHD01SettingBtn") + await this.page.waitForSelector("#context_menu li#settings", {visible: true}).then(e => e.click()) + await this.page.waitForSelector("#settings_contents", {visible: true}) + + this.log("Getting own profile info") + ownProfile = { + id: this.ownID, + name: await this.page.$eval("#settings_basic_name_input", e => e.innerText), + avatar: { + path: null, + url: await this.page.$eval(".mdCMN09ImgInput", e => { + const imgStr = e.style?.getPropertyValue("background-image") + const matches = imgStr.match(/url\("(blob:.*)"\)/) + return matches?.length == 2 ? matches[1] : null + }), + }, + } + + const backSelector = "#label_setting button" + await this.page.click(backSelector) + await this.page.waitForSelector(backSelector, {visible: false}) + return ownProfile + } + _listItemSelector(id) { return `#_chat_list_body div[data-chatid="${id}"]` }