finish up invites

This commit is contained in:
keyan 2021-10-15 18:07:51 -05:00
parent 3a52f8967a
commit 955d1aa1b2
12 changed files with 156 additions and 6 deletions

View File

@ -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
}
}
}

View File

@ -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')
}

View File

@ -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(`

View File

@ -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!
}
`

View File

@ -23,6 +23,7 @@ export default gql`
freePosts: Int!
freeComments: Int!
hasNewNotes: Boolean!
hasInvites: Boolean!
tipDefault: Int!
bio: Item
sats: Int!

View File

@ -64,7 +64,12 @@ export default function Header () {
</Link>
<NavDropdown.Divider />
<Link href='/invites' passHref>
<NavDropdown.Item>invites</NavDropdown.Item>
<NavDropdown.Item>invites
{me && !me.hasInvites &&
<div className='p-1 d-inline-block bg-success ml-1'>
<span className='invisible'>{' '}</span>
</div>}
</NavDropdown.Item>
</Link>
<div>
<NavDropdown.Divider />

View File

@ -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 (
<LayoutCenter noFooter>
<div className={styles.login}>
{Header && <Header />}
{errorMessage &&
<Alert variant='danger' onClose={() => setErrorMessage(undefined)} dismissible>{errorMessage}</Alert>}
{router.query.type === 'lightning'

View File

@ -20,6 +20,7 @@ export function MeProvider ({ children }) {
bio {
id
}
hasInvites
}
}`
const { data } = useQuery(query, { pollInterval: 1000 })

View File

@ -11,5 +11,10 @@ export const INVITE_FIELDS = gql`
gift
limit
revoked
user {
name
id
}
poor
}
`

View File

@ -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 = () => <div className='text-danger'>this invite link expired</div>
} else if ((invite.limit && invite.limit <= invite.invitees.length) || invite.poor) {
Inner = () => <div className='text-danger'>this invite link has no more sats</div>
} else {
Inner = () => (
<div>
get <span className='text-success'>{invite.gift} free sats</span> from{' '}
<Link href={`/${invite.user.name}`} passHref><a>@{invite.user.name}</a></Link>{' '}
when you sign up today
</div>
)
}
return (
<h2 className='text-center pb-3'>
<Inner />
</h2>
)
}
export default function Invite ({ invite, ...props }) {
return <Login Header={() => <InviteHeader invite={invite} />} {...props} />
}

View File

@ -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
/>
<div className={styles.other}>
<span>{invite.gift} sat gift</span>
<span> \ </span>
<span>{invite.invitees.length} joined{invite.limit ? ` of ${invite.limit}` : ''}</span>
{active
? (

View File

@ -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;
$$;