mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-12 09:24:06 +02:00
Merge branch 'develop' into mahjong
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { WritableStream } from 'node:stream/web';
|
||||
import type { PathLike } from 'node:fs';
|
||||
|
||||
/**
|
||||
|
||||
40
packages/backend/src/misc/bigint.ts
Normal file
40
packages/backend/src/misc/bigint.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
function parseBigIntChunked(str: string, base: number, chunkSize: number, powerOfChunkSize: bigint): bigint {
|
||||
const chunks = [];
|
||||
while (str.length > 0) {
|
||||
chunks.unshift(str.slice(-chunkSize));
|
||||
str = str.slice(0, -chunkSize);
|
||||
}
|
||||
let result = 0n;
|
||||
for (const chunk of chunks) {
|
||||
result *= powerOfChunkSize;
|
||||
const int = parseInt(chunk, base);
|
||||
if (Number.isNaN(int)) {
|
||||
throw new Error('Invalid base36 string');
|
||||
}
|
||||
result += BigInt(int);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseBigInt36(str: string): bigint {
|
||||
// log_36(Number.MAX_SAFE_INTEGER) => 10.251599391715352
|
||||
// so we process 10 chars at once
|
||||
return parseBigIntChunked(str, 36, 10, 36n ** 10n);
|
||||
}
|
||||
|
||||
export function parseBigInt16(str: string): bigint {
|
||||
// log_16(Number.MAX_SAFE_INTEGER) => 13.25
|
||||
// so we process 13 chars at once
|
||||
return parseBigIntChunked(str, 16, 13, 16n ** 13n);
|
||||
}
|
||||
|
||||
export function parseBigInt32(str: string): bigint {
|
||||
// log_32(Number.MAX_SAFE_INTEGER) => 10.6
|
||||
// so we process 10 chars at once
|
||||
return parseBigIntChunked(str, 32, 10, 32n ** 10n);
|
||||
}
|
||||
@@ -7,23 +7,23 @@ import * as Redis from 'ioredis';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
export class RedisKVCache<T> {
|
||||
private redisClient: Redis.Redis;
|
||||
private name: string;
|
||||
private lifetime: number;
|
||||
private memoryCache: MemoryKVCache<T>;
|
||||
private fetcher: (key: string) => Promise<T>;
|
||||
private toRedisConverter: (value: T) => string;
|
||||
private fromRedisConverter: (value: string) => T | undefined;
|
||||
private readonly lifetime: number;
|
||||
private readonly memoryCache: MemoryKVCache<T>;
|
||||
private readonly fetcher: (key: string) => Promise<T>;
|
||||
private readonly toRedisConverter: (value: T) => string;
|
||||
private readonly fromRedisConverter: (value: string) => T | undefined;
|
||||
|
||||
constructor(redisClient: RedisKVCache<T>['redisClient'], name: RedisKVCache<T>['name'], opts: {
|
||||
lifetime: RedisKVCache<T>['lifetime'];
|
||||
memoryCacheLifetime: number;
|
||||
fetcher: RedisKVCache<T>['fetcher'];
|
||||
toRedisConverter: RedisKVCache<T>['toRedisConverter'];
|
||||
fromRedisConverter: RedisKVCache<T>['fromRedisConverter'];
|
||||
}) {
|
||||
this.redisClient = redisClient;
|
||||
this.name = name;
|
||||
constructor(
|
||||
private redisClient: Redis.Redis,
|
||||
private name: string,
|
||||
opts: {
|
||||
lifetime: RedisKVCache<T>['lifetime'];
|
||||
memoryCacheLifetime: number;
|
||||
fetcher: RedisKVCache<T>['fetcher'];
|
||||
toRedisConverter: RedisKVCache<T>['toRedisConverter'];
|
||||
fromRedisConverter: RedisKVCache<T>['fromRedisConverter'];
|
||||
},
|
||||
) {
|
||||
this.lifetime = opts.lifetime;
|
||||
this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime);
|
||||
this.fetcher = opts.fetcher;
|
||||
@@ -55,7 +55,13 @@ export class RedisKVCache<T> {
|
||||
|
||||
const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`);
|
||||
if (cached == null) return undefined;
|
||||
return this.fromRedisConverter(cached);
|
||||
|
||||
const value = this.fromRedisConverter(cached);
|
||||
if (value !== undefined) {
|
||||
this.memoryCache.set(key, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -66,6 +72,10 @@ export class RedisKVCache<T> {
|
||||
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons:
|
||||
* * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster.
|
||||
* * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value.
|
||||
* * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses.
|
||||
*/
|
||||
@bindThis
|
||||
public async fetch(key: string): Promise<T> {
|
||||
@@ -77,14 +87,14 @@ export class RedisKVCache<T> {
|
||||
|
||||
// Cache MISS
|
||||
const value = await this.fetcher(key);
|
||||
this.set(key, value);
|
||||
await this.set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async refresh(key: string) {
|
||||
const value = await this.fetcher(key);
|
||||
this.set(key, value);
|
||||
await this.set(key, value);
|
||||
|
||||
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
|
||||
}
|
||||
@@ -101,23 +111,23 @@ export class RedisKVCache<T> {
|
||||
}
|
||||
|
||||
export class RedisSingleCache<T> {
|
||||
private redisClient: Redis.Redis;
|
||||
private name: string;
|
||||
private lifetime: number;
|
||||
private memoryCache: MemorySingleCache<T>;
|
||||
private fetcher: () => Promise<T>;
|
||||
private toRedisConverter: (value: T) => string;
|
||||
private fromRedisConverter: (value: string) => T | undefined;
|
||||
private readonly lifetime: number;
|
||||
private readonly memoryCache: MemorySingleCache<T>;
|
||||
private readonly fetcher: () => Promise<T>;
|
||||
private readonly toRedisConverter: (value: T) => string;
|
||||
private readonly fromRedisConverter: (value: string) => T | undefined;
|
||||
|
||||
constructor(redisClient: RedisSingleCache<T>['redisClient'], name: RedisSingleCache<T>['name'], opts: {
|
||||
lifetime: RedisSingleCache<T>['lifetime'];
|
||||
memoryCacheLifetime: number;
|
||||
fetcher: RedisSingleCache<T>['fetcher'];
|
||||
toRedisConverter: RedisSingleCache<T>['toRedisConverter'];
|
||||
fromRedisConverter: RedisSingleCache<T>['fromRedisConverter'];
|
||||
}) {
|
||||
this.redisClient = redisClient;
|
||||
this.name = name;
|
||||
constructor(
|
||||
private redisClient: Redis.Redis,
|
||||
private name: string,
|
||||
opts: {
|
||||
lifetime: number;
|
||||
memoryCacheLifetime: number;
|
||||
fetcher: RedisSingleCache<T>['fetcher'];
|
||||
toRedisConverter: RedisSingleCache<T>['toRedisConverter'];
|
||||
fromRedisConverter: RedisSingleCache<T>['fromRedisConverter'];
|
||||
},
|
||||
) {
|
||||
this.lifetime = opts.lifetime;
|
||||
this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime);
|
||||
this.fetcher = opts.fetcher;
|
||||
@@ -149,7 +159,13 @@ export class RedisSingleCache<T> {
|
||||
|
||||
const cached = await this.redisClient.get(`singlecache:${this.name}`);
|
||||
if (cached == null) return undefined;
|
||||
return this.fromRedisConverter(cached);
|
||||
|
||||
const value = this.fromRedisConverter(cached);
|
||||
if (value !== undefined) {
|
||||
this.memoryCache.set(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -160,6 +176,10 @@ export class RedisSingleCache<T> {
|
||||
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons:
|
||||
* * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster.
|
||||
* * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value.
|
||||
* * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses.
|
||||
*/
|
||||
@bindThis
|
||||
public async fetch(): Promise<T> {
|
||||
@@ -171,14 +191,14 @@ export class RedisSingleCache<T> {
|
||||
|
||||
// Cache MISS
|
||||
const value = await this.fetcher();
|
||||
this.set(value);
|
||||
await this.set(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async refresh() {
|
||||
const value = await this.fetcher();
|
||||
this.set(value);
|
||||
await this.set(value);
|
||||
|
||||
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
|
||||
}
|
||||
@@ -187,22 +207,12 @@ export class RedisSingleCache<T> {
|
||||
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
||||
|
||||
export class MemoryKVCache<T> {
|
||||
/**
|
||||
* データを持つマップ
|
||||
* @deprecated これを直接操作するべきではない
|
||||
*/
|
||||
public cache: Map<string, { date: number; value: T; }>;
|
||||
private lifetime: number;
|
||||
private gcIntervalHandle: NodeJS.Timeout;
|
||||
private readonly cache = new Map<string, { date: number; value: T; }>();
|
||||
private readonly gcIntervalHandle = setInterval(() => this.gc(), 1000 * 60 * 3); // 3m
|
||||
|
||||
constructor(lifetime: MemoryKVCache<never>['lifetime']) {
|
||||
this.cache = new Map();
|
||||
this.lifetime = lifetime;
|
||||
|
||||
this.gcIntervalHandle = setInterval(() => {
|
||||
this.gc();
|
||||
}, 1000 * 60 * 3);
|
||||
}
|
||||
constructor(
|
||||
private readonly lifetime: number,
|
||||
) {}
|
||||
|
||||
@bindThis
|
||||
/**
|
||||
@@ -287,10 +297,14 @@ export class MemoryKVCache<T> {
|
||||
@bindThis
|
||||
public gc(): void {
|
||||
const now = Date.now();
|
||||
|
||||
for (const [key, { date }] of this.cache.entries()) {
|
||||
if ((now - date) > this.lifetime) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
// The map is ordered from oldest to youngest.
|
||||
// We can stop once we find an entry that's still active, because all following entries must *also* be active.
|
||||
const age = now - date;
|
||||
if (age < this.lifetime) break;
|
||||
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,16 +312,19 @@ export class MemoryKVCache<T> {
|
||||
public dispose(): void {
|
||||
clearInterval(this.gcIntervalHandle);
|
||||
}
|
||||
|
||||
public get entries() {
|
||||
return this.cache.entries();
|
||||
}
|
||||
}
|
||||
|
||||
export class MemorySingleCache<T> {
|
||||
private cachedAt: number | null = null;
|
||||
private value: T | undefined;
|
||||
private lifetime: number;
|
||||
|
||||
constructor(lifetime: MemorySingleCache<never>['lifetime']) {
|
||||
this.lifetime = lifetime;
|
||||
}
|
||||
constructor(
|
||||
private lifetime: number,
|
||||
) {}
|
||||
|
||||
@bindThis
|
||||
public set(value: T): void {
|
||||
|
||||
44
packages/backend/src/misc/collapsed-queue.ts
Normal file
44
packages/backend/src/misc/collapsed-queue.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
type Job<V> = {
|
||||
value: V;
|
||||
timer: NodeJS.Timeout;
|
||||
};
|
||||
|
||||
// TODO: redis使えるようにする
|
||||
export class CollapsedQueue<K, V> {
|
||||
private jobs: Map<K, Job<V>> = new Map();
|
||||
|
||||
constructor(
|
||||
private timeout: number,
|
||||
private collapse: (oldValue: V, newValue: V) => V,
|
||||
private perform: (key: K, value: V) => Promise<void>,
|
||||
) {}
|
||||
|
||||
enqueue(key: K, value: V) {
|
||||
if (this.jobs.has(key)) {
|
||||
const old = this.jobs.get(key)!;
|
||||
const merged = this.collapse(old.value, value);
|
||||
this.jobs.set(key, { ...old, value: merged });
|
||||
} else {
|
||||
const timer = setTimeout(() => {
|
||||
const job = this.jobs.get(key)!;
|
||||
this.jobs.delete(key);
|
||||
this.perform(key, job.value);
|
||||
}, this.timeout);
|
||||
this.jobs.set(key, { value, timer });
|
||||
}
|
||||
}
|
||||
|
||||
async performAllNow() {
|
||||
const entries = [...this.jobs.entries()];
|
||||
this.jobs.clear();
|
||||
for (const [_key, job] of entries) {
|
||||
clearTimeout(job.timer);
|
||||
}
|
||||
await Promise.allSettled(entries.map(([key, job]) => this.perform(key, job.value)));
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import type { onRequestHookHandler } from 'fastify';
|
||||
export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {
|
||||
const index = request.url.indexOf('?');
|
||||
if (~index) {
|
||||
reply.redirect(301, request.url.slice(0, index));
|
||||
reply.redirect(request.url.slice(0, index), 301);
|
||||
}
|
||||
done();
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ2の[ノイズ文字列]
|
||||
|
||||
import * as crypto from 'node:crypto';
|
||||
import { parseBigInt36 } from '@/misc/bigint.js';
|
||||
|
||||
export const aidRegExp = /^[0-9a-z]{10}$/;
|
||||
|
||||
@@ -35,6 +36,12 @@ export function parseAid(id: string): { date: Date; } {
|
||||
return { date: new Date(time) };
|
||||
}
|
||||
|
||||
export function parseAidFull(id: string): { date: number; additional: bigint; } {
|
||||
const date = parseInt(id.slice(0, 8), 36) + TIME2000;
|
||||
const additional = parseBigInt36(id.slice(8, 10));
|
||||
return { date, additional };
|
||||
}
|
||||
|
||||
export function isSafeAidT(t: number): boolean {
|
||||
return t > TIME2000;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
// https://misskey.m544.net/notes/71899acdcc9859ec5708ac24
|
||||
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { parseBigInt36 } from '@/misc/bigint.js';
|
||||
|
||||
export const aidxRegExp = /^[0-9a-z]{16}$/;
|
||||
|
||||
@@ -16,6 +17,7 @@ const TIME2000 = 946684800000;
|
||||
const TIME_LENGTH = 8;
|
||||
const NODE_LENGTH = 4;
|
||||
const NOISE_LENGTH = 4;
|
||||
const AIDX_LENGTH = TIME_LENGTH + NODE_LENGTH + NOISE_LENGTH;
|
||||
|
||||
const nodeId = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', NODE_LENGTH)();
|
||||
let counter = 0;
|
||||
@@ -42,6 +44,12 @@ export function parseAidx(id: string): { date: Date; } {
|
||||
return { date: new Date(time) };
|
||||
}
|
||||
|
||||
export function parseAidxFull(id: string): { date: number; additional: bigint; } {
|
||||
const date = parseInt(id.slice(0, TIME_LENGTH), 36) + TIME2000;
|
||||
const additional = parseBigInt36(id.slice(TIME_LENGTH, AIDX_LENGTH));
|
||||
return { date, additional };
|
||||
}
|
||||
|
||||
export function isSafeAidxT(t: number): boolean {
|
||||
return t > TIME2000;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { parseBigInt16 } from '@/misc/bigint.js';
|
||||
|
||||
const CHARS = '0123456789abcdef';
|
||||
|
||||
// same as object-id
|
||||
@@ -39,6 +41,13 @@ export function parseMeid(id: string): { date: Date; } {
|
||||
};
|
||||
}
|
||||
|
||||
export function parseMeidFull(id: string): { date: number; additional: bigint; } {
|
||||
return {
|
||||
date: parseInt(id.slice(0, 12), 16) - 0x800000000000,
|
||||
additional: parseBigInt16(id.slice(12, 24)),
|
||||
};
|
||||
}
|
||||
|
||||
export function isSafeMeidT(t: number): boolean {
|
||||
return t > 0;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { parseBigInt16 } from '@/misc/bigint.js';
|
||||
|
||||
const CHARS = '0123456789abcdef';
|
||||
|
||||
// 4bit Fixed hex value 'g'
|
||||
@@ -39,6 +41,13 @@ export function parseMeidg(id: string): { date: Date; } {
|
||||
};
|
||||
}
|
||||
|
||||
export function parseMeidgFull(id: string): { date: number; additional: bigint; } {
|
||||
return {
|
||||
date: parseInt(id.slice(1, 12), 16),
|
||||
additional: parseBigInt16(id.slice(12, 24)),
|
||||
};
|
||||
}
|
||||
|
||||
export function isSafeMeidgT(t: number): boolean {
|
||||
return t > 0;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { parseBigInt16 } from '@/misc/bigint.js';
|
||||
|
||||
const CHARS = '0123456789abcdef';
|
||||
|
||||
// same as meid
|
||||
@@ -39,6 +41,13 @@ export function parseObjectId(id: string): { date: Date; } {
|
||||
};
|
||||
}
|
||||
|
||||
export function parseObjectIdFull(id: string): { date: number; additional: bigint; } {
|
||||
return {
|
||||
date: parseInt(id.slice(0, 8), 16) * 1000,
|
||||
additional: parseBigInt16(id.slice(8, 24)),
|
||||
};
|
||||
}
|
||||
|
||||
export function isSafeObjectIdT(t: number): boolean {
|
||||
return t > 0;
|
||||
}
|
||||
|
||||
@@ -5,15 +5,27 @@
|
||||
|
||||
// Crockford's Base32
|
||||
// https://github.com/ulid/spec#encoding
|
||||
import { parseBigInt32 } from '@/misc/bigint.js';
|
||||
|
||||
const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
||||
|
||||
export const ulidRegExp = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/;
|
||||
|
||||
export function parseUlid(id: string): { date: Date; } {
|
||||
const timestamp = id.slice(0, 10);
|
||||
function parseBase32(timestamp: string) {
|
||||
let time = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let i = 0; i < timestamp.length; i++) {
|
||||
time = time * 32 + CHARS.indexOf(timestamp[i]);
|
||||
}
|
||||
return { date: new Date(time) };
|
||||
return time;
|
||||
}
|
||||
|
||||
export function parseUlid(id: string): { date: Date; } {
|
||||
return { date: new Date(parseBase32(id.slice(0, 10))) };
|
||||
}
|
||||
|
||||
export function parseUlidFull(id: string): { date: number; additional: bigint; } {
|
||||
return {
|
||||
date: parseBase32(id.slice(0, 10)),
|
||||
additional: parseBigInt32(id.slice(10, 26)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default (token: string) => token.length === 16;
|
||||
@@ -6,6 +6,8 @@
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
|
||||
// NoteEntityService.isPureRenote とよしなにリンク
|
||||
|
||||
type Renote =
|
||||
MiNote & {
|
||||
renoteId: NonNullable<MiNote['renoteId']>
|
||||
|
||||
@@ -33,7 +33,11 @@ import { packedClipSchema } from '@/models/json-schema/clip.js';
|
||||
import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js';
|
||||
import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
|
||||
import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js';
|
||||
import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js';
|
||||
import {
|
||||
packedEmojiDetailedAdminSchema,
|
||||
packedEmojiDetailedSchema,
|
||||
packedEmojiSimpleSchema,
|
||||
} from '@/models/json-schema/emoji.js';
|
||||
import { packedFlashSchema } from '@/models/json-schema/flash.js';
|
||||
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
|
||||
import { packedSigninSchema } from '@/models/json-schema/signin.js';
|
||||
@@ -59,6 +63,10 @@ import {
|
||||
} from '@/models/json-schema/meta.js';
|
||||
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
|
||||
import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
|
||||
import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessageLiteForRoomSchema, packedChatMessageLiteFor1on1Schema } from '@/models/json-schema/chat-message.js';
|
||||
import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js';
|
||||
import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js';
|
||||
import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
|
||||
import { packedMahjongRoomDetailedSchema } from '@/models/json-schema/mahjong-room.js';
|
||||
|
||||
export const refs = {
|
||||
@@ -96,6 +104,7 @@ export const refs = {
|
||||
GalleryPost: packedGalleryPostSchema,
|
||||
EmojiSimple: packedEmojiSimpleSchema,
|
||||
EmojiDetailed: packedEmojiDetailedSchema,
|
||||
EmojiDetailedAdmin: packedEmojiDetailedAdminSchema,
|
||||
Flash: packedFlashSchema,
|
||||
Signin: packedSigninSchema,
|
||||
RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema,
|
||||
@@ -116,6 +125,13 @@ export const refs = {
|
||||
MetaDetailed: packedMetaDetailedSchema,
|
||||
SystemWebhook: packedSystemWebhookSchema,
|
||||
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
|
||||
ChatMessage: packedChatMessageSchema,
|
||||
ChatMessageLite: packedChatMessageLiteSchema,
|
||||
ChatMessageLiteFor1on1: packedChatMessageLiteFor1on1Schema,
|
||||
ChatMessageLiteForRoom: packedChatMessageLiteForRoomSchema,
|
||||
ChatRoom: packedChatRoomSchema,
|
||||
ChatRoomInvitation: packedChatRoomInvitationSchema,
|
||||
ChatRoomMembership: packedChatRoomMembershipSchema,
|
||||
MahjongRoomDetailed: packedMahjongRoomDetailedSchema,
|
||||
};
|
||||
|
||||
@@ -140,13 +156,15 @@ type OfSchema = {
|
||||
readonly anyOf?: ReadonlyArray<Schema>;
|
||||
readonly oneOf?: ReadonlyArray<Schema>;
|
||||
readonly allOf?: ReadonlyArray<Schema>;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Schema extends OfSchema {
|
||||
readonly type?: TypeStringef;
|
||||
readonly nullable?: boolean;
|
||||
readonly optional?: boolean;
|
||||
readonly prefixItems?: ReadonlyArray<Schema>;
|
||||
readonly items?: Schema;
|
||||
readonly unevaluatedItems?: Schema | boolean;
|
||||
readonly properties?: Obj;
|
||||
readonly required?: ReadonlyArray<Extract<keyof NonNullable<this['properties']>, string>>;
|
||||
readonly description?: string;
|
||||
@@ -161,15 +179,16 @@ export interface Schema extends OfSchema {
|
||||
readonly maximum?: number;
|
||||
readonly minimum?: number;
|
||||
readonly pattern?: string;
|
||||
readonly additionalProperties?: Schema | boolean;
|
||||
}
|
||||
|
||||
type RequiredPropertyNames<s extends Obj> = {
|
||||
[K in keyof s]:
|
||||
// K is not optional
|
||||
s[K]['optional'] extends false ? K :
|
||||
// K has default value
|
||||
s[K]['default'] extends null | string | number | boolean | Record<string, unknown> ? K :
|
||||
never
|
||||
// K is not optional
|
||||
s[K]['optional'] extends false ? K :
|
||||
// K has default value
|
||||
s[K]['default'] extends null | string | number | boolean | Record<string, unknown> ? K :
|
||||
never
|
||||
}[keyof s];
|
||||
|
||||
export type Obj = Record<string, Schema>;
|
||||
@@ -200,6 +219,7 @@ type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X
|
||||
//type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
|
||||
type UnionObjType<s extends Obj, a extends readonly any[], X extends ReadonlyArray<keyof s> = a[number]> = X extends any ? ObjType<s, X> : never;
|
||||
type ArrayUnion<T> = T extends any ? Array<T> : never;
|
||||
type ArrayToTuple<X extends ReadonlyArray<Schema>> = { [K in keyof X]: SchemaType<X[K]> };
|
||||
|
||||
type ObjectSchemaTypeDef<p extends Schema> =
|
||||
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
|
||||
@@ -207,11 +227,18 @@ type ObjectSchemaTypeDef<p extends Schema> =
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? p['anyOf'][number]['required'] extends ReadonlyArray<keyof p['properties']> ?
|
||||
UnionObjType<p['properties'], NonNullable<p['anyOf'][number]['required']>> & ObjType<p['properties'], NonNullable<p['required']>>
|
||||
: never
|
||||
: ObjType<p['properties'], NonNullable<p['required']>>
|
||||
:
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
|
||||
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
|
||||
any
|
||||
: ObjType<p['properties'], NonNullable<p['required']>>
|
||||
:
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
|
||||
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
|
||||
p['additionalProperties'] extends true ? Record<string, any> :
|
||||
p['additionalProperties'] extends Schema ?
|
||||
p['additionalProperties'] extends infer AdditionalProperties ?
|
||||
AdditionalProperties extends Schema ?
|
||||
Record<string, SchemaType<AdditionalProperties>> :
|
||||
never :
|
||||
never :
|
||||
any;
|
||||
|
||||
type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>;
|
||||
|
||||
@@ -221,24 +248,30 @@ export type SchemaTypeDef<p extends Schema> =
|
||||
p['type'] extends 'number' ? number :
|
||||
p['type'] extends 'string' ? (
|
||||
p['enum'] extends readonly (string | null)[] ?
|
||||
p['enum'][number] :
|
||||
p['format'] extends 'date-time' ? string : // Dateにする??
|
||||
string
|
||||
p['enum'][number] :
|
||||
p['format'] extends 'date-time' ? string : // Dateにする??
|
||||
string
|
||||
) :
|
||||
p['type'] extends 'boolean' ? boolean :
|
||||
p['type'] extends 'object' ? ObjectSchemaTypeDef<p> :
|
||||
p['type'] extends 'array' ? (
|
||||
p['items'] extends OfSchema ? (
|
||||
p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] :
|
||||
p['items']['oneOf'] extends ReadonlyArray<Schema> ? ArrayUnion<UnionSchemaType<NonNullable<p['items']['oneOf']>>> :
|
||||
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
|
||||
never
|
||||
p['type'] extends 'boolean' ? boolean :
|
||||
p['type'] extends 'object' ? ObjectSchemaTypeDef<p> :
|
||||
p['type'] extends 'array' ? (
|
||||
p['items'] extends OfSchema ? (
|
||||
p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] :
|
||||
p['items']['oneOf'] extends ReadonlyArray<Schema> ? ArrayUnion<UnionSchemaType<NonNullable<p['items']['oneOf']>>> :
|
||||
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
|
||||
never
|
||||
) :
|
||||
p['prefixItems'] extends ReadonlyArray<Schema> ? (
|
||||
p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] :
|
||||
p['items'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||
p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||
[...ArrayToTuple<p['prefixItems']>, ...unknown[]]
|
||||
) :
|
||||
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
|
||||
any[]
|
||||
) :
|
||||
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
|
||||
any[]
|
||||
) :
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
|
||||
p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> :
|
||||
any;
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
|
||||
p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> :
|
||||
any;
|
||||
|
||||
export type SchemaType<p extends Schema> = NullOrUndefined<p, SchemaTypeDef<p>>;
|
||||
|
||||
@@ -4,5 +4,9 @@
|
||||
*/
|
||||
|
||||
export type JsonValue = JsonArray | JsonObject | string | number | boolean | null;
|
||||
export type JsonObject = {[K in string]?: JsonValue};
|
||||
export type JsonObject = { [K in string]?: JsonValue };
|
||||
export type JsonArray = JsonValue[];
|
||||
|
||||
export function isJsonObject(value: JsonValue | undefined): value is JsonObject {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
*/
|
||||
|
||||
export function sqlLikeEscape(s: string) {
|
||||
return s.replace(/([%_])/g, '\\$1');
|
||||
return s.replace(/([\\%_])/g, '\\$1');
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default () => secureRndstr(16);
|
||||
export const generateNativeUserToken = () => secureRndstr(16);
|
||||
|
||||
export const isNativeUserToken = (token: string) => token.length === 16;
|
||||
Reference in New Issue
Block a user