mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-05-15 03:25:40 +08:00
139 lines
4.0 KiB
TypeScript
139 lines
4.0 KiB
TypeScript
import { readBody } from 'h3'
|
|
import { requireUserSession } from '../../utils/auth'
|
|
import { LearnProfile } from '../../models/LearnProfile'
|
|
import type { LearnConfig, LearnProgress, LessonProgress } from '~~/shared/learn/config'
|
|
import { createDefaultLearnConfig } from '~~/shared/learn/config'
|
|
|
|
interface LearnStateUpdateBody {
|
|
xp?: number
|
|
progress?: LearnProgress
|
|
config?: Partial<LearnConfig>
|
|
unlockedModules?: string[]
|
|
}
|
|
|
|
function sanitizeProgress(input: LearnProgress | undefined): LearnProgress | undefined {
|
|
if (!input || typeof input !== 'object') {
|
|
return undefined
|
|
}
|
|
|
|
const sanitized: LearnProgress = {}
|
|
|
|
for (const [moduleId, lessons] of Object.entries(input)) {
|
|
if (!lessons || typeof lessons !== 'object') {
|
|
continue
|
|
}
|
|
|
|
const lessonProgress: Record<string, LessonProgress> = {}
|
|
|
|
for (const [lessonId, value] of Object.entries(lessons as Record<string, any>)) {
|
|
if (!value || typeof value !== 'object') {
|
|
continue
|
|
}
|
|
|
|
const bestRaw = (value as any).best
|
|
const doneRaw = (value as any).done
|
|
|
|
const best = typeof bestRaw === 'number' && Number.isFinite(bestRaw) ? Math.max(0, Math.min(100, Math.round(bestRaw))) : 0
|
|
const done = Boolean(doneRaw)
|
|
|
|
lessonProgress[lessonId] = { best, done }
|
|
}
|
|
|
|
sanitized[moduleId] = lessonProgress
|
|
}
|
|
|
|
return sanitized
|
|
}
|
|
|
|
function sanitizeConfig(input: Partial<LearnConfig> | undefined): LearnConfig | undefined {
|
|
if (!input || typeof input !== 'object') {
|
|
return undefined
|
|
}
|
|
|
|
const config = createDefaultLearnConfig()
|
|
|
|
if (typeof input.tts === 'boolean') {
|
|
config.tts = input.tts
|
|
}
|
|
|
|
if (typeof input.audioChallenge === 'boolean') {
|
|
config.audioChallenge = input.audioChallenge
|
|
}
|
|
|
|
if (typeof input.radioLevel === 'number' && Number.isFinite(input.radioLevel)) {
|
|
const level = Math.round(input.radioLevel)
|
|
config.radioLevel = Math.min(5, Math.max(1, level))
|
|
}
|
|
|
|
if (typeof input.audioSpeed === 'number' && Number.isFinite(input.audioSpeed)) {
|
|
const rounded = Math.round(input.audioSpeed * 20) / 20
|
|
config.audioSpeed = Math.min(1.3, Math.max(0.7, rounded))
|
|
}
|
|
|
|
if (typeof input.voice === 'string') {
|
|
config.voice = input.voice.slice(0, 120)
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
function sanitizeUnlockedModules(input: string[] | undefined): string[] | undefined {
|
|
if (!Array.isArray(input)) {
|
|
return undefined
|
|
}
|
|
|
|
const sanitized = input
|
|
.filter(value => typeof value === 'string')
|
|
.map(value => value.trim())
|
|
.filter(value => value.length > 0)
|
|
|
|
return Array.from(new Set(sanitized))
|
|
}
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const user = await requireUserSession(event)
|
|
const body = await readBody<LearnStateUpdateBody>(event)
|
|
|
|
let profile = await LearnProfile.findOne({ user: user._id })
|
|
if (!profile) {
|
|
profile = new LearnProfile({ user: user._id })
|
|
}
|
|
|
|
if (typeof body.xp === 'number' && Number.isFinite(body.xp)) {
|
|
profile.xp = Math.max(0, Math.round(body.xp))
|
|
}
|
|
|
|
const progress = sanitizeProgress(body.progress)
|
|
if (progress !== undefined) {
|
|
profile.progress = progress
|
|
}
|
|
|
|
const config = sanitizeConfig(body.config)
|
|
if (config !== undefined) {
|
|
profile.config = config
|
|
}
|
|
|
|
const unlockedModules = sanitizeUnlockedModules(body.unlockedModules)
|
|
if (unlockedModules !== undefined) {
|
|
profile.unlockedModules = unlockedModules
|
|
}
|
|
|
|
await profile.save()
|
|
|
|
const responseConfig = { ...createDefaultLearnConfig(), ...(profile.config || {}) }
|
|
const responseProgress =
|
|
profile.progress && typeof profile.progress === 'object'
|
|
? JSON.parse(JSON.stringify(profile.progress))
|
|
: {}
|
|
const responseUnlockedModules = Array.isArray(profile.unlockedModules)
|
|
? Array.from(new Set((profile.unlockedModules as unknown[]).filter(item => typeof item === 'string').map(item => item.trim())))
|
|
: []
|
|
|
|
return {
|
|
xp: typeof profile.xp === 'number' ? profile.xp : 0,
|
|
progress: responseProgress,
|
|
config: responseConfig,
|
|
unlockedModules: responseUnlockedModules,
|
|
}
|
|
})
|