Too many fixes

This commit is contained in:
Andrew Ferrazzutti 2021-05-28 02:27:14 -04:00
parent 1e8c64a31a
commit 3cca9f9606
11 changed files with 756 additions and 389 deletions

View File

@ -52,7 +52,7 @@
* [x] Rooms (unnamed chats / "multi-user direct chats")
* [ ] Membership actions
* [x] Add member
* [x] Remove member
* [ ] Remove member
* [ ] Block
* Misc
* [x] Automatic portal creation

View File

@ -38,3 +38,5 @@ An easy way to do so is to install `xvfb` from your distribution, and run the Pu
# Upgrading
Simply `git pull` or `git rebase` the latest changes, and rerun any installation commands (`yarn --production`, `pip install -Ur ...`).
To upgrade the LINE extension used by Puppeteer, simply download and extract the latest .crx in the same location as for initial setup.

View File

@ -73,7 +73,10 @@ class Config(BaseBridgeConfig):
copy("bridge.delivery_receipts")
copy("bridge.delivery_error_reports")
copy("bridge.resend_bridge_info")
copy("bridge.command_prefix")
copy("bridge.receive_stickers")
copy("bridge.resend_bridge_info")
copy("bridge.use_sticker_events")
copy("bridge.emoji_scale_factor")
copy("bridge.user")
copy("puppeteer.connection.type")

View File

@ -29,12 +29,14 @@ class Media:
media_id: str
mxc: ContentURI
# TODO Consider whether mime_type, file_name, and size are needed.
mime_type: str
file_name: str
size: int
async def insert(self) -> None:
q = ("INSERT INTO media (media_id, mxc) "
"VALUES ($1, $2)")
await self.db.execute(q, self.media_id, self.mxc)
q = ("INSERT INTO media (media_id, mxc, mime_type, file_name, size) "
"VALUES ($1, $2, $3, $4, $5)")
await self.db.execute(q, self.media_id, self.mxc, self.mime_type, self.file_name, self.size)
async def update(self) -> None:
q = ("UPDATE media SET mxc=$2 "
@ -42,8 +44,8 @@ class Media:
await self.db.execute(q, self.media_id, self.mxc)
@classmethod
async def get_by_id(cls, media_id: str) -> Optional[ContentURI]:
q = ("SELECT media_id, mxc "
async def get_by_id(cls, media_id: str) -> Optional['DBMedia']:
q = ("SELECT media_id, mxc, mime_type, file_name, size "
"FROM media WHERE media_id=$1")
row = await cls.db.fetchrow(q, media_id)
if not row:

View File

@ -103,4 +103,13 @@ async def upgrade_read_receipts(conn: Connection) -> None:
FOREIGN KEY (mx_room)
REFERENCES portal (mxid)
ON DELETE CASCADE
)""")
)""")
@upgrade_table.register(description="Media metadata")
async def upgrade_deduplicate_blob(conn: Connection) -> None:
await conn.execute("""ALTER TABLE media
ADD COLUMN IF NOT EXISTS mime_type TEXT,
ADD COLUMN IF NOT EXISTS file_name TEXT,
ADD COLUMN IF NOT EXISTS size INTEGER
""")

View File

@ -131,6 +131,12 @@ bridge:
# This field will automatically be changed back to false after it,
# except if the config file is not writable.
resend_bridge_info: false
# Set this to false to disable bridging stickers and emoji.
receive_stickers: true
# Set this to false to use m.image events for stickers instead of m.sticker.
use_sticker_events: true
# The scale by which to display emojis with. Must be a positive integer.
emoji_scale_factor: 1
# The prefix for commands. Only required in non-management rooms.
command_prefix: "!line"

View File

@ -29,14 +29,14 @@ from mautrix.types import (EventID, MessageEventContent, RoomID, EventType, Mess
TextMessageEventContent, MediaMessageEventContent, Membership, Format,
ContentURI, EncryptedFile, ImageInfo,
RelatesTo, RelationType)
from mautrix.errors import IntentError, MatrixError
from mautrix.errors import IntentError
from mautrix.util.simple_lock import SimpleLock
from .db import Portal as DBPortal, Message as DBMessage, ReceiptReaction as DBReceiptReaction, Media as DBMedia
from .config import Config
from .rpc import ChatInfo, Participant, Message, Receipt, Client, PathImage
from .rpc.types import RPCError
from . import user as u, puppet as p, matrix as m
from . import user as u, puppet as p
if TYPE_CHECKING:
from .__main__ import MessagesBridge
@ -48,9 +48,9 @@ except ImportError:
StateBridge = EventType.find("m.bridge", EventType.Class.STATE)
StateHalfShotBridge = EventType.find("uk.half-shot.bridge", EventType.Class.STATE)
ReuploadedMediaInfo = NamedTuple('ReuploadedMediaInfo', mxc=Optional[ContentURI],
decryption_info=Optional[EncryptedFile],
mime_type=str, file_name=str, size=int)
MediaInfo = NamedTuple('MediaInfo', mxc=Optional[ContentURI],
decryption_info=Optional[EncryptedFile],
mime_type=str, file_name=str, size=int)
class Portal(DBPortal, BasePortal):
@ -112,6 +112,7 @@ class Portal(DBPortal, BasePortal):
cls.loop = bridge.loop
cls.bridge = bridge
cls.invite_own_puppet_to_pm = cls.config["bridge.invite_own_puppet_to_pm"]
cls.emoji_scale_factor = max(int(cls.config["bridge.emoji_scale_factor"]), 1)
NotificationDisabler.puppet_cls = p.Puppet
NotificationDisabler.config_enabled = cls.config["bridge.backfill.disable_notifications"]
@ -170,7 +171,7 @@ class Portal(DBPortal, BasePortal):
self.log.debug(f"Handled Matrix message {event_id} -> {message_id}")
except UniqueViolationError as e:
self.log.warning(f"Failed to handle Matrix message {event_id} -> {message_id}: {e}")
if not msg:
if not msg and self.config["bridge.delivery_error_reports"]:
await self.main_intent.send_notice(
self.mxid,
"Posting this message to LINE may have failed.",
@ -179,12 +180,6 @@ class Portal(DBPortal, BasePortal):
async def handle_matrix_leave(self, user: 'u.User') -> None:
self.log.info(f"{user.mxid} left portal to {self.chat_id}, "
f"cleaning up and deleting...")
if self.invite_own_puppet_to_pm:
# TODO Use own puppet instead of bridge bot. Then cleanup_and_delete will handle it
try:
await self.az.intent.leave_room(self.mxid)
except MatrixError:
pass
await self.cleanup_and_delete()
async def _bridge_own_message_pm(self, source: 'u.User', sender: Optional['p.Puppet'], mid: str,
@ -234,15 +229,38 @@ class Portal(DBPortal, BasePortal):
self.log.debug(f"Ignoring duplicate message {evt.id}")
return
event_id = None
if evt.image_url:
# TODO Deduplicate stickers, but only if encryption is disabled
content = await self._handle_remote_photo(source, intent, evt)
if not content:
content = TextMessageEventContent(
msgtype=MessageType.NOTICE,
body="<unbridgeable media>")
event_id = await self._send_message(intent, content, timestamp=evt.timestamp)
if evt.image and evt.image.url:
if not evt.image.is_sticker or self.config["bridge.receive_stickers"]:
media_info = await self._handle_remote_media(
source, intent, evt.image.url,
deduplicate=not self.encrypted and evt.image.is_sticker)
image_info = ImageInfo(
# Element Web doesn't animate PNGs, but setting the mimetype to GIF works.
# (PNG stickers never animate, and PNG images only animate after being clicked on.)
# Making this exception since E.W. seems to be the only client that supports inline animated stickers & images.
# TODO Open an E.W. issue for this
# TODO Test Element Android
# TODO Find & test other non-GIF formats for animated images
mimetype="image/gif" if evt.image.is_animated and media_info.mime_type == "image/png" else media_info.mime_type,
size=media_info.size) if media_info else None
else:
media_info = None
send_sticker = self.config["bridge.use_sticker_events"] and evt.image.is_sticker and not self.encrypted and media_info
if send_sticker:
event_id = await intent.send_sticker(
self.mxid, media_info.mxc, image_info, "<sticker>", timestamp=evt.timestamp)
else:
if media_info:
content = MediaMessageEventContent(
url=media_info.mxc, file=media_info.decryption_info,
msgtype=MessageType.IMAGE,
body=media_info.file_name,
info=image_info)
else:
content = TextMessageEventContent(
msgtype=MessageType.NOTICE,
body=f"<{'sticker' if evt.image.is_sticker else 'image'}>")
event_id = await self._send_message(intent, content, timestamp=evt.timestamp)
elif evt.html and not evt.html.isspace():
chunks = []
@ -251,12 +269,11 @@ class Portal(DBPortal, BasePortal):
chunks.append({"type": "data", "data": data})
def handle_starttag(tag, attrs):
if tag == "img":
obj = {"type": tag}
for attr in attrs:
obj[attr[0]] = attr[1]
nonlocal chunks
chunks.append(obj)
obj = {"type": tag}
for attr in attrs:
obj[attr[0]] = attr[1]
nonlocal chunks
chunks.append(obj)
parser = HTMLParser()
parser.handle_data = handle_data
@ -268,11 +285,17 @@ class Portal(DBPortal, BasePortal):
for chunk in chunks:
ctype = chunk["type"]
if ctype == "data":
if ctype == "br":
msg_text += "\n"
if not msg_html:
msg_html = msg_text
msg_html += "<br>"
elif ctype == "data":
msg_text += chunk["data"]
if msg_html:
msg_html += chunk["data"]
elif ctype == "img":
height = int(chunk.get("height", 19)) * self.emoji_scale_factor
cclass = chunk["class"]
if cclass == "emojione":
alt = chunk["alt"]
@ -283,11 +306,11 @@ class Portal(DBPortal, BasePortal):
media_id = f'{chunk.get("data-stickon-pkg-cd", 0)}/{chunk.get("data-stickon-stk-cd", 0)}'
# NOTE Not encrypting content linked to by HTML tags
if not self.encrypted:
media_mxc = await self._get_mxc_for_remote_media(source, intent, chunk["src"], media_id)
if not self.encrypted and self.config["bridge.receive_stickers"]:
media_info = await self._handle_remote_media(source, intent, chunk["src"], media_id, deduplicate=True)
if not msg_html:
msg_html = msg_text
msg_html += f'<img data-mx-emoticon src="{media_mxc}" alt="{alt}" title="{alt}" height="32">'
msg_html += f'<img data-mx-emoticon src="{media_info.mxc}" alt="{alt}" title="{alt}" height="{height}">'
msg_text += alt
content = TextMessageEventContent(
@ -295,16 +318,21 @@ class Portal(DBPortal, BasePortal):
format=Format.HTML if msg_html else None,
body=msg_text, formatted_body=msg_html)
event_id = await self._send_message(intent, content, timestamp=evt.timestamp)
if event_id:
if evt.is_outgoing and evt.receipt_count:
await self._handle_receipt(event_id, evt.receipt_count)
msg = DBMessage(mxid=event_id, mx_room=self.mxid, mid=evt.id, chat_id=self.chat_id)
try:
await msg.insert()
await self._send_delivery_receipt(event_id)
self.log.debug(f"Handled remote message {evt.id} -> {event_id}")
except UniqueViolationError as e:
self.log.debug(f"Failed to handle remote message {evt.id} -> {event_id}: {e}")
else:
content = TextMessageEventContent(
msgtype=MessageType.NOTICE,
body="<Unbridgeable message>")
event_id = await self._send_message(intent, content, timestamp=evt.timestamp)
if evt.is_outgoing and evt.receipt_count:
await self._handle_receipt(event_id, evt.receipt_count)
msg = DBMessage(mxid=event_id, mx_room=self.mxid, mid=evt.id, chat_id=self.chat_id)
try:
await msg.insert()
await self._send_delivery_receipt(event_id)
self.log.debug(f"Handled remote message {evt.id} -> {event_id}")
except UniqueViolationError as e:
self.log.debug(f"Failed to handle remote message {evt.id} -> {event_id}: {e}")
async def handle_remote_receipt(self, receipt: Receipt) -> None:
msg = await DBMessage.get_by_mid(receipt.id)
@ -321,46 +349,44 @@ class Portal(DBPortal, BasePortal):
if reaction:
await self.main_intent.redact(self.mxid, reaction.mxid)
await reaction.delete()
# TODO Not just -1 if there are multiple _OWN_ puppets...
if receipt_count == len(self._last_participant_update) - 1:
for participant in self._last_participant_update:
puppet = await p.Puppet.get_by_mid(participant.id)
for participant in filter(lambda participant: not p.Puppet.is_mid_for_own_puppet(participant), self._last_participant_update):
puppet = await p.Puppet.get_by_mid(participant)
await puppet.intent.send_receipt(self.mxid, event_id)
else:
# TODO Translatable string for "Read by"
reaction_mxid = await self.main_intent.react(self.mxid, event_id, f"(Read by {receipt_count})")
await DBReceiptReaction(reaction_mxid, self.mxid, event_id, receipt_count).insert()
async def _handle_remote_photo(self, source: 'u.User', intent: IntentAPI, message: Message
) -> Optional[MediaMessageEventContent]:
try:
resp = await source.client.read_image(message.image_url)
except (RPCError, TypeError) as e:
self.log.warning(f"Failed to download remote photo from chat {self.chat_id}: {e}")
return None
media_info = await self._reupload_remote_media(resp.data, intent, resp.mime)
return MediaMessageEventContent(url=media_info.mxc, file=media_info.decryption_info,
msgtype=MessageType.IMAGE, body=media_info.file_name,
info=ImageInfo(mimetype=media_info.mime_type, size=media_info.size))
async def _get_mxc_for_remote_media(self, source: 'u.User', intent: IntentAPI,
media_url: str, media_id: Optional[str] = None
) -> ContentURI:
async def _handle_remote_media(self, source: 'u.User', intent: IntentAPI,
media_url: str, media_id: Optional[str] = None,
deduplicate: bool = False) -> MediaInfo:
if not media_id:
media_id = media_url
media_info = await DBMedia.get_by_id(media_id)
if not media_info:
self.log.debug(f"Did not find existing mxc URL for {media_id}, uploading media now")
resp = await source.client.read_image(media_url)
media_info = await self._reupload_remote_media(resp.data, intent, resp.mime, disable_encryption=True)
await DBMedia(media_id=media_id, mxc=media_info.mxc).insert()
self.log.debug(f"Uploaded media as {media_info.mxc}")
db_media_info = await DBMedia.get_by_id(media_id) if deduplicate else None
if not db_media_info:
# NOTE Blob URL of stickers only persists for a single session...still better than nothing.
self.log.debug(f"{'Did not find existing mxc URL for' if deduplicate else 'Not deduplicating'} {media_id}, uploading media now")
try:
resp = await source.client.read_image(media_url)
except (RPCError, TypeError) as e:
self.log.warning(f"Failed to download remote media from chat {self.chat_id}: {e}")
return None
media_info = await self._reupload_remote_media(resp.data, intent, resp.mime, disable_encryption=deduplicate)
if deduplicate:
await DBMedia(
media_id=media_id, mxc=media_info.mxc,
size=media_info.size, mime_type=media_info.mime_type, file_name=media_info.file_name
).insert()
return media_info
else:
self.log.debug(f"Found existing mxc URL for {media_id}: {media_info.mxc}")
return media_info.mxc
self.log.debug(f"Found existing mxc URL for {media_id}: {db_media_info.mxc}")
return MediaInfo(db_media_info.mxc, None, db_media_info.mime_type, db_media_info.file_name, db_media_info.size)
async def _reupload_remote_media(self, data: bytes, intent: IntentAPI,
mime_type: str = None, file_name: str = None,
disable_encryption: bool = True) -> ReuploadedMediaInfo:
disable_encryption: bool = True) -> MediaInfo:
if not mime_type:
mime_type = magic.from_buffer(data, mime=True)
upload_mime_type = mime_type
@ -378,10 +404,13 @@ class Portal(DBPortal, BasePortal):
filename=upload_file_name)
if decryption_info:
self.log.debug(f"Uploaded encrypted media as {mxc}")
decryption_info.url = mxc
mxc = None
else:
self.log.debug(f"Uploaded media as {mxc}")
return ReuploadedMediaInfo(mxc, decryption_info, mime_type, file_name, len(data))
return MediaInfo(mxc, decryption_info, mime_type, file_name, len(data))
async def update_info(self, conv: ChatInfo, client: Optional[Client]) -> None:
if self.is_direct:
@ -449,10 +478,10 @@ class Portal(DBPortal, BasePortal):
# Make sure puppets who should be here are here
for participant in participants:
puppet = await p.Puppet.get_by_mid(participant.id)
if forbid_own_puppets and p.Puppet.is_mid_for_own_puppet(participant.id):
continue
await puppet.intent.ensure_joined(self.mxid)
intent = (await p.Puppet.get_by_mid(participant.id)).intent
await intent.ensure_joined(self.mxid)
print(current_members)
@ -599,9 +628,11 @@ class Portal(DBPortal, BasePortal):
"users": {
self.az.bot_mxid: 100,
self.main_intent.mxid: 100,
source.mxid: 25,
},
"events": {
str(EventType.REACTION): 1
str(EventType.REACTION): 100,
str(EventType.ROOM_ENCRYPTION): 25,
}
}
})

View File

@ -51,6 +51,13 @@ class ChatInfo(ChatListInfo, SerializableAttrs['ChatInfo']):
participants: List[Participant]
@dataclass
class MessageImage(SerializableAttrs['MessageImage']):
url: str
is_sticker: bool
is_animated: bool
@dataclass
class Message(SerializableAttrs['Message']):
id: int
@ -59,7 +66,7 @@ class Message(SerializableAttrs['Message']):
sender: Optional[Participant]
timestamp: int = None
html: Optional[str] = None
image_url: Optional[str] = None
image: Optional[MessageImage] = None
receipt_count: Optional[int] = None

View File

@ -64,8 +64,9 @@ class User(DBUser, BaseUser):
cls.loop = bridge.loop
Client.config = bridge.config
async def send_notice(self, text) -> None:
async def send_bridge_notice(self, text) -> None:
if self.notice_room:
self.log.debug(f"Sending bridge notice: {text}")
await self.az.intent.send_notice(self.notice_room, text)
async def is_logged_in(self) -> bool:
@ -96,17 +97,17 @@ class User(DBUser, BaseUser):
self.loop.create_task(self.connect_double_puppet())
self.client = Client(self.mxid)
self.log.debug("Starting client")
await self.send_notice("Starting up...")
await self.send_bridge_notice("Starting up...")
state = await self.client.start()
await self.client.on_message(self.handle_message)
await self.client.on_receipt(self.handle_receipt)
if state.is_connected:
self._track_metric(METRIC_CONNECTED, True)
if state.is_logged_in:
await self.send_notice("Already logged in to LINE")
await self.send_bridge_notice("Already logged in to LINE")
self.loop.create_task(self._try_sync())
else:
await self.send_notice("Ready to log in to LINE")
await self.send_bridge_notice("Ready to log in to LINE")
async def _try_sync(self) -> None:
try:
@ -123,22 +124,24 @@ class User(DBUser, BaseUser):
if self._connection_check_task:
self._connection_check_task.cancel()
self._connection_check_task = self.loop.create_task(self._check_connection_loop())
await self.client.set_last_message_ids(await DBMessage.get_max_mids())
self.log.info("Syncing chats")
await self.send_notice("Synchronizing chats...")
await self.client.pause()
chats = await self.client.get_chats()
await self.client.set_last_message_ids(await DBMessage.get_max_mids())
limit = self.config["bridge.initial_conversation_sync"]
self.log.info("Syncing chats")
await self.send_bridge_notice("Synchronizing chats...")
chats = await self.client.get_chats()
num_created = 0
for index, chat in enumerate(chats):
portal = await po.Portal.get_by_chat_id(chat.id, create=True)
if portal.mxid or index < limit:
if portal.mxid or num_created < limit:
chat = await self.client.get_chat(chat.id)
if portal.mxid:
await portal.update_matrix_room(self, chat)
else:
await portal.create_matrix_room(self, chat)
num_created += 1
await self.send_bridge_notice("Synchronization complete")
await self.client.resume()
await self.send_notice("Synchronization complete")
async def stop(self) -> None:
# TODO Notices for shutdown messages

File diff suppressed because it is too large Load Diff

View File

@ -97,7 +97,6 @@ export default class MessagesPuppeteer {
await this.page.exposeFunction("__mautrixReceiveQR", this._receiveQRChange.bind(this))
await this.page.exposeFunction("__mautrixSendEmailCredentials", this._sendEmailCredentials.bind(this))
await this.page.exposeFunction("__mautrixReceivePIN", this._receivePIN.bind(this))
await this.page.exposeFunction("__mautrixExpiry", this._receiveExpiry.bind(this))
await this.page.exposeFunction("__mautrixReceiveMessageID",
id => this.sentMessageIDs.add(id))
await this.page.exposeFunction("__mautrixReceiveChanges",
@ -108,7 +107,6 @@ export default class MessagesPuppeteer {
this._receiveReceiptDirectLatest.bind(this))
await this.page.exposeFunction("__mautrixReceiveReceiptMulti",
this._receiveReceiptMulti.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
@ -126,6 +124,7 @@ export default class MessagesPuppeteer {
}
this.log("Injecting content script")
await this.page.addScriptTag({ path: "./src/contentscript.js", type: "module" })
}
/**
@ -183,9 +182,7 @@ export default class MessagesPuppeteer {
await this.page.evaluate(
element => window.__mautrixController.addPINAppearObserver(element), loginContentArea)
await this.page.$eval("#layer_contents",
element => window.__mautrixController.addExpiryObserver(element))
this.log("Waiting for login response")
let doneWaiting = false
let loginSuccess = false
@ -226,7 +223,6 @@ export default class MessagesPuppeteer {
await this.page.evaluate(() => window.__mautrixController.removeQRAppearObserver())
await this.page.evaluate(() => window.__mautrixController.removeEmailAppearObserver())
await this.page.evaluate(() => window.__mautrixController.removePINAppearObserver())
await this.page.evaluate(() => window.__mautrixController.removeExpiryObserver())
delete this.login_email
delete this.login_password
@ -252,7 +248,16 @@ export default class MessagesPuppeteer {
}
this.loginRunning = false
await this.startObserving()
// Don't start observing yet, instead wait for explicit request.
// But at least view the most recent chat.
try {
let mostRecentChatID = await this.page.$eval("#_chat_list_body li",
element => window.getChatListItemID(element))
await this._switchChat(mostRecentChatID)
this.log("Focused on most recent chat")
} catch (e) {
this.log("No chats available to focus on")
}
this.log("Login complete")
}
@ -380,15 +385,7 @@ export default class MessagesPuppeteer {
* @return {Promise<[MessageData]>} - The messages visible in the chat.
*/
async getMessages(chatID) {
return await this.taskQueue.push(async () => {
const messages = await this._getMessagesUnsafe(chatID)
if (messages.length > 0) {
for (const message of messages) {
message.chat_id = chatID
}
}
return messages
})
return await this.taskQueue.push(async () => this._getMessagesUnsafe(chatID))
}
setLastMessageIDs(ids) {
@ -396,7 +393,8 @@ export default class MessagesPuppeteer {
for (const [chatID, messageID] of Object.entries(ids)) {
this.mostRecentMessages.set(chatID, messageID)
}
this.log("Updated most recent message ID map:", this.mostRecentMessages)
this.log("Updated most recent message ID map:")
this.log(this.mostRecentMessages)
}
async readImage(imageUrl) {
@ -411,11 +409,15 @@ export default class MessagesPuppeteer {
}
async startObserving() {
this.log("Adding observers")
const chatID = await this.page.evaluate(() => window.__mautrixController.getCurrentChatID())
this.log(`Adding observers for ${chatID || "empty chat"}`)
await this.page.evaluate(
() => window.__mautrixController.addChatListObserver())
await this.page.evaluate(
() => window.__mautrixController.addMsgListObserver())
if (chatID) {
await this.page.evaluate(
(mostRecentMessage) => window.__mautrixController.addMsgListObserver(mostRecentMessage),
this.mostRecentMessages.get(chatID))
}
}
async stopObserving() {
@ -448,9 +450,10 @@ export default class MessagesPuppeteer {
if (await this.page.evaluate(isCorrectChatVisible, chatName)) {
this.log("Already viewing chat, no need to switch")
} else {
this.log("Switching chat, so remove msg list observer")
this.log("Ensuring msg list observer is removed")
const hadMsgListObserver = await this.page.evaluate(
() => window.__mautrixController.removeMsgListObserver())
this.log(hadMsgListObserver ? "Observer was already removed" : "Removed observer")
await chatListItem.click()
this.log(`Waiting for chat header title to be "${chatName}"`)
@ -459,39 +462,28 @@ export default class MessagesPuppeteer {
{polling: "mutation"},
chatName)
// For consistent behaviour later, wait for the chat details sidebar to be hidden
// Always show the chat details sidebar, as this makes life easier
this.log("Waiting for detail area to be auto-hidden upon entering chat")
await this.page.waitForFunction(
detailArea => detailArea.childElementCount == 0,
{},
await this.page.$("#_chat_detail_area"))
this.log("Clicking chat header to show detail area")
await this.page.click("#_chat_header_area > .mdRGT04Link")
this.log("Waiting for detail area")
await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info")
if (hadMsgListObserver) {
this.log("Restoring msg list observer")
await this.page.evaluate(
() => window.__mautrixController.addMsgListObserver())
(mostRecentMessage) => window.__mautrixController.addMsgListObserver(mostRecentMessage),
this.mostRecentMessages.get(chatID))
} else {
this.log("Not restoring msg list observer, as there never was one")
}
}
}
// 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")
// Use no timeout since the browser itself is using this
await this.page.waitForSelector(selector, {timeout: 0})
}
}
async _getChatInfoUnsafe(chatID) {
const chatListItem = await this.page.$(this._listItemSelector(chatID))
const chatListInfo = await chatListItem.evaluate(
@ -516,7 +508,7 @@ export default class MessagesPuppeteer {
this.log("Found multi-user chat, so clicking chat header to get participants")
// TODO This will mark the chat as "read"!
await this._switchChat(chatID)
const participantList = await this._getParticipantList()
const participantList = await this.page.$("#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul")
// 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(
@ -546,6 +538,7 @@ export default class MessagesPuppeteer {
async _sendMessageUnsafe(chatID, text) {
await this._switchChat(chatID)
// TODO Initiate the promise in the content script
await this.page.evaluate(
() => window.__mautrixController.promiseOwnMessage(5000, "time"))
@ -597,15 +590,12 @@ export default class MessagesPuppeteer {
}
}
_receiveMessages(chatID, messages) {
async _receiveMessages(chatID, messages) {
if (this.client) {
messages = this._filterMessages(chatID, messages)
if (messages.length > 0) {
for (const message of messages) {
message.chat_id = chatID
this.client.sendMessage(message).catch(err =>
this.error("Failed to send message", message.id, "to client:", err))
}
messages = await this._processMessages(chatID, messages)
for (const message of messages) {
this.client.sendMessage(message).catch(err =>
this.error("Failed to send message", message.id, "to client:", err))
}
} else {
this.log("No client connected, not sending messages")
@ -613,27 +603,52 @@ export default class MessagesPuppeteer {
}
async _getMessagesUnsafe(chatID) {
// TODO Also handle "decrypting" state
// TODO Consider making a wrapper for pausing/resuming the msg list observers
this.log("Ensuring msg list observer is removed")
const hadMsgListObserver = await this.page.evaluate(
() => window.__mautrixController.removeMsgListObserver())
this.log(hadMsgListObserver ? "Observer was already removed" : "Removed observer")
// TODO Handle unloaded messages. Maybe scroll up
// TODO This will mark the chat as "read"!
await this._switchChat(chatID)
const messages = await this.page.evaluate(() =>
window.__mautrixController.parseMessageList())
return this._filterMessages(chatID, messages)
const messages = await this.page.evaluate(
mostRecentMessage => window.__mautrixController.parseMessageList(mostRecentMessage),
this.mostRecentMessages.get(chatID))
// Doing this before restoring the observer since it updates minID
const filteredMessages = await this._processMessages(chatID, messages)
if (hadMsgListObserver) {
this.log("Restoring msg list observer")
await this.page.evaluate(
mostRecentMessage => window.__mautrixController.addMsgListObserver(mostRecentMessage),
this.mostRecentMessages.get(chatID))
} else {
this.log("Not restoring msg list observer, as there never was one")
}
return filteredMessages
}
_filterMessages(chatID, messages) {
async _processMessages(chatID, messages) {
// TODO Probably don't need minID filtering if Puppeteer context handles it now
const minID = this.mostRecentMessages.get(chatID) || 0
const filtered_messages = messages.filter(msg => msg.id > minID && !this.sentMessageIDs.has(msg.id))
const filteredMessages = messages.filter(msg => msg.id > minID && !this.sentMessageIDs.has(msg.id))
if (filtered_messages.length > 0) {
const newFirstID = filtered_messages[0].id
const newLastID = filtered_messages[filtered_messages.length - 1].id
if (filteredMessages.length > 0) {
const newFirstID = filteredMessages[0].id
const newLastID = filteredMessages[filteredMessages.length - 1].id
this.mostRecentMessages.set(chatID, newLastID)
const range = newFirstID === newLastID ? newFirstID : `${newFirstID}-${newLastID}`
this.log(`Loaded ${messages.length} messages in ${chatID}, got ${filtered_messages.length} newer than ${minID} (${range})`)
this.log(`Loaded ${messages.length} messages in ${chatID}, got ${filteredMessages.length} newer than ${minID} (${range})`)
for (const message of filteredMessages) {
message.chat_id = chatID
}
return filteredMessages
} else {
return []
}
return filtered_messages
}
async _processChatListChangeUnsafe(chatID) {
@ -647,7 +662,6 @@ export default class MessagesPuppeteer {
if (this.client) {
for (const message of messages) {
message.chat_id = chatID
await this.client.sendMessage(message).catch(err =>
this.error("Failed to send message", message.id, "to client:", err))
}
@ -739,10 +753,4 @@ export default class MessagesPuppeteer {
this.log("No client connected, not sending failure reason")
}
}
async _receiveExpiry(button) {
this.log("Something expired, clicking OK button to continue")
this.page.click(button).catch(err =>
this.error("Failed to dismiss expiry dialog:", err))
}
}