このコミットが含まれているのは:
@@ -1,7 +1,9 @@
|
||||
import { CATEGORIES } from '@/consts'
|
||||
import { resultRepairMode } from '@/lib/postImportRows'
|
||||
import { validatePostImportSessionId } from '@/lib/postImportStorage'
|
||||
|
||||
import type {
|
||||
PostImportDisplayTag,
|
||||
PostImportOrigin,
|
||||
PostImportRepairMode,
|
||||
PostImportRow,
|
||||
@@ -18,9 +20,11 @@ type QueryRowState = {
|
||||
duration: string
|
||||
tags: string
|
||||
parent_post_ids: string
|
||||
display_tags: PostImportDisplayTag[]
|
||||
manual: string[]
|
||||
skip: PostImportSkipReason | null
|
||||
existing_post_id: number | null
|
||||
existing_post: PostImportRow['existingPost'] | null
|
||||
import_status: PostImportRow['importStatus'] | null
|
||||
created_post_id: number | null
|
||||
recoverable: boolean | null }
|
||||
@@ -45,9 +49,11 @@ type EncodedRowState = {
|
||||
d?: string
|
||||
g?: string
|
||||
p?: string
|
||||
k?: EncodedDisplayTag[]
|
||||
m?: string[]
|
||||
s?: EncodedSkip
|
||||
e?: number
|
||||
o?: EncodedExistingPost
|
||||
i?: EncodedImportStatus
|
||||
c?: number
|
||||
r?: boolean }
|
||||
@@ -56,6 +62,18 @@ type EncodedState = {
|
||||
v: 1
|
||||
rows: EncodedRowState[] }
|
||||
|
||||
type EncodedDisplayTag = {
|
||||
n: string
|
||||
c: PostImportDisplayTag['category']
|
||||
s?: string[] }
|
||||
|
||||
type EncodedExistingPost = {
|
||||
i: number
|
||||
t: string
|
||||
u: string
|
||||
h?: string | null
|
||||
b?: string | null }
|
||||
|
||||
export type ParsedPostNewState = {
|
||||
rows: PostImportRow[]
|
||||
source: string
|
||||
@@ -77,9 +95,11 @@ const BASIC_KEYS = [
|
||||
'tags',
|
||||
'parent_post_ids'] as const
|
||||
const STATE_KEYS = [
|
||||
'display_tags',
|
||||
'manual',
|
||||
'skip',
|
||||
'existing_post_id',
|
||||
'existing_post',
|
||||
'import_status',
|
||||
'created_post_id',
|
||||
'recoverable'] as const
|
||||
@@ -95,9 +115,11 @@ const ENCODED_ROW_KEYS = [
|
||||
'd',
|
||||
'g',
|
||||
'p',
|
||||
'k',
|
||||
'm',
|
||||
's',
|
||||
'e',
|
||||
'o',
|
||||
'i',
|
||||
'c',
|
||||
'r'] as const
|
||||
@@ -200,6 +222,153 @@ const decodeImportStatus = (
|
||||
return null
|
||||
}
|
||||
|
||||
const sanitiseDisplayTag = (value: unknown): PostImportDisplayTag | null => {
|
||||
if (!(isPlainObject (value)))
|
||||
return null
|
||||
if (!(hasOnlyKeys (value, ['n', 'c', 's'])))
|
||||
return null
|
||||
if (typeof value.n !== 'string' || value.n === '')
|
||||
return null
|
||||
if (!(CATEGORIES.includes (value.c as never)))
|
||||
return null
|
||||
if (value.s != null && !(Array.isArray (value.s)) || !(value.s ?? []).every (entry => typeof entry === 'string'))
|
||||
return null
|
||||
|
||||
return {
|
||||
name: value.n,
|
||||
category: value.c,
|
||||
sectionLiterals: value.s == null ? undefined : [...value.s] }
|
||||
}
|
||||
|
||||
const sanitiseDisplayTags = (
|
||||
value: unknown,
|
||||
): PostImportDisplayTag[] | null => {
|
||||
if (value == null)
|
||||
return []
|
||||
if (!(Array.isArray (value)))
|
||||
return null
|
||||
|
||||
const tags = value.map (sanitiseDisplayTag)
|
||||
return tags.some (tag => tag == null) ? null : (tags as PostImportDisplayTag[])
|
||||
}
|
||||
|
||||
const encodeDisplayTags = (
|
||||
tags: PostImportDisplayTag[] | undefined,
|
||||
): EncodedDisplayTag[] | undefined =>
|
||||
tags == null || tags.length === 0
|
||||
? undefined
|
||||
: tags.map (tag => ({
|
||||
n: tag.name,
|
||||
c: tag.category,
|
||||
s:
|
||||
tag.sectionLiterals == null || tag.sectionLiterals.length === 0
|
||||
? undefined
|
||||
: [...tag.sectionLiterals] }))
|
||||
|
||||
const encodeDisplayTagsParam = (
|
||||
tags: PostImportDisplayTag[] | undefined,
|
||||
): string | null => {
|
||||
const encodedTags = encodeDisplayTags (tags)
|
||||
if (encodedTags == null)
|
||||
return null
|
||||
|
||||
return encodeBase64UrlBytes (textEncoder.encode (JSON.stringify (encodedTags)))
|
||||
}
|
||||
|
||||
const decodeDisplayTagsParam = (
|
||||
value: string | null,
|
||||
): PostImportDisplayTag[] | null => {
|
||||
if (value == null || value === '')
|
||||
return []
|
||||
|
||||
const bytes = decodeBase64UrlBytes (value)
|
||||
if (bytes == null || bytes.byteLength > MAX_DECOMPRESSED_STATE_BYTES)
|
||||
return null
|
||||
|
||||
try
|
||||
{
|
||||
return sanitiseDisplayTags (JSON.parse (textDecoder.decode (bytes)))
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const sanitiseExistingPost = (
|
||||
value: unknown,
|
||||
): PostImportRow['existingPost'] | null => {
|
||||
if (value == null)
|
||||
return null
|
||||
if (!(isPlainObject (value)))
|
||||
return null
|
||||
if (!(hasOnlyKeys (value, ['i', 't', 'u', 'h', 'b'])))
|
||||
return null
|
||||
if (!(typeof value.i === 'number'
|
||||
&& Number.isSafeInteger (value.i)
|
||||
&& value.i > 0))
|
||||
return null
|
||||
if (typeof value.t !== 'string' || typeof value.u !== 'string')
|
||||
return null
|
||||
if (value.h != null && typeof value.h !== 'string')
|
||||
return null
|
||||
if (value.b != null && typeof value.b !== 'string')
|
||||
return null
|
||||
|
||||
return {
|
||||
id: value.i,
|
||||
title: value.t,
|
||||
url: value.u,
|
||||
thumbnail: value.h,
|
||||
thumbnailBase: value.b }
|
||||
}
|
||||
|
||||
|
||||
const encodeExistingPost = (
|
||||
post: PostImportRow['existingPost'] | undefined,
|
||||
): EncodedExistingPost | undefined =>
|
||||
post == null
|
||||
? undefined
|
||||
: {
|
||||
i: post.id,
|
||||
t: post.title,
|
||||
u: post.url,
|
||||
h: post.thumbnail,
|
||||
b: post.thumbnailBase }
|
||||
|
||||
|
||||
const encodeExistingPostParam = (
|
||||
post: PostImportRow['existingPost'] | undefined,
|
||||
): string | null => {
|
||||
const encodedPost = encodeExistingPost (post)
|
||||
if (encodedPost == null)
|
||||
return null
|
||||
|
||||
return encodeBase64UrlBytes (textEncoder.encode (JSON.stringify (encodedPost)))
|
||||
}
|
||||
|
||||
|
||||
const decodeExistingPostParam = (
|
||||
value: string | null,
|
||||
): PostImportRow['existingPost'] | null => {
|
||||
if (value == null || value === '')
|
||||
return null
|
||||
|
||||
const bytes = decodeBase64UrlBytes (value)
|
||||
if (bytes == null || bytes.byteLength > MAX_DECOMPRESSED_STATE_BYTES)
|
||||
return null
|
||||
|
||||
try
|
||||
{
|
||||
return sanitiseExistingPost (JSON.parse (textDecoder.decode (bytes)))
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const isStrictPositiveIntegerString = (value: string): boolean =>
|
||||
/^[1-9][0-9]*$/.test (value)
|
||||
@@ -338,9 +507,11 @@ const baseRowState = (row: PostImportRow): QueryRowState => ({
|
||||
duration: String (row.attributes.duration ?? ''),
|
||||
tags: String (row.attributes.tags ?? ''),
|
||||
parent_post_ids: String (row.attributes.parentPostIds ?? ''),
|
||||
display_tags: row.displayTags ?? [],
|
||||
manual: manualFields (row),
|
||||
skip: row.skipReason ?? null,
|
||||
existing_post_id: row.existingPostId ?? null,
|
||||
existing_post: row.existingPost ?? null,
|
||||
import_status: row.importStatus ?? null,
|
||||
created_post_id: row.createdPostId ?? null,
|
||||
recoverable: row.recoverable ?? null })
|
||||
@@ -379,13 +550,36 @@ const buildRow = (state: FullRowState): PostImportRow => {
|
||||
baseWarnings: [],
|
||||
validationErrors: { },
|
||||
provenance,
|
||||
displayTags: state.display_tags.map (tag => ({
|
||||
name: tag.name,
|
||||
category: tag.category,
|
||||
sectionLiterals:
|
||||
tag.sectionLiterals == null
|
||||
? undefined
|
||||
: [...tag.sectionLiterals] })),
|
||||
tagSources,
|
||||
status: 'pending',
|
||||
skipReason: state.skip ?? undefined,
|
||||
existingPostId: state.existing_post_id ?? undefined,
|
||||
existingPost:
|
||||
state.existing_post == null
|
||||
? undefined
|
||||
: {
|
||||
id: state.existing_post.id,
|
||||
title: state.existing_post.title,
|
||||
url: state.existing_post.url,
|
||||
thumbnail: state.existing_post.thumbnail,
|
||||
thumbnailBase: state.existing_post.thumbnailBase },
|
||||
resetSnapshot: {
|
||||
url: state.url,
|
||||
attributes: { ...attributes },
|
||||
displayTags: state.display_tags.map (tag => ({
|
||||
name: tag.name,
|
||||
category: tag.category,
|
||||
sectionLiterals:
|
||||
tag.sectionLiterals == null
|
||||
? undefined
|
||||
: [...tag.sectionLiterals] })),
|
||||
provenance: { ...provenance },
|
||||
tagSources: { ...tagSources },
|
||||
fieldWarnings: { },
|
||||
@@ -429,6 +623,8 @@ const parseEncodedRow = (
|
||||
const createdPostId = row['c']
|
||||
const recoverable = row['r']
|
||||
const sourceRow = row['n']
|
||||
const displayTags = sanitiseDisplayTags (row['k'])
|
||||
const existingPost = sanitiseExistingPost (row['o'])
|
||||
|
||||
if (requireUrl && typeof url !== 'string')
|
||||
return null
|
||||
@@ -454,6 +650,10 @@ const parseEncodedRow = (
|
||||
return null
|
||||
if (recoverable != null && typeof recoverable !== 'boolean')
|
||||
return null
|
||||
if (displayTags == null)
|
||||
return null
|
||||
if (row['o'] != null && existingPost == null)
|
||||
return null
|
||||
if (sourceRow != null
|
||||
&& !(typeof sourceRow === 'number'
|
||||
&& Number.isSafeInteger (sourceRow)
|
||||
@@ -461,6 +661,8 @@ const parseEncodedRow = (
|
||||
return null
|
||||
if (skip === 'existing' && existingPostId == null)
|
||||
return null
|
||||
if (existingPost != null && existingPost.id !== existingPostId)
|
||||
return null
|
||||
if (importStatus === 'created' && createdPostId == null)
|
||||
return null
|
||||
if (importStatus !== 'created' && createdPostId != null)
|
||||
@@ -512,9 +714,11 @@ const parseEncodedRow = (
|
||||
duration: parsedStrings.duration,
|
||||
tags: parsedStrings.tags,
|
||||
parent_post_ids: parsedStrings.parent_post_ids,
|
||||
display_tags: displayTags,
|
||||
manual: manual ?? [],
|
||||
skip,
|
||||
existing_post_id: existingPostId ?? null,
|
||||
existing_post: existingPost,
|
||||
import_status: importStatus,
|
||||
created_post_id: createdPostId ?? null,
|
||||
recoverable: recoverable ?? null }
|
||||
@@ -543,9 +747,11 @@ const parseSingleRow = (params: URLSearchParams): ParsedPostNewState | null => {
|
||||
duration: '',
|
||||
tags: '',
|
||||
parent_post_ids: '',
|
||||
display_tags: [],
|
||||
manual: [],
|
||||
skip: null,
|
||||
existing_post_id: null,
|
||||
existing_post: null,
|
||||
import_status: null,
|
||||
created_post_id: null,
|
||||
recoverable: null })
|
||||
@@ -580,14 +786,22 @@ const parseSingleRow = (params: URLSearchParams): ParsedPostNewState | null => {
|
||||
return null
|
||||
const existingPostIdValue = params.get ('existing_post_id')
|
||||
const createdPostIdValue = params.get ('created_post_id')
|
||||
const displayTags = decodeDisplayTagsParam (params.get ('display_tags'))
|
||||
const existingPost = decodeExistingPostParam (params.get ('existing_post'))
|
||||
const existingPostId = parsePositiveInt (existingPostIdValue)
|
||||
const createdPostId = parsePositiveInt (createdPostIdValue)
|
||||
if (displayTags == null)
|
||||
return null
|
||||
if (params.get ('existing_post') != null && existingPost == null)
|
||||
return null
|
||||
if (existingPostIdValue != null && existingPostId == null)
|
||||
return null
|
||||
if (createdPostIdValue != null && createdPostId == null)
|
||||
return null
|
||||
if (skipValue === 'existing' && existingPostId == null)
|
||||
return null
|
||||
if (existingPost != null && existingPost.id !== existingPostId)
|
||||
return null
|
||||
if (importStatus === 'created' && createdPostId == null)
|
||||
return null
|
||||
if (importStatus !== 'created' && createdPostIdValue != null)
|
||||
@@ -608,9 +822,11 @@ const parseSingleRow = (params: URLSearchParams): ParsedPostNewState | null => {
|
||||
duration: params.get ('duration') ?? '',
|
||||
tags: params.get ('tags') ?? '',
|
||||
parent_post_ids: params.get ('parent_post_ids') ?? '',
|
||||
display_tags: displayTags,
|
||||
manual: parseManual (params.get ('manual')),
|
||||
skip: skipValue,
|
||||
existing_post_id: existingPostId,
|
||||
existing_post: existingPost,
|
||||
import_status: importStatus,
|
||||
created_post_id: createdPostId,
|
||||
recoverable })
|
||||
@@ -727,6 +943,12 @@ const regularQuery = (rows: PostImportRow[]): URLSearchParams => {
|
||||
params.set ('duration', String (row?.attributes.duration ?? ''))
|
||||
params.set ('tags', String (row?.attributes.tags ?? ''))
|
||||
params.set ('parent_post_ids', String (row?.attributes.parentPostIds ?? ''))
|
||||
const displayTags = encodeDisplayTagsParam (row?.displayTags)
|
||||
if (displayTags != null)
|
||||
params.set ('display_tags', displayTags)
|
||||
const existingPost = encodeExistingPostParam (row?.existingPost)
|
||||
if (existingPost != null)
|
||||
params.set ('existing_post', existingPost)
|
||||
const manual = manualFields (row).join (',')
|
||||
if (manual !== '')
|
||||
params.set ('manual', manual)
|
||||
@@ -776,12 +998,16 @@ const encodeRowState = (
|
||||
encoded.g = base.tags
|
||||
if (base.parent_post_ids !== '')
|
||||
encoded.p = base.parent_post_ids
|
||||
if (base.display_tags.length > 0)
|
||||
encoded.k = encodeDisplayTags (base.display_tags)
|
||||
if (base.manual.length > 0)
|
||||
encoded.m = [...base.manual]
|
||||
if (base.skip != null)
|
||||
encoded.s = encodeSkip (base.skip)
|
||||
if (base.existing_post_id != null)
|
||||
encoded.e = base.existing_post_id
|
||||
if (base.existing_post != null)
|
||||
encoded.o = encodeExistingPost (base.existing_post)
|
||||
if (base.import_status != null)
|
||||
encoded.i = encodeImportStatus (base.import_status)
|
||||
if (base.created_post_id != null)
|
||||
|
||||
新しい課題から参照
ユーザをブロックする