matrix-nio backend start, QGuiApplication class
- Started work on the matrix-nio backend, which will be used instead of matrix-python-sdk for greater control and cleaner design - Have an Application (QGuiApplication) class to habdle argument parsing and setting some Qt properties like application name
This commit is contained in:
parent
3b47fee77d
commit
4f9a47027c
|
@ -3,10 +3,11 @@
|
|||
|
||||
"""<SHORTDESC>"""
|
||||
|
||||
__pkg_name__ = "harmonyqml"
|
||||
__version__ = "0.1.0"
|
||||
__status__ = "Development"
|
||||
# __status__ = "Production"
|
||||
__pkg_name__ = "harmonyqml"
|
||||
__pretty_name__ = "Harmony QML"
|
||||
__version__ = "0.1.0"
|
||||
__status__ = "Development"
|
||||
# __status__ = "Production"
|
||||
|
||||
__author__ = "miruka"
|
||||
__email__ = "miruka@disroot.org"
|
||||
|
|
|
@ -3,20 +3,10 @@
|
|||
|
||||
import sys
|
||||
|
||||
from PyQt5.QtGui import QGuiApplication
|
||||
|
||||
from .engine import Engine
|
||||
from . import app
|
||||
|
||||
# logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
def run() -> None:
|
||||
try:
|
||||
sys.argv.index("--debug")
|
||||
debug = True
|
||||
except ValueError:
|
||||
debug = False
|
||||
|
||||
app = QGuiApplication(sys.argv)
|
||||
engine = Engine(app, debug=debug)
|
||||
engine.show_window()
|
||||
_ = app.Application(sys.argv)
|
||||
|
|
26
harmonyqml/app.py
Normal file
26
harmonyqml/app.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Copyright 2019 miruka
|
||||
# This file is part of harmonyqml, licensed under GPLv3.
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from PyQt5.QtGui import QGuiApplication
|
||||
|
||||
from . import __about__
|
||||
|
||||
|
||||
class Application(QGuiApplication):
|
||||
def __init__(self, args: Optional[List[str]] = None) -> None:
|
||||
try:
|
||||
args.index("--debug") # type: ignore
|
||||
debug = True
|
||||
except (AttributeError, ValueError):
|
||||
debug = False
|
||||
|
||||
super().__init__(args or [])
|
||||
|
||||
self.setApplicationName(__about__.__pkg_name__)
|
||||
self.setApplicationDisplayName(__about__.__pretty_name__)
|
||||
|
||||
from .engine import Engine
|
||||
engine = Engine(self, debug=debug)
|
||||
engine.show_window()
|
|
@ -4,28 +4,19 @@ from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union
|
|||
|
||||
from namedlist import namedlist
|
||||
from PyQt5.QtCore import (
|
||||
QAbstractListModel, QModelIndex, Qt, pyqtProperty, pyqtSlot, pyqtSignal
|
||||
QAbstractListModel, QModelIndex, Qt, pyqtProperty, pyqtSlot
|
||||
)
|
||||
|
||||
NewValue = Union[Mapping[str, Any], Sequence]
|
||||
|
||||
|
||||
class _QtListModel(QAbstractListModel):
|
||||
updated = pyqtSignal()
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._ref_namedlist = None
|
||||
self._roles: Tuple[str, ...] = ()
|
||||
self._list: list = []
|
||||
|
||||
self._update_counter: int = 0
|
||||
|
||||
for sig in (self.dataChanged, self.layoutChanged, self.modelReset,
|
||||
self.rowsInserted, self.rowsMoved, self.rowsRemoved):
|
||||
sig.connect(self.updated.emit)
|
||||
|
||||
|
||||
def roleNames(self) -> Dict[int, bytes]:
|
||||
return {Qt.UserRole + i: bytes(f, "utf-8")
|
||||
for i, f in enumerate(self._roles, 1)}
|
||||
|
@ -70,7 +61,7 @@ class _QtListModel(QAbstractListModel):
|
|||
|
||||
|
||||
@pyqtSlot(int, result="QVariantMap")
|
||||
def get(self, index: int) -> Optional[Dict[str, Any]]:
|
||||
def get(self, index: int) -> Dict[str, Any]:
|
||||
return self._list[index]._asdict()
|
||||
|
||||
|
||||
|
@ -82,7 +73,7 @@ class _QtListModel(QAbstractListModel):
|
|||
self.endInsertRows()
|
||||
|
||||
|
||||
@pyqtProperty(int, constant=True)
|
||||
@pyqtProperty(int)
|
||||
def count(self) -> int:
|
||||
return self.rowCount()
|
||||
|
||||
|
@ -151,13 +142,6 @@ class _QtListModel(QAbstractListModel):
|
|||
self.endRemoveRows()
|
||||
|
||||
|
||||
@pyqtProperty(int, notify=updated)
|
||||
def reloadThis(self) -> int:
|
||||
# http://www.mardy.it/blog/2016/11/qml-trick-force-re-evaluation-of.html
|
||||
self._update_counter += 1
|
||||
return self._update_counter
|
||||
|
||||
|
||||
class ListModel(MutableSequence):
|
||||
def __init__(self, initial_data: Optional[List[NewValue]] = None) -> None:
|
||||
super().__init__()
|
||||
|
|
4
harmonyqml/backend/matrix_nio/__init__.py
Normal file
4
harmonyqml/backend/matrix_nio/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Copyright 2019 miruka
|
||||
# This file is part of harmonyqml, licensed under GPLv3.
|
||||
|
||||
from .backend import MatrixNioBackend
|
27
harmonyqml/backend/matrix_nio/backend.py
Normal file
27
harmonyqml/backend/matrix_nio/backend.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Copyright 2019 miruka
|
||||
# This file is part of harmonyqml, licensed under GPLv3.
|
||||
|
||||
from typing import Any, DefaultDict, Dict, NamedTuple, Optional
|
||||
|
||||
from PyQt5.QtCore import QDateTime, QObject, pyqtProperty, pyqtSlot
|
||||
|
||||
from matrix_client.user import User as MatrixUser
|
||||
|
||||
from ..base import Backend, User
|
||||
from .client_manager import ClientManager
|
||||
|
||||
|
||||
class MatrixNioBackend(Backend):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._client_manager = ClientManager()
|
||||
|
||||
# a = self._client_manager
|
||||
# from PyQt5.QtCore import pyqtRemoveInputHook as PRI; import pdb; PRI(); pdb.set_trace()
|
||||
|
||||
self._client_manager.configLoad()
|
||||
|
||||
|
||||
@pyqtProperty("QVariant")
|
||||
def clientManager(self):
|
||||
return self._client_manager
|
85
harmonyqml/backend/matrix_nio/client.py
Normal file
85
harmonyqml/backend/matrix_nio/client.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
# Copyright 2019 miruka
|
||||
# This file is part of harmonyqml, licensed under GPLv3.
|
||||
|
||||
import functools
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
from threading import Event
|
||||
from typing import Callable, DefaultDict
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
import nio
|
||||
import nio.responses as nr
|
||||
|
||||
# One pool per hostname/remote server;
|
||||
# multiple Client for different accounts on the same server can exist.
|
||||
_POOLS: DefaultDict[str, ThreadPoolExecutor] = \
|
||||
DefaultDict(lambda: ThreadPoolExecutor(max_workers=6))
|
||||
|
||||
|
||||
def futurize(func: Callable) -> Callable:
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs) -> Future:
|
||||
return args[0].pool.submit(func, *args, **kwargs) # args[0] = self
|
||||
return wrapper
|
||||
|
||||
|
||||
class Client(QObject):
|
||||
def __init__(self, hostname: str, username: str, device_id: str = ""
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
host, *port = hostname.split(":")
|
||||
self.host: str = host
|
||||
self.port: int = int(port[0]) if port else 443
|
||||
|
||||
self.nio: nio.client.HttpClient = \
|
||||
nio.client.HttpClient(self.host, username, device_id)
|
||||
|
||||
self.pool: ThreadPoolExecutor = _POOLS[self.host]
|
||||
|
||||
from .net import NetworkManager
|
||||
self.net: NetworkManager = NetworkManager(self)
|
||||
|
||||
self._stop_sync: Event = Event()
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "%s(host=%r, port=%r, user_id=%r)" % \
|
||||
(type(self).__name__, self.host, self.port, self.nio.user_id)
|
||||
|
||||
|
||||
@pyqtSlot(str)
|
||||
@pyqtSlot(str, str)
|
||||
@futurize
|
||||
def login(self, password: str, device_name: str = "") -> None:
|
||||
self.net.write(self.nio.connect())
|
||||
self.net.talk(self.nio.login, password, device_name)
|
||||
self.startSyncing()
|
||||
|
||||
|
||||
@pyqtSlot(str, str, str)
|
||||
@futurize
|
||||
def resumeSession(self, user_id: str, token: str, device_id: str
|
||||
) -> None:
|
||||
self.net.write(self.nio.connect())
|
||||
response = nr.LoginResponse(user_id, device_id, token)
|
||||
self.nio.receive_response(response)
|
||||
self.startSyncing()
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
@futurize
|
||||
def logout(self) -> None:
|
||||
self._stop_sync.set()
|
||||
self.net.write(self.nio.disconnect())
|
||||
|
||||
|
||||
@futurize
|
||||
def startSyncing(self) -> None:
|
||||
while True:
|
||||
print(self, self.net.talk(self.nio.sync, timeout=10))
|
||||
|
||||
if self._stop_sync.is_set():
|
||||
self._stop_sync.clear()
|
||||
break
|
147
harmonyqml/backend/matrix_nio/client_manager.py
Normal file
147
harmonyqml/backend/matrix_nio/client_manager.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
# Copyright 2018 miruka
|
||||
# This file is part of harmonyqt, licensed under GPLv3.
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import threading
|
||||
from concurrent.futures import Future
|
||||
from typing import Dict, Optional
|
||||
|
||||
from atomicfile import AtomicFile
|
||||
from PyQt5.QtCore import QObject, QStandardPaths, pyqtProperty, pyqtSlot
|
||||
|
||||
from harmonyqml import __about__
|
||||
|
||||
from .client import Client
|
||||
|
||||
AccountConfig = Dict[str, Dict[str, str]]
|
||||
|
||||
_CONFIG_LOCK = threading.Lock()
|
||||
# _CRYPT_DB_LOCK = threading.Lock()
|
||||
|
||||
|
||||
class ClientManager(QObject):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._clients: Dict[str, Client] = {}
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{type(self).__name__}(clients={self.clients!r})"
|
||||
|
||||
|
||||
@pyqtProperty("QVariantMap")
|
||||
def clients(self):
|
||||
return self._clients
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def configLoad(self) -> None:
|
||||
for user_id, info in self.configAccounts().items():
|
||||
cli = Client(info["hostname"], user_id)
|
||||
|
||||
def on_done(_: Future, cli=cli) -> None:
|
||||
self._clients[cli.nio.user_id] = cli
|
||||
|
||||
cli.resumeSession(user_id, info["token"], info["device_id"])\
|
||||
.add_done_callback(on_done)
|
||||
|
||||
|
||||
@pyqtSlot(str, str, str)
|
||||
@pyqtSlot(str, str, str)
|
||||
def new(self, hostname: str, username: str, password: str,
|
||||
device_id: str = "") -> None:
|
||||
|
||||
cli = Client(hostname, username, device_id)
|
||||
|
||||
def on_done(_: Future, cli=cli) -> None:
|
||||
self._clients[cli.nio.user_id] = cli
|
||||
|
||||
cli.login(password, self.defaultDeviceName).add_done_callback(on_done)
|
||||
|
||||
|
||||
@pyqtSlot(str)
|
||||
def delete(self, user_id: str) -> None:
|
||||
client = self._clients.pop(user_id, None)
|
||||
if client:
|
||||
client.logout()
|
||||
|
||||
|
||||
@pyqtProperty(str)
|
||||
def defaultDeviceName(self) -> str: # pylint: disable=no-self-use
|
||||
os_ = f" on {platform.system()}".rstrip()
|
||||
os_ = f"{os_} {platform.release()}".rstrip() if os_ != " on" else ""
|
||||
return f"{__about__.__pretty_name__}{os}"
|
||||
|
||||
|
||||
# Standard file paths
|
||||
|
||||
@staticmethod
|
||||
def _get_standard_path(kind: QStandardPaths.StandardLocation,
|
||||
file: str,
|
||||
initial_content: Optional[str] = None) -> str:
|
||||
relative_path = file.replace("/", os.sep)
|
||||
|
||||
path = QStandardPaths.locate(kind, relative_path)
|
||||
if path:
|
||||
return path
|
||||
|
||||
base_dir = QStandardPaths.writableLocation(kind)
|
||||
path = f"{base_dir}{os.sep}{relative_path}"
|
||||
os.makedirs(os.path.split(path)[0], exist_ok=True)
|
||||
|
||||
if initial_content is not None:
|
||||
with AtomicFile(path, "w") as new:
|
||||
new.write(initial_content)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def getAccountConfigPath(self) -> str:
|
||||
return self._get_standard_path(
|
||||
QStandardPaths.AppConfigLocation, "accounts.json", "[]"
|
||||
)
|
||||
|
||||
|
||||
def getCryptDBPath(self, user_id: str) -> str:
|
||||
safe_filename = hashlib.md5(user_id.encode("utf-8")).hexdigest()
|
||||
return self._get_standard_path(
|
||||
QStandardPaths.AppDataLocation, f"encryption/{safe_filename}.db"
|
||||
)
|
||||
|
||||
|
||||
# Config file operations
|
||||
|
||||
def configAccounts(self) -> AccountConfig:
|
||||
with open(self.getAccountConfigPath(), "r") as file:
|
||||
return json.loads(file.read().strip()) or {}
|
||||
|
||||
|
||||
@pyqtSlot(Client)
|
||||
def configAdd(self, client: Client) -> None:
|
||||
self._write_config({
|
||||
**self.configAccounts(),
|
||||
**{client.nio.user_id: {
|
||||
"hostname": client.nio.host,
|
||||
"token": client.nio.access_token,
|
||||
"device_id": client.nio.device_id,
|
||||
}}
|
||||
})
|
||||
|
||||
|
||||
@pyqtSlot(str)
|
||||
def configDelete(self, user_id: str) -> None:
|
||||
self._write_config({
|
||||
uid: info
|
||||
for uid, info in self.configAccounts().items() if uid != user_id
|
||||
})
|
||||
|
||||
|
||||
def _write_config(self, accounts: AccountConfig) -> None:
|
||||
js = json.dumps(accounts, indent=4, ensure_ascii=False, sort_keys=True)
|
||||
|
||||
with _CONFIG_LOCK:
|
||||
with AtomicFile(self.getAccountConfigPath(), "w") as new:
|
||||
new.write(js)
|
95
harmonyqml/backend/matrix_nio/net.py
Normal file
95
harmonyqml/backend/matrix_nio/net.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
# Copyright 2019 miruka
|
||||
# This file is part of harmonyqml, licensed under GPLv3.
|
||||
|
||||
import logging
|
||||
import socket
|
||||
import ssl
|
||||
import time
|
||||
from typing import Callable, Optional, Tuple
|
||||
from uuid import UUID
|
||||
|
||||
import nio.responses as nr
|
||||
|
||||
from .client import Client
|
||||
|
||||
OptSock = Optional[ssl.SSLSocket]
|
||||
NioRequestFunc = Callable[..., Tuple[UUID, bytes]]
|
||||
|
||||
|
||||
class NioErrorResponse(Exception):
|
||||
def __init__(self, response: nr.ErrorResponse) -> None:
|
||||
self.response = response
|
||||
super().__init__(str(response))
|
||||
|
||||
|
||||
class NetworkManager:
|
||||
def __init__(self, client: Client) -> None:
|
||||
self.client = client
|
||||
self._ssl_context: ssl.SSLContext = ssl.create_default_context()
|
||||
self._ssl_session: Optional[ssl.SSLSession] = None
|
||||
|
||||
|
||||
def _get_socket(self) -> ssl.SSLSocket:
|
||||
sock = self._ssl_context.wrap_socket( # type: ignore
|
||||
socket.create_connection((self.client.host, self.client.port)),
|
||||
server_hostname = self.client.host,
|
||||
session = self._ssl_session,
|
||||
)
|
||||
self._ssl_session = self._ssl_session or sock.session
|
||||
return sock
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _close_socket(sock: socket.socket) -> None:
|
||||
sock.shutdown(how=socket.SHUT_RDWR)
|
||||
sock.close()
|
||||
|
||||
|
||||
def read(self, with_sock: OptSock = None) -> nr.Response:
|
||||
sock = with_sock or self._get_socket()
|
||||
|
||||
response = None
|
||||
while not response:
|
||||
self.client.nio.receive(sock.recv(4096))
|
||||
response = self.client.nio.next_response()
|
||||
|
||||
if isinstance(response, nr.ErrorResponse):
|
||||
raise NioErrorResponse(response)
|
||||
|
||||
if not with_sock:
|
||||
self._close_socket(sock)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def write(self, data: bytes, with_sock: OptSock = None) -> None:
|
||||
sock = with_sock or self._get_socket()
|
||||
sock.sendall(data)
|
||||
|
||||
if not with_sock:
|
||||
self._close_socket(sock)
|
||||
|
||||
|
||||
def talk(self, nio_func: NioRequestFunc, *args, **kwargs) -> nr.Response:
|
||||
while True:
|
||||
to_send = nio_func(*args, **kwargs)[1]
|
||||
sock = self._get_socket()
|
||||
|
||||
try:
|
||||
self.write(to_send, sock)
|
||||
response = self.read(sock)
|
||||
|
||||
except NioErrorResponse as err:
|
||||
logging.error("read bad response: %s", err)
|
||||
self._close_socket(sock)
|
||||
time.sleep(10)
|
||||
|
||||
except Exception as err:
|
||||
logging.error("talk exception: %r", err)
|
||||
break
|
||||
|
||||
else:
|
||||
break
|
||||
|
||||
self._close_socket(sock)
|
||||
return response
|
|
@ -24,10 +24,10 @@ Controls1.SplitView {
|
|||
}
|
||||
|
||||
id: "pageStack"
|
||||
initialItem: Chat.Root {
|
||||
user: Backend.accountsModel.get(0)
|
||||
room: Backend.roomsModel[Backend.accountsModel.get(0).user_id].get(0)
|
||||
}
|
||||
// initialItem: Chat.Root {
|
||||
//user: Backend.accountsModel.get(0)
|
||||
//room: Backend.roomsModel[Backend.accountsModel.get(0).user_id].get(0)
|
||||
//}
|
||||
|
||||
onCurrentItemChanged: currentItem.forceActiveFocus()
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ ApplicationWindow {
|
|||
visible: true
|
||||
width: 640
|
||||
height: 700
|
||||
title: "Harmony QML"
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
|
|
|
@ -7,23 +7,23 @@ from pathlib import Path
|
|||
from typing import Generator, Optional
|
||||
|
||||
from PyQt5.QtCore import QFileSystemWatcher, QObject, QTimer
|
||||
from PyQt5.QtGui import QGuiApplication
|
||||
from PyQt5.QtQml import QQmlApplicationEngine
|
||||
|
||||
from .__about__ import __doc__
|
||||
from .backend import DummyBackend
|
||||
from .app import Application
|
||||
from .backend.matrix_nio.backend import MatrixNioBackend as CurrentBackend
|
||||
|
||||
# logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
class Engine(QQmlApplicationEngine):
|
||||
def __init__(self,
|
||||
app: QGuiApplication,
|
||||
app: Application,
|
||||
debug: bool = False,
|
||||
parent: Optional[QObject] = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.app = app
|
||||
self.backend = DummyBackend()
|
||||
self.backend = CurrentBackend()
|
||||
self.app_dir = Path(sys.argv[0]).resolve().parent
|
||||
|
||||
# Set QML properties
|
||||
|
|
Loading…
Reference in New Issue
Block a user