Merge branch 'develop' into enh-i-update-0

This commit is contained in:
かっこかり 2024-07-30 12:59:28 +09:00 committed by GitHub
commit cd26043c82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
88 changed files with 3932 additions and 3728 deletions

View file

@ -31,7 +31,7 @@
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
"test-and-coverage": "pnpm jest-and-coverage",
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
"generate-api-json": "pnpm build && node ./scripts/generate_api_json.js"
"generate-api-json": "node ./scripts/generate_api_json.js"
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
@ -65,11 +65,11 @@
"utf-8-validate": "6.0.3"
},
"dependencies": {
"@aws-sdk/client-s3": "3.600.0",
"@aws-sdk/lib-storage": "3.600.0",
"@bull-board/api": "5.20.5",
"@bull-board/fastify": "5.20.5",
"@bull-board/ui": "5.20.5",
"@aws-sdk/client-s3": "3.620.0",
"@aws-sdk/lib-storage": "3.620.0",
"@bull-board/api": "5.21.1",
"@bull-board/fastify": "5.21.1",
"@bull-board/ui": "5.21.1",
"@discordapp/twemoji": "15.0.3",
"@fastify/accepts": "4.3.0",
"@fastify/cookie": "9.3.1",
@ -86,22 +86,22 @@
"@nestjs/core": "10.3.10",
"@nestjs/testing": "10.3.10",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "8.13.0",
"@sentry/profiling-node": "8.13.0",
"@simplewebauthn/server": "10.0.0",
"@sentry/node": "8.20.0",
"@sentry/profiling-node": "8.20.0",
"@simplewebauthn/server": "10.0.1",
"@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.3.12",
"@swc/core": "1.6.6",
"@twemoji/parser": "15.1.1",
"accepts": "1.3.8",
"ajv": "8.16.0",
"ajv": "8.17.1",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "5.8.3",
"bullmq": "5.10.4",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.3.0",
@ -115,10 +115,10 @@
"fastify": "4.28.1",
"fastify-raw-body": "4.3.0",
"feed": "4.2.2",
"file-type": "19.0.0",
"file-type": "19.3.0",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.0",
"got": "14.4.1",
"got": "14.4.2",
"happy-dom": "10.0.3",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
@ -128,7 +128,7 @@
"ipaddr.js": "2.2.0",
"is-svg": "5.0.1",
"js-yaml": "4.1.0",
"jsdom": "24.1.0",
"jsdom": "24.1.1",
"json5": "2.2.3",
"jsonld": "8.3.2",
"jsrsasign": "11.1.0",
@ -177,11 +177,11 @@
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typeorm": "0.3.20",
"typescript": "5.5.3",
"typescript": "5.5.4",
"ulid": "2.3.0",
"vary": "1.1.2",
"web-push": "3.6.7",
"ws": "8.17.1",
"ws": "8.18.0",
"xev": "3.0.2"
},
"devDependencies": {
@ -201,11 +201,11 @@
"@types/jest": "29.5.12",
"@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.7",
"@types/jsonld": "1.5.14",
"@types/jsonld": "1.5.15",
"@types/jsrsasign": "10.5.14",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "20.14.9",
"@types/node": "20.14.12",
"@types/nodemailer": "6.4.15",
"@types/oauth": "0.9.5",
"@types/oauth2orize": "1.11.5",
@ -225,18 +225,18 @@
"@types/tmp": "0.2.6",
"@types/vary": "1.1.3",
"@types/web-push": "3.6.3",
"@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.15.0",
"@types/ws": "8.5.11",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"aws-sdk-client-mock": "4.0.1",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.29.1",
"execa": "9.2.0",
"execa": "9.3.0",
"fkill": "9.0.0",
"jest": "29.7.0",
"jest-mock": "29.7.0",
"nodemon": "3.1.4",
"pid-port": "1.0.0",
"simple-oauth2": "5.0.1"
"simple-oauth2": "5.1.0"
}
}

View file

@ -3,11 +3,34 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { loadConfig } from '../built/config.js'
import { genOpenapiSpec } from '../built/server/api/openapi/gen-spec.js'
import { writeFileSync } from "node:fs";
import { execa } from 'execa';
import { writeFileSync, existsSync } from "node:fs";
const config = loadConfig();
const spec = genOpenapiSpec(config, true);
async function main() {
if (!process.argv.includes('--no-build')) {
await execa('pnpm', ['run', 'build'], {
stdout: process.stdout,
stderr: process.stderr,
});
}
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
if (!existsSync('./built')) {
throw new Error('`built` directory does not exist.');
}
/** @type {import('../src/config.js')} */
const { loadConfig } = await import('../built/config.js');
/** @type {import('../src/server/api/openapi/gen-spec.js')} */
const { genOpenapiSpec } = await import('../built/server/api/openapi/gen-spec.js');
const config = loadConfig();
const spec = genOpenapiSpec(config, true);
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
}
main().catch(e => {
console.error(e);
process.exit(1);
});

View file

@ -44,7 +44,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
/**
* Redisイベントを用いて{@link abuseReports}.
* {@link RoleService.getModeratorIds}.
* {@link getModeratorIds}.
*
* @see RoleService.getModeratorIds
* @see GlobalEventService.publishAdminStream

View file

@ -933,10 +933,13 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
// 自分自身の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);
if (note.fileIds.length > 0) {
this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
}
}

View file

@ -505,14 +505,15 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
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', {
roleId: roleId,
});
}
if (moderator) {
const user = await this.usersRepository.findOneByOrFail({ id: userId });
this.moderationLogService.log(moderator, 'assignRole', {
roleId: roleId,
roleName: role.name,

View file

@ -21,6 +21,7 @@ import { bindThis } from '@/decorators.js';
import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js';
import { MetaService } from '@/core/MetaService.js';
import { UserService } from '@/core/UserService.js';
@Injectable()
export class SignupService {
@ -35,6 +36,7 @@ export class SignupService {
private usedUsernamesRepository: UsedUsernamesRepository,
private utilityService: UtilityService,
private userService: UserService,
private userEntityService: UserEntityService,
private idService: IdService,
private metaService: MetaService,
@ -148,7 +150,8 @@ export class SignupService {
}));
});
this.usersChart.update(account, true);
this.usersChart.update(account, true).then();
this.userService.notifySystemWebhook(account, 'userCreated').then();
return { account, secret };
}

View file

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

View file

@ -8,15 +8,18 @@ import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@Injectable()
export class UserService {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private systemWebhookService: SystemWebhookService,
private userEntityService: UserEntityService,
) {
}
@ -50,4 +53,23 @@ export class UserService {
});
}
}
/**
* SystemWebhookを用いてユーザに関する操作内容を管理者各位に通知する.
* JobQueueへのエンキューのみを行うため.
*
* @see SystemWebhookService.enqueueSystemWebhook
*/
@bindThis
public async notifySystemWebhook(user: MiUser, type: 'userCreated') {
const packedUser = await this.userEntityService.pack(user, null, { schema: 'UserLite' });
const recipientWebhookIds = await this.systemWebhookService.fetchSystemWebhooks({ isActive: true, on: [type] });
for (const webhookId of recipientWebhookIds) {
await this.systemWebhookService.enqueueSystemWebhook(
webhookId,
type,
packedUser,
);
}
}
}

View file

@ -12,6 +12,8 @@ export const systemWebhookEventTypes = [
'abuseReport',
// 通報を処理したとき
'abuseReportResolved',
// ユーザが作成された時
'userCreated',
] as const;
export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];

View file

@ -204,6 +204,7 @@ export const packedNoteSchema = {
reactionAcceptance: {
type: 'string',
optional: false, nullable: true,
enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null],
},
reactionEmojis: {
type: 'object',

View file

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

View file

@ -5,65 +5,24 @@
import { entities } from 'misskey-js';
import { beforeEach, describe, test } from '@jest/globals';
import Fastify from 'fastify';
import { api, randomString, role, signup, startJobQueue, UserToken } from '../../utils.js';
import {
api,
captureWebhook,
randomString,
role,
signup,
startJobQueue,
UserToken,
WEBHOOK_HOST,
} from '../../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
const WEBHOOK_HOST = 'http://localhost:15080';
const WEBHOOK_PORT = 15080;
process.env.NODE_ENV = 'test';
describe('[シナリオ] ユーザ通報', () => {
let queue: INestApplicationContext;
let admin: entities.SignupResponse;
let alice: entities.SignupResponse;
let bob: entities.SignupResponse;
type SystemWebhookPayload = {
server: string;
hookId: string;
eventId: string;
createdAt: string;
type: string;
body: any;
}
// -------------------------------------------------------------------------------------------
async function captureWebhook<T = SystemWebhookPayload>(postAction: () => Promise<void>): Promise<T> {
const fastify = Fastify();
let timeoutHandle: NodeJS.Timeout | null = null;
const result = await new Promise<string>(async (resolve, reject) => {
fastify.all('/', async (req, res) => {
timeoutHandle && clearTimeout(timeoutHandle);
const body = JSON.stringify(req.body);
res.status(200).send('ok');
await fastify.close();
resolve(body);
});
await fastify.listen({ port: WEBHOOK_PORT });
timeoutHandle = setTimeout(async () => {
await fastify.close();
reject(new Error('timeout'));
}, 3000);
try {
await postAction();
} catch (e) {
await fastify.close();
reject(e);
}
});
await fastify.close();
return JSON.parse(result) as T;
}
async function createSystemWebhook(args?: Partial<entities.AdminSystemWebhookCreateRequest>, credential?: UserToken): Promise<entities.AdminSystemWebhookCreateResponse> {
const res = await api(
'admin/system-webhook/create',

View file

@ -0,0 +1,130 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { setTimeout } from 'node:timers/promises';
import { entities } from 'misskey-js';
import { beforeEach, describe, test } from '@jest/globals';
import {
api,
captureWebhook,
randomString,
role,
signup,
startJobQueue,
UserToken,
WEBHOOK_HOST,
} from '../../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
describe('[シナリオ] ユーザ作成', () => {
let queue: INestApplicationContext;
let admin: entities.SignupResponse;
async function createSystemWebhook(args?: Partial<entities.AdminSystemWebhookCreateRequest>, credential?: UserToken): Promise<entities.AdminSystemWebhookCreateResponse> {
const res = await api(
'admin/system-webhook/create',
{
isActive: true,
name: randomString(),
on: ['userCreated'],
url: WEBHOOK_HOST,
secret: randomString(),
...args,
},
credential ?? admin,
);
return res.body;
}
// -------------------------------------------------------------------------------------------
beforeAll(async () => {
queue = await startJobQueue();
admin = await signup({ username: 'admin' });
await role(admin, { isAdministrator: true });
}, 1000 * 60 * 2);
afterAll(async () => {
await queue.close();
});
// -------------------------------------------------------------------------------------------
describe('SystemWebhook', () => {
beforeEach(async () => {
const webhooks = await api('admin/system-webhook/list', {}, admin);
for (const webhook of webhooks.body) {
await api('admin/system-webhook/delete', { id: webhook.id }, admin);
}
});
test('ユーザが作成された -> userCreatedが送出される', async () => {
const webhook = await createSystemWebhook({
on: ['userCreated'],
isActive: true,
});
let alice: any = null;
const webhookBody = await captureWebhook(async () => {
alice = await signup({ username: 'alice' });
});
// webhookの送出後にいろいろやってるのでちょっと待つ必要がある
await setTimeout(2000);
console.log(alice);
console.log(JSON.stringify(webhookBody, null, 2));
expect(webhookBody.hookId).toBe(webhook.id);
expect(webhookBody.type).toBe('userCreated');
const body = webhookBody.body as entities.UserLite;
expect(alice.id).toBe(body.id);
expect(alice.name).toBe(body.name);
expect(alice.username).toBe(body.username);
expect(alice.host).toBe(body.host);
expect(alice.avatarUrl).toBe(body.avatarUrl);
expect(alice.avatarBlurhash).toBe(body.avatarBlurhash);
expect(alice.avatarDecorations).toEqual(body.avatarDecorations);
expect(alice.isBot).toBe(body.isBot);
expect(alice.isCat).toBe(body.isCat);
expect(alice.instance).toEqual(body.instance);
expect(alice.emojis).toEqual(body.emojis);
expect(alice.onlineStatus).toBe(body.onlineStatus);
expect(alice.badgeRoles).toEqual(body.badgeRoles);
});
test('ユーザ作成 -> userCreatedが未許可の場合は送出されない', async () => {
await createSystemWebhook({
on: [],
isActive: true,
});
let alice: any = null;
const webhookBody = await captureWebhook(async () => {
alice = await signup({ username: 'alice' });
}).catch(e => e.message);
expect(webhookBody).toBe('timeout');
expect(alice.id).not.toBeNull();
});
test('ユーザ作成 -> Webhookが無効の場合は送出されない', async () => {
await createSystemWebhook({
on: ['userCreated'],
isActive: false,
});
let alice: any = null;
const webhookBody = await captureWebhook(async () => {
alice = await signup({ username: 'alice' });
}).catch(e => e.message);
expect(webhookBody).toBe('timeout');
expect(alice.id).not.toBeNull();
});
});
});

View file

@ -9,8 +9,8 @@
import * as assert from 'assert';
import { setTimeout } from 'node:timers/promises';
import { Redis } from 'ioredis';
import { loadConfig } from '@/config.js';
import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js';
import { loadConfig } from '@/config.js';
function genHost() {
return randomString() + '.example.com';
@ -492,6 +492,44 @@ describe('Timelines', () => {
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', () => {

View file

@ -12,13 +12,14 @@ import WebSocket, { ClientOptions } from 'ws';
import fetch, { File, RequestInit, type Headers } from 'node-fetch';
import { DataSource } from 'typeorm';
import { JSDOM } from 'jsdom';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { type Response } from 'node-fetch';
import Fastify from 'fastify';
import { entities } from '../src/postgres.js';
import { loadConfig } from '../src/config.js';
import type * as misskey from 'misskey-js';
import { type Response } from 'node-fetch';
import { ApiError } from "@/server/api/error.js";
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { ApiError } from '@/server/api/error.js';
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
@ -27,11 +28,23 @@ export interface UserToken {
bearer?: boolean;
}
export type SystemWebhookPayload = {
server: string;
hookId: string;
eventId: string;
createdAt: string;
type: string;
body: any;
}
const config = loadConfig();
export const port = config.port;
export const origin = config.url;
export const host = new URL(config.url).host;
export const WEBHOOK_HOST = 'http://localhost:15080';
export const WEBHOOK_PORT = 15080;
export const cookie = (me: UserToken): string => {
return `token=${me.token};`;
};
@ -645,3 +658,37 @@ export async function sendEnvResetRequest() {
export function castAsError(obj: Record<string, unknown>): { error: ApiError } {
return obj as { error: ApiError };
}
export async function captureWebhook<T = SystemWebhookPayload>(postAction: () => Promise<void>, port = WEBHOOK_PORT): Promise<T> {
const fastify = Fastify();
let timeoutHandle: NodeJS.Timeout | null = null;
const result = await new Promise<string>(async (resolve, reject) => {
fastify.all('/', async (req, res) => {
timeoutHandle && clearTimeout(timeoutHandle);
const body = JSON.stringify(req.body);
res.status(200).send('ok');
await fastify.close();
resolve(body);
});
await fastify.listen({ port });
timeoutHandle = setTimeout(async () => {
await fastify.close();
reject(new Error('timeout'));
}, 3000);
try {
await postAction();
} catch (e) {
await fastify.close();
reject(e);
}
});
await fastify.close();
return JSON.parse(result) as T;
}

View file

@ -27,8 +27,8 @@
"@syuilo/aiscript": "0.19.0",
"@tabler/icons-webfont": "3.3.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.0.5",
"@vue/compiler-sfc": "3.4.31",
"@vitejs/plugin-vue": "5.1.0",
"@vue/compiler-sfc": "3.4.34",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
"astring": "1.8.6",
"broadcast-channel": "7.0.0",
@ -39,9 +39,9 @@
"chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.1",
"chromatic": "11.5.4",
"compare-versions": "6.1.0",
"cropperjs": "2.0.0-beta.5",
"chromatic": "11.5.6",
"compare-versions": "6.1.1",
"cropperjs": "2.0.0-rc.1",
"date-fns": "2.30.0",
"escape-regexp": "0.0.1",
"estree-walker": "3.0.3",
@ -57,85 +57,85 @@
"misskey-reversi": "workspace:*",
"photoswipe": "5.4.4",
"punycode": "2.3.1",
"rollup": "4.18.0",
"rollup": "4.19.1",
"sanitize-html": "2.13.0",
"sass": "1.77.6",
"shiki": "1.10.0",
"sass": "1.77.8",
"shiki": "1.12.0",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.165.0",
"three": "0.167.0",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typescript": "5.5.3",
"typescript": "5.5.4",
"uuid": "10.0.0",
"v-code-diff": "1.12.0",
"vite": "5.3.2",
"vue": "3.4.31",
"vite": "5.3.5",
"vue": "3.4.34",
"vuedraggable": "next"
},
"devDependencies": {
"@misskey-dev/summaly": "5.1.0",
"@storybook/addon-actions": "8.1.11",
"@storybook/addon-essentials": "8.1.11",
"@storybook/addon-interactions": "8.1.11",
"@storybook/addon-links": "8.1.11",
"@storybook/addon-mdx-gfm": "8.1.11",
"@storybook/addon-storysource": "8.1.11",
"@storybook/blocks": "8.1.11",
"@storybook/components": "8.1.11",
"@storybook/core-events": "8.1.11",
"@storybook/manager-api": "8.1.11",
"@storybook/preview-api": "8.1.11",
"@storybook/react": "8.1.11",
"@storybook/react-vite": "8.1.11",
"@storybook/test": "8.1.11",
"@storybook/theming": "8.1.11",
"@storybook/types": "8.1.11",
"@storybook/vue3": "8.1.11",
"@storybook/addon-actions": "8.2.6",
"@storybook/addon-essentials": "8.2.6",
"@storybook/addon-interactions": "8.2.6",
"@storybook/addon-links": "8.2.6",
"@storybook/addon-mdx-gfm": "8.2.6",
"@storybook/addon-storysource": "8.2.6",
"@storybook/blocks": "8.2.6",
"@storybook/components": "8.2.6",
"@storybook/core-events": "8.2.6",
"@storybook/manager-api": "8.2.6",
"@storybook/preview-api": "8.2.6",
"@storybook/react": "8.2.6",
"@storybook/react-vite": "8.2.6",
"@storybook/test": "8.2.6",
"@storybook/theming": "8.2.6",
"@storybook/types": "8.2.6",
"@storybook/vue3": "8.2.6",
"@storybook/vue3-vite": "8.1.11",
"@testing-library/vue": "8.1.0",
"@types/escape-regexp": "0.0.3",
"@types/estree": "1.0.5",
"@types/matter-js": "0.19.6",
"@types/matter-js": "0.19.7",
"@types/micromatch": "4.0.9",
"@types/node": "20.14.9",
"@types/node": "20.14.12",
"@types/punycode": "2.1.4",
"@types/sanitize-html": "2.11.0",
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/uuid": "10.0.0",
"@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.15.0",
"@types/ws": "8.5.11",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"@vitest/coverage-v8": "1.6.0",
"@vue/runtime-core": "3.4.31",
"acorn": "8.12.0",
"@vue/runtime-core": "3.4.34",
"acorn": "8.12.1",
"cross-env": "7.0.3",
"cypress": "13.13.0",
"cypress": "13.13.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-vue": "9.26.0",
"eslint-plugin-vue": "9.27.0",
"fast-glob": "3.3.2",
"happy-dom": "10.0.3",
"intersection-observer": "0.12.2",
"micromatch": "4.0.7",
"msw": "2.3.1",
"msw-storybook-addon": "2.0.2",
"msw": "2.3.4",
"msw-storybook-addon": "2.0.3",
"nodemon": "3.1.4",
"prettier": "3.3.2",
"prettier": "3.3.3",
"react": "18.3.1",
"react-dom": "18.3.1",
"seedrandom": "3.0.5",
"start-server-and-test": "2.0.4",
"storybook": "8.1.11",
"storybook": "8.2.6",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "1.6.0",
"vitest-fetch-mock": "0.2.2",
"vue-component-type-helpers": "2.0.24",
"vue-component-type-helpers": "2.0.29",
"vue-eslint-parser": "9.4.3",
"vue-tsc": "2.0.24"
"vue-tsc": "2.0.29"
}
}

View file

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

View file

@ -7,12 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="emit('closed')" @esc="emit('esc')">
<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }">
<div ref="headerEl" :class="$style.header">
<button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
<button v-if="withOkButton && withCloseButton" :class="$style.headerButton" class="_button" @click="emit('close')"><i class="ti ti-x"></i></button>
<span :class="$style.title">
<slot name="header"></slot>
</span>
<button v-if="!withOkButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ti ti-x"></i></button>
<button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ti ti-check"></i></button>
<button v-if="!withOkButton && withCloseButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="emit('close')"><i class="ti ti-x"></i></button>
<button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="emit('ok')"><i class="ti ti-check"></i></button>
</div>
<div :class="$style.body">
<slot :width="bodyWidth" :height="bodyHeight"></slot>
@ -27,11 +27,13 @@ import MkModal from './MkModal.vue';
const props = withDefaults(defineProps<{
withOkButton: boolean;
withCloseButton: boolean;
okButtonDisabled: boolean;
width: number;
height: number;
}>(), {
withOkButton: false,
withCloseButton: true,
okButtonDisabled: false,
width: 400,
height: 500,
@ -51,13 +53,13 @@ const headerEl = shallowRef<HTMLElement>();
const bodyWidth = ref(0);
const bodyHeight = ref(0);
const close = () => {
function close() {
modal.value?.close();
};
}
const onBgClick = () => {
function onBgClick() {
emit('click');
};
}
const ro = new ResizeObserver((entries, observer) => {
if (rootEl.value == null || headerEl.value == null) return;

View file

@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
</section>
<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>
</MkInput>
<MkSelect v-model="unit" small>

View file

@ -367,6 +367,8 @@ function watchForDraft() {
watch(files, () => saveDraft(), { deep: true });
watch(visibility, () => saveDraft());
watch(localOnly, () => saveDraft());
watch(quoteId, () => saveDraft());
watch(reactionAcceptance, () => saveDraft());
}
function checkMissingMention() {
@ -703,6 +705,8 @@ function saveDraft() {
files: files.value,
poll: poll.value,
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined,
quoteId: quoteId.value,
reactionAcceptance: reactionAcceptance.value,
},
};
@ -991,6 +995,8 @@ onMounted(() => {
users.forEach(u => pushVisibleUser(u));
});
}
quoteId.value = draft.data.quoteId;
reactionAcceptance.value = draft.data.reactionAcceptance;
}
}
@ -998,9 +1004,11 @@ onMounted(() => {
if (props.initialNote) {
const init = props.initialNote;
text.value = init.text ? init.text : '';
files.value = init.files ?? [];
cw.value = init.cw ?? null;
useCw.value = init.cw != null;
cw.value = init.cw ?? null;
visibility.value = init.visibility;
localOnly.value = init.localOnly ?? false;
files.value = init.files ?? [];
if (init.poll) {
poll.value = {
choices: init.poll.choices.map(x => x.text),
@ -1009,9 +1017,13 @@ onMounted(() => {
expiredAfter: null,
};
}
visibility.value = init.visibility;
localOnly.value = init.localOnly ?? false;
if (init.visibleUserIds) {
misskeyApi('users/show', { userIds: init.visibleUserIds }).then(users => {
users.forEach(u => pushVisibleUser(u));
});
}
quoteId.value = init.renote ? init.renote.id : null;
reactionAcceptance.value = init.reactionAcceptance;
}
nextTick(() => watchForDraft());

View file

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

View file

@ -24,22 +24,23 @@ export type MkSystemWebhookResult = {
};
export async function showSystemWebhookEditorDialog(props: MkSystemWebhookEditorProps): Promise<MkSystemWebhookResult | null> {
const { dispose, result } = await new Promise<{ dispose: () => void, result: MkSystemWebhookResult | null }>(async resolve => {
const { dispose: _dispose } = os.popup(
const { result } = await new Promise<{ result: MkSystemWebhookResult | null }>(async resolve => {
const { dispose } = os.popup(
defineAsyncComponent(() => import('@/components/MkSystemWebhookEditor.vue')),
props,
{
submitted: (ev: MkSystemWebhookResult) => {
resolve({ dispose: _dispose, result: ev });
resolve({ result: ev });
},
canceled: () => {
resolve({ result: null });
},
closed: () => {
resolve({ dispose: _dispose, result: null });
dispose();
},
},
);
});
dispose();
return result;
}

View file

@ -5,6 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkModalWindow
ref="dialogEl"
:width="450"
:height="590"
:canClose="true"
@ -12,55 +13,59 @@ SPDX-License-Identifier: AGPL-3.0-only
:okButtonDisabled="false"
@click="onCancelClicked"
@close="onCancelClicked"
@closed="onCancelClicked"
@closed="emit('closed')"
>
<template #header>
{{ mode === 'create' ? i18n.ts._webhookSettings.createWebhook : i18n.ts._webhookSettings.modifyWebhook }}
</template>
<MkSpacer :marginMin="20" :marginMax="28">
<MkLoading v-if="loading !== 0"/>
<div v-else :class="$style.root" class="_gaps_m">
<MkInput v-model="title">
<template #label>{{ i18n.ts._webhookSettings.name }}</template>
</MkInput>
<MkInput v-model="url">
<template #label>URL</template>
</MkInput>
<MkInput v-model="secret">
<template #label>{{ i18n.ts._webhookSettings.secret }}</template>
</MkInput>
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts._webhookSettings.events }}</template>
<div class="_gaps_s">
<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
</MkSwitch>
<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
</MkSwitch>
</div>
</MkFolder>
<div>
<MkSpacer :marginMin="20" :marginMax="28">
<MkLoading v-if="loading !== 0"/>
<div v-else :class="$style.root" class="_gaps_m">
<MkInput v-model="title">
<template #label>{{ i18n.ts._webhookSettings.name }}</template>
</MkInput>
<MkInput v-model="url">
<template #label>URL</template>
</MkInput>
<MkInput v-model="secret">
<template #label>{{ i18n.ts._webhookSettings.secret }}</template>
</MkInput>
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts._webhookSettings.events }}</template>
<MkSwitch v-model="isActive">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<div class="_gaps_s">
<MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport">
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template>
</MkSwitch>
<MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved">
<template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template>
</MkSwitch>
<MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated">
<template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template>
</MkSwitch>
</div>
</MkFolder>
<div :class="$style.footer" class="_buttonsCenter">
<MkButton primary :disabled="disableSubmitButton" @click="onSubmitClicked">
<i class="ti ti-check"></i>
{{ i18n.ts.ok }}
</MkButton>
<MkButton @click="onCancelClicked"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
<MkSwitch v-model="isActive">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</div>
</MkSpacer>
<div :class="$style.footer" class="_buttonsCenter">
<MkButton primary rounded :disabled="disableSubmitButton" @click="onSubmitClicked">
<i class="ti ti-check"></i>
{{ i18n.ts.ok }}
</MkButton>
<MkButton rounded @click="onCancelClicked"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
</div>
</MkSpacer>
</div>
</MkModalWindow>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, toRefs } from 'vue';
import FormSection from '@/components/form/section.vue';
import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import {
@ -78,13 +83,17 @@ import * as os from '@/os.js';
type EventType = {
abuseReport: boolean;
abuseReportResolved: boolean;
userCreated: boolean;
}
const emit = defineEmits<{
(ev: 'submitted', result: MkSystemWebhookResult): void;
(ev: 'canceled'): void;
(ev: 'closed'): void;
}>();
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
const props = defineProps<MkSystemWebhookEditorProps>();
const { mode, id, requiredEvents } = toRefs(props);
@ -97,12 +106,14 @@ const secret = ref<string>('');
const events = ref<EventType>({
abuseReport: true,
abuseReportResolved: true,
userCreated: true,
});
const isActive = ref<boolean>(true);
const disabledEvents = ref<EventType>({
abuseReport: false,
abuseReportResolved: false,
userCreated: false,
});
const disableSubmitButton = computed(() => {
@ -133,12 +144,14 @@ async function onSubmitClicked() {
switch (mode.value) {
case 'create': {
const result = await misskeyApi('admin/system-webhook/create', params);
dialogEl.value?.close();
emit('submitted', result);
break;
}
case 'edit': {
// eslint-disable-next-line
const result = await misskeyApi('admin/system-webhook/update', { id: id.value!, ...params });
dialogEl.value?.close();
emit('submitted', result);
break;
}
@ -147,13 +160,15 @@ async function onSubmitClicked() {
} catch (ex: any) {
const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
emit('closed');
dialogEl.value?.close();
emit('canceled');
}
});
}
function onCancelClicked() {
emit('closed');
dialogEl.value?.close();
emit('canceled');
}
async function loadingScope<T>(fn: () => Promise<T>): Promise<T> {
@ -183,11 +198,12 @@ onMounted(async () => {
for (const ev of Object.keys(events.value)) {
events.value[ev] = res.on.includes(ev as SystemWebhookEventType);
}
// eslint-disable-next-line
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (ex: any) {
const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
emit('closed');
dialogEl.value?.close();
emit('canceled');
}
break;
}
@ -209,9 +225,12 @@ onMounted(async () => {
}
.footer {
display: flex;
justify-content: center;
align-items: flex-end;
margin-top: 20px;
position: sticky;
bottom: 0;
left: 0;
padding: 12px;
border-top: solid 0.5px var(--divider);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
</style>

View file

@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { onMounted, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import MkInput from '@/components/MkInput.vue';
import FormSplit from '@/components/form/split.vue';
@ -91,7 +91,7 @@ const host = ref('');
const users = ref<Misskey.entities.UserLite[]>([]);
const recentUsers = ref<Misskey.entities.UserDetailed[]>([]);
const selected = ref<Misskey.entities.UserLite | null>(null);
const dialogEl = ref();
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
function search() {
if (username.value === '' && host.value === '') {
@ -122,7 +122,7 @@ async function ok() {
});
emit('ok', user);
dialogEl.value.close();
dialogEl.value?.close();
// 使
let recents = defaultStore.state.recentlyUsedUsers;
@ -133,7 +133,7 @@ async function ok() {
function cancel() {
emit('cancel');
dialogEl.value.close();
dialogEl.value?.close();
}
onMounted(() => {

View file

@ -53,7 +53,7 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
const current = resolveNested(router.current)!;
const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
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 }) {
const current = resolveNested(resolved);

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkModalWindow
ref="dialog"
ref="dialogEl"
:width="400"
:height="490"
:withOkButton="false"
@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, toRefs } from 'vue';
import { computed, onMounted, ref, shallowRef, toRefs } from 'vue';
import { entities } from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
@ -88,6 +88,7 @@ type NotificationRecipientMethod = 'email' | 'webhook';
const emit = defineEmits<{
(ev: 'submitted'): void;
(ev: 'canceled'): void;
(ev: 'closed'): void;
}>();
@ -98,6 +99,8 @@ const props = defineProps<{
const { mode, id } = toRefs(props);
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
const loading = ref<number>(0);
const title = ref<string>('');
@ -166,18 +169,21 @@ async function onSubmitClicked() {
}
}
dialogEl.value?.close();
emit('submitted');
// eslint-disable-next-line
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (ex: any) {
const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
emit('closed');
dialogEl.value?.close();
emit('canceled');
}
});
}
function onCancelClicked() {
emit('closed');
dialogEl.value?.close();
emit('canceled');
}
async function onEditSystemWebhookClicked() {
@ -262,7 +268,8 @@ onMounted(async () => {
} catch (ex: any) {
const msg = ex.message ?? i18n.ts.internalServerErrorDescription;
await os.alert({ type: 'error', title: i18n.ts.error, text: msg });
emit('closed');
dialogEl.value?.close();
emit('canceled');
}
} else {
userId.value = moderators.value[0]?.id ?? null;
@ -296,11 +303,13 @@ onMounted(async () => {
gap: 8px;
button {
width: 2.5em;
height: 2.5em;
min-width: 2.5em;
min-height: 2.5em;
min-width: 0;
min-height: 0;
width: 34px;
height: 34px;
flex-shrink: 0;
box-sizing: border-box;
margin: 1px 0;
padding: 6px;
}
}

View file

@ -108,26 +108,27 @@ async function onDeleteButtonClicked(id: string) {
}
async function showEditor(mode: 'create' | 'edit', id?: string) {
const { dispose, needLoad } = await new Promise<{ dispose: () => void, needLoad: boolean }>(async resolve => {
const { dispose: _dispose } = os.popup(
const { needLoad } = await new Promise<{ needLoad: boolean }>(async resolve => {
const { dispose } = os.popup(
defineAsyncComponent(() => import('./notification-recipient.editor.vue')),
{
mode,
id,
},
{
submitted: async () => {
resolve({ dispose: _dispose, needLoad: true });
submitted: () => {
resolve({ needLoad: true });
},
canceled: () => {
resolve({ needLoad: false });
},
closed: () => {
resolve({ dispose: _dispose, needLoad: false });
dispose();
},
},
);
});
dispose();
if (needLoad) {
await fetchRecipients();
}

View file

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local">
<template #label>{{ i18n.ts.expirationDate }}</template>
</MkInput>
<MkInput v-model="createCount" type="number">
<MkInput v-model="createCount" type="number" min="1">
<template #label>{{ i18n.ts.createCount }}</template>
</MkInput>
<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">
<template #label>{{ i18n.ts.shuffle }}</template>
</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>
</MkInput>
<MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1">
@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</template>
<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>
</MkInput>
<MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1">

View file

@ -124,10 +124,23 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; })
*/
export function playMisskeySfx(operationType: OperationType) {
const sound = defaultStore.state[`sound_${operationType}`];
if (sound.type == null || !canPlay || ('userActivation' in navigator && !navigator.userActivation.hasBeenActive)) return;
playMisskeySfxFile(sound);
}
/**
*
* @param soundStore
*/
export function playMisskeySfxFile(soundStore: SoundStore) {
// 連続して再生しない
if (!canPlay) return;
// ユーザーアクティベーションが必要な場合はそれがない場合は再生しない
if ('userActivation' in navigator && !navigator.userActivation.hasBeenActive) return;
// サウンドがない場合は再生しない
if (soundStore.type === null || soundStore.type === '_driveFile_' && !soundStore.fileUrl) return;
canPlay = false;
playMisskeySfxFile(sound).finally(() => {
playMisskeySfxFileInternal(soundStore).finally(() => {
// ごく短時間に音が重複しないように
setTimeout(() => {
canPlay = true;
@ -135,11 +148,7 @@ export function playMisskeySfx(operationType: OperationType) {
});
}
/**
*
* @param soundStore
*/
export async function playMisskeySfxFile(soundStore: SoundStore) {
async function playMisskeySfxFileInternal(soundStore: SoundStore) {
if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
return;
}

View file

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

View file

@ -1,6 +1,7 @@
import tsParser from '@typescript-eslint/parser';
import sharedConfig from '../shared/eslint.config.js';
// eslint-disable-next-line import/no-default-export
export default [
...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
//
// @public (undocumented)
export abstract class ChannelConnection<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> {
export abstract class ChannelConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> {
constructor(stream: Stream, channel: string, name?: string);
// (undocumented)
channel: string;
@ -771,12 +771,12 @@ export type Channels = {
user1: boolean;
user2: boolean;
}) => void;
updateSettings: (payload: {
updateSettings: <K extends ReversiUpdateKey>(payload: {
userId: User['id'];
key: string;
value: any;
key: K;
value: ReversiGameDetailed[K];
}) => void;
log: (payload: Record<string, any>) => void;
log: (payload: Record<string, unknown>) => void;
};
receives: {
putStone: {
@ -785,10 +785,7 @@ export type Channels = {
};
ready: boolean;
cancel: null | Record<string, never>;
updateSettings: {
key: string;
value: any;
};
updateSettings: ReversiUpdateSettings<ReversiUpdateKey>;
claimTimeIsUp: null | Record<string, never>;
};
};
@ -2730,7 +2727,7 @@ type PagesUnlikeRequest = operations['pages___unlike']['requestBody']['content']
type PagesUpdateRequest = operations['pages___update']['requestBody']['content']['application/json'];
// @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
//
@ -2969,7 +2966,7 @@ export class Stream extends EventEmitter<StreamEvents> {
constructor(origin: string, user: {
token: string;
} | null, options?: {
WebSocket?: any;
WebSocket?: WebSocket;
});
// (undocumented)
close(): void;
@ -2992,9 +2989,9 @@ export class Stream extends EventEmitter<StreamEvents> {
// (undocumented)
send(typeOrPayload: string): void;
// (undocumented)
send(typeOrPayload: string, payload: any): void;
send(typeOrPayload: string, payload: unknown): void;
// (undocumented)
send(typeOrPayload: Record<string, any> | any[]): void;
send(typeOrPayload: Record<string, unknown> | unknown[]): void;
// (undocumented)
state: 'initializing' | 'reconnecting' | 'connected';
// (undocumented)
@ -3229,7 +3226,9 @@ type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['
// 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)

View file

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2024.7.0-beta.1",
"version": "2024.7.0-rc.4",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
@ -35,23 +35,23 @@
"directory": "packages/misskey-js"
},
"devDependencies": {
"@microsoft/api-extractor": "7.47.0",
"@microsoft/api-extractor": "7.47.4",
"@swc/jest": "0.2.36",
"@types/jest": "29.5.12",
"@types/node": "20.14.9",
"@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.15.0",
"@types/node": "20.14.12",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"jest": "29.7.0",
"jest-fetch-mock": "3.0.3",
"jest-websocket-mock": "2.5.0",
"mock-socket": "9.3.1",
"ncp": "2.0.0",
"nodemon": "3.1.4",
"execa": "9.2.0",
"execa": "9.3.0",
"tsd": "0.31.1",
"typescript": "5.5.3",
"esbuild": "0.22.0",
"glob": "10.4.2"
"typescript": "5.5.4",
"esbuild": "0.23.0",
"glob": "11.0.0"
},
"files": [
"built"

View file

@ -3,7 +3,8 @@ export type Acct = {
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);
const split = acct.split('@', 2);
return { username: split[0], host: split[1] || null };

View file

@ -14,6 +14,7 @@ export type APIError = {
code: string;
message: string;
kind: 'client' | 'server';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
info: Record<string, any>;
};
@ -29,6 +30,7 @@ export type FetchLike = (input: string, init?: {
headers: { [key in string]: string }
}) => Promise<{
status: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
json(): Promise<any>;
}>;
@ -49,6 +51,7 @@ export class APIClient {
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> {
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> =
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
: false
type GetCaseResult<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> =
Endpoints[E]['res'] extends SwitchCase
// eslint-disable-next-line @typescript-eslint/no-explicit-any
? StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>[1]
: never

View file

@ -4089,7 +4089,8 @@ export type components = {
userId: string | null;
}) | null;
localOnly?: boolean;
reactionAcceptance: string | null;
/** @enum {string|null} */
reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null;
reactionEmojis: {
[key: string]: string;
};
@ -4969,7 +4970,7 @@ export type components = {
latestSentAt: string | null;
latestStatus: number | null;
name: string;
on: ('abuseReport' | 'abuseReportResolved')[];
on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
url: string;
secret: string;
};
@ -10041,7 +10042,7 @@ export type operations = {
'application/json': {
isActive: boolean;
name: string;
on: ('abuseReport' | 'abuseReportResolved')[];
on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
url: string;
secret: string;
};
@ -10151,7 +10152,7 @@ export type operations = {
content: {
'application/json': {
isActive?: boolean;
on?: ('abuseReport' | 'abuseReportResolved')[];
on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
};
};
};
@ -10264,7 +10265,7 @@ export type operations = {
id: string;
isActive: boolean;
name: string;
on: ('abuseReport' | 'abuseReportResolved')[];
on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
url: string;
secret: string;
};

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 noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
@ -135,10 +145,30 @@ export const moderationLogTypes = [
'unsetUserBanner',
] 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 = {
updateServerSettings: {
before: any | null;
after: any | null;
before: MetaDetailed | null;
after: MetaDetailed | null;
};
suspend: {
userId: string;
@ -159,16 +189,16 @@ export type ModerationLogPayloads = {
};
addCustomEmoji: {
emojiId: string;
emoji: any;
emoji: EmojiDetailed;
};
updateCustomEmoji: {
emojiId: string;
before: any;
after: any;
before: EmojiDetailed;
after: EmojiDetailed;
};
deleteCustomEmoji: {
emojiId: string;
emoji: any;
emoji: EmojiDetailed;
};
assignRole: {
userId: string;
@ -187,16 +217,16 @@ export type ModerationLogPayloads = {
};
createRole: {
roleId: string;
role: any;
role: Role;
};
updateRole: {
roleId: string;
before: any;
after: any;
before: Role;
after: Role;
};
deleteRole: {
roleId: string;
role: any;
role: Role;
};
clearQueue: Record<string, never>;
promoteQueue: Record<string, never>;
@ -211,39 +241,39 @@ export type ModerationLogPayloads = {
noteUserId: string;
noteUserUsername: string;
noteUserHost: string | null;
note: any;
note: Note;
};
createGlobalAnnouncement: {
announcementId: string;
announcement: any;
announcement: Announcement;
};
createUserAnnouncement: {
announcementId: string;
announcement: any;
announcement: Announcement;
userId: string;
userUsername: string;
userHost: string | null;
};
updateGlobalAnnouncement: {
announcementId: string;
before: any;
after: any;
before: Announcement;
after: Announcement;
};
updateUserAnnouncement: {
announcementId: string;
before: any;
after: any;
before: Announcement;
after: Announcement;
userId: string;
userUsername: string;
userHost: string | null;
};
deleteGlobalAnnouncement: {
announcementId: string;
announcement: any;
announcement: Announcement;
};
deleteUserAnnouncement: {
announcementId: string;
announcement: any;
announcement: Announcement;
userId: string;
userUsername: string;
userHost: string | null;
@ -281,37 +311,37 @@ export type ModerationLogPayloads = {
};
resolveAbuseReport: {
reportId: string;
report: any;
report: ReceivedAbuseReport;
forwarded: boolean;
};
createInvitation: {
invitations: any[];
invitations: InviteCode[];
};
createAd: {
adId: string;
ad: any;
ad: Ad;
};
updateAd: {
adId: string;
before: any;
after: any;
before: Ad;
after: Ad;
};
deleteAd: {
adId: string;
ad: any;
ad: Ad;
};
createAvatarDecoration: {
avatarDecorationId: string;
avatarDecoration: any;
avatarDecoration: AvatarDecoration;
};
updateAvatarDecoration: {
avatarDecorationId: string;
before: any;
after: any;
before: AvatarDecoration;
after: AvatarDecoration;
};
deleteAvatarDecoration: {
avatarDecorationId: string;
avatarDecoration: any;
avatarDecoration: AvatarDecoration;
};
unsetUserAvatar: {
userId: string;
@ -327,28 +357,28 @@ export type ModerationLogPayloads = {
};
createSystemWebhook: {
systemWebhookId: string;
webhook: any;
webhook: SystemWebhook;
};
updateSystemWebhook: {
systemWebhookId: string;
before: any;
after: any;
before: SystemWebhook;
after: SystemWebhook;
};
deleteSystemWebhook: {
systemWebhookId: string;
webhook: any;
webhook: SystemWebhook;
};
createAbuseReportNotificationRecipient: {
recipientId: string;
recipient: any;
recipient: AbuseReportNotificationRecipient;
};
updateAbuseReportNotificationRecipient: {
recipientId: string;
before: any;
after: any;
before: AbuseReportNotificationRecipient;
after: AbuseReportNotificationRecipient;
};
deleteAbuseReportNotificationRecipient: {
recipientId: string;
recipient: any;
recipient: AbuseReportNotificationRecipient;
};
};

View file

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

View file

@ -15,7 +15,7 @@ export function urlQuery(obj: Record<string, string | number | boolean | undefin
.join('&');
}
type AnyOf<T extends Record<any, any>> = T[keyof T];
type AnyOf<T extends Record<PropertyKey, unknown>> = T[keyof T];
type StreamEvents = {
_connected_: void;
@ -25,6 +25,7 @@ type StreamEvents = {
/**
* Misskey stream connection
*/
// eslint-disable-next-line import/no-default-export
export default class Stream extends EventEmitter<StreamEvents> {
private stream: _ReconnectingWebsocket.default;
public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing';
@ -34,7 +35,7 @@ export default class Stream extends EventEmitter<StreamEvents> {
private idCounter = 0;
constructor(origin: string, user: { token: string; } | null, options?: {
WebSocket?: any;
WebSocket?: WebSocket;
}) {
super();
@ -51,6 +52,7 @@ export default class Stream extends EventEmitter<StreamEvents> {
this.send = this.send.bind(this);
this.close = this.close.bind(this);
// eslint-disable-next-line no-param-reassign
options = options ?? { };
const query = urlQuery({
@ -91,8 +93,8 @@ export default class Stream extends EventEmitter<StreamEvents> {
this.sharedConnectionPools.push(pool);
}
const connection = new SharedConnection(this, channel, pool, name);
this.sharedConnections.push(connection);
const connection = new SharedConnection<Channels[C]>(this, channel, pool, name);
this.sharedConnections.push(connection as unknown as SharedConnection);
return connection;
}
@ -106,7 +108,7 @@ export default class Stream extends EventEmitter<StreamEvents> {
private connectToChannel<C extends keyof Channels>(channel: C, params: Channels[C]['params']): NonSharedConnection<Channels[C]> {
const connection = new NonSharedConnection(this, channel, this.genId(), params);
this.nonSharedConnections.push(connection);
this.nonSharedConnections.push(connection as unknown as NonSharedConnection);
return connection;
}
@ -174,9 +176,9 @@ export default class Stream extends EventEmitter<StreamEvents> {
* ! JSONで行われます !
*/
public send(typeOrPayload: string): void
public send(typeOrPayload: string, payload: any): void
public send(typeOrPayload: Record<string, any> | any[]): void
public send(typeOrPayload: string | Record<string, any> | any[], payload?: any): void {
public send(typeOrPayload: string, payload: unknown): void
public send(typeOrPayload: Record<string, unknown> | unknown[]): void
public send(typeOrPayload: string | Record<string, unknown> | unknown[], payload?: unknown): void {
if (typeof typeOrPayload === 'string') {
this.stream.send(JSON.stringify({
type: typeOrPayload,
@ -211,7 +213,7 @@ class Pool {
public id: string;
protected stream: Stream;
public users = 0;
private disposeTimerId: any;
private disposeTimerId: ReturnType<typeof setTimeout> | null = null;
private isConnected = false;
constructor(stream: Stream, channel: string, id: string) {
@ -275,7 +277,7 @@ class Pool {
}
}
export abstract class Connection<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> {
export abstract class Connection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> {
public channel: string;
protected stream: Stream;
public abstract id: string;
@ -309,7 +311,7 @@ export abstract class Connection<Channel extends AnyOf<Channels> = any> extends
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;
public get id(): string {
@ -328,11 +330,11 @@ class SharedConnection<Channel extends AnyOf<Channels> = any> extends Connection
public dispose(): void {
this.pool.dec();
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;
protected params: Channel['params'];
@ -359,6 +361,6 @@ class NonSharedConnection<Channel extends AnyOf<Channels> = any> extends Connect
public dispose(): void {
this.removeAllListeners();
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,
ReversiGameDetailed,
} from './entities.js';
import {
ReversiUpdateKey,
} from './consts.js';
type ReversiUpdateSettings<K extends ReversiUpdateKey> = {
key: K;
value: ReversiGameDetailed[K];
};
export type Channels = {
main: {
@ -51,6 +59,7 @@ export type Channels = {
registryUpdated: (payload: {
scope?: string[];
key: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any | null;
}) => void;
driveFileCreated: (payload: DriveFile) => void;
@ -208,8 +217,8 @@ export type Channels = {
ended: (payload: { winnerId: User['id'] | null; game: ReversiGameDetailed; }) => void;
canceled: (payload: { userId: User['id']; }) => void;
changeReadyStates: (payload: { user1: boolean; user2: boolean; }) => void;
updateSettings: (payload: { userId: User['id']; key: string; value: any; }) => void;
log: (payload: Record<string, any>) => void;
updateSettings: <K extends ReversiUpdateKey>(payload: { userId: User['id']; key: K; value: ReversiGameDetailed[K]; }) => void;
log: (payload: Record<string, unknown>) => void;
};
receives: {
putStone: {
@ -218,10 +227,7 @@ export type Channels = {
};
ready: boolean;
cancel: null | Record<string, never>;
updateSettings: {
key: string;
value: any;
};
updateSettings: ReversiUpdateSettings<ReversiUpdateKey>;
claimTimeIsUp: null | Record<string, never>;
}
}

View file

@ -9,16 +9,16 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"dependencies": {
"esbuild": "0.22.0",
"esbuild": "0.23.0",
"idb-keyval": "6.2.1",
"misskey-js": "workspace:*"
},
"devDependencies": {
"@typescript-eslint/parser": "7.15.0",
"@typescript-eslint/parser": "7.17.0",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
"eslint-plugin-import": "2.29.1",
"nodemon": "3.1.4",
"typescript": "5.5.3"
"typescript": "5.5.4"
},
"type": "module"
}