forked from fair/matrix-puppeteer-line
multi-user chats
This commit is contained in:
parent
b25bac8cea
commit
20f419a5c3
|
@ -58,6 +58,7 @@ class Config(BaseBridgeConfig):
|
|||
copy("bridge.displayname_max_length")
|
||||
|
||||
copy("bridge.initial_conversation_sync")
|
||||
copy("bridge.invite_own_puppet_to_pm")
|
||||
copy("bridge.login_shared_secret")
|
||||
copy("bridge.federate_rooms")
|
||||
copy("bridge.backfill.invite_own_puppet")
|
||||
|
|
|
@ -78,6 +78,9 @@ bridge:
|
|||
# Number of conversations to sync (and create portals for) on login.
|
||||
# Set 0 to disable automatic syncing.
|
||||
initial_conversation_sync: 10
|
||||
# Whether or not the LINE users of logged in Matrix users should be
|
||||
# invited to private chats when the user sends a message from another client.
|
||||
invite_own_puppet_to_pm: false
|
||||
# Shared secret for https://github.com/devture/matrix-synapse-shared-secret-auth
|
||||
#
|
||||
# If set, custom puppets will be enabled automatically for local users
|
||||
|
|
|
@ -22,7 +22,7 @@ import magic
|
|||
from mautrix.appservice import AppService, IntentAPI
|
||||
from mautrix.bridge import BasePortal, NotificationDisabler
|
||||
from mautrix.types import (EventID, MessageEventContent, RoomID, EventType, MessageType,
|
||||
TextMessageEventContent, MediaMessageEventContent,
|
||||
TextMessageEventContent, MediaMessageEventContent, Membership,
|
||||
ContentURI, EncryptedFile)
|
||||
from mautrix.errors import MatrixError
|
||||
from mautrix.util.simple_lock import SimpleLock
|
||||
|
@ -49,6 +49,7 @@ ReuploadedMediaInfo = NamedTuple('ReuploadedMediaInfo', mxc=Optional[ContentURI]
|
|||
|
||||
|
||||
class Portal(DBPortal, BasePortal):
|
||||
invite_own_puppet_to_pm: bool = False
|
||||
by_mxid: Dict[RoomID, 'Portal'] = {}
|
||||
by_chat_id: Dict[int, 'Portal'] = {}
|
||||
config: Config
|
||||
|
@ -60,8 +61,6 @@ class Portal(DBPortal, BasePortal):
|
|||
backfill_lock: SimpleLock
|
||||
_last_participant_update: Set[str]
|
||||
|
||||
_main_intent: IntentAPI
|
||||
|
||||
def __init__(self, chat_id: int, other_user: Optional[str] = None,
|
||||
mxid: Optional[RoomID] = None, name: Optional[str] = None, encrypted: bool = False
|
||||
) -> None:
|
||||
|
@ -77,7 +76,15 @@ class Portal(DBPortal, BasePortal):
|
|||
|
||||
@property
|
||||
def is_direct(self) -> bool:
|
||||
return self.other_user is not None
|
||||
return self.chat_id[0] == "u"
|
||||
|
||||
@property
|
||||
def is_group(self) -> bool:
|
||||
return self.chat_id[0] == "c"
|
||||
|
||||
@property
|
||||
def is_room(self) -> bool:
|
||||
return self.chat_id[0] == "r"
|
||||
|
||||
@property
|
||||
def main_intent(self) -> IntentAPI:
|
||||
|
@ -92,6 +99,7 @@ class Portal(DBPortal, BasePortal):
|
|||
cls.az = bridge.az
|
||||
cls.loop = bridge.loop
|
||||
cls.bridge = bridge
|
||||
cls.invite_own_puppet_to_pm = cls.config["bridge.invite_own_puppet_to_pm"]
|
||||
NotificationDisabler.puppet_cls = p.Puppet
|
||||
NotificationDisabler.config_enabled = cls.config["bridge.backfill.disable_notifications"]
|
||||
|
||||
|
@ -145,17 +153,39 @@ class Portal(DBPortal, BasePortal):
|
|||
self.log.debug(f"{user.mxid} left portal to {self.chat_id}")
|
||||
# TODO cleanup if empty
|
||||
|
||||
async def handle_remote_message(self, source: 'u.User', evt: Message) -> None:
|
||||
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, if it's available
|
||||
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.invite_own_puppet_to_pm and invite:
|
||||
await self.main_intent.invite_user(self.mxid, intent.mxid)
|
||||
elif await self.az.state_store.get_membership(self.mxid,
|
||||
intent.mxid) != Membership.JOIN:
|
||||
self.log.warning(f"Ignoring own {mid} in private chat because own puppet is not in"
|
||||
" room.")
|
||||
intent = None
|
||||
return intent
|
||||
|
||||
async def handle_remote_message(self, source: 'u.User', sender: Optional['p.Puppet'],
|
||||
evt: Message) -> None:
|
||||
if evt.is_outgoing:
|
||||
if not source.intent:
|
||||
self.log.warning(f"Ignoring message {evt.id}: double puppeting isn't enabled")
|
||||
return
|
||||
intent = source.intent
|
||||
if source.intent:
|
||||
intent = source.intent
|
||||
else:
|
||||
if not self.invite_own_puppet_to_pm:
|
||||
self.log.warning(f"Ignoring message {evt.id}: double puppeting isn't enabled")
|
||||
return
|
||||
intent = await self._bridge_own_message_pm(source, sender, f"message {evt.id}")
|
||||
if not intent:
|
||||
return
|
||||
elif self.other_user:
|
||||
intent = (await p.Puppet.get_by_mid(self.other_user)).intent
|
||||
elif sender:
|
||||
intent = sender.intent
|
||||
else:
|
||||
# TODO group chats
|
||||
self.log.warning(f"Ignoring message {evt.id}: group chats aren't supported yet")
|
||||
self.log.warning(f"Ignoring message {evt.id}: sender puppet is unavailable")
|
||||
return
|
||||
|
||||
if await DBMessage.get_by_mid(evt.id):
|
||||
|
@ -200,19 +230,22 @@ class Portal(DBPortal, BasePortal):
|
|||
return ReuploadedMediaInfo(mxc, decryption_info, mime_type, file_name, len(data))
|
||||
|
||||
async def update_info(self, conv: ChatInfo) -> None:
|
||||
# TODO Not true: a single-participant chat could be a group!
|
||||
if len(conv.participants) == 1:
|
||||
if self.is_direct:
|
||||
self.other_user = conv.participants[0].id
|
||||
if self._main_intent is self.az.intent:
|
||||
self._main_intent = (await p.Puppet.get_by_mid(self.other_user)).intent
|
||||
for participant in conv.participants:
|
||||
puppet = await p.Puppet.get_by_mid(participant.id)
|
||||
await puppet.update_info(participant)
|
||||
changed = await self._update_name(conv.name)
|
||||
# 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)")
|
||||
if changed:
|
||||
await self.update_bridge_info()
|
||||
await self.update()
|
||||
await self._update_participants(conv.participants)
|
||||
# NOTE Don't call this yet, lest puppets join earlier than
|
||||
# when their user actually joined or sent a message.
|
||||
#await self._update_participants(conv.participants)
|
||||
|
||||
async def _update_name(self, name: str) -> bool:
|
||||
if self.name != name:
|
||||
|
@ -251,8 +284,11 @@ class Portal(DBPortal, BasePortal):
|
|||
reason="User had left this chat")
|
||||
|
||||
async def backfill(self, source: 'u.User') -> None:
|
||||
with self.backfill_lock:
|
||||
await self._backfill(source)
|
||||
try:
|
||||
with self.backfill_lock:
|
||||
await self._backfill(source)
|
||||
except Exception:
|
||||
self.log.exception("Failed to backfill portal")
|
||||
|
||||
async def _backfill(self, source: 'u.User') -> None:
|
||||
self.log.debug("Backfilling history through %s", source.mxid)
|
||||
|
@ -267,8 +303,15 @@ class Portal(DBPortal, BasePortal):
|
|||
|
||||
self.log.debug("Got %d messages from server", len(messages))
|
||||
async with NotificationDisabler(self.mxid, source):
|
||||
# Member joins/leaves are not shown in chat history.
|
||||
# Best we can do is have a puppet join if its user had sent a message.
|
||||
members_known = set(await self.main_intent.get_room_members(self.mxid)) if not self.is_direct else None
|
||||
for evt in messages:
|
||||
await self.handle_remote_message(source, evt)
|
||||
puppet = await p.Puppet.get_by_mid(evt.sender.id) if not self.is_direct else None
|
||||
if puppet and evt.sender.id not in members_known:
|
||||
await puppet.update_info(evt.sender)
|
||||
members_known.add(evt.sender.id)
|
||||
await self.handle_remote_message(source, puppet, evt)
|
||||
self.log.info("Backfilled %d messages through %s", len(messages), source.mxid)
|
||||
|
||||
@property
|
||||
|
@ -325,6 +368,8 @@ class Portal(DBPortal, BasePortal):
|
|||
await puppet.az.intent.ensure_joined(self.mxid)
|
||||
|
||||
await self.update_info(info)
|
||||
await self.backfill(source)
|
||||
await self._update_participants(info.participants)
|
||||
|
||||
async def _create_matrix_room(self, source: 'u.User', info: ChatInfo) -> Optional[RoomID]:
|
||||
if self.mxid:
|
||||
|
@ -352,8 +397,10 @@ class Portal(DBPortal, BasePortal):
|
|||
})
|
||||
if self.is_direct:
|
||||
invites.append(self.az.bot_mxid)
|
||||
if self.encrypted or not self.is_direct:
|
||||
name = self.name
|
||||
# NOTE Set the room title even for direct chats, because
|
||||
# the LINE bot itself may appear in the title otherwise.
|
||||
#if self.encrypted or not self.is_direct:
|
||||
name = self.name
|
||||
if self.config["appservice.community_id"]:
|
||||
initial_state.append({
|
||||
"type": "m.room.related_groups",
|
||||
|
@ -394,6 +441,11 @@ class Portal(DBPortal, BasePortal):
|
|||
self.log.debug(f"Matrix room created: {self.mxid}")
|
||||
self.by_mxid[self.mxid] = self
|
||||
if not self.is_direct:
|
||||
# For multi-user chats, backfill before updating participants,
|
||||
# to act as as a best guess of when users actually joined.
|
||||
# No way to tell when a user actually left, so just check the
|
||||
# participants list after backfilling.
|
||||
await self.backfill(source)
|
||||
await self._update_participants(info.participants)
|
||||
else:
|
||||
puppet = await p.Puppet.get_by_custom_mxid(source.mxid)
|
||||
|
@ -403,11 +455,7 @@ class Portal(DBPortal, BasePortal):
|
|||
except MatrixError:
|
||||
self.log.debug("Failed to join custom puppet into newly created portal",
|
||||
exc_info=True)
|
||||
|
||||
try:
|
||||
await self.backfill(source)
|
||||
except Exception:
|
||||
self.log.exception("Failed to backfill new portal")
|
||||
|
||||
return self.mxid
|
||||
|
||||
|
|
|
@ -98,6 +98,7 @@ class Puppet(DBPuppet, BasePuppet):
|
|||
|
||||
@classmethod
|
||||
async def get_by_mid(cls, mid: str, create: bool = True) -> Optional['Puppet']:
|
||||
# TODO Might need to parse a real id from "_OWN"
|
||||
try:
|
||||
return cls.by_mid[mid]
|
||||
except KeyError:
|
||||
|
|
|
@ -35,6 +35,7 @@ class ChatListInfo(SerializableAttrs['ChatListInfo']):
|
|||
@dataclass
|
||||
class Participant(SerializableAttrs['Participant']):
|
||||
id: str
|
||||
# TODO avatar: str
|
||||
name: str
|
||||
|
||||
|
||||
|
@ -48,6 +49,7 @@ class Message(SerializableAttrs['Message']):
|
|||
id: int
|
||||
chat_id: int
|
||||
is_outgoing: bool
|
||||
sender: Optional[Participant]
|
||||
timestamp: int = None
|
||||
text: Optional[str] = None
|
||||
image: Optional[str] = None
|
||||
|
|
|
@ -124,7 +124,6 @@ class User(DBUser, BaseUser):
|
|||
chat = await self.client.get_chat(chat.id)
|
||||
if portal.mxid:
|
||||
await portal.update_matrix_room(self, chat)
|
||||
await portal.backfill(self)
|
||||
else:
|
||||
await portal.create_matrix_room(self, chat)
|
||||
|
||||
|
@ -145,10 +144,11 @@ class User(DBUser, BaseUser):
|
|||
async def handle_message(self, evt: Message) -> None:
|
||||
self.log.trace("Received message %s", evt)
|
||||
portal = await po.Portal.get_by_chat_id(evt.chat_id, create=True)
|
||||
puppet = await pu.Puppet.get_by_mid(evt.sender.id) if not portal.is_direct else None
|
||||
if not portal.mxid:
|
||||
chat_info = await self.client.get_chat(evt.chat_id)
|
||||
await portal.create_matrix_room(self, chat_info)
|
||||
await portal.handle_remote_message(self, evt)
|
||||
await portal.handle_remote_message(self, puppet, evt)
|
||||
|
||||
def _add_to_cache(self) -> None:
|
||||
self.by_mxid[self.mxid] = self
|
||||
|
|
|
@ -99,7 +99,7 @@ export default class Client {
|
|||
}
|
||||
|
||||
sendMessage(message) {
|
||||
this.log("Sending", message, "to client")
|
||||
this.log(`Sending message ${message.id} to client`)
|
||||
return this._write({
|
||||
id: --this.notificationID,
|
||||
command: "message",
|
||||
|
@ -204,7 +204,9 @@ export default class Client {
|
|||
this.log("Ignoring old request", req.id)
|
||||
return
|
||||
}
|
||||
this.log("Received request", req.id, "with command", req.command)
|
||||
if (req.command != "is_connected") {
|
||||
this.log("Received request", req.id, "with command", req.command)
|
||||
}
|
||||
this.maxCommandID = req.id
|
||||
let handler
|
||||
if (!this.userID) {
|
||||
|
|
|
@ -51,15 +51,46 @@ window.__mautrixExpiry = function (button) {}
|
|||
* @return {Promise<void>}
|
||||
*/
|
||||
window.__mautrixReceiveMessageID = function(id) {}
|
||||
/**
|
||||
* @return {Promise<Element>}
|
||||
*/
|
||||
window.__mautrixGetParticipantsList = function() {}
|
||||
|
||||
const ChatTypeEnum = Object.freeze({
|
||||
DIRECT: 1,
|
||||
GROUP: 2,
|
||||
ROOM: 3,
|
||||
})
|
||||
|
||||
class MautrixController {
|
||||
constructor() {
|
||||
constructor(ownID) {
|
||||
this.chatListObserver = null
|
||||
this.qrChangeObserver = null
|
||||
this.qrAppearObserver = null
|
||||
this.emailAppearObserver = null
|
||||
this.pinAppearObserver = null
|
||||
this.expiryObserver = null
|
||||
this.ownID = null
|
||||
}
|
||||
|
||||
setOwnID(ownID) {
|
||||
// Remove characters that will conflict with mxid grammar
|
||||
const suffix = ownID.slice(1).replace(":", "_ON_")
|
||||
this.ownID = `_OWN_${suffix}`
|
||||
}
|
||||
|
||||
// TODO Commonize with Node context
|
||||
getChatType(id) {
|
||||
switch (id.charAt(0)) {
|
||||
case "u":
|
||||
return ChatTypeEnum.DIRECT
|
||||
case "c":
|
||||
return ChatTypeEnum.GROUP
|
||||
case "r":
|
||||
return ChatTypeEnum.ROOM
|
||||
default:
|
||||
throw `Invalid chat ID: ${id}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,12 +129,23 @@ class MautrixController {
|
|||
return newDate && newDate <= now ? newDate : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to match a user against an entry in the friends list to get their ID.
|
||||
*
|
||||
* @param {Element} element - The display name of the user to find the ID for.
|
||||
* @return {null|str} - The user's ID if found.
|
||||
*/
|
||||
getUserIdFromFriendsList(senderName) {
|
||||
return document.querySelector(`#contact_wrap_friends > ul > li[title='${senderName}']`)?.getAttribute("data-mid")
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef MessageData
|
||||
* @type {object}
|
||||
* @property {number} id - The ID of the message. Seems to be sequential.
|
||||
* @property {number} timestamp - The unix timestamp of the message. Not very accurate.
|
||||
* @property {boolean} is_outgoing - Whether or not this user sent the message.
|
||||
* @property {null|Participant} sender - Full data of the participant who sent the message, if needed and available.
|
||||
* @property {string} [text] - The text in the message.
|
||||
* @property {string} [image] - The URL to the image in the message.
|
||||
*/
|
||||
|
@ -113,14 +155,59 @@ class MautrixController {
|
|||
*
|
||||
* @param {Date} date - The most recent date indicator.
|
||||
* @param {Element} element - The message element.
|
||||
* @param {int} chatType - What kind of chat this message is part of.
|
||||
* @return {MessageData}
|
||||
* @private
|
||||
*/
|
||||
_tryParseMessage(date, element) {
|
||||
async _tryParseMessage(date, element, chatType) {
|
||||
const is_outgoing = element.classList.contains("mdRGT07Own")
|
||||
let sender = {}
|
||||
|
||||
// TODO Clean up participantsList access...
|
||||
const participantsListSelector = "#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul"
|
||||
|
||||
// Don't need sender ID for direct chats, since the portal will have it already.
|
||||
if (chatType == ChatTypeEnum.DIRECT) {
|
||||
sender = null
|
||||
} else if (!is_outgoing) {
|
||||
sender.name = element.querySelector(".mdRGT07Body > .mdRGT07Ttl").innerText
|
||||
// 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.
|
||||
if (!sender.id) {
|
||||
await window.__mautrixShowParticipantsList()
|
||||
const participantsList = document.querySelector(participantsListSelector)
|
||||
sender.id = participantsList.querySelector(`img[alt='${senderName}'`).parentElement.parentElement.getAttribute("data-mid")
|
||||
}
|
||||
// TODO Avatar
|
||||
} else {
|
||||
// TODO Get own ID and store it somewhere appropriate.
|
||||
// Unable to get own ID from a room chat...
|
||||
// if (chatType == ChatTypeEnum.GROUP) {
|
||||
// await window.__mautrixShowParticipantsList()
|
||||
// const participantsList = document.querySelector("#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul")
|
||||
// // TODO The first member is always yourself, right?
|
||||
// // TODO Cache this so own ID can be used later
|
||||
// sender = participantsList.children[0].getAttribute("data-mid")
|
||||
// }
|
||||
await window.__mautrixShowParticipantsList()
|
||||
const participantsList = document.querySelector(participantsListSelector)
|
||||
sender.name = this.getParticipantListItemName(participantsList.children[0])
|
||||
// TODO avatar
|
||||
sender.id = this.ownID
|
||||
}
|
||||
|
||||
const messageData = {
|
||||
id: +element.getAttribute("data-local-id"),
|
||||
timestamp: date ? date.getTime() : null,
|
||||
is_outgoing: element.classList.contains("mdRGT07Own"),
|
||||
is_outgoing: is_outgoing,
|
||||
sender: sender,
|
||||
}
|
||||
const messageElement = element.querySelector(".mdRGT07Body > .mdRGT07Msg")
|
||||
if (messageElement.classList.contains("mdRGT07Text")) {
|
||||
|
@ -184,9 +271,14 @@ class MautrixController {
|
|||
/**
|
||||
* Parse the message list of whatever the currently-viewed chat is.
|
||||
*
|
||||
* @param {null|string} chatId - The ID of the currently-viewed chat, if known.
|
||||
* @return {[MessageData]} - A list of messages.
|
||||
*/
|
||||
async parseMessageList() {
|
||||
async parseMessageList(chatId) {
|
||||
if (!chatId) {
|
||||
chatId = this.getChatListItemId(document.querySelector("#_chat_list_body > .ExSelected > div"))
|
||||
}
|
||||
const chatType = this.getChatType(chatId);
|
||||
const msgList = document.querySelector("#_chat_room_msg_list")
|
||||
const messages = []
|
||||
let refDate = null
|
||||
|
@ -202,7 +294,7 @@ class MautrixController {
|
|||
const timeElement = child.querySelector("time")
|
||||
if (timeElement) {
|
||||
const messageDate = await this._tryParseDate(timeElement.innerText, refDate)
|
||||
messages.push(this._tryParseMessage(messageDate, child))
|
||||
messages.push(await this._tryParseMessage(messageDate, child, chatType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -218,6 +310,15 @@ class MautrixController {
|
|||
* @property {string} name - The contact list name of the participant
|
||||
*/
|
||||
|
||||
getParticipantListItemName(element) {
|
||||
return element.querySelector(".mdRGT13Ttl").innerText
|
||||
}
|
||||
|
||||
getParticipantListItemId(element) {
|
||||
// TODO Cache own ID
|
||||
return element.getAttribute("data-mid")
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a group participants list.
|
||||
* TODO Find what works for a *room* participants list...!
|
||||
|
@ -226,16 +327,26 @@ class MautrixController {
|
|||
* @return {[Participant]} - The list of participants.
|
||||
*/
|
||||
parseParticipantList(element) {
|
||||
// TODO Slice to exclude first member, which is always yourself (right?)
|
||||
// TODO Only slice if double-puppeting is enabled!
|
||||
//return Array.from(element.children).slice(1).map(child => {
|
||||
return Array.from(element.children).map(child => {
|
||||
// TODO Might need to explicitly exclude own user if double-puppeting is enabled.
|
||||
// TODO The first member is always yourself, right?
|
||||
const ownParticipant = {
|
||||
// TODO Find way to make this work with multiple mxids using the bridge.
|
||||
// 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,
|
||||
name: this.getParticipantListItemName(element.children[0]),
|
||||
}
|
||||
|
||||
return [ownParticipant].concat(Array.from(element.children).slice(1).map(child => {
|
||||
const name = this.getParticipantListItemName(child)
|
||||
const id = this.getParticipantListItemId(child) || this.getUserIdFromFriendsList(name)
|
||||
return {
|
||||
id: child.getAttribute("data-mid"),
|
||||
id: id, // NOTE Don't want non-own user's ID to ever be null.
|
||||
// TODO avatar: child.querySelector("img").src,
|
||||
name: child.querySelector(".mdRGT13Ttl").innerText,
|
||||
name: name,
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -29,7 +29,7 @@ export default class MessagesPuppeteer {
|
|||
static noSandbox = false
|
||||
static viewport = { width: 960, height: 880 }
|
||||
static url = undefined
|
||||
static extensionDir = 'extension_files'
|
||||
static extensionDir = "extension_files"
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -58,6 +58,14 @@ export default class MessagesPuppeteer {
|
|||
console.error(`[Puppeteer/${this.id}]`, ...text)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inner text of an element.
|
||||
* To be called in browser context.
|
||||
*/
|
||||
_getInnerText(element) {
|
||||
return element?.innerText
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the browser and open the messages for web page.
|
||||
* This must be called before doing anything else.
|
||||
|
@ -97,6 +105,7 @@ export default class MessagesPuppeteer {
|
|||
id => this.sentMessageIDs.add(id))
|
||||
await this.page.exposeFunction("__mautrixReceiveChanges",
|
||||
this._receiveChatListChanges.bind(this))
|
||||
await this.page.exposeFunction("__mautrixShowParticipantsList", this._showParticipantList.bind(this))
|
||||
await this.page.exposeFunction("__chronoParseDate", chrono.parseDate)
|
||||
|
||||
// NOTE Must *always* re-login on a browser session, so no need to check if already logged in
|
||||
|
@ -204,11 +213,12 @@ export default class MessagesPuppeteer {
|
|||
return value
|
||||
}),
|
||||
() => this.page.waitForSelector("#login_incorrect", {visible: true, timeout: 2000})
|
||||
.then(value => this.page.evaluate(element => element.innerText, value)),
|
||||
.then(value => this.page.evaluate(_getInnerText, value)),
|
||||
() => this._waitForLoginCancel(),
|
||||
].map(promiseFn => cancelableResolve(promiseFn)))
|
||||
|
||||
this.log("Removing observers")
|
||||
await this.page.evaluate(ownID => window.__mautrixController.setOwnID(ownID), this.id)
|
||||
await this.page.evaluate(() => window.__mautrixController.removeQRChangeObserver())
|
||||
await this.page.evaluate(() => window.__mautrixController.removeQRAppearObserver())
|
||||
await this.page.evaluate(() => window.__mautrixController.removeEmailAppearObserver())
|
||||
|
@ -402,68 +412,99 @@ export default class MessagesPuppeteer {
|
|||
}
|
||||
|
||||
async _switchChat(id) {
|
||||
// TODO Allow passing in an element directly
|
||||
this.log(`Switching to chat ${id}`)
|
||||
const chatListItem = await this.page.$(this._listItemSelector(id))
|
||||
await chatListItem.click()
|
||||
|
||||
const chatHeader = await this.page.waitForSelector("#_chat_header_area > .mdRGT04Link")
|
||||
const chatListInfo = await chatListItem.evaluate(
|
||||
(e, id) => window.__mautrixController.parseChatListItem(e, id),
|
||||
id)
|
||||
const chatName = await chatListItem.evaluate(
|
||||
element => window.__mautrixController.getChatListItemName(element))
|
||||
|
||||
this.log(`Waiting for chat header title to be "${chatListInfo.name}"`)
|
||||
const chatHeaderTitleElement = await chatHeader.$(".mdRGT04Ttl")
|
||||
await this.page.waitForFunction(
|
||||
(element, targetText) => element.innerText == targetText,
|
||||
{},
|
||||
chatHeaderTitleElement, chatListInfo.name)
|
||||
const isCorrectChatVisible = (targetText) => {
|
||||
const chatHeader = document.querySelector("#_chat_header_area > .mdRGT04Link")
|
||||
if (!chatHeader) return false
|
||||
const chatHeaderTitleElement = chatHeader.querySelector(".mdRGT04Ttl")
|
||||
return chatHeaderTitleElement.innerText == targetText
|
||||
}
|
||||
|
||||
return [chatListItem, chatListInfo, chatHeader]
|
||||
if (await this.page.evaluate(isCorrectChatVisible, chatName)) {
|
||||
this.log("Already viewing chat, no need to switch")
|
||||
} else {
|
||||
await chatListItem.click()
|
||||
this.log(`Waiting for chat header title to be "${chatName}"`)
|
||||
await this.page.waitForFunction(
|
||||
isCorrectChatVisible,
|
||||
{polling: "mutation"},
|
||||
chatName)
|
||||
|
||||
// For consistent behaviour later, wait for the chat details sidebar to be hidden
|
||||
await this.page.waitForFunction(
|
||||
detailArea => detailArea.childElementCount == 0,
|
||||
{},
|
||||
await this.page.$("#_chat_detail_area"))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Commonize
|
||||
async getParticipantList() {
|
||||
await this._showParticipantList()
|
||||
return await this.page.$("#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul")
|
||||
}
|
||||
|
||||
async _showParticipantList() {
|
||||
const selector = "#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul"
|
||||
let participantList = await this.page.$(selector)
|
||||
if (!participantList) {
|
||||
this.log("Participant list hidden, so clicking chat header to show it")
|
||||
await this.page.click("#_chat_header_area > .mdRGT04Link")
|
||||
participantList = await this.page.waitForSelector(selector)
|
||||
}
|
||||
//return participantList
|
||||
}
|
||||
|
||||
async _getChatInfoUnsafe(id) {
|
||||
const chatListItem = await this.page.$(this._listItemSelector(id))
|
||||
const chatListInfo = await chatListItem.evaluate(
|
||||
(element, id) => window.__mautrixController.parseChatListItem(element, id),
|
||||
id)
|
||||
|
||||
let [isDirect, isGroup, isRoom] = [false,false,false]
|
||||
switch (id.charAt(0)) {
|
||||
case 'u':
|
||||
case "u":
|
||||
isDirect = true
|
||||
break
|
||||
case 'c':
|
||||
case "c":
|
||||
isGroup = true
|
||||
break
|
||||
case 'r':
|
||||
case "r":
|
||||
isRoom = true
|
||||
break
|
||||
}
|
||||
|
||||
// TODO This will mark the chat as "read"!
|
||||
const [chatListItem, chatListInfo, chatHeader] = await this._switchChat(id)
|
||||
|
||||
let participants
|
||||
if (isGroup || isRoom) {
|
||||
if (!isDirect) {
|
||||
this.log("Found multi-user chat, so clicking chat header to get participants")
|
||||
await chatHeader.click()
|
||||
const participantList = await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul")
|
||||
if (isGroup) {
|
||||
// TODO Is a group not actually created until a message is sent(?)
|
||||
// If so, maybe don't create a portal until there is a message.
|
||||
participants = await participantList.evaluate(
|
||||
elem => window.__mautrixController.parseParticipantList(elem))
|
||||
} else if (isRoom) {
|
||||
this.log("TODO: Room participant lists don't have user IDs...")
|
||||
participants = []
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO This will mark the chat as "read"!
|
||||
await this._switchChat(id)
|
||||
const participantList = await this.getParticipantList()
|
||||
// TODO Is a group not actually created until a message is sent(?)
|
||||
// If so, maybe don't create a portal until there is a message.
|
||||
participants = await participantList.evaluate(
|
||||
element => window.__mautrixController.parseParticipantList(element))
|
||||
} else {
|
||||
this.log(`Found direct chat with ${id}`)
|
||||
//const chatDetailArea = await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info")
|
||||
//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
|
||||
name: chatListInfo.name,
|
||||
}]
|
||||
}
|
||||
|
||||
this.log("Found participants:")
|
||||
for (const participant of participants) {
|
||||
this.log(participant)
|
||||
}
|
||||
return {participants, ...chatListInfo}
|
||||
}
|
||||
|
||||
|
@ -504,7 +545,7 @@ export default class MessagesPuppeteer {
|
|||
await this._switchChat(id)
|
||||
this.log("Waiting for messages to load")
|
||||
const messages = await this.page.evaluate(
|
||||
() => window.__mautrixController.parseMessageList())
|
||||
id => window.__mautrixController.parseMessageList(id), id)
|
||||
return messages.filter(msg => msg.id > minID && !this.sentMessageIDs.has(msg.id))
|
||||
}
|
||||
|
||||
|
@ -582,7 +623,7 @@ export default class MessagesPuppeteer {
|
|||
|
||||
_sendLoginFailure(reason) {
|
||||
this.loginRunning = false
|
||||
this.error(`Login failure: ${reason ? reason : 'cancelled'}`)
|
||||
this.error(`Login failure: ${reason ? reason : "cancelled"}`)
|
||||
if (this.client) {
|
||||
this.client.sendFailure(reason).catch(err =>
|
||||
this.error("Failed to send failure reason to client:", err))
|
||||
|
|
Loading…
Reference in New Issue