Skip to content

Commit

Permalink
Fix #8 files.upload V2 (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
seratch authored Apr 11, 2024
1 parent 38f31b6 commit 96a0e59
Show file tree
Hide file tree
Showing 9 changed files with 368 additions and 3 deletions.
98 changes: 98 additions & 0 deletions src/client/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ import type {
DndSetSnoozeRequest,
DndTeamInfoRequest,
EmojiListRequest,
FileUploadComplete,
FileUploadV2,
FilesCompleteUploadExternalRequest,
FilesDeleteRequest,
FilesGetUploadURLExternalRequest,
Expand All @@ -167,6 +169,7 @@ import type {
FilesRevokePublicURLRequest,
FilesSharedPublicURLRequest,
FilesUploadRequest,
FilesUploadV2Request,
FunctionsCompleteErrorRequest,
FunctionsCompleteSuccessRequest,
MigrationExchangeRequest,
Expand Down Expand Up @@ -462,6 +465,7 @@ import type {
WorkflowsTriggersListResponse,
WorkflowsTriggersUpdateResponse,
} from "./automation-response/index";
import { FilesUploadV2Response } from "./custom-response/FilesUploadV2Response";

export interface SlackAPI<
Req extends SlackAPIRequest,
Expand Down Expand Up @@ -581,6 +585,90 @@ export class SlackAPIClient {
return result;
}

async uploadFilesV2(
params: FilesUploadV2Request,
): Promise<FilesUploadV2Response> {
const files =
"files" in params ? params.files! : [{ ...(params as FileUploadV2) }];
const completes: FileUploadComplete[] = [];
const uploadErrors: string[] = [];
for (const f of files) {
const body: Uint8Array = f.file
? new Uint8Array(
f.file instanceof Blob ? await f.file.arrayBuffer() : f.file,
)
: new TextEncoder().encode(f.content);
const getUrl = await this.files.getUploadURLExternal({
filename: f.filename,
length: body.length,
snippet_type: f.snippet_type,
});
if (isDebugLogEnabled(this.#options.logLevel)) {
console.log(
`Slack API response (files.getUploadURLExternal): ${JSON.stringify(getUrl)}}`,
);
}
if (getUrl.error) {
throw new SlackAPIError(
"files.getUploadURLExternal",
getUrl.error,
getUrl,
);
}
const { upload_url, file_id } = getUrl;
const upload = await fetch(upload_url!, { method: "POST", body });
const uploadBody = await upload.text();
if (isDebugLogEnabled(this.#options.logLevel)) {
console.log(
`Slack file upload result: (status: ${upload.status}, body: ${uploadBody})`,
);
}
if (upload.status !== 200) {
uploadErrors.push(uploadBody);
}
if (uploadErrors.length > 0) {
const errorResponse = {
ok: false,
error: "upload_failure",
uploadErrors,
headers: upload.headers,
};
throw new SlackAPIError(
"files.completeUploadExternal",
"upload_error",
errorResponse,
);
}
completes.push({
id: file_id!,
title: f.title ?? f.filename,
});
}
const completion = await this.files.completeUploadExternal({
files: completes,
channel_id: params.channel_id,
initial_comment: params.initial_comment,
thread_ts: params.thread_ts,
});
if (isDebugLogEnabled(this.#options.logLevel)) {
console.log(
`Slack API response (files.completeUploadExternal): ${JSON.stringify(completion)}}`,
);
}
if (completion.error) {
throw new SlackAPIError(
"files.completeUploadExternal",
completion.error,
completion,
);
}
return {
ok: true,
files: completion.files!,
headers: completion.headers,
};
}

bindApiCall<A extends SlackAPIRequest, R extends SlackAPIResponse>(
self: SlackAPIClient,
method: string,
Expand All @@ -595,6 +683,15 @@ export class SlackAPIClient {
return self.sendMultipartData.bind(self, method) as SlackAPI<A, R>;
}

bindFilesUploadV2(
self: SlackAPIClient,
): SlackAPI<FilesUploadV2Request, FilesUploadV2Response> {
return self.uploadFilesV2.bind(self) as SlackAPI<
FilesUploadV2Request,
FilesUploadV2Response
>;
}

public readonly admin = {
apps: {
approve: this.bindApiCall<
Expand Down Expand Up @@ -1328,6 +1425,7 @@ export class SlackAPIClient {
this,
"files.upload",
),
uploadV2: this.bindFilesUploadV2(this),
getUploadURLExternal: this.bindApiCall<
FilesGetUploadURLExternalRequest,
FilesGetUploadURLExternalResponse
Expand Down
17 changes: 17 additions & 0 deletions src/client/custom-response/FilesUploadV2Response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// deno-lint-ignore-file ban-unused-ignore no-explicit-any no-empty-interface

import { SlackAPIResponse } from "../response";
import { File } from "../generated-response/FilesCompleteUploadExternalResponse";

export type FilesUploadV2ErrorResponse = SlackAPIResponse & {
ok: false;
error: string;
uploadErrors?: string[];
needed?: string;
provided?: string;
};

export type FilesUploadV2Response = SlackAPIResponse & {
ok: true;
files: File[];
};
32 changes: 31 additions & 1 deletion src/client/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,36 @@ interface FileUpload {
}
export type FilesUploadRequest = FileUpload & SlackAPIRequest;

/**
* files.upload V2 API files upload Request
* */
export interface FileUploadV2 {
filename: string;
title?: string;
alt_text?: string; // Description of image for screen-reader
snippet_type?: string; // Syntax type of the snippet being uploaded. E.g. `python`
content?: string; // if omitted, must provide `file`
file?: Blob | ArrayBuffer; // if omitted, must provide `content`
filetype?: string;
}
export interface FileUploadV2Share {
channel_id?: string;
thread_ts?: string; // if specified, `channel_id` must be set
initial_comment?: string; // if specified, `channel_id` must be set
}
export interface FilesUploadV2Request1
extends SlackAPIRequest,
FileUploadV2,
FileUploadV2Share {}
export interface FilesUploadV2RequestN
extends SlackAPIRequest,
FileUploadV2Share {
files: FileUploadV2[];
}
export type FilesUploadV2Request =
| FilesUploadV2Request1
| FilesUploadV2RequestN;

/**
* Gets a URL for an edge external file upload. Method:
* {@link https://api.slack.com/methods/files.getUploadURLExternal files.getUploadURLExternal}
Expand All @@ -971,7 +1001,7 @@ export interface FilesCompleteUploadExternalRequest extends SlackAPIRequest {
initial_comment?: string;
thread_ts?: string;
}
interface FileUploadComplete {
export interface FileUploadComplete {
id: string; // file id
title?: string; // filename
}
Expand Down
103 changes: 103 additions & 0 deletions src_deno/client/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ import type {
FilesRevokePublicURLRequest,
FilesSharedPublicURLRequest,
FilesUploadRequest,
FilesUploadV2Request,
FileUploadComplete,
FileUploadV2,
FunctionsCompleteErrorRequest,
FunctionsCompleteSuccessRequest,
MigrationExchangeRequest,
Expand Down Expand Up @@ -462,6 +465,7 @@ import type {
WorkflowsTriggersListResponse,
WorkflowsTriggersUpdateResponse,
} from "./automation-response/index.ts";
import { FilesUploadV2Response } from "./custom-response/FilesUploadV2Response.ts";

export interface SlackAPI<
Req extends SlackAPIRequest,
Expand Down Expand Up @@ -581,6 +585,95 @@ export class SlackAPIClient {
return result;
}

async uploadFilesV2(
params: FilesUploadV2Request,
): Promise<FilesUploadV2Response> {
const files = "files" in params
? params.files!
: [{ ...(params as FileUploadV2) }];
const completes: FileUploadComplete[] = [];
const uploadErrors: string[] = [];
for (const f of files) {
const body: Uint8Array = f.file
? new Uint8Array(
f.file instanceof Blob ? await f.file.arrayBuffer() : f.file,
)
: new TextEncoder().encode(f.content);
const getUrl = await this.files.getUploadURLExternal({
filename: f.filename,
length: body.length,
snippet_type: f.snippet_type,
});
if (isDebugLogEnabled(this.#options.logLevel)) {
console.log(
`Slack API response (files.getUploadURLExternal): ${
JSON.stringify(getUrl)
}}`,
);
}
if (getUrl.error) {
throw new SlackAPIError(
"files.getUploadURLExternal",
getUrl.error,
getUrl,
);
}
const { upload_url, file_id } = getUrl;
const upload = await fetch(upload_url!, { method: "POST", body });
const uploadBody = await upload.text();
if (isDebugLogEnabled(this.#options.logLevel)) {
console.log(
`Slack file upload result: (status: ${upload.status}, body: ${uploadBody})`,
);
}
if (upload.status !== 200) {
uploadErrors.push(uploadBody);
}
if (uploadErrors.length > 0) {
const errorResponse = {
ok: false,
error: "upload_failure",
uploadErrors,
headers: upload.headers,
};
throw new SlackAPIError(
"files.completeUploadExternal",
"upload_error",
errorResponse,
);
}
completes.push({
id: file_id!,
title: f.title ?? f.filename,
});
}
const completion = await this.files.completeUploadExternal({
files: completes,
channel_id: params.channel_id,
initial_comment: params.initial_comment,
thread_ts: params.thread_ts,
});
if (isDebugLogEnabled(this.#options.logLevel)) {
console.log(
`Slack API response (files.completeUploadExternal): ${
JSON.stringify(completion)
}}`,
);
}
if (completion.error) {
throw new SlackAPIError(
"files.completeUploadExternal",
completion.error,
completion,
);
}
return {
ok: true,
files: completion.files!,
headers: completion.headers,
};
}

bindApiCall<A extends SlackAPIRequest, R extends SlackAPIResponse>(
self: SlackAPIClient,
method: string,
Expand All @@ -595,6 +688,15 @@ export class SlackAPIClient {
return self.sendMultipartData.bind(self, method) as SlackAPI<A, R>;
}

bindFilesUploadV2(
self: SlackAPIClient,
): SlackAPI<FilesUploadV2Request, FilesUploadV2Response> {
return self.uploadFilesV2.bind(self) as SlackAPI<
FilesUploadV2Request,
FilesUploadV2Response
>;
}

public readonly admin = {
apps: {
approve: this.bindApiCall<
Expand Down Expand Up @@ -1328,6 +1430,7 @@ export class SlackAPIClient {
this,
"files.upload",
),
uploadV2: this.bindFilesUploadV2(this),
getUploadURLExternal: this.bindApiCall<
FilesGetUploadURLExternalRequest,
FilesGetUploadURLExternalResponse
Expand Down
17 changes: 17 additions & 0 deletions src_deno/client/custom-response/FilesUploadV2Response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// deno-lint-ignore-file ban-unused-ignore no-explicit-any no-empty-interface

import { SlackAPIResponse } from "../response.ts";
import { File } from "../generated-response/FilesCompleteUploadExternalResponse.ts";

export type FilesUploadV2ErrorResponse = SlackAPIResponse & {
ok: false;
error: string;
uploadErrors?: string[];
needed?: string;
provided?: string;
};

export type FilesUploadV2Response = SlackAPIResponse & {
ok: true;
files: File[];
};
Loading

0 comments on commit 96a0e59

Please sign in to comment.