603 lines
28 KiB
TypeScript
603 lines
28 KiB
TypeScript
import { createHash, randomUUID } from "node:crypto";
|
|
import { readFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { basename, extname } from "node:path";
|
|
import { Readable } from "node:stream";
|
|
|
|
import type BetterSqlite3 from "better-sqlite3";
|
|
import sharp, { type Metadata } from "sharp";
|
|
|
|
import type { StaticStickerCatalogItem } from "@dada/static-sticker-catalog";
|
|
|
|
import {
|
|
auditRetentionMilliseconds,
|
|
isSafeAuditRef,
|
|
isSafeAuditSummaryJson,
|
|
serializeAuditSummary,
|
|
} from "./audit-policy.js";
|
|
import { ManagedStorage, type StagedManagedFile } from "./managed-storage.js";
|
|
import { StickerReleaseError } from "./sticker-release-errors.js";
|
|
import { classifyCapacity } from "./storage-policy.js";
|
|
|
|
export { StickerReleaseError } from "./sticker-release-errors.js";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const Database = require("better-sqlite3") as typeof BetterSqlite3;
|
|
const stableIdPattern = /^STK([0-9]{4,})$/;
|
|
const idempotencyPattern = /^[A-Za-z0-9_-]{32,200}$/;
|
|
const sha256Pattern = /^[0-9a-f]{64}$/i;
|
|
const maximumOriginalBytes = 20 * 1024 * 1024;
|
|
const maximumDimension = 8_192;
|
|
const bundledPartCounts = [203, 36, 27, 48, 38, 75, 37, 67, 48, 24, 40, 30, 27, 51, 62, 19, 36, 45, 92, 53, 69, 31, 36, 30, 183] as const;
|
|
|
|
type StickerMime = "image/png" | "image/webp";
|
|
type StickerVariant = "original" | "thumbnail";
|
|
|
|
interface StickerItemRow {
|
|
enabled: 0 | 1;
|
|
height: number;
|
|
mime_type: StickerMime;
|
|
order_index: number;
|
|
original_byte_size: number;
|
|
original_file_id: string;
|
|
original_filename: string;
|
|
original_relative_path: string;
|
|
original_sha256: string;
|
|
part: number;
|
|
release_version: string;
|
|
stable_id: string;
|
|
thumbnail_byte_size: number;
|
|
thumbnail_file_id: string;
|
|
thumbnail_relative_path: string;
|
|
thumbnail_sha256: string;
|
|
width: number;
|
|
}
|
|
|
|
export interface StickerUploadInput {
|
|
actorId: string;
|
|
content: Readable;
|
|
enabled: boolean;
|
|
expectedByteSize: number;
|
|
expectedMimeType: StickerMime;
|
|
expectedSha256: string;
|
|
fileName: string;
|
|
idempotencyKey: string;
|
|
order: number;
|
|
part: number;
|
|
stableId: string;
|
|
}
|
|
|
|
function digest(value: string) {
|
|
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
}
|
|
|
|
function stableJson(value: unknown): string {
|
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
if (value && typeof value === "object") {
|
|
return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
|
|
}
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
function iso(timestamp: number) {
|
|
return new Date(timestamp).toISOString();
|
|
}
|
|
|
|
function itemView(row: StickerItemRow): StaticStickerCatalogItem {
|
|
const originalReference = `/api/v1/assets/public/${encodeURIComponent(row.release_version)}/${encodeURIComponent(row.stable_id)}`;
|
|
return {
|
|
enabled: row.enabled === 1,
|
|
height: row.height,
|
|
mime: row.mime_type,
|
|
mime_type: row.mime_type,
|
|
order: row.order_index,
|
|
original_filename: row.original_filename,
|
|
original_reference: originalReference,
|
|
origin: "admin_uploaded",
|
|
part: row.part,
|
|
relative_path: `static-stickers/${row.stable_id}${row.mime_type === "image/png" ? ".png" : ".webp"}`,
|
|
resource_version: row.release_version,
|
|
sha256: row.original_sha256,
|
|
stable_id: row.stable_id,
|
|
thumbnail_reference: {
|
|
media: "thumbnail",
|
|
resource_id: row.stable_id,
|
|
resource_version: row.release_version,
|
|
url: `${originalReference}?variant=thumbnail`,
|
|
},
|
|
width: row.width,
|
|
};
|
|
}
|
|
|
|
export class StickerReleaseService {
|
|
private readonly clock: () => number;
|
|
private readonly database: BetterSqlite3.Database;
|
|
private readonly storage: ManagedStorage;
|
|
|
|
constructor(input: { clock?: () => number; databasePath: string; storage: ManagedStorage }) {
|
|
this.clock = input.clock ?? Date.now;
|
|
const nativeBinding = process.env.DADA_SQLITE_NATIVE_BINDING;
|
|
this.database = new Database(input.databasePath, nativeBinding ? { nativeBinding } : undefined);
|
|
this.database.pragma("journal_mode = WAL");
|
|
this.database.pragma("foreign_keys = ON");
|
|
this.database.pragma("busy_timeout = 5000");
|
|
this.database.function("dada_audit_ref_is_safe", { deterministic: true }, isSafeAuditRef);
|
|
this.database.function("dada_audit_summary_is_safe", { deterministic: true }, isSafeAuditSummaryJson);
|
|
this.database.function("dada_allow_privacy_purge", { deterministic: false }, () => 0);
|
|
this.database.function("dada_privacy_purge_subject", { deterministic: false }, () => "");
|
|
this.database.function("dada_allow_retention_purge", { deterministic: false }, () => 0);
|
|
this.database.function("dada_retention_purge_now", { deterministic: false }, () => 0);
|
|
this.storage = input.storage;
|
|
this.migrate();
|
|
}
|
|
|
|
close() {
|
|
this.database.close();
|
|
}
|
|
|
|
async upload(input: StickerUploadInput) {
|
|
this.validateUpload(input);
|
|
const requestHash = digest(stableJson({
|
|
enabled: input.enabled,
|
|
expected_byte_size: input.expectedByteSize,
|
|
expected_mime_type: input.expectedMimeType,
|
|
expected_sha256: input.expectedSha256.toLowerCase(),
|
|
order: input.order,
|
|
part: input.part,
|
|
stable_id: input.stableId,
|
|
}));
|
|
const keyDigest = digest(input.idempotencyKey);
|
|
const receipt = this.database.prepare(`
|
|
SELECT request_hash, release_version FROM sticker_upload_receipts
|
|
WHERE actor_id = ? AND idempotency_key_digest = ?
|
|
`).get(input.actorId, keyDigest) as { release_version: string; request_hash: string } | undefined;
|
|
if (receipt) {
|
|
input.content.destroy();
|
|
if (receipt.request_hash !== requestHash) throw new StickerReleaseError("sticker_idempotency_conflict", 409);
|
|
return this.uploadResult(receipt.release_version, input.stableId, false);
|
|
}
|
|
this.assertNewPosition(input.stableId, input.part, input.order);
|
|
|
|
const staged: StagedManagedFile[] = [];
|
|
try {
|
|
const original = await this.storage.stageManagedImage({
|
|
content: input.content,
|
|
expectedMimeType: input.expectedMimeType,
|
|
expectedSha256: input.expectedSha256,
|
|
fileKind: "sticker_original",
|
|
fileName: `${input.stableId}${input.expectedMimeType === "image/png" ? ".png" : ".webp"}`,
|
|
maximumBytes: maximumOriginalBytes,
|
|
operationId: randomUUID(),
|
|
ownerRef: input.actorId,
|
|
projectedWriteBytes: input.expectedByteSize,
|
|
});
|
|
staged.push(original);
|
|
if (original.bytes !== input.expectedByteSize) throw new StickerReleaseError("content_size_invalid");
|
|
|
|
let metadata: Metadata;
|
|
let thumbnail: Buffer;
|
|
const decoder = sharp(readFileSync(original.stagingPath), { failOn: "warning", limitInputPixels: maximumDimension * maximumDimension });
|
|
try {
|
|
metadata = await decoder.metadata();
|
|
if (metadata.format !== (input.expectedMimeType === "image/png" ? "png" : "webp")
|
|
|| !metadata.width || !metadata.height || metadata.width > maximumDimension || metadata.height > maximumDimension) {
|
|
throw new Error("content_decode_invalid");
|
|
}
|
|
thumbnail = await decoder
|
|
.rotate()
|
|
.resize({ fit: "inside", height: 256, width: 256, withoutEnlargement: true })
|
|
.png({ adaptiveFiltering: true, compressionLevel: 9 })
|
|
.toBuffer();
|
|
} catch {
|
|
throw new StickerReleaseError("content_decode_invalid");
|
|
} finally {
|
|
decoder.destroy();
|
|
}
|
|
|
|
const thumbnailStaged = await this.storage.stageManagedImage({
|
|
content: Readable.from(thumbnail),
|
|
expectedMimeType: "image/png",
|
|
fileKind: "sticker_thumbnail",
|
|
fileName: `${input.stableId}-thumbnail.png`,
|
|
maximumBytes: maximumOriginalBytes,
|
|
operationId: randomUUID(),
|
|
ownerRef: input.actorId,
|
|
projectedWriteBytes: thumbnail.byteLength,
|
|
});
|
|
staged.push(thumbnailStaged);
|
|
const releaseVersion = this.immediate(() => this.commitUpload({
|
|
...input,
|
|
height: metadata.height!,
|
|
keyDigest,
|
|
original,
|
|
requestHash,
|
|
thumbnail: thumbnailStaged,
|
|
width: metadata.width!,
|
|
}));
|
|
return this.uploadResult(releaseVersion, input.stableId, true);
|
|
} catch (error) {
|
|
for (const file of staged) this.storage.abandonStagedFile(file);
|
|
if (!(error instanceof StickerReleaseError) && error instanceof Error
|
|
&& new Set(["content_hash_invalid", "content_mime_invalid", "content_size_invalid", "file_name_invalid"]).has(error.message)) {
|
|
throw new StickerReleaseError("sticker_upload_invalid");
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
update(input: { actorId: string; enabled?: boolean; order?: number; part?: number; stableId: string }) {
|
|
const current = this.currentVersion();
|
|
if (!current) throw new StickerReleaseError("sticker_not_found", 404);
|
|
const existing = this.readItem(current, input.stableId);
|
|
if (!existing) throw new StickerReleaseError("sticker_not_found", 404);
|
|
const part = input.part ?? existing.part;
|
|
const order = input.order ?? existing.order_index;
|
|
this.validatePosition(input.stableId, part, order);
|
|
const releaseVersion = this.immediate(() => {
|
|
const version = this.nextReleaseVersion();
|
|
this.copyRelease(current, version);
|
|
const conflict = this.database.prepare(`
|
|
SELECT stable_id FROM sticker_release_items
|
|
WHERE release_version = ? AND part = ? AND order_index = ? AND stable_id <> ?
|
|
`).get(version, part, order, input.stableId);
|
|
if (conflict) throw new StickerReleaseError("sticker_order_conflict", 409);
|
|
this.database.prepare(`
|
|
UPDATE sticker_release_items SET enabled = ?, part = ?, order_index = ?
|
|
WHERE release_version = ? AND stable_id = ?
|
|
`).run((input.enabled ?? existing.enabled === 1) ? 1 : 0, part, order, version, input.stableId);
|
|
this.finalizeRelease(version, current, input.actorId);
|
|
this.insertReleaseAudit({
|
|
actorId: input.actorId,
|
|
afterSummary: { enabled: input.enabled ?? existing.enabled === 1, order, part, stable_id: input.stableId },
|
|
beforeSummary: { enabled: existing.enabled === 1, order: existing.order_index, part: existing.part, stable_id: input.stableId },
|
|
operationType: "sticker_release_update",
|
|
releaseVersion: version,
|
|
});
|
|
return version;
|
|
});
|
|
return { item: itemView(this.readItem(releaseVersion, input.stableId)!), release_version: releaseVersion };
|
|
}
|
|
|
|
listPublic(releaseVersion = this.currentVersion()) {
|
|
if (!releaseVersion) return { count: 0, items: [], release_version: null };
|
|
const exists = this.database.prepare("SELECT 1 FROM sticker_releases WHERE release_version = ?").get(releaseVersion);
|
|
if (!exists) return { count: 0, items: [], release_version: null };
|
|
const items = (this.database.prepare(`
|
|
SELECT * FROM sticker_release_items WHERE release_version = ? AND enabled = 1
|
|
ORDER BY part, order_index, stable_id
|
|
`).all(releaseVersion) as StickerItemRow[]).map(itemView);
|
|
return { count: items.length, items, release_version: releaseVersion };
|
|
}
|
|
|
|
adminView() {
|
|
const releaseVersion = this.currentVersion();
|
|
const items = releaseVersion
|
|
? (this.database.prepare("SELECT * FROM sticker_release_items WHERE release_version = ? ORDER BY part, order_index, stable_id").all(releaseVersion) as StickerItemRow[])
|
|
: [];
|
|
return {
|
|
count: items.length,
|
|
items: items.map((row) => ({
|
|
...itemView(row),
|
|
file_state: "committed" as const,
|
|
original_byte_size: row.original_byte_size,
|
|
thumbnail_byte_size: row.thumbnail_byte_size,
|
|
})),
|
|
release_version: releaseVersion,
|
|
storage: this.storage.getState(),
|
|
};
|
|
}
|
|
|
|
readPublicAsset(releaseVersion: string, stableId: string, variant: StickerVariant) {
|
|
const row = this.readItem(releaseVersion, stableId);
|
|
if (!row || row.enabled !== 1) return undefined;
|
|
const fileId = variant === "thumbnail" ? row.thumbnail_file_id : row.original_file_id;
|
|
const path = this.storage.resolveManagedFile(fileId);
|
|
if (!path) return undefined;
|
|
return {
|
|
bytes: readFileSync(path),
|
|
mimeType: variant === "thumbnail" ? "image/png" as const : row.mime_type,
|
|
sha256: variant === "thumbnail" ? row.thumbnail_sha256 : row.original_sha256,
|
|
};
|
|
}
|
|
|
|
inspectCounts() {
|
|
const count = (table: string) => (this.database.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get() as { count: number }).count;
|
|
return { items: count("sticker_release_items"), releases: count("sticker_releases"), upload_receipts: count("sticker_upload_receipts") };
|
|
}
|
|
|
|
private uploadResult(releaseVersion: string, stableId: string, created: boolean) {
|
|
const row = this.readItem(releaseVersion, stableId);
|
|
if (!row) throw new StickerReleaseError("sticker_not_found", 404);
|
|
return {
|
|
created,
|
|
item: itemView(row),
|
|
original: { byte_size: row.original_byte_size, file_id: row.original_file_id, sha256: row.original_sha256 },
|
|
release_version: releaseVersion,
|
|
thumbnail: { byte_size: row.thumbnail_byte_size, file_id: row.thumbnail_file_id, sha256: row.thumbnail_sha256 },
|
|
};
|
|
}
|
|
|
|
private commitUpload(input: StickerUploadInput & {
|
|
height: number;
|
|
keyDigest: string;
|
|
original: StagedManagedFile;
|
|
requestHash: string;
|
|
thumbnail: StagedManagedFile;
|
|
width: number;
|
|
}) {
|
|
this.assertNewPosition(input.stableId, input.part, input.order);
|
|
const previous = this.currentVersion();
|
|
const releaseVersion = this.nextReleaseVersion();
|
|
if (previous) this.copyRelease(previous, releaseVersion);
|
|
for (const file of [input.original, input.thumbnail]) {
|
|
this.storage.moveStagedFile(file);
|
|
this.database.prepare(`
|
|
INSERT INTO managed_files (file_id, file_kind, owner_ref, relative_path, byte_size, mime_type, sha256, status, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, 'committed', ?)
|
|
`).run(file.fileId, file.fileKind, file.ownerRef, file.relativePath, file.bytes, file.mimeType, file.sha256, iso(this.clock()));
|
|
}
|
|
this.database.prepare(`
|
|
INSERT INTO sticker_release_items (
|
|
release_version, stable_id, part, order_index, original_filename, original_relative_path,
|
|
width, height, mime_type, original_sha256, original_file_id, original_byte_size,
|
|
thumbnail_file_id, thumbnail_relative_path, thumbnail_sha256, thumbnail_byte_size, enabled
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run(
|
|
releaseVersion, input.stableId, input.part, input.order, input.fileName, input.original.relativePath,
|
|
input.width, input.height, input.expectedMimeType, input.original.sha256, input.original.fileId, input.original.bytes,
|
|
input.thumbnail.fileId, input.thumbnail.relativePath, input.thumbnail.sha256, input.thumbnail.bytes, input.enabled ? 1 : 0,
|
|
);
|
|
this.database.prepare(`
|
|
INSERT OR IGNORE INTO sticker_managed_file_history (
|
|
managed_file_id, stable_id, resource_version, file_kind, created_at
|
|
) VALUES (?, ?, ?, 'original', ?), (?, ?, ?, 'thumbnail', ?)
|
|
`).run(
|
|
input.original.fileId, input.stableId, releaseVersion, this.clock(),
|
|
input.thumbnail.fileId, input.stableId, releaseVersion, this.clock(),
|
|
);
|
|
this.consumeStagedStorage([input.original, input.thumbnail]);
|
|
this.database.prepare(`
|
|
INSERT INTO sticker_upload_receipts (actor_id, idempotency_key_digest, request_hash, release_version, stable_id, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`).run(input.actorId, input.keyDigest, input.requestHash, releaseVersion, input.stableId, iso(this.clock()));
|
|
this.finalizeRelease(releaseVersion, previous, input.actorId);
|
|
this.insertReleaseAudit({
|
|
actorId: input.actorId,
|
|
afterSummary: { enabled: input.enabled, order: input.order, part: input.part, stable_id: input.stableId },
|
|
beforeSummary: previous ? { release_version: previous } : null,
|
|
operationType: "sticker_release_publish",
|
|
releaseVersion,
|
|
});
|
|
return releaseVersion;
|
|
}
|
|
|
|
private insertReleaseAudit(input: {
|
|
actorId: string;
|
|
afterSummary: Record<string, unknown>;
|
|
beforeSummary: Record<string, unknown> | null;
|
|
operationType: "sticker_release_publish" | "sticker_release_update";
|
|
releaseVersion: string;
|
|
}) {
|
|
const occurredAt = this.clock();
|
|
this.database.prepare(`
|
|
INSERT INTO admin_operation_logs (
|
|
log_id, actor_type, actor_ref, operation_type, target_type, target_ref,
|
|
result, before_summary, after_summary, occurred_at, expires_at
|
|
) VALUES (?, 'super_admin', ?, ?, 'sticker_release', ?, 'succeeded', ?, ?, ?, ?)
|
|
`).run(
|
|
randomUUID(), input.actorId, input.operationType, input.releaseVersion,
|
|
serializeAuditSummary(input.beforeSummary), serializeAuditSummary(input.afterSummary),
|
|
occurredAt, occurredAt + auditRetentionMilliseconds,
|
|
);
|
|
}
|
|
|
|
private finalizeRelease(releaseVersion: string, previous: string | null, actorId: string) {
|
|
const rows = this.database.prepare(`
|
|
SELECT stable_id, part, order_index, original_sha256, thumbnail_sha256, enabled
|
|
FROM sticker_release_items WHERE release_version = ? ORDER BY stable_id
|
|
`).all(releaseVersion);
|
|
const manifestSha256 = digest(stableJson(rows));
|
|
this.database.prepare(`
|
|
INSERT INTO sticker_releases (release_version, previous_release_version, manifest_sha256, published_at, published_by)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`).run(releaseVersion, previous, manifestSha256, iso(this.clock()), actorId);
|
|
this.database.prepare(`
|
|
INSERT INTO current_sticker_release (singleton, release_version) VALUES (1, ?)
|
|
ON CONFLICT(singleton) DO UPDATE SET release_version = excluded.release_version
|
|
`).run(releaseVersion);
|
|
const files = this.database.prepare(`
|
|
SELECT original_file_id AS file_id FROM sticker_release_items WHERE release_version = ?
|
|
UNION SELECT thumbnail_file_id AS file_id FROM sticker_release_items WHERE release_version = ?
|
|
`).all(releaseVersion, releaseVersion) as Array<{ file_id: string }>;
|
|
for (const file of files) {
|
|
this.database.prepare(`
|
|
INSERT INTO project_asset_refs (reference_id, managed_file_id, reference_type, created_at)
|
|
VALUES (?, ?, 'release', ?)
|
|
`).run(`release:${releaseVersion}:${file.file_id}`, file.file_id, iso(this.clock()));
|
|
}
|
|
}
|
|
|
|
private copyRelease(from: string, to: string) {
|
|
this.database.prepare(`
|
|
INSERT INTO sticker_release_items (
|
|
release_version, stable_id, part, order_index, original_filename, original_relative_path,
|
|
width, height, mime_type, original_sha256, original_file_id, original_byte_size,
|
|
thumbnail_file_id, thumbnail_relative_path, thumbnail_sha256, thumbnail_byte_size, enabled
|
|
)
|
|
SELECT ?, stable_id, part, order_index, original_filename, original_relative_path,
|
|
width, height, mime_type, original_sha256, original_file_id, original_byte_size,
|
|
thumbnail_file_id, thumbnail_relative_path, thumbnail_sha256, thumbnail_byte_size, enabled
|
|
FROM sticker_release_items WHERE release_version = ?
|
|
`).run(to, from);
|
|
}
|
|
|
|
private consumeStagedStorage(files: StagedManagedFile[]) {
|
|
const timestamp = iso(this.clock());
|
|
for (const file of files) {
|
|
this.database.prepare(`
|
|
UPDATE storage_reservations SET status = 'consumed', resolved_at = ?
|
|
WHERE operation_id = ? AND status = 'active'
|
|
`).run(timestamp, file.operationId);
|
|
}
|
|
const total = files.reduce((sum, file) => sum + file.bytes, 0);
|
|
const state = this.database.prepare("SELECT managed_content_bytes FROM local_backend_storage_state WHERE singleton = 1").get() as { managed_content_bytes: number };
|
|
const active = this.database.prepare("SELECT COALESCE(SUM(projected_bytes), 0) AS bytes FROM storage_reservations WHERE status = 'active'").get() as { bytes: number };
|
|
const nextBytes = state.managed_content_bytes + total;
|
|
const classification = classifyCapacity(nextBytes, active.bytes);
|
|
this.database.prepare(`
|
|
UPDATE local_backend_storage_state
|
|
SET managed_content_bytes = ?, capacity_notice_level = ?, storage_status = ?, measured_at = ?, version = version + 1
|
|
WHERE singleton = 1
|
|
`).run(nextBytes, classification.capacity_notice_level, classification.storage_status, timestamp);
|
|
}
|
|
|
|
private currentVersion() {
|
|
return (this.database.prepare("SELECT release_version FROM current_sticker_release WHERE singleton = 1").get() as { release_version: string } | undefined)?.release_version ?? null;
|
|
}
|
|
|
|
private nextReleaseVersion() {
|
|
const date = new Date(this.clock()).toISOString().slice(0, 10).replaceAll("-", "");
|
|
const row = this.database.prepare("SELECT next_sequence FROM sticker_release_sequences WHERE release_date = ?").get(date) as { next_sequence: number } | undefined;
|
|
const sequence = row?.next_sequence ?? 1;
|
|
this.database.prepare(`
|
|
INSERT INTO sticker_release_sequences (release_date, next_sequence) VALUES (?, ?)
|
|
ON CONFLICT(release_date) DO UPDATE SET next_sequence = excluded.next_sequence
|
|
`).run(date, sequence + 1);
|
|
return `asset-${date}.${sequence}`;
|
|
}
|
|
|
|
private readItem(releaseVersion: string, stableId: string) {
|
|
return this.database.prepare("SELECT * FROM sticker_release_items WHERE release_version = ? AND stable_id = ?")
|
|
.get(releaseVersion, stableId) as StickerItemRow | undefined;
|
|
}
|
|
|
|
private assertNewPosition(stableId: string, part: number, order: number) {
|
|
this.validatePosition(stableId, part, order);
|
|
const current = this.currentVersion();
|
|
if (!current) return;
|
|
if (this.readItem(current, stableId)) throw new StickerReleaseError("sticker_stable_id_conflict", 409);
|
|
const conflict = this.database.prepare(`
|
|
SELECT stable_id FROM sticker_release_items WHERE release_version = ? AND part = ? AND order_index = ?
|
|
`).get(current, part, order);
|
|
if (conflict) throw new StickerReleaseError("sticker_order_conflict", 409);
|
|
}
|
|
|
|
private validatePosition(stableId: string, part: number, order: number) {
|
|
const matched = stableId.match(stableIdPattern);
|
|
const numericId = matched ? Number(matched[1]) : Number.NaN;
|
|
if (!matched || !Number.isSafeInteger(numericId) || numericId <= 1_407) throw new StickerReleaseError("sticker_stable_id_invalid");
|
|
if (!Number.isSafeInteger(part) || part < 1 || part > bundledPartCounts.length
|
|
|| !Number.isSafeInteger(order) || order <= bundledPartCounts[part - 1]!) {
|
|
throw new StickerReleaseError("sticker_part_order_invalid");
|
|
}
|
|
}
|
|
|
|
private validateUpload(input: StickerUploadInput) {
|
|
this.validatePosition(input.stableId, input.part, input.order);
|
|
if (!/^[0-9a-f-]{36}$/i.test(input.actorId) || !idempotencyPattern.test(input.idempotencyKey)
|
|
|| !sha256Pattern.test(input.expectedSha256) || !Number.isSafeInteger(input.expectedByteSize)
|
|
|| input.expectedByteSize <= 0 || input.expectedByteSize > maximumOriginalBytes
|
|
|| !new Set(["image/png", "image/webp"]).has(input.expectedMimeType)) {
|
|
throw new StickerReleaseError("sticker_upload_invalid");
|
|
}
|
|
const expectedExtension = input.expectedMimeType === "image/png" ? ".png" : ".webp";
|
|
if (input.fileName.length > 255 || basename(input.fileName) !== input.fileName || /[\u0000-\u001f]/.test(input.fileName)
|
|
|| extname(input.fileName).toLowerCase() !== expectedExtension) throw new StickerReleaseError("sticker_upload_invalid");
|
|
}
|
|
|
|
private immediate<T>(action: () => T) {
|
|
this.database.exec("BEGIN IMMEDIATE");
|
|
try {
|
|
const result = action();
|
|
this.database.exec("COMMIT");
|
|
return result;
|
|
} catch (error) {
|
|
if (this.database.inTransaction) this.database.exec("ROLLBACK");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private migrate() {
|
|
this.database.exec(`
|
|
CREATE TABLE IF NOT EXISTS sticker_release_sequences (
|
|
release_date TEXT PRIMARY KEY,
|
|
next_sequence INTEGER NOT NULL CHECK (next_sequence >= 1)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS sticker_releases (
|
|
release_version TEXT PRIMARY KEY,
|
|
previous_release_version TEXT,
|
|
manifest_sha256 TEXT NOT NULL CHECK (length(manifest_sha256) = 64),
|
|
published_at TEXT NOT NULL,
|
|
published_by TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS sticker_release_items (
|
|
release_version TEXT NOT NULL,
|
|
stable_id TEXT NOT NULL,
|
|
part INTEGER NOT NULL CHECK (part BETWEEN 1 AND 25),
|
|
order_index INTEGER NOT NULL CHECK (order_index > 0),
|
|
original_filename TEXT NOT NULL,
|
|
original_relative_path TEXT NOT NULL,
|
|
width INTEGER NOT NULL CHECK (width > 0),
|
|
height INTEGER NOT NULL CHECK (height > 0),
|
|
mime_type TEXT NOT NULL CHECK (mime_type IN ('image/png', 'image/webp')),
|
|
original_sha256 TEXT NOT NULL CHECK (length(original_sha256) = 64),
|
|
original_file_id TEXT NOT NULL REFERENCES managed_files(file_id),
|
|
original_byte_size INTEGER NOT NULL CHECK (original_byte_size > 0),
|
|
thumbnail_file_id TEXT NOT NULL REFERENCES managed_files(file_id),
|
|
thumbnail_relative_path TEXT NOT NULL,
|
|
thumbnail_sha256 TEXT NOT NULL CHECK (length(thumbnail_sha256) = 64),
|
|
thumbnail_byte_size INTEGER NOT NULL CHECK (thumbnail_byte_size > 0),
|
|
enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
|
|
PRIMARY KEY (release_version, stable_id),
|
|
UNIQUE (release_version, part, order_index)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS current_sticker_release (
|
|
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
release_version TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS sticker_upload_receipts (
|
|
actor_id TEXT NOT NULL,
|
|
idempotency_key_digest TEXT NOT NULL CHECK (length(idempotency_key_digest) = 64),
|
|
request_hash TEXT NOT NULL CHECK (length(request_hash) = 64),
|
|
release_version TEXT NOT NULL,
|
|
stable_id TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
PRIMARY KEY (actor_id, idempotency_key_digest)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS sticker_managed_file_history (
|
|
managed_file_id TEXT NOT NULL,
|
|
stable_id TEXT NOT NULL,
|
|
resource_version TEXT NOT NULL,
|
|
file_kind TEXT NOT NULL CHECK (file_kind IN ('original', 'thumbnail')),
|
|
created_at INTEGER NOT NULL,
|
|
PRIMARY KEY (managed_file_id, file_kind),
|
|
FOREIGN KEY (managed_file_id) REFERENCES managed_files(file_id)
|
|
);
|
|
CREATE TRIGGER IF NOT EXISTS sticker_releases_no_update
|
|
BEFORE UPDATE ON sticker_releases BEGIN SELECT RAISE(ABORT, 'sticker_releases_immutable'); END;
|
|
CREATE TRIGGER IF NOT EXISTS sticker_releases_no_delete
|
|
BEFORE DELETE ON sticker_releases BEGIN SELECT RAISE(ABORT, 'sticker_releases_immutable'); END;
|
|
CREATE TRIGGER IF NOT EXISTS sticker_release_items_no_update
|
|
BEFORE UPDATE ON sticker_release_items
|
|
WHEN EXISTS (SELECT 1 FROM sticker_releases WHERE release_version = OLD.release_version)
|
|
BEGIN SELECT RAISE(ABORT, 'sticker_release_items_immutable'); END;
|
|
CREATE TRIGGER IF NOT EXISTS sticker_release_items_no_delete
|
|
BEFORE DELETE ON sticker_release_items
|
|
WHEN EXISTS (SELECT 1 FROM sticker_releases WHERE release_version = OLD.release_version)
|
|
BEGIN SELECT RAISE(ABORT, 'sticker_release_items_immutable'); END;
|
|
`);
|
|
this.database.exec(`
|
|
INSERT OR IGNORE INTO sticker_managed_file_history (
|
|
managed_file_id, stable_id, resource_version, file_kind, created_at
|
|
)
|
|
SELECT original_file_id, stable_id, release_version, 'original', strftime('%s', 'now') * 1000
|
|
FROM sticker_release_items;
|
|
INSERT OR IGNORE INTO sticker_managed_file_history (
|
|
managed_file_id, stable_id, resource_version, file_kind, created_at
|
|
)
|
|
SELECT thumbnail_file_id, stable_id, release_version, 'thumbnail', strftime('%s', 'now') * 1000
|
|
FROM sticker_release_items;
|
|
`);
|
|
}
|
|
}
|