Compare commits

9 Commits

Author SHA1 Message Date
6ac15333f8 Invite only on direct chat 2021-06-04 19:48:11 +09:00
92fd74afa2 Missing invite for bridgebot
Most evident at room creation, but more might need to be added
2021-06-04 19:29:44 +09:00
47a0284e81 Safety null checks for avatars 2021-06-03 20:08:03 -04:00
b06e4532a1 Use real sender name ya dingus 2021-06-03 19:37:46 -04:00
8613ad1256 Support LINE users with no discoverable ID
AKA "strangers". Should only happen to non-friends in rooms (not groups!)
2021-06-03 01:13:00 -04:00
3c5c8cd610 Allow syncing pathless avatar images
But only if a path wasn't yet found for that image
2021-05-30 19:11:39 -04:00
54099caf87 Startup fixes 2021-05-30 19:10:52 -04:00
a3195955cc Catch getting logged out
Happens when logging into Line on Chrome somewhere else
2021-05-30 17:41:28 -04:00
3cca9f9606 Too many fixes 2021-05-28 02:27:14 -04:00
9 changed files with 45 additions and 93 deletions

View File

@@ -2,7 +2,7 @@
A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer. A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer.
Fork of [https://mau.dev/tulir/mautrix-amp/](mautrix-amp). Fork of [https://mau.dev/tulir/mautrix-amp/](mautrix-amp).
## Features, roadmap, and limitations ## Features & roadmap
[ROADMAP.md](ROADMAP.md) [ROADMAP.md](ROADMAP.md)
## Setup ## Setup

View File

@@ -6,13 +6,11 @@
* [x] Images * [x] Images
* [ ] Files * [ ] Files
* [x] Stickers * [x] Stickers
* [x] Notification for message send failure
* [ ] Read receipts (currently eagerly-sent since message sync requires "reading" a chat) * [ ] Read receipts (currently eagerly-sent since message sync requires "reading" a chat)
* [ ] Room metadata changes * [ ] Room metadata changes
* [ ] Name * [ ] Name
* [ ] Avatar * [ ] Avatar
* [ ] Member events
* [ ] Invite
* [ ] Kick
* LINE → Matrix * LINE → Matrix
* [ ] Message content * [ ] Message content
* [x] Text * [x] Text
@@ -22,54 +20,45 @@
* [ ] Location * [ ] Location
* [ ] Videos * [ ] Videos
* [x] Stickers * [x] Stickers
* [x] Emoji * [x] Sticons
* [x] Single
* [x] Multiple or mixed with text
* [x] EmojiOne
* [ ] Message unsend * [ ] Message unsend
* [ ] Read receipts * [ ] Read receipts
* [x] For most recently active chat * [x] For most recently active chat
* [ ] For any chat * [ ] For any chat
* [x] User metadata * [x] User metadata
* [ ] Name * [ ] Name
* [x] On sync * [x] On initial sync
* [ ] On change * [ ] On change
* [ ] Avatar * [ ] Avatar
* [x] On sync * [x] On initial sync
* [ ] On change * [ ] On change
* [ ] Chat metadata * [ ] Chat metadata
* [ ] Name * [ ] Name
* [x] On sync * [x] On initial sync
* [ ] On change * [ ] On change
* [ ] Icon * [ ] Icon
* [x] On sync * [x] On initial sync
* [ ] On change * [ ] On change
* [ ] Message history * [x] Message history
* [x] When creating portal * [x] When creating portal
* [x] Missed messages * [x] Missed messages
* [x] Message timestamps * [x] Message timestamps
* [ ] As many messages that are visible in LINE extension
* [x] Chat types * [x] Chat types
* [x] Direct chats * [x] Direct chats
* [x] Groups (named chats) * [x] Groups (named chats)
* [x] Rooms (unnamed chats / "multi-user direct chats") * [x] Rooms (unnamed chats / "multi-user direct chats")
* [ ] Membership actions * [ ] Membership actions
* [ ] Join * [x] Add member
* [x] When message is sent by new participant * [ ] Remove member
* [x] On sync * [ ] Block
* [ ] At join time
* [ ] Leave
* [x] On sync
* [ ] At leave time
* [ ] Invite
* [ ] Remove
* [ ] Friend actions
* [ ] Add friend
* [ ] Block user
* [ ] Unblock user
* Misc * Misc
* [x] Automatic portal creation * [x] Automatic portal creation
* [x] At startup * [x] At startup
* [x] When receiving invite or message * [x] When receiving invite or message
* [ ] When sending message in new chat from LINE app * [ ] When sending message in new chat from LINE app
* [x] Notification for message send failure
* [ ] Provisioning API for logging in * [ ] Provisioning API for logging in
* [x] Use bridge bot for messages sent from LINE app (when double-puppeting is disabled and `bridge.invite_own_puppet_to_pm` is enabled) * [x] Use bridge bot for messages sent from LINE app (when double-puppeting is disabled and `bridge.invite_own_puppet_to_pm` is enabled)
* [x] Use own Matrix account for messages sent from LINE app (when double-puppeting is enabled) * [x] Use own Matrix account for messages sent from LINE app (when double-puppeting is enabled)
@@ -77,8 +66,8 @@
* [ ] Multiple bridge users * [ ] Multiple bridge users
* [ ] Relay bridging * [ ] Relay bridging
# Missing features ## Missing features
## Missing from LINE ### Missing from LINE
* Typing notifications * Typing notifications
* Message edits * Message edits
* Formatted messages * Formatted messages
@@ -86,22 +75,13 @@
* Timestamped read receipts * Timestamped read receipts
* Read receipts between users other than yourself * Read receipts between users other than yourself
## Missing from LINE on Chrome ### Missing from LINE on Chrome
* Unlimited message history
* Messages that are very old may not be available in LINE on Chrome at all, even after a full sync
* Voice/video calls
* No notification is sent when a call begins
* When a call ends, an automated message of "Your OS version doesn't support this feature" is sent as an ordinary text message from the user who began the call
* Message redaction (delete/unsend) * Message redaction (delete/unsend)
* But messages unsent from other LINE clients do disappear from LINE on Chrome
* Replies * Replies
* Appear as ordinary messages
* Mentions
* Appear as ordinary text
* Audio message sending * Audio message sending
* But audio messages can be received
* Location sending * Location sending
* But locations can be received * Voice/video calls
* Unlimited message history
## Missing from matrix-puppeteer-line ### Missing from matrix-puppeteer-line
* TODO * TODO

View File

@@ -55,11 +55,6 @@ async def login_do(evt: CommandEvent, gen: AsyncGenerator[Tuple[str, str], None]
failure = False failure = False
async for item in gen: async for item in gen:
if item[0] == "qr": if item[0] == "qr":
message = "Open LINE on your primary device and scan this QR code:"
content = TextMessageEventContent(body=message, msgtype=MessageType.NOTICE)
content.set_reply(evt.event_id)
await evt.az.intent.send_message(evt.room_id, content)
url = item[1] url = item[1]
buffer = io.BytesIO() buffer = io.BytesIO()
image = qrcode.make(url) image = qrcode.make(url)
@@ -74,6 +69,7 @@ async def login_do(evt: CommandEvent, gen: AsyncGenerator[Tuple[str, str], None]
content.set_edit(qr_event_id) content.set_edit(qr_event_id)
await evt.az.intent.send_message(evt.room_id, content) await evt.az.intent.send_message(evt.room_id, content)
else: else:
content.set_reply(evt.event_id)
qr_event_id = await evt.az.intent.send_message(evt.room_id, content) qr_event_id = await evt.az.intent.send_message(evt.room_id, content)
elif item[0] == "pin": elif item[0] == "pin":
pin = item[1] pin = item[1]
@@ -83,10 +79,9 @@ async def login_do(evt: CommandEvent, gen: AsyncGenerator[Tuple[str, str], None]
content.set_edit(pin_event_id) content.set_edit(pin_event_id)
await evt.az.intent.send_message(evt.room_id, content) await evt.az.intent.send_message(evt.room_id, content)
else: else:
content.set_reply(evt.event_id)
pin_event_id = await evt.az.intent.send_message(evt.room_id, content) pin_event_id = await evt.az.intent.send_message(evt.room_id, content)
elif item[0] == "login_success": elif item[0] in ("failure", "error"):
await evt.reply("Successfully logged in, waiting for LINE to load...")
elif item[0] in ("login_failure", "error"):
# TODO Handle errors differently? # TODO Handle errors differently?
failure = True failure = True
reason = item[1] reason = item[1]
@@ -96,7 +91,7 @@ async def login_do(evt: CommandEvent, gen: AsyncGenerator[Tuple[str, str], None]
# else: pass # else: pass
if not failure and evt.sender.command_status: if not failure and evt.sender.command_status:
await evt.reply("LINE loading complete") await evt.reply("Successfully logged in")
await evt.sender.sync() await evt.sender.sync()
# else command was cancelled or failed. Don't post message about it, "cancel" command or failure did already # else command was cancelled or failed. Don't post message about it, "cancel" command or failure did already
evt.sender.command_status = None evt.sender.command_status = None

View File

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

View File

@@ -119,12 +119,8 @@ class Client(RPCClient):
data.append(("pin", req["pin"])) data.append(("pin", req["pin"]))
event.set() event.set()
async def success_handler(req: LoginCommand) -> None:
data.append(("login_success", None))
event.set()
async def failure_handler(req: LoginCommand) -> None: async def failure_handler(req: LoginCommand) -> None:
data.append(("login_failure", req.get("reason"))) data.append(("failure", req.get("reason")))
event.set() event.set()
async def cancel_watcher() -> None: async def cancel_watcher() -> None:
@@ -149,8 +145,7 @@ class Client(RPCClient):
self.add_event_handler("qr", qr_handler) self.add_event_handler("qr", qr_handler)
self.add_event_handler("pin", pin_handler) self.add_event_handler("pin", pin_handler)
self.add_event_handler("login_success", success_handler) self.add_event_handler("failure", failure_handler)
self.add_event_handler("login_failure", failure_handler)
try: try:
while True: while True:
await event.wait() await event.wait()
@@ -163,5 +158,4 @@ class Client(RPCClient):
finally: finally:
self.remove_event_handler("qr", qr_handler) self.remove_event_handler("qr", qr_handler)
self.remove_event_handler("pin", pin_handler) self.remove_event_handler("pin", pin_handler)
self.remove_event_handler("login_success", success_handler) self.remove_event_handler("failure", failure_handler)
self.remove_event_handler("login_failure", failure_handler)

View File

@@ -177,7 +177,7 @@ class User(DBUser, BaseUser):
await portal.handle_remote_receipt(receipt) await portal.handle_remote_receipt(receipt)
async def handle_logged_out(self) -> None: async def handle_logged_out(self) -> None:
await self.send_bridge_notice("Logged out of LINE. Please run either \"login-qr\" or \"login-email\" to log back in.") await self.send_bridge_notice("Logged out of LINE. Please run the \"login\" command to log back in.")
if self._connection_check_task: if self._connection_check_task:
self._connection_check_task.cancel() self._connection_check_task.cancel()
self._connection_check_task = None self._connection_check_task = None

View File

@@ -135,19 +135,11 @@ export default class Client {
}) })
} }
sendLoginSuccess() { sendFailure(reason) {
this.log("Sending login success to client") this.log(`Sending failure to client${reason ? `: "${reason}"` : ""}`)
return this._write({ return this._write({
id: --this.notificationID, id: --this.notificationID,
command: "login_success", command: "failure",
})
}
sendLoginFailure(reason) {
this.log(`Sending login failure to client${reason ? `: "${reason}"` : ""}`)
return this._write({
id: --this.notificationID,
command: "login_failure",
reason, reason,
}) })
} }

View File

@@ -126,6 +126,7 @@ export default class MessagesPuppeteer {
} }
this.log("Injecting content script") this.log("Injecting content script")
await this.page.addScriptTag({ path: "./src/contentscript.js", type: "module" }) await this.page.addScriptTag({ path: "./src/contentscript.js", type: "module" })
} }
/** /**
@@ -207,7 +208,7 @@ export default class MessagesPuppeteer {
} }
const result = await Promise.race([ const result = await Promise.race([
() => this.page.waitForSelector("#mainApp:not(.MdNonDisp)", {timeout: 2000}) () => this.page.waitForSelector("#wrap_message_sync", {timeout: 2000})
.then(value => { .then(value => {
loginSuccess = true loginSuccess = true
return value return value
@@ -227,13 +228,11 @@ export default class MessagesPuppeteer {
delete this.login_email delete this.login_email
delete this.login_password delete this.login_password
const messageSyncElement = loginSuccess ? await this.page.waitForSelector("#wrap_message_sync") : null if (!loginSuccess) {
if (!loginSuccess || !messageSyncElement) {
this._sendLoginFailure(result) this._sendLoginFailure(result)
return return
} }
this._sendLoginSuccess()
this.log("Waiting for sync") this.log("Waiting for sync")
try { try {
await this.page.waitForFunction( await this.page.waitForFunction(
@@ -244,12 +243,14 @@ export default class MessagesPuppeteer {
// TODO Sometimes it gets stuck at 99%...?? // TODO Sometimes it gets stuck at 99%...??
}, },
{timeout: 10000}, // Assume 10 seconds is long enough {timeout: 10000}, // Assume 10 seconds is long enough
messageSyncElement) result)
} catch (err) { } catch (err) {
//this._sendLoginFailure(`Failed to sync: ${err}`) //this._sendLoginFailure(`Failed to sync: ${err}`)
this.log("LINE's sync took too long, assume it's fine and carry on...") this.log("LINE's sync took too long, assume it's fine and carry on...")
} finally { } finally {
const syncText = await messageSyncElement.evaluate(e => e.innerText) const syncText = await this.page.evaluate(
messageSyncElement => messageSyncElement.innerText,
result)
this.log(`Final sync text is: "${syncText}"`) this.log(`Final sync text is: "${syncText}"`)
} }
@@ -758,24 +759,14 @@ export default class MessagesPuppeteer {
} }
} }
_sendLoginSuccess() {
this.error("Login success")
if (this.client) {
this.client.sendLoginSuccess().catch(err =>
this.error("Failed to send login success to client:", err))
} else {
this.log("No client connected, not sending login success")
}
}
_sendLoginFailure(reason) { _sendLoginFailure(reason) {
this.loginRunning = false this.loginRunning = false
this.error(`Login failure: ${reason ? reason : "cancelled"}`) this.error(`Login failure: ${reason ? reason : "cancelled"}`)
if (this.client) { if (this.client) {
this.client.sendLoginFailure(reason).catch(err => this.client.sendFailure(reason).catch(err =>
this.error("Failed to send login failure to client:", err)) this.error("Failed to send failure reason to client:", err))
} else { } else {
this.log("No client connected, not sending login failure") this.log("No client connected, not sending failure reason")
} }
} }