Ask for server URL before showing sign in box

Contact the server's .well-known API before anything to get
available login flows instead of blindly assuming it will be
m.login.password, and to get the server's real URL instead of
requiring users to remember that e.g. it's "chat.privacytools.io"
and not just "privacytools.io" despite user IDs making it look like so.

The server field will also now remember the last accepted URL.
This commit is contained in:
miruka 2020-07-24 01:30:35 -04:00
parent b94d1e8168
commit d7907db547
13 changed files with 294 additions and 109 deletions

View File

@ -1,7 +1,11 @@
# TODO # TODO
- Image viewer: - fallback page
- hflickable: support kinetic scrolling disabler - SSO page
- sever list
- cursor shape in HBox/HTabbedBox pages over fields
- login with account already added → infinite spinner in room list
- verify onKeyboardAccept/Cancel things respect button enabled state
- global presence control - global presence control

View File

@ -6,14 +6,14 @@ import os
import sys import sys
import traceback import traceback
from pathlib import Path from pathlib import Path
from typing import Any, DefaultDict, Dict, List, Optional from typing import Any, DefaultDict, Dict, List, Optional, Tuple
from appdirs import AppDirs from appdirs import AppDirs
import nio import nio
from . import __app_name__ from . import __app_name__
from .errors import MatrixError from .errors import MatrixError, MatrixForbidden, MatrixNotFound
from .matrix_client import MatrixClient from .matrix_client import MatrixClient
from .media_cache import MediaCache from .media_cache import MediaCache
from .models import SyncId from .models import SyncId
@ -129,6 +129,30 @@ class Backend:
# Clients management # Clients management
async def server_info(self, homeserver: str) -> Tuple[str, List[str]]:
"""Return server's real URL and supported login flows.
Retrieving the real URL uses the `.well-known` API.
Possible login methods include `m.login.password` or `m.login.sso`.
"""
client = MatrixClient(self, homeserver=homeserver)
try:
homeserver = (await client.discovery_info()).homeserver_url
except (MatrixNotFound, MatrixForbidden):
# This is either already the real URL, or an invalid URL.
pass
else:
await client.close()
client = MatrixClient(self, homeserver=homeserver)
try:
return (homeserver, (await client.login_info()).flows)
finally:
await client.close()
async def login_client(self, async def login_client(self,
user: str, user: str,
password: str, password: str,

View File

@ -152,11 +152,13 @@ class MatrixClient(nio.AsyncClient):
} }
def __init__(self, def __init__(
backend, self,
user: str, backend,
homeserver: str = "https://matrix.org", user: str = "",
device_id: Optional[str] = None) -> None: homeserver: str = "https://matrix.org",
device_id: Optional[str] = None,
) -> None:
if not urlparse(homeserver).scheme: if not urlparse(homeserver).scheme:
raise ValueError( raise ValueError(

View File

@ -5,7 +5,6 @@ import QtQuick.Layouts 1.12
HFlickableColumnPage { HFlickableColumnPage {
implicitWidth: Math.min(parent.width, theme.controls.box.defaultWidth) implicitWidth: Math.min(parent.width, theme.controls.box.defaultWidth)
implicitHeight: Math.min(parent.height, flickable.contentHeight)
background: Rectangle { background: Rectangle {
color: theme.controls.box.background color: theme.controls.box.background

View File

@ -18,6 +18,7 @@ HPage {
implicitWidth: theme.controls.box.defaultWidth implicitWidth: theme.controls.box.defaultWidth
implicitHeight: contentHeight + implicitHeaderHeight + implicitFooterHeight
contentHeight: contentHeight:
flickable.contentHeight + flickable.topMargin + flickable.bottomMargin flickable.contentHeight + flickable.topMargin + flickable.bottomMargin

View File

@ -4,22 +4,48 @@ import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
HPage { HSwipeView {
id: page id: swipeView
clip: true
interactive: currentIndex !== 0 || signIn.serverUrl
onCurrentItemChanged: if (currentIndex === 0) serverBrowser.takeFocus()
Component.onCompleted: serverBrowser.takeFocus()
HTabbedBox { HPage {
anchors.centerIn: parent id: serverPage
width: Math.min(implicitWidth, page.availableWidth)
height: Math.min(implicitHeight, page.availableHeight)
header: HTabBar { ServerBrowser {
HTabButton { text: qsTr("Sign in") } id: serverBrowser
HTabButton { text: qsTr("Register") } anchors.centerIn: parent
HTabButton { text: qsTr("Reset") } width: Math.min(implicitWidth, serverPage.availableWidth)
height: Math.min(implicitHeight, serverPage.availableHeight)
onAccepted: swipeView.currentIndex = 1
} }
}
SignIn {} HPage {
Register {} id: tabPage
Reset {}
HTabbedBox {
anchors.centerIn: parent
width: Math.min(implicitWidth, tabPage.availableWidth)
height: Math.min(implicitHeight, tabPage.availableHeight)
header: HTabBar {
HTabButton { text: qsTr("Sign in") }
HTabButton { text: qsTr("Register") }
HTabButton { text: qsTr("Reset") }
}
SignIn {
id: signIn
serverUrl: serverBrowser.acceptedUrl
displayUrl: serverBrowser.acceptedUserUrl
onExitRequested: swipeView.currentIndex = 0
}
Register {}
Reset {}
}
} }
} }

View File

@ -0,0 +1,155 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
import "../../Base/Buttons"
import "../../PythonBridge"
HBox {
id: page
property string acceptedUserUrl: ""
property string acceptedUrl: ""
property var loginFlows: ["m.login.password"]
property string saveName: "serverBrowser"
property var saveProperties: ["acceptedUserUrl"]
property Future connectFuture: null
signal accepted()
function takeFocus() { serverField.item.forceActiveFocus() }
function connect() {
if (connectFuture) connectFuture.cancel()
connectTimeout.restart()
const args = [serverField.item.cleanText]
connectFuture = py.callCoro("server_info", args, ([url, flows]) => {
connectTimeout.stop()
errorMessage.text = ""
connectFuture = null
acceptedUrl = url
acceptedUserUrl = args[0]
loginFlows = flows
accepted()
}, (type, args, error, traceback, uuid) => {
connectTimeout.stop()
connectFuture = null
let text = qsTr("Unexpected error: %1 [%2]").arg(type).arg(args)
type === "MatrixNotFound" ?
text = qsTr("Invalid homeserver address") :
type.startsWith("Matrix") ?
text = qsTr("Error contacting server: %1").arg(type) :
utils.showError(type, traceback, uuid)
errorMessage.text = text
})
}
function cancel() {
if (page.connectFuture) return
connectTimeout.stop()
connectFuture.cancel()
connectFuture = null
}
footer: AutoDirectionLayout {
ApplyButton {
id: applyButton
enabled: serverField.item.cleanText && ! serverField.item.error
text: qsTr("Connect")
icon.name: "server-connect"
loading: page.connectFuture !== null
disableWhileLoading: false
onClicked: page.connect()
}
CancelButton {
id: cancelButton
enabled: page.connectFuture !== null
onClicked: page.cancel()
}
}
onKeyboardAccept: if (applyButton.enabled) page.connect()
onKeyboardCancel: if (cancelButton.enabled) page.cancel()
onAccepted: window.saveState(this)
Timer {
id: connectTimeout
interval: 30 * 1000
onTriggered: {
errorMessage.text =
serverField.knownServerChosen ?
qsTr("This homeserver seems unavailable. Verify your inter" +
"net connection or try again in a few minutes.") :
qsTr("This homeserver seems unavailable. Verify the " +
"entered address, your internet connection or try " +
"again in a few minutes.")
}
}
HLabeledItem {
id: serverField
// 2019-11-11 https://www.hello-matrix.net/public_servers.php
readonly property var knownServers: [
"https://matrix.org",
"https://chat.weho.st",
"https://tchncs.de",
"https://chat.privacytools.io",
"https://hackerspaces.be",
"https://matrix.allmende.io",
"https://feneas.org",
"https://junta.pl",
"https://perthchat.org",
"https://matrix.tedomum.net",
"https://converser.eu",
"https://ru-matrix.org",
"https://matrix.sibnsk.net",
"https://alternanet.fr",
]
readonly property bool knownServerChosen:
knownServers.includes(item.cleanText)
label.text: qsTr("Homeserver:")
Layout.fillWidth: true
HTextField {
readonly property string cleanText: text.toLowerCase().trim()
width: parent.width
error: ! /https?:\/\/.+/.test(cleanText)
defaultText:
window.getState(page, "acceptedUserUrl", "https://matrix.org")
}
}
HLabel {
id: errorMessage
wrapMode: HLabel.Wrap
horizontalAlignment: Text.AlignHCenter
color: theme.colors.errorText
visible: Layout.maximumHeight > 0
Layout.maximumHeight: text ? implicitHeight : 0
Behavior on Layout.maximumHeight { HNumberAnimation {} }
Layout.fillWidth: true
}
}

View File

@ -8,33 +8,42 @@ import "../../Base/Buttons"
HFlickableColumnPage { HFlickableColumnPage {
id: page id: page
enum Security { Insecure, LocalHttp, Secure }
property string serverUrl
property string displayUrl: serverUrl
property var loginFuture: null property var loginFuture: null
property string signInWith: "username" signal exitRequested()
readonly property bool canSignIn: readonly property int security:
serverField.item.text.trim() && idField.item.text.trim() && serverUrl.startsWith("https://") ?
passwordField.item.text && ! serverField.item.error SignIn.Security.Secure :
["//localhost", "//127.0.0.1", "//:1"].includes(
serverUrl.split(":")[1],
) ?
SignIn.Security.LocalHttp :
SignIn.Security.Insecure
function takeFocus() { idField.item.forceActiveFocus() } function takeFocus() { idField.item.forceActiveFocus() }
function signIn() { function signIn() {
if (page.loginFuture) page.loginFuture.cancel() if (page.loginFuture) page.loginFuture.cancel()
signInTimeout.restart()
errorMessage.text = "" errorMessage.text = ""
const args = [ const args = [
idField.item.text.trim(), passwordField.item.text, idField.item.text.trim(), passwordField.item.text,
undefined, serverField.item.text.trim(), undefined, page.serverUrl,
] ]
page.loginFuture = py.callCoro("login_client", args, userId => { page.loginFuture = py.callCoro("login_client", args, userId => {
signInTimeout.stop()
errorMessage.text = "" errorMessage.text = ""
page.loginFuture = null page.loginFuture = null
print(rememberAccount.checked)
py.callCoro( py.callCoro(
rememberAccount.checked ? rememberAccount.checked ?
"saved_accounts.add": "saved_accounts.delete", "saved_accounts.add": "saved_accounts.delete",
@ -48,7 +57,6 @@ HFlickableColumnPage {
}, (type, args, error, traceback, uuid) => { }, (type, args, error, traceback, uuid) => {
page.loginFuture = null page.loginFuture = null
signInTimeout.stop()
let txt = qsTr( let txt = qsTr(
"Invalid request, login type or unknown error: %1", "Invalid request, login type or unknown error: %1",
@ -67,17 +75,23 @@ HFlickableColumnPage {
} }
function cancel() { function cancel() {
if (! page.loginFuture) return if (! page.loginFuture) {
page.exitRequested()
return
}
signInTimeout.stop()
page.loginFuture.cancel() page.loginFuture.cancel()
page.loginFuture = null page.loginFuture = null
} }
flickable.topMargin: theme.spacing * 1.5
flickable.bottomMargin: flickable.topMargin
footer: AutoDirectionLayout { footer: AutoDirectionLayout {
ApplyButton { ApplyButton {
enabled: page.canSignIn id: applyButton
enabled: idField.item.text.trim() && passwordField.item.text
text: qsTr("Sign in") text: qsTr("Sign in")
icon.name: "sign-in" icon.name: "sign-in"
loading: page.loginFuture !== null loading: page.loginFuture !== null
@ -90,54 +104,39 @@ HFlickableColumnPage {
} }
} }
onKeyboardAccept: page.signIn() onKeyboardAccept: if (applyButton.enabled) page.signIn()
onKeyboardCancel: page.cancel() onKeyboardCancel: page.cancel()
Timer { HButton {
id: signInTimeout icon.name: "sign-in-" + (
interval: 30 * 1000 page.security === SignIn.Security.Insecure ? "insecure" :
onTriggered: { page.security === SignIn.Security.LocalHttp ? "local-http" :
errorMessage.text = "secure"
serverField.knownServerChosen ? )
qsTr("This server seems unavailable. Verify your inter" + icon.color:
"net connection or try again in a few minutes.") : page.security === SignIn.Security.Insecure ?
theme.colors.negativeBackground :
qsTr("This server seems unavailable. Verify the " + page.security === SignIn.Security.LocalHttp ?
"entered URL, your internet connection or try " + theme.colors.middleBackground :
"again in a few minutes.")
}
}
HRowLayout { theme.colors.positiveBackground
visible: false // TODO
spacing: theme.spacing * 1.25
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: theme.spacing text:
Layout.bottomMargin: Layout.topMargin page.security === SignIn.Security.Insecure ?
page.serverUrl :
page.displayUrl.replace(/^(https?:\/\/)?(www\.)?/, "")
Repeater { onClicked: page.exitRequested()
model: ["username", "email", "phone"]
HButton { Layout.alignment: Qt.AlignCenter
icon.name: modelData Layout.maximumWidth: parent.width
circle: true
checked: page.signInWith === modelData
enabled: modelData === "username"
autoExclusive: true
onClicked: page.signInWith = modelData
}
}
} }
HLabeledItem { HLabeledItem {
id: idField id: idField
label.text: qsTr( label.text: qsTr("Username:")
page.signInWith === "email" ? "Email:" :
page.signInWith === "phone" ? "Phone:" :
"Username:"
)
Layout.fillWidth: true Layout.fillWidth: true
@ -158,43 +157,6 @@ HFlickableColumnPage {
} }
} }
HLabeledItem {
id: serverField
// 2019-11-11 https://www.hello-matrix.net/public_servers.php
readonly property var knownServers: [
"https://matrix.org",
"https://chat.weho.st",
"https://tchncs.de",
"https://chat.privacytools.io",
"https://hackerspaces.be",
"https://matrix.allmende.io",
"https://feneas.org",
"https://junta.pl",
"https://perthchat.org",
"https://matrix.tedomum.net",
"https://converser.eu",
"https://ru-matrix.org",
"https://matrix.sibnsk.net",
"https://alternanet.fr",
]
readonly property bool knownServerChosen:
knownServers.includes(item.cleanText)
label.text: qsTr("Homeserver:")
Layout.fillWidth: true
HTextField {
readonly property string cleanText: text.toLowerCase().trim()
width: parent.width
text: "https://matrix.org"
error: ! /.+:\/\/.+/.test(cleanText)
}
}
HCheckBox { HCheckBox {
id: rememberAccount id: rememberAccount
checked: true checked: true
@ -206,7 +168,6 @@ HFlickableColumnPage {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: theme.spacing / 2 Layout.topMargin: theme.spacing / 2
Layout.bottomMargin: Layout.topMargin
} }
HLabel { HLabel {

View File

@ -440,6 +440,7 @@ QtObject {
flickable.maximumFlickVelocity = 5000 flickable.maximumFlickVelocity = 5000
flickable.flickDeceleration = Math.max( flickable.flickDeceleration = Math.max(
goFaster ? normalDecel : -Infinity, goFaster ? normalDecel : -Infinity,
Math.abs(normalDecel * magicNumber * pages), Math.abs(normalDecel * magicNumber * pages),

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m12 0c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5.73 20.183c-.106-.526-.236-1.046-.398-1.555.897-.811.86-2.197-.072-2.883.582-2.318.61-4.849-.002-7.288.186-.111.348-.254.475-.425 1.466.412 2.831 1.066 4.052 1.911.14.665.215 1.352.215 2.057 0 3.382-1.692 6.372-4.27 8.183zm-15.73-8.183c0-.855.12-1.682.323-2.475.699-.044 1.393-.04 2.147.032l.04.226c-.921.775-1.75 1.661-2.487 2.674zm3.183-1.19c.848.643 2.083.436 2.662-.49 2.898 1.06 5.339 3.077 6.94 5.666-.766.775-.756 1.998.019 2.695-.681 1.231-1.548 2.345-2.56 3.307-4.902.117-8.924-3.262-9.969-7.697.772-1.316 1.755-2.494 2.908-3.481zm2.886-1.901c1.991-.974 4.155-1.432 6.324-1.377.305.611.93 1.076 1.666 1.166h.006c.557 2.157.583 4.472.029 6.7l-.223.023c-1.724-2.825-4.433-5.131-7.763-6.301zm6.062 12.857c.702-.817 1.311-1.695 1.813-2.627l.27-.008c.172.562.308 1.139.408 1.729-.777.406-1.612.714-2.491.906zm7.103-13.598c-1.009-.56-2.076-1.002-3.189-1.311-.108-.995-1.041-1.824-2.119-1.816-.552-1.019-1.232-1.975-2.024-2.854 3.321.642 6.061 2.93 7.332 5.981zm-6.472-2.708c-.257.22-.443.515-.524.858-2.456-.03-4.778.526-6.848 1.565-.85-.638-2.07-.421-2.646.483-.728-.076-1.379-.092-2.024-.072 1.476-3.683 5.076-6.294 9.28-6.294h.001c1.097.994 2.034 2.16 2.761 3.46z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m8 9v-3c0-2.206 1.794-4 4-4s4 1.794 4 4v3h2v-3c0-3.313-2.687-6-6-6s-6 2.687-6 6v3zm.746 2h2.831l-8.577 8.787v-2.9zm12.254 1.562v-1.562h-1.37l-12.69 13h2.894zm-6.844-1.562-11.156 11.431v1.569h1.361l12.689-13zm6.844 7.13v-2.927l-8.586 8.797h2.858zm-3.149 5.87h3.149v-3.226zm-11.685-13h-3.166v3.244z"/>
</svg>

After

Width:  |  Height:  |  Size: 404 B

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m8 9v-3c0-2.206 1.794-4 4-4s4 1.794 4 4v3h2v-3c0-3.313-2.687-6-6-6s-6 2.687-6 6v3zm.746 2h2.831l-8.577 8.787v-2.9zm12.254 1.562v-1.562h-1.37l-12.69 13h2.894zm-6.844-1.562-11.156 11.431v1.569h1.361l12.689-13zm6.844 7.13v-2.927l-8.586 8.797h2.858zm-3.149 5.87h3.149v-3.226zm-11.685-13h-3.166v3.244z"/>
</svg>

After

Width:  |  Height:  |  Size: 404 B

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="m21 21v3h-18v-3zm-13-15c0-2.206 1.795-4 4-4s4 1.794 4 4v3h2v-3c0-3.313-2.687-6-6-6s-6 2.687-6 6v3h2zm13 8v-3h-18v3zm0 5v-3h-18v3z"/>
</svg>

After

Width:  |  Height:  |  Size: 237 B