diff --git a/lib/actions/slack/utils.js b/lib/actions/slack/utils.js index 26f6443c2..8ae00e1f7 100644 --- a/lib/actions/slack/utils.js +++ b/lib/actions/slack/utils.js @@ -131,7 +131,7 @@ const handleExecute = async (request, slack) => { const fileName = request.formParams.filename ? request.completeFilename() : request.suggestedFilename(); try { const isUserToken = request.formParams.channel.startsWith("U") || request.formParams.channel.startsWith("W"); - const forceV1Upload = process.env.FORCE_V1_UPLOAD; + const channel = request.formParams.channel; if (!request.empty()) { const buffs = []; await request.stream(async (readable) => { @@ -148,39 +148,58 @@ const handleExecute = async (request, slack) => { const buffer = Buffer.concat(buffs); const comment = request.formParams.initial_comment ? request.formParams.initial_comment : ""; winston.info(`${LOG_PREFIX} Attempting to send ${buffer.byteLength} bytes to Slack`, { webhookId }); - // Unfortunately UploadV2 does not provide a way to upload files - // to user tokens which are common in Looker schedules - // (UXXXXXXX) - if (isUserToken || forceV1Upload) { - winston.info(`${LOG_PREFIX} V1 Upload of file`, { webhookId }); - await slack.files.upload({ - file: buffer, - filename: fileName, - channels: request.formParams.channel, - initial_comment: comment, + winston.info(`${LOG_PREFIX} V2 Upload of file`, { webhookId }); + const res = await slack.files.getUploadURLExternal({ + filename: fileName, + length: buffer.byteLength, + }); + const upload_url = res.upload_url; + // Upload file to Slack + await gaxios.request({ + method: "POST", + url: upload_url, + data: buffer, + }); + // Finalize upload and give metadata for channel, title and + // comment for the file to be posted. + if (isUserToken) { + // If we have a user token, we need to convert the + // UXXXXX token into a DXXXXX token. Conversations.open() + // requires channels:manage and extra *:write scopes + // which will break existing schedules. Posting a message + // first only requires chat:write which we already have. + // the response returns a DXXXX token which we can use + // to upload to the channel + const postResponse = await slack.chat.postMessage({ + channel, + text: comment.length !== 0 ? comment : "Looker Data", + }).catch((e) => { + winston.error("Issue posting message", { webhookId }); + reject(e); }); + const directChannel = postResponse === null || postResponse === void 0 ? void 0 : postResponse.channel; + const resp2 = await slack.files.completeUploadExternal({ + files: [{ + id: res.file_id ? res.file_id : "", + title: fileName, + }], + channel_id: directChannel, + }).catch((e) => { + winston.error(`${LOG_PREFIX} ${e.message}`, { webhookId }); + reject(e); + }); + winston.info(`response complete : ${JSON.stringify(resp2)}`); + // Workaround for regression in V2 upload, the initial + // comment does not support markdown formatting, breaking + // customer links } else { - winston.info(`${LOG_PREFIX} V2 Upload of file`, { webhookId }); - const res = await slack.files.getUploadURLExternal({ - filename: fileName, - length: buffer.byteLength, - }); - const upload_url = res.upload_url; - // Upload file to Slack - await gaxios.request({ - method: "POST", - url: upload_url, - data: buffer, - }); - // Finalize upload and give metadata for channel, title and - // comment for the file to be posted. await slack.files.completeUploadExternal({ files: [{ id: res.file_id ? res.file_id : "", title: fileName, }], - channel_id: request.formParams.channel, + channel_id: channel, }).catch((e) => { winston.error(`${LOG_PREFIX} ${e.message}`, { webhookId }); reject(e); @@ -190,7 +209,7 @@ const handleExecute = async (request, slack) => { // customer links if (comment !== "") { await slack.chat.postMessage({ - channel: request.formParams.channel, + channel, text: comment, }).catch((e) => { winston.error(`${LOG_PREFIX} ${e.message}`, { webhookId }); diff --git a/src/actions/slack/test_utils.ts b/src/actions/slack/test_utils.ts index 0b6b655f1..9e2ff557e 100644 --- a/src/actions/slack/test_utils.ts +++ b/src/actions/slack/test_utils.ts @@ -8,25 +8,6 @@ import { getDisplayedFormFields, handleExecute } from "./utils" const stubFileName = "stubSuggestedFilename" -function expectSlackMatchV1(request: Hub.ActionRequest, optionsMatch: FilesUploadArguments) { - const slackClient = new WebClient("someToken") - const expectedBuffer = optionsMatch.file as Buffer - delete optionsMatch.file - const filesUploadSpy = sinon.spy(async (params: any) => { - chai.expect(params.file.toString()).to.equal(expectedBuffer.toString()) - return {} - }) - const stubClient = sinon.stub(slackClient.files, "upload") - .callsFake(filesUploadSpy) - const stubSuggestedFilename = sinon.stub(request as any, "suggestedFilename") - .callsFake(() => stubFileName) - return chai.expect(handleExecute(request, slackClient)).to.be.fulfilled.then(() => { - chai.expect(filesUploadSpy).to.have.been.calledWithMatch(optionsMatch) - stubClient.restore() - stubSuggestedFilename.restore() - }) -} - function expectSlackMatch(request: Hub.ActionRequest, optionsMatch: FilesUploadArguments) { const slackClient = new WebClient("someToken") @@ -366,28 +347,7 @@ describe(`slack/utils unit tests`, () => { }) }) - it("uses V1 if channel if FORCE_V1_UPLOAD is on", () => { - const request = new Hub.ActionRequest() - request.type = Hub.ActionType.Query - request.formParams = { - channel: "mychannel", - initial_comment: "mycomment", - } - request.attachment = { - dataBuffer: Buffer.from("1,2,3,4", "utf8"), - fileExtension: "csv", - } - process.env.FORCE_V1_UPLOAD = "on" - const results = expectSlackMatchV1(request, { - channels: request.formParams.channel, - initial_comment: request.formParams.initial_comment, - file: Buffer.from("1,2,3,4", "utf8"), - }) - process.env.FORCE_V1_UPLOAD = "" - return results - }) - - it("uses V1 if channel is a User token", () => { + it("uses convertUM if channel is a User token", () => { const request = new Hub.ActionRequest() request.type = Hub.ActionType.Query request.formParams = { @@ -398,7 +358,7 @@ describe(`slack/utils unit tests`, () => { dataBuffer: Buffer.from("1,2,3,4", "utf8"), fileExtension: "csv", } - return expectSlackMatchV1(request, { + return expectSlackMatch(request, { channels: request.formParams.channel, initial_comment: request.formParams.initial_comment, file: Buffer.from("1,2,3,4", "utf8"), @@ -416,7 +376,7 @@ describe(`slack/utils unit tests`, () => { dataBuffer: Buffer.from("1,2,3,4", "utf8"), fileExtension: "csv", } - return expectSlackMatchV1(request, { + return expectSlackMatch(request, { channels: request.formParams.channel, initial_comment: request.formParams.initial_comment, file: Buffer.from("1,2,3,4", "utf8"), diff --git a/src/actions/slack/utils.ts b/src/actions/slack/utils.ts index 58985f1b7..85eb6077c 100644 --- a/src/actions/slack/utils.ts +++ b/src/actions/slack/utils.ts @@ -124,7 +124,6 @@ export const getDisplayedFormFields = async (slack: WebClient, channelType: stri return response } - export const handleExecute = async (request: Hub.ActionRequest, slack: WebClient): Promise => { const response = new Hub.ActionResponse({success: true}) if (!request.formParams.channel) { @@ -146,7 +145,7 @@ export const handleExecute = async (request: Hub.ActionRequest, slack: WebClient try { const isUserToken = request.formParams.channel.startsWith("U") || request.formParams.channel.startsWith("W") - const forceV1Upload = process.env.FORCE_V1_UPLOAD + const channel = request.formParams.channel if (!request.empty()) { const buffs: any[] = [] await request.stream(async (readable) => { @@ -167,40 +166,59 @@ export const handleExecute = async (request: Hub.ActionRequest, slack: WebClient {webhookId}, ) - // Unfortunately UploadV2 does not provide a way to upload files - // to user tokens which are common in Looker schedules - // (UXXXXXXX) - if (isUserToken || forceV1Upload) { - winston.info(`${LOG_PREFIX} V1 Upload of file`, {webhookId}) - await slack.files.upload({ - file: buffer, - filename: fileName, - channels: request.formParams.channel, - initial_comment: comment, - }) - } else { - winston.info(`${LOG_PREFIX} V2 Upload of file`, {webhookId}) - const res = await slack.files.getUploadURLExternal({ - filename: fileName, - length: buffer.byteLength, - }) - const upload_url = res.upload_url + winston.info(`${LOG_PREFIX} V2 Upload of file`, {webhookId}) + const res = await slack.files.getUploadURLExternal({ + filename: fileName, + length: buffer.byteLength, + }) + const upload_url = res.upload_url - // Upload file to Slack - await gaxios.request({ - method: "POST", - url: upload_url, - data: buffer, - }) + // Upload file to Slack + await gaxios.request({ + method: "POST", + url: upload_url, + data: buffer, + }) - // Finalize upload and give metadata for channel, title and - // comment for the file to be posted. + // Finalize upload and give metadata for channel, title and + // comment for the file to be posted. + if (isUserToken) { + // If we have a user token, we need to convert the + // UXXXXX token into a DXXXXX token. Conversations.open() + // requires channels:manage and extra *:write scopes + // which will break existing schedules. Posting a message + // first only requires chat:write which we already have. + // the response returns a DXXXX token which we can use + // to upload to the channel + const postResponse = await slack.chat.postMessage({ + channel, + text: comment.length !== 0 ? comment : "Looker Data", + }).catch((e: any) => { + winston.error("Issue posting message", {webhookId}) + reject(e) + }) + const directChannel = postResponse?.channel + const resp2 = await slack.files.completeUploadExternal({ + files: [{ + id: res.file_id ? res.file_id : "", + title: fileName, + }], + channel_id: directChannel, + }).catch((e: any) => { + winston.error(`${LOG_PREFIX} ${e.message}`, {webhookId}) + reject(e) + }) + winston.info(`response complete : ${JSON.stringify(resp2)}`) + // Workaround for regression in V2 upload, the initial + // comment does not support markdown formatting, breaking + // customer links + } else { await slack.files.completeUploadExternal({ files: [{ id: res.file_id ? res.file_id : "", title: fileName, }], - channel_id: request.formParams.channel, + channel_id: channel, }).catch((e: any) => { winston.error(`${LOG_PREFIX} ${e.message}`, {webhookId}) reject(e) @@ -210,7 +228,7 @@ export const handleExecute = async (request: Hub.ActionRequest, slack: WebClient // customer links if (comment !== "") { await slack.chat.postMessage({ - channel: request.formParams.channel!, + channel, text: comment, }).catch((e: any) => { winston.error(`${LOG_PREFIX} ${e.message}`, {webhookId})