-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Impl uploadv2 #90
Impl uploadv2 #90
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this works the same way as the node SDK. If Can we make this work the same way as the node SDK? Also for reference: the code for how the python SDK handles these parameters. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had overlooked how the node SDK behaves when a string is passed to the file field 😮. It's definitely not good for the behavior to differ between SDKs. I will make corrections so that the handling of the content field and file field behaves the same as in other SDKs. |
||
? "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( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { BaseResponse } from "../types.ts"; | ||
|
||
export type FileUploadV2 = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think about potentially implementing the type for FileUpload similar to the node sdk and then building this type from it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good idea. By having separate types for FileUpload and FileUploadV2, the FileUpload type will be useful when trying to implement the old files.upload. Thanks for the advice. |
||
/** @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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't implement the content field because it's not usable for file upload v2 (when uploading via getExternalUploadURL). In node-slack-sdk, whether you use the content or file field, the data is processed like the file field and an HTTP request is made. reference There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I learned from @filmaj that the content field is an important element! I was mistaken in my previous comment, so I will implement the content field. 🙇♂️ |
||
}; | ||
|
||
export type FileUploadV2Args = { | ||
file_uploads: FileUploadV2[]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @filmaj seems like the node-sdk has a deprecated additional field request_file_info since it does not appear here would we want to do something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FileUploadV2Args might indeed be better as a FileUploadV2[] type. Is it okay to modify it that way? 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While it may seem awkward to nest it under an additional property name, I believe having consistency across all SDKs is more important than a sense of property cleanliness. My vote would be to keep the structure as-is, to allow for easier conceptual movement from one language / bolt framework to another. |
||
}; | ||
|
||
export type GetUploadURLExternalResponse = BaseResponse & { | ||
file_id: string; | ||
upload_url: string; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When actually POSTing the file contents to the URL provided by
getUploadURLExternal
, we post the data as binary (raw bytes), or multipart-form-encoded (see the docs on this here).As a result, I don't think we should be providing headers like Content-Type. The file-upload-external edge URL that we POST file data to will infer the content type based on the data uploaded.
In the node SDK, the headers are empty other than the token.
Similarly, in the python SDK, same thing.
I think the trick in implementing this API is to ensure we encode the POST data in the appropriate format, based on what is provided by the user. Assuming we support the
content
parameter, like the other SDKs do, then thecontent
parameter, which is a string, should be encoded as UTF-8 bytes. Otherwise, iffile
is a string, we should interpret that string as a file path, and read the raw bytes directly from that path. Finally,file
may also be a buffer-like object. In this situation, we should provide the raw buffer directly tofetch
as its data payload.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. Indeed, delegating the setting of Content-Type to the fetch function is appropriate. Thank you for the detailed explanation 😊. Based on this, I will make the following modifications to the source code: