forked from fair/matrix-puppeteer-line
Support auto-login
This commit is contained in:
parent
0202dd182a
commit
e02c91c093
|
@ -13,7 +13,7 @@
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
from typing import Optional, AsyncGenerator, Tuple
|
from typing import Optional, AsyncGenerator, Tuple, TYPE_CHECKING
|
||||||
import io
|
import io
|
||||||
|
|
||||||
import qrcode
|
import qrcode
|
||||||
|
@ -26,8 +26,13 @@ from .typehint import CommandEvent
|
||||||
|
|
||||||
SECTION_AUTH = HelpSection("Authentication", 10, "")
|
SECTION_AUTH = HelpSection("Authentication", 10, "")
|
||||||
|
|
||||||
|
from ..db import LoginCredential
|
||||||
|
|
||||||
async def login_prep(evt: CommandEvent, login_type: str) -> bool:
|
if TYPE_CHECKING:
|
||||||
|
from ..user import User
|
||||||
|
|
||||||
|
|
||||||
|
async def _login_prep(evt: CommandEvent, login_type: str) -> bool:
|
||||||
status = await evt.sender.client.start()
|
status = await evt.sender.client.start()
|
||||||
if status.is_logged_in:
|
if status.is_logged_in:
|
||||||
await evt.reply("You're already logged in")
|
await evt.reply("You're already logged in")
|
||||||
|
@ -49,16 +54,36 @@ async def login_prep(evt: CommandEvent, login_type: str) -> bool:
|
||||||
}
|
}
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def login_do(evt: CommandEvent, gen: AsyncGenerator[Tuple[str, str], None]) -> None:
|
async def _login_do(
|
||||||
|
gen: AsyncGenerator[Tuple[str, str], None],
|
||||||
|
*,
|
||||||
|
evt: Optional[CommandEvent] = None,
|
||||||
|
sender: Optional["User"] = None,
|
||||||
|
) -> bool:
|
||||||
qr_event_id: Optional[EventID] = None
|
qr_event_id: Optional[EventID] = None
|
||||||
pin_event_id: Optional[EventID] = None
|
pin_event_id: Optional[EventID] = None
|
||||||
failure = False
|
failure = False
|
||||||
|
|
||||||
|
if not evt and not sender:
|
||||||
|
raise ValueError("Must set either a CommandEvent or a User")
|
||||||
|
if evt:
|
||||||
|
sender = evt.sender
|
||||||
|
az = evt.az
|
||||||
|
room_id = evt.room_id
|
||||||
|
else:
|
||||||
|
az = sender.az
|
||||||
|
room_id = sender.notice_room
|
||||||
|
if not room_id:
|
||||||
|
sender.log.warning("Cannot auto-loggin: must have a notice room to do so")
|
||||||
|
return False
|
||||||
|
|
||||||
async for item in gen:
|
async for item in gen:
|
||||||
if item[0] == "qr":
|
if item[0] == "qr":
|
||||||
message = "Open LINE on your smartphone and scan this QR code:"
|
message = "Open LINE on your smartphone and scan this QR code:"
|
||||||
content = TextMessageEventContent(body=message, msgtype=MessageType.NOTICE)
|
content = TextMessageEventContent(body=message, msgtype=MessageType.NOTICE)
|
||||||
content.set_reply(evt.event_id)
|
if evt:
|
||||||
await evt.az.intent.send_message(evt.room_id, content)
|
content.set_reply(evt.event_id)
|
||||||
|
await az.intent.send_message(room_id, content)
|
||||||
|
|
||||||
url = item[1]
|
url = item[1]
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
|
@ -66,61 +91,131 @@ async def login_do(evt: CommandEvent, gen: AsyncGenerator[Tuple[str, str], None]
|
||||||
size = image.pixel_size
|
size = image.pixel_size
|
||||||
image.save(buffer, "PNG")
|
image.save(buffer, "PNG")
|
||||||
qr = buffer.getvalue()
|
qr = buffer.getvalue()
|
||||||
mxc = await evt.az.intent.upload_media(qr, "image/png", "login-qr.png", len(qr))
|
mxc = await az.intent.upload_media(qr, "image/png", "login-qr.png", len(qr))
|
||||||
content = MediaMessageEventContent(body=url, url=mxc, msgtype=MessageType.IMAGE,
|
content = MediaMessageEventContent(body=url, url=mxc, msgtype=MessageType.IMAGE,
|
||||||
info=ImageInfo(mimetype="image/png", size=len(qr),
|
info=ImageInfo(mimetype="image/png", size=len(qr),
|
||||||
width=size, height=size))
|
width=size, height=size))
|
||||||
if qr_event_id:
|
if qr_event_id:
|
||||||
content.set_edit(qr_event_id)
|
content.set_edit(qr_event_id)
|
||||||
await evt.az.intent.send_message(evt.room_id, content)
|
await az.intent.send_message(room_id, content)
|
||||||
else:
|
else:
|
||||||
qr_event_id = await evt.az.intent.send_message(evt.room_id, content)
|
qr_event_id = await az.intent.send_message(room_id, content)
|
||||||
elif item[0] == "pin":
|
elif item[0] == "pin":
|
||||||
pin = item[1]
|
pin = item[1]
|
||||||
message = f"Enter this PIN in LINE on your smartphone:\n{pin}"
|
message = f"Enter this PIN in LINE on your smartphone:\n{pin}"
|
||||||
content = TextMessageEventContent(body=message, msgtype=MessageType.NOTICE)
|
content = TextMessageEventContent(body=message, msgtype=MessageType.NOTICE)
|
||||||
if pin_event_id:
|
if pin_event_id:
|
||||||
content.set_edit(pin_event_id)
|
content.set_edit(pin_event_id)
|
||||||
await evt.az.intent.send_message(evt.room_id, content)
|
await az.intent.send_message(room_id, content)
|
||||||
else:
|
else:
|
||||||
pin_event_id = await evt.az.intent.send_message(evt.room_id, content)
|
pin_event_id = await az.intent.send_message(room_id, content)
|
||||||
elif item[0] == "login_success":
|
elif item[0] == "login_success":
|
||||||
await evt.reply("Successfully logged in, waiting for LINE to load...")
|
await az.intent.send_notice(room_id, "Successfully logged in, waiting for LINE to load...")
|
||||||
elif item[0] in ("login_failure", "error"):
|
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]
|
||||||
if reason:
|
if reason:
|
||||||
content = TextMessageEventContent(body=reason, msgtype=MessageType.NOTICE)
|
await az.intent.send_notice(room_id, reason)
|
||||||
await evt.az.intent.send_message(evt.room_id, content)
|
|
||||||
# else: pass
|
# else: pass
|
||||||
|
|
||||||
if not failure and evt.sender.command_status:
|
login_success = not failure and sender.command_status
|
||||||
await evt.reply("LINE loading complete")
|
if login_success:
|
||||||
await evt.sender.sync()
|
await az.intent.send_notice(room_id, "LINE loading complete")
|
||||||
|
await 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
|
sender.command_status = None
|
||||||
|
return login_success
|
||||||
|
|
||||||
@command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH,
|
@command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH,
|
||||||
help_text="Log into LINE via QR code")
|
help_text="Log into LINE via QR code")
|
||||||
async def login_qr(evt: CommandEvent) -> None:
|
async def login_qr(evt: CommandEvent) -> None:
|
||||||
if not await login_prep(evt, "qr"):
|
if not await _login_prep(evt, "qr"):
|
||||||
return
|
return
|
||||||
gen = evt.sender.client.login(evt.sender)
|
gen = evt.sender.client.login(evt.sender)
|
||||||
await login_do(evt, gen)
|
await _login_do(gen, evt=evt)
|
||||||
|
|
||||||
@command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH,
|
@command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH,
|
||||||
help_text="Log into LINE via email/password",
|
help_text="Log into LINE via email/password, and optionally save credentials for auto-login",
|
||||||
help_args="<_email_> <_password_>")
|
help_args="[--save] <_email_> <_password_>")
|
||||||
async def login_email(evt: CommandEvent) -> None:
|
async def login_email(evt: CommandEvent) -> None:
|
||||||
await evt.az.intent.redact(evt.room_id, evt.event_id)
|
await evt.az.intent.redact(evt.room_id, evt.event_id)
|
||||||
|
if evt.args and evt.args[0] == "--save":
|
||||||
|
save = True
|
||||||
|
evt.args.pop(0)
|
||||||
|
else:
|
||||||
|
save = False
|
||||||
if len(evt.args) != 2:
|
if len(evt.args) != 2:
|
||||||
await evt.reply("Usage: `$cmdprefix+sp login-email <email> <password>`")
|
await evt.reply("Usage: `$cmdprefix+sp login-email [--save] <email> <password>`")
|
||||||
return
|
return
|
||||||
if not await login_prep(evt, "email"):
|
if not await _login_prep(evt, "email"):
|
||||||
return
|
return
|
||||||
await evt.reply("Logging in...")
|
await evt.reply("Logging in...")
|
||||||
|
login_data = {
|
||||||
|
"email": evt.args[0],
|
||||||
|
"password": evt.args[1]
|
||||||
|
}
|
||||||
gen = evt.sender.client.login(
|
gen = evt.sender.client.login(
|
||||||
evt.sender,
|
evt.sender,
|
||||||
login_data=dict(email=evt.args[0], password=evt.args[1]))
|
login_data=login_data
|
||||||
await login_do(evt, gen)
|
)
|
||||||
|
login_success = await _login_do(gen, evt=evt)
|
||||||
|
if login_success and save:
|
||||||
|
if not evt.sender.notice_room:
|
||||||
|
await evt.reply("WARNING: You do not have a notice room, but auto-login requires one.")
|
||||||
|
await _save_password_helper(evt)
|
||||||
|
|
||||||
|
async def auto_login(sender: "User") -> bool:
|
||||||
|
status = await sender.client.start()
|
||||||
|
if status.is_logged_in:
|
||||||
|
return True
|
||||||
|
if sender.command_status is not None:
|
||||||
|
return False
|
||||||
|
creds = await LoginCredential.get_by_mxid(sender.mxid)
|
||||||
|
if not creds:
|
||||||
|
return False
|
||||||
|
sender.command_status = {
|
||||||
|
"action": "Login",
|
||||||
|
"login_type": "email",
|
||||||
|
}
|
||||||
|
gen = sender.client.login(
|
||||||
|
sender,
|
||||||
|
login_data={
|
||||||
|
"email": creds.email,
|
||||||
|
"password": creds.password,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return await _login_do(gen, sender=sender)
|
||||||
|
|
||||||
|
@command_handler(needs_auth=True, management_only=True, help_section=SECTION_AUTH,
|
||||||
|
help_text="Remember email/password credentials for auto-login",
|
||||||
|
help_args="<_email_> <_password_>")
|
||||||
|
async def save_password(evt: CommandEvent) -> None:
|
||||||
|
await evt.az.intent.redact(evt.room_id, evt.event_id)
|
||||||
|
if len(evt.args) != 2:
|
||||||
|
await evt.reply("Usage: `$cmdprefix+sp save_password <email> <password>`")
|
||||||
|
return
|
||||||
|
await _save_password_helper(evt)
|
||||||
|
|
||||||
|
async def _save_password_helper(evt: CommandEvent) -> None:
|
||||||
|
creds = await LoginCredential.get_by_mxid(evt.sender.mxid)
|
||||||
|
if creds:
|
||||||
|
creds.email = evt.args[0]
|
||||||
|
creds.password = evt.args[1]
|
||||||
|
await creds.update()
|
||||||
|
else:
|
||||||
|
await LoginCredential(evt.sender.mxid, email=evt.args[0], password=evt.args[1]).insert()
|
||||||
|
await evt.reply("Login email/password saved, and will be used to log you back in if your LINE connection ends.")
|
||||||
|
|
||||||
|
@command_handler(needs_auth=False, management_only=True, help_section=SECTION_AUTH,
|
||||||
|
help_text="Delete saved email/password credentials")
|
||||||
|
async def forget_password(evt: CommandEvent) -> None:
|
||||||
|
creds = await LoginCredential.get_by_mxid(evt.sender.mxid)
|
||||||
|
if not creds:
|
||||||
|
await evt.reply("The bridge wasn't storing your email/password, so there was nothing to forget.")
|
||||||
|
else:
|
||||||
|
await creds.delete()
|
||||||
|
await evt.reply(
|
||||||
|
"This bridge is no longer storing your email/password. \n"
|
||||||
|
"You will have to log in manually the next time your LINE connection ends."
|
||||||
|
)
|
||||||
|
|
|
@ -9,11 +9,23 @@ from .message import Message
|
||||||
from .media import Media
|
from .media import Media
|
||||||
from .receipt import Receipt
|
from .receipt import Receipt
|
||||||
from .receipt_reaction import ReceiptReaction
|
from .receipt_reaction import ReceiptReaction
|
||||||
|
from .login_credential import LoginCredential
|
||||||
|
|
||||||
|
|
||||||
def init(db: Database) -> None:
|
def init(db: Database) -> None:
|
||||||
for table in (User, Puppet, Stranger, Portal, Message, Media, Receipt, ReceiptReaction):
|
for table in (User, Puppet, Stranger, Portal, Message, Media, Receipt, ReceiptReaction, LoginCredential):
|
||||||
table.db = db
|
table.db = db
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["upgrade_table", "User", "Puppet", "Stranger", "Portal", "Message", "Media", "Receipt", "ReceiptReaction"]
|
__all__ = [
|
||||||
|
"upgrade_table",
|
||||||
|
"User",
|
||||||
|
"Puppet",
|
||||||
|
"Stranger",
|
||||||
|
"Portal",
|
||||||
|
"Message",
|
||||||
|
"Media",
|
||||||
|
"Receipt",
|
||||||
|
"ReceiptReaction",
|
||||||
|
"LoginCredential"
|
||||||
|
]
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
# matrix-puppeteer-line - A very hacky Matrix-LINE bridge based on running LINE's Chrome extension in Puppeteer
|
||||||
|
# Copyright (C) 2020-2022 Tulir Asokan, Andrew Ferrazzutti
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
from typing import Optional, ClassVar, TYPE_CHECKING
|
||||||
|
|
||||||
|
from attr import dataclass
|
||||||
|
|
||||||
|
from mautrix.types import UserID
|
||||||
|
from mautrix.util.async_db import Database
|
||||||
|
|
||||||
|
fake_db = Database("") if TYPE_CHECKING else None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LoginCredential:
|
||||||
|
db: ClassVar[Database] = fake_db
|
||||||
|
|
||||||
|
mxid: UserID
|
||||||
|
email: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
async def insert(self) -> None:
|
||||||
|
q = ("INSERT INTO login_credential (mxid, email, password) "
|
||||||
|
"VALUES ($1, $2, $3)")
|
||||||
|
await self.db.execute(q, self.mxid, self.email, self.password)
|
||||||
|
|
||||||
|
async def update(self) -> None:
|
||||||
|
await self.db.execute("UPDATE login_credential SET email=$2, password=$3 WHERE mxid=$1",
|
||||||
|
self.mxid, self.email, self.password)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_by_mxid(cls, mxid: UserID) -> Optional["LoginCredential"]:
|
||||||
|
q = ("SELECT mxid, email, password "
|
||||||
|
"FROM login_credential WHERE mxid=$1")
|
||||||
|
row = await cls.db.fetchrow(q, mxid)
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
return cls(**row)
|
||||||
|
|
||||||
|
async def delete(self) -> None:
|
||||||
|
await self.db.execute("DELETE FROM login_credential WHERE mxid=$1",
|
||||||
|
self.mxid)
|
|
@ -170,3 +170,16 @@ async def upgrade_latest_read_receipts(conn: Connection) -> None:
|
||||||
@upgrade_table.register(description="Allow messages with no mxid")
|
@upgrade_table.register(description="Allow messages with no mxid")
|
||||||
async def upgrade_nomxid_msgs(conn: Connection) -> None:
|
async def upgrade_nomxid_msgs(conn: Connection) -> None:
|
||||||
await conn.execute("ALTER TABLE message ALTER COLUMN mxid DROP NOT NULL")
|
await conn.execute("ALTER TABLE message ALTER COLUMN mxid DROP NOT NULL")
|
||||||
|
|
||||||
|
|
||||||
|
@upgrade_table.register(description="Allow storing email/password login credentials")
|
||||||
|
async def upgrade_login_credentials(conn: Connection) -> None:
|
||||||
|
await conn.execute("""CREATE TABLE IF NOT EXISTS login_credential (
|
||||||
|
mxid TEXT PRIMARY KEY,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (mxid)
|
||||||
|
REFERENCES "user" (mxid)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
)""")
|
|
@ -22,6 +22,8 @@ from mautrix.types import UserID, RoomID
|
||||||
from mautrix.appservice import AppService, IntentAPI
|
from mautrix.appservice import AppService, IntentAPI
|
||||||
from mautrix.util.opt_prometheus import Gauge
|
from mautrix.util.opt_prometheus import Gauge
|
||||||
|
|
||||||
|
from .commands.auth import auto_login
|
||||||
|
|
||||||
from .db import User as DBUser, Portal as DBPortal, Message as DBMessage, Receipt as DBReceipt
|
from .db import User as DBUser, Portal as DBPortal, Message as DBMessage, Receipt as DBReceipt
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .rpc import Client, Message, Receipt
|
from .rpc import Client, Message, Receipt
|
||||||
|
@ -117,6 +119,8 @@ class User(DBUser, BaseUser):
|
||||||
if state.is_logged_in:
|
if state.is_logged_in:
|
||||||
await self.send_bridge_notice("Already logged in to LINE")
|
await self.send_bridge_notice("Already logged in to LINE")
|
||||||
self.loop.create_task(self._try_sync())
|
self.loop.create_task(self._try_sync())
|
||||||
|
elif await auto_login(self):
|
||||||
|
await self.send_bridge_notice("Auto-logged in to LINE")
|
||||||
else:
|
else:
|
||||||
await self.send_bridge_notice("Ready to log in to LINE")
|
await self.send_bridge_notice("Ready to log in to LINE")
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue