diff --git a/apps/dashboard/jobs/tasks/inbox/slack-upload.ts b/apps/dashboard/jobs/tasks/inbox/slack-upload.ts index b5ff60a76f..355045ed85 100644 --- a/apps/dashboard/jobs/tasks/inbox/slack-upload.ts +++ b/apps/dashboard/jobs/tasks/inbox/slack-upload.ts @@ -1,198 +1,194 @@ -// import { createSlackWebClient, downloadFile } from "@midday/app-store/slack"; -// import { DocumentClient, prepareDocument } from "@midday/documents"; -// import { eventTrigger } from "@trigger.dev/sdk"; -// import { format } from "date-fns"; -// import { z } from "zod"; -// import { client, supabase } from "../client"; -// import { Events, Jobs } from "../constants"; - -// const concurrencyLimit = client.defineConcurrencyLimit({ -// id: "inbox-slack-upload", -// limit: 25, -// }); - -// client.defineJob({ -// id: Jobs.INBOX_SLACK_UPLOAD, -// name: "Inbox - Slack Upload", -// version: "0.0.1", -// concurrencyLimit, -// trigger: eventTrigger({ -// name: Events.INBOX_SLACK_UPLOAD, -// schema: z.object({ -// teamId: z.string(), -// token: z.string(), -// channelId: z.string(), -// threadId: z.string().optional(), -// file: z.object({ -// id: z.string(), -// name: z.string(), -// mimetype: z.string(), -// size: z.number(), -// url: z.string(), -// }), -// }), -// }), -// integrations: { -// supabase, -// }, -// run: async (payload, io) => { -// const { -// teamId, -// token, -// channelId, -// threadId, -// file: { id, name, mimetype, size, url }, -// } = payload; - -// const slackApp = createSlackWebClient({ -// token, -// }); - -// if (threadId) { -// await slackApp.assistant.threads.setStatus({ -// channel_id: channelId, -// thread_ts: threadId, -// status: "Is thinking...", -// }); -// } - -// const fileData = await downloadFile({ -// privateDownloadUrl: url, -// token, -// }); - -// if (!fileData) { -// throw Error("No file data"); -// } - -// const document = await prepareDocument({ -// Content: Buffer.from(fileData).toString("base64"), -// ContentType: mimetype, -// ContentLength: size, -// Name: name, -// }); - -// const pathTokens = [teamId, "inbox", document.fileName]; - -// // Upload file to vault -// await io.supabase.client.storage -// .from("vault") -// .upload(pathTokens.join("/"), new Uint8Array(document.content), { -// contentType: document.mimeType, -// upsert: true, -// }); - -// const { data: inboxData } = await io.supabase.client -// .from("inbox") -// .insert({ -// // NOTE: If we can't parse the name using OCR this will be the fallback name -// display_name: document.name, -// team_id: teamId, -// file_path: pathTokens, -// file_name: document.fileName, -// content_type: document.mimeType, -// reference_id: `${id}_${document.fileName}`, -// size, -// }) -// .select("*") -// .single() -// .throwOnError(); - -// try { -// const document = new DocumentClient({ -// contentType: inboxData?.content_type, -// }); - -// const result = await document.getDocument({ -// content: Buffer.from(fileData).toString("base64"), -// }); - -// const { data: updatedInbox } = await io.supabase.client -// .from("inbox") -// .update({ -// amount: result.amount, -// currency: result.currency, -// display_name: result.name, -// website: result.website, -// date: result.date && new Date(result.date), -// type: result.type, -// description: result.description, -// status: "pending", -// }) -// .eq("id", inboxData.id) -// .select() -// .single(); - -// if (updatedInbox?.amount) { -// // Send notification to slack -// try { -// await slackApp.chat.postMessage({ -// channel: channelId, -// thread_ts: threadId, -// unfurl_links: false, -// unfurl_media: false, -// blocks: [ -// { -// type: "section", -// text: { -// type: "mrkdwn", -// text: `Here's the information I extracted from your receipt:\n\n• *Vendor:* ${updatedInbox.display_name}\n• *Amount:* ${new Intl.NumberFormat( -// "en-US", -// { -// style: "currency", -// currency: updatedInbox.currency, -// }, -// ).format( -// updatedInbox.amount, -// )}\n• *Date:* ${updatedInbox.date ? format(new Date(updatedInbox.date), "MMM d") : ""}\n\nWe'll notify you when we match it to a transaction.`, -// }, -// }, -// { -// type: "actions", -// elements: [ -// { -// type: "button", -// text: { -// type: "plain_text", -// text: "Show receipt", -// emoji: true, -// }, -// url: `https://app.midday.ai/inbox?id=${encodeURIComponent(updatedInbox.id)}`, -// action_id: "view_receipt", -// }, -// ], -// }, -// ], -// }); - -// if (threadId) { -// await slackApp.assistant.threads.setStatus({ -// channel_id: channelId, -// thread_ts: threadId, -// status: "", -// }); -// } -// } catch (err) { -// console.error(err); -// } - -// // TODO: Send event to match inbox -// } -// } catch { -// // If we end up here we could not parse the document -// // But we want to update the status so we show the record with fallback name -// await io.supabase.client -// .from("inbox") -// .update({ status: "pending" }) -// .eq("id", inboxData.id); - -// if (threadId) { -// await slackApp.assistant.threads.setStatus({ -// channel_id: channelId, -// thread_ts: threadId, -// status: "", -// }); -// } -// } -// }, -// }); +import { + createSlackWebClient, + downloadFile, +} from "@midday/app-store/slack-client"; +import { DocumentClient, prepareDocument } from "@midday/documents"; +import { createClient } from "@midday/supabase/job"; +import { schemaTask } from "@trigger.dev/sdk/v3"; +import { format } from "date-fns"; +import { z } from "zod"; + +export const inboxSlackUpload = schemaTask({ + id: "inbox-slack-upload", + schema: z.object({ + teamId: z.string(), + token: z.string(), + channelId: z.string(), + threadId: z.string().optional(), + file: z.object({ + id: z.string(), + name: z.string(), + mimetype: z.string(), + size: z.number(), + url: z.string(), + }), + }), + maxDuration: 300, + queue: { + concurrencyLimit: 10, + }, + run: async ({ + teamId, + token, + channelId, + threadId, + file: { id, name, mimetype, size, url }, + }) => { + const supabase = createClient(); + + const slackApp = createSlackWebClient({ + token, + }); + + if (threadId) { + await slackApp.assistant.threads.setStatus({ + channel_id: channelId, + thread_ts: threadId, + status: "Is thinking...", + }); + } + + const fileData = await downloadFile({ + privateDownloadUrl: url, + token, + }); + + if (!fileData) { + throw Error("No file data"); + } + + const document = await prepareDocument({ + Content: Buffer.from(fileData).toString("base64"), + ContentType: mimetype, + ContentLength: size, + Name: name, + }); + + const pathTokens = [teamId, "inbox", document.fileName]; + + // Upload file to vault + await supabase.storage + .from("vault") + .upload(pathTokens.join("/"), new Uint8Array(document.content), { + contentType: document.mimeType, + upsert: true, + }); + + const { data: inboxData } = await supabase + .from("inbox") + .insert({ + // NOTE: If we can't parse the name using OCR this will be the fallback name + display_name: document.name, + team_id: teamId, + file_path: pathTokens, + file_name: document.fileName, + content_type: document.mimeType, + reference_id: `${id}_${document.fileName}`, + size, + }) + .select("*") + .single() + .throwOnError(); + + if (!inboxData) { + throw Error("Inbox data not found"); + } + + try { + const document = new DocumentClient({ + contentType: inboxData.content_type!, + }); + + const result = await document.getDocument({ + content: Buffer.from(fileData).toString("base64"), + }); + + const { data: updatedInbox } = await supabase + .from("inbox") + .update({ + amount: result.amount, + currency: result.currency, + display_name: result.name, + website: result.website, + date: result.date ? new Date(result.date).toISOString() : null, + type: result.type, + description: result.description, + status: "pending", + }) + .eq("id", inboxData.id) + .select() + .single(); + + if (updatedInbox?.amount) { + // Send notification to slack + try { + await slackApp.chat.postMessage({ + channel: channelId, + thread_ts: threadId, + unfurl_links: false, + unfurl_media: false, + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: `Here's the information I extracted from your receipt:\n\n• *Vendor:* ${updatedInbox.display_name}\n• *Amount:* ${new Intl.NumberFormat( + "en-US", + { + style: "currency", + currency: updatedInbox.currency!, + }, + ).format( + updatedInbox.amount, + )}\n• *Date:* ${updatedInbox.date ? format(new Date(updatedInbox.date), "MMM d") : ""}\n\nWe'll notify you when we match it to a transaction.`, + }, + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { + type: "plain_text", + text: "Show receipt", + emoji: true, + }, + url: `https://app.midday.ai/inbox?id=${encodeURIComponent(updatedInbox.id)}`, + action_id: "view_receipt", + }, + ], + }, + ], + }); + + if (threadId) { + await slackApp.assistant.threads.setStatus({ + channel_id: channelId, + thread_ts: threadId, + status: "", + }); + } + } catch (err) { + console.error(err); + } + + // TODO: Send event to match inbox + } + } catch { + // If we end up here we could not parse the document + // But we want to update the status so we show the record with fallback name + await supabase + .from("inbox") + .update({ status: "pending" }) + .eq("id", inboxData.id); + + if (threadId) { + await slackApp.assistant.threads.setStatus({ + channel_id: channelId, + thread_ts: threadId, + status: "", + }); + } + } + }, +}); diff --git a/packages/app-store/package.json b/packages/app-store/package.json index 34cb0f3105..9b995713c5 100644 --- a/packages/app-store/package.json +++ b/packages/app-store/package.json @@ -24,6 +24,7 @@ ".": "./src/index.ts", "./slack": "./src/slack/index.ts", "./slack-notifications": "./src/slack/lib/notifications/transactions.ts", + "./slack-client": "./src/slack/lib/client.ts", "./db": "./src/db/index.ts" } } diff --git a/packages/app-store/src/slack/lib/events/file/share.ts b/packages/app-store/src/slack/lib/events/file/share.ts index 6f2870e19e..b1ea5dfe6a 100644 --- a/packages/app-store/src/slack/lib/events/file/share.ts +++ b/packages/app-store/src/slack/lib/events/file/share.ts @@ -1,32 +1,35 @@ -// import { Events, client } from "@midday/jobs"; -// import type { FileShareMessageEvent } from "@slack/web-api"; +import { inboxSlackUpload } from "@midday/dashboard/jobs/tasks/inbox/slack-upload"; +import type { FileShareMessageEvent } from "@slack/web-api"; -// export async function fileShare( -// event: FileShareMessageEvent, -// { teamId, token }: { teamId: string; token: string }, -// ) { -// const files = event?.files?.map((file) => ({ -// id: file.id, -// name: file.name, -// mimetype: file.mimetype, -// size: file.size, -// url: file.url_private_download, -// })); +export async function fileShare( + event: FileShareMessageEvent, + { teamId, token }: { teamId: string; token: string }, +) { + const files = event?.files?.map((file) => ({ + id: file.id, + name: file.name, + mimetype: file.mimetype, + size: file.size, + url: file.url_private_download, + })); -// if (files && files.length > 0) { -// await Promise.all( -// files.map((file) => -// client.sendEvent({ -// name: Events.INBOX_SLACK_UPLOAD, -// payload: { -// teamId, -// token, -// channelId: event.channel, -// threadId: event.thread_ts, -// file, -// }, -// }), -// ), -// ); -// } -// } + if (files && files.length > 0) { + await inboxSlackUpload.batchTrigger( + files.map((file) => ({ + payload: { + teamId, + token, + channelId: event.channel, + threadId: event.thread_ts, + file: { + id: file.id, + name: file.name!, + mimetype: file.mimetype, + size: file.size, + url: file.url!, + }, + }, + })), + ); + } +}