forked from fair/matrix-puppeteer-line
Compare commits
No commits in common. "968832545ed5bb51bccd423be720d9826055de1b" and "9d4f2dafac6c7ab0cfcecb1e9c27f8c07012d88b" have entirely different histories.
968832545e
...
9d4f2dafac
|
@ -1,6 +1,4 @@
|
||||||
/.idea/
|
/.idea/
|
||||||
/.*project
|
|
||||||
/.settings/
|
|
||||||
|
|
||||||
/.venv
|
/.venv
|
||||||
/env/
|
/env/
|
||||||
|
@ -15,8 +13,8 @@ __pycache__
|
||||||
profiles
|
profiles
|
||||||
puppet/extension_files
|
puppet/extension_files
|
||||||
|
|
||||||
/config*.yaml
|
/config.yaml
|
||||||
/registration*.yaml
|
/registration.yaml
|
||||||
*.log*
|
*.log*
|
||||||
*.db
|
*.db
|
||||||
*.pickle
|
*.pickle
|
||||||
|
|
|
@ -29,18 +29,20 @@ class Message:
|
||||||
|
|
||||||
mxid: EventID
|
mxid: EventID
|
||||||
mx_room: RoomID
|
mx_room: RoomID
|
||||||
mid: Optional[int]
|
mid: int
|
||||||
chat_id: str
|
chat_id: str
|
||||||
|
|
||||||
async def insert(self) -> None:
|
async def insert(self) -> None:
|
||||||
q = "INSERT INTO message (mxid, mx_room, mid, chat_id) VALUES ($1, $2, $3, $4)"
|
q = "INSERT INTO message (mxid, mx_room, mid, chat_id) VALUES ($1, $2, $3, $4)"
|
||||||
await self.db.execute(q, self.mxid, self.mx_room, self.mid, self.chat_id)
|
await self.db.execute(q, self.mxid, self.mx_room, self.mid, self.chat_id)
|
||||||
|
|
||||||
async def update_ids(self, new_mxid: EventID, new_mid: int) -> None:
|
async def delete(self) -> None:
|
||||||
q = ("UPDATE message SET mxid=$1, mid=$2 "
|
q = "DELETE FROM message WHERE mid=$1"
|
||||||
"WHERE mxid=$3 AND mx_room=$4 AND chat_id=$5")
|
await self.db.execute(q, self.mid)
|
||||||
await self.db.execute(q, new_mxid, new_mid,
|
|
||||||
self.mxid, self.mx_room, self.chat_id)
|
@classmethod
|
||||||
|
async def delete_all(cls, room_id: RoomID) -> None:
|
||||||
|
await cls.db.execute("DELETE FROM message WHERE mx_room=$1", room_id)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_max_mid(cls, room_id: RoomID) -> int:
|
async def get_max_mid(cls, room_id: RoomID) -> int:
|
||||||
|
@ -55,23 +57,6 @@ class Message:
|
||||||
data[row["chat_id"]] = row["max_mid"]
|
data[row["chat_id"]] = row["max_mid"]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def get_num_noid_msgs(cls, room_id: RoomID) -> int:
|
|
||||||
return await cls.db.fetchval("SELECT COUNT(*) FROM message "
|
|
||||||
"WHERE mid IS NULL AND mx_room=$1", room_id)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def is_last_by_mxid(cls, mxid: EventID, room_id: RoomID) -> bool:
|
|
||||||
q = ("SELECT mxid "
|
|
||||||
"FROM message INNER JOIN ( "
|
|
||||||
" SELECT mx_room, MAX(mid) AS max_mid "
|
|
||||||
" FROM message GROUP BY mx_room "
|
|
||||||
") by_room "
|
|
||||||
"ON mid=max_mid "
|
|
||||||
"WHERE by_room.mx_room=$1")
|
|
||||||
last_mxid = await cls.db.fetchval(q, room_id)
|
|
||||||
return last_mxid == mxid
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_by_mxid(cls, mxid: EventID, mx_room: RoomID) -> Optional['Message']:
|
async def get_by_mxid(cls, mxid: EventID, mx_room: RoomID) -> Optional['Message']:
|
||||||
row = await cls.db.fetchrow("SELECT mxid, mx_room, mid, chat_id "
|
row = await cls.db.fetchrow("SELECT mxid, mx_room, mid, chat_id "
|
||||||
|
@ -87,18 +72,3 @@ class Message:
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
return cls(**row)
|
return cls(**row)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def get_next_noid_msg(cls, room_id: RoomID) -> Optional['Message']:
|
|
||||||
row = await cls.db.fetchrow("SELECT mxid, mx_room, mid, chat_id FROM message "
|
|
||||||
"WHERE mid IS NULL AND mx_room=$1", room_id)
|
|
||||||
if not row:
|
|
||||||
return None
|
|
||||||
return cls(**row)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def delete_all_noid_msgs(cls, room_id: RoomID) -> None:
|
|
||||||
status = await cls.db.execute("DELETE FROM message "
|
|
||||||
"WHERE mid IS NULL AND mx_room=$1", room_id)
|
|
||||||
# Skip leading "DELETE "
|
|
||||||
return int(status[7:])
|
|
||||||
|
|
|
@ -129,10 +129,3 @@ async def upgrade_strangers(conn: Connection) -> None:
|
||||||
REFERENCES puppet (mid)
|
REFERENCES puppet (mid)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
)""")
|
)""")
|
||||||
|
|
||||||
|
|
||||||
@upgrade_table.register(description="Track messages that lack an ID")
|
|
||||||
async def upgrade_noid_msgs(conn: Connection) -> None:
|
|
||||||
await conn.execute("ALTER TABLE message DROP CONSTRAINT IF EXISTS message_pkey")
|
|
||||||
await conn.execute("ALTER TABLE message ALTER COLUMN mid DROP NOT NULL")
|
|
||||||
await conn.execute("ALTER TABLE message ADD UNIQUE (mid)")
|
|
|
@ -57,12 +57,6 @@ appservice:
|
||||||
as_token: "This value is generated when generating the registration"
|
as_token: "This value is generated when generating the registration"
|
||||||
hs_token: "This value is generated when generating the registration"
|
hs_token: "This value is generated when generating the registration"
|
||||||
|
|
||||||
# Whether or not to receive ephemeral events via appservice transactions.
|
|
||||||
# Requires MSC2409 support (i.e. Synapse 1.22+).
|
|
||||||
# This is REQUIRED in order to bypass Puppeteer needing to "view" a LINE chat
|
|
||||||
# (thus triggering a LINE read receipt on your behalf) to sync its messages.
|
|
||||||
ephemeral_events: false
|
|
||||||
|
|
||||||
# Prometheus telemetry config. Requires prometheus-client to be installed.
|
# Prometheus telemetry config. Requires prometheus-client to be installed.
|
||||||
metrics:
|
metrics:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
|
@ -17,11 +17,9 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from mautrix.bridge import BaseMatrixHandler
|
from mautrix.bridge import BaseMatrixHandler
|
||||||
from mautrix.types import (Event, ReactionEvent, MessageEvent, StateEvent, EncryptedEvent, RedactionEvent,
|
from mautrix.types import (Event, ReactionEvent, MessageEvent, StateEvent, EncryptedEvent, RedactionEvent,
|
||||||
ReceiptEvent, SingleReceiptEventContent,
|
|
||||||
EventID, RoomID, UserID)
|
EventID, RoomID, UserID)
|
||||||
|
|
||||||
from . import portal as po, puppet as pu, user as u
|
from . import portal as po, puppet as pu, user as u
|
||||||
from .db import Message as DBMessage
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .__main__ import MessagesBridge
|
from .__main__ import MessagesBridge
|
||||||
|
@ -37,9 +35,8 @@ class MatrixHandler(BaseMatrixHandler):
|
||||||
super().__init__(bridge=bridge)
|
super().__init__(bridge=bridge)
|
||||||
|
|
||||||
def filter_matrix_event(self, evt: Event) -> bool:
|
def filter_matrix_event(self, evt: Event) -> bool:
|
||||||
if isinstance(evt, ReceiptEvent):
|
if not isinstance(evt, (ReactionEvent, MessageEvent, StateEvent, EncryptedEvent,
|
||||||
return False
|
RedactionEvent)):
|
||||||
if not isinstance(evt, (MessageEvent, StateEvent, EncryptedEvent)):
|
|
||||||
return True
|
return True
|
||||||
return (evt.sender == self.az.bot_mxid
|
return (evt.sender == self.az.bot_mxid
|
||||||
or pu.Puppet.get_id_from_mxid(evt.sender) is not None)
|
or pu.Puppet.get_id_from_mxid(evt.sender) is not None)
|
||||||
|
@ -62,16 +59,3 @@ class MatrixHandler(BaseMatrixHandler):
|
||||||
return
|
return
|
||||||
|
|
||||||
await portal.handle_matrix_leave(user)
|
await portal.handle_matrix_leave(user)
|
||||||
|
|
||||||
async def handle_read_receipt(self, user: 'u.User', portal: 'po.Portal', event_id: EventID,
|
|
||||||
data: SingleReceiptEventContent) -> None:
|
|
||||||
# When reading a bridged message, view its chat in LINE, to make it send a read receipt.
|
|
||||||
|
|
||||||
# TODO Use *null* mids for last messages in a chat!!
|
|
||||||
# Only visit a LINE chat when its LAST bridge message has been read,
|
|
||||||
# because LINE lacks per-message read receipts--it's all or nothing!
|
|
||||||
# TODO Also view if message is non-last but for media, so it can be loaded.
|
|
||||||
#if await DBMessage.is_last_by_mxid(event_id, portal.mxid):
|
|
||||||
|
|
||||||
# Viewing a chat by updating it whole-hog, lest a ninja arrives
|
|
||||||
await user.sync_portal(portal)
|
|
||||||
|
|
|
@ -124,11 +124,6 @@ class Portal(DBPortal, BasePortal):
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception("Failed to send delivery receipt for %s", event_id)
|
self.log.exception("Failed to send delivery receipt for %s", event_id)
|
||||||
|
|
||||||
async def _cleanup_noid_msgs(self) -> None:
|
|
||||||
num_noid_msgs = await DBMessage.delete_all_noid_msgs(self.mxid)
|
|
||||||
if num_noid_msgs > 0:
|
|
||||||
self.log.warn(f"Found {num_noid_msgs} messages in chat {self.chat_id} with no ID that could not be matched with a real ID")
|
|
||||||
|
|
||||||
async def handle_matrix_message(self, sender: 'u.User', message: MessageEventContent,
|
async def handle_matrix_message(self, sender: 'u.User', message: MessageEventContent,
|
||||||
event_id: EventID) -> None:
|
event_id: EventID) -> None:
|
||||||
if not sender.client:
|
if not sender.client:
|
||||||
|
@ -168,8 +163,6 @@ class Portal(DBPortal, BasePortal):
|
||||||
self.log.warning(f"Failed to upload media {event_id} to chat {self.chat_id}: {e}")
|
self.log.warning(f"Failed to upload media {event_id} to chat {self.chat_id}: {e}")
|
||||||
message_id = -1
|
message_id = -1
|
||||||
remove(file_path)
|
remove(file_path)
|
||||||
|
|
||||||
await self._cleanup_noid_msgs()
|
|
||||||
msg = None
|
msg = None
|
||||||
if message_id != -1:
|
if message_id != -1:
|
||||||
try:
|
try:
|
||||||
|
@ -182,18 +175,20 @@ class Portal(DBPortal, BasePortal):
|
||||||
if not msg and self.config["bridge.delivery_error_reports"]:
|
if not msg and self.config["bridge.delivery_error_reports"]:
|
||||||
await self.main_intent.send_notice(
|
await self.main_intent.send_notice(
|
||||||
self.mxid,
|
self.mxid,
|
||||||
"Posting this message to LINE may have failed.",
|
"Posting this message to LINE may have failed.",
|
||||||
relates_to=RelatesTo(rel_type=RelationType.REPLY, event_id=event_id))
|
relates_to=RelatesTo(rel_type=RelationType.REPLY, event_id=event_id))
|
||||||
|
|
||||||
async def handle_matrix_leave(self, user: 'u.User') -> None:
|
async def handle_matrix_leave(self, user: 'u.User') -> None:
|
||||||
self.log.info(f"{user.mxid} left portal to {self.chat_id}, "
|
self.log.info(f"{user.mxid} left portal to {self.chat_id}, "
|
||||||
f"cleaning up and deleting...")
|
f"cleaning up and deleting...")
|
||||||
await self.cleanup_and_delete()
|
await self.cleanup_and_delete()
|
||||||
|
|
||||||
async def _bridge_own_message_pm(self, source: 'u.User', puppet: 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]:
|
||||||
intent = puppet.intent if puppet else (await source.get_own_puppet()).intent
|
# Use bridge bot as puppet for own user when puppet for own user is unavailable
|
||||||
if self.is_direct and (not puppet or puppet.mid == source.mid and not puppet.is_real_user):
|
# 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.invite_own_puppet_to_pm and invite:
|
if self.invite_own_puppet_to_pm and invite:
|
||||||
try:
|
try:
|
||||||
await intent.ensure_joined(self.mxid)
|
await intent.ensure_joined(self.mxid)
|
||||||
|
@ -218,55 +213,29 @@ class Portal(DBPortal, BasePortal):
|
||||||
|
|
||||||
if evt.is_outgoing:
|
if evt.is_outgoing:
|
||||||
if source.intent:
|
if source.intent:
|
||||||
|
sender = None
|
||||||
intent = source.intent
|
intent = source.intent
|
||||||
else:
|
else:
|
||||||
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
|
||||||
puppet = await p.Puppet.get_by_mid(evt.sender.id) if evt.sender else None
|
sender = p.Puppet.get_by_mid(evt.sender.id) if not self.is_direct else None
|
||||||
intent = await self._bridge_own_message_pm(source, puppet, 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
|
||||||
else:
|
else:
|
||||||
if self.is_direct:
|
sender = await p.Puppet.get_by_mid(self.other_user if self.is_direct else evt.sender.id)
|
||||||
# TODO Respond to name/avatar changes of users in a DM
|
# TODO Respond to name/avatar changes of users in a DM
|
||||||
intent = (await p.Puppet.get_by_mid(self.other_user)).intent
|
if not self.is_direct:
|
||||||
elif evt.sender:
|
if sender:
|
||||||
puppet = await p.Puppet.get_by_mid(evt.sender.id)
|
await sender.update_info(evt.sender, source.client)
|
||||||
if puppet:
|
|
||||||
await puppet.update_info(evt.sender, source.client)
|
|
||||||
else:
|
else:
|
||||||
self.log.warning(f"Could not find ID of LINE user who sent message {evt.id or 'with no ID'}")
|
self.log.warning(f"Could not find ID of LINE user who sent event {evt.id}")
|
||||||
puppet = await p.Puppet.get_by_profile(evt.sender, source.client)
|
sender = await p.Puppet.get_by_profile(evt.sender, source.client)
|
||||||
intent = puppet.intent
|
intent = sender.intent
|
||||||
else:
|
|
||||||
self.log.info(f"Using bridgebot for unknown sender of message {evt.id or 'with no ID'}")
|
|
||||||
intent = self.az.intent
|
|
||||||
await intent.ensure_joined(self.mxid)
|
await intent.ensure_joined(self.mxid)
|
||||||
|
|
||||||
if evt.id:
|
if evt.image and evt.image.url:
|
||||||
msg = await DBMessage.get_next_noid_msg(self.mxid)
|
|
||||||
if not msg:
|
|
||||||
self.log.info(f"Handling new message {evt.id} in chat {self.mxid}")
|
|
||||||
prev_event_id = None
|
|
||||||
else:
|
|
||||||
self.log.info(f"Handling preseen message {evt.id} in chat {self.mxid}: {msg.mxid}")
|
|
||||||
if not self.is_direct:
|
|
||||||
# Non-DM previews are always sent by bridgebot.
|
|
||||||
# Must delete the bridgebot message and send a new message from the correct puppet.
|
|
||||||
await self.az.intent.redact(self.mxid, msg.mxid, "Found actual sender")
|
|
||||||
prev_event_id = None
|
|
||||||
else:
|
|
||||||
prev_event_id = msg.mxid
|
|
||||||
else:
|
|
||||||
self.log.info(f"Handling new message with no ID in chat {self.mxid}")
|
|
||||||
msg = None
|
|
||||||
prev_event_id = None
|
|
||||||
|
|
||||||
if prev_event_id and evt.html:
|
|
||||||
# No need to update a previewed text message, as their previews are accurate
|
|
||||||
event_id = prev_event_id
|
|
||||||
elif evt.image and evt.image.url:
|
|
||||||
if not evt.image.is_sticker or self.config["bridge.receive_stickers"]:
|
if not evt.image.is_sticker or self.config["bridge.receive_stickers"]:
|
||||||
media_info = await self._handle_remote_media(
|
media_info = await self._handle_remote_media(
|
||||||
source, intent, evt.image.url,
|
source, intent, evt.image.url,
|
||||||
|
@ -283,11 +252,9 @@ class Portal(DBPortal, BasePortal):
|
||||||
else:
|
else:
|
||||||
media_info = None
|
media_info = None
|
||||||
send_sticker = self.config["bridge.use_sticker_events"] and evt.image.is_sticker and not self.encrypted and media_info
|
send_sticker = self.config["bridge.use_sticker_events"] and evt.image.is_sticker and not self.encrypted and media_info
|
||||||
# TODO Element Web messes up text->sticker edits!!
|
if send_sticker:
|
||||||
# File a case on it
|
event_id = await intent.send_sticker(
|
||||||
if send_sticker and not prev_event_id:
|
self.mxid, media_info.mxc, image_info, "<sticker>", timestamp=evt.timestamp)
|
||||||
#relates_to = RelatesTo(rel_type=RelationType.REPLACE, event_id=prev_event_id) if prev_event_id else None
|
|
||||||
event_id = await intent.send_sticker(self.mxid, media_info.mxc, image_info, "<sticker>", timestamp=evt.timestamp)
|
|
||||||
else:
|
else:
|
||||||
if media_info:
|
if media_info:
|
||||||
content = MediaMessageEventContent(
|
content = MediaMessageEventContent(
|
||||||
|
@ -299,11 +266,8 @@ class Portal(DBPortal, BasePortal):
|
||||||
content = TextMessageEventContent(
|
content = TextMessageEventContent(
|
||||||
msgtype=MessageType.NOTICE,
|
msgtype=MessageType.NOTICE,
|
||||||
body=f"<{'sticker' if evt.image.is_sticker else 'image'}>")
|
body=f"<{'sticker' if evt.image.is_sticker else 'image'}>")
|
||||||
if prev_event_id:
|
|
||||||
content.set_edit(prev_event_id)
|
|
||||||
event_id = await self._send_message(intent, content, timestamp=evt.timestamp)
|
event_id = await self._send_message(intent, content, timestamp=evt.timestamp)
|
||||||
elif evt.html and not evt.html.isspace():
|
elif evt.html and not evt.html.isspace():
|
||||||
|
|
||||||
chunks = []
|
chunks = []
|
||||||
|
|
||||||
def handle_data(data):
|
def handle_data(data):
|
||||||
|
@ -367,24 +331,17 @@ class Portal(DBPortal, BasePortal):
|
||||||
content = TextMessageEventContent(
|
content = TextMessageEventContent(
|
||||||
msgtype=MessageType.NOTICE,
|
msgtype=MessageType.NOTICE,
|
||||||
body="<Unbridgeable message>")
|
body="<Unbridgeable message>")
|
||||||
if prev_event_id:
|
|
||||||
content.set_edit(prev_event_id)
|
|
||||||
event_id = await self._send_message(intent, content, timestamp=evt.timestamp)
|
event_id = await self._send_message(intent, content, timestamp=evt.timestamp)
|
||||||
|
|
||||||
if evt.is_outgoing and evt.receipt_count:
|
if evt.is_outgoing and evt.receipt_count:
|
||||||
await self._handle_receipt(event_id, 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)
|
||||||
if not msg:
|
try:
|
||||||
msg = DBMessage(mxid=event_id, mx_room=self.mxid, mid=evt.id, chat_id=self.chat_id)
|
await msg.insert()
|
||||||
try:
|
await self._send_delivery_receipt(event_id)
|
||||||
await msg.insert()
|
self.log.debug(f"Handled remote message {evt.id} -> {event_id}")
|
||||||
#await self._send_delivery_receipt(event_id)
|
except UniqueViolationError as e:
|
||||||
self.log.debug(f"Handled remote message {evt.id or 'with no ID'} -> {event_id}")
|
self.log.debug(f"Failed to handle remote message {evt.id} -> {event_id}: {e}")
|
||||||
except UniqueViolationError as e:
|
|
||||||
self.log.debug(f"Failed to handle remote message {evt.id or 'with no ID'} -> {event_id}: {e}")
|
|
||||||
else:
|
|
||||||
await msg.update_ids(new_mxid=event_id, new_mid=evt.id)
|
|
||||||
self.log.debug(f"Handled preseen remote message {evt.id} -> {event_id}")
|
|
||||||
|
|
||||||
async def handle_remote_receipt(self, receipt: Receipt) -> None:
|
async def handle_remote_receipt(self, receipt: Receipt) -> None:
|
||||||
msg = await DBMessage.get_by_mid(receipt.id)
|
msg = await DBMessage.get_by_mid(receipt.id)
|
||||||
|
@ -572,12 +529,11 @@ class Portal(DBPortal, BasePortal):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mid = p.Puppet.get_id_from_mxid(user_id)
|
mid = p.Puppet.get_id_from_mxid(user_id)
|
||||||
is_own_puppet = p.Puppet.is_mid_for_own_puppet(mid)
|
if mid and mid not in current_members:
|
||||||
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 is_own_puppet:
|
elif forbid_own_puppets and p.Puppet.is_mid_for_own_puppet(mid):
|
||||||
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")
|
||||||
|
|
||||||
|
@ -597,7 +553,6 @@ class Portal(DBPortal, BasePortal):
|
||||||
|
|
||||||
if not messages:
|
if not messages:
|
||||||
self.log.debug("Didn't get any entries from server")
|
self.log.debug("Didn't get any entries from server")
|
||||||
await self._cleanup_noid_msgs()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log.debug("Got %d messages from server", len(messages))
|
self.log.debug("Got %d messages from server", len(messages))
|
||||||
|
@ -605,7 +560,6 @@ class Portal(DBPortal, BasePortal):
|
||||||
for evt in messages:
|
for evt in messages:
|
||||||
await self.handle_remote_message(source, evt)
|
await self.handle_remote_message(source, evt)
|
||||||
self.log.info("Backfilled %d messages through %s", len(messages), source.mxid)
|
self.log.info("Backfilled %d messages through %s", len(messages), source.mxid)
|
||||||
await self._cleanup_noid_msgs()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bridge_info_state_key(self) -> str:
|
def bridge_info_state_key(self) -> str:
|
||||||
|
@ -759,6 +713,9 @@ class Portal(DBPortal, BasePortal):
|
||||||
self._main_intent = self.az.intent
|
self._main_intent = self.az.intent
|
||||||
|
|
||||||
async def delete(self) -> None:
|
async def delete(self) -> None:
|
||||||
|
if self.mxid:
|
||||||
|
# TODO Handle this with db foreign keys instead
|
||||||
|
await DBMessage.delete_all(self.mxid)
|
||||||
self.by_chat_id.pop(self.chat_id, None)
|
self.by_chat_id.pop(self.chat_id, None)
|
||||||
self.by_mxid.pop(self.mxid, None)
|
self.by_mxid.pop(self.mxid, None)
|
||||||
await super().delete()
|
await super().delete()
|
||||||
|
|
|
@ -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, ImageData, Message, Participant, Receipt, StartStatus
|
from .types import ChatListInfo, ChatInfo, Message, Receipt, ImageData, StartStatus
|
||||||
|
|
||||||
|
|
||||||
class LoginCommand(TypedDict):
|
class LoginCommand(TypedDict):
|
||||||
|
@ -41,15 +41,12 @@ 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]
|
||||||
|
|
||||||
async def get_chat(self, chat_id: str, force_view: bool = False) -> ChatInfo:
|
async def get_chat(self, chat_id: str) -> ChatInfo:
|
||||||
return ChatInfo.deserialize(await self.request("get_chat", chat_id=chat_id, force_view=force_view))
|
return ChatInfo.deserialize(await self.request("get_chat", chat_id=chat_id))
|
||||||
|
|
||||||
async def get_messages(self, chat_id: str) -> List[Message]:
|
async def get_messages(self, chat_id: str) -> List[Message]:
|
||||||
resp = await self.request("get_messages", chat_id=chat_id)
|
resp = await self.request("get_messages", chat_id=chat_id)
|
||||||
|
|
|
@ -33,7 +33,6 @@ class RPCClient:
|
||||||
log: TraceLogger = logging.getLogger("mau.rpc")
|
log: TraceLogger = logging.getLogger("mau.rpc")
|
||||||
|
|
||||||
user_id: UserID
|
user_id: UserID
|
||||||
ephemeral_events: bool
|
|
||||||
_reader: Optional[asyncio.StreamReader]
|
_reader: Optional[asyncio.StreamReader]
|
||||||
_writer: Optional[asyncio.StreamWriter]
|
_writer: Optional[asyncio.StreamWriter]
|
||||||
_req_id: int
|
_req_id: int
|
||||||
|
@ -41,12 +40,10 @@ 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, own_id: str, ephemeral_events: bool) -> None:
|
def __init__(self, user_id: UserID) -> 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.ephemeral_events = ephemeral_events
|
|
||||||
self._req_id = 0
|
self._req_id = 0
|
||||||
self._min_broadcast_id = 0
|
self._min_broadcast_id = 0
|
||||||
self._event_handlers = {}
|
self._event_handlers = {}
|
||||||
|
@ -70,10 +67,7 @@ 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",
|
await self.request("register", user_id=self.user_id)
|
||||||
user_id=self.user_id,
|
|
||||||
own_id = self.own_id,
|
|
||||||
ephemeral_events=self.ephemeral_events)
|
|
||||||
|
|
||||||
async def disconnect(self) -> None:
|
async def disconnect(self) -> None:
|
||||||
self._writer.write_eof()
|
self._writer.write_eof()
|
||||||
|
|
|
@ -60,7 +60,7 @@ class MessageImage(SerializableAttrs['MessageImage']):
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Message(SerializableAttrs['Message']):
|
class Message(SerializableAttrs['Message']):
|
||||||
id: Optional[int]
|
id: int
|
||||||
chat_id: int
|
chat_id: int
|
||||||
is_outgoing: bool
|
is_outgoing: bool
|
||||||
sender: Optional[Participant]
|
sender: Optional[Participant]
|
||||||
|
|
|
@ -69,14 +69,6 @@ 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
|
||||||
|
@ -103,7 +95,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.own_id, self.config["appservice.ephemeral_events"])
|
self.client = Client(self.mxid)
|
||||||
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()
|
||||||
|
@ -134,7 +126,6 @@ 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")
|
||||||
|
@ -153,20 +144,6 @@ class User(DBUser, BaseUser):
|
||||||
await self.send_bridge_notice("Synchronization complete")
|
await self.send_bridge_notice("Synchronization complete")
|
||||||
await self.client.resume()
|
await self.client.resume()
|
||||||
|
|
||||||
async def sync_portal(self, portal: 'po.Portal') -> None:
|
|
||||||
chat_id = portal.chat_id
|
|
||||||
self.log.info(f"Viewing (and syncing) chat {chat_id}")
|
|
||||||
await self.client.pause()
|
|
||||||
chat = await self.client.get_chat(chat_id, True)
|
|
||||||
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:
|
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:
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
/node_modules
|
/node_modules
|
||||||
/config*.json
|
/config.json
|
||||||
|
|
|
@ -99,7 +99,7 @@ export default class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(message) {
|
sendMessage(message) {
|
||||||
this.log(`Sending message ${message.id || "with no ID"} to client`)
|
this.log(`Sending message ${message.id} to client`)
|
||||||
return this._write({
|
return this._write({
|
||||||
id: --this.notificationID,
|
id: --this.notificationID,
|
||||||
command: "message",
|
command: "message",
|
||||||
|
@ -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.ownID, this.sendPlaceholders, this)
|
this.puppet = new MessagesPuppeteer(this.userID, 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,13 +194,11 @@ export default class Client {
|
||||||
|
|
||||||
handleRegister = async (req) => {
|
handleRegister = async (req) => {
|
||||||
this.userID = req.user_id
|
this.userID = req.user_id
|
||||||
this.ownID = req.own_id
|
this.log("Registered socket", this.connID, "->", this.userID)
|
||||||
this.sendPlaceholders = req.ephemeral_events
|
|
||||||
this.log(`Registered socket ${this.connID} -> ${this.userID}${!this.sendPlaceholders ? "" : " (with placeholder message support)"}`)
|
|
||||||
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)
|
||||||
|
@ -260,9 +258,8 @@ 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),
|
||||||
get_messages: req => this.puppet.getMessages(req.chat_id),
|
get_messages: req => this.puppet.getMessages(req.chat_id),
|
||||||
read_image: req => this.puppet.readImage(req.image_url),
|
read_image: req => this.puppet.readImage(req.image_url),
|
||||||
is_connected: async () => ({ is_connected: !await this.puppet.isDisconnected() }),
|
is_connected: async () => ({ is_connected: !await this.puppet.isDisconnected() }),
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
*/
|
*/
|
||||||
window.__chronoParseDate = function (text, ref, option) {}
|
window.__chronoParseDate = function (text, ref, option) {}
|
||||||
/**
|
/**
|
||||||
* @param {ChatListInfo[]} changes - The chats that changed.
|
* @param {string[]} changes - The hrefs of the chats that changed.
|
||||||
* @return {Promise<void>}
|
* @return {Promise<void>}
|
||||||
*/
|
*/
|
||||||
window.__mautrixReceiveChanges = function (changes) {}
|
window.__mautrixReceiveChanges = function (changes) {}
|
||||||
|
@ -102,7 +102,9 @@ class MautrixController {
|
||||||
}
|
}
|
||||||
|
|
||||||
setOwnID(ownID) {
|
setOwnID(ownID) {
|
||||||
this.ownID = 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
|
// TODO Commonize with Node context
|
||||||
|
@ -525,7 +527,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} url - The URL of the image. Mandatory.
|
* @property {string} src - The URL of the image. Mandatory.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -607,8 +609,6 @@ class MautrixController {
|
||||||
* May be prefixed by sender name.
|
* May be prefixed by sender name.
|
||||||
* @property {string} lastMsgDate - An imprecise date for the most recent message
|
* @property {string} lastMsgDate - An imprecise date for the most recent message
|
||||||
* (e.g. "7:16 PM", "Thu" or "Aug 4")
|
* (e.g. "7:16 PM", "Thu" or "Aug 4")
|
||||||
* @property {number} notificationCount - The number of unread messages in the chat,
|
|
||||||
* signified by the number in its notification badge.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getChatListItemID(element) {
|
getChatListItemID(element) {
|
||||||
|
@ -624,17 +624,13 @@ class MautrixController {
|
||||||
}
|
}
|
||||||
|
|
||||||
getChatListItemLastMsg(element) {
|
getChatListItemLastMsg(element) {
|
||||||
return element.querySelector(".mdCMN04Desc").innerHTML
|
return element.querySelector(".mdCMN04Desc").innerText
|
||||||
}
|
}
|
||||||
|
|
||||||
getChatListItemLastMsgDate(element) {
|
getChatListItemLastMsgDate(element) {
|
||||||
return element.querySelector("time").innerText
|
return element.querySelector("time").innerText
|
||||||
}
|
}
|
||||||
|
|
||||||
getChatListItemNotificationCount(element) {
|
|
||||||
return Number.parseInt(element.querySelector(".MdIcoBadge01:not(.MdNonDisp)")?.innerText) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a conversation list item element.
|
* Parse a conversation list item element.
|
||||||
*
|
*
|
||||||
|
@ -649,7 +645,6 @@ class MautrixController {
|
||||||
icon: this.getChatListItemIcon(element),
|
icon: this.getChatListItemIcon(element),
|
||||||
lastMsg: this.getChatListItemLastMsg(element),
|
lastMsg: this.getChatListItemLastMsg(element),
|
||||||
lastMsgDate: this.getChatListItemLastMsgDate(element),
|
lastMsgDate: this.getChatListItemLastMsgDate(element),
|
||||||
notificationCount: this.getChatListItemNotificationCount(element),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -681,56 +676,13 @@ 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 = 500
|
|
||||||
|
|
||||||
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
|
||||||
*/
|
*/
|
||||||
_observeChatListMutations(mutations) {
|
_observeChatListMutations(mutations) {
|
||||||
// TODO Observe *added/removed* chats, not just new messages
|
// TODO Observe *added/removed* chats, not just new messages
|
||||||
const changedChats = new Set()
|
const changedChatIDs = new Set()
|
||||||
for (const change of mutations) {
|
for (const change of mutations) {
|
||||||
if (change.target.id == "_chat_list_body") {
|
if (change.target.id == "_chat_list_body") {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -740,24 +692,26 @@ class MautrixController {
|
||||||
for (const node of change.addedNodes) {
|
for (const node of change.addedNodes) {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
} else if (change.target.tagName == "LI" && change.addedNodes.length == 1) {
|
} else if (change.target.tagName == "LI") {
|
||||||
if (change.target.classList.contains("ExSelected")) {
|
if (change.target.classList.contains("ExSelected")) {
|
||||||
console.debug("Not using chat list mutation response for currently-active chat")
|
console.debug("Not using chat list mutation response for currently-active chat")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const chat = this.parseChatListItem(change.addedNodes[0])
|
for (const node of change.addedNodes) {
|
||||||
if (chat) {
|
const chat = this.parseChatListItem(node)
|
||||||
console.log("Added chat list item:", chat)
|
if (chat) {
|
||||||
changedChats.add(chat)
|
console.log("Added chat list item:", chat)
|
||||||
} else {
|
changedChatIDs.add(chat.id)
|
||||||
console.debug("Could not parse added node as a chat list item:", node)
|
} else {
|
||||||
|
console.debug("Could not parse added node as a chat list item:", node)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// change.removedNodes tells you which chats that had notifications are now read.
|
// change.removedNodes tells you which chats that had notifications are now read.
|
||||||
}
|
}
|
||||||
if (changedChats.size > 0) {
|
if (changedChatIDs.size > 0) {
|
||||||
console.debug("Dispatching chat list mutations:", changedChats)
|
console.debug("Dispatching chat list mutations:", changedChatIDs)
|
||||||
window.__mautrixReceiveChanges(Array.from(changedChats)).then(
|
window.__mautrixReceiveChanges(Array.from(changedChatIDs)).then(
|
||||||
() => console.debug("Chat list mutations dispatched"),
|
() => console.debug("Chat list mutations dispatched"),
|
||||||
err => console.error("Error dispatching chat list mutations:", err))
|
err => console.error("Error dispatching chat list mutations:", err))
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default class MessagesPuppeteer {
|
||||||
static executablePath = undefined
|
static executablePath = undefined
|
||||||
static devtools = false
|
static devtools = false
|
||||||
static noSandbox = false
|
static noSandbox = false
|
||||||
static viewport = { width: 960, height: 840 }
|
static viewport = { width: 960, height: 880 }
|
||||||
static url = undefined
|
static url = undefined
|
||||||
static extensionDir = "extension_files"
|
static extensionDir = "extension_files"
|
||||||
|
|
||||||
|
@ -36,19 +36,16 @@ export default class MessagesPuppeteer {
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @param {?Client} [client]
|
* @param {?Client} [client]
|
||||||
*/
|
*/
|
||||||
constructor(id, ownID, sendPlaceholders, client = null) {
|
constructor(id, 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.sendPlaceholders = sendPlaceholders
|
|
||||||
this.profilePath = profilePath
|
this.profilePath = profilePath
|
||||||
this.updatedChats = new Set()
|
this.updatedChats = new Set()
|
||||||
this.sentMessageIDs = new Set()
|
this.sentMessageIDs = new Set()
|
||||||
this.mostRecentMessages = new Map()
|
this.mostRecentMessages = new Map()
|
||||||
this.numChatNotifications = new Map()
|
|
||||||
this.taskQueue = new TaskQueue(this.id)
|
this.taskQueue = new TaskQueue(this.id)
|
||||||
this.client = client
|
this.client = client
|
||||||
}
|
}
|
||||||
|
@ -68,19 +65,18 @@ export default class MessagesPuppeteer {
|
||||||
async start() {
|
async start() {
|
||||||
this.log("Launching browser")
|
this.log("Launching browser")
|
||||||
|
|
||||||
const args = [
|
let extensionArgs = [
|
||||||
`--disable-extensions-except=${MessagesPuppeteer.extensionDir}`,
|
`--disable-extensions-except=${MessagesPuppeteer.extensionDir}`,
|
||||||
`--load-extension=${MessagesPuppeteer.extensionDir}`,
|
`--load-extension=${MessagesPuppeteer.extensionDir}`
|
||||||
`--window-size=${MessagesPuppeteer.viewport.width},${MessagesPuppeteer.viewport.height+120}`,
|
|
||||||
]
|
]
|
||||||
if (MessagesPuppeteer.noSandbox) {
|
if (MessagesPuppeteer.noSandbox) {
|
||||||
args = args.concat(`--no-sandbox`)
|
extensionArgs = extensionArgs.concat(`--no-sandbox`)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.browser = await puppeteer.launch({
|
this.browser = await puppeteer.launch({
|
||||||
executablePath: MessagesPuppeteer.executablePath,
|
executablePath: MessagesPuppeteer.executablePath,
|
||||||
userDataDir: this.profilePath,
|
userDataDir: this.profilePath,
|
||||||
args: args,
|
args: extensionArgs,
|
||||||
headless: false, // Needed to load extensions
|
headless: false, // Needed to load extensions
|
||||||
defaultViewport: MessagesPuppeteer.viewport,
|
defaultViewport: MessagesPuppeteer.viewport,
|
||||||
devtools: MessagesPuppeteer.devtools,
|
devtools: MessagesPuppeteer.devtools,
|
||||||
|
@ -93,10 +89,6 @@ export default class MessagesPuppeteer {
|
||||||
} else {
|
} else {
|
||||||
this.page = await this.browser.newPage()
|
this.page = await this.browser.newPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.blankPage = await this.browser.newPage()
|
|
||||||
await this.page.bringToFront()
|
|
||||||
|
|
||||||
this.log("Opening", MessagesPuppeteer.url)
|
this.log("Opening", MessagesPuppeteer.url)
|
||||||
await this.page.setBypassCSP(true) // Needed to load content scripts
|
await this.page.setBypassCSP(true) // Needed to load content scripts
|
||||||
await this._preparePage(true)
|
await this._preparePage(true)
|
||||||
|
@ -127,7 +119,6 @@ export default class MessagesPuppeteer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _preparePage(navigateTo) {
|
async _preparePage(navigateTo) {
|
||||||
await this.page.bringToFront()
|
|
||||||
if (navigateTo) {
|
if (navigateTo) {
|
||||||
await this.page.goto(MessagesPuppeteer.url)
|
await this.page.goto(MessagesPuppeteer.url)
|
||||||
} else {
|
} else {
|
||||||
|
@ -137,30 +128,6 @@ export default class MessagesPuppeteer {
|
||||||
await this.page.addScriptTag({ path: "./src/contentscript.js", type: "module" })
|
await this.page.addScriptTag({ path: "./src/contentscript.js", type: "module" })
|
||||||
}
|
}
|
||||||
|
|
||||||
async _interactWithPage(promiser) {
|
|
||||||
await this.page.bringToFront()
|
|
||||||
try {
|
|
||||||
await promiser()
|
|
||||||
} catch (e) {
|
|
||||||
this.error(`Error while interacting with page: ${e}`)
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
await this.blankPage.bringToFront()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
|
@ -169,7 +136,6 @@ export default class MessagesPuppeteer {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.loginRunning = true
|
this.loginRunning = true
|
||||||
await this.page.bringToFront()
|
|
||||||
|
|
||||||
const loginContentArea = await this.page.waitForSelector("#login_content")
|
const loginContentArea = await this.page.waitForSelector("#login_content")
|
||||||
|
|
||||||
|
@ -253,7 +219,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.ownID)
|
await this.page.evaluate(ownID => window.__mautrixController.setOwnID(ownID), this.id)
|
||||||
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())
|
||||||
|
@ -288,7 +254,15 @@ export default class MessagesPuppeteer {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loginRunning = false
|
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")
|
this.log("Login complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -402,11 +376,10 @@ export default class MessagesPuppeteer {
|
||||||
* Get info about a chat.
|
* Get info about a chat.
|
||||||
*
|
*
|
||||||
* @param {string} chatID - The chat ID whose info to get.
|
* @param {string} chatID - The chat ID whose info to get.
|
||||||
* @param {boolean} forceView - Whether the LINE tab should always be viewed, even if the chat is already active.
|
|
||||||
* @return {Promise<ChatInfo>} - Info about the chat.
|
* @return {Promise<ChatInfo>} - Info about the chat.
|
||||||
*/
|
*/
|
||||||
async getChatInfo(chatID, forceView) {
|
async getChatInfo(chatID) {
|
||||||
return await this.taskQueue.push(() => this._getChatInfoUnsafe(chatID, forceView))
|
return await this.taskQueue.push(() => this._getChatInfoUnsafe(chatID))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -427,7 +400,7 @@ export default class MessagesPuppeteer {
|
||||||
* @return {Promise<[MessageData]>} - The messages visible in the chat.
|
* @return {Promise<[MessageData]>} - The messages visible in the chat.
|
||||||
*/
|
*/
|
||||||
async getMessages(chatID) {
|
async getMessages(chatID) {
|
||||||
return await this.taskQueue.push(() => this._getMessagesUnsafe(chatID))
|
return await this.taskQueue.push(async () => this._getMessagesUnsafe(chatID))
|
||||||
}
|
}
|
||||||
|
|
||||||
setLastMessageIDs(ids) {
|
setLastMessageIDs(ids) {
|
||||||
|
@ -470,46 +443,11 @@ 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}"]`
|
||||||
}
|
}
|
||||||
|
|
||||||
async _switchChat(chatID, forceView = false) {
|
async _switchChat(chatID) {
|
||||||
// TODO Allow passing in an element directly
|
// TODO Allow passing in an element directly
|
||||||
this.log(`Switching to chat ${chatID}`)
|
this.log(`Switching to chat ${chatID}`)
|
||||||
const chatListItem = await this.page.$(this._listItemSelector(chatID))
|
const chatListItem = await this.page.$(this._listItemSelector(chatID))
|
||||||
|
@ -525,63 +463,30 @@ export default class MessagesPuppeteer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.page.evaluate(isCorrectChatVisible, chatName)) {
|
if (await this.page.evaluate(isCorrectChatVisible, chatName)) {
|
||||||
if (!forceView) {
|
this.log("Already viewing chat, no need to switch")
|
||||||
this.log("Already viewing chat, no need to switch")
|
|
||||||
} else {
|
|
||||||
await this._interactWithPage(async () => {
|
|
||||||
this.log("Already viewing chat, but got request to view it")
|
|
||||||
this.page.waitForTimeout(500)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.log("Ensuring msg list observer is removed")
|
this.log("Ensuring msg list observer is removed")
|
||||||
const hadMsgListObserver = await this.page.evaluate(
|
const hadMsgListObserver = await this.page.evaluate(
|
||||||
() => window.__mautrixController.removeMsgListObserver())
|
() => window.__mautrixController.removeMsgListObserver())
|
||||||
this.log(hadMsgListObserver ? "Observer was already removed" : "Removed observer")
|
this.log(hadMsgListObserver ? "Observer was already removed" : "Removed observer")
|
||||||
|
|
||||||
await this._interactWithPage(async () => {
|
await chatListItem.click()
|
||||||
let numTries = 3
|
this.log(`Waiting for chat header title to be "${chatName}"`)
|
||||||
while (true) {
|
await this.page.waitForFunction(
|
||||||
try {
|
isCorrectChatVisible,
|
||||||
this.log("Clicking chat list item")
|
{polling: "mutation"},
|
||||||
chatListItem.click()
|
chatName)
|
||||||
this.log(`Waiting for chat header title to be "${chatName}"`)
|
|
||||||
await this.page.waitForFunction(
|
|
||||||
isCorrectChatVisible,
|
|
||||||
{polling: "mutation", timeout: 1000},
|
|
||||||
chatName)
|
|
||||||
break
|
|
||||||
} catch (e) {
|
|
||||||
if (--numTries == 0) {
|
|
||||||
throw e
|
|
||||||
} else {
|
|
||||||
this.log("Clicking chat list item didn't work...try again")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always show the chat details sidebar, as this makes life easier
|
// Always show the chat details sidebar, as this makes life easier
|
||||||
this.log("Waiting for detail area to be auto-hidden upon entering chat")
|
this.log("Waiting for detail area to be auto-hidden upon entering chat")
|
||||||
await this.page.waitForFunction(
|
await this.page.waitForFunction(
|
||||||
detailArea => detailArea.childElementCount == 0,
|
detailArea => detailArea.childElementCount == 0,
|
||||||
{},
|
{},
|
||||||
await this.page.$("#_chat_detail_area"))
|
await this.page.$("#_chat_detail_area"))
|
||||||
|
this.log("Clicking chat header to show detail area")
|
||||||
this.log("Clicking chat header to show detail area")
|
await this.page.click("#_chat_header_area > .mdRGT04Link")
|
||||||
await this.page.click("#_chat_header_area > .mdRGT04Link")
|
this.log("Waiting for detail area")
|
||||||
this.log("Waiting for detail area")
|
await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info")
|
||||||
await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info")
|
|
||||||
})
|
|
||||||
|
|
||||||
this.log("Waiting for any item to appear in chat")
|
|
||||||
try {
|
|
||||||
await this.page.waitForSelector("#_chat_room_msg_list div", {timeout: 2000})
|
|
||||||
|
|
||||||
this.log("Waiting for chat to stabilize")
|
|
||||||
await this.page.evaluate(() => window.__mautrixController.waitForMessageListStability())
|
|
||||||
} catch (e) {
|
|
||||||
this.log("No messages in chat found. Maybe no messages were ever sent yet?")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hadMsgListObserver) {
|
if (hadMsgListObserver) {
|
||||||
this.log("Restoring msg list observer")
|
this.log("Restoring msg list observer")
|
||||||
|
@ -594,7 +499,7 @@ export default class MessagesPuppeteer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getChatInfoUnsafe(chatID, forceView) {
|
async _getChatInfoUnsafe(chatID) {
|
||||||
const chatListInfo = await this.page.$eval(this._listItemSelector(chatID),
|
const chatListInfo = await this.page.$eval(this._listItemSelector(chatID),
|
||||||
(element, chatID) => window.__mautrixController.parseChatListItem(element, chatID),
|
(element, chatID) => window.__mautrixController.parseChatListItem(element, chatID),
|
||||||
chatID)
|
chatID)
|
||||||
|
@ -616,7 +521,7 @@ export default class MessagesPuppeteer {
|
||||||
if (!isDirect) {
|
if (!isDirect) {
|
||||||
this.log("Found multi-user chat, so viewing it to get participants")
|
this.log("Found multi-user chat, so viewing it to get participants")
|
||||||
// TODO This will mark the chat as "read"!
|
// TODO This will mark the chat as "read"!
|
||||||
await this._switchChat(chatID, forceView)
|
await this._switchChat(chatID)
|
||||||
const participantList = await this.page.$("#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul")
|
const participantList = await this.page.$("#_chat_detail_area > .mdRGT02Info ul.mdRGT13Ul")
|
||||||
// TODO Is a group not actually created until a message is sent(?)
|
// 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.
|
// If so, maybe don't create a portal until there is a message.
|
||||||
|
@ -624,10 +529,6 @@ export default class MessagesPuppeteer {
|
||||||
element => window.__mautrixController.parseParticipantList(element))
|
element => window.__mautrixController.parseParticipantList(element))
|
||||||
} else {
|
} else {
|
||||||
this.log(`Found direct chat with ${chatID}`)
|
this.log(`Found direct chat with ${chatID}`)
|
||||||
if (forceView) {
|
|
||||||
this.log("Viewing chat on request")
|
|
||||||
await this._switchChat(chatID, forceView)
|
|
||||||
}
|
|
||||||
//const chatDetailArea = await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info")
|
//const chatDetailArea = await this.page.waitForSelector("#_chat_detail_area > .mdRGT02Info")
|
||||||
//await chatDetailArea.$(".MdTxtDesc02") || // 1:1 chat with custom title - get participant's real name
|
//await chatDetailArea.$(".MdTxtDesc02") || // 1:1 chat with custom title - get participant's real name
|
||||||
participants = [{
|
participants = [{
|
||||||
|
@ -650,27 +551,21 @@ export default class MessagesPuppeteer {
|
||||||
// Always present, just made visible via classes
|
// Always present, just made visible via classes
|
||||||
|
|
||||||
async _sendMessageUnsafe(chatID, text) {
|
async _sendMessageUnsafe(chatID, text) {
|
||||||
// Sync all messages in this chat first
|
await this._switchChat(chatID)
|
||||||
this._receiveMessages(chatID, await this._getMessagesUnsafe(chatID), true)
|
|
||||||
// TODO Initiate the promise in the content script
|
// TODO Initiate the promise in the content script
|
||||||
await this.page.evaluate(
|
await this.page.evaluate(
|
||||||
() => window.__mautrixController.promiseOwnMessage(5000, "time"))
|
() => window.__mautrixController.promiseOwnMessage(5000, "time"))
|
||||||
|
|
||||||
const input = await this.page.$("#_chat_room_input")
|
const input = await this.page.$("#_chat_room_input")
|
||||||
await this._interactWithPage(async () => {
|
await input.click()
|
||||||
// Live-typing in the field can have its text mismatch what was requested!!
|
await input.type(text)
|
||||||
// Probably because the input element is a div instead of a real text input...ugh!
|
await input.press("Enter")
|
||||||
// Setting its innerText directly works fine though...
|
|
||||||
await input.click()
|
|
||||||
await input.evaluate((e, text) => e.innerText = text, text)
|
|
||||||
await input.press("Enter")
|
|
||||||
})
|
|
||||||
|
|
||||||
return await this._waitForSentMessage(chatID)
|
return await this._waitForSentMessage(chatID)
|
||||||
}
|
}
|
||||||
|
|
||||||
async _sendFileUnsafe(chatID, filePath) {
|
async _sendFileUnsafe(chatID, filePath) {
|
||||||
this._receiveMessages(chatID, await this._getMessagesUnsafe(chatID), true)
|
await this._switchChat(chatID)
|
||||||
await this.page.evaluate(
|
await this.page.evaluate(
|
||||||
() => window.__mautrixController.promiseOwnMessage(
|
() => window.__mautrixController.promiseOwnMessage(
|
||||||
10000, // Use longer timeout for file uploads
|
10000, // Use longer timeout for file uploads
|
||||||
|
@ -678,15 +573,13 @@ export default class MessagesPuppeteer {
|
||||||
"#_chat_message_fail_menu"))
|
"#_chat_message_fail_menu"))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._interactWithPage(async () => {
|
this.log(`About to ask for file chooser in ${chatID}`)
|
||||||
this.log(`About to ask for file chooser in ${chatID}`)
|
const [fileChooser] = await Promise.all([
|
||||||
const [fileChooser] = await Promise.all([
|
this.page.waitForFileChooser(),
|
||||||
this.page.waitForFileChooser(),
|
this.page.click("#_chat_room_plus_btn")
|
||||||
this.page.click("#_chat_room_plus_btn")
|
])
|
||||||
])
|
this.log(`About to upload ${filePath}`)
|
||||||
this.log(`About to upload ${filePath}`)
|
await fileChooser.accept([filePath])
|
||||||
await fileChooser.accept([filePath])
|
|
||||||
})
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.log(`Failed to upload file to ${chatID}`)
|
this.log(`Failed to upload file to ${chatID}`)
|
||||||
return -1
|
return -1
|
||||||
|
@ -711,11 +604,9 @@ export default class MessagesPuppeteer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_receiveMessages(chatID, messages, skipProcessing = false) {
|
async _receiveMessages(chatID, messages) {
|
||||||
if (this.client) {
|
if (this.client) {
|
||||||
if (!skipProcessing) {
|
messages = await this._processMessages(chatID, messages)
|
||||||
messages = this._processMessages(chatID, messages)
|
|
||||||
}
|
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
this.client.sendMessage(message).catch(err =>
|
this.client.sendMessage(message).catch(err =>
|
||||||
this.error("Failed to send message", message.id, "to client:", err))
|
this.error("Failed to send message", message.id, "to client:", err))
|
||||||
|
@ -735,13 +626,11 @@ export default class MessagesPuppeteer {
|
||||||
// TODO Handle unloaded messages. Maybe scroll up
|
// TODO Handle unloaded messages. Maybe scroll up
|
||||||
// TODO This will mark the chat as "read"!
|
// TODO This will mark the chat as "read"!
|
||||||
await this._switchChat(chatID)
|
await this._switchChat(chatID)
|
||||||
// TODO Is it better to reset the notification count in _switchChat instead of here?
|
const messages = await this.page.evaluate(
|
||||||
this.numChatNotifications.set(chatID, 0)
|
|
||||||
let messages = await this.page.evaluate(
|
|
||||||
mostRecentMessage => window.__mautrixController.parseMessageList(mostRecentMessage),
|
mostRecentMessage => window.__mautrixController.parseMessageList(mostRecentMessage),
|
||||||
this.mostRecentMessages.get(chatID))
|
this.mostRecentMessages.get(chatID))
|
||||||
// Doing this before restoring the observer since it updates minID
|
// Doing this before restoring the observer since it updates minID
|
||||||
messages = this._processMessages(chatID, messages)
|
const filteredMessages = await this._processMessages(chatID, messages)
|
||||||
|
|
||||||
if (hadMsgListObserver) {
|
if (hadMsgListObserver) {
|
||||||
this.log("Restoring msg list observer")
|
this.log("Restoring msg list observer")
|
||||||
|
@ -752,10 +641,10 @@ export default class MessagesPuppeteer {
|
||||||
this.log("Not restoring msg list observer, as there never was one")
|
this.log("Not restoring msg list observer, as there never was one")
|
||||||
}
|
}
|
||||||
|
|
||||||
return messages
|
return filteredMessages
|
||||||
}
|
}
|
||||||
|
|
||||||
_processMessages(chatID, messages) {
|
async _processMessages(chatID, messages) {
|
||||||
// TODO Probably don't need minID filtering if Puppeteer context handles it now
|
// TODO Probably don't need minID filtering if Puppeteer context handles it now
|
||||||
const minID = this.mostRecentMessages.get(chatID) || 0
|
const minID = this.mostRecentMessages.get(chatID) || 0
|
||||||
const filteredMessages = messages.filter(msg => msg.id > minID && !this.sentMessageIDs.has(msg.id))
|
const filteredMessages = messages.filter(msg => msg.id > minID && !this.sentMessageIDs.has(msg.id))
|
||||||
|
@ -766,6 +655,7 @@ export default class MessagesPuppeteer {
|
||||||
this.mostRecentMessages.set(chatID, newLastID)
|
this.mostRecentMessages.set(chatID, newLastID)
|
||||||
const range = newFirstID === newLastID ? newFirstID : `${newFirstID}-${newLastID}`
|
const range = newFirstID === newLastID ? newFirstID : `${newFirstID}-${newLastID}`
|
||||||
this.log(`Loaded ${messages.length} messages in ${chatID}, got ${filteredMessages.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) {
|
for (const message of filteredMessages) {
|
||||||
message.chat_id = chatID
|
message.chat_id = chatID
|
||||||
}
|
}
|
||||||
|
@ -775,62 +665,19 @@ export default class MessagesPuppeteer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _processChatListChangeUnsafe(chatListInfo) {
|
async _processChatListChangeUnsafe(chatID) {
|
||||||
const chatID = chatListInfo.id
|
|
||||||
this.updatedChats.delete(chatID)
|
this.updatedChats.delete(chatID)
|
||||||
this.log("Processing change to", chatID)
|
this.log("Processing change to", chatID)
|
||||||
// TODO Also process name/icon changes
|
const messages = await this._getMessagesUnsafe(chatID)
|
||||||
|
if (messages.length === 0) {
|
||||||
const prevNumNotifications = this.numChatNotifications.get(chatID) || 0
|
this.log("No new messages found in", chatID)
|
||||||
const diffNumNotifications = chatListInfo.notificationCount - prevNumNotifications
|
|
||||||
|
|
||||||
if (chatListInfo.notificationCount == 0 && diffNumNotifications < 0) {
|
|
||||||
this.log("Notifications dropped--must have read messages from another LINE client, skip")
|
|
||||||
this.numChatNotifications.set(chatID, 0)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const mustSync =
|
|
||||||
// If >1, a notification was missed. Only way to get them is to view the chat.
|
|
||||||
// If == 0, might be own message...or just a shuffled chat, or something else.
|
|
||||||
// To play it safe, just sync them. Should be no harm, as they're viewed already.
|
|
||||||
diffNumNotifications != 1
|
|
||||||
// Without placeholders, some messages require visiting their chat to be synced.
|
|
||||||
|| !this.sendPlaceholders
|
|
||||||
&& (
|
|
||||||
// Can only use previews for DMs, because sender can't be found otherwise!
|
|
||||||
chatListInfo.id.charAt(0) != 'u'
|
|
||||||
// Sync when lastMsg is a canned message for a non-previewable message type.
|
|
||||||
|| chatListInfo.lastMsg.endsWith(" sent a photo.")
|
|
||||||
|| chatListInfo.lastMsg.endsWith(" sent a sticker.")
|
|
||||||
|| chatListInfo.lastMsg.endsWith(" sent a location.")
|
|
||||||
// TODO More?
|
|
||||||
)
|
|
||||||
|
|
||||||
let messages
|
|
||||||
if (!mustSync) {
|
|
||||||
messages = [{
|
|
||||||
chat_id: chatListInfo.id,
|
|
||||||
id: null, // because sidebar messages have no ID
|
|
||||||
timestamp: null, // because this message was sent right now
|
|
||||||
is_outgoing: false, // because there's no reliable way to detect own messages...
|
|
||||||
sender: null, // because there's no way to tell who sent a message
|
|
||||||
html: chatListInfo.lastMsg,
|
|
||||||
}]
|
|
||||||
this.numChatNotifications.set(chatID, chatListInfo.notificationCount)
|
|
||||||
} else {
|
|
||||||
messages = await this._getMessagesUnsafe(chatListInfo.id)
|
|
||||||
this.numChatNotifications.set(chatID, 0)
|
|
||||||
if (messages.length === 0) {
|
|
||||||
this.log("No new messages found in", chatListInfo.id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.client) {
|
if (this.client) {
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
await this.client.sendMessage(message).catch(err =>
|
await this.client.sendMessage(message).catch(err =>
|
||||||
this.error("Failed to send message", message.id || "with no ID", "to client:", err))
|
this.error("Failed to send message", message.id, "to client:", err))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.log("No client connected, not sending messages")
|
this.log("No client connected, not sending messages")
|
||||||
|
@ -838,10 +685,10 @@ export default class MessagesPuppeteer {
|
||||||
}
|
}
|
||||||
|
|
||||||
_receiveChatListChanges(changes) {
|
_receiveChatListChanges(changes) {
|
||||||
this.log(`Received chat list changes: ${changes.map(item => item.id)}`)
|
this.log("Received chat list changes:", changes)
|
||||||
for (const item of changes) {
|
for (const item of changes) {
|
||||||
if (!this.updatedChats.has(item.id)) {
|
if (!this.updatedChats.has(item)) {
|
||||||
this.updatedChats.add(item.id)
|
this.updatedChats.add(item)
|
||||||
this.taskQueue.push(() => this._processChatListChangeUnsafe(item))
|
this.taskQueue.push(() => this._processChatListChangeUnsafe(item))
|
||||||
.catch(err => this.error("Error handling chat list changes:", err))
|
.catch(err => this.error("Error handling chat list changes:", err))
|
||||||
}
|
}
|
||||||
|
@ -877,8 +724,18 @@ 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)
|
|
||||||
await this._enterText(await this.page.$("#line_login_pwd"), this.login_password)
|
// 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.page.click("button#login_btn")
|
await this.page.click("button#login_btn")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -924,7 +781,6 @@ export default class MessagesPuppeteer {
|
||||||
_onLoggedOut() {
|
_onLoggedOut() {
|
||||||
this.log("Got logged out!")
|
this.log("Got logged out!")
|
||||||
this.stopObserving()
|
this.stopObserving()
|
||||||
this.page.bringToFront()
|
|
||||||
if (this.client) {
|
if (this.client) {
|
||||||
this.client.sendLoggedOut().catch(err =>
|
this.client.sendLoggedOut().catch(err =>
|
||||||
this.error("Failed to send logout notice to client:", err))
|
this.error("Failed to send logout notice to client:", err))
|
||||||
|
|
Loading…
Reference in New Issue