forked from fair/matrix-puppeteer-line
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			3286d7e6e2
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1fffbc625c | |||
| c5eea7b50b | 
| @@ -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 | ## Features, roadmap, and limitations | ||||||
| [ROADMAP.md](ROADMAP.md) | [ROADMAP.md](ROADMAP.md) | ||||||
|  |  | ||||||
| ## Setup | ## Setup | ||||||
|   | |||||||
							
								
								
									
										64
									
								
								ROADMAP.md
									
									
									
									
									
								
							
							
						
						
									
										64
									
								
								ROADMAP.md
									
									
									
									
									
								
							| @@ -6,11 +6,13 @@ | |||||||
|     * [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 | ||||||
| @@ -20,45 +22,54 @@ | |||||||
|     * [ ] Location |     * [ ] Location | ||||||
|     * [ ] Videos |     * [ ] Videos | ||||||
|     * [x] Stickers |     * [x] Stickers | ||||||
|     * [x] Sticons |     * [x] Emoji | ||||||
|       * [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 initial sync |       * [x] On sync | ||||||
|       * [ ] On change |       * [ ] On change | ||||||
|     * [ ] Avatar |     * [ ] Avatar | ||||||
|       * [x] On initial sync |       * [x] On sync | ||||||
|       * [ ] On change |       * [ ] On change | ||||||
|   * [ ] Chat metadata |   * [ ] Chat metadata | ||||||
|     * [ ] Name |     * [ ] Name | ||||||
|       * [x] On initial sync |       * [x] On sync | ||||||
|       * [ ] On change |       * [ ] On change | ||||||
|     * [ ] Icon |     * [ ] Icon | ||||||
|       * [x] On initial sync |       * [x] On sync | ||||||
|       * [ ] On change |       * [ ] On change | ||||||
|   * [x] Message history |   * [ ] 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 | ||||||
|     * [x] Add member |     * [ ] Join | ||||||
|     * [ ] Remove member |       * [x] When message is sent by new participant | ||||||
|     * [ ] Block |       * [x] On sync | ||||||
|  |       * [ ] 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) | ||||||
| @@ -66,8 +77,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 | ||||||
| @@ -75,13 +86,22 @@ | |||||||
| * 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 | ||||||
| * Message redaction (delete/unsend) |  | ||||||
| * Replies |  | ||||||
| * Audio message sending |  | ||||||
| * Location sending |  | ||||||
| * Voice/video calls |  | ||||||
| * Unlimited message history | * 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) | ||||||
|  |     * But messages unsent from other LINE clients do disappear from LINE on Chrome | ||||||
|  | * Replies | ||||||
|  |     * Appear as ordinary messages | ||||||
|  | * Mentions | ||||||
|  |     * Appear as ordinary text | ||||||
|  | * Audio message sending | ||||||
|  |     * But audio messages can be received | ||||||
|  | * Location sending | ||||||
|  |     * But locations can be received | ||||||
|  |  | ||||||
| ### Missing from matrix-puppeteer-line | ## Missing from matrix-puppeteer-line | ||||||
| * TODO | * TODO | ||||||
|   | |||||||
| @@ -55,6 +55,11 @@ 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) | ||||||
| @@ -69,7 +74,6 @@ 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] | ||||||
| @@ -79,9 +83,10 @@ 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] in ("failure", "error"): |         elif item[0] == "login_success": | ||||||
|  |             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] | ||||||
| @@ -91,7 +96,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("Successfully logged in") |         await evt.reply("LINE loading complete") | ||||||
|         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 | ||||||
|   | |||||||
| @@ -119,8 +119,12 @@ 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(("failure", req.get("reason"))) |             data.append(("login_failure", req.get("reason"))) | ||||||
|             event.set() |             event.set() | ||||||
|  |  | ||||||
|         async def cancel_watcher() -> None: |         async def cancel_watcher() -> None: | ||||||
| @@ -145,7 +149,8 @@ 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("failure", failure_handler) |         self.add_event_handler("login_success", success_handler) | ||||||
|  |         self.add_event_handler("login_failure", failure_handler) | ||||||
|         try: |         try: | ||||||
|             while True: |             while True: | ||||||
|                 await event.wait() |                 await event.wait() | ||||||
| @@ -158,4 +163,5 @@ 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("failure", failure_handler) |             self.remove_event_handler("login_success", success_handler) | ||||||
|  |             self.remove_event_handler("login_failure", failure_handler) | ||||||
|   | |||||||
| @@ -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 the \"login\" command to log back in.") |         await self.send_bridge_notice("Logged out of LINE. Please run either \"login-qr\" or \"login-email\" 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 | ||||||
|   | |||||||
| @@ -135,11 +135,19 @@ export default class Client { | |||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	sendFailure(reason) { | 	sendLoginSuccess() { | ||||||
| 		this.log(`Sending failure to client${reason ? `: "${reason}"` : ""}`) | 		this.log("Sending login success to client") | ||||||
| 		return this._write({ | 		return this._write({ | ||||||
| 			id: --this.notificationID, | 			id: --this.notificationID, | ||||||
| 			command: "failure", | 			command: "login_success", | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sendLoginFailure(reason) { | ||||||
|  | 		this.log(`Sending login failure to client${reason ? `: "${reason}"` : ""}`) | ||||||
|  | 		return this._write({ | ||||||
|  | 			id: --this.notificationID, | ||||||
|  | 			command: "login_failure", | ||||||
| 			reason, | 			reason, | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -207,7 +207,7 @@ export default class MessagesPuppeteer { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		const result = await Promise.race([ | 		const result = await Promise.race([ | ||||||
| 			() => this.page.waitForSelector("#wrap_message_sync", {timeout: 2000}) | 			() => this.page.waitForSelector("#mainApp:not(.MdNonDisp)", {timeout: 2000}) | ||||||
| 				.then(value => { | 				.then(value => { | ||||||
| 					loginSuccess = true | 					loginSuccess = true | ||||||
| 					return value | 					return value | ||||||
| @@ -227,11 +227,13 @@ export default class MessagesPuppeteer { | |||||||
| 		delete this.login_email | 		delete this.login_email | ||||||
| 		delete this.login_password | 		delete this.login_password | ||||||
|  |  | ||||||
| 		if (!loginSuccess) { | 		const messageSyncElement = loginSuccess ? await this.page.waitForSelector("#wrap_message_sync") : null | ||||||
|  | 		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( | ||||||
| @@ -242,14 +244,12 @@ 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 | ||||||
| 				result) | 				messageSyncElement) | ||||||
| 		} 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 this.page.evaluate( | 			const syncText = await messageSyncElement.evaluate(e => e.innerText) | ||||||
| 				messageSyncElement => messageSyncElement.innerText, |  | ||||||
| 				result) |  | ||||||
| 			this.log(`Final sync text is: "${syncText}"`) | 			this.log(`Final sync text is: "${syncText}"`) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -758,14 +758,24 @@ 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.sendFailure(reason).catch(err => | 			this.client.sendLoginFailure(reason).catch(err => | ||||||
| 				this.error("Failed to send failure reason to client:", err)) | 				this.error("Failed to send login failure to client:", err)) | ||||||
| 		} else { | 		} else { | ||||||
| 			this.log("No client connected, not sending failure reason") | 			this.log("No client connected, not sending login failure") | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user