diff --git a/sticker/server/api/setup.py b/sticker/server/api/setup.py
index 7a9dc30..3a60001 100644
--- a/sticker/server/api/setup.py
+++ b/sticker/server/api/setup.py
@@ -13,7 +13,20 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-
from aiohttp import web
+from ..database import User, AccessToken
+
routes = web.RouteTableDef()
+
+
+@routes.get("/whoami")
+async def whoami(req: web.Request) -> web.Response:
+ user: User = req["user"]
+ token: AccessToken = req["token"]
+ return web.json_response({
+ "id": user.id,
+ "widget_secret": user.widget_secret,
+ "homeserver_url": user.homeserver_url,
+ "last_seen": int(token.last_seen_date.timestamp() / 60) * 60,
+ })
diff --git a/sticker/server/database/access_token.py b/sticker/server/database/access_token.py
index 1771328..a5d04ce 100644
--- a/sticker/server/database/access_token.py
+++ b/sticker/server/database/access_token.py
@@ -37,7 +37,8 @@ class AccessToken(Base):
@classmethod
async def get(cls, token_id: int) -> Optional['AccessToken']:
- q = "SELECT user_id, token_hash, last_seen_ip, last_seen_date FROM pack WHERE token_id=$1"
+ q = ("SELECT user_id, token_hash, last_seen_ip, last_seen_date "
+ "FROM access_token WHERE token_id=$1")
row: asyncpg.Record = await cls.db.fetchrow(q, token_id)
if row is None:
return None
@@ -48,7 +49,7 @@ class AccessToken(Base):
== datetime.now().replace(second=0, microsecond=0)):
# Same IP and last seen on this minute, skip update
return
- q = ("UPDATE access_token SET last_seen_ip=$3, last_seen_date=current_timestamp "
+ q = ("UPDATE access_token SET last_seen_ip=$2, last_seen_date=current_timestamp "
"WHERE token_id=$1 RETURNING last_seen_date")
self.last_seen_date = await self.db.fetchval(q, self.token_id, ip)
self.last_seen_ip = ip
diff --git a/web/src/setup/App.js b/web/src/setup/App.js
new file mode 100644
index 0000000..7029b02
--- /dev/null
+++ b/web/src/setup/App.js
@@ -0,0 +1,73 @@
+// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
+// Copyright (C) 2020 Tulir Asokan
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import { useEffect, useState } from "../../lib/preact/hooks.js"
+import { html } from "../../lib/htm/preact.js"
+
+import LoginView from "./LoginView.js"
+import Spinner from "../Spinner.js"
+import * as matrix from "./matrix-api.js"
+import * as sticker from "./sticker-api.js"
+
+const App = () => {
+ const [loggedIn, setLoggedIn] = useState(Boolean(localStorage.mxAccessToken))
+ const [widgetSecret, setWidgetSecret] = useState(null)
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+ if (!loggedIn) {
+ return html`
+ <${LoginView}
+ onLoggedIn=${() => setLoggedIn(Boolean(localStorage.mxAccessToken))}
+ />`
+ }
+
+ useEffect(() => {
+ if (widgetSecret === null) {
+ setLoading(true)
+ const whoamiReceived = data => {
+ setLoading(false)
+ setWidgetSecret(data.widget_secret)
+ }
+ const reauth = async () => {
+ const openIDToken = await matrix.requestOpenIDToken(
+ localStorage.mxHomeserver, localStorage.mxUserID, localStorage.mxAccessToken)
+ const integrationData = await matrix.requestIntegrationToken(openIDToken)
+ localStorage.stickerSetupAccessToken = integrationData.token
+ return await sticker.whoami()
+ }
+ const whoamiErrored = err => {
+ console.error("Setup API whoami returned", err)
+ if (err.code === "NET.MAUNIUM_TOKEN_EXPIRED" || err.code === "M_UNKNOWN_TOKEN") {
+ return reauth().then(whoamiReceived)
+ } else {
+ throw err
+ }
+ }
+ sticker.whoami().then(whoamiReceived, whoamiErrored).catch(err => {
+ setLoading(false)
+ setError(err.message)
+ })
+ }
+ }, [])
+
+ if (loading) {
+ return html`<${Spinner} size=80 green />`
+ }
+
+ return html`${widgetSecret}`
+}
+
+export default App
diff --git a/web/src/setup/LoginView.js b/web/src/setup/LoginView.js
index ce5abe7..4563234 100644
--- a/web/src/setup/LoginView.js
+++ b/web/src/setup/LoginView.js
@@ -16,13 +16,7 @@
import { useEffect, useLayoutEffect, useRef, useState } from "../../lib/preact/hooks.js"
import { html } from "../../lib/htm/preact.js"
-import {
- getLoginFlows,
- loginMatrix,
- requestIntegrationToken,
- requestOpenIDToken,
- resolveWellKnown,
-} from "./matrix-api.js"
+import * as matrix from "./matrix-api.js"
import Button from "../Button.js"
import Spinner from "../Spinner.js"
@@ -87,11 +81,11 @@ const LoginView = ({ onLoggedIn }) => {
previousServerValue.current = server
setSupportedFlows(null)
setError(null)
- resolveWellKnown(server).then(url => {
+ matrix.resolveWellKnown(server).then(url => {
setServerURL(url)
localStorage.mxServerName = server
localStorage.mxHomeserver = url
- return getLoginFlows(url)
+ return matrix.getLoginFlows(url)
}).then(flows => {
setSupportedFlows(flows)
}).catch(err => {
@@ -135,15 +129,13 @@ const LoginView = ({ onLoggedIn }) => {
}
try {
const actualServerURL = serverURLOverride || serverURL
- const [accessToken, userID, realURL] = await loginMatrix(actualServerURL, authInfo)
- console.log(userID, realURL)
- const openIDToken = await requestOpenIDToken(realURL, userID, accessToken)
- console.log(openIDToken)
- const integrationData = await requestIntegrationToken(openIDToken)
- console.log(integrationData)
+ const [accessToken, userID, realURL] = await matrix.login(actualServerURL, authInfo)
+ const openIDToken = await matrix.requestOpenIDToken(realURL, userID, accessToken)
+ const integrationData = await matrix.requestIntegrationToken(openIDToken)
+ localStorage.mxHomeserver = realURL
localStorage.mxAccessToken = accessToken
localStorage.mxUserID = userID
- localStorage.accessToken = integrationData.token
+ localStorage.stickerSetupAccessToken = integrationData.token
onLoggedIn()
} catch (err) {
setError(err.message)
diff --git a/web/src/setup/index.js b/web/src/setup/index.js
index efa122e..11b0140 100644
--- a/web/src/setup/index.js
+++ b/web/src/setup/index.js
@@ -15,6 +15,6 @@
// along with this program. If not, see .
import { html, render } from "../../lib/htm/preact.js"
-import LoginView from "./LoginView.js"
+import App from "./App.js"
-render(html`<${LoginView} onLoggedIn=${() => console.log("Logged in")}/>`, document.body)
+render(html`<${App} />`, document.body)
diff --git a/web/src/setup/matrix-api.js b/web/src/setup/matrix-api.js
index 09ef99b..ca36c20 100644
--- a/web/src/setup/matrix-api.js
+++ b/web/src/setup/matrix-api.js
@@ -13,7 +13,6 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-
import { tryFetch, integrationPrefix } from "./tryGet.js"
export const resolveWellKnown = async (server) => {
@@ -41,7 +40,7 @@ export const getLoginFlows = async (address) => {
return flows
}
-export const loginMatrix = async (address, authInfo) => {
+export const login = async (address, authInfo) => {
const data = await tryFetch(`${address}/_matrix/client/r0/login`, {
method: "POST",
body: JSON.stringify({
@@ -67,6 +66,17 @@ export const loginMatrix = async (address, authInfo) => {
return [data.access_token, data.user_id, address]
}
+export const whoami = (address, accessToken) => tryFetch(
+ `${address}/_matrix/client/r0/account/whoami`,
+ {
+ headers: { Authorization: `Bearer ${accessToken}` },
+ },
+ {
+ service: address,
+ requestType: "whoami",
+ },
+)
+
export const requestOpenIDToken = (address, userID, accessToken) => tryFetch(
`${address}/_matrix/client/r0/user/${userID}/openid/request_token`,
{
diff --git a/web/src/setup/sticker-api.js b/web/src/setup/sticker-api.js
new file mode 100644
index 0000000..ca5ba3e
--- /dev/null
+++ b/web/src/setup/sticker-api.js
@@ -0,0 +1,34 @@
+// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
+// Copyright (C) 2020 Tulir Asokan
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { tryFetch as tryFetchDefault, setupPrefix } from "./tryGet.js"
+
+const service = "setup API"
+
+const tryFetch = (url, options, reqInfo) => {
+ if (!options.headers?.Authorization) {
+ if (!options.headers) {
+ options.headers = {}
+ }
+ options.headers.Authorization = `Bearer ${localStorage.stickerSetupAccessToken}`
+ }
+ return tryFetchDefault(url, options, reqInfo)
+}
+
+export const whoami = () => tryFetch(
+ `${setupPrefix}/whoami`,
+ {}, { service, requestType: "whoami" },
+)
diff --git a/web/src/setup/tryGet.js b/web/src/setup/tryGet.js
index 39d3680..a671524 100644
--- a/web/src/setup/tryGet.js
+++ b/web/src/setup/tryGet.js
@@ -13,8 +13,8 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-
export const integrationPrefix = "../_matrix/integrations/v1"
+export const setupPrefix = "api"
export const queryToURL = (url, query) => {
if (!Array.isArray(query)) {
@@ -26,15 +26,19 @@ export const queryToURL = (url, query) => {
return url
}
+class MatrixError extends Error {
+ constructor(data, status) {
+ super(data.error)
+ this.code = data.errcode
+ this.httpStatus = status
+ }
+}
+
export const tryFetch = async (url, options, reqInfo) => {
if (options.query) {
url = queryToURL(url, options.query)
delete options.query
}
- options.headers = {
- Authorization: `Bearer ${localStorage.accessToken}`,
- ...options.headers,
- }
const reqName = `${reqInfo.service} ${reqInfo.requestType}`
let resp
try {
@@ -59,7 +63,9 @@ export const tryFetch = async (url, options, reqInfo) => {
console.error(reqName, "request JSON parse failed:", err)
throw new Error(`Invalid response from ${reqInfo.service}`)
}
- if (resp.status >= 400) {
+ if (data.error && data.errcode) {
+ throw new MatrixError(data, resp.status)
+ } else if (resp.status >= 400) {
console.error("Unexpected", reqName, "request status:", resp.status, data)
throw new Error(data.error || data.message || `Invalid response from ${reqInfo.service}`)
}