Files
delete-your-element/src/matrix/mreq.js
2026-01-07 02:43:20 +13:00

114 lines
3.6 KiB
JavaScript

// @ts-check
const stream = require("stream")
const streamWeb = require("stream/web")
const {buffer} = require("stream/consumers")
const mixin = require("@cloudrac3r/mixin-deep")
const {reg} = require("./read-registration.js")
const baseUrl = `${reg.ooye.server_origin}/_matrix`
class MatrixServerError extends Error {
constructor(data, opts) {
super(data.error || data.errcode)
this.data = data
/** @type {string} */
this.errcode = data.errcode
this.opts = opts
}
}
/**
* @param {Response} res
* @param {object} opts
*/
async function makeMatrixServerError(res, opts = {}) {
delete opts.headers?.["Authorization"]
if (res.headers.get("content-type") === "application/json") {
return new MatrixServerError(await res.json(), opts)
} else if (res.headers.get("content-type")?.startsWith("text/")) {
return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, message: await res.text()}, opts)
} else {
return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, content_type: res.headers.get("content-type")}, opts)
}
}
/**
* @param {undefined | string | object | streamWeb.ReadableStream | stream.Readable} body
* @returns {Promise<string | streamWeb.ReadableStream | stream.Readable | Buffer>}
*/
async function _convertBody(body) {
if (body == undefined || Object.is(body.constructor, Object)) {
return JSON.stringify(body) // almost every POST request is going to follow this one
} else if (body instanceof stream.Readable && reg.ooye.content_length_workaround) {
return await buffer(body) // content length workaround is set, so convert to buffer. the buffer consumer accepts node streams.
} else if (body instanceof stream.Readable) {
return stream.Readable.toWeb(body) // native fetch can only consume web streams
} else if (body instanceof streamWeb.ReadableStream && reg.ooye.content_length_workaround) {
return await buffer(body) // content lenght workaround is set, so convert to buffer. the buffer consumer accepts async iterables, which web streams are.
}
return body
}
/* c8 ignore start */
/**
* @param {string} method
* @param {string} url
* @param {string | object | streamWeb.ReadableStream | stream.Readable} [bodyIn]
* @param {any} [extra]
*/
async function mreq(method, url, bodyIn, extra = {}) {
const body = await _convertBody(bodyIn)
/** @type {RequestInit} */
const opts = mixin({
method,
body,
headers: {
Authorization: `Bearer ${reg.as_token}`
},
...(body && {duplex: "half"}), // https://github.com/octokit/request.js/pull/571/files
}, extra)
const res = await fetch(baseUrl + url, opts)
const text = await res.text()
try {
/** @type {any} */
var root = JSON.parse(text)
} catch (e) {
throw new MatrixServerError(text, {baseUrl, url, ...opts})
}
if (!res.ok || root.errcode) {
delete opts.headers?.["Authorization"]
throw new MatrixServerError(root, {baseUrl, url, ...opts})
}
return root
}
/**
* JavaScript doesn't have Racket-like parameters with dynamic scoping, so
* do NOT do anything else at the same time as this.
* @template T
* @param {string} token
* @param {(...arg: any[]) => Promise<T>} callback
* @returns {Promise<T>}
*/
async function withAccessToken(token, callback) {
const prevToken = reg.as_token
reg.as_token = token
try {
return await callback()
} finally {
reg.as_token = prevToken
}
}
module.exports.MatrixServerError = MatrixServerError
module.exports.makeMatrixServerError = makeMatrixServerError
module.exports.baseUrl = baseUrl
module.exports.mreq = mreq
module.exports.withAccessToken = withAccessToken
module.exports._convertBody = _convertBody