Implement signed cdn urls
This commit is contained in:
parent
1d8e081fd8
commit
080b2c7d38
@ -212,6 +212,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"GuildSubscriptionSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"activities": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"threads": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"typing": {
|
||||
"enum": [
|
||||
true
|
||||
],
|
||||
"type": "boolean"
|
||||
},
|
||||
"members": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"member_updates": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"thread_member_lists": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ActivitySchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -3972,6 +4014,21 @@
|
||||
"widget_enabled"
|
||||
]
|
||||
},
|
||||
"RefreshedUrl": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"original": {
|
||||
"type": "string"
|
||||
},
|
||||
"refreshed": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"original",
|
||||
"refreshed"
|
||||
]
|
||||
},
|
||||
"TenorGifResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -4137,6 +4194,12 @@
|
||||
"$ref": "#/components/schemas/Channel"
|
||||
}
|
||||
},
|
||||
"members": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Member"
|
||||
}
|
||||
},
|
||||
"region": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -4215,12 +4278,6 @@
|
||||
"presence_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"members": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Member"
|
||||
}
|
||||
},
|
||||
"template_id": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -4620,9 +4677,7 @@
|
||||
"channel": {
|
||||
"$ref": "#/components/schemas/RateLimitOptions"
|
||||
},
|
||||
"auth": {
|
||||
"$ref": "#/components/schemas/AuthRateLimit"
|
||||
}
|
||||
"auth": {}
|
||||
},
|
||||
"required": [
|
||||
"auth",
|
||||
@ -4631,21 +4686,6 @@
|
||||
"webhook"
|
||||
]
|
||||
},
|
||||
"AuthRateLimit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"login": {
|
||||
"$ref": "#/components/schemas/RateLimitOptions"
|
||||
},
|
||||
"register": {
|
||||
"$ref": "#/components/schemas/RateLimitOptions"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"login",
|
||||
"register"
|
||||
]
|
||||
},
|
||||
"GlobalRateLimits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -5390,6 +5430,68 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LazyRequestSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"guild_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"channels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"activities": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"threads": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"typing": {
|
||||
"enum": [
|
||||
true
|
||||
],
|
||||
"type": "boolean"
|
||||
},
|
||||
"members": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"member_updates": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"thread_member_lists": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"guild_id"
|
||||
]
|
||||
},
|
||||
"GuildSubscriptionsBulkSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"subscriptions": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/components/schemas/GuildSubscriptionSchema"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"subscriptions"
|
||||
]
|
||||
},
|
||||
"GuildTemplateCreateSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -5740,51 +5842,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LazyRequestSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"guild_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"channels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"activities": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"threads": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"typing": {
|
||||
"enum": [
|
||||
true
|
||||
],
|
||||
"type": "boolean"
|
||||
},
|
||||
"members": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"thread_member_lists": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"guild_id"
|
||||
]
|
||||
},
|
||||
"LoginSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -6249,6 +6306,20 @@
|
||||
"before"
|
||||
]
|
||||
},
|
||||
"RefreshUrlsRequestSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attachment_urls": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"attachment_urls"
|
||||
]
|
||||
},
|
||||
"RegisterSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -7090,6 +7161,20 @@
|
||||
"location"
|
||||
]
|
||||
},
|
||||
"RefreshUrlsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"refreshed_urls": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/RefreshedUrl"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"refreshed_urls"
|
||||
]
|
||||
},
|
||||
"TeamListResponse": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
@ -9055,6 +9140,9 @@
|
||||
{
|
||||
"name": "applications"
|
||||
},
|
||||
{
|
||||
"name": "attachments"
|
||||
},
|
||||
{
|
||||
"name": "auth"
|
||||
},
|
||||
@ -11652,6 +11740,25 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/policies/instance/config/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"policies"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ping/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
@ -18302,6 +18409,50 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/attachments/refresh-urls/": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RefreshUrlsRequestSchema"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RefreshUrlsResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/APIErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"attachments"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/applications/": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
15494
assets/schemas.json
15494
assets/schemas.json
File diff suppressed because it is too large
Load Diff
1
package-lock.json
generated
1
package-lock.json
generated
@ -36,6 +36,7 @@
|
||||
"missing-native-js-functions": "^1.4.3",
|
||||
"module-alias": "^2.2.3",
|
||||
"morgan": "^1.10.0",
|
||||
"ms": "^2.1.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"node-2fa": "^2.0.3",
|
||||
|
@ -96,6 +96,7 @@
|
||||
"missing-native-js-functions": "^1.4.3",
|
||||
"module-alias": "^2.2.3",
|
||||
"morgan": "^1.10.0",
|
||||
"ms": "^2.1.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"node-2fa": "^2.0.3",
|
||||
|
48
src/api/routes/attachments/refresh-urls.ts
Normal file
48
src/api/routes/attachments/refresh-urls.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { route } from "@spacebar/api";
|
||||
import { RefreshUrlsRequestSchema, resignUrl } from "@spacebar/util";
|
||||
import { Request, Response, Router } from "express";
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
route({
|
||||
requestBody: "RefreshUrlsRequestSchema",
|
||||
responses: {
|
||||
200: {
|
||||
body: "RefreshUrlsResponse",
|
||||
},
|
||||
400: {
|
||||
body: "APIErrorResponse",
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (req: Request, res: Response) => {
|
||||
const { attachment_urls } = req.body as RefreshUrlsRequestSchema;
|
||||
|
||||
const refreshed_urls = attachment_urls.map(resignUrl);
|
||||
|
||||
return res.status(200).json({
|
||||
refreshed_urls,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
@ -35,6 +35,7 @@ import {
|
||||
emitEvent,
|
||||
getPermission,
|
||||
isTextChannel,
|
||||
resignUrl,
|
||||
uploadFile,
|
||||
} from "@spacebar/util";
|
||||
import { Request, Response, Router } from "express";
|
||||
@ -199,16 +200,18 @@ router.get(
|
||||
? y.proxy_url
|
||||
: `https://example.org${y.proxy_url}`;
|
||||
|
||||
let pathname = new URL(uri).pathname;
|
||||
while (
|
||||
pathname.split("/")[0] != "attachments" &&
|
||||
pathname.length > 30
|
||||
) {
|
||||
pathname = pathname.split("/").slice(1).join("/");
|
||||
const url = new URL(uri);
|
||||
if (endpoint) {
|
||||
const newBase = new URL(endpoint);
|
||||
url.protocol = newBase.protocol;
|
||||
url.hostname = newBase.hostname;
|
||||
url.port = newBase.port;
|
||||
}
|
||||
if (!endpoint?.endsWith("/")) pathname = "/" + pathname;
|
||||
|
||||
y.proxy_url = `${endpoint == null ? "" : endpoint}${pathname}`;
|
||||
y.proxy_url = url.toString();
|
||||
|
||||
y.proxy_url = resignUrl(y.proxy_url);
|
||||
y.url = resignUrl(y.url);
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -16,13 +16,18 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Router, Response, Request } from "express";
|
||||
import { Config, Snowflake } from "@spacebar/util";
|
||||
import { storage } from "../util/Storage";
|
||||
import {
|
||||
Config,
|
||||
getUrlSignature,
|
||||
hasValidSignature,
|
||||
Snowflake,
|
||||
} from "@spacebar/util";
|
||||
import { Request, Response, Router } from "express";
|
||||
import FileType from "file-type";
|
||||
import imageSize from "image-size";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { multer } from "../util/multer";
|
||||
import imageSize from "image-size";
|
||||
import { storage } from "../util/Storage";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -39,6 +44,7 @@ router.post(
|
||||
async (req: Request, res: Response) => {
|
||||
if (req.headers.signature !== Config.get().security.requestSignature)
|
||||
throw new HTTPError("Invalid request signature");
|
||||
|
||||
if (!req.file) throw new HTTPError("file missing");
|
||||
|
||||
const { buffer, mimetype, size, originalname } = req.file;
|
||||
@ -63,12 +69,20 @@ router.post(
|
||||
}
|
||||
}
|
||||
|
||||
let finalUrl = `${endpoint}/${path}`;
|
||||
|
||||
if (Config.get().security.cdnSignUrls) {
|
||||
const signatureData = getUrlSignature(path);
|
||||
console.log(signatureData);
|
||||
finalUrl = `${finalUrl}?ex=${signatureData.expiresAt}&is=${signatureData.issuedAt}&hm=${signatureData.hash}&`;
|
||||
}
|
||||
|
||||
const file = {
|
||||
id,
|
||||
content_type: mimetype,
|
||||
filename: filename,
|
||||
size,
|
||||
url: `${endpoint}/${path}`,
|
||||
url: finalUrl,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
@ -84,6 +98,14 @@ router.get(
|
||||
// const { format } = req.query;
|
||||
|
||||
const path = `attachments/${channel_id}/${id}/${filename}`;
|
||||
|
||||
if (
|
||||
Config.get().security.cdnSignUrls &&
|
||||
!hasValidSignature(path, req.query)
|
||||
) {
|
||||
return res.status(404).send("This content is no longer available.");
|
||||
}
|
||||
|
||||
const file = await storage.get(path);
|
||||
if (!file) throw new HTTPError("File not found");
|
||||
const type = await FileType.fromBuffer(file);
|
||||
|
136
src/util/Signing.ts
Normal file
136
src/util/Signing.ts
Normal file
@ -0,0 +1,136 @@
|
||||
/*
|
||||
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Config } from "@spacebar/util";
|
||||
import { createHmac, timingSafeEqual } from "crypto";
|
||||
import ms, { StringValue } from "ms";
|
||||
import { ParsedQs } from "qs";
|
||||
|
||||
export const getUrlSignature = (path: string) => {
|
||||
const { cdnSignatureKey, cdnSignatureDuration } = Config.get().security;
|
||||
|
||||
// calculate the expiration time
|
||||
const now = Date.now();
|
||||
const issuedAt = now.toString(16);
|
||||
const expiresAt = (now + ms(cdnSignatureDuration as StringValue)).toString(
|
||||
16,
|
||||
);
|
||||
|
||||
// hash the url with the cdnSignatureKey
|
||||
const hash = createHmac("sha256", cdnSignatureKey as string)
|
||||
.update(path)
|
||||
.update(issuedAt)
|
||||
.update(expiresAt)
|
||||
.digest("hex");
|
||||
|
||||
return {
|
||||
hash,
|
||||
issuedAt,
|
||||
expiresAt,
|
||||
};
|
||||
};
|
||||
|
||||
export const calculateHash = (
|
||||
url: string,
|
||||
issuedAt: string,
|
||||
expiresAt: string,
|
||||
) => {
|
||||
const { cdnSignatureKey } = Config.get().security;
|
||||
const hash = createHmac("sha256", cdnSignatureKey as string)
|
||||
.update(url)
|
||||
.update(issuedAt)
|
||||
.update(expiresAt)
|
||||
.digest("hex");
|
||||
return hash;
|
||||
};
|
||||
|
||||
export const isExpired = (ex: string, is: string) => {
|
||||
// convert issued at
|
||||
const issuedAt = parseInt(is, 16);
|
||||
const expiresAt = parseInt(ex, 16);
|
||||
|
||||
if (Number.isNaN(issuedAt) || Number.isNaN(expiresAt)) {
|
||||
// console.debug("Invalid timestamps in query");
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentTime = Date.now();
|
||||
const isExpired = expiresAt < currentTime;
|
||||
const isValidIssuedAt = issuedAt < currentTime;
|
||||
if (isExpired || !isValidIssuedAt) {
|
||||
// console.debug("Signature expired");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const hasValidSignature = (path: string, query: ParsedQs) => {
|
||||
// get url path
|
||||
const { ex, is, hm } = query;
|
||||
|
||||
// if the required query parameters are not present, return false
|
||||
if (!ex || !is || !hm) return false;
|
||||
|
||||
// check if the signature is expired
|
||||
if (isExpired(ex as string, is as string)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const calcd = calculateHash(path, is as string, ex as string);
|
||||
const calculated = Buffer.from(calcd);
|
||||
const received = Buffer.from(hm as string);
|
||||
|
||||
const isHashValid =
|
||||
calculated.length === received.length &&
|
||||
timingSafeEqual(calculated, received);
|
||||
// if (!isHashValid) {
|
||||
// console.debug("Invalid signature");
|
||||
// console.debug(calcd, hm);
|
||||
// }
|
||||
return isHashValid;
|
||||
};
|
||||
|
||||
export const resignUrl = (attachmentUrl: string) => {
|
||||
const url = new URL(attachmentUrl);
|
||||
|
||||
// if theres an existing signature, check if its expired or not. no reason to resign if its not expired
|
||||
if (url.searchParams.has("ex") && url.searchParams.has("is")) {
|
||||
// extract the ex and is
|
||||
const ex = url.searchParams.get("ex");
|
||||
const is = url.searchParams.get("is");
|
||||
|
||||
if (!isExpired(ex as string, is as string)) {
|
||||
// if the signature is not expired, return the url as is
|
||||
return attachmentUrl;
|
||||
}
|
||||
}
|
||||
|
||||
let path = url.pathname;
|
||||
// strip / from the start
|
||||
if (path.startsWith("/")) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
|
||||
const { hash, issuedAt, expiresAt } = getUrlSignature(path);
|
||||
url.searchParams.set("ex", expiresAt);
|
||||
url.searchParams.set("is", issuedAt);
|
||||
url.searchParams.set("hm", hash);
|
||||
|
||||
return url.toString();
|
||||
};
|
@ -37,4 +37,8 @@ export class SecurityConfiguration {
|
||||
mfaBackupCodeCount: number = 10;
|
||||
statsWorldReadable: boolean = true;
|
||||
defaultRegistrationTokenExpiration: number = 1000 * 60 * 60 * 24 * 7; //1 week
|
||||
// cdn signed urls
|
||||
cdnSignUrls: boolean = false;
|
||||
cdnSignatureKey: string = crypto.randomBytes(32).toString("base64");
|
||||
cdnSignatureDuration: string = "24h";
|
||||
}
|
||||
|
@ -16,6 +16,8 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// NOTE: !! DO NOT REORDER THE IMPORTS !!
|
||||
|
||||
import "reflect-metadata";
|
||||
|
||||
export * from "./util/index";
|
||||
@ -26,3 +28,4 @@ export * from "./schemas";
|
||||
export * from "./imports";
|
||||
export * from "./config";
|
||||
export * from "./connections";
|
||||
export * from "./Signing"
|
21
src/util/schemas/RefreshUrlsRequestSchema.ts
Normal file
21
src/util/schemas/RefreshUrlsRequestSchema.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export interface RefreshUrlsRequestSchema {
|
||||
attachment_urls: string[];
|
||||
}
|
@ -59,6 +59,7 @@ export * from "./MfaCodesSchema";
|
||||
export * from "./ModifyGuildStickerSchema";
|
||||
export * from "./PasswordResetSchema";
|
||||
export * from "./PurgeSchema";
|
||||
export * from "./RefreshUrlsRequestSchema";
|
||||
export * from "./RegisterSchema";
|
||||
export * from "./RelationshipPostSchema";
|
||||
export * from "./RelationshipPutSchema";
|
||||
|
26
src/util/schemas/responses/RefreshUrlsResponse.ts
Normal file
26
src/util/schemas/responses/RefreshUrlsResponse.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||
Copyright (C) 2023 Spacebar and Spacebar Contributors
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export interface RefreshedUrl {
|
||||
original: string;
|
||||
refreshed: string;
|
||||
}
|
||||
|
||||
export interface RefreshUrlsResponse {
|
||||
refreshed_urls: RefreshedUrl[];
|
||||
}
|
@ -44,6 +44,7 @@ export * from "./InstanceStatsResponse";
|
||||
export * from "./LocationMetadataResponse";
|
||||
export * from "./MemberJoinGuildResponse";
|
||||
export * from "./OAuthAuthorizeResponse";
|
||||
export * from "./RefreshUrlsResponse";
|
||||
export * from "./TeamListResponse";
|
||||
export * from "./Tenor";
|
||||
export * from "./TokenResponse";
|
||||
|
Loading…
x
Reference in New Issue
Block a user