@sebbo2002/tgtg-ical 1.0.0-develop.0 → 1.0.0-develop.2
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/LICENSE +1 -1
- package/README.md +31 -1
- package/dist/start.js +1 -1
- package/dist/start.js.map +1 -1
- package/package.json +5 -4
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2023 Sebastian Pekarek
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
6
6
|
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
package/README.md
CHANGED
|
@@ -1,10 +1,40 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tgtg-ical
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
4
|
|
|
5
5
|
A small server that receives mails from TGTG, parses them and generates an iCal feed from them.
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
## 📦 Installation
|
|
9
|
+
|
|
10
|
+
git clone https://github.com/sebbo2002/tgtg-ical.git
|
|
11
|
+
cd ./tgtg-ical
|
|
12
|
+
|
|
13
|
+
echo 'DATABASE_URL="mysql://root@localhost:3306/tgtg-ical"' > .env
|
|
14
|
+
|
|
15
|
+
npm install
|
|
16
|
+
npx prisma migrate deploy
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## 🙋 FAQ
|
|
20
|
+
|
|
21
|
+
### 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
|
|
24
|
+
rule), collection appointments will be displayed in the corresponding calendar feed.
|
|
25
|
+
|
|
26
|
+
### Which languages are supported?
|
|
27
|
+
Currently only German and English are supported. If you want to add another language, feel free to create a pull request.
|
|
28
|
+
|
|
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
|
|
32
|
+
two weeks at the latest.
|
|
33
|
+
|
|
34
|
+
### 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
|
+
|
|
37
|
+
|
|
8
38
|
## 🙆🏼♂️ Copyright and license
|
|
9
39
|
|
|
10
40
|
Copyright (c) Sebastian Pekarek under the [MIT license](LICENSE).
|
package/dist/start.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import{a as i,b as o,c as d}from"./chunk-HTOHNK4X.js";import p from"express";import y from"cookie-parser";import{randomUUID as h}from"crypto";import{generateName as f,generateNameWithNumber as g}from"@criblinc/docker-names";import{ICalAlarmType as w,ICalCalendar as E,ICalEventStatus as l}from"ical-generator";import I 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 i.user.create({data:{prefix:r}}),e)break}catch(a){if(a.code==="P2002")continue;throw a}}if(!e)throw new Error("User not created");return e}static generatePrefix(e=0){return e>100?h():e<10?f():g()}static async getUser(e){let t=await i.user.findUniqueOrThrow({where:{id:e}});return this.updateUserLastSeen(t.id),t}static updateUserLastSeen(e){i.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(o.src("templates/user.html"),"utf-8")]);return r.replace(/\${CALENDAR_URL}/g,`${o.baseUrl}/${t.id}/calendar.ical`).replace(/\${EMAIL_ADDRESS}/g,`${t.prefix}${o.baseMail}`)}static async generateCalendar(e){let t=await i.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 m=new Intl.NumberFormat("de-DE",{style:"currency",currency:"EUR"}).format(a.price/100),u=l.CONFIRMED;return a.canceledAt&&(u=l.CANCELLED),{id:a.id,start:a.from,end:a.to,timestamp:a.createdAt,summary:`${a.location.emoji||d} ${a.location.name}`,description:`${a.amount}x
|
|
3
|
-
${m}`,url:`https://share.toogoodtogo.com/receipts/details/${a.orderId}`,status:u,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:w.display,trigger:600}]}})});return this.updateUserLastSeen(e),r.toString()}static async isHealthy(){let e=await i.mail.count({where:{createdAt:{gte:I().subtract(30,"minutes").toDate()}}});if(e>0)throw new Error(`There are ${e} unahandled mails in the queue!`)}};
|
|
3
|
+
${m}`,url:`https://share.toogoodtogo.com/receipts/details/${a.orderId}`,status:u,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:w.display,trigger:600}]}})});return this.updateUserLastSeen(e),r.toString()}static async isHealthy(){let e=await i.mail.count({where:{createdAt:{gte:I().subtract(30,"minutes").toDate()}}});if(e>0)throw new Error(`There are ${e} unahandled mails in the queue!`)}};var n=class c{static run(){new c}app;server;constructor(){this.app=p(),this.app.use(y()),this.setupRoutes(),this.server=this.app.listen(process.env.PORT||8080),process.on("SIGINT",()=>this.stop()),process.on("SIGTERM",()=>this.stop())}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(p.static(o.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(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 i.$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, { Express, 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 { Prisma } from '@prisma/client';\nimport Config from '../lib/config';\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\n process.on('SIGINT', () => this.stop());\n process.on('SIGTERM', () => this.stop());\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(201))\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: Prisma.PrismaClientKnownRequestError | unknown, res: Response) {\n if(error instanceof Prisma.PrismaClientKnownRequestError && 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 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.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 gte: moment().subtract(30, 'minutes').toDate()\n }\n }\n });\n if(c > 0) {\n throw new Error(`There are ${c} unahandled 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","Prisma","AppServer","_AppServer","express","cookieParser","req","res","ServerLib","user","error","config_default","html","json","ical","Prisma","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 express, { Express, 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';\nimport type { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';\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\n process.on('SIGINT', () => this.stop());\n process.on('SIGTERM', () => this.stop());\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(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 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.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 gte: moment().subtract(30, 'minutes').toDate()\n }\n }\n });\n if(c > 0) {\n throw new Error(`There are ${c} unahandled 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","req","res","ServerLib","user","error","config_default","html","json","ical","cb","db_default"]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Sebastian Pekarek <mail@sebbo.net>",
|
|
3
3
|
"bin": {
|
|
4
|
-
"tgtg-ical-
|
|
5
|
-
"tgtg-ical-
|
|
4
|
+
"tgtg-ical-inhale-mail": "./dist/bin/inhale-mail.js",
|
|
5
|
+
"tgtg-ical-server": "./dist/start.js"
|
|
6
6
|
},
|
|
7
7
|
"bugs": {
|
|
8
8
|
"url": "https://github.com/sebbo2002/tgtg-ical/issues"
|
|
@@ -68,12 +68,13 @@
|
|
|
68
68
|
"build": "tsup",
|
|
69
69
|
"build-all": "./.github/workflows/build.sh",
|
|
70
70
|
"coverage": "c8 mocha",
|
|
71
|
+
"deploy": "./.github/workflows/deploy.sh",
|
|
71
72
|
"develop": "ts-node ./src/bin/start.ts",
|
|
72
73
|
"license-check": "license-checker --production --summary",
|
|
73
74
|
"lint": "eslint . --ext .ts,.json",
|
|
74
|
-
"start": "node ./dist/
|
|
75
|
+
"start": "node ./dist/start.js",
|
|
75
76
|
"test": "mocha"
|
|
76
77
|
},
|
|
77
78
|
"type": "module",
|
|
78
|
-
"version": "1.0.0-develop.
|
|
79
|
+
"version": "1.0.0-develop.2"
|
|
79
80
|
}
|