Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PRETALX_API_TOKEN=
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:

jobs:
checks:
env:
PRETALX_API_TOKEN: ${{ secrets.PRETALX_API_TOKEN }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ permissions:

jobs:
build:
env:
PRETALX_API_TOKEN: ${{ secrets.PRETALX_API_TOKEN }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand Down
26 changes: 26 additions & 0 deletions server/plugins/generateJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { pretalx2Opass } from '~~/server/utils/opass/pretalx2Opass'
import pretalxData from '~~/server/utils/pretalx'

export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('close', async () => {
if (process.env.NODE_ENV !== 'prerender') {
return
}

const data = await pretalxData()
const opass = pretalx2Opass(data)

const dirs = resolve('.output', 'public', 'json')

if (!existsSync(dirs)) {
mkdirSync(dirs, { recursive: true })
}

writeFileSync(
resolve(dirs, 'session.json'),
JSON.stringify(opass),
)
})
})
53 changes: 53 additions & 0 deletions server/routes/api/session/[id]/index.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import pretalxData from '~~/server/utils/pretalx'
import { parseAnswer, parseSlot, parseSpeaker, parseType } from '~~/server/utils/pretalx/parser'

export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')!

const data = await pretalxData()
const submission = data.submissions?.map[id]

if (!submission) {
throw createError({
statusCode: 404,
statusMessage: 'Not Found',
})
}

if (submission.state !== 'accepted') {
throw createError({
statusCode: 404,
statusMessage: 'Not Found',
})
}

const answers = parseAnswer(submission.answers, data)
const slot = parseSlot(submission.slots[0], data)
const speakers = parseSpeaker(submission.speakers, data)
const type = parseType(submission.submission_type, data)

return {
id: submission.code,
room: slot.room?.name,
start: slot.start,
end: slot.end,
lanaguage: answers.language,
speakers,
zh: {
title: submission.title,
describe: submission.abstract,
type: type.name['zh-hans'] || type.name.en,
},
en: {
title: answers.EnTitle || submission.title,
describe: answers.EnDesc || submission.abstract,
type: type.name.en || type.name['zh-hans'],
},
tags: [],
uri: `https://coscup.org/2026/session/${submission.code}`,
co_write: null,
qa: null,
slide: null,
record: null,
}
})
39 changes: 39 additions & 0 deletions server/routes/api/session/index.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { ISubmission } from '~~/server/utils/pretalx/type'
import pretalxData from '~~/server/utils/pretalx'
import { parseAnswer, parseSlot, parseSpeaker, parseType } from '~~/server/utils/pretalx/parser'

export default defineEventHandler(async () => {
const data = await pretalxData()

const submissions = data.submissions?.arr || []

return submissions
.filter((submission: ISubmission) => submission.state === 'confirmed')
.map((submission: ISubmission) => {
const answers = parseAnswer(submission.answers, data)
const slot = parseSlot(submission.slots[0]!, data)
const speakers = parseSpeaker(submission.speakers, data)
const type = parseType(submission.submission_type, data)

return {
id: submission.code,
room: slot.room?.name,
start: slot.start,
end: slot.end,
lanaguage: answers.language,
speakers,
zh: {
title: submission.title,
describe: submission.abstract,
type: type.name['zh-hans'] || type.name.en,
},
en: {
title: answers.EnTitle || submission.title,
describe: answers.EnDesc || submission.abstract,
type: type.name.en || type.name['zh-hans'],
},
tags: [],
uri: `https://coscup.org/2026/session/${submission.code}`,
}
})
})
93 changes: 93 additions & 0 deletions server/utils/opass/pretalx2Opass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { IPretalxResult, IRoom, ISubmission, ISubmissionType } from '~~/server/utils/pretalx/type'
import { parseAnswer, parseSlot } from '~~/server/utils/pretalx/parser'

export function pretalx2Opass(pretalxData: IPretalxResult) {
const speakerIds: Set<ISpeaker['code']> = new Set()
const roomIds: Set<IRoom['id']> = new Set()
const typeIds: Set<ISubmissionType['id']> = new Set()

const sessions = pretalxData.submissions.arr
.filter((submission: ISubmission) => submission.state === 'confirmed')
.map((submission: ISubmission) => {
const answer = parseAnswer(submission.answers, pretalxData)
const slot = parseSlot(submission.slots[0]!, pretalxData)

submission.speakers.forEach((id) => speakerIds.add(id))
roomIds.add(slot.room?.id)
typeIds.add(submission.submission_type)

return {
id: submission.code,
type: submission.submission_type,
room: slot.room?.id,
start: slot.start,
end: slot.end,
lanaguage: answer.language,
speakers: submission.speakers,
zh: {
title: submission.title,
describe: submission.abstract,
},
en: {
title: answer.EnTitle || submission.title,
describe: answer.EnDesc || submission.abstract,
},
tags: [],
uri: `https://coscup.org/2026/session/${submission.code}`,
co_write: null,
qa: null,
slide: null,
record: null,
}
})

const speakers = [...speakerIds].map((id: ISpeaker['code']) => {
const speaker = pretalxData.speakers.map[id]
const answer = parseAnswer(speaker.answers, pretalxData)

return {
id: speaker.code,
avatar: speaker.avatar_url,
zh: {
name: answer.enName || speaker.name,
bio: answer.enBio || speaker.biography,
},
en: {
name: answer.zhName || speaker.name,
bio: answer.zhBio || speaker.biography,
},
}
})

const types = [...typeIds].map((id: ISubmissionType['id']) => {
const type = pretalxData['submission-types'].map[id]
return {
id: type.id,
zh: {
name: type.name['zh-hans'] || type.name.en,
},
en: {
name: type.name.en || type.name['zh-hans'],
},
}
})

const rooms = [...roomIds]
.filter(Boolean)
.map((id: IRoom['id']) => {
const room = pretalxData.rooms.map[id]
return {
id: room.id,
zh: {
name: room.name['zh-hans'] || room.name.en,
},
en: {
name: room.name.en || room.name['zh-hans'],
},
}
})

// TODO: tags

return { sessions, speakers, session_types: types, rooms }
}
38 changes: 38 additions & 0 deletions server/utils/pretalx/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { IPretalxResponse, IPretalxResult } from './type'

const TOKEN = process.env.PRETALX_API_TOKEN
const BASE_RUL = 'https://pretalx.coscup.org/api/events/coscup-2025'

export default defineCachedFunction(
async () => {
const tables = ['submissions', 'submission-types', 'speakers', 'rooms', 'answers', 'slots'] as const
const results: Partial<IPretalxResult> = {}

for (const table of tables) {
let url: string = table
results[table] = { arr: [], map: {} } satisfies IPretalxData<typeof table>

while (url) {
const response = await $fetch<IPretalxResponse<typeof table>>(
url,
{
baseURL: BASE_RUL,
headers: {
Authorization: `Token ${TOKEN}`,
},
},
)

results[table].arr.push(...response.results)
Object.assign(results[table].map, Object.fromEntries(response.results.map((item: any) => [item.id || item.code, item])))
url = response.next
}
}

return results as IPretalxResult
},
{
maxAge: 99999,
name: 'pretalxData',
},
)
90 changes: 90 additions & 0 deletions server/utils/pretalx/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { IAnswer, IPretalxResult, ISlot, ISubmissionType } from './type'

const QUESTION_MAP: Record<string, number | null> = {
language: 269,
languageOther: 300,
enTitle: 257,
enDesc: 259,
difficulty: 270,
zhName: 45,
enName: 46,
zhBio: 47,
enBio: 48,
coWrite: null,
qa: null,
slide: null,
record: null,
} as const

export function parseAnswer(answers: IAnswer['id'][], pretalxData: IPretalxResult): any {
const answerMap = pretalxData.answers.map
const results: Record<keyof typeof QUESTION_MAP, unknown> = {}

const questionMap = answers.reduce((acc: Record<IAnswer['id'], IAnswer>, cur: IAnswer['id']) => {
const ans = answerMap[cur]

if (!ans) {
console.warn('answer not found', cur)
return acc
}

acc[ans.question] = ans
return acc
}, {})

for (const question in QUESTION_MAP) {
const questionId = QUESTION_MAP[question]

if (!questionId) {
continue
}

results[questionId] = questionMap[questionId]?.answer
}

return results
}

export function parseSlot(slotId: ISlot['id'], pretalxData: IPretalxResult) {
const slotMap = pretalxData.slots.map
const roomMap = pretalxData.rooms.map

const slot = slotMap[slotId]

if (!slot) {
console.warn('slot not found', slotId)
return {}
}

const roomId = slot.room
slot.room = roomMap[roomId]

return slot
}

export function parseSpeaker(speakerIds: ISubmission['speakers'], pretalxData: IPretalxResult) {
const speakerMap = pretalxData.speakers.map

return speakerIds.map((speakerId: string) => {
const speaker = speakerMap[speakerId]
const answer = parseAnswer(speaker.answers, pretalxData)

return {
id: speaker.code,
avatar: speaker.avatar_url,
zh: {
name: answer.enName || speaker.name,
bio: answer.enBio || speaker.biography,
},
en: {
name: answer.zhName || speaker.name,
bio: answer.zhBio || speaker.biography,
},
}
})
}

export function parseType(typeId: ISubmissionType['id'], pretalxData: IPretalxResult) {
const typeMap = pretalxData['submission-types'].map
return typeMap[typeId]
}
Loading