1
0
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:
syuilo
2024-07-02 10:10:01 +09:00
840 changed files with 44657 additions and 21435 deletions

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -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
*/

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const fallback = Symbol('fallback');