diff --git a/lib/cacheable-bucket-item.js b/lib/cacheable-bucket-item.js index 7ffabd286..6f5d90242 100644 --- a/lib/cacheable-bucket-item.js +++ b/lib/cacheable-bucket-item.js @@ -8,18 +8,28 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", { value: true }); +const lodash_1 = require("lodash"); const identity = a => a; class CacheableBucketItem { constructor(opts) { + this.getDatedValue = () => __awaiter(this, void 0, void 0, function* () { + const value = yield this.get(); + return { + value, + lastModified: this.lastModified + }; + }); this.get = (opts) => __awaiter(this, void 0, void 0, function* () { - const value = yield this.value.get(opts); - return this.parse(value); + const { Body, LastModified } = yield this.value.get(opts); + this.lastModified = new Date(LastModified).getTime(); + return this.parse(Body); }); this.put = (value, opts = {}) => __awaiter(this, void 0, void 0, function* () { return yield this.value.put(Object.assign({ value }, opts)); }); - this.value = opts.bucket.getCacheable(opts); - this.parse = identity; + this.value = opts.bucket.getCacheable(lodash_1.omit(opts, ['parse'])); + this.parse = opts.parse || identity; + this.lastModified = null; } } exports.CacheableBucketItem = CacheableBucketItem; diff --git a/lib/delivery-http.js b/lib/delivery-http.js index 0756b84d5..d3a6f134c 100644 --- a/lib/delivery-http.js +++ b/lib/delivery-http.js @@ -12,7 +12,7 @@ const _zlib = require("zlib"); const events_1 = require("events"); const utils_1 = require("./utils"); const zlib = utils_1.promisify(_zlib); -const COMPRESSION_THRESHOLD = 2000; +const COMPRESSION_THRESHOLD = 0; const FETCH_TIMEOUT = 10000; class Delivery extends events_1.EventEmitter { constructor({ env, logger }) { diff --git a/lib/s3-utils.js b/lib/s3-utils.js index 73e66edf6..42e441228 100644 --- a/lib/s3-utils.js +++ b/lib/s3-utils.js @@ -102,6 +102,8 @@ module.exports = function createUtils({ s3, logger }) { throw new Error('expected "key"'); if (!bucket) throw new Error('expected "bucket"'); + if (!ttl) + throw new Error('expected "ttl"'); let cached; let type; let etag; diff --git a/lib/samplebot/configure.js b/lib/samplebot/configure.js index 68aa7e55d..94abbd74a 100644 --- a/lib/samplebot/configure.js +++ b/lib/samplebot/configure.js @@ -37,30 +37,47 @@ exports.LENSES_KEY = 'conf/lenses.json'; exports.STYLE_KEY = 'conf/style.json'; exports.ORG_KEY = 'org/org.json'; exports.INFO_KEY = 'info/info.json'; +exports.TERMS_AND_CONDITIONS_KEY = 'conf/terms-and-conditions.md'; +const MINUTE = 3600000; +const HALF_HOUR = MINUTE * 30; +const HOUR = HALF_HOUR * 2; +const DEFAULT_TTL = HALF_HOUR; const parts = { org: { bucket: 'PrivateConf', - key: exports.ORG_KEY + key: exports.ORG_KEY, + ttl: DEFAULT_TTL }, style: { bucket: 'PrivateConf', - key: exports.STYLE_KEY + key: exports.STYLE_KEY, + ttl: DEFAULT_TTL }, info: { bucket: 'PrivateConf', - key: exports.INFO_KEY + key: exports.INFO_KEY, + ttl: DEFAULT_TTL }, botConf: { bucket: 'PrivateConf', - key: exports.BOT_CONF_KEY + key: exports.BOT_CONF_KEY, + ttl: DEFAULT_TTL }, models: { bucket: 'PrivateConf', - key: exports.MODELS_KEY + key: exports.MODELS_KEY, + ttl: DEFAULT_TTL }, lenses: { bucket: 'PrivateConf', - key: exports.LENSES_KEY + key: exports.LENSES_KEY, + ttl: DEFAULT_TTL + }, + termsAndConditions: { + bucket: 'PrivateConf', + key: exports.TERMS_AND_CONDITIONS_KEY, + ttl: DEFAULT_TTL, + parse: value => value.toString() } }; class Conf { diff --git a/lib/samplebot/customize.js b/lib/samplebot/customize.js index fd7d85931..b314f5624 100644 --- a/lib/samplebot/customize.js +++ b/lib/samplebot/customize.js @@ -17,9 +17,12 @@ const Errors = require("../errors"); const ONFIDO_PLUGIN_PATH = 'products.plugins.onfido'; function customize(opts) { return __awaiter(this, void 0, void 0, function* () { - const { bot, delayReady, event } = opts; + let { lambda, bot, delayReady, event } = opts; + if (!bot) + bot = lambda.bot; + const { logger } = lambda || bot; const confy = configure_1.createConf({ bot }); - let [org, conf, customModels, style] = yield Promise.all([ + let [org, conf, customModels, style, termsAndConditions] = yield Promise.all([ confy.org.get(), confy.botConf.get(), confy.models.get().catch(err => { @@ -27,6 +30,9 @@ function customize(opts) { }), confy.style.get().catch(err => { Errors.ignore(err, Errors.NotFound); + }), + confy.termsAndConditions.getDatedValue().catch(err => { + Errors.ignore(err, Errors.NotFound); }) ]); const { domain } = org; @@ -43,8 +49,10 @@ function customize(opts) { } const components = strategy_1.default({ bot, + logger, namespace, conf, + termsAndConditions, customModels, style, event diff --git a/lib/samplebot/lambda/http/inbox.js b/lib/samplebot/lambda/http/inbox.js index 652de3778..7ae93c897 100644 --- a/lib/samplebot/lambda/http/inbox.js +++ b/lib/samplebot/lambda/http/inbox.js @@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const bot_1 = require("../../../bot"); const customize_1 = require("../../customize"); const bot = bot_1.createBot({ ready: false }); -customize_1.customize({ bot, event: 'message' }); const lambda = bot.lambdas.inbox(); +customize_1.customize({ lambda, event: 'message' }); exports.handler = lambda.handler; //# sourceMappingURL=inbox.js.map \ No newline at end of file diff --git a/lib/samplebot/lambda/http/onfido-webhook.js b/lib/samplebot/lambda/http/onfido-webhook.js index bd2158988..50c1adef7 100644 --- a/lib/samplebot/lambda/http/onfido-webhook.js +++ b/lib/samplebot/lambda/http/onfido-webhook.js @@ -15,7 +15,7 @@ const bot_1 = require("../../../bot"); const customize_1 = require("../../customize"); const bot = bot_1.createBot({ ready: false }); const lambda = bot.createLambda({ source: lambda_1.EventSource.HTTP }); -const promiseCustomize = customize_1.customize({ bot, event: 'onfido:webhook' }); +const promiseCustomize = customize_1.customize({ lambda, event: 'onfido:webhook' }); lambda.tasks.add({ name: 'init', promise: promiseCustomize diff --git a/lib/samplebot/lambda/mqtt/onmessage.js b/lib/samplebot/lambda/mqtt/onmessage.js index 2d16d2100..dcd021f58 100644 --- a/lib/samplebot/lambda/mqtt/onmessage.js +++ b/lib/samplebot/lambda/mqtt/onmessage.js @@ -3,7 +3,7 @@ const bot_1 = require("../../../bot"); const customize_1 = require("../../customize"); const bot = bot_1.createBot({ ready: false }); const lambda = bot.lambdas.onmessage(); -customize_1.customize({ bot, event: 'message' }); +customize_1.customize({ lambda, event: 'message' }); const { handler } = lambda; module.exports = lambda; //# sourceMappingURL=onmessage.js.map \ No newline at end of file diff --git a/lib/samplebot/lambda/onmessagestream.js b/lib/samplebot/lambda/onmessagestream.js index 075c0577a..3b55f5d45 100644 --- a/lib/samplebot/lambda/onmessagestream.js +++ b/lib/samplebot/lambda/onmessagestream.js @@ -4,6 +4,6 @@ const bot_1 = require("../../bot"); const customize_1 = require("../customize"); const bot = bot_1.createBot({ ready: false }); const lambda = bot.lambdas.onmessagestream(); -customize_1.customize({ bot, event: 'messagestream' }); +customize_1.customize({ lambda, event: 'messagestream' }); exports.handler = lambda.handler; //# sourceMappingURL=onmessagestream.js.map \ No newline at end of file diff --git a/lib/samplebot/strategy/index.js b/lib/samplebot/strategy/index.js index 67ec8a510..aeb6e76ce 100644 --- a/lib/samplebot/strategy/index.js +++ b/lib/samplebot/strategy/index.js @@ -19,6 +19,7 @@ const keep_fresh_1 = require("./keep-fresh"); const keep_models_fresh_1 = require("./keep-models-fresh"); const deployment_models_1 = require("../deployment-models"); const bank_models_1 = require("../bank-models"); +const TermsAndConditions = require("./ts-and-cs"); const debug = require('debug')('tradle:sls:products'); const { parseStub } = validateResource.utils; const baseModels = require('../../models'); @@ -32,9 +33,9 @@ const DONT_FORWARD_FROM_EMPLOYEE = [ ]; const USE_ONFIDO = true; const willHandleMessages = event => event === 'message'; -function createProductsBot({ bot, namespace, conf, customModels, style, event }) { +function createProductsBot({ bot, logger, namespace, conf, termsAndConditions, customModels, style, event }) { const { enabled, plugins = {}, autoApprove, approveAllEmployees, graphqlRequiresAuth } = conf.products; - bot.logger.debug('setting up products strategy'); + logger.debug('setting up products strategy'); const deploymentModels = deployment_models_1.default(namespace); const DEPLOYMENT = deploymentModels.deployment.id; const bankModels = bank_models_1.default(namespace); @@ -113,6 +114,14 @@ function createProductsBot({ bot, namespace, conf, customModels, style, event }) }, productsAPI }), true)); + if (termsAndConditions) { + const tcPlugin = TermsAndConditions.createPlugin({ + termsAndConditions, + productsAPI, + logger + }); + productsAPI.plugins.use(tcPlugin, true); + } productsAPI.plugins.use({ onmessage: keepModelsFresh }, true); productsAPI.plugins.use({ 'onmessage:tradle.SimpleMessage': (req) => __awaiter(this, void 0, void 0, function* () { @@ -176,7 +185,7 @@ function createProductsBot({ bot, namespace, conf, customModels, style, event }) productsAPI.plugins.use(customizeMessage({ models: productsAPI.models.all, conf: customizeMessageOpts, - logger: bot.logger + logger })); } return { diff --git a/lib/samplebot/strategy/ts-and-cs.js b/lib/samplebot/strategy/ts-and-cs.js new file mode 100644 index 000000000..868153679 --- /dev/null +++ b/lib/samplebot/strategy/ts-and-cs.js @@ -0,0 +1,61 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const dotProp = require("dot-prop"); +const constants_1 = require("@tradle/constants"); +const TERMS_AND_CONDITIONS = 'tradle.TermsAndConditions'; +const DATE_PRESENTED_PROP = 'tsAndCsState.datePresented'; +const DATE_ACCEPTED_PROP = 'tsAndCsState.datePresented'; +exports.createPlugin = ({ logger, productsAPI, termsAndConditions }) => { + const onmessage = (req) => __awaiter(this, void 0, void 0, function* () { + const { user, payload, type } = req; + if (type === TERMS_AND_CONDITIONS && + payload.termsAndConditions.trim() === termsAndConditions.value.trim()) { + logger.debug(`updating ${user.id}.${DATE_ACCEPTED_PROP}`); + dotProp.set(user, DATE_ACCEPTED_PROP, Date.now()); + return; + } + const accepted = yield exports.ensureAccepted({ + termsAndConditions, + user, + productsAPI, + logger + }); + if (!accepted) + return false; + }); + return { + onmessage + }; +}; +exports.ensureAccepted = ({ req, termsAndConditions, user, productsAPI, logger }) => __awaiter(this, void 0, void 0, function* () { + const dateAccepted = dotProp.get(user, DATE_ACCEPTED_PROP); + if (dateAccepted && dateAccepted > termsAndConditions.lastModified) { + return true; + } + const datePresented = dotProp.get(user, DATE_PRESENTED_PROP); + if (!(datePresented && datePresented > termsAndConditions.lastModified)) { + dotProp.set(user, DATE_PRESENTED_PROP, Date.now()); + logger.debug(`requesting ${user.id} to accept T's and C's`); + yield productsAPI.requestItem({ + req: req || productsAPI.state.newRequestState({ user }), + item: { + form: 'tradle.TermsAndConditions', + prefill: { + [constants_1.TYPE]: 'tradle.TermsAndConditions', + termsAndConditions: termsAndConditions.value + } + } + }); + } + logger.debug(`${user.id} has still not accepted T's and C's!`); + return false; +}); +//# sourceMappingURL=ts-and-cs.js.map \ No newline at end of file diff --git a/src/cacheable-bucket-item.ts b/src/cacheable-bucket-item.ts index 0425e160f..65d5a965f 100644 --- a/src/cacheable-bucket-item.ts +++ b/src/cacheable-bucket-item.ts @@ -1,24 +1,36 @@ - +import { omit } from 'lodash' import { Bucket } from './bucket' +import { DatedValue } from './types' const identity = a => a export class CacheableBucketItem { private value: any private parse: Function + private lastModified?: number constructor(opts: { bucket:Bucket, key:string, ttl?: number parse?: Function }) { - this.value = opts.bucket.getCacheable(opts) - this.parse = identity + this.value = opts.bucket.getCacheable(omit(opts, ['parse'])) + this.parse = opts.parse || identity + this.lastModified = null + } + + public getDatedValue = async ():Promise => { + const value = await this.get() + return { + value, + lastModified: this.lastModified + } } - public get = async (opts?) => { - const value = await this.value.get(opts) - return this.parse(value) + public get = async (opts?):Promise => { + const { Body, LastModified } = await this.value.get(opts) + this.lastModified = new Date(LastModified).getTime() + return this.parse(Body) } public put = async (value:any, opts={}) => { diff --git a/src/delivery-http.ts b/src/delivery-http.ts index 800714cae..eeb526b48 100644 --- a/src/delivery-http.ts +++ b/src/delivery-http.ts @@ -9,7 +9,7 @@ import Env from './env' import Errors = require('./errors') const zlib = promisify(_zlib) -const COMPRESSION_THRESHOLD = 2000 +const COMPRESSION_THRESHOLD = 0//2000 const FETCH_TIMEOUT = 10000 export default class Delivery extends EventEmitter implements IDelivery { diff --git a/src/s3-utils.ts b/src/s3-utils.ts index 56592a272..5b632ce50 100644 --- a/src/s3-utils.ts +++ b/src/s3-utils.ts @@ -114,6 +114,7 @@ export = function createUtils ({ s3, logger }) { }) => { if (!key) throw new Error('expected "key"') if (!bucket) throw new Error('expected "bucket"') + if (!ttl) throw new Error('expected "ttl"') let cached let type diff --git a/src/samplebot/configure.ts b/src/samplebot/configure.ts index d3b9ef5b8..53e0c8cf7 100644 --- a/src/samplebot/configure.ts +++ b/src/samplebot/configure.ts @@ -35,31 +35,49 @@ export const LENSES_KEY = 'conf/lenses.json' export const STYLE_KEY = 'conf/style.json' export const ORG_KEY = 'org/org.json' export const INFO_KEY = 'info/info.json' +export const TERMS_AND_CONDITIONS_KEY = 'conf/terms-and-conditions.md' + +const MINUTE = 3600000 +const HALF_HOUR = MINUTE * 30 +const HOUR = HALF_HOUR * 2 +const DEFAULT_TTL = HALF_HOUR const parts = { org: { bucket: 'PrivateConf', - key: ORG_KEY + key: ORG_KEY, + ttl: DEFAULT_TTL }, style: { bucket: 'PrivateConf', - key: STYLE_KEY + key: STYLE_KEY, + ttl: DEFAULT_TTL }, info: { bucket: 'PrivateConf', - key: INFO_KEY + key: INFO_KEY, + ttl: DEFAULT_TTL }, botConf: { bucket: 'PrivateConf', - key: BOT_CONF_KEY + key: BOT_CONF_KEY, + ttl: DEFAULT_TTL }, models: { bucket: 'PrivateConf', - key: MODELS_KEY + key: MODELS_KEY, + ttl: DEFAULT_TTL }, lenses: { bucket: 'PrivateConf', - key: LENSES_KEY + key: LENSES_KEY, + ttl: DEFAULT_TTL + }, + termsAndConditions: { + bucket: 'PrivateConf', + key: TERMS_AND_CONDITIONS_KEY, + ttl: DEFAULT_TTL, + parse: value => value.toString() } } @@ -73,6 +91,7 @@ export class Conf { public style: CacheableBucketItem public org: CacheableBucketItem public info: CacheableBucketItem + public termsAndConditions: CacheableBucketItem constructor({ bot, logger }: { bot, logger? diff --git a/src/samplebot/customize.ts b/src/samplebot/customize.ts index 0a2b313bd..6194d61a5 100644 --- a/src/samplebot/customize.ts +++ b/src/samplebot/customize.ts @@ -10,9 +10,12 @@ import Errors = require('../errors') const ONFIDO_PLUGIN_PATH = 'products.plugins.onfido' export async function customize (opts) { - const { bot, delayReady, event } = opts + let { lambda, bot, delayReady, event } = opts + if (!bot) bot = lambda.bot + + const { logger } = lambda || bot const confy = createConf({ bot }) - let [org, conf, customModels, style] = await Promise.all([ + let [org, conf, customModels, style, termsAndConditions] = await Promise.all([ confy.org.get(), confy.botConf.get(), confy.models.get().catch(err => { @@ -20,6 +23,10 @@ export async function customize (opts) { }), confy.style.get().catch(err => { Errors.ignore(err, Errors.NotFound) + }), + confy.termsAndConditions.getDatedValue().catch(err => { + // TODO: maybe store in local fs instead of in memory + Errors.ignore(err, Errors.NotFound) }) ]) @@ -37,8 +44,10 @@ export async function customize (opts) { const components = createProductsStrategy({ bot, + logger, namespace, conf, + termsAndConditions, customModels, style, event diff --git a/src/samplebot/lambda/http/auth.ts b/src/samplebot/lambda/http/auth.ts index 52058a92a..53561e509 100644 --- a/src/samplebot/lambda/http/auth.ts +++ b/src/samplebot/lambda/http/auth.ts @@ -3,7 +3,7 @@ import { customize } from '../../customize' const bot = createBot() const lambda = bot.lambdas.auth() -// const promiseCustomize = customize({ bot }) +// const promiseCustomize = customize({ lambda }) // lambda.use(async (ctx) => { // const { employeeManager } = await promiseCustomize // const { userId } = ctx.userId diff --git a/src/samplebot/lambda/http/inbox.ts b/src/samplebot/lambda/http/inbox.ts index dc41a7f49..4905a106f 100644 --- a/src/samplebot/lambda/http/inbox.ts +++ b/src/samplebot/lambda/http/inbox.ts @@ -2,6 +2,6 @@ import { createBot } from '../../../bot' import { customize } from '../../customize' const bot = createBot({ ready: false }) -customize({ bot, event: 'message' }) const lambda = bot.lambdas.inbox() +customize({ lambda, event: 'message' }) export const handler = lambda.handler diff --git a/src/samplebot/lambda/http/onfido-webhook.ts b/src/samplebot/lambda/http/onfido-webhook.ts index 61f8359ec..f9be14461 100644 --- a/src/samplebot/lambda/http/onfido-webhook.ts +++ b/src/samplebot/lambda/http/onfido-webhook.ts @@ -6,7 +6,7 @@ import { customize } from '../../customize' const bot = createBot({ ready: false }) const lambda = bot.createLambda({ source: EventSource.HTTP }) -const promiseCustomize = customize({ bot, event: 'onfido:webhook' }) +const promiseCustomize = customize({ lambda, event: 'onfido:webhook' }) lambda.tasks.add({ name: 'init', promise: promiseCustomize diff --git a/src/samplebot/lambda/http/preauth.ts b/src/samplebot/lambda/http/preauth.ts index dffb79ce2..d10d4fe21 100644 --- a/src/samplebot/lambda/http/preauth.ts +++ b/src/samplebot/lambda/http/preauth.ts @@ -5,6 +5,6 @@ import { EventSource } from '../../../lambda' const bot = createBot() const lambda = bot.lambdas.preauth() -// const promiseCustomize = customize({ bot, event: 'preauth' }) +// const promiseCustomize = customize({ lambda, event: 'preauth' }) export const handler = lambda.handler diff --git a/src/samplebot/lambda/mqtt/onmessage.ts b/src/samplebot/lambda/mqtt/onmessage.ts index 72fb86016..146e14a21 100644 --- a/src/samplebot/lambda/mqtt/onmessage.ts +++ b/src/samplebot/lambda/mqtt/onmessage.ts @@ -4,6 +4,6 @@ import { customize } from '../../customize' const bot = createBot({ ready: false }) const lambda = bot.lambdas.onmessage() -customize({ bot, event: 'message' }) +customize({ lambda, event: 'message' }) const { handler } = lambda export = lambda diff --git a/src/samplebot/lambda/onmessagestream.ts b/src/samplebot/lambda/onmessagestream.ts index 3f1ae1e86..79869efa9 100644 --- a/src/samplebot/lambda/onmessagestream.ts +++ b/src/samplebot/lambda/onmessagestream.ts @@ -4,6 +4,6 @@ import { customize } from '../customize' const bot = createBot({ ready: false }) const lambda = bot.lambdas.onmessagestream() -customize({ bot, event: 'messagestream' }) +customize({ lambda, event: 'messagestream' }) export const handler = lambda.handler diff --git a/src/samplebot/strategy/index.ts b/src/samplebot/strategy/index.ts index fe76dcafe..d6544cc1f 100644 --- a/src/samplebot/strategy/index.ts +++ b/src/samplebot/strategy/index.ts @@ -16,8 +16,11 @@ import { import { createGraphQLAuth } from './graphql-auth' // import { tradle as defaultTradleInstance } from '../../' import createBot = require('../../bot') +import { DatedValue } from '../../types' import createDeploymentModels from '../deployment-models' import createBankModels from '../bank-models' +import * as TermsAndConditions from './ts-and-cs' +import Logger from '../../logger' const debug = require('debug')('tradle:sls:products') const { parseStub } = validateResource.utils @@ -39,11 +42,22 @@ const willHandleMessages = event => event === 'message' export default function createProductsBot ({ bot, + logger, namespace, conf, + termsAndConditions, customModels, style, event +}: { + bot: any, + logger: Logger, + namespace: string, + conf: any, + customModels: any, + style?: any, + termsAndConditions?: DatedValue, + event?: string }) { const { enabled, @@ -55,7 +69,7 @@ export default function createProductsBot ({ graphqlRequiresAuth } = conf.products - bot.logger.debug('setting up products strategy') + logger.debug('setting up products strategy') const deploymentModels = createDeploymentModels(namespace) const DEPLOYMENT = deploymentModels.deployment.id @@ -103,7 +117,6 @@ export default function createProductsBot ({ productsAPI.emit('bot', bot) } - // prepend let commands if (handleMessages) { const { Commander } = require('./commander') @@ -155,6 +168,17 @@ export default function createProductsBot ({ }), true)) // prepend + + if (termsAndConditions) { + const tcPlugin = TermsAndConditions.createPlugin({ + termsAndConditions, + productsAPI, + logger + }) + + productsAPI.plugins.use(tcPlugin, true) // prepend + } + productsAPI.plugins.use({ onmessage: keepModelsFresh }, true) productsAPI.plugins.use({ // 'onmessage:tradle.Form': async (req) => { @@ -272,7 +296,7 @@ export default function createProductsBot ({ productsAPI.plugins.use(customizeMessage({ models: productsAPI.models.all, conf: customizeMessageOpts, - logger: bot.logger + logger })) } diff --git a/src/samplebot/strategy/ts-and-cs.ts b/src/samplebot/strategy/ts-and-cs.ts new file mode 100644 index 000000000..d5cfafeff --- /dev/null +++ b/src/samplebot/strategy/ts-and-cs.ts @@ -0,0 +1,81 @@ +import crypto = require('crypto') +import dotProp = require('dot-prop') +import { TYPE } from '@tradle/constants' +import { DatedValue } from '../../types' +import Logger from '../../logger' + +const TERMS_AND_CONDITIONS = 'tradle.TermsAndConditions' +const DATE_PRESENTED_PROP = 'tsAndCsState.datePresented' +const DATE_ACCEPTED_PROP = 'tsAndCsState.datePresented' + +export const createPlugin = ({ + logger, + productsAPI, + termsAndConditions +}: { + logger: Logger, + productsAPI: any, + termsAndConditions: DatedValue +}) => { + const onmessage = async (req) => { + const { user, payload, type } = req + if (type === TERMS_AND_CONDITIONS && + payload.termsAndConditions.trim() === termsAndConditions.value.trim()) { + logger.debug(`updating ${user.id}.${DATE_ACCEPTED_PROP}`) + dotProp.set(user, DATE_ACCEPTED_PROP, Date.now()) + return + } + + const accepted = await ensureAccepted({ + termsAndConditions, + user, + productsAPI, + logger + }) + + if (!accepted) return false // exit middleware + } + + return { + onmessage + } +} + +export const ensureAccepted = async ({ + req, + termsAndConditions, + user, + productsAPI, + logger +}: { + req?: any, + termsAndConditions: DatedValue, + user: any, + productsAPI: any, + logger: Logger +}) => { + const dateAccepted = dotProp.get(user, DATE_ACCEPTED_PROP) + if (dateAccepted && dateAccepted > termsAndConditions.lastModified) { + return true + } + + const datePresented = dotProp.get(user, DATE_PRESENTED_PROP) + if (!(datePresented && datePresented > termsAndConditions.lastModified)) { + dotProp.set(user, DATE_PRESENTED_PROP, Date.now()) + + logger.debug(`requesting ${user.id} to accept T's and C's`) + await productsAPI.requestItem({ + req: req || productsAPI.state.newRequestState({ user }), + item: { + form: 'tradle.TermsAndConditions', + prefill: { + [TYPE]: 'tradle.TermsAndConditions', + termsAndConditions: termsAndConditions.value + } + } + }) + } + + logger.debug(`${user.id} has still not accepted T's and C's!`) + return false +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 45a25de7a..fe0fb32be 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -183,3 +183,8 @@ export type CacheContainer = { cache: any logger: Logger } + +export type DatedValue = { + lastModified: number + value: any +}