forked from fair/matrix-puppeteer-line
Merge branch 'master' into better-receipts-msc2409
This commit is contained in:
commit
a3e7caac27
|
@ -192,9 +192,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:
|
||||
|
@ -228,7 +226,7 @@ class Portal(DBPortal, BasePortal):
|
|||
if not self.invite_own_puppet_to_pm:
|
||||
self.log.warning(f"Ignoring message {evt.id}: double puppeting isn't enabled")
|
||||
return
|
||||
sender = p.Puppet.get_by_mid(evt.sender.id) if not self.is_direct else None
|
||||
sender = await p.Puppet.get_by_mid(evt.sender.id) if evt.sender else None
|
||||
intent = await self._bridge_own_message_pm(source, sender, f"message {evt.id}")
|
||||
if not intent:
|
||||
return
|
||||
|
@ -552,11 +550,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")
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
@ -152,6 +161,12 @@ class User(DBUser, BaseUser):
|
|||
await portal.update_matrix_room(self, chat)
|
||||
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:
|
||||
|
|
|
@ -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, req.force_view),
|
||||
get_messages: req => this.puppet.getMessages(req.chat_id),
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -683,6 +681,49 @@ class MautrixController {
|
|||
return promise
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for updates to the active chat's message list to settle down.
|
||||
* Wait an additional bit of time every time an update is observed.
|
||||
* TODO Look (harder) for an explicit signal of when a chat is fully updated...
|
||||
*
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
waitForMessageListStability() {
|
||||
// Increase this if messages get missed on sync / chat change.
|
||||
// Decrease it if response times are too slow.
|
||||
const delayMillis = 2000
|
||||
|
||||
let myResolve
|
||||
const promise = new Promise(resolve => {myResolve = resolve})
|
||||
|
||||
let observer
|
||||
const onTimeout = () => {
|
||||
console.log("Message list looks stable, continue")
|
||||
console.debug(`timeoutID = ${timeoutID}`)
|
||||
observer.disconnect()
|
||||
myResolve()
|
||||
}
|
||||
|
||||
let timeoutID
|
||||
const startTimer = () => {
|
||||
timeoutID = setTimeout(onTimeout, delayMillis)
|
||||
}
|
||||
|
||||
observer = new MutationObserver(changes => {
|
||||
clearTimeout(timeoutID)
|
||||
console.log("CHANGE to message list detected! Wait a bit longer...")
|
||||
console.debug(`timeoutID = ${timeoutID}`)
|
||||
console.debug(changes)
|
||||
startTimer()
|
||||
})
|
||||
observer.observe(
|
||||
document.querySelector("#_chat_message_area"),
|
||||
{childList: true, attributes: true, subtree: true})
|
||||
startTimer()
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[MutationRecord]} mutations - The mutation records that occurred
|
||||
* @private
|
||||
|
|
|
@ -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()
|
||||
|
@ -146,6 +147,18 @@ export default class MessagesPuppeteer {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the contents of a text input field to the given text.
|
||||
* Works by triple-clicking the input field to select all existing text, to replace it on type.
|
||||
*
|
||||
* @param {ElementHandle} inputElement - The input element to type into.
|
||||
* @param {string} text - The text to input.
|
||||
*/
|
||||
async _enterText(inputElement, text) {
|
||||
await inputElement.click({clickCount: 3})
|
||||
await inputElement.type(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the session to be logged in and monitor changes while it's not.
|
||||
*/
|
||||
|
@ -238,7 +251,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())
|
||||
|
@ -274,15 +287,6 @@ export default class MessagesPuppeteer {
|
|||
|
||||
this.loginRunning = false
|
||||
await this.blankPage.bringToFront()
|
||||
// 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")
|
||||
}
|
||||
|
||||
|
@ -464,6 +468,41 @@ 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
|
||||
await this._interactWithPage(async () => {
|
||||
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}"]`
|
||||
}
|
||||
|
@ -520,6 +559,9 @@ export default class MessagesPuppeteer {
|
|||
await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info")
|
||||
})
|
||||
|
||||
this.log("Waiting for chat to stabilize")
|
||||
await this.page.evaluate(() => window.__mautrixController.waitForMessageListStability())
|
||||
|
||||
if (hadMsgListObserver) {
|
||||
this.log("Restoring msg list observer")
|
||||
await this.page.evaluate(
|
||||
|
@ -595,8 +637,11 @@ export default class MessagesPuppeteer {
|
|||
|
||||
const input = await this.page.$("#_chat_room_input")
|
||||
await this._interactWithPage(async () => {
|
||||
// Live-typing in the field can have its text mismatch what was requested!!
|
||||
// Probably because the input element is a div instead of a real text input...ugh!
|
||||
// Setting its innerText directly works fine though...
|
||||
await input.click()
|
||||
await input.type(text)
|
||||
await input.evaluate((e, text) => e.innerText = text, text)
|
||||
await input.press("Enter")
|
||||
})
|
||||
|
||||
|
@ -808,18 +853,8 @@ export default class MessagesPuppeteer {
|
|||
|
||||
async _sendEmailCredentials() {
|
||||
this.log("Inputting login credentials")
|
||||
|
||||
// Triple-click input fields to select all existing text and replace it on type
|
||||
let input
|
||||
|
||||
input = await this.page.$("#line_login_email")
|
||||
await input.click({clickCount: 3})
|
||||
await input.type(this.login_email)
|
||||
|
||||
input = await this.page.$("#line_login_pwd")
|
||||
await input.click({clickCount: 3})
|
||||
await input.type(this.login_password)
|
||||
|
||||
await this._enterText(await this.page.$("#line_login_email"), this.login_email)
|
||||
await this._enterText(await this.page.$("#line_login_pwd"), this.login_password)
|
||||
await this.page.click("button#login_btn")
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue