a3f0b49112
Replace the stubbed chat API with a real WebSocket hook that connects to /ws/chat and supports DM creation, messaging, session join/leave, and history retrieval. Keep REST stub functions for backward compatibility with existing pages that require listChatSessions/getChatSessionById imports.
127 lines
3.3 KiB
TypeScript
127 lines
3.3 KiB
TypeScript
import { isApiError } from "@/lib/errors"
|
|
|
|
export type UploadFileType = "avatar" | "chat" | "post" | "verification" | "dispute"
|
|
|
|
function getCookieValue(name: string): string | null {
|
|
if (typeof document === "undefined") return null
|
|
if (!document.cookie) return null
|
|
|
|
for (const part of document.cookie.split("; ")) {
|
|
if (part.startsWith(`${name}=`)) return part.slice(name.length + 1)
|
|
}
|
|
return null
|
|
}
|
|
|
|
let csrfReady: Promise<void> | null = null
|
|
|
|
async function prepareCsrf() {
|
|
if (getCookieValue("__Host-XSRF-TOKEN") && getCookieValue("__Host-XSRF-GUARD")) return
|
|
|
|
csrfReady ??= fetch("/healthz", {
|
|
cache: "no-store",
|
|
credentials: "include",
|
|
})
|
|
.then(() => {})
|
|
.finally(() => {
|
|
csrfReady = null
|
|
})
|
|
await csrfReady
|
|
|
|
if (!getCookieValue("__Host-XSRF-TOKEN")) {
|
|
await fetch("/api/v1/games?offset=0&limit=1", {
|
|
cache: "no-store",
|
|
credentials: "include",
|
|
})
|
|
}
|
|
}
|
|
|
|
async function readJsonBody(res: Response): Promise<{ json: unknown | null; text: string }> {
|
|
const text = await res.text()
|
|
if (!text) return { json: null, text: "" }
|
|
|
|
try {
|
|
return { json: JSON.parse(text) as unknown, text }
|
|
} catch {
|
|
return { json: null, text }
|
|
}
|
|
}
|
|
|
|
function messageFromJson(json: unknown): string | undefined {
|
|
if (typeof json !== "object" || json === null) return undefined
|
|
const value = json as { message?: unknown; msg?: unknown }
|
|
if (typeof value.message === "string") return value.message
|
|
if (typeof value.msg === "string") return value.msg
|
|
return undefined
|
|
}
|
|
|
|
export async function getFileById(fileId: string): Promise<Blob> {
|
|
const res = await fetch(`/api/v1/files?key=${encodeURIComponent(fileId)}`)
|
|
|
|
if (res.ok) return await res.blob()
|
|
|
|
const text = await res.text()
|
|
let json: unknown = null
|
|
if (text) {
|
|
try {
|
|
json = JSON.parse(text) as unknown
|
|
} catch {
|
|
json = null
|
|
}
|
|
}
|
|
|
|
const maybeObj = (typeof json === "object" && json !== null ? json : null) as {
|
|
code?: unknown
|
|
message?: unknown
|
|
msg?: unknown
|
|
} | null
|
|
|
|
const code = (typeof maybeObj?.code === "number" ? maybeObj.code : res.status) as number
|
|
const msg =
|
|
typeof maybeObj?.message === "string"
|
|
? maybeObj.message
|
|
: typeof maybeObj?.msg === "string"
|
|
? maybeObj.msg
|
|
: text || res.statusText || "Request failed"
|
|
|
|
if (res.status === 401 || code === 401) throw new Error("UNAUTHORIZED")
|
|
|
|
throw { code, msg }
|
|
}
|
|
|
|
export async function uploadFile(file: File, type: UploadFileType): Promise<string> {
|
|
const formData = new FormData()
|
|
formData.set("type", type)
|
|
formData.set("file", file)
|
|
|
|
await prepareCsrf()
|
|
|
|
const headers = new Headers()
|
|
const xsrfToken = getCookieValue("__Host-XSRF-TOKEN")
|
|
if (xsrfToken) headers.set("XSRF-TOKEN", xsrfToken)
|
|
|
|
const res = await fetch("/api/v1/upload", {
|
|
method: "POST",
|
|
headers,
|
|
body: formData,
|
|
credentials: "include",
|
|
})
|
|
|
|
const { json, text } = await readJsonBody(res)
|
|
|
|
if (res.ok) {
|
|
if (typeof json === "object" && json !== null) {
|
|
const value = json as { url?: unknown }
|
|
if (typeof value.url === "string") return value.url
|
|
}
|
|
throw { code: 500, msg: "上传响应缺少文件地址" }
|
|
}
|
|
|
|
if (res.status === 401) throw new Error("UNAUTHORIZED")
|
|
if (isApiError(json)) throw json
|
|
|
|
throw {
|
|
code: res.status,
|
|
msg: messageFromJson(json) ?? (text || res.statusText || "Request failed"),
|
|
}
|
|
}
|