From d597e1dda827535eec34ee67fd2102e83ec57a5c Mon Sep 17 00:00:00 2001 From: miruka Date: Thu, 18 Jul 2019 20:30:41 -0400 Subject: [PATCH] Refactor Backend and config file operations --- README.md | 2 +- TODO.md | 1 + src/python/backend.py | 97 ++++++++----------------------- src/python/config_files.py | 78 +++++++++++++++++++++++++ src/python/events/event.py | 6 -- src/python/events/rooms.py | 4 +- src/python/utils.py | 13 +++++ src/qml/Pages/RememberAccount.qml | 4 +- src/qml/Python.qml | 6 +- 9 files changed, 124 insertions(+), 87 deletions(-) create mode 100644 src/python/config_files.py create mode 100644 src/python/utils.py diff --git a/README.md b/README.md index e64c7ad4..98017a7e 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ After this, verify the permissions of the installed plugin files. Dependencies on Pypi: pip3 install --user --upgrade \ - Pillow atomicfile dataclasses filetype lxml mistune uvloop + Pillow aiofiles dataclasses filetype lxml mistune uvloop Dependencies on Github (most recent version needed): diff --git a/TODO.md b/TODO.md index e14ee244..baaeb68a 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,4 @@ +- aiofiles Thumbnails - Devices and client settings in edit account page - Multiaccount aliases - If avatar is set, name color from average color? diff --git a/src/python/backend.py b/src/python/backend.py index 64cade33..0c7ab967 100644 --- a/src/python/backend.py +++ b/src/python/backend.py @@ -2,26 +2,23 @@ # This file is part of harmonyqml, licensed under LGPLv3. import asyncio -import json import random -from pathlib import Path from typing import Dict, Optional, Set, Tuple -from atomicfile import AtomicFile - from .app import App from .events import users from .html_filter import HTML_FILTER from .matrix_client import MatrixClient -SavedAccounts = Dict[str, Dict[str, str]] -CONFIG_LOCK = asyncio.Lock() - class Backend: def __init__(self, app: App) -> None: self.app = app + from . import config_files + self.saved_accounts = config_files.Accounts(self) + self.ui_settings = config_files.UISettings(self) + self.clients: Dict[str, MatrixClient] = {} self.past_tokens: Dict[str, str] = {} # {room_id: token} @@ -64,6 +61,22 @@ class Backend: users.AccountUpdated(client.user_id) + async def load_saved_accounts(self) -> Tuple[str, ...]: + async def resume(user_id: str, info: Dict[str, str]) -> str: + await self.resume_client( + user_id = user_id, + token = info["token"], + device_id = info["device_id"], + homeserver = info["homeserver"], + ) + return user_id + + return await asyncio.gather(*( + resume(uid, info) + for uid, info in (await self.saved_accounts.read()).items() + )) + + async def logout_client(self, user_id: str) -> None: client = self.clients.pop(user_id, None) if client: @@ -77,75 +90,13 @@ class Backend: )) - # Saved account operations - TODO: Use aiofiles? - - @property - def saved_accounts_path(self) -> Path: - return Path(self.app.appdirs.user_config_dir) / "accounts.json" - - - @property - def saved_accounts(self) -> SavedAccounts: - try: - return json.loads(self.saved_accounts_path.read_text()) - except (json.JSONDecodeError, FileNotFoundError): - return {} - - - async def has_saved_accounts(self) -> bool: - return bool(self.saved_accounts) - - - async def load_saved_accounts(self) -> Tuple[str, ...]: - async def resume(user_id: str, info: Dict[str, str]) -> str: - await self.resume_client( - user_id = user_id, - token = info["token"], - device_id = info["device_id"], - homeserver = info["homeserver"], - ) - return user_id - - return await asyncio.gather(*( - resume(uid, info) for uid, info in self.saved_accounts.items() - )) - - - async def save_account(self, user_id: str) -> None: - client = self.clients[user_id] - - await self._write_config({ - **self.saved_accounts, - client.user_id: { - "homeserver": client.homeserver, - "token": client.access_token, - "device_id": client.device_id, - } - }) - - - async def forget_account(self, user_id: str) -> None: - await self._write_config({ - uid: info - for uid, info in self.saved_accounts.items() if uid != user_id - }) - - - async def _write_config(self, accounts: SavedAccounts) -> None: - js = json.dumps(accounts, indent=4, ensure_ascii=False, sort_keys=True) - - async with CONFIG_LOCK: - self.saved_accounts_path.parent.mkdir(parents=True, exist_ok=True) - - with AtomicFile(self.saved_accounts_path, "w") as new: - new.write(js) - - # General functions async def request_user_update_event(self, user_id: str) -> None: - client = self.clients.get(user_id, - random.choice(tuple(self.clients.values()))) + client = self.clients.get( + user_id, + random.choice(tuple(self.clients.values())) + ) await client.request_user_update_event(user_id) diff --git a/src/python/config_files.py b/src/python/config_files.py new file mode 100644 index 00000000..ab779666 --- /dev/null +++ b/src/python/config_files.py @@ -0,0 +1,78 @@ +# Copyright 2019 miruka +# This file is part of harmonyqml, licensed under LGPLv3. + +import asyncio +import json +from pathlib import Path +from typing import Any, Dict + +import aiofiles +from dataclasses import dataclass, field + +from .backend import Backend + +WRITE_LOCK = asyncio.Lock() + + +@dataclass +class ConfigFile: + backend: Backend = field() + filename: str = field() + + @property + def path(self) -> Path: + # pylint: disable=no-member + return Path(self.backend.app.appdirs.user_config_dir) / self.filename + + +@dataclass +class JSONConfigFile(ConfigFile): + async def read(self) -> Dict[str, Any]: + try: + return json.loads(self.path.read_text()) + except (json.JSONDecodeError, FileNotFoundError): + return {} + + + async def write(self, data: Dict[str, Any]) -> None: + js = json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True) + + async with WRITE_LOCK: + self.path.parent.mkdir(parents=True, exist_ok=True) + + async with aiofiles.open(self.path, "w") as new: + await new.write(js) + + +@dataclass +class Accounts(JSONConfigFile): + filename: str = "accounts.json" + + async def any_saved(self) -> bool: + return bool(await self.read()) + + + async def add(self, user_id: str) -> None: + # pylint: disable=no-member + client = self.backend.clients[user_id] + + await self.write({ + **await self.read(), + client.user_id: { + "homeserver": client.homeserver, + "token": client.access_token, + "device_id": client.device_id, + } + }) + + + async def delete(self, user_id: str) -> None: + await self.write({ + uid: info + for uid, info in (await self.read()).items() if uid != user_id + }) + + +@dataclass +class UISettings(JSONConfigFile): + filename: str = "ui-settings.json" diff --git a/src/python/events/event.py b/src/python/events/event.py index 03e9b4ea..d06cabab 100644 --- a/src/python/events/event.py +++ b/src/python/events/event.py @@ -9,12 +9,6 @@ from dataclasses import dataclass import pyotherside -class AutoStrEnum(Enum): - @staticmethod - def _generate_next_value_(name, *_): - return name - - @dataclass class Event: def __post_init__(self) -> None: diff --git a/src/python/events/rooms.py b/src/python/events/rooms.py index 48e26970..cc357ff9 100644 --- a/src/python/events/rooms.py +++ b/src/python/events/rooms.py @@ -2,7 +2,6 @@ # This file is part of harmonyqml, licensed under LGPLv3. from datetime import datetime -from enum import auto from typing import Any, Dict, List, Sequence, Type, Union from dataclasses import dataclass, field @@ -10,7 +9,8 @@ from dataclasses import dataclass, field import nio from nio.rooms import MatrixRoom -from .event import AutoStrEnum, Event +from ..utils import AutoStrEnum, auto +from .event import Event @dataclass diff --git a/src/python/utils.py b/src/python/utils.py new file mode 100644 index 00000000..373a6b1a --- /dev/null +++ b/src/python/utils.py @@ -0,0 +1,13 @@ +# Copyright 2019 miruka +# This file is part of harmonyqml, licensed under LGPLv3. + +from enum import Enum +from enum import auto as autostr + +auto = autostr # pylint: disable=invalid-name + + +class AutoStrEnum(Enum): + @staticmethod + def _generate_next_value_(name, *_): + return name diff --git a/src/qml/Pages/RememberAccount.qml b/src/qml/Pages/RememberAccount.qml index fa6b3eb8..98fb24ea 100644 --- a/src/qml/Pages/RememberAccount.qml +++ b/src/qml/Pages/RememberAccount.qml @@ -23,11 +23,11 @@ Item { buttonCallbacks: ({ yes: button => { - py.callCoro("save_account", [userId]) + py.callCoro("saved_accounts.add", [userId]) pageStack.showPage("Default") }, no: button => { - py.callCoro("forget_account", [userId]) + py.callCoro("saved_accounts.delete", [userId]) pageStack.showPage("Default") }, }) diff --git a/src/qml/Python.qml b/src/qml/Python.qml index 25e5f709..a82ea070 100644 --- a/src/qml/Python.qml +++ b/src/qml/Python.qml @@ -46,11 +46,11 @@ Python { call("APP.is_debug_on", [Qt.application.arguments], on => { window.debug = on - callCoro("has_saved_accounts", [], has => { + callCoro("saved_accounts.any_saved", [], any => { py.ready = true - willLoadAccounts(has) + willLoadAccounts(any) - if (has) { + if (any) { py.loadingAccounts = true py.callCoro("load_saved_accounts", [], () => { py.loadingAccounts = false