@epic-web/workshop-presence 5.3.0 → 5.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -187,7 +187,7 @@ function organizeUsersByWorkshop(users) {
187
187
  return workshopUsers;
188
188
  }
189
189
  function generateUserListItem(user) {
190
- const avatarUrl = user.avatarUrl ?? '/avatar.png';
190
+ const imageUrl = user.imageUrlLarge ?? user.avatarUrl ?? '/avatar.png';
191
191
  const name = user.name ?? 'Anonymous';
192
192
  const location = user.location?.exercise
193
193
  ? [
@@ -203,7 +203,7 @@ function generateUserListItem(user) {
203
203
  const accessLabel = typeof user.hasAccess === 'boolean' ? (user.hasAccess ? '🔑' : '🆓') : '';
204
204
  return `
205
205
  <li>
206
- <img class="user-avatar" src="${avatarUrl}" alt="${name}" />
206
+ <img class="user-avatar" src="${imageUrl}" alt="${name}" />
207
207
  <strong>${name}</strong> - ${location} ${accessLabel}
208
208
  </li>
209
209
  `;
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"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","sourcesContent":["import type * as Party from 'partykit/server'\nimport { z } from 'zod'\nimport { UserSchema } from './presence.js'\n\ntype User = z.infer<typeof UserSchema>\nconst ConnectionStateSchema = z\n\t.object({\n\t\tuser: UserSchema.nullable().optional(),\n\t})\n\t.nullable()\n\ntype ConnectionState = z.infer<typeof ConnectionStateSchema>\n\nconst MessageSchema = z\n\t.object({\n\t\ttype: z.literal('remove-user'),\n\t\tpayload: z.object({ id: z.string() }),\n\t})\n\t.or(z.object({ type: z.literal('add-user'), payload: UserSchema }))\n\t.or(\n\t\tz.object({\n\t\t\ttype: z.literal('add-anonymous-user'),\n\t\t\tpayload: z.object({ id: z.string() }),\n\t\t}),\n\t)\n\t.or(\n\t\tz.object({\n\t\t\ttype: z.literal('presence'),\n\t\t\tpayload: z.object({ users: z.array(UserSchema) }),\n\t\t}),\n\t)\ntype Message = z.infer<typeof MessageSchema>\n\nexport default (class Server implements Party.Server {\n\toptions: Party.ServerOptions = {\n\t\thibernate: true,\n\t}\n\n\treadonly party: Party.Room\n\n\tconstructor(party: Party.Room) {\n\t\tthis.party = party\n\t}\n\n\tonClose() {\n\t\tthis.updateUsers()\n\t}\n\n\tonError() {\n\t\tthis.updateUsers()\n\t}\n\n\tupdateUsers() {\n\t\tconst presenceMessage = JSON.stringify(this.getPresenceMessage())\n\t\tfor (const connection of this.party.getConnections()) {\n\t\t\tconnection.send(presenceMessage)\n\t\t}\n\t}\n\n\tgetPresenceMessage() {\n\t\treturn {\n\t\t\ttype: 'presence',\n\t\t\tpayload: { users: this.getUsers() },\n\t\t} satisfies Message\n\t}\n\n\tgetUsers() {\n\t\tconst users = new Map<string, z.infer<typeof UserSchema>>()\n\n\t\tfor (const connection of this.party.getConnections()) {\n\t\t\tconst state = getConnectionState(connection)\n\t\t\tif (state?.user) {\n\t\t\t\tusers.set(state.user.id, state.user)\n\t\t\t}\n\t\t}\n\n\t\treturn sortUsers(Array.from(users.values()))\n\t}\n\n\tonMessage(message: string, sender: Party.Connection) {\n\t\tconst result = MessageSchema.safeParse(JSON.parse(message))\n\t\tif (!result.success) return\n\n\t\tif (result.data.type === 'add-user') {\n\t\t\tshallowMergeConnectionState(sender, { user: result.data.payload })\n\t\t\tthis.updateUsers()\n\t\t} else if (result.data.type === 'remove-user') {\n\t\t\tsetConnectionState(sender, null)\n\t\t\tthis.updateUsers()\n\t\t} else if (result.data.type === 'add-anonymous-user') {\n\t\t\tsetConnectionState(sender, { user: result.data.payload })\n\t\t}\n\t}\n\n\tonRequest(req: Party.Request): Response | Promise<Response> {\n\t\tconst url = new URL(req.url)\n\t\tif (url.pathname.endsWith('/presence')) {\n\t\t\treturn Response.json(this.getPresenceMessage().payload)\n\t\t}\n\t\tif (url.pathname.endsWith('/show')) {\n\t\t\tconst users = this.getUsers()\n\t\t\tconst workshopUsers = organizeUsersByWorkshop(users)\n\t\t\treturn new Response(\n\t\t\t\t`\n\t\t\t\t<!DOCTYPE html>\n\t\t\t\t<html lang=\"en\">\n\t\t\t\t<head>\n\t\t\t\t\t<meta charset=\"UTF-8\">\n\t\t\t\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t\t\t\t\t<meta http-equiv=\"refresh\" content=\"5\">\n\t\t\t\t\t<title>Epic Web Presence</title>\n\t\t\t\t\t<style>\n\t\t\t\t\t\tbody { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }\n\t\t\t\t\t\th1, h2 { color: #333; }\n\t\t\t\t\t\tul { padding: 0; }\n\t\t\t\t\t\tli { list-style: none; margin-bottom: 10px; }\n\t\t\t\t\t\t.user-avatar { width: 64px; height: 64px; border-radius: 50%; vertical-align: middle; margin-right: 10px; }\n\t\t\t\t\t</style>\n\t\t\t\t</head>\n\t\t\t\t<body>\n\t\t\t\t\t<h1>Epic Web Presence</h1>\n\t\t\t\t\t<p>Total Users: ${users.length}</p>\n\t\t\t\t\t${Object.entries(workshopUsers)\n\t\t\t\t\t\t.map(\n\t\t\t\t\t\t\t([workshop, workshopUsers]) => `\n\t\t\t\t\t\t\t<h2>${workshop} (${workshopUsers.length})</h2>\n\t\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t\t${workshopUsers.map(generateUserListItem).join('')}\n\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t`,\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.join('')}\n\t\t\t\t</body>\n\t\t\t\t</html>\n\t\t\t\t`,\n\t\t\t\t{\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t'Content-Type': 'text/html',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\t\treturn new Response('not found', { status: 404 })\n\t}\n} satisfies Party.Worker)\n\nfunction shallowMergeConnectionState(\n\tconnection: Party.Connection,\n\tstate: ConnectionState,\n) {\n\tsetConnectionState(connection, (prev) => ({ ...prev, ...state }))\n}\n\nfunction setConnectionState(\n\tconnection: Party.Connection,\n\tstate:\n\t\t| ConnectionState\n\t\t| ((prev: ConnectionState | null) => ConnectionState | null),\n) {\n\tif (typeof state !== 'function') {\n\t\treturn connection.setState(state)\n\t}\n\tconnection.setState((prev: unknown) => {\n\t\tconst prevParseResult = ConnectionStateSchema.safeParse(prev)\n\t\tif (prevParseResult.success) {\n\t\t\treturn state(prevParseResult.data)\n\t\t} else {\n\t\t\treturn state(null)\n\t\t}\n\t})\n}\n\nfunction getConnectionState(connection: Party.Connection) {\n\tconst result = ConnectionStateSchema.safeParse(connection.state)\n\tif (result.success) {\n\t\treturn result.data\n\t} else {\n\t\tsetConnectionState(connection, null)\n\t\treturn null\n\t}\n}\n\nfunction sortUsers(users: Array<User>) {\n\treturn [...users].sort((a, b) => {\n\t\tconst aScore = getScore(a)\n\t\tconst bScore = getScore(b)\n\t\tif (aScore === bScore) return 0\n\t\treturn aScore > bScore ? -1 : 1\n\t})\n}\n\nfunction getScore(user: User) {\n\tlet score = 0\n\tif (user.avatarUrl) score += 1\n\tif (user.avatarUrl?.includes('discordapp')) score += 0.5\n\tif (user.name) score += 1\n\treturn score\n}\n\nfunction organizeUsersByWorkshop(users: Array<User>) {\n\tconst workshopUsers: Record<string, Array<User>> = {}\n\n\tfor (const user of users) {\n\t\tconst workshop = user.location?.workshopTitle ?? 'Unknown Workshop'\n\t\tif (!workshopUsers[workshop]) {\n\t\t\tworkshopUsers[workshop] = []\n\t\t}\n\t\tworkshopUsers[workshop]?.push(user)\n\t}\n\n\t// Sort users within each workshop by exercise and step number\n\tfor (const workshop in workshopUsers) {\n\t\tworkshopUsers[workshop]?.sort((a, b) => {\n\t\t\tconst aExercise = a.location?.exercise?.exerciseNumber ?? 0\n\t\t\tconst bExercise = b.location?.exercise?.exerciseNumber ?? 0\n\t\t\tif (aExercise !== bExercise) return aExercise - bExercise\n\n\t\t\tconst aStep = a.location?.exercise?.stepNumber ?? 0\n\t\t\tconst bStep = b.location?.exercise?.stepNumber ?? 0\n\t\t\treturn aStep - bStep\n\t\t})\n\t}\n\n\treturn workshopUsers\n}\n\nfunction generateUserListItem(user: User) {\n\tconst avatarUrl = user.avatarUrl ?? '/avatar.png'\n\tconst name = user.name ?? 'Anonymous'\n\tconst location = user.location?.exercise\n\t\t? [\n\t\t\t\t`Exercise ${user.location.exercise.exerciseNumber}`,\n\t\t\t\tuser.location.exercise.stepNumber &&\n\t\t\t\t\t`Step ${user.location.exercise.stepNumber}`,\n\t\t\t]\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(', ')\n\t\t: user.location?.origin\n\t\t\t? user.location.origin\n\t\t\t: 'Unknown location'\n\n\tconst accessLabel =\n\t\ttypeof user.hasAccess === 'boolean' ? (user.hasAccess ? '🔑' : '🆓') : ''\n\n\treturn `\n\t\t<li>\n\t\t\t<img class=\"user-avatar\" src=\"${avatarUrl}\" alt=\"${name}\" />\n\t\t\t<strong>${name}</strong> - ${location} ${accessLabel}\n\t\t</li>\n\t`\n}\n"]}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAG1C,MAAM,qBAAqB,GAAG,CAAC;KAC7B,MAAM,CAAC;IACP,IAAI,EAAE,UAAU,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CACtC,CAAC;KACD,QAAQ,EAAE,CAAA;AAIZ,MAAM,aAAa,GAAG,CAAC;KACrB,MAAM,CAAC;IACP,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC;IAC9B,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;CACrC,CAAC;KACD,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;KAClE,EAAE,CACF,CAAC,CAAC,MAAM,CAAC;IACR,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,oBAAoB,CAAC;IACrC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;CACrC,CAAC,CACF;KACA,EAAE,CACF,CAAC,CAAC,MAAM,CAAC;IACR,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;IAC3B,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;CACjD,CAAC,CACF,CAAA;AAGF,gBAAgB,MAAM,MAAM;IAC3B,OAAO,GAAwB;QAC9B,SAAS,EAAE,IAAI;KACf,CAAA;IAEQ,KAAK,CAAY;IAE1B,YAAY,KAAiB;QAC5B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACnB,CAAC;IAED,OAAO;QACN,IAAI,CAAC,WAAW,EAAE,CAAA;IACnB,CAAC;IAED,OAAO;QACN,IAAI,CAAC,WAAW,EAAE,CAAA;IACnB,CAAC;IAED,WAAW;QACV,MAAM,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC,CAAA;QACjE,KAAK,MAAM,UAAU,IAAI,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;YACtD,UAAU,CAAC,IAAI,CAAC,eAAe,CAAC,CAAA;QACjC,CAAC;IACF,CAAC;IAED,kBAAkB;QACjB,OAAO;YACN,IAAI,EAAE,UAAU;YAChB,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE;SACjB,CAAA;IACpB,CAAC;IAED,QAAQ;QACP,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsC,CAAA;QAE3D,KAAK,MAAM,UAAU,IAAI,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;YACtD,MAAM,KAAK,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAA;YAC5C,IAAI,KAAK,EAAE,IAAI,EAAE,CAAC;gBACjB,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;YACrC,CAAC;QACF,CAAC;QAED,OAAO,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;IAC7C,CAAC;IAED,SAAS,CAAC,OAAe,EAAE,MAAwB;QAClD,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAA;QAC3D,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,OAAM;QAE3B,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YACrC,2BAA2B,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAA;YAClE,IAAI,CAAC,WAAW,EAAE,CAAA;QACnB,CAAC;aAAM,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YAC/C,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;YAChC,IAAI,CAAC,WAAW,EAAE,CAAA;QACnB,CAAC;aAAM,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,oBAAoB,EAAE,CAAC;YACtD,kBAAkB,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAA;QAC1D,CAAC;IACF,CAAC;IAED,SAAS,CAAC,GAAkB;QAC3B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC5B,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;YACxC,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC,OAAO,CAAC,CAAA;QACxD,CAAC;QACD,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YACpC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAA;YAC7B,MAAM,aAAa,GAAG,uBAAuB,CAAC,KAAK,CAAC,CAAA;YACpD,OAAO,IAAI,QAAQ,CAClB;;;;;;;;;;;;;;;;;;uBAkBmB,KAAK,CAAC,MAAM;OAC5B,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC;iBAC7B,GAAG,CACH,CAAC,CAAC,QAAQ,EAAE,aAAa,CAAC,EAAE,EAAE,CAAC;aACzB,QAAQ,KAAK,aAAa,CAAC,MAAM;;UAEpC,aAAa,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;;OAEnD,CACA;iBACA,IAAI,CAAC,EAAE,CAAC;;;KAGV,EACD;gBACC,OAAO,EAAE;oBACR,cAAc,EAAE,WAAW;iBAC3B;aACD,CACD,CAAA;QACF,CAAC;QACD,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAClD,CAAC;CACuB,EAAA;AAEzB,SAAS,2BAA2B,CACnC,UAA4B,EAC5B,KAAsB;IAEtB,kBAAkB,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAA;AAClE,CAAC;AAED,SAAS,kBAAkB,CAC1B,UAA4B,EAC5B,KAE6D;IAE7D,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;QACjC,OAAO,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;IAClC,CAAC;IACD,UAAU,CAAC,QAAQ,CAAC,CAAC,IAAa,EAAE,EAAE;QACrC,MAAM,eAAe,GAAG,qBAAqB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;QAC7D,IAAI,eAAe,CAAC,OAAO,EAAE,CAAC;YAC7B,OAAO,KAAK,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;QACnC,CAAC;aAAM,CAAC;YACP,OAAO,KAAK,CAAC,IAAI,CAAC,CAAA;QACnB,CAAC;IACF,CAAC,CAAC,CAAA;AACH,CAAC;AAED,SAAS,kBAAkB,CAAC,UAA4B;IACvD,MAAM,MAAM,GAAG,qBAAqB,CAAC,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,CAAA;IAChE,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,MAAM,CAAC,IAAI,CAAA;IACnB,CAAC;SAAM,CAAC;QACP,kBAAkB,CAAC,UAAU,EAAE,IAAI,CAAC,CAAA;QACpC,OAAO,IAAI,CAAA;IACZ,CAAC;AACF,CAAC;AAED,SAAS,SAAS,CAAC,KAAkB;IACpC,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAC/B,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;QAC1B,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;QAC1B,IAAI,MAAM,KAAK,MAAM;YAAE,OAAO,CAAC,CAAA;QAC/B,OAAO,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAChC,CAAC,CAAC,CAAA;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,IAAU;IAC3B,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,IAAI,IAAI,CAAC,SAAS;QAAE,KAAK,IAAI,CAAC,CAAA;IAC9B,IAAI,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,YAAY,CAAC;QAAE,KAAK,IAAI,GAAG,CAAA;IACxD,IAAI,IAAI,CAAC,IAAI;QAAE,KAAK,IAAI,CAAC,CAAA;IACzB,OAAO,KAAK,CAAA;AACb,CAAC;AAED,SAAS,uBAAuB,CAAC,KAAkB;IAClD,MAAM,aAAa,GAAgC,EAAE,CAAA;IAErD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,aAAa,IAAI,kBAAkB,CAAA;QACnE,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9B,aAAa,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAA;QAC7B,CAAC;QACD,aAAa,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;IACpC,CAAC;IAED,8DAA8D;IAC9D,KAAK,MAAM,QAAQ,IAAI,aAAa,EAAE,CAAC;QACtC,aAAa,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;YACtC,MAAM,SAAS,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,cAAc,IAAI,CAAC,CAAA;YAC3D,MAAM,SAAS,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,cAAc,IAAI,CAAC,CAAA;YAC3D,IAAI,SAAS,KAAK,SAAS;gBAAE,OAAO,SAAS,GAAG,SAAS,CAAA;YAEzD,MAAM,KAAK,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,IAAI,CAAC,CAAA;YACnD,MAAM,KAAK,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,IAAI,CAAC,CAAA;YACnD,OAAO,KAAK,GAAG,KAAK,CAAA;QACrB,CAAC,CAAC,CAAA;IACH,CAAC;IAED,OAAO,aAAa,CAAA;AACrB,CAAC;AAED,SAAS,oBAAoB,CAAC,IAAU;IACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,SAAS,IAAI,aAAa,CAAA;IACtE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,WAAW,CAAA;IACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ;QACvC,CAAC,CAAC;YACA,YAAY,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,cAAc,EAAE;YACnD,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU;gBAChC,QAAQ,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,EAAE;SAC5C;aACC,MAAM,CAAC,OAAO,CAAC;aACf,IAAI,CAAC,IAAI,CAAC;QACb,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM;YACtB,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM;YACtB,CAAC,CAAC,kBAAkB,CAAA;IAEtB,MAAM,WAAW,GAChB,OAAO,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAE1E,OAAO;;mCAE2B,QAAQ,UAAU,IAAI;aAC5C,IAAI,eAAe,QAAQ,IAAI,WAAW;;EAErD,CAAA;AACF,CAAC","sourcesContent":["import type * as Party from 'partykit/server'\nimport { z } from 'zod'\nimport { UserSchema } from './presence.js'\n\ntype User = z.infer<typeof UserSchema>\nconst ConnectionStateSchema = z\n\t.object({\n\t\tuser: UserSchema.nullable().optional(),\n\t})\n\t.nullable()\n\ntype ConnectionState = z.infer<typeof ConnectionStateSchema>\n\nconst MessageSchema = z\n\t.object({\n\t\ttype: z.literal('remove-user'),\n\t\tpayload: z.object({ id: z.string() }),\n\t})\n\t.or(z.object({ type: z.literal('add-user'), payload: UserSchema }))\n\t.or(\n\t\tz.object({\n\t\t\ttype: z.literal('add-anonymous-user'),\n\t\t\tpayload: z.object({ id: z.string() }),\n\t\t}),\n\t)\n\t.or(\n\t\tz.object({\n\t\t\ttype: z.literal('presence'),\n\t\t\tpayload: z.object({ users: z.array(UserSchema) }),\n\t\t}),\n\t)\ntype Message = z.infer<typeof MessageSchema>\n\nexport default (class Server implements Party.Server {\n\toptions: Party.ServerOptions = {\n\t\thibernate: true,\n\t}\n\n\treadonly party: Party.Room\n\n\tconstructor(party: Party.Room) {\n\t\tthis.party = party\n\t}\n\n\tonClose() {\n\t\tthis.updateUsers()\n\t}\n\n\tonError() {\n\t\tthis.updateUsers()\n\t}\n\n\tupdateUsers() {\n\t\tconst presenceMessage = JSON.stringify(this.getPresenceMessage())\n\t\tfor (const connection of this.party.getConnections()) {\n\t\t\tconnection.send(presenceMessage)\n\t\t}\n\t}\n\n\tgetPresenceMessage() {\n\t\treturn {\n\t\t\ttype: 'presence',\n\t\t\tpayload: { users: this.getUsers() },\n\t\t} satisfies Message\n\t}\n\n\tgetUsers() {\n\t\tconst users = new Map<string, z.infer<typeof UserSchema>>()\n\n\t\tfor (const connection of this.party.getConnections()) {\n\t\t\tconst state = getConnectionState(connection)\n\t\t\tif (state?.user) {\n\t\t\t\tusers.set(state.user.id, state.user)\n\t\t\t}\n\t\t}\n\n\t\treturn sortUsers(Array.from(users.values()))\n\t}\n\n\tonMessage(message: string, sender: Party.Connection) {\n\t\tconst result = MessageSchema.safeParse(JSON.parse(message))\n\t\tif (!result.success) return\n\n\t\tif (result.data.type === 'add-user') {\n\t\t\tshallowMergeConnectionState(sender, { user: result.data.payload })\n\t\t\tthis.updateUsers()\n\t\t} else if (result.data.type === 'remove-user') {\n\t\t\tsetConnectionState(sender, null)\n\t\t\tthis.updateUsers()\n\t\t} else if (result.data.type === 'add-anonymous-user') {\n\t\t\tsetConnectionState(sender, { user: result.data.payload })\n\t\t}\n\t}\n\n\tonRequest(req: Party.Request): Response | Promise<Response> {\n\t\tconst url = new URL(req.url)\n\t\tif (url.pathname.endsWith('/presence')) {\n\t\t\treturn Response.json(this.getPresenceMessage().payload)\n\t\t}\n\t\tif (url.pathname.endsWith('/show')) {\n\t\t\tconst users = this.getUsers()\n\t\t\tconst workshopUsers = organizeUsersByWorkshop(users)\n\t\t\treturn new Response(\n\t\t\t\t`\n\t\t\t\t<!DOCTYPE html>\n\t\t\t\t<html lang=\"en\">\n\t\t\t\t<head>\n\t\t\t\t\t<meta charset=\"UTF-8\">\n\t\t\t\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t\t\t\t\t<meta http-equiv=\"refresh\" content=\"5\">\n\t\t\t\t\t<title>Epic Web Presence</title>\n\t\t\t\t\t<style>\n\t\t\t\t\t\tbody { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }\n\t\t\t\t\t\th1, h2 { color: #333; }\n\t\t\t\t\t\tul { padding: 0; }\n\t\t\t\t\t\tli { list-style: none; margin-bottom: 10px; }\n\t\t\t\t\t\t.user-avatar { width: 64px; height: 64px; border-radius: 50%; vertical-align: middle; margin-right: 10px; }\n\t\t\t\t\t</style>\n\t\t\t\t</head>\n\t\t\t\t<body>\n\t\t\t\t\t<h1>Epic Web Presence</h1>\n\t\t\t\t\t<p>Total Users: ${users.length}</p>\n\t\t\t\t\t${Object.entries(workshopUsers)\n\t\t\t\t\t\t.map(\n\t\t\t\t\t\t\t([workshop, workshopUsers]) => `\n\t\t\t\t\t\t\t<h2>${workshop} (${workshopUsers.length})</h2>\n\t\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t\t${workshopUsers.map(generateUserListItem).join('')}\n\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t`,\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.join('')}\n\t\t\t\t</body>\n\t\t\t\t</html>\n\t\t\t\t`,\n\t\t\t\t{\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t'Content-Type': 'text/html',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\t\treturn new Response('not found', { status: 404 })\n\t}\n} satisfies Party.Worker)\n\nfunction shallowMergeConnectionState(\n\tconnection: Party.Connection,\n\tstate: ConnectionState,\n) {\n\tsetConnectionState(connection, (prev) => ({ ...prev, ...state }))\n}\n\nfunction setConnectionState(\n\tconnection: Party.Connection,\n\tstate:\n\t\t| ConnectionState\n\t\t| ((prev: ConnectionState | null) => ConnectionState | null),\n) {\n\tif (typeof state !== 'function') {\n\t\treturn connection.setState(state)\n\t}\n\tconnection.setState((prev: unknown) => {\n\t\tconst prevParseResult = ConnectionStateSchema.safeParse(prev)\n\t\tif (prevParseResult.success) {\n\t\t\treturn state(prevParseResult.data)\n\t\t} else {\n\t\t\treturn state(null)\n\t\t}\n\t})\n}\n\nfunction getConnectionState(connection: Party.Connection) {\n\tconst result = ConnectionStateSchema.safeParse(connection.state)\n\tif (result.success) {\n\t\treturn result.data\n\t} else {\n\t\tsetConnectionState(connection, null)\n\t\treturn null\n\t}\n}\n\nfunction sortUsers(users: Array<User>) {\n\treturn [...users].sort((a, b) => {\n\t\tconst aScore = getScore(a)\n\t\tconst bScore = getScore(b)\n\t\tif (aScore === bScore) return 0\n\t\treturn aScore > bScore ? -1 : 1\n\t})\n}\n\nfunction getScore(user: User) {\n\tlet score = 0\n\tif (user.avatarUrl) score += 1\n\tif (user.avatarUrl?.includes('discordapp')) score += 0.5\n\tif (user.name) score += 1\n\treturn score\n}\n\nfunction organizeUsersByWorkshop(users: Array<User>) {\n\tconst workshopUsers: Record<string, Array<User>> = {}\n\n\tfor (const user of users) {\n\t\tconst workshop = user.location?.workshopTitle ?? 'Unknown Workshop'\n\t\tif (!workshopUsers[workshop]) {\n\t\t\tworkshopUsers[workshop] = []\n\t\t}\n\t\tworkshopUsers[workshop]?.push(user)\n\t}\n\n\t// Sort users within each workshop by exercise and step number\n\tfor (const workshop in workshopUsers) {\n\t\tworkshopUsers[workshop]?.sort((a, b) => {\n\t\t\tconst aExercise = a.location?.exercise?.exerciseNumber ?? 0\n\t\t\tconst bExercise = b.location?.exercise?.exerciseNumber ?? 0\n\t\t\tif (aExercise !== bExercise) return aExercise - bExercise\n\n\t\t\tconst aStep = a.location?.exercise?.stepNumber ?? 0\n\t\t\tconst bStep = b.location?.exercise?.stepNumber ?? 0\n\t\t\treturn aStep - bStep\n\t\t})\n\t}\n\n\treturn workshopUsers\n}\n\nfunction generateUserListItem(user: User) {\n\tconst imageUrl = user.imageUrlLarge ?? user.avatarUrl ?? '/avatar.png'\n\tconst name = user.name ?? 'Anonymous'\n\tconst location = user.location?.exercise\n\t\t? [\n\t\t\t\t`Exercise ${user.location.exercise.exerciseNumber}`,\n\t\t\t\tuser.location.exercise.stepNumber &&\n\t\t\t\t\t`Step ${user.location.exercise.stepNumber}`,\n\t\t\t]\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(', ')\n\t\t: user.location?.origin\n\t\t\t? user.location.origin\n\t\t\t: 'Unknown location'\n\n\tconst accessLabel =\n\t\ttypeof user.hasAccess === 'boolean' ? (user.hasAccess ? '🔑' : '🆓') : ''\n\n\treturn `\n\t\t<li>\n\t\t\t<img class=\"user-avatar\" src=\"${imageUrl}\" alt=\"${name}\" />\n\t\t\t<strong>${name}</strong> - ${location} ${accessLabel}\n\t\t</li>\n\t`\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epic-web/workshop-presence",
3
- "version": "5.3.0",
3
+ "version": "5.3.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -11,7 +11,7 @@
11
11
  "deploy": "partykit deploy"
12
12
  },
13
13
  "dependencies": {
14
- "@epic-web/workshop-utils": "5.3.0",
14
+ "@epic-web/workshop-utils": "5.3.2",
15
15
  "zod": "^3.23.8"
16
16
  },
17
17
  "devDependencies": {