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}"]` }