|
|
@@ -2,212 +2,68 @@ import loggerFactory from '~/utils/logger';
|
|
|
|
|
|
const logger = loggerFactory('growi:service:SlackCommandHandler:search');
|
|
|
|
|
|
-const { markdownSectionBlock, divider } = require('@growi/slack');
|
|
|
+const {
|
|
|
+ markdownSectionBlock, divider, respond, respondInChannel, replaceOriginal, deleteOriginal,
|
|
|
+} = require('@growi/slack');
|
|
|
const { formatDistanceStrict } = require('date-fns');
|
|
|
-const axios = require('axios');
|
|
|
-const SlackbotError = require('../../models/vo/slackbot-error');
|
|
|
|
|
|
-const PAGINGLIMIT = 10;
|
|
|
+const PAGINGLIMIT = 7;
|
|
|
+
|
|
|
|
|
|
module.exports = (crowi) => {
|
|
|
const BaseSlackCommandHandler = require('./slack-command-handler');
|
|
|
const handler = new BaseSlackCommandHandler(crowi);
|
|
|
|
|
|
- handler.handleCommand = async function(client, body, args) {
|
|
|
- let searchResult;
|
|
|
- try {
|
|
|
- searchResult = await this.retrieveSearchResults(client, body, args);
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- logger.error('Failed to get search results.', err);
|
|
|
- throw new SlackbotError({
|
|
|
- method: 'postEphemeral',
|
|
|
- to: 'channel',
|
|
|
- popupMessage: 'Failed To Search',
|
|
|
- mainMessage: '*Failed to search.*\n Hint\n `/growi search [keyword]`',
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- const appUrl = crowi.appService.getSiteUrl();
|
|
|
- const appTitle = crowi.appService.getAppTitle();
|
|
|
-
|
|
|
- const {
|
|
|
- pages, offset, resultsTotal,
|
|
|
- } = searchResult;
|
|
|
-
|
|
|
- if (pages.length === 0) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const keywords = this.getKeywords(args);
|
|
|
-
|
|
|
-
|
|
|
- let searchResultsDesc;
|
|
|
-
|
|
|
- switch (resultsTotal) {
|
|
|
- case 1:
|
|
|
- searchResultsDesc = `*${resultsTotal}* page is found.`;
|
|
|
- break;
|
|
|
-
|
|
|
- default:
|
|
|
- searchResultsDesc = `*${resultsTotal}* pages are found.`;
|
|
|
- break;
|
|
|
- }
|
|
|
|
|
|
+ function getKeywords(growiCommandArgs) {
|
|
|
+ const keywords = growiCommandArgs.join(' ');
|
|
|
+ return keywords;
|
|
|
+ }
|
|
|
|
|
|
- const contextBlock = {
|
|
|
- type: 'context',
|
|
|
- elements: [
|
|
|
- {
|
|
|
- type: 'mrkdwn',
|
|
|
- text: `keyword(s) : *"${keywords}"* | Current: ${offset + 1} - ${offset + pages.length} | Total ${resultsTotal} pages`,
|
|
|
- },
|
|
|
- ],
|
|
|
- };
|
|
|
+ function appendSpeechBaloon(mrkdwn, commentCount) {
|
|
|
+ return (commentCount != null && commentCount > 0)
|
|
|
+ ? `${mrkdwn} :speech_balloon: ${commentCount}`
|
|
|
+ : mrkdwn;
|
|
|
+ }
|
|
|
|
|
|
- const now = new Date();
|
|
|
- const blocks = [
|
|
|
- markdownSectionBlock(`:mag: <${decodeURI(appUrl)}|*${appTitle}*>\n${searchResultsDesc}`),
|
|
|
- contextBlock,
|
|
|
- { type: 'divider' },
|
|
|
- // create an array by map and extract
|
|
|
- ...pages.map((page) => {
|
|
|
- const { path, updatedAt, commentCount } = page;
|
|
|
- // generate URL
|
|
|
- const url = new URL(path, appUrl);
|
|
|
- const { href, pathname } = url;
|
|
|
+ function generateSearchResultPageLinkMrkdwn(appUrl, growiCommandArgs) {
|
|
|
+ const url = new URL('/_search', appUrl);
|
|
|
+ url.searchParams.append('q', growiCommandArgs.map(kwd => encodeURIComponent(kwd)).join('+'));
|
|
|
+ return `<${url.href} | Results page>`;
|
|
|
+ }
|
|
|
|
|
|
- return {
|
|
|
- type: 'section',
|
|
|
- text: {
|
|
|
- type: 'mrkdwn',
|
|
|
- text: `${this.appendSpeechBaloon(`*${this.generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`
|
|
|
- + `\n Last updated: ${this.generateLastUpdateMrkdwn(updatedAt, now)}`,
|
|
|
- },
|
|
|
- accessory: {
|
|
|
- type: 'button',
|
|
|
- action_id: 'search:shareSinglePageResult',
|
|
|
- text: {
|
|
|
- type: 'plain_text',
|
|
|
- text: 'Share',
|
|
|
- },
|
|
|
- value: JSON.stringify({ page, href, pathname }),
|
|
|
- },
|
|
|
- };
|
|
|
- }),
|
|
|
- { type: 'divider' },
|
|
|
- contextBlock,
|
|
|
- ];
|
|
|
+ function generatePageLinkMrkdwn(pathname, href) {
|
|
|
+ return `<${decodeURI(href)} | ${decodeURI(pathname)}>`;
|
|
|
+ }
|
|
|
|
|
|
- // DEFAULT show "Share" button
|
|
|
- // const actionBlocks = {
|
|
|
- // type: 'actions',
|
|
|
- // elements: [
|
|
|
- // {
|
|
|
- // type: 'button',
|
|
|
- // text: {
|
|
|
- // type: 'plain_text',
|
|
|
- // text: 'Share',
|
|
|
- // },
|
|
|
- // style: 'primary',
|
|
|
- // action_id: 'shareSearchResults',
|
|
|
- // },
|
|
|
- // ],
|
|
|
- // };
|
|
|
- const actionBlocks = {
|
|
|
- type: 'actions',
|
|
|
- elements: [
|
|
|
- {
|
|
|
- type: 'button',
|
|
|
- text: {
|
|
|
- type: 'plain_text',
|
|
|
- text: 'Dismiss',
|
|
|
- },
|
|
|
- style: 'danger',
|
|
|
- action_id: 'search:dismissSearchResults',
|
|
|
- },
|
|
|
- ],
|
|
|
- };
|
|
|
- // show "Next" button if next page exists
|
|
|
- if (resultsTotal > offset + PAGINGLIMIT) {
|
|
|
- actionBlocks.elements.unshift(
|
|
|
- {
|
|
|
- type: 'button',
|
|
|
- text: {
|
|
|
- type: 'plain_text',
|
|
|
- text: 'Next',
|
|
|
- },
|
|
|
- action_id: 'search:showNextResults',
|
|
|
- value: JSON.stringify({ offset, body, args }),
|
|
|
- },
|
|
|
- );
|
|
|
+ function generateLastUpdateMrkdwn(updatedAt, baseDate) {
|
|
|
+ if (updatedAt != null) {
|
|
|
+ // cast to date
|
|
|
+ const date = new Date(updatedAt);
|
|
|
+ return formatDistanceStrict(date, baseDate);
|
|
|
}
|
|
|
- blocks.push(actionBlocks);
|
|
|
-
|
|
|
- await client.chat.postEphemeral({
|
|
|
- channel: body.channel_id,
|
|
|
- user: body.user_id,
|
|
|
- text: 'Successed To Search',
|
|
|
- blocks,
|
|
|
- });
|
|
|
- };
|
|
|
-
|
|
|
- handler.handleBlockActions = async function(client, payload, handlerMethodName) {
|
|
|
- await this[handlerMethodName](client, payload);
|
|
|
- };
|
|
|
-
|
|
|
- handler.shareSinglePageResult = async function(client, payload) {
|
|
|
- const { channel, user, actions } = payload;
|
|
|
-
|
|
|
- const appUrl = crowi.appService.getSiteUrl();
|
|
|
- const appTitle = crowi.appService.getAppTitle();
|
|
|
+ return '';
|
|
|
+ }
|
|
|
|
|
|
- const channelId = channel.id;
|
|
|
- const action = actions[0]; // shareSinglePage action must have button action
|
|
|
+ async function retrieveSearchResults(growiCommandArgs, offset = 0) {
|
|
|
+ const keywords = getKeywords(growiCommandArgs);
|
|
|
|
|
|
- // restore page data from value
|
|
|
- const { page, href, pathname } = JSON.parse(action.value);
|
|
|
- const { updatedAt, commentCount } = page;
|
|
|
+ const { searchService } = crowi;
|
|
|
+ const options = { limit: PAGINGLIMIT, offset };
|
|
|
+ const results = await searchService.searchKeyword(keywords, null, {}, options);
|
|
|
+ const resultsTotal = results.meta.total;
|
|
|
|
|
|
- // share
|
|
|
- const now = new Date();
|
|
|
- return client.chat.postMessage({
|
|
|
- channel: channelId,
|
|
|
- blocks: [
|
|
|
- { type: 'divider' },
|
|
|
- markdownSectionBlock(`${this.appendSpeechBaloon(`*${this.generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`),
|
|
|
- {
|
|
|
- type: 'context',
|
|
|
- elements: [
|
|
|
- {
|
|
|
- type: 'mrkdwn',
|
|
|
- text: `<${decodeURI(appUrl)}|*${appTitle}*> | Last updated: ${this.generateLastUpdateMrkdwn(updatedAt, now)} | Shared by *${user.username}*`,
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- ],
|
|
|
+ const pages = results.data.map((data) => {
|
|
|
+ const { path, updated_at: updatedAt, comment_count: commentCount } = data._source;
|
|
|
+ return { path, updatedAt, commentCount };
|
|
|
});
|
|
|
- };
|
|
|
|
|
|
- handler.showNextResults = async function(client, payload) {
|
|
|
- const parsedValue = JSON.parse(payload.actions[0].value);
|
|
|
-
|
|
|
- const { body, args, offset: offsetNum } = parsedValue;
|
|
|
- const newOffsetNum = offsetNum + 10;
|
|
|
- let searchResult;
|
|
|
- try {
|
|
|
- searchResult = await this.retrieveSearchResults(client, body, args, newOffsetNum);
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- logger.error('Failed to get search results.', err);
|
|
|
- throw new SlackbotError({
|
|
|
- method: 'postEphemeral',
|
|
|
- to: 'channel',
|
|
|
- popupMessage: 'Failed To Search',
|
|
|
- mainMessage: '*Failed to search.*\n Hint\n `/growi search [keyword]`',
|
|
|
- });
|
|
|
- }
|
|
|
+ return {
|
|
|
+ pages, offset, resultsTotal,
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
+ function buildRespondBodyForSearchResult(searchResult, growiCommandArgs) {
|
|
|
const appUrl = crowi.appService.getSiteUrl();
|
|
|
const appTitle = crowi.appService.getAppTitle();
|
|
|
|
|
|
@@ -215,28 +71,27 @@ module.exports = (crowi) => {
|
|
|
pages, offset, resultsTotal,
|
|
|
} = searchResult;
|
|
|
|
|
|
- const keywords = this.getKeywords(args);
|
|
|
-
|
|
|
+ const keywords = getKeywords(growiCommandArgs);
|
|
|
|
|
|
let searchResultsDesc;
|
|
|
-
|
|
|
switch (resultsTotal) {
|
|
|
case 1:
|
|
|
searchResultsDesc = `*${resultsTotal}* page is found.`;
|
|
|
break;
|
|
|
-
|
|
|
default:
|
|
|
searchResultsDesc = `*${resultsTotal}* pages are found.`;
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
-
|
|
|
const contextBlock = {
|
|
|
type: 'context',
|
|
|
elements: [
|
|
|
{
|
|
|
type: 'mrkdwn',
|
|
|
- text: `keyword(s) : *"${keywords}"* | Current: ${offset + 1} - ${offset + pages.length} | Total ${resultsTotal} pages`,
|
|
|
+ text: `keyword(s) : *"${keywords}"*`
|
|
|
+ + ` | Total ${resultsTotal} pages`
|
|
|
+ + ` | Current: ${offset + 1} - ${offset + pages.length}`
|
|
|
+ + ` | ${generateSearchResultPageLinkMrkdwn(appUrl, growiCommandArgs)}`,
|
|
|
},
|
|
|
],
|
|
|
};
|
|
|
@@ -257,8 +112,8 @@ module.exports = (crowi) => {
|
|
|
type: 'section',
|
|
|
text: {
|
|
|
type: 'mrkdwn',
|
|
|
- text: `${this.appendSpeechBaloon(`*${this.generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`
|
|
|
- + `\n Last updated: ${this.generateLastUpdateMrkdwn(updatedAt, now)}`,
|
|
|
+ text: `${appendSpeechBaloon(`*${generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`
|
|
|
+ + ` \`${generateLastUpdateMrkdwn(updatedAt, now)}\``,
|
|
|
},
|
|
|
accessory: {
|
|
|
type: 'button',
|
|
|
@@ -275,21 +130,6 @@ module.exports = (crowi) => {
|
|
|
contextBlock,
|
|
|
];
|
|
|
|
|
|
- // DEFAULT show "Share" button
|
|
|
- // const actionBlocks = {
|
|
|
- // type: 'actions',
|
|
|
- // elements: [
|
|
|
- // {
|
|
|
- // type: 'button',
|
|
|
- // text: {
|
|
|
- // type: 'plain_text',
|
|
|
- // text: 'Share',
|
|
|
- // },
|
|
|
- // style: 'primary',
|
|
|
- // action_id: 'shareSearchResults',
|
|
|
- // },
|
|
|
- // ],
|
|
|
- // };
|
|
|
const actionBlocks = {
|
|
|
type: 'actions',
|
|
|
elements: [
|
|
|
@@ -304,65 +144,65 @@ module.exports = (crowi) => {
|
|
|
},
|
|
|
],
|
|
|
};
|
|
|
+ // show "Prev" button if previous page exists
|
|
|
+ // eslint-disable-next-line yoda
|
|
|
+ if (0 < offset) {
|
|
|
+ actionBlocks.elements.unshift(
|
|
|
+ {
|
|
|
+ type: 'button',
|
|
|
+ text: {
|
|
|
+ type: 'plain_text',
|
|
|
+ text: '< Prev',
|
|
|
+ },
|
|
|
+ action_id: 'search:showPrevResults',
|
|
|
+ value: JSON.stringify({ offset, growiCommandArgs }),
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }
|
|
|
// show "Next" button if next page exists
|
|
|
- if (resultsTotal > offset + PAGINGLIMIT) {
|
|
|
+ if (offset + PAGINGLIMIT < resultsTotal) {
|
|
|
actionBlocks.elements.unshift(
|
|
|
{
|
|
|
type: 'button',
|
|
|
text: {
|
|
|
type: 'plain_text',
|
|
|
- text: 'Next',
|
|
|
+ text: 'Next >',
|
|
|
},
|
|
|
action_id: 'search:showNextResults',
|
|
|
- value: JSON.stringify({ offset, body, args }),
|
|
|
+ value: JSON.stringify({ offset, growiCommandArgs }),
|
|
|
},
|
|
|
);
|
|
|
}
|
|
|
blocks.push(actionBlocks);
|
|
|
|
|
|
- await client.chat.postEphemeral({
|
|
|
- channel: body.channel_id,
|
|
|
- user: body.user_id,
|
|
|
+ return {
|
|
|
text: 'Successed To Search',
|
|
|
blocks,
|
|
|
- });
|
|
|
- };
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
- handler.dismissSearchResults = async function(client, payload) {
|
|
|
- const { response_url: responseUrl } = payload;
|
|
|
|
|
|
- return axios.post(responseUrl, {
|
|
|
- delete_original: true,
|
|
|
- });
|
|
|
- };
|
|
|
+ async function buildRespondBody(growiCommandArgs) {
|
|
|
+ const firstKeyword = growiCommandArgs[0];
|
|
|
|
|
|
- handler.retrieveSearchResults = async function(client, body, args, offset = 0) {
|
|
|
- const firstKeyword = args[1];
|
|
|
+ // enpty keyword
|
|
|
if (firstKeyword == null) {
|
|
|
- client.chat.postEphemeral({
|
|
|
- channel: body.channel_id,
|
|
|
- user: body.user_id,
|
|
|
+ return {
|
|
|
text: 'Input keywords',
|
|
|
blocks: [
|
|
|
markdownSectionBlock('*Input keywords.*\n Hint\n `/growi search [keyword]`'),
|
|
|
],
|
|
|
- });
|
|
|
- return { pages: [] };
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- const keywords = this.getKeywords(args);
|
|
|
-
|
|
|
- const { searchService } = crowi;
|
|
|
- const options = { limit: 10, offset };
|
|
|
- const results = await searchService.searchKeyword(keywords, null, {}, options);
|
|
|
- const resultsTotal = results.meta.total;
|
|
|
+ const searchResult = await retrieveSearchResults(growiCommandArgs);
|
|
|
|
|
|
// no search results
|
|
|
- if (results.data.length === 0) {
|
|
|
+ if (searchResult.resultsTotal === 0) {
|
|
|
+ const keywords = getKeywords(growiCommandArgs);
|
|
|
logger.info(`No page found with "${keywords}"`);
|
|
|
- client.chat.postEphemeral({
|
|
|
- channel: body.channel_id,
|
|
|
- user: body.user_id,
|
|
|
+
|
|
|
+ return {
|
|
|
text: `No page found with "${keywords}"`,
|
|
|
blocks: [
|
|
|
markdownSectionBlock(`*No page matches your keyword(s) "${keywords}".*`),
|
|
|
@@ -382,43 +222,106 @@ module.exports = (crowi) => {
|
|
|
divider(),
|
|
|
markdownSectionBlock('`-tag:wiki` \n Exclude pages with wiki tag'),
|
|
|
],
|
|
|
- });
|
|
|
- return { pages: [] };
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- const pages = results.data.map((data) => {
|
|
|
- const { path, updated_at: updatedAt, comment_count: commentCount } = data._source;
|
|
|
- return { path, updatedAt, commentCount };
|
|
|
- });
|
|
|
+ return buildRespondBodyForSearchResult(searchResult, growiCommandArgs);
|
|
|
+ }
|
|
|
|
|
|
- return {
|
|
|
- pages, offset, resultsTotal,
|
|
|
- };
|
|
|
- };
|
|
|
|
|
|
- handler.getKeywords = function(args) {
|
|
|
- const keywordsArr = args.slice(1);
|
|
|
- const keywords = keywordsArr.join(' ');
|
|
|
- return keywords;
|
|
|
+ handler.handleCommand = async function(growiCommand, client, body) {
|
|
|
+ const { responseUrl, growiCommandArgs } = growiCommand;
|
|
|
+
|
|
|
+ const respondBody = await buildRespondBody(growiCommandArgs);
|
|
|
+ await respond(responseUrl, respondBody);
|
|
|
};
|
|
|
|
|
|
- handler.appendSpeechBaloon = function(mrkdwn, commentCount) {
|
|
|
- return (commentCount != null && commentCount > 0)
|
|
|
- ? `${mrkdwn} :speech_balloon: ${commentCount}`
|
|
|
- : mrkdwn;
|
|
|
+ handler.handleInteractions = async function(client, interactionPayload, interactionPayloadAccessor, handlerMethodName) {
|
|
|
+ await this[handlerMethodName](client, interactionPayload, interactionPayloadAccessor);
|
|
|
};
|
|
|
|
|
|
- handler.generatePageLinkMrkdwn = function(pathname, href) {
|
|
|
- return `<${decodeURI(href)} | ${decodeURI(pathname)}>`;
|
|
|
+ handler.shareSinglePageResult = async function(client, payload, interactionPayloadAccessor) {
|
|
|
+ const { user } = payload;
|
|
|
+ const responseUrl = interactionPayloadAccessor.getResponseUrl();
|
|
|
+
|
|
|
+ const appUrl = crowi.appService.getSiteUrl();
|
|
|
+ const appTitle = crowi.appService.getAppTitle();
|
|
|
+
|
|
|
+ const value = interactionPayloadAccessor.firstAction()?.value; // shareSinglePage action must have button action
|
|
|
+ if (value == null) {
|
|
|
+ await respond(responseUrl, {
|
|
|
+ text: 'Error occurred',
|
|
|
+ blocks: [
|
|
|
+ markdownSectionBlock('Failed to share the result.'),
|
|
|
+ ],
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // restore page data from value
|
|
|
+ const { page, href, pathname } = JSON.parse(value);
|
|
|
+ const { updatedAt, commentCount } = page;
|
|
|
+
|
|
|
+ // share
|
|
|
+ const now = new Date();
|
|
|
+ return respondInChannel(responseUrl, {
|
|
|
+ blocks: [
|
|
|
+ { type: 'divider' },
|
|
|
+ markdownSectionBlock(`${appendSpeechBaloon(`*${generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`),
|
|
|
+ {
|
|
|
+ type: 'context',
|
|
|
+ elements: [
|
|
|
+ {
|
|
|
+ type: 'mrkdwn',
|
|
|
+ text: `<${decodeURI(appUrl)}|*${appTitle}*>`
|
|
|
+ + ` | Last updated: \`${generateLastUpdateMrkdwn(updatedAt, now)}\``
|
|
|
+ + ` | Shared by *${user.username}*`,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
- handler.generateLastUpdateMrkdwn = function(updatedAt, baseDate) {
|
|
|
- if (updatedAt != null) {
|
|
|
- // cast to date
|
|
|
- const date = new Date(updatedAt);
|
|
|
- return formatDistanceStrict(date, baseDate);
|
|
|
+ async function showPrevOrNextResults(interactionPayloadAccessor, isNext = true) {
|
|
|
+ const responseUrl = interactionPayloadAccessor.getResponseUrl();
|
|
|
+
|
|
|
+ const value = interactionPayloadAccessor.firstAction()?.value;
|
|
|
+ if (value == null) {
|
|
|
+ await respond(responseUrl, {
|
|
|
+ text: 'Error occurred',
|
|
|
+ blocks: [
|
|
|
+ markdownSectionBlock('Failed to show the next results.'),
|
|
|
+ ],
|
|
|
+ });
|
|
|
+ return;
|
|
|
}
|
|
|
- return '';
|
|
|
+ const parsedValue = JSON.parse(value);
|
|
|
+
|
|
|
+ const { growiCommandArgs, offset: offsetNum } = parsedValue;
|
|
|
+ const newOffsetNum = isNext
|
|
|
+ ? offsetNum + PAGINGLIMIT
|
|
|
+ : offsetNum - PAGINGLIMIT;
|
|
|
+
|
|
|
+ const searchResult = await retrieveSearchResults(growiCommandArgs, newOffsetNum);
|
|
|
+
|
|
|
+ await replaceOriginal(responseUrl, buildRespondBodyForSearchResult(searchResult, growiCommandArgs));
|
|
|
+ }
|
|
|
+
|
|
|
+ handler.showPrevResults = async function(client, payload, interactionPayloadAccessor) {
|
|
|
+ return showPrevOrNextResults(interactionPayloadAccessor, false);
|
|
|
+ };
|
|
|
+
|
|
|
+ handler.showNextResults = async function(client, payload, interactionPayloadAccessor) {
|
|
|
+ return showPrevOrNextResults(interactionPayloadAccessor, true);
|
|
|
+ };
|
|
|
+
|
|
|
+ handler.dismissSearchResults = async function(client, payload) {
|
|
|
+ const { response_url: responseUrl } = payload;
|
|
|
+
|
|
|
+ return deleteOriginal(responseUrl, {
|
|
|
+ delete_original: true,
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
return handler;
|