Skip to content

Commit

Permalink
Convert resume pdfs to images (#270)
Browse files Browse the repository at this point in the history
* initial proof of concept

* intermediate commit

* added functionality

* added check if no attachments

* fix package.json

* implemented handling for 2 pages

* added mkdir tmp

* fixed too many pages, updated dockerfiles

* addressed comments

* added vars.json
  • Loading branch information
mcpenguin authored Aug 10, 2022
1 parent 0a20c06 commit ebbc359
Show file tree
Hide file tree
Showing 11 changed files with 206 additions and 7 deletions.
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
.env
.env
/tmp
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
!/db/.gitkeep
yarn-error.log
**/.DS_Store
/config/dev
/config/dev
/tmp
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ FROM node:18-alpine
# Create app directory
WORKDIR /usr/app

RUN mkdir ./tmp
# Install the dependencies needed for pdf2pic and calipers
RUN apk add --update ghostscript
RUN apk add --update graphicsmagick

# Install app dependencies
COPY package.json yarn.lock ./
RUN yarn --frozen-lockfile
Expand Down
3 changes: 2 additions & 1 deletion config/production/vars.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"COFFEE_ROLE_ID": "936899047816589354",
"NOTIF_CHANNEL_ID": "872305316522508329",
"ANNOUNCEMENTS_CHANNEL_ID": "669294029804011540",
"OFFICE_STATUS_CHANNEL_ID": "997926133083422740"
"OFFICE_STATUS_CHANNEL_ID": "997926133083422740",
"RESUME_CHANNEL_ID": "824068952933924924"
}
3 changes: 2 additions & 1 deletion config/staging/vars.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"COFFEE_ROLE_ID": "861744590775779358",
"NOTIF_CHANNEL_ID": "866861080301797386",
"ANNOUNCEMENTS_CHANNEL_ID": "892952559520710677",
"OFFICE_CHANNEL_ID": "866861080301797386"
"OFFICE_CHANNEL_ID": "866861080301797386",
"RESUME_CHANNEL_ID": "866861080301797386"
}
3 changes: 3 additions & 0 deletions docker/production/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Stage 1: Typescript build app
FROM node:18-alpine AS ts-build
WORKDIR /usr/app
RUN apk add --update ghostscript
RUN apk add --update graphicsmagick
RUN mkdir ./tmp
COPY package.json yarn.lock ./
RUN yarn --frozen-lockfile
COPY . .
Expand Down
3 changes: 3 additions & 0 deletions docker/staging/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Stage 1: Typescript build app
FROM node:18-alpine AS ts-build
WORKDIR /usr/app
RUN apk add --update ghostscript
RUN apk add --update graphicsmagick
RUN mkdir ./tmp
COPY package.json yarn.lock ./
RUN yarn --frozen-lockfile
COPY . .
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"lodash": "^4.17.21",
"moment": "^2.29.1",
"node-fetch": "^2.6.2",
"pdf-lib": "^1.17.1",
"pdf2pic": "^2.1.4",
"sqlite": "^4.0.22",
"sqlite3": "^5.0.2",
"stable-marriage": "^1.0.2",
Expand Down
41 changes: 41 additions & 0 deletions src/listeners/messageCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ import { Message } from 'discord.js';
import { applyBonusByUserId } from '../components/coin';
import { vars } from '../config';
import { sendKickEmbed } from '../utils/embeds';
import { convertPdfToPic } from '../utils/pdfToPic';

import { readFileSync } from 'fs';
import { writeFile } from 'fs/promises';
import axios from 'axios';
import { PDFDocument } from 'pdf-lib';

const ANNOUNCEMENTS_CHANNEL_ID: string = vars.ANNOUNCEMENTS_CHANNEL_ID;
const RESUME_CHANNEL_ID: string = vars.RESUME_CHANNEL_ID;

/*
* If honeypot is to exist again, then add HONEYPOT_CHANNEL_ID to the config
Expand Down Expand Up @@ -60,6 +67,35 @@ const punishSpammersAndTrolls = async (message: Message): Promise<boolean> => {
return false;
};

/**
* Convert any pdfs sent in the #resumes channel to an image.
*/
const convertResumePdfsIntoImages = async (message: Message): Promise<Message<boolean> | undefined> => {
// If no resume pdf is provided, do nothing
const attachments = message.attachments;
if (attachments.size === 0) return;

// Get resume pdf from message and write locally to tmp
const pdfLink = Array.from(attachments.values()).map((file) => file.attachment)[0];
const pdfResponse = await axios.get(pdfLink, { responseType: 'stream' });
const pdfContent = pdfResponse.data;
await writeFile('tmp/resume.pdf', pdfContent);

// Get the size of the pdf
const pdfDocument = await PDFDocument.load(readFileSync('tmp/resume.pdf'));
const { width, height } = pdfDocument.getPage(0).getSize();
if (pdfDocument.getPageCount() > 10) {
return await message.channel.send('Resumes must be less than 10 pages.');
}

// Convert the resume pdf into image
const imgResponse = await convertPdfToPic('tmp/resume.pdf', 'resume', width * 2, height * 2);
// Send the image back to the channel
return await message.channel.send({
files: imgResponse.map((img) => img.path)
});
};

@ApplyOptions<Listener.Options>({
event: 'messageCreate'
})
Expand All @@ -80,6 +116,11 @@ export class MessageCreateListener extends Listener {
return;
}

// If channel is in resumes, convert the message attachment to an image
if (message.channelId === RESUME_CHANNEL_ID) {
await convertResumePdfsIntoImages(message);
}

// Ignore DMs; include announcements, thread, and regular text channels
if (message.channel.type !== 'DM') {
await applyBonusByUserId(message.author.id);
Expand Down
22 changes: 22 additions & 0 deletions src/utils/pdfToPic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { fromPath } from 'pdf2pic';
import { WriteImageResponse } from 'pdf2pic/dist/types/writeImageResponse';

export const convertPdfToPic = async (
filePath: string,
saveFileName: string,
width: number,
height: number
): Promise<WriteImageResponse[]> => {
const options = {
density: 500,
saveFilename: saveFileName,
savePath: './tmp',
format: 'png',
width: width,
height: height
};

const convert = fromPath(filePath, options);
const res = await convert.bulk!(-1);
return res;
};
125 changes: 122 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,20 @@
mkdirp "^1.0.4"
rimraf "^3.0.2"

"@pdf-lib/standard-fonts@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz#8ba691c4421f71662ed07c9a0294b44528af2d7f"
integrity sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==
dependencies:
pako "^1.0.6"

"@pdf-lib/upng@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@pdf-lib/upng/-/upng-1.0.1.tgz#7dc9c636271aca007a9df4deaf2dd7e7960280cb"
integrity sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==
dependencies:
pako "^1.0.10"

"@sapphire/async-queue@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.3.1.tgz#9d861e626dbffae02d808e13f823d4510e450a78"
Expand Down Expand Up @@ -514,6 +528,16 @@ argparse@^2.0.1:
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==

array-parallel@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/array-parallel/-/array-parallel-0.1.3.tgz#8f785308926ed5aa478c47e64d1b334b6c0c947d"
integrity sha512-TDPTwSWW5E4oiFiKmz6RGJ/a80Y91GuLgUYuLd49+XBS75tYo8PNgaT2K/OxuQYqkoI852MDGBorg9OcUSTQ8w==

array-series@~0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/array-series/-/array-series-0.1.5.tgz#df5d37bfc5c2ef0755e2aa4f92feae7d4b5a972f"
integrity sha512-L0XlBwfx9QetHOsbLDrE/vh2t018w9462HM3iaFfxRiK83aJjAt/Ja3NMkOW7FICwWTlQBa3ZbL5FKhuQWkDrg==

array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
Expand All @@ -529,6 +553,11 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==

at-least-node@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==

axios@^0.27.2:
version "0.27.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972"
Expand Down Expand Up @@ -714,6 +743,14 @@ cron@^1.8.2:
dependencies:
luxon "^1.23.x"

cross-spawn@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41"
integrity sha512-yAXz/pA1tD8Gtg2S98Ekf/sewp3Lcp3YoFKJ4Hkp5h5yLWnKVTDU0kwjKJ8NDCYcfTLfyGkzTikst+jWypT1iA==
dependencies:
lru-cache "^4.0.1"
which "^1.2.9"

cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
Expand All @@ -735,7 +772,7 @@ debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.3:
dependencies:
ms "2.1.2"

debug@^3.2.7:
debug@^3.1.0, debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
Expand Down Expand Up @@ -1101,6 +1138,16 @@ from@~0:
resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==

fs-extra@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
dependencies:
at-least-node "^1.0.0"
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"

fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
Expand Down Expand Up @@ -1195,7 +1242,17 @@ globby@^11.0.3:
merge2 "^1.4.1"
slash "^3.0.0"

graceful-fs@^4.2.6:
gm@^1.23.1:
version "1.23.1"
resolved "https://registry.yarnpkg.com/gm/-/gm-1.23.1.tgz#2edeeb958084d0f8ea7988e5d995b1c7dfc14777"
integrity sha512-wYGVAa8/sh9ggF5qWoOs6eArcAgwEPkDNvf637jHRHkMUznvs7m/Q2vrc0KLN6B8px3nnRJqJcXK4mTK6lLFmg==
dependencies:
array-parallel "~0.1.3"
array-series "~0.1.5"
cross-spawn "^4.0.0"
debug "^3.1.0"

graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.6:
version "4.2.10"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
Expand Down Expand Up @@ -1381,6 +1438,15 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==

jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
dependencies:
universalify "^2.0.0"
optionalDependencies:
graceful-fs "^4.1.6"

levn@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
Expand Down Expand Up @@ -1414,6 +1480,14 @@ lodash@^4.17.21:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==

lru-cache@^4.0.1:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
dependencies:
pseudomap "^1.0.2"
yallist "^2.1.2"

lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
Expand Down Expand Up @@ -1695,6 +1769,11 @@ p-map@^4.0.0:
dependencies:
aggregate-error "^3.0.0"

pako@^1.0.10, pako@^1.0.11, pako@^1.0.6:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==

parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
Expand Down Expand Up @@ -1724,6 +1803,24 @@ [email protected]:
dependencies:
through "~2.3"

pdf-lib@^1.17.1:
version "1.17.1"
resolved "https://registry.yarnpkg.com/pdf-lib/-/pdf-lib-1.17.1.tgz#9e7dd21261a0c1fb17992580885b39e7d08f451f"
integrity sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==
dependencies:
"@pdf-lib/standard-fonts" "^1.0.0"
"@pdf-lib/upng" "^1.0.1"
pako "^1.0.11"
tslib "^1.11.1"

pdf2pic@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/pdf2pic/-/pdf2pic-2.1.4.tgz#5c6cbd7809a60b11f6504eb85c386aa2012b1dcf"
integrity sha512-8QOCvxZlYs7wsmBQbqpIApSWD/eqgXMQU+4VM0vWrXifLgXS+Ns3484sUa1Q7ddr+gwnruq/xKHzu7x1scqX6w==
dependencies:
fs-extra "^9.1.0"
gm "^1.23.1"

picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
Expand Down Expand Up @@ -1771,6 +1868,11 @@ ps-tree@^1.2.0:
dependencies:
event-stream "=3.3.4"

pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==

pstree.remy@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
Expand Down Expand Up @@ -2129,7 +2231,7 @@ tsc-watch@^4.4.0:
string-argv "^0.1.1"
strip-ansi "^6.0.0"

tslib@^1.8.1, tslib@^1.9.0:
tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
Expand Down Expand Up @@ -2182,6 +2284,11 @@ unique-slug@^2.0.0:
dependencies:
imurmurhash "^0.1.4"

universalify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==

uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
Expand Down Expand Up @@ -2212,6 +2319,13 @@ whatwg-url@^5.0.0:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"

which@^1.2.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
dependencies:
isexe "^2.0.0"

which@^2.0.1, which@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
Expand Down Expand Up @@ -2255,6 +2369,11 @@ y18n@^5.0.5:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==

yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==

yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
Expand Down

0 comments on commit ebbc359

Please sign in to comment.