Skip to content

Commit

Permalink
Add fileUploadV2 method to BaseSlackAPIClient
Browse files Browse the repository at this point in the history
and add new tests to api_test.ts.
  • Loading branch information
racTaiga533 committed Dec 15, 2023
1 parent 5ab4181 commit b735fec
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/api-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const ProxifyAndTypeClient = (baseClient: BaseSlackAPIClient) => {
setSlackApiUrl: baseClient.setSlackApiUrl.bind(baseClient),
apiCall: baseClient.apiCall.bind(baseClient),
response: baseClient.response.bind(baseClient),
fileUploadV2: baseClient.fileUploadV2.bind(baseClient),
};

// Create our proxy, and type it w/ our api method types
Expand Down
195 changes: 195 additions & 0 deletions src/api_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,201 @@ Deno.test("SlackAPI class", async (t) => {
},
);

await t.step(
"fileUploadV2 method",
async (t) => {
const client = SlackAPI("test-token");
await t.step(
"should successfully upload a single file",
async () => {
const testFile = {
file: new Blob(["test"]),
filename: "test.txt",
length: "6",
fileId: "test_id",
};
mf.mock("POST@/api/files.getUploadURLExternal", () => {
return new Response(
JSON.stringify({
"ok": true,
"upload_url": "https://files.slack.com/test",
"file_id": "test_id",
}),
);
});
mf.mock("POST@/test", () => {
return new Response(
undefined,
{ status: 200 },
);
});
mf.mock("POST@/api/files.completeUploadExternal", () => {
return new Response(
`{"ok":true}`,
);
});
const response = await client.fileUploadV2({
file_uploads: [
testFile,
],
});
response.forEach((res) => assertEquals(res.ok, true));

mf.reset();
},
);

await t.step(
"should successfully upload multiple file",
async () => {
const testFile = {
file: new Blob(["test"]),
filename: "test.txt",
length: "6",
fileId: "test_id",
};
const testTextFile = {
file: "test",
filename: "test.txt",
length: "6",
fileId: "test_id",
};
mf.mock("POST@/api/files.getUploadURLExternal", () => {
return new Response(
JSON.stringify({
"ok": true,
"upload_url": "https://files.slack.com/test",
"file_id": "test_id",
}),
);
});
mf.mock("POST@/test", () => {
return new Response(
undefined,
{ status: 200 },
);
});
mf.mock("POST@/api/files.completeUploadExternal", () => {
return new Response(
`{"ok":true}`,
);
});
const response = await client.fileUploadV2({
file_uploads: [
testFile,
testTextFile,
],
});
response.forEach((res) => assertEquals(res.ok, true));

mf.reset();
},
);
await t.step(
"should rejects when get upload url fails",
async () => {
const testFile = {
file: new Blob(["test"]),
filename: "test.txt",
length: "6",
fileId: "test_id",
};
mf.mock("POST@/api/files.getUploadURLExternal", () => {
return new Response(
JSON.stringify({
"ok": false,
}),
);
});
await assertRejects(async () =>
await client.fileUploadV2({
file_uploads: [
testFile,
],
})
);

mf.reset();
},
);
await t.step(
"should rejects when upload fails",
async () => {
const testFile = {
file: new Blob(["test"]),
filename: "test.txt",
length: "6",
fileId: "test_id",
};
mf.mock("POST@/api/files.getUploadURLExternal", () => {
return new Response(
JSON.stringify({
"ok": true,
"upload_url": "https://files.slack.com/test",
"file_id": "test_id",
}),
);
});
mf.mock("POST@/test", () => {
return new Response(
undefined,
{ status: 500 },
);
});
await assertRejects(async () =>
await client.fileUploadV2({
file_uploads: [
testFile,
],
})
);

mf.reset();
},
);
await t.step(
"should rejects when upload complete fails",
async () => {
const testFile = {
file: new Blob(["test"]),
filename: "test.txt",
length: "6",
fileId: "test_id",
};
mf.mock("POST@/api/files.getUploadURLExternal", () => {
return new Response(
JSON.stringify({
"ok": true,
"upload_url": "https://files.slack.com/test",
"file_id": "test_id",
}),
);
});
mf.mock("POST@/test", () => {
return new Response(
undefined,
{ status: 200 },
);
});
mf.mock("POST@/api/files.completeUploadExternal", () => {
return new Response(
`{"ok":false}`,
);
});
await assertRejects(async () =>
await client.fileUploadV2({
file_uploads: [
testFile,
],
})
);

mf.reset();
},
);
},
);

mf.uninstall();
});

Expand Down
78 changes: 78 additions & 0 deletions src/base-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "./types.ts";
import { createHttpError, HttpError } from "./deps.ts";
import { getUserAgent, serializeData } from "./base-client-helpers.ts";
import { FileUploadV2, FileUploadV2Args } from "./typed-method-types/files.ts";

export class BaseSlackAPIClient implements BaseSlackClient {
#token?: string;
Expand Down Expand Up @@ -72,6 +73,83 @@ export class BaseSlackAPIClient implements BaseSlackClient {
return await this.createBaseResponse(response);
}

async fileUploadV2(
args: FileUploadV2Args,
) {
const { file_uploads } = args;
const uploadUrls = await Promise.all(
file_uploads.map((file) => this.getFileUploadUrl(file)),
);

await Promise.all(
uploadUrls.map((uploadUrl, index) =>
this.uploadFile(uploadUrl.upload_url, file_uploads[index].file)
),
);

return await Promise.all(
uploadUrls.map((uploadUrl, index) =>
this.completeFileUpload(uploadUrl.file_id, file_uploads[index])
),
);
}

private async getFileUploadUrl(file: FileUploadV2) {
const fileMetaData = {
filename: file.filename,
length: file.length,
alt_text: file.alt_text,
snippet_type: file.snippet_type,
};
const response = await this.apiCall(
"files.getUploadURLExternal",
fileMetaData,
);

if (!response.ok) {
throw new Error(JSON.stringify(response.response_metadata));
}
return response;
}

private async completeFileUpload(fileID: string, file: FileUploadV2) {
const fileMetaData = {
files: JSON.stringify([{ id: fileID, title: file.title }]),
channel_id: file.channel_id,
initial_comment: file.initial_comment,
thread_ts: file.thread_ts,
};
const response = await this.apiCall(
"files.completeUploadExternal",
fileMetaData,
);
if (!response.ok) {
throw new Error(JSON.stringify(response.response_metadata));
}
return response;
}

private async uploadFile(
uploadUrl: string,
file: FileUploadV2["file"],
) {
const response = await fetch(uploadUrl, {
headers: {
"Content-Type": typeof file === "string"
? "text/plain"
: "application/octet-stream",
"User-Agent": getUserAgent(),
},
method: "POST",
body: file,
});

if (!response.ok) {
throw await this.createHttpError(response);
}
return;
}

private async createHttpError(response: Response): Promise<HttpError> {
const text = await response.text();
return createHttpError(
Expand Down
1 change: 1 addition & 0 deletions src/dev_deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {
assertExists,
assertInstanceOf,
assertRejects,
fail,
} from "https://deno.land/[email protected]/testing/asserts.ts";
export * as mf from "https://deno.land/x/[email protected]/mod.ts";
export { isHttpError } from "https://deno.land/[email protected]/http/http_errors.ts";
Expand Down
32 changes: 32 additions & 0 deletions src/typed-method-types/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { BaseResponse } from "../types.ts";

export type FileUploadV2 = {
/** @description Description of image for screen-reader. */
alt_text?: string;
/** @description Syntax type of the snippet being uploaded. */
snippet_type?: string;
/** @description The message text introducing the file in specified channels. */
channel_id?: string;
/** @description Provide another message's ts value to upload this file as a reply. Never use a reply's ts value; use its parent instead. */
thread_ts?: string;
/** @description The message text introducing the file in specified channels. */
initial_comment?: string;
/** @description Title of the file being uploaded */
title?: string;

/** @description Size in bytes of the file being uploaded. */
length: string;
/** @description Name of the file being uploaded. */
filename: string;
/** @description Filetype of the file being uploaded. */
file: Blob | ReadableStream<Uint8Array> | string | ArrayBuffer;
};

export type FileUploadV2Args = {
file_uploads: FileUploadV2[];
};

export type GetUploadURLExternalResponse = BaseResponse & {
file_id: string;
upload_url: string;
};
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TypedSlackAPIMethodsType } from "./typed-method-types/mod.ts";
import { SlackAPIMethodsType } from "./generated/method-types/mod.ts";
import { FileUploadV2Args } from "./typed-method-types/files.ts";

export type { DatastoreItem } from "./typed-method-types/apps.ts";

Expand Down Expand Up @@ -52,6 +53,9 @@ export type BaseSlackClient = {
setSlackApiUrl: (slackApiUrl: string) => BaseSlackClient;
apiCall: BaseClientCall;
response: BaseClientResponse;
fileUploadV2: (
args: FileUploadV2Args,
) => Promise<BaseResponse[]>;
};

// TODO: [brk-chg] return a `Promise<Response>` object
Expand Down

0 comments on commit b735fec

Please sign in to comment.