Merge branch 'develop' into feat-1714

This commit is contained in:
かっこかり 2024-07-25 17:42:47 +09:00 committed by GitHub
commit fa356fa149
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 212 additions and 92 deletions

View File

@ -40,6 +40,9 @@
- Fix: 子メニューの高さがウィンドウからはみ出ることがある問題を修正 - Fix: 子メニューの高さがウィンドウからはみ出ることがある問題を修正
- Fix: 個人宛てのダイアログ形式のお知らせが即時表示されない問題を修正 - Fix: 個人宛てのダイアログ形式のお知らせが即時表示されない問題を修正
- Fix: 一部の画像がセンシティブ指定されているときに画面に何も表示されないことがあるのを修正 - Fix: 一部の画像がセンシティブ指定されているときに画面に何も表示されないことがあるのを修正
- Fix: リアクションしたユーザー一覧のユーザー名がはみ出る問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/672)
- Fix: `/share`ページにおいて絵文字ピッカーを開くことができない問題を修正
### Server ### Server
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
@ -70,6 +73,8 @@
- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正 - Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652) (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652)
- Fix: ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正 - Fix: ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正
- Fix: FTT有効時にリモートユーザーのートがHTLにキャッシュされる問題を修正
- Fix: 一部の通知がローカル上のリモートユーザーに対して行われていた問題を修正
- Fix: エラーメッセージの誤字を修正 (#14213) - Fix: エラーメッセージの誤字を修正 (#14213)
- Fix: ソーシャルタイムラインにローカルタイムラインに表示される自分へのリプライが表示されない問題を修正 - Fix: ソーシャルタイムラインにローカルタイムラインに表示される自分へのリプライが表示されない問題を修正
- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正 - Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正

View File

@ -20,13 +20,29 @@ Before creating an issue, please check the following:
> **Warning** > **Warning**
> Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged. > Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
## Before implementation
### Recommended discussing before implementation
We welcome your purposal.
When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented. When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented.
At this point, you also need to clarify the goals of the PR you will create, and make sure that the other members of the team are aware of them. At this point, you also need to clarify the goals of the PR you will create, and make sure that the other members of the team are aware of them.
PRs that do not have a clear set of do's and don'ts tend to be bloated and difficult to review. PRs that do not have a clear set of do's and don'ts tend to be bloated and difficult to review.
Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask another member to assign you). By expressing your intention to work the Issue, you can prevent conflicts in the work. Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask Commiter to assign you).
By expressing your intention to work on the Issue, you can prevent conflicts in the work.
To the Committers: you should not assign someone on it before the Final Decision.
### How issues are triaged
The Commiters may:
* close an issue that is not reproducible on latest stable release,
* merge an issue into another issue,
* split an issue into multiple issues,
* or re-open that has been closed for some reason which is not applicable anymore.
@syuilo reserves the Final Desicion rights including whether the project will implement feature and how to implement, these rights are not always exercised.
## Well-known branches ## Well-known branches
- **`master`** branch is tracking the latest release and used for production purposes. - **`master`** branch is tracking the latest release and used for production purposes.

View File

@ -933,12 +933,15 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
} }
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL // 自分自身のHTL
if (note.userHost == null) {
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) {
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) { if (note.fileIds.length > 0) {
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
} }
} }
}
// 自分自身以外への返信 // 自分自身以外への返信
if (isReply(note)) { if (isReply(note)) {

View File

@ -505,14 +505,15 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
this.globalEventService.publishInternalEvent('userRoleAssigned', created); this.globalEventService.publishInternalEvent('userRoleAssigned', created);
if (role.isPublic) { const user = await this.usersRepository.findOneByOrFail({ id: userId });
if (role.isPublic && user.host === null) {
this.notificationService.createNotification(userId, 'roleAssigned', { this.notificationService.createNotification(userId, 'roleAssigned', {
roleId: roleId, roleId: roleId,
}); });
} }
if (moderator) { if (moderator) {
const user = await this.usersRepository.findOneByOrFail({ id: userId });
this.moderationLogService.log(moderator, 'assignRole', { this.moderationLogService.log(moderator, 'assignRole', {
roleId: roleId, roleId: roleId,
roleName: role.name, roleName: role.name,

View File

@ -279,9 +279,11 @@ export class UserFollowingService implements OnModuleInit {
}); });
// 通知を作成 // 通知を作成
if (follower.host === null) {
this.notificationService.createNotification(follower.id, 'followRequestAccepted', { this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
}, followee.id); }, followee.id);
} }
}
if (alreadyFollowed) return; if (alreadyFollowed) return;

View File

@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { PollVotesRepository, NotesRepository } from '@/models/_.js'; import type { PollVotesRepository, NotesRepository } from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { CacheService } from '@/core/CacheService.js';
import { NotificationService } from '@/core/NotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
@ -24,6 +25,7 @@ export class EndedPollNotificationProcessorService {
@Inject(DI.pollVotesRepository) @Inject(DI.pollVotesRepository)
private pollVotesRepository: PollVotesRepository, private pollVotesRepository: PollVotesRepository,
private cacheService: CacheService,
private notificationService: NotificationService, private notificationService: NotificationService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
) { ) {
@ -47,9 +49,12 @@ export class EndedPollNotificationProcessorService {
const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])]; const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])];
for (const userId of userIds) { for (const userId of userIds) {
const profile = await this.cacheService.userProfileCache.fetch(userId);
if (profile.userHost === null) {
this.notificationService.createNotification(userId, 'pollEnded', { this.notificationService.createNotification(userId, 'pollEnded', {
noteId: note.id, noteId: note.id,
}); });
} }
} }
}
} }

View File

@ -9,8 +9,8 @@
import * as assert from 'assert'; import * as assert from 'assert';
import { setTimeout } from 'node:timers/promises'; import { setTimeout } from 'node:timers/promises';
import { Redis } from 'ioredis'; import { Redis } from 'ioredis';
import { loadConfig } from '@/config.js';
import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js'; import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js';
import { loadConfig } from '@/config.js';
function genHost() { function genHost() {
return randomString() + '.example.com'; return randomString() + '.example.com';
@ -492,6 +492,44 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
}); });
test.concurrent('FTT: ローカルユーザーの HTL にはプッシュされる', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', {
userId: alice.id,
}, bob);
const aliceNote = await post(alice, { text: 'I\'m Alice.' });
const bobNote = await post(bob, { text: 'I\'m Bob.' });
const carolNote = await post(carol, { text: 'I\'m Carol.' });
await waitForPushToTl();
// NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる
assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 1);
const bobHTL = await redisForTimelines.lrange(`list:homeTimeline:${bob.id}`, 0, -1);
assert.strictEqual(bobHTL.includes(aliceNote.id), true);
assert.strictEqual(bobHTL.includes(bobNote.id), true);
assert.strictEqual(bobHTL.includes(carolNote.id), false);
});
test.concurrent('FTT: リモートユーザーの HTL にはプッシュされない', async () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
await api('following/create', {
userId: alice.id,
}, bob);
await post(alice, { text: 'I\'m Alice.' });
await post(bob, { text: 'I\'m Bob.' });
await waitForPushToTl();
// NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる
assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 0);
});
}); });
describe('Local TL', () => { describe('Local TL', () => {

View File

@ -6,9 +6,12 @@
import { createApp, defineAsyncComponent } from 'vue'; import { createApp, defineAsyncComponent } from 'vue';
import { common } from './common.js'; import { common } from './common.js';
import type { CommonBootOptions } from './common.js'; import type { CommonBootOptions } from './common.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
export async function subBoot(options?: Partial<CommonBootOptions>) { export async function subBoot(options?: Partial<CommonBootOptions>) {
const { isClientUpdated } = await common(() => createApp( const { isClientUpdated } = await common(() => createApp(
defineAsyncComponent(() => import('@/ui/minimum.vue')), defineAsyncComponent(() => import('@/ui/minimum.vue')),
), options); ), options);
emojiPicker.init();
} }

View File

@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput> </MkInput>
</section> </section>
<section v-else-if="expiration === 'after'"> <section v-else-if="expiration === 'after'">
<MkInput v-model="after" small type="number" class="input"> <MkInput v-model="after" small type="number" min="1" class="input">
<template #label>{{ i18n.ts._poll.duration }}</template> <template #label>{{ i18n.ts._poll.duration }}</template>
</MkInput> </MkInput>
<MkSelect v-model="unit" small> <MkSelect v-model="unit" small>

View File

@ -81,6 +81,7 @@ function getReactionName(reaction: string): string {
} }
.user { .user {
display: flex;
line-height: 24px; line-height: 24px;
padding-top: 4px; padding-top: 4px;
white-space: nowrap; white-space: nowrap;

View File

@ -53,7 +53,7 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
const current = resolveNested(router.current)!; const current = resolveNested(router.current)!;
const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage); const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
const currentPageProps = ref(current.props); const currentPageProps = ref(current.props);
const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props))); const key = ref(router.getCurrentKey() + JSON.stringify(Object.fromEntries(current.props)));
function onChange({ resolved, key: newKey }) { function onChange({ resolved, key: newKey }) {
const current = resolveNested(resolved); const current = resolveNested(resolved);

View File

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local"> <MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local">
<template #label>{{ i18n.ts.expirationDate }}</template> <template #label>{{ i18n.ts.expirationDate }}</template>
</MkInput> </MkInput>
<MkInput v-model="createCount" type="number"> <MkInput v-model="createCount" type="number" min="1">
<template #label>{{ i18n.ts.createCount }}</template> <template #label>{{ i18n.ts.createCount }}</template>
</MkInput> </MkInput>
<MkButton primary rounded @click="createWithOptions">{{ i18n.ts.create }}</MkButton> <MkButton primary rounded @click="createWithOptions">{{ i18n.ts.create }}</MkButton>

View File

@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="statusbar.props.shuffle"> <MkSwitch v-model="statusbar.props.shuffle">
<template #label>{{ i18n.ts.shuffle }}</template> <template #label>{{ i18n.ts.shuffle }}</template>
</MkSwitch> </MkSwitch>
<MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number"> <MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number" min="1">
<template #label>{{ i18n.ts.refreshInterval }}</template> <template #label>{{ i18n.ts.refreshInterval }}</template>
</MkInput> </MkInput>
<MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1"> <MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1">
@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch> </MkSwitch>
</template> </template>
<template v-else-if="statusbar.type === 'federation'"> <template v-else-if="statusbar.type === 'federation'">
<MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number"> <MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number" min="1">
<template #label>{{ i18n.ts.refreshInterval }}</template> <template #label>{{ i18n.ts.refreshInterval }}</template>
</MkInput> </MkInput>
<MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1"> <MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1">

View File

@ -64,6 +64,7 @@ const devConfig: UserConfig = {
'/bios': httpUrl, '/bios': httpUrl,
'/cli': httpUrl, '/cli': httpUrl,
'/inbox': httpUrl, '/inbox': httpUrl,
'/emoji/': httpUrl,
'/notes': { '/notes': {
target: httpUrl, target: httpUrl,
bypass: varyHandler, bypass: varyHandler,

View File

@ -1,6 +1,7 @@
import tsParser from '@typescript-eslint/parser'; import tsParser from '@typescript-eslint/parser';
import sharedConfig from '../shared/eslint.config.js'; import sharedConfig from '../shared/eslint.config.js';
// eslint-disable-next-line import/no-default-export
export default [ export default [
...sharedConfig, ...sharedConfig,
{ {

View File

@ -551,7 +551,7 @@ type Channel = components['schemas']['Channel'];
// Warning: (ae-forgotten-export) The symbol "AnyOf" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "AnyOf" needs to be exported by the entry point index.d.ts
// //
// @public (undocumented) // @public (undocumented)
export abstract class ChannelConnection<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> { export abstract class ChannelConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> {
constructor(stream: Stream, channel: string, name?: string); constructor(stream: Stream, channel: string, name?: string);
// (undocumented) // (undocumented)
channel: string; channel: string;
@ -771,12 +771,12 @@ export type Channels = {
user1: boolean; user1: boolean;
user2: boolean; user2: boolean;
}) => void; }) => void;
updateSettings: (payload: { updateSettings: <K extends ReversiUpdateKey>(payload: {
userId: User['id']; userId: User['id'];
key: string; key: K;
value: any; value: ReversiGameDetailed[K];
}) => void; }) => void;
log: (payload: Record<string, any>) => void; log: (payload: Record<string, unknown>) => void;
}; };
receives: { receives: {
putStone: { putStone: {
@ -785,10 +785,7 @@ export type Channels = {
}; };
ready: boolean; ready: boolean;
cancel: null | Record<string, never>; cancel: null | Record<string, never>;
updateSettings: { updateSettings: ReversiUpdateSettings<ReversiUpdateKey>;
key: string;
value: any;
};
claimTimeIsUp: null | Record<string, never>; claimTimeIsUp: null | Record<string, never>;
}; };
}; };
@ -2782,7 +2779,7 @@ type PagesUnlikeRequest = operations['pages___unlike']['requestBody']['content']
type PagesUpdateRequest = operations['pages___update']['requestBody']['content']['application/json']; type PagesUpdateRequest = operations['pages___update']['requestBody']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
function parse(acct: string): Acct; function parse(_acct: string): Acct;
// Warning: (ae-forgotten-export) The symbol "Values" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Values" needs to be exported by the entry point index.d.ts
// //
@ -3019,7 +3016,7 @@ export class Stream extends EventEmitter<StreamEvents> implements IStream {
constructor(origin: string, user: { constructor(origin: string, user: {
token: string; token: string;
} | null, options?: { } | null, options?: {
WebSocket?: any; WebSocket?: WebSocket;
}); });
// (undocumented) // (undocumented)
close(): void; close(): void;
@ -3036,9 +3033,9 @@ export class Stream extends EventEmitter<StreamEvents> implements IStream {
// (undocumented) // (undocumented)
send(typeOrPayload: string): void; send(typeOrPayload: string): void;
// (undocumented) // (undocumented)
send(typeOrPayload: string, payload: any): void; send(typeOrPayload: string, payload: unknown): void;
// (undocumented) // (undocumented)
send(typeOrPayload: Record<string, any> | any[]): void; send(typeOrPayload: Record<string, unknown> | unknown[]): void;
// (undocumented) // (undocumented)
state: 'initializing' | 'reconnecting' | 'connected'; state: 'initializing' | 'reconnecting' | 'connected';
// (undocumented) // (undocumented)
@ -3281,7 +3278,9 @@ type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['
// Warnings were encountered during analysis: // Warnings were encountered during analysis:
// //
// src/entities.ts:34:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/entities.ts:35:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:220:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:230:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package) // (No @packageDocumentation comment for this package)

View File

@ -3,7 +3,8 @@ export type Acct = {
host: string | null; host: string | null;
}; };
export function parse(acct: string): Acct { export function parse(_acct: string): Acct {
let acct = _acct;
if (acct.startsWith('@')) acct = acct.substring(1); if (acct.startsWith('@')) acct = acct.substring(1);
const split = acct.split('@', 2); const split = acct.split('@', 2);
return { username: split[0], host: split[1] || null }; return { username: split[0], host: split[1] || null };

View File

@ -14,6 +14,7 @@ export type APIError = {
code: string; code: string;
message: string; message: string;
kind: 'client' | 'server'; kind: 'client' | 'server';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
info: Record<string, any>; info: Record<string, any>;
}; };
@ -29,6 +30,7 @@ export type FetchLike = (input: string, init?: {
headers: { [key in string]: string } headers: { [key in string]: string }
}) => Promise<{ }) => Promise<{
status: number; status: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
json(): Promise<any>; json(): Promise<any>;
}>; }>;
@ -49,6 +51,7 @@ export class APIClient {
this.fetch = opts.fetch ?? ((...args) => fetch(...args)); this.fetch = opts.fetch ?? ((...args) => fetch(...args));
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private assertIsRecord<T>(obj: T): obj is T & Record<string, any> { private assertIsRecord<T>(obj: T): obj is T & Record<string, any> {
return obj !== null && typeof obj === 'object' && !Array.isArray(obj); return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
} }

View File

@ -28,11 +28,13 @@ type StrictExtract<Union, Cond> = Cond extends Union ? Union : never;
type IsCaseMatched<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> = type IsCaseMatched<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> =
Endpoints[E]['res'] extends SwitchCase Endpoints[E]['res'] extends SwitchCase
// eslint-disable-next-line @typescript-eslint/no-explicit-any
? IsNeverType<StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>> extends false ? true : false ? IsNeverType<StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>> extends false ? true : false
: false : false
type GetCaseResult<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> = type GetCaseResult<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> =
Endpoints[E]['res'] extends SwitchCase Endpoints[E]['res'] extends SwitchCase
// eslint-disable-next-line @typescript-eslint/no-explicit-any
? StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>[1] ? StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>[1]
: never : never

View File

@ -1,3 +1,13 @@
import type { operations } from './autogen/types.js';
import type {
AbuseReportNotificationRecipient, Ad,
Announcement,
EmojiDetailed, InviteCode,
MetaDetailed,
Note,
Role, SystemWebhook, UserLite,
} from './autogen/models.js';
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const; export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
@ -135,10 +145,30 @@ export const moderationLogTypes = [
'unsetUserBanner', 'unsetUserBanner',
] as const; ] as const;
// See: packages/backend/src/core/ReversiService.ts@L410
export const reversiUpdateKeys = [
'map',
'bw',
'isLlotheo',
'canPutEverywhere',
'loopedBoard',
'timeLimitForEachTurn',
] as const;
export type ReversiUpdateKey = typeof reversiUpdateKeys[number];
type AvatarDecoration = UserLite['avatarDecorations'][number];
type ReceivedAbuseReport = {
reportId: AbuseReportNotificationRecipient['id'];
report: operations['admin___abuse-user-reports']['responses'][200]['content']['application/json'];
forwarded: boolean;
};
export type ModerationLogPayloads = { export type ModerationLogPayloads = {
updateServerSettings: { updateServerSettings: {
before: any | null; before: MetaDetailed | null;
after: any | null; after: MetaDetailed | null;
}; };
suspend: { suspend: {
userId: string; userId: string;
@ -159,16 +189,16 @@ export type ModerationLogPayloads = {
}; };
addCustomEmoji: { addCustomEmoji: {
emojiId: string; emojiId: string;
emoji: any; emoji: EmojiDetailed;
}; };
updateCustomEmoji: { updateCustomEmoji: {
emojiId: string; emojiId: string;
before: any; before: EmojiDetailed;
after: any; after: EmojiDetailed;
}; };
deleteCustomEmoji: { deleteCustomEmoji: {
emojiId: string; emojiId: string;
emoji: any; emoji: EmojiDetailed;
}; };
assignRole: { assignRole: {
userId: string; userId: string;
@ -187,16 +217,16 @@ export type ModerationLogPayloads = {
}; };
createRole: { createRole: {
roleId: string; roleId: string;
role: any; role: Role;
}; };
updateRole: { updateRole: {
roleId: string; roleId: string;
before: any; before: Role;
after: any; after: Role;
}; };
deleteRole: { deleteRole: {
roleId: string; roleId: string;
role: any; role: Role;
}; };
clearQueue: Record<string, never>; clearQueue: Record<string, never>;
promoteQueue: Record<string, never>; promoteQueue: Record<string, never>;
@ -211,39 +241,39 @@ export type ModerationLogPayloads = {
noteUserId: string; noteUserId: string;
noteUserUsername: string; noteUserUsername: string;
noteUserHost: string | null; noteUserHost: string | null;
note: any; note: Note;
}; };
createGlobalAnnouncement: { createGlobalAnnouncement: {
announcementId: string; announcementId: string;
announcement: any; announcement: Announcement;
}; };
createUserAnnouncement: { createUserAnnouncement: {
announcementId: string; announcementId: string;
announcement: any; announcement: Announcement;
userId: string; userId: string;
userUsername: string; userUsername: string;
userHost: string | null; userHost: string | null;
}; };
updateGlobalAnnouncement: { updateGlobalAnnouncement: {
announcementId: string; announcementId: string;
before: any; before: Announcement;
after: any; after: Announcement;
}; };
updateUserAnnouncement: { updateUserAnnouncement: {
announcementId: string; announcementId: string;
before: any; before: Announcement;
after: any; after: Announcement;
userId: string; userId: string;
userUsername: string; userUsername: string;
userHost: string | null; userHost: string | null;
}; };
deleteGlobalAnnouncement: { deleteGlobalAnnouncement: {
announcementId: string; announcementId: string;
announcement: any; announcement: Announcement;
}; };
deleteUserAnnouncement: { deleteUserAnnouncement: {
announcementId: string; announcementId: string;
announcement: any; announcement: Announcement;
userId: string; userId: string;
userUsername: string; userUsername: string;
userHost: string | null; userHost: string | null;
@ -281,37 +311,37 @@ export type ModerationLogPayloads = {
}; };
resolveAbuseReport: { resolveAbuseReport: {
reportId: string; reportId: string;
report: any; report: ReceivedAbuseReport;
forwarded: boolean; forwarded: boolean;
}; };
createInvitation: { createInvitation: {
invitations: any[]; invitations: InviteCode[];
}; };
createAd: { createAd: {
adId: string; adId: string;
ad: any; ad: Ad;
}; };
updateAd: { updateAd: {
adId: string; adId: string;
before: any; before: Ad;
after: any; after: Ad;
}; };
deleteAd: { deleteAd: {
adId: string; adId: string;
ad: any; ad: Ad;
}; };
createAvatarDecoration: { createAvatarDecoration: {
avatarDecorationId: string; avatarDecorationId: string;
avatarDecoration: any; avatarDecoration: AvatarDecoration;
}; };
updateAvatarDecoration: { updateAvatarDecoration: {
avatarDecorationId: string; avatarDecorationId: string;
before: any; before: AvatarDecoration;
after: any; after: AvatarDecoration;
}; };
deleteAvatarDecoration: { deleteAvatarDecoration: {
avatarDecorationId: string; avatarDecorationId: string;
avatarDecoration: any; avatarDecoration: AvatarDecoration;
}; };
unsetUserAvatar: { unsetUserAvatar: {
userId: string; userId: string;
@ -327,28 +357,28 @@ export type ModerationLogPayloads = {
}; };
createSystemWebhook: { createSystemWebhook: {
systemWebhookId: string; systemWebhookId: string;
webhook: any; webhook: SystemWebhook;
}; };
updateSystemWebhook: { updateSystemWebhook: {
systemWebhookId: string; systemWebhookId: string;
before: any; before: SystemWebhook;
after: any; after: SystemWebhook;
}; };
deleteSystemWebhook: { deleteSystemWebhook: {
systemWebhookId: string; systemWebhookId: string;
webhook: any; webhook: SystemWebhook;
}; };
createAbuseReportNotificationRecipient: { createAbuseReportNotificationRecipient: {
recipientId: string; recipientId: string;
recipient: any; recipient: AbuseReportNotificationRecipient;
}; };
updateAbuseReportNotificationRecipient: { updateAbuseReportNotificationRecipient: {
recipientId: string; recipientId: string;
before: any; before: AbuseReportNotificationRecipient;
after: any; after: AbuseReportNotificationRecipient;
}; };
deleteAbuseReportNotificationRecipient: { deleteAbuseReportNotificationRecipient: {
recipientId: string; recipientId: string;
recipient: any; recipient: AbuseReportNotificationRecipient;
}; };
}; };

View File

@ -7,7 +7,7 @@ import {
Role, Role,
RolePolicies, RolePolicies,
User, User,
UserDetailedNotMe UserDetailedNotMe,
} from './autogen/models.js'; } from './autogen/models.js';
export * from './autogen/entities.js'; export * from './autogen/entities.js';
@ -19,6 +19,7 @@ export type DateString = string;
export type PageEvent = { export type PageEvent = {
pageId: Page['id']; pageId: Page['id'];
event: string; event: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
var: any; var: any;
userId: User['id']; userId: User['id'];
user: User; user: User;

View File

@ -15,7 +15,7 @@ export function urlQuery(obj: Record<string, string | number | boolean | undefin
.join('&'); .join('&');
} }
type AnyOf<T extends Record<any, any>> = T[keyof T]; type AnyOf<T extends Record<PropertyKey, unknown>> = T[keyof T];
export type StreamEvents = { export type StreamEvents = {
_connected_: void; _connected_: void;
@ -41,6 +41,7 @@ export interface IStream extends EventEmitter<StreamEvents> {
/** /**
* Misskey stream connection * Misskey stream connection
*/ */
// eslint-disable-next-line import/no-default-export
export default class Stream extends EventEmitter<StreamEvents> implements IStream { export default class Stream extends EventEmitter<StreamEvents> implements IStream {
private stream: _ReconnectingWebsocket.default; private stream: _ReconnectingWebsocket.default;
public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing'; public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing';
@ -50,7 +51,7 @@ export default class Stream extends EventEmitter<StreamEvents> implements IStrea
private idCounter = 0; private idCounter = 0;
constructor(origin: string, user: { token: string; } | null, options?: { constructor(origin: string, user: { token: string; } | null, options?: {
WebSocket?: any; WebSocket?: WebSocket;
}) { }) {
super(); super();
@ -67,6 +68,7 @@ export default class Stream extends EventEmitter<StreamEvents> implements IStrea
this.send = this.send.bind(this); this.send = this.send.bind(this);
this.close = this.close.bind(this); this.close = this.close.bind(this);
// eslint-disable-next-line no-param-reassign
options = options ?? { }; options = options ?? { };
const query = urlQuery({ const query = urlQuery({
@ -107,8 +109,8 @@ export default class Stream extends EventEmitter<StreamEvents> implements IStrea
this.sharedConnectionPools.push(pool); this.sharedConnectionPools.push(pool);
} }
const connection = new SharedConnection(this, channel, pool, name); const connection = new SharedConnection<Channels[C]>(this, channel, pool, name);
this.sharedConnections.push(connection); this.sharedConnections.push(connection as unknown as SharedConnection);
return connection; return connection;
} }
@ -122,7 +124,7 @@ export default class Stream extends EventEmitter<StreamEvents> implements IStrea
private connectToChannel<C extends keyof Channels>(channel: C, params: Channels[C]['params']): NonSharedConnection<Channels[C]> { private connectToChannel<C extends keyof Channels>(channel: C, params: Channels[C]['params']): NonSharedConnection<Channels[C]> {
const connection = new NonSharedConnection(this, channel, this.genId(), params); const connection = new NonSharedConnection(this, channel, this.genId(), params);
this.nonSharedConnections.push(connection); this.nonSharedConnections.push(connection as unknown as NonSharedConnection);
return connection; return connection;
} }
@ -190,9 +192,9 @@ export default class Stream extends EventEmitter<StreamEvents> implements IStrea
* ! JSONで行われます ! * ! JSONで行われます !
*/ */
public send(typeOrPayload: string): void public send(typeOrPayload: string): void
public send(typeOrPayload: string, payload: any): void public send(typeOrPayload: string, payload: unknown): void
public send(typeOrPayload: Record<string, any> | any[]): void public send(typeOrPayload: Record<string, unknown> | unknown[]): void
public send(typeOrPayload: string | Record<string, any> | any[], payload?: any): void { public send(typeOrPayload: string | Record<string, unknown> | unknown[], payload?: unknown): void {
if (typeof typeOrPayload === 'string') { if (typeof typeOrPayload === 'string') {
this.stream.send(JSON.stringify({ this.stream.send(JSON.stringify({
type: typeOrPayload, type: typeOrPayload,
@ -227,7 +229,7 @@ class Pool {
public id: string; public id: string;
protected stream: Stream; protected stream: Stream;
public users = 0; public users = 0;
private disposeTimerId: any; private disposeTimerId: ReturnType<typeof setTimeout> | null = null;
private isConnected = false; private isConnected = false;
constructor(stream: Stream, channel: string, id: string) { constructor(stream: Stream, channel: string, id: string) {
@ -302,7 +304,7 @@ export interface IChannelConnection<Channel extends AnyOf<Channels> = any> exten
dispose(): void; dispose(): void;
} }
export abstract class Connection<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> { export abstract class Connection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> {
public channel: string; public channel: string;
protected stream: Stream; protected stream: Stream;
public abstract id: string; public abstract id: string;
@ -336,7 +338,7 @@ export abstract class Connection<Channel extends AnyOf<Channels> = any> extends
public abstract dispose(): void; public abstract dispose(): void;
} }
class SharedConnection<Channel extends AnyOf<Channels> = any> extends Connection<Channel> { class SharedConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends Connection<Channel> {
private pool: Pool; private pool: Pool;
public get id(): string { public get id(): string {
@ -355,11 +357,11 @@ class SharedConnection<Channel extends AnyOf<Channels> = any> extends Connection
public dispose(): void { public dispose(): void {
this.pool.dec(); this.pool.dec();
this.removeAllListeners(); this.removeAllListeners();
this.stream.removeSharedConnection(this); this.stream.removeSharedConnection(this as unknown as SharedConnection);
} }
} }
class NonSharedConnection<Channel extends AnyOf<Channels> = any> extends Connection<Channel> { class NonSharedConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends Connection<Channel> {
public id: string; public id: string;
protected params: Channel['params']; protected params: Channel['params'];
@ -386,6 +388,6 @@ class NonSharedConnection<Channel extends AnyOf<Channels> = any> extends Connect
public dispose(): void { public dispose(): void {
this.removeAllListeners(); this.removeAllListeners();
this.stream.send('disconnect', { id: this.id }); this.stream.send('disconnect', { id: this.id });
this.stream.disconnectToChannel(this); this.stream.disconnectToChannel(this as unknown as NonSharedConnection);
} }
} }

View File

@ -21,6 +21,14 @@ import {
ServerStatsLog, ServerStatsLog,
ReversiGameDetailed, ReversiGameDetailed,
} from './entities.js'; } from './entities.js';
import {
ReversiUpdateKey,
} from './consts.js';
type ReversiUpdateSettings<K extends ReversiUpdateKey> = {
key: K;
value: ReversiGameDetailed[K];
};
export type Channels = { export type Channels = {
main: { main: {
@ -51,6 +59,7 @@ export type Channels = {
registryUpdated: (payload: { registryUpdated: (payload: {
scope?: string[]; scope?: string[];
key: string; key: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any | null; value: any | null;
}) => void; }) => void;
driveFileCreated: (payload: DriveFile) => void; driveFileCreated: (payload: DriveFile) => void;
@ -208,8 +217,8 @@ export type Channels = {
ended: (payload: { winnerId: User['id'] | null; game: ReversiGameDetailed; }) => void; ended: (payload: { winnerId: User['id'] | null; game: ReversiGameDetailed; }) => void;
canceled: (payload: { userId: User['id']; }) => void; canceled: (payload: { userId: User['id']; }) => void;
changeReadyStates: (payload: { user1: boolean; user2: boolean; }) => void; changeReadyStates: (payload: { user1: boolean; user2: boolean; }) => void;
updateSettings: (payload: { userId: User['id']; key: string; value: any; }) => void; updateSettings: <K extends ReversiUpdateKey>(payload: { userId: User['id']; key: K; value: ReversiGameDetailed[K]; }) => void;
log: (payload: Record<string, any>) => void; log: (payload: Record<string, unknown>) => void;
}; };
receives: { receives: {
putStone: { putStone: {
@ -218,10 +227,7 @@ export type Channels = {
}; };
ready: boolean; ready: boolean;
cancel: null | Record<string, never>; cancel: null | Record<string, never>;
updateSettings: { updateSettings: ReversiUpdateSettings<ReversiUpdateKey>;
key: string;
value: any;
};
claimTimeIsUp: null | Record<string, never>; claimTimeIsUp: null | Record<string, never>;
} }
} }