new referral scheme (#1255)
* capture/store data for new referral scheme * simplify signup/forever referral rules * no self-referrals and other fixes * better post/comment distinction and support /items/1/related
This commit is contained in:
parent
fc781047d5
commit
3bada4b5da
|
@ -157,6 +157,12 @@ export default {
|
|||
SELECT date_trunc('day', (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago' as from,
|
||||
(date_trunc('day', (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago') + interval '1 day - 1 second' as to`
|
||||
return await topUsers(parent, { when: 'custom', to: new Date(to).getTime().toString(), from: new Date(from).getTime().toString(), limit: 100 }, { models, ...context })
|
||||
},
|
||||
total: async (parent, args, { models }) => {
|
||||
if (!parent.total) {
|
||||
return 0
|
||||
}
|
||||
return parent.total
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
|
|
|
@ -54,6 +54,60 @@ export default async function getSSRApolloClient ({ req, res, me = null }) {
|
|||
return client
|
||||
}
|
||||
|
||||
function oneDayReferral (request, { me }) {
|
||||
if (!me) return
|
||||
const refHeader = request.headers['x-stacker-news-referrer']
|
||||
if (!refHeader) return
|
||||
|
||||
const referrers = refHeader.split('; ').filter(Boolean)
|
||||
for (const referrer of referrers) {
|
||||
let prismaPromise, getData
|
||||
|
||||
if (referrer.startsWith('item-')) {
|
||||
prismaPromise = models.item.findUnique({ where: { id: parseInt(referrer.slice(5)) } })
|
||||
getData = item => ({
|
||||
referrerId: item.userId,
|
||||
refereeId: parseInt(me.id),
|
||||
type: item.parentId ? 'COMMENT' : 'POST',
|
||||
typeId: String(item.id)
|
||||
})
|
||||
} else if (referrer.startsWith('profile-')) {
|
||||
prismaPromise = models.user.findUnique({ where: { name: referrer.slice(8) } })
|
||||
getData = user => ({
|
||||
referrerId: user.id,
|
||||
refereeId: parseInt(me.id),
|
||||
type: 'PROFILE',
|
||||
typeId: String(user.id)
|
||||
})
|
||||
} else if (referrer.startsWith('territory-')) {
|
||||
prismaPromise = models.sub.findUnique({ where: { name: referrer.slice(10) } })
|
||||
getData = sub => ({
|
||||
referrerId: sub.userId,
|
||||
refereeId: parseInt(me.id),
|
||||
type: 'TERRITORY',
|
||||
typeId: sub.name
|
||||
})
|
||||
} else {
|
||||
prismaPromise = models.user.findUnique({ where: { name: referrer } })
|
||||
getData = user => ({
|
||||
referrerId: user.id,
|
||||
refereeId: parseInt(me.id),
|
||||
type: 'REFERRAL',
|
||||
typeId: String(user.id)
|
||||
})
|
||||
}
|
||||
|
||||
prismaPromise?.then(ref => {
|
||||
if (ref && getData) {
|
||||
const data = getData(ref)
|
||||
// can't refer yourself
|
||||
if (data.refereeId === data.referrerId) return
|
||||
models.oneDayReferral.create({ data }).catch(console.error)
|
||||
}
|
||||
}).catch(console.error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a query and variables and returns a getServerSideProps function
|
||||
*
|
||||
|
@ -124,6 +178,8 @@ export function getGetServerSideProps (
|
|||
}
|
||||
}
|
||||
|
||||
oneDayReferral(req, { me })
|
||||
|
||||
return {
|
||||
props: {
|
||||
...props,
|
||||
|
|
|
@ -1,23 +1,76 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
import { NextResponse, URLPattern } from 'next/server'
|
||||
|
||||
const referrerRegex = /(\/.*)?\/r\/([\w_]+)/
|
||||
const referrerPattern = new URLPattern({ pathname: ':pathname(*)/r/:referrer([\\w_]+)' })
|
||||
const itemPattern = new URLPattern({ pathname: '/items/:id(\\d+){/:other(\\w+)}?' })
|
||||
const profilePattern = new URLPattern({ pathname: '/:name([\\w_]+){/:type(\\w+)}?' })
|
||||
const territoryPattern = new URLPattern({ pathname: '/~:name([\\w_]+){/*}?' })
|
||||
|
||||
// key for /r/... link referrers
|
||||
const SN_REFERRER = 'sn_referrer'
|
||||
// we use this to hold /r/... referrers through the redirect
|
||||
const SN_REFERRER_NONCE = 'sn_referrer_nonce'
|
||||
|
||||
// we store the referrers in cookies for a future signup event
|
||||
// we pass the referrers in the request headers so we can use them in referral rewards for logged in stackers
|
||||
function referrerMiddleware (request) {
|
||||
const m = referrerRegex.exec(request.nextUrl.pathname)
|
||||
if (referrerPattern.test(request.url)) {
|
||||
const { pathname, referrer } = referrerPattern.exec(request.url).pathname.groups
|
||||
|
||||
const url = new URL(m[1] || '/', request.url)
|
||||
url.search = request.nextUrl.search
|
||||
url.hash = request.nextUrl.hash
|
||||
const url = new URL(pathname || '/', request.url)
|
||||
url.search = request.nextUrl.search
|
||||
url.hash = request.nextUrl.hash
|
||||
|
||||
const resp = NextResponse.redirect(url)
|
||||
resp.cookies.set('sn_referrer', m[2])
|
||||
return resp
|
||||
const response = NextResponse.redirect(url)
|
||||
// explicit referrers are set for a day and can only be overriden by other explicit
|
||||
// referrers. Content referrers do not override explicit referrers because
|
||||
// explicit referees might click around before signing up.
|
||||
response.cookies.set(SN_REFERRER, referrer, { maxAge: 60 * 60 * 24 })
|
||||
// store the explicit referrer for one page load
|
||||
// this allows us to attribute both explicit and implicit referrers after the redirect
|
||||
// e.g. items/<num>/r/<referrer> links should attribute both the item op and the referrer
|
||||
// without this the /r/<referrer> would be lost on redirect
|
||||
response.cookies.set(SN_REFERRER_NONCE, referrer, { maxAge: 1 })
|
||||
return response
|
||||
}
|
||||
|
||||
let contentReferrer
|
||||
if (itemPattern.test(request.url)) {
|
||||
let id = request.nextUrl.searchParams.get('commentId')
|
||||
if (!id) {
|
||||
({ id } = itemPattern.exec(request.url).pathname.groups)
|
||||
}
|
||||
contentReferrer = `item-${id}`
|
||||
} else if (profilePattern.test(request.url)) {
|
||||
const { name } = profilePattern.exec(request.url).pathname.groups
|
||||
contentReferrer = `profile-${name}`
|
||||
} else if (territoryPattern.test(request.url)) {
|
||||
const { name } = territoryPattern.exec(request.url).pathname.groups
|
||||
contentReferrer = `territory-${name}`
|
||||
}
|
||||
|
||||
// pass the referrers to SSR in the request headers
|
||||
const requestHeaders = new Headers(request.headers)
|
||||
const referrers = [request.cookies.get(SN_REFERRER_NONCE)?.value, contentReferrer].filter(Boolean)
|
||||
if (referrers.length) {
|
||||
requestHeaders.set('x-stacker-news-referrer', referrers.join('; '))
|
||||
}
|
||||
|
||||
const response = NextResponse.next({
|
||||
request: {
|
||||
headers: requestHeaders
|
||||
}
|
||||
})
|
||||
|
||||
// if we don't already have an explicit referrer, give them the content referrer as one
|
||||
if (!request.cookies.has(SN_REFERRER) && contentReferrer) {
|
||||
response.cookies.set(SN_REFERRER, contentReferrer, { maxAge: 60 * 60 * 24 })
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export function middleware (request) {
|
||||
let resp = NextResponse.next()
|
||||
if (referrerRegex.test(request.nextUrl.pathname)) {
|
||||
resp = referrerMiddleware(request)
|
||||
}
|
||||
const resp = referrerMiddleware(request)
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
|
|
|
@ -36,6 +36,22 @@ function getEventCallbacks () {
|
|||
}
|
||||
}
|
||||
|
||||
async function getReferrerId (referrer) {
|
||||
try {
|
||||
if (referrer.startsWith('item-')) {
|
||||
return (await prisma.item.findUnique({ where: { id: parseInt(referrer.slice(5)) } }))?.userId
|
||||
} else if (referrer.startsWith('profile-')) {
|
||||
return (await prisma.user.findUnique({ where: { name: referrer.slice(8) } }))?.id
|
||||
} else if (referrer.startsWith('territory-')) {
|
||||
return (await prisma.sub.findUnique({ where: { name: referrer.slice(10) } }))?.userId
|
||||
} else {
|
||||
return (await prisma.user.findUnique({ where: { name: referrer } }))?.id
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error getting referrer id', error)
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {Partial<import('next-auth').CallbacksOptions>} */
|
||||
function getCallbacks (req) {
|
||||
return {
|
||||
|
@ -57,10 +73,10 @@ function getCallbacks (req) {
|
|||
// isNewUser doesn't work for nostr/lightning auth because we create the user before nextauth can
|
||||
// this means users can update their referrer if they don't have one, which is fine
|
||||
if (req.cookies.sn_referrer && user?.id) {
|
||||
const referrer = await prisma.user.findUnique({ where: { name: req.cookies.sn_referrer } })
|
||||
if (referrer && referrer.id !== Number(user.id)) {
|
||||
await prisma.user.updateMany({ where: { id: user.id, referrerId: null }, data: { referrerId: referrer.id } })
|
||||
notifyReferral(referrer.id)
|
||||
const referrerId = await getReferrerId(req.cookies.sn_referrer)
|
||||
if (referrerId && referrerId !== parseInt(user?.id)) {
|
||||
await prisma.user.updateMany({ where: { id: user.id, referrerId: null }, data: { referrerId } })
|
||||
notifyReferral(referrerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "OneDayReferralType" AS ENUM ('REFERRAL', 'POST', 'COMMENT', 'PROFILE', 'TERRITORY');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OneDayReferral" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"referrerId" INTEGER NOT NULL,
|
||||
"refereeId" INTEGER NOT NULL,
|
||||
"type" "OneDayReferralType" NOT NULL,
|
||||
"typeId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "OneDayReferral_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OneDayReferral_created_at_idx" ON "OneDayReferral"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OneDayReferral_referrerId_idx" ON "OneDayReferral"("referrerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OneDayReferral_refereeId_idx" ON "OneDayReferral"("refereeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OneDayReferral_type_typeId_idx" ON "OneDayReferral"("type", "typeId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OneDayReferral" ADD CONSTRAINT "OneDayReferral_referrerId_fkey" FOREIGN KEY ("referrerId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OneDayReferral" ADD CONSTRAINT "OneDayReferral_refereeId_fkey" FOREIGN KEY ("refereeId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -127,6 +127,8 @@ model User {
|
|||
Reminder Reminder[]
|
||||
PollBlindVote PollBlindVote[]
|
||||
ItemUserAgg ItemUserAgg[]
|
||||
oneDayReferrals OneDayReferral[] @relation("OneDayReferral_referrer")
|
||||
oneDayReferrees OneDayReferral[] @relation("OneDayReferral_referrees")
|
||||
|
||||
@@index([photoId])
|
||||
@@index([createdAt], map: "users.created_at_index")
|
||||
|
@ -134,6 +136,31 @@ model User {
|
|||
@@map("users")
|
||||
}
|
||||
|
||||
enum OneDayReferralType {
|
||||
REFERRAL
|
||||
POST
|
||||
COMMENT
|
||||
PROFILE
|
||||
TERRITORY
|
||||
}
|
||||
|
||||
model OneDayReferral {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
referrerId Int
|
||||
refereeId Int
|
||||
referrer User @relation("OneDayReferral_referrer", fields: [referrerId], references: [id], onDelete: Cascade)
|
||||
referee User @relation("OneDayReferral_referrees", fields: [refereeId], references: [id], onDelete: Cascade)
|
||||
type OneDayReferralType
|
||||
typeId String
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([referrerId])
|
||||
@@index([refereeId])
|
||||
@@index([type, typeId])
|
||||
}
|
||||
|
||||
enum WalletType {
|
||||
LIGHTNING_ADDRESS
|
||||
LND
|
||||
|
|
Loading…
Reference in New Issue