From 955d1aa1b2728689195e408692f5318206b11654 Mon Sep 17 00:00:00 2001 From: keyan Date: Fri, 15 Oct 2021 18:07:51 -0500 Subject: [PATCH] finish up invites --- api/resolvers/invite.js | 17 +++++ api/resolvers/serial.js | 6 ++ api/resolvers/user.js | 6 ++ api/typeDefs/invite.js | 3 + api/typeDefs/user.js | 1 + components/header.js | 7 ++- components/login.js | 3 +- components/me.js | 1 + fragments/invites.js | 5 ++ pages/invites/[id].js | 62 ++++++++++++++++++- pages/invites/index.js | 6 +- .../20211015180613_invite_func/migration.sql | 45 ++++++++++++++ 12 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 prisma/migrations/20211015180613_invite_func/migration.sql diff --git a/api/resolvers/invite.js b/api/resolvers/invite.js index 056a356a..df6bdc1b 100644 --- a/api/resolvers/invite.js +++ b/api/resolvers/invite.js @@ -10,6 +10,16 @@ export default { return await models.invite.findMany({ where: { userId: me.id + }, + orderBy: { + createdAt: 'desc' + } + }) + }, + invite: async (parent, { id }, { me, models }) => { + return await models.invite.findUnique({ + where: { + id } }) } @@ -44,6 +54,13 @@ export default { Invite: { invitees: async (invite, args, { me, models }) => { return await models.user.findMany({ where: { inviteId: invite.id } }) + }, + user: async (invite, args, { me, models }) => { + return await models.user.findUnique({ where: { id: invite.userId } }) + }, + poor: async (invite, args, { me, models }) => { + const user = await models.user.findUnique({ where: { id: invite.userId } }) + return user.msats < invite.gift * 1000 } } } diff --git a/api/resolvers/serial.js b/api/resolvers/serial.js index da841435..3ab93108 100644 --- a/api/resolvers/serial.js +++ b/api/resolvers/serial.js @@ -23,6 +23,12 @@ async function serialize (models, call) { if (error.message.includes('SN_PENDING_WITHDRAWL_EXISTS')) { bail(new Error('withdrawal invoice exists and is pending')) } + if (error.message.includes('SN_INELIGIBLE')) { + bail(new Error('user ineligible for gift')) + } + if (error.message.includes('SN_REVOKED_OR_EXHAUSTED')) { + bail(new Error('faucet has been revoked or is exhausted')) + } if (error.message.includes('40001')) { throw new Error('wallet balance serialization failure - retry again') } diff --git a/api/resolvers/user.js b/api/resolvers/user.js index 983a0b15..a0ed9062 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -84,6 +84,12 @@ export default { bio: async (user, args, { models }) => { return getItem(user, { id: user.bioId }, { models }) }, + hasInvites: async (user, args, { models }) => { + const anInvite = await models.invite.findFirst({ + where: { userId: user.id } + }) + return !!anInvite + }, hasNewNotes: async (user, args, { models }) => { // check if any votes have been cast for them since checkedNotesAt const votes = await models.$queryRaw(` diff --git a/api/typeDefs/invite.js b/api/typeDefs/invite.js index 4c996776..8d377f84 100644 --- a/api/typeDefs/invite.js +++ b/api/typeDefs/invite.js @@ -3,6 +3,7 @@ import { gql } from 'apollo-server-micro' export default gql` extend type Query { invites: [Invite!]! + invite(id: ID!): Invite } extend type Mutation { @@ -16,6 +17,8 @@ export default gql` invitees: [User!]! gift: Int! limit: Int + user: User! revoked: Boolean! + poor: Boolean! } ` diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index 8ce4d543..a0407f06 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -23,6 +23,7 @@ export default gql` freePosts: Int! freeComments: Int! hasNewNotes: Boolean! + hasInvites: Boolean! tipDefault: Int! bio: Item sats: Int! diff --git a/components/header.js b/components/header.js index db3ce78b..1c70570a 100644 --- a/components/header.js +++ b/components/header.js @@ -64,7 +64,12 @@ export default function Header () { - invites + invites + {me && !me.hasInvites && +
+ {' '} +
} +
diff --git a/components/login.js b/components/login.js index 881d982f..0d320a07 100644 --- a/components/login.js +++ b/components/login.js @@ -17,7 +17,7 @@ export const EmailSchema = Yup.object({ email: Yup.string().email('email is no good').required('required').trim() }) -export default function Login ({ providers, callbackUrl, error }) { +export default function Login ({ providers, callbackUrl, error, Header }) { const errors = { Signin: 'Try signing with a different account.', OAuthSignin: 'Try signing with a different account.', @@ -37,6 +37,7 @@ export default function Login ({ providers, callbackUrl, error }) { return (
+ {Header &&
} {errorMessage && setErrorMessage(undefined)} dismissible>{errorMessage}} {router.query.type === 'lightning' diff --git a/components/me.js b/components/me.js index 0dc9831d..b41969c3 100644 --- a/components/me.js +++ b/components/me.js @@ -20,6 +20,7 @@ export function MeProvider ({ children }) { bio { id } + hasInvites } }` const { data } = useQuery(query, { pollInterval: 1000 }) diff --git a/fragments/invites.js b/fragments/invites.js index 4ed25a8b..11e8436a 100644 --- a/fragments/invites.js +++ b/fragments/invites.js @@ -11,5 +11,10 @@ export const INVITE_FIELDS = gql` gift limit revoked + user { + name + id + } + poor } ` diff --git a/pages/invites/[id].js b/pages/invites/[id].js index 5c436962..ca009fe6 100644 --- a/pages/invites/[id].js +++ b/pages/invites/[id].js @@ -1,11 +1,42 @@ import Login from '../../components/login' import { providers, getSession } from 'next-auth/client' +import models from '../../api/models' +import serialize from '../../api/resolvers/serial' +import { gql } from '@apollo/client' +import { INVITE_FIELDS } from '../../fragments/invites' +import getSSRApolloClient from '../../api/ssrApollo' +import Link from 'next/link' export async function getServerSideProps ({ req, res, query: { id, error = null } }) { const session = await getSession({ req }) + const client = await getSSRApolloClient(req) + const { data } = await client.query({ + query: gql` + ${INVITE_FIELDS} + { + invite(id: "${id}") { + ...InviteFields + } + }` + }) + + if (!data?.invite) { + return { + notFound: true + } + } + if (session && res) { - // send down the userid and the invite to the db + try { + // attempt to send gift + // catch any errors and just ignore them for now + await serialize(models, + models.$queryRaw('SELECT invite_drain($1, $2)', session.user.id, id)) + } catch (e) { + console.log(e) + } + res.writeHead(302, { Location: '/' }) @@ -17,9 +48,36 @@ export async function getServerSideProps ({ req, res, query: { id, error = null props: { providers: await providers({ req, res }), callbackUrl: process.env.SELF_URL + req.url, + invite: data.invite, error } } } -export default Login +function InviteHeader ({ invite }) { + console.log(invite.poor) + let Inner + if (invite.revoked) { + Inner = () =>
this invite link expired
+ } else if ((invite.limit && invite.limit <= invite.invitees.length) || invite.poor) { + Inner = () =>
this invite link has no more sats
+ } else { + Inner = () => ( +
+ get {invite.gift} free sats from{' '} + @{invite.user.name}{' '} + when you sign up today +
+ ) + } + + return ( +

+ +

+ ) +} + +export default function Invite ({ invite, ...props }) { + return } {...props} /> +} diff --git a/pages/invites/index.js b/pages/invites/index.js index b0b64e69..621d5bb3 100644 --- a/pages/invites/index.js +++ b/pages/invites/index.js @@ -46,10 +46,10 @@ function InviteForm () { limit: undefined }} schema={InviteSchema} - onSubmit={async ({ limit, ...values }) => { + onSubmit={async ({ limit, gift }) => { const { error } = await createInvite({ variables: { - ...values, limit: limit ? Number(limit) : limit + gift: Number(gift), limit: limit ? Number(limit) : limit } }) if (error) { @@ -95,6 +95,8 @@ function Invite ({ invite, active }) { placeholder={`https://stacker.news/invites/${invite.id}`} readOnly />
+ {invite.gift} sat gift + \ {invite.invitees.length} joined{invite.limit ? ` of ${invite.limit}` : ''} {active ? ( diff --git a/prisma/migrations/20211015180613_invite_func/migration.sql b/prisma/migrations/20211015180613_invite_func/migration.sql new file mode 100644 index 00000000..1ec96af8 --- /dev/null +++ b/prisma/migrations/20211015180613_invite_func/migration.sql @@ -0,0 +1,45 @@ +CREATE OR REPLACE FUNCTION invite_drain(user_id INTEGER, invite_id TEXT) +RETURNS INTEGER +LANGUAGE plpgsql +AS $$ +DECLARE + inviter_id INTEGER; + inviter_sats INTEGER; + gift INTEGER; +BEGIN + PERFORM ASSERT_SERIALIZED(); + -- check user was created in last hour + -- check user did not already redeem an invite + PERFORM FROM users + WHERE id = user_id AND users.created_at >= NOW() AT TIME ZONE 'UTC' - INTERVAL '1 HOUR' + AND users."inviteId" IS NULL; + IF NOT FOUND THEN + RAISE EXCEPTION 'SN_INELIGIBLE'; + END IF; + + -- check that invite has not reached limit + -- check that invite is not revoked + SELECT "Invite"."userId", "Invite".gift INTO inviter_id, gift FROM "Invite" + LEFT JOIN users ON users."inviteId" = invite_id + WHERE "Invite".id = invite_id AND NOT "Invite".revoked + GROUP BY "Invite".id + HAVING COUNT(DISTINCT users.id) < "Invite".limit OR "Invite".limit IS NULL; + IF NOT FOUND THEN + RAISE EXCEPTION 'SN_REVOKED_OR_EXHAUSTED'; + END IF; + + -- check that inviter has sufficient balance + SELECT (msats / 1000) INTO inviter_sats + FROM users WHERE id = inviter_id; + IF inviter_sats < gift THEN + RAISE EXCEPTION 'SN_REVOKED_OR_EXHAUSTED'; + END IF; + + -- subtract amount from inviter + UPDATE users SET msats = msats - (1000 * gift) WHERE id = inviter_id; + -- add amount to invitee + UPDATE users SET msats = msats + (1000 * gift), "inviteId" = invite_id WHERE id = user_id; + + RETURN 0; +END; +$$; \ No newline at end of file