const readline = require('./fix-readline.js') const nodemailer = require('nodemailer') const fs = require('fs'); const {google} = require('googleapis'); const r2 = require('r2') const qs = require('querystring') const {promisify} = require('util') const readFile = promisify(fs.readFile) const base64url = require('base64url') const process = require('process') const path = require('path') const exec = promisify(require('child_process').exec) const {spawn} = require('child_process') // If modifying these scopes, delete token.json. const SCOPES = ['https://www.googleapis.com/auth/drive','https://www.googleapis.com/auth/gmail.compose']; const USER = process.env['USER'] const HOME = process.env['HOME'] const UserCapitalized = USER.charAt(0).toUpperCase() + USER.slice(1) const config = { fileId: "1CqUglyFNoEWL0lx-e7arqUGOOZEYW__JjcwBmNaXE6E", template: `${UserCapitalized} ${(new Date()).getFullYear()} Template`, to: "billing@serokell.io", destdir: `${HOME}/serokell/invoices/payslips` } async function authAll() { const {stdout, stderr} = await exec(`gpg --decrypt < ${__dirname}/credentials.json.gpg`) return authorize(JSON.parse(stdout)) } async function authorize(credentials) { const {client_secret, client_id, redirect_uris} = credentials.installed; const oAuth2Client = new google.auth.OAuth2( client_id, client_secret, redirect_uris[0]); // Check if we have previously stored a token. let token try { token = await pass_get("skl-auto-payslip") } catch(e) { return getAccessToken(oAuth2Client) } oAuth2Client.setCredentials(JSON.parse(token)); return oAuth2Client } async function ask(question) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const answer = await rl.questionAsync(question) rl.close() return answer } function pass_insert(name, val) { return new Promise((resolve, reject) => { const pass = spawn("pass", ['insert', 'skl-auto-payslip', '-m'], {stdio: ['pipe', 'inherit', 'inherit']}) pass.stdin.write(val) pass.stdin.end() pass.on('close', (code) => { if (code != 0) reject(`pass exited with code ${code}`) else { resolve() } }) }) } async function pass_get(name) { const {stdout, stderr} = await exec("pass skl-auto-payslip") return stdout } async function getAccessToken(oAuth2Client) { const authUrl = oAuth2Client.generateAuthUrl({ access_type: 'offline', scope: SCOPES, }); console.log('Authorize this app by visiting this url:', authUrl); const code = await ask('Enter the code from that page here: ') const token = (await oAuth2Client.getToken(code)).tokens oAuth2Client.setCredentials(token); await pass_insert('skl-auto-payslip', JSON.stringify(token)) return oAuth2Client } class InvoiceDocument { constructor(auth, fileId) { Object.assign(this, { auth, fileId, sheets: google.sheets({version: 'v4', auth}), }) } async sheetByName(sheetname) { const {sheets} = (await this.sheets.spreadsheets.get({ spreadsheetId: this.fileId, includeGridData: false })).data const r = sheets.find(({properties: {title}}) => title == sheetname) return r ? r.properties.sheetId : r } async sheetAsPdf(destFile, gid) { const params = { format: 'pdf', size: 7, fzr: true, portrait: true, fitw: false, gridlines: false, printtitle: false, sheetnames: false, pagenum: "UNDEFINED", attachment: false, gid, printnotes: false } const token = this.auth.credentials.access_token const dest = fs.createWriteStream(destFile) // thanks, https://gist.github.com/Spencer-Easton/78f9867a691e549c9c70#gistcomment-2622947 const res = await r2(`https://docs.google.com/spreadsheets/d/${this.fileId}/export?${qs.stringify(params)}`, {headers: {"Authorization": `Bearer ${token}`}}).response res.body.pipe(dest) return new Promise((resolve, reject) => { res.body.on('end', resolve) .on('error', reject) }) } async copyTo(src, dest) { const templateId = await this.sheetByName(src) if (!templateId) throw "template sheet not found" const res = await this.sheets.spreadsheets.sheets.copyTo({ spreadsheetId: this.fileId, sheetId: templateId, resource: { destinationSpreadsheetId: this.fileId } }) const updres = await this.sheets.spreadsheets.batchUpdate({ spreadsheetId: this.fileId, resource: { requests: [{ updateSheetProperties: { fields: "title", properties: { sheetId: res.data.sheetId, title: dest } } }] } }) } async set(sheetname, vals) { await this.sheets.spreadsheets.values.batchUpdate({ spreadsheetId: this.fileId, resource: { valueInputOption: "USER_ENTERED", data: Object.keys(vals).map(range => ({range: `'${sheetname}'!${range}`, values: [[vals[range]]]})) } }) } async get(sheetname, cell) { const res = await this.sheets.spreadsheets.values.get({ spreadsheetId: this.fileId, range: `'${sheetname}'!${cell}` }) return res.data.values } } async function composeDraft(title, iban, total, pdf, auth) { const gmail = google.gmail({version: 'v1', auth}) const user = await gmail.users.getProfile({userId: 'me'}) const email = user.data.emailAddress const transporter = nodemailer.createTransport({ streamTransport: true, newline: 'unix' }) const body = `Hi! The total is ${total} to ${iban} (same as always). Thanks!` // TODO: check if same as always const msg = { from: email, to: config.to, subject: `payslip - ${title}`, text: body, attachments: [{ filename: `payslip - ${title}.pdf`, path: pdf }] } transporter.sendMailAsync = promisify(transporter.sendMail) const info = await transporter.sendMailAsync(msg) const pdfStream = fs.createReadStream(pdf) const draft = await gmail.users.drafts.create({ userId: 'me', media: {mimeType: 'message/rfc822', body: info.message} }) console.log(`Draft created, please review and send: https://mail.google.com/mail/u/${email}/#drafts?compose=${draft.data.message.id}`) } const datefmt = date => `${date.getFullYear()}-${(""+(date.getMonth()+1)).padStart(2, '0')}-${(""+date.getDate()).padStart(2, '0')}` class App { constructor(auth) { this.auth = auth this.doc = new InvoiceDocument(auth, config.fileId) } async export(sheetname, pdfOut) { // save sheet as pdf console.log("Exporting", sheetname, "to", pdfOut) const sheetId = await this.doc.sheetByName(sheetname) return this.doc.sheetAsPdf(pdfOut, sheetId) } async create(sheetname, template=config.template) { // copy sheet from template console.log("Creating", sheetname, "from", template) return this.doc.copyTo(template, sheetname) } async update(sheetname, startDate, endDate, hours) { // set values in sheet console.log("Setting data in", sheetname) hours = +hours const values = { regular: Math.min(hours, 80), overwork: Math.max(0, hours-80), startDate, endDate } console.log(values) this.doc.set(sheetname, { D16: values.regular, D17: values.overwork, B9: values.startDate, D9: values.endDate }) } async draft(sheetname) { // export pdf+create gmail draft const pdfname = `${config.destdir}/${sheetname}.pdf` try { await promisify(fs.mkdir)(config.destdir, {recursive: true}) } catch(e) { if (e.code != 'EEXIST') throw e } await this.export(sheetname, pdfname) const [[paid,,,, total]] = await this.doc.get(sheetname, "B33:F33") const iban = paid.replace(/^Paid to /, '') console.log(paid, total) composeDraft(sheetname, iban, total, pdfname, this.auth) } async auto(hours) { // just do everything const date = new Date() const day = date.getDate() let part = 'B' if (day >= 10 && day < 28) { part = 'A' } else if (day < 10) { // talking about last month date.setMonth(date.getMonth()-1) } const month = date.toLocaleString('en-us', {month: 'short'}) const sheetname = `${UserCapitalized} ${date.getFullYear()} ${month} ${part}` let startDate, endDate if (part == 'A') { date.setDate(1) startDate = datefmt(date) date.setDate(15) endDate = datefmt(date) } else { date.setDate(16) startDate = datefmt(date) date.setMonth(date.getMonth()+1, 0) endDate = datefmt(date) } const sheetId = await this.doc.sheetByName(sheetname) if (!sheetId) { await this.create(sheetname) } else { const answer = await ask(`Overwrite '${sheetname}'? [yN]: `) if (answer.trim().toLowerCase()[0] != 'y') { console.log("Aborting!") return null } } await this.update(sheetname, startDate, endDate, hours) await this.draft(sheetname) } async authenticate() { // just auth } } module.exports = App module.exports.authAll = authAll