mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-11 21:43:58 +02:00
Merge branch 'develop' into mahjong
This commit is contained in:
36
packages/backend/src/misc/FileWriterStream.ts
Normal file
36
packages/backend/src/misc/FileWriterStream.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs/promises';
|
||||
import type { PathLike } from 'node:fs';
|
||||
|
||||
/**
|
||||
* `fs.createWriteStream()`相当のことを行う`WritableStream` (Web標準)
|
||||
*/
|
||||
export class FileWriterStream extends WritableStream<Uint8Array> {
|
||||
constructor(path: PathLike) {
|
||||
let file: fs.FileHandle | null = null;
|
||||
|
||||
super({
|
||||
start: async () => {
|
||||
file = await fs.open(path, 'a');
|
||||
},
|
||||
write: async (chunk, controller) => {
|
||||
if (file === null) {
|
||||
controller.error();
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
await file.write(chunk);
|
||||
},
|
||||
close: async () => {
|
||||
await file?.close();
|
||||
},
|
||||
abort: async () => {
|
||||
await file?.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
35
packages/backend/src/misc/JsonArrayStream.ts
Normal file
35
packages/backend/src/misc/JsonArrayStream.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { TransformStream } from 'node:stream/web';
|
||||
|
||||
/**
|
||||
* ストリームに流れてきた各データについて`JSON.stringify()`した上で、それらを一つの配列にまとめる
|
||||
*/
|
||||
export class JsonArrayStream extends TransformStream<unknown, string> {
|
||||
constructor() {
|
||||
/** 最初の要素かどうかを変数に記録 */
|
||||
let isFirst = true;
|
||||
|
||||
super({
|
||||
start(controller) {
|
||||
controller.enqueue('[');
|
||||
},
|
||||
flush(controller) {
|
||||
controller.enqueue(']');
|
||||
},
|
||||
transform(chunk, controller) {
|
||||
if (isFirst) {
|
||||
isFirst = false;
|
||||
} else {
|
||||
// 妥当なJSON配列にするためには最初以外の要素の前に`,`を挿入しなければならない
|
||||
controller.enqueue(',\n');
|
||||
}
|
||||
|
||||
controller.enqueue(JSON.stringify(chunk));
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -186,28 +186,18 @@ export class RedisSingleCache<T> {
|
||||
|
||||
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
||||
|
||||
function nothingToDo<T, V = T>(value: T): V {
|
||||
return value as unknown as V;
|
||||
}
|
||||
|
||||
export class MemoryKVCache<T, V = T> {
|
||||
public cache: Map<string, { date: number; value: V; }>;
|
||||
export class MemoryKVCache<T> {
|
||||
/**
|
||||
* データを持つマップ
|
||||
* @deprecated これを直接操作するべきではない
|
||||
*/
|
||||
public cache: Map<string, { date: number; value: T; }>;
|
||||
private lifetime: number;
|
||||
private gcIntervalHandle: NodeJS.Timeout;
|
||||
private toMapConverter: (value: T) => V;
|
||||
private fromMapConverter: (cached: V) => T | undefined;
|
||||
|
||||
constructor(lifetime: MemoryKVCache<never>['lifetime'], options: {
|
||||
toMapConverter: (value: T) => V;
|
||||
fromMapConverter: (cached: V) => T | undefined;
|
||||
} = {
|
||||
toMapConverter: nothingToDo,
|
||||
fromMapConverter: nothingToDo,
|
||||
}) {
|
||||
constructor(lifetime: MemoryKVCache<never>['lifetime']) {
|
||||
this.cache = new Map();
|
||||
this.lifetime = lifetime;
|
||||
this.toMapConverter = options.toMapConverter;
|
||||
this.fromMapConverter = options.fromMapConverter;
|
||||
|
||||
this.gcIntervalHandle = setInterval(() => {
|
||||
this.gc();
|
||||
@@ -215,10 +205,14 @@ export class MemoryKVCache<T, V = T> {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
/**
|
||||
* Mapにキャッシュをセットします
|
||||
* @deprecated これを直接呼び出すべきではない。InternalEventなどで変更を全てのプロセス/マシンに通知するべき
|
||||
*/
|
||||
public set(key: string, value: T): void {
|
||||
this.cache.set(key, {
|
||||
date: Date.now(),
|
||||
value: this.toMapConverter(value),
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -230,7 +224,7 @@ export class MemoryKVCache<T, V = T> {
|
||||
this.cache.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
return this.fromMapConverter(cached.value);
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -241,10 +235,9 @@ export class MemoryKVCache<T, V = T> {
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||
* fetcherの引数はcacheに保存されている値があれば渡されます
|
||||
*/
|
||||
@bindThis
|
||||
public async fetch(key: string, fetcher: (value: V | undefined) => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
|
||||
public async fetch(key: string, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
|
||||
const cachedValue = this.get(key);
|
||||
if (cachedValue !== undefined) {
|
||||
if (validator) {
|
||||
@@ -259,7 +252,7 @@ export class MemoryKVCache<T, V = T> {
|
||||
}
|
||||
|
||||
// Cache MISS
|
||||
const value = await fetcher(this.cache.get(key)?.value);
|
||||
const value = await fetcher();
|
||||
this.set(key, value);
|
||||
return value;
|
||||
}
|
||||
@@ -267,10 +260,9 @@ export class MemoryKVCache<T, V = T> {
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||
* fetcherの引数はcacheに保存されている値があれば渡されます
|
||||
*/
|
||||
@bindThis
|
||||
public async fetchMaybe(key: string, fetcher: (value: V | undefined) => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
|
||||
public async fetchMaybe(key: string, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
|
||||
const cachedValue = this.get(key);
|
||||
if (cachedValue !== undefined) {
|
||||
if (validator) {
|
||||
@@ -285,7 +277,7 @@ export class MemoryKVCache<T, V = T> {
|
||||
}
|
||||
|
||||
// Cache MISS
|
||||
const value = await fetcher(this.cache.get(key)?.value);
|
||||
const value = await fetcher();
|
||||
if (value !== undefined) {
|
||||
this.set(key, value);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { onRequestHookHandler } from 'fastify';
|
||||
|
||||
export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => {
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
* https://en.wikipedia.org/wiki/Identicon
|
||||
*/
|
||||
|
||||
import * as p from 'pureimage';
|
||||
import { createCanvas } from '@napi-rs/canvas';
|
||||
import gen from 'random-seed';
|
||||
import type { WriteStream } from 'node:fs';
|
||||
|
||||
const size = 128; // px
|
||||
const n = 5; // resolution
|
||||
@@ -45,9 +44,9 @@ const sideN = Math.floor(n / 2);
|
||||
/**
|
||||
* Generate buffer of an identicon by seed
|
||||
*/
|
||||
export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
|
||||
export async function genIdenticon(seed: string): Promise<Buffer> {
|
||||
const rand = gen.create(seed);
|
||||
const canvas = p.make(size, size, undefined);
|
||||
const canvas = createCanvas(size, size);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const bgColors = colors[rand(colors.length)];
|
||||
@@ -101,5 +100,5 @@ export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
return p.encodePNGToStream(canvas, stream);
|
||||
return await canvas.encode('png');
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// we are using {} as "any non-nullish value" as expected
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export function isNotNull<T extends {}>(input: T | undefined | null): input is T {
|
||||
return input != null;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
|
||||
export function isPureRenote(note: MiNote): note is MiNote & { renoteId: NonNullable<MiNote['renoteId']> } {
|
||||
if (!note.renoteId) return false;
|
||||
|
||||
if (note.text) return false; // it's quoted with text
|
||||
if (note.fileIds.length !== 0) return false; // it's quoted with files
|
||||
if (note.hasPoll) return false; // it's quoted with poll
|
||||
return true;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function(note: MiNote): boolean {
|
||||
// sync with NoteCreateService.isQuote
|
||||
return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0));
|
||||
}
|
||||
67
packages/backend/src/misc/is-renote.ts
Normal file
67
packages/backend/src/misc/is-renote.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
|
||||
type Renote =
|
||||
MiNote & {
|
||||
renoteId: NonNullable<MiNote['renoteId']>
|
||||
};
|
||||
|
||||
type Quote =
|
||||
Renote & ({
|
||||
text: NonNullable<MiNote['text']>
|
||||
} | {
|
||||
cw: NonNullable<MiNote['cw']>
|
||||
} | {
|
||||
replyId: NonNullable<MiNote['replyId']>
|
||||
reply: NonNullable<MiNote['reply']>
|
||||
} | {
|
||||
hasPoll: true
|
||||
});
|
||||
|
||||
export function isRenote(note: MiNote): note is Renote {
|
||||
return note.renoteId != null;
|
||||
}
|
||||
|
||||
export function isQuote(note: Renote): note is Quote {
|
||||
// NOTE: SYNC WITH NoteCreateService.isQuote
|
||||
return note.text != null ||
|
||||
note.cw != null ||
|
||||
note.replyId != null ||
|
||||
note.hasPoll ||
|
||||
note.fileIds.length > 0;
|
||||
}
|
||||
|
||||
type PackedRenote =
|
||||
Packed<'Note'> & {
|
||||
renoteId: NonNullable<Packed<'Note'>['renoteId']>
|
||||
};
|
||||
|
||||
type PackedQuote =
|
||||
PackedRenote & ({
|
||||
text: NonNullable<Packed<'Note'>['text']>
|
||||
} | {
|
||||
cw: NonNullable<Packed<'Note'>['cw']>
|
||||
} | {
|
||||
replyId: NonNullable<Packed<'Note'>['replyId']>
|
||||
} | {
|
||||
poll: NonNullable<Packed<'Note'>['poll']>
|
||||
} | {
|
||||
fileIds: NonNullable<Packed<'Note'>['fileIds']>
|
||||
});
|
||||
|
||||
export function isRenotePacked(note: Packed<'Note'>): note is PackedRenote {
|
||||
return note.renoteId != null;
|
||||
}
|
||||
|
||||
export function isQuotePacked(note: PackedRenote): note is PackedQuote {
|
||||
return note.text != null ||
|
||||
note.cw != null ||
|
||||
note.replyId != null ||
|
||||
note.poll != null ||
|
||||
(note.fileIds != null && note.fileIds.length > 0);
|
||||
}
|
||||
@@ -4,12 +4,12 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
packedUserLiteSchema,
|
||||
packedUserDetailedNotMeOnlySchema,
|
||||
packedMeDetailedOnlySchema,
|
||||
packedUserDetailedNotMeSchema,
|
||||
packedMeDetailedSchema,
|
||||
packedUserDetailedNotMeOnlySchema,
|
||||
packedUserDetailedNotMeSchema,
|
||||
packedUserDetailedSchema,
|
||||
packedUserLiteSchema,
|
||||
packedUserSchema,
|
||||
} from '@/models/json-schema/user.js';
|
||||
import { packedNoteSchema } from '@/models/json-schema/note.js';
|
||||
@@ -25,7 +25,7 @@ import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
|
||||
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
|
||||
import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
|
||||
import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
|
||||
import { packedPageSchema, packedPageBlockSchema } from '@/models/json-schema/page.js';
|
||||
import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
|
||||
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
|
||||
import { packedChannelSchema } from '@/models/json-schema/channel.js';
|
||||
import { packedAntennaSchema } from '@/models/json-schema/antenna.js';
|
||||
@@ -37,9 +37,28 @@ import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/jso
|
||||
import { packedFlashSchema } from '@/models/json-schema/flash.js';
|
||||
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
|
||||
import { packedSigninSchema } from '@/models/json-schema/signin.js';
|
||||
import { packedRoleLiteSchema, packedRoleSchema, packedRolePoliciesSchema } from '@/models/json-schema/role.js';
|
||||
import {
|
||||
packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
|
||||
packedRoleCondFormulaLogicsSchema,
|
||||
packedRoleCondFormulaValueAssignedRoleSchema,
|
||||
packedRoleCondFormulaValueCreatedSchema,
|
||||
packedRoleCondFormulaValueIsLocalOrRemoteSchema,
|
||||
packedRoleCondFormulaValueNot,
|
||||
packedRoleCondFormulaValueSchema,
|
||||
packedRoleCondFormulaValueUserSettingBooleanSchema,
|
||||
packedRoleLiteSchema,
|
||||
packedRolePoliciesSchema,
|
||||
packedRoleSchema,
|
||||
} from '@/models/json-schema/role.js';
|
||||
import { packedAdSchema } from '@/models/json-schema/ad.js';
|
||||
import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
|
||||
import { packedReversiGameDetailedSchema, packedReversiGameLiteSchema } from '@/models/json-schema/reversi-game.js';
|
||||
import {
|
||||
packedMetaDetailedOnlySchema,
|
||||
packedMetaDetailedSchema,
|
||||
packedMetaLiteSchema,
|
||||
} 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 { packedMahjongRoomDetailedSchema } from '@/models/json-schema/mahjong-room.js';
|
||||
|
||||
export const refs = {
|
||||
@@ -79,11 +98,24 @@ export const refs = {
|
||||
EmojiDetailed: packedEmojiDetailedSchema,
|
||||
Flash: packedFlashSchema,
|
||||
Signin: packedSigninSchema,
|
||||
RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema,
|
||||
RoleCondFormulaValueNot: packedRoleCondFormulaValueNot,
|
||||
RoleCondFormulaValueIsLocalOrRemote: packedRoleCondFormulaValueIsLocalOrRemoteSchema,
|
||||
RoleCondFormulaValueUserSettingBooleanSchema: packedRoleCondFormulaValueUserSettingBooleanSchema,
|
||||
RoleCondFormulaValueAssignedRole: packedRoleCondFormulaValueAssignedRoleSchema,
|
||||
RoleCondFormulaValueCreated: packedRoleCondFormulaValueCreatedSchema,
|
||||
RoleCondFormulaFollowersOrFollowingOrNotes: packedRoleCondFormulaFollowersOrFollowingOrNotesSchema,
|
||||
RoleCondFormulaValue: packedRoleCondFormulaValueSchema,
|
||||
RoleLite: packedRoleLiteSchema,
|
||||
Role: packedRoleSchema,
|
||||
RolePolicies: packedRolePoliciesSchema,
|
||||
ReversiGameLite: packedReversiGameLiteSchema,
|
||||
ReversiGameDetailed: packedReversiGameDetailedSchema,
|
||||
MetaLite: packedMetaLiteSchema,
|
||||
MetaDetailedOnly: packedMetaDetailedOnlySchema,
|
||||
MetaDetailed: packedMetaDetailedSchema,
|
||||
SystemWebhook: packedSystemWebhookSchema,
|
||||
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
|
||||
MahjongRoomDetailed: packedMahjongRoomDetailedSchema,
|
||||
};
|
||||
|
||||
@@ -202,7 +234,7 @@ export type SchemaTypeDef<p extends Schema> =
|
||||
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
|
||||
never
|
||||
) :
|
||||
p['items'] extends NonNullable<Schema> ? SchemaTypeDef<p['items']>[] :
|
||||
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
|
||||
any[]
|
||||
) :
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export type FetchFunction<K, V> = (key: K) => Promise<V>;
|
||||
|
||||
type ResolveReject<V> = Parameters<ConstructorParameters<typeof Promise<V>>[0]>;
|
||||
|
||||
@@ -65,44 +65,6 @@ export function maximum(xs: number[]): number {
|
||||
return Math.max(...xs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits an array based on the equivalence relation.
|
||||
* The concatenation of the result is equal to the argument.
|
||||
*/
|
||||
export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
|
||||
const groups = [] as T[][];
|
||||
for (const x of xs) {
|
||||
const lastGroup = groups.at(-1);
|
||||
if (lastGroup !== undefined && f(lastGroup[0], x)) {
|
||||
lastGroup.push(x);
|
||||
} else {
|
||||
groups.push([x]);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits an array based on the equivalence relation induced by the function.
|
||||
* The concatenation of the result is equal to the argument.
|
||||
*/
|
||||
export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
|
||||
return groupBy((a, b) => f(a) === f(b), xs);
|
||||
}
|
||||
|
||||
export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
|
||||
return collections.reduce((obj: Record<string, T[]>, item: T) => {
|
||||
const key = keySelector(item);
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
obj[key] = [];
|
||||
}
|
||||
|
||||
obj[key].push(item);
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two arrays by lexicographical order
|
||||
*/
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function gcd(a: number, b: number): number {
|
||||
return b === 0 ? a : gcd(b, a % b);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export interface IMaybe<T> {
|
||||
isJust(): this is IJust<T>;
|
||||
}
|
||||
|
||||
export interface IJust<T> extends IMaybe<T> {
|
||||
get(): T;
|
||||
}
|
||||
|
||||
export function just<T>(value: T): IJust<T> {
|
||||
return {
|
||||
isJust: () => true,
|
||||
get: () => value,
|
||||
};
|
||||
}
|
||||
|
||||
export function nothing<T>(): IMaybe<T> {
|
||||
return {
|
||||
isJust: () => false,
|
||||
};
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function concat(xs: string[]): string {
|
||||
return xs.join('');
|
||||
}
|
||||
|
||||
export function capitalize(s: string): string {
|
||||
return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1));
|
||||
}
|
||||
|
||||
export function toUpperCase(s: string): string {
|
||||
return s.toUpperCase();
|
||||
}
|
||||
|
||||
export function toLowerCase(s: string): string {
|
||||
return s.toLowerCase();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const fallback = Symbol('fallback');
|
||||
Reference in New Issue
Block a user