From 2db757e2f4da629d49e933be426f71ea52ec9790 Mon Sep 17 00:00:00 2001 From: Mentor Date: Thu, 9 Jun 2022 09:53:01 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20changin=20room=20email=20notificati?= =?UTF-8?q?on=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- functions/index.js | 6 + functions/integrations/changingroom.js | 72 ++++++- functions/integrations/ses.js | 70 +++++++ functions/integrations/signer_is.js | 23 +++ functions/modules/helpers.js | 2 +- .../modules/templates/css-resets/LICENSE.md | 21 ++ .../modules/templates/css-resets/README.md | 67 ++++++ .../modules/templates/css-resets/example.html | 59 ++++++ .../modules/templates/css-resets/extra.css | 29 +++ .../templates/css-resets/normalize.css | 64 ++++++ .../modules/templates/css-resets/outlook.css | 24 +++ functions/modules/templates/signer.css | 83 ++++++++ functions/modules/templates/verify.email.pug | 13 ++ functions/modules/templates/verify.email.txt | 13 ++ functions/package-lock.json | 190 +++++++++++++++++- functions/package.json | 3 +- .../templates/outfit-available.email.pug | 17 -- functions/templates/outfit-available.txt | 5 - .../templates/outfit_available.email.pug | 10 + .../templates/outfit_available.email.txt | 9 + website/src/assets/discord-logo-black.svg | 10 - 21 files changed, 748 insertions(+), 42 deletions(-) create mode 100644 functions/integrations/ses.js create mode 100644 functions/integrations/signer_is.js create mode 100644 functions/modules/templates/css-resets/LICENSE.md create mode 100644 functions/modules/templates/css-resets/README.md create mode 100644 functions/modules/templates/css-resets/example.html create mode 100644 functions/modules/templates/css-resets/extra.css create mode 100644 functions/modules/templates/css-resets/normalize.css create mode 100644 functions/modules/templates/css-resets/outlook.css create mode 100644 functions/modules/templates/signer.css create mode 100644 functions/modules/templates/verify.email.pug create mode 100644 functions/modules/templates/verify.email.txt delete mode 100644 functions/templates/outfit-available.email.pug delete mode 100644 functions/templates/outfit-available.txt create mode 100644 functions/templates/outfit_available.email.pug create mode 100644 functions/templates/outfit_available.email.txt delete mode 100644 website/src/assets/discord-logo-black.svg diff --git a/functions/index.js b/functions/index.js index 2d01dd4..35f4113 100644 --- a/functions/index.js +++ b/functions/index.js @@ -20,3 +20,9 @@ exports.mainnetMetadata = functions.runWith( runtime ).https.onRequest( mainnetA const { handleQueuedRocketeerOutfit } = require( './nft-media/changing-room' ) exports.mainnetGenerateOutfitsOnQueue = functions.runWith( runtime ).firestore.document( `mainnetQueueOutfitGeneration/{rocketeerId}` ).onWrite( handleQueuedRocketeerOutfit ) exports.rinkebyGenerateOutfitsOnQueue = functions.runWith( runtime ).firestore.document( `rinkebyQueueOutfitGeneration/{rocketeerId}` ).onWrite( handleQueuedRocketeerOutfit ) + +/* /////////////////////////////// +// Daemons +// /////////////////////////////*/ +const { notify_holders_of_changing_room_updates } = require( './integrations/changingroom' ) +exports.notify_holders_of_changing_room_updates = functions.pubsub.schedule( '0 0 * * *' ).onRun( notify_holders_of_changing_room_updates ) \ No newline at end of file diff --git a/functions/integrations/changingroom.js b/functions/integrations/changingroom.js index 845062b..95a936f 100644 --- a/functions/integrations/changingroom.js +++ b/functions/integrations/changingroom.js @@ -1,5 +1,8 @@ const { generateNewOutfitFromId, queueRocketeersOfAddressForOutfitChange } = require( '../nft-media/changing-room' ) -const { db, dataFromSnap } = require( '../modules/firebase' ) +const { db, dataFromSnap, FieldPath } = require( '../modules/firebase' ) +const { dev, log } = require( '../modules/helpers' ) +const { ask_signer_is_for_available_emails } = require( './signer_is' ) +const { send_outfit_available_email } = require( './ses' ) // Web3 APIs const { getOwingAddressOfTokenId } = require( '../modules/contract' ) @@ -197,4 +200,71 @@ exports.setPrimaryOutfit = async function( req, res ) { } +} + +/* /////////////////////////////// +// Notify of changing room updates +// /////////////////////////////*/ +exports.notify_holders_of_changing_room_updates = async context => { + + try { + + // Get all Rocketeers with outfits available + const network = dev ? `rinkeby` : `mainnet` + const limit = dev ? 50 : 2000 + log( `Getting ${ limit } rocketeers on ${ network }` ) + const all_rocketeers = await db.collection( `${ network }Rocketeers` ) + .limit( limit ).get().then( dataFromSnap ) + log( `Got ${ all_rocketeers.length } Rocketeers` ) + const has_outfit_available = all_rocketeers.filter( rocketeer => { + + const { attributes } = rocketeer + const outfit_available = attributes.find( ( { trait_type } ) => trait_type == 'last outfit change' ) + // If outfit available is in the past, keep it + if( outfit_available?.value < Date.now() ) return true + + // If outfit available in the future, discard + return false + + } ) + + const owners = await Promise.all( has_outfit_available.map( async ( { uid } ) => { + log( `Getting owner of `, uid ) + const owning_address = await getOwingAddressOfTokenId( uid ) + return { uid, owning_address } + } ) ) + + const owners_with_signer_email = await ask_signer_is_for_available_emails( owners.map( ( { owning_address } ) => owning_address ) ) + + const rocketeers_by_address = has_outfit_available.reduce( ( wallets, rocketeer ) => { + + const new_wallet_list = { ...wallets } + const { owning_address } = owners.find( ( { uid } ) => uid == rocketeer.uid ) + + // If this owner has no email, ignore it + if( !owners_with_signer_email.includes( owning_address ) ) return new_wallet_list + + // If the wallet object does now have this one yet, add an empty array + if( !new_wallet_list[ owning_address ] ) new_wallet_list[owning_address] = [] + + new_wallet_list[owning_address] = [ ...new_wallet_list[owning_address], rocketeer ] + return new_wallet_list + + }, {} ) + + const owners_to_email = Object.keys( rocketeers_by_address ) + + // Send emails to the relevant owners + await Promise.all( owners_to_email.map( async owning_address => { + + const rocketeers = rocketeers_by_address[ owning_address ] + await send_outfit_available_email( rocketeers, `${ owning_address }@signer.is` ) + + } ) ) + + + } catch( e ) { + console.error( `notify_holders_of_changing_room_updates error: `, e ) + } + } \ No newline at end of file diff --git a/functions/integrations/ses.js b/functions/integrations/ses.js new file mode 100644 index 0000000..d7433b0 --- /dev/null +++ b/functions/integrations/ses.js @@ -0,0 +1,70 @@ +const AWS = require('aws-sdk') +const functions = require( 'firebase-functions' ) +const { aws } = functions.config() +const { log } = require( '../modules/helpers' ) +const SES_CONFIG = { + accessKeyId: aws?.ses?.keyid, + secretAccessKey: aws?.ses?.secretkey, + region: aws?.ses?.region, +} +const AWS_SES = new AWS.SES( SES_CONFIG ) + +// Email templates +const pug = require('pug') +const { promises: fs } = require( 'fs' ) +const csso = require('csso') +const juice = require('juice') + +async function compile_pug_to_email( pugFile, data ) { + + const [ emailPug, inlineNormalise, styleExtra, styleOutlook, rocketeerStyles ] = await Promise.all( [ + fs.readFile( pugFile ), + fs.readFile( `${ __dirname }/../templates/css-resets/normalize.css`, 'utf8' ), + fs.readFile( `${ __dirname }/../templates/css-resets/extra.css`, 'utf8' ), + fs.readFile( `${ __dirname }/../templates/css-resets/outlook.css`, 'utf8' ), + fs.readFile( `${ __dirname }/../templates/rocketeers.css`, 'utf8' ) + ] ) + + const { css } = csso.minify( [ styleExtra, styleOutlook, inlineNormalise, rocketeerStyles ].join( '\n' ) ) + const html = pug.render( emailPug, { data, headStyles: css } ) + const emailifiedHtml = juice.inlineContent( html, [ inlineNormalise, rocketeerStyles ].join( '\n' ), { removeStyleTags: false } ) + + return emailifiedHtml + +} + +async function send_email( recipient, subject, html, text ) { + + const options = { + Source: aws.ses.fromemail, + Destination: { + ToAddresses: [ recipient ] + }, + ReplyToAddresses: [], + Message: { + Body: { + Html: { Charset: 'UTF-8', Data: html }, + Text: { Charset: 'UTF-8', Data: text } + }, + Subject: { Charset: 'UTF-8', Data: subject } + } + + } + + log( `Sending email "${ options.Message.Subject.Data }" from ${ options.Source } to ${ options.Destination.ToAddresses[0] }` ) + + return AWS_SES.sendEmail( options ).promise() + +} + +exports.send_outfit_available_email = async ( rocketeers, email ) => { + + + const email_html = await compile_pug_to_email( `${ __dirname }/../templates/outfit_available.email.pug`, rocketeers ) + const email_text = ( await fs.readFile( `${ __dirname }/../templates/outfit_available.email.txt`, 'utf8' ) ) + // .replace( '%%address%%', email_data.address ) + + return send_email( email, `Your Rocketeer${rocketeers.length > 1 ? 's' : '' } ${rocketeers.length > 1 ? 'have outfits' : 'has an outfit' } available!`, email_html, email_text ) + + +} \ No newline at end of file diff --git a/functions/integrations/signer_is.js b/functions/integrations/signer_is.js new file mode 100644 index 0000000..9005cfb --- /dev/null +++ b/functions/integrations/signer_is.js @@ -0,0 +1,23 @@ +const fetch = require( 'isomorphic-fetch' ) + +exports.ask_signer_is_for_available_emails = async function( addresses ) { + + /* /////////////////////////////// + // Check available email addresses */ + const endpoint = `https://signer.is/check_availability/` + const options = { + method: 'POST', + headers:{ + 'Content-Type': 'application/json' + }, + body: JSON.stringify( { + addresses + } ) + } + + const res = await fetch( endpoint, options ) + const available_addresses = await res.json() + + return available_addresses?.emails_available || [] + +} \ No newline at end of file diff --git a/functions/modules/helpers.js b/functions/modules/helpers.js index 8cd19af..6384318 100644 --- a/functions/modules/helpers.js +++ b/functions/modules/helpers.js @@ -1,7 +1,7 @@ // /////////////////////////////// // Helper functions // /////////////////////////////// - +exports.dev = !!process.env.development exports.log = ( ...messages ) => { if( process.env.development ) console.log( ...messages ) } diff --git a/functions/modules/templates/css-resets/LICENSE.md b/functions/modules/templates/css-resets/LICENSE.md new file mode 100644 index 0000000..bac7830 --- /dev/null +++ b/functions/modules/templates/css-resets/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright © Arthur Koch + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/functions/modules/templates/css-resets/README.md b/functions/modules/templates/css-resets/README.md new file mode 100644 index 0000000..5ba1300 --- /dev/null +++ b/functions/modules/templates/css-resets/README.md @@ -0,0 +1,67 @@ +# normalize.email.css + +CSS resets for HTML emails + +It's just a little css library for best default email compatibility. You can use it with your favourite email framework and self-coded templates. + +## What does it do? + +- Preserves useful defaults for most email clients +- Makes native platform font styling +- Corrects some popular bugs +- Explains what code does using comments + +Please let me know if comments not informative and must be detailed + +## Contents + +- normalize.css - must be inlined to your newsletter in production +- extra.css - must be placed between ` + + + + + + +
+ + + + + + + + + +
+ + +
+
+ + + + +``` diff --git a/functions/modules/templates/css-resets/example.html b/functions/modules/templates/css-resets/example.html new file mode 100644 index 0000000..91c5472 --- /dev/null +++ b/functions/modules/templates/css-resets/example.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+

You are receiving this because a known security researcher submitted proof of finding credentials for your npm user account on the internet.

+ +

In order to prevent unauthorized access, we've changed the password to your account and invalidated all of your active npm tokens.

+ +

Please click on the following link, or paste this into your browser to reset your password:

+ + + +

When you reset your password please do not set it back to the old value.

+ +

We have no reason to believe that your account was compromised, but cannot be certain of this. This reset is preemptive, to prevent future compromise.

+ +

If you have questions:

+
    +
  1. You can reply to this message or email support@npmjs.com.
  2. +
  3. You can also read more about this undertaking in our blog post.
  4. +
+ +

Npm loves you.

+
+
+ + + diff --git a/functions/modules/templates/css-resets/extra.css b/functions/modules/templates/css-resets/extra.css new file mode 100644 index 0000000..546a069 --- /dev/null +++ b/functions/modules/templates/css-resets/extra.css @@ -0,0 +1,29 @@ +/* Extra.css */ +/* Contents of this file must be placed between