-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add email subscriptions #470
base: master
Are you sure you want to change the base?
Changes from all commits
f97e238
fb49cc2
48da325
81aa889
48c73f0
6993a1a
50aea60
386135e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,10 @@ const NETWORK_METADATA = { | |
} | ||
}; | ||
|
||
function shouldPinIpfs(type: string, message: any) { | ||
return !(type === 'email-subscription' && message.email); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think we can exclude if it is |
||
} | ||
|
||
export default async function ingestor(req) { | ||
if (flaggedIps.includes(sha256(getIp(req)))) { | ||
return Promise.reject('unauthorized'); | ||
|
@@ -92,7 +96,11 @@ export default async function ingestor(req) { | |
} | ||
|
||
let aliased = false; | ||
if (!['settings', 'alias', 'profile'].includes(type)) { | ||
if ( | ||
!['settings', 'alias', 'profile', 'email-subscription', 'delete-email-subscription'].includes( | ||
type | ||
) | ||
) { | ||
if (!message.space) return Promise.reject('unknown space'); | ||
|
||
try { | ||
|
@@ -106,7 +114,16 @@ export default async function ingestor(req) { | |
} | ||
|
||
// Check if signing address is an alias | ||
const aliasTypes = ['follow', 'unfollow', 'subscribe', 'unsubscribe', 'profile', 'statement']; | ||
const aliasTypes = [ | ||
'follow', | ||
'unfollow', | ||
'subscribe', | ||
'unsubscribe', | ||
'profile', | ||
'statement', | ||
'email-subscription', | ||
'delete-email-subscription' | ||
]; | ||
const aliasOptionTypes = ['vote', 'vote-array', 'vote-string', 'proposal', 'delete-proposal']; | ||
if (body.address !== message.from) { | ||
if (!aliasTypes.includes(type) && !aliasOptionTypes.includes(type)) | ||
|
@@ -206,6 +223,13 @@ export default async function ingestor(req) { | |
type = 'vote'; | ||
} | ||
|
||
if (type === 'email-subscription') { | ||
payload = { | ||
email: message.email, | ||
subscriptions: message.subscriptions | ||
}; | ||
} | ||
|
||
let legacyBody: any = { | ||
address: message.from, | ||
msg: JSON.stringify({ | ||
|
@@ -245,7 +269,7 @@ export default async function ingestor(req) { | |
...restBody | ||
}; | ||
[pinned, receipt] = await Promise.all([ | ||
pin(ipfsBody, process.env.PINEAPPLE_URL), | ||
shouldPinIpfs(type, message) ? pin(ipfsBody, process.env.PINEAPPLE_URL) : { cid: '' }, | ||
issueReceipt(formattedSignature) | ||
]); | ||
} catch (e) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { envelopDB } from '../helpers/mysql'; | ||
|
||
type Message = { address: string }; | ||
|
||
export async function verify(message: Message): Promise<boolean> { | ||
const result = await envelopDB.queryAsync( | ||
'SELECT * FROM subscribers WHERE address = ? AND verified > 0 LIMIT 1', | ||
[message.address] | ||
); | ||
|
||
if (!result[0]) { | ||
return Promise.reject('user not subscribed'); | ||
} | ||
|
||
return true; | ||
} | ||
|
||
export async function action(message: Message): Promise<void> { | ||
await envelopDB.queryAsync('DELETE FROM subscribers WHERE address = ? AND verified > 0 LIMIT 1', [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should delete even if it is not verified no? if I add some wrong email by mistake |
||
message.address | ||
]); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import snapshot from '@snapshot-labs/snapshot.js'; | ||
import log from '../helpers/log'; | ||
import { envelopDB } from '../helpers/mysql'; | ||
import { jsonParse } from '../helpers/utils'; | ||
|
||
type Message = { msg: string; address: string }; | ||
type Payload = { | ||
email?: string; | ||
subscriptions?: string[]; | ||
}; | ||
|
||
function extractPayload(message: Message): Payload { | ||
return jsonParse(message.msg).payload; | ||
} | ||
|
||
export async function verify(message: Message): Promise<boolean> { | ||
const payload = extractPayload(message); | ||
|
||
const schemaIsValid = snapshot.utils.validateSchema(snapshot.schemas.emailSubscription, payload); | ||
Check failure on line 19 in src/writer/email-subscription.ts GitHub Actions / test / Test
Check failure on line 19 in src/writer/email-subscription.ts GitHub Actions / lint / Lint
|
||
if (schemaIsValid !== true) { | ||
log.warn(`[writer] Wrong email subscription format ${JSON.stringify(schemaIsValid)}`); | ||
return Promise.reject('wrong email subscription format'); | ||
} | ||
|
||
if (payload.email?.length) { | ||
return verifySubscriptionCreation(message, payload); | ||
} else { | ||
return verifySubscriptionUpdate(message, payload); | ||
} | ||
} | ||
|
||
export async function action(message: Message): Promise<void> { | ||
const payload = extractPayload(message); | ||
|
||
if (payload.email?.length) { | ||
await createAction(message, payload); | ||
} else { | ||
await updateAction(message, payload); | ||
} | ||
} | ||
|
||
async function verifySubscriptionCreation(message: Message, payload: Payload): Promise<boolean> { | ||
const result = await envelopDB.queryAsync( | ||
`SELECT * FROM subscribers WHERE address = ? AND email = ? LIMIT 1`, | ||
[message.address, payload.email] | ||
); | ||
|
||
if (result[0]) { | ||
return Promise.reject('email already subscribed'); | ||
} | ||
|
||
return true; | ||
} | ||
|
||
async function verifySubscriptionUpdate(message: Message, payload: Payload): Promise<boolean> { | ||
const result = await envelopDB.queryAsync( | ||
`SELECT * FROM subscribers WHERE address = ? ORDER BY verified DESC LIMIT 1`, | ||
[message.address, payload.email] | ||
); | ||
|
||
if (!result[0]) { | ||
return Promise.reject('email not subscribed'); | ||
} | ||
|
||
if (!result[0].verified) { | ||
return Promise.reject('email not verified'); | ||
} | ||
|
||
return true; | ||
} | ||
|
||
async function createAction(message: Message, payload: Payload) { | ||
await envelopDB.queryAsync(`INSERT INTO subscribers (email, address, created) VALUES(?, ?, ?)`, [ | ||
payload.email, | ||
message.address, | ||
(Date.now() / 1e3).toFixed() | ||
]); | ||
} | ||
|
||
async function updateAction(message: Message, payload: Payload) { | ||
await envelopDB.queryAsync( | ||
`UPDATE subscribers SET subscriptions = ? WHERE address = ? AND verified > 0 LIMIT 1`, | ||
[JSON.stringify(payload.subscriptions), message.address] | ||
); | ||
} |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -5,6 +5,7 @@ import { DEFAULT_NETWORK_ID, jsonParse, NETWORK_IDS } from '../helpers/utils'; | |||
|
||||
export async function verify(body): Promise<any> { | ||||
const msg = jsonParse(body.msg, {}); | ||||
|
||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||
const schemaIsValid = snapshot.utils.validateSchema(snapshot.schemas.statement, msg.payload); | ||||
if (schemaIsValid !== true) { | ||||
log.warn(`[writer] Wrong statement format ${JSON.stringify(schemaIsValid)}`); | ||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
HUB_DATABASE_URL=mysql://root:[email protected]:3306/snapshot_sequencer_test | ||
SEQ_DATABASE_URL=mysql://root:[email protected]:3306/snapshot_sequencer_test | ||
ENVELOP_DATABASE_URL=mysql://root:[email protected]:3306/snapshot_sequencer_test | ||
NETWORK=mainnet | ||
RELAYER_PK=01686849e86499c1860ea0afc97f29c11018cbac049abf843df875c60054076e | ||
NODE_ENV=test | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import db, { envelopDB, sequencerDB } from '../../../src/helpers/mysql'; | ||
import { action, verify } from '../../../src/writer/delete-email-subscription'; | ||
|
||
describe('writer/delete-subscription', () => { | ||
const TEST_PREFIX = 'test-delete-subscription'; | ||
|
||
afterAll(async () => { | ||
await envelopDB.queryAsync('DELETE FROM subscribers'); | ||
await envelopDB.endAsync(); | ||
await db.endAsync(); | ||
await sequencerDB.endAsync(); | ||
}); | ||
|
||
describe('verify()', () => { | ||
beforeAll(async () => { | ||
await Promise.all([ | ||
envelopDB.queryAsync( | ||
'INSERT INTO subscribers SET address = ?, email = ?, subscriptions = ?, created = ?, verified = ?', | ||
[`${TEST_PREFIX}-0x0`, '[email protected]', '[]', 0, 0] | ||
), | ||
envelopDB.queryAsync( | ||
'INSERT INTO subscribers SET address = ?, email = ?, subscriptions = ?, created = ?, verified = ?', | ||
[`${TEST_PREFIX}-0x1`, '[email protected]', '[]', 0, 1] | ||
) | ||
]); | ||
}); | ||
|
||
it('rejects when the address is not subscribed', () => { | ||
return expect(verify({ address: '0x0' })).rejects.toEqual(`user not subscribed`); | ||
}); | ||
|
||
it('rejects when the address is not verified', () => { | ||
return expect(verify({ address: `${TEST_PREFIX}-0x0` })).rejects.toEqual( | ||
`user not subscribed` | ||
); | ||
}); | ||
|
||
it('resolves when the address is verified', () => { | ||
expect(verify({ address: `${TEST_PREFIX}-0x1` })).resolves; | ||
}); | ||
}); | ||
|
||
describe('action()', () => { | ||
const address = `${TEST_PREFIX}-0x3`; | ||
|
||
beforeAll(async () => { | ||
await Promise.all([ | ||
envelopDB.queryAsync( | ||
'INSERT INTO subscribers SET address = ?, email = ?, subscriptions = ?, created = ?, verified = ?', | ||
[address, '[email protected]', '[]', 0, 0] | ||
), | ||
envelopDB.queryAsync( | ||
'INSERT INTO subscribers SET address = ?, email = ?, subscriptions = ?, created = ?, verified = ?', | ||
[address, '[email protected]', '[]', 0, 1] | ||
) | ||
]); | ||
}); | ||
|
||
it('deletes the subscription', async () => { | ||
await action({ address: address }); | ||
|
||
const results = await envelopDB.queryAsync('SELECT * FROM subscribers WHERE address = ?', [ | ||
address | ||
]); | ||
|
||
// Only delete the verified subscription | ||
expect(results.length).toBe(1); | ||
expect(results[0].email).toEqual('[email protected]'); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import db, { envelopDB, sequencerDB } from '../../../src/helpers/mysql'; | ||
import { action, verify } from '../../../src/writer/email-subscription'; | ||
|
||
describe('writer/subscription', () => { | ||
const TEST_PREFIX = 'test-subscription'; | ||
const msg = JSON.stringify({ payload: { email: '[email protected]', subscriptions: [] } }); | ||
|
||
afterAll(async () => { | ||
await envelopDB.queryAsync('DELETE FROM subscribers'); | ||
await envelopDB.endAsync(); | ||
await db.endAsync(); | ||
await sequencerDB.endAsync(); | ||
}); | ||
|
||
describe('verify()', () => { | ||
const address = `${TEST_PREFIX}-0x0`; | ||
const invalidMsg = JSON.stringify({ | ||
payload: { email: 'not an email' } | ||
}); | ||
|
||
beforeAll(async () => { | ||
await envelopDB.queryAsync( | ||
'INSERT INTO subscribers SET address = ?, email = ?, subscriptions = ?, created = ?, verified = ?', | ||
[address, '[email protected]', '[]', 0, 0] | ||
); | ||
}); | ||
|
||
it('rejects when the address is already subscribed', () => { | ||
return expect(verify({ address: address, msg })).rejects.toEqual('email already subscribed'); | ||
}); | ||
|
||
it('rejects when the subscription type is not valid', () => { | ||
return expect(verify({ address: address, msg: invalidMsg })).rejects.toEqual( | ||
'wrong email subscription format' | ||
); | ||
}); | ||
|
||
it('resolves when all args are valid', () => { | ||
return expect(verify({ address: `${TEST_PREFIX}-0x1`, msg })).resolves.toBe(true); | ||
}); | ||
}); | ||
|
||
describe('action()', () => { | ||
const address = `${TEST_PREFIX}-0x1`; | ||
|
||
it('creates a subscription', async () => { | ||
await action({ | ||
address: address, | ||
msg | ||
}); | ||
|
||
const result = await envelopDB.queryAsync( | ||
`SELECT * FROM subscribers WHERE address = ? LIMIT 1`, | ||
[address] | ||
); | ||
|
||
expect(result[0].email).toEqual('[email protected]'); | ||
expect(result[0].verified).toEqual(0); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should make this optional