feat: complete Ancestor quote bot with production-ready Docker support

This commit is contained in:
unfunny
2026-09-13 14:14:38 -04:00
parent f2016da05a
commit fe7351a7dc
1145 changed files with 30890 additions and 97684 deletions

View File

@@ -1,5 +1,5 @@
/*
Copyright 2022 - 2024 The Matrix.org Foundation C.I.C.
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,18 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { parse as parseContentType, type ContentType } from "content-type";
import { parse as parseContentType, ParsedMediaType } from "content-type";
import { logger } from "../logger.ts";
import { sleep } from "../utils.ts";
import {
ConnectionError,
HTTPError,
MatrixError,
MatrixSafetyError,
MatrixSafetyErrorCode,
safeGetRetryAfterMs,
} from "./errors.ts";
import { logger } from "../logger";
import { sleep } from "../utils";
import { ConnectionError, HTTPError, MatrixError } from "./errors";
// Ponyfill for https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout
export function timeoutSignal(ms: number): AbortSignal {
@@ -39,7 +32,7 @@ export function timeoutSignal(ms: number): AbortSignal {
export function anySignal(signals: AbortSignal[]): {
signal: AbortSignal;
cleanup(this: void): void;
cleanup(): void;
} {
const controller = new AbortController();
@@ -79,48 +72,24 @@ export function anySignal(signals: AbortSignal[]): {
* @returns
*/
export function parseErrorResponse(response: XMLHttpRequest | Response, body?: string): Error {
const httpHeaders = isXhr(response)
? new Headers(
response
.getAllResponseHeaders()
.trim()
.split(/[\r\n]+/)
.map((header): [string, string] => {
const colonIdx = header.indexOf(":");
return [header.substring(0, colonIdx), header.substring(colonIdx + 1)];
}),
)
: response.headers;
let contentType: ContentType | null;
let contentType: ParsedMediaType | null;
try {
contentType = getResponseContentType(httpHeaders);
contentType = getResponseContentType(response);
} catch (e) {
return <Error>e;
}
if (contentType?.type === "application/json" && body) {
const errorBody = JSON.parse(body);
if (errorBody.errcode && MatrixSafetyErrorCode.matches(errorBody.errcode)) {
return new MatrixSafetyError(
errorBody,
response.status,
isXhr(response) ? response.responseURL : response.url,
undefined,
httpHeaders,
);
}
return new MatrixError(
errorBody,
JSON.parse(body),
response.status,
isXhr(response) ? response.responseURL : response.url,
undefined,
httpHeaders,
);
}
if (contentType?.type === "text/plain") {
return new HTTPError(`Server returned ${response.status} error: ${body}`, response.status, httpHeaders);
return new HTTPError(`Server returned ${response.status} error: ${body}`, response.status);
}
return new HTTPError(`Server returned ${response.status} error`, response.status, httpHeaders);
return new HTTPError(`Server returned ${response.status} error`, response.status);
}
function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest {
@@ -128,7 +97,7 @@ function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest
}
/**
* extract the Content-Type header from response headers, and
* extract the Content-Type header from the response object, and
* parse it to a `{type, parameters}` object.
*
* returns null if no content-type header could be found.
@@ -136,10 +105,21 @@ function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest
* @param response - response object
* @returns parsed content-type header, or null if not found
*/
function getResponseContentType(headers: Headers): ContentType | null {
const contentType = headers.get("Content-Type");
if (contentType === null) return null;
return parseContentType(contentType);
function getResponseContentType(response: XMLHttpRequest | Response): ParsedMediaType | null {
let contentType: string | null;
if (isXhr(response)) {
contentType = response.getResponseHeader("Content-Type");
} else {
contentType = response.headers.get("Content-Type");
}
if (!contentType) return null;
try {
return parseContentType(contentType);
} catch (e) {
throw new Error(`Error parsing Content-Type '${contentType}': ${e}`);
}
}
/**
@@ -171,42 +151,3 @@ export async function retryNetworkOperation<T>(maxAttempts: number, callback: ()
}
throw lastConnectionError;
}
/**
* Calculate the backoff time for a request retry attempt.
* This produces wait times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the
* failure was due to a rate limited request, the time specified in the error is returned.
*
* Returns -1 if the error is not retryable, or if we reach the maximum number of attempts.
*
* @param err - The error thrown by the http call
* @param attempts - The number of attempts made so far, including the one that just failed.
* @param retryConnectionError - Whether to retry on {@link ConnectionError} (CORS, connection is down, etc.)
*/
export function calculateRetryBackoff(err: any, attempts: number, retryConnectionError: boolean): number {
if (attempts > 4) {
return -1; // give up
}
if (err instanceof ConnectionError && !retryConnectionError) {
return -1;
}
if (err.httpStatus && Math.floor(err.httpStatus / 100) === 4 && err.httpStatus !== 429) {
// client error; no amount of retrying will save you now (except for rate limiting which is handled below)
return -1;
}
if (err.name === "AbortError") {
// this is a client timeout, that is already very high 60s/80s
// we don't want to retry, as it could do it for very long
return -1;
}
// If we are trying to send an event (or similar) that is too large in any way, then retrying won't help
if (err.name === "M_TOO_LARGE") {
return -1;
}
return safeGetRetryAfterMs(err, 1000 * Math.pow(2, attempts));
}