1
0
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:
syuilo
2025-04-10 11:39:35 +09:00
1539 changed files with 96547 additions and 41240 deletions

View File

@@ -4,6 +4,7 @@
*/
import * as fs from 'node:fs/promises';
import { WritableStream } from 'node:stream/web';
import type { PathLike } from 'node:fs';
/**

View 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);
}

View File

@@ -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 {

View 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)));
}
}

View File

@@ -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();
};

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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)),
};
}

View File

@@ -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;

View File

@@ -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']>

View File

@@ -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>>;

View File

@@ -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);
}

View File

@@ -4,5 +4,5 @@
*/
export function sqlLikeEscape(s: string) {
return s.replace(/([%_])/g, '\\$1');
return s.replace(/([\\%_])/g, '\\$1');
}

View File

@@ -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;