diff --git a/.env.example b/.env.example index 145016ca..634b4892 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,7 @@ PORT=3000 NETWORK=testnet HUB_DATABASE_URL=mysql://... SEQ_DATABASE_URL=mysql:// +ENVELOP_DATABASE_URL=mysql:// SEQUENCER_URL=https://testnet.seq.snapshot.org STAMP_URL=https://stamp.fyi KEYCARD_URL=https://keycard.snapshot.org diff --git a/src/graphql/operations/users.ts b/src/graphql/operations/users.ts index ef9414ff..a70ddce3 100644 --- a/src/graphql/operations/users.ts +++ b/src/graphql/operations/users.ts @@ -1,6 +1,6 @@ import { capture } from '@snapshot-labs/snapshot-sentry'; import log from '../../helpers/log'; -import db from '../../helpers/mysql'; +import db, { envelopDB } from '../../helpers/mysql'; import { buildWhereQuery, checkLimits, formatUser } from '../helpers'; export default async function (parent, args) { @@ -72,6 +72,15 @@ export default async function (parent, args) { user.lastVote = count.lastVote; }); } + + const subscribers = await getEmailSubscribers(ids); + users.forEach((user: any) => { + user.emailSubscription = subscribers[user.id] || { + status: 'NOT_SUBSCRIBED', + subscriptions: [] + }; + }); + return users.map(formatUser); } catch (e: any) { log.error(`[graphql] users, ${JSON.stringify(e)}`); @@ -79,3 +88,26 @@ export default async function (parent, args) { return Promise.reject(new Error('request failed')); } } + +async function getEmailSubscribers( + user_ids: string[] +): Promise< + Record +> { + if (!envelopDB) return {}; + + const subscribers = await envelopDB.queryAsync( + `SELECT verified, address, subscriptions FROM subscribers WHERE address IN (?)`, + user_ids + ); + + return Object.fromEntries( + subscribers.map((s: any) => [ + s.address, + { + status: s.verified > 0 ? 'VERIFIED' : 'UNVERIFIED', + subscriptions: JSON.parse(s.subscriptions) + } + ]) + ); +} diff --git a/src/graphql/schema.gql b/src/graphql/schema.gql index c4823f34..2d4d1a59 100644 --- a/src/graphql/schema.gql +++ b/src/graphql/schema.gql @@ -520,6 +520,11 @@ type DelegationPortal { delegationApi: String! } +type EmailSubscription { + status: String! + subscriptions: [String] +} + type Vote { id: String! ipfs: String @@ -581,6 +586,7 @@ type User { votesCount: Int proposalsCount: Int lastVote: Int + emailSubscription: EmailSubscription } type Statement { diff --git a/src/helpers/mysql.ts b/src/helpers/mysql.ts index f5903bdb..21fd314f 100644 --- a/src/helpers/mysql.ts +++ b/src/helpers/mysql.ts @@ -19,7 +19,6 @@ hubConfig.connectTimeout = 60e3; hubConfig.acquireTimeout = 60e3; hubConfig.timeout = 60e3; hubConfig.charset = 'utf8mb4'; - const hubDB = mysql.createPool(hubConfig); // @ts-ignore @@ -35,6 +34,22 @@ sequencerConfig.timeout = 60e3; sequencerConfig.charset = 'utf8mb4'; const sequencerDB = mysql.createPool(sequencerConfig); +let envelopDB; +if (process.env.ENVELOP_DATABASE_URL) { + // @ts-ignore + const envelopConfig = parse(process.env.ENVELOP_DATABASE_URL); + envelopConfig.connectionLimit = connectionLimit; + envelopConfig.multipleStatements = true; + envelopConfig.database = envelopConfig.path[0]; + envelopConfig.host = envelopConfig.hosts[0].name; + envelopConfig.port = envelopConfig.hosts[0].port; + envelopConfig.connectTimeout = 60e3; + envelopConfig.acquireTimeout = 60e3; + envelopConfig.timeout = 60e3; + envelopConfig.charset = 'utf8mb4'; + envelopDB = mysql.createPool(envelopConfig); +} + bluebird.promisifyAll([Pool, Connection]); -export { hubDB as default, sequencerDB }; +export { hubDB as default, sequencerDB, envelopDB }; diff --git a/test/.env.test b/test/.env.test index 7346081a..e7669e7e 100644 --- a/test/.env.test +++ b/test/.env.test @@ -1,3 +1,4 @@ HUB_DATABASE_URL=mysql://root:root@127.0.0.1:3306/hub_test SEQ_DATABASE_URL=mysql://root:root@127.0.0.1:3306/hub_test +ENVELOP_DATABASE_URL=mysql://root:root@127.0.0.1:3306/hub_test NODE_ENV=test diff --git a/test/schema_envelop.sql b/test/schema_envelop.sql new file mode 100644 index 00000000..8e67b7ab --- /dev/null +++ b/test/schema_envelop.sql @@ -0,0 +1,11 @@ +CREATE TABLE subscribers ( + email VARCHAR(256) NOT NULL, + address VARCHAR(256) NOT NULL, + subscriptions JSON DEFAULT NULL, + created BIGINT NOT NULL, + verified BIGINT NOT NULL DEFAULT 0, + PRIMARY KEY (email, address), + UNIQUE KEY idx_address_email (address, email), + INDEX created (created), + INDEX verified (verified) +); diff --git a/test/setupDb.ts b/test/setupDb.ts index 8dbec39a..867f374a 100644 --- a/test/setupDb.ts +++ b/test/setupDb.ts @@ -15,6 +15,8 @@ bluebird.promisifyAll([Pool, Connection]); const db = mysql.createPool(config); const dbName = config.path[0]; +const schemaFiles = ['./src/helpers/schema.sql', './test/schema_envelop.sql']; + if (!dbName.endsWith(TEST_DATABASE_SUFFIX)) { console.error( `Invalid test database name. Must end with ${TEST_DATABASE_SUFFIX}` @@ -33,8 +35,9 @@ async function run() { console.info(`- Creating new database: ${dbName}`); await db.queryAsync(`CREATE DATABASE ${dbName}`); - const schema = fs - .readFileSync('./src/helpers/schema.sql', 'utf8') + const schema = schemaFiles + .map(file => fs.readFileSync(file, 'utf8')) + .join(' ') .replaceAll('CREATE TABLE ', `CREATE TABLE ${dbName}.`) .split(splitToken) .filter(s => s.trim().length > 0);