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,
|
async def _bridge_own_message_pm(self, source: 'u.User', sender: Optional['p.Puppet'], mid: str,
|
||||||
invite: bool = True) -> Optional[IntentAPI]:
|
invite: bool = True) -> Optional[IntentAPI]:
|
||||||
# Use bridge bot as puppet for own user when puppet for own user is unavailable
|
intent = sender.intent if sender else (await source.get_own_puppet()).intent
|
||||||
# TODO Use own LINE puppet instead, and create it if it's not available yet
|
|
||||||
intent = sender.intent if sender else self.az.intent
|
|
||||||
if self.is_direct and (sender is None or sender.mid == source.mid and not sender.is_real_user):
|
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:
|
if self.invite_own_puppet_to_pm and invite:
|
||||||
try:
|
try:
|
||||||
@ -228,7 +226,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
if not self.invite_own_puppet_to_pm:
|
if not self.invite_own_puppet_to_pm:
|
||||||
self.log.warning(f"Ignoring message {evt.id}: double puppeting isn't enabled")
|
self.log.warning(f"Ignoring message {evt.id}: double puppeting isn't enabled")
|
||||||
return
|
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}")
|
intent = await self._bridge_own_message_pm(source, sender, f"message {evt.id}")
|
||||||
if not intent:
|
if not intent:
|
||||||
return
|
return
|
||||||
@ -552,11 +550,12 @@ class Portal(DBPortal, BasePortal):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
mid = p.Puppet.get_id_from_mxid(user_id)
|
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)
|
print(mid)
|
||||||
await self.main_intent.kick_user(self.mxid, user_id,
|
await self.main_intent.kick_user(self.mxid, user_id,
|
||||||
reason="User had left this chat")
|
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,
|
await self.main_intent.kick_user(self.mxid, user_id,
|
||||||
reason="Kicking own puppet")
|
reason="Kicking own puppet")
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ from base64 import b64decode
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from .rpc import RPCClient
|
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):
|
class LoginCommand(TypedDict):
|
||||||
@ -41,6 +41,9 @@ class Client(RPCClient):
|
|||||||
async def resume(self) -> None:
|
async def resume(self) -> None:
|
||||||
await self.request("resume")
|
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]:
|
async def get_chats(self) -> List[ChatListInfo]:
|
||||||
resp = await self.request("get_chats")
|
resp = await self.request("get_chats")
|
||||||
return [ChatListInfo.deserialize(data) for data in resp]
|
return [ChatListInfo.deserialize(data) for data in resp]
|
||||||
|
@ -40,10 +40,11 @@ class RPCClient:
|
|||||||
_response_waiters: Dict[int, asyncio.Future]
|
_response_waiters: Dict[int, asyncio.Future]
|
||||||
_event_handlers: Dict[str, List[EventHandler]]
|
_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.log = self.log.getChild(user_id)
|
||||||
self.loop = asyncio.get_running_loop()
|
self.loop = asyncio.get_running_loop()
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
|
self.own_id = own_id
|
||||||
self._req_id = 0
|
self._req_id = 0
|
||||||
self._min_broadcast_id = 0
|
self._min_broadcast_id = 0
|
||||||
self._event_handlers = {}
|
self._event_handlers = {}
|
||||||
@ -67,7 +68,9 @@ class RPCClient:
|
|||||||
self._writer = w
|
self._writer = w
|
||||||
self.loop.create_task(self._try_read_loop())
|
self.loop.create_task(self._try_read_loop())
|
||||||
self.loop.create_task(self._command_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:
|
async def disconnect(self) -> None:
|
||||||
self._writer.write_eof()
|
self._writer.write_eof()
|
||||||
|
@ -69,6 +69,14 @@ class User(DBUser, BaseUser):
|
|||||||
self.log.debug(f"Sending bridge notice: {text}")
|
self.log.debug(f"Sending bridge notice: {text}")
|
||||||
await self.az.intent.send_notice(self.notice_room, 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:
|
async def is_logged_in(self) -> bool:
|
||||||
try:
|
try:
|
||||||
return self.client and (await self.client.start()).is_logged_in
|
return self.client and (await self.client.start()).is_logged_in
|
||||||
@ -95,7 +103,7 @@ class User(DBUser, BaseUser):
|
|||||||
|
|
||||||
async def connect(self) -> None:
|
async def connect(self) -> None:
|
||||||
self.loop.create_task(self.connect_double_puppet())
|
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")
|
self.log.debug("Starting client")
|
||||||
await self.send_bridge_notice("Starting up...")
|
await self.send_bridge_notice("Starting up...")
|
||||||
state = await self.client.start()
|
state = await self.client.start()
|
||||||
@ -126,6 +134,7 @@ class User(DBUser, BaseUser):
|
|||||||
self._connection_check_task.cancel()
|
self._connection_check_task.cancel()
|
||||||
self._connection_check_task = self.loop.create_task(self._check_connection_loop())
|
self._connection_check_task = self.loop.create_task(self._check_connection_loop())
|
||||||
await self.client.pause()
|
await self.client.pause()
|
||||||
|
await self.sync_own_profile()
|
||||||
await self.client.set_last_message_ids(await DBMessage.get_max_mids())
|
await self.client.set_last_message_ids(await DBMessage.get_max_mids())
|
||||||
limit = self.config["bridge.initial_conversation_sync"]
|
limit = self.config["bridge.initial_conversation_sync"]
|
||||||
self.log.info("Syncing chats")
|
self.log.info("Syncing chats")
|
||||||
@ -152,6 +161,12 @@ class User(DBUser, BaseUser):
|
|||||||
await portal.update_matrix_room(self, chat)
|
await portal.update_matrix_room(self, chat)
|
||||||
await self.client.resume()
|
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:
|
async def stop(self) -> None:
|
||||||
# TODO Notices for shutdown messages
|
# TODO Notices for shutdown messages
|
||||||
if self._connection_check_task:
|
if self._connection_check_task:
|
||||||
|
@ -164,7 +164,7 @@ export default class Client {
|
|||||||
let started = false
|
let started = false
|
||||||
if (this.puppet === null) {
|
if (this.puppet === null) {
|
||||||
this.log("Opening new puppeteer for", this.userID)
|
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)
|
this.manager.puppets.set(this.userID, this.puppet)
|
||||||
await this.puppet.start(!!req.debug)
|
await this.puppet.start(!!req.debug)
|
||||||
started = true
|
started = true
|
||||||
@ -194,11 +194,12 @@ export default class Client {
|
|||||||
|
|
||||||
handleRegister = async (req) => {
|
handleRegister = async (req) => {
|
||||||
this.userID = req.user_id
|
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)) {
|
if (this.manager.clients.has(this.userID)) {
|
||||||
const oldClient = this.manager.clients.get(this.userID)
|
const oldClient = this.manager.clients.get(this.userID)
|
||||||
this.manager.clients.set(this.userID, this)
|
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")
|
await oldClient.stop("Socket replaced by new connection")
|
||||||
} else {
|
} else {
|
||||||
this.manager.clients.set(this.userID, this)
|
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),
|
set_last_message_ids: req => this.puppet.setLastMessageIDs(req.msg_ids),
|
||||||
pause: () => this.puppet.stopObserving(),
|
pause: () => this.puppet.stopObserving(),
|
||||||
resume: () => this.puppet.startObserving(),
|
resume: () => this.puppet.startObserving(),
|
||||||
|
get_own_profile: () => this.puppet.getOwnProfile(),
|
||||||
get_chats: () => this.puppet.getRecentChats(),
|
get_chats: () => this.puppet.getRecentChats(),
|
||||||
get_chat: req => this.puppet.getChatInfo(req.chat_id, req.force_view),
|
get_chat: req => this.puppet.getChatInfo(req.chat_id, req.force_view),
|
||||||
get_messages: req => this.puppet.getMessages(req.chat_id),
|
get_messages: req => this.puppet.getMessages(req.chat_id),
|
||||||
|
@ -102,9 +102,7 @@ class MautrixController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setOwnID(ownID) {
|
setOwnID(ownID) {
|
||||||
// Remove characters that will conflict with mxid grammar
|
this.ownID = ownID
|
||||||
const suffix = ownID.slice(1).replace(":", "_ON_")
|
|
||||||
this.ownID = `_OWN_${suffix}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Commonize with Node context
|
// TODO Commonize with Node context
|
||||||
@ -527,7 +525,7 @@ class MautrixController {
|
|||||||
* @typedef PathImage
|
* @typedef PathImage
|
||||||
* @type object
|
* @type object
|
||||||
* @property {?string} path - The virtual path of the image (behaves like an ID). Optional.
|
* @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
|
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
|
* @param {[MutationRecord]} mutations - The mutation records that occurred
|
||||||
* @private
|
* @private
|
||||||
|
@ -36,12 +36,13 @@ export default class MessagesPuppeteer {
|
|||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {?Client} [client]
|
* @param {?Client} [client]
|
||||||
*/
|
*/
|
||||||
constructor(id, client = null) {
|
constructor(id, ownID, client = null) {
|
||||||
let profilePath = path.join(MessagesPuppeteer.profileDir, id)
|
let profilePath = path.join(MessagesPuppeteer.profileDir, id)
|
||||||
if (!profilePath.startsWith("/")) {
|
if (!profilePath.startsWith("/")) {
|
||||||
profilePath = path.join(process.cwd(), profilePath)
|
profilePath = path.join(process.cwd(), profilePath)
|
||||||
}
|
}
|
||||||
this.id = id
|
this.id = id
|
||||||
|
this.ownID = ownID
|
||||||
this.profilePath = profilePath
|
this.profilePath = profilePath
|
||||||
this.updatedChats = new Set()
|
this.updatedChats = new Set()
|
||||||
this.sentMessageIDs = 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.
|
* 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")
|
this.log("Removing observers")
|
||||||
// TODO __mautrixController is undefined when cancelling, why?
|
// 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.removeQRChangeObserver())
|
||||||
await this.page.evaluate(() => window.__mautrixController.removeQRAppearObserver())
|
await this.page.evaluate(() => window.__mautrixController.removeQRAppearObserver())
|
||||||
await this.page.evaluate(() => window.__mautrixController.removeEmailAppearObserver())
|
await this.page.evaluate(() => window.__mautrixController.removeEmailAppearObserver())
|
||||||
@ -274,15 +287,6 @@ export default class MessagesPuppeteer {
|
|||||||
|
|
||||||
this.loginRunning = false
|
this.loginRunning = false
|
||||||
await this.blankPage.bringToFront()
|
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")
|
this.log("Login complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -464,6 +468,41 @@ export default class MessagesPuppeteer {
|
|||||||
() => window.__mautrixController.removeMsgListObserver())
|
() => 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) {
|
_listItemSelector(id) {
|
||||||
return `#_chat_list_body div[data-chatid="${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")
|
await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.log("Waiting for chat to stabilize")
|
||||||
|
await this.page.evaluate(() => window.__mautrixController.waitForMessageListStability())
|
||||||
|
|
||||||
if (hadMsgListObserver) {
|
if (hadMsgListObserver) {
|
||||||
this.log("Restoring msg list observer")
|
this.log("Restoring msg list observer")
|
||||||
await this.page.evaluate(
|
await this.page.evaluate(
|
||||||
@ -595,8 +637,11 @@ export default class MessagesPuppeteer {
|
|||||||
|
|
||||||
const input = await this.page.$("#_chat_room_input")
|
const input = await this.page.$("#_chat_room_input")
|
||||||
await this._interactWithPage(async () => {
|
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.click()
|
||||||
await input.type(text)
|
await input.evaluate((e, text) => e.innerText = text, text)
|
||||||
await input.press("Enter")
|
await input.press("Enter")
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -808,18 +853,8 @@ export default class MessagesPuppeteer {
|
|||||||
|
|
||||||
async _sendEmailCredentials() {
|
async _sendEmailCredentials() {
|
||||||
this.log("Inputting login credentials")
|
this.log("Inputting login credentials")
|
||||||
|
await this._enterText(await this.page.$("#line_login_email"), this.login_email)
|
||||||
// Triple-click input fields to select all existing text and replace it on type
|
await this._enterText(await this.page.$("#line_login_pwd"), this.login_password)
|
||||||
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.page.click("button#login_btn")
|
await this.page.click("button#login_btn")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user