Fix sending files through a rest proxy (#2593)

* Fix rest proxy not working with files

* Fix some credits

* Add tests

* Fix test

* Remove some usage of any

* Fix mime matching

* Fix formatting issues
This commit is contained in:
Danny May
2022-11-20 22:50:50 +00:00
committed by GitHub
parent 5ad03ac562
commit 0910965de5
5 changed files with 351 additions and 120 deletions

View File

@@ -1,7 +1,8 @@
import { RestManager } from "./restManager.ts";
import { FileContent } from "../types/discordeno.ts";
import { decode } from "../util/base64.ts";
import { USER_AGENT } from "../util/constants.ts";
import { RequestMethod } from "./rest.ts";
import { RestManager } from "./restManager.ts";
/** Creates the request body and headers that are necessary to send a request. Will handle different types of methods and everything necessary for discord. */
export function createRequestBody(rest: RestManager, options: CreateRequestBodyOptions) {
@@ -31,29 +32,19 @@ export function createRequestBody(rest: RestManager, options: CreateRequestBodyO
// IF A FILE/ATTACHMENT IS PRESENT WE NEED SPECIAL HANDLING
if (options.body?.file) {
if (!Array.isArray(options.body.file)) {
options.body.file = [options.body.file];
}
const files = findFiles(options.body.file);
const form = new FormData();
// WHEN CREATING A STICKER, DISCORD WANTS FORM DATA ONLY
if (options.url?.endsWith("/stickers") && options.method === "POST") {
form.append(
`file`,
(options.body.file as FileContent[])[0].blob,
(options.body.file as FileContent[])[0].name,
);
form.append(`file`, files[0].blob, files[0].name);
form.append(`name`, options.body.name as string);
form.append(`description`, options.body.description as string);
form.append(`tags`, options.body.tags as string);
} else {
for (let i = 0; i < (options.body.file as FileContent[]).length; i++) {
form.append(
`file${i}`,
(options.body.file as FileContent[])[i].blob,
(options.body.file as FileContent[])[i].name,
);
for (let i = 0; i < files.length; i++) {
form.append(`file${i}`, files[i].blob, files[i].name);
}
form.append("payload_json", JSON.stringify({ ...options.body, file: undefined }));
@@ -78,3 +69,39 @@ export interface CreateRequestBodyOptions {
unauthorized?: boolean;
url?: string;
}
function findFiles(file: unknown): FileContent[] {
if (!file) {
return [];
}
const files: unknown[] = Array.isArray(file) ? file : [file];
return files.filter(coerceToFileContent);
}
function coerceToFileContent(value: unknown): value is FileContent {
if (!value || typeof value !== "object") {
return false;
}
const file = value as Record<string, unknown>;
if (typeof file.name !== "string") {
return false;
}
switch (typeof file.blob) {
case "string": {
const match = file.blob.match(/^data:(?<mimeType>[a-zA-Z0-9\/]*);base64,(?<content>.*)$/);
if (match?.groups === undefined) {
return false;
}
const { mimeType, content } = match.groups;
file.blob = new Blob([decode(content)], { type: mimeType });
return true;
}
case "object":
return file.blob instanceof Blob;
default:
return false;
}
}

View File

@@ -1,6 +1,6 @@
import { FileContent } from "../mod.ts";
import { API_VERSION, BASE_URL, baseEndpoints } from "../util/constants.ts";
import { encode } from "../util/urlToBase64.ts";
import { encode } from "../util/base64.ts";
import { RequestMethod, RestRequestRejection, RestRequestResponse } from "./rest.ts";
import { RestManager } from "./restManager.ts";

View File

@@ -1,4 +1,5 @@
import { Collection, formatImageURL, hasProperty, iconBigintToHash, iconHashToBigInt, validateLength } from "../mod.ts";
import { decode, encode } from "../util/base64.ts";
import { bigintToSnowflake, snowflakeToBigint } from "../util/bigint.ts";
import { removeTokenPrefix } from "../util/token.ts";
import { assertEquals, assertExists, assertNotEquals } from "./deps.ts";
@@ -407,3 +408,34 @@ Deno.test({
});
},
});
Deno.test({
name: "[utils] encode some bytes to base64",
ignore: Deno.env.get("TEST_ENV") === "INTEGRATION",
async fn(t) {
assertEquals(encode(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])), "AQIDBAUGBwgJCg==");
},
});
Deno.test({
name: "[utils] decode some base64 to bytes",
ignore: Deno.env.get("TEST_ENV") === "INTEGRATION",
async fn(t) {
assertEquals(decode("AQIDBAUGBwgJCg=="), new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));
},
});
Deno.test({
name: "[utils] encode/decode base64 roundtrip should work",
ignore: Deno.env.get("TEST_ENV") === "INTEGRATION",
async fn(t) {
for (let i = 0; i < 10; i++) {
const bytes = [];
for (let i = 0; i < 10000; i++) {
bytes.push(Math.floor(Math.random() * 256));
}
const data = new Uint8Array(bytes);
assertEquals(decode(encode(data)), data);
}
},
});

274
util/base64.ts Normal file
View File

@@ -0,0 +1,274 @@
/**
* CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
* Encodes a given Uint8Array, ArrayBuffer or string into RFC4648 base64 representation
* @param data
*/
export function encode(data: ArrayBuffer | string): string {
const uint8 = typeof data === "string"
? new TextEncoder().encode(data)
: data instanceof Uint8Array
? data
: new Uint8Array(data);
let result = "",
i;
const l = uint8.length;
for (i = 2; i < l; i += 3) {
result += base64abc[uint8[i - 2] >> 2];
result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)];
result += base64abc[((uint8[i - 1] & 0x0f) << 2) | (uint8[i] >> 6)];
result += base64abc[uint8[i] & 0x3f];
}
if (i === l + 1) {
// 1 octet yet to write
result += base64abc[uint8[i - 2] >> 2];
result += base64abc[(uint8[i - 2] & 0x03) << 4];
result += "==";
}
if (i === l) {
// 2 octets yet to write
result += base64abc[uint8[i - 2] >> 2];
result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)];
result += base64abc[(uint8[i - 1] & 0x0f) << 2];
result += "=";
}
return result;
}
/**
* CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
* Decodes RFC4648 base64 string into an Uint8Array
* @param data
*/
export function decode(data: string): Uint8Array {
if (data.length % 4 !== 0) {
throw new Error("Unable to parse base64 string.");
}
const index = data.indexOf("=");
if (index !== -1 && index < data.length - 2) {
throw new Error("Unable to parse base64 string.");
}
let missingOctets = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0,
n = data.length,
result = new Uint8Array(3 * (n / 4)),
buffer;
for (let i = 0, j = 0; i < n; i += 4, j += 3) {
buffer = getBase64Code(data.charCodeAt(i)) << 18 |
getBase64Code(data.charCodeAt(i + 1)) << 12 |
getBase64Code(data.charCodeAt(i + 2)) << 6 |
getBase64Code(data.charCodeAt(i + 3));
result[j] = buffer >> 16;
result[j + 1] = (buffer >> 8) & 0xFF;
result[j + 2] = buffer & 0xFF;
}
return result.subarray(0, result.length - missingOctets);
}
/**
* CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
* @param charCode
*/
function getBase64Code(charCode: number): number {
if (charCode >= base64codes.length) {
throw new Error("Unable to parse base64 string.");
}
const code = base64codes[charCode];
if (code === 255) {
throw new Error("Unable to parse base64 string.");
}
return code;
}
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
const base64abc = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"+",
"/",
];
// CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
const base64codes = [
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
62,
255,
255,
255,
63,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
255,
255,
255,
0,
255,
255,
255,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
255,
255,
255,
255,
255,
255,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
];

View File

@@ -1,3 +1,5 @@
import { encode } from "./base64.ts";
/** Converts a url to base 64. Useful for example, uploading/creating server emojis. */
export async function urlToBase64(url: string) {
const buffer = await fetch(url).then((res) => res.arrayBuffer());
@@ -5,107 +7,3 @@ export async function urlToBase64(url: string) {
const type = url.substring(url.lastIndexOf(".") + 1);
return `data:image/${type};base64,${imageStr}`;
}
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
const base64abc = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"+",
"/",
];
/**
* CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
* Encodes a given Uint8Array, ArrayBuffer or string into RFC4648 base64 representation
* @param data
*/
export function encode(data: ArrayBuffer | string): string {
const uint8 = typeof data === "string"
? new TextEncoder().encode(data)
: data instanceof Uint8Array
? data
: new Uint8Array(data);
let result = "",
i;
const l = uint8.length;
for (i = 2; i < l; i += 3) {
result += base64abc[uint8[i - 2] >> 2];
result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)];
result += base64abc[((uint8[i - 1] & 0x0f) << 2) | (uint8[i] >> 6)];
result += base64abc[uint8[i] & 0x3f];
}
if (i === l + 1) {
// 1 octet yet to write
result += base64abc[uint8[i - 2] >> 2];
result += base64abc[(uint8[i - 2] & 0x03) << 4];
result += "==";
}
if (i === l) {
// 2 octets yet to write
result += base64abc[uint8[i - 2] >> 2];
result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)];
result += base64abc[(uint8[i - 1] & 0x0f) << 2];
result += "=";
}
return result;
}