Skip to content
Open
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
36 changes: 20 additions & 16 deletions server/utils/opass/pretalxToOpass.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { OpassTag } from '#server/utils/opass/tags'
import type { OpassNamedEntity, OpassSchedule, OpassSession, OpassSpeaker } from '#server/utils/opass/types'
import type { PretalxResult, Room, Speaker, Submission, SubmissionType } from '#shared/types/pretalx'
import { sessionTags } from '#server/utils/opass/tags'
import { opassScheduleSchema } from '#server/utils/opass/types'
import { parseAnswer, parseSlot } from '#server/utils/pretalx/parser'

export function pretalxToOpass(pretalxData: PretalxResult) {
export function pretalxToOpass(pretalxData: PretalxResult): OpassSchedule {
const speakerIds: Set<Speaker['code']> = new Set()
const roomIds: Set<Room['id']> = new Set()
const typeIds: Set<SubmissionType['id']> = new Set()
const tagMap = new Map<string, OpassTag>()
const tagMap = new Map<string, OpassNamedEntity>()

const sessions = pretalxData.submissions.arr
.filter((submission: Submission) => submission.state === 'confirmed')
Expand All @@ -29,9 +30,9 @@ export function pretalxToOpass(pretalxData: PretalxResult) {
return {
id: submission.code,
type: String(submission.submission_type),
room: slot?.room?.id != null ? String(slot.room.id) : undefined,
start: slot?.start,
end: slot?.end,
room: slot?.room?.id?.toString() ?? '',
start: slot?.start ?? '2026-08-08T08:00:00Z',
end: slot?.end ?? '2026-08-08T08:00:00Z',
language: answer.language,
speakers: submission.speakers,
zh: {
Expand All @@ -44,11 +45,7 @@ export function pretalxToOpass(pretalxData: PretalxResult) {
},
tags: tags.map((tag) => tag.id),
uri: `https://coscup.org/2026/session/${submission.code}`,
co_write: '',
qa: '',
slide: '',
record: '',
}
} satisfies OpassSession
})

const speakers = Array.from(speakerIds, (id: Speaker['code']) => {
Expand All @@ -63,7 +60,7 @@ export function pretalxToOpass(pretalxData: PretalxResult) {

return {
id: speaker.code,
avatar: speaker.avatar_url ?? '',
avatar: speaker.avatar_url ?? 'https://placehold.co/300x300.png',
zh: {
name: answer.zhName || speaker.name,
bio: answer.zhBio || speaker.biography,
Expand All @@ -72,7 +69,7 @@ export function pretalxToOpass(pretalxData: PretalxResult) {
name: answer.enName || speaker.name,
bio: answer.enBio || speaker.biography,
},
}
} satisfies OpassSpeaker
})
.filter((x): x is NonNullable<typeof x> => x !== null)

Expand All @@ -92,7 +89,7 @@ export function pretalxToOpass(pretalxData: PretalxResult) {
en: {
name: type.name.en || type.name['zh-hant'],
},
}
} satisfies OpassNamedEntity
})
.filter((x): x is NonNullable<typeof x> => x !== null)

Expand All @@ -114,9 +111,16 @@ export function pretalxToOpass(pretalxData: PretalxResult) {
en: {
name: room.name.en || room.name['zh-hant'],
},
}
} satisfies OpassNamedEntity
})
.filter((x): x is NonNullable<typeof x> => x !== null)

return { sessions, speakers, session_types: types, rooms, tags: [...tagMap.values()] }
// run schema check to ensure the structure is valid
return opassScheduleSchema.parse({
Comment thread
pan93412 marked this conversation as resolved.
sessions,
speakers,
session_types: types,
rooms,
tags: [...tagMap.values()],
} satisfies OpassSchedule)
}
17 changes: 4 additions & 13 deletions server/utils/opass/tags.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import type { OpassNamedEntity } from './types'
import { parseDifficulty } from '#server/utils/pretalx/parser'

// OPass app 的 tag 分類沿用 2025:議程以 slug id 參照 tag,
// 而頂層 `tags` 字典提供每個 slug 的多語名稱。分類僅含語言與難度,
// 不含 pretalx 自訂標籤。

export interface OpassTag {
id: string
zh: { name: string }
en: { name: string }
}

// 投稿語言原始字串 → 通用語言鍵(沿用 2025)。
const LANGUAGE_GENERALIZE_MAP: Record<string, string> = {
中文: 'zh-tw',
Expand Down Expand Up @@ -42,14 +33,14 @@ const TAG_NAMES: Record<string, { zh: string, en: string }> = {
'Professional': { zh: '專業', en: 'Professional' },
}

function buildTag(id: string, key: string): OpassTag {
function buildTag(id: string, key: string): OpassNamedEntity {
const name = TAG_NAMES[key]!
return { id, zh: { name: name.zh }, en: { name: name.en } }
}

// 由語言與難度算出議程的 tag 字典項目(含 slug id 與多語名稱)。
export function sessionTags(language: string | undefined, difficultyRaw: string | undefined): OpassTag[] {
const tags: OpassTag[] = []
export function sessionTags(language: string | undefined, difficultyRaw: string | undefined): OpassNamedEntity[] {
const tags: OpassNamedEntity[] = []

const lang = language ? LANGUAGE_GENERALIZE_MAP[language] : undefined
if (lang) {
Expand Down
70 changes: 70 additions & 0 deletions server/utils/opass/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Generated from https://github.com/CCIP-App/schedule-json-validator/blob/38cff3eabbde4cb6d84e3a19accf7e620a29eab7/schemas/opass-schedule.v1.schema.json
*/

import { z } from 'zod'

const idSchema = z.string().min(1)

const stringListSchema = z.array(z.string())

const localizedSessionSchema = z.looseObject({
title: z.string(),
description: z.string(),
})

const localizedSpeakerSchema = z.looseObject({
name: z.string(),
bio: z.string(),
})

const localizedNameSchema = z.looseObject({
name: z.string(),
description: z.string().optional(),
})

export const opassSessionSchema = z.looseObject({
id: idSchema,
room: idSchema,
type: idSchema.optional(),
start: z.iso.datetime({ offset: true }),
end: z.iso.datetime({ offset: true }),
zh: localizedSessionSchema,
en: localizedSessionSchema,
speakers: stringListSchema,
tags: stringListSchema,
broadcast: stringListSchema.optional(),
uri: z.string().optional(),
qa: z.string().optional(),
slide: z.string().optional(),
live: z.string().optional(),
record: z.string().optional(),
language: z.string().optional(),
co_write: z.string().optional(),
})

export const opassSpeakerSchema = z.looseObject({
id: idSchema,
avatar: z.string(),
zh: localizedSpeakerSchema,
en: localizedSpeakerSchema,
})

export const opassNamedEntitySchema = z.looseObject({
id: idSchema,
zh: localizedNameSchema,
en: localizedNameSchema,
})

export const opassScheduleSchema = z.looseObject({
sessions: z.array(opassSessionSchema),
speakers: z.array(opassSpeakerSchema),
session_types: z.array(opassNamedEntitySchema),
rooms: z.array(opassNamedEntitySchema),
tags: z.array(opassNamedEntitySchema),
})

export type OpassSession = z.infer<typeof opassSessionSchema>
export type OpassSpeaker = z.infer<typeof opassSpeakerSchema>
export type OpassNamedEntity = z.infer<typeof opassNamedEntitySchema>
export type OpassSchedule = z.infer<typeof opassScheduleSchema>