Refactor Backend and config file operations

This commit is contained in:
miruka 2019-07-18 20:30:41 -04:00
parent 31184071db
commit d597e1dda8
9 changed files with 124 additions and 87 deletions

View File

@ -26,7 +26,7 @@ After this, verify the permissions of the installed plugin files.
Dependencies on Pypi: Dependencies on Pypi:
pip3 install --user --upgrade \ 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): Dependencies on Github (most recent version needed):

View File

@ -1,3 +1,4 @@
- aiofiles Thumbnails
- Devices and client settings in edit account page - Devices and client settings in edit account page
- Multiaccount aliases - Multiaccount aliases
- If avatar is set, name color from average color? - If avatar is set, name color from average color?

View File

@ -2,26 +2,23 @@
# This file is part of harmonyqml, licensed under LGPLv3. # This file is part of harmonyqml, licensed under LGPLv3.
import asyncio import asyncio
import json
import random import random
from pathlib import Path
from typing import Dict, Optional, Set, Tuple from typing import Dict, Optional, Set, Tuple
from atomicfile import AtomicFile
from .app import App from .app import App
from .events import users from .events import users
from .html_filter import HTML_FILTER from .html_filter import HTML_FILTER
from .matrix_client import MatrixClient from .matrix_client import MatrixClient
SavedAccounts = Dict[str, Dict[str, str]]
CONFIG_LOCK = asyncio.Lock()
class Backend: class Backend:
def __init__(self, app: App) -> None: def __init__(self, app: App) -> None:
self.app = app 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.clients: Dict[str, MatrixClient] = {}
self.past_tokens: Dict[str, str] = {} # {room_id: token} self.past_tokens: Dict[str, str] = {} # {room_id: token}
@ -64,6 +61,22 @@ class Backend:
users.AccountUpdated(client.user_id) 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: async def logout_client(self, user_id: str) -> None:
client = self.clients.pop(user_id, None) client = self.clients.pop(user_id, None)
if client: 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 # General functions
async def request_user_update_event(self, user_id: str) -> None: async def request_user_update_event(self, user_id: str) -> None:
client = self.clients.get(user_id, client = self.clients.get(
random.choice(tuple(self.clients.values()))) user_id,
random.choice(tuple(self.clients.values()))
)
await client.request_user_update_event(user_id) await client.request_user_update_event(user_id)

View File

@ -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"

View File

@ -9,12 +9,6 @@ from dataclasses import dataclass
import pyotherside import pyotherside
class AutoStrEnum(Enum):
@staticmethod
def _generate_next_value_(name, *_):
return name
@dataclass @dataclass
class Event: class Event:
def __post_init__(self) -> None: def __post_init__(self) -> None:

View File

@ -2,7 +2,6 @@
# This file is part of harmonyqml, licensed under LGPLv3. # This file is part of harmonyqml, licensed under LGPLv3.
from datetime import datetime from datetime import datetime
from enum import auto
from typing import Any, Dict, List, Sequence, Type, Union from typing import Any, Dict, List, Sequence, Type, Union
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -10,7 +9,8 @@ from dataclasses import dataclass, field
import nio import nio
from nio.rooms import MatrixRoom from nio.rooms import MatrixRoom
from .event import AutoStrEnum, Event from ..utils import AutoStrEnum, auto
from .event import Event
@dataclass @dataclass

13
src/python/utils.py Normal file
View File

@ -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

View File

@ -23,11 +23,11 @@ Item {
buttonCallbacks: ({ buttonCallbacks: ({
yes: button => { yes: button => {
py.callCoro("save_account", [userId]) py.callCoro("saved_accounts.add", [userId])
pageStack.showPage("Default") pageStack.showPage("Default")
}, },
no: button => { no: button => {
py.callCoro("forget_account", [userId]) py.callCoro("saved_accounts.delete", [userId])
pageStack.showPage("Default") pageStack.showPage("Default")
}, },
}) })

View File

@ -46,11 +46,11 @@ Python {
call("APP.is_debug_on", [Qt.application.arguments], on => { call("APP.is_debug_on", [Qt.application.arguments], on => {
window.debug = on window.debug = on
callCoro("has_saved_accounts", [], has => { callCoro("saved_accounts.any_saved", [], any => {
py.ready = true py.ready = true
willLoadAccounts(has) willLoadAccounts(any)
if (has) { if (any) {
py.loadingAccounts = true py.loadingAccounts = true
py.callCoro("load_saved_accounts", [], () => { py.callCoro("load_saved_accounts", [], () => {
py.loadingAccounts = false py.loadingAccounts = false