@sebbo2002/tgtg-ical 2.0.9-develop.8 → 3.0.0-develop.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,10 +4,9 @@
4
4
 
5
5
  A small server that receives mails from TGTG, parses them and generates an iCal feed from them.
6
6
 
7
-
8
7
  ## 📦 Installation
9
8
 
10
- git clone https://github.com/sebbo2002/tgtg-ical.git
9
+ git clone https://github.com/sebbo2002/tgtg-ical.git
11
10
  cd ./tgtg-ical
12
11
 
13
12
  echo 'DATABASE_URL="mysql://root@localhost:3306/tgtg-ical"' > .env
@@ -15,25 +14,27 @@ A small server that receives mails from TGTG, parses them and generates an iCal
15
14
  npm install
16
15
  npx prisma migrate deploy
17
16
 
18
-
19
17
  ## 🙋 FAQ
20
18
 
21
19
  ### How does this work?
22
- With the help of tgtg-ical you can generate a personal email address and a corresponding calendar feed. If you store
23
- this email address at Too Good To Go as an email address for notifications or forward the messages (e.g. via a filter
20
+
21
+ With the help of tgtg-ical you can generate a personal email address and a corresponding calendar feed. If you store
22
+ this email address at Too Good To Go as an email address for notifications or forward the messages (e.g. via a filter
24
23
  rule), collection appointments will be displayed in the corresponding calendar feed.
25
24
 
26
25
  ### Which languages are supported?
26
+
27
27
  Currently only German and English are supported. If you want to add another language, feel free to create a pull request.
28
28
 
29
29
  ### How long are emails stored?
30
- In the best case, the incoming e-mail can be completely analyzed and is then deleted directly. Then only the information
31
- needed to provide the calendar is stored. If the analysis fails, the email is kept for manual analysis and deleted after
30
+
31
+ In the best case, the incoming e-mail can be completely analyzed and is then deleted directly. Then only the information
32
+ needed to provide the calendar is stored. If the analysis fails, the email is kept for manual analysis and deleted after
32
33
  two weeks at the latest.
33
34
 
34
35
  ### Which databases are supported?
35
- We use [Prisma](https://www.prisma.io/) as ORM. All databases supported by Prisma should therefore work with tgtg-ical.
36
36
 
37
+ We use [Prisma](https://www.prisma.io/) as ORM. All databases supported by Prisma should therefore work with tgtg-ical.
37
38
 
38
39
  ## 🙆🏼‍♂️ Copyright and license
39
40
 
@@ -0,0 +1,2 @@
1
+ import{init as I}from"@sentry/node";import{readFileSync as A}from"node:fs";import{dirname as k,resolve as g}from"node:path";import{fileURLToPath as x}from"node:url";I({dsn:process.env.SENTRY_DSN||"https://5e4630d58e5f4c778ce22140c53b3684@glitch.sebbo.net/9"});var u=k(x(import.meta.url));u.endsWith("/dist")?u=g(u,"..","src"):u=g(u,"..");function v(l){return g(u,l||"")}var D;try{D=JSON.parse(A(v("../package.json"),"utf8")).version}catch(l){console.log("Failed getting tgtg-ical version:"),console.error(l)}var c={baseMail:process.env.BASE_MAIL||"@tgtg-ical.sebbo.net",baseUrl:process.env.BASE_URL||"https://tgtg-ical.sebbo.net",src:v,version:D};import{PrismaClient as P}from"@prisma/client";var z=new P,i=z;import{captureException as B}from"@sentry/node";import E from"he";import{simpleParser as T}from"mailparser";import n from"moment-timezone";var O="\u{1F374}",b={"\u{1F32D}":[/Ikea/],"\u{1F32E}":[/Enchilada/,/Besitos/],"\u{1F354}":[/Burger/,/McDonald[‘|']?s/,/Burger King/,/Hans im Glück/,/Peter Pane/],"\u{1F355}":[/Domino‘s/,/Pizza Hut/,/L[‘|']Osteria/,/Call a Pizza/,/Smiley[‘|']s Pizza/],"\u{1F357}":[/KFC/],"\u{1F35D}":[/LaTagliatella/,/Vapiano/],"\u{1F364}":[/Nordsee/],"\u{1F369}":[/Donut/,/Dunkin/],"\u{1F36A}":[/Starbucks/],"\u{1F950}":[/LeCroBag/],"\u{1F956}":[/Bäckerei/,/Back/,/BackWerk/,/Kamps/,/Kamps Backstuben/,/Junge Die Bäckerei/,/Back-Factory/],"\u{1F957}":[/dean ?& ?david/],"\u{1F968}":[/Brezel/,/Ditsch/],"\u{1F969}":[/Steakhouse/,/Block House/,/Jim Block/],"\u{1F96A}":[/Sandwich/,/Tank ?& ?Rast/,/T&R Raststätten/,/Autohöfe/,/Aral/,/Shell/,/Jet Tank/,/Subway/,/Caf[é|e] bonjour/,/Total Deutschland/,/Esso:? Snack ?& ?Shop/],"\u{1F6D2}":[/Edeka/,/Netto/,/Rewe/,/Penny/,/Lidl/,/Kaufland/,/Aldi/,/dm/,/Rossmann/,/Globus/,/Metro/,/Norma/,/Tegut/]};function w(l){for(let t in b)if(!!b[t].map(a=>new RegExp(a,"i")).find(a=>a.test(l)))return t;return null}var M=class{static async geocode(t){let r=await fetch("https://nominatim.openstreetmap.org/search?format=json&limit=1&q="+encodeURIComponent(t),{headers:{Referer:c.baseUrl,"User-Agent":`tgtg-ical/${c.version} (${c.baseUrl})`}});if(!r.ok)throw new Error("Geocoding failed: "+r.statusText);await new Promise(a=>setTimeout(a,1e3));let e=await r.json();return!Array.isArray(e)||e.length===0?{latitude:null,longitude:null}:{latitude:parseFloat(e[0].lat),longitude:parseFloat(e[0].lon)}}static async handleMail(t){try{let r=await this.parseMail(t.raw);r&&await this.applyParsedMail(r),await i.mail.delete({where:{id:t.id}})}catch(r){let e=B(r);await i.mail.update({data:{error:r instanceof Error?r.stack:String(r),erroredAt:new Date,errorId:e,version:c.version},where:{id:t.id}})}}static async inhaleMail(t){let r=await i.mail.create({data:{raw:t}});await this.handleMail(r)}static async parseMail(t,r=c.baseMail){let e=await T(t,{skipHtmlToText:!0,skipImageLinks:!0,skipTextLinks:!0,skipTextToHtml:!0});if(!e.from?.value[0].address?.endsWith("toogoodtogo.com"))throw new Error("Not a TGTG email!");let a=(Array.isArray(e.to)?e.to:[e.to]).map(o=>o?.value).flat().filter(o=>!!o).map(o=>o.address).find(o=>o?.endsWith(r));if(!a){let o=e.headers.get("received"),d=new RegExp(`([\\w-]+${c.baseMail})`,"i");Array.isArray(o)&&o.forEach(s=>{let m=(typeof s=="string"?s:s.value).match(d);m&&(a=m[1])})}if(!a)throw new Error("No recipient found!");if(e.headers.get("x-pm-tag")==="consumer_order_confirm"){let o=this.parseOrderMail(e);return{to:a,...o}}if(e.headers.get("x-pm-tag")==="collection_time_changed"){let o=this.parseChangeMail(e);return{to:a,...o}}if(e.headers.get("x-pm-tag")==="consumer_order_reverted"){let o=this.parseCancellationMail(e);if(o)return{to:a,...o}}if(e.headers.get("x-pm-tag")==="invoice"){let o=this.parseInvoiceMail(e);if(o)return{to:a,...o}}if(!(!e.headers.get("x-pm-tag")&&!e.headers.get("x-pm-message-id")))throw e.headers.get("x-pm-tag")?new Error(`Unsupported email type: ${e.headers.get("x-pm-tag")}`):new Error("Not implemented!")}static async runCleanup(){let t=await i.mail.findMany({orderBy:{erroredAt:"asc"},take:10,where:{OR:[{error:null},{version:null},{version:{not:c.version}}]}});for(let r of t)await this.handleMail(r);await i.user.deleteMany({where:{OR:[{lastSeenAt:{lt:n().subtract(8,"weeks").toDate()}},{createdAt:{lt:n().subtract(3,"hours").toDate()},lastSeenAt:{equals:i.user.fields.createdAt}}]}}),await i.event.deleteMany({where:{to:{lt:n().subtract(4,"weeks").toDate()}}}),await i.mail.deleteMany({where:{createdAt:{lt:n().subtract(2,"weeks").toDate()}}})}static async applyParsedMail(t){let r=await this.findUser(t.to);if(t.type==="order"){let e=await this.getLocation(t.location);await i.event.upsert({create:{amount:t.amount,from:t.time.from.toDate(),location:{connect:{id:e.id}},orderedAt:t.time.order.toDate(),orderId:t.orderId,price:t.price,to:t.time.to.toDate(),user:{connect:{id:r.id}}},update:{amount:t.amount,from:t.time.from.toDate(),location:{connect:{id:e.id}},price:t.price,to:t.time.to.toDate()},where:{orderId:t.orderId,userId:r.id}})}else if(t.type==="change")await i.event.update({data:{from:t.time.from.toDate(),to:t.time.to.toDate()},where:{orderId:t.orderId,userId:r.id}});else if(t.type==="cancel")await i.event.update({data:{canceledAt:t.cancelledAt.toDate()},where:{orderId:t.orderId,userId:r.id}});else if(t.type==="invoice")await i.event.update({data:{invoicedAt:t.invoicedAt.toDate()},where:{orderId:t.orderId,userId:r.id}});else throw new Error("Unknown email type!")}static async findUser(t){if(!t)throw new Error("Did not found a valid recipient!");let r=t.split("@")[0],e=await i.user.findUnique({where:{prefix:r}});if(!e)throw new Error(`User with email prefix ${r} not found!`);return e}static async getLocation(t){let r=await i.location.findFirst({where:{address:t.address,name:t.name}});if(!r){let e=w(t.name),{latitude:a,longitude:o}=await this.geocode(t.address);r=await i.location.create({data:{address:t.address,emoji:e,latitude:a,longitude:o,name:t.name}})}return r}static parseCancellationMail(t){let e=(t.subject||"").match(/\((\w+)\)/);if(e)return{cancelledAt:n(t.date),orderId:e[1],type:"cancel"}}static parseChangeMail(t){let r=[(t.html||"").match(/https:\/\/share.toogoodtogo.com\/receipts\/details\/(\w+)/),(t.html||"").match(/(\d{1,2}\.\d{2}\.\d{2}) zwischen (\d{1,2}:\d{2}) und (\d{1,2}:\d{2})(?: Uhr)? (\w+)+ \(/)];if(!r[0])throw new Error("Order ID not found!");if(!r[1])throw new Error("Date / Time not found!");let e=r[1][4];e==="MEZ"&&(e="MET");let a=n.tz(r[1][1]+" "+r[1][2],"DD.MM.YY HH:mm",e),o=n.tz(r[1][1]+" "+r[1][3],"DD.MM.YY HH:mm",e);return{orderId:r[0][1].trim(),time:{from:a,to:o},type:"change"}}static parseInvoiceMail(t){let e=(t.html||"").match(/Die Rechnung für deine Bestellung (\w+)/);if(e)return{invoicedAt:n(t.date),orderId:e[1],type:"invoice"};throw new Error("Order Id not found!")}static parseOrderMail(t){let r=t.html||"",e=[r.match(/\/order\/([^/]+)\//),r.match(/Wir bestätigen hiermit deine Bestellung bei ([^(.<]+)/),r.match(/<span>Du kannst deine Bestellung am (\d{1,2}\.\d{2}\.\d{2}) zwischen (\d{1,2}:\d{2}) und (\d{1,2}:\d{2}) Uhr (\w+)[^:]+: (.+).<\/span><\/div>/),r.match(/<span>Du kannst deine Bestellung zwischen (\d{1,2}\.\d{1,2}), (\d{1,2}:\d{2}) und (\d{1,2}:\d{2})[^:]+: (.+).<\/span><\/div>/),r.match(/<b>Datum:<\/b>\s+<span>(\d{1,2}\.\d{2}\.\d{2})<\/span>/),r.match(/Abholzeit:<\/b>\s+<span>(\d{1,2}:\d{2}) - (\d{1,2}:\d{2}) (\w+)/),r.match(/Anzahl: (\d+)/),r.match(/Anzahl:<\/b>\s+<span>(\d+)/),r.match(/Gesamtpreis: ([\d,.]+)[^\d,.]/),r.match(/Gesamtpreis:<\/b>\s+<span>([\d,.]+)[^\d,.]/),r.match(/Standort:<\/b>\s+<span>([^<]+)/)];if(!e[0])throw new Error("Order ID not found!");if(!e[1])throw new Error("Location name not found!");if(!e[2]&&!e[3]&&!(e[4]&&e[5]))throw new Error("Date, time and address not found (1)!");let a="MET";e[2]&&(a=e[2][4]),e[5]&&(a=e[5][3]),a==="MEZ"&&(a="MET");let o=n(t.date),d,s,p;if(e[2]&&(d=n.tz(e[2][1]+" "+e[2][2],"DD.MM.YY HH:mm",a),s=n.tz(e[2][1]+" "+e[2][3],"DD.MM.YY HH:mm",a),p=e[2][5].trim()),e[3]){let y=o.year();d=n.tz(e[3][1]+"."+y+" "+e[3][2],"DD.MM.YYYY HH:mm",a),s=n.tz(e[3][1]+"."+y+" "+e[3][3],"DD.MM.YYYY HH:mm",a),p=e[3][4].trim(),d.isBefore(o)&&d.add(1,"year"),s.isBefore(d)&&s.add(1,"year")}if(e[4]&&e[5]&&e[10]&&(d=n.tz(e[4][1]+" "+e[5][1],"DD.MM.YY HH:mm",a),s=n.tz(e[4][1]+" "+e[5][2],"DD.MM.YY HH:mm",a),p=e[10][1].trim()),!d||!s||!p)throw new Error("Date, time or address not found (2)!");let m=0;if(e[6])m=parseInt(e[6][1],10);else if(e[7])m=parseInt(e[7][1],10);else throw new Error("Amount not found!");if(isNaN(m))throw new Error("Amount (1) is not a number!");let f=0;if(e[8])f=parseInt(e[8][1].replace(/[.|,]/g,""));else if(e[9])f=parseInt(e[9][1].replace(/[.|,]/g,""));else throw new Error("Price not found!");if(isNaN(f))throw new Error("Price is not a number!");let h=E.decode(e[1][1].trim());return h.endsWith("!")&&(h=h.slice(0,-1)),{amount:m,location:{address:E.decode(p),name:h},orderId:e[0][1].trim(),price:f,time:{from:d,order:o,to:s},type:"order"}}};export{c as a,i as b,O as c,M as d};
2
+ //# sourceMappingURL=chunk-HNGICWTV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/config.ts","../src/lib/db.ts","../src/lib/parser.ts","../src/lib/emoji.ts"],"sourcesContent":["import { init } from '@sentry/node';\nimport { readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\ninit({\n dsn:\n process.env.SENTRY_DSN ||\n 'https://5e4630d58e5f4c778ce22140c53b3684@glitch.sebbo.net/9',\n});\n\nlet dir = dirname(fileURLToPath(import.meta.url));\nif (dir.endsWith('/dist')) {\n dir = resolve(dir, '..', 'src');\n} else {\n dir = resolve(dir, '..');\n}\n\nfunction src(path?: string) {\n return resolve(dir, path || '');\n}\n\nlet version: string | undefined;\ntry {\n const pkg = JSON.parse(readFileSync(src('../package.json'), 'utf8'));\n version = pkg.version;\n} catch (error) {\n console.log('Failed getting tgtg-ical version:');\n console.error(error);\n}\n\nexport default {\n baseMail: process.env.BASE_MAIL || '@tgtg-ical.sebbo.net',\n baseUrl: process.env.BASE_URL || 'https://tgtg-ical.sebbo.net',\n src,\n version,\n};\n","import { PrismaClient } from '@prisma/client';\n\nconst prisma = new PrismaClient();\nexport default prisma;\n","import type { Location, Mail, User } from '@prisma/client';\n\nimport { captureException } from '@sentry/node';\nimport he from 'he';\nimport { type ParsedMail, simpleParser } from 'mailparser';\nimport moment from 'moment-timezone';\n\nimport config from './config.js';\nimport Config from './config.js';\nimport prisma from './db.js';\nimport getEmoji from './emoji.js';\n\ntype AnyMail = (CancellationMail | ChangeMail | InvoiceMail | OrderMail) & {\n to: string;\n};\n\ninterface CancellationMail {\n cancelledAt: moment.Moment;\n orderId: string;\n type: 'cancel';\n}\n\ninterface ChangeMail {\n orderId: string;\n time: {\n from: moment.Moment;\n to: moment.Moment;\n };\n type: 'change';\n}\n\ninterface InvoiceMail {\n invoicedAt: moment.Moment;\n orderId: string;\n type: 'invoice';\n}\n\ninterface OrderMail {\n amount: number;\n location: {\n address: string;\n name: string;\n };\n orderId: string;\n price: number;\n time: {\n from: moment.Moment;\n order: moment.Moment;\n to: moment.Moment;\n };\n type: 'order';\n}\n\nexport default class Parser {\n public static async geocode(\n address: string,\n ): Promise<\n | { latitude: null; longitude: null }\n | { latitude: number; longitude: number }\n > {\n const response = await fetch(\n 'https://nominatim.openstreetmap.org/search?format=json&limit=1&q=' +\n encodeURIComponent(address),\n {\n headers: {\n Referer: config.baseUrl,\n 'User-Agent': `tgtg-ical/${config.version} (${config.baseUrl})`,\n },\n },\n );\n if (!response.ok) {\n throw new Error('Geocoding failed: ' + response.statusText);\n }\n\n // super simple way to ensure a maximum of 1 request per second in cronjobs\n await new Promise((resolve) => setTimeout(resolve, 1000));\n\n const data = await response.json();\n if (!Array.isArray(data) || data.length === 0) {\n return {\n latitude: null,\n longitude: null,\n };\n }\n\n return {\n latitude: parseFloat(data[0].lat),\n longitude: parseFloat(data[0].lon),\n };\n }\n\n public static async handleMail(mail: Mail) {\n try {\n const parsed = await this.parseMail(mail.raw);\n if (parsed) {\n await this.applyParsedMail(parsed);\n }\n\n await prisma.mail.delete({\n where: {\n id: mail.id,\n },\n });\n } catch (error) {\n const errorId = captureException(error);\n await prisma.mail.update({\n data: {\n error: error instanceof Error ? error.stack : String(error),\n erroredAt: new Date(),\n errorId,\n version: config.version,\n },\n where: {\n id: mail.id,\n },\n });\n }\n }\n\n public static async inhaleMail(email: string): Promise<void> {\n const mail = await prisma.mail.create({\n data: {\n raw: email,\n },\n });\n\n await this.handleMail(mail);\n }\n\n public static async parseMail(\n mail: string,\n baseMailPostfix = config.baseMail,\n ): Promise<AnyMail | undefined> {\n // parse email\n const email = await simpleParser(mail, {\n skipHtmlToText: true,\n skipImageLinks: true,\n skipTextLinks: true,\n skipTextToHtml: true,\n });\n\n if (!email.from?.value[0].address?.endsWith('toogoodtogo.com')) {\n throw new Error('Not a TGTG email!');\n }\n\n let to: string | undefined = (\n Array.isArray(email.to) ? email.to : [email.to]\n )\n .map((to) => to?.value)\n .flat()\n .filter((address) => !!address)\n .map((address) => address.address)\n .find((address) => address?.endsWith(baseMailPostfix));\n\n if (!to) {\n const received = email.headers.get('received');\n const regexp = new RegExp(`([\\\\w-]+${Config.baseMail})`, 'i');\n if (Array.isArray(received)) {\n received.forEach((r) => {\n const s = typeof r === 'string' ? r : r.value;\n const match = s.match(regexp);\n if (match) {\n to = match[1];\n }\n });\n }\n }\n if (!to) {\n throw new Error('No recipient found!');\n }\n\n // Order confirmation\n if (email.headers.get('x-pm-tag') === 'consumer_order_confirm') {\n const order = this.parseOrderMail(email);\n return {\n to,\n ...order,\n };\n }\n\n // Time Changed\n if (email.headers.get('x-pm-tag') === 'collection_time_changed') {\n const change = this.parseChangeMail(email);\n return {\n to,\n ...change,\n };\n }\n\n // Cancellation\n if (email.headers.get('x-pm-tag') === 'consumer_order_reverted') {\n const cancellation = this.parseCancellationMail(email);\n if (cancellation) {\n return {\n to,\n ...cancellation,\n };\n }\n }\n\n // is invoice?\n if (email.headers.get('x-pm-tag') === 'invoice') {\n const invoice = this.parseInvoiceMail(email);\n if (invoice) {\n return {\n to,\n ...invoice,\n };\n }\n }\n\n // Non-transactional email\n if (\n !email.headers.get('x-pm-tag') &&\n !email.headers.get('x-pm-message-id')\n ) {\n return undefined;\n }\n\n // Unsupported email\n if (email.headers.get('x-pm-tag')) {\n throw new Error(\n `Unsupported email type: ${email.headers.get('x-pm-tag')}`,\n );\n }\n\n throw new Error('Not implemented!');\n }\n\n public static async runCleanup(): Promise<void> {\n const mails = await prisma.mail.findMany({\n orderBy: {\n erroredAt: 'asc',\n },\n take: 10,\n where: {\n OR: [\n { error: null },\n { version: null },\n { version: { not: config.version } },\n ],\n },\n });\n\n for (const mail of mails) {\n await this.handleMail(mail);\n }\n\n // Cleanup Users\n await prisma.user.deleteMany({\n where: {\n OR: [\n {\n lastSeenAt: {\n lt: moment().subtract(8, 'weeks').toDate(),\n },\n },\n {\n createdAt: {\n lt: moment().subtract(3, 'hours').toDate(),\n },\n lastSeenAt: {\n equals: prisma.user.fields.createdAt,\n },\n },\n ],\n },\n });\n\n // Cleanup Events\n await prisma.event.deleteMany({\n where: {\n to: {\n lt: moment().subtract(4, 'weeks').toDate(),\n },\n },\n });\n\n // Cleanup Mails\n await prisma.mail.deleteMany({\n where: {\n createdAt: {\n lt: moment().subtract(2, 'weeks').toDate(),\n },\n },\n });\n }\n\n private static async applyParsedMail(email: AnyMail): Promise<void> {\n const user = await this.findUser(email.to);\n\n if (email.type === 'order') {\n const location = await this.getLocation(email.location);\n\n await prisma.event.upsert({\n create: {\n amount: email.amount,\n from: email.time.from.toDate(),\n location: {\n connect: {\n id: location.id,\n },\n },\n orderedAt: email.time.order.toDate(),\n orderId: email.orderId,\n price: email.price,\n to: email.time.to.toDate(),\n user: {\n connect: {\n id: user.id,\n },\n },\n },\n update: {\n amount: email.amount,\n from: email.time.from.toDate(),\n location: {\n connect: {\n id: location.id,\n },\n },\n price: email.price,\n to: email.time.to.toDate(),\n },\n where: {\n orderId: email.orderId,\n userId: user.id,\n },\n });\n } else if (email.type === 'change') {\n await prisma.event.update({\n data: {\n from: email.time.from.toDate(),\n to: email.time.to.toDate(),\n },\n where: {\n orderId: email.orderId,\n userId: user.id,\n },\n });\n } else if (email.type === 'cancel') {\n await prisma.event.update({\n data: {\n canceledAt: email.cancelledAt.toDate(),\n },\n where: {\n orderId: email.orderId,\n userId: user.id,\n },\n });\n } else if (email.type === 'invoice') {\n await prisma.event.update({\n data: {\n invoicedAt: email.invoicedAt.toDate(),\n },\n where: {\n orderId: email.orderId,\n userId: user.id,\n },\n });\n } else {\n throw new Error('Unknown email type!');\n }\n }\n\n private static async findUser(to: string): Promise<User> {\n if (!to) {\n throw new Error('Did not found a valid recipient!');\n }\n\n const prefix = to.split('@')[0];\n const user = await prisma.user.findUnique({\n where: { prefix },\n });\n if (!user) {\n throw new Error(`User with email prefix ${prefix} not found!`);\n }\n\n return user;\n }\n\n private static async getLocation(\n input: OrderMail['location'],\n ): Promise<Location> {\n let location = await prisma.location.findFirst({\n where: {\n address: input.address,\n name: input.name,\n },\n });\n if (!location) {\n const emoji = getEmoji(input.name);\n const { latitude, longitude } = await this.geocode(input.address);\n location = await prisma.location.create({\n data: {\n address: input.address,\n emoji,\n latitude,\n longitude,\n name: input.name,\n },\n });\n }\n\n return location;\n }\n\n private static parseCancellationMail(\n email: ParsedMail,\n ): CancellationMail | undefined {\n const subject = email.subject || '';\n const match = subject.match(/\\((\\w+)\\)/);\n if (match) {\n return {\n cancelledAt: moment(email.date),\n orderId: match[1],\n type: 'cancel',\n };\n }\n }\n\n private static parseChangeMail(email: ParsedMail): ChangeMail {\n const matches = [\n (email.html || '').match(\n /https:\\/\\/share.toogoodtogo.com\\/receipts\\/details\\/(\\w+)/,\n ),\n (email.html || '').match(\n /(\\d{1,2}\\.\\d{2}\\.\\d{2}) zwischen (\\d{1,2}:\\d{2}) und (\\d{1,2}:\\d{2})(?: Uhr)? (\\w+)+ \\(/,\n ),\n ];\n if (!matches[0]) {\n throw new Error('Order ID not found!');\n }\n if (!matches[1]) {\n throw new Error('Date / Time not found!');\n }\n\n let timezone = matches[1][4];\n if (timezone === 'MEZ') {\n timezone = 'MET';\n }\n\n const from = moment.tz(\n matches[1][1] + ' ' + matches[1][2],\n 'DD.MM.YY HH:mm',\n timezone,\n );\n const to = moment.tz(\n matches[1][1] + ' ' + matches[1][3],\n 'DD.MM.YY HH:mm',\n timezone,\n );\n\n return {\n orderId: matches[0][1].trim(),\n time: {\n from,\n to,\n },\n type: 'change',\n };\n }\n\n private static parseInvoiceMail(email: ParsedMail): InvoiceMail {\n const html = email.html || '';\n const match = html.match(/Die Rechnung für deine Bestellung (\\w+)/);\n if (match) {\n return {\n invoicedAt: moment(email.date),\n orderId: match[1],\n type: 'invoice',\n };\n }\n\n throw new Error('Order Id not found!');\n }\n\n private static parseOrderMail(email: ParsedMail): OrderMail {\n const html = email.html || '';\n const matches = [\n html.match(/\\/order\\/([^/]+)\\//),\n html.match(/Wir bestätigen hiermit deine Bestellung bei ([^(.<]+)/),\n html.match(\n /<span>Du kannst deine Bestellung am (\\d{1,2}\\.\\d{2}\\.\\d{2}) zwischen (\\d{1,2}:\\d{2}) und (\\d{1,2}:\\d{2}) Uhr (\\w+)[^:]+: (.+).<\\/span><\\/div>/,\n ),\n html.match(\n /<span>Du kannst deine Bestellung zwischen (\\d{1,2}\\.\\d{1,2}), (\\d{1,2}:\\d{2}) und (\\d{1,2}:\\d{2})[^:]+: (.+).<\\/span><\\/div>/,\n ),\n html.match(\n /<b>Datum:<\\/b>\\s+<span>(\\d{1,2}\\.\\d{2}\\.\\d{2})<\\/span>/,\n ),\n html.match(\n /Abholzeit:<\\/b>\\s+<span>(\\d{1,2}:\\d{2}) - (\\d{1,2}:\\d{2}) (\\w+)/,\n ),\n html.match(/Anzahl: (\\d+)/),\n html.match(/Anzahl:<\\/b>\\s+<span>(\\d+)/),\n html.match(/Gesamtpreis: ([\\d,.]+)[^\\d,.]/),\n html.match(/Gesamtpreis:<\\/b>\\s+<span>([\\d,.]+)[^\\d,.]/),\n html.match(/Standort:<\\/b>\\s+<span>([^<]+)/),\n ];\n if (!matches[0]) {\n throw new Error('Order ID not found!');\n }\n if (!matches[1]) {\n throw new Error('Location name not found!');\n }\n if (!matches[2] && !matches[3] && !(matches[4] && matches[5])) {\n throw new Error('Date, time and address not found (1)!');\n }\n\n let timezone = 'MET';\n if (matches[2]) {\n timezone = matches[2][4];\n }\n if (matches[5]) {\n timezone = matches[5][3];\n }\n if (timezone === 'MEZ') {\n timezone = 'MET';\n }\n\n const now = moment(email.date);\n let from: moment.Moment | undefined;\n let to: moment.Moment | undefined;\n let address: string | undefined;\n\n if (matches[2]) {\n from = moment.tz(\n matches[2][1] + ' ' + matches[2][2],\n 'DD.MM.YY HH:mm',\n timezone,\n );\n to = moment.tz(\n matches[2][1] + ' ' + matches[2][3],\n 'DD.MM.YY HH:mm',\n timezone,\n );\n address = matches[2][5].trim();\n }\n if (matches[3]) {\n const year = now.year();\n from = moment.tz(\n matches[3][1] + '.' + year + ' ' + matches[3][2],\n 'DD.MM.YYYY HH:mm',\n timezone,\n );\n to = moment.tz(\n matches[3][1] + '.' + year + ' ' + matches[3][3],\n 'DD.MM.YYYY HH:mm',\n timezone,\n );\n address = matches[3][4].trim();\n\n if (from.isBefore(now)) {\n from.add(1, 'year');\n }\n if (to.isBefore(from)) {\n to.add(1, 'year');\n }\n }\n if (matches[4] && matches[5] && matches[10]) {\n from = moment.tz(\n matches[4][1] + ' ' + matches[5][1],\n 'DD.MM.YY HH:mm',\n timezone,\n );\n to = moment.tz(\n matches[4][1] + ' ' + matches[5][2],\n 'DD.MM.YY HH:mm',\n timezone,\n );\n address = matches[10][1].trim();\n }\n\n if (!from || !to || !address) {\n throw new Error('Date, time or address not found (2)!');\n }\n\n let amount = 0;\n if (matches[6]) {\n amount = parseInt(matches[6][1], 10);\n } else if (matches[7]) {\n amount = parseInt(matches[7][1], 10);\n } else {\n throw new Error('Amount not found!');\n }\n if (isNaN(amount)) {\n throw new Error('Amount (1) is not a number!');\n }\n\n let price = 0;\n if (matches[8]) {\n price = parseInt(matches[8][1].replace(/[.|,]/g, ''));\n } else if (matches[9]) {\n price = parseInt(matches[9][1].replace(/[.|,]/g, ''));\n } else {\n throw new Error('Price not found!');\n }\n if (isNaN(price)) {\n throw new Error('Price is not a number!');\n }\n\n let name = he.decode(matches[1][1].trim());\n if (name.endsWith('!')) {\n name = name.slice(0, -1);\n }\n\n return {\n amount,\n location: {\n address: he.decode(address),\n name,\n },\n orderId: matches[0][1].trim(),\n price,\n time: {\n from,\n order: now,\n to,\n },\n type: 'order',\n };\n }\n}\n","export const DEFAULT_EMOJI = '🍴';\nexport const EMOJIS: Record<string, RegExp[]> = {\n '🌭': [/Ikea/],\n '🌮': [/Enchilada/, /Besitos/],\n '🍔': [\n /Burger/,\n /McDonald[‘|']?s/,\n /Burger King/,\n /Hans im Glück/,\n /Peter Pane/,\n ],\n '🍕': [\n /Domino‘s/,\n /Pizza Hut/,\n /L[‘|']Osteria/,\n /Call a Pizza/,\n /Smiley[‘|']s Pizza/,\n ],\n '🍗': [/KFC/],\n '🍝': [/LaTagliatella/, /Vapiano/],\n '🍤': [/Nordsee/],\n '🍩': [/Donut/, /Dunkin/],\n '🍪': [/Starbucks/],\n '🥐': [/LeCroBag/],\n '🥖': [\n /Bäckerei/,\n /Back/,\n /BackWerk/,\n /Kamps/,\n /Kamps Backstuben/,\n /Junge Die Bäckerei/,\n /Back-Factory/,\n ],\n '🥗': [/dean ?& ?david/],\n '🥨': [/Brezel/, /Ditsch/],\n '🥩': [/Steakhouse/, /Block House/, /Jim Block/],\n '🥪': [\n /Sandwich/,\n /Tank ?& ?Rast/,\n /T&R Raststätten/,\n /Autohöfe/,\n /Aral/,\n /Shell/,\n /Jet Tank/,\n /Subway/,\n /Caf[é|e] bonjour/,\n /Total Deutschland/,\n /Esso:? Snack ?& ?Shop/,\n ],\n '🛒': [\n /Edeka/,\n /Netto/,\n /Rewe/,\n /Penny/,\n /Lidl/,\n /Kaufland/,\n /Aldi/,\n /dm/,\n /Rossmann/,\n /Globus/,\n /Metro/,\n /Norma/,\n /Tegut/,\n ],\n};\n\nexport default function getEmoji(location: string): null | string {\n for (const emoji in EMOJIS) {\n const regExps = EMOJIS[emoji].map((name) => new RegExp(name, 'i'));\n const match = !!regExps.find((regExp) => regExp.test(location));\n if (match) {\n return emoji;\n }\n }\n\n return null;\n}\n"],"mappings":"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","names":["init","readFileSync","dirname","resolve","fileURLToPath","dir","src","path","version","error","config_default","PrismaClient","prisma","db_default","captureException","he","simpleParser","moment","DEFAULT_EMOJI","EMOJIS","getEmoji","location","emoji","name","regExp","Parser","address","response","config_default","resolve","data","mail","parsed","db_default","error","errorId","captureException","email","baseMailPostfix","simpleParser","to","received","regexp","r","match","order","change","cancellation","invoice","mails","moment","user","location","prefix","input","emoji","getEmoji","latitude","longitude","matches","timezone","from","html","now","year","amount","price","name","he"]}
package/dist/cleanup.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{d as r}from"./chunk-2HK3PBFJ.js";r.runCleanup().catch(e=>{console.error(e),process.exit(1)});
2
+ import{d as r}from"./chunk-HNGICWTV.js";r.runCleanup().catch(e=>{console.error(e),process.exit(1)});
3
3
  //# sourceMappingURL=cleanup.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bin/cleanup.ts"],"sourcesContent":["#!/usr/bin/env node\n'use strict';\n\n/* istanbul ignore file */\nimport Parser from '../lib/parser.js';\n\nParser.runCleanup()\n .catch(error => {\n console.error(error);\n process.exit(1);\n });\n"],"mappings":";wCAMAA,EAAO,WAAW,EACb,MAAMC,GAAS,CACZ,QAAQ,MAAMA,CAAK,EACnB,QAAQ,KAAK,CAAC,CAClB,CAAC","names":["Parser","error"]}
1
+ {"version":3,"sources":["../src/bin/cleanup.ts"],"sourcesContent":["#!/usr/bin/env node\n'use strict';\n\n/* istanbul ignore file */\nimport Parser from '../lib/parser.js';\n\nParser.runCleanup().catch((error) => {\n console.error(error);\n process.exit(1);\n});\n"],"mappings":";wCAMAA,EAAO,WAAW,EAAE,MAAOC,GAAU,CACjC,QAAQ,MAAMA,CAAK,EACnB,QAAQ,KAAK,CAAC,CAClB,CAAC","names":["Parser","error"]}
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{d as r}from"./chunk-2HK3PBFJ.js";var s="";process.stdin.on("data",e=>{s+=e});process.stdin.on("end",()=>{r.inhaleMail(s).catch(e=>{console.error(e),process.exit(1)})});
2
+ import{d as r}from"./chunk-HNGICWTV.js";var s="";process.stdin.on("data",e=>{s+=e});process.stdin.on("end",()=>{r.inhaleMail(s).catch(e=>{console.error(e),process.exit(1)})});
3
3
  //# sourceMappingURL=inhale-mail.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bin/inhale-mail.ts"],"sourcesContent":["#!/usr/bin/env node\n'use strict';\n\n/* istanbul ignore file */\nimport Parser from '../lib/parser.js';\n\nlet buffer = '';\nprocess.stdin.on('data', (chunk) => {\n buffer += chunk;\n});\nprocess.stdin.on('end', () => {\n Parser.inhaleMail(buffer)\n .catch(error => {\n console.error(error);\n process.exit(1);\n });\n});\n"],"mappings":";wCAMA,IAAIA,EAAS,GACb,QAAQ,MAAM,GAAG,OAASC,GAAU,CAChCD,GAAUC,CACd,CAAC,EACD,QAAQ,MAAM,GAAG,MAAO,IAAM,CAC1BC,EAAO,WAAWF,CAAM,EACnB,MAAMG,GAAS,CACZ,QAAQ,MAAMA,CAAK,EACnB,QAAQ,KAAK,CAAC,CAClB,CAAC,CACT,CAAC","names":["buffer","chunk","Parser","error"]}
1
+ {"version":3,"sources":["../src/bin/inhale-mail.ts"],"sourcesContent":["#!/usr/bin/env node\n'use strict';\n\n/* istanbul ignore file */\nimport Parser from '../lib/parser.js';\n\nlet buffer = '';\nprocess.stdin.on('data', (chunk) => {\n buffer += chunk;\n});\nprocess.stdin.on('end', () => {\n Parser.inhaleMail(buffer).catch((error) => {\n console.error(error);\n process.exit(1);\n });\n});\n"],"mappings":";wCAMA,IAAIA,EAAS,GACb,QAAQ,MAAM,GAAG,OAASC,GAAU,CAChCD,GAAUC,CACd,CAAC,EACD,QAAQ,MAAM,GAAG,MAAO,IAAM,CAC1BC,EAAO,WAAWF,CAAM,EAAE,MAAOG,GAAU,CACvC,QAAQ,MAAMA,CAAK,EACnB,QAAQ,KAAK,CAAC,CAClB,CAAC,CACL,CAAC","names":["buffer","chunk","Parser","error"]}
package/dist/start.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import{a as o,b as i,c as u,d}from"./chunk-2HK3PBFJ.js";import m from"express";import C from"cookie-parser";import{randomUUID as f}from"node:crypto";import{generateName as g,generateNameWithNumber as w}from"@criblinc/docker-names";import"@prisma/client";import{ICalAlarmType as I,ICalCalendar as E,ICalEventStatus as p}from"ical-generator";import y from"moment-timezone";import{readFile as U}from"fs/promises";var s=class{static async createUser(){let e;for(let t=0;;t++){let r=this.generatePrefix(t);try{if(e=await o.user.create({data:{prefix:r}}),e)break}catch(a){if(a&&typeof a=="object"&&"code"in a&&a.code==="P2002")continue;throw a}}if(!e)throw new Error("User not created");return e}static generatePrefix(e=0){return e>100?f():e<10?g():w()}static async getUser(e){let t=await o.user.findUniqueOrThrow({where:{id:e}});return this.updateUserLastSeen(t.id),t}static updateUserLastSeen(e){o.user.update({where:{id:e},data:{lastSeenAt:new Date}}).catch(t=>{console.log(t)})}static async generateUserPage(e){let[t,r]=await Promise.all([this.getUser(e),U(i.src("templates/user.html"),"utf-8")]);return r.replace(/\${CALENDAR_URL}/g,`${i.baseUrl}/${t.id}/calendar.ical`).replace(/\${EMAIL_ADDRESS}/g,`${t.prefix}${i.baseMail}`)}static async generateCalendar(e){let t=await o.user.findUniqueOrThrow({where:{id:e},select:{id:!0,event:{select:{id:!0,orderId:!0,from:!0,to:!0,amount:!0,price:!0,location:{select:{name:!0,address:!0,emoji:!0,latitude:!0,longitude:!0}},createdAt:!0,canceledAt:!0}}}}),r=new E({name:"TGTG",ttl:60*60,events:t.event.map(a=>{let h=new Intl.NumberFormat("de-DE",{style:"currency",currency:"EUR"}).format(a.price/100),l=p.CONFIRMED;return a.canceledAt&&(l=p.CANCELLED),{id:a.id,start:a.from,end:a.to,timestamp:a.createdAt,summary:`${a.location.emoji||u} ${a.location.name}`,description:`${a.amount}x
3
- ${h}`,url:`https://share.toogoodtogo.com/receipts/details/${a.orderId}`,status:l,created:a.createdAt,location:{title:a.location.name,address:a.location.address,geo:a.location.latitude&&a.location.longitude?{lat:a.location.latitude,lon:a.location.longitude}:void 0},alarms:[{type:I.display,trigger:600}]}})});return this.updateUserLastSeen(e),r.toString()}static async isHealthy(){let e=await o.mail.count({where:{createdAt:{lt:y().subtract(30,"minutes").toDate()}}});if(e>0)throw new Error(`There are ${e} unhandled mails in the queue!`)}};var n=class c{static run(){new c}app;server;constructor(){this.app=m(),this.app.use(C()),this.setupRoutes(),this.server=this.app.listen(process.env.PORT||8080),console.log(`tgtg-ical v${i.version} listening on port ${process.env.PORT||8080}`),process.on("SIGINT",()=>this.stop()),process.on("SIGTERM",()=>this.stop()),d.runCleanup().then(()=>console.log("Initial cleanup succeeded.")).catch(e=>{console.log("Initial cleanup failed:"),console.error(e),process.exit(1)})}setupRoutes(){this.app.get("/ping",(e,t)=>{t.send("pong")}),this.app.get("/",(e,t)=>{if("userId"in e.cookies&&e.cookies.userId){t.redirect("/"+e.cookies.userId);return}s.createUser().then(r=>{t.cookie("userId",r.id),t.redirect("/"+r.id)}).catch(r=>this.handleError(r,t))}),this.app.get("/_health",(e,t)=>{s.isHealthy().then(()=>t.sendStatus(204)).catch(r=>this.handleError(r,t))}),this.app.use(m.static(i.src("./assets"))),this.app.get("/:userId",(e,t)=>{t.format({"text/html":()=>{s.generateUserPage(e.params.userId).then(r=>{t.cookie("userId",e.params.userId),t.send(r)}).catch(r=>this.handleError(r,t))},"application/json":()=>{s.getUser(e.params.userId).then(r=>t.send(r)).catch(r=>this.handleError(r,t))}})}),this.app.get("/:userId/calendar.ical",(e,t)=>{s.generateCalendar(e.params.userId).then(r=>{t.set("Content-Type","text/calendar"),t.send(r)}).catch(r=>this.handleError(r,t))})}handleError(e,t){if(e&&typeof e=="object"&&"code"in e&&e.code==="P2025"){t.sendStatus(404);return}console.log(e),t.sendStatus(500)}async stop(){await new Promise(e=>this.server.close(e)),await o.$disconnect(),process.exit()}};n.run();
2
+ import{a as i,b as o,c as u,d}from"./chunk-HNGICWTV.js";import C from"cookie-parser";import m from"express";import{generateName as f,generateNameWithNumber as g}from"@criblinc/docker-names";import"@prisma/client";import{readFile as w}from"fs/promises";import{ICalAlarmType as I,ICalCalendar as E,ICalEventStatus as p}from"ical-generator";import y from"moment-timezone";import{randomUUID as U}from"node:crypto";var s=class{static async createUser(){let e;for(let t=0;;t++){let r=this.generatePrefix(t);try{if(e=await o.user.create({data:{prefix:r}}),e)break}catch(a){if(a&&typeof a=="object"&&"code"in a&&a.code==="P2002")continue;throw a}}if(!e)throw new Error("User not created");return e}static async generateCalendar(e){let t=await o.user.findUniqueOrThrow({select:{event:{select:{amount:!0,canceledAt:!0,createdAt:!0,from:!0,id:!0,location:{select:{address:!0,emoji:!0,latitude:!0,longitude:!0,name:!0}},orderId:!0,price:!0,to:!0}},id:!0},where:{id:e}}),r=new E({events:t.event.map(a=>{let h=new Intl.NumberFormat("de-DE",{currency:"EUR",style:"currency"}).format(a.price/100),l=p.CONFIRMED;return a.canceledAt&&(l=p.CANCELLED),{alarms:[{trigger:600,type:I.display}],created:a.createdAt,description:`${a.amount}x
3
+ ${h}`,end:a.to,id:a.id,location:{address:a.location.address,geo:a.location.latitude&&a.location.longitude?{lat:a.location.latitude,lon:a.location.longitude}:void 0,title:a.location.name},start:a.from,status:l,summary:`${a.location.emoji||u} ${a.location.name}`,timestamp:a.createdAt,url:`https://share.toogoodtogo.com/receipts/details/${a.orderId}`}}),name:"TGTG",ttl:60*60});return this.updateUserLastSeen(e),r.toString()}static generatePrefix(e=0){return e>100?U():e<10?f():g()}static async generateUserPage(e){let[t,r]=await Promise.all([this.getUser(e),w(i.src("templates/user.html"),"utf-8")]);return r.replace(/\${CALENDAR_URL}/g,`${i.baseUrl}/${t.id}/calendar.ical`).replace(/\${EMAIL_ADDRESS}/g,`${t.prefix}${i.baseMail}`)}static async getUser(e){let t=await o.user.findUniqueOrThrow({where:{id:e}});return this.updateUserLastSeen(t.id),t}static async isHealthy(){let e=await o.mail.count({where:{createdAt:{lt:y().subtract(30,"minutes").toDate()}}});if(e>0)throw new Error(`There are ${e} unhandled mails in the queue!`)}static updateUserLastSeen(e){o.user.update({data:{lastSeenAt:new Date},where:{id:e}}).catch(t=>{console.log(t)})}};var n=class c{app;server;constructor(){this.app=m(),this.app.use(C()),this.setupRoutes(),this.server=this.app.listen(process.env.PORT||8080),console.log(`tgtg-ical v${i.version} listening on port ${process.env.PORT||8080}`),process.on("SIGINT",()=>this.stop()),process.on("SIGTERM",()=>this.stop()),d.runCleanup().then(()=>console.log("Initial cleanup succeeded.")).catch(e=>{console.log("Initial cleanup failed:"),console.error(e),process.exit(1)})}static run(){new c}handleError(e,t){if(e&&typeof e=="object"&&"code"in e&&e.code==="P2025"){t.sendStatus(404);return}console.log(e),t.sendStatus(500)}setupRoutes(){this.app.get("/ping",(e,t)=>{t.send("pong")}),this.app.get("/",(e,t)=>{if("userId"in e.cookies&&e.cookies.userId){t.redirect("/"+e.cookies.userId);return}s.createUser().then(r=>{t.cookie("userId",r.id),t.redirect("/"+r.id)}).catch(r=>this.handleError(r,t))}),this.app.get("/_health",(e,t)=>{s.isHealthy().then(()=>t.sendStatus(204)).catch(r=>this.handleError(r,t))}),this.app.use(m.static(i.src("./assets"))),this.app.get("/:userId",(e,t)=>{t.format({"application/json":()=>{s.getUser(e.params.userId).then(r=>t.send(r)).catch(r=>this.handleError(r,t))},"text/html":()=>{s.generateUserPage(e.params.userId).then(r=>{t.cookie("userId",e.params.userId),t.send(r)}).catch(r=>this.handleError(r,t))}})}),this.app.get("/:userId/calendar.ical",(e,t)=>{s.generateCalendar(e.params.userId).then(r=>{t.set("Content-Type","text/calendar"),t.send(r)}).catch(r=>this.handleError(r,t))})}async stop(){await new Promise(e=>this.server.close(e)),await o.$disconnect(),process.exit()}};n.run();
4
4
  //# sourceMappingURL=start.js.map
package/dist/start.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bin/start.ts","../src/lib/server.ts"],"sourcesContent":["#!/usr/bin/env node\n'use strict';\n\nimport express, { type Express, type Response } from 'express';\nimport cookieParser from 'cookie-parser';\nimport {Server} from 'http';\n\nimport prisma from '../lib/db.js';\nimport ServerLib from '../lib/server.js';\nimport Config from '../lib/config.js';\nimport type { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';\nimport Parser from '../lib/parser.js';\n\n\nclass AppServer {\n static run() {\n new AppServer();\n }\n\n private app: Express;\n private server: Server;\n\n constructor() {\n this.app = express();\n this.app.use(cookieParser());\n\n this.setupRoutes();\n this.server = this.app.listen(process.env.PORT || 8080);\n console.log(`tgtg-ical v${Config.version} listening on port ${process.env.PORT || 8080}`);\n\n process.on('SIGINT', () => this.stop());\n process.on('SIGTERM', () => this.stop());\n\n Parser.runCleanup()\n .then(() => console.log('Initial cleanup succeeded.'))\n .catch(error => {\n console.log('Initial cleanup failed:');\n console.error(error);\n process.exit(1);\n });\n }\n\n setupRoutes() {\n this.app.get('/ping', (req, res) => {\n res.send('pong');\n });\n\n this.app.get('/', (req, res) => {\n if('userId' in req.cookies && req.cookies.userId) {\n res.redirect('/' + req.cookies.userId);\n return;\n }\n\n ServerLib.createUser()\n .then(user => {\n res.cookie('userId', user.id);\n res.redirect('/' + user.id);\n })\n .catch(error => this.handleError(error, res));\n });\n\n this.app.get('/_health', (req, res) => {\n ServerLib.isHealthy()\n .then(() => res.sendStatus(204))\n .catch(error => this.handleError(error, res));\n });\n\n this.app.use(express.static(Config.src('./assets')));\n\n this.app.get('/:userId', (req, res) => {\n res.format({\n 'text/html': () => {\n ServerLib.generateUserPage(req.params.userId)\n .then(html => {\n res.cookie('userId', req.params.userId);\n res.send(html);\n })\n .catch(error => this.handleError(error, res));\n },\n 'application/json': () => {\n ServerLib.getUser(req.params.userId)\n .then(json => res.send(json))\n .catch(error => this.handleError(error, res));\n }\n });\n });\n\n this.app.get('/:userId/calendar.ical', (req, res) => {\n ServerLib.generateCalendar(req.params.userId)\n .then(ical => {\n res.set('Content-Type', 'text/calendar');\n res.send(ical);\n })\n .catch(error => this.handleError(error, res));\n });\n }\n\n handleError(error: PrismaClientKnownRequestError | unknown, res: Response) {\n if(error && typeof error === 'object' && 'code' in error && error.code === 'P2025') {\n res.sendStatus(404);\n return;\n }\n\n console.log(error);\n res.sendStatus(500);\n }\n\n async stop() {\n await new Promise(cb => this.server.close(cb));\n await prisma.$disconnect();\n\n process.exit();\n }\n}\n\nAppServer.run();\n","import prisma from './db.js';\nimport { randomUUID } from 'node:crypto';\nimport { generateName, generateNameWithNumber } from '@criblinc/docker-names';\nimport { User } from '@prisma/client';\nimport { ICalAlarmType, ICalCalendar, ICalEventStatus } from 'ical-generator';\nimport { DEFAULT_EMOJI } from './emoji.js';\nimport moment from 'moment-timezone';\nimport { readFile } from 'fs/promises';\nimport Config from './config.js';\n\nexport default class ServerLib {\n static async createUser() {\n let user: User | undefined;\n\n // eslint-disable-next-line no-constant-condition\n for (let c = 0; true; c++) {\n const prefix = this.generatePrefix(c);\n try {\n user = await prisma.user.create({\n data: {\n prefix\n }\n });\n if(user) {\n break;\n }\n }\n catch(error) {\n if(error && typeof error === 'object' && 'code' in error && error.code === 'P2002') {\n continue;\n }\n\n throw error;\n }\n }\n if(!user) {\n throw new Error('User not created');\n }\n\n return user;\n }\n\n static generatePrefix(c = 0) {\n if(c > 100) {\n return randomUUID();\n }\n\n if(c < 10) {\n return generateName();\n }\n\n return generateNameWithNumber();\n }\n\n static async getUser(userId: string) {\n const user = await prisma.user.findUniqueOrThrow({\n where: {\n id: userId\n }\n });\n\n this.updateUserLastSeen(user.id);\n return user;\n }\n\n static updateUserLastSeen(userId: string) {\n prisma.user.update({\n where: { id: userId },\n data: { lastSeenAt: new Date() }\n }).catch(error => {\n console.log(error);\n });\n }\n\n static async generateUserPage(userId: string) {\n const [user, html] = await Promise.all([\n this.getUser(userId),\n readFile(Config.src('templates/user.html'), 'utf-8')\n ]);\n\n return html\n .replace(/\\${CALENDAR_URL}/g, `${Config.baseUrl}/${user.id}/calendar.ical`)\n .replace(/\\${EMAIL_ADDRESS}/g, `${user.prefix}${Config.baseMail}`);\n }\n\n static async generateCalendar(userId: string) {\n const user = await prisma.user.findUniqueOrThrow({\n where: {\n id: userId\n },\n select: {\n id: true,\n event: {\n select: {\n id: true,\n orderId: true,\n from: true,\n to: true,\n amount: true,\n price: true,\n location: {\n select: {\n name: true,\n address: true,\n emoji: true,\n latitude: true,\n longitude: true\n }\n },\n createdAt: true,\n canceledAt: true\n }\n }\n }\n });\n\n const cal = new ICalCalendar({\n name: 'TGTG',\n ttl: 60 * 60,\n events: user.event.map(event => {\n const price = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' })\n .format(event.price / 100);\n\n let status: ICalEventStatus = ICalEventStatus.CONFIRMED;\n if(event.canceledAt) {\n status = ICalEventStatus.CANCELLED;\n }\n\n return {\n id: event.id,\n start: event.from,\n end: event.to,\n timestamp: event.createdAt,\n summary: `${event.location.emoji || DEFAULT_EMOJI} ${event.location.name}`,\n description: `${event.amount}x\\n${price}`,\n url: `https://share.toogoodtogo.com/receipts/details/${event.orderId}`,\n status,\n created: event.createdAt,\n location: {\n title: event.location.name,\n address: event.location.address,\n geo: event.location.latitude && event.location.longitude ? {\n lat: event.location.latitude,\n lon: event.location.longitude\n } : undefined\n },\n alarms: [\n {type: ICalAlarmType.display, trigger: 600},\n ]\n };\n })\n });\n\n this.updateUserLastSeen(userId);\n return cal.toString();\n }\n\n static async isHealthy() {\n const c = await prisma.mail.count({\n where: {\n createdAt: {\n lt: moment().subtract(30, 'minutes').toDate()\n }\n }\n });\n if(c > 0) {\n throw new Error(`There are ${c} unhandled mails in the queue!`);\n }\n }\n}\n"],"mappings":"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","names":["express","cookieParser","randomUUID","generateName","generateNameWithNumber","ICalAlarmType","ICalCalendar","ICalEventStatus","moment","readFile","ServerLib","user","c","prefix","db_default","error","randomUUID","generateName","generateNameWithNumber","userId","html","readFile","config_default","cal","ICalCalendar","event","price","status","ICalEventStatus","DEFAULT_EMOJI","ICalAlarmType","moment","AppServer","_AppServer","express","cookieParser","config_default","Parser","error","req","res","ServerLib","user","html","json","ical","cb","db_default"]}
1
+ {"version":3,"sources":["../src/bin/start.ts","../src/lib/server.ts"],"sourcesContent":["#!/usr/bin/env node\n'use strict';\n\nimport type { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';\n\nimport cookieParser from 'cookie-parser';\nimport express, { type Express, type Response } from 'express';\nimport { Server } from 'http';\n\nimport Config from '../lib/config.js';\nimport prisma from '../lib/db.js';\nimport Parser from '../lib/parser.js';\nimport ServerLib from '../lib/server.js';\n\nclass AppServer {\n private app: Express;\n\n private server: Server;\n constructor() {\n this.app = express();\n this.app.use(cookieParser());\n\n this.setupRoutes();\n this.server = this.app.listen(process.env.PORT || 8080);\n console.log(\n `tgtg-ical v${Config.version} listening on port ${process.env.PORT || 8080}`,\n );\n\n process.on('SIGINT', () => this.stop());\n process.on('SIGTERM', () => this.stop());\n\n Parser.runCleanup()\n .then(() => console.log('Initial cleanup succeeded.'))\n .catch((error) => {\n console.log('Initial cleanup failed:');\n console.error(error);\n process.exit(1);\n });\n }\n\n static run() {\n new AppServer();\n }\n\n handleError(error: PrismaClientKnownRequestError | unknown, res: Response) {\n if (\n error &&\n typeof error === 'object' &&\n 'code' in error &&\n error.code === 'P2025'\n ) {\n res.sendStatus(404);\n return;\n }\n\n console.log(error);\n res.sendStatus(500);\n }\n\n setupRoutes() {\n this.app.get('/ping', (req, res) => {\n res.send('pong');\n });\n\n this.app.get('/', (req, res) => {\n if ('userId' in req.cookies && req.cookies.userId) {\n res.redirect('/' + req.cookies.userId);\n return;\n }\n\n ServerLib.createUser()\n .then((user) => {\n res.cookie('userId', user.id);\n res.redirect('/' + user.id);\n })\n .catch((error) => this.handleError(error, res));\n });\n\n this.app.get('/_health', (req, res) => {\n ServerLib.isHealthy()\n .then(() => res.sendStatus(204))\n .catch((error) => this.handleError(error, res));\n });\n\n this.app.use(express.static(Config.src('./assets')));\n\n this.app.get('/:userId', (req, res) => {\n res.format({\n 'application/json': () => {\n ServerLib.getUser(req.params.userId)\n .then((json) => res.send(json))\n .catch((error) => this.handleError(error, res));\n },\n 'text/html': () => {\n ServerLib.generateUserPage(req.params.userId)\n .then((html) => {\n res.cookie('userId', req.params.userId);\n res.send(html);\n })\n .catch((error) => this.handleError(error, res));\n },\n });\n });\n\n this.app.get('/:userId/calendar.ical', (req, res) => {\n ServerLib.generateCalendar(req.params.userId)\n .then((ical) => {\n res.set('Content-Type', 'text/calendar');\n res.send(ical);\n })\n .catch((error) => this.handleError(error, res));\n });\n }\n\n async stop() {\n await new Promise((cb) => this.server.close(cb));\n await prisma.$disconnect();\n\n process.exit();\n }\n}\n\nAppServer.run();\n","import { generateName, generateNameWithNumber } from '@criblinc/docker-names';\nimport { User } from '@prisma/client';\nimport { readFile } from 'fs/promises';\nimport { ICalAlarmType, ICalCalendar, ICalEventStatus } from 'ical-generator';\nimport moment from 'moment-timezone';\nimport { randomUUID } from 'node:crypto';\n\nimport Config from './config.js';\nimport prisma from './db.js';\nimport { DEFAULT_EMOJI } from './emoji.js';\n\nexport default class ServerLib {\n static async createUser() {\n let user: undefined | User;\n\n // eslint-disable-next-line no-constant-condition\n for (let c = 0; true; c++) {\n const prefix = this.generatePrefix(c);\n try {\n user = await prisma.user.create({\n data: {\n prefix,\n },\n });\n if (user) {\n break;\n }\n } catch (error) {\n if (\n error &&\n typeof error === 'object' &&\n 'code' in error &&\n error.code === 'P2002'\n ) {\n continue;\n }\n\n throw error;\n }\n }\n if (!user) {\n throw new Error('User not created');\n }\n\n return user;\n }\n\n static async generateCalendar(userId: string) {\n const user = await prisma.user.findUniqueOrThrow({\n select: {\n event: {\n select: {\n amount: true,\n canceledAt: true,\n createdAt: true,\n from: true,\n id: true,\n location: {\n select: {\n address: true,\n emoji: true,\n latitude: true,\n longitude: true,\n name: true,\n },\n },\n orderId: true,\n price: true,\n to: true,\n },\n },\n id: true,\n },\n where: {\n id: userId,\n },\n });\n\n const cal = new ICalCalendar({\n events: user.event.map((event) => {\n const price = new Intl.NumberFormat('de-DE', {\n currency: 'EUR',\n style: 'currency',\n }).format(event.price / 100);\n\n let status: ICalEventStatus = ICalEventStatus.CONFIRMED;\n if (event.canceledAt) {\n status = ICalEventStatus.CANCELLED;\n }\n\n return {\n alarms: [{ trigger: 600, type: ICalAlarmType.display }],\n created: event.createdAt,\n description: `${event.amount}x\\n${price}`,\n end: event.to,\n id: event.id,\n location: {\n address: event.location.address,\n geo:\n event.location.latitude && event.location.longitude\n ? {\n lat: event.location.latitude,\n lon: event.location.longitude,\n }\n : undefined,\n title: event.location.name,\n },\n start: event.from,\n status,\n summary: `${event.location.emoji || DEFAULT_EMOJI} ${event.location.name}`,\n timestamp: event.createdAt,\n url: `https://share.toogoodtogo.com/receipts/details/${event.orderId}`,\n };\n }),\n name: 'TGTG',\n ttl: 60 * 60,\n });\n\n this.updateUserLastSeen(userId);\n return cal.toString();\n }\n\n static generatePrefix(c = 0) {\n if (c > 100) {\n return randomUUID();\n }\n\n if (c < 10) {\n return generateName();\n }\n\n return generateNameWithNumber();\n }\n\n static async generateUserPage(userId: string) {\n const [user, html] = await Promise.all([\n this.getUser(userId),\n readFile(Config.src('templates/user.html'), 'utf-8'),\n ]);\n\n return html\n .replace(\n /\\${CALENDAR_URL}/g,\n `${Config.baseUrl}/${user.id}/calendar.ical`,\n )\n .replace(/\\${EMAIL_ADDRESS}/g, `${user.prefix}${Config.baseMail}`);\n }\n\n static async getUser(userId: string) {\n const user = await prisma.user.findUniqueOrThrow({\n where: {\n id: userId,\n },\n });\n\n this.updateUserLastSeen(user.id);\n return user;\n }\n\n static async isHealthy() {\n const c = await prisma.mail.count({\n where: {\n createdAt: {\n lt: moment().subtract(30, 'minutes').toDate(),\n },\n },\n });\n if (c > 0) {\n throw new Error(`There are ${c} unhandled mails in the queue!`);\n }\n }\n\n static updateUserLastSeen(userId: string) {\n prisma.user\n .update({\n data: { lastSeenAt: new Date() },\n where: { id: userId },\n })\n .catch((error) => {\n console.log(error);\n });\n }\n}\n"],"mappings":"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","names":["cookieParser","express","generateName","generateNameWithNumber","readFile","ICalAlarmType","ICalCalendar","ICalEventStatus","moment","randomUUID","ServerLib","user","c","prefix","db_default","error","userId","cal","ICalCalendar","event","price","status","ICalEventStatus","ICalAlarmType","DEFAULT_EMOJI","randomUUID","generateName","generateNameWithNumber","html","readFile","config_default","moment","AppServer","_AppServer","express","cookieParser","config_default","Parser","error","res","req","ServerLib","user","json","html","ical","cb","db_default"]}
package/package.json CHANGED
@@ -1,88 +1,92 @@
1
1
  {
2
- "author": "Sebastian Pekarek <mail@sebbo.net>",
3
- "bin": {
4
- "tgtg-ical-inhale-mail": "./dist/bin/inhale-mail.js",
5
- "tgtg-ical-server": "./dist/start.js"
6
- },
7
- "bugs": {
8
- "url": "https://github.com/sebbo2002/tgtg-ical/issues"
9
- },
10
- "dependencies": {
11
- "@criblinc/docker-names": "^1.2.1",
12
- "@prisma/client": "^6.6.0",
13
- "@sentry/node": "^9.10.1",
14
- "cookie-parser": "^1.4.6",
15
- "express": "^5.1.0",
16
- "he": "^1.2.0",
17
- "ical-generator": "^8.1.1",
18
- "mailparser": "^3.7.2",
19
- "moment-timezone": "^0.5.48"
20
- },
21
- "description": "A small server that receives mails from TGTG, parses them and generates an iCal feed from them.",
22
- "devDependencies": {
23
- "@eslint/js": "^9.23.0",
24
- "@qiwi/semantic-release-gh-pages-plugin": "^5.4.3",
25
- "@sebbo2002/semantic-release-docker": "^5.0.4",
26
- "@semantic-release/changelog": "^6.0.3",
27
- "@semantic-release/exec": "^7.0.3",
28
- "@semantic-release/git": "^10.0.1",
29
- "@semantic-release/npm": "^12.0.1",
30
- "@types/cookie-parser": "^1.4.8",
31
- "@types/express": "^5.0.1",
32
- "@types/he": "^1.2.3",
33
- "@types/mailparser": "^3.4.5",
34
- "@types/mocha": "^10.0.10",
35
- "@types/node": "^22.14.0",
36
- "c8": "^10.1.3",
37
- "eslint": "^9.24.0",
38
- "eslint-plugin-jsonc": "^2.20.0",
39
- "esm": "^3.2.25",
40
- "license-checker": "^25.0.1",
41
- "mocha": "^11.1.0",
42
- "mochawesome": "^7.1.3",
43
- "prisma": "^6.5.0",
44
- "semantic-release-license": "^1.0.3",
45
- "source-map-support": "^0.5.21",
46
- "tsup": "^8.4.0",
47
- "tsx": "^4.19.3",
48
- "typedoc": "^0.28.1",
49
- "typescript": "^5.8.3",
50
- "typescript-eslint": "^8.29.0"
51
- },
52
- "engines": {
53
- "node": "18 || 20 || >=22.0.0"
54
- },
55
- "exports": {
56
- "import": "./dist/lib/index.js",
57
- "require": "./dist/lib/index.cjs"
58
- },
59
- "files": [
60
- "/dist",
61
- "/src/assets",
62
- "/src/prisma",
63
- "/src/templates"
64
- ],
65
- "homepage": "https://github.com/sebbo2002/tgtg-ical#readme",
66
- "license": "MIT",
67
- "name": "@sebbo2002/tgtg-ical",
68
- "prisma": {
69
- "schema": "./src/prisma/schema.prisma"
70
- },
71
- "repository": {
72
- "type": "git",
73
- "url": "git+https://github.com/sebbo2002/tgtg-ical.git"
74
- },
75
- "scripts": {
76
- "build": "tsup",
77
- "build-all": "./.github/workflows/build.sh",
78
- "coverage": "c8 mocha",
79
- "deploy": "./.github/workflows/deploy.sh",
80
- "develop": "tsx src/bin/start.ts",
81
- "license-check": "license-checker --production --summary",
82
- "lint": "eslint .",
83
- "start": "node ./dist/bin/start.js",
84
- "test": "mocha"
85
- },
86
- "type": "module",
87
- "version": "2.0.9-develop.8"
2
+ "author": "Sebastian Pekarek <mail@sebbo.net>",
3
+ "bin": {
4
+ "tgtg-ical-inhale-mail": "./dist/bin/inhale-mail.js",
5
+ "tgtg-ical-server": "./dist/start.js"
6
+ },
7
+ "bugs": {
8
+ "url": "https://github.com/sebbo2002/tgtg-ical/issues"
9
+ },
10
+ "dependencies": {
11
+ "@criblinc/docker-names": "^1.2.1",
12
+ "@prisma/client": "^6.7.0",
13
+ "@sentry/node": "^9.15.0",
14
+ "cookie-parser": "^1.4.6",
15
+ "express": "^5.1.0",
16
+ "he": "^1.2.0",
17
+ "ical-generator": "^8.1.1",
18
+ "mailparser": "^3.7.2",
19
+ "moment-timezone": "^0.5.48"
20
+ },
21
+ "description": "A small server that receives mails from TGTG, parses them and generates an iCal feed from them.",
22
+ "devDependencies": {
23
+ "@eslint/js": "^9.25.0",
24
+ "@qiwi/semantic-release-gh-pages-plugin": "^5.4.3",
25
+ "@sebbo2002/semantic-release-docker": "^5.0.4",
26
+ "@semantic-release/changelog": "^6.0.3",
27
+ "@semantic-release/exec": "^7.0.3",
28
+ "@semantic-release/git": "^10.0.1",
29
+ "@semantic-release/npm": "^12.0.1",
30
+ "@types/cookie-parser": "^1.4.8",
31
+ "@types/express": "^5.0.1",
32
+ "@types/he": "^1.2.3",
33
+ "@types/mailparser": "^3.4.5",
34
+ "@types/mocha": "^10.0.10",
35
+ "@types/node": "^22.14.0",
36
+ "c8": "^10.1.3",
37
+ "eslint": "^9.25.1",
38
+ "eslint-config-prettier": "^10.1.2",
39
+ "eslint-plugin-jsonc": "^2.20.0",
40
+ "eslint-plugin-perfectionist": "^4.12.3",
41
+ "esm": "^3.2.25",
42
+ "husky": "^9.1.7",
43
+ "license-checker": "^25.0.1",
44
+ "mocha": "^11.1.0",
45
+ "mochawesome": "^7.1.3",
46
+ "prettier": "^3.5.3",
47
+ "prisma": "^6.7.0",
48
+ "semantic-release-license": "^1.0.3",
49
+ "source-map-support": "^0.5.21",
50
+ "tsup": "^8.4.0",
51
+ "tsx": "^4.19.3",
52
+ "typedoc": "^0.28.2",
53
+ "typescript": "^5.8.3",
54
+ "typescript-eslint": "^8.31.1"
55
+ },
56
+ "engines": {
57
+ "node": "20 || >=22.0.0"
58
+ },
59
+ "exports": {
60
+ "import": "./dist/lib/index.js",
61
+ "require": "./dist/lib/index.cjs"
62
+ },
63
+ "files": [
64
+ "/dist",
65
+ "/src/assets",
66
+ "/src/prisma",
67
+ "/src/templates"
68
+ ],
69
+ "homepage": "https://github.com/sebbo2002/tgtg-ical#readme",
70
+ "license": "MIT",
71
+ "name": "@sebbo2002/tgtg-ical",
72
+ "prisma": {
73
+ "schema": "./src/prisma/schema.prisma"
74
+ },
75
+ "repository": {
76
+ "type": "git",
77
+ "url": "git+https://github.com/sebbo2002/tgtg-ical.git"
78
+ },
79
+ "scripts": {
80
+ "build": "tsup",
81
+ "build-all": "./.github/workflows/build.sh",
82
+ "coverage": "c8 mocha",
83
+ "deploy": "./.github/workflows/deploy.sh",
84
+ "develop": "tsx src/bin/start.ts",
85
+ "license-check": "license-checker --production --summary",
86
+ "lint": "npx eslint . --fix && npx prettier . --write",
87
+ "start": "node ./dist/bin/start.js",
88
+ "test": "mocha"
89
+ },
90
+ "type": "module",
91
+ "version": "3.0.0-develop.1"
88
92
  }
@@ -1,5 +1,5 @@
1
1
  :root {
2
- --color-primary: #4B8150;
2
+ --color-primary: #4b8150;
3
3
  --color-primary-rgb: 75, 129, 80;
4
4
  --color-primary-dark: #38613c;
5
5
  --color-primary-dark-rgb: 56, 97, 60;
@@ -21,21 +21,35 @@ a:visited {
21
21
  html {
22
22
  box-sizing: border-box;
23
23
  font-size: 16px;
24
- font-family: Seravek, 'Gill Sans Nova', Ubuntu, Calibri, 'DejaVu Sans', source-sans-pro, sans-serif;
24
+ font-family:
25
+ Seravek, 'Gill Sans Nova', Ubuntu, Calibri, 'DejaVu Sans',
26
+ source-sans-pro, sans-serif;
25
27
  min-height: 100vh;
26
28
  }
27
29
 
28
- *, *:before, *:after {
30
+ *,
31
+ *:before,
32
+ *:after {
29
33
  box-sizing: inherit;
30
34
  }
31
35
 
32
- body, h1, h2, h3, h4, h5, h6, p, ol, ul {
36
+ body,
37
+ h1,
38
+ h2,
39
+ h3,
40
+ h4,
41
+ h5,
42
+ h6,
43
+ p,
44
+ ol,
45
+ ul {
33
46
  margin: 0;
34
47
  padding: 0;
35
48
  font-weight: normal;
36
49
  }
37
50
 
38
- ol, ul {
51
+ ol,
52
+ ul {
39
53
  list-style: none;
40
54
  }
41
55
 
@@ -151,7 +165,9 @@ body {
151
165
  .button--ok {
152
166
  pointer-events: none;
153
167
  color: transparent;
154
- background: var(--color-primary) url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="white" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg>') center center no-repeat;
168
+ background: var(--color-primary)
169
+ url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="white" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg>')
170
+ center center no-repeat;
155
171
  }
156
172
 
157
173
  .footer {
@@ -162,4 +178,4 @@ body {
162
178
  font-size: 0.8em;
163
179
  margin: 0;
164
180
  opacity: 0.6;
165
- }
181
+ }
@@ -1,81 +1,153 @@
1
1
  <!doctype html>
2
2
  <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>TGTG → iCal</title>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>TGTG → iCal</title>
8
8
 
9
- <meta name="title" content="TGTG → iCal" />
10
- <meta name="description" content="Create a subscribable calendar with your TGTG pickup dates so you never miss them." />
11
- <meta property="og:type" content="website" />
12
- <meta property="og:url" content="https://tgtg-ical.sebbo.net" />
13
- <meta property="og:title" content="TGTG → iCal" />
14
- <meta property="og:description" content="Create a subscribable calendar with your TGTG pickup dates so you never miss them." />
15
- <meta property="og:image" content="/preview.png" />
9
+ <meta name="title" content="TGTG → iCal" />
10
+ <meta
11
+ name="description"
12
+ content="Create a subscribable calendar with your TGTG pickup dates so you never miss them."
13
+ />
14
+ <meta property="og:type" content="website" />
15
+ <meta property="og:url" content="https://tgtg-ical.sebbo.net" />
16
+ <meta property="og:title" content="TGTG → iCal" />
17
+ <meta
18
+ property="og:description"
19
+ content="Create a subscribable calendar with your TGTG pickup dates so you never miss them."
20
+ />
21
+ <meta property="og:image" content="/preview.png" />
16
22
 
17
- <link rel="stylesheet" href="/style.css">
18
- <script defer src="https://p.sebbo.net/script.js" data-website-id="119b2adc-26f2-45e7-ae53-000dd411908f"></script>
19
- </head>
20
- <body>
21
- <div class="app">
22
- <header class="header">
23
- <span class="header__logo">&nbsp;</span>
24
- <h1 class="header__title">TGTG → iCal</h1>
25
- </header>
26
- <main class="main">
27
- <ul class="main__todos">
28
- <li class="todo">
29
- <span class="todo__number">1</span>
30
- <h3 class="todo__title">Subscribe to your calendar feed</h3>
31
- <p class="todo__notes">
32
- Calendars can be subscribed to in all common calendar apps,
33
- e.g. Google Calendar, Apple Calendar, Outlook, etc.
23
+ <link rel="stylesheet" href="/style.css" />
24
+ <script
25
+ defer
26
+ src="https://p.sebbo.net/script.js"
27
+ data-website-id="119b2adc-26f2-45e7-ae53-000dd411908f"
28
+ ></script>
29
+ </head>
30
+ <body>
31
+ <div class="app">
32
+ <header class="header">
33
+ <span class="header__logo">&nbsp;</span>
34
+ <h1 class="header__title">TGTG → iCal</h1>
35
+ </header>
36
+ <main class="main">
37
+ <ul class="main__todos">
38
+ <li class="todo">
39
+ <span class="todo__number">1</span>
40
+ <h3 class="todo__title">
41
+ Subscribe to your calendar feed
42
+ </h3>
43
+ <p class="todo__notes">
44
+ Calendars can be subscribed to in all common
45
+ calendar apps, e.g. Google Calendar, Apple Calendar,
46
+ Outlook, etc.
47
+ </p>
48
+ <button
49
+ class="button button--copy"
50
+ data-copy="${CALENDAR_URL}"
51
+ >
52
+ Copy calendar URL
53
+ </button>
54
+ </li>
55
+ <li class="todo">
56
+ <span class="todo__number">2</span>
57
+ <h3 class="todo__title">Forward your TGTG emails</h3>
58
+ <p class="todo__notes">
59
+ It is best to set up a forwarding rule with your
60
+ email provider so that mails from TGTG are
61
+ automatically forwarded to your personal email
62
+ address.
63
+ </p>
64
+ <button
65
+ class="button button--copy"
66
+ data-copy="${EMAIL_ADDRESS}"
67
+ >
68
+ Copy email address
69
+ </button>
70
+ </li>
71
+ <li class="todo">
72
+ <span class="todo__number">3</span>
73
+ <h3 class="todo__title">Tadaa. 🎉</h3>
74
+ <p class="todo__notes">
75
+ From now on your forwarded mails by TGTG will be
76
+ automatically analyzed and displayed in your
77
+ calendar. If not, feel free to
78
+ <a
79
+ href="https://github.com/sebbo2002/tgtg-ical/issues/new"
80
+ target="_blank"
81
+ rel="noopener noreferrer"
82
+ >report a bug</a
83
+ >
84
+ :)
85
+ </p>
86
+ </li>
87
+ </ul>
88
+ <footer class="footer">
89
+ <p class="footer__text">
90
+ Made by
91
+ <a href="https://sebbo.net">Sebastian Pekarek</a> |
92
+ Hosted on
93
+ <a
94
+ href="https://uberspace.de"
95
+ target="_blank"
96
+ rel="noopener noreferrer"
97
+ >Asteroids</a
98
+ >
99
+ | Not related to
100
+ <a
101
+ href="https://www.toogoodtogo.com/"
102
+ target="_blank"
103
+ rel="noopener noreferrer"
104
+ >Too Good To Go</a
105
+ >
106
+ | Uses
107
+ <a
108
+ href="https://openstreetmap.org/"
109
+ target="_blank"
110
+ rel="noopener noreferrer"
111
+ >OpenStreetMap</a
112
+ >
113
+ for Geocoding |
114
+ <a
115
+ href="https://sebbo.net/contact"
116
+ target="_blank"
117
+ rel="noopener noreferrer"
118
+ >Impressum</a
119
+ >
120
+ |
121
+ <a
122
+ href="https://github.com/sebbo2002/tgtg-ical"
123
+ target="_blank"
124
+ rel="noopener noreferrer"
125
+ >OpenSource on GitHub</a
126
+ >
127
+ |
128
+ <a
129
+ href="https://github.com/sebbo2002/tgtg-ical/issues/new"
130
+ target="_blank"
131
+ rel="noopener noreferrer"
132
+ >Report a bug</a
133
+ >
34
134
  </p>
35
- <button class="button button--copy" data-copy="${CALENDAR_URL}">Copy calendar URL</button>
36
- </li>
37
- <li class="todo">
38
- <span class="todo__number">2</span>
39
- <h3 class="todo__title">Forward your TGTG emails</h3>
40
- <p class="todo__notes">
41
- It is best to set up a forwarding rule with your email provider so that mails from TGTG are
42
- automatically forwarded to your personal email address.
43
- </p>
44
- <button class="button button--copy" data-copy="${EMAIL_ADDRESS}">Copy email address</button>
45
- </li>
46
- <li class="todo">
47
- <span class="todo__number">3</span>
48
- <h3 class="todo__title">Tadaa. 🎉</h3>
49
- <p class="todo__notes">
50
- From now on your forwarded mails by TGTG will be automatically
51
- analyzed and displayed in your calendar. If not, feel free to
52
- <a href="https://github.com/sebbo2002/tgtg-ical/issues/new" target="_blank" rel="noopener noreferrer">report a bug</a> :)
53
- </p>
54
- </li>
55
- </ul>
56
- <footer class="footer">
57
- <p class="footer__text">
58
- Made by <a href="https://sebbo.net">Sebastian Pekarek</a> |
59
- Hosted on <a href="https://uberspace.de" target="_blank" rel="noopener noreferrer">Asteroids</a> |
60
- Not related to <a href="https://www.toogoodtogo.com/" target="_blank" rel="noopener noreferrer">Too Good To Go</a> |
61
- Uses <a href="https://openstreetmap.org/" target="_blank" rel="noopener noreferrer">OpenStreetMap</a> for Geocoding |
62
- <a href="https://sebbo.net/contact" target="_blank" rel="noopener noreferrer">Impressum</a> |
63
- <a href="https://github.com/sebbo2002/tgtg-ical" target="_blank" rel="noopener noreferrer">OpenSource on GitHub</a> |
64
- <a href="https://github.com/sebbo2002/tgtg-ical/issues/new" target="_blank" rel="noopener noreferrer">Report a bug</a>
65
- </footer>
66
- </main>
67
- </div>
68
- <script>
69
- document.querySelectorAll('.button--copy').forEach(button => {
70
- button.addEventListener('click', () => {
71
- navigator.clipboard.writeText(button.attributes['data-copy'].value);
72
- button.classList.add('button--ok');
135
+ </footer>
136
+ </main>
137
+ </div>
138
+ <script>
139
+ document.querySelectorAll('.button--copy').forEach((button) => {
140
+ button.addEventListener('click', () => {
141
+ navigator.clipboard.writeText(
142
+ button.attributes['data-copy'].value,
143
+ );
144
+ button.classList.add('button--ok');
73
145
 
74
- setTimeout(() => {
75
- button.classList.remove('button--ok');
76
- }, 1000);
146
+ setTimeout(() => {
147
+ button.classList.remove('button--ok');
148
+ }, 1000);
149
+ });
77
150
  });
78
- });
79
- </script>
80
- </body>
151
+ </script>
152
+ </body>
81
153
  </html>
@@ -1,2 +0,0 @@
1
- import{PrismaClient as I}from"@prisma/client";var A=new I,i=A;import{init as k}from"@sentry/node";import{dirname as x,resolve as g}from"node:path";import{fileURLToPath as P}from"node:url";import{readFileSync as z}from"node:fs";k({dsn:process.env.SENTRY_DSN||"https://5e4630d58e5f4c778ce22140c53b3684@glitch.sebbo.net/9"});var u=x(P(import.meta.url));u.endsWith("/dist")?u=g(u,"..","src"):u=g(u,"..");function v(l){return g(u,l||"")}var D;try{D=JSON.parse(z(v("../package.json"),"utf8")).version}catch(l){console.log("Failed getting tgtg-ical version:"),console.error(l)}var c={baseUrl:process.env.BASE_URL||"https://tgtg-ical.sebbo.net",baseMail:process.env.BASE_MAIL||"@tgtg-ical.sebbo.net",version:D,src:v};import{captureException as B}from"@sentry/node";import{simpleParser as T}from"mailparser";import E from"he";import n from"moment-timezone";var O="\u{1F374}",b={"\u{1F354}":[/Burger/,/McDonald[‘|']?s/,/Burger King/,/Hans im Glück/,/Peter Pane/],"\u{1F96A}":[/Sandwich/,/Tank ?& ?Rast/,/T&R Raststätten/,/Autohöfe/,/Aral/,/Shell/,/Jet Tank/,/Subway/,/Caf[é|e] bonjour/,/Total Deutschland/,/Esso:? Snack ?& ?Shop/],"\u{1F355}":[/Domino‘s/,/Pizza Hut/,/L[‘|']Osteria/,/Call a Pizza/,/Smiley[‘|']s Pizza/],"\u{1F357}":[/KFC/],"\u{1F35D}":[/LaTagliatella/,/Vapiano/],"\u{1F364}":[/Nordsee/],"\u{1F969}":[/Steakhouse/,/Block House/,/Jim Block/],"\u{1F32E}":[/Enchilada/,/Besitos/],"\u{1F32D}":[/Ikea/],"\u{1F957}":[/dean ?& ?david/],"\u{1F956}":[/Bäckerei/,/Back/,/BackWerk/,/Kamps/,/Kamps Backstuben/,/Junge Die Bäckerei/,/Back-Factory/],"\u{1F968}":[/Brezel/,/Ditsch/],"\u{1F950}":[/LeCroBag/],"\u{1F36A}":[/Starbucks/],"\u{1F369}":[/Donut/,/Dunkin/],"\u{1F6D2}":[/Edeka/,/Netto/,/Rewe/,/Penny/,/Lidl/,/Kaufland/,/Aldi/,/dm/,/Rossmann/,/Globus/,/Metro/,/Norma/,/Tegut/]};function w(l){for(let t in b)if(!!b[t].map(a=>new RegExp(a,"i")).find(a=>a.test(l)))return t;return null}var M=class{static async runCleanup(){let t=await i.mail.findMany({where:{OR:[{error:null},{version:null},{version:{not:c.version}}]},orderBy:{erroredAt:"asc"},take:10});for(let r of t)await this.handleMail(r);await i.user.deleteMany({where:{OR:[{lastSeenAt:{lt:n().subtract(8,"weeks").toDate()}},{lastSeenAt:{equals:i.user.fields.createdAt},createdAt:{lt:n().subtract(3,"hours").toDate()}}]}}),await i.event.deleteMany({where:{to:{lt:n().subtract(4,"weeks").toDate()}}}),await i.mail.deleteMany({where:{createdAt:{lt:n().subtract(2,"weeks").toDate()}}})}static async inhaleMail(t){let r=await i.mail.create({data:{raw:t}});await this.handleMail(r)}static async handleMail(t){try{let r=await this.parseMail(t.raw);r&&await this.applyParsedMail(r),await i.mail.delete({where:{id:t.id}})}catch(r){let e=B(r);await i.mail.update({where:{id:t.id},data:{error:r instanceof Error?r.stack:String(r),erroredAt:new Date,errorId:e,version:c.version}})}}static async parseMail(t,r=c.baseMail){let e=await T(t,{skipHtmlToText:!0,skipImageLinks:!0,skipTextToHtml:!0,skipTextLinks:!0});if(!e.from?.value[0].address?.endsWith("toogoodtogo.com"))throw new Error("Not a TGTG email!");let a=(Array.isArray(e.to)?e.to:[e.to]).map(o=>o?.value).flat().filter(o=>!!o).map(o=>o.address).find(o=>o?.endsWith(r));if(!a){let o=e.headers.get("received"),d=new RegExp(`([\\w-]+${c.baseMail})`,"i");Array.isArray(o)&&o.forEach(s=>{let m=(typeof s=="string"?s:s.value).match(d);m&&(a=m[1])})}if(!a)throw new Error("No recipient found!");if(e.headers.get("x-pm-tag")==="consumer_order_confirm"){let o=this.parseOrderMail(e);return{to:a,...o}}if(e.headers.get("x-pm-tag")==="collection_time_changed"){let o=this.parseChangeMail(e);return{to:a,...o}}if(e.headers.get("x-pm-tag")==="consumer_order_reverted"){let o=this.parseCancellationMail(e);if(o)return{to:a,...o}}if(e.headers.get("x-pm-tag")==="invoice"){let o=this.parseInvoiceMail(e);if(o)return{to:a,...o}}if(!(!e.headers.get("x-pm-tag")&&!e.headers.get("x-pm-message-id")))throw e.headers.get("x-pm-tag")?new Error(`Unsupported email type: ${e.headers.get("x-pm-tag")}`):new Error("Not implemented!")}static async findUser(t){if(!t)throw new Error("Did not found a valid recipient!");let r=t.split("@")[0],e=await i.user.findUnique({where:{prefix:r}});if(!e)throw new Error(`User with email prefix ${r} not found!`);return e}static parseOrderMail(t){let r=t.html||"",e=[r.match(/\/order\/([^/]+)\//),r.match(/Wir bestätigen hiermit deine Bestellung bei ([^(.<]+)/),r.match(/<span>Du kannst deine Bestellung am (\d{1,2}\.\d{2}\.\d{2}) zwischen (\d{1,2}:\d{2}) und (\d{1,2}:\d{2}) Uhr (\w+)[^:]+: (.+).<\/span><\/div>/),r.match(/<span>Du kannst deine Bestellung zwischen (\d{1,2}\.\d{1,2}), (\d{1,2}:\d{2}) und (\d{1,2}:\d{2})[^:]+: (.+).<\/span><\/div>/),r.match(/<b>Datum:<\/b>\s+<span>(\d{1,2}\.\d{2}\.\d{2})<\/span>/),r.match(/Abholzeit:<\/b>\s+<span>(\d{1,2}:\d{2}) - (\d{1,2}:\d{2}) (\w+)/),r.match(/Anzahl: (\d+)/),r.match(/Anzahl:<\/b>\s+<span>(\d+)/),r.match(/Gesamtpreis: ([\d,.]+)[^\d,.]/),r.match(/Gesamtpreis:<\/b>\s+<span>([\d,.]+)[^\d,.]/),r.match(/Standort:<\/b>\s+<span>([^<]+)/)];if(!e[0])throw new Error("Order ID not found!");if(!e[1])throw new Error("Location name not found!");if(!e[2]&&!e[3]&&!(e[4]&&e[5]))throw new Error("Date, time and address not found (1)!");let a="MET";e[2]&&(a=e[2][4]),e[5]&&(a=e[5][3]),a==="MEZ"&&(a="MET");let o=n(t.date),d,s,p;if(e[2]&&(d=n.tz(e[2][1]+" "+e[2][2],"DD.MM.YY HH:mm",a),s=n.tz(e[2][1]+" "+e[2][3],"DD.MM.YY HH:mm",a),p=e[2][5].trim()),e[3]){let y=o.year();d=n.tz(e[3][1]+"."+y+" "+e[3][2],"DD.MM.YYYY HH:mm",a),s=n.tz(e[3][1]+"."+y+" "+e[3][3],"DD.MM.YYYY HH:mm",a),p=e[3][4].trim(),d.isBefore(o)&&d.add(1,"year"),s.isBefore(d)&&s.add(1,"year")}if(e[4]&&e[5]&&e[10]&&(d=n.tz(e[4][1]+" "+e[5][1],"DD.MM.YY HH:mm",a),s=n.tz(e[4][1]+" "+e[5][2],"DD.MM.YY HH:mm",a),p=e[10][1].trim()),!d||!s||!p)throw new Error("Date, time or address not found (2)!");let m=0;if(e[6])m=parseInt(e[6][1],10);else if(e[7])m=parseInt(e[7][1],10);else throw new Error("Amount not found!");if(isNaN(m))throw new Error("Amount (1) is not a number!");let f=0;if(e[8])f=parseInt(e[8][1].replace(/[.|,]/g,""));else if(e[9])f=parseInt(e[9][1].replace(/[.|,]/g,""));else throw new Error("Price not found!");if(isNaN(f))throw new Error("Price is not a number!");let h=E.decode(e[1][1].trim());return h.endsWith("!")&&(h=h.slice(0,-1)),{type:"order",orderId:e[0][1].trim(),location:{name:h,address:E.decode(p)},time:{order:o,from:d,to:s},amount:m,price:f}}static parseChangeMail(t){let r=[(t.html||"").match(/https:\/\/share.toogoodtogo.com\/receipts\/details\/(\w+)/),(t.html||"").match(/(\d{1,2}\.\d{2}\.\d{2}) zwischen (\d{1,2}:\d{2}) und (\d{1,2}:\d{2})(?: Uhr)? (\w+)+ \(/)];if(!r[0])throw new Error("Order ID not found!");if(!r[1])throw new Error("Date / Time not found!");let e=r[1][4];e==="MEZ"&&(e="MET");let a=n.tz(r[1][1]+" "+r[1][2],"DD.MM.YY HH:mm",e),o=n.tz(r[1][1]+" "+r[1][3],"DD.MM.YY HH:mm",e);return{type:"change",orderId:r[0][1].trim(),time:{from:a,to:o}}}static parseCancellationMail(t){let e=(t.subject||"").match(/\((\w+)\)/);if(e)return{type:"cancel",orderId:e[1],cancelledAt:n(t.date)}}static parseInvoiceMail(t){let e=(t.html||"").match(/Die Rechnung für deine Bestellung (\w+)/);if(e)return{type:"invoice",orderId:e[1],invoicedAt:n(t.date)};throw new Error("Order Id not found!")}static async applyParsedMail(t){let r=await this.findUser(t.to);if(t.type==="order"){let e=await this.getLocation(t.location);await i.event.upsert({where:{orderId:t.orderId,userId:r.id},create:{orderId:t.orderId,orderedAt:t.time.order.toDate(),from:t.time.from.toDate(),to:t.time.to.toDate(),amount:t.amount,price:t.price,user:{connect:{id:r.id}},location:{connect:{id:e.id}}},update:{from:t.time.from.toDate(),to:t.time.to.toDate(),amount:t.amount,price:t.price,location:{connect:{id:e.id}}}})}else if(t.type==="change")await i.event.update({where:{orderId:t.orderId,userId:r.id},data:{from:t.time.from.toDate(),to:t.time.to.toDate()}});else if(t.type==="cancel")await i.event.update({where:{orderId:t.orderId,userId:r.id},data:{canceledAt:t.cancelledAt.toDate()}});else if(t.type==="invoice")await i.event.update({where:{orderId:t.orderId,userId:r.id},data:{invoicedAt:t.invoicedAt.toDate()}});else throw new Error("Unknown email type!")}static async getLocation(t){let r=await i.location.findFirst({where:{name:t.name,address:t.address}});if(!r){let e=w(t.name),{latitude:a,longitude:o}=await this.geocode(t.address);r=await i.location.create({data:{name:t.name,address:t.address,latitude:a,longitude:o,emoji:e}})}return r}static async geocode(t){let r=await fetch("https://nominatim.openstreetmap.org/search?format=json&limit=1&q="+encodeURIComponent(t),{headers:{"User-Agent":`tgtg-ical/${c.version} (${c.baseUrl})`,Referer:c.baseUrl}});if(!r.ok)throw new Error("Geocoding failed: "+r.statusText);await new Promise(a=>setTimeout(a,1e3));let e=await r.json();return!Array.isArray(e)||e.length===0?{latitude:null,longitude:null}:{latitude:parseFloat(e[0].lat),longitude:parseFloat(e[0].lon)}}};export{i as a,c as b,O as c,M as d};
2
- //# sourceMappingURL=chunk-2HK3PBFJ.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/lib/db.ts","../src/lib/config.ts","../src/lib/parser.ts","../src/lib/emoji.ts"],"sourcesContent":["import { PrismaClient } from '@prisma/client';\n\nconst prisma = new PrismaClient();\nexport default prisma;\n","import { init } from '@sentry/node';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { readFileSync } from 'node:fs';\n\ninit({\n dsn: process.env.SENTRY_DSN || 'https://5e4630d58e5f4c778ce22140c53b3684@glitch.sebbo.net/9',\n});\n\nlet dir = dirname(fileURLToPath(import.meta.url));\nif (dir.endsWith('/dist')) {\n dir = resolve(dir, '..', 'src');\n} else {\n dir = resolve(dir, '..');\n}\n\nfunction src(path?: string) {\n return resolve(dir, path || '');\n}\n\nlet version: string | undefined;\ntry {\n const pkg = JSON.parse(readFileSync(src('../package.json'), 'utf8'));\n version = pkg.version;\n}\ncatch(error) {\n console.log('Failed getting tgtg-ical version:');\n console.error(error);\n}\n\nexport default {\n baseUrl: process.env.BASE_URL || 'https://tgtg-ical.sebbo.net',\n baseMail: process.env.BASE_MAIL || '@tgtg-ical.sebbo.net',\n version,\n src\n};\n","import type { Mail, User, Location } from '@prisma/client';\nimport { captureException } from '@sentry/node';\nimport { type ParsedMail, simpleParser } from 'mailparser';\nimport he from 'he';\nimport prisma from './db.js';\nimport config from './config.js';\nimport moment from 'moment-timezone';\nimport getEmoji from './emoji.js';\nimport Config from './config.js';\n\ntype AnyMail = { to: string; } & (OrderMail | ChangeMail | InvoiceMail | CancellationMail);\n\ninterface OrderMail {\n type: 'order';\n orderId: string;\n location: {\n name: string;\n address: string;\n };\n time: {\n order: moment.Moment;\n from: moment.Moment;\n to: moment.Moment;\n };\n amount: number;\n price: number;\n}\n\ninterface ChangeMail {\n type: 'change';\n orderId: string;\n time: {\n from: moment.Moment;\n to: moment.Moment;\n };\n}\n\ninterface InvoiceMail {\n type: 'invoice';\n orderId: string;\n invoicedAt: moment.Moment;\n}\n\ninterface CancellationMail {\n type: 'cancel';\n orderId: string;\n cancelledAt: moment.Moment;\n}\n\nexport default class Parser {\n public static async runCleanup(): Promise<void> {\n const mails = await prisma.mail.findMany({\n where: {\n OR: [\n { error: null },\n { version: null },\n { version: { not: config.version } }\n ]\n },\n orderBy: {\n erroredAt: 'asc'\n },\n take: 10\n });\n\n for(const mail of mails) {\n await this.handleMail(mail);\n }\n\n // Cleanup Users\n await prisma.user.deleteMany({\n where: {\n OR: [\n {\n lastSeenAt: {\n lt: moment().subtract(8, 'weeks').toDate()\n }\n },\n {\n lastSeenAt: {\n equals: prisma.user.fields.createdAt\n },\n createdAt: {\n lt: moment().subtract(3, 'hours').toDate()\n }\n }\n ]\n }\n });\n\n // Cleanup Events\n await prisma.event.deleteMany({\n where: {\n to: {\n lt: moment().subtract(4, 'weeks').toDate()\n }\n }\n });\n\n // Cleanup Mails\n await prisma.mail.deleteMany({\n where: {\n createdAt: {\n lt: moment().subtract(2, 'weeks').toDate()\n }\n }\n });\n }\n\n public static async inhaleMail (email: string): Promise<void> {\n const mail = await prisma.mail.create({\n data: {\n raw: email\n }\n });\n\n await this.handleMail(mail);\n }\n\n public static async handleMail(mail: Mail) {\n try {\n const parsed = await this.parseMail(mail.raw);\n if(parsed) {\n await this.applyParsedMail(parsed);\n }\n\n await prisma.mail.delete({\n where: {\n id: mail.id\n }\n });\n }\n catch(error) {\n const errorId = captureException(error);\n await prisma.mail.update({\n where: {\n id: mail.id\n },\n data: {\n error: error instanceof Error ? error.stack : String(error),\n erroredAt: new Date(),\n errorId,\n version: config.version\n }\n });\n }\n }\n\n public static async parseMail(mail: string, baseMailPostfix = config.baseMail): Promise<AnyMail|undefined> {\n // parse email\n const email = await simpleParser(mail, {\n skipHtmlToText: true,\n skipImageLinks: true,\n skipTextToHtml: true,\n skipTextLinks: true\n });\n\n if(!email.from?.value[0].address?.endsWith('toogoodtogo.com')) {\n throw new Error('Not a TGTG email!');\n }\n\n let to: string | undefined = (Array.isArray(email.to) ? email.to : [email.to])\n .map(to => to?.value)\n .flat()\n .filter(address => !!address)\n .map(address => address.address)\n .find(address => address?.endsWith(baseMailPostfix));\n\n if (!to) {\n const received = email.headers.get('received');\n const regexp = new RegExp(`([\\\\w-]+${Config.baseMail})`, 'i');;\n if(Array.isArray(received)) {\n received.forEach(r => {\n const s = typeof r === 'string' ? r : r.value;\n const match = s.match(regexp);\n if(match) {\n to = match[1];\n }\n });\n }\n }\n if (!to) {\n throw new Error('No recipient found!');\n }\n\n // Order confirmation\n if(email.headers.get('x-pm-tag') === 'consumer_order_confirm') {\n const order = this.parseOrderMail(email);\n return {\n to,\n ...order\n };\n }\n\n // Time Changed\n if(email.headers.get('x-pm-tag') === 'collection_time_changed') {\n const change = this.parseChangeMail(email);\n return {\n to,\n ...change\n };\n }\n\n // Cancellation\n if(email.headers.get('x-pm-tag') === 'consumer_order_reverted') {\n const cancellation = this.parseCancellationMail(email);\n if(cancellation) {\n return {\n to,\n ...cancellation\n };\n }\n }\n\n // is invoice?\n if(email.headers.get('x-pm-tag') === 'invoice') {\n const invoice = this.parseInvoiceMail(email);\n if(invoice) {\n return {\n to,\n ...invoice\n };\n }\n }\n\n // Non-transactional email\n if(!email.headers.get('x-pm-tag') && !email.headers.get('x-pm-message-id')) {\n return undefined;\n }\n\n // Unsupported email\n if(email.headers.get('x-pm-tag')) {\n throw new Error(`Unsupported email type: ${email.headers.get('x-pm-tag')}`);\n }\n\n throw new Error('Not implemented!');\n }\n\n private static async findUser(to: string): Promise<User> {\n if(!to) {\n throw new Error('Did not found a valid recipient!');\n }\n\n const prefix = to.split('@')[0];\n const user = await prisma.user.findUnique({\n where: { prefix }\n });\n if(!user) {\n throw new Error(`User with email prefix ${prefix} not found!`);\n }\n\n return user;\n }\n\n private static parseOrderMail(email: ParsedMail): OrderMail {\n const html = email.html || '';\n const matches = [\n html.match(/\\/order\\/([^/]+)\\//),\n html.match(/Wir bestätigen hiermit deine Bestellung bei ([^(.<]+)/),\n html.match(/<span>Du kannst deine Bestellung am (\\d{1,2}\\.\\d{2}\\.\\d{2}) zwischen (\\d{1,2}:\\d{2}) und (\\d{1,2}:\\d{2}) Uhr (\\w+)[^:]+: (.+).<\\/span><\\/div>/),\n html.match(/<span>Du kannst deine Bestellung zwischen (\\d{1,2}\\.\\d{1,2}), (\\d{1,2}:\\d{2}) und (\\d{1,2}:\\d{2})[^:]+: (.+).<\\/span><\\/div>/),\n html.match(/<b>Datum:<\\/b>\\s+<span>(\\d{1,2}\\.\\d{2}\\.\\d{2})<\\/span>/),\n html.match(/Abholzeit:<\\/b>\\s+<span>(\\d{1,2}:\\d{2}) - (\\d{1,2}:\\d{2}) (\\w+)/),\n html.match(/Anzahl: (\\d+)/),\n html.match(/Anzahl:<\\/b>\\s+<span>(\\d+)/),\n html.match(/Gesamtpreis: ([\\d,.]+)[^\\d,.]/),\n html.match(/Gesamtpreis:<\\/b>\\s+<span>([\\d,.]+)[^\\d,.]/),\n html.match(/Standort:<\\/b>\\s+<span>([^<]+)/)\n ];\n if(!matches[0]) {\n throw new Error('Order ID not found!');\n }\n if(!matches[1]) {\n throw new Error('Location name not found!');\n }\n if(!matches[2] && !matches[3] && !(matches[4] && matches[5])) {\n throw new Error('Date, time and address not found (1)!');\n }\n\n let timezone = 'MET';\n if (matches[2]) {\n timezone = matches[2][4];\n }\n if (matches[5]) {\n timezone = matches[5][3];\n }\n if(timezone === 'MEZ') {\n timezone = 'MET';\n }\n\n const now = moment(email.date);\n let from: moment.Moment | undefined;\n let to: moment.Moment | undefined;\n let address: string | undefined;\n\n if(matches[2]) {\n from = moment.tz(matches[2][1] + ' ' + matches[2][2], 'DD.MM.YY HH:mm', timezone);\n to = moment.tz(matches[2][1] + ' ' + matches[2][3], 'DD.MM.YY HH:mm', timezone);\n address = matches[2][5].trim();\n }\n if(matches[3]) {\n const year = now.year();\n from = moment.tz(matches[3][1] + '.' + year + ' ' + matches[3][2], 'DD.MM.YYYY HH:mm', timezone);\n to = moment.tz(matches[3][1] + '.' + year + ' ' + matches[3][3], 'DD.MM.YYYY HH:mm', timezone);\n address = matches[3][4].trim();\n\n if(from.isBefore(now)) {\n from.add(1, 'year');\n }\n if(to.isBefore(from)) {\n to.add(1, 'year');\n }\n }\n if(matches[4] && matches[5] && matches[10]) {\n from = moment.tz(matches[4][1] + ' ' + matches[5][1], 'DD.MM.YY HH:mm', timezone);\n to = moment.tz(matches[4][1] + ' ' + matches[5][2], 'DD.MM.YY HH:mm', timezone);\n address = matches[10][1].trim();\n }\n\n if(!from || !to || !address) {\n throw new Error('Date, time or address not found (2)!');\n }\n\n let amount = 0;\n if (matches[6]) {\n amount = parseInt(matches[6][1], 10);\n }\n else if (matches[7]) {\n amount = parseInt(matches[7][1], 10);\n }\n else {\n throw new Error('Amount not found!');\n }\n if(isNaN(amount)) {\n throw new Error('Amount (1) is not a number!');\n }\n\n\n let price = 0;\n if (matches[8]) {\n price = parseInt(matches[8][1].replace(/[.|,]/g, ''));\n }\n else if (matches[9]) {\n price = parseInt(matches[9][1].replace(/[.|,]/g, ''));\n }\n else {\n throw new Error('Price not found!');\n }\n if(isNaN(price)) {\n throw new Error('Price is not a number!');\n }\n\n let name = he.decode(matches[1][1].trim());\n if (name.endsWith('!')) {\n name = name.slice(0, -1);\n }\n\n return {\n type: 'order',\n orderId: matches[0][1].trim(),\n location: {\n name,\n address: he.decode(address)\n },\n time: {\n order: now,\n from,\n to\n },\n amount,\n price\n };\n }\n\n private static parseChangeMail(email: ParsedMail): ChangeMail {\n const matches = [\n (email.html || '').match(/https:\\/\\/share.toogoodtogo.com\\/receipts\\/details\\/(\\w+)/),\n (email.html || '').match(/(\\d{1,2}\\.\\d{2}\\.\\d{2}) zwischen (\\d{1,2}:\\d{2}) und (\\d{1,2}:\\d{2})(?: Uhr)? (\\w+)+ \\(/)\n ];\n if(!matches[0]) {\n throw new Error('Order ID not found!');\n }\n if(!matches[1]) {\n throw new Error('Date / Time not found!');\n }\n\n let timezone = matches[1][4];\n if(timezone === 'MEZ') {\n timezone = 'MET';\n }\n\n const from = moment.tz(matches[1][1] + ' ' + matches[1][2], 'DD.MM.YY HH:mm', timezone);\n const to = moment.tz(matches[1][1] + ' ' + matches[1][3], 'DD.MM.YY HH:mm', timezone);\n\n return {\n type: 'change',\n orderId: matches[0][1].trim(),\n time: {\n from,\n to\n }\n };\n }\n\n private static parseCancellationMail(email: ParsedMail): CancellationMail | undefined {\n const subject = email.subject || '';\n const match = subject.match(/\\((\\w+)\\)/);\n if(match) {\n return {\n type: 'cancel',\n orderId: match[1],\n cancelledAt: moment(email.date)\n };\n }\n }\n\n private static parseInvoiceMail(email: ParsedMail): InvoiceMail {\n const html = email.html || '';\n const match = html.match(/Die Rechnung für deine Bestellung (\\w+)/);\n if(match) {\n return {\n type: 'invoice',\n orderId: match[1],\n invoicedAt: moment(email.date)\n };\n }\n\n throw new Error('Order Id not found!');\n }\n\n private static async applyParsedMail(email: AnyMail): Promise<void> {\n const user = await this.findUser(email.to);\n\n if(email.type === 'order') {\n const location = await this.getLocation(email.location);\n\n await prisma.event.upsert({\n where: {\n orderId: email.orderId,\n userId: user.id\n },\n create: {\n orderId: email.orderId,\n orderedAt: email.time.order.toDate(),\n from: email.time.from.toDate(),\n to: email.time.to.toDate(),\n amount: email.amount,\n price: email.price,\n user: {\n connect: {\n id: user.id\n }\n },\n location: {\n connect: {\n id: location.id\n }\n }\n },\n update: {\n from: email.time.from.toDate(),\n to: email.time.to.toDate(),\n amount: email.amount,\n price: email.price,\n location: {\n connect: {\n id: location.id\n }\n }\n }\n });\n }\n else if(email.type === 'change') {\n await prisma.event.update({\n where: {\n orderId: email.orderId,\n userId: user.id\n },\n data: {\n from: email.time.from.toDate(),\n to: email.time.to.toDate()\n }\n });\n }\n else if (email.type === 'cancel') {\n await prisma.event.update({\n where: {\n orderId: email.orderId,\n userId: user.id\n },\n data: {\n canceledAt: email.cancelledAt.toDate()\n }\n });\n }\n else if (email.type === 'invoice') {\n await prisma.event.update({\n where: {\n orderId: email.orderId,\n userId: user.id\n },\n data: {\n invoicedAt: email.invoicedAt.toDate()\n }\n });\n }\n else {\n throw new Error('Unknown email type!');\n }\n }\n\n private static async getLocation(input: OrderMail['location']): Promise<Location> {\n let location = await prisma.location.findFirst({\n where: {\n name: input.name,\n address: input.address\n }\n });\n if(!location) {\n const emoji = getEmoji(input.name);\n const { latitude, longitude } = await this.geocode(input.address);\n location = await prisma.location.create({\n data: {\n name: input.name,\n address: input.address,\n latitude,\n longitude,\n emoji\n }\n });\n }\n\n return location;\n }\n\n public static async geocode(address: string): Promise<{ latitude: number, longitude: number } | { latitude: null, longitude: null }> {\n const response = await fetch('https://nominatim.openstreetmap.org/search?format=json&limit=1&q=' + encodeURIComponent(address), {\n headers: {\n 'User-Agent': `tgtg-ical/${config.version} (${config.baseUrl})`,\n 'Referer': config.baseUrl\n }\n });\n if(!response.ok) {\n throw new Error('Geocoding failed: ' + response.statusText);\n }\n\n // super simple way to ensure a maximum of 1 request per second in cronjobs\n await new Promise(resolve => setTimeout(resolve, 1000));\n\n const data = await response.json();\n if(!Array.isArray(data) || data.length === 0) {\n return {\n latitude: null,\n longitude: null\n };\n }\n\n return {\n latitude: parseFloat(data[0].lat),\n longitude: parseFloat(data[0].lon)\n };\n }\n}\n","export const DEFAULT_EMOJI = '🍴';\nexport const EMOJIS: Record<string, RegExp[]> = {\n '🍔': [\n /Burger/,\n /McDonald[‘|']?s/,\n /Burger King/,\n /Hans im Glück/,\n /Peter Pane/,\n\n ],\n '🥪': [\n /Sandwich/,\n /Tank ?& ?Rast/,\n /T&R Raststätten/,\n /Autohöfe/,\n /Aral/,\n /Shell/,\n /Jet Tank/,\n /Subway/,\n /Caf[é|e] bonjour/,\n /Total Deutschland/,\n /Esso:? Snack ?& ?Shop/,\n\n ],\n '🍕': [\n /Domino‘s/,\n /Pizza Hut/,\n /L[‘|']Osteria/,\n /Call a Pizza/,\n /Smiley[‘|']s Pizza/,\n\n ],\n '🍗': [\n /KFC/,\n\n ],\n '🍝': [\n /LaTagliatella/,\n /Vapiano/,\n\n ],\n '🍤': [\n /Nordsee/\n ],\n '🥩': [\n /Steakhouse/,\n /Block House/,\n /Jim Block/,\n\n ],\n '🌮': [\n /Enchilada/,\n /Besitos/,\n\n ],\n '🌭': [\n /Ikea/,\n\n ],\n '🥗': [\n /dean ?& ?david/\n ],\n '🥖': [\n /Bäckerei/,\n /Back/,\n /BackWerk/,\n /Kamps/,\n /Kamps Backstuben/,\n /Junge Die Bäckerei/,\n /Back-Factory/,\n\n ],\n '🥨': [\n /Brezel/,\n /Ditsch/,\n\n ],\n '🥐': [\n /LeCroBag/,\n\n ],\n '🍪': [\n /Starbucks/\n ],\n '🍩': [\n /Donut/,\n /Dunkin/,\n\n ],\n '🛒': [\n /Edeka/,\n /Netto/,\n /Rewe/,\n /Penny/,\n /Lidl/,\n /Kaufland/,\n /Aldi/,\n /dm/,\n /Rossmann/,\n /Globus/,\n /Metro/,\n /Norma/,\n /Tegut/,\n\n ]\n};\n\nexport default function getEmoji(location: string): string | null {\n for (const emoji in EMOJIS) {\n const regExps = EMOJIS[emoji].map((name) => new RegExp(name, 'i'));\n const match = !!regExps.find(regExp => regExp.test(location));\n if(match) {\n return emoji;\n }\n }\n\n return null;\n}\n"],"mappings":"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","names":["PrismaClient","prisma","db_default","init","dirname","resolve","fileURLToPath","readFileSync","dir","src","path","version","error","config_default","captureException","simpleParser","he","moment","DEFAULT_EMOJI","EMOJIS","getEmoji","location","emoji","name","regExp","Parser","mails","db_default","config_default","mail","moment","email","parsed","error","errorId","captureException","baseMailPostfix","simpleParser","to","address","received","regexp","r","match","order","change","cancellation","invoice","prefix","user","html","matches","timezone","now","from","year","amount","price","name","he","location","input","emoji","getEmoji","latitude","longitude","response","resolve","data"]}