{"flow":{"id":21,"summary":"OCR receipt pictures in gdrive folder and send result to Slack","versions":[51,52,53,54,56,57,58,61,64,69,70,71,72,73,74,83,87,88,89,90,91,92,96,105,110,114,143],"created_by":"fatonramadani","created_at":"2022-08-08T12:44:57.131Z","votes":1,"approved":true,"apps":["gdrive","slack"],"value":{"modules":[{"id":"a","value":{"type":"rawscript","content":"import * as wmill from \"https://deno.land/x/windmill@v1.28.1/mod.ts\";\nconst API_URL = `https://www.googleapis.com/drive/v3/files`;\n\nexport async function main(\n  gdrive_auth: wmill.Resource<\"gdrive\">,\n  folderId: string,\n) {\n  const parameters = {\n    q: `'${folderId}' in parents `,\n    orderBy: \"createdTime\",\n  };\n\n  const url = buildUrl(parameters);\n\n  const response = await fetch(url, {\n    method: \"GET\",\n    headers: {\n      Authorization: \"Bearer \" + gdrive_auth[\"token\"],\n      \"Content-Type\": \"application/json\",\n    },\n  });\n\n  const json = await response.json();\n  const newIds = await getNewIds(json.files.map((f: any) => f.id));\n\n  return newIds\n}\n\nfunction buildUrl(parameters: Record<string, string>): string {\n  const searchParams = new URLSearchParams(parameters);\n  const url = `${API_URL}?${searchParams.toString()}`;\n  return url;\n}\n\nasync function getNewIds(filesIds: string[]): Promise<string[]> {\n  const lastState = await wmill.getInternalState();\n  const previousFilesIds = lastState ? JSON.parse(lastState) : [];\n  const difference = filesIds.filter((file) =>\n    !previousFilesIds.includes(file)\n  );\n\n  await wmill.setInternalState(JSON.stringify(filesIds));\n\n  return difference;\n}\n","language":"deno","input_transforms":{"folderId":{"expr":"`${flow_input.folderId}`","type":"javascript"},"gdrive_auth":{"expr":"flow_input.gdrive_auth","type":"javascript"}}},"summary":"Watch for new files in gdrive folder","stop_after_if":{"expr":"result.length == 0","skip_if_stopped":true}},{"id":"b","value":{"type":"forloopflow","modules":[{"id":"c","value":{"path":"hub/144/download_a_file_as_base_64_string","type":"script","input_transforms":{"fileId":{"expr":"`${flow_input.iter.value}`","type":"javascript"},"gdrive_auth":{"expr":"flow_input.gdrive_auth","type":"javascript"}}},"summary":"Download file"},{"id":"d","value":{"type":"rawscript","content":"import * as tesseract from \"https://deno.land/x/tesseract@1.0.1/mod.ts\";\nimport { decode } from \"https://deno.land/std@0.82.0/encoding/base64.ts\";\n\nexport async function main(\n  base64: string,\n) {\n  const bin = decode(base64);\n\n  const path = \"tmp\";\n  if (bin) {\n    await Deno.writeFile(path, bin);\n    const output = await tesseract.recognize(path);\n    return { text: output };\n  } else {\n    return { text: \"\" };\n  }\n}\n","language":"deno","input_transforms":{"base64":{"expr":"`${previous_result}`","type":"javascript"}}},"summary":"OCR using tesseract"},{"id":"e","value":{"type":"rawscript","content":"// only do the following import if you require your script to interact with the windmill\n// for instance to get a variable or resource\n// import * as wmill from 'https://deno.land/x/windmill@v1.28.1/mod.ts'\n\nconst PRICE_REGEX = new RegExp(/\\d{1,3}(?:[.'`,]\\d{3})*(?:[.,]\\d{2})/g);\nconst DATE_REGEX = new RegExp(/\\d{1,2}[/.-]\\d{1,2}[/.-]\\d{2,4}/g);\n\nfunction DESC_SORT(a: number, b: number): number {\n  return b - a;\n}\n\nfunction findNLargestPrices(\n  extractedText: string,\n  n: number,\n): Array<number> {\n  const extractedPrices: string[] | null = extractedText.match(PRICE_REGEX);\n\n  if (!extractedPrices || extractedPrices.length === 0) {\n    return [];\n  }\n\n  const prices = extractedPrices\n    .map((extractedPrice: string) => extractedPrice.replace(\",\", \"\"))\n    .map((extractedPrice: string) => extractedPrice.replace(\"'\", \"\"))\n    .map(Number);\n\n  const uniquePrices: number[] = Array.from(new Set(prices));\n  return uniquePrices.sort(DESC_SORT).slice(0, n);\n}\n\nfunction findDate(extractedText: string) {\n  const extractedDates: string[] | null = extractedText.match(DATE_REGEX);\n  if (!extractedDates || extractedDates.length === 0) {\n    return [];\n  }\n\n  return extractedDates;\n}\n\nexport async function main(extractedText: string,  fileId: string) {\n  const [price] = findNLargestPrices(extractedText, 1);\n  const [date] = findDate(extractedText);\n  return { price, date , fileId};\n}\n","language":"deno","input_transforms":{"fileId":{"expr":"`${flow_input.iter.value}`","type":"javascript"},"extractedText":{"expr":"`${previous_result.text}`","type":"javascript"}}},"summary":"Detect red flags"},{"id":"f","value":{"path":"hub/111/send_message_to_channel","type":"rawscript","content":"import { WebClient } from \"https://deno.land/x/slack_web_api@1.0.0/mod.ts\";\nimport type { Resource } from \"https://deno.land/x/windmill@v1.24.1/mod.ts\";\n\nexport async function main(\n  content: { price: number; date: string; },\n  channel: string,\n  slack: Resource<\"slack\">,\n  fileId: string\n) {\n  const web = new WebClient(slack.token);\n\n  const isWeekend = !(new Date(content.date).getDay() % 6);\n  const isAboveThreshold = content.price > 100;\n\n  if (isWeekend) {\n    await web.chat.postMessage({\n      channel,\n      text:\n        `The Expense with fileID: ${fileId} was made during the weekend.`,\n    });\n  }\n\n  if (isAboveThreshold) {\n    await web.chat.postMessage({\n      channel,\n      text:\n        `The Expense with fileID: ${fileId} is above threshold: ${content.price}.`,\n    });\n  }\n\n  return {\n    isWeekend,\n    isAboveThreshold,\n  };\n}\n","language":"deno","input_transforms":{"slack":{"expr":"flow_input.slack_auth","type":"javascript"},"fileId":{"expr":"`${flow_input.iter.value}`","type":"javascript"},"channel":{"expr":"`${flow_input.channel}`","type":"javascript"},"content":{"expr":"previous_result","type":"javascript"}}}}],"iterator":{"expr":"result","type":"javascript"},"skip_failures":true},"input_transforms":{}}]},"schema":{"type":"object","$schema":"https://json-schema.org/draft/2020-12/schema","required":["gdrive_auth","folderId","slack_auth","channel","channel","folderId","slack_auth","gdrive_auth"],"properties":{"channel":{"type":"string","format":"","description":"The Slack channel where the warning messages are sent."},"folderId":{"type":"string","format":"","description":"The ID of the folder where the photos of expenses are uploaded."},"slack_auth":{"type":"object","format":"resource-slack","description":"The Slack authentication resource"},"gdrive_auth":{"type":"object","format":"resource-gdrive","description":"The Google Drive authentication resource"}}},"description":"When new expenses are uploaded to Google Drive, the flow downloads the image, extracts the text using Tesseract, finds the date and price of the expense and finally sends warning messages on Slack if the expense was made during the weekend or is above a given threshold.","recording":null,"vcreated_at":"2022-12-14T19:59:03.263Z","vcreated_by":"admin","comments":[]}}