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
- Image viewer:
- hflickable: support kinetic scrolling disabler
- fallback page
- 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

View File

@ -6,14 +6,14 @@ import os
import sys
import traceback
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
import nio
from . import __app_name__
from .errors import MatrixError
from .errors import MatrixError, MatrixForbidden, MatrixNotFound
from .matrix_client import MatrixClient
from .media_cache import MediaCache
from .models import SyncId
@ -129,6 +129,30 @@ class Backend:
# 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,
user: str,
password: str,

View File

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

View File

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

View File

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

View File

@ -4,13 +4,32 @@ import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../../Base"
HPage {
id: page
HSwipeView {
id: swipeView
clip: true
interactive: currentIndex !== 0 || signIn.serverUrl
onCurrentItemChanged: if (currentIndex === 0) serverBrowser.takeFocus()
Component.onCompleted: serverBrowser.takeFocus()
HPage {
id: serverPage
ServerBrowser {
id: serverBrowser
anchors.centerIn: parent
width: Math.min(implicitWidth, serverPage.availableWidth)
height: Math.min(implicitHeight, serverPage.availableHeight)
onAccepted: swipeView.currentIndex = 1
}
}
HPage {
id: tabPage
HTabbedBox {
anchors.centerIn: parent
width: Math.min(implicitWidth, page.availableWidth)
height: Math.min(implicitHeight, page.availableHeight)
width: Math.min(implicitWidth, tabPage.availableWidth)
height: Math.min(implicitHeight, tabPage.availableHeight)
header: HTabBar {
HTabButton { text: qsTr("Sign in") }
@ -18,8 +37,15 @@ HPage {
HTabButton { text: qsTr("Reset") }
}
SignIn {}
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 {
id: page
enum Security { Insecure, LocalHttp, Secure }
property string serverUrl
property string displayUrl: serverUrl
property var loginFuture: null
property string signInWith: "username"
signal exitRequested()
readonly property bool canSignIn:
serverField.item.text.trim() && idField.item.text.trim() &&
passwordField.item.text && ! serverField.item.error
readonly property int security:
serverUrl.startsWith("https://") ?
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 signIn() {
if (page.loginFuture) page.loginFuture.cancel()
signInTimeout.restart()
errorMessage.text = ""
const args = [
idField.item.text.trim(), passwordField.item.text,
undefined, serverField.item.text.trim(),
undefined, page.serverUrl,
]
page.loginFuture = py.callCoro("login_client", args, userId => {
signInTimeout.stop()
errorMessage.text = ""
page.loginFuture = null
print(rememberAccount.checked)
py.callCoro(
rememberAccount.checked ?
"saved_accounts.add": "saved_accounts.delete",
@ -48,7 +57,6 @@ HFlickableColumnPage {
}, (type, args, error, traceback, uuid) => {
page.loginFuture = null
signInTimeout.stop()
let txt = qsTr(
"Invalid request, login type or unknown error: %1",
@ -67,17 +75,23 @@ HFlickableColumnPage {
}
function cancel() {
if (! page.loginFuture) return
if (! page.loginFuture) {
page.exitRequested()
return
}
signInTimeout.stop()
page.loginFuture.cancel()
page.loginFuture = null
}
flickable.topMargin: theme.spacing * 1.5
flickable.bottomMargin: flickable.topMargin
footer: AutoDirectionLayout {
ApplyButton {
enabled: page.canSignIn
id: applyButton
enabled: idField.item.text.trim() && passwordField.item.text
text: qsTr("Sign in")
icon.name: "sign-in"
loading: page.loginFuture !== null
@ -90,54 +104,39 @@ HFlickableColumnPage {
}
}
onKeyboardAccept: page.signIn()
onKeyboardAccept: if (applyButton.enabled) page.signIn()
onKeyboardCancel: page.cancel()
Timer {
id: signInTimeout
interval: 30 * 1000
onTriggered: {
errorMessage.text =
serverField.knownServerChosen ?
qsTr("This server seems unavailable. Verify your inter" +
"net connection or try again in a few minutes.") :
qsTr("This server seems unavailable. Verify the " +
"entered URL, your internet connection or try " +
"again in a few minutes.")
}
}
HRowLayout {
visible: false // TODO
spacing: theme.spacing * 1.25
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: theme.spacing
Layout.bottomMargin: Layout.topMargin
Repeater {
model: ["username", "email", "phone"]
HButton {
icon.name: modelData
circle: true
checked: page.signInWith === modelData
enabled: modelData === "username"
autoExclusive: true
onClicked: page.signInWith = modelData
}
}
icon.name: "sign-in-" + (
page.security === SignIn.Security.Insecure ? "insecure" :
page.security === SignIn.Security.LocalHttp ? "local-http" :
"secure"
)
icon.color:
page.security === SignIn.Security.Insecure ?
theme.colors.negativeBackground :
page.security === SignIn.Security.LocalHttp ?
theme.colors.middleBackground :
theme.colors.positiveBackground
text:
page.security === SignIn.Security.Insecure ?
page.serverUrl :
page.displayUrl.replace(/^(https?:\/\/)?(www\.)?/, "")
onClicked: page.exitRequested()
Layout.alignment: Qt.AlignCenter
Layout.maximumWidth: parent.width
}
HLabeledItem {
id: idField
label.text: qsTr(
page.signInWith === "email" ? "Email:" :
page.signInWith === "phone" ? "Phone:" :
"Username:"
)
label.text: qsTr("Username:")
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 {
id: rememberAccount
checked: true
@ -206,7 +168,6 @@ HFlickableColumnPage {
Layout.fillWidth: true
Layout.topMargin: theme.spacing / 2
Layout.bottomMargin: Layout.topMargin
}
HLabel {

View File

@ -440,6 +440,7 @@ QtObject {
flickable.maximumFlickVelocity = 5000
flickable.flickDeceleration = Math.max(
goFaster ? normalDecel : -Infinity,
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