const tippy = require("tippy.js"); const {q, ElemJS, ejs} = require("./basic.js") const {S_RE_DOMAIN, S_RE_IPV6, S_RE_IPV4} = require("./re.js") const password = q("#password") // A regex matching a lossy MXID // Groups: // 1: username/localpart // MAYBE WITH // 2: hostname/serverpart // 3: domain // OR // 4: IP address // 5: IPv4 address // OR // 6: IPv6 address // MAYBE WITH // 7: port number const RE_LOSSY_MXID = new RegExp(`^@?([a-z0-9._=-]+?)(?::((?:(${S_RE_DOMAIN})|((${S_RE_IPV4})|(\\[${S_RE_IPV6}])))(?::(\\d+))?))?$`) window.RE_LOSSY_MXID = RE_LOSSY_MXID class Username extends ElemJS { constructor(homeserver) { super(q("#username")) this.homeserver = homeserver; this.on("change", this.updateServer.bind(this)) } isValid() { return !!this.element.value.match(RE_LOSSY_MXID) } getUsername() { return this.element.value.match(RE_LOSSY_MXID)[1] } getServer() { const server = this.element.value.match(RE_LOSSY_MXID) if (server && server[2]) return server[2] else return null } updateServer() { if (!this.isValid()) return if (this.getServer()) this.homeserver.suggest(this.getServer()) } } class Homeserver extends ElemJS { constructor() { super(q("#homeserver")); this.tippy = tippy.default(q("#homeserver-question"), { content: q("#homeserver-popup-template").innerHTML, allowHTML: true, interactive: true, interactiveBorder: 10, trigger: "mouseenter", theme: "carbon", arrow: tippy.roundArrow, }) } suggest(value) { this.element.placeholder = value } getServer() { return this.element.value || this.element.placeholder; } } class Feedback extends ElemJS { constructor() { super(q("#feedback")) this.loading = false this.loadingIcon = ejs("span").class("loading-icon") this.messageSpan = ejs("span") this.child(this.messageSpan) } setLoading(state) { if (this.loading && !state) { this.loadingIcon.remove() } else if (!this.loading && state) { this.childAt(0, this.loadingIcon) } this.loading = state } message(content, isError) { this.removeClass("form-feedback") this.removeClass("form-error") if (content) this.class("form-feedback") if (isError) this.class("form-error") this.messageSpan.text(content) } } class Form extends ElemJS { constructor(username, feedback, homeserver) { super(q("#form")) this.processing = false this.username = username this.feedback = feedback this.homeserver = homeserver this.on("submit", this.submit.bind(this)) } async submit() { if (this.processing) return this.processing = true if (!this.username.isValid()) return this.cancel("Username is not valid.") // Resolve homeserver address let domain try { domain = await this.findHomeserver(this.homeserver.getServer()) } catch (e) { return this.cancel(e.message) } // Request access token this.status("Logging in...") const root = await fetch(`${domain}/_matrix/client/r0/login`, { method: "POST", body: JSON.stringify({ type: "m.login.password", user: this.username.getUsername(), password: password.value }) }).then(res => res.json()) if (!root.access_token) { if (root.error) { this.cancel(`Server said: ${root.error}`) } else { this.cancel("Login mysteriously failed.") console.error(root) } return } localStorage.setItem("mx_user_id", root.user_id) localStorage.setItem("domain", domain) localStorage.setItem("access_token", root.access_token) location.assign("../") } async findHomeserver(address, maxDepth = 5) { //Protects from servers sending us on a redirect loop maxDepth-- if (maxDepth <= 0) throw new Error(`Failed to look up homeserver, maximum search depth reached`) //Normalise the address if (!address.match(/^https?:\/\//)) { console.warn(`${address} doesn't specify the protocol, assuming https`) address = "https://" + address } address = address.replace(/\/*$/, "") this.status(`Looking up homeserver... trying ${address}`) // Check if we found the actual matrix server try { const versionsReq = await fetch(`${address}/_matrix/client/versions`) if (versionsReq.ok) { const versions = await versionsReq.json() if (Array.isArray(versions.versions)) return address } } catch (e) {} // Find the next matrix server in the chain const root = await fetch(`${address}/.well-known/matrix/client`).then(res => res.json()).catch(e => { console.error(e) throw new Error(`Failed to look up server ${address}`) }) let nextAddress = root["m.homeserver"].base_url nextAddress = nextAddress.replace(/\/*$/, "") if (address === nextAddress) { throw new Error(`Failed to look up server ${address}, /.well-known/matrix/client found a redirect loop`); } return this.findHomeserver(nextAddress, maxDepth) } status(message) { this.feedback.setLoading(true) this.feedback.message(message) } cancel(message) { this.processing = false this.feedback.setLoading(false) this.feedback.message(message, true) } } const homeserver = new Homeserver() const username = new Username(homeserver) const feedback = new Feedback() const form = new Form(username, feedback, homeserver)