[READ-ONLY] Mirror of https://github.com/danielroe/roe.dev. This is the code and content for my personal website, built in Nuxt.
roe.dev
1import type { PartyKitServer, Room } from 'partykit/server'
2import { isValidEmoji } from '#shared/utils/emoji'
3
4export default {
5 async onConnect (ws, party) {
6 // 1. send current vote count if applicable
7 ws.send(`count:${await getCount(party)}`)
8
9 // 2. let everyone know someone new is viewing the site
10 party.broadcast(`connections:${[...party.getConnections()].length}`)
11
12 // 3. send current feedback level
13 if (party.id === 'feedback') {
14 ws.send(`feedback:${JSON.stringify(await party.storage.get('feedback') || [])}`)
15 }
16
17 // 4. let people know if I'm streaming
18 ws.send(`status:${(await party.storage.get('status')) || 'default'}`)
19 },
20 async onRequest (request, party) {
21 if (request.method !== 'POST')
22 return new Response('Invalid request', { status: 422 })
23
24 const body = await request.text().then(r => (r ? JSON.parse(r) : {}))
25 const { status, emoji, type = status ? 'status' : (emoji ? 'reaction' : 'vote') } = body as {
26 type?: 'vote' | 'status' | 'feedback' | 'reaction'
27 status?: string
28 emoji?: string
29 }
30
31 // 5. allow one-off live voting via link
32 if (type === 'vote') {
33 const val = (await getCount(party)) + 1
34 await party.storage.put('count', val)
35 party.broadcast(`count:${val}`)
36 return new Response(null, { status: 204 })
37 }
38
39 // 6. allow one-off feedback submissions via link
40 if (type === 'feedback') {
41 await party.storage.transaction(async tx => {
42 const feedback = await tx.get<string[]>('feedback') || []
43 feedback.push(status!)
44 await tx.put('feedback', feedback)
45 party.broadcast(`feedback:${JSON.stringify(feedback)}`)
46 })
47 return new Response(null, { status: 204 })
48 }
49
50 // 7. tell people if I'm going live
51 if (type === 'status') {
52 if (!status || !['live', 'default'].includes(status))
53 return new Response('Invalid status', { status: 422 })
54
55 if (request.headers.get('authorization') !== party.env.PARTYKIT_TOKEN)
56 return new Response('Unauthorised', { status: 401 })
57
58 party.storage.put('status', status)
59 party.broadcast(`status:${status}`)
60 return new Response(null, { status: 204 })
61 }
62
63 return new Response('Invalid request', { status: 422 })
64 },
65 async onMessage (message, ws, party) {
66 const messageStr = message.toString()
67
68 // 8. handle clearing votes from slide deck
69 if (messageStr === 'clear') {
70 if (party.id === 'feedback') {
71 await party.storage.put('feedback', [])
72 party.broadcast(`feedback:[]`)
73 }
74 else {
75 await party.storage.put('count', 0)
76 party.broadcast(`count:0`)
77 }
78 }
79
80 // 7. handle reaction messages
81 if (party.id === 'reactions' && messageStr.startsWith('reaction:')) {
82 const emoji = messageStr.replace('reaction:', '')
83
84 // Validate emoji
85 if (!isValidEmoji(emoji)) {
86 return
87 }
88
89 // Store the reaction
90 await party.storage.transaction(async tx => {
91 const reactions = await tx.get<string[]>('reactions') || []
92 reactions.push(emoji)
93 // Keep only the last 100 reactions
94 if (reactions.length > 100) reactions.shift()
95 await tx.put('reactions', reactions)
96 })
97
98 // Broadcast to all clients
99 party.broadcast(messageStr)
100 }
101 },
102} satisfies PartyKitServer
103
104const getCount = (room: Room) => room.storage.get<number>('count').then(r => r || 0)