Skip to content

Commit 8a5b24b

Browse files
authored
Merge pull request #741 from hackclub/signup-lottery-rebased
Signup lottery rebased
2 parents b3e7599 + 2d57e34 commit 8a5b24b

8 files changed

Lines changed: 152 additions & 19 deletions

File tree

src/app/api/cron/every-minute/create-background-job.ts renamed to src/app/api/cron/create-background-job.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { sql } from '@vercel/postgres'
22

33
export default async function createBackgroundJob(
4-
type: string,
4+
type: 'run_lottery' | 'create_person' | 'invite',
55
args: {},
66
status: 'pending' | 'completed' | 'failed' = 'pending',
77
) {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const dynamic = 'force-dynamic'
2+
export const fetchCache = 'force-no-store'
3+
4+
import createBackgroundJob from '../create-background-job'
5+
6+
async function processDailyJobs() {
7+
console.log('Processing daily jobs')
8+
9+
await createBackgroundJob('run_lottery', {})
10+
}
11+
12+
export async function GET() {
13+
await processDailyJobs()
14+
return Response.json({ success: true })
15+
}
Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
1-
import type { NextRequest } from 'next/server'
2-
import { processBackgroundJobs } from './process-background-jobs'
1+
import { processBackgroundJobs } from '../process-background-jobs'
32

4-
export async function GET(request: NextRequest) {
5-
const authHeader = request.headers.get('authorization')
6-
const isDev = process.env.NODE_ENV === 'development'
7-
if (!isDev && authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
8-
return new Response('Unauthorized', {
9-
status: 401,
10-
})
11-
}
3+
export const dynamic = 'force-dynamic'
4+
export const fetchCache = 'force-no-store'
125

6+
export async function GET() {
137
await processBackgroundJobs()
148

159
return Response.json({ success: true })
1610
}
17-
18-
export const maxDuration = 60
19-
export const fetchCache = 'force-no-store'

src/app/api/cron/every-minute/process-background-jobs.ts renamed to src/app/api/cron/process-background-jobs.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use server'
22

33
import { sql } from '@vercel/postgres'
4+
import Airtable from 'airtable'
45

56
async function processPendingInviteJobs() {
67
const { rows } =
@@ -120,9 +121,110 @@ async function processPendingPersonInitJobs() {
120121
)
121122
}
122123

124+
async function processLotteryJobs() {
125+
const { rows } = await sql`
126+
SELECT *
127+
FROM background_job
128+
WHERE type = 'run_lottery'
129+
AND status = 'pending'
130+
LIMIT 1;
131+
`
132+
133+
if (rows.length === 0) {
134+
return
135+
}
136+
137+
const previous = (
138+
await sql`
139+
SELECT *
140+
FROM background_job
141+
WHERE type = 'run_lottery'
142+
AND status = 'completed'
143+
ORDER BY created_at DESC
144+
LIMIT 1;`
145+
).rows[0]
146+
147+
console.log('Previous lottery job', previous)
148+
149+
if (
150+
previous &&
151+
previous.created_at > new Date(Date.now() - 1000 * 60 * 60 * 23)
152+
) {
153+
return
154+
}
155+
156+
Airtable.configure({
157+
apiKey: process.env.AIRTABLE_API_KEY,
158+
endpointUrl: process.env.AIRTABLE_ENDPOINT_URL,
159+
})
160+
161+
const base = Airtable.base('appTeNFYcUiYfGcR6')
162+
163+
const highSeasChannelId = 'C07PZMBUNDS'
164+
await base('arrpheus_message_requests').create({
165+
message_text: `Each day, a newly signed up user will win a free Raspberry Pi Zero! Today's winner is...`,
166+
target_slack_id: highSeasChannelId,
167+
requester_identifier: 'cron-job',
168+
})
169+
170+
// read all free sticker orders created in the last 24 hours
171+
const eligibleUsers = await base('people')
172+
.select({
173+
filterByFormula: `AND(
174+
has_ordered_free_stickers = TRUE(),
175+
verified_eligible = TRUE(),
176+
verified_ineligible = FALSE(),
177+
DATETIME_DIFF(NOW(), verification_updated_at, 'hours') <= 24
178+
)`,
179+
})
180+
.all()
181+
182+
const winner = eligibleUsers.sort(() => Math.random() - 0.5)[0]
183+
console.log('Winner', winner)
184+
185+
// create the order
186+
const order = await base('shop_orders').create({
187+
status: 'fresh',
188+
shop_item: ['recKV56D2PATOqK4W'],
189+
recipient: [winner?.id],
190+
})
191+
192+
// send a DM to the winner
193+
const messageRequests = [
194+
{
195+
message_text: `Hey, congrats <@${winner?.fields['slack_id']}>! You won today's free Raspberry Pi Zero! 🎉 We're shipping it to the same address as your sticker bundle.`,
196+
target_slack_id: winner?.fields['slack_id'],
197+
requester_identifier: 'cron-job',
198+
},
199+
{
200+
message_text: `Heads up, <@${winner?.fields['slack_id']}> won today's Raspberry Pi Zero! 🎉`,
201+
target_slack_id: 'U0C7B14Q3', // notify msw for observability
202+
requester_identifier: 'cron-job',
203+
},
204+
{
205+
message_text: `Congratulations to <@${winner?.fields['slack_id']}> for winning a free Raspberry Pi Zero! 🎉 Every day a newly signed up person will get one.`,
206+
target_slack_id: highSeasChannelId,
207+
requester_identifier: 'cron-job',
208+
},
209+
]
210+
211+
const messagePromise = base('arrpheus_message_requests').create(
212+
messageRequests.map((m) => ({ fields: m })),
213+
)
214+
215+
const upsert = await sql`
216+
UPDATE background_job
217+
SET status='completed',
218+
output=${JSON.stringify(order)}
219+
WHERE type='run_lottery'
220+
AND status='pending'`
221+
222+
await Promise.all([upsert, messagePromise])
223+
}
123224
export async function processBackgroundJobs() {
124225
await Promise.all([
125226
processPendingInviteJobs(),
126227
processPendingPersonInitJobs(),
228+
processLotteryJobs(),
127229
])
128230
}

src/app/marketing/marketing-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Airtable from 'airtable'
44
import { createWaka } from '../utils/waka'
55
import { getSession } from '../utils/auth'
66

7-
import createBackgroundJob from '../api/cron/every-minute/create-background-job'
7+
import createBackgroundJob from '../api/cron/create-background-job'
88

99
const highSeasPeopleTable = () => {
1010
const highSeasBaseId = process.env.BASE_ID

src/app/signout/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
export const dynamic = 'force-dynamic'
2+
13
import { redirect } from 'next/navigation'
2-
import { NextRequest } from 'next/server'
34
import { deleteSession } from '../utils/auth'
45

5-
export async function GET(request: NextRequest) {
6+
export async function GET() {
67
console.log('SIGNING OUT!!!!!!')
78
await deleteSession()
89
return redirect('/')

src/middleware.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
person,
1010
} from './app/utils/data'
1111

12-
export async function middleware(request: NextRequest) {
12+
export async function userPageMiddleware(request: NextRequest) {
1313
const session = await getSession()
1414
const slackId = session?.slackId
1515

@@ -125,6 +125,26 @@ export async function middleware(request: NextRequest) {
125125
return response
126126
}
127127

128+
const cronjobMiddleware = async (request: NextRequest) => {
129+
const authHeader = request.headers.get('authorization')
130+
const isDev = process.env.NODE_ENV === 'development'
131+
if (!isDev && authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
132+
return Response.json(
133+
{ success: false, message: 'authentication failed' },
134+
{ status: 401 },
135+
)
136+
}
137+
return NextResponse.next()
138+
}
139+
140+
export async function middleware(request: NextRequest) {
141+
if (request.nextUrl.pathname.startsWith('/api/cron')) {
142+
return cronjobMiddleware(request)
143+
} else {
144+
return userPageMiddleware(request)
145+
}
146+
}
147+
128148
export const config = {
129-
matcher: ['/signpost', '/shipyard', '/wonderdome', '/shop'],
149+
matcher: ['/signpost', '/shipyard', '/wonderdome', '/shop', '/api/cron/'],
130150
}

vercel.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
{
44
"path": "/api/cron/every-minute",
55
"schedule": "* * * * *"
6+
},
7+
{
8+
"path": "/api/cron/every-day",
9+
"schedule": "0 18 * * *"
610
}
711
]
812
}

0 commit comments

Comments
 (0)