From 9b2e960999868ea8416aea687a87eb2daf58c5ab Mon Sep 17 00:00:00 2001 From: James Date: Sat, 31 Oct 2020 15:46:18 +1300 Subject: [PATCH 01/10] Add framework to preview share email --- .gitignore | 2 + cli/emailPreview.js | 17 +++++ {fixtures => cli/fixtures}/original.ical | 0 cli/fixtures/share.yml | 17 +++++ cli/package.json | 16 +++++ cli/yarn.lock | 85 ++++++++++++++++++++++++ functions/locales/en.yml | 8 +++ functions/templates/share.hbs | 60 +++++++++++++++++ 8 files changed, 205 insertions(+) create mode 100644 cli/emailPreview.js rename {fixtures => cli/fixtures}/original.ical (100%) create mode 100644 cli/fixtures/share.yml create mode 100644 cli/package.json create mode 100644 cli/yarn.lock create mode 100644 functions/templates/share.hbs diff --git a/.gitignore b/.gitignore index b99ac66..3f745cc 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,5 @@ node_modules/ # dotenv environment variables file .env + +cli/dist diff --git a/cli/emailPreview.js b/cli/emailPreview.js new file mode 100644 index 0000000..bb0e4c8 --- /dev/null +++ b/cli/emailPreview.js @@ -0,0 +1,17 @@ +const { compile } = require('handlebars') +const { readFileSync, writeFile } = require('fs') +const open = require('open') +const { safeLoad } = require('js-yaml') + +const templateName = process.argv[2] +const template = readFileSync(`../functions/templates/${templateName}.hbs`).toString() +const fixture = { + ...safeLoad(readFileSync(`./fixtures/${templateName}.yml`)), + translations: safeLoad(readFileSync(`../functions/locales/en.yml`)) +} + +writeFile( + `./dist/${templateName}.html`, + compile(template)(fixture), + (arg) => console.log(arg) || open(`dist/${templateName}.html`) +) diff --git a/fixtures/original.ical b/cli/fixtures/original.ical similarity index 100% rename from fixtures/original.ical rename to cli/fixtures/original.ical diff --git a/cli/fixtures/share.yml b/cli/fixtures/share.yml new file mode 100644 index 0000000..7259f87 --- /dev/null +++ b/cli/fixtures/share.yml @@ -0,0 +1,17 @@ +cadences: + - id: daily + name: Daily + ceremonies: + - id: checkin + name: Check in + async: true + notes: We'll do a synchronous stand-up in Slack, with one person bringing a 'Question of the Day' + - id: quarterly + name: Quarterly + ceremonies: + - id: documentation + async: true + name: Documentation + - id: hackathon + name: Hackathon + notes: To be led by the Team leads to gather ideas, we'll take 2 days to spike out some features diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 0000000..dc07034 --- /dev/null +++ b/cli/package.json @@ -0,0 +1,16 @@ +{ + "name": "cli", + "description": "Testing CLI for Minimum Viable Ceremonies", + "engines": { + "node": "10" + }, + "scripts": { + "preview": "node ./emailPreview.js" + }, + "private": true, + "dependencies": { + "handlebars": "^4.7.6", + "js-yaml": "^3.14.0", + "open": "^7.3.0" + } +} diff --git a/cli/yarn.lock b/cli/yarn.lock new file mode 100644 index 0000000..7efe509 --- /dev/null +++ b/cli/yarn.lock @@ -0,0 +1,85 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +handlebars@^4.7.6: + version "4.7.6" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" + integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +is-docker@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156" + integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== + +is-wsl@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +js-yaml@^3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +neo-async@^2.6.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +open@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/open/-/open-7.3.0.tgz#45461fdee46444f3645b6e14eb3ca94b82e1be69" + integrity sha512-mgLwQIx2F/ye9SmbrUkurZCnkoXyXyu9EbHtJZrICjVAJfyMArdHp3KkixGdZx1ZHFPNIwl0DDM1dFFqXbTLZw== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +uglify-js@^3.1.4: + version "3.11.4" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.11.4.tgz#b47b7ae99d4bd1dca65b53aaa69caa0909e6fadf" + integrity sha512-FyYnoxVL1D6+jDGQpbK5jW6y/2JlVfRfEeQ67BPCUg5wfCjaKOpr2XeceE4QL+MkhxliLtf5EbrMDZgzpt2CNw== + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= diff --git a/functions/locales/en.yml b/functions/locales/en.yml index b794ce1..57111df 100644 --- a/functions/locales/en.yml +++ b/functions/locales/en.yml @@ -17,3 +17,11 @@ common: name: Name is required uuid: Uuuid is required 400: bad request +templates: + share: + title: Congratulations! It's an agile process! + helptext: Here's a summary of the process your team just agreed on with + noNotes: (No agenda set!) + async: Async + unsubscribe: Unsubscribe + unsubscribePreferences: Unsubscribe preferences diff --git a/functions/templates/share.hbs b/functions/templates/share.hbs new file mode 100644 index 0000000..1b55b8c --- /dev/null +++ b/functions/templates/share.hbs @@ -0,0 +1,60 @@ + + + {{@root.translations.temaplates.share.title}} + + + +
+ {{@root.translations.templates.share.helptext}} + Minimum Viable Ceremonies + : +
+
+ {{#each cadences}} +
+

{{name}}

+ {{#each ceremonies}} +
+ + +
+ {{#if notes}} + {{notes}} + {{else}} + {{@root.translations.templates.share.noNotes}} + {{/if}} +
+
+ {{/each}} +
+ {{/each}} +
+
+

+ + {{@root.translations.templates.unsubscribe}} + + - + + {{@root.translations.templates.unsubscribePreferences}} + +

+
+ + From b679be1b0665a4666185304a8db9515c10411ee2 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 2 Nov 2020 22:30:19 +1300 Subject: [PATCH 02/10] Email design improvements --- cli/emailPreview.js | 4 +- cli/fixtures/share.yml | 6 +- functions/locales/en.yml | 13 +++-- functions/sendgrid.js | 40 +++++++++++++ functions/templates/share.hbs | 103 +++++++++++++++++++++------------- 5 files changed, 118 insertions(+), 48 deletions(-) diff --git a/cli/emailPreview.js b/cli/emailPreview.js index bb0e4c8..16c2a19 100644 --- a/cli/emailPreview.js +++ b/cli/emailPreview.js @@ -7,11 +7,11 @@ const templateName = process.argv[2] const template = readFileSync(`../functions/templates/${templateName}.hbs`).toString() const fixture = { ...safeLoad(readFileSync(`./fixtures/${templateName}.yml`)), - translations: safeLoad(readFileSync(`../functions/locales/en.yml`)) + translations: safeLoad(readFileSync(`../functions/locales/en.yml`)).templates[templateName], } writeFile( `./dist/${templateName}.html`, compile(template)(fixture), - (arg) => console.log(arg) || open(`dist/${templateName}.html`) + () => open(`dist/${templateName}.html`) ) diff --git a/cli/fixtures/share.yml b/cli/fixtures/share.yml index 7259f87..4ccff50 100644 --- a/cli/fixtures/share.yml +++ b/cli/fixtures/share.yml @@ -1,17 +1,15 @@ cadences: - id: daily - name: Daily ceremonies: - id: checkin name: Check in async: true notes: We'll do a synchronous stand-up in Slack, with one person bringing a 'Question of the Day' - id: quarterly - name: Quarterly ceremonies: - id: documentation async: true - name: Documentation - id: hackathon - name: Hackathon notes: To be led by the Team leads to gather ideas, we'll take 2 days to spike out some features + async: false + startTime: 9:00am diff --git a/functions/locales/en.yml b/functions/locales/en.yml index 57111df..a21d9fc 100644 --- a/functions/locales/en.yml +++ b/functions/locales/en.yml @@ -19,9 +19,14 @@ common: 400: bad request templates: share: - title: Congratulations! It's an agile process! - helptext: Here's a summary of the process your team just agreed on with + mvc: Minimum Viable Ceremonies + title: Congratulations! You made an agile process! + greeting: Congrats on completing your MVC session! 🎉 + encouragement: Now that you've got your process sorted, all that's left is getting on with it. + team: This process was completed with {{teamName}} on {{date}} + results: Your results noNotes: (No agenda set!) async: Async - unsubscribe: Unsubscribe - unsubscribePreferences: Unsubscribe preferences + unsubscribe: Unsubscribe + unsubscribePreferences: Unsubscribe preferences + backToRoom: Back to room diff --git a/functions/sendgrid.js b/functions/sendgrid.js index c5d6d72..362a7a7 100644 --- a/functions/sendgrid.js +++ b/functions/sendgrid.js @@ -1,4 +1,5 @@ const axios = require('axios') +const { database } = require('firebase-admin') const { https, config } = require('firebase-functions') const cors = require('cors') const { setLanguage } = require('./common') @@ -19,3 +20,42 @@ exports.subscribe = https.onRequest((req, res) => ( )) )) )) + +exports.share = https.onRequest((req, res) => ( + setLanguage(req).then(t => ( + cors({origin: config().mvc.cors_origin})(req, res, () => ( + database().ref(`/rooms/${req.body.uuid}/ceremonies`).once('value').then(snapshot => ( + axios.post('https://api.sendgrid.com/v3/mail/send', { + from: { email: "noreply@minimal.cards" }, + personalizations: [{ + to: [{ email: "james.kiesel@gmail.com" }], + dynamic_template_data: { + uuid: req.body.uuid, + cadences: transformCeremonies(snapshot.toJSON()), + translations: t('templates.share', { + userName: req.body.username, + teamName: snapshot.toJSON().name, + date: '10 Feb 2020', + returnObjects: true + }) + } + }], + template_id: config().sendgrid.share_template_id + }, { + headers: { + 'Authorization': `Bearer ${config().sendgrid.api_key}`, + 'Content-Type': 'application/json' + } + }).then(() => res.status(200).send({status: t('common.messages.200') })) + .catch(({ request, response: { status, data } }) => res.status(status).send(data)) + )) + )) + )) +)) + +const transformCeremonies = ceremonies => + Object.values(ceremonies).reduce((result, ceremony) => { + result[ceremony.placement] = result[ceremony.placement] || { id: ceremony.placement, ceremonies: [] } + result[ceremony.placement].ceremonies.push(ceremony) + return result + }, {}) diff --git a/functions/templates/share.hbs b/functions/templates/share.hbs index 1b55b8c..a9bce8d 100644 --- a/functions/templates/share.hbs +++ b/functions/templates/share.hbs @@ -1,58 +1,85 @@ - {{@root.translations.temaplates.share.title}} + {{@root.translations.title}} + + + -
- {{@root.translations.templates.share.helptext}} - Minimum Viable Ceremonies - : +
+ {{@root.translations.logoAlt}} +
+

{{@root.translations.mvc}}

+ https://minimal.cards +
+
+
+

{{@root.translations.greeting}}

+

{{@root.translations.encouragement}}

-
+
+

{{@root.translations.results}}

{{#each cadences}} -
-

{{name}}

- {{#each ceremonies}} -
- - -
- {{#if notes}} - {{notes}} - {{else}} - {{@root.translations.templates.share.noNotes}} +

{{id}}

+ {{#each ceremonies}} +
+
+
+ {{id}} + {{#if async}} + {{@root.translations.async}} + {{else if startTime}} + {{startTime}} {{/if}} -
+ +
+
+ {{#if notes}} + {{notes}} + {{else}} + {{@root.translations.noNotes}} + {{/if}}
- {{/each}} -
+
+ {{/each}} {{/each}}
+
+
+

+ {{@root.translations.team}} +

+ + {{@root.translations.backToRoom}} + +
+
From 9dc1f54f1f544a3e06302f572a756cbbf812f30d Mon Sep 17 00:00:00 2001 From: James Date: Mon, 2 Nov 2020 23:42:45 +1300 Subject: [PATCH 03/10] Add translations to email --- cli/emailPreview.js | 2 +- cli/fixtures/share.yml | 4 ++++ functions/sendgrid.js | 15 +++++++++++---- functions/templates/share.hbs | 4 ++-- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/cli/emailPreview.js b/cli/emailPreview.js index 16c2a19..539b6c1 100644 --- a/cli/emailPreview.js +++ b/cli/emailPreview.js @@ -7,7 +7,7 @@ const templateName = process.argv[2] const template = readFileSync(`../functions/templates/${templateName}.hbs`).toString() const fixture = { ...safeLoad(readFileSync(`./fixtures/${templateName}.yml`)), - translations: safeLoad(readFileSync(`../functions/locales/en.yml`)).templates[templateName], + translations: safeLoad(readFileSync(`../functions/locales/server.en.yml`)).templates[templateName] } writeFile( diff --git a/cli/fixtures/share.yml b/cli/fixtures/share.yml index 4ccff50..58ed289 100644 --- a/cli/fixtures/share.yml +++ b/cli/fixtures/share.yml @@ -1,15 +1,19 @@ cadences: - id: daily + name: Daily ceremonies: - id: checkin name: Check in async: true notes: We'll do a synchronous stand-up in Slack, with one person bringing a 'Question of the Day' - id: quarterly + name: Quarterly ceremonies: - id: documentation + name: Documentation async: true - id: hackathon + name: Hackathon notes: To be led by the Team leads to gather ideas, we'll take 2 days to spike out some features async: false startTime: 9:00am diff --git a/functions/sendgrid.js b/functions/sendgrid.js index 362a7a7..ab51e74 100644 --- a/functions/sendgrid.js +++ b/functions/sendgrid.js @@ -31,7 +31,7 @@ exports.share = https.onRequest((req, res) => ( to: [{ email: "james.kiesel@gmail.com" }], dynamic_template_data: { uuid: req.body.uuid, - cadences: transformCeremonies(snapshot.toJSON()), + cadences: transformCeremonies(snapshot.toJSON(), t), translations: t('templates.share', { userName: req.body.username, teamName: snapshot.toJSON().name, @@ -53,9 +53,16 @@ exports.share = https.onRequest((req, res) => ( )) )) -const transformCeremonies = ceremonies => +const transformCeremonies = (ceremonies, t) => Object.values(ceremonies).reduce((result, ceremony) => { - result[ceremony.placement] = result[ceremony.placement] || { id: ceremony.placement, ceremonies: [] } - result[ceremony.placement].ceremonies.push(ceremony) + result[ceremony.placement] = result[ceremony.placement] || { + id: ceremony.placement, + name: t(`client.cadences.${ceremony.placement}.name`) + ceremonies: [] + } + result[ceremony.placement].ceremonies.push({ + ...ceremony, + name: t(`client.ceremonies.${ceremony.id}.name`) + }) return result }, {}) diff --git a/functions/templates/share.hbs b/functions/templates/share.hbs index a9bce8d..37a6640 100644 --- a/functions/templates/share.hbs +++ b/functions/templates/share.hbs @@ -38,12 +38,12 @@

{{@root.translations.results}}

{{#each cadences}} -

{{id}}

+

{{name}}

{{#each ceremonies}}
- {{id}} + {{name}} {{#if async}} {{@root.translations.async}} {{else if startTime}} From ddbc424ce6757f19dc38428fcd2c923066e214c4 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 7 Nov 2020 10:36:26 +1300 Subject: [PATCH 04/10] Add submodule --- functions/locales | 1 + 1 file changed, 1 insertion(+) create mode 160000 functions/locales diff --git a/functions/locales b/functions/locales new file mode 160000 index 0000000..6aa3bbe --- /dev/null +++ b/functions/locales @@ -0,0 +1 @@ +Subproject commit 6aa3bbe9a0a1e6adbe0e4366a71b397f78158dc1 From 4592f95e067531b0b103483abba07572dcaf4d99 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 7 Nov 2020 13:15:08 +1300 Subject: [PATCH 05/10] Share email starting to look like it works! --- cli/dist/.gitkeep | 0 cli/emailPreview.js | 2 +- cli/fixtures/share.yml | 14 ++++-- functions/common.js | 5 ++ functions/package.json | 3 +- functions/sendgrid/share.js | 91 ++++++++++++++++++++--------------- functions/templates/share.hbs | 58 ++++++++++++++-------- functions/yarn.lock | 39 ++++++++++++++- 8 files changed, 145 insertions(+), 67 deletions(-) create mode 100644 cli/dist/.gitkeep diff --git a/cli/dist/.gitkeep b/cli/dist/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/cli/emailPreview.js b/cli/emailPreview.js index 539b6c1..03b2079 100644 --- a/cli/emailPreview.js +++ b/cli/emailPreview.js @@ -7,7 +7,7 @@ const templateName = process.argv[2] const template = readFileSync(`../functions/templates/${templateName}.hbs`).toString() const fixture = { ...safeLoad(readFileSync(`./fixtures/${templateName}.yml`)), - translations: safeLoad(readFileSync(`../functions/locales/server.en.yml`)).templates[templateName] + translations: safeLoad(readFileSync(`../functions/locales/server/en.yml`)).templates[templateName] } writeFile( diff --git a/cli/fixtures/share.yml b/cli/fixtures/share.yml index 58ed289..c9adde8 100644 --- a/cli/fixtures/share.yml +++ b/cli/fixtures/share.yml @@ -3,17 +3,25 @@ cadences: name: Daily ceremonies: - id: checkin - name: Check in + title: Check in async: true notes: We'll do a synchronous stand-up in Slack, with one person bringing a 'Question of the Day' - id: quarterly name: Quarterly ceremonies: - id: documentation - name: Documentation + title: Documentation async: true + people: + - initial: J + name: James + - initial: A + name: Ashlyn - id: hackathon - name: Hackathon + title: Hackathon notes: To be led by the Team leads to gather ideas, we'll take 2 days to spike out some features async: false startTime: 9:00am + people: + - initial: J + name: James diff --git a/functions/common.js b/functions/common.js index c23bdc8..ef33f28 100644 --- a/functions/common.js +++ b/functions/common.js @@ -1,5 +1,7 @@ const { database } = require('firebase-admin') const { https } = require('firebase-functions') +const { compile } = require('handlebars') +const { readFileSync } = require('fs') const phrase = require('random-words') const setLanguage = require('./locales/node') const cors = require('cors') @@ -38,6 +40,9 @@ exports.createRoom = ({ return { uuid, name, features, weekCount, ceremonies } } +exports.compileTemplate = (template, data) => + compile(readFileSync(`./templates/${template}.hbs`).toString())(data) + const setResponse = (req, res, fn, t) => Promise.resolve(fn(req, t)) .then(([status, body]) => status === 302 diff --git a/functions/package.json b/functions/package.json index db0808b..31a78f5 100644 --- a/functions/package.json +++ b/functions/package.json @@ -15,12 +15,13 @@ "accept-language-parser": "^1.5.0", "axios": "^0.20.0", "crypto": "^1.0.1", + "dayjs": "^1.9.5", "firebase-admin": "^8.10.0", "firebase-functions": "^3.11.0", + "handlebars": "^4.7.6", "i18next": "^19.8.2", "i18next-fs-backend": "^1.0.7", "ical-generator": "^1.12.1", - "moment": "^2.27.0", "querystring": "^0.2.0", "random-words": "^1.1.1", "tsscmp": "^1.0.6" diff --git a/functions/sendgrid/share.js b/functions/sendgrid/share.js index 1d69d71..b936eff 100644 --- a/functions/sendgrid/share.js +++ b/functions/sendgrid/share.js @@ -1,45 +1,56 @@ const axios = require('axios') +const i18n = require('i18next') +const { compileTemplate } = require('../common') const { database } = require('firebase-admin') const { config } = require('firebase-functions') +const dayjs = require('dayjs') -exports.share = (req, t) => ( - database().ref(`/rooms/${req.body.uuid}/ceremonies`).once('value').then(snapshot => ( - axios.post('https://api.sendgrid.com/v3/mail/send', { - from: { email: "noreply@minimal.cards" }, - personalizations: [{ - to: [{ email: "james.kiesel@gmail.com" }], - dynamic_template_data: { - uuid: req.body.uuid, - cadences: transformCeremonies(snapshot.toJSON(), t), - translations: t('templates.share', { - userName: req.body.username, - teamName: snapshot.toJSON().name, - date: '10 Feb 2020', - returnObjects: true - }) - } - }], - template_id: config().sendgrid.share_template_id - }, { - headers: { - 'Authorization': `Bearer ${config().sendgrid.api_key}`, - 'Content-Type': 'application/json' - } - }).then(() => [200, { status: t('common.messages.200') }]) - .catch(({ request, response: { status, data } }) => [status, data]) - )) -)) +module.exports = (req, t) => + i18n.loadNamespaces('app').then(() => + database().ref(`/rooms/${req.body.uuid}`).once('value') + .then(snapshot => snapshot.toJSON()) + .then(({ ceremonies, name }) => + axios.post('https://api.sendgrid.com/v3/mail/send', { + from: { email: "noreply@minimal.cards", name: "Minimum Viable Ceremonies" }, + personalizations: [{ + to: [{ email: req.body.recipients || 'james.kiesel@gmail.com' }], + subject: t('templates.share.title'), + }], + content: [{ + type: "text/html", + value: compileTemplate('share', { + cadences: Object.values(ceremonies).reduce((result, ceremony) => { + if (['void', 'undecided'].includes(ceremony.placement)) { return result } -const transformCeremonies = (ceremonies, t) => - Object.values(ceremonies).reduce((result, ceremony) => { - result[ceremony.placement] = result[ceremony.placement] || { - id: ceremony.placement, - name: t(`client.cadences.${ceremony.placement}.name`) - ceremonies: [] - } - result[ceremony.placement].ceremonies.push({ - ...ceremony, - name: t(`client.ceremonies.${ceremony.id}.name`) - }) - return result - }, {}) + result[ceremony.placement] = result[ceremony.placement] || { + id: ceremony.placement, + name: t(`app:cadences.${ceremony.placement}.name`), + ceremonies: [] + } + result[ceremony.placement].ceremonies.push({ + ...ceremony, + title: ceremony.title || t(`app:ceremonies.${ceremony.id}.name`), + pill: ( + (ceremony.async && t('templates.share.async')) || + (ceremony.startTime && dayjs().set('hour', 0).set('minute', ceremony.startTime).format('H:mm a')) + ), + participants: (ceremony.people || []) + .filter(person => person.label) + .map(({ label }) => ({ initial: label[0], name: label }) + }) + return result + }), + translations: { + ...t('templates.share', { returnObjects: true }), + footer: t('templates.share.footer', { name, date: dayjs().format('DD MMM YY') }), + roomUrl: `${config().app.cors_origin}/room/${req.body.uuid}` + } + }) + }] + }, { + headers: { + 'Authorization': `Bearer ${config().sendgrid.api_key}`, + 'Content-Type': 'application/json' + } + }).then(() => [200, { status: t('common.messages.200') }]) + .catch(({ request, response }) => [response.status, response.data]))) diff --git a/functions/templates/share.hbs b/functions/templates/share.hbs index 37a6640..f180e62 100644 --- a/functions/templates/share.hbs +++ b/functions/templates/share.hbs @@ -10,8 +10,6 @@ font-weight: 300; } - h1, h2, h3, h4, h5 { margin: 0; } - .pill { padding: 2px 6px; border-radius: 16px; @@ -21,10 +19,28 @@ font-size: 12px; font-weight: bold; } + + .people { + margin-top: 12px; + } + + .person { + margin-top: 12px; + width: 24px; + height: 24px; + display: inline-block; + border-radius: 100%; + background: #abcabc; + font-size: 18px; + margin: 4px; + text-align: center; + } + + h1, h2, h3, h4, h5 { margin: 0; } -
+
{{@root.translations.logoAlt}}

{{@root.translations.mvc}}

@@ -43,11 +59,11 @@
- {{name}} - {{#if async}} - {{@root.translations.async}} - {{else if startTime}} - {{startTime}} + {{title}} + {{#if pill}} + + {{pill}} + {{/if}}
@@ -58,6 +74,17 @@ {{@root.translations.noNotes}} {{/if}}
+
+ {{#if people}} +
+ {{#each people}} +
+ {{initial}} +
+ {{/each}} +
+ {{/if}} +
{{/each}} {{/each}} @@ -65,23 +92,12 @@

- {{@root.translations.team}} + {{@root.translations.footer}}

- + {{@root.translations.backToRoom}}
- diff --git a/functions/yarn.lock b/functions/yarn.lock index 3d0fa08..05396d6 100644 --- a/functions/yarn.lock +++ b/functions/yarn.lock @@ -590,6 +590,11 @@ date-and-time@^0.13.0: resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-0.13.1.tgz#d12ba07ac840d5b112dc4c83f8a03e8a51f78dd6" integrity sha512-/Uge9DJAT+s+oAcDxtBhyR8+sKjUnZbYmyhbmWjTHNtX7B7oWD8YyYdeXcBRbwSj6hVvj+IQegJam7m7czhbFw== +dayjs@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.9.5.tgz#fd49994ebe71639d2ce9575e97186642dfce9808" + integrity sha512-WULIw7UpW/E0y6VywewpbXAMH3d5cZijEhoHLwM+OMVbk/NtchKS/W+57H/0P1rqU7gHrAArjiRLHCUhgMQl6w== + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -1096,6 +1101,18 @@ gtoken@^4.1.0: jws "^4.0.0" mime "^2.2.0" +handlebars@^4.7.6: + version "4.7.6" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" + integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -1626,7 +1643,7 @@ moment-timezone@^0.5.31: dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@^2.27.0: +"moment@>= 2.9.0": version "2.27.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== @@ -1651,6 +1668,11 @@ negotiator@0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== +neo-async@^2.6.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + node-environment-flags@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088" @@ -2007,6 +2029,11 @@ snakeize@^0.1.0: resolved "https://registry.yarnpkg.com/snakeize/-/snakeize-0.1.0.tgz#10c088d8b58eb076b3229bb5a04e232ce126422d" integrity sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0= +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -2180,6 +2207,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +uglify-js@^3.1.4: + version "3.11.5" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.11.5.tgz#d6788bc83cf35ff18ea78a65763e480803409bc6" + integrity sha512-btvv/baMqe7HxP7zJSF7Uc16h1mSfuuSplT0/qdjxseesDU+yYzH33eHBH+eMdeRXwujXspaCTooWHQVVBh09w== + unique-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" @@ -2283,6 +2315,11 @@ wide-align@1.1.3: dependencies: string-width "^1.0.2 || 2" +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" From 66252003d931da21b1b59b2e4808416f35763304 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 7 Nov 2020 13:20:46 +1300 Subject: [PATCH 06/10] Update submodule --- functions/locales | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/locales b/functions/locales index 6aa3bbe..fe961ec 160000 --- a/functions/locales +++ b/functions/locales @@ -1 +1 @@ -Subproject commit 6aa3bbe9a0a1e6adbe0e4366a71b397f78158dc1 +Subproject commit fe961ec2d8660ae8716e1edf6167d7951819714a From e290830b72445dd4a20a8726fa6bd3ee191bc38e Mon Sep 17 00:00:00 2001 From: James Date: Tue, 10 Nov 2020 00:12:38 +1300 Subject: [PATCH 07/10] Further email styling / formatting --- cli/emailPreview.js | 7 +- cli/fixtures/share.yml | 72 ++++++++------ functions/common.js | 7 +- functions/sendgrid/share.js | 9 +- functions/templates/cadence.hbs | 28 ++++++ functions/templates/share.hbs | 138 ++++++++++----------------- functions/templates/shareStyles.hbs | 140 ++++++++++++++++++++++++++++ 7 files changed, 275 insertions(+), 126 deletions(-) create mode 100644 functions/templates/cadence.hbs create mode 100644 functions/templates/shareStyles.hbs diff --git a/cli/emailPreview.js b/cli/emailPreview.js index 03b2079..7bf8ac4 100644 --- a/cli/emailPreview.js +++ b/cli/emailPreview.js @@ -1,4 +1,4 @@ -const { compile } = require('handlebars') +const Handlebars = require('handlebars') const { readFileSync, writeFile } = require('fs') const open = require('open') const { safeLoad } = require('js-yaml') @@ -10,8 +10,11 @@ const fixture = { translations: safeLoad(readFileSync(`../functions/locales/server/en.yml`)).templates[templateName] } +Handlebars.registerPartial('cadence', readFileSync('../functions/templates/cadence.hbs').toString()) +Handlebars.registerPartial('shareStyles', readFileSync('../functions/templates/shareStyles.hbs').toString()) + writeFile( `./dist/${templateName}.html`, - compile(template)(fixture), + Handlebars.compile(template)(fixture), () => open(`dist/${templateName}.html`) ) diff --git a/cli/fixtures/share.yml b/cli/fixtures/share.yml index c9adde8..d382432 100644 --- a/cli/fixtures/share.yml +++ b/cli/fixtures/share.yml @@ -1,27 +1,47 @@ +stats: + - figure: 8 + description: Total Scheduled Ceremonies + - figure: 2 + description: Average People Per Meeting + - figure: 5 + description: Average Ceremonies Per Week cadences: - - id: daily - name: Daily - ceremonies: - - id: checkin - title: Check in - async: true - notes: We'll do a synchronous stand-up in Slack, with one person bringing a 'Question of the Day' - - id: quarterly - name: Quarterly - ceremonies: - - id: documentation - title: Documentation - async: true - people: - - initial: J - name: James - - initial: A - name: Ashlyn - - id: hackathon - title: Hackathon - notes: To be led by the Team leads to gather ideas, we'll take 2 days to spike out some features - async: false - startTime: 9:00am - people: - - initial: J - name: James + sprint: + - id: wednesday-1 + name: First Wednesday + ceremonies: + - id: planning + title: Sprint Planning + icon: '👓' + startTime: 10:00am + notes: Rotating team member to bring donuts + general: + - id: daily + name: Daily + ceremonies: + - id: checkin + title: Check in + icon: '🔗' + async: true + notes: We'll do a synchronous stand-up in Slack, with one person bringing a 'Question of the Day' + - id: quarterly + name: Quarterly + ceremonies: + - id: documentation + title: Documentation + icon: '📒' + async: true + people: + - initial: J + name: James + - initial: A + name: Ashlyn + - id: hackathon + title: Hackathon + icon: '🍕' + notes: To be led by the Team leads to gather ideas, we'll take 2 days to spike out some features + async: false + startTime: 9:00am + people: + - initial: J + name: James diff --git a/functions/common.js b/functions/common.js index ef33f28..a586817 100644 --- a/functions/common.js +++ b/functions/common.js @@ -1,12 +1,15 @@ const { database } = require('firebase-admin') const { https } = require('firebase-functions') -const { compile } = require('handlebars') +const Handlebars = require('handlebars') const { readFileSync } = require('fs') const phrase = require('random-words') const setLanguage = require('./locales/node') const cors = require('cors') const fs = require('fs') +Handlebars.registerPartial('cadence', fs.readFileSync('./templates/cadence.hbs').toString()) +Handlebars.registerPartial('shareStyles', fs.readFileSync('./templates/shareStyles.hbs').toString()) + exports.endpoint = (origin, fn) => https.onRequest((req, res) => setLanguage(req).then(t => @@ -41,7 +44,7 @@ exports.createRoom = ({ } exports.compileTemplate = (template, data) => - compile(readFileSync(`./templates/${template}.hbs`).toString())(data) + Handlebars.compile(readFileSync(`./templates/${template}.hbs`).toString())(data) const setResponse = (req, res, fn, t) => Promise.resolve(fn(req, t)) diff --git a/functions/sendgrid/share.js b/functions/sendgrid/share.js index b936eff..809f4c7 100644 --- a/functions/sendgrid/share.js +++ b/functions/sendgrid/share.js @@ -21,22 +21,21 @@ module.exports = (req, t) => value: compileTemplate('share', { cadences: Object.values(ceremonies).reduce((result, ceremony) => { if (['void', 'undecided'].includes(ceremony.placement)) { return result } - result[ceremony.placement] = result[ceremony.placement] || { id: ceremony.placement, name: t(`app:cadences.${ceremony.placement}.name`), ceremonies: [] } - result[ceremony.placement].ceremonies.push({ + result[ceremony.placement].ceremonies.unshift({ ...ceremony, title: ceremony.title || t(`app:ceremonies.${ceremony.id}.name`), pill: ( (ceremony.async && t('templates.share.async')) || (ceremony.startTime && dayjs().set('hour', 0).set('minute', ceremony.startTime).format('H:mm a')) ), - participants: (ceremony.people || []) + people: Object.values(ceremony.people || []) .filter(person => person.label) - .map(({ label }) => ({ initial: label[0], name: label }) + .map(({ label }) => ({ initial: label[0], name: label })) }) return result }), @@ -45,7 +44,7 @@ module.exports = (req, t) => footer: t('templates.share.footer', { name, date: dayjs().format('DD MMM YY') }), roomUrl: `${config().app.cors_origin}/room/${req.body.uuid}` } - }) + }, ['ceremony']) }] }, { headers: { diff --git a/functions/templates/cadence.hbs b/functions/templates/cadence.hbs new file mode 100644 index 0000000..0b67c03 --- /dev/null +++ b/functions/templates/cadence.hbs @@ -0,0 +1,28 @@ +
+

{{name}}

+ {{#each ceremonies}} +
+
+
{{icon}} {{title}}
+ {{#if pill}} + {{pill}} + {{/if}} +
+ {{#if people}} +
+ {{#each people}} +
{{initial}}
+ {{/each}} +
+ {{/if}} +
+
+ {{#if notes}} + {{notes}} + {{else}} + {{@root.translations.noNotes}} + {{/if}} +
+
+ {{/each}} +
diff --git a/functions/templates/share.hbs b/functions/templates/share.hbs index f180e62..777024c 100644 --- a/functions/templates/share.hbs +++ b/functions/templates/share.hbs @@ -4,100 +4,56 @@ - - - -
- {{@root.translations.logoAlt}} -
-

{{@root.translations.mvc}}

- https://minimal.cards + {{#if cadences.sprint}} +

{{@root.translations.schedule.general}}

+ {{#each cadences.general}} + {{> cadence}} + {{/each}} + {{/if}} +
+
-
-
-

{{@root.translations.greeting}}

-

{{@root.translations.encouragement}}

-
-
-

{{@root.translations.results}}

- {{#each cadences}} -

{{name}}

- {{#each ceremonies}} -
-
-
- {{title}} - {{#if pill}} - - {{pill}} - - {{/if}} -
-
-
- {{#if notes}} - {{notes}} - {{else}} - {{@root.translations.noNotes}} - {{/if}} -
-
- {{#if people}} -
- {{#each people}} -
- {{initial}} -
- {{/each}} -
- {{/if}} -
-
- {{/each}} - {{/each}} -
-
-
-

- {{@root.translations.footer}} -

- - {{@root.translations.backToRoom}} - -
-
+ diff --git a/functions/templates/shareStyles.hbs b/functions/templates/shareStyles.hbs new file mode 100644 index 0000000..23b28d2 --- /dev/null +++ b/functions/templates/shareStyles.hbs @@ -0,0 +1,140 @@ + From fc4e1bbe65249d25b4c02d7a07cd03c2497a6153 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 14 Nov 2020 22:34:23 +1300 Subject: [PATCH 08/10] Add additional slack endpoints --- functions/common.js | 34 ++++++++++++++++++++++ functions/sendgrid/share.js | 33 ++++----------------- functions/slack/common.js | 2 +- functions/slack/index.js | 5 +++- functions/slack/join.js | 15 ++++++++++ functions/slack/list.js | 21 ++++++++++++++ functions/slack/share.js | 58 +++++++++++++++++++++++++++++++++++++ 7 files changed, 139 insertions(+), 29 deletions(-) create mode 100644 functions/slack/join.js create mode 100644 functions/slack/list.js create mode 100644 functions/slack/share.js diff --git a/functions/common.js b/functions/common.js index a586817..9be5c3c 100644 --- a/functions/common.js +++ b/functions/common.js @@ -43,6 +43,40 @@ exports.createRoom = ({ return { uuid, name, features, weekCount, ceremonies } } +exports.snapshotToShare = (snapshot, t) => { + const { ceremonies = [] } = snapshot.toJSON() || {} + const cadences = Object.values(ceremonies).reduce((result, ceremony) => { + if (['void', 'undecided'].includes(ceremony.placement)) { return result } + result[ceremony.placement] = result[ceremony.placement] || { + id: ceremony.placement, + name: t(`app:cadences.${ceremony.placement}.name`), + ceremonies: [] + } + result[ceremony.placement].ceremonies.unshift({ + ...ceremony, + title: ceremony.title || t(`app:ceremonies.${ceremony.id}.name`), + pill: ( + (ceremony.async && t('templates.share.async')) || + (ceremony.startTime && dayjs().set('hour', 0).set('minute', ceremony.startTime).format('H:mm a')) + ), + people: Object.values(ceremony.people || []) + .filter(person => person.label) + .map(({ label }) => ({ initial: label[0], name: label })) + }) + return result + }, []) + + return { + cadences, + stats: { + // TODO: calculate stats + ceremonies: 6, + people: 2, + weekly: 4.3 + } + } +} + exports.compileTemplate = (template, data) => Handlebars.compile(readFileSync(`./templates/${template}.hbs`).toString())(data) diff --git a/functions/sendgrid/share.js b/functions/sendgrid/share.js index 809f4c7..8370bc6 100644 --- a/functions/sendgrid/share.js +++ b/functions/sendgrid/share.js @@ -1,6 +1,6 @@ const axios = require('axios') const i18n = require('i18next') -const { compileTemplate } = require('../common') +const { compileTemplate, snapshotToShare } = require('../common') const { database } = require('firebase-admin') const { config } = require('firebase-functions') const dayjs = require('dayjs') @@ -8,8 +8,7 @@ const dayjs = require('dayjs') module.exports = (req, t) => i18n.loadNamespaces('app').then(() => database().ref(`/rooms/${req.body.uuid}`).once('value') - .then(snapshot => snapshot.toJSON()) - .then(({ ceremonies, name }) => + .then(snapshot => axios.post('https://api.sendgrid.com/v3/mail/send', { from: { email: "noreply@minimal.cards", name: "Minimum Viable Ceremonies" }, personalizations: [{ @@ -19,37 +18,17 @@ module.exports = (req, t) => content: [{ type: "text/html", value: compileTemplate('share', { - cadences: Object.values(ceremonies).reduce((result, ceremony) => { - if (['void', 'undecided'].includes(ceremony.placement)) { return result } - result[ceremony.placement] = result[ceremony.placement] || { - id: ceremony.placement, - name: t(`app:cadences.${ceremony.placement}.name`), - ceremonies: [] - } - result[ceremony.placement].ceremonies.unshift({ - ...ceremony, - title: ceremony.title || t(`app:ceremonies.${ceremony.id}.name`), - pill: ( - (ceremony.async && t('templates.share.async')) || - (ceremony.startTime && dayjs().set('hour', 0).set('minute', ceremony.startTime).format('H:mm a')) - ), - people: Object.values(ceremony.people || []) - .filter(person => person.label) - .map(({ label }) => ({ initial: label[0], name: label })) - }) - return result - }), + ...snapshotToShare(snapshot, t), translations: { ...t('templates.share', { returnObjects: true }), - footer: t('templates.share.footer', { name, date: dayjs().format('DD MMM YY') }), + footer: t('templates.share.footer', { name: (snapshot.toJSON() || {}).name }), roomUrl: `${config().app.cors_origin}/room/${req.body.uuid}` } - }, ['ceremony']) + }) }] }, { headers: { - 'Authorization': `Bearer ${config().sendgrid.api_key}`, - 'Content-Type': 'application/json' + 'Authorization': `Bearer ${token}` } }).then(() => [200, { status: t('common.messages.200') }]) .catch(({ request, response }) => [response.status, response.data]))) diff --git a/functions/slack/common.js b/functions/slack/common.js index 9578137..c63b30e 100644 --- a/functions/slack/common.js +++ b/functions/slack/common.js @@ -1,6 +1,6 @@ const crypto = require('crypto') const tsscmp = require('tsscmp') -const { config } = require('firebase-admin') +const { config } = require('firebase-functions') exports.returnMessage = text => { blocks: [{ type: 'section', text: { type: 'mrkdwn', text } }] } diff --git a/functions/slack/index.js b/functions/slack/index.js index 29193da..aa194d0 100644 --- a/functions/slack/index.js +++ b/functions/slack/index.js @@ -3,5 +3,8 @@ const { endpoint } = require('../common') module.exports = { authorize: endpoint(config().marketing.cors_origin, require('./authorize')), - create: endpoint(config().slack.cors_origin, require('./create')) + create: endpoint(config().slack.cors_origin, require('./create')), + share: endpoint(config().app.cors_origin, require('./share')), + join: endpoint(config().app.cors_origin, require('./join')), + list: endpoint(config().app.cors_origin, require('./list')), } diff --git a/functions/slack/join.js b/functions/slack/join.js new file mode 100644 index 0000000..c8087e5 --- /dev/null +++ b/functions/slack/join.js @@ -0,0 +1,15 @@ +const axios = require('axios') +const i18n = require('i18next') +const { config } = require('firebase-functions') + +module.exports = (req, t) => + axios.post('https://slack.com/api/conversations.join', { + channel: req.body.channel + }, { + headers: { 'Authorization': `Bearer ${config().slack.access_token}` } + }).catch(error => [400, t('slack.errors.400', { errors: error })]) + .then(({ data: { ok, error, conversations } }) => + ok + ? [200, { status: t('common.messages.200') }] + : [400, { status: t('slack.errors.400', { errors: error }) }] + ) diff --git a/functions/slack/list.js b/functions/slack/list.js new file mode 100644 index 0000000..3ede8b3 --- /dev/null +++ b/functions/slack/list.js @@ -0,0 +1,21 @@ +const axios = require('axios') +const i18n = require('i18next') +const { config } = require('firebase-functions') + +module.exports = (req, t) => + axios.post('https://slack.com/api/conversations.list', {}, { + headers: { 'Authorization': `Bearer ${config().slack.access_token}` } + }).catch(error => [400, t('slack.errors.400', { errors: error })]) + .then(({ data: { ok, error, channels } }) => + ok + ? [200, { channels: transformChannels(channels) }] + : [400, { status: t('slack.errors.400', { errors: error }) }]) + +const transformChannels = channels => + channels + .filter(channel => !channel.is_archived) + .map(channel => ({ + name: channel.name, + purpose: channel.purpose.value, + member: channel.is_member, + })) diff --git a/functions/slack/share.js b/functions/slack/share.js new file mode 100644 index 0000000..ac2509f --- /dev/null +++ b/functions/slack/share.js @@ -0,0 +1,58 @@ +const axios = require('axios') +const i18n = require('i18next') +const { config } = require('firebase-functions') +const { database } = require('firebase-admin') +const { getAccessToken } = require('./common') +const { snapshotToShare } = require('../common') + +module.exports = (req, t) => + i18n.loadNamespaces('app').then(() => + database().ref(`/rooms/${req.body.uuid}`).once('value').then(snapshot => { + const { stats, cadences } = snapshotToShare(snapshot, t) + + return axios.post('https://slack.com/api/chat.postMessage', { + channel: req.body.channel, + blocks: [{ + type: 'header', + text: { type: 'plain_text', text: t('templates.share.congrats') } + }, { + type: 'section', + text: { type: 'mrkdwn', text: t('templates.share.encouragement') } + }, { + type: 'divider' + }, { + type: 'header', + text: { type: 'plain_text', text: t('templates.share.stats.title') } + }, { + type: 'section', + fields: [ + { type: 'mrkdwn', text: `• ${stats.ceremonies} ${t('templates.share.stats.ceremonies')}` }, + { type: 'mrkdwn', text: `• ${stats.people} ${t('templates.share.stats.people')}` }, + { type: 'mrkdwn', text: `• ${stats.weekly} ${t('templates.share.stats.weekly')}` } + ] + }, { + type: 'divider' + }, { + type: 'header', + text: { type: 'plain_text', text: t('templates.share.schedule.title') } + }, { + type: 'divider' + }, { + type: 'section', + text: { type: 'plain_text', text: t('templates.share.footer') }, + accessory: { + type: 'button', + text: { type: 'plain_text', text: t('templates.share.backToRoom') }, + url: `${config().app.cors_origin}/room/${req.body.uuid}` + } + }] + }, { + headers: { 'Authorization': `Bearer ${config().slack.access_token}` } + }).catch(error => [400, t('slack.errors.400', { errors: error })]) + .then(({ data: { ok, error } }) => + ok + ? [200, { status: t('common.messages.200') }] + : [400, { status: t('slack.errors.400', { errors: error }) }] + ) + }) + ) From 5ed7f6df0800a74b89ba6f7a45754fce92d57bf2 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 16 Nov 2020 22:21:19 +1300 Subject: [PATCH 09/10] Parse json body correctly --- functions/slack/list.js | 3 +-- functions/slack/share.js | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/functions/slack/list.js b/functions/slack/list.js index 3ede8b3..b7336a7 100644 --- a/functions/slack/list.js +++ b/functions/slack/list.js @@ -15,7 +15,6 @@ const transformChannels = channels => channels .filter(channel => !channel.is_archived) .map(channel => ({ + id: channel.id, name: channel.name, - purpose: channel.purpose.value, - member: channel.is_member, })) diff --git a/functions/slack/share.js b/functions/slack/share.js index ac2509f..c7f7e87 100644 --- a/functions/slack/share.js +++ b/functions/slack/share.js @@ -7,11 +7,12 @@ const { snapshotToShare } = require('../common') module.exports = (req, t) => i18n.loadNamespaces('app').then(() => - database().ref(`/rooms/${req.body.uuid}`).once('value').then(snapshot => { + database().ref(`/rooms/${JSON.parse(req.body).uuid}`).once('value').then(snapshot => { + const { channel, uuid } = JSON.parse(req.body) const { stats, cadences } = snapshotToShare(snapshot, t) return axios.post('https://slack.com/api/chat.postMessage', { - channel: req.body.channel, + channel, blocks: [{ type: 'header', text: { type: 'plain_text', text: t('templates.share.congrats') } @@ -43,7 +44,7 @@ module.exports = (req, t) => accessory: { type: 'button', text: { type: 'plain_text', text: t('templates.share.backToRoom') }, - url: `${config().app.cors_origin}/room/${req.body.uuid}` + url: `${config().app.cors_origin}/room/${uuid}` } }] }, { From f12995ec57894bd6188c02cb75686d8f8b9fa5df Mon Sep 17 00:00:00 2001 From: James Date: Mon, 1 Mar 2021 09:30:11 +1300 Subject: [PATCH 10/10] WIP :/ --- functions/sendgrid/share.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/sendgrid/share.js b/functions/sendgrid/share.js index 8370bc6..993cca3 100644 --- a/functions/sendgrid/share.js +++ b/functions/sendgrid/share.js @@ -28,7 +28,7 @@ module.exports = (req, t) => }] }, { headers: { - 'Authorization': `Bearer ${token}` + 'Authorization': `Bearer ${config().sendgrid.api_key}` } }).then(() => [200, { status: t('common.messages.200') }]) .catch(({ request, response }) => [response.status, response.data])))